mirror of
https://github.com/stack-auth/stack.git
synced 2026-07-20 21:29:36 +08:00
Add unit tests for spacetimeDB client error handling
- Introduced a new test suite for the spacetimeDB client, validating the mapping of upstream errors to appropriate assertion and status errors. - Ensured that upstream 4xx errors are converted to HexclaveAssertionError, while 5xx errors are mapped to BadGateway StatusError. - Verified that upstream error bodies are not returned to API callers, maintaining security and integrity of error messages. - Updated the spacetimeDbError function to improve error detail formatting for upstream errors.
This commit is contained in:
parent
10a3cdbfa9
commit
750e01745a
88
apps/internal-tool/src/lib/server/spacetimedb-client.test.ts
Normal file
88
apps/internal-tool/src/lib/server/spacetimedb-client.test.ts
Normal file
@ -0,0 +1,88 @@
|
||||
import { HexclaveAssertionError, StatusError } from "@hexclave/shared/dist/utils/errors";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { handleApiError } from "./route-utils";
|
||||
import { callReducerStrict, callSql } from "./spacetimedb-client";
|
||||
|
||||
// The modules under test import the `server-only` marker package, which throws
|
||||
// when loaded outside a React Server Components bundler context.
|
||||
vi.mock("server-only", () => ({}));
|
||||
|
||||
// A stand-in for what SpacetimeDB actually puts in error bodies: reducer panic
|
||||
// text with module internals that must never reach an API caller.
|
||||
const UPSTREAM_BODY = "panic in reducer update_mcp_qa_review: no such row in mcp_call_log";
|
||||
|
||||
function stubDefaultEnv() {
|
||||
vi.stubEnv("HEXCLAVE_SPACETIMEDB_URL", "http://spacetimedb.example.com");
|
||||
vi.stubEnv("HEXCLAVE_SPACETIMEDB_DB_NAME", "test-db");
|
||||
// getEnvVariable also reads the legacy STACK_-prefixed twins; stub them to
|
||||
// empty (= unset) so a stray var in the runner's environment can't interfere.
|
||||
vi.stubEnv("STACK_SPACETIMEDB_URL", "");
|
||||
vi.stubEnv("STACK_SPACETIMEDB_DB_NAME", "");
|
||||
}
|
||||
|
||||
function stubUpstreamResponse(status: number, body: string) {
|
||||
vi.stubGlobal("fetch", vi.fn(async () => new Response(body, { status })));
|
||||
}
|
||||
|
||||
async function rejectionOf(promise: Promise<unknown>): Promise<unknown> {
|
||||
try {
|
||||
await promise;
|
||||
} catch (err) {
|
||||
return err;
|
||||
}
|
||||
throw new Error("expected promise to reject");
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
describe.each([
|
||||
{ name: "callReducerStrict", call: () => callReducerStrict("token", "some_reducer", []) },
|
||||
{ name: "callSql", call: () => callSql("token", "SELECT 1") },
|
||||
])("$name upstream error mapping", ({ call }) => {
|
||||
it("maps an upstream 4xx to an assertion error, never a client StatusError", async () => {
|
||||
stubDefaultEnv();
|
||||
stubUpstreamResponse(400, UPSTREAM_BODY);
|
||||
|
||||
const err = await rejectionOf(call());
|
||||
// A client (4xx) StatusError would make handleApiError return the message
|
||||
// — including the upstream body — verbatim to the API caller.
|
||||
expect(err).toBeInstanceOf(HexclaveAssertionError);
|
||||
expect(String((err as Error).message)).toContain(UPSTREAM_BODY);
|
||||
});
|
||||
|
||||
it("maps an upstream 5xx to a BadGateway StatusError, keeping the body in the message for logs", async () => {
|
||||
stubDefaultEnv();
|
||||
stubUpstreamResponse(503, UPSTREAM_BODY);
|
||||
|
||||
const err = await rejectionOf(call());
|
||||
expect(StatusError.isStatusError(err)).toBe(true);
|
||||
expect((err as StatusError).statusCode).toBe(502);
|
||||
expect((err as StatusError).isClientError()).toBe(false);
|
||||
expect((err as Error).message).toContain(UPSTREAM_BODY);
|
||||
});
|
||||
});
|
||||
|
||||
describe("handleApiError responses for upstream failures", () => {
|
||||
it("never returns the upstream body to the API caller", async () => {
|
||||
stubDefaultEnv();
|
||||
for (const upstreamStatus of [400, 401, 404, 500, 503]) {
|
||||
stubUpstreamResponse(upstreamStatus, UPSTREAM_BODY);
|
||||
const err = await rejectionOf(callReducerStrict("token", "some_reducer", []));
|
||||
|
||||
const res = handleApiError("test-scope", err);
|
||||
const body = await res.text();
|
||||
expect(res.status).toBeGreaterThanOrEqual(500);
|
||||
expect(body).not.toContain(UPSTREAM_BODY);
|
||||
expect(body).not.toContain("some_reducer");
|
||||
}
|
||||
});
|
||||
|
||||
it("still returns authored client StatusError messages verbatim", async () => {
|
||||
const res = handleApiError("test-scope", new StatusError(StatusError.BadRequest, "Request body must be valid JSON."));
|
||||
expect(res.status).toBe(400);
|
||||
expect(await res.text()).toBe("Request body must be valid JSON.");
|
||||
});
|
||||
});
|
||||
@ -46,10 +46,8 @@ export async function callReducerStrict(accessToken: string, reducer: string, ar
|
||||
}
|
||||
|
||||
function spacetimeDbError(label: string, status: number, preview: string): Error {
|
||||
const detail = `${label} (${status}): ${preview}`;
|
||||
if (status >= 400 && status < 500) return new StatusError(status, detail);
|
||||
if (status >= 500) return new StatusError(StatusError.BadGateway, `${label} (upstream ${status})`);
|
||||
return new HexclaveAssertionError(detail);
|
||||
if (status >= 500) return new StatusError(StatusError.BadGateway, `${label} (upstream ${status}): ${preview}`);
|
||||
return new HexclaveAssertionError(`${label} (upstream ${status}): ${preview}`);
|
||||
}
|
||||
|
||||
export function opt<T>(value: T | null | undefined): { some: T } | { none: [] } {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user