Georgios Kalpakas 20556346a3 feat(aio): add API endpoint for notifying about PR updates
This commit adds an API endpoint for notifying the preview server about PR
updates (`/pr-updated`). According to the update, the preview server can take
several actions. Currently, it will only check and (if necessary) update the
PR's preview visibility (but more actions could be supported in the future).
The API can be used with an automatic trigger (e.g. a GitHub webhook) to
instantly update a PR's preview visibility when it changes.

Fixes #16526
2017-07-10 12:30:45 +01:00

320 lines
12 KiB
TypeScript

// Imports
import * as path from 'path';
import * as c from './constants';
import {helper as h} from './helper';
// Tests
h.runForAllSupportedSchemes((scheme, port) => describe(`integration (on ${scheme.toUpperCase()})`, () => {
const hostname = h.nginxHostname;
const host = `${hostname}:${port}`;
const pr9 = '9';
const sha9 = '9'.repeat(40);
const sha0 = '0'.repeat(40);
const archivePath = path.join(h.buildsDir, 'snapshot.tar.gz');
const getFile = (pr: string, sha: string, file: string) =>
h.runCmd(`curl -iL ${scheme}://pr${pr}-${h.getShordSha(sha)}.${host}/${file}`);
const uploadBuild = (pr: string, sha: string, archive: string, authHeader = 'Token FOO') => {
const curlPost = `curl -iLX POST --header "Authorization: ${authHeader}"`;
return h.runCmd(`${curlPost} --data-binary "@${archive}" ${scheme}://${host}/create-build/${pr}/${sha}`);
};
const prUpdated = (pr: number, action?: string) => {
const url = `${scheme}://${host}/pr-updated`;
const payloadStr = JSON.stringify({number: pr, action});
return h.runCmd(`curl -iLX POST --header "Content-Type: application/json" --data '${payloadStr}' ${url}`);
};
beforeEach(() => jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000);
afterEach(() => {
h.deletePrDir(pr9);
h.deletePrDir(pr9, false);
h.cleanUp();
});
describe('for a new/non-existing PR', () => {
it('should be able to upload and serve a public build', done => {
const regexPrefix9 = `^PR: uploaded\\/${pr9} \\| SHA: ${sha9} \\| File:`;
const idxContentRegex9 = new RegExp(`${regexPrefix9} \\/index\\.html$`);
const barContentRegex9 = new RegExp(`${regexPrefix9} \\/foo\\/bar\\.js$`);
h.createDummyArchive(pr9, sha9, archivePath);
uploadBuild(pr9, sha9, archivePath).
then(() => Promise.all([
getFile(pr9, sha9, 'index.html').then(h.verifyResponse(200, idxContentRegex9)),
getFile(pr9, sha9, 'foo/bar.js').then(h.verifyResponse(200, barContentRegex9)),
])).
then(done);
});
it('should be able to upload but not serve a hidden build', done => {
const regexPrefix9 = `^PR: uploaded\\/${pr9} \\| SHA: ${sha9} \\| File:`;
const idxContentRegex9 = new RegExp(`${regexPrefix9} \\/index\\.html$`);
const barContentRegex9 = new RegExp(`${regexPrefix9} \\/foo\\/bar\\.js$`);
h.createDummyArchive(pr9, sha9, archivePath);
uploadBuild(pr9, sha9, archivePath, c.BV_verify_verifiedNotTrusted).
then(() => Promise.all([
getFile(pr9, sha9, 'index.html').then(h.verifyResponse(404)),
getFile(pr9, sha9, 'foo/bar.js').then(h.verifyResponse(404)),
])).
then(() => {
expect(h.buildExists(pr9, sha9)).toBe(false);
expect(h.buildExists(pr9, sha9, false)).toBe(true);
expect(h.readBuildFile(pr9, sha9, 'index.html', false)).toMatch(idxContentRegex9);
expect(h.readBuildFile(pr9, sha9, 'foo/bar.js', false)).toMatch(barContentRegex9);
}).
then(done);
});
it('should reject an upload if verification fails', done => {
const errorRegex9 = new RegExp(`Error while verifying upload for PR ${pr9}: Test`);
h.createDummyArchive(pr9, sha9, archivePath);
uploadBuild(pr9, sha9, archivePath, c.BV_verify_error).
then(h.verifyResponse(403, errorRegex9)).
then(() => {
expect(h.buildExists(pr9)).toBe(false);
expect(h.buildExists(pr9, '', false)).toBe(false);
}).
then(done);
});
it('should be able to notify that a PR has been updated (and do nothing)', done => {
prUpdated(+pr9).
then(h.verifyResponse(200)).
then(() => {
// The PR should still not exist.
expect(h.buildExists(pr9, '', false)).toBe(false);
expect(h.buildExists(pr9, '', true)).toBe(false);
}).
then(done);
});
});
describe('for an existing PR', () => {
it('should be able to upload and serve a public build', done => {
const regexPrefix0 = `^PR: ${pr9} \\| SHA: ${sha0} \\| File:`;
const idxContentRegex0 = new RegExp(`${regexPrefix0} \\/index\\.html$`);
const barContentRegex0 = new RegExp(`${regexPrefix0} \\/foo\\/bar\\.js$`);
const regexPrefix9 = `^PR: uploaded\\/${pr9} \\| SHA: ${sha9} \\| File:`;
const idxContentRegex9 = new RegExp(`${regexPrefix9} \\/index\\.html$`);
const barContentRegex9 = new RegExp(`${regexPrefix9} \\/foo\\/bar\\.js$`);
h.createDummyBuild(pr9, sha0);
h.createDummyArchive(pr9, sha9, archivePath);
uploadBuild(pr9, sha9, archivePath).
then(() => Promise.all([
getFile(pr9, sha0, 'index.html').then(h.verifyResponse(200, idxContentRegex0)),
getFile(pr9, sha0, 'foo/bar.js').then(h.verifyResponse(200, barContentRegex0)),
getFile(pr9, sha9, 'index.html').then(h.verifyResponse(200, idxContentRegex9)),
getFile(pr9, sha9, 'foo/bar.js').then(h.verifyResponse(200, barContentRegex9)),
])).
then(done);
});
it('should be able to upload but not serve a hidden build', done => {
const regexPrefix0 = `^PR: ${pr9} \\| SHA: ${sha0} \\| File:`;
const idxContentRegex0 = new RegExp(`${regexPrefix0} \\/index\\.html$`);
const barContentRegex0 = new RegExp(`${regexPrefix0} \\/foo\\/bar\\.js$`);
const regexPrefix9 = `^PR: uploaded\\/${pr9} \\| SHA: ${sha9} \\| File:`;
const idxContentRegex9 = new RegExp(`${regexPrefix9} \\/index\\.html$`);
const barContentRegex9 = new RegExp(`${regexPrefix9} \\/foo\\/bar\\.js$`);
h.createDummyBuild(pr9, sha0, false);
h.createDummyArchive(pr9, sha9, archivePath);
uploadBuild(pr9, sha9, archivePath, c.BV_verify_verifiedNotTrusted).
then(() => Promise.all([
getFile(pr9, sha0, 'index.html').then(h.verifyResponse(404)),
getFile(pr9, sha0, 'foo/bar.js').then(h.verifyResponse(404)),
getFile(pr9, sha9, 'index.html').then(h.verifyResponse(404)),
getFile(pr9, sha9, 'foo/bar.js').then(h.verifyResponse(404)),
])).
then(() => {
expect(h.buildExists(pr9, sha9)).toBe(false);
expect(h.buildExists(pr9, sha9, false)).toBe(true);
expect(h.readBuildFile(pr9, sha0, 'index.html', false)).toMatch(idxContentRegex0);
expect(h.readBuildFile(pr9, sha0, 'foo/bar.js', false)).toMatch(barContentRegex0);
expect(h.readBuildFile(pr9, sha9, 'index.html', false)).toMatch(idxContentRegex9);
expect(h.readBuildFile(pr9, sha9, 'foo/bar.js', false)).toMatch(barContentRegex9);
}).
then(done);
});
it('should reject an upload if verification fails', done => {
const errorRegex9 = new RegExp(`Error while verifying upload for PR ${pr9}: Test`);
h.createDummyBuild(pr9, sha0);
h.createDummyArchive(pr9, sha9, archivePath);
uploadBuild(pr9, sha9, archivePath, c.BV_verify_error).
then(h.verifyResponse(403, errorRegex9)).
then(() => {
expect(h.buildExists(pr9)).toBe(true);
expect(h.buildExists(pr9, sha0)).toBe(true);
expect(h.buildExists(pr9, sha9)).toBe(false);
}).
then(done);
});
it('should not be able to overwrite an existing public build', done => {
const regexPrefix9 = `^PR: ${pr9} \\| SHA: ${sha9} \\| File:`;
const idxContentRegex9 = new RegExp(`${regexPrefix9} \\/index\\.html$`);
const barContentRegex9 = new RegExp(`${regexPrefix9} \\/foo\\/bar\\.js$`);
h.createDummyBuild(pr9, sha9);
h.createDummyArchive(pr9, sha9, archivePath);
uploadBuild(pr9, sha9, archivePath).
then(h.verifyResponse(409)).
then(() => Promise.all([
getFile(pr9, sha9, 'index.html').then(h.verifyResponse(200, idxContentRegex9)),
getFile(pr9, sha9, 'foo/bar.js').then(h.verifyResponse(200, barContentRegex9)),
])).
then(done);
});
it('should not be able to overwrite an existing hidden build', done => {
const regexPrefix9 = `^PR: ${pr9} \\| SHA: ${sha9} \\| File:`;
const idxContentRegex9 = new RegExp(`${regexPrefix9} \\/index\\.html$`);
const barContentRegex9 = new RegExp(`${regexPrefix9} \\/foo\\/bar\\.js$`);
h.createDummyBuild(pr9, sha9, false);
h.createDummyArchive(pr9, sha9, archivePath);
uploadBuild(pr9, sha9, archivePath, c.BV_verify_verifiedNotTrusted).
then(h.verifyResponse(409)).
then(() => {
expect(h.readBuildFile(pr9, sha9, 'index.html', false)).toMatch(idxContentRegex9);
expect(h.readBuildFile(pr9, sha9, 'foo/bar.js', false)).toMatch(barContentRegex9);
}).
then(done);
});
it('should be able to request re-checking visibility (if outdated)', done => {
const publicPr = pr9;
const hiddenPr = String(c.BV_getPrIsTrusted_notTrusted);
h.createDummyBuild(publicPr, sha9, false);
h.createDummyBuild(hiddenPr, sha9, true);
// PR visibilities are outdated (i.e. the opposte of what the should).
expect(h.buildExists(publicPr, '', false)).toBe(true);
expect(h.buildExists(publicPr, '', true)).toBe(false);
expect(h.buildExists(hiddenPr, '', false)).toBe(false);
expect(h.buildExists(hiddenPr, '', true)).toBe(true);
Promise.
all([
prUpdated(+publicPr).then(h.verifyResponse(200)),
prUpdated(+hiddenPr).then(h.verifyResponse(200)),
]).
then(() => {
// PR visibilities should have been updated.
expect(h.buildExists(publicPr, '', false)).toBe(false);
expect(h.buildExists(publicPr, '', true)).toBe(true);
expect(h.buildExists(hiddenPr, '', false)).toBe(true);
expect(h.buildExists(hiddenPr, '', true)).toBe(false);
}).
then(() => {
h.deletePrDir(publicPr, true);
h.deletePrDir(hiddenPr, false);
}).
then(done);
});
it('should be able to request re-checking visibility (if up-to-date)', done => {
const publicPr = pr9;
const hiddenPr = String(c.BV_getPrIsTrusted_notTrusted);
h.createDummyBuild(publicPr, sha9, true);
h.createDummyBuild(hiddenPr, sha9, false);
// PR visibilities are already up-to-date.
expect(h.buildExists(publicPr, '', false)).toBe(false);
expect(h.buildExists(publicPr, '', true)).toBe(true);
expect(h.buildExists(hiddenPr, '', false)).toBe(true);
expect(h.buildExists(hiddenPr, '', true)).toBe(false);
Promise.
all([
prUpdated(+publicPr).then(h.verifyResponse(200)),
prUpdated(+hiddenPr).then(h.verifyResponse(200)),
]).
then(() => {
// PR visibilities are still up-to-date.
expect(h.buildExists(publicPr, '', false)).toBe(false);
expect(h.buildExists(publicPr, '', true)).toBe(true);
expect(h.buildExists(hiddenPr, '', false)).toBe(true);
expect(h.buildExists(hiddenPr, '', true)).toBe(false);
}).
then(done);
});
it('should reject a request if re-checking visibility fails', done => {
const errorPr = String(c.BV_getPrIsTrusted_error);
h.createDummyBuild(errorPr, sha9, true);
expect(h.buildExists(errorPr, '', false)).toBe(false);
expect(h.buildExists(errorPr, '', true)).toBe(true);
prUpdated(+errorPr).
then(h.verifyResponse(500, /Test/)).
then(() => {
// PR visibility should not have been updated.
expect(h.buildExists(errorPr, '', false)).toBe(false);
expect(h.buildExists(errorPr, '', true)).toBe(true);
}).
then(done);
});
it('should reject a request if updating visibility fails', done => {
// One way to cause an error is to have both a public and a hidden directory for the same PR.
h.createDummyBuild(pr9, sha9, false);
h.createDummyBuild(pr9, sha9, true);
const hiddenPrDir = h.getPrDir(pr9, false);
const publicPrDir = h.getPrDir(pr9, true);
const bodyRegex = new RegExp(`Request to move '${hiddenPrDir}' to existing directory '${publicPrDir}'`);
expect(h.buildExists(pr9, '', false)).toBe(true);
expect(h.buildExists(pr9, '', true)).toBe(true);
prUpdated(+pr9).
then(h.verifyResponse(409, bodyRegex)).
then(() => {
// PR visibility should not have been updated.
expect(h.buildExists(pr9, '', false)).toBe(true);
expect(h.buildExists(pr9, '', true)).toBe(true);
}).
then(done);
});
});
}));