import { beforeEach, describe, expect, it } from "bun:test"; import { createHash, randomBytes } from "node:crypto"; import { createAuthorizationCode, exchangeCode, getClient, refreshAccessToken, registerClient, verifyAccessToken, } from "../../src/server/services/oauth.service.ts"; import { createTestDb } from "../helpers/db.ts"; function generatePkce() { const verifier = randomBytes(32).toString("hex"); const challenge = createHash("sha256").update(verifier).digest("base64url"); return { verifier, challenge }; } describe("OAuth Service", () => { let db: any; let userId: number; beforeEach(async () => { ({ db, userId } = await createTestDb()); }); describe("Client Registration", () => { it("registers a client and returns clientId (string, non-empty)", async () => { const result = await registerClient(db, "Test App", [ "http://localhost:8080/callback", ]); expect(result).toBeDefined(); expect(typeof result.clientId).toBe("string"); expect(result.clientId.length).toBeGreaterThan(0); }); it("getClient returns registered client with correct clientName and redirectUris (JSON parsed)", async () => { const redirectUris = ["http://localhost:8080/callback"]; const { clientId } = await registerClient(db, "Test App", redirectUris); const client = await getClient(db, clientId); expect(client).not.toBeNull(); expect(client!.clientName).toBe("Test App"); expect(JSON.parse(client!.redirectUris)).toEqual(redirectUris); }); it("getClient returns null for unknown client", async () => { const client = await getClient(db, "unknown-client-id"); expect(client).toBeNull(); }); }); describe("Authorization Code + PKCE", () => { it("creates an authorization code (non-empty)", async () => { const { clientId } = await registerClient(db, "Test App", [ "http://localhost:8080/callback", ]); const { challenge } = generatePkce(); const result = await createAuthorizationCode( db, clientId, userId, challenge, "S256", "http://localhost:8080/callback", ); expect(result).toBeDefined(); expect(typeof result.code).toBe("string"); expect(result.code.length).toBeGreaterThan(0); }); it("exchanges code for tokens with valid PKCE verifier (returns accessToken, refreshToken, expiresIn=3600)", async () => { const { clientId } = await registerClient(db, "Test App", [ "http://localhost:8080/callback", ]); const { verifier, challenge } = generatePkce(); const { code } = await createAuthorizationCode( db, clientId, userId, challenge, "S256", "http://localhost:8080/callback", ); const tokens = await exchangeCode( db, code, verifier, clientId, "http://localhost:8080/callback", ); expect(tokens).not.toBeNull(); expect(typeof tokens!.accessToken).toBe("string"); expect(tokens!.accessToken.length).toBeGreaterThan(0); expect(typeof tokens!.refreshToken).toBe("string"); expect(tokens!.refreshToken.length).toBeGreaterThan(0); expect(tokens!.expiresIn).toBe(3600); }); it("rejects code exchange with wrong PKCE verifier (returns null)", async () => { const { clientId } = await registerClient(db, "Test App", [ "http://localhost:8080/callback", ]); const { challenge } = generatePkce(); const { code } = await createAuthorizationCode( db, clientId, userId, challenge, "S256", "http://localhost:8080/callback", ); const tokens = await exchangeCode( db, code, "wrongverifier", clientId, "http://localhost:8080/callback", ); expect(tokens).toBeNull(); }); it("rejects code exchange with wrong redirect_uri (returns null)", async () => { const { clientId } = await registerClient(db, "Test App", [ "http://localhost:8080/callback", ]); const { verifier, challenge } = generatePkce(); const { code } = await createAuthorizationCode( db, clientId, userId, challenge, "S256", "http://localhost:8080/callback", ); const tokens = await exchangeCode( db, code, verifier, clientId, "http://localhost:9999/wrong", userId, ); expect(tokens).toBeNull(); }); it("rejects replayed code - single use (second exchange returns null)", async () => { const { clientId } = await registerClient(db, "Test App", [ "http://localhost:8080/callback", ]); const { verifier, challenge } = generatePkce(); const { code } = await createAuthorizationCode( db, clientId, userId, challenge, "S256", "http://localhost:8080/callback", ); // First exchange succeeds const first = await exchangeCode( db, code, verifier, clientId, "http://localhost:8080/callback", ); expect(first).not.toBeNull(); // Second exchange is rejected const second = await exchangeCode( db, code, verifier, clientId, "http://localhost:8080/callback", ); expect(second).toBeNull(); }); }); describe("Token Verification", () => { it("verifies a valid access token returns { userId }", async () => { const { clientId } = await registerClient(db, "Test App", [ "http://localhost:8080/callback", ]); const { verifier, challenge } = generatePkce(); const { code } = await createAuthorizationCode( db, clientId, userId, challenge, "S256", "http://localhost:8080/callback", ); const tokens = await exchangeCode( db, code, verifier, clientId, "http://localhost:8080/callback", ); const verified = await verifyAccessToken(db, tokens!.accessToken); expect(verified).not.toBeNull(); expect(verified?.userId).toBe(userId); }); it("rejects an unknown token (returns null)", async () => { const verified = await verifyAccessToken(db, "unknowntoken12345678"); expect(verified).toBeNull(); }); }); describe("Token Refresh", () => { it("refreshes a valid refresh token and returns new tokens (different accessToken)", async () => { const { clientId } = await registerClient(db, "Test App", [ "http://localhost:8080/callback", ]); const { verifier, challenge } = generatePkce(); const { code } = await createAuthorizationCode( db, clientId, userId, challenge, "S256", "http://localhost:8080/callback", ); const tokens = await exchangeCode( db, code, verifier, clientId, "http://localhost:8080/callback", ); const newTokens = await refreshAccessToken( db, tokens!.refreshToken, clientId, ); expect(newTokens).not.toBeNull(); expect(newTokens!.accessToken).not.toBe(tokens!.accessToken); expect(typeof newTokens!.refreshToken).toBe("string"); expect(newTokens!.expiresIn).toBe(3600); }); it("rejects refresh with wrong clientId (returns null)", async () => { const { clientId } = await registerClient(db, "Test App", [ "http://localhost:8080/callback", ]); const { verifier, challenge } = generatePkce(); const { code } = await createAuthorizationCode( db, clientId, userId, challenge, "S256", "http://localhost:8080/callback", ); const tokens = await exchangeCode( db, code, verifier, clientId, "http://localhost:8080/callback", ); const newTokens = await refreshAccessToken( db, tokens!.refreshToken, "wrong-client-id", userId, ); expect(newTokens).toBeNull(); }); }); });