Skip to content

Commit

Permalink
fix(platforms): catching and reporting errors from the Idena API (#3174)
Browse files Browse the repository at this point in the history
  • Loading branch information
lucianHymer authored Jan 13, 2025
1 parent 3224dfd commit bb68c49
Show file tree
Hide file tree
Showing 2 changed files with 157 additions and 111 deletions.
255 changes: 148 additions & 107 deletions platforms/src/Idena/__tests__/provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,136 +50,177 @@ type IdenaCache = {
signature?: string;
};

beforeAll(() => {
jest.useFakeTimers("modern");
jest.setSystemTime(new Date(Date.UTC(2023, 0, 1)));
});
describe("Idena", () => {
beforeAll(() => {
jest.useFakeTimers("modern");
jest.setSystemTime(new Date(Date.UTC(2023, 0, 1)));
});

afterAll(() => {
jest.useRealTimers();
});
afterAll(() => {
jest.useRealTimers();
});

beforeEach(async () => {
await initCacheSession(MOCK_SESSION_KEY);
const session = await loadCacheSession<IdenaCache>(MOCK_SESSION_KEY);
await session.set("address", MOCK_ADDRESS);
await session.set("signature", "signature");

mockedAxios.get.mockImplementation(async (url, config) => {
switch (url) {
case `/api/identity/${MOCK_ADDRESS}/age`:
return ageResponse;
case `/api/identity/${MOCK_ADDRESS}`:
return identityResponse;
case `/api/address/${MOCK_ADDRESS}`:
return addressResponse;
case "/api/epoch/last":
return lastEpochResponse;
}
beforeEach(async () => {
await initCacheSession(MOCK_SESSION_KEY);
const session = await loadCacheSession<IdenaCache>(MOCK_SESSION_KEY);
await session.set("address", MOCK_ADDRESS);
await session.set("signature", "signature");

mockedAxios.get.mockImplementation(async (url, config) => {
switch (url) {
case `/api/identity/${MOCK_ADDRESS}/age`:
return ageResponse;
case `/api/identity/${MOCK_ADDRESS}`:
return identityResponse;
case `/api/address/${MOCK_ADDRESS}`:
return addressResponse;
case "/api/epoch/last":
return lastEpochResponse;
}
});

mockedAxios.create = jest.fn(() => mockedAxios);
});

mockedAxios.create = jest.fn(() => mockedAxios);
});
afterEach(() => {
jest.clearAllMocks();
});

afterEach(() => {
jest.clearAllMocks();
});
describe("Check valid cases for state providers", function () {
it("Expected Human state", async () => {
const provider = new IdenaStateHumanProvider();
const payload = {
proofs: {
sessionKey: MOCK_SESSION_KEY,
},
};
const verifiedPayload = await provider.verify(payload as unknown as RequestPayload, {} as IdenaContext);

expect(verifiedPayload).toEqual({
valid: true,
record: {
address: MOCK_ADDRESS,
state: "Human",
},
expiresInSeconds: 86401,
});
});

describe("Check valid cases for state providers", function () {
it("Expected Human state", async () => {
const provider = new IdenaStateHumanProvider();
const payload = {
proofs: {
sessionKey: MOCK_SESSION_KEY,
},
};
const verifiedPayload = await provider.verify(payload as unknown as RequestPayload, {} as IdenaContext);
it("Expected Newbie state", async () => {
identityResponse.data.result.state = "Newbie";
const provider = new IdenaStateNewbieProvider();
const payload = {
proofs: {
sessionKey: MOCK_SESSION_KEY,
},
};
const verifiedPayload = await provider.verify(payload as unknown as RequestPayload, {} as IdenaContext);

expect(verifiedPayload).toEqual({
valid: true,
record: {
address: MOCK_ADDRESS,
state: "Newbie",
},
expiresInSeconds: 86401,
});
});

expect(verifiedPayload).toEqual({
valid: true,
record: {
address: MOCK_ADDRESS,
state: "Human",
},
expiresInSeconds: 86401,
it("Incorrect state", async () => {
identityResponse.data.result.state = "Newbie";
const provider = new IdenaStateVerifiedProvider();
const payload = {
proofs: {
sessionKey: MOCK_SESSION_KEY,
},
};
const verifiedPayload = await provider.verify(payload as unknown as RequestPayload, {} as IdenaContext);

expect(verifiedPayload).toEqual(
expect.objectContaining({
valid: false,
errors: [`State "${identityResponse.data.result.state}" does not match acceptable state(s) Verified, Human`],
})
);
});
});

it("Expected Newbie state", async () => {
identityResponse.data.result.state = "Newbie";
const provider = new IdenaStateNewbieProvider();
const payload = {
proofs: {
sessionKey: MOCK_SESSION_KEY,
},
};
const verifiedPayload = await provider.verify(payload as unknown as RequestPayload, {} as IdenaContext);
it("Expected Verified state", async () => {
identityResponse.data.result.state = "Verified";
const provider = new IdenaStateVerifiedProvider();
const payload = {
proofs: {
sessionKey: MOCK_SESSION_KEY,
},
};
const verifiedPayload = await provider.verify(payload as unknown as RequestPayload, {} as IdenaContext);

expect(verifiedPayload).toEqual({
valid: true,
record: {
address: MOCK_ADDRESS,
state: "Verified",
},
expiresInSeconds: 86401,
});
});

expect(verifiedPayload).toEqual({
valid: true,
record: {
address: MOCK_ADDRESS,
state: "Newbie",
},
expiresInSeconds: 86401,
it("Higher states acceptable for lower state stamps", async () => {
identityResponse.data.result.state = "Human";
const provider = new IdenaStateVerifiedProvider();
const payload = {
proofs: {
sessionKey: MOCK_SESSION_KEY,
},
};
const verifiedPayload = await provider.verify(payload as unknown as RequestPayload, {} as IdenaContext);

expect(verifiedPayload).toEqual({
valid: true,
record: {
address: MOCK_ADDRESS,
state: "Verified",
},
expiresInSeconds: 86401,
});
});
});
});

it("Incorrect state", async () => {
identityResponse.data.result.state = "Newbie";
const provider = new IdenaStateVerifiedProvider();
const payload = {
proofs: {
sessionKey: MOCK_SESSION_KEY,
},
};
const verifiedPayload = await provider.verify(payload as unknown as RequestPayload, {} as IdenaContext);

expect(verifiedPayload).toEqual(
expect.objectContaining({
valid: false,
errors: [`State "${identityResponse.data.result.state}" does not match acceptable state(s) Verified, Human`],
})
);
describe("Idena Error", () => {
beforeAll(() => {
jest.useFakeTimers("modern");
jest.setSystemTime(new Date(Date.UTC(2023, 0, 1)));
});

it("Expected Verified state", async () => {
identityResponse.data.result.state = "Verified";
const provider = new IdenaStateVerifiedProvider();
const payload = {
proofs: {
sessionKey: MOCK_SESSION_KEY,
},
};
const verifiedPayload = await provider.verify(payload as unknown as RequestPayload, {} as IdenaContext);
afterAll(() => {
jest.useRealTimers();
});

expect(verifiedPayload).toEqual({
valid: true,
record: {
address: MOCK_ADDRESS,
state: "Verified",
},
expiresInSeconds: 86401,
});
afterEach(() => {
jest.clearAllMocks();
});

it("Higher states acceptable for lower state stamps", async () => {
identityResponse.data.result.state = "Human";
it("should report errors from the API", async () => {
await initCacheSession(MOCK_SESSION_KEY);
const session = await loadCacheSession<IdenaCache>(MOCK_SESSION_KEY);
await session.set("address", MOCK_ADDRESS);
await session.set("signature", "signature");

mockedAxios.get.mockImplementation(async () => ({
data: { error: { message: "Idena API error" } },
status: 200,
}));

mockedAxios.create = jest.fn(() => mockedAxios);

const provider = new IdenaStateVerifiedProvider();
const payload = {
proofs: {
sessionKey: MOCK_SESSION_KEY,
},
};
const verifiedPayload = await provider.verify(payload as unknown as RequestPayload, {} as IdenaContext);

expect(verifiedPayload).toEqual({
valid: true,
record: {
address: MOCK_ADDRESS,
state: "Verified",
},
expiresInSeconds: 86401,
});
await expect(provider.verify(payload as unknown as RequestPayload, {} as IdenaContext)).rejects.toThrow(
"Idena API returned error: Idena API error"
);
});
});
13 changes: 9 additions & 4 deletions platforms/src/Idena/procedures/idenaSignIn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import crypto from "crypto";
import axios, { AxiosInstance, AxiosResponse } from "axios";
import { initCacheSession, loadCacheSession, clearCacheSession, PlatformSession } from "../../utils/platform-cache";
import { ProviderContext } from "@gitcoin/passport-types";
import { ProviderInternalVerificationError } from "../../types";
import { ProviderExternalVerificationError, ProviderInternalVerificationError } from "../../types";
import { handleProviderAxiosError } from "../../utils/handleProviderAxiosError";

type IdenaCache = {
Expand Down Expand Up @@ -94,7 +94,7 @@ export type IdenaContext = ProviderContext & {
idena: {
address?: string;
responses: {
[key in IdenaMethod]?: AxiosResponse;
[key in IdenaMethod]?: AxiosResponse<{ error?: { message?: string } }>;
};
};
};
Expand Down Expand Up @@ -167,12 +167,17 @@ const request = async <T>(token: string, context: IdenaContext, method: IdenaMet
let response = context.idena.responses[method];
if (!response) {
try {
response = await apiClient().get(method.replace("_address_", address));
response = await apiClient().get<{ error?: { message?: string } }>(method.replace("_address_", address));
} catch (error: unknown) {
handleProviderAxiosError(error, `Idena ${method}`);
}
context.idena.responses[method] = response;
}

return { ...response.data, address } as T;
const { data } = response;
if (data.error?.message) {
throw new ProviderExternalVerificationError("Idena API returned error: " + data.error.message);
}

return { ...data, address } as T;
};

0 comments on commit bb68c49

Please sign in to comment.