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.
| 06139e6 | 1 | /** |
| 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 | ||
| 11 | import { describe, it, expect } from "bun:test"; | |
| 12 | import app from "../app"; | |
| 13 | import { | |
| 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 | ||
| 26 | describe("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 | ||
| 49 | describe("marketplace — botUsername", () => { | |
| 50 | it("appends [bot] suffix", () => { | |
| 51 | expect(botUsername("my-app")).toBe("my-app[bot]"); | |
| 52 | }); | |
| 53 | }); | |
| 54 | ||
| 55 | describe("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 | ||
| 131 | describe("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 | ||
| 157 | describe("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 | ||
| 214 | describe("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 | }); |