Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 40 additions & 9 deletions src/support/slack/commands/add-repo.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
Expand All @@ -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);
Expand All @@ -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);
Expand Down
215 changes: 209 additions & 6 deletions test/support/slack/commands/add-repo.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ describe('AddRepoCommand', () => {
let siteStub;

beforeEach(() => {
process.env.AEMY_API_KEY = 'test-api-key';

sqsStub = {
sendMessage: sinon.stub().resolves(),
};
Expand Down Expand Up @@ -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,
};
});
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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 <undefined|undefined>*\n'
+ '\n'
Expand Down Expand Up @@ -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',
Expand All @@ -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 () => {
Expand All @@ -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;
});
});
});