Skip to content

Commit 3ebb091

Browse files
authored
Merge pull request #4790 from cloud-gov/feat-add-editor-site-build-webhook
feat: Add webhook to create editor site builds
2 parents 52af2bb + fed05f0 commit 3ebb091

File tree

5 files changed

+179
-17
lines changed

5 files changed

+179
-17
lines changed

api/controllers/webhook.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,13 @@ module.exports = wrapHandlers({
4444

4545
return res.ok();
4646
},
47+
48+
async siteBuild(req, res) {
49+
const { body } = req;
50+
const siteId = decrypt(body.siteId, encryption.key);
51+
52+
await Webhooks.createBuildForEditor(siteId);
53+
54+
return res.ok();
55+
},
4756
});

api/routers/webhook.js

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -32,32 +32,47 @@ function verifySignature(req, res, next) {
3232
next();
3333
}
3434

35-
function verifySiteRequest(req, res, next) {
36-
const { body: payload } = req;
37-
const expectedKeys = ['userEmail', 'apiKey', 'siteId', 'siteName', 'org'].sort();
35+
function verifySiteRequest(expectedKeys) {
36+
return (req, res, next) => {
37+
const { body: payload } = req;
38+
const sortedExpectedKeys = expectedKeys.sort();
3839

39-
// ToDo Add additional headers to check if request is legit
40+
// ToDo Add additional headers to check if request is legit
4041

41-
try {
42-
const payloadKeys = Object.keys(payload).sort();
42+
try {
43+
const payloadKeys = Object.keys(payload).sort();
4344

44-
if (payloadKeys.length !== expectedKeys.length) {
45-
throw new Error('Invalid request payload');
46-
}
45+
if (payloadKeys.length !== sortedExpectedKeys.length) {
46+
throw new Error('Invalid request payload');
47+
}
4748

48-
const hasKeys = payloadKeys.every((value, index) => value === expectedKeys[index]);
49+
const hasKeys = payloadKeys.every(
50+
(value, index) => value === sortedExpectedKeys[index],
51+
);
4952

50-
if (!hasKeys) throw new Error('Invalid request payload');
51-
} catch (err) {
52-
res.badRequest();
53-
next(err);
54-
}
53+
if (!hasKeys) throw new Error('Invalid request payload');
54+
} catch (err) {
55+
res.badRequest();
56+
next(err);
57+
}
5558

56-
next();
59+
next();
60+
};
5761
}
5862

63+
const verifyNewEditorSite = verifySiteRequest([
64+
'userEmail',
65+
'apiKey',
66+
'siteId',
67+
'siteName',
68+
'org',
69+
]);
70+
71+
const verifyEditorSiteBuild = verifySiteRequest(['siteId']);
72+
5973
router.post('/webhook/github', verifySignature, WebhookController.github);
6074
router.post('/webhook/organization', verifySignature, WebhookController.organization);
61-
router.post('/webhook/site', verifySiteRequest, WebhookController.site);
75+
router.post('/webhook/site', verifyNewEditorSite, WebhookController.site);
76+
router.post('/webhook/site/build', verifyEditorSiteBuild, WebhookController.siteBuild);
6277

6378
module.exports = router;

api/services/Webhooks.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ const { Build, User, Site, Event, Organization } = require('../models');
33
const GithubBuildHelper = require('./GithubBuildHelper');
44
const EventCreator = require('./EventCreator');
55

6+
const { OPS_EMAIL } = process.env;
7+
68
const findSiteForWebhookRequest = (payload) => {
79
const [owner, repository] = payload.repository.full_name.split('/');
810

@@ -15,6 +17,32 @@ const findSiteForWebhookRequest = (payload) => {
1517
});
1618
};
1719

20+
const createBuildForEditor = async (siteId) => {
21+
const branch = 'main';
22+
const { id: userId, username } = await User.byUAAEmail(OPS_EMAIL).findOne();
23+
24+
const queuedBuild = await Build.findOne({
25+
where: {
26+
branch,
27+
state: ['created', 'queued'],
28+
site: siteId,
29+
},
30+
});
31+
32+
if (queuedBuild) {
33+
return;
34+
}
35+
36+
const build = await Build.create({
37+
branch,
38+
site: siteId,
39+
user: userId,
40+
username,
41+
});
42+
43+
return build.enqueue();
44+
};
45+
1846
const shouldBuildForSite = (site) =>
1947
site?.isActive && (!site.Organization || site.Organization.isActive);
2048

@@ -116,6 +144,7 @@ const pushWebhookRequest = async (payload) => {
116144
};
117145

118146
module.exports = {
147+
createBuildForEditor,
119148
organizationWebhookRequest,
120149
pushWebhookRequest,
121150
};

