135 lines
4.9 KiB
JavaScript
135 lines
4.9 KiB
JavaScript
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import assert from "node:assert/strict";
|
|
import { execFileSync } from "node:child_process";
|
|
import { afterEach, describe, it } from "vitest";
|
|
|
|
import { buildInventory, hasInventoryIntegrityFailure, renderText } from "./verify-api-auth-surface.mjs";
|
|
|
|
const fixtures = [];
|
|
|
|
function writeFile(rootDir, relativePath, content) {
|
|
const absolutePath = path.join(rootDir, relativePath);
|
|
fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
|
|
fs.writeFileSync(absolutePath, content);
|
|
}
|
|
|
|
function createFixture() {
|
|
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "api-auth-surface-"));
|
|
fixtures.push(rootDir);
|
|
return rootDir;
|
|
}
|
|
|
|
afterEach(() => {
|
|
while (fixtures.length > 0) {
|
|
fs.rmSync(fixtures.pop(), { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
describe("verify-api-auth-surface", () => {
|
|
it("builds stable JSON inventory for apiService imports and callsites", async () => {
|
|
const rootDir = createFixture();
|
|
writeFile(
|
|
rootDir,
|
|
"apps/api/src/entity-route.ts",
|
|
`import { validateApiKey } from "~/utils/services/apiService";
|
|
export async function loader({ request, params }) {
|
|
return validateApiKey(request, params);
|
|
}
|
|
`,
|
|
);
|
|
writeFile(
|
|
rootDir,
|
|
"apps/api/src/usage-route.ts",
|
|
`import { validateTenantApiKey, getEntityApiKeyFromRequest } from "~/utils/services/apiService";
|
|
export async function loader({ request, params }) {
|
|
await validateTenantApiKey(request, params);
|
|
return getEntityApiKeyFromRequest(request, params);
|
|
}
|
|
`,
|
|
);
|
|
writeFile(
|
|
rootDir,
|
|
"apps/api/src/relationships-route.ts",
|
|
`import { getRelationshipApiKeyFromRequest } from "~/utils/services/apiService";
|
|
export async function action({ request, params }) {
|
|
return getRelationshipApiKeyFromRequest(request, params);
|
|
}
|
|
`,
|
|
);
|
|
writeFile(
|
|
rootDir,
|
|
"apps/api/src/workflows-route.ts",
|
|
`import { getWorkflowApiKeyFromRequest } from "~/utils/services/apiService";
|
|
export async function action({ request, params }) {
|
|
return getWorkflowApiKeyFromRequest(request, params);
|
|
}
|
|
`,
|
|
);
|
|
|
|
const inventory = await buildInventory(rootDir);
|
|
|
|
assert.deepEqual(inventory.scannedFiles, [
|
|
"apps/api/src/entity-route.ts",
|
|
"apps/api/src/relationships-route.ts",
|
|
"apps/api/src/usage-route.ts",
|
|
"apps/api/src/workflows-route.ts",
|
|
]);
|
|
assert.deepEqual(inventory.scannedRoots, ["apps/api/src"]);
|
|
assert.deepEqual(inventory.missingRoots, ["apps/web/app/routes/internal/api"]);
|
|
assert.deepEqual(inventory.requiredMissingRoots, []);
|
|
assert.equal(hasInventoryIntegrityFailure(inventory), false);
|
|
assert.deepEqual(
|
|
inventory.directValidateApiKeyCallsites.map((entry) => `${entry.file}:${entry.line}:${entry.symbol}`),
|
|
["apps/api/src/entity-route.ts:3:validateApiKey"],
|
|
);
|
|
assert.deepEqual(
|
|
inventory.tenantValidateCallsites.map((entry) => `${entry.file}:${entry.line}:${entry.symbol}`),
|
|
["apps/api/src/usage-route.ts:3:validateTenantApiKey"],
|
|
);
|
|
assert.deepEqual(
|
|
inventory.entityResolverCallsites.map((entry) => `${entry.file}:${entry.line}:${entry.symbol}`),
|
|
["apps/api/src/usage-route.ts:4:getEntityApiKeyFromRequest"],
|
|
);
|
|
assert.deepEqual(
|
|
inventory.relationshipHelperCallsites.map((entry) => `${entry.file}:${entry.line}:${entry.symbol}`),
|
|
["apps/api/src/relationships-route.ts:3:getRelationshipApiKeyFromRequest"],
|
|
);
|
|
assert.deepEqual(
|
|
inventory.workflowHelperCallsites.map((entry) => `${entry.file}:${entry.line}:${entry.symbol}`),
|
|
["apps/api/src/workflows-route.ts:3:getWorkflowApiKeyFromRequest"],
|
|
);
|
|
assert.equal(inventory.apiServiceImports.length, 4);
|
|
assert.equal(inventory.centralResolverFile, "apps/web/app/utils/services/apiService.ts");
|
|
assert.doesNotThrow(() => JSON.parse(JSON.stringify(inventory)));
|
|
const text = renderText(inventory);
|
|
assert.match(text, /API Auth Surface API auth surface inventory/);
|
|
assert.match(text, /getRelationshipApiKeyFromRequest callsites/);
|
|
assert.match(text, /getWorkflowApiKeyFromRequest callsites/);
|
|
});
|
|
|
|
it("CLI exits 2 for unknown flags", () => {
|
|
const scriptPath = path.resolve("scripts/verify-api-auth-surface.mjs");
|
|
|
|
assert.throws(
|
|
() => execFileSync(process.execPath, [scriptPath, "--bogus"], { encoding: "utf8", stdio: "pipe" }),
|
|
(error) => {
|
|
assert.equal(error.status, 2);
|
|
assert.match(error.stderr, /Unknown flag: --bogus/);
|
|
return true;
|
|
},
|
|
);
|
|
});
|
|
|
|
it("marks missing required API source roots as inventory failures", async () => {
|
|
const rootDir = createFixture();
|
|
|
|
const inventory = await buildInventory(rootDir);
|
|
|
|
assert.deepEqual(inventory.scannedFiles, []);
|
|
assert.deepEqual(inventory.requiredMissingRoots, ["apps/api/src"]);
|
|
assert.equal(hasInventoryIntegrityFailure(inventory), true);
|
|
});
|
|
});
|