From a0d983aff9c7d5e491b74fa23c846a88937bba03 Mon Sep 17 00:00:00 2001 From: Erez Rokah Date: Mon, 21 Sep 2020 10:59:00 -0700 Subject: [PATCH] feat: edge handlers deploy (#1244) --- src/commands/deploy.js | 30 +++++-- src/lib/api.js | 78 +++++++++++++++++ src/lib/fs.js | 26 ++++++ src/lib/spinner.js | 21 +++++ src/utils/command.js | 35 ++++---- src/utils/edge-handlers.js | 88 +++++++++++++++++++ src/utils/env.js | 12 +-- src/utils/gitignore.js | 23 ++--- tests/command.addons.test.js | 32 +------ tests/command.deploy.test.js | 137 ++++++++++++++++++++++++++++++ tests/command.env.test.js | 32 +------ tests/utils/callCli.js | 3 +- tests/utils/createLiveTestSite.js | 33 +++++-- tests/utils/siteBuilder.js | 13 +++ 14 files changed, 451 insertions(+), 112 deletions(-) create mode 100644 src/lib/api.js create mode 100644 src/lib/fs.js create mode 100644 src/lib/spinner.js create mode 100644 src/utils/edge-handlers.js create mode 100644 tests/command.deploy.test.js diff --git a/src/commands/deploy.js b/src/commands/deploy.js index c6b57386c97..ace4193938f 100644 --- a/src/commands/deploy.js +++ b/src/commands/deploy.js @@ -4,8 +4,6 @@ const path = require('path') const chalk = require('chalk') const { flags } = require('@oclif/command') const get = require('lodash.get') -const fs = require('fs') -const { promisify } = require('util') const prettyjson = require('prettyjson') const ora = require('ora') const logSymbols = require('log-symbols') @@ -16,8 +14,8 @@ const isObject = require('lodash.isobject') const SitesCreateCommand = require('./sites/create') const LinkCommand = require('./link') const { NETLIFYDEV, NETLIFYDEVLOG, NETLIFYDEVERR } = require('../utils/logo') - -const statAsync = promisify(fs.stat) +const { statAsync } = require('../lib/fs') +const { deployEdgeHandlers } = require('../utils/edge-handlers') const DEFAULT_DEPLOY_TIMEOUT = 1.2e6 @@ -136,6 +134,7 @@ const validateFolders = async ({ deployFolder, functionsFolder, error, log }) => const runDeploy = async ({ flags, deployToProduction, + site, siteData, api, siteId, @@ -170,15 +169,28 @@ const runDeploy = async ({ log('Deploying to draft URL...') } + const draft = !deployToProduction && !alias + const title = flags.message + results = await api.createSiteDeploy({ siteId, title, body: { draft, branch: alias } }) + const deployId = results.id + + const silent = flags.json || flags.silent + await deployEdgeHandlers({ + site, + deployId, + api, + silent, + error, + warn, + }) results = await api.deploy(siteId, deployFolder, { configPath, fnDir: functionsFolder, - statusCb: flags.json || flags.silent ? () => {} : deployProgressCb(), - draft: !deployToProduction && !alias, - message: flags.message, + statusCb: silent ? () => {} : deployProgressCb(), deployTimeout: flags.timeout * 1000 || DEFAULT_DEPLOY_TIMEOUT, syncFileLimit: 100, - branch: alias, + // pass an existing deployId to update + deployId, }) } catch (e) { switch (true) { @@ -212,7 +224,6 @@ const runDeploy = async ({ const logsUrl = `${get(results, 'deploy.admin_url')}/deploys/${get(results, 'deploy.id')}` return { - name: results.deploy.deployId, siteId: results.deploy.site_id, siteName: results.deploy.name, deployId: results.deployId, @@ -359,6 +370,7 @@ class DeployCommand extends Command { const results = await runDeploy({ flags, deployToProduction, + site, siteData, api, siteId, diff --git a/src/lib/api.js b/src/lib/api.js new file mode 100644 index 00000000000..c799c1650f2 --- /dev/null +++ b/src/lib/api.js @@ -0,0 +1,78 @@ +// This file should be used to wrap API methods that are not part of our open API spec yet +// Once they become part of the spec, js-client should be used +const fetch = require('node-fetch') + +const getHeaders = ({ token }) => { + return { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}`, + } +} + +const getErrorMessage = async ({ response }) => { + const contentType = response.headers.get('content-type') + if (contentType && contentType.indexOf('application/json') !== -1) { + const json = await response.json() + return json.message + } else { + const text = await response.text() + return text + } +} + +const checkResponse = async ({ response }) => { + if (!response.ok) { + const message = await getErrorMessage({ response }).catch(() => undefined) + const errorPostfix = message && message ? ` and message '${message}'` : '' + throw new Error(`Request failed with status '${response.status}'${errorPostfix}`) + } +} + +const getApiUrl = ({ api }) => { + return `${api.scheme}://${api.host}${api.pathPrefix}` +} + +const apiPost = async ({ api, path, data }) => { + const apiUrl = getApiUrl({ api }) + const response = await fetch(`${apiUrl}/${path}`, { + method: 'POST', + body: JSON.stringify(data), + headers: getHeaders({ token: api.accessToken }), + agent: api.agent, + }) + + await checkResponse({ response }) + + return response +} + +const uploadEdgeHandlers = async ({ api, deployId, bundleBuffer, manifest }) => { + const response = await apiPost({ api, path: `deploys/${deployId}/edge_handlers`, data: manifest }) + const { error, exists, upload_url: uploadUrl } = await response.json() + if (error) { + throw new Error(error) + } + + if (exists) { + return false + } + + if (!uploadUrl) { + throw new Error('Missing upload URL') + } + + const putResponse = await fetch(uploadUrl, { + method: 'PUT', + body: bundleBuffer, + headers: { + 'Content-Type': 'application/javascript', + }, + agent: api.agent, + }) + + await checkResponse({ response: putResponse }) + + return true +} + +module.exports = { uploadEdgeHandlers } diff --git a/src/lib/fs.js b/src/lib/fs.js new file mode 100644 index 00000000000..4b8f88676bc --- /dev/null +++ b/src/lib/fs.js @@ -0,0 +1,26 @@ +const fs = require('fs') +const { promisify } = require('util') + +const statAsync = promisify(fs.stat) +const readFileAsync = promisify(fs.readFile) +const writeFileAsync = promisify(fs.writeFile) +const accessAsync = promisify(fs.access) + +const readFileAsyncCatchError = async filepath => { + try { + return { content: await readFileAsync(filepath) } + } catch (error) { + return { error } + } +} + +const fileExistsAsync = async filePath => { + try { + await accessAsync(filePath, fs.F_OK) + return true + } catch (_) { + return false + } +} + +module.exports = { statAsync, readFileAsync, readFileAsyncCatchError, writeFileAsync, fileExistsAsync } diff --git a/src/lib/spinner.js b/src/lib/spinner.js new file mode 100644 index 00000000000..cc963e43200 --- /dev/null +++ b/src/lib/spinner.js @@ -0,0 +1,21 @@ +const ora = require('ora') +const logSymbols = require('log-symbols') + +const startSpinner = ({ text }) => { + return ora({ + text, + }).start() +} + +const stopSpinner = ({ spinner, text, error }) => { + if (!spinner) { + return + } + const symbol = error ? logSymbols.error : logSymbols.success + spinner.stopAndPersist({ + text, + symbol, + }) +} + +module.exports = { startSpinner, stopSpinner } diff --git a/src/utils/command.js b/src/utils/command.js index 7bd846d3b3c..ce739313a0a 100644 --- a/src/utils/command.js +++ b/src/utils/command.js @@ -17,6 +17,24 @@ const { NETLIFY_AUTH_TOKEN, NETLIFY_API_URL } = process.env // Todo setup client for multiple environments const CLIENT_ID = 'd6f37de6614df7ae58664cfca524744d73807a377f5ee71f1a254f78412e3750' +const getToken = tokenFromFlag => { + // 1. First honor command flag --auth + if (tokenFromFlag) { + return [tokenFromFlag, 'flag'] + } + // 2. then Check ENV var + if (NETLIFY_AUTH_TOKEN && NETLIFY_AUTH_TOKEN !== 'null') { + return [NETLIFY_AUTH_TOKEN, 'env'] + } + // 3. If no env var use global user setting + const userId = globalConfig.get('userId') + const tokenFromConfig = globalConfig.get(`users.${userId}.auth.token`) + if (tokenFromConfig) { + return [tokenFromConfig, 'config'] + } + return [null, 'not found'] +} + class BaseCommand extends Command { constructor(...args) { super(...args) @@ -179,21 +197,7 @@ class BaseCommand extends Command { * @return {[string, string]} - tokenValue & location of resolved Netlify API token */ getConfigToken(tokenFromFlag) { - // 1. First honor command flag --auth - if (tokenFromFlag) { - return [tokenFromFlag, 'flag'] - } - // 2. then Check ENV var - if (NETLIFY_AUTH_TOKEN && NETLIFY_AUTH_TOKEN !== 'null') { - return [NETLIFY_AUTH_TOKEN, 'env'] - } - // 3. If no env var use global user setting - const userId = globalConfig.get('userId') - const tokenFromConfig = globalConfig.get(`users.${userId}.auth.token`) - if (tokenFromConfig) { - return [tokenFromConfig, 'config'] - } - return [null, 'not found'] + return getToken(tokenFromFlag) } async authenticate(tokenFromFlag) { @@ -291,4 +295,5 @@ BaseCommand.flags = { }), } +BaseCommand.getToken = getToken module.exports = BaseCommand diff --git a/src/utils/edge-handlers.js b/src/utils/edge-handlers.js new file mode 100644 index 00000000000..5ca753d1902 --- /dev/null +++ b/src/utils/edge-handlers.js @@ -0,0 +1,88 @@ +const path = require('path') +const { statAsync, readFileAsyncCatchError } = require('../lib/fs') +const { uploadEdgeHandlers } = require('../lib/api') +const { startSpinner, stopSpinner } = require('../lib/spinner') + +const MANIFEST_FILENAME = 'manifest.json' +const EDGE_HANDLERS_FOLDER = '.netlify/edge-handlers' + +const validateEdgeHandlerFolder = async ({ site, error }) => { + try { + const resolvedFolder = path.resolve(site.root, EDGE_HANDLERS_FOLDER) + const stat = await statAsync(resolvedFolder) + if (!stat.isDirectory()) { + error(`Edge Handlers folder ${EDGE_HANDLERS_FOLDER} must be a path to a directory`) + } + return resolvedFolder + } catch (e) { + // ignore errors at the moment + // TODO: report error if 'edge_handlers' config exists after + // https://github.com/netlify/build/pull/1829 is published + } +} + +const readBundleAndManifest = async ({ edgeHandlersResolvedFolder, error }) => { + const manifestPath = path.resolve(edgeHandlersResolvedFolder, MANIFEST_FILENAME) + const { content: manifest, error: manifestError } = await readFileAsyncCatchError(manifestPath) + if (manifestError) { + error(`Could not read Edge Handlers manifest file ${manifestPath}: ${manifestError.message}`) + } + + let manifestJson + try { + manifestJson = JSON.parse(manifest) + } catch (e) { + error(`Edge Handlers manifest file is not a valid JSON file: ${e.message}`) + } + + if (!manifestJson.sha) { + error(`Edge Handlers manifest file is missing the 'sha' property`) + } + + const bundlePath = path.resolve(edgeHandlersResolvedFolder, manifestJson.sha) + const { content: bundleBuffer, error: bundleError } = await readFileAsyncCatchError(bundlePath) + + if (bundleError) { + error(`Could not read Edge Handlers bundle file ${bundlePath}: ${bundleError.message}`) + } + + return { bundleBuffer, manifest: manifestJson } +} + +const deployEdgeHandlers = async ({ site, deployId, api, silent, error, warn }) => { + const edgeHandlersResolvedFolder = await validateEdgeHandlerFolder({ site, error }) + if (edgeHandlersResolvedFolder) { + let spinner + try { + spinner = silent + ? null + : startSpinner({ text: `Deploying Edge Handlers from directory ${edgeHandlersResolvedFolder}` }) + + const { bundleBuffer, manifest } = await readBundleAndManifest({ edgeHandlersResolvedFolder, error }) + // returns false if the bundle exists, true on success, throws on error + const success = await uploadEdgeHandlers({ + api, + deployId, + bundleBuffer, + manifest, + }) + + const text = success + ? `Finished deploying Edge Handlers from directory: ${edgeHandlersResolvedFolder}` + : `Skipped deploying Edge Handlers since the bundle already exists` + stopSpinner({ spinner, text, error: false }) + } catch (e) { + const text = `Failed deploying Edge Handlers: ${e.message}` + stopSpinner({ spinner, text, error: true }) + try { + await api.cancelSiteDeploy({ deploy_id: deployId }) + } catch (e) { + warn(`Failed canceling deploy with id ${deployId}: ${e.message}`) + } + // no need to report the error again + error('') + } + } +} + +module.exports = { deployEdgeHandlers } diff --git a/src/utils/env.js b/src/utils/env.js index 4f543b034ac..4001e8f0164 100644 --- a/src/utils/env.js +++ b/src/utils/env.js @@ -1,10 +1,6 @@ const path = require('path') -const fs = require('fs') -const { promisify } = require('util') const dotenv = require('dotenv') - -const fileStat = promisify(fs.stat) -const readFile = promisify(fs.readFile) +const { statAsync, readFileAsync } = require('../lib/fs') async function getEnvSettings(projectDir) { const envDevelopmentFile = path.resolve(projectDir, '.env.development') @@ -13,17 +9,17 @@ async function getEnvSettings(projectDir) { const settings = {} try { - if ((await fileStat(envFile)).isFile()) settings.file = envFile + if ((await statAsync(envFile)).isFile()) settings.file = envFile } catch (err) { // nothing } try { - if ((await fileStat(envDevelopmentFile)).isFile()) settings.file = envDevelopmentFile + if ((await statAsync(envDevelopmentFile)).isFile()) settings.file = envDevelopmentFile } catch (err) { // nothing } - if (settings.file) settings.vars = dotenv.parse(await readFile(settings.file)) || {} + if (settings.file) settings.vars = dotenv.parse(await readFileAsync(settings.file)) || {} return settings } diff --git a/src/utils/gitignore.js b/src/utils/gitignore.js index 76bc79ef5a6..fabf2b57ee8 100644 --- a/src/utils/gitignore.js +++ b/src/utils/gitignore.js @@ -1,22 +1,11 @@ const path = require('path') -const fs = require('fs') const parseIgnore = require('parse-gitignore') -const { promisify } = require('util') -const readFile = promisify(fs.readFile) -const writeFile = promisify(fs.writeFile) - -function fileExists(filePath) { - return new Promise((resolve, reject) => { - fs.access(filePath, fs.F_OK, err => { - if (err) return resolve(false) - return resolve(true) - }) - }) -} + +const { readFileAsync, writeFileAsync, fileExistsAsync } = require('../lib/fs') async function hasGitIgnore(dir) { const gitIgnorePath = path.join(dir, '.gitignore') - const hasIgnore = await fileExists(gitIgnorePath) + const hasIgnore = await fileExistsAsync(gitIgnorePath) return hasIgnore } @@ -89,14 +78,14 @@ async function ensureNetlifyIgnore(dir) { /* No .gitignore file. Create one and ignore .netlify folder */ if (!(await hasGitIgnore(dir))) { - await writeFile(gitIgnorePath, ignoreContent, 'utf8') + await writeFileAsync(gitIgnorePath, ignoreContent, 'utf8') return false } let gitIgnoreContents let ignorePatterns try { - gitIgnoreContents = await readFile(gitIgnorePath, 'utf8') + gitIgnoreContents = await readFileAsync(gitIgnorePath, 'utf8') ignorePatterns = parseIgnore.parse(gitIgnoreContents) } catch (e) { // ignore @@ -104,7 +93,7 @@ async function ensureNetlifyIgnore(dir) { /* Not ignoring .netlify folder. Add to .gitignore */ if (!ignorePatterns || !ignorePatterns.patterns.includes('.netlify')) { const newContents = `${gitIgnoreContents}\n${ignoreContent}` - await writeFile(gitIgnorePath, newContents, 'utf8') + await writeFileAsync(gitIgnorePath, newContents, 'utf8') } } diff --git a/tests/command.addons.test.js b/tests/command.addons.test.js index 98aff327565..cd48a4f242d 100644 --- a/tests/command.addons.test.js +++ b/tests/command.addons.test.js @@ -1,41 +1,17 @@ const test = require('ava') const { createSiteBuilder } = require('./utils/siteBuilder') const callCli = require('./utils/callCli') -const createLiveTestSite = require('./utils/createLiveTestSite') +const { generateSiteName, createLiveTestSite } = require('./utils/createLiveTestSite') -const siteName = - 'netlify-test-addons-' + - Math.random() - .toString(36) - .replace(/[^a-z]+/g, '') - .substr(0, 8) - -async function listAccounts() { - return JSON.parse(await callCli(['api', 'listAccountsForUser'])) -} +const siteName = generateSiteName('netlify-test-addons-') if (process.env.IS_FORK !== 'true') { test.before(async t => { - const accounts = await listAccounts() - t.is(Array.isArray(accounts), true) - t.truthy(accounts.length) - - const account = accounts[0] - + const siteId = await createLiveTestSite(siteName) const builder = createSiteBuilder({ siteName: 'site-with-addons' }) await builder.buildAsync() - const execOptions = { - cwd: builder.directory, - windowsHide: true, - windowsVerbatimArguments: true, - } - - console.log('creating new site for tests: ' + siteName) - const siteId = await createLiveTestSite(siteName, account.slug, execOptions) - t.truthy(siteId != null) - - t.context.execOptions = { ...execOptions, env: { ...process.env, NETLIFY_SITE_ID: siteId } } + t.context.execOptions = { cwd: builder.directory, env: { NETLIFY_SITE_ID: siteId } } t.context.builder = builder }) diff --git a/tests/command.deploy.test.js b/tests/command.deploy.test.js new file mode 100644 index 00000000000..b713b41161f --- /dev/null +++ b/tests/command.deploy.test.js @@ -0,0 +1,137 @@ +const test = require('ava') +const { getToken } = require('../src/utils/command') +const fetch = require('node-fetch') +const { withSiteBuilder } = require('./utils/siteBuilder') +const callCli = require('./utils/callCli') +const { generateSiteName, createLiveTestSite } = require('./utils/createLiveTestSite') + +const siteName = generateSiteName('netlify-test-deploy-') + +const validateDeploy = async ({ deploy, siteName, content, t }) => { + t.truthy(deploy.site_name) + t.truthy(deploy.deploy_url) + t.truthy(deploy.deploy_id) + t.truthy(deploy.logs) + t.is(deploy.site_name, siteName) + + const actualContent = await fetch(deploy.deploy_url) + .then(r => r.text()) + .catch(() => undefined) + + t.is(actualContent, content) +} + +if (process.env.IS_FORK !== 'true') { + test.before(async t => { + const siteId = await createLiveTestSite(siteName) + t.context.siteId = siteId + }) + + test.serial('should deploy site when dir flag is passed', async t => { + await withSiteBuilder('site-with-public-folder', async builder => { + const content = '

⊂◉‿◉つ

' + builder.withContentFile({ + path: 'public/index.html', + content, + }) + + await builder.buildAsync() + + const deploy = await callCli(['deploy', '--json', '--dir', 'public'], { + cwd: builder.directory, + env: { NETLIFY_SITE_ID: t.context.siteId }, + }).then(output => JSON.parse(output)) + + validateDeploy({ deploy, siteName, content, t }) + }) + }) + + test.serial('should deploy site when publish directory set in netlify.toml', async t => { + await withSiteBuilder('site-with-public-folder', async builder => { + const content = '

⊂◉‿◉つ

' + builder + .withContentFile({ + path: 'public/index.html', + content, + }) + .withNetlifyToml({ + config: { + build: { publish: 'public' }, + }, + }) + + await builder.buildAsync() + + const deploy = await callCli(['deploy', '--json'], { + cwd: builder.directory, + env: { NETLIFY_SITE_ID: t.context.siteId }, + }).then(output => JSON.parse(output)) + + validateDeploy({ deploy, siteName, content, t }) + }) + }) + + // the edge handlers plugin only works on node >= 10 and not on windows at the moment + const version = parseInt(process.version.substring(1).split('.')[0]) + if (process.platform !== 'win32' && version >= 10) { + test.serial('should deploy edge handlers when directory exists', async t => { + await withSiteBuilder('site-with-public-folder', async builder => { + const content = '

⊂◉‿◉つ

' + builder + .withContentFile({ + path: 'public/index.html', + content, + }) + .withNetlifyToml({ + config: { + build: { publish: 'public', command: 'echo "no op"' }, + }, + }) + .withEdgeHandlers({ + handlers: { + onRequest: event => { + console.log(`Incoming request for ${event.request.url}`) + }, + }, + }) + + await builder.buildAsync() + + const options = { + cwd: builder.directory, + env: { NETLIFY_SITE_ID: t.context.siteId }, + } + // build the edge handlers first + await callCli(['build'], options) + const deploy = await callCli(['deploy', '--json'], options).then(output => JSON.parse(output)) + + validateDeploy({ deploy, siteName, content, t }) + + // validate edge handlers + // use this until we can use `netlify api` + const [apiToken] = getToken() + const resp = await fetch(`https://api.netlify.com/api/v1/deploys/${deploy.deploy_id}/edge_handlers`, { + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${apiToken}`, + }, + }) + + t.is(resp.status, 200) + const { created_at, sha, content_length, ...rest } = await resp.json() + t.deepEqual(rest, { + content_type: 'application/javascript', + handlers: ['index'], + valid: true, + }) + t.is(content_length > 400, true) + }) + }) + } + + test.after('cleanup', async t => { + const { siteId } = t.context + console.log(`deleting test site "${siteName}". ${siteId}`) + await callCli(['sites:delete', siteId, '--force']) + }) +} diff --git a/tests/command.env.test.js b/tests/command.env.test.js index 4b7efa4df3f..34e2079ccfa 100644 --- a/tests/command.env.test.js +++ b/tests/command.env.test.js @@ -1,16 +1,11 @@ const test = require('ava') const { createSiteBuilder } = require('./utils/siteBuilder') const callCli = require('./utils/callCli') -const createLiveTestSite = require('./utils/createLiveTestSite') +const { generateSiteName, createLiveTestSite } = require('./utils/createLiveTestSite') const isObject = require('lodash.isobject') const isEmpty = require('lodash.isempty') -const siteName = - 'netlify-test-env-' + - Math.random() - .toString(36) - .replace(/[^a-z]+/g, '') - .substr(0, 8) +const siteName = generateSiteName('netlify-test-env-') // Input and return values for each test scenario: const ENV_VAR_STATES = { @@ -40,10 +35,6 @@ const ENV_FILE_NAME = '.env' const REPLACE_ENV_FILE_NAME = '.env.replace' const FAIL_ENV_FILE_NAME = '.env.unknown' // file which should result in error -async function listAccounts() { - return JSON.parse(await callCli(['api', 'listAccountsForUser'])) -} - async function injectNetlifyToml(builder) { const builderWithToml = builder.withNetlifyToml({ config: { @@ -69,12 +60,7 @@ function getArgsFromState(state) { if (process.env.IS_FORK !== 'true') { test.before(async t => { - const accounts = await listAccounts() - t.is(Array.isArray(accounts), true) - t.truthy(accounts.length) - - const account = accounts[0] - + const siteId = await createLiveTestSite(siteName) const builder = createSiteBuilder({ siteName: 'site-with-env-vars' }) .withEnvFile({ path: ENV_FILE_NAME, @@ -86,17 +72,7 @@ if (process.env.IS_FORK !== 'true') { }) await builder.buildAsync() - const execOptions = { - cwd: builder.directory, - windowsHide: true, - windowsVerbatimArguments: true, - } - - console.log('creating new site for tests: ' + siteName) - const siteId = await createLiveTestSite(siteName, account.slug, execOptions) - t.truthy(siteId != null) - - t.context.execOptions = { ...execOptions, env: { ...process.env, NETLIFY_SITE_ID: siteId } } + t.context.execOptions = { cwd: builder.directory, env: { NETLIFY_SITE_ID: siteId } } t.context.builder = builder }) diff --git a/tests/utils/callCli.js b/tests/utils/callCli.js index 2e2728a8fdc..54683f22208 100644 --- a/tests/utils/callCli.js +++ b/tests/utils/callCli.js @@ -2,7 +2,8 @@ const execa = require('execa') const cliPath = require('./cliPath') async function callCli(args, execOptions) { - return (await execa(cliPath, args, execOptions)).stdout + const { stdout } = await execa(cliPath, args, { windowsHide: true, windowsVerbatimArguments: true, ...execOptions }) + return stdout } module.exports = callCli diff --git a/tests/utils/createLiveTestSite.js b/tests/utils/createLiveTestSite.js index 36d84803126..145a85f5239 100644 --- a/tests/utils/createLiveTestSite.js +++ b/tests/utils/createLiveTestSite.js @@ -1,20 +1,41 @@ const stripAnsi = require('strip-ansi') const callCli = require('./callCli') -async function createLiveTestSite(siteName, accountSlug, execOptions) { - const cliResponse = await callCli(['sites:create', '--name', siteName, '--account-slug', accountSlug], execOptions) +function generateSiteName(prefix) { + const randomString = Math.random() + .toString(36) + .replace(/[^a-z]+/g, '') + .substr(0, 8) + return `${prefix}${randomString}` +} + +async function listAccounts() { + return JSON.parse(await callCli(['api', 'listAccountsForUser'])) +} + +async function createLiveTestSite(siteName) { + console.log(`Creating new site for tests: ${siteName}`) + const accounts = await listAccounts() + if (!Array.isArray(accounts) || accounts.length <= 0) { + throw new Error(`Can't find suitable account to create a site`) + } + const accountSlug = accounts[0].slug + console.log(`Using account ${accountSlug} to create site: ${siteName}`) + const cliResponse = await callCli(['sites:create', '--name', siteName, '--account-slug', accountSlug]) const isSiteCreated = /Site Created/.test(cliResponse) if (!isSiteCreated) { - return null + throw new Error(`Failed creating site: ${cliResponse}`) } const matches = /Site ID:\s+([a-zA-Z0-9-]+)/m.exec(stripAnsi(cliResponse)) if (matches && Object.prototype.hasOwnProperty.call(matches, 1) && matches[1]) { - return matches[1] + const siteId = matches[1] + console.log(`Done creating site ${siteName} for account '${accountSlug}'. Site Id: ${siteId}`) + return siteId } - return null + throw new Error(`Failed creating site: ${cliResponse}`) } -module.exports = createLiveTestSite +module.exports = { generateSiteName, createLiveTestSite } diff --git a/tests/utils/siteBuilder.js b/tests/utils/siteBuilder.js index 8363bbea096..ae97afedf5a 100644 --- a/tests/utils/siteBuilder.js +++ b/tests/utils/siteBuilder.js @@ -43,6 +43,19 @@ const createSiteBuilder = ({ siteName }) => { }) return builder }, + withEdgeHandlers: ({ handlers }) => { + const dest = path.join(directory, 'edge-handlers', 'index.js') + tasks.push(async () => { + await fs.ensureFile(dest) + const content = Object.entries(handlers) + .map(([event, handler]) => { + return `export const ${event} = ${handler.toString()}` + }) + .join('\n') + await fs.writeFile(dest, content) + }) + return builder + }, withRedirectsFile: ({ redirects = [], pathPrefix = '' }) => { const dest = path.join(directory, pathPrefix, '_redirects') tasks.push(async () => {