Skip to content

Commit

Permalink
fix: adding tests for express app
Browse files Browse the repository at this point in the history
  • Loading branch information
nutrina committed Dec 30, 2024
1 parent 25516f7 commit b0876eb
Show file tree
Hide file tree
Showing 6 changed files with 272 additions and 32 deletions.
18 changes: 18 additions & 0 deletions embed/__mocks__/ioredis.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// __mocks__/ioredis.ts
const RedisMock = jest.fn().mockImplementation(() => {
return {
get: jest.fn((key) => Promise.resolve(null)),
set: jest.fn((key, value) => {
return Promise.resolve("OK");
}),
on: jest.fn((key, func) => {}),
call: jest.fn((type, ...args) => {
if(type === "EVALSHA") {
return Promise.resolve([ 1, 60000 ]);
}
return Promise.resolve("OK");
}),
};
});

module.exports = RedisMock;
193 changes: 193 additions & 0 deletions embed/__tests__/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
// ---- Testing libraries

// jest.mock("ioredis");

import request from "supertest";
import { Response, Request } from "express";
import { apiKeyRateLimit, keyGenerator } from "../src/rate-limiter";
import {
PassportScore,
AutoVerificationResponseBodyType,
AutoVerificationRequestBodyType,
} from "../src/autoVerification";
import { ParamsDictionary } from "express-serve-static-core";
import { VerifiableEip712Credential } from "@gitcoin/passport-types";
// ---- Test subject

const mockedScore: PassportScore = {
address: "0x0000000000000000000000000000000000000000",
score: "12",
passing_score: true,
last_score_timestamp: new Date().toISOString(),
expiration_timestamp: new Date().toISOString(),
threshold: "20.000",
error: "",
stamps: { "provider-1": { score: "12", dedup: true, expiration_date: new Date().toISOString() } },
};

// const createMockVerifiableCredential = (address: string): VerifiableEip712Credential => ({
// "@context": ["https://www.w3.org/2018/credentials/v1", "https://w3id.org/security/suites/eip712sig-2021/v1"],
// type: ["VerifiableCredential", "EVMCredential"],
// credentialSubject: {
// id: `did:pkh:eip155:1:${address}`,
// "@context": {
// hash: "https://schema.org/Text",
// provider: "https://schema.org/Text",
// address: "https://schema.org/Text",
// challenge: "https://schema.org/Text",
// metaPointer: "https://schema.org/URL",
// },
// hash: "0x123456789",
// provider: "test-provider",
// address: address,
// challenge: "test-challenge",
// metaPointer: "https://example.com/metadata",
// },
// issuer: "did:key:test-issuer",
// issuanceDate: new Date().toISOString(),
// expirationDate: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(), // 30 days from now
// proof: {
// "@context": "https://w3id.org/security/suites/eip712sig-2021/v1",
// type: "EthereumEip712Signature2021",
// proofPurpose: "assertionMethod",
// proofValue: "0xabcdef1234567890",
// verificationMethod: "did:key:test-verification",
// created: new Date().toISOString(),
// eip712Domain: {
// domain: {
// name: "GitcoinVerifiableCredential",
// },
// primaryType: "VerifiableCredential",
// types: {
// EIP712Domain: [
// { name: "name", type: "string" },
// { name: "version", type: "string" },
// ],
// VerifiableCredential: [
// { name: "id", type: "string" },
// { name: "address", type: "string" },
// ],
// },
// },
// },
// });

jest.mock("../src/rate-limiter", () => {
const originalModule = jest.requireActual<typeof import("../src/rate-limiter")>("../src/rate-limiter");

return {
...originalModule,
apiKeyRateLimit: jest.fn((req, res) => {
return new Promise((resolve, reject) => {
resolve(10000);
});
}),
keyGenerator: jest.fn(originalModule.keyGenerator),
};
});



jest.mock("../src/autoVerification", () => {
const originalModule = jest.requireActual<typeof import("../src/autoVerification")>("../src/autoVerification");

return {
// __esModule: true, // Use it when dealing with esModules
...originalModule,
autoVerificationHandler: jest.fn(
(
req: Request<ParamsDictionary, AutoVerificationResponseBodyType, AutoVerificationRequestBodyType>,
res: Response
) => {
return new Promise((resolve, reject) => {
res.status(200).json(mockedScore);
resolve();
});
}
),
};
});



