Initial Commit
This commit is contained in:
59
e2e/foundation-stack-engines/tests/engines-coexist.spec.ts
Normal file
59
e2e/foundation-stack-engines/tests/engines-coexist.spec.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
/**
|
||||
* Stage-7 closure smoke — proves that the four Stage-7 engines
|
||||
* (`api-graphql-yoga`, `api-graphql-apollo`, `api-ws-bun`,
|
||||
* `api-sse-bun`) coexist on a single Bun.serve host without route
|
||||
* collision, and that GraphQL federation across two plugins works
|
||||
* identically through both engines.
|
||||
*/
|
||||
|
||||
const FED_QUERY = `
|
||||
query Federated {
|
||||
pluginA
|
||||
pluginB
|
||||
bothA
|
||||
bothB
|
||||
}
|
||||
`;
|
||||
|
||||
interface GraphqlResponse {
|
||||
readonly data?: Record<string, unknown> | null;
|
||||
readonly errors?: ReadonlyArray<{ readonly message: string }>;
|
||||
}
|
||||
|
||||
test.describe("foundation-stack-engines — Stage-7 coexistence", () => {
|
||||
test("1. yoga federates GraphQL across plugin-a + plugin-b", async ({
|
||||
request,
|
||||
}) => {
|
||||
const res = await request.post("/graphql/yoga", {
|
||||
data: { query: FED_QUERY, operationName: "Federated" },
|
||||
});
|
||||
expect(res.status()).toBe(200);
|
||||
const body = (await res.json()) as GraphqlResponse;
|
||||
expect(body.errors).toBeUndefined();
|
||||
expect(body.data).toEqual({
|
||||
pluginA: "from plugin-a",
|
||||
pluginB: "from plugin-b",
|
||||
bothA: "A-half-of-both",
|
||||
bothB: "B-half-of-both",
|
||||
});
|
||||
});
|
||||
|
||||
test("2. apollo federates the same SDL across plugin-a + plugin-b", async ({
|
||||
request,
|
||||
}) => {
|
||||
const res = await request.post("/graphql/apollo", {
|
||||
data: { query: FED_QUERY, operationName: "Federated" },
|
||||
});
|
||||
expect(res.status()).toBe(200);
|
||||
const body = (await res.json()) as GraphqlResponse;
|
||||
expect(body.errors).toBeUndefined();
|
||||
expect(body.data).toEqual({
|
||||
pluginA: "from plugin-a",
|
||||
pluginB: "from plugin-b",
|
||||
bothA: "A-half-of-both",
|
||||
bothB: "B-half-of-both",
|
||||
});
|
||||
});
|
||||
});
|
||||
33
e2e/foundation-stack-engines/tests/engines-listing.spec.ts
Normal file
33
e2e/foundation-stack-engines/tests/engines-listing.spec.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
/**
|
||||
* Stage-7 closure smoke — proves all four engines are mounted on the
|
||||
* SAME Bun.serve listener at distinct routes (no path collision).
|
||||
*/
|
||||
|
||||
interface EngineDescriptor {
|
||||
readonly name: string;
|
||||
readonly path: string;
|
||||
readonly method: string;
|
||||
}
|
||||
|
||||
test.describe("foundation-stack-engines — all engines mounted", () => {
|
||||
test("5. /internal/engines lists yoga + apollo + ws-bun + sse-bun", async ({
|
||||
request,
|
||||
}) => {
|
||||
const res = await request.get("/internal/engines");
|
||||
expect(res.status()).toBe(200);
|
||||
const body = (await res.json()) as {
|
||||
engines: ReadonlyArray<EngineDescriptor>;
|
||||
host: { wsConnections: number };
|
||||
};
|
||||
const names = body.engines.map((e) => e.name).sort();
|
||||
expect(names).toEqual(["apollo", "sse-bun", "ws-bun", "yoga"]);
|
||||
const paths = body.engines.map((e) => e.path);
|
||||
expect(new Set(paths).size).toBe(paths.length); // no collisions
|
||||
expect(paths).toContain("/graphql/yoga");
|
||||
expect(paths).toContain("/graphql/apollo");
|
||||
expect(paths).toContain("/ws");
|
||||
expect(paths).toContain("/sse");
|
||||
});
|
||||
});
|
||||
87
e2e/foundation-stack-engines/tests/sse-stream.spec.ts
Normal file
87
e2e/foundation-stack-engines/tests/sse-stream.spec.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
/**
|
||||
* Stage-7 closure smoke — `api-sse-bun` event stream.
|
||||
*
|
||||
* Opens a real `fetch` against `/sse` to attach as an SSE subscriber,
|
||||
* then publishes via `/sse/publish` and asserts that the stream body
|
||||
* carries the expected `data:` frame.
|
||||
*/
|
||||
|
||||
async function readUntilEvent(
|
||||
body: ReadableStream<Uint8Array>,
|
||||
pattern: RegExp,
|
||||
timeoutMs = 5000
|
||||
): Promise<string> {
|
||||
const reader = body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = "";
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
try {
|
||||
while (Date.now() < deadline) {
|
||||
const { value, done } = await reader.read();
|
||||
if (done) break;
|
||||
if (value !== undefined) {
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
if (pattern.test(buffer)) {
|
||||
return buffer;
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
try {
|
||||
await reader.cancel();
|
||||
} catch {
|
||||
/* idempotent */
|
||||
}
|
||||
}
|
||||
throw new Error(
|
||||
`sse stream did not yield ${pattern} within ${timeoutMs}ms; got: ${buffer}`
|
||||
);
|
||||
}
|
||||
|
||||
test.describe("foundation-stack-engines — sse-bun stream", () => {
|
||||
test("4. sse subscriber receives a published event", async ({
|
||||
request,
|
||||
baseURL,
|
||||
}) => {
|
||||
const ac = new AbortController();
|
||||
const url = `${baseURL}/sse?tenant=tenant-a&topic=ticker`;
|
||||
const sseResponse = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
accept: "text/event-stream",
|
||||
"accept-encoding": "identity",
|
||||
},
|
||||
signal: ac.signal,
|
||||
});
|
||||
expect(sseResponse.status).toBe(200);
|
||||
expect(sseResponse.headers.get("content-type")).toContain(
|
||||
"text/event-stream"
|
||||
);
|
||||
expect(sseResponse.body).not.toBeNull();
|
||||
|
||||
// Give the handshake pipeline a moment to register the
|
||||
// subscription on the engine's bus.
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, 200));
|
||||
|
||||
const pub = await request.post("/sse/publish", {
|
||||
data: {
|
||||
topic: "ticker",
|
||||
tenant: "tenant-a",
|
||||
data: "hello-sse",
|
||||
id: "evt-1",
|
||||
},
|
||||
});
|
||||
expect(pub.status()).toBe(200);
|
||||
const pubBody = (await pub.json()) as { delivered: number };
|
||||
expect(pubBody.delivered).toBeGreaterThanOrEqual(1);
|
||||
|
||||
const stream = sseResponse.body as ReadableStream<Uint8Array>;
|
||||
const seen = await readUntilEvent(stream, /data:\s*hello-sse/);
|
||||
expect(seen).toContain("data: hello-sse");
|
||||
expect(seen).toContain("id: evt-1");
|
||||
|
||||
ac.abort();
|
||||
});
|
||||
});
|
||||
122
e2e/foundation-stack-engines/tests/ws-tenant-isolation.spec.ts
Normal file
122
e2e/foundation-stack-engines/tests/ws-tenant-isolation.spec.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
/**
|
||||
* Stage-7 closure smoke — `api-ws-bun` tenant-scoped broadcast.
|
||||
*
|
||||
* Two clients connect to `/ws` on different tenants (`tenant-a` /
|
||||
* `tenant-b`) and subscribe to the SAME topic (`updates`). A publish
|
||||
* scoped to `tenant-a` MUST reach only the tenant-a client; the
|
||||
* tenant-b client MUST NOT see it. This proves the
|
||||
* `SubscriptionBus.broadcast` tenant key — design §11.4 — is honoured
|
||||
* by the engine's `broadcast(...)` helper.
|
||||
*/
|
||||
|
||||
interface WsConnection {
|
||||
readonly ws: WebSocket;
|
||||
readonly received: unknown[];
|
||||
close(): void;
|
||||
}
|
||||
|
||||
async function openWs(
|
||||
baseURL: string,
|
||||
tenant: string,
|
||||
topic: string
|
||||
): Promise<WsConnection> {
|
||||
const wsUrl = baseURL.replace(/^http/, "ws");
|
||||
const ws = new WebSocket(
|
||||
`${wsUrl}/ws?tenant=${encodeURIComponent(tenant)}&topic=${encodeURIComponent(topic)}`
|
||||
);
|
||||
const received: unknown[] = [];
|
||||
ws.addEventListener("message", (ev) => {
|
||||
try {
|
||||
received.push(JSON.parse(String(ev.data)));
|
||||
} catch {
|
||||
received.push(ev.data);
|
||||
}
|
||||
});
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const timer = setTimeout(
|
||||
() => reject(new Error("ws open timeout")),
|
||||
5000
|
||||
);
|
||||
ws.addEventListener(
|
||||
"open",
|
||||
() => {
|
||||
clearTimeout(timer);
|
||||
resolve();
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
ws.addEventListener(
|
||||
"error",
|
||||
(e) => {
|
||||
clearTimeout(timer);
|
||||
reject(new Error(`ws error: ${String(e)}`));
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
});
|
||||
return {
|
||||
ws,
|
||||
received,
|
||||
close() {
|
||||
try {
|
||||
ws.close();
|
||||
} catch {
|
||||
/* idempotent */
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function settle(ms = 200): Promise<void> {
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
test.describe("foundation-stack-engines — ws-bun tenant isolation", () => {
|
||||
test("3. tenant-A broadcast reaches only tenant-A subscriber, never tenant-B", async ({
|
||||
request,
|
||||
baseURL,
|
||||
}) => {
|
||||
expect(baseURL).toBeDefined();
|
||||
const clientA = await openWs(baseURL as string, "tenant-a", "updates");
|
||||
const clientB = await openWs(baseURL as string, "tenant-b", "updates");
|
||||
|
||||
try {
|
||||
// Let the engine.attach + subscribe calls settle.
|
||||
await settle(150);
|
||||
|
||||
const publish = await request.post("/ws/publish", {
|
||||
data: {
|
||||
topic: "updates",
|
||||
tenant: "tenant-a",
|
||||
payload: { hello: "from-tenant-a" },
|
||||
},
|
||||
});
|
||||
expect(publish.status()).toBe(200);
|
||||
const pBody = (await publish.json()) as { delivered: number };
|
||||
// Exactly one subscriber (the tenant-a client) should match.
|
||||
expect(pBody.delivered).toBe(1);
|
||||
|
||||
// Allow the send pipeline to flush over the socket.
|
||||
await settle(300);
|
||||
|
||||
expect(clientA.received.length).toBe(1);
|
||||
const env = clientA.received[0] as {
|
||||
type?: string;
|
||||
topic?: string;
|
||||
payload?: Record<string, unknown>;
|
||||
};
|
||||
expect(env.type).toBe("broadcast");
|
||||
expect(env.topic).toBe("updates");
|
||||
expect(env.payload).toEqual({ hello: "from-tenant-a" });
|
||||
|
||||
// Tenant-B subscriber must NOT have received the tenant-A
|
||||
// broadcast — proves cross-tenant isolation.
|
||||
expect(clientB.received.length).toBe(0);
|
||||
} finally {
|
||||
clientA.close();
|
||||
clientB.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user