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}`;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shall we read it from an environment variable as done here. You may need to ask in the channel to add the environment variable in the api service.

https://github.com/adobe/spacecat-import-worker/pull/442/files#diff-a4f3f338bc03c3a0a8b82bb5ed97342471ed7c5caf7e74576d342a089e472938R39

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @vdua, I was also thinking the same. Would you also happen to if env variable AEMY_API_KEY is available to spacecat-api-worker or not?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not have access to check the env variables. Can you check in the slack channel.

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;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bosschaert Since we are invoking AEMY shall we not get all the info from there itself ? Can we add a JIRA in the backlog for the same. We would need to know the default_branch in the long term.

}

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;
});
});
});