import { app } from "../src/index";

beforeEach(() => {
// CLear the spy stats
jest.clearAllMocks();
});

describe("POST /embed/verify", function () {
it("handles valid verify requests", async () => {
// as each signature is unique, each request results in unique output
const payload = {
address: "0x0000000000000000000000000000000000000000",
scorerId: "123",
};

// create a req against the express app
const verifyRequest = await request(app)
.post("/embed/verify")
.send(payload)
.set("Accept", "application/json")
.set("X-API-KEY", "MY.SECRET-KEY");

expect(apiKeyRateLimit as jest.Mock).toHaveBeenCalledTimes(1);
expect(keyGenerator as jest.Mock).toHaveBeenCalledTimes(1);
expect(verifyRequest.status).toBe(200);
expect(verifyRequest.body).toStrictEqual(mockedScore);
});

it("handles invalid verify requests - missing api key", async () => {
// as each signature is unique, each request results in unique output
const payload = {
address: "0x0000000000000000000000000000000000000000",
scorerId: "123",
};

// create a req against the express app
const verifyRequest = await request(app).post("/embed/verify").send(payload).set("Accept", "application/json");

expect(apiKeyRateLimit as jest.Mock).toHaveBeenCalledTimes(0);
expect(keyGenerator as jest.Mock).toHaveBeenCalledTimes(1);
expect(verifyRequest.status).toBe(401);
expect(verifyRequest.body).toStrictEqual({ message: "Unauthorized! No 'X-API-KEY' present in the header!" });
});

it("handles invalid verify requests - api key validation fails", async () => {
// as each signature is unique, each request results in unique output
const payload = {
address: "0x0000000000000000000000000000000000000000",
scorerId: "123",
};

(apiKeyRateLimit as jest.Mock).mockImplementationOnce(() => {
throw "Invalid API-KEY";
});

// create a req against the express app
const verifyRequest = await request(app)
.post("/embed/verify")
.send(payload)
.set("Accept", "application/json")
.set("X-API-KEY", "MY.SECRET-KEY");

expect(apiKeyRateLimit as jest.Mock).toHaveBeenCalledTimes(1);
expect(keyGenerator as jest.Mock).toHaveBeenCalledTimes(1);
expect(verifyRequest.status).toBe(500);
});
});

describe("POST /health", function () {
it("handles valid health requests", async () => {
// create a req against the express app
const verifyRequest = await request(app).get("/health").set("Accept", "application/json");

expect(apiKeyRateLimit as jest.Mock).toHaveBeenCalledTimes(0);
expect(keyGenerator as jest.Mock).toHaveBeenCalledTimes(0);
expect(verifyRequest.status).toBe(200);
expect(verifyRequest.body).toStrictEqual({
message: "Ok",
});
});
});
53 changes: 36 additions & 17 deletions embed/src/autoVerification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,23 @@ type VerifyTypeResult = {
code?: number;
};

export type PassportProviderPoints = {
score: string;
dedup: boolean;
expiration_date: string;
};

export type PassportScore = {
address: string;
score: string;
passing_score: boolean;
last_score_timestamp: string;
expiration_timestamp: string;
threshold: string;
error: string;
stamps: Record<string, PassportProviderPoints>;
};

