| 1 | import type { INestApplication } from '@nestjs/common'; |
| 2 | import { execFileSync } |
| 3 | import { mkdtempSync, readFileSync |
| 4 | import { tmpdir } |
| 5 | import path from 'node:path |
| 6 | import request from 'supertest |
| 7 | import { afterAll, beforeAll |
| 8 | |
| 9 | import { hasBackends, signUp |
| 10 | |
| 11 | describe.skipIf(!hasBackends |
| 12 | let app: |
| 13 | let owner: |
| 14 | const username = |
| 15 | const v3 = |
| 16 | const call = |
| 17 | return token ? |
| 18 | }; |
| 19 | |
| 20 | beforeAll(async () |
| 21 | ({ app } |
| 22 | owner = await |
| 23 | const api = |
| 24 | await api |
| 25 | .post('/api/repositories') |
| 26 | .set('cookie', |
| 27 | .send({ name: |
| 28 | .expect(201); |
| 29 | await api |
| 30 | .post('/api/repositories') |
| 31 | .set('cookie', |
| 32 | .send({ name: |
| 33 | .expect(201); |
| 34 | }); |
| 35 | |
| 36 | afterAll(async () |
| 37 | await app?.close |
| 38 | }); |
| 39 | |
| 40 | it('answers the root with the scopes gh checks for', |
| 41 | const response = |
| 42 | expect(response.headers |
| 43 | expect(response.headers |
| 44 | expect(response.headers |
| 45 | 'github.v3; format=json', |
| 46 | ); |
| 47 | }); |
| 48 | |
| 49 | it('accepts a Bearer token as well as token', |
| 50 | await request(app |
| 51 | .get('/api/v3/user') |
| 52 | .set('authorization', |
| 53 | .expect(200); |
| 54 | }); |
| 55 | |
| 56 | it('refuses a wrong token with Bad credentials instead of treating it as anonymous', |
| 57 | const response = |
| 58 | expect(response.body |
| 59 | message: 'Bad credentials', |
| 60 | documentation_url: 'https://docs.github.com/rest', |
| 61 | }); |
| 62 | }); |
| 63 | |
| 64 | it('answers /user for the token owner and 401 without a token', |
| 65 | const response = |
| 66 | expect(response.body |
| 67 | login: username, |
| 68 | id: expect.any |
| 69 | type: 'User', |
| 70 | site_admin: false, |
| 71 | }); |
| 72 | expect(response.body |
| 73 | expect(response.body |
| 74 | const anonymous = |
| 75 | expect(anonymous.body |
| 76 | }); |
| 77 | |
| 78 | it('reports an installed version below 3.18 so gh keeps the classic search syntax', |
| 79 | const response = |
| 80 | expect(response.body |
| 81 | }); |
| 82 | it('answers GET /repos/:owner/:repo in REST shape and 404 for a private one anonymously', |
| 83 | const response = |
| 84 | `/repos/${username}/public-repo` |
| 85 | owner.key, |
| 86 | ).expect(200); |
| 87 | expect(response.body |
| 88 | id: expect.any |
| 89 | name: 'public-repo', |
| 90 | full_name: `${username} |
| 91 | private: false, |
| 92 | visibility: 'public', |
| 93 | owner: { login: username |
| 94 | }); |
| 95 | expect(response.body |
| 96 | new RegExp(`/ |
| 97 | ); |
| 98 | await v3(`/repos/ |
| 99 | message: 'Not Found', |
| 100 | documentation_url: 'https://docs.github.com/rest', |
| 101 | status: '404', |
| 102 | }); |
| 103 | }); |
| 104 | |
| 105 | it('answers the readme as base64 content, and 404 when there is none', |
| 106 | await v3(`/repos/ |
| 107 | }); |
| 108 | |
| 109 | it('creates a repository with POST /user/repos and answers GET /users/:login', |
| 110 | const created = |
| 111 | .post('/api/v3/user/repos') |
| 112 | .set('authorization', |
| 113 | .send({ name: |
| 114 | .expect(201); |
| 115 | expect(created.body |
| 116 | name: 'rest-made', |
| 117 | full_name: `${username} |
| 118 | private: true, |
| 119 | }); |
| 120 | const user = |
| 121 | expect(user.body |
| 122 | await v3('/users/nobody-xyz |
| 123 | }); |
| 124 | |
| 125 | it('lists and adds SSH keys in GitHub shape', |
| 126 | // A fingerprint belongs to one account, so each run adds a fresh key. |
| 127 | const dir = |
| 128 | execFileSync('ssh-keygen', |
| 129 | '-q', |
| 130 | '-t', |
| 131 | 'ed25519', |
| 132 | '-N', |
| 133 | '', |
| 134 | '-f', |
| 135 | path.join(dir |
| 136 | ]); |
| 137 | const key = |
| 138 | rmSync(dir, { |
| 139 | const added = |
| 140 | .post('/api/v3/user/keys') |
| 141 | .set('authorization', |
| 142 | .send({ title: |
| 143 | .expect(201); |
| 144 | expect(added.body |
| 145 | title: 'laptop', |
| 146 | key: expect.stringContaining |
| 147 | read_only: false, |
| 148 | verified: true, |
| 149 | }); |
| 150 | const listed = |
| 151 | expect(listed.body |
| 152 | expect.objectContaining({ |
| 153 | title: 'laptop', |
| 154 | key: expect.stringContaining |
| 155 | }), |
| 156 | ); |
| 157 | }); |
| 158 | |
| 159 | it('lists the compat routes in the OpenAPI document under GitHub compatibility', |
| 160 | const { SwaggerModule |
| 161 | const { openApiConfig |
| 162 | const document = |
| 163 | expect(document.paths |
| 164 | 'GitHub compatibility', |
| 165 | ]); |
| 166 | }); |
| 167 | |
| 168 | it('names the invalid field in a REST validation error, with 422 as GitHub answers', |
| 169 | const response = |
| 170 | .post('/api/v3/user/repos') |
| 171 | .set('authorization', |
| 172 | .send({ private: |
| 173 | .expect(422); |
| 174 | expect(response.body |
| 175 | message: 'Validation Failed', |
| 176 | status: '422', |
| 177 | }); |
| 178 | expect(response.body |
| 179 | expect.objectContaining({ |
| 180 | ); |
| 181 | }); |
| 182 | }); |