Initial Commit
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
import { Router } from "@suaveplan/api-router";
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import {
|
||||
createGraphiqlUi,
|
||||
GRAPHQL_UI_GRAPHIQL_CODES,
|
||||
type GraphiqlRouteHandler,
|
||||
type RouterResponseValue,
|
||||
} from "../../src/index.js";
|
||||
|
||||
/**
|
||||
* End-to-end integration test.
|
||||
*
|
||||
* Wires the GraphiQL UI plugin into an in-memory `Router` alongside a
|
||||
* minimal POST `/graphql` handler that echoes a canned response, then
|
||||
* exercises the full request flow:
|
||||
* 1. GET `/graphql/ui` -> HTML shell (status 200, CSP header, hashed
|
||||
* asset references embedded).
|
||||
* 2. GET `/graphql/ui/assets/<hashed-name>` -> bundled JS / CSS
|
||||
* payload (status 200, immutable cache, no CDN refs).
|
||||
* 3. POST `/graphql` -> JSON response (the explorer would issue this
|
||||
* via `fetch` once the user clicks "Run"; here we drive it
|
||||
* directly so the integration scope stays in-process).
|
||||
*
|
||||
* The umbrella Stage-7 plan calls for a Playwright-driven browser test
|
||||
* once `api-graphql-yoga` is wired. This test stands in until that
|
||||
* package lands — it pins every contract this plugin is responsible
|
||||
* for and stays meaningful without a real GraphQL engine.
|
||||
*/
|
||||
|
||||
type ProtocolHandler = (request: {
|
||||
readonly method: string;
|
||||
readonly path: string;
|
||||
readonly body?: string;
|
||||
}) => RouterResponseValue;
|
||||
|
||||
function dispatch(
|
||||
router: Router,
|
||||
method: "GET" | "POST",
|
||||
path: string,
|
||||
body?: string
|
||||
): RouterResponseValue {
|
||||
const match = router.match(method, path);
|
||||
if (match === null) throw new Error(`no match for ${method} ${path}`);
|
||||
const handler = match.handler as unknown as
|
||||
| GraphiqlRouteHandler
|
||||
| ProtocolHandler;
|
||||
return handler({ method: match.method, path, body });
|
||||
}
|
||||
|
||||
describe("api-graphql-ui-graphiql — integration", () => {
|
||||
it("boots end-to-end: shell loads, asset serves, query round-trips", {
|
||||
timeout: 30000,
|
||||
}, () => {
|
||||
const router = new Router({ plugin: "host" });
|
||||
|
||||
// Minimal /graphql endpoint — JSON in, JSON out.
|
||||
const protocolHandler: ProtocolHandler = (req) => {
|
||||
const parsed: { query?: string } = req.body
|
||||
? (JSON.parse(req.body) as { query?: string })
|
||||
: {};
|
||||
return {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
data: { echo: parsed.query ?? null },
|
||||
}),
|
||||
};
|
||||
};
|
||||
router.post(
|
||||
"/graphql",
|
||||
protocolHandler as unknown as Parameters<Router["post"]>[1]
|
||||
);
|
||||
|
||||
createGraphiqlUi({}).register(router);
|
||||
|
||||
// 1. Shell loads at /graphql/ui.
|
||||
const shell = dispatch(router, "GET", "/graphql/ui");
|
||||
expect(shell.status).toBe(200);
|
||||
expect(shell.headers["Content-Security-Policy"]).toContain(
|
||||
"default-src 'self'"
|
||||
);
|
||||
expect(shell.body).toContain("<!DOCTYPE html>");
|
||||
// No external host references.
|
||||
expect(shell.body).not.toMatch(/https?:\/\//);
|
||||
|
||||
// Extract the JS asset filename from the shell.
|
||||
const jsMatch = shell.body.match(
|
||||
/\/graphql\/ui\/assets\/(graphiql\.[0-9a-f]{8}\.js)/
|
||||
);
|
||||
expect(jsMatch).not.toBeNull();
|
||||
const jsFilename = jsMatch?.[1] ?? "";
|
||||
expect(jsFilename.length).toBeGreaterThan(0);
|
||||
|
||||
// 2. Asset round-trips.
|
||||
const asset = dispatch(
|
||||
router,
|
||||
"GET",
|
||||
`/graphql/ui/assets/${jsFilename}`
|
||||
);
|
||||
expect(asset.status).toBe(200);
|
||||
expect(asset.headers["Content-Type"]).toContain(
|
||||
"application/javascript"
|
||||
);
|
||||
expect(asset.headers["Cache-Control"]).toBe(
|
||||
"public, max-age=31536000, immutable"
|
||||
);
|
||||
expect(asset.body.length).toBeGreaterThan(0);
|
||||
|
||||
// 3. Query round-trips through the /graphql protocol endpoint
|
||||
// — exactly what the in-browser explorer does on "Run".
|
||||
const protocol = dispatch(
|
||||
router,
|
||||
"POST",
|
||||
"/graphql",
|
||||
JSON.stringify({ query: "{ ping }" })
|
||||
);
|
||||
expect(protocol.status).toBe(200);
|
||||
const decoded = JSON.parse(protocol.body) as {
|
||||
data?: { echo?: string };
|
||||
};
|
||||
expect(decoded.data?.echo).toBe("{ ping }");
|
||||
});
|
||||
|
||||
it("stale-hash asset names resolve during a rolling deploy", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const router = new Router({ plugin: "host" });
|
||||
createGraphiqlUi({}).register(router);
|
||||
const stale = dispatch(
|
||||
router,
|
||||
"GET",
|
||||
"/graphql/ui/assets/graphiql.deadbeef.js"
|
||||
);
|
||||
expect(stale.status).toBe(200);
|
||||
expect(stale.body.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("asset endpoint rejects unknown filenames with ASSET_MISSING", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const router = new Router({ plugin: "host" });
|
||||
createGraphiqlUi({}).register(router);
|
||||
try {
|
||||
dispatch(router, "GET", "/graphql/ui/assets/unknown.deadbeef.png");
|
||||
throw new Error("expected throw");
|
||||
} catch (err) {
|
||||
const code = (err as { code?: string }).code;
|
||||
expect(code).toBe(GRAPHQL_UI_GRAPHIQL_CODES.ASSET_MISSING);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user