Skip to content

Commit 22573d3

Browse files
committed
Add tests for whitespace-only resolved identity (CL-6286)
Adversarial review: "string >= 1" on ResolvedCallerSchema is a LENGTH constraint, not a content one -- " " has length 1 and passes it, so a whitespace-only tenantId/principalId was still seated as a "valid" scope. Same bug class PR #34 fixed in optionalEnv (v.length > 0 accepted " "). Extends the malformed-output regression test (deps.test.ts and the end-to-end test in routes.test.ts) to cover a whitespace-only id alongside the empty-string case. Red against the current resolveCaller, which still seats it; the next commit rejects it.
1 parent 311ed87 commit 22573d3

2 files changed

Lines changed: 51 additions & 17 deletions

File tree

‎src/routes/deps.test.ts‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,30 @@ describe("resolveCaller", () => {
230230
expect(sets.tenant).toBeUndefined();
231231
});
232232

233+
test("rejects a whitespace-only tenantId/principalId with 500, never seating it", async () => {
234+
// "string >= 1" is a LENGTH constraint -- " " has length 1 and would
235+
// pass it. This is the same class of bug PR #34 fixed in optionalEnv
236+
// (v.length > 0 accepted " "); this test is the regression guard for
237+
// it at this boundary.
238+
const { ctx, sets, jsonCalls } = fakeContext();
239+
const routeDeps: RouteDeps = {
240+
...deps(grantsWith()),
241+
callerResolver: () => ({ tenantId: " ", principalId: "\t\n" }),
242+
};
243+
let nextCalled = false;
244+
await resolveCaller(routeDeps)(ctx, async () => {
245+
nextCalled = true;
246+
});
247+
expect(nextCalled).toBe(false);
248+
expect(jsonCalls).toHaveLength(1);
249+
expect(jsonCalls[0]?.status).toBe(500);
250+
expect(jsonCalls[0]?.body).toMatchObject({
251+
error: { code: "invalid_resolved_caller" },
252+
});
253+
expect(sets.principal).toBeUndefined();
254+
expect(sets.tenant).toBeUndefined();
255+
});
256+
233257
test("rejects a resolved value missing principalId with 500", async () => {
234258
const { ctx, jsonCalls } = fakeContext();
235259
const routeDeps: RouteDeps = {

‎src/routes/routes.test.ts‎

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -615,23 +615,33 @@ describe("memory HTTP routes — resolver trust-boundary and row-fabrication con
615615
expect(added).toHaveLength(0);
616616
});
617617

618-
test("a resolver returning an empty-string identity is rejected, not seated as a garbage scope", async () => {
619-
// A grant that would (wrongly) authorize the empty-string principal if
620-
// the malformed identity were ever seated — proving rejection happens
621-
// before grantGuard, not that no grant happened to match.
622-
const { app, added } = buildAppWithCallerResolver(
623-
[grant("", "add")],
624-
() => ({ tenantId: "", principalId: "" }),
625-
);
626-
const res = await app.request(
627-
"/api/tenants/t1/memory/add",
628-
jsonPost({ title: "t", text: "body" }),
629-
);
630-
expect(res.status).toBe(500);
631-
const body = (await res.json()) as { error: { code: string } };
632-
expect(body.error.code).toBe("invalid_resolved_caller");
633-
expect(added).toHaveLength(0);
634-
});
618+
test.each([
619+
["empty-string", { tenantId: "", principalId: "" }],
620+
// "string >= 1" would be a LENGTH constraint only -- " " has length 1
621+
// and would pass it, seating a whitespace-only scope wearing the same
622+
// costume as the empty-string case above. This is the regression guard
623+
// for that boundary (same bug class PR #34 fixed in optionalEnv).
624+
["whitespace-only", { tenantId: " ", principalId: "\t\n" }],
625+
] as const)(
626+
"a resolver returning a %s identity is rejected, not seated as a garbage scope",
627+
async (_label, resolved) => {
628+
// A grant that would (wrongly) authorize the malformed principal if
629+
// the identity were ever seated — proving rejection happens before
630+
// grantGuard, not that no grant happened to match.
631+
const { app, added } = buildAppWithCallerResolver(
632+
[grant(resolved.principalId, "add")],
633+
() => resolved,
634+
);
635+
const res = await app.request(
636+
"/api/tenants/t1/memory/add",
637+
jsonPost({ title: "t", text: "body" }),
638+
);
639+
expect(res.status).toBe(500);
640+
const body = (await res.json()) as { error: { code: string } };
641+
expect(body.error.code).toBe("invalid_resolved_caller");
642+
expect(added).toHaveLength(0);
643+
},
644+
);
635645

636646
/**
637647
* `principalRowFor`/`tenantRowFor` (deps.ts) fabricate `PrincipalRow`/

0 commit comments

Comments
 (0)