diff --git a/src/support/slack/commands/add-repo.js b/src/support/slack/commands/add-repo.js index a82bed3a7..314c89447 100644 --- a/src/support/slack/commands/add-repo.js +++ b/src/support/slack/commands/add-repo.js @@ -38,7 +38,7 @@ function AddRepoCommand(context) { name: 'Add GitHub Repo', description: 'Adds a Github repository to previously added site.', phrases: PHRASES, - usageText: `${PHRASES.join(' or ')} {site} {githubRepoURL}`, + usageText: `${PHRASES.join(' or ')} {site} {githubRepoURL} [branch]`, }); const { dataAccess, log } = context; @@ -68,6 +68,21 @@ function AddRepoCommand(context) { } } + async function isOnboardedWithAemy(owner, repo, branch) { + const AEMY_ENDPOINT = `https://ec-xp-fapp-coordinator.azurewebsites.net/api/fn-ghapp/functions/get_installation_token/${owner}/${repo}/${branch}`; + try { + const response = await fetch(AEMY_ENDPOINT, { headers: { 'x-api-key': process.env.AEMY_API_KEY } }); + if (response.ok) { + const data = await response.json(); + return data.token !== null; + } else { + throw new Error('Failed to check if repository is onboarded with Aemy'); + } + } catch (error) { + throw new Error(`Failed to check if repository is onboarded with Aemy: ${error.message}`); + } + } + /** * Execute function for AddRepoCommand. This function validates the input, fetches the repository * information from the GitHub API, and saves it as a site in the database. @@ -81,7 +96,7 @@ function AddRepoCommand(context) { const { say } = slackContext; try { - const [baseURLInput, repoUrlInput] = args; + const [baseURLInput, repoUrlInput, branchInput] = args; const baseURL = extractURLFromSlackInput(baseURLInput); let repoUrl = extractURLFromSlackInput(repoUrlInput, false, false); @@ -106,22 +121,38 @@ function AddRepoCommand(context) { const repoInfo = await fetchRepoInfo(repoUrl); + let owner; + let repoName; + let branch; + if (repoInfo === null) { - await say(`:warning: The GitHub repository '${repoUrl}' could not be found (private repo?).`); - return; + [owner, repoName] = repoUrl.split('github.com/')[1].split('/'); + branch = branchInput || 'main'; + + await say(`:warning: GitHub API returned 404 for ${repoUrl}. Adding as private repo with branch: ${branch}`); + } else { + if (repoInfo.archived) { + await say(`:warning: The GitHub repository '${repoUrl}' is archived. Please unarchive it before adding it to a site.`); + return; + } + + owner = repoInfo.owner.login; + repoName = repoInfo.name; + branch = branchInput || repoInfo.default_branch; } - if (repoInfo.archived) { - await say(`:warning: The GitHub repository '${repoUrl}' is archived. Please unarchive it before adding it to a site.`); + const isOnboarded = await isOnboardedWithAemy(owner, repoName, branch); + if (!isOnboarded) { + await say(`:warning: The repository '${repoUrl}' is not onboarded with Aemy. Please onboard it with Aemy before adding it to a site.`); return; } site.setGitHubURL(repoUrl); const codeConfig = { type: 'github', - owner: repoInfo.owner.login, - repo: repoInfo.name, - ref: repoInfo.default_branch, + owner, + repo: repoName, + ref: branch, url: repoUrl, }; site.setCode(codeConfig); diff --git a/test/support/slack/commands/add-repo.test.js b/test/support/slack/commands/add-repo.test.js index 6febd553a..520f9364c 100644 --- a/test/support/slack/commands/add-repo.test.js +++ b/test/support/slack/commands/add-repo.test.js @@ -26,6 +26,8 @@ describe('AddRepoCommand', () => { let siteStub; beforeEach(() => { + process.env.AEMY_API_KEY = 'test-api-key'; + sqsStub = { sendMessage: sinon.stub().resolves(), }; @@ -66,7 +68,10 @@ describe('AddRepoCommand', () => { context = { dataAccess: dataAccessStub, sqs: sqsStub, - env: { AUDIT_JOBS_QUEUE_URL: 'testQueueUrl' }, + env: { + AUDIT_JOBS_QUEUE_URL: 'testQueueUrl', + AEMY_API_KEY: 'test-api-key', + }, log: console, }; }); @@ -94,11 +99,16 @@ describe('AddRepoCommand', () => { default_branch: 'main', }); + const aemyScope = nock('https://ec-xp-fapp-coordinator.azurewebsites.net') + .get('/api/fn-ghapp/functions/get_installation_token/valid/repo/main') + .reply(200, { token: 'some-token' }); + const args = ['validSite.com', 'https://github.com/valid/repo']; const command = AddRepoCommand(context); await command.handleExecution(args, slackContext); + expect(aemyScope.isDone()).to.be.true; expect(slackContext.say.called).to.be.true; expect(siteStub.setGitHubURL).to.have.been.calledWith('https://github.com/valid/repo'); expect(siteStub.setCode).to.have.been.calledWith({ @@ -134,11 +144,17 @@ describe('AddRepoCommand', () => { default_branch: 'main', }); + const aemyScope = nock('https://ec-xp-fapp-coordinator.azurewebsites.net') + .get('/api/fn-ghapp/functions/get_installation_token/valid/repo/main') + .reply(200, { token: 'some-token' }); + const args = ['validSite.com', 'github.com/valid/repo']; const command = AddRepoCommand(context); await command.handleExecution(args, slackContext); + expect(aemyScope.isDone()).to.be.true; + expect(slackContext.say).calledWith('\n' + ' :white_check_mark: *GitHub repo added for *\n' + '\n' @@ -221,9 +237,14 @@ describe('AddRepoCommand', () => { default_branch: 'main', }); + const aemyScope = nock('https://ec-xp-fapp-coordinator.azurewebsites.net') + .get('/api/fn-ghapp/functions/get_installation_token/valid/repo/main') + .reply(200, { token: 'some-token' }); + await command.handleExecution(args, slackContext); // Assertions to confirm repo info was fetched and handled correctly + expect(aemyScope.isDone()).to.be.true; expect(slackContext.say).calledWithMatch(/GitHub repo added/); expect(siteStub.setCode).to.have.been.calledWith({ type: 'github', @@ -234,16 +255,36 @@ describe('AddRepoCommand', () => { }); }); - it('handles non-existent repository (404 error)', async () => { + it('handles private repo', async () => { nock('https://api.github.com') - .get('/repos/invalid/repo') + .get('/repos/private/repo') .reply(404); - args[1] = 'https://github.com/invalid/repo'; + const aemyScope = nock('https://ec-xp-fapp-coordinator.azurewebsites.net') + .get('/api/fn-ghapp/functions/get_installation_token/private/repo/main') + .reply(200, { token: 'some-token' }); + + args[1] = 'https://github.com/private/repo'; await command.handleExecution(args, slackContext); - // Assertions to confirm handling of non-existent repository - expect(slackContext.say.calledWith(':warning: The GitHub repository \'https://github.com/invalid/repo\' could not be found (private repo?).')).to.be.true; + // Should verify Aemy check was made + expect(aemyScope.isDone()).to.be.true; + + // Should parse URL manually and add as private repo with 'main' branch + expect(siteStub.setCode).to.have.been.calledWith({ + type: 'github', + owner: 'private', + repo: 'repo', + ref: 'main', // defaults to 'main' for private repos + url: 'https://github.com/private/repo', + }); + expect(siteStub.save).to.have.been.called; + + // Should send warning about adding as private repo + expect(slackContext.say.calledWithMatch(/GitHub API returned 404/)).to.be.true; + expect(slackContext.say.calledWithMatch(/Adding as private repo/)).to.be.true; + // Should send success message + expect(slackContext.say.calledWithMatch(/GitHub repo added/)).to.be.true; }); it('handles errors other than 404 from GitHub API', async () => { @@ -270,4 +311,166 @@ describe('AddRepoCommand', () => { expect(slackContext.say.calledWithMatch(/Network error occurred/)).to.be.true; }); }); + + describe('Branch Support', () => { + it('handles custom branch for public repository', async () => { + nock('https://api.github.com') + .get('/repos/valid/repo') + .reply(200, { + archived: false, + name: 'repo', + owner: { login: 'valid' }, + default_branch: 'main', + }); + + const aemyScope = nock('https://ec-xp-fapp-coordinator.azurewebsites.net') + .get('/api/fn-ghapp/functions/get_installation_token/valid/repo/develop') + .reply(200, { token: 'some-token' }); + + const args = ['validSite.com', 'https://github.com/valid/repo', 'develop']; + const command = AddRepoCommand(context); + + await command.handleExecution(args, slackContext); + + expect(aemyScope.isDone()).to.be.true; + expect(siteStub.setCode).to.have.been.calledWith({ + type: 'github', + owner: 'valid', + repo: 'repo', + ref: 'develop', // Should use custom branch + url: 'https://github.com/valid/repo', + }); + expect(siteStub.save).to.have.been.called; + }); + + it('handles custom branch for private repository', async () => { + nock('https://api.github.com') + .get('/repos/private/repo') + .reply(404); + + const aemyScope = nock('https://ec-xp-fapp-coordinator.azurewebsites.net') + .get('/api/fn-ghapp/functions/get_installation_token/private/repo/feature-branch') + .reply(200, { token: 'some-token' }); + + const args = ['validSite.com', 'https://github.com/private/repo', 'feature-branch']; + const command = AddRepoCommand(context); + + await command.handleExecution(args, slackContext); + + expect(aemyScope.isDone()).to.be.true; + expect(slackContext.say.calledWithMatch(/GitHub API returned 404/)).to.be.true; + expect(slackContext.say.calledWithMatch(/Adding as private repo/)).to.be.true; + expect(slackContext.say.calledWithMatch(/feature-branch/)).to.be.true; + expect(siteStub.setCode).to.have.been.calledWith({ + type: 'github', + owner: 'private', + repo: 'repo', + ref: 'feature-branch', // Should use custom branch + url: 'https://github.com/private/repo', + }); + expect(siteStub.save).to.have.been.called; + + // Should send success message + expect(slackContext.say.calledWithMatch(/GitHub repo added/)).to.be.true; + }); + + it('handles private repository without custom branch (defaults to main)', async () => { + nock('https://api.github.com') + .get('/repos/private/repo') + .reply(404); + + const aemyScope = nock('https://ec-xp-fapp-coordinator.azurewebsites.net') + .get('/api/fn-ghapp/functions/get_installation_token/private/repo/main') + .reply(200, { token: 'some-token' }); + + const args = ['validSite.com', 'https://github.com/private/repo']; + const command = AddRepoCommand(context); + + await command.handleExecution(args, slackContext); + + expect(aemyScope.isDone()).to.be.true; + expect(siteStub.setCode).to.have.been.calledWith({ + type: 'github', + owner: 'private', + repo: 'repo', + ref: 'main', // Should default to 'main' for private repos + url: 'https://github.com/private/repo', + }); + expect(siteStub.save).to.have.been.called; + }); + }); + + describe('Aemy Integration', () => { + it('blocks repository not onboarded with Aemy', async () => { + nock('https://api.github.com') + .get('/repos/valid/repo') + .reply(200, { + archived: false, + name: 'repo', + owner: { login: 'valid' }, + default_branch: 'main', + }); + + const aemyNock = nock('https://ec-xp-fapp-coordinator.azurewebsites.net') + .get('/api/fn-ghapp/functions/get_installation_token/valid/repo/main') + .reply(200, { token: null }); + + const args = ['validSite.com', 'https://github.com/valid/repo']; + const command = AddRepoCommand(context); + + await command.handleExecution(args, slackContext); + + expect(aemyNock.isDone()).to.be.true; + expect(slackContext.say.calledWith(':warning: The repository \'https://github.com/valid/repo\' is not onboarded with Aemy. Please onboard it with Aemy before adding it to a site.')).to.be.true; + expect(siteStub.save).to.not.have.been.called; + }); + + it('handles Aemy API error responses', async () => { + nock('https://api.github.com') + .get('/repos/valid/repo') + .reply(200, { + archived: false, + name: 'repo', + owner: { login: 'valid' }, + default_branch: 'main', + }); + + const aemyNock = nock('https://ec-xp-fapp-coordinator.azurewebsites.net') + .get('/api/fn-ghapp/functions/get_installation_token/valid/repo/main') + .reply(500, { error: 'Internal Server Error' }); + + const args = ['validSite.com', 'https://github.com/valid/repo']; + const command = AddRepoCommand(context); + + await command.handleExecution(args, slackContext); + + expect(aemyNock.isDone()).to.be.true; + expect(slackContext.say.calledWithMatch(/Failed to check if repository is onboarded with Aemy/)).to.be.true; + expect(siteStub.save).to.not.have.been.called; + }); + + it('handles Aemy API network errors', async () => { + nock('https://api.github.com') + .get('/repos/valid/repo') + .reply(200, { + archived: false, + name: 'repo', + owner: { login: 'valid' }, + default_branch: 'main', + }); + + const aemyNock = nock('https://ec-xp-fapp-coordinator.azurewebsites.net') + .get('/api/fn-ghapp/functions/get_installation_token/valid/repo/main') + .replyWithError('Network error'); + + const args = ['validSite.com', 'https://github.com/valid/repo']; + const command = AddRepoCommand(context); + + await command.handleExecution(args, slackContext); + + expect(aemyNock.isDone()).to.be.true; + expect(slackContext.say.calledWithMatch(/Network error/)).to.be.true; + expect(siteStub.save).to.not.have.been.called; + }); + }); });