From f90b35a85eca0aec581dc8005cf5f112a1d28819 Mon Sep 17 00:00:00 2001 From: Georgios Kalpakas Date: Tue, 20 Jun 2017 20:22:32 +0300 Subject: [PATCH] test(aio): add e2e tests for non-public previews --- .../lib/upload-server/index-test.ts | 16 +- .../scripts-js/lib/verify-setup/helper.ts | 35 +- .../scripts-js/lib/verify-setup/nginx.e2e.ts | 202 ++++++------ .../verify-setup/server-integration.e2e.ts | 204 +++++++++--- .../lib/verify-setup/upload-server.e2e.ts | 299 ++++++++++++------ 5 files changed, 508 insertions(+), 248 deletions(-) diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/upload-server/index-test.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/upload-server/index-test.ts index fb78e4710d..e5a1d6ed57 100644 --- a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/upload-server/index-test.ts +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/upload-server/index-test.ts @@ -1,10 +1,24 @@ // Imports import {GithubPullRequests} from '../common/github-pull-requests'; import {BUILD_VERIFICATION_STATUS, BuildVerifier} from './build-verifier'; +import {UploadError} from './upload-error'; // Run // TODO(gkalpak): Add e2e tests to cover these interactions as well. GithubPullRequests.prototype.addComment = () => Promise.resolve(); -BuildVerifier.prototype.verify = () => Promise.resolve(BUILD_VERIFICATION_STATUS.verifiedAndTrusted); +BuildVerifier.prototype.verify = (expectedPr: number, authHeader: string) => { + switch (authHeader) { + case 'FAKE_VERIFICATION_ERROR': + // For e2e tests, fake a verification error. + return Promise.reject(new UploadError(403, `Error while verifying upload for PR ${expectedPr}: Test`)); + case 'FAKE_VERIFIED_NOT_TRUSTED': + // For e2e tests, fake a `verifiedNotTrusted` verification status. + return Promise.resolve(BUILD_VERIFICATION_STATUS.verifiedNotTrusted); + default: + // For e2e tests, default to `verifiedAndTrusted` verification status. + return Promise.resolve(BUILD_VERIFICATION_STATUS.verifiedAndTrusted); + } +}; + // tslint:disable-next-line: no-var-requires require('./index'); diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/helper.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/helper.ts index 373449c0e2..040d7faba7 100644 --- a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/helper.ts +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/helper.ts @@ -5,6 +5,7 @@ import * as http from 'http'; import * as path from 'path'; import * as shell from 'shelljs'; import {getEnvVar} from '../common/utils'; +import {BuildCreator} from '../upload-server/build-creator'; // Constans const TEST_AIO_BUILDS_DIR = getEnvVar('TEST_AIO_BUILDS_DIR'); @@ -31,10 +32,10 @@ class Helper { public get nginxHostname() { return TEST_AIO_NGINX_HOSTNAME; } public get nginxPortHttp() { return TEST_AIO_NGINX_PORT_HTTP; } public get nginxPortHttps() { return TEST_AIO_NGINX_PORT_HTTPS; } - public get wwwUser() { return WWW_USER; } public get uploadHostname() { return TEST_AIO_UPLOAD_HOSTNAME; } public get uploadPort() { return TEST_AIO_UPLOAD_PORT; } public get uploadMaxSize() { return TEST_AIO_UPLOAD_MAX_SIZE; } + public get wwwUser() { return WWW_USER; } // Properties - Protected protected cleanUpFns: CleanUpFn[] = []; @@ -50,6 +51,11 @@ class Helper { } // Methods - Public + public buildExists(pr: string, sha = '', isPublic = true): boolean { + const dir = path.join(this.getPrDir(pr, isPublic), sha); + return fs.existsSync(dir); + } + public cleanUp() { while (this.cleanUpFns.length) { // Clean-up fns remove themselves from the list. @@ -66,7 +72,7 @@ class Helper { const cmd1 = `tar --create --gzip --directory "${inputDir}" --file "${archivePath}" .`; const cmd2 = `chown ${this.wwwUser} ${archivePath}`; - const cleanUpTemp = this.createDummyBuild(`uploaded/${pr}`, sha, true); + const cleanUpTemp = this.createDummyBuild(`uploaded/${pr}`, sha, true, true); shell.exec(cmd1); shell.exec(cmd2); cleanUpTemp(); @@ -74,8 +80,8 @@ class Helper { return this.createCleanUpFn(() => shell.rm('-rf', archivePath)); } - public createDummyBuild(pr: string, sha: string, force = false): CleanUpFn { - const prDir = path.join(this.buildsDir, pr); + public createDummyBuild(pr: string, sha: string, isPublic = true, force = false): CleanUpFn { + const prDir = this.getPrDir(pr, isPublic); const shaDir = path.join(prDir, sha); const idxPath = path.join(shaDir, 'index.html'); const barPath = path.join(shaDir, 'foo', 'bar.js'); @@ -87,8 +93,8 @@ class Helper { return this.createCleanUpFn(() => shell.rm('-rf', prDir)); } - public deletePrDir(pr: string) { - const prDir = path.join(this.buildsDir, pr); + public deletePrDir(pr: string, isPublic = true) { + const prDir = this.getPrDir(pr, isPublic); if (fs.existsSync(prDir)) { // Undocumented signature (see https://github.com/shelljs/shelljs/pull/663). @@ -97,8 +103,14 @@ class Helper { } } - public readBuildFile(pr: string, sha: string, relFilePath: string): string { - const absFilePath = path.join(this.buildsDir, pr, sha, relFilePath); + public getPrDir(pr: string, isPublic: boolean): string { + const prDirName = isPublic ? pr : BuildCreator.HIDDEN_DIR_PREFIX + pr; + return path.join(this.buildsDir, prDirName); + } + + public readBuildFile(pr: string, sha: string, relFilePath: string, isPublic = true): string { + const prDir = this.getPrDir(pr, isPublic); + const absFilePath = path.join(prDir, sha, relFilePath); return fs.readFileSync(absFilePath, 'utf8'); } @@ -129,7 +141,8 @@ class Helper { const [headers, body] = result.stdout. split(/(?:\r?\n){2,}/). map(s => s.trim()). - slice(-2); + slice(-2); // In case of redirect, discard the previous headers. + // Only keep the last to sections (final headers and body). if (!result.success) { console.log('Stdout:', result.stdout); @@ -143,8 +156,8 @@ class Helper { }; } - public writeBuildFile(pr: string, sha: string, relFilePath: string, content: string): CleanUpFn { - const absFilePath = path.join(this.buildsDir, pr, sha, relFilePath); + public writeBuildFile(pr: string, sha: string, relFilePath: string, content: string, isPublic = true): CleanUpFn { + const absFilePath = path.join(this.getPrDir(pr, isPublic), sha, relFilePath); return this.writeFile(absFilePath, {content}, true); } diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/nginx.e2e.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/nginx.e2e.ts index 99959c2e44..ba6a253d07 100644 --- a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/nginx.e2e.ts +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/nginx.e2e.ts @@ -31,7 +31,7 @@ describe(`nginx`, () => { }); - h.runForAllSupportedSchemes((scheme, port) => describe(`nginx (on ${scheme.toUpperCase()})`, () => { + h.runForAllSupportedSchemes((scheme, port) => describe(`(on ${scheme.toUpperCase()})`, () => { const hostname = h.nginxHostname; const host = `${hostname}:${port}`; const pr = '9'; @@ -41,101 +41,127 @@ describe(`nginx`, () => { describe(`pr-.${host}/*`, () => { - beforeEach(() => { - h.createDummyBuild(pr, sha9); - h.createDummyBuild(pr, sha0); + describe('(for public builds)', () => { + + beforeEach(() => { + h.createDummyBuild(pr, sha9); + h.createDummyBuild(pr, sha0); + }); + + + it('should return /index.html', done => { + const origin = `${scheme}://pr${pr}-${sha9}.${host}`; + const bodyRegex = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /index\\.html$`); + + Promise.all([ + h.runCmd(`curl -iL ${origin}/index.html`).then(h.verifyResponse(200, bodyRegex)), + h.runCmd(`curl -iL ${origin}/`).then(h.verifyResponse(200, bodyRegex)), + h.runCmd(`curl -iL ${origin}`).then(h.verifyResponse(200, bodyRegex)), + ]).then(done); + }); + + + it('should return /foo/bar.js', done => { + const bodyRegex = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /foo/bar\\.js$`); + + h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha9}.${host}/foo/bar.js`). + then(h.verifyResponse(200, bodyRegex)). + then(done); + }); + + + it('should respond with 403 for directories', done => { + Promise.all([ + h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha9}.${host}/foo/`).then(h.verifyResponse(403)), + h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha9}.${host}/foo`).then(h.verifyResponse(403)), + ]).then(done); + }); + + + it('should respond with 404 for unknown paths to files', done => { + h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha9}.${host}/foo/baz.css`). + then(h.verifyResponse(404)). + then(done); + }); + + + it('should rewrite to \'index.html\' for unknown paths that don\'t look like files', done => { + const bodyRegex = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /index\\.html$`); + + Promise.all([ + h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha9}.${host}/foo/baz`).then(h.verifyResponse(200, bodyRegex)), + h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha9}.${host}/foo/baz/`).then(h.verifyResponse(200, bodyRegex)), + ]).then(done); + }); + + + it('should respond with 404 for unknown PRs/SHAs', done => { + const otherPr = 54321; + const otherSha = '8'.repeat(40); + + Promise.all([ + h.runCmd(`curl -iL ${scheme}://pr${pr}9-${sha9}.${host}`).then(h.verifyResponse(404)), + h.runCmd(`curl -iL ${scheme}://pr${otherPr}-${sha9}.${host}`).then(h.verifyResponse(404)), + h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha9}9.${host}`).then(h.verifyResponse(404)), + h.runCmd(`curl -iL ${scheme}://pr${pr}-${otherSha}.${host}`).then(h.verifyResponse(404)), + ]).then(done); + }); + + + it('should respond with 404 if the subdomain format is wrong', done => { + Promise.all([ + h.runCmd(`curl -iL ${scheme}://xpr${pr}-${sha9}.${host}`).then(h.verifyResponse(404)), + h.runCmd(`curl -iL ${scheme}://prx${pr}-${sha9}.${host}`).then(h.verifyResponse(404)), + h.runCmd(`curl -iL ${scheme}://xx${pr}-${sha9}.${host}`).then(h.verifyResponse(404)), + h.runCmd(`curl -iL ${scheme}://p${pr}-${sha9}.${host}`).then(h.verifyResponse(404)), + h.runCmd(`curl -iL ${scheme}://r${pr}-${sha9}.${host}`).then(h.verifyResponse(404)), + h.runCmd(`curl -iL ${scheme}://${pr}-${sha9}.${host}`).then(h.verifyResponse(404)), + h.runCmd(`curl -iL ${scheme}://pr${pr}${sha9}.${host}`).then(h.verifyResponse(404)), + h.runCmd(`curl -iL ${scheme}://pr${pr}_${sha9}.${host}`).then(h.verifyResponse(404)), + ]).then(done); + }); + + + it('should reject PRs with leading zeros', done => { + h.runCmd(`curl -iL ${scheme}://pr0${pr}-${sha9}.${host}`). + then(h.verifyResponse(404)). + then(done); + }); + + + it('should accept SHAs with leading zeros (but not trim the zeros)', done => { + const bodyRegex9 = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /index\\.html$`); + const bodyRegex0 = new RegExp(`^PR: ${pr} | SHA: ${sha0} | File: /index\\.html$`); + + Promise.all([ + h.runCmd(`curl -iL ${scheme}://pr${pr}-0${sha9}.${host}`).then(h.verifyResponse(404)), + h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha9}.${host}`).then(h.verifyResponse(200, bodyRegex9)), + h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha0}.${host}`).then(h.verifyResponse(200, bodyRegex0)), + ]).then(done); + }); + }); - it('should return /index.html', done => { - const origin = `${scheme}://pr${pr}-${sha9}.${host}`; - const bodyRegex = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /index\\.html$`); + describe('(for hidden builds)', () => { - Promise.all([ - h.runCmd(`curl -iL ${origin}/index.html`).then(h.verifyResponse(200, bodyRegex)), - h.runCmd(`curl -iL ${origin}/`).then(h.verifyResponse(200, bodyRegex)), - h.runCmd(`curl -iL ${origin}`).then(h.verifyResponse(200, bodyRegex)), - ]).then(done); - }); + beforeEach(() => h.createDummyBuild(pr, sha9, false)); - it('should return /foo/bar.js', done => { - const bodyRegex = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /foo/bar\\.js$`); + it('should respond with 404 for any file or directory', done => { + const origin = `${scheme}://pr${pr}-${sha9}.${host}`; + const assert404 = h.verifyResponse(404); - h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha9}.${host}/foo/bar.js`). - then(h.verifyResponse(200, bodyRegex)). - then(done); - }); + Promise.all([ + h.runCmd(`curl -iL ${origin}/index.html`).then(assert404), + h.runCmd(`curl -iL ${origin}/`).then(assert404), + h.runCmd(`curl -iL ${origin}`).then(assert404), + h.runCmd(`curl -iL ${origin}/foo/bar.js`).then(assert404), + h.runCmd(`curl -iL ${origin}/foo/`).then(assert404), + h.runCmd(`curl -iL ${origin}/foo`).then(assert404), + ]).then(done); + }); - - it('should respond with 403 for directories', done => { - Promise.all([ - h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha9}.${host}/foo/`).then(h.verifyResponse(403)), - h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha9}.${host}/foo`).then(h.verifyResponse(403)), - ]).then(done); - }); - - - it('should respond with 404 for unknown paths to files', done => { - h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha9}.${host}/foo/baz.css`). - then(h.verifyResponse(404)). - then(done); - }); - - - it('should rewrite to \'index.html\' for unknown paths that don\'t look like files', done => { - const bodyRegex = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /index\\.html$`); - - Promise.all([ - h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha9}.${host}/foo/baz`).then(h.verifyResponse(200, bodyRegex)), - h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha9}.${host}/foo/baz/`).then(h.verifyResponse(200, bodyRegex)), - ]).then(done); - }); - - - it('should respond with 404 for unknown PRs/SHAs', done => { - const otherPr = 54321; - const otherSha = '8'.repeat(40); - - Promise.all([ - h.runCmd(`curl -iL ${scheme}://pr${pr}9-${sha9}.${host}`).then(h.verifyResponse(404)), - h.runCmd(`curl -iL ${scheme}://pr${otherPr}-${sha9}.${host}`).then(h.verifyResponse(404)), - h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha9}9.${host}`).then(h.verifyResponse(404)), - h.runCmd(`curl -iL ${scheme}://pr${pr}-${otherSha}.${host}`).then(h.verifyResponse(404)), - ]).then(done); - }); - - - it('should respond with 404 if the subdomain format is wrong', done => { - Promise.all([ - h.runCmd(`curl -iL ${scheme}://xpr${pr}-${sha9}.${host}`).then(h.verifyResponse(404)), - h.runCmd(`curl -iL ${scheme}://prx${pr}-${sha9}.${host}`).then(h.verifyResponse(404)), - h.runCmd(`curl -iL ${scheme}://xx${pr}-${sha9}.${host}`).then(h.verifyResponse(404)), - h.runCmd(`curl -iL ${scheme}://p${pr}-${sha9}.${host}`).then(h.verifyResponse(404)), - h.runCmd(`curl -iL ${scheme}://r${pr}-${sha9}.${host}`).then(h.verifyResponse(404)), - h.runCmd(`curl -iL ${scheme}://${pr}-${sha9}.${host}`).then(h.verifyResponse(404)), - h.runCmd(`curl -iL ${scheme}://pr${pr}${sha9}.${host}`).then(h.verifyResponse(404)), - h.runCmd(`curl -iL ${scheme}://pr${pr}_${sha9}.${host}`).then(h.verifyResponse(404)), - ]).then(done); - }); - - - it('should reject PRs with leading zeros', done => { - h.runCmd(`curl -iL ${scheme}://pr0${pr}-${sha9}.${host}`). - then(h.verifyResponse(404)). - then(done); - }); - - - it('should accept SHAs with leading zeros (but not trim the zeros)', done => { - const bodyRegex9 = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /index\\.html$`); - const bodyRegex0 = new RegExp(`^PR: ${pr} | SHA: ${sha0} | File: /index\\.html$`); - - Promise.all([ - h.runCmd(`curl -iL ${scheme}://pr${pr}-0${sha9}.${host}`).then(h.verifyResponse(404)), - h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha9}.${host}`).then(h.verifyResponse(200, bodyRegex9)), - h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha0}.${host}`).then(h.verifyResponse(200, bodyRegex0)), - ]).then(done); }); }); diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/server-integration.e2e.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/server-integration.e2e.ts index 3f0ffa1418..a77a030680 100644 --- a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/server-integration.e2e.ts +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/server-integration.e2e.ts @@ -13,72 +13,188 @@ h.runForAllSupportedSchemes((scheme, port) => describe(`integration (on ${scheme const getFile = (pr: string, sha: string, file: string) => h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha}.${host}/${file}`); - const uploadBuild = (pr: string, sha: string, archive: string) => { - const curlPost = 'curl -iLX POST --header "Authorization: Token FOO"'; + const uploadBuild = (pr: string, sha: string, archive: string, authHeader = 'Token FOO') => { + // Using `FAKE_VERIFICATION_ERROR` or `FAKE_VERIFIED_NOT_TRUSTED` as `authHeader`, + // we can fake the response of the overwritten `BuildVerifier.verify()` method. + // (See 'lib/upload-server/index-test.ts'.) + const curlPost = `curl -iLX POST --header "Authorization: ${authHeader}"`; return h.runCmd(`${curlPost} --data-binary "@${archive}" ${scheme}://${host}/create-build/${pr}/${sha}`); }; beforeEach(() => jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000); afterEach(() => { h.deletePrDir(pr9); + h.deletePrDir(pr9, false); h.cleanUp(); }); - it('should be able to upload and serve a build for a new PR', done => { - const regexPrefix9 = `^PR: uploaded\\/${pr9} \\| SHA: ${sha9} \\| File:`; - const idxContentRegex9 = new RegExp(`${regexPrefix9} \\/index\\.html$`); - const barContentRegex9 = new RegExp(`${regexPrefix9} \\/foo\\/bar\\.js$`); + describe('for a new PR', () => { - h.createDummyArchive(pr9, sha9, archivePath); + 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, 'FAKE_VERIFIED_NOT_TRUSTED'). + 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, 'FAKE_VERIFICATION_ERROR'). + then(h.verifyResponse(403, errorRegex9)). + then(() => { + expect(h.buildExists(pr9)).toBe(false); + expect(h.buildExists(pr9, '', false)).toBe(false); + }). + then(done); + }); - 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 and serve a build for an existing PR', done => { - const regexPrefix0 = `^PR: ${pr9} \\| SHA: ${sha0} \\| File:`; - const idxContentRegex0 = new RegExp(`${regexPrefix0} \\/index\\.html$`); - const barContentRegex0 = new RegExp(`${regexPrefix0} \\/foo\\/bar\\.js$`); + describe('for an existing PR', () => { - const regexPrefix9 = `^PR: uploaded\\/${pr9} \\| SHA: ${sha9} \\| File:`; - const idxContentRegex9 = new RegExp(`${regexPrefix9} \\/index\\.html$`); - const barContentRegex9 = new RegExp(`${regexPrefix9} \\/foo\\/bar\\.js$`); + 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$`); - h.createDummyBuild(pr9, sha0); - h.createDummyArchive(pr9, sha9, archivePath); + const regexPrefix9 = `^PR: uploaded\\/${pr9} \\| SHA: ${sha9} \\| File:`; + const idxContentRegex9 = new RegExp(`${regexPrefix9} \\/index\\.html$`); + const barContentRegex9 = new RegExp(`${regexPrefix9} \\/foo\\/bar\\.js$`); - 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); - }); + 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 not be able to overwrite a build', done => { - const regexPrefix9 = `^PR: ${pr9} \\| SHA: ${sha9} \\| File:`; - const idxContentRegex9 = new RegExp(`${regexPrefix9} \\/index\\.html$`); - const barContentRegex9 = new RegExp(`${regexPrefix9} \\/foo\\/bar\\.js$`); + 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$`); - h.createDummyBuild(pr9, sha9); - h.createDummyArchive(pr9, sha9, archivePath); + 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, 'FAKE_VERIFIED_NOT_TRUSTED'). + 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, 'FAKE_VERIFICATION_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, 'FAKE_VERIFIED_NOT_TRUSTED'). + 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); + }); - 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); }); })); diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/upload-server.e2e.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/upload-server.e2e.ts index dc5adbb49d..7f2a37e9a9 100644 --- a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/upload-server.e2e.ts +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/upload-server.e2e.ts @@ -19,7 +19,8 @@ describe('upload-server (on HTTP)', () => { describe(`${host}/create-build//`, () => { const authorizationHeader = `--header "Authorization: Token FOO"`; const xFileHeader = `--header "X-File: ${h.buildsDir}/snapshot.tar.gz"`; - const curl = `curl -iL ${authorizationHeader} ${xFileHeader}`; + const defaultHeaders = `${authorizationHeader} ${xFileHeader}`; + const curl = (url: string, headers = defaultHeaders) => `curl -iL ${headers} ${url}`; it('should disallow non-GET requests', done => { @@ -42,8 +43,8 @@ describe('upload-server (on HTTP)', () => { const bodyRegex = /^Missing or empty 'AUTHORIZATION' header/; Promise.all([ - h.runCmd(`curl -iL ${headers1} ${url}`).then(h.verifyResponse(401, bodyRegex)), - h.runCmd(`curl -iL ${headers2} ${url}`).then(h.verifyResponse(401, bodyRegex)), + h.runCmd(curl(url, headers1)).then(h.verifyResponse(401, bodyRegex)), + h.runCmd(curl(url, headers2)).then(h.verifyResponse(401, bodyRegex)), ]).then(done); }); @@ -55,14 +56,25 @@ describe('upload-server (on HTTP)', () => { const bodyRegex = /^Missing or empty 'X-FILE' header/; Promise.all([ - h.runCmd(`curl -iL ${headers1} ${url}`).then(h.verifyResponse(400, bodyRegex)), - h.runCmd(`curl -iL ${headers2} ${url}`).then(h.verifyResponse(400, bodyRegex)), + h.runCmd(curl(url, headers1)).then(h.verifyResponse(400, bodyRegex)), + h.runCmd(curl(url, headers2)).then(h.verifyResponse(400, bodyRegex)), ]).then(done); }); + it('should reject requests for which the PR verification fails', done => { + const headers = `--header "Authorization: FAKE_VERIFICATION_ERROR" ${xFileHeader}`; + const url = `http://${host}/create-build/${pr}/${sha9}`; + const bodyRegex = new RegExp(`Error while verifying upload for PR ${pr}: Test`); + + h.runCmd(curl(url, headers)). + then(h.verifyResponse(403, bodyRegex)). + then(done); + }); + + it('should respond with 404 for unknown paths', done => { - const cmdPrefix = `${curl} http://${host}`; + const cmdPrefix = curl(`http://${host}`); Promise.all([ h.runCmd(`${cmdPrefix}/foo/create-build/${pr}/${sha9}`).then(h.verifyResponse(404)), @@ -78,7 +90,7 @@ describe('upload-server (on HTTP)', () => { it('should reject PRs with leading zeros', done => { - h.runCmd(`${curl} http://${host}/create-build/0${pr}/${sha9}`). + h.runCmd(curl(`http://${host}/create-build/0${pr}/${sha9}`)). then(h.verifyResponse(404)). then(done); }); @@ -86,129 +98,208 @@ describe('upload-server (on HTTP)', () => { it('should accept SHAs with leading zeros (but not trim the zeros)', done => { Promise.all([ - h.runCmd(`${curl} http://${host}/create-build/${pr}/0${sha9}`).then(h.verifyResponse(404)), - h.runCmd(`${curl} http://${host}/create-build/${pr}/${sha9}`).then(h.verifyResponse(500)), - h.runCmd(`${curl} http://${host}/create-build/${pr}/${sha0}`).then(h.verifyResponse(500)), + h.runCmd(curl(`http://${host}/create-build/${pr}/0${sha9}`)).then(h.verifyResponse(404)), + h.runCmd(curl(`http://${host}/create-build/${pr}/${sha9}`)).then(h.verifyResponse(500)), + h.runCmd(curl(`http://${host}/create-build/${pr}/${sha0}`)).then(h.verifyResponse(500)), ]).then(done); }); - it('should not overwrite existing builds', done => { - h.createDummyBuild(pr, sha9); - expect(h.readBuildFile(pr, sha9, 'index.html')).toContain('index.html'); - - h.writeBuildFile(pr, sha9, 'index.html', 'My content'); - expect(h.readBuildFile(pr, sha9, 'index.html')).toBe('My content'); - - h.runCmd(`${curl} http://${host}/create-build/${pr}/${sha9}`). - then(h.verifyResponse(409, /^Request to overwrite existing directory/)). - then(() => expect(h.readBuildFile(pr, sha9, 'index.html')).toBe('My content')). - then(done); - }); + [true, false].forEach(isPublic => describe(`(for ${isPublic ? 'public' : 'hidden'} builds)`, () => { + const authorizationHeader2 = isPublic ? + authorizationHeader : '--header "Authorization: FAKE_VERIFIED_NOT_TRUSTED"'; + const cmdPrefix = curl('', `${authorizationHeader2} ${xFileHeader}`); - it('should delete the PR directory on error (for new PR)', done => { - const prDir = path.join(h.buildsDir, pr); + it('should not overwrite existing builds', done => { + h.createDummyBuild(pr, sha9, isPublic); + expect(h.readBuildFile(pr, sha9, 'index.html', isPublic)).toContain('index.html'); - h.runCmd(`${curl} http://${host}/create-build/${pr}/${sha9}`). - then(h.verifyResponse(500)). - then(() => expect(fs.existsSync(prDir)).toBe(false)). - then(done); - }); + h.writeBuildFile(pr, sha9, 'index.html', 'My content', isPublic); + expect(h.readBuildFile(pr, sha9, 'index.html', isPublic)).toBe('My content'); - - it('should only delete the SHA directory on error (for existing PR)', done => { - const prDir = path.join(h.buildsDir, pr); - const shaDir = path.join(prDir, sha9); - - h.createDummyBuild(pr, sha0); - - h.runCmd(`${curl} http://${host}/create-build/${pr}/${sha9}`). - then(h.verifyResponse(500)). - then(() => { - expect(fs.existsSync(shaDir)).toBe(false); - expect(fs.existsSync(prDir)).toBe(true); - }). - then(done); - }); - - - describe('on successful upload', () => { - const archivePath = path.join(h.buildsDir, 'snapshot.tar.gz'); - let uploadPromise: Promise; - - beforeEach(() => { - h.createDummyArchive(pr, sha9, archivePath); - uploadPromise = h.runCmd(`${curl} http://${host}/create-build/${pr}/${sha9}`); - }); - afterEach(() => h.deletePrDir(pr)); - - - it('should respond with 201', done => { - uploadPromise.then(h.verifyResponse(201)).then(done); + h.runCmd(`${cmdPrefix} http://${host}/create-build/${pr}/${sha9}`). + then(h.verifyResponse(409, /^Request to overwrite existing directory/)). + then(() => expect(h.readBuildFile(pr, sha9, 'index.html', isPublic)).toBe('My content')). + then(done); }); - it('should extract the contents of the uploaded file', done => { - uploadPromise. + it('should delete the PR directory on error (for new PR)', done => { + h.runCmd(`${cmdPrefix} http://${host}/create-build/${pr}/${sha9}`). + then(h.verifyResponse(500)). + then(() => expect(h.buildExists(pr, '', isPublic)).toBe(false)). + then(done); + }); + + + it('should only delete the SHA directory on error (for existing PR)', done => { + h.createDummyBuild(pr, sha0, isPublic); + + h.runCmd(`${cmdPrefix} http://${host}/create-build/${pr}/${sha9}`). + then(h.verifyResponse(500)). then(() => { - expect(h.readBuildFile(pr, sha9, 'index.html')).toContain(`uploaded/${pr}`); - expect(h.readBuildFile(pr, sha9, 'foo/bar.js')).toContain(`uploaded/${pr}`); + expect(h.buildExists(pr, sha9, isPublic)).toBe(false); + expect(h.buildExists(pr, '', isPublic)).toBe(true); }). then(done); }); - it(`should create files/directories owned by '${h.wwwUser}'`, done => { - const shaDir = path.join(h.buildsDir, pr, sha9); - const idxPath = path.join(shaDir, 'index.html'); - const barPath = path.join(shaDir, 'foo', 'bar.js'); + describe('on successful upload', () => { + const archivePath = path.join(h.buildsDir, 'snapshot.tar.gz'); + const statusCode = isPublic ? 201 : 202; + let uploadPromise: Promise; + + beforeEach(() => { + h.createDummyArchive(pr, sha9, archivePath); + uploadPromise = h.runCmd(`${cmdPrefix} http://${host}/create-build/${pr}/${sha9}`); + }); + afterEach(() => h.deletePrDir(pr, isPublic)); + + + it(`should respond with ${statusCode}`, done => { + uploadPromise.then(h.verifyResponse(statusCode)).then(done); + }); + + + it('should extract the contents of the uploaded file', done => { + uploadPromise. + then(() => { + expect(h.readBuildFile(pr, sha9, 'index.html', isPublic)).toContain(`uploaded/${pr}`); + expect(h.readBuildFile(pr, sha9, 'foo/bar.js', isPublic)).toContain(`uploaded/${pr}`); + }). + then(done); + }); + + + it(`should create files/directories owned by '${h.wwwUser}'`, done => { + const prDir = h.getPrDir(pr, isPublic); + const shaDir = path.join(prDir, sha9); + const idxPath = path.join(shaDir, 'index.html'); + const barPath = path.join(shaDir, 'foo', 'bar.js'); + + uploadPromise. + then(() => Promise.all([ + h.runCmd(`find ${shaDir}`), + h.runCmd(`find ${shaDir} -user ${h.wwwUser}`), + ])). + then(([{stdout: allFiles}, {stdout: userFiles}]) => { + expect(userFiles).toBe(allFiles); + expect(userFiles).toContain(shaDir); + expect(userFiles).toContain(idxPath); + expect(userFiles).toContain(barPath); + }). + then(done); + }); + + + it('should delete the uploaded file', done => { + expect(fs.existsSync(archivePath)).toBe(true); + uploadPromise. + then(() => expect(fs.existsSync(archivePath)).toBe(false)). + then(done); + }); + + + it('should make the build directory non-writable', done => { + const prDir = h.getPrDir(pr, isPublic); + const shaDir = path.join(prDir, sha9); + const idxPath = path.join(shaDir, 'index.html'); + const barPath = path.join(shaDir, 'foo', 'bar.js'); + + // See https://github.com/nodejs/node-v0.x-archive/issues/3045#issuecomment-4862588. + const isNotWritable = (fileOrDir: string) => { + const mode = fs.statSync(fileOrDir).mode; + // tslint:disable-next-line: no-bitwise + return !(mode & parseInt('222', 8)); + }; + + uploadPromise. + then(() => { + expect(isNotWritable(shaDir)).toBe(true); + expect(isNotWritable(idxPath)).toBe(true); + expect(isNotWritable(barPath)).toBe(true); + }). + then(done); + }); - uploadPromise. - then(() => Promise.all([ - h.runCmd(`find ${shaDir}`), - h.runCmd(`find ${shaDir} -user ${h.wwwUser}`), - ])). - then(([{stdout: allFiles}, {stdout: userFiles}]) => { - expect(userFiles).toBe(allFiles); - expect(userFiles).toContain(shaDir); - expect(userFiles).toContain(idxPath); - expect(userFiles).toContain(barPath); - }). - then(done); }); - it('should delete the uploaded file', done => { - expect(fs.existsSync(archivePath)).toBe(true); - uploadPromise. - then(() => expect(fs.existsSync(archivePath)).toBe(false)). - then(done); - }); + describe('when the PR\'s visibility has changed', () => { + const archivePath = path.join(h.buildsDir, 'snapshot.tar.gz'); + const statusCode = isPublic ? 201 : 202; - - it('should make the build directory non-writable', done => { - const shaDir = path.join(h.buildsDir, pr, sha9); - const idxPath = path.join(shaDir, 'index.html'); - const barPath = path.join(shaDir, 'foo', 'bar.js'); - - // See https://github.com/nodejs/node-v0.x-archive/issues/3045#issuecomment-4862588. - const isNotWritable = (fileOrDir: string) => { - const mode = fs.statSync(fileOrDir).mode; - // tslint:disable-next-line: no-bitwise - return !(mode & parseInt('222', 8)); + const checkPrVisibility = (isPublic2: boolean) => { + expect(h.buildExists(pr, '', isPublic2)).toBe(true); + expect(h.buildExists(pr, '', !isPublic2)).toBe(false); + expect(h.buildExists(pr, sha0, isPublic2)).toBe(true); + expect(h.buildExists(pr, sha0, !isPublic2)).toBe(false); }; + const uploadBuild = (sha: string) => h.runCmd(`${cmdPrefix} http://${host}/create-build/${pr}/${sha}`); + + beforeEach(() => { + h.createDummyBuild(pr, sha0, !isPublic); + h.createDummyArchive(pr, sha9, archivePath); + checkPrVisibility(!isPublic); + }); + afterEach(() => h.deletePrDir(pr, isPublic)); + + + it('should update the PR\'s visibility', done => { + uploadBuild(sha9). + then(h.verifyResponse(statusCode)). + then(() => { + checkPrVisibility(isPublic); + expect(h.buildExists(pr, sha9, isPublic)).toBe(true); + expect(h.readBuildFile(pr, sha9, 'index.html', isPublic)).toContain(`uploaded/${pr}`); + expect(h.readBuildFile(pr, sha9, 'index.html', isPublic)).toContain(sha9); + }). + then(done); + }); + + + it('should not overwrite existing builds (but keep the updated visibility)', done => { + expect(h.buildExists(pr, sha0, isPublic)).toBe(false); + + uploadBuild(sha0). + then(h.verifyResponse(409, /^Request to overwrite existing directory/)). + then(() => { + checkPrVisibility(isPublic); + expect(h.readBuildFile(pr, sha0, 'index.html', isPublic)).toContain(pr); + expect(h.readBuildFile(pr, sha0, 'index.html', isPublic)).not.toContain(`uploaded/${pr}`); + expect(h.readBuildFile(pr, sha0, 'index.html', isPublic)).toContain(sha0); + expect(h.readBuildFile(pr, sha0, 'index.html', isPublic)).not.toContain(sha9); + }). + then(done); + }); + + + it('should reject the request if it fails to update the PR\'s visibility', done => { + // One way to cause an error is to have both a public and a hidden directory for the sme PR. + h.createDummyBuild(pr, sha0, isPublic); + + expect(h.buildExists(pr, sha0, isPublic)).toBe(true); + expect(h.buildExists(pr, sha0, !isPublic)).toBe(true); + + const errorRegex = new RegExp(`^Request to move '${h.getPrDir(pr, !isPublic)}' ` + + `to existing directory '${h.getPrDir(pr, isPublic)}'.`); + + uploadBuild(sha9). + then(h.verifyResponse(409, errorRegex)). + then(() => { + expect(h.buildExists(pr, sha0, isPublic)).toBe(true); + expect(h.buildExists(pr, sha0, !isPublic)).toBe(true); + expect(h.buildExists(pr, sha9, isPublic)).toBe(false); + expect(h.buildExists(pr, sha9, !isPublic)).toBe(false); + }). + then(done); + }); - uploadPromise. - then(() => { - expect(isNotWritable(shaDir)).toBe(true); - expect(isNotWritable(idxPath)).toBe(true); - expect(isNotWritable(barPath)).toBe(true); - }). - then(done); }); - }); + })); });