113 lines
3.9 KiB
TypeScript
113 lines
3.9 KiB
TypeScript
import { readFile } from "node:fs/promises";
|
|
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
|
|
import {
|
|
PublicApiAuthError,
|
|
assertInternalWorkerToken,
|
|
authenticatePublicApiRequest,
|
|
getPublicApiClients,
|
|
publicApiOwnerId,
|
|
type PublicApiClient
|
|
} from "@/lib/server/public-api-auth";
|
|
|
|
type AuthError = { status: number; message: string };
|
|
type HeaderMap = Record<string, string>;
|
|
type PublicApiAuthFixture = {
|
|
version: 1;
|
|
apiKeys: string;
|
|
clients: PublicApiClient[];
|
|
authenticationCases: Array<{
|
|
name: string;
|
|
headers: HeaderMap;
|
|
expected?: { client: PublicApiClient; owner: string };
|
|
error?: AuthError;
|
|
}>;
|
|
ownerCases: Array<{ id: string; owner: string; maxPartLength?: number }>;
|
|
workerCases: Array<{
|
|
name: string;
|
|
production: boolean;
|
|
configuredToken: string;
|
|
headers: HeaderMap;
|
|
allowed?: boolean;
|
|
error?: AuthError;
|
|
}>;
|
|
};
|
|
|
|
const fixtureUrl = new URL("../contracts/auth/public-api-auth-v1.json", import.meta.url);
|
|
const environmentKeys = ["ZHINIAN_API_KEYS", "ZHINIAN_INTERNAL_WORKER_TOKEN", "NODE_ENV"] as const;
|
|
const originalEnvironment = new Map(environmentKeys.map((key) => [key, process.env[key]]));
|
|
|
|
afterEach(() => {
|
|
for (const key of environmentKeys) {
|
|
const value = originalEnvironment.get(key);
|
|
if (value === undefined) Reflect.deleteProperty(process.env, key);
|
|
else Reflect.set(process.env, key, value);
|
|
}
|
|
});
|
|
|
|
async function loadFixture(): Promise<PublicApiAuthFixture> {
|
|
return JSON.parse(await readFile(fixtureUrl, "utf8")) as PublicApiAuthFixture;
|
|
}
|
|
|
|
function request(headers: HeaderMap): Request {
|
|
return new Request("https://api.example.test/api/v1/generations", { headers });
|
|
}
|
|
|
|
function expectTypedError(action: () => unknown, expected: AuthError) {
|
|
try {
|
|
action();
|
|
throw new Error("expected PublicApiAuthError");
|
|
} catch (error) {
|
|
expect(error).toBeInstanceOf(PublicApiAuthError);
|
|
expect(error).toMatchObject(expected);
|
|
}
|
|
}
|
|
|
|
describe("public API authentication v1 cross-language contract", () => {
|
|
it("freezes comma/newline parsing, trimming, default ids, and empty filtering", async () => {
|
|
const fixture = await loadFixture();
|
|
process.env.ZHINIAN_API_KEYS = fixture.apiKeys;
|
|
|
|
expect(fixture.version).toBe(1);
|
|
expect(getPublicApiClients()).toEqual(fixture.clients);
|
|
});
|
|
|
|
it("freezes public credential transport, precedence, errors, client, and owner", async () => {
|
|
const fixture = await loadFixture();
|
|
process.env.ZHINIAN_API_KEYS = fixture.apiKeys;
|
|
|
|
for (const testCase of fixture.authenticationCases) {
|
|
if (testCase.error) {
|
|
expectTypedError(() => authenticatePublicApiRequest(request(testCase.headers)), testCase.error);
|
|
continue;
|
|
}
|
|
const client = authenticatePublicApiRequest(request(testCase.headers));
|
|
expect(client, testCase.name).toEqual(testCase.expected?.client);
|
|
expect(publicApiOwnerId(client), testCase.name).toBe(testCase.expected?.owner);
|
|
}
|
|
});
|
|
|
|
it("sanitizes and bounds public owner ids", async () => {
|
|
const fixture = await loadFixture();
|
|
for (const testCase of fixture.ownerCases) {
|
|
const owner = publicApiOwnerId(testCase.id);
|
|
expect(owner, testCase.id).toBe(testCase.owner);
|
|
if (testCase.maxPartLength) expect(owner.slice(4)).toHaveLength(testCase.maxPartLength);
|
|
}
|
|
});
|
|
|
|
it("freezes internal worker development bypass and production failure semantics", async () => {
|
|
const fixture = await loadFixture();
|
|
for (const testCase of fixture.workerCases) {
|
|
Reflect.set(process.env, "NODE_ENV", testCase.production ? "production" : "development");
|
|
Reflect.set(process.env, "ZHINIAN_INTERNAL_WORKER_TOKEN", testCase.configuredToken);
|
|
if (testCase.error) {
|
|
expectTypedError(() => assertInternalWorkerToken(request(testCase.headers)), testCase.error);
|
|
} else {
|
|
expect(() => assertInternalWorkerToken(request(testCase.headers)), testCase.name).not.toThrow();
|
|
}
|
|
}
|
|
});
|
|
});
|