diff --git a/__mocks__/controllers/api-builder.controller.ts b/__mocks__/controllers/api-builder.controller.ts new file mode 100644 index 0000000..8530f13 --- /dev/null +++ b/__mocks__/controllers/api-builder.controller.ts @@ -0,0 +1,54 @@ +export const apiBuilderController = jest + .fn() + .mockImplementation((routeType: string, collectionName: string, findWhere: any, routeData: any) => { + return { + routeType, + collectionName, + findWhere, + routeData, + fetchData: jest.fn().mockImplementation((_findType?: string) => { + if (collectionName === "users") { + const mockUsers = [ + { id: "1", username: "mock_user", permission_group: "admin", email: "mock1@test.com" }, + { id: "2", username: "mock_user", permission_group: "admin", email: "mock2@test.com" }, + { id: "3", username: "mock_user", permission_group: "admin", email: "mock3@test.com" }, + { id: "4", username: "other_user", permission_group: "editor", email: "other1@test.com" }, + { id: "5", username: "root_user", permission_group: "root", email: "root@test.com" } + ] + + if (routeType === "index") { + return mockUsers + } else if (routeType === "single-param") { + if (findWhere === "permission_group") { + return mockUsers.filter((u) => u.permission_group === routeData) + } else if (findWhere === "username") { + return mockUsers.filter((u) => u.username === routeData) + } + return mockUsers + } + return [] + } + + const mockEntryTypes = [ + { id: "1", name: "Type 1", namespace: "test-entry-type", slug: "type-1" }, + { id: "2", name: "Type 2", namespace: "test-entry-type", slug: "type-2" }, + { id: "3", name: "Type 3", namespace: "newtest.newtest1", slug: "type-3" }, + { id: "4", name: "Type 4", namespace: "newtest.newtest1", slug: "type-4" }, + { id: "5", name: "Type 5", namespace: "other-namespace", slug: "type-5" } + ] + + if (routeType === "index") { + return mockEntryTypes + } else if (routeType === "single-param") { + if (routeData === "test-entry-type") { + return mockEntryTypes.filter((et) => et.namespace === "test-entry-type") + } + return mockEntryTypes + } else if (routeType === "multi-param") { + const ns = Array.isArray(routeData) ? routeData.join(".") : routeData + return mockEntryTypes.filter((et) => et.namespace === ns) + } + return [] + }) + } + }) diff --git a/__mocks__/controllers/api-key.controller.ts b/__mocks__/controllers/api-key.controller.ts new file mode 100644 index 0000000..6cf39cc --- /dev/null +++ b/__mocks__/controllers/api-key.controller.ts @@ -0,0 +1,3 @@ +export const apiKeyController = jest.fn().mockImplementation(() => ({ + findKey: jest.fn().mockResolvedValue([{ key: "valid-key" }]) +})) diff --git a/__mocks__/lib/api/rate-limits.ts b/__mocks__/lib/api/rate-limits.ts new file mode 100644 index 0000000..10d7777 --- /dev/null +++ b/__mocks__/lib/api/rate-limits.ts @@ -0,0 +1,2 @@ +// Mock rate limiting to be a direct pass-through to simplify route testing +export const withRateLimit = (handler: any) => handler diff --git a/__mocks__/lib/api/utils.ts b/__mocks__/lib/api/utils.ts new file mode 100644 index 0000000..be343e1 --- /dev/null +++ b/__mocks__/lib/api/utils.ts @@ -0,0 +1,3 @@ +// Mock API key controller and utility checks +export const isSystemApiKey = jest.fn().mockImplementation((key: string) => key === "system-key") +export const isValidApiKey = jest.fn().mockImplementation((_keyData: any, key: string) => key === "valid-key") diff --git a/__mocks__/lib/check-permission-api.ts b/__mocks__/lib/check-permission-api.ts new file mode 100644 index 0000000..795a21f --- /dev/null +++ b/__mocks__/lib/check-permission-api.ts @@ -0,0 +1,3 @@ +// Mock permission checking API helper to isolate controller validation +const checkPermissionApi = jest.fn() +export default checkPermissionApi diff --git a/__tests__/api/entries/entries.api.test.ts b/__tests__/api/entries/entries.api.test.ts index 9f59e81..5016357 100644 --- a/__tests__/api/entries/entries.api.test.ts +++ b/__tests__/api/entries/entries.api.test.ts @@ -1,33 +1,59 @@ +/** + * @jest-environment node + */ +/* eslint-disable @typescript-eslint/no-require-imports */ import httpMocks from "node-mocks-http" import { NextApiRequest, NextApiResponse } from "next" -import indexRouteHandler from "../../../pages/api/v1/entries/index" -import slugRouteHandler from "../../../pages/api/v1/entries/[slug]" -import paramRouteHandler from "../../../pages/api/v1/entries/[...param]" -import { Entry } from "../../../interfaces" +import indexRouteHandler from "@/pages/api/v1/entries/index" +import slugRouteHandler from "@/pages/api/v1/entries/[slug]" +import paramRouteHandler from "@/pages/api/v1/entries/[...param]" +import { Entry } from "@/interfaces" +import checkPermissionApi from "@/lib/check-permission-api" -describe("/api/v1/entries/", () => { - afterEach(() => { +// Mock rate limiting to be a direct pass-through to simplify route testing +jest.mock("@/lib/api/rate-limits", () => require("@/__mocks__/lib/api/rate-limits")) + +// Mock permission checking API helper to isolate controller validation +jest.mock("@/lib/check-permission-api", () => require("@/__mocks__/lib/check-permission-api")) + +// Mock API key controller and utility checks +jest.mock("@/lib/api/utils", () => require("@/__mocks__/lib/api/utils")) + +jest.mock("@/controllers/api-key.controller", () => require("@/__mocks__/controllers/api-key.controller")) + +// Mock apiBuilderController to bypass MongoDB queries and return mock objects directly +jest.mock("@/controllers/api-builder.controller", () => + require("@/__mocks__/controllers/api-builder.controller") +) + +describe("/api/v1/entries", () => { + const mockCheckPermissionApi = checkPermissionApi as jest.Mock + + beforeEach(() => { jest.clearAllMocks() }) + it("GET / - should return all entries", async () => { + mockCheckPermissionApi.mockResolvedValue(true) + const request = httpMocks.createRequest({ method: "GET", url: "/api/v1/entries", query: { - apikey: "dbbf764d6ce785adb15a184699867b24" + apikey: "system-key" } }) const response = httpMocks.createResponse() - await indexRouteHandler(request, response) const data = await response._getJSONData() + expect(response._getStatusCode()).toBe(200) - expect(data.message).not.toBe({ message: "You're not authorized!" }) expect(Array.isArray(data)).toBe(true) + expect(data.length).toBe(5) }) - it("GET / - should return 'You're not authorized' if there is no API key ", async () => { + it("GET / - should return 'You're not authorized' if there is no API key", async () => { const request = httpMocks.createRequest({ method: "GET", url: "/api/v1/entries", @@ -37,19 +63,21 @@ describe("/api/v1/entries/", () => { }) const response = httpMocks.createResponse() - await indexRouteHandler(request, response) const data = await response._getJSONData() - expect(response._getStatusCode()).toBe(200) + + expect(response._getStatusCode()).toBe(401) expect(data.message).toBe("You're not authorized!") }) - it("GET /first_X - should return first X entries", async () => { + it("GET /first_X - should return first X entries", async () => { + mockCheckPermissionApi.mockResolvedValue(true) + const request = httpMocks.createRequest({ method: "GET", url: "/api/v1/entries", query: { - apikey: "dbbf764d6ce785adb15a184699867b24", + apikey: "system-key", slug: "first_3" } }) @@ -57,17 +85,19 @@ describe("/api/v1/entries/", () => { await slugRouteHandler(request, response) const data = await response._getJSONData() + expect(response._getStatusCode()).toBe(200) - expect(data.message).not.toBe({ message: "You're not authorized!" }) expect(data.length).toBeLessThanOrEqual(3) }) - it("GET /last_X - should return last X entries", async () => { + it("GET /last_X - should return last X entries", async () => { + mockCheckPermissionApi.mockResolvedValue(true) + const request = httpMocks.createRequest({ method: "GET", url: "/api/v1/entries", query: { - apikey: "dbbf764d6ce785adb15a184699867b24", + apikey: "system-key", slug: "last_3" } }) @@ -75,17 +105,19 @@ describe("/api/v1/entries/", () => { await slugRouteHandler(request, response) const data = await response._getJSONData() + expect(response._getStatusCode()).toBe(200) - expect(data.message).not.toBe({ message: "You're not authorized!" }) expect(data.length).toBeLessThanOrEqual(3) }) - it("GET /random_X - should return random X entries", async () => { + it("GET /random_X - should return random X entries", async () => { + mockCheckPermissionApi.mockResolvedValue(true) + const request = httpMocks.createRequest({ method: "GET", url: "/api/v1/entries", query: { - apikey: "dbbf764d6ce785adb15a184699867b24", + apikey: "system-key", slug: "random_3" } }) @@ -93,17 +125,19 @@ describe("/api/v1/entries/", () => { await slugRouteHandler(request, response) const data = await response._getJSONData() + expect(response._getStatusCode()).toBe(200) - expect(data.message).not.toBe({ message: "You're not authorized!" }) expect(data.length).toBeLessThanOrEqual(3) }) - it("GET /api/v1/entries/:namespace - should return entries from a namespace", async () => { + it("GET /api/v1/entries/:namespace - should return entries from a namespace", async () => { + mockCheckPermissionApi.mockResolvedValue(true) + const request = httpMocks.createRequest({ method: "GET", url: "/api/v1/entries", query: { - apikey: "dbbf764d6ce785adb15a184699867b24", + apikey: "system-key", slug: "test-entry-type" } }) @@ -111,69 +145,101 @@ describe("/api/v1/entries/", () => { await slugRouteHandler(request, response) const data = await response._getJSONData() + expect(response._getStatusCode()).toBe(200) data.forEach((entry: Entry) => { expect(entry.namespace).toEqual("test-entry-type") }) }) - it("GET /api/v1/entries/:namespace/first_X - should return first X entries from a namespace", async () => { + it("GET /api/v1/entries/:namespace-part-1/:namespace-part-2/ - should return entries from sub-namespace", async () => { + mockCheckPermissionApi.mockResolvedValue(true) + const request = httpMocks.createRequest({ method: "GET", url: "/api/v1/entries", query: { - apikey: "dbbf764d6ce785adb15a184699867b24", - param: ["test-entry-type", "first_3"] + apikey: "system-key", + param: ["newtest", "newtest1"] } }) const response = httpMocks.createResponse() await paramRouteHandler(request, response) const data = await response._getJSONData() + + expect(response._getStatusCode()).toBe(200) + data.forEach((entry: Entry) => { + expect(entry.namespace).toEqual("newtest.newtest1") + }) + }) + + it("GET /api/v1/entries/:namespace-part-1/:namespace-part-2/first_x - should return first X entries from sub-namespace", async () => { + mockCheckPermissionApi.mockResolvedValue(true) + + const request = httpMocks.createRequest({ + method: "GET", + url: "/api/v1/entries", + query: { + apikey: "system-key", + param: ["newtest", "newtest1", "first_3"] + } + }) + const response = httpMocks.createResponse() + + await paramRouteHandler(request, response) + const data = await response._getJSONData() + expect(response._getStatusCode()).toBe(200) expect(data.length).toBeLessThanOrEqual(3) data.forEach((entry: Entry) => { - expect(entry.namespace).toEqual("test-entry-type") + expect(entry.namespace).toEqual("newtest.newtest1") }) }) - it("GET /api/v1/entries/:namespace/last_X - should return last X entries from a namespace", async () => { + it("GET /api/v1/entries/:namespace-part-1/:namespace-part-2/last_x - should return last X entries from sub-namespace", async () => { + mockCheckPermissionApi.mockResolvedValue(true) + const request = httpMocks.createRequest({ method: "GET", url: "/api/v1/entries", query: { - apikey: "dbbf764d6ce785adb15a184699867b24", - param: ["test-entry-type", "last_3"] + apikey: "system-key", + param: ["newtest", "newtest1", "last_3"] } }) const response = httpMocks.createResponse() await paramRouteHandler(request, response) const data = await response._getJSONData() + expect(response._getStatusCode()).toBe(200) expect(data.length).toBeLessThanOrEqual(3) data.forEach((entry: Entry) => { - expect(entry.namespace).toEqual("test-entry-type") + expect(entry.namespace).toEqual("newtest.newtest1") }) }) - it("GET /api/v1/entries/:namespace/random_X - should return random X entries from a namespace", async () => { + it("GET /api/v1/entries/:namespace-part-1/:namespace-part-2/random_x - should return random X entries from sub-namespace", async () => { + mockCheckPermissionApi.mockResolvedValue(true) + const request = httpMocks.createRequest({ method: "GET", url: "/api/v1/entries", query: { - apikey: "dbbf764d6ce785adb15a184699867b24", - param: ["test-entry-type", "random_3"] + apikey: "system-key", + param: ["newtest", "newtest1", "random_3"] } }) const response = httpMocks.createResponse() await paramRouteHandler(request, response) const data = await response._getJSONData() + expect(response._getStatusCode()).toBe(200) expect(data.length).toBeLessThanOrEqual(3) data.forEach((entry: Entry) => { - expect(entry.namespace).toEqual("test-entry-type") + expect(entry.namespace).toEqual("newtest.newtest1") }) }) }) diff --git a/__tests__/api/entries/entry.api.test.ts b/__tests__/api/entries/entry.api.test.ts index 2818134..5432e2e 100644 --- a/__tests__/api/entries/entry.api.test.ts +++ b/__tests__/api/entries/entry.api.test.ts @@ -1,224 +1,289 @@ +/** + * @jest-environment node + */ +/* eslint-disable @typescript-eslint/no-require-imports */ import httpMocks from "node-mocks-http" -import createRouteHandler from "../../../pages/api/v1/entry/create" -import updateRouteHandler from "../../../pages/api/v1/entry/update/[slug]" -import deleteRouteHandler from "../../../pages/api/v1/entry/delete/[slug]" +import createRouteHandler from "@/pages/api/v1/entry/create" +import updateRouteHandler from "@/pages/api/v1/entry/update/[slug]" +import deleteRouteHandler from "@/pages/api/v1/entry/delete/[slug]" import { NextApiRequest, NextApiResponse } from "next" +import { prisma } from "@/lib/prisma" +import checkPermissionApi from "@/lib/check-permission-api" + +// Mock rate limiting to be a direct pass-through to simplify route testing +jest.mock("@/lib/api/rate-limits", () => require("@/__mocks__/lib/api/rate-limits")) + +// Mock permission checking API helper to isolate controller validation +jest.mock("@/lib/check-permission-api", () => require("@/__mocks__/lib/check-permission-api")) + +// Mock API key controller and utility checks +jest.mock("@/lib/api/utils", () => require("@/__mocks__/lib/api/utils")) + +jest.mock("@/controllers/api-key.controller", () => require("@/__mocks__/controllers/api-key.controller")) + +// Mock Prisma client operations to run queries completely in-memory +jest.mock("@/lib/prisma") describe("/api/v1/entry/", () => { - let createdEntryId: string - afterEach(() => { + const mockCheckPermissionApi = checkPermissionApi as jest.Mock + const mockEntryCreate = prisma.entry.create as jest.Mock + const mockEntryUpdate = prisma.entry.update as jest.Mock + const mockEntryFindUnique = prisma.entry.findUnique as jest.Mock + const mockEntryDeleteMany = prisma.entry.deleteMany as jest.Mock + + beforeEach(() => { jest.clearAllMocks() + process.env.SECRET_KEY = "test-secret" }) - it("POST /create - should create the entry", async () => { - const request = httpMocks.createRequest({ - method: "POST", - url: "/api/v1/entry/create", - query: { - apikey: "dbbf764d6ce785adb15a184699867b24", - secretkey: "test", - mockclient: "true" - }, - body: { - author: "tester", - content: "This is a test entry created during API test", - name: "API Test Entry", - namespace: "test-entry-type", - slug: "api-test-entry" - } - }) - const response = httpMocks.createResponse() - await createRouteHandler(request, response) - const data = await response._getJSONData() - expect(response._getStatusCode()).toBe(200) - expect(data.result.status).toBe("success") - expect(data.result.message).toBe("Entry has been created.") - createdEntryId = data.entryId - }) + describe("POST /create", () => { + it("should successfully create the entry", async () => { + mockCheckPermissionApi.mockResolvedValue(true) + mockEntryCreate.mockResolvedValue({ id: "mock-entry-id" }) - it("GET /create - should return 'only POST request' message ", async () => { - const request = httpMocks.createRequest({ - method: "GET", - url: "/api/v1/entry/create", - query: { - apikey: "dbbf764d6ce785adb15a184699867b24", - secretkey: "test", - mockclient: "true" - }, - body: { - author: "tester", - content: "This is a test entry created during API test", - name: "API Test Entry", - namespace: "test-entry-type", - slug: "api-test-entry" - } - }) + const request = httpMocks.createRequest({ + method: "POST", + url: "/api/v1/entry/create", + query: { + apikey: "system-key", + secretkey: "test-secret", + mockclient: "true" + }, + body: { + author: "tester", + content: "This is a test entry created during API test", + name: "API Test Entry", + namespace: "test-entry-type", + slug: "api-test-entry" + } + }) - const response = httpMocks.createResponse() - await createRouteHandler(request, response) - const data = await response._getJSONData() - expect(response._getStatusCode()).toBe(200) - expect(data.message).toBe("You can only do POST request for this endpoint!") - }) + const response = httpMocks.createResponse() + await createRouteHandler(request, response) - it("POST /create - should return 'You're not authorized!' message if there is no API key or secret key ", async () => { - const request = httpMocks.createRequest({ - method: "POST", - url: "/api/v1/entry/create", - query: { - apikey: "", - secretkey: "test", - mockclient: "true" - }, - body: { - author: "tester", - content: "This is a test entry created during API test", - name: "API Test Entry", - namespace: "test-entry-type", - slug: "api-test-entry" - } + expect(response._getStatusCode()).toBe(200) + const data = response._getJSONData() + expect(data.result.status).toBe("success") + expect(data.result.message).toBe("Entry has been created.") + expect(data.entryId).toBe("mock-entry-id") }) - const response = httpMocks.createResponse() - await createRouteHandler(request, response) - const data = await response._getJSONData() - expect(response._getStatusCode()).toBe(200) - expect(data.message).toBe("You're not authorized!") - }) + it("should return error if not authorized", async () => { + const request = httpMocks.createRequest({ + method: "POST", + url: "/api/v1/entry/create", + query: { + apikey: "invalid-key", + secretkey: "wrong-secret", + mockclient: "true" + }, + body: { + author: "tester", + content: "This is a test entry created during API test", + name: "API Test Entry", + namespace: "test-entry-type", + slug: "api-test-entry" + } + }) - it("PUT /update - should update the entry", async () => { - const request = httpMocks.createRequest({ - method: "PUT", - url: "/api/v1/entry/update", - query: { - apikey: "dbbf764d6ce785adb15a184699867b24", - secretkey: "test", - mockclient: "true", - slug: createdEntryId - }, - body: { - author: "tester", - content: "This is a test entry updated during API test", - name: "API Test Entry", - namespace: "test-entry-type", - slug: "api-test-entry" - } - }) + const response = httpMocks.createResponse() + await createRouteHandler(request, response) - const response = httpMocks.createResponse() - await updateRouteHandler(request, response) - const data = await response._getJSONData() - expect(response._getStatusCode()).toBe(200) - expect(data.status).toBe("success") - expect(data.message).toBe("Entry has been updated.") - }) - it("GET /update - should return 'only PUT request' message", async () => { - const request = httpMocks.createRequest({ - method: "GET", - url: "/api/v1/entry/update", - query: { - apikey: "dbbf764d6ce785adb15a184699867b24", - secretkey: "test", - mockclient: "true", - slug: createdEntryId - }, - body: { - author: "tester", - content: "This is a test entry created during API test", - name: "API Test Entry", - namespace: "test-entry-type", - slug: "api-test-entry" - } + expect(response._getStatusCode()).toBe(401) + const data = response._getJSONData() + expect(data.message).toBe("You're not authorized!") }) - const response = httpMocks.createResponse() - await updateRouteHandler(request, response) - const data = await response._getJSONData() - expect(response._getStatusCode()).toBe(200) - expect(data.message).toBe("You can only do PUT request for this endpoint!") + it("should accept only POST requests", async () => { + mockCheckPermissionApi.mockResolvedValue(true) + + const request = httpMocks.createRequest({ + method: "GET", + url: "/api/v1/entry/create", + query: { + apikey: "system-key", + secretkey: "test-secret", + mockclient: "true" + }, + body: { + author: "tester", + content: "This is a test entry created during API test", + name: "API Test Entry", + namespace: "test-entry-type", + slug: "api-test-entry" + } + }) + + const response = httpMocks.createResponse() + await createRouteHandler(request, response) + + expect(response._getStatusCode()).toBe(200) + const data = response._getJSONData() + expect(data.message).toBe("You can only do POST request for this endpoint!") + }) }) - it("PUT /update - should return 'You're not authorized!' message if there is no API key or secret key ", async () => { - const request = httpMocks.createRequest({ - method: "PUT", - url: "/api/v1/entry/update", - query: { - apikey: "", - secretkey: "test", - mockclient: "true", - slug: createdEntryId - }, - body: { - author: "tester", - content: "This is a test entry created during API test", - name: "API Test Entry", + describe("PUT /update/[slug]", () => { + it("should successfully update the entry", async () => { + mockCheckPermissionApi.mockResolvedValue(true) + mockEntryFindUnique.mockResolvedValue({ + id: "mock-entry-id", + name: "Old Name", namespace: "test-entry-type", - slug: "api-test-entry" - } + slug: "api-test-entry", + data: {} + }) + mockEntryUpdate.mockResolvedValue({ id: "mock-entry-id" }) + + const request = httpMocks.createRequest({ + method: "PUT", + url: "/api/v1/entry/update", + query: { + slug: "mock-entry-id", + apikey: "system-key", + secretkey: "test-secret", + mockclient: "true" + }, + body: { + author: "tester", + content: "This is a test entry updated during API test", + name: "API Test Entry", + namespace: "test-entry-type", + slug: "api-test-entry" + } + }) + + const response = httpMocks.createResponse() + await updateRouteHandler(request, response) + + expect(response._getStatusCode()).toBe(200) + const data = response._getJSONData() + expect(data.status).toBe("success") + expect(data.message).toBe("Entry has been updated.") }) - const response = httpMocks.createResponse() - await updateRouteHandler(request, response) - const data = await response._getJSONData() - expect(response._getStatusCode()).toBe(200) - expect(data.message).toBe("You're not authorized!") - }) + it("should return error if not authorized", async () => { + const request = httpMocks.createRequest({ + method: "PUT", + url: "/api/v1/entry/update", + query: { + slug: "mock-entry-id", + apikey: "invalid-key", + secretkey: "wrong-secret", + mockclient: "true" + }, + body: { + author: "tester", + content: "This is a test entry created during API test", + name: "API Test Entry", + namespace: "test-entry-type", + slug: "api-test-entry" + } + }) + + const response = httpMocks.createResponse() + await updateRouteHandler(request, response) - it("DELETE /delete - should delete the entry", async () => { - const request = httpMocks.createRequest({ - method: "DELETE", - url: "/api/v1/entry/delete", - query: { - apikey: "dbbf764d6ce785adb15a184699867b24", - secretkey: "test", - mockclient: "true", - slug: createdEntryId - } + expect(response._getStatusCode()).toBe(401) + const data = response._getJSONData() + expect(data.message).toBe("You're not authorized!") }) - const response = httpMocks.createResponse() - await deleteRouteHandler(request, response) - const data = await response._getJSONData() - expect(response._getStatusCode()).toBe(200) - expect(data.status).toBe("success") - expect(data.message).toBe("Entry has been deleted.") + it("should accept only PUT requests", async () => { + mockCheckPermissionApi.mockResolvedValue(true) + + const request = httpMocks.createRequest({ + method: "GET", + url: "/api/v1/entry/update", + query: { + slug: "mock-entry-id", + apikey: "system-key", + secretkey: "test-secret", + mockclient: "true" + }, + body: { + author: "tester", + content: "This is a test entry created during API test", + name: "API Test Entry", + namespace: "test-entry-type", + slug: "api-test-entry" + } + }) + + const response = httpMocks.createResponse() + await updateRouteHandler(request, response) + + expect(response._getStatusCode()).toBe(200) + const data = response._getJSONData() + expect(data.message).toBe("You can only do PUT request for this endpoint!") + }) }) - it("GET /delete - should return 'only DELETE request' message ", async () => { - const request = httpMocks.createRequest({ - method: "GET", - url: "/api/v1/entry/delete", - query: { - apikey: "dbbf764d6ce785adb15a184699867b24", - secretkey: "test", - mockclient: "true", - slug: createdEntryId - } + describe("DELETE /delete/[slug]", () => { + it("should successfully delete the entry", async () => { + mockCheckPermissionApi.mockResolvedValue(true) + mockEntryDeleteMany.mockResolvedValue({ count: 1 }) + + const request = httpMocks.createRequest({ + method: "DELETE", + url: "/api/v1/entry/delete", + query: { + slug: "api-test-entry", + apikey: "system-key", + secretkey: "test-secret", + mockclient: "true" + } + }) + + const response = httpMocks.createResponse() + await deleteRouteHandler(request, response) + + expect(response._getStatusCode()).toBe(200) + const data = response._getJSONData() + expect(data.status).toBe("success") + expect(data.message).toBe("Entry has been deleted.") }) - const response = httpMocks.createResponse() - await deleteRouteHandler(request, response) - const data = await response._getJSONData() - expect(response._getStatusCode()).toBe(200) - expect(data.message).toBe("You can only do DELETE request for this endpoint!") - }) + it("should return error if not authorized", async () => { + const request = httpMocks.createRequest({ + method: "DELETE", + url: "/api/v1/entry/delete", + query: { + slug: "mock-entry-id", + apikey: "invalid-key", + secretkey: "wrong-secret", + mockclient: "true" + } + }) - it("DELETE /delete - should return 'You're not authorized!' message if there is no API key or secret key ", async () => { - const request = httpMocks.createRequest({ - method: "DELETE", - url: "/api/v1/entry/delete", - query: { - apikey: "", - secretkey: "", - mockclient: "true", - slug: createdEntryId - } + const response = httpMocks.createResponse() + await deleteRouteHandler(request, response) + + expect(response._getStatusCode()).toBe(401) + const data = response._getJSONData() + expect(data.message).toBe("You're not authorized!") }) - const response = httpMocks.createResponse() - await deleteRouteHandler(request, response) - const data = await response._getJSONData() - expect(response._getStatusCode()).toBe(200) - expect(data.message).toBe("You're not authorized!") + it("should accept only DELETE requests", async () => { + mockCheckPermissionApi.mockResolvedValue(true) + + const request = httpMocks.createRequest({ + method: "GET", + url: "/api/v1/entry/delete", + query: { + slug: "mock-entry-id", + apikey: "system-key", + secretkey: "test-secret", + mockclient: "true" + } + }) + + const response = httpMocks.createResponse() + await deleteRouteHandler(request, response) + + expect(response._getStatusCode()).toBe(200) + const data = response._getJSONData() + expect(data.message).toBe("You can only do DELETE request for this endpoint!") + }) }) }) diff --git a/__tests__/api/entry-types/entry-type.api.test.ts b/__tests__/api/entry-types/entry-type.api.test.ts index d8c92a5..56dbf6a 100644 --- a/__tests__/api/entry-types/entry-type.api.test.ts +++ b/__tests__/api/entry-types/entry-type.api.test.ts @@ -1,218 +1,332 @@ +/** + * @jest-environment node + */ +/* eslint-disable @typescript-eslint/no-require-imports */ import httpMocks from "node-mocks-http" -import createRouteHandler from "../../../pages/api/v1/entry-type/create" -import updateRouteHandler from "../../../pages/api/v1/entry-type/update/[slug]" -import deleteRouteHandler from "../../../pages/api/v1/entry-type/delete/[slug]" +import createRouteHandler from "@/pages/api/v1/entry-type/create" +import updateRouteHandler from "@/pages/api/v1/entry-type/update/[slug]" +import deleteRouteHandler from "@/pages/api/v1/entry-type/delete/[slug]" import { NextApiRequest, NextApiResponse } from "next" +import { prisma } from "@/lib/prisma" +import checkPermissionApi from "@/lib/check-permission-api" + +// Mock rate limiting to be a direct pass-through to simplify route testing +jest.mock("@/lib/api/rate-limits", () => require("@/__mocks__/lib/api/rate-limits")) + +// Mock permission checking API helper to isolate controller validation +jest.mock("@/lib/check-permission-api", () => require("@/__mocks__/lib/check-permission-api")) + +// Mock API key controller and utility checks +jest.mock("@/lib/api/utils", () => require("@/__mocks__/lib/api/utils")) + +jest.mock("@/controllers/api-key.controller", () => require("@/__mocks__/controllers/api-key.controller")) + +// Mock Prisma client operations to run queries completely in-memory +jest.mock("@/lib/prisma") describe("/api/v1/entry-type/", () => { - let createdEntryTypeId: string - afterEach(() => { + const mockCheckPermissionApi = checkPermissionApi as jest.Mock + const mockEntryTypeCreate = prisma.entryType.create as jest.Mock + const mockPermissionGroupFindFirst = prisma.permissionGroup.findFirst as jest.Mock + const mockPermissionGroupUpdate = prisma.permissionGroup.update as jest.Mock + const mockEntryTypeFindUnique = prisma.entryType.findUnique as jest.Mock + const mockEntryTypeUpdate = prisma.entryType.update as jest.Mock + const mockEntryTypeDeleteMany = prisma.entryType.deleteMany as jest.Mock + + beforeEach(() => { jest.clearAllMocks() + process.env.SECRET_KEY = "test-secret" }) - it("POST /create - should create the entry type", async () => { - const request = httpMocks.createRequest({ - method: "POST", - url: "/api/v1/entry-type/create", - query: { - apikey: "dbbf764d6ce785adb15a184699867b24", - secretkey: "test", - mockclient: "true" - }, - body: { - name: "Test Type", - namespace: "test-type", - fields: [], - createdBy: "admin" - } + + describe("POST /create", () => { + it("should successfully create the entry type", async () => { + mockCheckPermissionApi.mockResolvedValue(true) + mockEntryTypeCreate.mockResolvedValue({ id: "mock-id" }) + mockPermissionGroupFindFirst.mockResolvedValue({ + id: "perm-group-id", + slug: "admin", + privileges: [] + }) + mockPermissionGroupUpdate.mockResolvedValue({ id: "perm-group-id" }) + + const request = httpMocks.createRequest({ + method: "POST", + url: "/api/v1/entry-type/create", + query: { + apikey: "system-key", + secretkey: "test-secret", + mockclient: "true" + }, + body: { + name: "Test Type", + namespace: "test-type", + fields: [], + createdBy: "admin", + status: "Published" + } + }) + + const response = httpMocks.createResponse() + await createRouteHandler(request, response) + + expect(response._getStatusCode()).toBe(200) + const data = response._getJSONData() + expect(data.result.status).toBe("success") + expect(data.entryTypeId).toBe("mock-id") }) - const response = httpMocks.createResponse() - await createRouteHandler(request, response) - const data = await response._getJSONData() - expect(response._getStatusCode()).toBe(200) - expect(data.result.status).toBe("success") - expect(data.result.message).toBe("Entry Type has been created.") - createdEntryTypeId = data.entryTypeId - }) + it("should return error if not authorized", async () => { + const request = httpMocks.createRequest({ + method: "POST", + url: "/api/v1/entry-type/create", + query: { + apikey: "invalid-key", + secretkey: "wrong-secret", + mockclient: "true" + } + }) - it("GET /create - should return 'only POST request' message ", async () => { - const request = httpMocks.createRequest({ - method: "GET", - url: "/api/v1/entry-type/create", - query: { - apikey: "dbbf764d6ce785adb15a184699867b24", - secretkey: "test", - mockclient: "true" - }, - body: { - name: "Test Type", - namespace: "test-type", - fields: [], - createdBy: "admin" - } + const response = httpMocks.createResponse() + await createRouteHandler(request, response) + + expect(response._getStatusCode()).toBe(401) + const data = response._getJSONData() + expect(data.message).toBe("You're not authorized!") }) - const response = httpMocks.createResponse() - await createRouteHandler(request, response) - const data = await response._getJSONData() - expect(response._getStatusCode()).toBe(200) - expect(data.message).toBe("You can only do POST request for this endpoint!") - }) + it("should return error if permission check fails", async () => { + mockCheckPermissionApi.mockResolvedValue(false) - it("POST /create - should return 'You're not authorized!' message if there is no API key or secret key ", async () => { - const request = httpMocks.createRequest({ - method: "POST", - url: "/api/v1/entry-type/create", - query: { - apikey: "", - secretkey: "test", - mockclient: "true" - }, - body: { - name: "Test Type1", - namespace: "test-type1", - fields: [], - createdBy: "admin" - } + const request = httpMocks.createRequest({ + method: "POST", + url: "/api/v1/entry-type/create", + query: { + apikey: "system-key", + secretkey: "test-secret", + mockclient: "true" + } + }) + + const response = httpMocks.createResponse() + await createRouteHandler(request, response) + + expect(response._getStatusCode()).toBe(401) + const data = response._getJSONData() + expect(data.message).toBe("You're not authorized!") }) - const response = httpMocks.createResponse() - await createRouteHandler(request, response) - const data = await response._getJSONData() - expect(response._getStatusCode()).toBe(200) - expect(data.message).toBe("You're not authorized!") + it("should accept only POST requests", async () => { + mockCheckPermissionApi.mockResolvedValue(true) + + const request = httpMocks.createRequest({ + method: "GET", + url: "/api/v1/entry-type/create", + query: { + apikey: "system-key", + secretkey: "test-secret", + mockclient: "true" + } + }) + + const response = httpMocks.createResponse() + await createRouteHandler(request, response) + + expect(response._getStatusCode()).toBe(200) + const data = response._getJSONData() + expect(data.message).toBe("You can only do POST request for this endpoint!") + }) }) - it("PUT /update - should update the Entry type", async () => { - const request = httpMocks.createRequest({ - method: "PUT", - url: "/api/v1/entry-type/update", - query: { - apikey: "dbbf764d6ce785adb15a184699867b24", - secretkey: "test", - mockclient: "true", - slug: createdEntryTypeId - }, - body: { - name: "Test Type1", - namespace: "test-type1", - fields: [], - createdBy: "admin" - } + describe("PUT /update/[slug]", () => { + it("should successfully update the entry type", async () => { + mockCheckPermissionApi.mockResolvedValue(true) + mockEntryTypeFindUnique.mockResolvedValue({ + id: "mock-id", + name: "Old Name", + namespace: "test-type", + slug: "test-type", + status: "Draft", + fieldsets: [] + }) + mockEntryTypeUpdate.mockResolvedValue({ id: "mock-id" }) + + const request = httpMocks.createRequest({ + method: "PUT", + url: "/api/v1/entry-type/update", + query: { + slug: "mock-id", + apikey: "system-key", + secretkey: "test-secret", + mockclient: "true" + }, + body: { + name: "New Name", + namespace: "test-type", + slug: "test-type", + createdBy: "admin", + status: "Published", + fieldsets: [] + } + }) + + const response = httpMocks.createResponse() + await updateRouteHandler(request, response) + + expect(response._getStatusCode()).toBe(200) + const data = response._getJSONData() + expect(data.status).toBe("success") + expect(data.message).toBe("Entry Type has been updated.") }) - const response = httpMocks.createResponse() - await updateRouteHandler(request, response) - const data = await response._getJSONData() - expect(response._getStatusCode()).toBe(200) - expect(data.status).toBe("success") - expect(data.message).toBe("Entry Type has been updated.") - }) - it("GET /update - should return 'only PUT request' message ", async () => { - const request = httpMocks.createRequest({ - method: "GET", - url: "/api/v1/entry-type/update", - query: { - apikey: "dbbf764d6ce785adb15a184699867b24", - secretkey: "test", - mockclient: "true", - slug: createdEntryTypeId - }, - body: { - name: "Test Type1", - namespace: "test-type1", - fields: [], - createdBy: "admin" + it("should return failure message if no changes were made", async () => { + mockCheckPermissionApi.mockResolvedValue(true) + const state = { + id: "mock-id", + name: "Same Name", + namespace: "test-type", + slug: "test-type", + status: "Published", + fieldsets: [] } - }) + mockEntryTypeFindUnique.mockResolvedValue(state) + mockEntryTypeUpdate.mockResolvedValue(state) - const response = httpMocks.createResponse() - await updateRouteHandler(request, response) - const data = await response._getJSONData() - expect(response._getStatusCode()).toBe(200) - expect(data.message).toBe("You can only do PUT request for this endpoint!") - }) + const request = httpMocks.createRequest({ + method: "PUT", + url: "/api/v1/entry-type/update", + query: { + slug: "mock-id", + apikey: "system-key", + secretkey: "test-secret", + mockclient: "true" + }, + body: { + name: "Same Name", + namespace: "test-type", + slug: "test-type", + createdBy: "admin", + status: "Published", + fieldsets: [] + } + }) - it("PUT /update - should return 'You're not authorized!' message if there is no API key or secret key ", async () => { - const request = httpMocks.createRequest({ - method: "PUT", - url: "/api/v1/entry-type/update", - query: { - apikey: "", - secretkey: "test", - mockclient: "true", - slug: createdEntryTypeId - }, - body: { - name: "Test Type1", - namespace: "test-type1", - fields: [], - createdBy: "admin" - } + const response = httpMocks.createResponse() + await updateRouteHandler(request, response) + + expect(response._getStatusCode()).toBe(200) + const data = response._getJSONData() + expect(data.status).toBe("failed") + expect(data.message).toBe("You didn't make any change.") }) - const response = httpMocks.createResponse() - await updateRouteHandler(request, response) - const data = await response._getJSONData() - expect(response._getStatusCode()).toBe(200) - expect(data.message).toBe("You're not authorized!") - }) + it("should return error if not authorized", async () => { + const request = httpMocks.createRequest({ + method: "PUT", + url: "/api/v1/entry-type/update", + query: { + slug: "mock-id", + apikey: "invalid-key", + secretkey: "wrong-secret", + mockclient: "true" + } + }) - it("DELETE /delete - should delete the entry type", async () => { - const request = httpMocks.createRequest({ - method: "DELETE", - url: "/api/v1/entry-type/delete", - query: { - apikey: "dbbf764d6ce785adb15a184699867b24", - secretkey: "test", - mockclient: "true", - slug: createdEntryTypeId - } + const response = httpMocks.createResponse() + await updateRouteHandler(request, response) + + expect(response._getStatusCode()).toBe(401) + const data = response._getJSONData() + expect(data.message).toBe("You're not authorized!") }) - const response = httpMocks.createResponse() - await deleteRouteHandler(request, response) - const data = await response._getJSONData() - expect(response._getStatusCode()).toBe(200) - expect(data.status).toBe("success") - expect(data.message).toBe("Entry Type has been deleted.") + it("should accept only PUT requests", async () => { + mockCheckPermissionApi.mockResolvedValue(true) + + const request = httpMocks.createRequest({ + method: "GET", + url: "/api/v1/entry-type/update", + query: { + slug: "mock-id", + apikey: "system-key", + secretkey: "test-secret", + mockclient: "true" + } + }) + + const response = httpMocks.createResponse() + await updateRouteHandler(request, response) + + expect(response._getStatusCode()).toBe(200) + const data = response._getJSONData() + expect(data.message).toBe("You can only do PUT request for this endpoint!") + }) }) - it("GET /delete - should return 'only DELETE request' message ", async () => { - const request = httpMocks.createRequest({ - method: "GET", - url: "/api/v1/entry-type/delete", - query: { - apikey: "dbbf764d6ce785adb15a184699867b24", - secretkey: "test", - mockclient: "true", - slug: createdEntryTypeId - } + describe("DELETE /delete/[slug]", () => { + it("should successfully delete the entry type", async () => { + mockCheckPermissionApi.mockResolvedValue(true) + mockEntryTypeDeleteMany.mockResolvedValue({ count: 1 }) + + const request = httpMocks.createRequest({ + method: "DELETE", + url: "/api/v1/entry-type/delete", + query: { + slug: "test-type", + apikey: "system-key", + secretkey: "test-secret", + mockclient: "true" + } + }) + + const response = httpMocks.createResponse() + await deleteRouteHandler(request, response) + + expect(response._getStatusCode()).toBe(200) + const data = response._getJSONData() + expect(data.status).toBe("success") + expect(data.message).toBe("Entry Type has been deleted.") }) - const response = httpMocks.createResponse() - await deleteRouteHandler(request, response) - const data = await response._getJSONData() - expect(response._getStatusCode()).toBe(200) - expect(data.message).toBe("You can only do DELETE request for this endpoint!") - }) + it("should return error if not authorized", async () => { + const request = httpMocks.createRequest({ + method: "DELETE", + url: "/api/v1/entry-type/delete", + query: { + slug: "test-type", + apikey: "invalid-key", + secretkey: "wrong-secret", + mockclient: "true" + } + }) - it("DELETE /delete - should return 'You're not authorized!' message if there is no API key or secret key ", async () => { - const request = httpMocks.createRequest({ - method: "DELETE", - url: "/api/v1/entry-type/delete", - query: { - apikey: "", - secretkey: "", - mockclient: "true", - slug: createdEntryTypeId - } + const response = httpMocks.createResponse() + await deleteRouteHandler(request, response) + + expect(response._getStatusCode()).toBe(401) + const data = response._getJSONData() + expect(data.message).toBe("You're not authorized!") }) - const response = httpMocks.createResponse() - await deleteRouteHandler(request, response) - const data = await response._getJSONData() - expect(response._getStatusCode()).toBe(200) - expect(data.message).toBe("You're not authorized!") + it("should accept only DELETE requests", async () => { + mockCheckPermissionApi.mockResolvedValue(true) + + const request = httpMocks.createRequest({ + method: "GET", + url: "/api/v1/entry-type/delete", + query: { + slug: "test-type", + apikey: "system-key", + secretkey: "test-secret", + mockclient: "true" + } + }) + + const response = httpMocks.createResponse() + await deleteRouteHandler(request, response) + + expect(response._getStatusCode()).toBe(200) + const data = response._getJSONData() + expect(data.message).toBe("You can only do DELETE request for this endpoint!") + }) }) }) diff --git a/__tests__/api/entry-types/entry-types.api.test.ts b/__tests__/api/entry-types/entry-types.api.test.ts index 66cd825..de40989 100644 --- a/__tests__/api/entry-types/entry-types.api.test.ts +++ b/__tests__/api/entry-types/entry-types.api.test.ts @@ -1,33 +1,59 @@ +/** + * @jest-environment node + */ +/* eslint-disable @typescript-eslint/no-require-imports */ import httpMocks from "node-mocks-http" import { NextApiRequest, NextApiResponse } from "next" -import indexRouteHandler from "../../../pages/api/v1/entry-types/index" -import slugRouteHandler from "../../../pages/api/v1/entry-types/[slug]" -import paramRouteHandler from "../../../pages/api/v1/entry-types/[...param]" -import { EntryType } from "../../../interfaces" +import indexRouteHandler from "@/pages/api/v1/entry-types/index" +import slugRouteHandler from "@/pages/api/v1/entry-types/[slug]" +import paramRouteHandler from "@/pages/api/v1/entry-types/[...param]" +import { EntryType } from "@/interfaces" +import checkPermissionApi from "@/lib/check-permission-api" + +// Mock rate limiting to be a direct pass-through to simplify route testing +jest.mock("@/lib/api/rate-limits", () => require("@/__mocks__/lib/api/rate-limits")) + +// Mock permission checking API helper to isolate controller validation +jest.mock("@/lib/check-permission-api", () => require("@/__mocks__/lib/check-permission-api")) + +// Mock API key controller and utility checks +jest.mock("@/lib/api/utils", () => require("@/__mocks__/lib/api/utils")) + +jest.mock("@/controllers/api-key.controller", () => require("@/__mocks__/controllers/api-key.controller")) + +// Mock apiBuilderController to bypass MongoDB queries and return mock objects directly +jest.mock("@/controllers/api-builder.controller", () => + require("@/__mocks__/controllers/api-builder.controller") +) describe("/api/v1/entry-types", () => { - afterEach(() => { + const mockCheckPermissionApi = checkPermissionApi as jest.Mock + + beforeEach(() => { jest.clearAllMocks() }) + it("GET / - should return all entry types", async () => { + mockCheckPermissionApi.mockResolvedValue(true) + const request = httpMocks.createRequest({ method: "GET", url: "/api/v1/entry-types", query: { - apikey: "dbbf764d6ce785adb15a184699867b24" + apikey: "system-key" } }) const response = httpMocks.createResponse() - await indexRouteHandler(request, response) const data = await response._getJSONData() + expect(response._getStatusCode()).toBe(200) - expect(data.message).not.toBe({ message: "You're not authorized!" }) expect(Array.isArray(data)).toBe(true) + expect(data.length).toBe(5) }) - it("GET / - should return 'You're not authorized' if there is no API key ", async () => { + it("GET / - should return 'You're not authorized' if there is no API key", async () => { const request = httpMocks.createRequest({ method: "GET", url: "/api/v1/entry-types", @@ -37,19 +63,21 @@ describe("/api/v1/entry-types", () => { }) const response = httpMocks.createResponse() - await indexRouteHandler(request, response) const data = await response._getJSONData() - expect(response._getStatusCode()).toBe(200) + + expect(response._getStatusCode()).toBe(401) expect(data.message).toBe("You're not authorized!") }) - it("GET /first_X - should return first X user", async () => { + it("GET /first_X - should return first X user/entry types", async () => { + mockCheckPermissionApi.mockResolvedValue(true) + const request = httpMocks.createRequest({ method: "GET", url: "/api/v1/entry-types", query: { - apikey: "dbbf764d6ce785adb15a184699867b24", + apikey: "system-key", slug: "first_3" } }) @@ -57,17 +85,19 @@ describe("/api/v1/entry-types", () => { await slugRouteHandler(request, response) const data = await response._getJSONData() + expect(response._getStatusCode()).toBe(200) - expect(data.message).not.toBe({ message: "You're not authorized!" }) expect(data.length).toBeLessThanOrEqual(3) }) - it("GET /last_X - should return last X entry types", async () => { + it("GET /last_X - should return last X entry types", async () => { + mockCheckPermissionApi.mockResolvedValue(true) + const request = httpMocks.createRequest({ method: "GET", url: "/api/v1/entry-types", query: { - apikey: "dbbf764d6ce785adb15a184699867b24", + apikey: "system-key", slug: "last_3" } }) @@ -75,17 +105,19 @@ describe("/api/v1/entry-types", () => { await slugRouteHandler(request, response) const data = await response._getJSONData() + expect(response._getStatusCode()).toBe(200) - expect(data.message).not.toBe({ message: "You're not authorized!" }) expect(data.length).toBeLessThanOrEqual(3) }) - it("GET /random_X - should return random X entry types", async () => { + it("GET /random_X - should return random X entry types", async () => { + mockCheckPermissionApi.mockResolvedValue(true) + const request = httpMocks.createRequest({ method: "GET", url: "/api/v1/entry-types", query: { - apikey: "dbbf764d6ce785adb15a184699867b24", + apikey: "system-key", slug: "random_3" } }) @@ -93,36 +125,19 @@ describe("/api/v1/entry-types", () => { await slugRouteHandler(request, response) const data = await response._getJSONData() + expect(response._getStatusCode()).toBe(200) - expect(data.message).not.toBe({ message: "You're not authorized!" }) expect(data.length).toBeLessThanOrEqual(3) }) - it("GET /api/v1/users/:namespace - should return entry types from a namespace", async () => { - const request = httpMocks.createRequest({ - method: "GET", - url: "/api/v1/entry-types", - query: { - apikey: "dbbf764d6ce785adb15a184699867b24", - slug: "test-entry-type" - } - }) - const response = httpMocks.createResponse() - - await slugRouteHandler(request, response) - const data = await response._getJSONData() - expect(response._getStatusCode()).toBe(200) - data.forEach((entryType: EntryType) => { - expect(entryType.namespace).toEqual("test-entry-type") - }) - }) + it("GET /api/v1/users/:namespace - should return entry types from a namespace", async () => { + mockCheckPermissionApi.mockResolvedValue(true) - it("GET /api/v1/users/:namespace - should return entry types from a namespace", async () => { const request = httpMocks.createRequest({ method: "GET", url: "/api/v1/entry-types", query: { - apikey: "dbbf764d6ce785adb15a184699867b24", + apikey: "system-key", slug: "test-entry-type" } }) @@ -130,18 +145,21 @@ describe("/api/v1/entry-types", () => { await slugRouteHandler(request, response) const data = await response._getJSONData() + expect(response._getStatusCode()).toBe(200) data.forEach((entryType: EntryType) => { expect(entryType.namespace).toEqual("test-entry-type") }) }) - it("GET /api/v1/users/:namespace-part-1/:namespace-part-2/ - should return entry types from sub-namespace", async () => { + it("GET /api/v1/users/:namespace-part-1/:namespace-part-2/ - should return entry types from sub-namespace", async () => { + mockCheckPermissionApi.mockResolvedValue(true) + const request = httpMocks.createRequest({ method: "GET", url: "/api/v1/entry-types", query: { - apikey: "dbbf764d6ce785adb15a184699867b24", + apikey: "system-key", param: ["newtest", "newtest1"] } }) @@ -149,18 +167,21 @@ describe("/api/v1/entry-types", () => { await paramRouteHandler(request, response) const data = await response._getJSONData() + expect(response._getStatusCode()).toBe(200) data.forEach((entryType: EntryType) => { expect(entryType.namespace).toEqual("newtest.newtest1") }) }) - it("GET /api/v1/users/:namespace-part-1/:namespace-part-2/first_x - should return first X entry types from sub-namespace", async () => { + it("GET /api/v1/users/:namespace-part-1/:namespace-part-2/first_x - should return first X entry types from sub-namespace", async () => { + mockCheckPermissionApi.mockResolvedValue(true) + const request = httpMocks.createRequest({ method: "GET", url: "/api/v1/entry-types", query: { - apikey: "dbbf764d6ce785adb15a184699867b24", + apikey: "system-key", param: ["newtest", "newtest1", "first_3"] } }) @@ -168,6 +189,7 @@ describe("/api/v1/entry-types", () => { await paramRouteHandler(request, response) const data = await response._getJSONData() + expect(response._getStatusCode()).toBe(200) expect(data.length).toBeLessThanOrEqual(3) data.forEach((entryType: EntryType) => { @@ -175,12 +197,14 @@ describe("/api/v1/entry-types", () => { }) }) - it("GET /api/v1/users/:namespace-part-1/:namespace-part-2/last_x - should return last X entry types from sub-namespace", async () => { + it("GET /api/v1/users/:namespace-part-1/:namespace-part-2/last_x - should return last X entry types from sub-namespace", async () => { + mockCheckPermissionApi.mockResolvedValue(true) + const request = httpMocks.createRequest({ method: "GET", url: "/api/v1/entry-types", query: { - apikey: "dbbf764d6ce785adb15a184699867b24", + apikey: "system-key", param: ["newtest", "newtest1", "last_3"] } }) @@ -188,6 +212,7 @@ describe("/api/v1/entry-types", () => { await paramRouteHandler(request, response) const data = await response._getJSONData() + expect(response._getStatusCode()).toBe(200) expect(data.length).toBeLessThanOrEqual(3) data.forEach((entryType: EntryType) => { @@ -195,12 +220,14 @@ describe("/api/v1/entry-types", () => { }) }) - it("GET /api/v1/users/:namespace-part-1/:namespace-part-2/random_x - should return random X entry types from sub-namespace", async () => { + it("GET /api/v1/users/:namespace-part-1/:namespace-part-2/random_x - should return random X entry types from sub-namespace", async () => { + mockCheckPermissionApi.mockResolvedValue(true) + const request = httpMocks.createRequest({ method: "GET", url: "/api/v1/entry-types", query: { - apikey: "dbbf764d6ce785adb15a184699867b24", + apikey: "system-key", param: ["newtest", "newtest1", "random_3"] } }) @@ -208,6 +235,7 @@ describe("/api/v1/entry-types", () => { await paramRouteHandler(request, response) const data = await response._getJSONData() + expect(response._getStatusCode()).toBe(200) expect(data.length).toBeLessThanOrEqual(3) data.forEach((entryType: EntryType) => { diff --git a/__tests__/api/perm-groups/perm-group.api.test.ts b/__tests__/api/perm-groups/perm-group.api.test.ts index 62cba97..4a5c656 100644 --- a/__tests__/api/perm-groups/perm-group.api.test.ts +++ b/__tests__/api/perm-groups/perm-group.api.test.ts @@ -1,230 +1,317 @@ +/** + * @jest-environment node + */ +/* eslint-disable @typescript-eslint/no-require-imports */ import httpMocks from "node-mocks-http" -import createRouteHandler from "../../../pages/api/v1/permission-group/create" -import updateRouteHandler from "../../../pages/api/v1/permission-group/update/[slug]" -import deleteRouteHandler from "../../../pages/api/v1/permission-group/delete/[slug]" +import createRouteHandler from "@/pages/api/v1/permission-group/create" +import updateRouteHandler from "@/pages/api/v1/permission-group/update/[slug]" +import deleteRouteHandler from "@/pages/api/v1/permission-group/delete/[slug]" import { NextApiRequest, NextApiResponse } from "next" +import { prisma } from "@/lib/prisma" +import checkPermissionApi from "@/lib/check-permission-api" + +// Mock rate limiting to be a direct pass-through to simplify route testing +jest.mock("@/lib/api/rate-limits", () => require("@/__mocks__/lib/api/rate-limits")) + +// Mock permission checking API helper to isolate controller validation +jest.mock("@/lib/check-permission-api", () => require("@/__mocks__/lib/check-permission-api")) + +// Mock API key controller and utility checks +jest.mock("@/lib/api/utils", () => require("@/__mocks__/lib/api/utils")) +jest.mock("@/controllers/api-key.controller", () => require("@/__mocks__/controllers/api-key.controller")) + +// Mock Prisma client operations to run queries completely in-memory +jest.mock("@/lib/prisma") describe("/api/v1/permission-group/", () => { - let createdPermGroupId: string - afterEach(() => { + const mockCheckPermissionApi = checkPermissionApi as jest.Mock + const mockPermGroupCreate = prisma.permissionGroup.create as jest.Mock + const mockPermGroupUpdate = prisma.permissionGroup.update as jest.Mock + const mockPermGroupFindUnique = prisma.permissionGroup.findUnique as jest.Mock + const mockPermGroupDeleteMany = prisma.permissionGroup.deleteMany as jest.Mock + + beforeEach(() => { jest.clearAllMocks() + process.env.SECRET_KEY = "test-secret" }) - it("POST /create - should create the permission group", async () => { - const request = httpMocks.createRequest({ - method: "POST", - url: "/api/v1/permission-group/create", - query: { - apikey: "dbbf764d6ce785adb15a184699867b24", - secretkey: "test", - mockclient: "true" - }, - body: { - name: "Tester", - privileges: [], - slug: "tester" - } - }) - const response = httpMocks.createResponse() - await createRouteHandler(request, response) - const data = await response._getJSONData() - expect(response._getStatusCode()).toBe(200) - expect(data.result.status).toBe("success") - expect(data.result.message).toBe("Permission group has been created.") - createdPermGroupId = data.permGroupId - }) + describe("POST /create", () => { + it("should create the permission group", async () => { + mockCheckPermissionApi.mockResolvedValue(true) + mockPermGroupCreate.mockResolvedValue({ id: "mock-perm-id" }) - it("GET /create - should return 'only POST request' message ", async () => { - const request = httpMocks.createRequest({ - method: "GET", - url: "/api/v1/permission-group/create", - query: { - apikey: "dbbf764d6ce785adb15a184699867b24", - secretkey: "test", - mockclient: "true" - }, - body: { - name: "Tester", - privileges: [], - slug: "tester" - } + const request = httpMocks.createRequest({ + method: "POST", + url: "/api/v1/permission-group/create", + query: { + apikey: "system-key", + secretkey: "test-secret", + mockclient: "true" + }, + body: { + name: "Tester", + privileges: [], + slug: "tester" + } + }) + + const response = httpMocks.createResponse() + await createRouteHandler(request, response) + const data = await response._getJSONData() + + expect(response._getStatusCode()).toBe(200) + expect(data.result.status).toBe("success") + expect(data.result.message).toBe("Permission group has been created.") + expect(data.permGroupId).toBe("mock-perm-id") }) - const response = httpMocks.createResponse() - await createRouteHandler(request, response) - const data = await response._getJSONData() - expect(response._getStatusCode()).toBe(200) - expect(data.message).toBe("You can only do POST request for this endpoint!") - }) + it("should return 'only POST request' message", async () => { + mockCheckPermissionApi.mockResolvedValue(true) - it("POST /create - should return 'You're not authorized!' message if there is no API key or secret key ", async () => { - const request = httpMocks.createRequest({ - method: "POST", - url: "/api/v1/permission-group/create", - query: { - apikey: "", - secretkey: "test", - mockclient: "true" - }, - body: { - name: "Tester", - privileges: [], - slug: "tester" - } + const request = httpMocks.createRequest({ + method: "GET", + url: "/api/v1/permission-group/create", + query: { + apikey: "system-key", + secretkey: "test-secret", + mockclient: "true" + }, + body: { + name: "Tester", + privileges: [], + slug: "tester" + } + }) + + const response = httpMocks.createResponse() + await createRouteHandler(request, response) + const data = await response._getJSONData() + + expect(response._getStatusCode()).toBe(200) + expect(data.message).toBe("You can only do POST request for this endpoint!") }) - const response = httpMocks.createResponse() - await createRouteHandler(request, response) - const data = await response._getJSONData() - expect(response._getStatusCode()).toBe(200) - expect(data.message).toBe("You're not authorized!") + it("should return 'You're not authorized!' message if there is no API key or secret key", async () => { + const request = httpMocks.createRequest({ + method: "POST", + url: "/api/v1/permission-group/create", + query: { + apikey: "invalid", + secretkey: "wrong-secret", + mockclient: "true" + }, + body: { + name: "Tester", + privileges: [], + slug: "tester" + } + }) + + const response = httpMocks.createResponse() + await createRouteHandler(request, response) + const data = await response._getJSONData() + + expect(response._getStatusCode()).toBe(401) + expect(data.message).toBe("You're not authorized!") + }) }) - it("PUT /update - should update the permission group", async () => { - const request = httpMocks.createRequest({ - method: "PUT", - url: "/api/v1/permission-group/update", - query: { - apikey: "dbbf764d6ce785adb15a184699867b24", - secretkey: "test", - mockclient: "true", - slug: createdPermGroupId - }, - body: { - name: "Tester1", - privileges: [ - { - products: { - permissions: ["read", "create", "update", "delete"] + describe("PUT /update/[slug]", () => { + it("should update the permission group", async () => { + mockCheckPermissionApi.mockResolvedValue(true) + mockPermGroupFindUnique.mockResolvedValue({ + id: "mock-perm-id", + name: "Tester", + slug: "tester", + privileges: [] + }) + mockPermGroupUpdate.mockResolvedValue({ id: "mock-perm-id" }) + + const request = httpMocks.createRequest({ + method: "PUT", + url: "/api/v1/permission-group/update", + query: { + apikey: "system-key", + secretkey: "test-secret", + mockclient: "true", + slug: "mock-perm-id" + }, + body: { + name: "Tester1", + privileges: [ + { + products: { + permissions: ["read", "create", "update", "delete"] + } } - } - ], - slug: "tester1" - } + ], + slug: "tester1" + } + }) + + const response = httpMocks.createResponse() + await updateRouteHandler(request, response) + const data = await response._getJSONData() + + expect(response._getStatusCode()).toBe(200) + expect(data.status).toBe("success") + expect(data.message).toBe("Permission group has been updated.") }) - const response = httpMocks.createResponse() - await updateRouteHandler(request, response) - const data = await response._getJSONData() - expect(response._getStatusCode()).toBe(200) - expect(data.status).toBe("success") - expect(data.message).toBe("Permission group has been updated.") - }) - it("GET /update - should return 'only PUT request' message ", async () => { - const request = httpMocks.createRequest({ - method: "GET", - url: "/api/v1/permission-group/update", - query: { - apikey: "dbbf764d6ce785adb15a184699867b24", - secretkey: "test", - mockclient: "true", - slug: createdPermGroupId - }, - body: { - name: "Tester1", - privileges: [ - { - products: { - permissions: ["read", "create", "update", "delete"] - } - } - ], - slug: "tester1" + it("should return 'failed' if no change is made", async () => { + mockCheckPermissionApi.mockResolvedValue(true) + const state = { + id: "mock-perm-id", + name: "Tester", + slug: "tester", + privileges: [] } - }) + mockPermGroupFindUnique.mockResolvedValue(state) + mockPermGroupUpdate.mockResolvedValue(state) - const response = httpMocks.createResponse() - await updateRouteHandler(request, response) - const data = await response._getJSONData() - expect(response._getStatusCode()).toBe(200) - expect(data.message).toBe("You can only do PUT request for this endpoint!") - }) + const request = httpMocks.createRequest({ + method: "PUT", + url: "/api/v1/permission-group/update", + query: { + apikey: "system-key", + secretkey: "test-secret", + mockclient: "true", + slug: "mock-perm-id" + }, + body: { + name: "Tester", + privileges: [], + slug: "tester" + } + }) - it("PUT /update - should return 'You're not authorized!' message if there is no API key or secret key ", async () => { - const request = httpMocks.createRequest({ - method: "PUT", - url: "/api/v1/permission-group/update", - query: { - apikey: "", - secretkey: "test", - mockclient: "true", - slug: createdPermGroupId - }, - body: { - name: "Tester1", - privileges: [ - { - products: { - permissions: ["read", "create", "update", "delete"] - } - } - ], - slug: "tester1" - } + const response = httpMocks.createResponse() + await updateRouteHandler(request, response) + const data = await response._getJSONData() + + expect(response._getStatusCode()).toBe(200) + expect(data.status).toBe("failed") + expect(data.message).toBe("You didn't make any change.") }) - const response = httpMocks.createResponse() - await updateRouteHandler(request, response) - const data = await response._getJSONData() - expect(response._getStatusCode()).toBe(200) - expect(data.message).toBe("You're not authorized!") - }) + it("should return 'only PUT request' message", async () => { + mockCheckPermissionApi.mockResolvedValue(true) - it("DELETE /delete - should delete the permission group", async () => { - const request = httpMocks.createRequest({ - method: "DELETE", - url: "/api/v1/permission-group/delete", - query: { - apikey: "dbbf764d6ce785adb15a184699867b24", - secretkey: "test", - mockclient: "true", - slug: createdPermGroupId - } + const request = httpMocks.createRequest({ + method: "GET", + url: "/api/v1/permission-group/update", + query: { + apikey: "system-key", + secretkey: "test-secret", + mockclient: "true", + slug: "mock-perm-id" + }, + body: { + name: "Tester", + privileges: [], + slug: "tester" + } + }) + + const response = httpMocks.createResponse() + await updateRouteHandler(request, response) + const data = await response._getJSONData() + + expect(response._getStatusCode()).toBe(200) + expect(data.message).toBe("You can only do PUT request for this endpoint!") }) - const response = httpMocks.createResponse() - await deleteRouteHandler(request, response) - const data = await response._getJSONData() - expect(response._getStatusCode()).toBe(200) - expect(data.status).toBe("success") - expect(data.message).toBe("Permission group has been deleted.") + it("should return 'You're not authorized!' message if there is no API key or secret key", async () => { + const request = httpMocks.createRequest({ + method: "PUT", + url: "/api/v1/permission-group/update", + query: { + apikey: "invalid", + secretkey: "wrong-secret", + mockclient: "true", + slug: "mock-perm-id" + }, + body: { + name: "Tester", + privileges: [], + slug: "tester" + } + }) + + const response = httpMocks.createResponse() + await updateRouteHandler(request, response) + const data = await response._getJSONData() + + expect(response._getStatusCode()).toBe(401) + expect(data.message).toBe("You're not authorized!") + }) }) - it("GET /delete - should return 'only DELETE request' message ", async () => { - const request = httpMocks.createRequest({ - method: "GET", - url: "/api/v1/permission-group/delete", - query: { - apikey: "dbbf764d6ce785adb15a184699867b24", - secretkey: "test", - mockclient: "true", - slug: createdPermGroupId - } + describe("DELETE /delete/[slug]", () => { + it("should delete the permission group", async () => { + mockCheckPermissionApi.mockResolvedValue(true) + mockPermGroupDeleteMany.mockResolvedValue({ count: 1 }) + + const request = httpMocks.createRequest({ + method: "DELETE", + url: "/api/v1/permission-group/delete", + query: { + apikey: "system-key", + secretkey: "test-secret", + mockclient: "true", + slug: "mock-perm-id" + } + }) + + const response = httpMocks.createResponse() + await deleteRouteHandler(request, response) + const data = await response._getJSONData() + + expect(response._getStatusCode()).toBe(200) + expect(data.status).toBe("success") + expect(data.message).toBe("Permission group has been deleted.") }) - const response = httpMocks.createResponse() - await deleteRouteHandler(request, response) - const data = await response._getJSONData() - expect(response._getStatusCode()).toBe(200) - expect(data.message).toBe("You can only do DELETE request for this endpoint!") - }) + it("should return 'only DELETE request' message", async () => { + mockCheckPermissionApi.mockResolvedValue(true) - it("DELETE /delete - should return 'You're not authorized!' message if there is no API key or secret key ", async () => { - const request = httpMocks.createRequest({ - method: "DELETE", - url: "/api/v1/permission-group/delete", - query: { - apikey: "", - secretkey: "", - mockclient: "true", - slug: createdPermGroupId - } + const request = httpMocks.createRequest({ + method: "GET", + url: "/api/v1/permission-group/delete", + query: { + apikey: "system-key", + secretkey: "test-secret", + mockclient: "true", + slug: "mock-perm-id" + } + }) + + const response = httpMocks.createResponse() + await deleteRouteHandler(request, response) + const data = await response._getJSONData() + + expect(response._getStatusCode()).toBe(200) + expect(data.message).toBe("You can only do DELETE request for this endpoint!") }) - const response = httpMocks.createResponse() - await deleteRouteHandler(request, response) - const data = await response._getJSONData() - expect(response._getStatusCode()).toBe(200) - expect(data.message).toBe("You're not authorized!") + it("should return 'You're not authorized!' message if there is no API key or secret key", async () => { + const request = httpMocks.createRequest({ + method: "DELETE", + url: "/api/v1/permission-group/delete", + query: { + apikey: "invalid", + secretkey: "wrong-secret", + mockclient: "true", + slug: "mock-perm-id" + } + }) + + const response = httpMocks.createResponse() + await deleteRouteHandler(request, response) + const data = await response._getJSONData() + + expect(response._getStatusCode()).toBe(401) + expect(data.message).toBe("You're not authorized!") + }) }) }) diff --git a/__tests__/api/perm-groups/perm-groups.api.test.ts b/__tests__/api/perm-groups/perm-groups.api.test.ts index ff48a75..fdb684b 100644 --- a/__tests__/api/perm-groups/perm-groups.api.test.ts +++ b/__tests__/api/perm-groups/perm-groups.api.test.ts @@ -1,18 +1,42 @@ +/** + * @jest-environment node + */ +/* eslint-disable @typescript-eslint/no-require-imports */ import httpMocks from "node-mocks-http" import { NextApiRequest, NextApiResponse } from "next" -import indexRouteHandler from "../../../pages/api/v1/permission-groups/index" +import indexRouteHandler from "@/pages/api/v1/permission-groups/index" +import checkPermissionApi from "@/lib/check-permission-api" + +// Mock rate limiting to be a direct pass-through to simplify route testing +jest.mock("@/lib/api/rate-limits", () => require("@/__mocks__/lib/api/rate-limits")) + +// Mock permission checking API helper to isolate controller validation +jest.mock("@/lib/check-permission-api", () => require("@/__mocks__/lib/check-permission-api")) + +// Mock API key controller and utility checks +jest.mock("@/lib/api/utils", () => require("@/__mocks__/lib/api/utils")) +jest.mock("@/controllers/api-key.controller", () => require("@/__mocks__/controllers/api-key.controller")) + +// Mock apiBuilderController to bypass MongoDB queries and return mock objects directly +jest.mock("@/controllers/api-builder.controller", () => + require("@/__mocks__/controllers/api-builder.controller") +) describe("/api/v1/permission-groups/", () => { - afterEach(() => { + const mockCheckPermissionApi = checkPermissionApi as jest.Mock + + beforeEach(() => { jest.clearAllMocks() }) it("GET / - should return all permission groups", async () => { + mockCheckPermissionApi.mockResolvedValue(true) + const request = httpMocks.createRequest({ method: "GET", url: "/api/v1/permission-groups", query: { - apikey: "dbbf764d6ce785adb15a184699867b24" + apikey: "system-key" } }) @@ -20,9 +44,11 @@ describe("/api/v1/permission-groups/", () => { await indexRouteHandler(request, response) const data = await response._getJSONData() + expect(response._getStatusCode()).toBe(200) - expect(data.message).not.toBe({ message: "You're not authorized!" }) expect(Array.isArray(data)).toBe(true) + // The mock currently returns 5 objects + expect(data.length).toBe(5) }) it("GET / - should return 'You're not authorized' if there is no API key ", async () => { @@ -38,7 +64,8 @@ describe("/api/v1/permission-groups/", () => { await indexRouteHandler(request, response) const data = await response._getJSONData() - expect(response._getStatusCode()).toBe(200) + + expect(response._getStatusCode()).toBe(401) expect(data.message).toBe("You're not authorized!") }) }) diff --git a/__tests__/api/users/user.api.test.ts b/__tests__/api/users/user.api.test.ts index 727b243..5e16bc8 100644 --- a/__tests__/api/users/user.api.test.ts +++ b/__tests__/api/users/user.api.test.ts @@ -1,228 +1,347 @@ +/** + * @jest-environment node + */ +/* eslint-disable @typescript-eslint/no-require-imports */ import httpMocks from "node-mocks-http" -import createRouteHandler from "../../../pages/api/v1/user/create" -import updateRouteHandler from "../../../pages/api/v1/user/update/[slug]" -import deleteRouteHandler from "../../../pages/api/v1/user/delete/[slug]" +import createRouteHandler from "@/pages/api/v1/user/create" +import updateRouteHandler from "@/pages/api/v1/user/update/[slug]" +import deleteRouteHandler from "@/pages/api/v1/user/delete/[slug]" import { NextApiRequest, NextApiResponse } from "next" +import { prisma } from "@/lib/prisma" +import checkPermissionApi from "@/lib/check-permission-api" + +// Mock rate limiting to be a direct pass-through to simplify route testing +jest.mock("@/lib/api/rate-limits", () => require("@/__mocks__/lib/api/rate-limits")) + +// Mock permission checking API helper to isolate controller validation +jest.mock("@/lib/check-permission-api", () => require("@/__mocks__/lib/check-permission-api")) + +// Mock API key controller and utility checks +jest.mock("@/lib/api/utils", () => require("@/__mocks__/lib/api/utils")) +jest.mock("@/controllers/api-key.controller", () => require("@/__mocks__/controllers/api-key.controller")) + +// Mock Prisma client operations to run queries completely in-memory +jest.mock("@/lib/prisma") describe("/api/v1/user/", () => { - let createdUserId: string - afterEach(() => { + const mockCheckPermissionApi = checkPermissionApi as jest.Mock + const mockUserCreate = prisma.user.create as jest.Mock + const mockUserUpdate = prisma.user.update as jest.Mock + const mockUserFindUnique = prisma.user.findUnique as jest.Mock + const mockUserDeleteMany = prisma.user.deleteMany as jest.Mock + + beforeEach(() => { jest.clearAllMocks() + process.env.SECRET_KEY = "test-secret" }) - it("POST /create - should create the user", async () => { - const request = httpMocks.createRequest({ - method: "POST", - url: "/api/v1/user/create", - query: { - apikey: "dbbf764d6ce785adb15a184699867b24", - secretkey: "test", - mockclient: "true" - }, - body: { - username: "mock_user", - password: "superpowerfulpassword", - email: "mock_user@test.local", - permission_group: "member" - } - }) - const response = httpMocks.createResponse() - await createRouteHandler(request, response) - const data = await response._getJSONData() - expect(response._getStatusCode()).toBe(200) - expect(data.result.status).toBe("success") - expect(data.result.message).toBe("User has been created.") - createdUserId = data.userId - }) + describe("POST /create", () => { + it("should successfully create the user", async () => { + mockCheckPermissionApi.mockResolvedValue(true) + mockUserCreate.mockResolvedValue({ id: "mock-user-id" }) - it("GET /create - should return 'only POST request' message ", async () => { - const request = httpMocks.createRequest({ - method: "GET", - url: "/api/v1/user/create", - query: { - apikey: "dbbf764d6ce785adb15a184699867b24", - secretkey: "test" - }, - body: { - username: "mock_user", - password: "superpowerfulpassword", - email: "mock_user@test.local", - permission_group: "member" - } + const request = httpMocks.createRequest({ + method: "POST", + url: "/api/v1/user/create", + query: { + apikey: "system-key", + secretkey: "test-secret", + mockclient: "true" + }, + body: { + email: "test@test.com", + fullname: "Tester", + password: "password123", + permission_group: "tester", + profile_img: "", + status: "Active", + username: "tester" + } + }) + + const response = httpMocks.createResponse() + await createRouteHandler(request, response) + const data = await response._getJSONData() + + expect(response._getStatusCode()).toBe(200) + expect(data.result.status).toBe("success") + expect(data.result.message).toBe("User has been created.") + expect(data.userId).toBe("mock-user-id") }) - const response = httpMocks.createResponse() - await createRouteHandler(request, response) - const data = await response._getJSONData() - expect(response._getStatusCode()).toBe(200) - expect(data.message).toBe("You can only do POST request for this endpoint!") - }) + it("should return error if not authorized", async () => { + const request = httpMocks.createRequest({ + method: "POST", + url: "/api/v1/user/create", + query: { + apikey: "invalid-key", + secretkey: "wrong-secret", + mockclient: "true" + }, + body: { + email: "test@test.com", + fullname: "Tester", + password: "password123", + permission_group: "tester", + profile_img: "", + status: "Active", + username: "tester" + } + }) - it("POST /create - should return 'You're not authorized!' message if there is no API key or secret key ", async () => { - const request = httpMocks.createRequest({ - method: "POST", - url: "/api/v1/user/create", - query: { - apikey: "", - secretkey: "", - mockclient: "true" - }, - body: { - username: "mock_user", - password: "superpowerfulpassword", - email: "mock_user@test.local", - permission_group: "member" - } + const response = httpMocks.createResponse() + await createRouteHandler(request, response) + const data = await response._getJSONData() + + expect(response._getStatusCode()).toBe(401) + expect(data.message).toBe("You're not authorized!") }) - const response = httpMocks.createResponse() - await createRouteHandler(request, response) - const data = await response._getJSONData() - expect(response._getStatusCode()).toBe(200) - expect(data.message).toBe("You're not authorized!") + it("should accept only POST requests", async () => { + mockCheckPermissionApi.mockResolvedValue(true) + + const request = httpMocks.createRequest({ + method: "GET", + url: "/api/v1/user/create", + query: { + apikey: "system-key", + secretkey: "test-secret", + mockclient: "true" + }, + body: { + email: "test@test.com", + fullname: "Tester", + password: "password123", + permission_group: "tester", + profile_img: "", + status: "Active", + username: "tester" + } + }) + + const response = httpMocks.createResponse() + await createRouteHandler(request, response) + const data = await response._getJSONData() + + expect(response._getStatusCode()).toBe(200) + expect(data.message).toBe("You can only do POST request for this endpoint!") + }) }) - it("PUT /update - should update the user", async () => { - const request = httpMocks.createRequest({ - method: "PUT", - url: "/api/v1/user/update", - query: { - apikey: "dbbf764d6ce785adb15a184699867b24", - secretkey: "test", - slug: createdUserId, - mockclient: "true" - }, - body: { - username: "mocki_user1", - password: "superpowerful1password1", - pwchanged: true, - email: "mock_user_new_email@test.local", - permission_group: "member" - } + describe("PUT /update/[slug]", () => { + it("should successfully update the user", async () => { + mockCheckPermissionApi.mockResolvedValue(true) + mockUserFindUnique.mockResolvedValue({ + id: "mock-user-id", + email: "test@test.com", + fullname: "Tester", + password: "password123", + permission_group: "tester", + profile_img: "", + status: "Active", + username: "tester" + }) + mockUserUpdate.mockResolvedValue({ id: "mock-user-id" }) + + const request = httpMocks.createRequest({ + method: "PUT", + url: "/api/v1/user/update", + query: { + slug: "mock-user-id", + apikey: "system-key", + secretkey: "test-secret", + mockclient: "true" + }, + body: { + email: "test@test.com", + fullname: "Tester1", + password: "password123", + permission_group: "tester", + profile_img: "", + status: "Active", + username: "tester" + } + }) + + const response = httpMocks.createResponse() + await updateRouteHandler(request, response) + const data = await response._getJSONData() + + expect(response._getStatusCode()).toBe(200) + expect(data.status).toBe("success") + expect(data.message).toBe("User has been updated.") }) - const response = httpMocks.createResponse() - await updateRouteHandler(request, response) - const data = await response._getJSONData() - expect(response._getStatusCode()).toBe(200) - expect(data.status).toBe("success") - expect(data.message).toBe("User has been updated.") - }) - it("GET /update - should return 'only PUT request' message ", async () => { - const request = httpMocks.createRequest({ - method: "GET", - url: "/api/v1/user/update", - query: { - apikey: "dbbf764d6ce785adb15a184699867b24", - secretkey: "test", - slug: createdUserId - }, - body: { - username: "mock_user", - password: "superpowerfulpassword", - email: "mock_user@test.local", - permission_group: "member" + it("should return failure message if no changes were made", async () => { + mockCheckPermissionApi.mockResolvedValue(true) + const state = { + id: "mock-user-id", + email: "test@test.com", + fullname: "Tester", + password: "password123", + permission_group: "tester", + profile_img: "", + status: "Active", + username: "tester" } - }) + mockUserFindUnique.mockResolvedValue(state) + mockUserUpdate.mockResolvedValue(state) - const response = httpMocks.createResponse() - await updateRouteHandler(request, response) - const data = await response._getJSONData() - expect(response._getStatusCode()).toBe(200) - expect(data.message).toBe("You can only do PUT request for this endpoint!") - }) + const request = httpMocks.createRequest({ + method: "PUT", + url: "/api/v1/user/update", + query: { + slug: "mock-user-id", + apikey: "system-key", + secretkey: "test-secret", + mockclient: "true" + }, + body: { + email: "test@test.com", + fullname: "Tester", + password: "password123", + permission_group: "tester", + profile_img: "", + status: "Active", + username: "tester" + } + }) - it("PUT /update - should return 'You're not authorized!' message if there is no API key or secret key ", async () => { - const request = httpMocks.createRequest({ - method: "POST", - url: "/api/v1/user/update", - query: { - apikey: "", - secretkey: "", - mockclient: "true", - slug: createdUserId - }, - body: { - username: "mock_user", - password: "superpowerfulpassword", - email: "mock_user@test.local", - permission_group: "member" - } + const response = httpMocks.createResponse() + await updateRouteHandler(request, response) + const data = await response._getJSONData() + + expect(response._getStatusCode()).toBe(200) + expect(data.status).toBe("failed") + expect(data.message).toBe("You didn't make any change.") }) - const response = httpMocks.createResponse() - await updateRouteHandler(request, response) - const data = await response._getJSONData() - expect(response._getStatusCode()).toBe(200) - expect(data.message).toBe("You're not authorized!") - }) + it("should return error if not authorized", async () => { + const request = httpMocks.createRequest({ + method: "PUT", + url: "/api/v1/user/update", + query: { + slug: "mock-user-id", + apikey: "invalid-key", + secretkey: "wrong-secret", + mockclient: "true" + }, + body: { + email: "test@test.com", + fullname: "Tester1", + password: "password123", + permission_group: "tester", + profile_img: "", + status: "Active", + username: "tester" + } + }) - it("DELETE /delete - should delete the user", async () => { - const request = httpMocks.createRequest({ - method: "DELETE", - url: "/api/v1/user/delete", - query: { - apikey: "dbbf764d6ce785adb15a184699867b24", - secretkey: "test", - slug: createdUserId, - mockclient: "true" - } + const response = httpMocks.createResponse() + await updateRouteHandler(request, response) + const data = await response._getJSONData() + + expect(response._getStatusCode()).toBe(401) + expect(data.message).toBe("You're not authorized!") }) - const response = httpMocks.createResponse() - await deleteRouteHandler(request, response) - const data = await response._getJSONData() - expect(response._getStatusCode()).toBe(200) - expect(data.status).toBe("success") - expect(data.message).toBe("User has been deleted.") + it("should accept only PUT requests", async () => { + mockCheckPermissionApi.mockResolvedValue(true) + + const request = httpMocks.createRequest({ + method: "GET", + url: "/api/v1/user/update", + query: { + slug: "mock-user-id", + apikey: "system-key", + secretkey: "test-secret", + mockclient: "true" + }, + body: { + email: "test@test.com", + fullname: "Tester1", + password: "password123", + permission_group: "tester", + profile_img: "", + status: "Active", + username: "tester" + } + }) + + const response = httpMocks.createResponse() + await updateRouteHandler(request, response) + const data = await response._getJSONData() + + expect(response._getStatusCode()).toBe(200) + expect(data.message).toBe("You can only do PUT request for this endpoint!") + }) }) - it("GET /delete - should return 'only DELETE request' message ", async () => { - const request = httpMocks.createRequest({ - method: "GET", - url: "/api/v1/user/delete", - query: { - apikey: "dbbf764d6ce785adb15a184699867b24", - secretkey: "test", - slug: createdUserId - }, - body: { - username: "mock_user", - password: "superpowerfulpassword", - email: "mock_user@test.local", - permission_group: "member" - } + describe("DELETE /delete/[slug]", () => { + it("should successfully delete the user", async () => { + mockCheckPermissionApi.mockResolvedValue(true) + mockUserDeleteMany.mockResolvedValue({ count: 1 }) + + const request = httpMocks.createRequest({ + method: "DELETE", + url: "/api/v1/user/delete", + query: { + slug: "mock-user-id", + apikey: "system-key", + secretkey: "test-secret", + mockclient: "true" + } + }) + + const response = httpMocks.createResponse() + await deleteRouteHandler(request, response) + const data = await response._getJSONData() + + expect(response._getStatusCode()).toBe(200) + expect(data.status).toBe("success") + expect(data.message).toBe("User has been deleted.") }) - const response = httpMocks.createResponse() - await deleteRouteHandler(request, response) - const data = await response._getJSONData() - expect(response._getStatusCode()).toBe(200) - expect(data.message).toBe("You can only do DELETE request for this endpoint!") - }) + it("should return error if not authorized", async () => { + const request = httpMocks.createRequest({ + method: "DELETE", + url: "/api/v1/user/delete", + query: { + slug: "mock-user-id", + apikey: "invalid-key", + secretkey: "wrong-secret", + mockclient: "true" + } + }) - it("DELETE /delete - should return 'You're not authorized!' message if there is no API key or secret key ", async () => { - const request = httpMocks.createRequest({ - method: "POST", - url: "/api/v1/user/create", - query: { - apikey: "", - secretkey: "", - mockclient: "true", - slug: createdUserId - }, - body: { - username: "mock_user", - password: "superpowerfulpassword", - email: "mock_user@test.local", - permission_group: "member" - } + const response = httpMocks.createResponse() + await deleteRouteHandler(request, response) + const data = await response._getJSONData() + + expect(response._getStatusCode()).toBe(401) + expect(data.message).toBe("You're not authorized!") }) - const response = httpMocks.createResponse() - await deleteRouteHandler(request, response) - const data = await response._getJSONData() - expect(response._getStatusCode()).toBe(200) - expect(data.message).toBe("You're not authorized!") + it("should accept only DELETE requests", async () => { + mockCheckPermissionApi.mockResolvedValue(true) + + const request = httpMocks.createRequest({ + method: "GET", + url: "/api/v1/user/delete", + query: { + slug: "mock-user-id", + apikey: "system-key", + secretkey: "test-secret", + mockclient: "true" + } + }) + + const response = httpMocks.createResponse() + await deleteRouteHandler(request, response) + const data = await response._getJSONData() + + expect(response._getStatusCode()).toBe(200) + expect(data.message).toBe("You can only do DELETE request for this endpoint!") + }) }) }) diff --git a/__tests__/api/users/users.api.test.ts b/__tests__/api/users/users.api.test.ts index 245cd19..5714ca0 100644 --- a/__tests__/api/users/users.api.test.ts +++ b/__tests__/api/users/users.api.test.ts @@ -1,20 +1,45 @@ +/** + * @jest-environment node + */ +/* eslint-disable @typescript-eslint/no-require-imports */ import httpMocks from "node-mocks-http" -import indexRouteHandler from "../../../pages/api/v1/users/index" -import slugRouteHandler from "../../../pages/api/v1/users/[slug]" -import paramRouteHandler from "../../../pages/api/v1/users/[...param]" +import indexRouteHandler from "@/pages/api/v1/users/index" +import slugRouteHandler from "@/pages/api/v1/users/[slug]" +import paramRouteHandler from "@/pages/api/v1/users/[...param]" import { NextApiRequest, NextApiResponse } from "next" -import { User } from "../../../interfaces" +import { User } from "@/interfaces" +import checkPermissionApi from "@/lib/check-permission-api" + +// Mock rate limiting to be a direct pass-through to simplify route testing +jest.mock("@/lib/api/rate-limits", () => require("@/__mocks__/lib/api/rate-limits")) + +// Mock permission checking API helper to isolate controller validation +jest.mock("@/lib/check-permission-api", () => require("@/__mocks__/lib/check-permission-api")) + +// Mock API key controller and utility checks +jest.mock("@/lib/api/utils", () => require("@/__mocks__/lib/api/utils")) +jest.mock("@/controllers/api-key.controller", () => require("@/__mocks__/controllers/api-key.controller")) + +// Mock apiBuilderController to bypass MongoDB queries and return mock objects directly +jest.mock("@/controllers/api-builder.controller", () => + require("@/__mocks__/controllers/api-builder.controller") +) describe("/api/v1/users", () => { - afterEach(() => { + const mockCheckPermissionApi = checkPermissionApi as jest.Mock + + beforeEach(() => { jest.clearAllMocks() }) + it("GET / - should return all users", async () => { + mockCheckPermissionApi.mockResolvedValue(true) + const request = httpMocks.createRequest({ method: "GET", url: "/api/v1/users", query: { - apikey: "dbbf764d6ce785adb15a184699867b24" + apikey: "system-key" } }) @@ -22,12 +47,14 @@ describe("/api/v1/users", () => { await indexRouteHandler(request, response) const data = await response._getJSONData() + expect(response._getStatusCode()).toBe(200) - expect(data.message).not.toBe({ message: "You're not authorized!" }) expect(Array.isArray(data)).toBe(true) + // users with permission_group "root" are filtered out, so it will be 4 instead of 5 + expect(data.length).toBe(4) }) - it("GET / - should return 'You're not authorized' if there is no API key ", async () => { + it("GET / - should return 'You're not authorized' if there is no API key", async () => { const request = httpMocks.createRequest({ method: "GET", url: "/api/v1/users", @@ -40,16 +67,19 @@ describe("/api/v1/users", () => { await indexRouteHandler(request, response) const data = await response._getJSONData() - expect(response._getStatusCode()).toBe(200) + + expect(response._getStatusCode()).toBe(401) expect(data.message).toBe("You're not authorized!") }) - it("GET /first_X - should return first X user", async () => { + it("GET /first_X - should return first X user", async () => { + mockCheckPermissionApi.mockResolvedValue(true) + const request = httpMocks.createRequest({ method: "GET", url: "/api/v1/users", query: { - apikey: "dbbf764d6ce785adb15a184699867b24", + apikey: "system-key", slug: "first_3" } }) @@ -57,17 +87,19 @@ describe("/api/v1/users", () => { await slugRouteHandler(request, response) const data = await response._getJSONData() + expect(response._getStatusCode()).toBe(200) - expect(data.message).not.toBe({ message: "You're not authorized!" }) expect(data.length).toBeLessThanOrEqual(3) }) - it("GET /last_X - should return last X user", async () => { + it("GET /last_X - should return last X user", async () => { + mockCheckPermissionApi.mockResolvedValue(true) + const request = httpMocks.createRequest({ method: "GET", url: "/api/v1/users", query: { - apikey: "dbbf764d6ce785adb15a184699867b24", + apikey: "system-key", slug: "last_3" } }) @@ -75,17 +107,19 @@ describe("/api/v1/users", () => { await slugRouteHandler(request, response) const data = await response._getJSONData() + expect(response._getStatusCode()).toBe(200) - expect(data.message).not.toBe({ message: "You're not authorized!" }) expect(data.length).toBeLessThanOrEqual(3) }) - it("GET /random_X - should return random X user", async () => { + it("GET /random_X - should return random X user", async () => { + mockCheckPermissionApi.mockResolvedValue(true) + const request = httpMocks.createRequest({ method: "GET", url: "/api/v1/users", query: { - apikey: "dbbf764d6ce785adb15a184699867b24", + apikey: "system-key", slug: "random_3" } }) @@ -93,17 +127,19 @@ describe("/api/v1/users", () => { await slugRouteHandler(request, response) const data = await response._getJSONData() + expect(response._getStatusCode()).toBe(200) - expect(data.message).not.toBe({ message: "You're not authorized!" }) expect(data.length).toBeLessThanOrEqual(3) }) - it("GET /api/v1/users/:permission-group - should return users from a permission group", async () => { + it("GET /api/v1/users/:permission-group - should return users from a permission group", async () => { + mockCheckPermissionApi.mockResolvedValue(true) + const request = httpMocks.createRequest({ method: "GET", url: "/api/v1/users", query: { - apikey: "dbbf764d6ce785adb15a184699867b24", + apikey: "system-key", slug: "admin" } }) @@ -111,18 +147,21 @@ describe("/api/v1/users", () => { await slugRouteHandler(request, response) const data = await response._getJSONData() + expect(response._getStatusCode()).toBe(200) data.forEach((user: User) => { expect(user.permission_group).toEqual("admin") }) }) - it("GET /api/v1/users/:property-key/:property-value/ - should return users from a property key and value", async () => { + it("GET /api/v1/users/:property-key/:property-value/ - should return users from a property key and value", async () => { + mockCheckPermissionApi.mockResolvedValue(true) + const request = httpMocks.createRequest({ method: "GET", url: "/api/v1/users", query: { - apikey: "dbbf764d6ce785adb15a184699867b24", + apikey: "system-key", param: ["username", "mock_user"] } }) @@ -130,18 +169,21 @@ describe("/api/v1/users", () => { await paramRouteHandler(request, response) const data = await response._getJSONData() + expect(response._getStatusCode()).toBe(200) data.forEach((user: User) => { expect(user.username).toEqual("mock_user") }) }) - it("GET /api/v1/users/:property-key/:property-value/first_X - should return first X users from a property key and value ", async () => { + it("GET /api/v1/users/:property-key/:property-value/first_X - should return first X users from a property key and value", async () => { + mockCheckPermissionApi.mockResolvedValue(true) + const request = httpMocks.createRequest({ method: "GET", url: "/api/v1/users", query: { - apikey: "dbbf764d6ce785adb15a184699867b24", + apikey: "system-key", param: ["username", "mock_user", "first_3"] } }) @@ -149,6 +191,7 @@ describe("/api/v1/users", () => { await paramRouteHandler(request, response) const data = await response._getJSONData() + expect(response._getStatusCode()).toBe(200) expect(data.length).toBeLessThanOrEqual(3) data.forEach((user: User) => { @@ -156,12 +199,14 @@ describe("/api/v1/users", () => { }) }) - it("GET /api/v1/users/:property-key/:property-value/last_X - should return last X users from a property key and value ", async () => { + it("GET /api/v1/users/:property-key/:property-value/last_X - should return last X users from a property key and value", async () => { + mockCheckPermissionApi.mockResolvedValue(true) + const request = httpMocks.createRequest({ method: "GET", url: "/api/v1/users", query: { - apikey: "dbbf764d6ce785adb15a184699867b24", + apikey: "system-key", param: ["username", "mock_user", "last_3"] } }) @@ -169,6 +214,7 @@ describe("/api/v1/users", () => { await paramRouteHandler(request, response) const data = await response._getJSONData() + expect(response._getStatusCode()).toBe(200) expect(data.length).toBeLessThanOrEqual(3) data.forEach((user: User) => { @@ -176,12 +222,14 @@ describe("/api/v1/users", () => { }) }) - it("GET /api/v1/users/:property-key/:property-value/random_X - should return random X users from a property key and value ", async () => { + it("GET /api/v1/users/:property-key/:property-value/random_X - should return random X users from a property key and value", async () => { + mockCheckPermissionApi.mockResolvedValue(true) + const request = httpMocks.createRequest({ method: "GET", url: "/api/v1/users", query: { - apikey: "dbbf764d6ce785adb15a184699867b24", + apikey: "system-key", param: ["username", "mock_user", "random_3"] } }) @@ -189,6 +237,7 @@ describe("/api/v1/users", () => { await paramRouteHandler(request, response) const data = await response._getJSONData() + expect(response._getStatusCode()).toBe(200) expect(data.length).toBeLessThanOrEqual(3) data.forEach((user: User) => { diff --git a/__tests__/app/setup/setup-action.test.ts b/__tests__/app/setup/setup-action.test.ts new file mode 100644 index 0000000..5e89d1b --- /dev/null +++ b/__tests__/app/setup/setup-action.test.ts @@ -0,0 +1,104 @@ +import { runSetupAction } from "@/app/setup/setup-action" +import { UserController } from "@/controllers/user.controller" +import { PermissionGroupController } from "@/controllers/permission-group.controller" +import { apiKeyController } from "@/controllers/api-key.controller" +import { exec } from "child_process" +import { UserStatus } from "@/interfaces" + +jest.mock("child_process", () => ({ + exec: jest.fn() +})) + +jest.mock("@/controllers/user.controller") +jest.mock("@/controllers/permission-group.controller") +jest.mock("@/controllers/api-key.controller") + +jest.mock("@/lib/handlers/error", () => ({ + __esModule: true, + default: jest.fn((err) => ({ + success: false, + error: { message: err.message } + })) +})) + +describe("runSetupAction", () => { + const mockValues = { + ADMIN_FULLNAME: "Admin User", + ADMIN_USERNAME: "admin", + ADMIN_EMAIL: "admin@example.com", + ADMIN_PASSWORD: "password123" + } + + beforeEach(() => { + jest.clearAllMocks() + ;(exec as unknown as jest.Mock).mockImplementation((_cmd, cb) => cb(null, { stdout: "", stderr: "" })) + }) + + it("should successfully run setup", async () => { + // Mock successful controller operations + ;(UserController.prototype.create as jest.Mock).mockResolvedValue({ status: "success" }) + ;(PermissionGroupController.prototype.create as jest.Mock).mockResolvedValue({ status: "success" }) + ;(apiKeyController.prototype.create as jest.Mock).mockResolvedValue({ status: "success" }) + + const result = await runSetupAction(mockValues) + + expect(result.success).toBe(true) + expect(result.data?.adminAccount.username).toBe("admin") + expect(result.data?.apiKey).toBeDefined() + + expect(UserController).toHaveBeenCalledWith( + expect.objectContaining({ + fullname: "Admin User", + username: "admin", + email: "admin@example.com", + permission_group: "root", + status: UserStatus.Active + }), + false + ) + + expect(PermissionGroupController).toHaveBeenCalledTimes(3) // root, admin, viewer + expect(apiKeyController).toHaveBeenCalled() + expect(exec).toHaveBeenCalledWith("npx prisma db push", expect.any(Function)) + expect(exec).toHaveBeenCalledWith("npx prisma db seed", expect.any(Function)) + }) + + it("should return error if admin creation fails", async () => { + ;(UserController.prototype.create as jest.Mock).mockResolvedValue({ status: "failed" }) + + const result = await runSetupAction(mockValues) + + expect(result.success).toBe(false) + expect(result.error?.message).toBe("Admin creation failed") + }) + + it("should return error if permission group creation fails", async () => { + ;(UserController.prototype.create as jest.Mock).mockResolvedValue({ status: "success" }) + ;(PermissionGroupController.prototype.create as jest.Mock).mockResolvedValue({ status: "failed" }) + + const result = await runSetupAction(mockValues) + + expect(result.success).toBe(false) + expect(result.error?.message).toBe("Permission groups creation failed") + }) + + it("should return error if API key generation fails", async () => { + ;(UserController.prototype.create as jest.Mock).mockResolvedValue({ status: "success" }) + ;(PermissionGroupController.prototype.create as jest.Mock).mockResolvedValue({ status: "success" }) + ;(apiKeyController.prototype.create as jest.Mock).mockResolvedValue({ status: "failed" }) + + const result = await runSetupAction(mockValues) + + expect(result.success).toBe(false) + expect(result.error?.message).toBe("Failed to generate API key") + }) + + it("should catch unexpected errors and return generic error message", async () => { + ;(UserController.prototype.create as jest.Mock).mockRejectedValue(new Error("Unexpected")) + + const result = await runSetupAction(mockValues) + + expect(result.success).toBe(false) + expect(result.error?.message).toBe("Something went wrong") + }) +}) diff --git a/__tests__/auth.test.ts b/__tests__/auth.test.ts new file mode 100644 index 0000000..fa6f1f0 --- /dev/null +++ b/__tests__/auth.test.ts @@ -0,0 +1,241 @@ +import { config } from "@/auth" +import bcrypt from "bcryptjs" +import { prisma } from "@/lib/prisma" +import createUser from "@/lib/actions/studio/users/create-user" +import { getSettingsValue } from "@/lib/actions/studio/settings/get-settings-value" +import updateUser from "@/lib/actions/studio/users/update-user" +import { UserStatus } from "@/interfaces" + +/* eslint-disable @typescript-eslint/no-require-imports */ +jest.mock("next-auth", () => jest.fn(() => ({ + handlers: { GET: jest.fn(), POST: jest.fn() }, + auth: jest.fn(), + signIn: jest.fn(), + signOut: jest.fn() +}))) + +jest.mock("next-auth/providers/credentials", () => jest.fn((options) => ({ ...options, id: "credentials", name: "credentials" }))) +jest.mock("next-auth/providers/github", () => jest.fn((options) => ({ ...options, id: "github", name: "GitHub" }))) +jest.mock("next-auth/providers/google", () => jest.fn((options) => ({ ...options, id: "google", name: "Google" }))) +jest.mock("next-auth/providers/gitlab", () => jest.fn((options) => ({ ...options, id: "gitlab", name: "GitLab" }))) +jest.mock("next-auth/providers/bitbucket", () => jest.fn((options) => ({ ...options, id: "bitbucket", name: "Bitbucket" }))) +jest.mock("next-auth/providers/atlassian", () => jest.fn((options) => ({ ...options, id: "atlassian", name: "Atlassian" }))) +jest.mock("next-auth/providers/apple", () => jest.fn((options) => ({ ...options, id: "apple", name: "Apple" }))) +jest.mock("next-auth/providers/slack", () => jest.fn((options) => ({ ...options, id: "slack", name: "Slack" }))) +jest.mock("next-auth/providers/zoom", () => jest.fn((options) => ({ ...options, id: "zoom", name: "Zoom" }))) +jest.mock("next-auth/providers/linkedin", () => jest.fn((options) => ({ ...options, id: "linkedin", name: "LinkedIn" }))) +jest.mock("next-auth/providers/click-up", () => jest.fn((options) => ({ ...options, id: "click-up", name: "ClickUp" }))) +jest.mock("next-auth/providers/yandex", () => jest.fn((options) => ({ ...options, id: "yandex", name: "Yandex" }))) +jest.mock("next-auth/providers/netlify", () => jest.fn((options) => ({ ...options, id: "netlify", name: "Netlify" }))) +jest.mock("next-auth/providers/huggingface", () => jest.fn((options) => ({ ...options, id: "huggingface", name: "Huggingface" }))) +jest.mock("next-auth/providers/mailru", () => jest.fn((options) => ({ ...options, id: "mailru", name: "Mailru" }))) + +jest.mock("bcryptjs", () => ({ + compareSync: jest.fn() +})) + +jest.mock("@/lib/prisma", () => ({ + prisma: require("@/lib/__mocks__/prisma").prisma +})) + +jest.mock("@/lib/actions/studio/users/create-user", () => ({ + __esModule: true, + default: jest.fn() +})) + +jest.mock("@/lib/actions/studio/settings/get-settings-value", () => ({ + getSettingsValue: jest.fn() +})) + +jest.mock("@/lib/actions/studio/users/update-user", () => ({ + __esModule: true, + default: jest.fn() +})) + +describe("auth.ts", () => { + const mockBcryptCompare = bcrypt.compareSync as jest.Mock + const mockCreateUser = createUser as unknown as jest.Mock + const mockGetSettingsValue = getSettingsValue as jest.Mock + const mockUpdateUser = updateUser as unknown as jest.Mock + const mockPrismaFindFirst = prisma.user.findFirst as jest.Mock + const mockPrismaFindMany = prisma.user.findMany as jest.Mock + + beforeEach(() => { + jest.clearAllMocks() + process.env.DB_NAME = "test-db" + }) + + describe("CredentialsProvider.authorize", () => { + const authorize = (config.providers.find((p: any) => p.id === "credentials") as any).authorize + + it("should authorize successfully with correct credentials", async () => { + const mockUser = { + id: "user-1", + email: "test@example.com", + password: "hashed-password", + fullname: "Test User", + username: "testuser", + permission_group: "admin", + profile_img: "img.png", + status: UserStatus.Active + } + + mockPrismaFindFirst.mockResolvedValue(mockUser) + mockBcryptCompare.mockReturnValue(true) + + const result = await authorize({ email: "test@example.com", password: "password" }) + + expect(result).toEqual({ + id: "user-1", + email: "test@example.com", + name: "Test User", + username: "testuser", + permission_group: "admin", + profile_img: "img.png", + status: UserStatus.Active, + email_verified: true + }) + expect(prisma.user.findFirst).toHaveBeenCalledWith({ + where: { email: "test@example.com" } + }) + }) + + it("should return null if user not found", async () => { + mockPrismaFindFirst.mockResolvedValue(null) + + const result = await authorize({ email: "wrong@example.com", password: "password" }) + + expect(result).toBeNull() + }) + + it("should return null if password incorrect", async () => { + const mockUser = { + id: "user-1", + email: "test@example.com", + password: "hashed-password" + } + + mockPrismaFindFirst.mockResolvedValue(mockUser) + mockBcryptCompare.mockReturnValue(false) + + const result = await authorize({ email: "test@example.com", password: "wrong-password" }) + + expect(result).toBeNull() + }) + }) + + describe("signIn callback", () => { + const signIn = config.callbacks.signIn as any + + it("should return true for credentials provider if user is active", async () => { + const result = await signIn({ user: { status: UserStatus.Active }, account: { provider: "credentials" } }) + expect(result).toBe(true) + }) + + it("should return false if user is disabled", async () => { + const result = await signIn({ user: { status: UserStatus.Disabled }, account: { provider: "credentials" } }) + expect(result).toBe(false) + }) + + describe("OAuth", () => { + const oauthAccount = { provider: "github", providerAccountId: "oauth-123" } + const oauthProfile = { email: "oauth@example.com", name: "OAuth User", login: "oauthuser" } + + it("should create a new user if not found and email verified", async () => { + mockPrismaFindMany.mockResolvedValue([]) + mockGetSettingsValue.mockResolvedValue("viewer") + mockCreateUser.mockResolvedValue({ success: true }) + + const userObj: any = { email: "oauth@example.com", name: "OAuth User" } + const result = await signIn({ + user: userObj, + account: oauthAccount, + profile: { ...oauthProfile, email_verified: true } + }) + + expect(result).toBe(true) + expect(mockCreateUser).toHaveBeenCalled() + expect(userObj.permission_group).toBe("viewer") + }) + + it("should link account if user found by email but no oauth_id", async () => { + const dbUser = { + id: "existing-id", + email: "oauth@example.com", + fullname: "Existing User", + username: "existing", + status: UserStatus.Active, + permission_group: "admin" + } + + mockPrismaFindMany.mockImplementation((params) => { + if (params.where.email) return Promise.resolve([dbUser]) + return Promise.resolve([]) + }) + + mockUpdateUser.mockResolvedValue({ success: true }) + + const userObj: any = { email: "oauth@example.com", name: "Existing User" } + const result = await signIn({ + user: userObj, + account: oauthAccount, + profile: oauthProfile + }) + + expect(result).toBe(true) + expect(mockUpdateUser).toHaveBeenCalled() + expect(userObj.id).toBe("existing-id") + }) + }) + }) + + describe("jwt callback", () => { + const jwt = config.callbacks.jwt as any + + it("should populate token with user data on initial sign in", () => { + const user = { + id: "u1", + username: "user1", + permission_group: "admin", + profile_img: "img.png", + status: "active" + } + const token = {} + const result = jwt({ token, user }) + expect(result).toEqual({ + id: "u1", + username: "user1", + permission_group: "admin", + profile_img: "img.png", + status: "active" + }) + }) + + it("should return token as is if no user provided", () => { + const token = { id: "u1" } + const result = jwt({ token }) + expect(result).toEqual(token) + }) + }) + + describe("session callback", () => { + const sessionCallback = config.callbacks.session as any + + it("should map token data to session", () => { + const session = { user: {} } + const token = { + id: "u1", + username: "user1", + permission_group: "admin", + profile_img: "img.png", + status: "active" + } + const result = sessionCallback({ session, token }) + expect(result.id).toBe("u1") + expect(result.user.username).toBe("user1") + expect(result.user.permission_group).toBe("admin") + expect(result.user.profile_img).toBe("img.png") + expect(result.user.status).toBe("active") + }) + }) +}) diff --git a/__tests__/controllers/api-builder.controller.test.ts b/__tests__/controllers/api-builder.controller.test.ts new file mode 100644 index 0000000..1b50b04 --- /dev/null +++ b/__tests__/controllers/api-builder.controller.test.ts @@ -0,0 +1,133 @@ +import { apiBuilderController } from "@/controllers/api-builder.controller" +import { prisma } from "@/lib/prisma" + +/* eslint-disable @typescript-eslint/no-require-imports */ +jest.mock("@/lib/prisma", () => ({ + prisma: require("@/lib/__mocks__/prisma").prisma +})) + +describe("apiBuilderController", () => { + const mockPrismaEntryFindMany = prisma.entry.findMany as jest.Mock + beforeEach(() => { + jest.clearAllMocks() + }) + + describe("fetchData", () => { + it("should fetch all entries when routeType is index", async () => { + const controller = new apiBuilderController("index", "entries") + const mockData = [{ id: "1", name: "Entry 1" }] + mockPrismaEntryFindMany.mockResolvedValue(mockData) + + const result = await controller.fetchData() + + expect(result).toEqual(mockData) + expect(prisma.entry.findMany).toHaveBeenCalled() + }) + + it("should fetch entry by single-param Equals", async () => { + const controller = new apiBuilderController("single-param", "entries", "slug", "test-slug") + const mockData = [{ id: "1", slug: "test-slug" }] + mockPrismaEntryFindMany.mockResolvedValue(mockData) + + const result = await controller.fetchData("Equals") + + expect(result).toEqual(mockData) + expect(prisma.entry.findMany).toHaveBeenCalledWith({ + where: { slug: "test-slug" } + }) + }) + + it("should fetch entry by single-param StartsWith", async () => { + const controller = new apiBuilderController("single-param", "entries", "slug", "test") + const mockData = [{ id: "1", slug: "test-slug" }] + mockPrismaEntryFindMany.mockResolvedValue(mockData) + + const result = await controller.fetchData("StartsWith") + + expect(result).toEqual(mockData) + expect(prisma.entry.findMany).toHaveBeenCalledWith({ + where: { + slug: { + startsWith: "test" + } + } + }) + }) + + it("should fetch entry by single-param EndsWith", async () => { + const controller = new apiBuilderController("single-param", "entries", "slug", "slug") + const mockData = [{ id: "1", slug: "test-slug" }] + mockPrismaEntryFindMany.mockResolvedValue(mockData) + + const result = await controller.fetchData("EndsWith") + + expect(result).toEqual(mockData) + expect(prisma.entry.findMany).toHaveBeenCalledWith({ + where: { + slug: { + endsWith: "slug" + } + } + }) + }) + + it("should fetch entry by single-param Contains", async () => { + const controller = new apiBuilderController("single-param", "entries", "slug", "est-slu") + const mockData = [{ id: "1", slug: "test-slug" }] + mockPrismaEntryFindMany.mockResolvedValue(mockData) + + const result = await controller.fetchData("Contains") + + expect(result).toEqual(mockData) + expect(prisma.entry.findMany).toHaveBeenCalledWith({ + where: { + slug: { + contains: "est-slu" + } + } + }) + }) + + it("should fetch entry by _id", async () => { + const controller = new apiBuilderController("single-param", "entries", "_id", "mock-id") + const mockData = [{ id: "mock-id", name: "Entry 1" }] + mockPrismaEntryFindMany.mockResolvedValue(mockData) + + const result = await controller.fetchData() + + expect(result).toEqual(mockData) + expect(prisma.entry.findMany).toHaveBeenCalledWith({ + where: { id: "mock-id" } + }) + }) + + it("should fetch entry by multi-param namespace", async () => { + const controller = new apiBuilderController("multi-param", "entries", "namespace", ["a", "b", "c"]) + const mockData = [{ id: "1", namespace: "a.b.c" }] + mockPrismaEntryFindMany.mockResolvedValue(mockData) + + const result = await controller.fetchData("Equals") + + expect(result).toEqual(mockData) + expect(prisma.entry.findMany).toHaveBeenCalledWith({ + where: { namespace: "a.b.c" } + }) + }) + + it("should return error for unexpected route type", async () => { + const controller = new apiBuilderController("invalid", "entries") + const result = await controller.fetchData() + + expect(result).toEqual([{ message: "Error: Unexpected route type!" }]) + }) + + it("should return error when database operation fails", async () => { + const controller = new apiBuilderController("index", "entries") + mockPrismaEntryFindMany.mockRejectedValue(new Error("DB Error")) + + const result = await controller.fetchData() + + expect(result).toEqual([{ message: "Database operation failed." }]) + }) + }) +}) diff --git a/__tests__/controllers/api-key.controller.test.ts b/__tests__/controllers/api-key.controller.test.ts new file mode 100644 index 0000000..35d636c --- /dev/null +++ b/__tests__/controllers/api-key.controller.test.ts @@ -0,0 +1,116 @@ +import { apiKeyController } from "@/controllers/api-key.controller" +import { prisma } from "@/lib/prisma" + +/* eslint-disable @typescript-eslint/no-require-imports */ +jest.mock("@/lib/prisma", () => ({ + prisma: require("@/lib/__mocks__/prisma").prisma +})) + +describe("apiKeyController", () => { + const mockPrismaApiKeyFindMany = prisma.apiKey.findMany as jest.Mock + const mockPrismaApiKeyCreate = prisma.apiKey.create as jest.Mock + const mockPrismaApiKeyDeleteMany = prisma.apiKey.deleteMany as jest.Mock + + const mockApiKey = { + key: "test-key", + description: "test-description", + permission_group: "admin", + rate_limits: { + time_window: 60, + req_per_window: 100, + time_interval: 1000 + } + } + + beforeEach(() => { + jest.clearAllMocks() + }) + + describe("findKey", () => { + it("should find an API key by key", async () => { + const controller = new apiKeyController({ key: "test-key" }) + mockPrismaApiKeyFindMany.mockResolvedValue([mockApiKey]) + + const result = await controller.findKey() + + expect(result).toEqual([mockApiKey]) + expect(prisma.apiKey.findMany).toHaveBeenCalledWith({ + where: { key: "test-key" } + }) + }) + + it("should return empty array if key not found", async () => { + const controller = new apiKeyController({ key: "non-existent" }) + mockPrismaApiKeyFindMany.mockResolvedValue([]) + + const result = await controller.findKey() + + expect(result).toEqual([]) + }) + }) + + describe("create", () => { + it("should create a new API key", async () => { + const controller = new apiKeyController(mockApiKey) + const mockInsertedId = "mock-id" + mockPrismaApiKeyCreate.mockResolvedValue({ ...mockApiKey, id: mockInsertedId }) + + const result = await controller.create() + + expect(result).toEqual({ + status: "success", + message: "API Key has been generated.", + keyId: mockInsertedId + }) + expect(prisma.apiKey.create).toHaveBeenCalledWith({ + data: expect.objectContaining({ + key: "test-key", + description: "test-description" + }) + }) + }) + + it("should return failed status if creation fails", async () => { + const controller = new apiKeyController(mockApiKey) + mockPrismaApiKeyCreate.mockResolvedValue(null) + + const result = await controller.create() + + expect(result).toEqual({ + status: "failed", + message: "Failed to create the api key." + }) + }) + }) + + describe("delete", () => { + it("should delete an API key by id", async () => { + const controller = new apiKeyController({}) + const mockId = "mock-id" + mockPrismaApiKeyDeleteMany.mockResolvedValue({ count: 1 }) + + const result = await controller.delete(mockId) + + expect(result).toEqual({ + status: "success", + message: "API Key has been removed." + }) + expect(prisma.apiKey.deleteMany).toHaveBeenCalledWith({ + where: { id: mockId } + }) + }) + + it("should return failed status if deletion fails", async () => { + const controller = new apiKeyController({}) + const mockId = "mock-id" + mockPrismaApiKeyDeleteMany.mockResolvedValue({ count: 0 }) + + const result = await controller.delete(mockId) + + expect(result).toEqual({ + status: "failed", + message: "Failed to delete the api key." + }) + }) + }) +}) diff --git a/__tests__/controllers/entry-type.controller.test.ts b/__tests__/controllers/entry-type.controller.test.ts index 371a9d1..7868791 100644 --- a/__tests__/controllers/entry-type.controller.test.ts +++ b/__tests__/controllers/entry-type.controller.test.ts @@ -1,64 +1,26 @@ import "@testing-library/jest-dom" -import { EntryType } from "../../interfaces" -import { EntryTypeController } from "../../controllers/entry-type.controller" -import { Collection, InsertOneResult, MongoClient, ObjectId, UpdateResult } from "mongodb" -import { connectDB } from "../../lib/mongodb" +import { EntryType, PublishStatus } from "@/interfaces" +import { EntryTypeController } from "@/controllers/entry-type.controller" +import { prisma } from "@/lib/prisma" +import { ObjectId } from "mongodb" -// Mock the mongodb module -jest.mock("../../lib/mongodb", () => ({ - connectDB: jest.fn() -})) +// Mock the prisma module +jest.mock("@/lib/prisma") describe("Check if the entry type controller handles actions properly", () => { let entryTypeController: EntryTypeController let entryTypeData: EntryType let entryTypeId: string - // Mock MongoDB client, collection and operations - const mockInsertOne = jest.fn() - const mockUpdateOne = jest.fn() - const mockDeleteOne = jest.fn() - const mockCollection = jest.fn(() => ({ - insertOne: mockInsertOne, - updateOne: mockUpdateOne, - deleteOne: mockDeleteOne - })) - const mockDb = jest.fn(() => ({ - collection: mockCollection - })) - const mockClient = { - db: mockDb, - close: jest.fn() - } - - beforeAll(() => { - // Setup mock return values for MongoDB operations - const mockObjectId = new ObjectId() - entryTypeId = mockObjectId.toString() - - // Setup connectDB mock - ;(connectDB as jest.Mock).mockResolvedValue(mockClient) - - // Setup insertOne mock - mockInsertOne.mockResolvedValue({ - acknowledged: true, - insertedId: mockObjectId - } as InsertOneResult) - - // Setup updateOne mock - mockUpdateOne.mockResolvedValue({ - acknowledged: true, - matchedCount: 1, - modifiedCount: 1, - upsertedCount: 0, - upsertedId: null - } as UpdateResult) - - // Setup deleteOne mock - mockDeleteOne.mockResolvedValue({ - acknowledged: true, - deletedCount: 1 - }) + const mockPrismaEntryTypeCreate = prisma.entryType.create as jest.Mock + const mockPrismaEntryTypeFindUnique = prisma.entryType.findUnique as jest.Mock + const mockPrismaEntryTypeUpdate = prisma.entryType.update as jest.Mock + const mockPrismaEntryTypeDeleteMany = prisma.entryType.deleteMany as jest.Mock + const mockPrismaPermissionGroupFindFirst = prisma.permissionGroup.findFirst as jest.Mock + const mockPrismaPermissionGroupUpdate = prisma.permissionGroup.update as jest.Mock + + beforeEach(() => { + entryTypeId = new ObjectId().toString() }) afterEach(() => { @@ -73,85 +35,250 @@ describe("Check if the entry type controller handles actions properly", () => { entryTypeData = { name: "mock type", namespace: "mock-type", - fields: [] + slug: "mock-type", + createdBy: "mock-user", + created_at: new Date(), + updated_at: new Date(), + status: PublishStatus.Draft, + fieldsets: [] } - + + // Setup mock return values for Prisma operations + mockPrismaEntryTypeCreate.mockResolvedValue({ + id: entryTypeId, + ...entryTypeData, + created_at: entryTypeData.created_at?.toISOString(), + updated_at: entryTypeData.updated_at?.toISOString() + }) + + mockPrismaPermissionGroupFindFirst.mockResolvedValue({ + id: "perm-group-id", + slug: "mock-user", + privileges: [] + }) + + mockPrismaPermissionGroupUpdate.mockResolvedValue({ + id: "perm-group-id", + slug: "mock-user", + privileges: [ + { + "mock-type": { + permissions: ["read", "update", "delete", "create"] + } + } + ] + }) + entryTypeController = new EntryTypeController(entryTypeData, true) - const createEntryType = await entryTypeController.create() - - // Assert connectDB was called - expect(connectDB).toHaveBeenCalledWith(true) - - // Assert db and collection methods were called - expect(mockDb).toHaveBeenCalledWith(process.env.DB_NAME) - expect(mockCollection).toHaveBeenCalledWith("entry_types") - - // Assert insertOne was called with the right data - expect(mockInsertOne).toHaveBeenCalledWith(entryTypeData) - + const createEntryType = (await entryTypeController.create()) as { + result: { status: string; message: string } + entryTypeId: string + } + + // Assert prisma.entryType.create was called with correct data + expect(mockPrismaEntryTypeCreate).toHaveBeenCalledWith({ + data: { + createdBy: entryTypeData.createdBy, + created_at: entryTypeData.created_at?.toISOString(), + fieldsets: [], + name: entryTypeData.name, + namespace: entryTypeData.namespace, + slug: entryTypeData.slug, + status: entryTypeData.status, + updated_at: entryTypeData.updated_at?.toISOString() + } + }) + + // Assert permission check and updates + expect(mockPrismaPermissionGroupFindFirst).toHaveBeenCalledWith({ + where: { slug: entryTypeData.createdBy } + }) + + expect(mockPrismaPermissionGroupUpdate).toHaveBeenCalledWith({ + where: { id: "perm-group-id" }, + data: { + privileges: [ + { + "mock-type": { + permissions: ["read", "update", "delete", "create"] + } + } + ] + } + }) + // Assert the returned result expect(createEntryType.result).toEqual({ status: "success", message: "Entry Type has been created." }) - expect(createEntryType.entryTypeId).toBeDefined() - - // Assert client.close was called - expect(mockClient.close).toHaveBeenCalled() + expect(createEntryType.entryTypeId).toBe(entryTypeId) }) it("Check if entry type updated properly", async () => { entryTypeData = { name: "mock type1", namespace: "mock-type1", - fields: [] + slug: "mock-type1", + createdBy: "mock-user", + created_at: new Date(), + updated_at: new Date(), + status: PublishStatus.Draft, + fieldsets: [] } - + + // Setup mock return values for Prisma operations + mockPrismaEntryTypeFindUnique.mockResolvedValue({ + id: entryTypeId, + name: "mock type", + namespace: "mock-type", + slug: "mock-type", + createdBy: "mock-user", + created_at: entryTypeData.created_at?.toISOString(), + updated_at: entryTypeData.updated_at?.toISOString() + }) + + mockPrismaEntryTypeUpdate.mockResolvedValue({ + id: entryTypeId, + ...entryTypeData, + created_at: entryTypeData.created_at?.toISOString(), + updated_at: entryTypeData.updated_at?.toISOString() + }) + + mockPrismaPermissionGroupFindFirst.mockResolvedValue({ + id: "perm-group-id", + slug: "mock-user", + privileges: [] + }) + + mockPrismaPermissionGroupUpdate.mockResolvedValue({ + id: "perm-group-id", + slug: "mock-user", + privileges: [ + { + "mock-type1": { + permissions: ["read", "update", "delete", "create"] + } + } + ] + }) + entryTypeController = new EntryTypeController(entryTypeData, true) const updateEntryType = await entryTypeController.update(entryTypeId) - // Assert connectDB was called - expect(connectDB).toHaveBeenCalledWith(true) - - // Assert db and collection methods were called - expect(mockDb).toHaveBeenCalledWith(process.env.DB_NAME) - expect(mockCollection).toHaveBeenCalledWith("entry_types") - - // Assert updateOne was called with the right data - expect(mockUpdateOne).toHaveBeenCalledWith( - { _id: expect.any(ObjectId) }, - { $set: entryTypeData }, - { upsert: false } - ) - + // Assert findUnique was called first to check prev state + expect(mockPrismaEntryTypeFindUnique).toHaveBeenCalledWith({ + where: { id: entryTypeId } + }) + + // Assert update was called with correct parameters + expect(mockPrismaEntryTypeUpdate).toHaveBeenCalledWith({ + where: { id: entryTypeId }, + data: { + createdBy: entryTypeData.createdBy, + created_at: entryTypeData.created_at?.toISOString(), + fieldsets: [], + name: entryTypeData.name, + namespace: entryTypeData.namespace, + slug: entryTypeData.slug, + status: entryTypeData.status, + updated_at: entryTypeData.updated_at?.toISOString() + } + }) + + // Assert permission group search & update since namespace changed from mock-type to mock-type1 + expect(mockPrismaPermissionGroupFindFirst).toHaveBeenCalledWith({ + where: { slug: "mock-user" } + }) + + expect(mockPrismaPermissionGroupUpdate).toHaveBeenCalledWith({ + where: { id: "perm-group-id" }, + data: { + privileges: [ + { + "mock-type1": { + permissions: ["read", "update", "delete", "create"] + } + } + ] + } + }) + // Assert the returned result expect(updateEntryType).toEqual({ status: "success", message: "Entry Type has been updated." }) - - // Assert client.close was called - expect(mockClient.close).toHaveBeenCalled() + }) + + it("Check if entry type update detects no-changes and returns appropriately", async () => { + entryTypeData = { + name: "mock type", + namespace: "mock-type", + slug: "mock-type", + createdBy: "mock-user", + created_at: new Date(), + updated_at: new Date(), + status: PublishStatus.Draft, + fieldsets: [] + } + + mockPrismaEntryTypeFindUnique.mockResolvedValue({ + id: entryTypeId, + name: "mock type", + namespace: "mock-type", + slug: "mock-type", + createdBy: "mock-user", + status: "Draft", + fieldsets: [], + created_at: entryTypeData.created_at?.toISOString(), + updated_at: entryTypeData.updated_at?.toISOString() + }) + + mockPrismaEntryTypeUpdate.mockResolvedValue({ + id: entryTypeId, + ...entryTypeData, + created_at: entryTypeData.created_at?.toISOString(), + updated_at: entryTypeData.updated_at?.toISOString() + }) + + entryTypeController = new EntryTypeController(entryTypeData, true) + const updateEntryType = await entryTypeController.update(entryTypeId) + + // Assert findUnique was called first + expect(mockPrismaEntryTypeFindUnique).toHaveBeenCalledWith({ + where: { id: entryTypeId } + }) + + // Assert update was called with same data + expect(mockPrismaEntryTypeUpdate).toHaveBeenCalled() + + // Assert that we did not attempt to modify privileges since namespace did not change + expect(mockPrismaPermissionGroupFindFirst).not.toHaveBeenCalled() + + // Assert it returned the "You didn't make any change." response + expect(updateEntryType).toEqual({ status: "failed", message: "You didn't make any change." }) }) it("Check if entry type deleted properly", async () => { entryTypeData = { name: "mock type1", namespace: "mock-type1", - fields: [] + slug: "mock-type1", + createdBy: "mock-user", + created_at: new Date(), + updated_at: new Date(), + status: PublishStatus.Draft, + fieldsets: [] } - + + mockPrismaEntryTypeDeleteMany.mockResolvedValue({ + count: 1 + }) + entryTypeController = new EntryTypeController(entryTypeData, true) const deleteEntryType = await entryTypeController.delete(entryTypeId) - // Assert connectDB was called - expect(connectDB).toHaveBeenCalledWith(true) - - // Assert db and collection methods were called - expect(mockDb).toHaveBeenCalledWith(process.env.DB_NAME) - expect(mockCollection).toHaveBeenCalledWith("entry_types") - - // Assert deleteOne was called with the right data - expect(mockDeleteOne).toHaveBeenCalledWith({ _id: expect.any(ObjectId) }) - + // Assert deleteMany was called with namespace as id + expect(mockPrismaEntryTypeDeleteMany).toHaveBeenCalledWith({ + where: { namespace: entryTypeId } + }) + // Assert the returned result expect(deleteEntryType).toEqual({ status: "success", message: "Entry Type has been deleted." }) - - // Assert client.close was called - expect(mockClient.close).toHaveBeenCalled() }) -}) \ No newline at end of file +}) diff --git a/__tests__/controllers/entry.controller.test.ts b/__tests__/controllers/entry.controller.test.ts index 2abba77..ddeb18e 100644 --- a/__tests__/controllers/entry.controller.test.ts +++ b/__tests__/controllers/entry.controller.test.ts @@ -1,64 +1,24 @@ import "@testing-library/jest-dom" -import { Entry } from "../../interfaces" -import { EntryController } from "../../controllers/entry.controller" -import { DeleteResult, InsertOneResult, ObjectId, UpdateResult } from "mongodb" -import { connectDB } from "../../lib/mongodb" +import { Entry, PublishStatus } from "@/interfaces" +import { EntryController } from "@/controllers/entry.controller" +import { prisma } from "@/lib/prisma" +import { ObjectId } from "mongodb" -// Mock the mongodb module -jest.mock("../../lib/mongodb", () => ({ - connectDB: jest.fn() -})) +// Mock the prisma module +jest.mock("@/lib/prisma") describe("Check if the entry controller handles actions properly", () => { let entryController: EntryController let entryData: Entry let entryId: string - - // Mock MongoDB client, collection and operations - const mockInsertOne = jest.fn() - const mockUpdateOne = jest.fn() - const mockDeleteOne = jest.fn() - const mockCollection = jest.fn(() => ({ - insertOne: mockInsertOne, - updateOne: mockUpdateOne, - deleteOne: mockDeleteOne - })) - const mockDb = jest.fn(() => ({ - collection: mockCollection - })) - const mockClient = { - db: mockDb, - close: jest.fn() - } - - beforeAll(() => { - // Setup mock return values for MongoDB operations - const mockObjectId = new ObjectId() - entryId = mockObjectId.toString() - - // Setup connectDB mock - ;(connectDB as jest.Mock).mockResolvedValue(mockClient) - - // Setup insertOne mock - mockInsertOne.mockResolvedValue({ - acknowledged: true, - insertedId: mockObjectId - } as InsertOneResult) - - // Setup updateOne mock - mockUpdateOne.mockResolvedValue({ - acknowledged: true, - matchedCount: 1, - modifiedCount: 1, - upsertedCount: 0, - upsertedId: null - } as UpdateResult) - - // Setup deleteOne mock - mockDeleteOne.mockResolvedValue({ - acknowledged: true, - deletedCount: 1 - } as DeleteResult) + + const mockPrismaEntryCreate = prisma.entry.create as jest.Mock + const mockPrismaEntryFindUnique = prisma.entry.findUnique as jest.Mock + const mockPrismaEntryUpdate = prisma.entry.update as jest.Mock + const mockPrismaEntryDeleteMany = prisma.entry.deleteMany as jest.Mock + + beforeEach(() => { + entryId = new ObjectId().toString() }) afterEach(() => { @@ -71,86 +31,160 @@ describe("Check if the entry controller handles actions properly", () => { it("Check if entry created properly", async () => { entryData = { + _id: entryId, name: "mock entry", namespace: "mock-type", - slug: "mock-entry" + slug: "mock-entry", + status: PublishStatus.Draft, + data: {}, + created_at: new Date(), + updated_at: new Date() } - + + mockPrismaEntryCreate.mockResolvedValue({ + id: entryId, + ...entryData, + created_at: entryData.created_at?.toISOString(), + updated_at: entryData.updated_at?.toISOString() + }) + entryController = new EntryController(entryData, true) - const createEntry = await entryController.create() - - // Assert connectDB was called - expect(connectDB).toHaveBeenCalledWith(true) - - // Assert db and collection methods were called - expect(mockDb).toHaveBeenCalledWith(process.env.DB_NAME) - expect(mockCollection).toHaveBeenCalledWith("entries") - - // Assert insertOne was called with the right data - expect(mockInsertOne).toHaveBeenCalledWith(entryData) - - // Assert the returned result + const createEntry = (await entryController.create()) as { + result: { status: string; message: string } + entryId: string + } + + expect(mockPrismaEntryCreate).toHaveBeenCalledWith({ + data: { + id: entryId, + data: entryData.data, + name: entryData.name, + namespace: entryData.namespace, + slug: entryData.slug, + status: entryData.status, + updated_at: entryData.updated_at ? new Date(entryData.updated_at).toISOString() : expect.any(String) + } + }) + expect(createEntry.result).toEqual({ status: "success", message: "Entry has been created." }) - - // Assert client.close was called - expect(mockClient.close).toHaveBeenCalled() + expect(createEntry.entryId).toBe(entryId) }) it("Check if entry updated properly", async () => { entryData = { + _id: entryId, name: "mock entry1", namespace: "mock-type", - slug: "mock-entry1" + slug: "mock-entry1", + status: PublishStatus.Draft, + data: {}, + created_at: new Date(), + updated_at: new Date() } - + + mockPrismaEntryFindUnique.mockResolvedValue({ + id: entryId, + name: "mock entry", + namespace: "mock-type", + slug: "mock-entry", + status: PublishStatus.Draft, + data: {}, + created_at: entryData.created_at?.toISOString(), + updated_at: entryData.updated_at?.toISOString() + }) + + mockPrismaEntryUpdate.mockResolvedValue({ + id: entryId, + ...entryData, + created_at: entryData.created_at?.toISOString(), + updated_at: entryData.updated_at?.toISOString() + }) + entryController = new EntryController(entryData, true) const updateEntry = await entryController.update(entryId) - // Assert connectDB was called - expect(connectDB).toHaveBeenCalledWith(true) - - // Assert db and collection methods were called - expect(mockDb).toHaveBeenCalledWith(process.env.DB_NAME) - expect(mockCollection).toHaveBeenCalledWith("entries") - - // Assert updateOne was called with the right data - expect(mockUpdateOne).toHaveBeenCalledWith( - { _id: expect.any(ObjectId) }, - { $set: entryData }, - { upsert: false } - ) - - // Assert the returned result + expect(mockPrismaEntryFindUnique).toHaveBeenCalledWith({ + where: { id: entryId } + }) + + expect(mockPrismaEntryUpdate).toHaveBeenCalledWith({ + where: { id: entryId }, + data: { + data: entryData.data, + name: entryData.name, + namespace: entryData.namespace, + slug: entryData.slug, + status: entryData.status, + updated_at: entryData.updated_at ? new Date(entryData.updated_at).toISOString() : expect.any(String) + } + }) + expect(updateEntry).toEqual({ status: "success", message: "Entry has been updated." }) - - // Assert client.close was called - expect(mockClient.close).toHaveBeenCalled() + }) + + it("Check if entry update detects no-changes and returns appropriately", async () => { + entryData = { + _id: entryId, + name: "mock entry", + namespace: "mock-type", + slug: "mock-entry", + status: PublishStatus.Draft, + data: {}, + created_at: new Date(), + updated_at: new Date() + } + + mockPrismaEntryFindUnique.mockResolvedValue({ + id: entryId, + name: "mock entry", + namespace: "mock-type", + slug: "mock-entry", + status: PublishStatus.Draft, + data: {}, + created_at: entryData.created_at?.toISOString(), + updated_at: entryData.updated_at?.toISOString() + }) + + mockPrismaEntryUpdate.mockResolvedValue({ + id: entryId, + ...entryData, + created_at: entryData.created_at?.toISOString(), + updated_at: entryData.updated_at?.toISOString() + }) + + entryController = new EntryController(entryData, true) + const updateEntry = await entryController.update(entryId) + + expect(mockPrismaEntryFindUnique).toHaveBeenCalledWith({ + where: { id: entryId } + }) + + expect(updateEntry).toEqual({ status: "failed", message: "You didn't make any change." }) }) it("Check if entry deleted properly", async () => { entryData = { + _id: entryId, name: "mock entry1", namespace: "mock-type", - slug: "mock-entry1" + slug: "mock-entry1", + status: PublishStatus.Draft, + data: {}, + created_at: new Date(), + updated_at: new Date() } - + + mockPrismaEntryDeleteMany.mockResolvedValue({ + count: 1 + }) + entryController = new EntryController(entryData, true) const deleteEntry = await entryController.delete(entryId) - // Assert connectDB was called - expect(connectDB).toHaveBeenCalledWith(true) - - // Assert db and collection methods were called - expect(mockDb).toHaveBeenCalledWith(process.env.DB_NAME) - expect(mockCollection).toHaveBeenCalledWith("entries") - - // Assert deleteOne was called with the right data - expect(mockDeleteOne).toHaveBeenCalledWith({ _id: expect.any(ObjectId) }) - - // Assert the returned result + expect(mockPrismaEntryDeleteMany).toHaveBeenCalledWith({ + where: { id: entryId } + }) + expect(deleteEntry).toEqual({ status: "success", message: "Entry has been deleted." }) - - // Assert client.close was called - expect(mockClient.close).toHaveBeenCalled() }) -}) \ No newline at end of file +}) diff --git a/__tests__/controllers/permission-group.controller.test.ts b/__tests__/controllers/permission-group.controller.test.ts index 7b23696..9a3bcb2 100644 --- a/__tests__/controllers/permission-group.controller.test.ts +++ b/__tests__/controllers/permission-group.controller.test.ts @@ -1,64 +1,23 @@ import "@testing-library/jest-dom" -import { PermissionGroup } from "../../interfaces" -import { PermissionGroupController } from "../../controllers/permission-group.controller" -import { InsertOneResult, ObjectId, UpdateResult } from "mongodb" -import { connectDB } from "../../lib/mongodb" - -// Mock the mongodb module -jest.mock("../../lib/mongodb", () => ({ - connectDB: jest.fn() -})) +import { PermissionGroup } from "@/interfaces" +import { PermissionGroupController } from "@/controllers/permission-group.controller" +import { prisma } from "@/lib/prisma" +import { ObjectId } from "mongodb" +// Mock the prisma module +jest.mock("@/lib/prisma") describe("Check if the permission group controller handles actions properly", () => { let permissionGroupController: PermissionGroupController let permGroupData: PermissionGroup let permGroupId: string - // Mock MongoDB client, collection and operations - const mockInsertOne = jest.fn() - const mockUpdateOne = jest.fn() - const mockDeleteOne = jest.fn() - const mockCollection = jest.fn(() => ({ - insertOne: mockInsertOne, - updateOne: mockUpdateOne, - deleteOne: mockDeleteOne - })) - const mockDb = jest.fn(() => ({ - collection: mockCollection - })) - const mockClient = { - db: mockDb, - close: jest.fn() - } - - beforeAll(() => { - // Setup mock return values for MongoDB operations - const mockObjectId = new ObjectId() - permGroupId = mockObjectId.toString() - - // Setup connectDB mock - ;(connectDB as jest.Mock).mockResolvedValue(mockClient) - - // Setup insertOne mock - mockInsertOne.mockResolvedValue({ - acknowledged: true, - insertedId: mockObjectId - } as InsertOneResult) - - // Setup updateOne mock - mockUpdateOne.mockResolvedValue({ - acknowledged: true, - matchedCount: 1, - modifiedCount: 1, - upsertedCount: 0, - upsertedId: null - } as UpdateResult) - - // Setup deleteOne mock - mockDeleteOne.mockResolvedValue({ - acknowledged: true, - deletedCount: 1 - }) + const mockPrismaPermissionGroupCreate = prisma.permissionGroup.create as jest.Mock + const mockPrismaPermissionGroupFindUnique = prisma.permissionGroup.findUnique as jest.Mock + const mockPrismaPermissionGroupUpdate = prisma.permissionGroup.update as jest.Mock + const mockPrismaPermissionGroupDeleteMany = prisma.permissionGroup.deleteMany as jest.Mock + + beforeEach(() => { + permGroupId = new ObjectId().toString() }) afterEach(() => { @@ -75,26 +34,31 @@ describe("Check if the permission group controller handles actions properly", () slug: "editors", privileges: [] } - + + // Setup mock return values for Prisma operations + mockPrismaPermissionGroupCreate.mockResolvedValue({ + id: permGroupId, + ...permGroupData + }) + permissionGroupController = new PermissionGroupController(permGroupData, true) - const createPermGroup = await permissionGroupController.create() - - // Assert connectDB was called - expect(connectDB).toHaveBeenCalledWith(true) - - // Assert db and collection methods were called - expect(mockDb).toHaveBeenCalledWith(process.env.DB_NAME) - expect(mockCollection).toHaveBeenCalledWith("permission_groups") - - // Assert insertOne was called with the right data - expect(mockInsertOne).toHaveBeenCalledWith(permGroupData) - + const createPermGroup = (await permissionGroupController.create()) as { + result: { status: string; message: string } + permGroupId: string + } + + // Assert prisma.permissionGroup.create was called with correct data + expect(mockPrismaPermissionGroupCreate).toHaveBeenCalledWith({ + data: { + name: permGroupData.name, + slug: permGroupData.slug, + privileges: [] + } + }) + // Assert the returned result expect(createPermGroup.result).toEqual({ status: "success", message: "Permission group has been created." }) - expect(createPermGroup.permGroupId).toBeDefined() - - // Assert client.close was called - expect(mockClient.close).toHaveBeenCalled() + expect(createPermGroup.permGroupId).toBe(permGroupId) }) it("Check if permission group updated properly", async () => { @@ -103,55 +67,87 @@ describe("Check if the permission group controller handles actions properly", () slug: "editors1", privileges: [] } - + + // Setup mock return values for Prisma operations + mockPrismaPermissionGroupFindUnique.mockResolvedValue({ + id: permGroupId, + name: "editors", + slug: "editors", + privileges: [] + }) + + mockPrismaPermissionGroupUpdate.mockResolvedValue({ + id: permGroupId, + ...permGroupData + }) + permissionGroupController = new PermissionGroupController(permGroupData, true) const updatePermGroup = await permissionGroupController.update(permGroupId) - // Assert connectDB was called - expect(connectDB).toHaveBeenCalledWith(true) - - // Assert db and collection methods were called - expect(mockDb).toHaveBeenCalledWith(process.env.DB_NAME) - expect(mockCollection).toHaveBeenCalledWith("permission_groups") - - // Assert updateOne was called with the right data - expect(mockUpdateOne).toHaveBeenCalledWith( - { _id: expect.any(ObjectId) }, - { $set: permGroupData }, - { upsert: false } - ) - + // Assert findUnique was called first to check prev state + expect(mockPrismaPermissionGroupFindUnique).toHaveBeenCalledWith({ + where: { id: permGroupId } + }) + + // Assert update was called with correct parameters + expect(mockPrismaPermissionGroupUpdate).toHaveBeenCalledWith({ + where: { id: permGroupId }, + data: { + name: permGroupData.name, + slug: permGroupData.slug, + privileges: [] + } + }) + // Assert the returned result expect(updatePermGroup).toEqual({ status: "success", message: "Permission group has been updated." }) - - // Assert client.close was called - expect(mockClient.close).toHaveBeenCalled() }) - it("Check if permission group deleted properly", async () => { + it("Check if permission group update detects no-changes and returns appropriately", async () => { permGroupData = { - name: "editors1", - slug: "editors1", + name: "editors", + slug: "editors", privileges: [] } - + + mockPrismaPermissionGroupFindUnique.mockResolvedValue({ + id: permGroupId, + name: "editors", + slug: "editors", + privileges: [] + }) + + mockPrismaPermissionGroupUpdate.mockResolvedValue({ + id: permGroupId, + ...permGroupData + }) + + permissionGroupController = new PermissionGroupController(permGroupData, true) + const updatePermGroup = await permissionGroupController.update(permGroupId) + + // Assert findUnique was called first + expect(mockPrismaPermissionGroupFindUnique).toHaveBeenCalledWith({ + where: { id: permGroupId } + }) + + // Assert it returned the "You didn't make any change." response + expect(updatePermGroup).toEqual({ status: "failed", message: "You didn't make any change." }) + }) + + it("Check if permission group deleted properly", async () => { + mockPrismaPermissionGroupDeleteMany.mockResolvedValue({ + count: 1 + }) + permissionGroupController = new PermissionGroupController(permGroupData, true) const deletePermGroup = await permissionGroupController.delete(permGroupId) - // Assert connectDB was called - expect(connectDB).toHaveBeenCalledWith(true) - - // Assert db and collection methods were called - expect(mockDb).toHaveBeenCalledWith(process.env.DB_NAME) - expect(mockCollection).toHaveBeenCalledWith("permission_groups") - - // Assert deleteOne was called with the right data - expect(mockDeleteOne).toHaveBeenCalledWith({ _id: expect.any(ObjectId) }) - + // Assert deleteMany was called with id + expect(mockPrismaPermissionGroupDeleteMany).toHaveBeenCalledWith({ + where: { id: permGroupId } + }) + // Assert the returned result expect(deletePermGroup).toEqual({ status: "success", message: "Permission group has been deleted." }) - - // Assert client.close was called - expect(mockClient.close).toHaveBeenCalled() }) -}) \ No newline at end of file +}) diff --git a/__tests__/controllers/user.controller.test.ts b/__tests__/controllers/user.controller.test.ts index 627594e..3274436 100644 --- a/__tests__/controllers/user.controller.test.ts +++ b/__tests__/controllers/user.controller.test.ts @@ -1,14 +1,13 @@ import "@testing-library/jest-dom" -import { UserController } from "../../controllers/user.controller" -import { User } from "../../interfaces" -import { Collection, InsertOneResult, MongoClient, ObjectId, UpdateResult, DeleteResult } from "mongodb" -import { connectDB } from "../../lib/mongodb" +import { UserController } from "@/controllers/user.controller" +import { UserStatus } from "@/interfaces" +import { User } from "@/interfaces/user" +import { prisma } from "@/lib/prisma" +import { ObjectId } from "mongodb" import bcrypt from "bcryptjs" -// Mock the mongodb module -jest.mock("../../lib/mongodb", () => ({ - connectDB: jest.fn() -})) +// Mock the prisma module +jest.mock("@/lib/prisma") // Mock bcryptjs jest.mock("bcryptjs", () => ({ @@ -20,51 +19,13 @@ describe("Check if the user controller handles actions properly", () => { let userData: User let userId: string - // Mock MongoDB client, collection and operations - const mockInsertOne = jest.fn() - const mockUpdateOne = jest.fn() - const mockDeleteOne = jest.fn() - const mockCollection = jest.fn(() => ({ - insertOne: mockInsertOne, - updateOne: mockUpdateOne, - deleteOne: mockDeleteOne - })) - const mockDb = jest.fn(() => ({ - collection: mockCollection - })) - const mockClient = { - db: mockDb, - close: jest.fn() - } - - beforeAll(() => { - // Setup mock return values for MongoDB operations - const mockObjectId = new ObjectId() - userId = mockObjectId - - // Setup connectDB mock - ;(connectDB as jest.Mock).mockResolvedValue(mockClient) - - // Setup insertOne mock - mockInsertOne.mockResolvedValue({ - acknowledged: true, - insertedId: mockObjectId - } as InsertOneResult) - - // Setup updateOne mock - mockUpdateOne.mockResolvedValue({ - acknowledged: true, - matchedCount: 1, - modifiedCount: 1, - upsertedCount: 0, - upsertedId: null - } as UpdateResult) - - // Setup deleteOne mock - mockDeleteOne.mockResolvedValue({ - acknowledged: true, - deletedCount: 1 - } as DeleteResult) + const mockPrismaUserCreate = prisma.user.create as jest.Mock + const mockPrismaUserFindUnique = prisma.user.findUnique as jest.Mock + const mockPrismaUserUpdate = prisma.user.update as jest.Mock + const mockPrismaUserDeleteMany = prisma.user.deleteMany as jest.Mock + + beforeEach(() => { + userId = new ObjectId().toString() }) afterEach(() => { @@ -76,38 +37,62 @@ describe("Check if the user controller handles actions properly", () => { }) it("Check if user created properly", async () => { + const createDate = new Date().toISOString(), userData = { username: "mock_user", password: "super.strong.password.1111", email: "mock_user@localhost.test", - permission_group: "member" + permission_group: "member", + fullname: "Mock User", + status: UserStatus.Active, + profile_img: "", + oauth_id: "oauth123", + oauth_provider: "github", + created_at: createDate, + created_by: "system", + updated_at: createDate, + updated_by: "system", + email_verified: false } - userController = new UserController(userData, true) - const createUser = await userController.create() + mockPrismaUserCreate.mockResolvedValue({ + id: userId, + ...userData, + password: "hashed_password" + }) - // Assert connectDB was called - expect(connectDB).toHaveBeenCalledWith(true) + userController = new UserController(userData, true) + const createUser = (await userController.create()) as { + result: { status: string; message: string } + userId: string + } // Assert bcrypt.hashSync was called with the password expect(bcrypt.hashSync).toHaveBeenCalledWith(userData.password, 8) - // Assert db and collection methods were called - expect(mockDb).toHaveBeenCalledWith(process.env.DB_NAME) - expect(mockCollection).toHaveBeenCalledWith("users") - - // Assert insertOne was called with the right data - expect(mockInsertOne).toHaveBeenCalledWith({ - ...userData, - password: "hashed_password" + // Assert prisma.user.create was called with the right data + expect(mockPrismaUserCreate).toHaveBeenCalledWith({ + data: { + email: userData.email, + fullname: userData.fullname, + password: "hashed_password", + permission_group: userData.permission_group, + profile_img: "", + status: userData.status, + username: userData.username, + email_verified: false, + oauth_id: "oauth123", + oauth_provider: "github", + created_at: createDate, + created_by: "system", + updated_at: createDate, + updated_by: "system" + } }) // Assert the returned result expect(createUser.result).toEqual({ status: "success", message: "User has been created." }) expect(createUser.userId).toBe(userId) - - // Assert client.close was called - expect(mockClient.close).toHaveBeenCalled() }) it("Check if user updated properly", async () => { @@ -115,64 +100,104 @@ describe("Check if the user controller handles actions properly", () => { username: "mock_user2", password: "super.strong.password.1111", email: "mock_user2@localhost.test", - permission_group: "member" + permission_group: "viewer", + fullname: "Mock User 2", + status: UserStatus.Active, + profile_img: "", + created_at: new Date().toISOString(), + created_by: "system", + updated_at: new Date().toISOString(), + updated_by: "system", + email_verified: false } + mockPrismaUserFindUnique.mockResolvedValue({ + id: userId, + username: "mock_user", + email: "mock_user@localhost.test", + permission_group: "viewer", + fullname: "Mock User", + status: UserStatus.Active, + profile_img: "", + password: "hashed_password" + }) + + mockPrismaUserUpdate.mockResolvedValue({ + id: userId, + ...userData + }) + userController = new UserController(userData, true) const updateUser = await userController.update(userId) - // Assert connectDB was called - expect(connectDB).toHaveBeenCalledWith(true) - - // Assert db and collection methods were called - expect(mockDb).toHaveBeenCalledWith(process.env.DB_NAME) - expect(mockCollection).toHaveBeenCalledWith("users") - - // Assert updateOne was called with the right data and ID - expect(mockUpdateOne).toHaveBeenCalledWith( - { _id: expect.any(ObjectId) }, - { - $set: expect.objectContaining({ - username: userData.username, - email: userData.email, - permission_group: userData.permission_group - }) - }, - { upsert: false } - ) + // Assert findUnique was called + expect(mockPrismaUserFindUnique).toHaveBeenCalledWith({ + where: { id: userId } + }) + + // Assert update was called with the right data and ID + expect(mockPrismaUserUpdate).toHaveBeenCalledWith({ + where: { id: userId }, + data: expect.objectContaining({ + username: userData.username, + email: userData.email, + permission_group: userData.permission_group, + fullname: userData.fullname, + status: userData.status, + profile_img: "" + }) + }) // Assert the returned result expect(updateUser).toEqual({ status: "success", message: "User has been updated." }) - - // Assert client.close was called - expect(mockClient.close).toHaveBeenCalled() }) - it("Check if user deleted properly", async () => { + it("Check if user update detects no-changes and returns appropriately", async () => { userData = { - username: "mock_user2", - password: "super.strong.password.1111", - email: "mock_user2@localhost.test", - permission_group: "member" + username: "mock_user", + password: "hashed_password", + email: "mock_user@localhost.test", + permission_group: "viewer", + fullname: "Mock User", + status: UserStatus.Active, + profile_img: "", + created_at: new Date().toISOString(), + created_by: "system", + updated_at: new Date().toISOString(), + updated_by: "system", + email_verified: false } + mockPrismaUserFindUnique.mockResolvedValue({ + id: userId, + ...userData + }) + + mockPrismaUserUpdate.mockResolvedValue({ + id: userId, + ...userData + }) + userController = new UserController(userData, true) - const deleteUser = await userController.delete(userId) + const updateUser = await userController.update(userId) + + expect(updateUser).toEqual({ status: "failed", message: "You didn't make any change." }) + }) - // Assert connectDB was called - expect(connectDB).toHaveBeenCalledWith(true) + it("Check if user deleted properly", async () => { + mockPrismaUserDeleteMany.mockResolvedValue({ + count: 1 + }) - // Assert db and collection methods were called - expect(mockDb).toHaveBeenCalledWith(process.env.DB_NAME) - expect(mockCollection).toHaveBeenCalledWith("users") + userController = new UserController({} as any, true) + const deleteUser = await userController.delete(userId) - // Assert deleteOne was called with the right ID - expect(mockDeleteOne).toHaveBeenCalledWith({ _id: expect.any(ObjectId) }) + // Assert deleteMany was called with the right ID + expect(mockPrismaUserDeleteMany).toHaveBeenCalledWith({ + where: { id: userId } + }) // Assert the returned result expect(deleteUser).toEqual({ status: "success", message: "User has been deleted." }) - - // Assert client.close was called - expect(mockClient.close).toHaveBeenCalled() }) -}) \ No newline at end of file +}) diff --git a/__tests__/lib/actions/auth/email-verification.test.ts b/__tests__/lib/actions/auth/email-verification.test.ts new file mode 100644 index 0000000..4dae7a2 --- /dev/null +++ b/__tests__/lib/actions/auth/email-verification.test.ts @@ -0,0 +1,149 @@ +import { EmailVerificationAction, verifyCode } from "@/lib/actions/auth/email-verification" +import { prisma } from "@/lib/prisma" +import { sendVerificationEmail } from "@/lib/verification-email" +import { getSettingsValue } from "@/lib/actions/studio/settings/get-settings-value" +import verifyUser from "@/lib/actions/studio/users/verify-user" +import { EmailVerification } from "@/interfaces" + +/* eslint-disable @typescript-eslint/no-require-imports */ +jest.mock("@/lib/prisma", () => ({ + prisma: require("@/lib/__mocks__/prisma").prisma +})) + +jest.mock("@/lib/verification-email", () => ({ + sendVerificationEmail: jest.fn() +})) + +jest.mock("@/lib/actions/studio/settings/get-settings-value", () => ({ + getSettingsValue: jest.fn() +})) + +jest.mock("@/lib/handlers/error", () => ({ + __esModule: true, + default: jest.fn((err) => ({ + success: false, + status: 500, + error: { message: err.message } + })) +})) + +jest.mock("@/lib/actions/studio/users/verify-user", () => ({ + __esModule: true, + default: jest.fn() +})) + +describe("email-verification.ts", () => { + const mockSendVerificationEmail = sendVerificationEmail as jest.Mock + const mockGetSettingsValue = getSettingsValue as jest.Mock + const mockVerifyUser = verifyUser as jest.Mock + const mockPrismaVerificationTokenCreate = prisma.verificationToken.create as jest.Mock + const mockPrismaVerificationTokenDelete = prisma.verificationToken.delete as jest.Mock + const mockPrismaVerificationTokenFindUnique = prisma.verificationToken.findUnique as jest.Mock + + beforeEach(() => { + jest.clearAllMocks() + }) + + describe("EmailVerificationAction", () => { + it("should generate and send a verification code", async () => { + const email = "test@example.com" + const siteName = "Test Site" + + mockPrismaVerificationTokenCreate.mockResolvedValue({ id: "1" }) + mockGetSettingsValue.mockResolvedValue(siteName) + mockSendVerificationEmail.mockResolvedValue(undefined) + + const result = await EmailVerificationAction(email) + + expect(result).toEqual({ + success: true, + status: 200, + data: "Verification code sent to your email." + }) + expect(prisma.verificationToken.create).toHaveBeenCalledWith(expect.objectContaining({ + data: expect.objectContaining({ + identifier: email, + type: EmailVerification.REGISTRATION + }) + })) + expect(mockSendVerificationEmail).toHaveBeenCalledWith(siteName, email, expect.any(String)) + }) + + it("should return error if token creation fails", async () => { + mockPrismaVerificationTokenCreate.mockResolvedValue(null) + + const result = await EmailVerificationAction("test@example.com") + + expect(result.success).toBe(false) + expect(result.error?.message).toBe("Failed to generate verification code") + }) + }) + + describe("verifyCode", () => { + it("should verify code and mark user as verified", async () => { + const email = "test@example.com" + const code = "123456" + const mockToken = { + identifier: email, + token: code, + expires: new Date(Date.now() + 10000) + } + + mockPrismaVerificationTokenFindUnique.mockResolvedValue(mockToken) + mockVerifyUser.mockResolvedValue({ success: true }) + mockPrismaVerificationTokenDelete.mockResolvedValue({ id: "1" }) + + const result = await verifyCode(email, code) + + expect(result).toEqual({ + success: true, + status: 200, + data: "User verified successfully." + }) + expect(mockVerifyUser).toHaveBeenCalledWith(email, true) + expect(prisma.verificationToken.delete).toHaveBeenCalledWith({ + where: { token: code, identifier: email } + }) + }) + + it("should return error for invalid or expired code", async () => { + mockPrismaVerificationTokenFindUnique.mockResolvedValue(null) + + const result = await verifyCode("test@example.com", "wrong") + + expect(result).toEqual({ + success: false, + status: 400, + error: { message: "Invalid or expired verification code." } + }) + }) + + it("should return error if verifyUser fails", async () => { + const mockToken = { + identifier: "test@example.com", + token: "123456", + expires: new Date(Date.now() + 10000) + } + + mockPrismaVerificationTokenFindUnique.mockResolvedValue(mockToken) + mockVerifyUser.mockResolvedValue({ success: false }) + + const result = await verifyCode("test@example.com", "123456") + + expect(result).toEqual({ + success: false, + status: 500, + error: { message: "Cannot verify user due to an error." } + }) + }) + + it("should handle unexpected errors in verifyCode", async () => { + mockPrismaVerificationTokenFindUnique.mockRejectedValue(new Error("DB Error")) + + const result = await verifyCode("test@example.com", "123456") + + expect(result.success).toBe(false) + expect(result.error?.message).toBe("DB Error") + }) + }) +}) diff --git a/__tests__/lib/actions/auth/has-permission-api.test.ts b/__tests__/lib/actions/auth/has-permission-api.test.ts new file mode 100644 index 0000000..fe87dc3 --- /dev/null +++ b/__tests__/lib/actions/auth/has-permission-api.test.ts @@ -0,0 +1,96 @@ +import { hasPermissionApi } from "@/lib/actions/auth/has-permission-api" +import getPermissionGroups from "@/lib/actions/studio/permission-groups/get-permission-groups" +import { getApiKeyInfo } from "@/lib/actions/studio/settings/get-api-key-info" +import { isSystemApiKey } from "@/lib/api/utils" + +jest.mock("@/lib/actions/studio/permission-groups/get-permission-groups", () => ({ + __esModule: true, + default: jest.fn() +})) + +jest.mock("@/lib/actions/studio/settings/get-api-key-info", () => ({ + getApiKeyInfo: jest.fn() +})) + +jest.mock("@/lib/api/utils", () => ({ + isSystemApiKey: jest.fn() +})) + +describe("has-permission-api.ts", () => { + const mockGetPermissionGroups = getPermissionGroups as jest.Mock + const mockGetApiKeyInfo = getApiKeyInfo as jest.Mock + const mockIsSystemApiKey = isSystemApiKey as jest.Mock + + beforeEach(() => { + jest.clearAllMocks() + }) + + it("should return true if it is a system API key", async () => { + mockIsSystemApiKey.mockReturnValue(true) + const result = await hasPermissionApi({ key: "system-key" }, "system.users.create") + expect(result).toBe(true) + }) + + it("should return true if API key has permission", async () => { + mockIsSystemApiKey.mockReturnValue(false) + mockGetApiKeyInfo.mockResolvedValue({ + success: true, + data: { permission_group: "editor" } + }) + mockGetPermissionGroups.mockResolvedValue({ + success: true, + data: [ + { + slug: "editor", + privileges: [ + { + "articles.coding": { + permissions: ["read", "update"] + } + } + ] + } + ] + }) + + const result = await hasPermissionApi({ key: "editor-key" }, "articles.coding.update") + expect(result).toBe(true) + }) + + it("should return false if API key does not have permission", async () => { + mockIsSystemApiKey.mockReturnValue(false) + mockGetApiKeyInfo.mockResolvedValue({ + success: true, + data: { permission_group: "viewer" } + }) + mockGetPermissionGroups.mockResolvedValue({ + success: true, + data: [ + { + slug: "viewer", + privileges: [ + { + "articles.coding": { + permissions: ["read"] + } + } + ] + } + ] + }) + + const result = await hasPermissionApi({ key: "viewer-key" }, "articles.coding.update") + expect(result).toBe(false) + }) + + it("should return false if API key has no permission group", async () => { + mockIsSystemApiKey.mockReturnValue(false) + mockGetApiKeyInfo.mockResolvedValue({ + success: true, + data: { permission_group: null } + }) + + const result = await hasPermissionApi({ key: "no-group-key" }, "system.entries.read") + expect(result).toBe(false) + }) +}) diff --git a/__tests__/lib/actions/auth/has-permission.test.ts b/__tests__/lib/actions/auth/has-permission.test.ts new file mode 100644 index 0000000..448f381 --- /dev/null +++ b/__tests__/lib/actions/auth/has-permission.test.ts @@ -0,0 +1,79 @@ +import { hasPermission } from "@/lib/actions/auth/has-permission" +import { auth } from "@/auth" +import { prisma } from "@/lib/prisma" + +/* eslint-disable @typescript-eslint/no-require-imports */ +jest.mock("@/auth", () => ({ + auth: jest.fn() +})) + +jest.mock("@/lib/prisma", () => ({ + prisma: require("@/lib/__mocks__/prisma").prisma +})) + +describe("has-permission.ts", () => { + const mockAuth = auth as jest.Mock + const mockPrismaPermissionGroupFindFirst = prisma.permissionGroup.findFirst as jest.Mock + beforeEach(() => { + jest.clearAllMocks() + }) + + it("should return true if user has permission", async () => { + mockAuth.mockResolvedValue({ + user: { permission_group: "admin" } + }) + + mockPrismaPermissionGroupFindFirst.mockResolvedValue({ + slug: "admin", + privileges: [ + { + "system.users": { + permissions: ["read", "update"] + } + } + ] + }) + + const result = await hasPermission("system.users.read") + expect(result).toBe(true) + expect(mockPrismaPermissionGroupFindFirst).toHaveBeenCalledWith({ + where: { slug: "admin" } + }) + }) + + it("should return false if user does not have permission", async () => { + mockAuth.mockResolvedValue({ + user: { permission_group: "viewer" } + }) + + mockPrismaPermissionGroupFindFirst.mockResolvedValue({ + slug: "viewer", + privileges: [ + { + "system.users": { + permissions: ["read"] + } + } + ] + }) + + const result = await hasPermission("system.users.update") + expect(result).toBe(false) + }) + + it("should return false if no session", async () => { + mockAuth.mockResolvedValue(null) + const result = await hasPermission("system.users.read") + expect(result).toBe(false) + }) + + it("should return false if group not found", async () => { + mockAuth.mockResolvedValue({ + user: { permission_group: "non-existent" } + }) + mockPrismaPermissionGroupFindFirst.mockResolvedValue(null) + + const result = await hasPermission("system.users.read") + expect(result).toBe(false) + }) +}) diff --git a/__tests__/lib/actions/studio/permission-groups/create-permission-group.test.ts b/__tests__/lib/actions/studio/permission-groups/create-permission-group.test.ts new file mode 100644 index 0000000..c568050 --- /dev/null +++ b/__tests__/lib/actions/studio/permission-groups/create-permission-group.test.ts @@ -0,0 +1,98 @@ +import createPermissionGroup from "@/lib/actions/studio/permission-groups/create-permission-group" +import { getPermissionGroup } from "@/lib/auth/get-session" +import handleError from "@/lib/handlers/error" + +/* eslint-disable @typescript-eslint/no-require-imports */ +jest.mock("@/lib/auth/get-session", () => ({ + getPermissionGroup: jest.fn() +})) +jest.mock("@/lib/handlers/error", () => ({ + __esModule: true, + default: jest.fn() +})) + +describe("createPermissionGroup", () => { + const mockGetPermissionGroup = getPermissionGroup as jest.Mock + const mockHandleError = handleError as unknown as jest.Mock + const mockFetch = jest.fn() + global.fetch = mockFetch + + beforeEach(() => { + jest.clearAllMocks() + process.env.BASE_URL = "http://localhost:3000" + process.env.API_KEY = "test-api-key" + process.env.SECRET_KEY = "test-secret-key" + + mockHandleError.mockImplementation((err: Error) => ({ + success: false, + error: { message: err.message } + })) + }) + + it("should create a permission group successfully", async () => { + mockGetPermissionGroup.mockResolvedValue({ slug: "admin" }) + const mockData = { id: "1", name: "Test Group", slug: "test-group", privileges: {} } + mockFetch.mockResolvedValue({ + ok: true, + status: 201, + json: async () => mockData + }) + + const formValues = { name: "Test Group", privileges: {} } + const result = await createPermissionGroup(formValues as any) + + expect(result.success).toBe(true) + if (result.success) { + expect(result.data).toEqual(mockData) + } + expect(mockFetch).toHaveBeenCalledWith( + expect.stringContaining("/api/v1/permission-group/create"), + expect.objectContaining({ + method: "POST", + body: expect.stringContaining('"name":"Test Group"') + }) + ) + }) + + it("should return error if unauthorized", async () => { + mockGetPermissionGroup.mockResolvedValue(null) + + const result = await createPermissionGroup({ name: "Test" } as any) + + expect(result.success).toBe(false) + if (!result.success && "error" in result) { + expect(result.error.message).toContain("Unauthorized") + } + expect(mockHandleError).toHaveBeenCalled() + }) + + it("should return error if fetch fails", async () => { + mockGetPermissionGroup.mockResolvedValue({ slug: "admin" }) + mockFetch.mockResolvedValue({ + ok: false, + status: 500, + json: async () => ({ message: "Server Error" }) + }) + + const result = await createPermissionGroup({ name: "Test" } as any) + + expect(result.success).toBe(false) + if (!result.success && "error" in result) { + expect(result.error.message).toContain("Failed to create permission group") + } + expect(mockHandleError).toHaveBeenCalled() + }) + + it("should return error if fetch throws", async () => { + mockGetPermissionGroup.mockResolvedValue({ slug: "admin" }) + mockFetch.mockRejectedValue(new Error("Network error")) + + const result = await createPermissionGroup({ name: "Test" } as any) + + expect(result.success).toBe(false) + if (!result.success && "error" in result) { + expect(result.error.message).toContain("Network error") + } + expect(mockHandleError).toHaveBeenCalled() + }) +}) diff --git a/__tests__/lib/actions/studio/permission-groups/delete-permission-group.test.ts b/__tests__/lib/actions/studio/permission-groups/delete-permission-group.test.ts new file mode 100644 index 0000000..024d9c9 --- /dev/null +++ b/__tests__/lib/actions/studio/permission-groups/delete-permission-group.test.ts @@ -0,0 +1,89 @@ +import deletePermissionGroupAction from "@/lib/actions/studio/permission-groups/delete-permission-group" +import { getPermissionGroup } from "@/lib/auth/get-session" +import handleError from "@/lib/handlers/error" +import { revalidatePath } from "next/cache" + +/* eslint-disable @typescript-eslint/no-require-imports */ +jest.mock("@/lib/auth/get-session", () => ({ + getPermissionGroup: jest.fn() +})) +jest.mock("@/lib/handlers/error", () => ({ + __esModule: true, + default: jest.fn() +})) +jest.mock("next/cache", () => ({ + revalidatePath: jest.fn() +})) + +describe("deletePermissionGroupAction", () => { + const mockGetPermissionGroup = getPermissionGroup as jest.Mock + const mockHandleError = handleError as unknown as jest.Mock + const mockRevalidatePath = revalidatePath as jest.Mock + const mockFetch = jest.fn() + global.fetch = mockFetch + + beforeEach(() => { + jest.clearAllMocks() + process.env.BASE_URL = "http://localhost:3000" + process.env.API_KEY = "test-api-key" + process.env.SECRET_KEY = "test-secret-key" + + mockHandleError.mockImplementation((err: Error) => ({ + success: false, + error: { message: err.message } + })) + }) + + it("should delete a permission group successfully", async () => { + mockGetPermissionGroup.mockResolvedValue({ slug: "admin" }) + const mockData = [{ id: "2", name: "Remaining Group" }] + mockFetch.mockResolvedValue({ + ok: true, + status: 200, + json: async () => mockData + }) + + const result = await deletePermissionGroupAction("1") + + expect(result.success).toBe(true) + if (result.success) { + expect(result.data).toEqual(mockData) + } + expect(mockFetch).toHaveBeenCalledWith( + expect.stringContaining("/api/v1/permission-group/delete/1"), + expect.objectContaining({ + method: "DELETE" + }) + ) + expect(mockRevalidatePath).toHaveBeenCalledWith("/studio/permission-groups") + }) + + it("should return error if unauthorized", async () => { + mockGetPermissionGroup.mockResolvedValue(null) + + const result = await deletePermissionGroupAction("1") + + expect(result.success).toBe(false) + if (!result.success && "error" in result) { + expect(result.error.message).toContain("Unauthorized") + } + expect(mockHandleError).toHaveBeenCalled() + }) + + it("should return error if fetch fails", async () => { + mockGetPermissionGroup.mockResolvedValue({ slug: "admin" }) + mockFetch.mockResolvedValue({ + ok: false, + status: 500, + json: async () => ({ message: "Server Error" }) + }) + + const result = await deletePermissionGroupAction("1") + + expect(result.success).toBe(false) + if (!result.success && "error" in result) { + expect(result.error.message).toContain("Failed to delete the permission group") + } + expect(mockHandleError).toHaveBeenCalled() + }) +}) diff --git a/__tests__/lib/actions/studio/permission-groups/get-permission-group-by-id.test.ts b/__tests__/lib/actions/studio/permission-groups/get-permission-group-by-id.test.ts new file mode 100644 index 0000000..b4890c6 --- /dev/null +++ b/__tests__/lib/actions/studio/permission-groups/get-permission-group-by-id.test.ts @@ -0,0 +1,79 @@ +import getPermissionGroupById from "@/lib/actions/studio/permission-groups/get-permission-group-by-id" +import handleError from "@/lib/handlers/error" + +/* eslint-disable @typescript-eslint/no-require-imports */ +jest.mock("@/lib/handlers/error", () => ({ + __esModule: true, + default: jest.fn() +})) + +describe("getPermissionGroupById", () => { + const mockHandleError = handleError as unknown as jest.Mock + const mockFetch = jest.fn() + global.fetch = mockFetch + + beforeEach(() => { + jest.clearAllMocks() + process.env.BASE_URL = "http://localhost:3000" + process.env.API_KEY = "test-api-key" + + mockHandleError.mockImplementation((err: Error) => ({ + success: false, + error: { message: err.message } + })) + }) + + it("should fetch a permission group by id successfully", async () => { + const mockGroup = { id: "1", name: "Test Group", slug: "test-group", privileges: {} } + mockFetch.mockResolvedValue({ + ok: true, + status: 200, + json: async () => mockGroup + }) + + const result = await getPermissionGroupById("1") + + expect(result.success).toBe(true) + if (result.success) { + expect(result.data).toEqual(mockGroup) + } + expect(mockFetch).toHaveBeenCalledWith( + expect.stringContaining("/api/v1/permission-group/1"), + expect.objectContaining({ + cache: "no-store" + }) + ) + }) + + it("should handle array response and return the first element", async () => { + const mockGroup = { id: "1", name: "Test Group", slug: "test-group", privileges: {} } + mockFetch.mockResolvedValue({ + ok: true, + status: 200, + json: async () => [mockGroup] + }) + + const result = await getPermissionGroupById("1") + + expect(result.success).toBe(true) + if (result.success) { + expect(result.data).toEqual(mockGroup) + } + }) + + it("should return error if fetch fails", async () => { + mockFetch.mockResolvedValue({ + ok: false, + status: 404, + json: async () => ({ message: "Not Found" }) + }) + + const result = await getPermissionGroupById("1") + + expect(result.success).toBe(false) + if (!result.success && "error" in result) { + expect(result.error.message).toContain("Failed to fetch permission group by slug") + } + expect(mockHandleError).toHaveBeenCalled() + }) +}) diff --git a/__tests__/lib/actions/studio/permission-groups/get-permission-groups.test.ts b/__tests__/lib/actions/studio/permission-groups/get-permission-groups.test.ts new file mode 100644 index 0000000..dec2f2d --- /dev/null +++ b/__tests__/lib/actions/studio/permission-groups/get-permission-groups.test.ts @@ -0,0 +1,39 @@ +import getPermissionGroups from "@/lib/actions/studio/permission-groups/get-permission-groups" +import { prisma } from "@/lib/prisma" +import { getPermissionGroup } from "@/lib/auth/get-session" + +/* eslint-disable @typescript-eslint/no-require-imports */ +jest.mock("@/lib/prisma", () => require("@/lib/__mocks__/prisma-actions")) +jest.mock("@/lib/auth/get-session", () => require("@/lib/__mocks__/get-session")) +jest.mock("@/lib/handlers/error", () => require("@/lib/__mocks__/error")) + +describe("getPermissionGroups", () => { + const mockPrismaFindMany = prisma.permissionGroup.findMany as jest.Mock + const mockGetPermissionGroup = getPermissionGroup as jest.Mock + + beforeEach(() => { + jest.clearAllMocks() + }) + + it("should fetch permission groups using prisma", async () => { + mockGetPermissionGroup.mockResolvedValue("admin") + const mockGroups = [{ id: "1", name: "Admin", slug: "admin", privileges: {} }] + mockPrismaFindMany.mockResolvedValue(mockGroups) + + const result = await getPermissionGroups() + + expect(mockPrismaFindMany).toHaveBeenCalled() + expect(result.success).toBe(true) + if (result.success) { + expect(result.data).toEqual(mockGroups.map(group => ({ ...group, _id: group.id }))) + } + }) + + it("should return error if unauthorized", async () => { + mockGetPermissionGroup.mockResolvedValue(null) + + const result = await getPermissionGroups() + + expect(result.success).toBe(false) + }) +}) diff --git a/__tests__/lib/actions/studio/permission-groups/update-permission-group.test.ts b/__tests__/lib/actions/studio/permission-groups/update-permission-group.test.ts new file mode 100644 index 0000000..d1df5d6 --- /dev/null +++ b/__tests__/lib/actions/studio/permission-groups/update-permission-group.test.ts @@ -0,0 +1,85 @@ +import updatePermissionGroup from "@/lib/actions/studio/permission-groups/update-permission-group" +import { getPermissionGroup } from "@/lib/auth/get-session" +import handleError from "@/lib/handlers/error" + +/* eslint-disable @typescript-eslint/no-require-imports */ +jest.mock("@/lib/auth/get-session", () => ({ + getPermissionGroup: jest.fn() +})) +jest.mock("@/lib/handlers/error", () => ({ + __esModule: true, + default: jest.fn() +})) + +describe("updatePermissionGroup", () => { + const mockGetPermissionGroup = getPermissionGroup as jest.Mock + const mockHandleError = handleError as unknown as jest.Mock + const mockFetch = jest.fn() + global.fetch = mockFetch + + beforeEach(() => { + jest.clearAllMocks() + process.env.BASE_URL = "http://localhost:3000" + process.env.API_KEY = "test-api-key" + process.env.SECRET_KEY = "test-secret-key" + + mockHandleError.mockImplementation((err: Error) => ({ + success: false, + error: { message: err.message } + })) + }) + + it("should update a permission group successfully", async () => { + mockGetPermissionGroup.mockResolvedValue({ slug: "admin" }) + const mockData = { id: "1", name: "Updated Group", slug: "updated-group", privileges: {} } + mockFetch.mockResolvedValue({ + ok: true, + status: 200, + json: async () => mockData + }) + + const formValues = { name: "Updated Group", privileges: {} } + const result = await updatePermissionGroup(formValues as any, "1") + + expect(result.success).toBe(true) + if (result.success) { + expect(result.data).toEqual(mockData) + } + expect(mockFetch).toHaveBeenCalledWith( + expect.stringContaining("/api/v1/permission-group/update/1"), + expect.objectContaining({ + method: "PUT", + body: expect.stringContaining('"name":"Updated Group"') + }) + ) + }) + + it("should return error if unauthorized", async () => { + mockGetPermissionGroup.mockResolvedValue(null) + + const result = await updatePermissionGroup({ name: "Test" } as any, "1") + + expect(result.success).toBe(false) + if (!result.success && "error" in result) { + expect(result.error.message).toContain("Unauthorized") + } + expect(mockHandleError).toHaveBeenCalled() + }) + + it("should return error if fetch fails", async () => { + mockGetPermissionGroup.mockResolvedValue({ slug: "admin" }) + mockFetch.mockResolvedValue({ + ok: false, + status: 500, + json: async () => ({ message: "Server Error" }) + }) + + const result = await updatePermissionGroup({ name: "Test" } as any, "1") + + expect(result.success).toBe(false) + if (!result.success && "error" in result) { + expect(result.error.message).toContain("Failed to update permission group") + } + expect(mockHandleError).toHaveBeenCalled() + }) +}) diff --git a/__tests__/lib/actions/studio/permission-groups/update-permission-groups.test.ts b/__tests__/lib/actions/studio/permission-groups/update-permission-groups.test.ts new file mode 100644 index 0000000..671ad3f --- /dev/null +++ b/__tests__/lib/actions/studio/permission-groups/update-permission-groups.test.ts @@ -0,0 +1,56 @@ +import updatePermissionGroups from "@/lib/actions/studio/permission-groups/update-permission-groups" +import { prisma } from "@/lib/prisma" +import { getPermissionGroup } from "@/lib/auth/get-session" + +/* eslint-disable @typescript-eslint/no-require-imports */ +jest.mock("@/lib/prisma", () => require("@/lib/__mocks__/prisma-actions")) +jest.mock("@/lib/auth/get-session", () => require("@/lib/__mocks__/get-session")) +jest.mock("@/lib/handlers/error", () => require("@/lib/__mocks__/error")) + +describe("updatePermissionGroups", () => { + const mockGetPermissionGroup = getPermissionGroup as jest.Mock + const mockPrismaUpdateMany = prisma.permissionGroup.updateMany as jest.Mock + const mockPrismaTransaction = prisma.$transaction as jest.Mock + + beforeEach(() => { + jest.clearAllMocks() + }) + + it("should update multiple permission groups using prisma transaction", async () => { + mockGetPermissionGroup.mockResolvedValue("admin") + const formValues = { + privileges: { + admin: { system: { entry_types: { read: true } }, namespaces: {} }, + editor: { system: { entry_types: { read: true } }, namespaces: {} } + } + } + mockPrismaUpdateMany.mockReturnValue("mock-promise") + mockPrismaTransaction.mockResolvedValue([{ count: 1 }, { count: 1 }]) + + const result = await updatePermissionGroups(formValues) + + expect(mockPrismaTransaction).toHaveBeenCalled() + expect(result.success).toBe(true) + expect(result.data.modifiedCount).toBe(2) + }) + + it("should return early if no changes", async () => { + mockGetPermissionGroup.mockResolvedValue("admin") + const formValues = { + privileges: {} + } + + const result = await updatePermissionGroups(formValues) + + expect(result.success).toBe(true) + expect(mockPrismaTransaction).not.toHaveBeenCalled() + }) + + it("should return error if unauthorized", async () => { + mockGetPermissionGroup.mockResolvedValue(null) + + const result = await updatePermissionGroups({ privileges: {} }) + + expect(result.success).toBe(false) + }) +}) diff --git a/__tests__/lib/actions/studio/settings/check-register-open.test.ts b/__tests__/lib/actions/studio/settings/check-register-open.test.ts new file mode 100644 index 0000000..3458e71 --- /dev/null +++ b/__tests__/lib/actions/studio/settings/check-register-open.test.ts @@ -0,0 +1,52 @@ +import { checkRegisterOpen } from "@/lib/actions/studio/settings/check-register-open" +import { prisma } from "@/lib/prisma" + +/* eslint-disable @typescript-eslint/no-require-imports */ +jest.mock("@/lib/prisma", () => ({ + prisma: require("@/lib/__mocks__/prisma").prisma +})) + +describe("checkRegisterOpen", () => { + const mockPrismaFindFirst = prisma.settings.findFirst as jest.Mock + beforeEach(() => { + jest.clearAllMocks() + }) + + it("should return open_registration value when set", async () => { + mockPrismaFindFirst.mockResolvedValue({ + settings: { open_registration: true } + }) + const result = await checkRegisterOpen() + expect(result).toBe(true) + expect(prisma.settings.findFirst).toHaveBeenCalledWith({ + where: { name: "identity_settings" } + }) + }) + + it("should return null if no record found", async () => { + mockPrismaFindFirst.mockResolvedValue(null) + const result = await checkRegisterOpen() + expect(result).toBe(null) + }) + + it("should return null if settings field is missing", async () => { + mockPrismaFindFirst.mockResolvedValue({ settings: null }) + const result = await checkRegisterOpen() + expect(result).toBe(null) + }) + + it("should return null if open_registration is not in settings", async () => { + mockPrismaFindFirst.mockResolvedValue({ + settings: { other_setting: true } + }) + const result = await checkRegisterOpen() + expect(result).toBe(null) + }) + + it("should throw error on prisma failure", async () => { + // Silence console.error for this test + jest.spyOn(console, "error").mockImplementation(() => {}) + mockPrismaFindFirst.mockRejectedValue(new Error("DB Error")) + await expect(checkRegisterOpen()).rejects.toThrow("Could not retrieve settings") + }) +}) diff --git a/__tests__/lib/actions/studio/settings/delete-api-key.test.ts b/__tests__/lib/actions/studio/settings/delete-api-key.test.ts new file mode 100644 index 0000000..6c6fa94 --- /dev/null +++ b/__tests__/lib/actions/studio/settings/delete-api-key.test.ts @@ -0,0 +1,70 @@ +import deleteApiKey from "@/lib/actions/studio/settings/delete-api-key" +import { getPermissionGroup } from "@/lib/auth/get-session" +import handleError from "@/lib/handlers/error" +import { prisma } from "@/lib/prisma" +import { revalidatePath } from "next/cache" + +/* eslint-disable @typescript-eslint/no-require-imports */ +jest.mock("@/lib/auth/get-session", () => ({ + getPermissionGroup: jest.fn() +})) +jest.mock("@/lib/handlers/error", () => ({ + __esModule: true, + default: jest.fn() +})) +jest.mock("@/lib/prisma", () => ({ + prisma: require("@/lib/__mocks__/prisma").prisma +})) +jest.mock("next/cache", () => ({ + revalidatePath: jest.fn() +})) + +describe("deleteApiKey", () => { + const mockGetPermissionGroup = getPermissionGroup as jest.Mock + const mockHandleError = handleError as unknown as jest.Mock + const mockRevalidatePath = revalidatePath as jest.Mock + const mockPrismaApiKeyDelete = prisma.apiKey.delete as jest.Mock + + beforeEach(() => { + jest.clearAllMocks() + mockHandleError.mockImplementation((err: Error) => ({ + success: false, + error: { message: err.message } + })) + }) + + it("should delete an api key successfully", async () => { + mockGetPermissionGroup.mockResolvedValue({ slug: "admin" }) + mockPrismaApiKeyDelete.mockResolvedValue({ id: "1" }) + + const result = await deleteApiKey("1") + + expect(result.success).toBe(true) + expect(prisma.apiKey.delete).toHaveBeenCalledWith({ + where: { id: "1" } + }) + expect(mockRevalidatePath).toHaveBeenCalledWith("/studio/settings") + }) + + it("should return error if unauthorized", async () => { + mockGetPermissionGroup.mockResolvedValue(null) + + const result = await deleteApiKey("1") + + expect(result.success).toBe(false) + if (!result.success && "error" in result) { + expect(result.error.message).toContain("Unauthorized") + } + }) + + it("should return error if api key not found", async () => { + mockGetPermissionGroup.mockResolvedValue({ slug: "admin" }) + // Prisma delete throws if record not found + mockPrismaApiKeyDelete.mockRejectedValue({ code: "P2025" }) + + const result = await deleteApiKey("non-existent") + + expect(result.success).toBe(false) + expect(mockHandleError).toHaveBeenCalled() + }) +}) diff --git a/__tests__/lib/actions/studio/settings/generate-api-key.test.ts b/__tests__/lib/actions/studio/settings/generate-api-key.test.ts new file mode 100644 index 0000000..297d551 --- /dev/null +++ b/__tests__/lib/actions/studio/settings/generate-api-key.test.ts @@ -0,0 +1,71 @@ +import generateApiKey from "@/lib/actions/studio/settings/generate-api-key" +import { getPermissionGroup } from "@/lib/auth/get-session" +import handleError from "@/lib/handlers/error" +import { prisma } from "@/lib/prisma" + +/* eslint-disable @typescript-eslint/no-require-imports */ +jest.mock("@/lib/auth/get-session", () => ({ + getPermissionGroup: jest.fn() +})) +jest.mock("@/lib/handlers/error", () => ({ + __esModule: true, + default: jest.fn() +})) +jest.mock("@/lib/prisma", () => ({ + prisma: require("@/lib/__mocks__/prisma").prisma +})) +jest.mock("uid", () => ({ + uid: jest.fn().mockReturnValue("mocked-api-key") +})) + +describe("generateApiKey", () => { + const mockGetPermissionGroup = getPermissionGroup as jest.Mock + const mockHandleError = handleError as unknown as jest.Mock + const mockPrismaApiKeyCreate = prisma.apiKey.create as jest.Mock + beforeEach(() => { + jest.clearAllMocks() + mockHandleError.mockImplementation((err: Error) => ({ + success: false, + error: { message: err.message } + })) + }) + + it("should generate an api key successfully", async () => { + mockGetPermissionGroup.mockResolvedValue({ slug: "admin" }) + const mockCreatedKey = { + id: "1", + key: "mocked-api-key", + description: "Test Key", + permission_group: "editor", + rate_limits: {}, + created_at: new Date() + } + mockPrismaApiKeyCreate.mockResolvedValue(mockCreatedKey) + + const formValues = { + description: "Test Key", + permission_group: "editor", + rate_limits: { + time_window: 1, + req_per_window: 60, + time_interval: 1000 + } + } + const result = await generateApiKey(formValues as any) + + expect(result.success).toBe(true) + expect(prisma.apiKey.create).toHaveBeenCalled() + if (result.success) { + expect(result.data.key).toBe("mocked-api-key") + } + }) + + it("should return error if unauthorized", async () => { + mockGetPermissionGroup.mockResolvedValue(null) + + const result = await generateApiKey({} as any) + + expect(result.success).toBe(false) + expect(mockHandleError).toHaveBeenCalled() + }) +}) diff --git a/__tests__/lib/actions/studio/settings/get-all-api-keys.test.ts b/__tests__/lib/actions/studio/settings/get-all-api-keys.test.ts new file mode 100644 index 0000000..5129dbb --- /dev/null +++ b/__tests__/lib/actions/studio/settings/get-all-api-keys.test.ts @@ -0,0 +1,57 @@ +import getAllApiKeys from "@/lib/actions/studio/settings/get-all-api-keys" +import { getPermissionGroup } from "@/lib/auth/get-session" +import handleError from "@/lib/handlers/error" +import { prisma } from "@/lib/prisma" + +/* eslint-disable @typescript-eslint/no-require-imports */ +jest.mock("@/lib/auth/get-session", () => ({ + getPermissionGroup: jest.fn() +})) +jest.mock("@/lib/handlers/error", () => ({ + __esModule: true, + default: jest.fn() +})) +jest.mock("@/lib/prisma", () => ({ + prisma: require("@/lib/__mocks__/prisma").prisma +})) + +describe("getAllApiKeys", () => { + const mockGetPermissionGroup = getPermissionGroup as jest.Mock + const mockHandleError = handleError as unknown as jest.Mock + const mockPrismaFindMany = prisma.apiKey.findMany as jest.Mock + beforeEach(() => { + jest.clearAllMocks() + mockHandleError.mockImplementation((err: Error) => ({ + success: false, + error: { message: err.message } + })) + }) + + it("should fetch all api keys successfully", async () => { + mockGetPermissionGroup.mockResolvedValue({ slug: "admin" }) + const mockKeys = [ + { id: "1", key: "key-1", created_at: new Date() }, + { id: "2", key: "key-2", created_at: new Date() } + ] + mockPrismaFindMany.mockResolvedValue(mockKeys) + + const result = await getAllApiKeys() + + expect(result.success).toBe(true) + expect(prisma.apiKey.findMany).toHaveBeenCalledWith({ + orderBy: { created_at: "desc" } + }) + if (result.success) { + expect(result.data).toHaveLength(2) + } + }) + + it("should return error if unauthorized", async () => { + mockGetPermissionGroup.mockResolvedValue(null) + + const result = await getAllApiKeys() + + expect(result.success).toBe(false) + expect(mockHandleError).toHaveBeenCalled() + }) +}) diff --git a/__tests__/lib/actions/studio/settings/get-all-settings.test.ts b/__tests__/lib/actions/studio/settings/get-all-settings.test.ts new file mode 100644 index 0000000..5af2730 --- /dev/null +++ b/__tests__/lib/actions/studio/settings/get-all-settings.test.ts @@ -0,0 +1,52 @@ +import getAllSettings from "@/lib/actions/studio/settings/get-all-settings" +import { getPermissionGroup } from "@/lib/auth/get-session" +import handleError from "@/lib/handlers/error" +import { prisma } from "@/lib/prisma" + +/* eslint-disable @typescript-eslint/no-require-imports */ +jest.mock("@/lib/auth/get-session", () => ({ + getPermissionGroup: jest.fn() +})) +jest.mock("@/lib/handlers/error", () => ({ + __esModule: true, + default: jest.fn() +})) +jest.mock("@/lib/prisma", () => ({ + prisma: require("@/lib/__mocks__/prisma").prisma +})) + +describe("getAllSettings", () => { + const mockGetPermissionGroup = getPermissionGroup as jest.Mock + const mockHandleError = handleError as unknown as jest.Mock + const mockPrismaFindMany = prisma.settings.findMany as jest.Mock + beforeEach(() => { + jest.clearAllMocks() + mockHandleError.mockImplementation((err: Error) => ({ + success: false, + error: { message: err.message } + })) + }) + + it("should fetch all settings successfully", async () => { + mockGetPermissionGroup.mockResolvedValue({ slug: "admin" }) + const mockSettings = [{ id: "1", name: "general_settings", settings: {} }] + mockPrismaFindMany.mockResolvedValue(mockSettings) + + const result = await getAllSettings() + + expect(result.success).toBe(true) + expect(prisma.settings.findMany).toHaveBeenCalled() + if (result.success) { + expect(result.data).toEqual(mockSettings) + } + }) + + it("should return error if unauthorized", async () => { + mockGetPermissionGroup.mockResolvedValue(null) + + const result = await getAllSettings() + + expect(result.success).toBe(false) + expect(mockHandleError).toHaveBeenCalled() + }) +}) diff --git a/__tests__/lib/actions/studio/settings/get-api-key-info.test.ts b/__tests__/lib/actions/studio/settings/get-api-key-info.test.ts new file mode 100644 index 0000000..0e5a865 --- /dev/null +++ b/__tests__/lib/actions/studio/settings/get-api-key-info.test.ts @@ -0,0 +1,51 @@ +import { getApiKeyInfo } from "@/lib/actions/studio/settings/get-api-key-info" +import handleError from "@/lib/handlers/error" +import { prisma } from "@/lib/prisma" + +/* eslint-disable @typescript-eslint/no-require-imports */ +jest.mock("@/lib/handlers/error", () => ({ + __esModule: true, + default: jest.fn() +})) +jest.mock("@/lib/prisma", () => ({ + prisma: require("@/lib/__mocks__/prisma").prisma +})) + +describe("getApiKeyInfo", () => { + const mockHandleError = handleError as unknown as jest.Mock + const mockPrismaFindFirst = prisma.apiKey.findFirst as jest.Mock + + beforeEach(() => { + jest.clearAllMocks() + mockHandleError.mockImplementation((err: Error) => ({ + success: false, + error: { message: err.message } + })) + }) + + it("should fetch api key info successfully", async () => { + const mockKey = { id: "1", key: "test-key", description: "Test" } + mockPrismaFindFirst.mockResolvedValue(mockKey) + + const result = await getApiKeyInfo("test-key") + + expect(result.success).toBe(true) + expect(prisma.apiKey.findFirst).toHaveBeenCalledWith({ + where: { key: "test-key" } + }) + if (result.success) { + expect(result.data.key).toBe("test-key") + } + }) + + it("should return error if api key not found", async () => { + mockPrismaFindFirst.mockResolvedValue(null) + + const result = await getApiKeyInfo("invalid-key") + + expect(result.success).toBe(false) + if (!result.success && "error" in result) { + expect(result.error.message).toBe("Invalid API key") + } + }) +}) diff --git a/__tests__/lib/actions/studio/settings/get-settings-by-name.test.ts b/__tests__/lib/actions/studio/settings/get-settings-by-name.test.ts new file mode 100644 index 0000000..f30aabf --- /dev/null +++ b/__tests__/lib/actions/studio/settings/get-settings-by-name.test.ts @@ -0,0 +1,67 @@ +import getSettingsByName from "@/lib/actions/studio/settings/get-settings-by-name" +import { getPermissionGroup } from "@/lib/auth/get-session" +import handleError from "@/lib/handlers/error" +import { prisma } from "@/lib/prisma" + +/* eslint-disable @typescript-eslint/no-require-imports */ +jest.mock("@/lib/auth/get-session", () => ({ + getPermissionGroup: jest.fn() +})) +jest.mock("@/lib/handlers/error", () => ({ + __esModule: true, + default: jest.fn() +})) +jest.mock("@/lib/prisma", () => ({ + prisma: require("@/lib/__mocks__/prisma").prisma +})) + +describe("getSettingsByName", () => { + const mockGetPermissionGroup = getPermissionGroup as jest.Mock + const mockHandleError = handleError as unknown as jest.Mock + const mockPrismaFindFirstOrThrow = prisma.settings.findFirstOrThrow as jest.Mock + + beforeEach(() => { + jest.clearAllMocks() + mockHandleError.mockImplementation((err: Error) => ({ + success: false, + error: { message: err.message } + })) + }) + + it("should fetch settings by name successfully", async () => { + mockGetPermissionGroup.mockResolvedValue({ slug: "admin" }) + const mockSetting = { id: "1", name: "general_settings", settings: {} } + mockPrismaFindFirstOrThrow.mockResolvedValue(mockSetting) + + const result = await getSettingsByName("general_settings") + + expect(result.success).toBe(true) + expect(prisma.settings.findFirstOrThrow).toHaveBeenCalledWith({ + where: { name: "general_settings" } + }) + if (result.success) { + expect(result.data).toEqual(mockSetting) + } + }) + + it("should return error if unauthorized", async () => { + mockGetPermissionGroup.mockResolvedValue(null) + + const result = await getSettingsByName("general_settings") + + expect(result.success).toBe(false) + expect(mockHandleError).toHaveBeenCalled() + }) + + it("should return error if name mismatch", async () => { + mockGetPermissionGroup.mockResolvedValue({ slug: "admin" }) + mockPrismaFindFirstOrThrow.mockResolvedValue({ name: "other" }) + + const result = await getSettingsByName("general_settings") + + expect(result.success).toBe(false) + if (!result.success && "error" in result) { + expect(result.error.message).toBe("Failed to get settings") + } + }) +}) diff --git a/__tests__/lib/actions/studio/settings/get-settings-value.test.ts b/__tests__/lib/actions/studio/settings/get-settings-value.test.ts new file mode 100644 index 0000000..828713c --- /dev/null +++ b/__tests__/lib/actions/studio/settings/get-settings-value.test.ts @@ -0,0 +1,37 @@ +import { getSettingsValue } from "@/lib/actions/studio/settings/get-settings-value" +import { prisma } from "@/lib/prisma" + +/* eslint-disable @typescript-eslint/no-require-imports */ +jest.mock("@/lib/prisma", () => ({ + prisma: require("@/lib/__mocks__/prisma").prisma +})) + +describe("getSettingsValue", () => { + const mockPrismaFindFirst = prisma.settings.findFirst as jest.Mock + beforeEach(() => { + jest.clearAllMocks() + }) + + it("should return settings value when found", async () => { + mockPrismaFindFirst.mockResolvedValue({ + settings: { rate_limits: { enabled: true } } + }) + const result = await getSettingsValue("api_settings", "rate_limits") + expect(result).toEqual({ enabled: true }) + expect(prisma.settings.findFirst).toHaveBeenCalledWith({ + where: { name: "api_settings" } + }) + }) + + it("should return null if no record found", async () => { + mockPrismaFindFirst.mockResolvedValue(null) + const result = await getSettingsValue("api_settings", "rate_limits") + expect(result).toBe(null) + }) + + it("should throw error on prisma failure", async () => { + jest.spyOn(console, "error").mockImplementation(() => {}) + mockPrismaFindFirst.mockRejectedValue(new Error("DB Error")) + await expect(getSettingsValue("api_settings", "key")).rejects.toThrow("Could not retrieve settings") + }) +}) diff --git a/__tests__/lib/actions/studio/settings/update-api-settings.test.ts b/__tests__/lib/actions/studio/settings/update-api-settings.test.ts new file mode 100644 index 0000000..3f96376 --- /dev/null +++ b/__tests__/lib/actions/studio/settings/update-api-settings.test.ts @@ -0,0 +1,56 @@ +import updateApiSettings from "@/lib/actions/studio/settings/update-api-settings" +import { getPermissionGroup } from "@/lib/auth/get-session" +import handleError from "@/lib/handlers/error" +import { prisma } from "@/lib/prisma" + +/* eslint-disable @typescript-eslint/no-require-imports */ +jest.mock("@/lib/auth/get-session", () => ({ + getPermissionGroup: jest.fn() +})) +jest.mock("@/lib/handlers/error", () => ({ + __esModule: true, + default: jest.fn() +})) +jest.mock("@/lib/prisma", () => ({ + prisma: require("@/lib/__mocks__/prisma").prisma +})) + +describe("updateApiSettings", () => { + const mockGetPermissionGroup = getPermissionGroup as jest.Mock + const mockHandleError = handleError as unknown as jest.Mock + const mockPrismaSettingsUpdate = prisma.settings.update as jest.Mock + + beforeEach(() => { + jest.clearAllMocks() + mockHandleError.mockImplementation((err: Error) => ({ + success: false, + error: { message: err.message } + })) + }) + + it("should update api settings successfully", async () => { + mockGetPermissionGroup.mockResolvedValue({ slug: "admin" }) + const mockResponse = { id: "1", name: "api_settings", settings: { some: "value" } } + mockPrismaSettingsUpdate.mockResolvedValue(mockResponse) + + const result = await updateApiSettings({ some: "value" }, "1") + + expect(result.success).toBe(true) + expect(prisma.settings.update).toHaveBeenCalledWith({ + where: { id: "1", name: "api_settings" }, + data: expect.objectContaining({ settings: { some: "value" } }) + }) + if (result.success) { + expect(result.data.settings).toEqual({ some: "value" }) + } + }) + + it("should return error if unauthorized", async () => { + mockGetPermissionGroup.mockResolvedValue(null) + + const result = await updateApiSettings({}, "1") + + expect(result.success).toBe(false) + expect(mockHandleError).toHaveBeenCalled() + }) +}) diff --git a/__tests__/lib/actions/studio/settings/update-appearance-settings.test.ts b/__tests__/lib/actions/studio/settings/update-appearance-settings.test.ts new file mode 100644 index 0000000..c88877f --- /dev/null +++ b/__tests__/lib/actions/studio/settings/update-appearance-settings.test.ts @@ -0,0 +1,56 @@ +import updateAppearanceSettings from "@/lib/actions/studio/settings/update-appearance-settings" +import { getPermissionGroup } from "@/lib/auth/get-session" +import handleError from "@/lib/handlers/error" +import { prisma } from "@/lib/prisma" + +/* eslint-disable @typescript-eslint/no-require-imports */ +jest.mock("@/lib/auth/get-session", () => ({ + getPermissionGroup: jest.fn() +})) +jest.mock("@/lib/handlers/error", () => ({ + __esModule: true, + default: jest.fn() +})) +jest.mock("@/lib/prisma", () => ({ + prisma: require("@/lib/__mocks__/prisma").prisma +})) + +describe("updateAppearanceSettings", () => { + const mockGetPermissionGroup = getPermissionGroup as jest.Mock + const mockHandleError = handleError as unknown as jest.Mock + const mockPrismaSettingsUpdate = prisma.settings.update as jest.Mock + + beforeEach(() => { + jest.clearAllMocks() + mockHandleError.mockImplementation((err: Error) => ({ + success: false, + error: { message: err.message } + })) + }) + + it("should update appearance settings successfully", async () => { + mockGetPermissionGroup.mockResolvedValue({ slug: "admin" }) + const mockResponse = { id: "1", name: "appearance_settings", settings: { theme: "dark" } } + mockPrismaSettingsUpdate.mockResolvedValue(mockResponse) + + const result = await updateAppearanceSettings({ theme: "dark" }, "1") + + expect(result.success).toBe(true) + expect(prisma.settings.update).toHaveBeenCalledWith({ + where: { id: "1", name: "appearance_settings" }, + data: expect.objectContaining({ settings: { theme: "dark" } }) + }) + if (result.success) { + expect(result.data.settings).toEqual({ theme: "dark" }) + } + }) + + it("should return error if unauthorized", async () => { + mockGetPermissionGroup.mockResolvedValue(null) + + const result = await updateAppearanceSettings({}, "1") + + expect(result.success).toBe(false) + expect(mockHandleError).toHaveBeenCalled() + }) +}) diff --git a/__tests__/lib/actions/studio/settings/update-general-settings.test.ts b/__tests__/lib/actions/studio/settings/update-general-settings.test.ts new file mode 100644 index 0000000..b759e32 --- /dev/null +++ b/__tests__/lib/actions/studio/settings/update-general-settings.test.ts @@ -0,0 +1,56 @@ +import updateGeneralSettings from "@/lib/actions/studio/settings/update-general-settings" +import { getPermissionGroup } from "@/lib/auth/get-session" +import handleError from "@/lib/handlers/error" +import { prisma } from "@/lib/prisma" + +/* eslint-disable @typescript-eslint/no-require-imports */ +jest.mock("@/lib/auth/get-session", () => ({ + getPermissionGroup: jest.fn() +})) +jest.mock("@/lib/handlers/error", () => ({ + __esModule: true, + default: jest.fn() +})) +jest.mock("@/lib/prisma", () => ({ + prisma: require("@/lib/__mocks__/prisma").prisma +})) + +describe("updateGeneralSettings", () => { + const mockGetPermissionGroup = getPermissionGroup as jest.Mock + const mockHandleError = handleError as unknown as jest.Mock + const mockPrismaSettingsUpdate = prisma.settings.update as jest.Mock + + beforeEach(() => { + jest.clearAllMocks() + mockHandleError.mockImplementation((err: Error) => ({ + success: false, + error: { message: err.message } + })) + }) + + it("should update general settings successfully", async () => { + mockGetPermissionGroup.mockResolvedValue({ slug: "admin" }) + const mockResponse = { id: "1", name: "general_settings", settings: { site_name: "My Site" } } + mockPrismaSettingsUpdate.mockResolvedValue(mockResponse) + + const result = await updateGeneralSettings({ site_name: "My Site" }, "1") + + expect(result.success).toBe(true) + expect(prisma.settings.update).toHaveBeenCalledWith({ + where: { id: "1", name: "general_settings" }, + data: expect.objectContaining({ settings: { site_name: "My Site" } }) + }) + if (result.success) { + expect(result.data.settings).toEqual({ site_name: "My Site" }) + } + }) + + it("should return error if unauthorized", async () => { + mockGetPermissionGroup.mockResolvedValue(null) + + const result = await updateGeneralSettings({}, "1") + + expect(result.success).toBe(false) + expect(mockHandleError).toHaveBeenCalled() + }) +}) diff --git a/__tests__/lib/actions/studio/settings/update-identity-setttings.test.ts b/__tests__/lib/actions/studio/settings/update-identity-setttings.test.ts new file mode 100644 index 0000000..3c1ab06 --- /dev/null +++ b/__tests__/lib/actions/studio/settings/update-identity-setttings.test.ts @@ -0,0 +1,55 @@ +import updateIdentitySettings from "@/lib/actions/studio/settings/update-identity-setttings" +import { getPermissionGroup } from "@/lib/auth/get-session" +import handleError from "@/lib/handlers/error" +import { prisma } from "@/lib/prisma" + +/* eslint-disable @typescript-eslint/no-require-imports */ +jest.mock("@/lib/auth/get-session", () => ({ + getPermissionGroup: jest.fn() +})) +jest.mock("@/lib/handlers/error", () => ({ + __esModule: true, + default: jest.fn() +})) +jest.mock("@/lib/prisma", () => ({ + prisma: require("@/lib/__mocks__/prisma").prisma +})) + +describe("updateIdentitySettings", () => { + const mockGetPermissionGroup = getPermissionGroup as jest.Mock + const mockHandleError = handleError as unknown as jest.Mock + const mockPrismaSettingsUpdate = prisma.settings.update as jest.Mock + beforeEach(() => { + jest.clearAllMocks() + mockHandleError.mockImplementation((err: Error) => ({ + success: false, + error: { message: err.message } + })) + }) + + it("should update identity settings successfully", async () => { + mockGetPermissionGroup.mockResolvedValue({ slug: "admin" }) + const mockResponse = { id: "1", name: "identity_settings", settings: { open_registration: true } } + mockPrismaSettingsUpdate.mockResolvedValue(mockResponse) + + const result = await updateIdentitySettings({ open_registration: true }, "1") + + expect(result.success).toBe(true) + expect(prisma.settings.update).toHaveBeenCalledWith({ + where: { id: "1", name: "identity_settings" }, + data: expect.objectContaining({ settings: { open_registration: true } }) + }) + if (result.success) { + expect(result.data.settings).toEqual({ open_registration: true }) + } + }) + + it("should return error if unauthorized", async () => { + mockGetPermissionGroup.mockResolvedValue(null) + + const result = await updateIdentitySettings({}, "1") + + expect(result.success).toBe(false) + expect(mockHandleError).toHaveBeenCalled() + }) +}) diff --git a/__tests__/lib/actions/studio/stats/get-analytics-stats.test.ts b/__tests__/lib/actions/studio/stats/get-analytics-stats.test.ts new file mode 100644 index 0000000..4d3c3e6 --- /dev/null +++ b/__tests__/lib/actions/studio/stats/get-analytics-stats.test.ts @@ -0,0 +1,47 @@ +import getAnalyticsStats from "@/lib/actions/studio/stats/get-analytics-stats" +import { prisma } from "@/lib/prisma" + +/* eslint-disable @typescript-eslint/no-require-imports */ +jest.mock("@/lib/prisma", () => require("@/lib/__mocks__/prisma-actions")) +jest.mock("@/lib/handlers/error", () => require("@/lib/__mocks__/error")) + +describe("getAnalyticsStats", () => { + const mockPrismaApiRequestLogFindMany = prisma.apiRequestLog.findMany as jest.Mock + const mockPrismaApiRequestLogAggregate = prisma.apiRequestLog.aggregate as jest.Mock + const mockPrismaApiRequestLogCount = prisma.apiRequestLog.count as jest.Mock + const mockPrismaApiRequestLogGroupBy = prisma.apiRequestLog.groupBy as jest.Mock + + beforeEach(() => { + jest.clearAllMocks() + }) + + it("should return analytics stats successfully", async () => { + mockPrismaApiRequestLogFindMany.mockResolvedValue([]) + mockPrismaApiRequestLogAggregate.mockResolvedValue({ _avg: { responseTime: 50 } }) + mockPrismaApiRequestLogCount.mockResolvedValue(10) + mockPrismaApiRequestLogGroupBy.mockResolvedValue([]) + + const result = await getAnalyticsStats() + + expect(result.success).toBe(true) + if (result.success) { + expect(result.data).toBeDefined() + expect(result.data.avgResponseTime).toBe(50) + expect(result.data.totalRequestStats.count).toBe(10) + } + expect(mockPrismaApiRequestLogFindMany).toHaveBeenCalled() + expect(mockPrismaApiRequestLogAggregate).toHaveBeenCalled() + expect(mockPrismaApiRequestLogCount).toHaveBeenCalled() + }) + + it("should return error if fetching fails", async () => { + mockPrismaApiRequestLogFindMany.mockRejectedValue(new Error("Database error")) + + const result = await getAnalyticsStats() + + expect(result.success).toBe(false) + if (!result.success) { + expect(result.error.message).toContain("Failed to fetch analytics stats") + } + }) +}) diff --git a/__tests__/lib/actions/studio/stats/get-logs.test.ts b/__tests__/lib/actions/studio/stats/get-logs.test.ts new file mode 100644 index 0000000..6521f0b --- /dev/null +++ b/__tests__/lib/actions/studio/stats/get-logs.test.ts @@ -0,0 +1,45 @@ +import getLogs from "@/lib/actions/studio/stats/get-logs" +import { prisma } from "@/lib/prisma" + +/* eslint-disable @typescript-eslint/no-require-imports */ +jest.mock("@/lib/prisma", () => require("@/lib/__mocks__/prisma-actions")) +jest.mock("@/lib/handlers/error", () => require("@/lib/__mocks__/error")) + +describe("getLogs", () => { + const mockPrismaApiRequestLogFindMany = prisma.apiRequestLog.findMany as jest.Mock + const mockPrismaApiRequestLogCount = prisma.apiRequestLog.count as jest.Mock + + beforeEach(() => { + jest.clearAllMocks() + }) + + it("should return logs successfully", async () => { + const mockLogs = [{ id: "1", endpoint: "/api/test", timestamp: new Date() }] + mockPrismaApiRequestLogFindMany.mockResolvedValue(mockLogs) + mockPrismaApiRequestLogCount.mockResolvedValue(1) + + const result = await getLogs({ take: 10, skip: 0 }) + + expect(result.success).toBe(true) + if (result.success) { + expect(result.data.logs).toEqual(mockLogs) + expect(result.data.total).toBe(1) + expect(result.data.hasMore).toBe(false) + } + expect(mockPrismaApiRequestLogFindMany).toHaveBeenCalledWith(expect.objectContaining({ + take: 10, + skip: 0 + })) + }) + + it("should return error if fetching fails", async () => { + mockPrismaApiRequestLogFindMany.mockRejectedValue(new Error("Database error")) + + const result = await getLogs() + + expect(result.success).toBe(false) + if (!result.success && "error" in result) { + expect(result.error.message).toContain("Failed to fetch logs") + } + }) +}) diff --git a/__tests__/lib/actions/studio/stats/get-overview-stats.test.ts b/__tests__/lib/actions/studio/stats/get-overview-stats.test.ts new file mode 100644 index 0000000..f31b158 --- /dev/null +++ b/__tests__/lib/actions/studio/stats/get-overview-stats.test.ts @@ -0,0 +1,50 @@ +import getOverviewStats from "@/lib/actions/studio/stats/get-overview-stats" +import { prisma } from "@/lib/prisma" + +/* eslint-disable @typescript-eslint/no-require-imports */ +jest.mock("@/lib/prisma", () => require("@/lib/__mocks__/prisma-actions")) +jest.mock("@/lib/handlers/error", () => require("@/lib/__mocks__/error")) + +describe("getOverviewStats", () => { + const mockPrismaApiRequestLogCount = prisma.apiRequestLog.count as jest.Mock + const mockPrismaEntryCount = prisma.entry.count as jest.Mock + const mockPrismaEntryTypeCount = prisma.entryType.count as jest.Mock + const mockPrismaUserCount = prisma.user.count as jest.Mock + const mockPrismaApiRequestLogAggregateRaw = prisma.apiRequestLog.aggregateRaw as jest.Mock + const mockPrismaApiRequestLogFindMany = prisma.apiRequestLog.findMany as jest.Mock + + beforeEach(() => { + jest.clearAllMocks() + }) + + it("should return overview stats successfully using Prisma", async () => { + mockPrismaApiRequestLogCount.mockResolvedValue(100) + mockPrismaEntryCount.mockResolvedValue(50) + mockPrismaEntryTypeCount.mockResolvedValue(5) + mockPrismaUserCount.mockResolvedValue(10) + mockPrismaApiRequestLogAggregateRaw.mockResolvedValue([]) + mockPrismaApiRequestLogFindMany.mockResolvedValue([]) + + const result = await getOverviewStats() + + expect(result.success).toBe(true) + if (result.success) { + expect(result.data.totalRequests).toBe(100) + expect(result.data.totalEntries).toBe(50) + expect(result.data.totalEntryTypes).toBe(5) + expect(result.data.totalActiveUsers).toBe(10) + } + }) + + it("should return an error if Prisma throws an error", async () => { + mockPrismaApiRequestLogCount.mockRejectedValue(new Error("Failed to fetch overview stats:")) + + const result = await getOverviewStats() + + expect(result.success).toBe(false) + if (!result.success) { + expect(result.error).toStrictEqual({"message": "Failed to fetch overview stats:"}) + } + }) + +}) diff --git a/app/setup/setup-action.ts b/app/setup/setup-action.ts index 1c0a769..41c26b6 100644 --- a/app/setup/setup-action.ts +++ b/app/setup/setup-action.ts @@ -7,22 +7,15 @@ import { uid } from "uid" import { apiKeyController } from "@/controllers/api-key.controller" import { UserController } from "@/controllers/user.controller" import { PermissionGroupController } from "@/controllers/permission-group.controller" -import { connectDB } from "@/lib/mongodb" import handleError from "@/lib/handlers/error" const execAsync = promisify(exec) export async function runSetupAction(values: SetupFormValues): Promise { try { - const client = await connectDB() - const dbConnection = client.db() - - // Creating required collections - await dbConnection.createCollection("api_keys") - await dbConnection.createCollection("entries") - await dbConnection.createCollection("entry_types") - await dbConnection.createCollection("permission_groups") - await dbConnection.createCollection("users") + // Run Prisma commands first to ensure database schema is ready + await execAsync("npx prisma db push") + await execAsync("npx prisma db seed") // Creating Admin Account const adminEmail = values.ADMIN_EMAIL @@ -123,10 +116,6 @@ export async function runSetupAction(values: SetupFormValues): Promise u.oauth_id === proverId && u.oauth_provider === oauthProvider + (u) => u.oauth_id === proverId && u.oauth_provider === oauthProvider ) // Second priority: Fallback to matching by email (Account linking scenario) @@ -202,7 +204,7 @@ export const config = { dbUser = existingUsersByEmail[0] } - if (!dbUser) { + if (!dbUser && !emailVerified) { return false } @@ -217,6 +219,7 @@ export const config = { status: UserStatus.Active, permission_group: defaultPermGroup, oauth_provider: oauthProvider, + emailVerified: true, oauth_id: proverId, password: `${account?.provider}-${user.id}-${uid(16)}!A1` } @@ -230,6 +233,8 @@ export const config = { user.permission_group = newUser.permission_group user.status = newUser.status } else { + if (!dbUser) return false + // 2. User exists. Check if we need to link the OAuth ID // If the user doesn't have an oauth_id yet, or if it was different, // we update it ONCE to bind this OAuth account to this email. @@ -255,13 +260,13 @@ export const config = { fullname: user.name || dbUser.fullname, username: user.username || dbUser.username, profile_img: user.profile_img || dbUser.profile_img, - status: dbUser.status, + status: dbUser.status as UserStatus, email: dbUser.email, permission_group: dbUser.permission_group, password: "" }, dbUser as any, - dbUser._id.toString(), + dbUser.id, true ) if ("error" in updateResponse || !updateResponse.success) { @@ -271,10 +276,10 @@ export const config = { } // 3. CRITICAL: Map the DB ID to the session 'user' object - // This ensures the rest of your app uses the MongoDB _id, not the Google 'sub' - user.id = dbUser._id.toString() - user.permission_group = dbUser.permission_group - user.status = dbUser.status + // This ensures the rest of your app uses the database ID, not the Google 'sub' + user.id = dbUser!.id + user.permission_group = dbUser!.permission_group + user.status = dbUser!.status } } catch (error) { console.error("Error during OAuth sign-in check:", error) diff --git a/controllers/api-builder.controller.ts b/controllers/api-builder.controller.ts index 7899ca1..1b61f23 100644 --- a/controllers/api-builder.controller.ts +++ b/controllers/api-builder.controller.ts @@ -1,6 +1,5 @@ -import { connectDB } from "../lib/mongodb" -import { MongoClient, ObjectId } from "mongodb" -import { FindType } from "../interfaces" +import { prisma } from "@/lib/prisma" +import { FindType } from "@/interfaces" export class apiBuilderController { routeType: string @@ -16,102 +15,60 @@ export class apiBuilderController { } async fetchData(findType?: FindType) { - let dataCollection: object[] | undefined - let isConnected = false - let client: MongoClient | undefined + const modelMap: Record = { + entries: prisma.entry, + entry_types: prisma.entryType, + permission_groups: prisma.permissionGroup, + users: prisma.user, + api_keys: prisma.apiKey + } - try { - client = await connectDB() - isConnected = true + const model = modelMap[this.collectionName] - // If there is more than one parameter in uri then convert into "param[0].param[1].param[i]" format to match with namespace field in the DB - if (this.routeType === "multi-param" && Array.isArray(this.routeData)) { - let namespace = "" + if (!model) { + return [{ message: `Error: Collection ${this.collectionName} not found` }] + } - for (let i = 0; i < this.routeData.length; i++) { - if (i === 0) namespace += `${this.routeData[i]}` - else namespace += `.${this.routeData[i]}` - } + try { + let where: any = {} - if (findType === undefined || findType === "Equals") { - dataCollection = await client - .db(`${process.env.DB_NAME}`) - .collection(this.collectionName) - .find({ namespace: namespace }) - .toArray() - } else if (findType === "StartsWith") { - dataCollection = await client - .db(process.env.DB_NAME) - .collection(this.collectionName) - .find({ namespace: { $regex: `^${String(namespace)}` } }) - .toArray() - } else if (findType === "EndsWith") { - dataCollection = await client - .db(process.env.DB_NAME) - .collection(this.collectionName) - .find({ namespace: { $regex: `${String(namespace)}$` } }) - .toArray() - } else if (findType === "Contains") { - dataCollection = await client - .db(process.env.DB_NAME) - .collection(this.collectionName) - .find({ namespace: { $regex: `${String(namespace)}$` } }) - .toArray() - } + if (this.routeType === "multi-param" && Array.isArray(this.routeData)) { + const namespace = this.routeData.join(".") + where = this.buildWhereClause("namespace", namespace, findType) } else if (this.routeType === "index") { - dataCollection = await client.db(process.env.DB_NAME).collection(this.collectionName).find().toArray() + where = {} } else if (this.routeType === "single-param") { - if (this.findWhere === "_id") { - dataCollection = await client - .db(process.env.DB_NAME) - .collection(this.collectionName) - .find({ [this.findWhere as string]: new ObjectId(this.routeData as string) }) - .toArray() - } else { - if (findType === undefined || findType === "Equals") { - dataCollection = await client - .db(process.env.DB_NAME) - .collection(this.collectionName) - .find({ [this.findWhere as string]: `${this.routeData}` }) - .toArray() - } else if (findType === "StartsWith") { - dataCollection = await client - .db(process.env.DB_NAME) - .collection(this.collectionName) - .find({ [this.findWhere as string]: { $regex: `^${String(this.routeData)}` } }) - .toArray() - } else if (findType === "EndsWith") { - dataCollection = await client - .db(process.env.DB_NAME) - .collection(this.collectionName) - .find({ [this.findWhere as string]: { $regex: `${String(this.routeData)}$` } }) - .toArray() - } else if (findType === "Contains") { - dataCollection = await client - .db(process.env.DB_NAME) - .collection(this.collectionName) - .find({ [this.findWhere as string]: { $regex: `${String(this.routeData)}$` } }) - .toArray() - } - } + const field = this.findWhere === "_id" ? "id" : (this.findWhere as string) + where = this.buildWhereClause(field, this.routeData as string, findType) } else if (this.routeType === "id") { - dataCollection = await client - .db("api_db") - .collection(this.collectionName) - .find({ _id: new ObjectId(this.routeData as string) }) - .toArray() + where = { id: this.routeData as string } } else { - dataCollection = [{ message: "Error: Unexpected route type!" }] + return [{ message: "Error: Unexpected route type!" }] } - if (isConnected) { - return dataCollection - } else { - return [{ message: "Database connection is NOT established" }] - } + const dataCollection = await model.findMany({ where }) + return dataCollection } catch (e) { console.error(e) return [{ message: "Database operation failed." }] } } + + private buildWhereClause(field: string, value: string, findType?: FindType) { + if (field === "id") { + return { id: value } + } + + switch (findType) { + case "StartsWith": + return { [field]: { startsWith: value } } + case "EndsWith": + return { [field]: { endsWith: value } } + case "Contains": + return { [field]: { contains: value } } + case "Equals": + default: + return { [field]: value } + } + } } diff --git a/controllers/api-key.controller.ts b/controllers/api-key.controller.ts index 1baf470..8aa9824 100644 --- a/controllers/api-key.controller.ts +++ b/controllers/api-key.controller.ts @@ -1,5 +1,4 @@ -import { connectDB } from "@/lib/mongodb" -import { Collection, DeleteResult, InsertOneResult, MongoClient, ObjectId } from "mongodb" +import { prisma } from "@/lib/prisma" //Interface import { ApiKey } from "@/interfaces" @@ -14,97 +13,54 @@ export class apiKeyController { } async findKey(): Promise { - let dbCollection: Collection - let isConnected = false - let client: MongoClient | undefined try { - try { - client = await connectDB() - isConnected = true - } catch (e) { - console.log(e) - } - if (client && isConnected) { - let findResult: ApiKey[] | undefined - try { - dbCollection = client.db(process.env.DB_NAME).collection("api_keys") as Collection - findResult = (await dbCollection.find({ key: this.apiKey.key }).toArray()) as ApiKey[] - } catch (e) { - console.log(e) - } - return findResult - } else { - return [{ message: "Database connection is NOT established" }] - } + const findResult = await prisma.apiKey.findMany({ + where: { key: this.apiKey.key } + }) + return findResult as unknown as ApiKey[] } catch (e) { console.error(e) + return [{ message: "Database operation failed" }] } } async create() { - let dbCollection: Collection - let isConnected = false - let client: MongoClient | undefined - try { - try { - client = await connectDB() - isConnected = true - } catch (e) { - console.log(e) - } - if (client && isConnected) { - let insertResult: InsertOneResult | undefined - try { - dbCollection = client.db(process.env.DB_NAME).collection("api_keys") - insertResult = await dbCollection.insertOne(this.apiKey) - } catch (e) { - console.log(e) + const insertResult = await prisma.apiKey.create({ + data: { + key: this.apiKey.key as string, + description: this.apiKey.description || "", + permission_group: this.apiKey.permission_group || "", + rate_limits: this.apiKey.rate_limits as any, + created_at: this.apiKey.created_at || new Date().toISOString() } + }) - if (insertResult && insertResult.insertedId) { - return { status: "success", message: "API Key has been generated.", keyId: insertResult.insertedId } - } else { - return { status: "failed", message: "Failed to create the api key." } - } + if (insertResult && insertResult.id) { + return { status: "success", message: "API Key has been generated.", keyId: insertResult.id } } else { - return [{ message: "Database connection is NOT established" }] + return { status: "failed", message: "Failed to create the api key." } } } catch (e) { console.error(e) + return { status: "failed", message: "Failed to create the api key." } } } async delete(id: string) { - let client: MongoClient | undefined - let dbCollection: Collection - let isConnected = false - try { - try { - client = await connectDB() - isConnected = true - } catch (e) { - console.log(e) - } - if (client && isConnected) { - let deleteResult: DeleteResult | undefined - try { - dbCollection = client.db(process.env.DB_NAME).collection("api_keys") - deleteResult = await dbCollection.deleteOne({ _id: new ObjectId(id) }) - } catch (e) { - console.error("Failed to delete API key:", e) //TODO: better error logging & displaying - } - if (deleteResult && deleteResult.deletedCount === 1) { - return { status: "success", message: "API Key has been removed." } - } else { - return { status: "failed", message: "Failed to delete the api key." } - } + const deleteResult = await prisma.apiKey.deleteMany({ + where: { id: id } + }) + + if (deleteResult && deleteResult.count === 1) { + return { status: "success", message: "API Key has been removed." } } else { - return [{ message: "Database connection is NOT established" }] + return { status: "failed", message: "Failed to delete the api key." } } } catch (e) { - console.error("An error occurred while deleting the API key:", e) //TODO: better error logging & displaying + console.error("An error occurred while deleting the API key:", e) + return { status: "failed", message: "An error occurred while deleting the API key." } } } } diff --git a/controllers/entry-type.controller.ts b/controllers/entry-type.controller.ts index 5b0f70d..47cd87d 100644 --- a/controllers/entry-type.controller.ts +++ b/controllers/entry-type.controller.ts @@ -1,6 +1,6 @@ //Database -import { connectDB } from "@/lib/mongodb" -import { Collection, InsertOneResult, MongoClient, ObjectId, UpdateResult } from "mongodb" +import { prisma } from "@/lib/prisma" +import { EntryType as PrismaEntryType, PermissionGroup, Prisma } from "@/prisma-client/client" //Interface import { EntryType } from "@/interfaces/entry_type" @@ -17,49 +17,60 @@ export class EntryTypeController { } async create() { - let client: MongoClient | undefined - let dbCollection: Collection + const client = prisma let isConnected = false try { - client = await connectDB(this.mockClient) isConnected = true } catch (e) { console.log(e) } if (client && isConnected) { - let insertResult: InsertOneResult | undefined - let addPrivileges: UpdateResult | undefined + let insertResult: PrismaEntryType | undefined + let addPrivileges: PermissionGroup | undefined try { - dbCollection = client.db(process.env.DB_NAME).collection("entry_types") - insertResult = await dbCollection.insertOne(this.entryType) + insertResult = await prisma.entryType.create({ + data: { + createdBy: this.entryType.createdBy, + created_at: this.entryType.created_at ? new Date(this.entryType.created_at).toISOString() : undefined, + fieldsets: this.entryType.fieldsets as any, + name: this.entryType.name, + namespace: this.entryType.namespace, + slug: this.entryType.slug, + status: this.entryType.status, + updated_at: this.entryType.updated_at ? new Date(this.entryType.updated_at).toISOString() : undefined + } + }) //add CRUD privileges to the user's permission group for this entry type - dbCollection = client.db(process.env.DB_NAME).collection("permission_groups") - const permGroupData = await dbCollection.findOne({ slug: this.entryType.createdBy }) + const permGroupData = await prisma.permissionGroup.findFirst({ + where: { slug: this.entryType.createdBy } + }) if (permGroupData && permGroupData.privileges) { - permGroupData.privileges.push({ + const privileges = permGroupData.privileges as any[] + privileges.push({ [this.entryType.namespace.split(" ").join("-").toLowerCase()]: { permissions: ["read", "update", "delete", "create"] } }) - addPrivileges = await dbCollection.updateOne( - { _id: new ObjectId(permGroupData._id) }, - { $set: permGroupData }, - { upsert: false } - ) + addPrivileges = await prisma.permissionGroup.update({ + where: { id: permGroupData.id }, + data: { + privileges: privileges + } + }) } - if (insertResult.insertedId && addPrivileges && addPrivileges.modifiedCount === 1) { + if (insertResult && insertResult.id && addPrivileges) { if (this.mockClient) { return { result: { status: "success", message: "Entry Type has been created." }, - entryTypeId: insertResult.insertedId + entryTypeId: insertResult.id } } return { status: "success", message: "Entry Type has been created." } - } else if (insertResult.insertedId && !addPrivileges) { + } else if (insertResult && insertResult.id && !addPrivileges) { // Entry type created but permission group not found or updated return { status: "failed", message: "Entry type created but failed to update permission group." } } else { @@ -75,50 +86,69 @@ export class EntryTypeController { } async update(id: string) { - let client: MongoClient | undefined - let dbCollection: Collection + const client = prisma let isConnected = false try { - client = await connectDB(this.mockClient) isConnected = true } catch (e) { console.log(e) } if (client && isConnected) { - let updateResult: UpdateResult | undefined - let addPrivileges: UpdateResult | undefined + let updateResult: PrismaEntryType | undefined + let addPrivileges: PermissionGroup | undefined + let isDifferent = false try { - dbCollection = client.db(process.env.DB_NAME).collection("entry_types") - const prevState = await dbCollection.findOne({ _id: new ObjectId(id) }) - - updateResult = await dbCollection.updateOne( - { _id: new ObjectId(id) }, - { $set: this.entryType }, - { upsert: false } - ) + const prevState = await prisma.entryType.findUnique({ + where: { id: id } + }) + + isDifferent = + prevState !== null && + (prevState.name !== this.entryType.name || + prevState.namespace !== this.entryType.namespace || + prevState.slug !== this.entryType.slug || + prevState.status !== this.entryType.status || + JSON.stringify(prevState.fieldsets) !== JSON.stringify(this.entryType.fieldsets)) + + updateResult = await prisma.entryType.update({ + where: { id: id }, + data: { + createdBy: this.entryType.createdBy, + created_at: this.entryType.created_at ? new Date(this.entryType.created_at).toISOString() : undefined, + fieldsets: this.entryType.fieldsets as any, + name: this.entryType.name, + namespace: this.entryType.namespace, + slug: this.entryType.slug, + status: this.entryType.status, + updated_at: this.entryType.updated_at + ? new Date(this.entryType.updated_at).toISOString() + : new Date().toISOString() + } + }) if (prevState && prevState.namespace !== this.entryType.namespace) { - //add CRUD privileges to the user's permission group for this entry type - dbCollection = client.db(process.env.DB_NAME).collection("permission_groups") - // Use createdBy from prevState if not provided in update payload const permGroupSlug = this.entryType.createdBy || prevState.createdBy if (permGroupSlug) { - const permGroupData = await dbCollection.findOne({ slug: permGroupSlug }) + const permGroupData = await prisma.permissionGroup.findFirst({ + where: { slug: permGroupSlug } + }) if (permGroupData && permGroupData.privileges) { - permGroupData.privileges.push({ + const privileges = permGroupData.privileges as any[] + privileges.push({ [this.entryType.namespace.split(" ").join("-").toLowerCase()]: { permissions: ["read", "update", "delete", "create"] } }) - addPrivileges = await dbCollection.updateOne( - { _id: new ObjectId(permGroupData._id) }, - { $set: permGroupData }, - { upsert: false } - ) + addPrivileges = await prisma.permissionGroup.update({ + where: { id: permGroupData.id }, + data: { + privileges: privileges + } + }) } } } @@ -129,17 +159,11 @@ export class EntryTypeController { // Check if namespace changed and privileges were updated const namespaceChanged = addPrivileges !== undefined - if ( - updateResult && - addPrivileges && - namespaceChanged && - updateResult.modifiedCount === 1 && - addPrivileges.modifiedCount === 1 - ) { + if (updateResult && addPrivileges && namespaceChanged && isDifferent) { return { status: "success", message: "Entry Type has been updated." } - } else if (updateResult && !namespaceChanged && updateResult.modifiedCount === 1) { + } else if (updateResult && !namespaceChanged && isDifferent) { return { status: "success", message: "Entry Type has been updated." } - } else if (updateResult && updateResult.matchedCount === 1 && updateResult.modifiedCount === 0) { + } else if (updateResult && !isDifferent) { return { status: "failed", message: "You didn't make any change." } } else { return { status: "failed", message: "Failed to update the entry type." } @@ -150,25 +174,24 @@ export class EntryTypeController { } async delete(id: string) { - let client: MongoClient | undefined - let dbCollection: Collection + const client = prisma let isConnected = false try { - client = await connectDB(this.mockClient) isConnected = true } catch (e) { console.log(e) } if (client && isConnected) { - let deleteResult + let deleteResult: Prisma.BatchPayload | undefined try { - dbCollection = client.db(process.env.DB_NAME).collection("entry_types") - deleteResult = await dbCollection.deleteOne({ namespace: id }) + deleteResult = await prisma.entryType.deleteMany({ + where: { namespace: id } + }) } catch (e) { console.log(e) } - if (deleteResult && deleteResult.deletedCount === 1) { + if (deleteResult && deleteResult.count >= 1) { return { status: "success", message: "Entry Type has been deleted." } } else { return { status: "failed", message: "Failed to delete the entry type." } diff --git a/controllers/entry.controller.ts b/controllers/entry.controller.ts index 10d3cd0..1d43c38 100644 --- a/controllers/entry.controller.ts +++ b/controllers/entry.controller.ts @@ -1,6 +1,6 @@ //Database -import { connectDB } from "@/lib/mongodb" -import { Collection, DeleteResult, InsertOneResult, MongoClient, ObjectId, UpdateResult } from "mongodb" +import { prisma } from "@/lib/prisma" +import { Entry as PrismaEntry, Prisma } from "@/prisma-client/client" //Interface import { Entry } from "@/interfaces/entry" @@ -14,26 +14,39 @@ export class EntryController { } async create() { - let client: MongoClient | undefined - let dbCollection: Collection + const client = prisma let isConnected = false try { - client = await connectDB(this.mockClient) isConnected = true } catch (e) { console.log(e) } if (client && isConnected) { - let insertResult: InsertOneResult + let insertResult: PrismaEntry | undefined try { - dbCollection = client.db(process.env.DB_NAME).collection("entries") - insertResult = await dbCollection.insertOne(this.entry) - if (insertResult.insertedId) { + const dataToInsert: Prisma.EntryCreateInput = { + data: this.entry.data as any, + name: this.entry.name, + namespace: this.entry.namespace, + slug: this.entry.slug, + status: this.entry.status, + updated_at: this.entry.updated_at ? new Date(this.entry.updated_at).toISOString() : new Date().toISOString() + } + + if (this.entry._id) { + dataToInsert.id = this.entry._id.toString() + } + + insertResult = await prisma.entry.create({ + data: dataToInsert + }) + + if (insertResult && insertResult.id) { if (this.mockClient) { return { result: { status: "success", message: "Entry has been created." }, - entryId: insertResult.insertedId + entryId: insertResult.id } } @@ -51,27 +64,48 @@ export class EntryController { } async update(id: string) { - let client: MongoClient | undefined - let dbCollection: Collection + const client = prisma let isConnected = false try { - client = await connectDB(this.mockClient) isConnected = true } catch (e) { console.log(e) } if (client && isConnected) { - let updateResult: UpdateResult | undefined + let updateResult: PrismaEntry | undefined + let isDifferent = false try { - dbCollection = client.db(process.env.DB_NAME).collection("entries") - updateResult = await dbCollection.updateOne({ _id: new ObjectId(id) }, { $set: this.entry }, { upsert: false }) + const prevState = await prisma.entry.findUnique({ + where: { id: id } + }) + + if (prevState) { + isDifferent = + prevState.name !== this.entry.name || + prevState.namespace !== this.entry.namespace || + prevState.slug !== this.entry.slug || + prevState.status !== this.entry.status || + JSON.stringify(prevState.data) !== JSON.stringify(this.entry.data) + } + + updateResult = await prisma.entry.update({ + where: { id: id }, + data: { + data: this.entry.data as any, + name: this.entry.name, + namespace: this.entry.namespace, + slug: this.entry.slug, + status: this.entry.status, + updated_at: this.entry.updated_at ? new Date(this.entry.updated_at).toISOString() : new Date().toISOString() + } + }) } catch (e) { console.log(e) } - if (updateResult && updateResult.modifiedCount === 1) { + if (updateResult && isDifferent) { return { status: "success", message: "Entry has been updated." } - } else if (updateResult && updateResult.matchedCount === 1 && updateResult.modifiedCount === 0) { + } else if (updateResult && !isDifferent) { return { status: "failed", message: "You didn't make any change." } } else { return { status: "failed", message: "Failed to update the entry." } @@ -82,25 +116,24 @@ export class EntryController { } async delete(id: string) { - let client: MongoClient | undefined - let dbCollection: Collection - let isConnected: boolean = false + const client = prisma + let isConnected = false try { - client = await connectDB(this.mockClient) isConnected = true } catch (e) { console.log(e) } if (client && isConnected) { - let deleteResult: DeleteResult | undefined + let deleteResult: Prisma.BatchPayload | undefined try { - dbCollection = client.db(process.env.DB_NAME).collection("entries") - deleteResult = await dbCollection.deleteOne({ _id: new ObjectId(id) }) + deleteResult = await prisma.entry.deleteMany({ + where: { id: id } + }) } catch (e) { console.log(e) } - if (deleteResult && deleteResult.deletedCount === 1) { + if (deleteResult && deleteResult.count === 1) { return { status: "success", message: "Entry has been deleted." } } else { return { status: "failed", message: "Failed to delete the entry." } diff --git a/controllers/permission-group.controller.ts b/controllers/permission-group.controller.ts index 6f7d685..a096178 100644 --- a/controllers/permission-group.controller.ts +++ b/controllers/permission-group.controller.ts @@ -1,9 +1,10 @@ //Database -import { connectDB } from "@/lib/mongodb" -import { ObjectId, MongoClient, Collection, UpdateResult, DeleteResult, InsertOneResult } from "mongodb" +import { prisma } from "@/lib/prisma" +import { PermissionGroup as PrismaPermissionGroup } from "@/prisma-client/client" //Interface import { PermissionGroup } from "@/interfaces/permission_group" + //=============================================== export class PermissionGroupController { @@ -16,28 +17,31 @@ export class PermissionGroupController { } async create() { - let client: MongoClient | undefined - let dbCollection: Collection + const client = prisma let isConnected = false try { - client = await connectDB(this.mockClient) isConnected = true } catch (e) { console.log(e) } if (client && isConnected) { - let insertResult: InsertOneResult + let insertResult: PrismaPermissionGroup | undefined try { - dbCollection = client.db(process.env.DB_NAME).collection("permission_groups") - insertResult = await dbCollection.insertOne(this.permissionGroup) + insertResult = await prisma.permissionGroup.create({ + data: { + name: this.permissionGroup.name, + slug: this.permissionGroup.slug, + privileges: this.permissionGroup.privileges as any + } + }) - if (insertResult.insertedId) { + if (insertResult && insertResult.id) { if (this.mockClient) { return { result: { status: "success", message: "Permission group has been created." }, - permGroupId: insertResult.insertedId + permGroupId: insertResult.id } } @@ -55,31 +59,43 @@ export class PermissionGroupController { } async update(id: string) { - let client: MongoClient | undefined - let dbCollection: Collection + const client = prisma let isConnected = false try { - client = await connectDB(this.mockClient) isConnected = true } catch (e) { console.log(e) } if (client && isConnected) { - let updateResult: UpdateResult | undefined + let updateResult: PrismaPermissionGroup | undefined + let isDifferent = false try { - dbCollection = client.db(process.env.DB_NAME).collection("permission_groups") - updateResult = await dbCollection.updateOne( - { _id: new ObjectId(id) }, - { $set: this.permissionGroup }, - { upsert: false } - ) + const prevState = await prisma.permissionGroup.findUnique({ + where: { id: id } + }) + + isDifferent = + prevState !== null && + (prevState.name !== this.permissionGroup.name || + prevState.slug !== this.permissionGroup.slug || + JSON.stringify(prevState.privileges) !== JSON.stringify(this.permissionGroup.privileges)) + + updateResult = await prisma.permissionGroup.update({ + where: { id: id }, + data: { + name: this.permissionGroup.name, + slug: this.permissionGroup.slug, + privileges: this.permissionGroup.privileges as any + } + }) } catch (e) { console.log(e) } - if (updateResult && updateResult["modifiedCount"] === 1) { + + if (updateResult && isDifferent) { return { status: "success", message: "Permission group has been updated." } - } else if (updateResult && updateResult["matchedCount"] === 1 && updateResult["modifiedCount"] === 0) { + } else if (updateResult && !isDifferent) { return { status: "failed", message: "You didn't make any change." } } else { return { status: "failed", message: "Failed to update the permission group." } @@ -90,25 +106,24 @@ export class PermissionGroupController { } async delete(id: string) { - let client: MongoClient | undefined - let dbCollection: Collection - let isConnected: boolean = false + const client = prisma + let isConnected = false try { - client = await connectDB(this.mockClient) isConnected = true } catch (e) { console.log(e) } if (client && isConnected) { - let deleteResult: DeleteResult | undefined + let deleteResult: { count: number } | undefined try { - dbCollection = client.db(process.env.DB_NAME).collection("permission_groups") - deleteResult = await dbCollection.deleteOne({ _id: new ObjectId(id) }) + deleteResult = await prisma.permissionGroup.deleteMany({ + where: { id: id } + }) } catch (e) { console.log(e) } - if (deleteResult && deleteResult.deletedCount === 1) { + if (deleteResult && deleteResult.count >= 1) { return { status: "success", message: "Permission group has been deleted." } } else { return { status: "failed", message: "Failed to delete the permission group." } diff --git a/controllers/user.controller.ts b/controllers/user.controller.ts index 05a41e6..963169f 100644 --- a/controllers/user.controller.ts +++ b/controllers/user.controller.ts @@ -1,5 +1,6 @@ -import { connectDB } from "@/lib/mongodb" -import { ObjectId, MongoClient, Collection, UpdateResult, DeleteResult, InsertOneResult } from "mongodb" +//Database +import { prisma } from "@/lib/prisma" +import { User as PrismaUser } from "@/prisma-client/client" import bcrypt from "bcryptjs" //Interface @@ -15,28 +16,42 @@ export class UserController { } async create() { - let client: MongoClient | undefined - let dbCollection: Collection + const client = prisma let isConnected = false try { - client = await connectDB(this.mockClient) isConnected = true } catch (e) { console.log(e) } if (client && isConnected) { - let insertResult: InsertOneResult + let insertResult: PrismaUser | undefined try { const plainPw = this.user.password const hashPw = bcrypt.hashSync(plainPw, 8) - dbCollection = client.db(process.env.DB_NAME).collection("users") - insertResult = await dbCollection.insertOne({ ...this.user, password: hashPw }) + insertResult = await prisma.user.create({ + data: { + email: this.user.email, + fullname: this.user.fullname, + password: hashPw, + permission_group: this.user.permission_group, + profile_img: this.user.profile_img || "", + status: this.user.status, + username: this.user.username, + email_verified: this.user.email_verified || false, + oauth_id: this.user.oauth_id, + oauth_provider: this.user.oauth_provider, + created_at: this.user.created_at, + created_by: this.user.created_by, + updated_at: this.user.updated_at, + updated_by: this.user.updated_by + } + }) - if (insertResult.insertedId) { + if (insertResult && insertResult.id) { if (this.mockClient) { - return { result: { status: "success", message: "User has been created." }, userId: insertResult.insertedId } + return { result: { status: "success", message: "User has been created." }, userId: insertResult.id } } return { status: "success", message: "User has been created." } @@ -53,41 +68,68 @@ export class UserController { } async update(id: string) { - let client: MongoClient | undefined - let dbCollection: Collection + const client = prisma let isConnected = false try { - client = await connectDB(this.mockClient) isConnected = true } catch (e) { console.log(e) } if (client && isConnected) { - let updateResult: UpdateResult | undefined + let updateResult: PrismaUser | undefined + let isDifferent = false try { - const setObj = { - ...this.user + const prevState = await prisma.user.findUnique({ + where: { id: id } + }) + + const hasPasswordChange = + this.user.password !== "" && + this.user.password !== undefined && + this.user.password !== null && + prevState?.password !== this.user.password + + isDifferent = + prevState !== null && + (prevState.fullname !== this.user.fullname || + prevState.username !== this.user.username || + prevState.email !== this.user.email || + prevState.profile_img !== this.user.profile_img || + prevState.status !== this.user.status || + prevState.permission_group !== this.user.permission_group || + prevState.oauth_id !== this.user.oauth_id || + prevState.oauth_provider !== this.user.oauth_provider || + hasPasswordChange) + + const updateData: any = { + fullname: this.user.fullname, + username: this.user.username, + email: this.user.email, + profile_img: this.user.profile_img || "", + status: this.user.status, + permission_group: this.user.permission_group, + oauth_id: this.user.oauth_id, + oauth_provider: this.user.oauth_provider, + updated_at: new Date().toISOString() } + if (this.user.password !== "" && this.user.password !== undefined && this.user.password !== null) { - setObj.password = this.user.password + updateData.password = this.user.password } - dbCollection = client.db(process.env.DB_NAME).collection("users") - updateResult = await dbCollection.updateOne( - { _id: new ObjectId(id) }, - { - $set: setObj - }, - { upsert: false } - ) + + updateResult = await prisma.user.update({ + where: { id: id }, + data: updateData + }) } catch (e) { console.log(e) } - if (updateResult && updateResult["matchedCount"] === 1 && updateResult["modifiedCount"] === 1) { + if (updateResult && isDifferent) { return { status: "success", message: "User has been updated." } - } else if (updateResult && updateResult["matchedCount"] === 1 && updateResult["modifiedCount"] === 0) { + } else if (updateResult && !isDifferent) { return { status: "failed", message: "You didn't make any change." } } else { return { status: "failed", message: "Failed to update the user." } @@ -98,26 +140,25 @@ export class UserController { } async delete(id: string) { - let client: MongoClient | undefined - let dbCollection: Collection - let isConnected: boolean = false + const client = prisma + let isConnected = false try { - client = await connectDB(this.mockClient) isConnected = true } catch (e) { console.log(e) } if (client && isConnected) { - let deleteResult: DeleteResult | undefined + let deleteResult: { count: number } | undefined try { - dbCollection = client.db(process.env.DB_NAME).collection("users") - deleteResult = await dbCollection.deleteOne({ _id: new ObjectId(id) }) + deleteResult = await prisma.user.deleteMany({ + where: { id: id } + }) } catch (e) { console.log(e) } - if (deleteResult && deleteResult.deletedCount === 1) { + if (deleteResult && deleteResult.count >= 1) { return { status: "success", message: "User has been deleted." } } else { return { status: "failed", message: "Failed to delete the user." } diff --git a/interfaces/index.ts b/interfaces/index.ts index 1fb076a..fb0e444 100644 --- a/interfaces/index.ts +++ b/interfaces/index.ts @@ -5,6 +5,7 @@ import { SetupFormSchema } from "@/lib/schemas/client/form-schemas" import { PermissionGroup } from "@/interfaces/permission_group" import { EntryType } from "@/interfaces/entry_type" export type { Entry } from "@/interfaces/entry" +export type { PermissionGroup, EntryType } enum FindLike { StartsWith, diff --git a/jest.config.ts b/jest.config.ts index e039889..047af1b 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -12,7 +12,10 @@ const config: Config = { testEnvironment: "jsdom", // Add more setup options before each test is run setupFilesAfterEnv: ["/jest.setup.ts"], - preset: "ts-jest" + preset: "ts-jest", + moduleNameMapper: { + "^@/(.*)$": "/$1" + } } // createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async diff --git a/jest.setup.ts b/jest.setup.ts index 8ce5e21..6054a19 100644 --- a/jest.setup.ts +++ b/jest.setup.ts @@ -1,4 +1,12 @@ import "@testing-library/jest-dom" import { TextEncoder, TextDecoder } from "util" -Object.assign(global, { TextDecoder, TextEncoder }) +Object.assign(global, { + TextDecoder, + TextEncoder, + Request: globalThis.Request, + Response: globalThis.Response, + Headers: globalThis.Headers, + fetch: globalThis.fetch +}) + diff --git a/lib/__mocks__/error.ts b/lib/__mocks__/error.ts new file mode 100644 index 0000000..d166d98 --- /dev/null +++ b/lib/__mocks__/error.ts @@ -0,0 +1,8 @@ +const handleError = jest.fn((error) => ({ + success: false, + error: { + message: error instanceof Error ? error.message : String(error), + }, +})); + +export default handleError; diff --git a/lib/__mocks__/get-session.ts b/lib/__mocks__/get-session.ts new file mode 100644 index 0000000..e56407b --- /dev/null +++ b/lib/__mocks__/get-session.ts @@ -0,0 +1,7 @@ +export const getPermissionGroup = jest.fn(); +export const getServerSession = jest.fn(); +export const requireAuth = jest.fn(); +export const getServerSessionWithUser = jest.fn(); +export const hasPermission = jest.fn(); +export const isAuthenticated = jest.fn(); +export const getUserId = jest.fn(); diff --git a/lib/__mocks__/next-cache.ts b/lib/__mocks__/next-cache.ts new file mode 100644 index 0000000..3265da6 --- /dev/null +++ b/lib/__mocks__/next-cache.ts @@ -0,0 +1,2 @@ +export const revalidatePath = jest.fn(); +export const revalidateTag = jest.fn(); diff --git a/lib/__mocks__/prisma-actions.ts b/lib/__mocks__/prisma-actions.ts new file mode 100644 index 0000000..8fe5bc7 --- /dev/null +++ b/lib/__mocks__/prisma-actions.ts @@ -0,0 +1,27 @@ +export const prisma = { + permissionGroup: { + create: jest.fn(), + delete: jest.fn(), + findMany: jest.fn(), + findUnique: jest.fn(), + update: jest.fn(), + updateMany: jest.fn(), + }, + apiRequestLog: { + findMany: jest.fn(), + aggregate: jest.fn(), + count: jest.fn(), + groupBy: jest.fn(), + aggregateRaw: jest.fn(), + }, + entry: { + count: jest.fn(), + }, + entryType: { + count: jest.fn(), + }, + user: { + count: jest.fn(), + }, + $transaction: jest.fn((promises) => Promise.all(promises)), +}; diff --git a/lib/__mocks__/prisma.ts b/lib/__mocks__/prisma.ts new file mode 100644 index 0000000..7f0ff20 --- /dev/null +++ b/lib/__mocks__/prisma.ts @@ -0,0 +1,55 @@ +// Mock Prisma client operations to run queries completely in-memory +export const prisma = { + entryType: { + create: jest.fn(), + findUnique: jest.fn(), + findFirst: jest.fn(), + findMany: jest.fn(), + update: jest.fn(), + deleteMany: jest.fn() + }, + entry: { + create: jest.fn(), + findUnique: jest.fn(), + findFirst: jest.fn(), + findMany: jest.fn(), + update: jest.fn(), + deleteMany: jest.fn() + }, + permissionGroup: { + create: jest.fn(), + findFirst: jest.fn(), + findUnique: jest.fn(), + findMany: jest.fn(), + update: jest.fn(), + deleteMany: jest.fn() + }, + user: { + create: jest.fn(), + findUnique: jest.fn(), + findFirst: jest.fn(), + findMany: jest.fn(), + update: jest.fn(), + deleteMany: jest.fn() + }, + apiKey: { + create: jest.fn(), + findUnique: jest.fn(), + findFirst: jest.fn(), + findMany: jest.fn(), + update: jest.fn(), + delete: jest.fn(), + deleteMany: jest.fn() + }, + settings: { + findFirst: jest.fn(), + findMany: jest.fn(), + findFirstOrThrow: jest.fn(), + update: jest.fn() + }, + verificationToken: { + create: jest.fn(), + findUnique: jest.fn(), + delete: jest.fn() + } +} diff --git a/lib/actions/auth/email-verification.ts b/lib/actions/auth/email-verification.ts index 21c29b9..20beb20 100644 --- a/lib/actions/auth/email-verification.ts +++ b/lib/actions/auth/email-verification.ts @@ -8,9 +8,6 @@ import { ActionResponse, EmailVerification } from "@/interfaces" import handleError from "@/lib/handlers/error" import verifyUser from "@/lib/actions/studio/users/verify-user" -// Mock database for demonstration - in production, use MongoDB/Prisma -// const verificationCodes = new Map(); - export async function EmailVerificationAction(email: string): Promise { try { const code = crypto.randomInt(100000, 999999).toString() @@ -44,35 +41,39 @@ export async function EmailVerificationAction(email: string): Promise { - const storedCode = await prisma.verificationToken.findUnique({ - where: { token: inputCode, identifier: email } - }) + try { + const storedCode = await prisma.verificationToken.findUnique({ + where: { token: inputCode, identifier: email } + }) - if (!storedCode || storedCode.expires < new Date()) { - return { - success: false, - status: 400, - error: { message: "Invalid or expired verification code." } + if (!storedCode || storedCode.expires < new Date()) { + return { + success: false, + status: 400, + error: { message: "Invalid or expired verification code." } + } } - } - const responseMarkVerified = await verifyUser(email, true) + const responseMarkVerified = await verifyUser(email, true) - if (!responseMarkVerified.success) { - return { - success: false, - status: 500, - error: { message: "Cannot verify user due to an error." } + if (!responseMarkVerified.success) { + return { + success: false, + status: 500, + error: { message: "Cannot verify user due to an error." } + } } - } - const deleteCode = await prisma.verificationToken.delete({ - where: { token: inputCode, identifier: email } - }) + const deleteCode = await prisma.verificationToken.delete({ + where: { token: inputCode, identifier: email } + }) + + if (!deleteCode) { + return handleError(new Error("Failed to delete verification code"), "server") + } - if (!deleteCode) { - return handleError(new Error("Failed to delete verification code"), "server") + return { success: true, status: 200, data: "User verified successfully." } + } catch (error) { + return handleError(error, "server") } - - return { success: true, status: 200, data: "User verified successfully." } } diff --git a/lib/actions/auth/has-permission.ts b/lib/actions/auth/has-permission.ts index f872092..460e329 100644 --- a/lib/actions/auth/has-permission.ts +++ b/lib/actions/auth/has-permission.ts @@ -2,8 +2,7 @@ import { auth } from "@/auth" import { DbPrivilege } from "@/interfaces" -import { PermissionGroup } from "@/interfaces/permission_group" -import { connectDB } from "@/lib/mongodb" +import { prisma } from "@/lib/prisma" /** * Server action to check if the current user has a specific permission. @@ -19,14 +18,12 @@ export async function hasPermission(requiredPermission: string): Promise g.slug === userGroupName) if (!group || !group.privileges) { return false } diff --git a/lib/actions/studio/permission-groups/get-permission-groups.ts b/lib/actions/studio/permission-groups/get-permission-groups.ts index 7f052b4..c9e6152 100644 --- a/lib/actions/studio/permission-groups/get-permission-groups.ts +++ b/lib/actions/studio/permission-groups/get-permission-groups.ts @@ -4,7 +4,7 @@ import handleError from "@/lib/handlers/error" import { ActionResponse, ErrorResponse, SuccessResponse } from "@/interfaces" import { PermissionGroup } from "@/interfaces/permission_group" import { getPermissionGroup } from "@/lib/auth/get-session" -import { connectDB } from "@/lib/mongodb" +import { prisma } from "@/lib/prisma" export default async function getPermissionGroups( isCheckingApiKeyPermGroup?: boolean @@ -15,30 +15,23 @@ export default async function getPermissionGroups( const perm_group = await getPermissionGroup() if (!perm_group) { - return handleError(new Error("Unauthorized to delete entry type"), "server") + return handleError(new Error("Unauthorized to fetch permission groups"), "server") } } - // 1. Establish direct database connection - const client = await connectDB() - const db = client.db(process.env.DB_NAME) - // 2. Query the permission_groups collection - // We filter out the 'root' group to match the logic previously handled by the API route - const groups = await db - .collection("permission_groups") - .find({ slug: { $ne: "root" } }) - .toArray() + const groups = await prisma.permissionGroup.findMany({ + where: { + slug: { + not: "root" + } + } + }) - // 3. Format the data to match the PermissionGroup interface (convert ObjectId to string) const formattedGroups = groups.map((group) => ({ ...group, - _id: group._id.toString() + _id: group.id })) as unknown as PermissionGroup[] - if (!groups) { - return handleError(new Error("Failed to fetch permission groups"), "server") - } - return { success: true, status: 200, diff --git a/lib/actions/studio/permission-groups/update-permission-groups.ts b/lib/actions/studio/permission-groups/update-permission-groups.ts index bab5eac..e8de31b 100644 --- a/lib/actions/studio/permission-groups/update-permission-groups.ts +++ b/lib/actions/studio/permission-groups/update-permission-groups.ts @@ -1,18 +1,16 @@ "use server" -import { connectDB } from "@/lib/mongodb" -import { MongoClient, Collection } from "mongodb" import handleError from "@/lib/handlers/error" import { ActionResponse, SuccessResponse } from "@/interfaces" import { PermissionGroupFormSchema } from "@/lib/schemas/client/form-schemas" import { z } from "zod" import { permGroupsFormToDb } from "@/lib/form-to-db" import { getPermissionGroup } from "@/lib/auth/get-session" +import { prisma } from "@/lib/prisma" export default async function updatePermissionGroups( formValues: Pick, "privileges"> ): Promise> { - let client: MongoClient | null = null try { // 1. Authorization Check const session_perm_group = await getPermissionGroup() @@ -27,34 +25,25 @@ export default async function updatePermissionGroups( return { success: true, status: 200, data: { message: "No changes to update." } } } - // 3. Database connection - client = await connectDB() - const db = client.db(process.env.DB_NAME) - const collection: Collection = db.collection("permission_groups") + // 3. Execute Updates via Transaction + const updatePromises = slugs.map((slug) => + prisma.permissionGroup.updateMany({ + where: { slug }, + data: { + privileges: groupsToUpdate[slug] as any + } + }) + ) - // 4. Prepare Bulk Operations - const bulkOps = slugs.map((slug) => ({ - updateOne: { - filter: { slug: slug }, - update: { - $set: { - privileges: groupsToUpdate[slug], - updated_at: new Date().toISOString() - } - }, - upsert: false - } - })) - - // 5. Execute Bulk Write - const result = await collection.bulkWrite(bulkOps) + const results = await prisma.$transaction(updatePromises) + const modifiedCount = results.reduce((acc, curr) => acc + curr.count, 0) return { success: true, status: 200, data: { - matchedCount: result.matchedCount, - modifiedCount: result.modifiedCount + matchedCount: slugs.length, + modifiedCount: modifiedCount } } as SuccessResponse } catch (error) { diff --git a/lib/actions/studio/settings/delete-api-key.ts b/lib/actions/studio/settings/delete-api-key.ts index d4be806..e6a5a3f 100644 --- a/lib/actions/studio/settings/delete-api-key.ts +++ b/lib/actions/studio/settings/delete-api-key.ts @@ -1,34 +1,26 @@ "use server" -import { connectDB } from "@/lib/mongodb" import { getPermissionGroup } from "@/lib/auth/get-session" import handleError from "@/lib/handlers/error" -import { SuccessResponse, ErrorResponse } from "@/interfaces" -import { ObjectId } from "mongodb" +import { SuccessResponse, ActionResponse } from "@/interfaces" import { revalidatePath } from "next/cache" +import { prisma } from "@/lib/prisma" /** * Server Action: deleteApiKey - * Deletes an API key from MongoDB by its ID. + * Deletes an API key from database by its ID. */ -export default async function deleteApiKey(id: string): Promise | ErrorResponse> { +export default async function deleteApiKey(id: string): Promise> { try { const permGroup = await getPermissionGroup() if (!permGroup) { - return handleError(new Error("Unauthorized to delete API keys")) as ErrorResponse + return handleError(new Error("Unauthorized to delete API keys"), "server") } - const client = await connectDB() - const db = client.db() - - const result = await db.collection("api_keys").deleteOne({ - _id: new ObjectId(id) + await prisma.apiKey.delete({ + where: { id } }) - if (result.deletedCount === 0) { - return handleError(new Error("API key not found or already deleted")) as ErrorResponse - } - revalidatePath("/studio/settings") return { @@ -37,6 +29,6 @@ export default async function deleteApiKey(id: string): Promise } catch (error) { - return handleError(error) as ErrorResponse + return handleError(error, "server") } } diff --git a/lib/actions/studio/settings/generate-api-key.ts b/lib/actions/studio/settings/generate-api-key.ts index 05db9ae..9db3ba3 100644 --- a/lib/actions/studio/settings/generate-api-key.ts +++ b/lib/actions/studio/settings/generate-api-key.ts @@ -5,7 +5,7 @@ import handleError from "@/lib/handlers/error" import { ApiKeyFormSchema } from "@/lib/schemas/client/form-schemas" import { ActionResponse, ApiKey, SuccessResponse } from "@/interfaces" import { z } from "zod" -import { connectDB } from "@/lib/mongodb" +import { prisma } from "@/lib/prisma" import { uid } from "uid" export default async function generateApiKey( @@ -20,19 +20,15 @@ export default async function generateApiKey( const validated = ApiKeyFormSchema.parse(formValues) const key = uid(32) // Generates 32 chars - const client = await connectDB() - const db = client.db() // Uses default db from connection string - - const newKey = { - key, - ...validated, - created_at: new Date().toISOString() - } + const newKey = await prisma.apiKey.create({ + data: { + key, + description: validated.description, + permission_group: validated.permission_group, + rate_limits: validated.rate_limits, + } + }) - const result = await db.collection("api_keys").insertOne(newKey) - if (!result.insertedId) { - return handleError(new Error("Failed to generate API key"), "server") - } return { success: true, data: { diff --git a/lib/actions/studio/settings/get-all-api-keys.ts b/lib/actions/studio/settings/get-all-api-keys.ts index ba8cee3..9d3a69c 100644 --- a/lib/actions/studio/settings/get-all-api-keys.ts +++ b/lib/actions/studio/settings/get-all-api-keys.ts @@ -1,43 +1,31 @@ "use server" -import { connectDB } from "@/lib/mongodb" import { getPermissionGroup } from "@/lib/auth/get-session" import handleError from "@/lib/handlers/error" -import { ApiKey, SuccessResponse, ErrorResponse } from "@/interfaces" +import { ApiKey, SuccessResponse, ActionResponse } from "@/interfaces" +import { prisma } from "@/lib/prisma" /** * Server Action: getAllApiKeys - * Fetches all API keys directly from MongoDB. + * Fetches all API keys directly from database. */ -export default async function getAllApiKeys(): Promise | ErrorResponse> { +export default async function getAllApiKeys(): Promise> { try { - // 1. Check permissions const permGroup = await getPermissionGroup() if (!permGroup) { - return handleError(new Error("Unauthorized to access API keys")) as ErrorResponse + return handleError(new Error("Unauthorized to access API keys"), "server") } - // 2. Connect to MongoDB - const client = await connectDB() - const db = client.db(process.env.DB_NAME) + const apiKeys = await prisma.apiKey.findMany({ + orderBy: { created_at: "desc" } + }) - // 3. Fetch all API keys from the collection - const apiKeysRaw = await db.collection("api_keys").find({}).sort({ created_at: -1 }).toArray() // Fixed typo from .toJ() to .toArray() - - // 4. Convert _id to plain string (Next.js Server Actions requirement) - const apiKeys = apiKeysRaw.map((key) => ({ - ...key, - _id: key._id.toString() - })) - - // 4. Return success response return { success: true, status: 200, - data: apiKeys - } as SuccessResponse + data: apiKeys as any + } as SuccessResponse } catch (error) { - // 5. Centralized error handling - return handleError(error) as ErrorResponse + return handleError(error, "server") } } diff --git a/lib/actions/studio/settings/get-all-settings.ts b/lib/actions/studio/settings/get-all-settings.ts index 017be1d..06827a8 100644 --- a/lib/actions/studio/settings/get-all-settings.ts +++ b/lib/actions/studio/settings/get-all-settings.ts @@ -1,7 +1,7 @@ "use server" import { getPermissionGroup } from "@/lib/auth/get-session" import handleError from "@/lib/handlers/error" -import { ActionResponse, ErrorResponse, SuccessResponse } from "@/interfaces" +import { ActionResponse, SuccessResponse } from "@/interfaces" import { prisma } from "@/lib/prisma" import { SettingsSchema } from "@/interfaces/settings" @@ -23,6 +23,6 @@ export default async function getAllSettings(): Promise } catch (error) { - return handleError(error) as ErrorResponse + return handleError(error, "server") } } diff --git a/lib/actions/studio/settings/get-api-key-info.ts b/lib/actions/studio/settings/get-api-key-info.ts index 0511dc9..3a615d1 100644 --- a/lib/actions/studio/settings/get-api-key-info.ts +++ b/lib/actions/studio/settings/get-api-key-info.ts @@ -1,21 +1,20 @@ "use server" -import { connectDB } from "@/lib/mongodb" import { ApiKey, ActionResponse, ErrorResponse } from "@/interfaces" import handleError from "@/lib/handlers/error" +import { prisma } from "@/lib/prisma" /** - * Retrieves information for a specific API key from MongoDB. + * Retrieves information for a specific API key from database. * * @param apiKey - The plain text API key string to look up. * @returns An ActionResponse containing the ApiKey data if found. */ export async function getApiKeyInfo(apiKey: string): Promise> { try { - const client = await connectDB() - const db = client.db() - - const apiKeyDoc = await db.collection("api_keys").findOne({ key: apiKey }) + const apiKeyDoc = await prisma.apiKey.findFirst({ + where: { key: apiKey } + }) if (!apiKeyDoc) { return { @@ -30,14 +29,7 @@ export async function getApiKeyInfo(apiKey: string): Promise> { try { // Check permission first. const perm_group = await getPermissionGroup() if (!perm_group) { - return handleError(new Error("Unauthorized to get settings")) + return handleError(new Error("Unauthorized to get settings"), "server") } const data = await prisma.settings.findFirstOrThrow({ @@ -20,11 +20,11 @@ export default async function getSettingsByName(name: string) { if (data === null || data.name !== name) { const unhandledError = new Error("Failed to get settings") - return handleError(unhandledError) + return handleError(unhandledError, "server") } return { success: true, status: 200, data } as SuccessResponse } catch (error) { - return handleError(error) as ErrorResponse + return handleError(error, "server") } } diff --git a/lib/actions/studio/stats/get-overview-stats.ts b/lib/actions/studio/stats/get-overview-stats.ts index 7cda67e..08ea99a 100644 --- a/lib/actions/studio/stats/get-overview-stats.ts +++ b/lib/actions/studio/stats/get-overview-stats.ts @@ -1,8 +1,6 @@ "use server" import { prisma } from "@/lib/prisma" -import { connectDB } from "@/lib/mongodb" -import { MongoClient } from "mongodb" import handleError from "@/lib/handlers/error" import { ActionResponse } from "@/interfaces" export type OverviewStats = { @@ -23,15 +21,11 @@ export type OverviewStats = { } export default async function getOverviewStats(): Promise> { - let client: MongoClient | null = null try { - client = await connectDB() - const db = client.db(process.env.DB_NAME) - const totalRequests = await prisma.apiRequestLog.count() - const totalEntries = await db.collection("entries").countDocuments() - const totalEntryTypes = await db.collection("entry_types").countDocuments() - const totalActiveUsers = await db.collection("users").countDocuments({ status: "active" }) + const totalEntries = await prisma.entry.count() + const totalEntryTypes = await prisma.entryType.count() + const totalActiveUsers = await prisma.user.count({ where: { status: "active" } }) const oneMonthAgo = new Date() oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1) @@ -39,9 +33,13 @@ export default async function getOverviewStats(): Promise | undefined -if (!dbUri) throw new Error("Please define the MongoDB Connection String inside .env file") -//if(!dbName) throw new Error('Please define the MongoDB database name inside .env file') - -export async function connectDB(mockClient?: boolean): Promise { - /*const client: MongoClient = await MongoClient.connect(dbUri) - clientPromise = (await client.connect()) as MongoClient - return clientPromise*/ - - if (clientPromise) return clientPromise - if (mockClient) dbUri = `${process.env.TEST_MONGODB_CONNECTION_STRING}` - - const client: MongoClient = new MongoClient(dbUri) - clientPromise = client.connect() - return clientPromise -} - -export default clientPromise diff --git a/package-lock.json b/package-lock.json index 227262d..e5f0e94 100644 --- a/package-lock.json +++ b/package-lock.json @@ -74,6 +74,7 @@ "jest-environment-jsdom": "^30.4.1", "next-devtools-mcp": "^0.4.0", "next-router-mock": "^1.0.5", + "node-mocks-http": "^1.18.1", "postcss": "^8.5.16", "prettier": "^3.9.4", "prettier-eslint": "^17.1.1", @@ -15805,6 +15806,16 @@ "node": ">= 8" } }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -16485,6 +16496,143 @@ "dev": true, "license": "MIT" }, + "node_modules/node-mocks-http": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/node-mocks-http/-/node-mocks-http-1.18.1.tgz", + "integrity": "sha512-hPMOLJZzhgT4i/zbYpfy1P2ulAlHtzFcrEGxPh/4pDRegCY3+p2sxDhQm1fxte5EgI/RwDx+NNW7v8kJcyxtMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "^1.3.7", + "content-disposition": "^0.5.3", + "depd": "^1.1.0", + "fresh": "^0.5.2", + "merge-descriptors": "^1.0.1", + "methods": "^1.1.2", + "mime": "^1.3.4", + "range-parser": "^1.2.0", + "type-is": "^1.6.18" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@types/express": "^4.17.21 || ^5.0.0", + "@types/node": "*" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + }, + "@types/node": { + "optional": true + } + } + }, + "node_modules/node-mocks-http/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-mocks-http/node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-mocks-http/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-mocks-http/node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-mocks-http/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-mocks-http/node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/node-mocks-http/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/node-mocks-http/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-mocks-http/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/node-releases": { "version": "2.0.50", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.50.tgz", diff --git a/package.json b/package.json index 0d1613c..0ec0018 100644 --- a/package.json +++ b/package.json @@ -84,6 +84,7 @@ "jest-environment-jsdom": "^30.4.1", "next-devtools-mcp": "^0.4.0", "next-router-mock": "^1.0.5", + "node-mocks-http": "^1.18.1", "postcss": "^8.5.16", "prettier": "^3.9.4", "prettier-eslint": "^17.1.1", diff --git a/pages/api/v1/entry-type/delete/[slug].ts b/pages/api/v1/entry-type/delete/[slug].ts index d1542e8..ba0f56d 100644 --- a/pages/api/v1/entry-type/delete/[slug].ts +++ b/pages/api/v1/entry-type/delete/[slug].ts @@ -32,8 +32,8 @@ async function handler(_req: NextApiRequest, res: NextApiResponse) { namespace: "", slug: "", createdBy: "", - created_at: new Date().toISOString(), - updated_at: new Date().toISOString(), + created_at: new Date(), + updated_at: new Date(), fieldsets: [], status: PublishStatus.Draft } as EntryType diff --git a/pages/api/v1/entry/delete/[slug].ts b/pages/api/v1/entry/delete/[slug].ts index 5ccf3d5..20ceb51 100644 --- a/pages/api/v1/entry/delete/[slug].ts +++ b/pages/api/v1/entry/delete/[slug].ts @@ -30,8 +30,8 @@ async function handler(_req: NextApiRequest, res: NextApiResponse) { slug: "", status: "Draft", data: {}, - created_at: new Date().toISOString(), - updated_at: new Date().toISOString() + created_at: new Date(), + updated_at: new Date() } as Entry const EntryData = new EntryController(dummyObj, mockclient === "true") const result = await EntryData.delete(slug as string) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 75b2d09..b57c8db 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -41,24 +41,27 @@ model ApiRequestLog { } model ApiKey { - id String @id @default(auto()) @map("_id") @db.ObjectId - key String + id String @id @default(auto()) @map("_id") @db.ObjectId + key String @unique + description String + permission_group String + rate_limits Json + created_at DateTime @default(now()) @@map("api_keys") - @@ignore } model Entry { - id String @id @default(auto()) @map("_id") @db.ObjectId + id String @id @default(auto()) @map("_id") @db.ObjectId data Json name String namespace String slug String status String + created_at String? updated_at String @@map("entries") - @@ignore } model EntryType { @@ -73,7 +76,6 @@ model EntryType { updated_at String? @@map("entry_types") - @@ignore } model PermissionGroup { @@ -83,7 +85,6 @@ model PermissionGroup { slug String @@map("permission_groups") - @@ignore } model User { @@ -96,6 +97,8 @@ model User { status String username String email_verified Boolean + oauth_id String? + oauth_provider String? created_at String? created_by String? updated_at String? diff --git a/tsconfig.json b/tsconfig.json index 3648892..c06500d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -24,7 +24,8 @@ ], "paths": { "@/*": ["./*"] - } + }, + "types": ["jest", "node"] }, "include": [ "next-env.d.ts", @@ -34,5 +35,12 @@ ".next/types/**/*.ts", ".next/dev/types/**/*.ts" ], - "exclude": ["node_modules", "__tests__", "__mocks__", "cypress"] + "exclude": [ + "node_modules", + "__tests__/lib/*.test.ts", + "__tests__/integration/**/*", + "__mocks__", + "cypress", + "cypress.config.ts" + ] }