From bb68c491a962e0a0819ab162518f2efbeb074ecb Mon Sep 17 00:00:00 2001 From: Lucian Date: Mon, 13 Jan 2025 09:25:50 -0700 Subject: [PATCH] fix(platforms): catching and reporting errors from the Idena API (#3174) --- .../src/Idena/__tests__/provider.test.ts | 255 ++++++++++-------- platforms/src/Idena/procedures/idenaSignIn.ts | 13 +- 2 files changed, 157 insertions(+), 111 deletions(-) diff --git a/platforms/src/Idena/__tests__/provider.test.ts b/platforms/src/Idena/__tests__/provider.test.ts index 247b16a014..4fa06a90db 100644 --- a/platforms/src/Idena/__tests__/provider.test.ts +++ b/platforms/src/Idena/__tests__/provider.test.ts @@ -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(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(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(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" + ); }); }); diff --git a/platforms/src/Idena/procedures/idenaSignIn.ts b/platforms/src/Idena/procedures/idenaSignIn.ts index 375b418c4a..92bb966c83 100644 --- a/platforms/src/Idena/procedures/idenaSignIn.ts +++ b/platforms/src/Idena/procedures/idenaSignIn.ts @@ -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 = { @@ -94,7 +94,7 @@ export type IdenaContext = ProviderContext & { idena: { address?: string; responses: { - [key in IdenaMethod]?: AxiosResponse; + [key in IdenaMethod]?: AxiosResponse<{ error?: { message?: string } }>; }; }; }; @@ -167,12 +167,17 @@ const request = async (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; };