diff --git a/jest.config.ts b/jest.config.ts index b250050d..267d69c2 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -9,6 +9,7 @@ const createJestConfig = nextJest({ const customJestConfig = { setupFilesAfterEnv: ["/jest.setup.js"], testEnvironment: "jest-environment-jsdom", + testTimeout: 20000, }; process.env = { diff --git a/space-configs/images/space_test_400x400.png b/space-configs/images/space_test_400x400.png new file mode 100644 index 00000000..88342767 Binary files /dev/null and b/space-configs/images/space_test_400x400.png differ diff --git a/src/app/api/zk-badge/image/[tokenId]/mocks.ts b/src/app/api/zk-badge/image/[tokenId]/mocks.ts new file mode 100644 index 00000000..40144462 --- /dev/null +++ b/src/app/api/zk-badge/image/[tokenId]/mocks.ts @@ -0,0 +1,80 @@ +import { SpaceConfig } from "@/space-configs/types"; +import { Network } from "@/src/libs/contracts/networks"; +import { AuthType } from "@sismo-core/sismo-connect-server"; + +export const spaceMockWithZkBadge: SpaceConfig = { + metadata: { + slug: "space", + name: "Space", + description: "Space description", + image: "space.png", + socialLinks: [ + { + type: "link", + link: "https://www.space.com", + }, + ], + }, + apps: [ + { + type: "zkBadge", + metadata: { + name: "ZK Badge name", + slug: "sismo-zk-badge-slug", + description: "Zk badge test description", + tags: ["Badge"], + image: "image.png", + createdAt: new Date("2023-07-03T18:00"), + }, + sismoConnectRequest: { + appId: "0x4", + authRequests: [{ authType: AuthType.VAULT }], + claimRequests: [{ groupId: "0x04" }], + }, + templateConfig: { + step2CtaText: "zkBadge step2CtaText", + tokenId: "40000001", + badgeMetadata: { + name: "Badge name", + description: "Badge description", + image: "space_test_400x400.png", + }, + chains: [ + { + name: Network.Gnosis, + }, + ], + }, + }, + { + type: "zkBadge", + metadata: { + name: "ZK Badge name not existing", + slug: "sismo-zk-badge-slug-not-existing", + description: "Zk badge test description", + tags: ["Badge"], + image: "image.png", + createdAt: new Date("2023-07-03T18:00"), + }, + sismoConnectRequest: { + appId: "0x4", + authRequests: [{ authType: AuthType.VAULT }], + claimRequests: [{ groupId: "0x04" }], + }, + templateConfig: { + step2CtaText: "zkBadge step2CtaText", + tokenId: "40000002", + badgeMetadata: { + name: "Badge name", + description: "Badge description", + image: "un_existing_image_400x400.png", + }, + chains: [ + { + name: Network.Gnosis, + }, + ], + }, + }, + ], +}; diff --git a/src/app/api/zk-badge/image/[tokenId]/route.test.ts b/src/app/api/zk-badge/image/[tokenId]/route.test.ts index af7c890f..c97f43fb 100644 --- a/src/app/api/zk-badge/image/[tokenId]/route.test.ts +++ b/src/app/api/zk-badge/image/[tokenId]/route.test.ts @@ -1,67 +1,34 @@ /** * @jest-environment node */ +import { spaceMockWithZkBadge } from "./mocks"; import ServiceFactory from "@/src/services/service-factory/service-factory"; import { GET } from "../../image/[tokenId]/route"; -import { spaceMock1, spaceMock2 } from "@/src/services/spaces-service/tests/spaces-mock"; -import fs from 'fs'; -import path from 'path'; +describe("GET /api/zk-badge/image/[tokenId]", () => { + beforeEach(() => { + ServiceFactory.reset(); + ServiceFactory.getSpaceConfigs([spaceMockWithZkBadge]); + }); -describe('GET /api/zk-badge/image/[tokenId]', () => { - - beforeEach(() => { - let spacesService = ServiceFactory.getSpacesService(); - const configs = [ - spaceMock1, - spaceMock2 - ] - spacesService.updateConfigs(configs); - }) - - afterEach(() => { - let configs = ServiceFactory.getSpaceConfigs(); - let spacesService = ServiceFactory.getSpacesService(); - spacesService.updateConfigs(configs); - jest.clearAllMocks(); - }) - - it('should return an error response if no badge is found', async () => { + it("should return an error response if no badge is found", async () => { const params = { tokenId: "1234" }; const response = await GET(null, { params }); const data = await response.json(); - expect(data).toEqual({ error: 'No badge found for tokenId: 1234' }); + expect(data).toEqual({ error: "No badge found for tokenId: 1234" }); }); - it('should return an error response if no badge image is found', async () => { + it("should return an image response if a badge is found", async () => { const params = { tokenId: "40000001" }; const response = await GET(null, { params }); - const data = await response.json(); - expect(data).toEqual({ error: 'No badge image found for tokenId: 40000001' }); + expect(response.status).toEqual(200); + expect(response.headers.get("Content-Type")).toEqual("image/jpeg"); }); - it('should return an image response if a badge is found', async () => { - jest.spyOn(fs, 'readFileSync'); - - const params = { tokenId: "40000001" }; - const dummyImageBuffer = Buffer.from([0, 1, 2, 3, 4, 5]); - const imagePath = path.join(process.cwd(), '/space-configs/images/image.png'); - - (fs.readFileSync as jest.Mock).mockImplementation((path) => { - if (path === imagePath) { - return dummyImageBuffer; - } - }); - + it("should return an error response if no badge image is found", async () => { + const params = { tokenId: "40000002" }; const response = await GET(null, { params }); - - const reader = response.body.getReader(); - const result = await reader.read(); - - const data = Buffer.from(result.value); - - expect(Buffer.compare(data, dummyImageBuffer)).toEqual(0); - expect(response.status).toEqual(200); - expect(response.headers.get('Content-Type')).toEqual('image/jpeg'); + const data = await response.json(); + expect(data).toEqual({ error: "No image found for badge tokenId: 40000002" }); }); -}); \ No newline at end of file +}); diff --git a/src/app/api/zk-badge/image/[tokenId]/route.ts b/src/app/api/zk-badge/image/[tokenId]/route.ts index 1e6c3c7e..abe3646b 100644 --- a/src/app/api/zk-badge/image/[tokenId]/route.ts +++ b/src/app/api/zk-badge/image/[tokenId]/route.ts @@ -1,41 +1,29 @@ +import { error404Response } from "@/src/libs/helper/api"; import ServiceFactory from "@/src/services/service-factory/service-factory"; import { ZkBadgeAppType } from "@/src/services/spaces-service"; -import fs from 'fs'; +import fs from "fs"; import { NextResponse } from "next/server"; -import path from 'path'; export async function GET(req: Request, { params }: { params: { tokenId: string } }) { - const tokenId = params.tokenId; - const spacesService = ServiceFactory.getSpacesService(); - let apps = await spacesService.getApps(); - apps = apps.filter(app => app.type === "zkBadge") ; - const badge = (apps as ZkBadgeAppType[]).find(app => app.tokenId === tokenId); - if (!badge) { - return NextResponse.json({ - error: `No badge found for tokenId: ${tokenId}` - }) - } + const tokenId = params.tokenId; + const spacesService = ServiceFactory.getSpacesService(); + let apps = await spacesService.getApps(); + apps = apps.filter((app) => app.type === "zkBadge"); + const badge = (apps as ZkBadgeAppType[]).find((app) => app.tokenId === tokenId); + if (!badge) { + return error404Response(`No badge found for tokenId: ${tokenId}`); + } - let readableStream; - try { - const imagePath = path.join(process.cwd(), `/space-configs/images/${badge.badgeMetadata.image}`); - const file = fs.readFileSync(imagePath); - readableStream = new ReadableStream({ - start(controller) { - controller.enqueue(new Uint8Array(file)); - controller.close(); - } - }); - } catch (e) { - return NextResponse.json({ - error: `No badge image found for tokenId: ${tokenId}` - }) - } - - return new NextResponse(readableStream, { - status: 200, - headers: { - 'Content-Type': 'image/jpeg' - }, - }) -} \ No newline at end of file + try { + const imagePath = `${__dirname}/../../../../../../space-configs/images/${badge.badgeMetadata.image}`; + const file = fs.readFileSync(imagePath); + return new NextResponse(file, { + status: 200, + headers: { + "Content-Type": "image/jpeg", + }, + }); + } catch (e) { + return error404Response(`No image found for badge tokenId: ${tokenId}`); + } +} diff --git a/src/app/api/zk-drop/relay-tx/route.test.ts b/src/app/api/zk-drop/relay-tx/route.test.ts index 801840b9..e369590f 100644 --- a/src/app/api/zk-drop/relay-tx/route.test.ts +++ b/src/app/api/zk-drop/relay-tx/route.test.ts @@ -6,92 +6,89 @@ import { POST } from "./route"; import { spaceMock1, spaceMock2 } from "@/src/services/spaces-service/tests/spaces-mock"; import { Network } from "@/src/libs/contracts/networks"; - -const mockMint = jest.fn().mockImplementation(() => Promise.resolve({ hash: 'mockTxHash' })); +const mockMint = jest.fn().mockImplementation(() => Promise.resolve({ hash: "mockTxHash" })); jest.mock("../../../../libs/contracts/signers", () => { - return { - getDefenderRelayerSigner: jest.fn().mockReturnValue({}), - }; + return { + getDefenderRelayerSigner: jest.fn().mockReturnValue({}), + }; }); -jest.mock('../../../../libs/contracts/zk-drop', () => { - const originalModule = jest.requireActual("../../../../libs/contracts/zk-drop"); - return { - __esModule: true, - ...originalModule, - ZkDropContract: jest.fn().mockImplementation(() => { - return { - mint: mockMint, - }; - }), - }; +jest.mock("../../../../libs/contracts/zk-drop", () => { + const originalModule = jest.requireActual("../../../../libs/contracts/zk-drop"); + return { + __esModule: true, + ...originalModule, + ZkDropContract: jest.fn().mockImplementation(() => { + return { + mint: mockMint, + }; + }), + }; }); -describe('POST /api/zk-drop/relay-tx', () => { - beforeEach(() => { - let spacesService = ServiceFactory.getSpacesService(); - const configs = [ - spaceMock1, - spaceMock2 - ] - spacesService.updateConfigs(configs); - }) - - afterEach(() => { - let configs = ServiceFactory.getSpaceConfigs(); - let spacesService = ServiceFactory.getSpacesService(); - spacesService.updateConfigs(configs); - jest.clearAllMocks(); - }) +describe("POST /api/zk-drop/relay-tx", () => { + beforeEach(() => { + let spacesService = ServiceFactory.getSpacesService(); + const configs = [spaceMock1, spaceMock2]; + spacesService.updateConfigs(configs); + }); - it('Should throw with an incorrect app slug', async () => { - const req: any = { - json: jest.fn().mockResolvedValue({ - responseBytes: 'mockResponseBytes', - destination: 'mockDestination', - spaceSlug: spaceMock1.metadata.slug, - appSlug: "zk-drop-slug-invalid", - chain: Network.Sepolia - }) - }; + afterEach(() => { + let configs = ServiceFactory.getSpaceConfigs(); + let spacesService = ServiceFactory.getSpacesService(); + spacesService.updateConfigs(configs); + jest.clearAllMocks(); + }); - const response = await POST(req); - const data = await response.json(); - expect(data.code).toEqual(`No app found for ${spaceMock1.metadata.slug}/zk-drop-slug-invalid`); - }); + it("Should throw with an incorrect app slug", async () => { + const req: any = { + json: jest.fn().mockResolvedValue({ + responseBytes: "mockResponseBytes", + destination: "mockDestination", + spaceSlug: spaceMock1.metadata.slug, + appSlug: "zk-drop-slug-invalid", + chain: Network.Sepolia, + }), + }; - it('Should throw with an incorrect chain', async () => { - const req: any = { - json: jest.fn().mockResolvedValue({ - responseBytes: 'mockResponseBytes', - destination: 'mockDestination', - spaceSlug: spaceMock1.metadata.slug, - appSlug: "zk-drop-slug", - chain: Network.Mumbai - }) - }; + const response = await POST(req); + const data = await response.json(); + expect(data.code).toEqual(`No app found for ${spaceMock1.metadata.slug}/zk-drop-slug-invalid`); + }); - const response = await POST(req); - const data = await response.json(); - expect(data.code).toEqual(`Chain mumbai not supported for the app ${spaceMock1.metadata.slug}/zk-drop-slug`); - }); + it("Should throw with an incorrect chain", async () => { + const req: any = { + json: jest.fn().mockResolvedValue({ + responseBytes: "mockResponseBytes", + destination: "mockDestination", + spaceSlug: spaceMock1.metadata.slug, + appSlug: "zk-drop-slug", + chain: Network.Mumbai, + }), + }; + + const response = await POST(req); + const data = await response.json(); + expect(data.code).toEqual( + `Chain mumbai not supported for the app ${spaceMock1.metadata.slug}/zk-drop-slug` + ); + }); - it('Should call mint with the correct arguments', async () => { - const req: any = { - json: jest.fn().mockResolvedValue({ - responseBytes: 'mockResponseBytes', - destination: 'mockDestination', - spaceSlug: spaceMock1.metadata.slug, - appSlug: "zk-drop-slug", - chain: Network.Sepolia - }) - }; - await POST(req); - console.log("Mock mint calls: ", mockMint.mock.calls); - expect(mockMint).toHaveBeenCalledWith({ - responseBytes: 'mockResponseBytes', - address: 'mockDestination' - }); + it("Should call mint with the correct arguments", async () => { + const req: any = { + json: jest.fn().mockResolvedValue({ + responseBytes: "mockResponseBytes", + destination: "mockDestination", + spaceSlug: spaceMock1.metadata.slug, + appSlug: "zk-drop-slug", + chain: Network.Sepolia, + }), + }; + await POST(req); + expect(mockMint).toHaveBeenCalledWith({ + responseBytes: "mockResponseBytes", + address: "mockDestination", }); -}); \ No newline at end of file + }); +}); diff --git a/src/app/api/zk-drop/relay-tx/route.ts b/src/app/api/zk-drop/relay-tx/route.ts index 473be860..d560a820 100644 --- a/src/app/api/zk-drop/relay-tx/route.ts +++ b/src/app/api/zk-drop/relay-tx/route.ts @@ -6,47 +6,44 @@ import { ZkDropAppType } from "@/src/services/spaces-service"; import { NextResponse } from "next/server"; export async function POST(req: Request) { - const { responseBytes, destination, chain, spaceSlug, appSlug } = await req.json(); + const { responseBytes, destination, chain, spaceSlug, appSlug } = await req.json(); - const spacesService = ServiceFactory.getSpacesService(); - const apps = await spacesService.getApps({ where: { spaceSlug, appSlug }}); - if (!apps || apps.length !== 1) { - return NextResponse.json({ - code: `No app found for ${spaceSlug}/${appSlug}` - }) - } - const app = apps[0] as ZkDropAppType; + const spacesService = ServiceFactory.getSpacesService(); + const apps = await spacesService.getApps({ where: { spaceSlug, appSlug } }); + if (!apps || apps.length !== 1) { + return NextResponse.json({ + code: `No app found for ${spaceSlug}/${appSlug}`, + }); + } + const app = apps[0] as ZkDropAppType; - const signer = getDefenderRelayerSigner(chain, env.defenderAPIKeys.zkDrop); - const chainConfig = app.chains.find(_chain => _chain.name === chain) - - if (!chainConfig) { - return NextResponse.json({ - code: `Chain ${chain} not supported for the app ${spaceSlug}/${appSlug}` - }) - } - const zkDropContract = new ZkDropContract({ signer, contractAddress: chainConfig.contractAddress }); - - try { - console.log({ - responseBytes, - address: destination, - chain, - contractAddress: chainConfig.contractAddress - }); - const tx = await zkDropContract.mint({ - responseBytes, - address: destination - }); - - return NextResponse.json({ - success: true, - txHash: tx.hash - }); - } catch (e) { - console.error(e); - return NextResponse.json({ - code: "minting-error" - }) - } -} \ No newline at end of file + const signer = getDefenderRelayerSigner(chain, env.defenderAPIKeys.zkDrop); + const chainConfig = app.chains.find((_chain) => _chain.name === chain); + + if (!chainConfig) { + return NextResponse.json({ + code: `Chain ${chain} not supported for the app ${spaceSlug}/${appSlug}`, + }); + } + const zkDropContract = new ZkDropContract({ + signer, + contractAddress: chainConfig.contractAddress, + }); + + try { + const tx = await zkDropContract.mint({ + responseBytes, + address: destination, + }); + + return NextResponse.json({ + success: true, + txHash: tx.hash, + }); + } catch (e) { + console.error(e); + return NextResponse.json({ + code: "minting-error", + }); + } +} diff --git a/src/libs/helper/api.ts b/src/libs/helper/api.ts index 8c02aa8d..97747688 100644 --- a/src/libs/helper/api.ts +++ b/src/libs/helper/api.ts @@ -10,3 +10,12 @@ export const errorResponse = (message: string): Response => { message: message, }); }; + +export const error404Response = (message: string): Response => { + return NextResponse.json( + { + error: message, + }, + { status: 404 } + ); +}; diff --git a/src/services/service-factory/service-factory.ts b/src/services/service-factory/service-factory.ts index ea3cd1ad..fbd6e319 100644 --- a/src/services/service-factory/service-factory.ts +++ b/src/services/service-factory/service-factory.ts @@ -123,6 +123,7 @@ const ServiceFactory = { loggerService = null; zkFormTableStore = null; sismoFactoryService = null; + spacesService = null; }, };