helpers.js
9,718 bytes
| 1 | import request from "supertest"; |
|---|---|
| 2 | import { createStore } from "../src/db.js"; |
| 3 | import { createApp } from "../src/app.js"; |
| 4 | |
| 5 | export const repositories = [ |
| 6 | { |
| 7 | id: 101, |
| 8 | name: "atlas", |
| 9 | fullName: "rasmus/atlas", |
| 10 | description: "A mapping tool", |
| 11 | private: true, |
| 12 | language: "JavaScript", |
| 13 | updatedAt: "2026-07-20T10:00:00.000Z", |
| 14 | defaultBranch: "main", |
| 15 | owner: "rasmus", |
| 16 | }, |
| 17 | { |
| 18 | id: 202, |
| 19 | name: "ledger", |
| 20 | fullName: "rasmus/ledger", |
| 21 | description: "An expense journal", |
| 22 | private: true, |
| 23 | language: "TypeScript", |
| 24 | updatedAt: "2026-07-21T10:00:00.000Z", |
| 25 | defaultBranch: "main", |
| 26 | owner: "rasmus", |
| 27 | }, |
| 28 | ]; |
| 29 | |
| 30 | function snapshot(repo) { |
| 31 | const headSha = `${repo.id}`.padEnd(40, "a"); |
| 32 | const branchSha = `${repo.id + 1}`.padEnd(40, "c"); |
| 33 | const tagSha = `${repo.id + 2}`.padEnd(40, "d"); |
| 34 | const files = [ |
| 35 | { |
| 36 | path: "README.md", |
| 37 | size: 114, |
| 38 | content: `# ${repo.name}\n\nSnapshot\n\n## Highlights\n\n- Read-only review\n- [Read the guide](docs/guide.md)\n\n<script>alert("x")</script>`, |
| 39 | truncated: false, |
| 40 | }, |
| 41 | { path: "docs/guide.md", size: 26, content: "# Guide\n\nProject details.", truncated: false }, |
| 42 | { path: "docs/large.md", size: 600_000, content: null, truncated: true }, |
| 43 | { |
| 44 | path: "assets/logo.png", |
| 45 | size: 68, |
| 46 | content: null, |
| 47 | binaryContent: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Wl2V6sAAAAASUVORK5CYII=", |
| 48 | mediaType: "image/png", |
| 49 | truncated: false, |
| 50 | }, |
| 51 | { path: "src/index.js", size: 22, content: "export const ready = true;", truncated: false }, |
| 52 | ]; |
| 53 | const commits = [ |
| 54 | { |
| 55 | sha: headSha, |
| 56 | message: `Finish ${repo.name} viewer`, |
| 57 | author: "Rasmus", |
| 58 | date: "2026-07-22T09:00:00.000Z", |
| 59 | additions: 4, |
| 60 | deletions: 1, |
| 61 | files: [{ |
| 62 | filename: "src/index.js", |
| 63 | status: "modified", |
| 64 | additions: 4, |
| 65 | deletions: 1, |
| 66 | patch: "@@ -1 +1 @@\n-false\n+true", |
| 67 | }, { |
| 68 | filename: "package-lock.json", |
| 69 | status: "modified", |
| 70 | additions: 1, |
| 71 | deletions: 1, |
| 72 | patch: "", |
| 73 | }], |
| 74 | }, |
| 75 | { |
| 76 | sha: `${repo.id}`.padEnd(40, "b"), |
| 77 | message: `Start ${repo.name}`, |
| 78 | author: "Rasmus", |
| 79 | date: "2026-07-19T09:00:00.000Z", |
| 80 | additions: 10, |
| 81 | deletions: 0, |
| 82 | files: [], |
| 83 | }, |
| 84 | ]; |
| 85 | return { |
| 86 | ...repo, |
| 87 | headSha, |
| 88 | createdAt: "2026-06-01T10:00:00.000Z", |
| 89 | homepage: `https://${repo.name}.test`, |
| 90 | topics: ["developer-tools", "snapshot"], |
| 91 | license: { name: "MIT License", spdxId: "MIT" }, |
| 92 | stargazersCount: 12, |
| 93 | forksCount: 3, |
| 94 | watchersCount: 4, |
| 95 | archived: false, |
| 96 | languages: [ |
| 97 | { name: repo.language, bytes: 700, percent: 70 }, |
| 98 | { name: "HTML", bytes: 300, percent: 30 }, |
| 99 | ], |
| 100 | branches: [ |
| 101 | { name: repo.defaultBranch, sha: headSha, protected: true }, |
| 102 | { name: "review-notes", sha: branchSha, protected: false }, |
| 103 | ], |
| 104 | tags: [{ name: "v1.0.0", sha: tagSha }], |
| 105 | refSnapshots: [{ |
| 106 | sha: branchSha, |
| 107 | files: [ |
| 108 | { path: "README.md", size: 33, content: `# ${repo.name}\n\nReview notes branch.`, truncated: false }, |
| 109 | { path: "src/preview.js", size: 43, content: 'export const branch = "review-notes";', truncated: false }, |
| 110 | ], |
| 111 | commits: [{ |
| 112 | sha: branchSha, |
| 113 | message: `Add ${repo.name} review notes`, |
| 114 | author: "Rasmus", |
| 115 | date: "2026-07-23T08:00:00.000Z", |
| 116 | additions: 7, |
| 117 | deletions: 0, |
| 118 | files: [{ |
| 119 | filename: "src/preview.js", |
| 120 | status: "added", |
| 121 | additions: 7, |
| 122 | deletions: 0, |
| 123 | patch: '+export const branch = "review-notes";', |
| 124 | }], |
| 125 | }], |
| 126 | }, { |
| 127 | sha: tagSha, |
| 128 | files: [ |
| 129 | { path: "README.md", size: 26, content: `# ${repo.name} v1.0.0`, truncated: false }, |
| 130 | { path: "CHANGELOG.md", size: 24, content: "# Changes\n\nFirst release.", truncated: false }, |
| 131 | ], |
| 132 | commits: [{ |
| 133 | sha: tagSha, |
| 134 | message: `Release ${repo.name} v1.0.0`, |
| 135 | author: "Rasmus", |
| 136 | date: "2026-07-21T08:00:00.000Z", |
| 137 | additions: 12, |
| 138 | deletions: 1, |
| 139 | files: [], |
| 140 | }], |
| 141 | }], |
| 142 | issues: [{ |
| 143 | number: 12, |
| 144 | title: `Improve ${repo.name} keyboard navigation`, |
| 145 | body: "The file browser should be usable without a mouse.", |
| 146 | state: "open", |
| 147 | stateReason: null, |
| 148 | locked: false, |
| 149 | author: { login: "reviewer", avatarUrl: "https://avatars.test/reviewer" }, |
| 150 | labels: [{ name: "accessibility", color: "0969da" }], |
| 151 | createdAt: "2026-07-20T08:00:00.000Z", |
| 152 | updatedAt: "2026-07-23T07:00:00.000Z", |
| 153 | closedAt: null, |
| 154 | comments: [{ |
| 155 | id: 1201, |
| 156 | type: "comment", |
| 157 | body: "Focus should return to the branch button after closing the menu.", |
| 158 | author: { login: "rasmus", avatarUrl: "https://avatars.test/rasmus" }, |
| 159 | createdAt: "2026-07-21T08:00:00.000Z", |
| 160 | updatedAt: "2026-07-21T08:00:00.000Z", |
| 161 | state: null, |
| 162 | path: null, |
| 163 | line: null, |
| 164 | }], |
| 165 | }], |
| 166 | pullRequests: [{ |
| 167 | number: 14, |
| 168 | title: `Add ${repo.name} keyboard shortcuts`, |
| 169 | body: "Adds focused navigation for repository reviewers.", |
| 170 | state: "open", |
| 171 | draft: false, |
| 172 | merged: false, |
| 173 | mergeableState: "clean", |
| 174 | author: { login: "rasmus", avatarUrl: "https://avatars.test/rasmus" }, |
| 175 | labels: [{ name: "enhancement", color: "1f883d" }], |
| 176 | base: "main", |
| 177 | head: "keyboard-navigation", |
| 178 | createdAt: "2026-07-21T10:00:00.000Z", |
| 179 | updatedAt: "2026-07-23T10:00:00.000Z", |
| 180 | closedAt: null, |
| 181 | mergedAt: null, |
| 182 | additions: 8, |
| 183 | deletions: 2, |
| 184 | changedFiles: 1, |
| 185 | commitCount: 2, |
| 186 | conversation: [{ |
| 187 | id: 1401, |
| 188 | type: "review", |
| 189 | body: "The keyboard flow works as expected.", |
| 190 | author: { login: "reviewer", avatarUrl: "https://avatars.test/reviewer" }, |
| 191 | createdAt: "2026-07-22T10:00:00.000Z", |
| 192 | updatedAt: "2026-07-22T10:00:00.000Z", |
| 193 | state: "APPROVED", |
| 194 | path: null, |
| 195 | line: null, |
| 196 | }], |
| 197 | files: [{ |
| 198 | filename: "src/index.js", |
| 199 | previousFilename: null, |
| 200 | status: "modified", |
| 201 | additions: 8, |
| 202 | deletions: 2, |
| 203 | changes: 10, |
| 204 | patch: "@@ -1 +1,2 @@\n export const ready = true;\n+export const keyboard = true;", |
| 205 | }], |
| 206 | }], |
| 207 | releases: [{ |
| 208 | id: repo.id * 10 + 1, |
| 209 | tagName: "v1.0.0", |
| 210 | targetCommitish: "main", |
| 211 | name: `${repo.name} v1.0.0`, |
| 212 | body: "## Highlights\n\n- Faster repository reviews\n- Frozen release notes", |
| 213 | draft: false, |
| 214 | prerelease: false, |
| 215 | author: { login: "rasmus", avatarUrl: "https://avatars.test/rasmus" }, |
| 216 | createdAt: "2026-07-20T10:00:00.000Z", |
| 217 | publishedAt: "2026-07-21T10:00:00.000Z", |
| 218 | assets: [{ |
| 219 | id: repo.id * 100 + 1, |
| 220 | name: `${repo.name}-v1.0.0.zip`, |
| 221 | label: "Source archive", |
| 222 | size: 2048, |
| 223 | downloadCount: 8, |
| 224 | contentType: "application/zip", |
| 225 | }], |
| 226 | }], |
| 227 | files, |
| 228 | commits, |
| 229 | }; |
| 230 | } |
| 231 | |
| 232 | export function makeHarness(options = {}) { |
| 233 | const store = createStore(); |
| 234 | let clock = options.now || new Date("2026-07-23T12:00:00.000Z"); |
| 235 | const calls = { snapshots: [], refreshes: [], repositoryTokens: [], viewerReads: 0 }; |
| 236 | const github = { |
| 237 | authorizationUrl: (state) => `https://github.test/authorize?state=${state}`, |
| 238 | installationUrl: (state) => `https://github.test/install?state=${state}`, |
| 239 | exchangeCode: async () => ({ |
| 240 | accessToken: "owner-token", |
| 241 | refreshToken: null, |
| 242 | expiresAt: null, |
| 243 | }), |
| 244 | refreshUserToken: async (refreshToken) => { |
| 245 | calls.refreshes.push(refreshToken); |
| 246 | return { |
| 247 | accessToken: "refreshed-token", |
| 248 | refreshToken: "next-refresh", |
| 249 | expiresAt: "2026-07-24T12:00:00.000Z", |
| 250 | }; |
| 251 | }, |
| 252 | getViewer: async () => { |
| 253 | calls.viewerReads += 1; |
| 254 | return { |
| 255 | id: 77, |
| 256 | login: "rasmus", |
| 257 | name: "Rasmus", |
| 258 | avatar_url: "https://avatars.test/rasmus", |
| 259 | bio: "Building useful software.", |
| 260 | }; |
| 261 | }, |
| 262 | listRepositories: async (token) => { |
| 263 | calls.repositoryTokens.push(token); |
| 264 | return repositories; |
| 265 | }, |
| 266 | snapshotRepositories: async (token, selected) => { |
| 267 | calls.snapshots.push(selected.map((repo) => repo.id)); |
| 268 | return selected.map(snapshot); |
| 269 | }, |
| 270 | }; |
| 271 | const config = { |
| 272 | baseUrl: "http://profileshare.test", |
| 273 | sessionSecret: "0123456789abcdef0123456789abcdef", |
| 274 | github: { clientId: "client", clientSecret: "secret", appSlug: "profileshare" }, |
| 275 | }; |
| 276 | const app = createApp({ config, store, github, now: () => new Date(clock) }); |
| 277 | return { |
| 278 | app, |
| 279 | agent: request.agent(app), |
| 280 | store, |
| 281 | github, |
| 282 | calls, |
| 283 | setNow(value) { clock = new Date(value); }, |
| 284 | close() { store.close(); }, |
| 285 | }; |
| 286 | } |
| 287 | |
| 288 | export async function authenticate(harness) { |
| 289 | const start = await harness.agent.get("/auth/github"); |
| 290 | const state = new URL(start.headers.location).searchParams.get("state"); |
| 291 | await harness.agent.get(`/auth/github/callback?code=ok&state=${state}`).expect(302); |
| 292 | const install = await harness.agent.get("/github/install").expect(302); |
| 293 | const installState = new URL(install.headers.location).searchParams.get("state"); |
| 294 | await harness.agent.get(`/github/installed?installation_id=333&state=${installState}`).expect(302); |
| 295 | } |
| 296 | |
| 297 | export async function createShare(harness, repoIds = [101], days = 7) { |
| 298 | await authenticate(harness); |
| 299 | const response = await harness.agent |
| 300 | .post("/shares") |
| 301 | .type("form") |
| 302 | .send({ repositories: repoIds, days }) |
| 303 | .expect(201); |
| 304 | const match = response.text.match(/http:\/\/profileshare\.test\/s\/([A-Za-z0-9_-]+)/); |
| 305 | return { id: match[1], response }; |
| 306 | } |
| 307 | |