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

api-v2.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.

api-v2.test.tsBlame644 lines · 1 contributor
3e8f8e8Claude1import { describe, it, expect, beforeAll, afterAll } from "bun:test";
2import { join } from "path";
3import { rm, mkdir } from "fs/promises";
4import app from "../app";
5import { clearRateLimitStore } from "../middleware/rate-limit";
6
7const TEST_REPOS = join(import.meta.dir, "../../.test-repos-api-v2");
8
9beforeAll(async () => {
10 process.env.GIT_REPOS_PATH = TEST_REPOS;
11 process.env.DATABASE_URL = process.env.DATABASE_URL || "";
12 clearRateLimitStore();
13 await mkdir(TEST_REPOS, { recursive: true });
14});
15
16afterAll(async () => {
17 await rm(TEST_REPOS, { recursive: true, force: true });
18});
19
20// ---------------------------------------------------------------------------
21// Helpers
22// ---------------------------------------------------------------------------
23
24function apiUrl(path: string): string {
25 if (path === "/") return "/api/v2";
26 return `/api/v2${path}`;
27}
28
29function jsonHeaders(extra: Record<string, string> = {}): Record<string, string> {
30 return { "Content-Type": "application/json", ...extra };
31}
32
33function bearerHeader(token: string): Record<string, string> {
34 return { Authorization: `Bearer ${token}` };
35}
36
37// ---------------------------------------------------------------------------
38// 1. API Info endpoint
39// ---------------------------------------------------------------------------
40
41describe("API v2 - Info endpoint", () => {
42 it("GET /api/v2/ returns 200 with JSON listing all endpoints", async () => {
43 const res = await app.request(apiUrl("/"));
44 expect(res.status).toBe(200);
45
46 const body = await res.json();
47 expect(body.name).toBe("gluecron API");
48 expect(body.version).toBe("2.0");
49 expect(body.endpoints).toBeDefined();
50 expect(body.endpoints.users).toBeDefined();
51 expect(body.endpoints.repositories).toBeDefined();
52 expect(body.endpoints.branches).toBeDefined();
53 expect(body.endpoints.commits).toBeDefined();
54 expect(body.endpoints.files).toBeDefined();
55 expect(body.endpoints.issues).toBeDefined();
56 expect(body.endpoints.pullRequests).toBeDefined();
57 expect(body.endpoints.stars).toBeDefined();
58 expect(body.endpoints.labels).toBeDefined();
59 expect(body.endpoints.search).toBeDefined();
60 expect(body.endpoints.topics).toBeDefined();
61 expect(body.endpoints.webhooks).toBeDefined();
62 expect(body.endpoints.activity).toBeDefined();
63 expect(body.authentication).toBeDefined();
64 expect(body.rateLimit).toBeDefined();
65 });
66});
67
68// ---------------------------------------------------------------------------
69// 2. User endpoints
70// ---------------------------------------------------------------------------
71
72describe("API v2 - User endpoints", () => {
73 it("GET /api/v2/user without auth returns 401", async () => {
74 const res = await app.request(apiUrl("/user"));
75 expect(res.status).toBe(401);
76
77 const body = await res.json();
78 expect(body.error).toBeDefined();
79 });
80
81 it("GET /api/v2/users/nobody returns 404 or 500 (no DB)", async () => {
82 const res = await app.request(apiUrl("/users/nobody"));
83 expect([404, 500]).toContain(res.status);
84 });
85});
86
87// ---------------------------------------------------------------------------
88// 3. Repository endpoints
89// ---------------------------------------------------------------------------
90
91describe("API v2 - Repository endpoints", () => {
92 it("POST /api/v2/repos without auth returns 401", async () => {
93 const res = await app.request(apiUrl("/repos"), {
94 method: "POST",
95 headers: jsonHeaders(),
96 body: JSON.stringify({ name: "test-repo" }),
97 });
98 expect(res.status).toBe(401);
99
100 const body = await res.json();
101 expect(body.error).toBeDefined();
102 });
103
104 it("GET /api/v2/repos/nobody/nothing returns 404 or 500 (no DB)", async () => {
105 const res = await app.request(apiUrl("/repos/nobody/nothing"));
106 expect([404, 500]).toContain(res.status);
107 });
108
109 it("PATCH /api/v2/repos/nobody/nothing without auth returns 401", async () => {
110 const res = await app.request(apiUrl("/repos/nobody/nothing"), {
111 method: "PATCH",
112 headers: jsonHeaders(),
113 body: JSON.stringify({ description: "updated" }),
114 });
115 expect(res.status).toBe(401);
116
117 const body = await res.json();
118 expect(body.error).toBeDefined();
119 });
120
121 it("DELETE /api/v2/repos/nobody/nothing without auth returns 401", async () => {
122 const res = await app.request(apiUrl("/repos/nobody/nothing"), {
123 method: "DELETE",
124 });
125 expect(res.status).toBe(401);
126
127 const body = await res.json();
128 expect(body.error).toBeDefined();
129 });
130});
131
132// ---------------------------------------------------------------------------
133// 4. Branch endpoints
134// ---------------------------------------------------------------------------
135
136describe("API v2 - Branch endpoints", () => {
137 it("GET /api/v2/repos/nobody/nothing/branches returns 404 or 500", async () => {
138 const res = await app.request(apiUrl("/repos/nobody/nothing/branches"));
139 expect([404, 500]).toContain(res.status);
140 });
141});
142
143// ---------------------------------------------------------------------------
144// 5. Commit endpoints
145// ---------------------------------------------------------------------------
146
147describe("API v2 - Commit endpoints", () => {
148 it("GET /api/v2/repos/nobody/nothing/commits returns 404 or 500", async () => {
149 const res = await app.request(apiUrl("/repos/nobody/nothing/commits"));
150 expect([404, 500]).toContain(res.status);
151 });
152
153 it("GET /api/v2/repos/nobody/nothing/commits/abc123 returns 404 or 500", async () => {
154 const res = await app.request(
155 apiUrl("/repos/nobody/nothing/commits/abc123")
156 );
157 expect([404, 500]).toContain(res.status);
158 });
159});
160
161// ---------------------------------------------------------------------------
162// 6. Tree / File endpoints
163// ---------------------------------------------------------------------------
164
165describe("API v2 - Tree and File endpoints", () => {
166 it("GET /api/v2/repos/nobody/nothing/tree/main returns 404 or 500", async () => {
167 const res = await app.request(
168 apiUrl("/repos/nobody/nothing/tree/main")
169 );
170 expect([404, 500]).toContain(res.status);
171 });
172
173 it("GET /api/v2/repos/nobody/nothing/contents/README.md returns 404 or 500", async () => {
174 const res = await app.request(
175 apiUrl("/repos/nobody/nothing/contents/README.md")
176 );
177 expect([404, 500]).toContain(res.status);
178 });
179});
180
181// ---------------------------------------------------------------------------
182// 7. Issue endpoints
183// ---------------------------------------------------------------------------
184
185describe("API v2 - Issue endpoints", () => {
186 it("GET /api/v2/repos/nobody/nothing/issues returns 404 or 500 (no DB)", async () => {
187 const res = await app.request(
188 apiUrl("/repos/nobody/nothing/issues")
189 );
190 expect([404, 500]).toContain(res.status);
191 });
192
193 it("POST /api/v2/repos/nobody/nothing/issues without auth returns 401", async () => {
194 const res = await app.request(
195 apiUrl("/repos/nobody/nothing/issues"),
196 {
197 method: "POST",
198 headers: jsonHeaders(),
199 body: JSON.stringify({ title: "Bug report" }),
200 }
201 );
202 expect(res.status).toBe(401);
203
204 const body = await res.json();
205 expect(body.error).toBeDefined();
206 });
207});
208
209// ---------------------------------------------------------------------------
210// 8. Pull Request endpoints
211// ---------------------------------------------------------------------------
212
213describe("API v2 - Pull Request endpoints", () => {
214 it("GET /api/v2/repos/nobody/nothing/pulls returns 404 or 500 (no DB)", async () => {
215 const res = await app.request(
216 apiUrl("/repos/nobody/nothing/pulls")
217 );
218 expect([404, 500]).toContain(res.status);
219 });
220
221 it("POST /api/v2/repos/nobody/nothing/pulls without auth returns 401", async () => {
222 const res = await app.request(
223 apiUrl("/repos/nobody/nothing/pulls"),
224 {
225 method: "POST",
226 headers: jsonHeaders(),
227 body: JSON.stringify({
228 title: "Feature branch",
229 baseBranch: "main",
230 headBranch: "feature",
231 }),
232 }
233 );
234 expect(res.status).toBe(401);
235
236 const body = await res.json();
237 expect(body.error).toBeDefined();
238 });
239});
240
241// ---------------------------------------------------------------------------
242// 9. Star endpoints
243// ---------------------------------------------------------------------------
244
245describe("API v2 - Star endpoints", () => {
246 it("PUT /api/v2/repos/nobody/nothing/star without auth returns 401", async () => {
247 const res = await app.request(
248 apiUrl("/repos/nobody/nothing/star"),
249 { method: "PUT" }
250 );
251 expect(res.status).toBe(401);
252
253 const body = await res.json();
254 expect(body.error).toBeDefined();
255 });
256
257 it("DELETE /api/v2/repos/nobody/nothing/star without auth returns 401", async () => {
258 const res = await app.request(
259 apiUrl("/repos/nobody/nothing/star"),
260 { method: "DELETE" }
261 );
262 expect(res.status).toBe(401);
263
264 const body = await res.json();
265 expect(body.error).toBeDefined();
266 });
267});
268
269// ---------------------------------------------------------------------------
270// 10. Label endpoints
271// ---------------------------------------------------------------------------
272
273describe("API v2 - Label endpoints", () => {
274 it("GET /api/v2/repos/nobody/nothing/labels returns 404 or 500 (no DB)", async () => {
275 const res = await app.request(
276 apiUrl("/repos/nobody/nothing/labels")
277 );
278 expect([404, 500]).toContain(res.status);
279 });
280
281 it("POST /api/v2/repos/nobody/nothing/labels without auth returns 401", async () => {
282 const res = await app.request(
283 apiUrl("/repos/nobody/nothing/labels"),
284 {
285 method: "POST",
286 headers: jsonHeaders(),
287 body: JSON.stringify({ name: "bug", color: "#ff0000" }),
288 }
289 );
290 expect(res.status).toBe(401);
291
292 const body = await res.json();
293 expect(body.error).toBeDefined();
294 });
295});
296
297// ---------------------------------------------------------------------------
298// 11. Search endpoints
299// ---------------------------------------------------------------------------
300
301describe("API v2 - Search endpoints", () => {
302 it("GET /api/v2/search/repos without query returns 400", async () => {
303 const res = await app.request(apiUrl("/search/repos"));
304 expect(res.status).toBe(400);
305
306 const body = await res.json();
307 expect(body.error).toContain("required");
308 });
309
310 it("GET /api/v2/search/repos?q=test returns 200 or 500 (no DB)", async () => {
311 const res = await app.request(apiUrl("/search/repos?q=test"));
312 expect([200, 500]).toContain(res.status);
313 });
314
315 it("GET /api/v2/repos/nobody/nothing/search/code?q=test returns 404 or 500", async () => {
316 const res = await app.request(
317 apiUrl("/repos/nobody/nothing/search/code?q=test")
318 );
319 expect([404, 500]).toContain(res.status);
320 });
321
322 it("GET /api/v2/search/repos with empty q returns 400", async () => {
323 const res = await app.request(apiUrl("/search/repos?q="));
324 expect(res.status).toBe(400);
325
326 const body = await res.json();
327 expect(body.error).toBeDefined();
328 });
329
330 it("GET /api/v2/repos/nobody/nothing/search/code without q returns 400", async () => {
331 const res = await app.request(
332 apiUrl("/repos/nobody/nothing/search/code")
333 );
334 expect(res.status).toBe(400);
335
336 const body = await res.json();
337 expect(body.error).toContain("required");
338 });
339});
340
341// ---------------------------------------------------------------------------
342// 12. Topic endpoints
343// ---------------------------------------------------------------------------
344
345describe("API v2 - Topic endpoints", () => {
346 it("GET /api/v2/repos/nobody/nothing/topics returns 404 or 500 (no DB)", async () => {
347 const res = await app.request(
348 apiUrl("/repos/nobody/nothing/topics")
349 );
350 expect([404, 500]).toContain(res.status);
351 });
352
353 it("PUT /api/v2/repos/nobody/nothing/topics without auth returns 401", async () => {
354 const res = await app.request(
355 apiUrl("/repos/nobody/nothing/topics"),
356 {
357 method: "PUT",
358 headers: jsonHeaders(),
359 body: JSON.stringify({ topics: ["javascript", "web"] }),
360 }
361 );
362 expect(res.status).toBe(401);
363
364 const body = await res.json();
365 expect(body.error).toBeDefined();
366 });
367});
368
369// ---------------------------------------------------------------------------
370// 13. Webhook endpoints
371// ---------------------------------------------------------------------------
372
373describe("API v2 - Webhook endpoints", () => {
374 it("GET /api/v2/repos/nobody/nothing/webhooks without auth returns 401", async () => {
375 const res = await app.request(
376 apiUrl("/repos/nobody/nothing/webhooks")
377 );
378 expect(res.status).toBe(401);
379
380 const body = await res.json();
381 expect(body.error).toBeDefined();
382 });
383
384 it("POST /api/v2/repos/nobody/nothing/webhooks without auth returns 401", async () => {
385 const res = await app.request(
386 apiUrl("/repos/nobody/nothing/webhooks"),
387 {
388 method: "POST",
389 headers: jsonHeaders(),
390 body: JSON.stringify({ url: "https://example.com/hook" }),
391 }
392 );
393 expect(res.status).toBe(401);
394
395 const body = await res.json();
396 expect(body.error).toBeDefined();
397 });
398});
399
400// ---------------------------------------------------------------------------
401// 14. Activity endpoint
402// ---------------------------------------------------------------------------
403
404describe("API v2 - Activity endpoint", () => {
405 it("GET /api/v2/repos/nobody/nothing/activity returns 404 or 500 (no DB)", async () => {
406 const res = await app.request(
407 apiUrl("/repos/nobody/nothing/activity")
408 );
409 expect([404, 500]).toContain(res.status);
410 });
411});
412
413// ---------------------------------------------------------------------------
414// 15. Rate limiting
415// ---------------------------------------------------------------------------
416
417describe("API v2 - Rate limiting", () => {
418 it("responses include X-RateLimit-Limit header", async () => {
419 const res = await app.request(apiUrl("/"));
420 expect(res.status).toBe(200);
421
422 const limitHeader = res.headers.get("X-RateLimit-Limit");
423 expect(limitHeader).toBeDefined();
424 expect(limitHeader).not.toBeNull();
425 expect(parseInt(limitHeader!)).toBeGreaterThan(0);
426 });
427
428 it("responses include X-RateLimit-Remaining header", async () => {
429 const res = await app.request(apiUrl("/"));
430 expect(res.status).toBe(200);
431
432 const remainingHeader = res.headers.get("X-RateLimit-Remaining");
433 expect(remainingHeader).toBeDefined();
434 expect(remainingHeader).not.toBeNull();
435 expect(parseInt(remainingHeader!)).toBeGreaterThanOrEqual(0);
436 });
437
438 it("responses include X-RateLimit-Reset header", async () => {
439 const res = await app.request(apiUrl("/"));
440 expect(res.status).toBe(200);
441
442 const resetHeader = res.headers.get("X-RateLimit-Reset");
443 expect(resetHeader).toBeDefined();
444 expect(resetHeader).not.toBeNull();
445 expect(parseInt(resetHeader!)).toBeGreaterThan(0);
446 });
447});
448
449// ---------------------------------------------------------------------------
450// 16. Invalid Bearer token
451// ---------------------------------------------------------------------------
452
453describe("API v2 - Invalid Bearer token", () => {
454 it("GET /api/v2/user with invalid Bearer token returns 401", async () => {
455 const res = await app.request(apiUrl("/user"), {
456 headers: bearerHeader("invalid-token-that-does-not-exist"),
457 });
458 expect(res.status).toBe(401);
459
460 const body = await res.json();
461 expect(body.error).toBeDefined();
462 });
463
464 it("GET /api/v2/user with malformed Bearer header returns 401", async () => {
465 const res = await app.request(apiUrl("/user"), {
466 headers: bearerHeader(""),
467 });
468 expect(res.status).toBe(401);
469
470 const body = await res.json();
471 expect(body.error).toBeDefined();
472 });
473});
474
475// ---------------------------------------------------------------------------
476// 17. JSON content type
477// ---------------------------------------------------------------------------
478
479describe("API v2 - JSON content type", () => {
480 it("API info endpoint returns application/json", async () => {
481 const res = await app.request(apiUrl("/"));
482 expect(res.status).toBe(200);
483
484 const contentType = res.headers.get("Content-Type");
485 expect(contentType).toBeDefined();
486 expect(contentType!).toContain("application/json");
487 });
488
489 it("401 responses return application/json", async () => {
490 const res = await app.request(apiUrl("/user"));
491 expect(res.status).toBe(401);
492
493 const contentType = res.headers.get("Content-Type");
494 expect(contentType).toBeDefined();
495 expect(contentType!).toContain("application/json");
496 });
497
498 it("400 responses return application/json", async () => {
499 const res = await app.request(apiUrl("/search/repos"));
500 expect(res.status).toBe(400);
501
502 const contentType = res.headers.get("Content-Type");
503 expect(contentType).toBeDefined();
504 expect(contentType!).toContain("application/json");
505 });
506});
507
508// ---------------------------------------------------------------------------
509// 18. Validation - POST /api/v2/repos with auth header but no body fields
510// ---------------------------------------------------------------------------
511
512describe("API v2 - Repo creation validation", () => {
513 it("POST /api/v2/repos with auth header but no body returns 400 or 401", async () => {
514 const res = await app.request(apiUrl("/repos"), {
515 method: "POST",
516 headers: jsonHeaders(bearerHeader("fake-token-for-validation-test")),
517 body: JSON.stringify({}),
518 });
519 // Without a valid token the auth middleware rejects first (401),
520 // but if auth somehow passes, missing name would be 400.
521 expect([400, 401]).toContain(res.status);
522
523 const body = await res.json();
524 expect(body.error).toBeDefined();
525 });
526
527 it("POST /api/v2/repos with invalid repo name format should return 400 or 401", async () => {
528 const res = await app.request(apiUrl("/repos"), {
529 method: "POST",
530 headers: jsonHeaders(bearerHeader("fake-token")),
531 body: JSON.stringify({ name: "invalid repo name with spaces!" }),
532 });
533 // Auth rejects first (401), but validates the pattern is checked
534 expect([400, 401]).toContain(res.status);
535
536 const body = await res.json();
537 expect(body.error).toBeDefined();
538 });
539});
540
541// ---------------------------------------------------------------------------
542// 19. Issue validation - POST issue without title
543// ---------------------------------------------------------------------------
544
545describe("API v2 - Issue creation validation", () => {
546 it("POST issue without title returns 400 or 401 (auth blocks first without DB)", async () => {
547 const res = await app.request(
548 apiUrl("/repos/nobody/nothing/issues"),
549 {
550 method: "POST",
551 headers: jsonHeaders(bearerHeader("fake-token")),
552 body: JSON.stringify({ body: "Issue body without title" }),
553 }
554 );
555 // Auth middleware rejects invalid tokens (401).
556 // With valid auth, missing title would produce 400.
557 expect([400, 401]).toContain(res.status);
558
559 const body = await res.json();
560 expect(body.error).toBeDefined();
561 });
562
563 it("POST issue with empty title returns 400 or 401", async () => {
564 const res = await app.request(
565 apiUrl("/repos/nobody/nothing/issues"),
566 {
567 method: "POST",
568 headers: jsonHeaders(bearerHeader("fake-token")),
569 body: JSON.stringify({ title: "", body: "Some body" }),
570 }
571 );
572 expect([400, 401]).toContain(res.status);
573
574 const body = await res.json();
575 expect(body.error).toBeDefined();
576 });
577});
578
579// ---------------------------------------------------------------------------
580// Additional edge cases
581// ---------------------------------------------------------------------------
582
583describe("API v2 - Additional edge cases", () => {
584 it("GET /api/v2/users/:username/repos returns 404 or 500 for nonexistent user", async () => {
585 const res = await app.request(apiUrl("/users/nobody/repos"));
586 expect([404, 500]).toContain(res.status);
587 });
588
589 it("PATCH /api/v2/user without auth returns 401", async () => {
590 const res = await app.request(apiUrl("/user"), {
591 method: "PATCH",
592 headers: jsonHeaders(),
593 body: JSON.stringify({ displayName: "New Name" }),
594 });
595 expect(res.status).toBe(401);
596
597 const body = await res.json();
598 expect(body.error).toBeDefined();
599 });
600
601 it("GET /api/v2/repos/nobody/nothing/issues/1 returns 404 or 500 (no DB)", async () => {
602 const res = await app.request(
603 apiUrl("/repos/nobody/nothing/issues/1")
604 );
605 expect([404, 500]).toContain(res.status);
606 });
607
608 it("GET /api/v2/repos/nobody/nothing/pulls/1 returns 404 or 500 (no DB)", async () => {
609 const res = await app.request(
610 apiUrl("/repos/nobody/nothing/pulls/1")
611 );
612 expect([404, 500]).toContain(res.status);
613 });
614
615 it("PATCH /api/v2/repos/nobody/nothing/issues/1 without auth returns 401", async () => {
616 const res = await app.request(
617 apiUrl("/repos/nobody/nothing/issues/1"),
618 {
619 method: "PATCH",
620 headers: jsonHeaders(),
621 body: JSON.stringify({ state: "closed" }),
622 }
623 );
624 expect(res.status).toBe(401);
625
626 const body = await res.json();
627 expect(body.error).toBeDefined();
628 });
629
630 it("POST /api/v2/repos/nobody/nothing/issues/1/comments without auth returns 401", async () => {
631 const res = await app.request(
632 apiUrl("/repos/nobody/nothing/issues/1/comments"),
633 {
634 method: "POST",
635 headers: jsonHeaders(),
636 body: JSON.stringify({ body: "A comment" }),
637 }
638 );
639 expect(res.status).toBe(401);
640
641 const body = await res.json();
642 expect(body.error).toBeDefined();
643 });
644});