Files
GearBox/tests/services/oauth.service.test.ts
Jean-Luc Makiola 5b702a0e98 feat(16-04): update all service tests to pass userId and add isolation tests
- Destructure { db, userId } from createTestDb() in all 8 service test files
- Pass userId to every service function call
- Add cross-user isolation tests for items, categories, threads, setups
- Add composite unique constraint test for categories
- Update verifyApiKey assertions to check { userId } return
- Update verifyAccessToken assertions to check { userId } return
- Pass userId to exchangeCode and refreshAccessToken calls
2026-04-05 11:01:51 +02:00

303 lines
7.3 KiB
TypeScript

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,
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,
challenge,
"S256",
"http://localhost:8080/callback",
);
const tokens = await exchangeCode(
db,
code,
verifier,
clientId,
"http://localhost:8080/callback",
userId,
);
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,
challenge,
"S256",
"http://localhost:8080/callback",
);
const tokens = await exchangeCode(
db,
code,
"wrongverifier",
clientId,
"http://localhost:8080/callback",
userId,
);
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,
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,
challenge,
"S256",
"http://localhost:8080/callback",
);
// First exchange succeeds
const first = await exchangeCode(
db,
code,
verifier,
clientId,
"http://localhost:8080/callback",
userId,
);
expect(first).not.toBeNull();
// Second exchange is rejected
const second = await exchangeCode(
db,
code,
verifier,
clientId,
"http://localhost:8080/callback",
userId,
);
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,
challenge,
"S256",
"http://localhost:8080/callback",
);
const tokens = await exchangeCode(
db,
code,
verifier,
clientId,
"http://localhost:8080/callback",
userId,
);
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,
challenge,
"S256",
"http://localhost:8080/callback",
);
const tokens = await exchangeCode(
db,
code,
verifier,
clientId,
"http://localhost:8080/callback",
userId,
);
const newTokens = await refreshAccessToken(
db,
tokens!.refreshToken,
clientId,
userId,
);
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,
challenge,
"S256",
"http://localhost:8080/callback",
);
const tokens = await exchangeCode(
db,
code,
verifier,
clientId,
"http://localhost:8080/callback",
userId,
);
const newTokens = await refreshAccessToken(
db,
tokens!.refreshToken,
"wrong-client-id",
userId,
);
expect(newTokens).toBeNull();
});
});
});