Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

graphql.test.ts

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

graphql.test.tsBlame162 lines · 1 contributor
eae38d1Claude1/**
2 * Block G2 — GraphQL parser + endpoint smoke tests.
3 */
4
5import { describe, it, expect } from "bun:test";
6import app from "../app";
7import { parseQuery, execute } from "../lib/graphql";
8
9describe("graphql — parseQuery", () => {
10 it("parses a bare selection set", () => {
11 const r = parseQuery("{ viewer { id username } }");
12 expect(r.ok).toBe(true);
13 if (r.ok) {
14 expect(r.fields.length).toBe(1);
15 expect(r.fields[0].name).toBe("viewer");
16 expect(r.fields[0].selections.map((s) => s.name)).toEqual([
17 "id",
18 "username",
19 ]);
20 }
21 });
22
23 it("parses a query with operation keyword", () => {
24 const r = parseQuery("query Foo { rateLimit { remaining } }");
25 expect(r.ok).toBe(true);
26 if (r.ok) {
27 expect(r.fields[0].name).toBe("rateLimit");
28 }
29 });
30
31 it("parses string args", () => {
32 const r = parseQuery('{ user(username:"alice") { id } }');
33 expect(r.ok).toBe(true);
34 if (r.ok) {
35 expect(r.fields[0].args.username).toBe("alice");
36 }
37 });
38
39 it("parses number + boolean args", () => {
40 const r = parseQuery('{ search(q:"x", limit:5) { id } }');
41 expect(r.ok).toBe(true);
42 if (r.ok) {
43 expect(r.fields[0].args.limit).toBe(5);
44 }
45 });
46
47 it("parses aliases", () => {
48 const r = parseQuery("{ me:viewer { id } }");
49 expect(r.ok).toBe(true);
50 if (r.ok) {
51 expect(r.fields[0].name).toBe("viewer");
52 expect(r.fields[0].alias).toBe("me");
53 }
54 });
55
56 it("parses nested selections", () => {
57 const r = parseQuery(
58 `{ repository(owner:"alice", name:"repo") { owner { username } issues(state:"open", limit:5) { title } } }`
59 );
60 expect(r.ok).toBe(true);
61 if (r.ok) {
62 const repo = r.fields[0];
63 expect(repo.name).toBe("repository");
64 const owner = repo.selections.find((s) => s.name === "owner");
65 expect(owner).toBeDefined();
66 expect(owner!.selections.map((s) => s.name)).toEqual(["username"]);
67 }
68 });
69
70 it("skips comments + commas", () => {
71 const r = parseQuery("{ # comment\n viewer, { id, username } }");
72 expect(r.ok).toBe(true);
73 });
74
75 it("returns an error on malformed input", () => {
76 const r = parseQuery("{ viewer {");
77 expect(r.ok).toBe(false);
78 });
79});
80
81describe("graphql — execute", () => {
82 it("returns data for rateLimit (no-side-effect field)", async () => {
83 const r = await execute("{ rateLimit { remaining reset } }", { user: null });
84 expect(r.data).toBeDefined();
85 expect(r.data?.rateLimit).toBeDefined();
86 expect(typeof r.data?.rateLimit.remaining).toBe("number");
87 expect(typeof r.data?.rateLimit.reset).toBe("number");
88 });
89
90 it("viewer returns null without auth", async () => {
91 const r = await execute("{ viewer { id } }", { user: null });
92 expect(r.data?.viewer).toBe(null);
93 });
94
95 it("unknown root field → error + null data", async () => {
96 const r = await execute("{ bogus { id } }", { user: null });
97 expect(r.errors).toBeDefined();
98 expect(r.errors![0].message).toContain("bogus");
99 expect(r.data?.bogus).toBe(null);
100 });
101
102 it("parse error surfaces in errors", async () => {
103 const r = await execute("{ viewer {", { user: null });
104 expect(r.errors).toBeDefined();
105 });
106
107 it("user(username) on nonexistent returns null", async () => {
108 const r = await execute(
109 '{ user(username:"__zzzz_doesnt_exist") { id } }',
110 { user: null }
111 );
112 expect(r.data?.user).toBe(null);
113 });
114
115 it("repository on nonexistent returns null", async () => {
116 const r = await execute(
117 '{ repository(owner:"__nope", name:"__nope") { id } }',
118 { user: null }
119 );
120 expect(r.data?.repository).toBe(null);
121 });
122});
123
124describe("graphql — HTTP endpoint", () => {
125 it("POST /api/graphql with empty query → 400", async () => {
126 const res = await app.request("/api/graphql", {
127 method: "POST",
128 headers: { "content-type": "application/json" },
129 body: JSON.stringify({ query: "" }),
130 });
131 expect(res.status).toBe(400);
132 });
133
134 it("POST /api/graphql with invalid JSON → 400", async () => {
135 const res = await app.request("/api/graphql", {
136 method: "POST",
137 headers: { "content-type": "application/json" },
138 body: "{not json",
139 });
140 expect(res.status).toBe(400);
141 });
142
143 it("POST /api/graphql rateLimit query returns JSON", async () => {
144 const res = await app.request("/api/graphql", {
145 method: "POST",
146 headers: { "content-type": "application/json" },
147 body: JSON.stringify({ query: "{ rateLimit { remaining } }" }),
148 });
149 expect(res.status).toBe(200);
150 const body = (await res.json()) as any;
151 expect(body.data.rateLimit.remaining).toBeGreaterThan(0);
152 });
153
154 it("GET /api/graphql serves a GraphiQL-lite explorer page", async () => {
155 const res = await app.request("/api/graphql");
156 expect(res.status).toBe(200);
157 expect(res.headers.get("content-type") || "").toContain("text/html");
158 const html = await res.text();
159 expect(html).toContain("gluecron");
160 expect(html).toContain("/api/graphql");
161 });
162});