Externalize audit pipeline services
This commit is contained in:
195
tests/leads-runs-auth-source.test.ts
Normal file
195
tests/leads-runs-auth-source.test.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import test from "node:test";
|
||||
|
||||
const source = async (relativePath: string) => {
|
||||
return await readFile(
|
||||
join(process.cwd(), ...relativePath.split("/")),
|
||||
"utf8",
|
||||
);
|
||||
};
|
||||
|
||||
function extractExportSource(sourceText: string, name: string) {
|
||||
const marker = `export const ${name} = `;
|
||||
const declarationIndex = sourceText.indexOf(marker);
|
||||
assert.notEqual(declarationIndex, -1, `Expected declaration for ${name}.`);
|
||||
|
||||
const openBraceIndex = sourceText.indexOf("{", declarationIndex);
|
||||
let depth = 0;
|
||||
let end = -1;
|
||||
|
||||
for (let index = openBraceIndex; index < sourceText.length; index += 1) {
|
||||
const char = sourceText[index];
|
||||
if (char === "{") {
|
||||
depth += 1;
|
||||
} else if (char === "}") {
|
||||
depth -= 1;
|
||||
if (depth === 0) {
|
||||
end = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert.notEqual(end, -1, `Expected balanced braces for ${name}.`);
|
||||
return sourceText.slice(openBraceIndex, end + 1);
|
||||
}
|
||||
|
||||
function assertRequiresOperatorBeforeDataAccess(
|
||||
moduleSource: string,
|
||||
exportName: string,
|
||||
helperName?: string,
|
||||
) {
|
||||
const functionSource = extractExportSource(moduleSource, exportName);
|
||||
const authIndex = functionSource.indexOf("await requireOperator(ctx)");
|
||||
const dbIndex = functionSource.indexOf("ctx.db");
|
||||
const helperIndex = helperName === undefined
|
||||
? -1
|
||||
: functionSource.indexOf(helperName);
|
||||
const dataAccessIndex = dbIndex === -1 ? helperIndex : dbIndex;
|
||||
|
||||
assert.notEqual(
|
||||
authIndex,
|
||||
-1,
|
||||
`${exportName} should call requireOperator before DB access.`,
|
||||
);
|
||||
assert.notEqual(
|
||||
dataAccessIndex,
|
||||
-1,
|
||||
`${exportName} should access ctx.db or call its DB helper.`,
|
||||
);
|
||||
assert.ok(
|
||||
authIndex < dataAccessIndex,
|
||||
`${exportName} should require operator auth before its first data access.`,
|
||||
);
|
||||
}
|
||||
|
||||
test("lead public APIs require operator auth before DB access", async () => {
|
||||
const leadsSource = await source("convex/leads.ts");
|
||||
|
||||
assert.match(
|
||||
leadsSource,
|
||||
/const requireOperator\s*=\s*async\s*\(\s*ctx:\s*(?:MutationCtx\s*\|\s*QueryCtx|QueryCtx\s*\|\s*MutationCtx)\s*\)/,
|
||||
"leads.ts should define a local requireOperator helper.",
|
||||
);
|
||||
assert.match(
|
||||
leadsSource,
|
||||
/ctx\.auth\.getUserIdentity\(\)[\s\S]*throw new Error\(["']Nicht autorisiert\.["']\)/,
|
||||
"requireOperator should derive operator identity from Convex auth.",
|
||||
);
|
||||
|
||||
for (const exportName of [
|
||||
"create",
|
||||
"reviewUpdate",
|
||||
"get",
|
||||
"list",
|
||||
"listFunnel",
|
||||
]) {
|
||||
assertRequiresOperatorBeforeDataAccess(
|
||||
leadsSource,
|
||||
exportName,
|
||||
exportName === "reviewUpdate" ? "reviewUpdateLead" : undefined,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("lead internal APIs exist for audit-generation action callsites", async () => {
|
||||
const [leadsSource, actionSource] = await Promise.all([
|
||||
source("convex/leads.ts"),
|
||||
source("convex/auditGenerationAction.ts"),
|
||||
]);
|
||||
|
||||
assert.match(
|
||||
leadsSource,
|
||||
/import\s*{[\s\S]*internalMutation[\s\S]*internalQuery[\s\S]*}/,
|
||||
"leads.ts should import internal Convex builders.",
|
||||
);
|
||||
assert.match(
|
||||
leadsSource,
|
||||
/export const getInternal\s*=\s*internalQuery\(/,
|
||||
"leads.ts should expose an internal lead get query for actions.",
|
||||
);
|
||||
assert.match(
|
||||
leadsSource,
|
||||
/export const reviewUpdateInternal\s*=\s*internalMutation\(/,
|
||||
"leads.ts should expose an internal lead review mutation for actions.",
|
||||
);
|
||||
assert.match(
|
||||
actionSource,
|
||||
/internal\.leads\.getInternal/,
|
||||
"auditGenerationAction should load leads through internal.leads.getInternal.",
|
||||
);
|
||||
assert.match(
|
||||
actionSource,
|
||||
/internal\.leads\.reviewUpdateInternal/,
|
||||
"auditGenerationAction should update leads through internal.leads.reviewUpdateInternal.",
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
actionSource,
|
||||
/api\.leads\.(get|reviewUpdate)/,
|
||||
"auditGenerationAction should not use public lead APIs for internal calls.",
|
||||
);
|
||||
});
|
||||
|
||||
test("run public APIs require operator auth before DB access", async () => {
|
||||
const runsSource = await source("convex/runs.ts");
|
||||
|
||||
assert.match(
|
||||
runsSource,
|
||||
/const requireOperator\s*=\s*async\s*\(\s*ctx:\s*(?:MutationCtx\s*\|\s*QueryCtx|QueryCtx\s*\|\s*MutationCtx)\s*\)/,
|
||||
"runs.ts should define a local requireOperator helper.",
|
||||
);
|
||||
assert.match(
|
||||
runsSource,
|
||||
/ctx\.auth\.getUserIdentity\(\)[\s\S]*throw new Error\(["']Nicht autorisiert\.["']\)/,
|
||||
"requireOperator should derive operator identity from Convex auth.",
|
||||
);
|
||||
|
||||
for (const exportName of [
|
||||
"create",
|
||||
"updateStatus",
|
||||
"list",
|
||||
"appendEvent",
|
||||
"listEvents",
|
||||
]) {
|
||||
assertRequiresOperatorBeforeDataAccess(
|
||||
runsSource,
|
||||
exportName,
|
||||
exportName === "appendEvent" ? "appendRunEvent" : undefined,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("actions append run events through internal run mutation", async () => {
|
||||
const [runsSource, auditAction, pageSpeedAction, enrichmentAction] =
|
||||
await Promise.all([
|
||||
source("convex/runs.ts"),
|
||||
source("convex/auditGenerationAction.ts"),
|
||||
source("convex/pageSpeedAction.ts"),
|
||||
source("convex/websiteEnrichmentAction.ts"),
|
||||
]);
|
||||
|
||||
assert.match(
|
||||
runsSource,
|
||||
/export const appendEventInternal\s*=\s*internalMutation\(/,
|
||||
"runs.ts should expose an internal append event mutation for actions.",
|
||||
);
|
||||
|
||||
for (const [name, actionSource] of [
|
||||
["auditGenerationAction", auditAction],
|
||||
["pageSpeedAction", pageSpeedAction],
|
||||
["websiteEnrichmentAction", enrichmentAction],
|
||||
] as const) {
|
||||
assert.match(
|
||||
actionSource,
|
||||
/internal\.runs\.appendEventInternal/,
|
||||
`${name} should append events through internal.runs.appendEventInternal.`,
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
actionSource,
|
||||
/api\.runs\.appendEvent/,
|
||||
`${name} should not append events through the public runs API.`,
|
||||
);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user