Keine der in dieser Session neu hinzugefuegten sicherheitskritischen Module (lib/csrf.ts, lib/entra.ts, lib/dhl.ts) hatte bisher Tests. - csrf.test.ts: Same-Origin-Erkennung, Ablehnung gefaelschter Cross-Origin-Requests, tolerantes Verhalten bei fehlendem/kaputtem Origin-Header. - entra.test.ts: PKCE-Paar-Erzeugung (RFC 7636), Authorize-URL-Aufbau, Extraktion des verifizierten E-Mail-Claims (nie ein unplausibles Feld raten). - dhl.test.ts: Adress-Mapping (Strasse/Hausnummer-Trennung, ISO2->ISO3-Laendercode), Fehlerbehandlung bei nicht unterstuetztem Land, DHL-Validierungsfehlern und HTTP-Fehlern, statt einen falschen Erfolg vorzutaeuschen. Alle 118 Tests (13 Dateien) gruen, tsc --noEmit clean. Co-Authored-By: Claude Sonnet 5 <[email protected]>
106 lines
4.0 KiB
TypeScript
106 lines
4.0 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
import { describe, test, expect, beforeEach, afterEach } from "vitest";
|
|
import {
|
|
generatePkcePair,
|
|
generateOAuthState,
|
|
buildEntraAuthorizeUrl,
|
|
getVerifiedEmail,
|
|
isEntraConfigured,
|
|
EntraNotConfiguredError,
|
|
} from "./entra";
|
|
|
|
const ENV_KEYS = ["ENTRA_TENANT_ID", "ENTRA_CLIENT_ID", "ENTRA_CLIENT_SECRET", "ENTRA_REDIRECT_URI"] as const;
|
|
const originalEnv: Record<string, string | undefined> = {};
|
|
|
|
beforeEach(() => {
|
|
for (const key of ENV_KEYS) {
|
|
originalEnv[key] = process.env[key];
|
|
delete process.env[key];
|
|
}
|
|
});
|
|
|
|
afterEach(() => {
|
|
for (const key of ENV_KEYS) {
|
|
if (originalEnv[key] === undefined) delete process.env[key];
|
|
else process.env[key] = originalEnv[key];
|
|
}
|
|
});
|
|
|
|
function configureEntra(): void {
|
|
process.env.ENTRA_TENANT_ID = "tenant-123";
|
|
process.env.ENTRA_CLIENT_ID = "client-456";
|
|
process.env.ENTRA_CLIENT_SECRET = "secret-789";
|
|
process.env.ENTRA_REDIRECT_URI = "https://wabenhain.com/api/auth/entra/callback";
|
|
}
|
|
|
|
describe("generatePkcePair", () => {
|
|
test("produces a code_verifier in the RFC 7636 charset and length range", () => {
|
|
const { codeVerifier } = generatePkcePair();
|
|
expect(codeVerifier).toMatch(/^[A-Za-z0-9_-]{43,128}$/);
|
|
});
|
|
|
|
test("derives code_challenge as base64url(SHA256(code_verifier))", () => {
|
|
const { codeVerifier, codeChallenge } = generatePkcePair();
|
|
const expected = createHash("sha256").update(codeVerifier).digest("base64url");
|
|
expect(codeChallenge).toBe(expected);
|
|
});
|
|
|
|
test("generates a different pair on every call (not reused across requests)", () => {
|
|
const a = generatePkcePair();
|
|
const b = generatePkcePair();
|
|
expect(a.codeVerifier).not.toBe(b.codeVerifier);
|
|
});
|
|
});
|
|
|
|
describe("generateOAuthState", () => {
|
|
test("generates a non-empty, URL-safe, unpredictable value each call", () => {
|
|
const a = generateOAuthState();
|
|
const b = generateOAuthState();
|
|
expect(a).toMatch(/^[A-Za-z0-9_-]+$/);
|
|
expect(a.length).toBeGreaterThanOrEqual(20);
|
|
expect(a).not.toBe(b);
|
|
});
|
|
});
|
|
|
|
describe("isEntraConfigured / buildEntraAuthorizeUrl", () => {
|
|
test("reports not configured when env vars are missing", () => {
|
|
expect(isEntraConfigured()).toBe(false);
|
|
});
|
|
|
|
test("buildEntraAuthorizeUrl throws EntraNotConfiguredError when unconfigured", () => {
|
|
expect(() => buildEntraAuthorizeUrl({ state: "s", codeChallenge: "c" })).toThrow(EntraNotConfiguredError);
|
|
});
|
|
|
|
test("reports configured and builds a correct authorize URL once env vars are set", () => {
|
|
configureEntra();
|
|
expect(isEntraConfigured()).toBe(true);
|
|
|
|
const url = new URL(buildEntraAuthorizeUrl({ state: "my-state", codeChallenge: "my-challenge" }));
|
|
expect(url.origin + url.pathname).toBe("https://login.microsoftonline.com/tenant-123/oauth2/v2.0/authorize");
|
|
expect(url.searchParams.get("client_id")).toBe("client-456");
|
|
expect(url.searchParams.get("redirect_uri")).toBe("https://wabenhain.com/api/auth/entra/callback");
|
|
expect(url.searchParams.get("response_type")).toBe("code");
|
|
expect(url.searchParams.get("state")).toBe("my-state");
|
|
expect(url.searchParams.get("code_challenge")).toBe("my-challenge");
|
|
expect(url.searchParams.get("code_challenge_method")).toBe("S256");
|
|
// "openid" ist zwingend fuer OIDC, "email" liefert den verifizierten
|
|
// E-Mail-Claim, den getVerifiedEmail() unten voraussetzt.
|
|
expect(url.searchParams.get("scope")).toContain("openid");
|
|
expect(url.searchParams.get("scope")).toContain("email");
|
|
});
|
|
});
|
|
|
|
describe("getVerifiedEmail", () => {
|
|
test("returns the lowercased, trimmed email claim when present and well-formed", () => {
|
|
expect(getVerifiedEmail({ email: " [email protected] " })).toBe("[email protected]");
|
|
});
|
|
|
|
test("returns null when the email claim is missing", () => {
|
|
expect(getVerifiedEmail({})).toBeNull();
|
|
});
|
|
|
|
test("returns null when the email claim doesn't look like an email (never guesses/trusts a bad claim)", () => {
|
|
expect(getVerifiedEmail({ email: "not-an-email" })).toBeNull();
|
|
});
|
|
});
|