const providerTypePlatformMap = Object.entries(platforms).reduce(
(acc, [platformName, { providers }]) => {
providers.forEach(({ type }) => {
Expand Down Expand Up @@ -244,14 +261,14 @@ export const issueCredentials = async (
);
};

type AutoVerificationRequestBodyType = {
export type AutoVerificationRequestBodyType = {
address: string;
scorerId: string;
};

type AutoVerificationFields = AutoVerificationRequestBodyType;

type AutoVerificationResponseBodyType = {
export type AutoVerificationResponseBodyType = {
score: string;
threshold: string;
};
Expand All @@ -265,6 +282,7 @@ export const autoVerificationHandler = async (
try {
console.log("====> step 1");
const { address, scorerId } = req.body;
console.log("====> step 1 --- ", address);

if (!isAddress(address)) {
return void errorRes(res, "Invalid address", 400);
Expand All @@ -274,19 +292,19 @@ export const autoVerificationHandler = async (
const stamps = await getPassingEvmStamps({ address, scorerId });

console.log("====> step 3");
const { score, threshold } = await addStampsAndGetScore({ address, scorerId, stamps });
const score = await addStampsAndGetScore({ address, scorerId, stamps });

// TODO should we issue a score VC?

console.log("====> step 4");
return void res.json({ score, threshold });
return void res.json(score);
} catch (error) {
console.log("====> ERROR", error);
if (error instanceof ApiError) {
return void errorRes(res, error.message, error.code);
}
const message = addErrorDetailsToMessage("Unexpected error when processing request", error);
return void errorRes(res, message, 500);
// if (error instanceof ApiError) {
// return void errorRes(res, error.message, error.code);
// }
// const message = addErrorDetailsToMessage("Unexpected error when processing request", error);
// return void errorRes(res, message, 500);
}
};

Expand All @@ -301,7 +319,10 @@ const getEvmProviders = ({ scorerId }: { scorerId: string }): PROVIDER_ID[] => {
.flat(2);
};

const getPassingEvmStamps = async ({ address, scorerId }: AutoVerificationFields): Promise<VerifiableCredential[]> => {
export const getPassingEvmStamps = async ({
address,
scorerId,
}: AutoVerificationFields): Promise<VerifiableCredential[]> => {
const evmProviders = getEvmProviders({ scorerId });

const credentialsInfo = {
Expand All @@ -327,10 +348,7 @@ const addStampsAndGetScore = async ({
address,
scorerId,
stamps,
}: AutoVerificationFields & { stamps: VerifiableCredential[] }): Promise<{
score: string;
threshold: string;
}> => {
}: AutoVerificationFields & { stamps: VerifiableCredential[] }): Promise<PassportScore> => {
const scorerResponse: {
data?: {
score?: {
Expand All @@ -356,10 +374,11 @@ const addStampsAndGetScore = async ({

const scoreData = scorerResponse.data?.score || {};

const score = String(scoreData.evidence?.rawScore || scoreData.score);
const threshold = String(scoreData.evidence?.threshold || 20);
console.log("geri scoreData", scoreData);
// const score = String(scoreData.evidence?.rawScore || scoreData.score);
// const threshold = String(scoreData.evidence?.threshold || 20);

return { score, threshold };
return scoreData as PassportScore;
};

export const checkConditionsAndIssueCredentials = async (
Expand Down
18 changes: 11 additions & 7 deletions embed/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import express from "express";
// ---- Production plugins
import cors from "cors";
import { rateLimit } from "express-rate-limit";
import { RedisStore } from "rate-limit-redis";
import { RedisReply, RedisStore } from "rate-limit-redis";

// --- Relative imports
import { apiKeyRateLimit } from "./rate-limiter.js";
import { keyGenerator, apiKeyRateLimit } from "./rate-limiter.js";
import { autoVerificationHandler } from "./autoVerification.js";
import { metadataHandler } from "./metadata.js";
import { redis } from "./redis.js";
Expand Down Expand Up @@ -81,22 +81,26 @@ app.use(cors());
// Use the rate limiting middleware
app.use(
rateLimit({
windowMs: 60 * 1000, // We claculate the limit for a 1 minute limit ...
windowMs: 60 * 1000, // We calculate the limit for a 1 minute limit ...
limit: apiKeyRateLimit,
// Redis store configuration
keyGenerator: (req, _res) => req.headers["x-api-key"] as string,
keyGenerator: keyGenerator,
store: new RedisStore({
// @ts-expect-error - Known issue: the `call` function is not present in @types/ioredis
sendCommand: (...args: string[]) => redis.call(...args),
sendCommand: async (...args: string[]): Promise<RedisReply> => {
// @ts-expect-error - Known issue: the `call` function is not present in @types/ioredis
return await redis.call(...args);
},
}),
skip: (req, res): boolean => {
return req.path === "/health";
},
})
);

// health check endpoint
app.get("/health", (_req, res) => {
const data = {
message: "Ok",
date: new Date(),
};

res.status(200).send(data);
Expand Down
Loading

0 comments on commit b0876eb

Please sign in to comment.