Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions server/lookup-phone.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { appRouter } from "./routers";
import type { TrpcContext } from "./_core/context";

type AuthenticatedUser = NonNullable<TrpcContext["user"]>;

const savedFetch = globalThis.fetch;

afterEach(() => {
Object.defineProperty(globalThis, "fetch", { value: savedFetch, writable: true, configurable: true });
});

function createContext(user: AuthenticatedUser | null): TrpcContext {
return {
user,
req: { protocol: "https", headers: {} } as TrpcContext["req"],
res: { clearCookie: () => undefined } as unknown as TrpcContext["res"],
};
}

const authUser: AuthenticatedUser = {
id: 1,
openId: "phone-lookup-user",
email: "analyst@example.com",
name: "Analyst",
loginMethod: "manus",
role: "user",
createdAt: new Date(),
updatedAt: new Date(),
lastSignedIn: new Date(),
};

function stubGatewayFetch() {
const calls: Array<{ url: string; headers: Record<string, string> }> = [];
const mock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
calls.push({ url: String(input), headers: (init?.headers ?? {}) as Record<string, string> });
return new Response(
JSON.stringify({
number: "08031234567",
e164: "+2348031234567",
carrier: "MTN Nigeria",
lineType: "mobile",
country: "NG",
source: "hlr",
}),
{ status: 200, headers: { "Content-Type": "application/json" } },
);
});
vi.stubGlobal("fetch", mock);
return calls;
}

describe("lookup.phone", () => {
it("proxies a valid number to the gateway /v1/phone path with the service credential", async () => {
const calls = stubGatewayFetch();
const caller = appRouter.createCaller(createContext(authUser));

const result = await caller.lookup.phone({ number: "08031234567" });

Check failure on line 58 in server/lookup-phone.test.ts

View workflow job for this annotation

GitHub Actions / node-tests

server/lookup-phone.test.ts > lookup.phone > proxies a valid number to the gateway /v1/phone path with the service credential

TRPCError: No procedure found on path "lookup,phone" ❯ node_modules/.pnpm/@trpc+server@11.15.0_typescript@5.9.3/node_modules/@trpc/server/src/unstable-core-do-not-import/router.ts:465:20 ❯ server/lookup-phone.test.ts:58:20 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { code: 'NOT_FOUND' }

expect(result).toMatchObject({ e164: "+2348031234567", lineType: "mobile", source: "hlr" });
expect(calls).toHaveLength(1);
expect(calls[0]!.url).toMatch(/\/v1\/phone\/08031234567$/);
expect(calls[0]!.headers["X-BIS-Key"]).toBeTruthy();
});

it("URL-encodes the number in the proxy path", async () => {
const calls = stubGatewayFetch();
const caller = appRouter.createCaller(createContext(authUser));

await caller.lookup.phone({ number: "+234 803 123 4567" });

Check failure on line 70 in server/lookup-phone.test.ts

View workflow job for this annotation

GitHub Actions / node-tests

server/lookup-phone.test.ts > lookup.phone > URL-encodes the number in the proxy path

TRPCError: No procedure found on path "lookup,phone" ❯ node_modules/.pnpm/@trpc+server@11.15.0_typescript@5.9.3/node_modules/@trpc/server/src/unstable-core-do-not-import/router.ts:465:20 ❯ server/lookup-phone.test.ts:70:5 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { code: 'NOT_FOUND' }

expect(calls[0]!.url).toMatch(/\/v1\/phone\/%2B234%20803%20123%204567$/);
});

it("rejects invalid numbers before touching the gateway", async () => {
const calls = stubGatewayFetch();
const caller = appRouter.createCaller(createContext(authUser));

for (const number of ["", "12", "not-a-phone-number", "+2348031234567;DROP TABLE", "9".repeat(21)]) {
await expect(caller.lookup.phone({ number })).rejects.toMatchObject({ code: "BAD_REQUEST" });

Check failure on line 80 in server/lookup-phone.test.ts

View workflow job for this annotation

GitHub Actions / node-tests

server/lookup-phone.test.ts > lookup.phone > rejects invalid numbers before touching the gateway

AssertionError: expected TRPCError: No procedure found on path "lo… { code: '…' } to match object { code: 'BAD_REQUEST' } (2 matching properties omitted from actual) - Expected + Received - { - "code": "BAD_REQUEST", + TRPCError { + "code": "NOT_FOUND", } ❯ server/lookup-phone.test.ts:80:52
}
expect(calls).toHaveLength(0);
});

it("requires an authenticated user", async () => {
const calls = stubGatewayFetch();
const caller = appRouter.createCaller(createContext(null));

await expect(caller.lookup.phone({ number: "08031234567" })).rejects.toMatchObject({ code: "UNAUTHORIZED" });

Check failure on line 89 in server/lookup-phone.test.ts

View workflow job for this annotation

GitHub Actions / node-tests

server/lookup-phone.test.ts > lookup.phone > requires an authenticated user

AssertionError: expected TRPCError: No procedure found on path "lo… { code: '…' } to match object { code: 'UNAUTHORIZED' } (2 matching properties omitted from actual) - Expected + Received - { - "code": "UNAUTHORIZED", + TRPCError { + "code": "NOT_FOUND", } ❯ server/lookup-phone.test.ts:89:65
expect(calls).toHaveLength(0);
});
});
1 change: 1 addition & 0 deletions services/gateway/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -1309,6 +1309,7 @@ func newRouter() http.Handler {
mux.HandleFunc("/v1/nin/", protected(handleNINLookup))
mux.HandleFunc("/v1/bvn/", protected(handleBVNLookup))
mux.HandleFunc("/v1/cac/", protected(handleCACLookup))
mux.HandleFunc("/v1/phone/", protected(handlePhoneLookup))
mux.HandleFunc("/v1/sanctions/", protected(handleSanctionsCheck))
mux.HandleFunc("/v1/pep/", protected(handlePEPCheck))
mux.HandleFunc("/v1/credit/", protected(handleCreditCheck))
Expand Down
Loading
Loading