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

marketplace.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.

marketplace.test.tsBlame239 lines · 1 contributor
06139e6Claude1/**
2 * Block H — Marketplace + app identities tests.
3 *
4 * Pure helpers (slugify, botUsername, normalisePermissions, parsePermissions,
5 * hasPermission, generateBearerToken, hashBearer, permissionsSubset) + route
6 * auth smoke. DB-dependent helpers (createApp, installApp, verifyInstallToken)
7 * are exercised via type/shape checks only — real integration happens on the
8 * live server.
9 */
10
11import { describe, it, expect } from "bun:test";
12import app from "../app";
13import {
14 KNOWN_PERMISSIONS,
15 KNOWN_EVENTS,
16 botUsername,
17 generateBearerToken,
18 hasPermission,
19 hashBearer,
20 normalisePermissions,
21 parsePermissions,
22 permissionsSubset,
23 slugify,
24} from "../lib/marketplace";
25
26describe("marketplace — slugify", () => {
27 it("lowercases + replaces spaces with dashes", () => {
28 expect(slugify("My Cool App")).toBe("my-cool-app");
29 });
30
31 it("strips non-alphanumeric", () => {
32 expect(slugify("Hello, World!")).toBe("hello-world");
33 });
34
35 it("trims leading/trailing dashes", () => {
36 expect(slugify("---foo---")).toBe("foo");
37 });
38
39 it("caps at 40 characters", () => {
40 const s = slugify("a".repeat(100));
41 expect(s.length).toBeLessThanOrEqual(40);
42 });
43
44 it("collapses consecutive separators", () => {
45 expect(slugify("foo bar")).toBe("foo-bar");
46 });
47});
48
49describe("marketplace — botUsername", () => {
50 it("appends [bot] suffix", () => {
51 expect(botUsername("my-app")).toBe("my-app[bot]");
52 });
53});
54
55describe("marketplace — permissions", () => {
56 it("KNOWN_PERMISSIONS includes contents + issues + pulls + checks", () => {
57 expect(KNOWN_PERMISSIONS).toContain("contents:read");
58 expect(KNOWN_PERMISSIONS).toContain("contents:write");
59 expect(KNOWN_PERMISSIONS).toContain("issues:write");
60 expect(KNOWN_PERMISSIONS).toContain("pulls:write");
61 expect(KNOWN_PERMISSIONS).toContain("checks:write");
62 });
63
64 it("KNOWN_EVENTS includes push + pull_request + issues", () => {
65 expect(KNOWN_EVENTS).toContain("push");
66 expect(KNOWN_EVENTS).toContain("pull_request");
67 expect(KNOWN_EVENTS).toContain("issues");
68 });
69
70 it("normalisePermissions drops unknown values", () => {
71 const perms = normalisePermissions([
72 "contents:read",
73 "bogus:thing",
74 "issues:write",
75 ]);
76 expect(perms).toEqual(["contents:read", "issues:write"]);
77 });
78
79 it("normalisePermissions de-duplicates", () => {
80 const perms = normalisePermissions([
81 "contents:read",
82 "contents:read",
83 "contents:read",
84 ]);
85 expect(perms.length).toBe(1);
86 });
87
88 it("parsePermissions reads JSON array out of DB column", () => {
89 const raw = JSON.stringify(["contents:read", "issues:write"]);
90 expect(parsePermissions(raw)).toEqual(["contents:read", "issues:write"]);
91 });
92
93 it("parsePermissions handles null/empty/invalid JSON", () => {
94 expect(parsePermissions(null)).toEqual([]);
95 expect(parsePermissions(undefined)).toEqual([]);
96 expect(parsePermissions("")).toEqual([]);
97 expect(parsePermissions("not json")).toEqual([]);
98 expect(parsePermissions("{}")).toEqual([]);
99 });
100
101 it("hasPermission direct match", () => {
102 expect(hasPermission(["issues:read"], "issues:read")).toBe(true);
103 });
104
105 it("hasPermission: write implies read", () => {
106 expect(hasPermission(["issues:write"], "issues:read")).toBe(true);
107 expect(hasPermission(["contents:write"], "contents:read")).toBe(true);
108 });
109
110 it("hasPermission: read does NOT imply write", () => {
111 expect(hasPermission(["issues:read"], "issues:write")).toBe(false);
112 });
113
114 it("hasPermission: empty grant fails", () => {
115 expect(hasPermission([], "issues:read")).toBe(false);
116 });
117
118 it("permissionsSubset checks containment", () => {
119 expect(
120 permissionsSubset(["contents:read"], ["contents:read", "issues:read"])
121 ).toBe(true);
122 expect(
123 permissionsSubset(
124 ["contents:read", "admin:god"],
125 ["contents:read"]
126 )
127 ).toBe(false);
128 });
129});
130
131describe("marketplace — bearer tokens", () => {
132 it("generateBearerToken produces ghi_ prefix + hex body", () => {
133 const { token, hash } = generateBearerToken();
134 expect(token.startsWith("ghi_")).toBe(true);
135 expect(token.length).toBeGreaterThan(10);
136 expect(hash.length).toBe(64); // sha256 hex
137 });
138
139 it("generateBearerToken yields unique tokens", () => {
140 const a = generateBearerToken();
141 const b = generateBearerToken();
142 expect(a.token).not.toBe(b.token);
143 expect(a.hash).not.toBe(b.hash);
144 });
145
146 it("hashBearer is deterministic", () => {
147 const t = "ghi_deadbeef";
148 expect(hashBearer(t)).toBe(hashBearer(t));
149 });
150
151 it("hashBearer of generated token matches returned hash", () => {
152 const { token, hash } = generateBearerToken();
153 expect(hashBearer(token)).toBe(hash);
154 });
155});
156
157describe("marketplace — route smoke", () => {
158 it("GET /marketplace → 200 (public)", async () => {
159 const res = await app.request("/marketplace");
160 expect(res.status).toBe(200);
161 });
162
163 it("GET /marketplace?q=foo → 200", async () => {
164 const res = await app.request("/marketplace?q=foo");
165 expect(res.status).toBe(200);
166 });
167
168 it("GET /marketplace/unknown-slug → 404", async () => {
169 const res = await app.request(
170 "/marketplace/this-app-does-not-exist-abcdef"
171 );
172 expect(res.status).toBe(404);
173 });
174
175 it("POST /marketplace/:slug/install without auth → 302 /login", async () => {
176 const res = await app.request("/marketplace/foo/install", {
177 method: "POST",
178 body: new URLSearchParams({}),
179 headers: { "content-type": "application/x-www-form-urlencoded" },
180 });
181 expect(res.status).toBe(302);
182 expect(res.headers.get("location") || "").toContain("/login");
183 });
184
185 it("GET /settings/apps without auth → 302 /login", async () => {
186 const res = await app.request("/settings/apps");
187 expect(res.status).toBe(302);
188 expect(res.headers.get("location") || "").toContain("/login");
189 });
190
191 it("GET /developer/apps-new without auth → 302 /login", async () => {
192 const res = await app.request("/developer/apps-new");
193 expect(res.status).toBe(302);
194 expect(res.headers.get("location") || "").toContain("/login");
195 });
196
197 it("POST /developer/apps-new without auth → 302 /login", async () => {
198 const res = await app.request("/developer/apps-new", {
199 method: "POST",
200 body: new URLSearchParams({ name: "My App" }),
201 headers: { "content-type": "application/x-www-form-urlencoded" },
202 });
203 expect(res.status).toBe(302);
204 expect(res.headers.get("location") || "").toContain("/login");
205 });
206
207 it("GET /developer/apps/:slug/manage without auth → 302 /login", async () => {
208 const res = await app.request("/developer/apps/foo/manage");
209 expect(res.status).toBe(302);
210 expect(res.headers.get("location") || "").toContain("/login");
211 });
212});
213
214describe("marketplace — lib exports", () => {
215 it("exports the full surface", async () => {
216 const mod = await import("../lib/marketplace");
217 expect(typeof mod.slugify).toBe("function");
218 expect(typeof mod.botUsername).toBe("function");
219 expect(typeof mod.normalisePermissions).toBe("function");
220 expect(typeof mod.parsePermissions).toBe("function");
221 expect(typeof mod.hasPermission).toBe("function");
222 expect(typeof mod.permissionsSubset).toBe("function");
223 expect(typeof mod.generateBearerToken).toBe("function");
224 expect(typeof mod.hashBearer).toBe("function");
225 expect(typeof mod.listPublicApps).toBe("function");
226 expect(typeof mod.getAppBySlug).toBe("function");
227 expect(typeof mod.createApp).toBe("function");
228 expect(typeof mod.installApp).toBe("function");
229 expect(typeof mod.uninstallApp).toBe("function");
230 expect(typeof mod.issueInstallToken).toBe("function");
231 expect(typeof mod.verifyInstallToken).toBe("function");
232 expect(typeof mod.listInstallationsForApp).toBe("function");
233 expect(typeof mod.listInstallationsForTarget).toBe("function");
234 expect(typeof mod.listEventsForApp).toBe("function");
235 expect(typeof mod.countInstalls).toBe("function");
236 expect(Array.isArray(mod.KNOWN_PERMISSIONS)).toBe(true);
237 expect(Array.isArray(mod.KNOWN_EVENTS)).toBe(true);
238 });
239});