Files
NianAIGC/tests/public-api-auth-contract.test.ts

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();
}
}
});
});