| 1 | import { describe, expect, it } from 'vitest'; |
| 2 | |
| 3 | import { GithubValidationError } |
| 4 | import { |
| 5 | labelNamesOf, |
| 6 | linkHeader, |
| 7 | pageOf, |
| 8 | type RestContext, |
| 9 | toCommentRest, |
| 10 | toIssueRest, |
| 11 | toUserRest, |
| 12 | } from './rest.js'; |
| 13 | |
| 14 | const origins = { |
| 15 | api: 'https://api.ghost.test', |
| 16 | web: 'https://ghost.test', |
| 17 | sshHost: '', |
| 18 | }; |
| 19 | |
| 20 | const ada = toUserRest |
| 21 | { ghostId: 'u1', |
| 22 | origins, |
| 23 | ); |
| 24 | |
| 25 | const context: RestContext |
| 26 | repository: { |
| 27 | ownerLogin: 'ada', |
| 28 | apiUrl: 'https://api.ghost.test/api/v3/repos/ada/tools', |
| 29 | webUrl: 'https://ghost.test/ada/tools', |
| 30 | }, |
| 31 | users: new Map |
| 32 | labelIds: new Map |
| 33 | }; |
| 34 | |
| 35 | const issue = { |
| 36 | id: 'issue_1', |
| 37 | number: 4, |
| 38 | title: 'Crash', |
| 39 | body: null, |
| 40 | state: 'closed' as |
| 41 | isPullRequest: false, |
| 42 | authorUsername: 'ada', |
| 43 | closedByUsername: null, |
| 44 | labels: [ |
| 45 | { |
| 46 | id: 'label_1', |
| 47 | name: 'help wanted', |
| 48 | description: null, |
| 49 | color: 'ededed', |
| 50 | createdAt: '2026-01-01T00:00:00.000Z', |
| 51 | updatedAt: '2026-01-01T00:00:00.000Z', |
| 52 | }, |
| 53 | ], |
| 54 | assignees: ['ada'], |
| 55 | commentCount: 2, |
| 56 | closedAt: '2026-01-02T00:00:00.000Z', |
| 57 | createdAt: '2026-01-01T00:00:00.000Z', |
| 58 | updatedAt: '2026-01-02T00:00:00.000Z', |
| 59 | viewerCanEdit: true, |
| 60 | }; |
| 61 | |
| 62 | describe('toUserRest', () |
| 63 | it('builds a simple-user with the URL templates typed SDKs expect', |
| 64 | expect(ada).toMatchObject |
| 65 | login: 'ada', |
| 66 | id: 7, |
| 67 | avatar_url: '', |
| 68 | gravatar_id: '', |
| 69 | url: 'https://api.ghost.test/api/v3/users/ada', |
| 70 | html_url: 'https://ghost.test/ada', |
| 71 | following_url: |
| 72 | 'https://api.ghost.test/api/v3/users/ada/following{/other_user}', |
| 73 | type: 'User', |
| 74 | site_admin: false, |
| 75 | }); |
| 76 | expect(ada.node_id |
| 77 | }); |
| 78 | |
| 79 | it('marks an organization as one', |
| 80 | const org = |
| 81 | { ghostId: 'o1', |
| 82 | origins, |
| 83 | 'Organization', |
| 84 | ); |
| 85 | expect(org).toMatchObject |
| 86 | expect(org.node_id |
| 87 | }); |
| 88 | }); |
| 89 | |
| 90 | describe('toIssueRest', () |
| 91 | it('embeds users and labels, links the API and the web app, and says nothing of pull requests', |
| 92 | const rest = |
| 93 | issue, |
| 94 | { numericId: 11, draft |
| 95 | context, |
| 96 | ); |
| 97 | expect(rest).toMatchObject |
| 98 | id: 11, |
| 99 | number: 4, |
| 100 | state: 'closed', |
| 101 | state_reason: 'completed', |
| 102 | url: 'https://api.ghost.test/api/v3/repos/ada/tools/issues/4', |
| 103 | html_url: 'https://ghost.test/ada/tools/issues/4', |
| 104 | labels_url: |
| 105 | 'https://api.ghost.test/api/v3/repos/ada/tools/issues/4/labels{/name}', |
| 106 | user: ada, |
| 107 | assignee: ada, |
| 108 | assignees: [ada], |
| 109 | closed_by: null, |
| 110 | comments: 2, |
| 111 | locked: false, |
| 112 | milestone: null, |
| 113 | author_association: 'OWNER', |
| 114 | }); |
| 115 | expect(rest.labels |
| 116 | expect.objectContaining({ |
| 117 | id: 3, |
| 118 | name: 'help wanted', |
| 119 | url: 'https://api.ghost.test/api/v3/repos/ada/tools/labels/help%20wanted', |
| 120 | default: false, |
| 121 | }), |
| 122 | ]); |
| 123 | expect(rest.node_id |
| 124 | expect(rest).not |
| 125 | expect(rest).not |
| 126 | }); |
| 127 | |
| 128 | it('marks a pull request with its pull_request links and merge time', |
| 129 | const rest = |
| 130 | { |
| 131 | ...issue, |
| 132 | state: 'open', |
| 133 | closedAt: null, |
| 134 | isPullRequest: true, |
| 135 | assignees: [], |
| 136 | authorUsername: 'bob', |
| 137 | }, |
| 138 | { |
| 139 | numericId: 12, |
| 140 | draft: true, |
| 141 | mergedAt: new Date |
| 142 | }, |
| 143 | context, |
| 144 | ); |
| 145 | expect(rest).toMatchObject |
| 146 | state_reason: null, |
| 147 | html_url: 'https://ghost.test/ada/tools/pull/4', |
| 148 | user: null, |
| 149 | assignee: null, |
| 150 | author_association: 'NONE', |
| 151 | draft: true, |
| 152 | pull_request: { |
| 153 | url: 'https://api.ghost.test/api/v3/repos/ada/tools/pulls/4', |
| 154 | html_url: 'https://ghost.test/ada/tools/pull/4', |
| 155 | diff_url: 'https://ghost.test/ada/tools/pull/4.diff', |
| 156 | patch_url: 'https://ghost.test/ada/tools/pull/4.patch', |
| 157 | merged_at: '2026-01-03T00:00:00.000Z', |
| 158 | }, |
| 159 | }); |
| 160 | expect(rest.node_id |
| 161 | }); |
| 162 | }); |
| 163 | |
| 164 | describe('toCommentRest', () |
| 165 | it('links by numeric id in the API and by Ghost id in the web app', |
| 166 | const rest = |
| 167 | { |
| 168 | id: 'ic_1', |
| 169 | numericId: 21, |
| 170 | body: 'Same here', |
| 171 | createdAt: '2026-01-01T00:00:00.000Z', |
| 172 | updatedAt: '2026-01-01T00:00:00.000Z', |
| 173 | authorUsername: 'ada', |
| 174 | issueNumber: 4, |
| 175 | isPullRequest: true, |
| 176 | }, |
| 177 | context, |
| 178 | ); |
| 179 | expect(rest).toMatchObject |
| 180 | id: 21, |
| 181 | url: 'https://api.ghost.test/api/v3/repos/ada/tools/issues/comments/21', |
| 182 | html_url: 'https://ghost.test/ada/tools/pull/4#issuecomment-ic_1', |
| 183 | issue_url: 'https://api.ghost.test/api/v3/repos/ada/tools/issues/4', |
| 184 | user: ada, |
| 185 | }); |
| 186 | expect(rest.node_id |
| 187 | }); |
| 188 | }); |
| 189 | |
| 190 | describe('pageOf', () |
| 191 | it('defaults to the first page of 30', |
| 192 | expect(pageOf({})) |
| 193 | }); |
| 194 | |
| 195 | it('caps per_page at 100 and ignores values that are not positive integers', |
| 196 | expect(pageOf({ |
| 197 | page: 3, |
| 198 | perPage: 100, |
| 199 | offset: 200, |
| 200 | }); |
| 201 | expect(pageOf({ |
| 202 | page: 1, |
| 203 | perPage: 30, |
| 204 | offset: 0, |
| 205 | }); |
| 206 | expect(pageOf({ |
| 207 | page: 1, |
| 208 | perPage: 30, |
| 209 | offset: 0, |
| 210 | }); |
| 211 | }); |
| 212 | }); |
| 213 | |
| 214 | describe('linkHeader', () |
| 215 | const url = |
| 216 | 'https://api.ghost.test/api/v3/repos/ada/tools/issues?state=all', |
| 217 | ); |
| 218 | const at = |
| 219 | |
| 220 | it('links next and last from the first page, keeping the query', |
| 221 | expect(linkHeader(url |
| 222 | [ |
| 223 | '<https://api.ghost.test/api/v3/repos/ada/tools/issues?state=all&page=2&per_page=10>; rel="next"', |
| 224 | '<https://api.ghost.test/api/v3/repos/ada/tools/issues?state=all&page=4&per_page=10>; rel="last"', |
| 225 | ].join(', ' |
| 226 | ); |
| 227 | }); |
| 228 | |
| 229 | it('links every way from a middle page', |
| 230 | const link = |
| 231 | expect(link).toContain |
| 232 | expect(link).toContain |
| 233 | expect(link).toContain |
| 234 | expect(link).toContain |
| 235 | }); |
| 236 | |
| 237 | it('links only back from the last page', |
| 238 | const link = |
| 239 | expect(link).toContain |
| 240 | expect(link).toContain |
| 241 | expect(link).not |
| 242 | }); |
| 243 | |
| 244 | it('sends nothing when everything fits on one page', |
| 245 | expect(linkHeader(url |
| 246 | expect(linkHeader(url |
| 247 | }); |
| 248 | }); |
| 249 | |
| 250 | describe('labelNamesOf', () |
| 251 | it('reads every body shape GitHub accepts', |
| 252 | expect(labelNamesOf({ |
| 253 | expect(labelNamesOf(['bug |
| 254 | expect(labelNamesOf([{ |
| 255 | expect(labelNamesOf({ |
| 256 | 'bug', |
| 257 | 'docs', |
| 258 | ]); |
| 259 | }); |
| 260 | |
| 261 | it('refuses anything else as an invalid labels field', |
| 262 | for (const body |
| 263 | expect(() => |
| 264 | }); |
| 265 | }); |