release: WRNexusJS 0.8.0
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { createContext, defineEndpoint } from "../src/index.ts";
|
||||
import { v } from "@wrnexus/validation";
|
||||
|
||||
const user = v.object({ name: v.string().min(2), email: v.string().email() });
|
||||
const endpoint = defineEndpoint({
|
||||
input: user,
|
||||
output: user,
|
||||
handler(input) {
|
||||
return input;
|
||||
},
|
||||
});
|
||||
|
||||
test("typed endpoints unwrap official validation schemas and return bounded validation errors", async () => {
|
||||
const request = new Request("https://example.test/api/user");
|
||||
const ctx = createContext(request, new URL(request.url));
|
||||
const invalid = await endpoint(ctx, { name: "A", email: "bad" });
|
||||
expect(invalid.status).toBe(400);
|
||||
expect(await invalid.json()).toEqual({
|
||||
error: {
|
||||
code: "VALIDATION_ERROR",
|
||||
message: "Endpoint validation failed.",
|
||||
details: {
|
||||
name: "Must be at least 2 characters",
|
||||
email: "Must be a valid email",
|
||||
},
|
||||
},
|
||||
});
|
||||
const valid = await endpoint(ctx, { name: "Ada", email: "ada@example.test" });
|
||||
expect(await valid.json()).toEqual({ data: { name: "Ada", email: "ada@example.test" } });
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { createContext, createExecutionContext, executionContextFromHttp } from "../src/index.ts";
|
||||
|
||||
test("unified execution context spans HTTP and background operations", async () => {
|
||||
const http = createContext(
|
||||
new Request("https://app.test/users"),
|
||||
new URL("https://app.test/users"),
|
||||
);
|
||||
http.lang = "fr";
|
||||
http.user = { id: "u1" };
|
||||
http.locals.db = { users: true };
|
||||
const execution = executionContextFromHttp(http, "action", {
|
||||
authorize: (permission) => {
|
||||
expect(permission).toBe("users.create");
|
||||
},
|
||||
});
|
||||
await execution.authorize("users.create");
|
||||
expect(execution).toMatchObject({
|
||||
kind: "action",
|
||||
locale: "fr",
|
||||
user: { id: "u1" },
|
||||
db: { users: true },
|
||||
});
|
||||
execution.response.setStatus(201);
|
||||
expect(execution.response.status).toBe(201);
|
||||
const queue = createExecutionContext({ kind: "queue", metadata: { job: "email" } });
|
||||
expect(queue.request.url).toBe("https://execution.wrnexus.invalid/queue");
|
||||
});
|
||||
@@ -9,6 +9,9 @@ import {
|
||||
defineFeatureFlags,
|
||||
defineLoader,
|
||||
tenantFromSubdomain,
|
||||
assertTenantAccess,
|
||||
createTenantDirectory,
|
||||
tenantKey,
|
||||
tracingMiddleware,
|
||||
} from "../src/index.ts";
|
||||
|
||||
@@ -37,6 +40,31 @@ test("typed endpoints validate authentication and preserve a stable JSON envelop
|
||||
});
|
||||
});
|
||||
|
||||
test("tenant boundaries, memberships, workspaces, quotas, and audit events fail closed", async () => {
|
||||
const events: string[] = [];
|
||||
const directory = createTenantDirectory({
|
||||
audit: (event) => {
|
||||
events.push(event.action);
|
||||
},
|
||||
now: () => 10,
|
||||
});
|
||||
await directory.addMembership({ tenantId: "acme", userId: "u1", workspaceIds: ["north"] });
|
||||
expect(await directory.switchWorkspace("acme", "u1", "north")).toEqual({
|
||||
tenantId: "acme",
|
||||
workspaceId: "north",
|
||||
});
|
||||
await expect(directory.switchWorkspace("acme", "u1", "south")).rejects.toThrow(
|
||||
"WRN-TENANT-WORKSPACE-DENIED",
|
||||
);
|
||||
directory.setQuota("acme", "storage", 100);
|
||||
expect(() => directory.enforceQuota("acme", "storage", 90, 11)).toThrow("WRN-TENANT-QUOTA");
|
||||
expect(() => assertTenantAccess({ id: "acme" }, { tenantId: "other" })).toThrow(
|
||||
"WRN-TENANT-CROSS-ACCESS",
|
||||
);
|
||||
expect(tenantKey("acme", "cache", 1)).toBe("tenant:acme:cache:1");
|
||||
expect(events).toEqual(["membership.added", "workspace.switched"]);
|
||||
});
|
||||
|
||||
test("loaders, actions, and request-local dedupe remain framework-agnostic", async () => {
|
||||
let calls = 0;
|
||||
const loader = defineLoader({ load: async () => ({ ready: true }) });
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import {
|
||||
Bulkhead,
|
||||
CircuitBreaker,
|
||||
ResilienceError,
|
||||
durationMs,
|
||||
resilientCall,
|
||||
} from "../src/index.ts";
|
||||
|
||||
describe("resilience primitives", () => {
|
||||
test("parses durations and validates bad configuration", () => {
|
||||
expect(durationMs("1.5s")).toBe(1_500);
|
||||
expect(durationMs("2m")).toBe(120_000);
|
||||
expect(() => durationMs("soon" as never)).toThrow("Invalid duration");
|
||||
});
|
||||
|
||||
test("retries with exponential backoff and reports attempts", async () => {
|
||||
const waits: number[] = [];
|
||||
let calls = 0;
|
||||
const value = await resilientCall({
|
||||
retries: 2,
|
||||
retryDelay: 1,
|
||||
backoff: "exponential",
|
||||
onRetry: (_error, _attempt, wait) => waits.push(wait),
|
||||
run: async (_signal, attempt) => {
|
||||
calls += 1;
|
||||
if (attempt < 3) throw new Error("temporary");
|
||||
return "ready";
|
||||
},
|
||||
});
|
||||
expect(value).toBe("ready");
|
||||
expect(calls).toBe(3);
|
||||
expect(waits).toEqual([1, 2]);
|
||||
});
|
||||
|
||||
test("times out cooperative operations and supports fallback", async () => {
|
||||
const value = await resilientCall({
|
||||
timeout: "5ms",
|
||||
fallback: (error) => (error as ResilienceError).code,
|
||||
run: (signal) =>
|
||||
new Promise((_resolve, reject) =>
|
||||
signal.addEventListener("abort", () => reject(signal.reason)),
|
||||
),
|
||||
});
|
||||
expect(value).toBe("WRN-RESILIENCE-TIMEOUT");
|
||||
});
|
||||
|
||||
test("times out integrations that ignore cancellation", async () => {
|
||||
await expect(
|
||||
resilientCall({ timeout: "2ms", run: () => new Promise(() => {}) }),
|
||||
).rejects.toMatchObject({ code: "WRN-RESILIENCE-TIMEOUT" });
|
||||
});
|
||||
|
||||
test("opens a circuit and exposes health", async () => {
|
||||
const breaker = new CircuitBreaker({ failures: 2, resetAfter: "1h" });
|
||||
for (let index = 0; index < 2; index += 1) {
|
||||
await expect(
|
||||
breaker.execute(async () => {
|
||||
throw new Error("down");
|
||||
}),
|
||||
).rejects.toThrow("down");
|
||||
}
|
||||
expect(breaker.snapshot().state).toBe("open");
|
||||
await expect(breaker.execute(async () => "nope")).rejects.toMatchObject({
|
||||
code: "WRN-RESILIENCE-CIRCUIT-OPEN",
|
||||
});
|
||||
});
|
||||
|
||||
test("retains circuit state for a reused declarative configuration", async () => {
|
||||
const circuitBreaker = { failures: 1, resetAfter: "1h" } as const;
|
||||
await expect(
|
||||
resilientCall({
|
||||
circuitBreaker,
|
||||
run: async () => {
|
||||
throw new Error("down");
|
||||
},
|
||||
}),
|
||||
).rejects.toThrow("down");
|
||||
await expect(
|
||||
resilientCall({ circuitBreaker, run: async () => "unreachable" }),
|
||||
).rejects.toMatchObject({ code: "WRN-RESILIENCE-CIRCUIT-OPEN" });
|
||||
});
|
||||
|
||||
test("bulkhead bounds concurrency and queue depth", async () => {
|
||||
const bulkhead = new Bulkhead({ concurrency: 1, queue: 1 });
|
||||
let release!: () => void;
|
||||
const first = bulkhead.execute(
|
||||
() =>
|
||||
new Promise<void>((resolve) => {
|
||||
release = resolve;
|
||||
}),
|
||||
);
|
||||
const second = bulkhead.execute(async () => "second");
|
||||
await expect(bulkhead.execute(async () => "third")).rejects.toMatchObject({
|
||||
code: "WRN-RESILIENCE-BULKHEAD-FULL",
|
||||
});
|
||||
expect(bulkhead.snapshot).toEqual({ active: 1, queued: 1, capacity: 1 });
|
||||
release();
|
||||
await first;
|
||||
expect(await second).toBe("second");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import {
|
||||
createPersistentTenantDirectory,
|
||||
memoryTenantDirectoryStore,
|
||||
migrateTenants,
|
||||
postgresTenantDirectoryStore,
|
||||
} from "../src/index.ts";
|
||||
|
||||
test("persistent tenant directory stores memberships, workspace access and quota usage", async () => {
|
||||
const events: string[] = [];
|
||||
const directory = createPersistentTenantDirectory(memoryTenantDirectoryStore(), {
|
||||
audit: (event) => {
|
||||
events.push(event.action);
|
||||
},
|
||||
});
|
||||
await directory.addMembership({
|
||||
tenantId: "acme",
|
||||
userId: "u1",
|
||||
roles: ["admin"],
|
||||
workspaceIds: ["w1"],
|
||||
});
|
||||
expect(await directory.membership("acme", "u1")).toMatchObject({ roles: ["admin"] });
|
||||
expect(await directory.switchWorkspace("acme", "u1", "w1")).toEqual({
|
||||
tenantId: "acme",
|
||||
workspaceId: "w1",
|
||||
});
|
||||
await directory.setQuota("acme", "projects", 2);
|
||||
expect(await directory.consumeQuota("acme", "projects", 1)).toMatchObject({ usage: 1 });
|
||||
await expect(directory.consumeQuota("acme", "projects", 2)).rejects.toThrow("QUOTA");
|
||||
expect(events).toEqual(["membership.added", "workspace.switched"]);
|
||||
});
|
||||
|
||||
test("tenant migration orchestrator bounds concurrency and reports isolated failures", async () => {
|
||||
let active = 0,
|
||||
peak = 0;
|
||||
const result = await migrateTenants(
|
||||
[{ id: "a" }, { id: "b" }, { id: "bad" }],
|
||||
async (tenant) => {
|
||||
active++;
|
||||
peak = Math.max(peak, active);
|
||||
await Promise.resolve();
|
||||
active--;
|
||||
if (tenant.id === "bad") throw new Error("migration failed");
|
||||
},
|
||||
{ concurrency: 2, continueOnError: true },
|
||||
);
|
||||
expect(result.migrated.sort()).toEqual(["a", "b"]);
|
||||
expect(result.failed[0]?.tenantId).toBe("bad");
|
||||
expect(peak).toBeLessThanOrEqual(2);
|
||||
});
|
||||
|
||||
test("PostgreSQL tenant store parameterizes identities", async () => {
|
||||
const calls: unknown[][] = [];
|
||||
const store = postgresTenantDirectoryStore({
|
||||
async query<T>(_sql: string, params?: unknown[]) {
|
||||
calls.push(params ?? []);
|
||||
return { rows: [] as T[] };
|
||||
},
|
||||
});
|
||||
await store.putMembership({ tenantId: "tenant", userId: "user" });
|
||||
expect(calls[0]?.slice(0, 2)).toEqual(["tenant", "user"]);
|
||||
});
|
||||
Reference in New Issue
Block a user