diff --git a/backend/tests/auth.test.js b/backend/tests/auth.test.js index 2677203..36b0998 100644 --- a/backend/tests/auth.test.js +++ b/backend/tests/auth.test.js @@ -439,6 +439,36 @@ test('POST /api/auth/storage/create returns probe failures without needsInstall' assert.match(res.body.error, /do not reinstall/i); }); +test('POST /api/auth/storage/create returns REPO_CREATE_FORBIDDEN on Administration 403', async () => { + const app = createAuthedApp({ + user: AUTHED_USER, + authService: { + clientsFor: async () => ({ githubUserClient: { mock: true } }), + getInstallationSetupUrl: async () => + 'https://github.com/apps/vizably/installations/new', + }, + storageService: { + createGitHubRepository: async () => { + const err = new Error( + 'GitHub App cannot create repositories. Add Repository permissions → Administration: Read and write, accept the permission upgrade on your installation, then sign out and sign in again.', + ); + err.status = 403; + err.code = 'REPO_CREATE_FORBIDDEN'; + throw err; + }, + }, + }); + const res = await request(app) + .post('/api/auth/storage/create') + .send({ name: 'vizably-new' }); + assert.equal(res.status, 403); + assert.equal(res.body.code, 'REPO_CREATE_FORBIDDEN'); + assert.equal(res.body.needsInstall, undefined); + assert.equal(res.body.storageRef, undefined); + assert.match(res.body.error, /Administration:\s*Read and write/i); + assert.match(res.body.error, /permission upgrade/i); +}); + test('POST /api/auth/storage/validate requires provider and storageRef', async () => { const app = createAuthedApp({ user: AUTHED_USER, diff --git a/backend/tests/storageService.test.js b/backend/tests/storageService.test.js index 267bb5a..4d502e5 100644 --- a/backend/tests/storageService.test.js +++ b/backend/tests/storageService.test.js @@ -537,6 +537,46 @@ test('createGitHubRepository maps name-taken conflicts', async () => { ); }); +test('createGitHubRepository maps Administration permission 403 to REPO_CREATE_FORBIDDEN', async () => { + const storageService = new StorageService(); + const forbidden = new Error('Resource not accessible by integration'); + forbidden.status = 403; + forbidden.response = { + data: { message: 'Resource not accessible by integration' }, + }; + const client = createMockGitHubClient({ createRepoError: forbidden }); + await assert.rejects( + () => storageService.createGitHubRepository('vizably-new', { githubUserClient: client }), + (err) => { + assert.equal(err.code, 'REPO_CREATE_FORBIDDEN'); + assert.equal(err.status, 403); + assert.match(err.message, /Administration:\s*Read and write/i); + assert.match(err.message, /permission upgrade/i); + return true; + }, + ); +}); + +test('createGitHubRepository maps generic create 403 to REPO_CREATE_FORBIDDEN', async () => { + const storageService = new StorageService(); + const forbidden = new Error('Forbidden'); + forbidden.status = 403; + forbidden.response = { + data: { message: 'Although you appear to have the correct authorization credentials, organization policy prevents creating repositories.' }, + }; + const client = createMockGitHubClient({ createRepoError: forbidden }); + await assert.rejects( + () => storageService.createGitHubRepository('vizably-new', { githubUserClient: client }), + (err) => { + assert.equal(err.code, 'REPO_CREATE_FORBIDDEN'); + assert.equal(err.status, 403); + assert.match(err.message, /organization policy prevents creating repositories/i); + assert.doesNotMatch(err.message, /Administration/); + return true; + }, + ); +}); + test('validateStorage returns initializable for empty repo', async () => { const storageService = new StorageService(); const client = createMockGitHubClient();