test/api/requests/webhook.test.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
const { expect } = require('chai');
12
const crypto = require('crypto');
23
const sinon = require('sinon');
34
const request = require('supertest');
@@ -7,6 +8,9 @@ const factory = require('../support/factory');
78
const { createSiteUserOrg } = require('../support/site-user');
89
const EventCreator = require('../../../api/services/EventCreator');
910
const Webhooks = require('../../../api/services/Webhooks');
11+
const Encryptor = require('../../../api/services/Encryptor');
12+
const { encryption } = require('../../../config');
13+
const QueueJobs = require('../../../api/queue-jobs');
1014

1115
describe('Webhook API', () => {
1216
const signWebhookPayload = (payload) => {
@@ -229,10 +233,56 @@ describe('Webhook API', () => {
229233
});
230234

231235
describe('POST /webhook/site', () => {
236+
it('should respon with a 200 with valid payload', async () => {
237+
const userEmail = '[email protected]';
238+
const apiKey = 'an-api-key';
239+
const siteId = '123';
240+
const siteName = 'site';
241+
const org = 'org';
242+
const payload = Encryptor.encryptObjectValues(
243+
{ userEmail, apiKey, siteId, siteName, org },
244+
encryption.key,
245+
);
246+
247+
const stub = sinon
248+
.stub(QueueJobs.prototype, 'startCreateEditorSiteTask')
249+
.resolves();
250+
251+
await request(app).post('/webhook/site').send(payload).expect(200);
252+
253+
expect(
254+
stub.calledOnceWith({
255+
userEmail,
256+
apiKey,
257+
siteId,
258+
siteName,
259+
orgName: org,
260+
}),
261+
).to.be.equal(true);
262+
});
263+
232264
it('should respond with a 400 if payload is invalid', async () => {
233265
const payload = { bad: 'payload' };
234266

235267
await request(app).post('/webhook/site').send(payload).expect(400);
236268
});
237269
});
270+
271+
describe('POST /webhook/site/build', () => {
272+
it('should respon with a 200 with valid payload', async () => {
273+
const siteId = '123';
274+
const payload = Encryptor.encryptObjectValues({ siteId }, encryption.key);
275+
276+
const stub = sinon.stub(Webhooks, 'createBuildForEditor').resolves();
277+
278+
await request(app).post('/webhook/site/build').send(payload).expect(200);
279+
expect(stub.calledOnceWith(siteId)).to.be.equal(true);
280+
});
281+
282+
it('should respond with a 400 if payload is invalid', async () => {
283+
const payload = { bad: 'payload' };
284+
285+
await request(app).post('/webhook/site/build').send(payload).expect(400);
286+
});
287+
});
238288
});

test/api/unit/services/Webhooks.test.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,65 @@ describe('Webhooks Service', () => {
5757
sinon.restore();
5858
});
5959

60+
describe('createBuildForEditor', () => {
61+
// Generate Ops User
62+
before(() => factory.userWithUAAIdentity.create({ email: process.env.OPS_EMAIL }));
63+
after(() => User.truncate());
64+
65+
it('should start a site build', async () => {
66+
const site = await factory.site();
67+
const stub = sinon.stub(Build.prototype, 'enqueue').resolves();
68+
const numBuildsBefore = await Build.count({
69+
where: {
70+
site: site.id,
71+
},
72+
});
73+
expect(numBuildsBefore).to.eq(0);
74+
75+
await Webhooks.createBuildForEditor(site.id);
76+
77+
expect(stub.calledOnce).to.be.equal(true);
78+
79+
const numBuildsAfter = await Build.count({
80+
where: {
81+
site: site.id,
82+
},
83+
});
84+
expect(numBuildsAfter).to.eq(1);
85+
86+
const build = await Build.findOne({ where: { site: site.id } });
87+
expect(build.branch).to.equal('main');
88+
});
89+
90+
it('should not start a new build if one is created or queued', async () => {
91+
const site = await factory.site();
92+
const stub = sinon.stub(Build.prototype, 'enqueue').resolves();
93+
await Build.create({
94+
branch: 'main',
95+
site: site.id,
96+
username: 'test',
97+
state: 'queued',
98+
});
99+
const numBuildsBefore = await Build.count({
100+
where: {
101+
site: site.id,
102+
},
103+
});
104+
expect(numBuildsBefore).to.eq(1);
105+
106+
await Webhooks.createBuildForEditor(site.id);
107+
108+
expect(stub.notCalled).to.be.equal(true);
109+
110+
const numBuildsAfter = await Build.count({
111+
where: {
112+
site: site.id,
113+
},
114+
});
115+
expect(numBuildsAfter).to.eq(1);
116+
});
117+
});
118+
60119
describe('pushWebhookRequest', () => {
61120
beforeEach(() => {
62121
nock.cleanAll();

0 commit comments

Comments
 (0)