mirror of
https://github.com/LukeHagar/vercel.git
synced 2025-12-11 04:22:13 +00:00
Reverts zeit/now#4227 This somehow got merged even though the tests didn't pass. Reverting until it can be merged cleanly.
1312 lines
34 KiB
JavaScript
1312 lines
34 KiB
JavaScript
import ms from 'ms';
|
|
import fs from 'fs-extra';
|
|
import test from 'ava';
|
|
import { join, resolve, delimiter } from 'path';
|
|
import _execa from 'execa';
|
|
import fetch from 'node-fetch';
|
|
import sleep from 'then-sleep';
|
|
import retry from 'async-retry';
|
|
import { satisfies } from 'semver';
|
|
import { getDistTag } from '../../src/util/get-dist-tag';
|
|
import { version as cliVersion } from '../../package.json';
|
|
import { fetchTokenWithRetry } from '../../../../test/lib/deployment/now-deploy';
|
|
|
|
const isCanary = () => getDistTag(cliVersion) === 'canary';
|
|
|
|
let port = 3000;
|
|
|
|
const binaryPath = resolve(__dirname, `../../scripts/start.js`);
|
|
const fixture = name => join('test', 'dev', 'fixtures', name);
|
|
const fixtureAbsolute = name => join(__dirname, 'fixtures', name);
|
|
|
|
let processCounter = 0;
|
|
const processList = new Map();
|
|
|
|
function execa(...args) {
|
|
const procId = ++processCounter;
|
|
const child = _execa(...args);
|
|
|
|
processList.set(procId, child);
|
|
child.on('exit', () => processList.delete(procId));
|
|
|
|
return child;
|
|
}
|
|
|
|
function fetchWithRetry(url, retries = 3, opts = {}) {
|
|
return retry(
|
|
async () => {
|
|
const res = await fetch(url, opts);
|
|
|
|
if (!res.ok) {
|
|
const text = await res.text();
|
|
throw new Error(
|
|
`Failed to fetch ${url} with status ${res.status}:` +
|
|
`\n\n${text}\n\n`
|
|
);
|
|
}
|
|
|
|
return res;
|
|
},
|
|
{
|
|
retries,
|
|
factor: 1,
|
|
}
|
|
);
|
|
}
|
|
|
|
function createResolver() {
|
|
let resolver;
|
|
const p = new Promise(res => (resolver = res));
|
|
p.resolve = resolver;
|
|
return p;
|
|
}
|
|
|
|
function formatOutput({ stderr, stdout }) {
|
|
return `Received:\n"${stderr}"\n"${stdout}"`;
|
|
}
|
|
|
|
function printOutput(fixture, stdout, stderr) {
|
|
const lines = (
|
|
`\nOutput for "${fixture}"\n` +
|
|
`\n----- stdout -----\n` +
|
|
stdout +
|
|
`\n----- stderr -----\n` +
|
|
stderr
|
|
).split('\n');
|
|
|
|
const getPrefix = nr => {
|
|
return nr === 0 ? '╭' : nr === lines.length - 1 ? '╰' : '│';
|
|
};
|
|
|
|
console.log(
|
|
lines.map((line, index) => ` ${getPrefix(index)} ${line}`).join('\n')
|
|
);
|
|
}
|
|
|
|
function shouldSkip(t, name, versions) {
|
|
if (!satisfies(process.version, versions)) {
|
|
console.log(`Skipping "${name}" because it requires "${versions}".`);
|
|
t.pass();
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function validateResponseHeaders(t, res) {
|
|
if (res.status < 500) {
|
|
t.truthy(res.headers.get('x-vercel-id'));
|
|
t.truthy(res.headers.get('cache-control').length > 0);
|
|
}
|
|
}
|
|
|
|
async function exec(directory, args = []) {
|
|
return execa(binaryPath, ['dev', directory, ...args], {
|
|
reject: false,
|
|
shell: true,
|
|
env: { __NOW_SKIP_DEV_COMMAND: 1 },
|
|
});
|
|
}
|
|
|
|
async function runNpmInstall(fixturePath) {
|
|
if (await fs.exists(join(fixturePath, 'package.json'))) {
|
|
await execa('yarn', ['install'], {
|
|
cwd: fixturePath,
|
|
shell: true,
|
|
});
|
|
}
|
|
}
|
|
|
|
async function getPackedBuilderPath(builderDirName) {
|
|
const packagePath = join(__dirname, '..', '..', '..', builderDirName);
|
|
const output = await execa('npm', ['pack'], {
|
|
cwd: packagePath,
|
|
shell: true,
|
|
});
|
|
|
|
if (output.exitCode !== 0 || output.stdout.trim() === '') {
|
|
throw new Error(
|
|
`Failed to pack ${builderDirName}: ${formatOutput(output)}`
|
|
);
|
|
}
|
|
|
|
return join(packagePath, output.stdout.trim());
|
|
}
|
|
|
|
async function testPath(
|
|
t,
|
|
origin,
|
|
status,
|
|
path,
|
|
expectedText,
|
|
headers = {},
|
|
method = 'GET'
|
|
) {
|
|
const opts = { redirect: 'manual-dont-change', method };
|
|
const url = `${origin}${path}`;
|
|
const res = await fetch(url, opts);
|
|
const msg = `Testing response from ${method} ${url}`;
|
|
console.log(msg);
|
|
t.is(res.status, status, msg);
|
|
validateResponseHeaders(t, res);
|
|
if (typeof expectedText === 'string') {
|
|
const actualText = await res.text();
|
|
t.is(actualText.trim(), expectedText.trim(), msg);
|
|
} else if (expectedText instanceof RegExp) {
|
|
const actualText = await res.text();
|
|
expectedText.lastIndex = 0; // reset since we test twice
|
|
t.regex(actualText, expectedText);
|
|
}
|
|
if (headers) {
|
|
Object.entries(headers).forEach(([key, expectedValue]) => {
|
|
let actualValue = res.headers.get(key);
|
|
if (key.toLowerCase() === 'location' && actualValue === '//') {
|
|
// HACK: `node-fetch` has strang behavior for location header so fix it
|
|
// with `manual-dont-change` opt and convert double slash to single.
|
|
// See https://github.com/node-fetch/node-fetch/issues/417#issuecomment-587233352
|
|
actualValue = '/';
|
|
}
|
|
t.is(actualValue, expectedValue, msg);
|
|
});
|
|
}
|
|
}
|
|
|
|
async function testFixture(directory, opts = {}, args = []) {
|
|
await runNpmInstall(directory);
|
|
|
|
const dev = execa(
|
|
binaryPath,
|
|
['dev', directory, '-l', String(port), ...args],
|
|
{
|
|
reject: false,
|
|
detached: true,
|
|
shell: true,
|
|
stdio: 'pipe',
|
|
...opts,
|
|
env: { ...opts.env, __NOW_SKIP_DEV_COMMAND: 1 },
|
|
}
|
|
);
|
|
|
|
const stdoutList = [];
|
|
const stderrList = [];
|
|
|
|
const exitResolver = createResolver();
|
|
|
|
dev.stderr.on('data', data => stderrList.push(Buffer.from(data)));
|
|
dev.stdout.on('data', data => stdoutList.push(Buffer.from(data)));
|
|
|
|
let printedOutput = false;
|
|
|
|
dev.on('exit', () => {
|
|
if (!printedOutput) {
|
|
const stdout = Buffer.concat(stdoutList).toString();
|
|
const stderr = Buffer.concat(stderrList).toString();
|
|
printOutput(directory, stdout, stderr);
|
|
printedOutput = true;
|
|
}
|
|
exitResolver.resolve();
|
|
});
|
|
|
|
dev.on('error', () => {
|
|
if (!printedOutput) {
|
|
const stdout = Buffer.concat(stdoutList).toString();
|
|
const stderr = Buffer.concat(stderrList).toString();
|
|
printOutput(directory, stdout, stderr);
|
|
printedOutput = true;
|
|
}
|
|
exitResolver.resolve();
|
|
});
|
|
|
|
dev._kill = dev.kill;
|
|
dev.kill = async (...args) => {
|
|
dev._kill(...args);
|
|
await exitResolver;
|
|
};
|
|
|
|
return {
|
|
dev,
|
|
port,
|
|
};
|
|
}
|
|
|
|
function testFixtureStdio(
|
|
directory,
|
|
fn,
|
|
{ expectedCode = 0, skipDeploy } = {}
|
|
) {
|
|
return async t => {
|
|
const dir = fixture(directory);
|
|
const token = await fetchTokenWithRetry();
|
|
let deploymentUrl;
|
|
|
|
// Deploy fixture and link project
|
|
if (!skipDeploy) {
|
|
const project = join(fixtureAbsolute(directory), '.now', 'project.json');
|
|
if (await fs.exists(project)) {
|
|
await fs.unlink(project);
|
|
}
|
|
const gitignore = join(fixtureAbsolute(directory), '.gitignore');
|
|
const gitignoreOrig = await fs.exists(gitignore);
|
|
let { stdout, stderr, exitCode } = await execa(
|
|
binaryPath,
|
|
[
|
|
dir,
|
|
'-t',
|
|
token,
|
|
'--confirm',
|
|
'--public',
|
|
'--no-clipboard',
|
|
'--debug',
|
|
],
|
|
{ reject: false }
|
|
);
|
|
console.log({ stdout, stderr, exitCode });
|
|
if (!gitignoreOrig && (await fs.exists(gitignore))) {
|
|
await fs.unlink(gitignore);
|
|
}
|
|
t.is(exitCode, expectedCode);
|
|
if (expectedCode === 0) {
|
|
deploymentUrl = new URL(stdout).host;
|
|
}
|
|
}
|
|
|
|
// Start dev
|
|
let dev;
|
|
|
|
await runNpmInstall(dir);
|
|
|
|
const stdoutList = [];
|
|
const stderrList = [];
|
|
|
|
const readyResolver = createResolver();
|
|
const exitResolver = createResolver();
|
|
|
|
try {
|
|
let stderr = '';
|
|
let printedOutput = false;
|
|
|
|
const env = skipDeploy
|
|
? { ...process.env, __NOW_SKIP_DEV_COMMAND: 1 }
|
|
: process.env;
|
|
dev = execa(
|
|
binaryPath,
|
|
['dev', dir, '-l', port, '-t', token, '--debug'],
|
|
{ env }
|
|
);
|
|
|
|
dev.stdout.pipe(process.stdout);
|
|
dev.stderr.pipe(process.stderr);
|
|
|
|
dev.stdout.on('data', data => {
|
|
stdoutList.push(data);
|
|
});
|
|
|
|
dev.stderr.on('data', data => {
|
|
stderrList.push(data);
|
|
|
|
stderr += data.toString();
|
|
if (stderr.includes('Ready! Available at')) {
|
|
readyResolver.resolve();
|
|
}
|
|
|
|
if (stderr.includes(`Requested port ${port} is already in use`)) {
|
|
dev.kill('SIGTERM');
|
|
throw new Error(
|
|
`Failed for "${directory}" with port ${port} with stderr "${stderr}".`
|
|
);
|
|
}
|
|
|
|
if (stderr.includes('Command failed') || stderr.includes('Error!')) {
|
|
dev.kill('SIGTERM');
|
|
throw new Error(`Failed for "${directory}" with stderr "${stderr}".`);
|
|
}
|
|
});
|
|
|
|
dev.on('exit', () => {
|
|
if (!printedOutput) {
|
|
const stdout = Buffer.concat(stdoutList).toString();
|
|
const stderr = Buffer.concat(stderrList).toString();
|
|
printOutput(directory, stdout, stderr);
|
|
printedOutput = true;
|
|
}
|
|
exitResolver.resolve();
|
|
});
|
|
|
|
dev.on('error', () => {
|
|
if (!printedOutput) {
|
|
const stdout = Buffer.concat(stdoutList).toString();
|
|
const stderr = Buffer.concat(stderrList).toString();
|
|
printOutput(directory, stdout, stderr);
|
|
printedOutput = true;
|
|
}
|
|
exitResolver.resolve();
|
|
});
|
|
|
|
await readyResolver;
|
|
|
|
const helperTestPath = async (...args) => {
|
|
if (!skipDeploy) {
|
|
await testPath(t, `https://${deploymentUrl}`, ...args);
|
|
}
|
|
await testPath(t, `http://localhost:${port}`, ...args);
|
|
};
|
|
await fn(helperTestPath, t, port);
|
|
} finally {
|
|
dev.kill('SIGTERM');
|
|
await exitResolver;
|
|
}
|
|
};
|
|
}
|
|
|
|
test.beforeEach(() => {
|
|
port = ++port;
|
|
});
|
|
|
|
test.afterEach(async () => {
|
|
await Promise.all(
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
Array.from(processList).map(([_procId, proc]) => {
|
|
if (proc.killed === false) {
|
|
console.log(
|
|
`killing process ${proc.pid} "${proc.spawnargs.join(' ')}"`
|
|
);
|
|
|
|
try {
|
|
process.kill(proc.pid, 'SIGTERM');
|
|
} catch (err) {
|
|
// Was already killed
|
|
if (err.errno !== 'ESRCH') {
|
|
throw err;
|
|
}
|
|
}
|
|
}
|
|
})
|
|
);
|
|
});
|
|
|
|
test(
|
|
'[now dev] validate routes that use `check: true`',
|
|
testFixtureStdio('routes-check-true', async testPath => {
|
|
await testPath(200, '/blog/post', 'Blog Home');
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[now dev] validate routes that use `check: true` and `status` code',
|
|
testFixtureStdio('routes-check-true-status', async testPath => {
|
|
await testPath(403, '/secret');
|
|
await testPath(200, '/post', 'This is a post.');
|
|
await testPath(200, '/post.html', 'This is a post.');
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[now dev] handles miss after route',
|
|
testFixtureStdio('handle-miss-after-route', async testPath => {
|
|
await testPath(200, '/post', 'Blog Post Page', {
|
|
test: '1',
|
|
override: 'one',
|
|
});
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[now dev] handles miss after rewrite',
|
|
testFixtureStdio('handle-miss-after-rewrite', async testPath => {
|
|
await testPath(200, '/post', 'Blog Post Page', {
|
|
test: '1',
|
|
override: 'one',
|
|
});
|
|
await testPath(200, '/blog/post', 'Blog Post Page', {
|
|
test: '1',
|
|
override: 'two',
|
|
});
|
|
await testPath(404, '/blog/about.html', undefined, {
|
|
test: '1',
|
|
override: 'two',
|
|
});
|
|
})
|
|
);
|
|
/*
|
|
test(
|
|
'[now dev] displays directory listing after miss',
|
|
testFixtureStdio('handle-miss-display-dir-list', async (testPath) => {
|
|
await testPath(404, '/post', /one.html/m);
|
|
})
|
|
);
|
|
*/
|
|
|
|
test(
|
|
'[now dev] does not display directory listing after 404',
|
|
testFixtureStdio('handle-miss-hide-dir-list', async testPath => {
|
|
await testPath(404, '/post');
|
|
await testPath(200, '/post/one.html', 'First Post');
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[now dev] handles hit after handle: filesystem',
|
|
testFixtureStdio('handle-hit-after-fs', async testPath => {
|
|
await testPath(200, '/blog.html', 'Blog Page', { test: '1' });
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[now dev] handles hit after dest',
|
|
testFixtureStdio('handle-hit-after-dest', async testPath => {
|
|
await testPath(200, '/post', 'Blog Post', { test: '1', override: 'one' });
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[now dev] handles hit after rewrite',
|
|
testFixtureStdio('handle-hit-after-rewrite', async testPath => {
|
|
await testPath(200, '/post', 'Blog Post', { test: '1', override: 'one' });
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[now dev] should serve the public directory and api functions',
|
|
testFixtureStdio('public-and-api', async testPath => {
|
|
await testPath(200, '/', 'This is the home page');
|
|
await testPath(200, '/about.html', 'This is the about page');
|
|
await testPath(200, '/api/date', /current date/);
|
|
await testPath(200, '/api/rand', /random number/);
|
|
await testPath(200, '/api/rand.js', /random number/);
|
|
await testPath(404, '/api/api');
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[now dev] should allow user rewrites for path segment files',
|
|
testFixtureStdio('test-zero-config-rewrite', async testPath => {
|
|
await testPath(404, '/');
|
|
await testPath(200, '/echo/1', '{"id":"1"}', {
|
|
'Access-Control-Allow-Origin': '*',
|
|
});
|
|
await testPath(200, '/echo/2', '{"id":"2"}', {
|
|
'Access-Control-Allow-Headers': '*',
|
|
});
|
|
})
|
|
);
|
|
|
|
test('[now dev] validate builds', async t => {
|
|
const directory = fixture('invalid-builds');
|
|
const output = await exec(directory);
|
|
|
|
t.is(output.exitCode, 1, formatOutput(output));
|
|
t.regex(
|
|
output.stderr,
|
|
/Invalid `builds` property: \[0\]\.src should be string/m
|
|
);
|
|
});
|
|
|
|
test('[now dev] validate routes', async t => {
|
|
const directory = fixture('invalid-routes');
|
|
const output = await exec(directory);
|
|
|
|
t.is(output.exitCode, 1, formatOutput(output));
|
|
t.regex(
|
|
output.stderr,
|
|
/Invalid `routes` property: \[0\]\.src should be string/m
|
|
);
|
|
});
|
|
|
|
test('[now dev] validate cleanUrls', async t => {
|
|
const directory = fixture('invalid-clean-urls');
|
|
const output = await exec(directory);
|
|
|
|
t.is(output.exitCode, 1, formatOutput(output));
|
|
t.regex(output.stderr, /Invalid `cleanUrls` property:\s+should be boolean/m);
|
|
});
|
|
|
|
test('[now dev] validate trailingSlash', async t => {
|
|
const directory = fixture('invalid-trailing-slash');
|
|
const output = await exec(directory);
|
|
|
|
t.is(output.exitCode, 1, formatOutput(output));
|
|
t.regex(
|
|
output.stderr,
|
|
/Invalid `trailingSlash` property:\s+should be boolean/m
|
|
);
|
|
});
|
|
|
|
test('[now dev] validate rewrites', async t => {
|
|
const directory = fixture('invalid-rewrites');
|
|
const output = await exec(directory);
|
|
|
|
t.is(output.exitCode, 1, formatOutput(output));
|
|
t.regex(
|
|
output.stderr,
|
|
/Invalid `rewrites` property: \[0\]\.destination should be string/m
|
|
);
|
|
});
|
|
|
|
test('[now dev] validate redirects', async t => {
|
|
const directory = fixture('invalid-redirects');
|
|
const output = await exec(directory);
|
|
|
|
t.is(output.exitCode, 1, formatOutput(output));
|
|
t.regex(
|
|
output.stderr,
|
|
/Invalid `redirects` property: \[0\]\.statusCode should be integer/m
|
|
);
|
|
});
|
|
|
|
test('[now dev] validate headers', async t => {
|
|
const directory = fixture('invalid-headers');
|
|
const output = await exec(directory);
|
|
|
|
t.is(output.exitCode, 1, formatOutput(output));
|
|
t.regex(
|
|
output.stderr,
|
|
/Invalid `headers` property: \[0\]\.headers\[0\]\.value should be string/m
|
|
);
|
|
});
|
|
|
|
test('[now dev] validate mixed routes and rewrites', async t => {
|
|
const directory = fixture('invalid-mixed-routes-rewrites');
|
|
const output = await exec(directory);
|
|
|
|
t.is(output.exitCode, 1, formatOutput(output));
|
|
t.regex(output.stderr, /Cannot define both `routes` and `rewrites`/m);
|
|
});
|
|
|
|
// Test seems unstable: It won't return sometimes.
|
|
test('[now dev] validate env var names', async t => {
|
|
const directory = fixture('invalid-env-var-name');
|
|
const { dev } = await testFixture(directory, { stdio: 'pipe' });
|
|
|
|
try {
|
|
let stderr = '';
|
|
dev.stderr.setEncoding('utf8');
|
|
|
|
await new Promise((resolve, reject) => {
|
|
dev.stderr.on('data', b => {
|
|
stderr += b.toString();
|
|
|
|
if (
|
|
stderr.includes('Ignoring env var "1" because name is invalid') &&
|
|
stderr.includes(
|
|
'Ignoring build env var "_a" because name is invalid'
|
|
) &&
|
|
stderr.includes(
|
|
'Env var names must start with letters, and can only contain alphanumeric characters and underscores'
|
|
)
|
|
) {
|
|
resolve();
|
|
}
|
|
});
|
|
|
|
dev.on('error', reject);
|
|
dev.on('exit', resolve);
|
|
});
|
|
|
|
t.pass();
|
|
} finally {
|
|
await dev.kill('SIGTERM');
|
|
}
|
|
|
|
t.pass();
|
|
});
|
|
|
|
test(
|
|
'[now dev] test rewrites with segments serve correct content',
|
|
testFixtureStdio('test-rewrites-with-segments', async testPath => {
|
|
await testPath(200, '/api/users/first', 'first');
|
|
await testPath(200, '/api/fourty-two', '42');
|
|
await testPath(200, '/rand', '42');
|
|
await testPath(200, '/api/dynamic', 'dynamic');
|
|
await testPath(404, '/api');
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[now dev] test rewrites serve correct content',
|
|
testFixtureStdio('test-rewrites', async testPath => {
|
|
await testPath(200, '/hello', 'Hello World');
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[now dev] test cleanUrls serve correct content',
|
|
testFixtureStdio('test-clean-urls', async testPath => {
|
|
await testPath(200, '/', 'Index Page');
|
|
await testPath(200, '/about', 'About Page');
|
|
await testPath(200, '/sub', 'Sub Index Page');
|
|
await testPath(200, '/sub/another', 'Sub Another Page');
|
|
await testPath(200, '/style.css', 'body { color: green }');
|
|
await testPath(308, '/index.html', 'Redirecting to / (308)', {
|
|
Location: '/',
|
|
});
|
|
await testPath(308, '/about.html', 'Redirecting to /about (308)', {
|
|
Location: '/about',
|
|
});
|
|
await testPath(308, '/sub/index.html', 'Redirecting to /sub (308)', {
|
|
Location: '/sub',
|
|
});
|
|
await testPath(
|
|
308,
|
|
'/sub/another.html',
|
|
'Redirecting to /sub/another (308)',
|
|
{ Location: '/sub/another' }
|
|
);
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[now dev] test cleanUrls and trailingSlash serve correct content',
|
|
testFixtureStdio('test-clean-urls-trailing-slash', async testPath => {
|
|
await testPath(200, '/', 'Index Page');
|
|
await testPath(200, '/about/', 'About Page');
|
|
await testPath(200, '/sub/', 'Sub Index Page');
|
|
await testPath(200, '/sub/another/', 'Sub Another Page');
|
|
await testPath(200, '/style.css', 'body { color: green }');
|
|
//TODO: fix this test so that location is `/` instead of `//`
|
|
//await testPath(308, '/index.html', 'Redirecting to / (308)', { Location: '/' });
|
|
await testPath(308, '/about.html', 'Redirecting to /about/ (308)', {
|
|
Location: '/about/',
|
|
});
|
|
await testPath(308, '/sub/index.html', 'Redirecting to /sub/ (308)', {
|
|
Location: '/sub/',
|
|
});
|
|
await testPath(
|
|
308,
|
|
'/sub/another.html',
|
|
'Redirecting to /sub/another/ (308)',
|
|
{
|
|
Location: '/sub/another/',
|
|
}
|
|
);
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[now dev] test cors headers work with OPTIONS',
|
|
testFixtureStdio('test-cors-routes', async testPath => {
|
|
const headers = {
|
|
'Access-Control-Allow-Origin': '*',
|
|
'Access-Control-Allow-Headers':
|
|
'Content-Type, Authorization, Accept, Content-Length, Origin, User-Agent',
|
|
'Access-Control-Allow-Methods':
|
|
'GET, POST, OPTIONS, HEAD, PATCH, PUT, DELETE',
|
|
};
|
|
await testPath(200, '/', 'status api', headers, 'GET');
|
|
await testPath(200, '/', 'status api', headers, 'POST');
|
|
await testPath(200, '/api/status.js', 'status api', headers, 'GET');
|
|
await testPath(200, '/api/status.js', 'status api', headers, 'POST');
|
|
await testPath(204, '/', '', headers, 'OPTIONS');
|
|
await testPath(204, '/api/status.js', '', headers, 'OPTIONS');
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[now dev] test trailingSlash true serve correct content',
|
|
testFixtureStdio('test-trailing-slash', async testPath => {
|
|
await testPath(200, '/', 'Index Page');
|
|
await testPath(200, '/index.html', 'Index Page');
|
|
await testPath(200, '/about.html', 'About Page');
|
|
await testPath(200, '/sub/', 'Sub Index Page');
|
|
await testPath(200, '/sub/index.html', 'Sub Index Page');
|
|
await testPath(200, '/sub/another.html', 'Sub Another Page');
|
|
await testPath(200, '/style.css', 'body { color: green }');
|
|
await testPath(308, '/about.html/', 'Redirecting to /about.html (308)', {
|
|
Location: '/about.html',
|
|
});
|
|
await testPath(308, '/style.css/', 'Redirecting to /style.css (308)', {
|
|
Location: '/style.css',
|
|
});
|
|
await testPath(308, '/sub', 'Redirecting to /sub/ (308)', {
|
|
Location: '/sub/',
|
|
});
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[now dev] test trailingSlash false serve correct content',
|
|
testFixtureStdio('test-trailing-slash-false', async testPath => {
|
|
await testPath(200, '/', 'Index Page');
|
|
await testPath(200, '/index.html', 'Index Page');
|
|
await testPath(200, '/about.html', 'About Page');
|
|
await testPath(200, '/sub', 'Sub Index Page');
|
|
await testPath(200, '/sub/index.html', 'Sub Index Page');
|
|
await testPath(200, '/sub/another.html', 'Sub Another Page');
|
|
await testPath(200, '/style.css', 'body { color: green }');
|
|
await testPath(308, '/about.html/', 'Redirecting to /about.html (308)', {
|
|
Location: '/about.html',
|
|
});
|
|
await testPath(308, '/sub/', 'Redirecting to /sub (308)', {
|
|
Location: '/sub',
|
|
});
|
|
await testPath(
|
|
308,
|
|
'/sub/another.html/',
|
|
'Redirecting to /sub/another.html (308)',
|
|
{
|
|
Location: '/sub/another.html',
|
|
}
|
|
);
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[now dev] throw when invalid builder routes detected',
|
|
testFixtureStdio(
|
|
'invalid-builder-routes',
|
|
async testPath => {
|
|
await testPath(500, '/', /Invalid regular expression/m);
|
|
},
|
|
{ skipDeploy: true }
|
|
)
|
|
);
|
|
|
|
test(
|
|
'[now dev] 00-list-directory',
|
|
testFixtureStdio('00-list-directory', async testPath => {
|
|
await testPath(200, '/', /Files within/m);
|
|
await testPath(200, '/', /test[0-3]\.txt/m);
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[now dev] 01-node',
|
|
testFixtureStdio('01-node', async testPath => {
|
|
await testPath(200, '/', /A simple deployment with the Now API!/m);
|
|
})
|
|
);
|
|
|
|
// Angular has `engines: { node: "10.x" }` in its `package.json`
|
|
test('[now dev] 02-angular-node', async t => {
|
|
if (shouldSkip(t, '02-angular-node', '10.x')) return;
|
|
|
|
const directory = fixture('02-angular-node');
|
|
const { dev, port } = await testFixture(directory, { stdio: 'pipe' }, [
|
|
'--debug',
|
|
]);
|
|
|
|
let stderr = '';
|
|
|
|
try {
|
|
dev.stderr.on('data', async data => {
|
|
stderr += data.toString();
|
|
});
|
|
|
|
// start `now dev` detached in child_process
|
|
dev.unref();
|
|
|
|
const response = await fetchWithRetry(`http://localhost:${port}`, 180);
|
|
|
|
validateResponseHeaders(t, response);
|
|
|
|
const body = await response.text();
|
|
t.regex(body, /Angular \+ Node.js API/m);
|
|
} finally {
|
|
dev.kill('SIGTERM');
|
|
}
|
|
|
|
await sleep(5000);
|
|
|
|
if (isCanary()) {
|
|
stderr.includes('@now/build-utils@canary');
|
|
} else {
|
|
stderr.includes('@now/build-utils@latest');
|
|
}
|
|
});
|
|
|
|
test(
|
|
'[now dev] 03-aurelia',
|
|
testFixtureStdio(
|
|
'03-aurelia',
|
|
async testPath => {
|
|
await testPath(200, '/', /Aurelia Navigation Skeleton/m);
|
|
},
|
|
{ skipDeploy: true }
|
|
)
|
|
);
|
|
|
|
test(
|
|
'[now dev] 04-create-react-app',
|
|
testFixtureStdio('04-create-react-app', async testPath => {
|
|
await testPath(200, '/', /React App/m);
|
|
})
|
|
);
|
|
/*
|
|
test(
|
|
'[now dev] 05-gatsby',
|
|
testFixtureStdio('05-gatsby', async testPath => {
|
|
await testPath(200, '/', /Gatsby Default Starter/m);
|
|
})
|
|
);
|
|
*/
|
|
test(
|
|
'[now dev] 06-gridsome',
|
|
testFixtureStdio('06-gridsome', async testPath => {
|
|
await testPath(200, '/');
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[now dev] 07-hexo-node',
|
|
testFixtureStdio('07-hexo-node', async testPath => {
|
|
await testPath(200, '/', /Hexo \+ Node.js API/m);
|
|
})
|
|
);
|
|
|
|
test('[now dev] 08-hugo', async t => {
|
|
if (process.platform === 'darwin') {
|
|
// Update PATH to find the Hugo executable installed via GH Actions
|
|
process.env.PATH = `${resolve(fixture('08-hugo'))}${delimiter}${
|
|
process.env.PATH
|
|
}`;
|
|
const tester = testFixtureStdio('08-hugo', async testPath => {
|
|
await testPath(200, '/', /Hugo/m);
|
|
});
|
|
await tester(t);
|
|
} else {
|
|
console.log(`Skipping 08-hugo on platform ${process.platform}`);
|
|
t.pass();
|
|
}
|
|
});
|
|
|
|
test(
|
|
'[now dev] 10-nextjs-node',
|
|
testFixtureStdio('10-nextjs-node', async testPath => {
|
|
await testPath(200, '/', /Next.js \+ Node.js API/m);
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[now dev] 12-polymer-node',
|
|
testFixtureStdio(
|
|
'12-polymer-node',
|
|
async testPath => {
|
|
await testPath(200, '/', /Polymer \+ Node.js API/m);
|
|
},
|
|
{ skipDeploy: true }
|
|
)
|
|
);
|
|
|
|
test(
|
|
'[now dev] 13-preact-node',
|
|
testFixtureStdio(
|
|
'13-preact-node',
|
|
async testPath => {
|
|
await testPath(200, '/', /Preact/m);
|
|
},
|
|
{ skipDeploy: true }
|
|
)
|
|
);
|
|
|
|
test(
|
|
'[now dev] 14-svelte-node',
|
|
testFixtureStdio(
|
|
'14-svelte-node',
|
|
async testPath => {
|
|
await testPath(200, '/', /Svelte/m);
|
|
},
|
|
{ skipDeploy: true }
|
|
)
|
|
);
|
|
|
|
test(
|
|
'[now dev] 16-vue-node',
|
|
testFixtureStdio(
|
|
'16-vue-node',
|
|
async testPath => {
|
|
await testPath(200, '/', /Vue.js \+ Node.js API/m);
|
|
},
|
|
{ skipDeploy: true }
|
|
)
|
|
);
|
|
|
|
test(
|
|
'[now dev] 17-vuepress-node',
|
|
testFixtureStdio(
|
|
'17-vuepress-node',
|
|
async testPath => {
|
|
await testPath(200, '/', /VuePress \+ Node.js API/m);
|
|
},
|
|
{ skipDeploy: true }
|
|
)
|
|
);
|
|
|
|
test(
|
|
'[now dev] double slashes redirect',
|
|
testFixtureStdio(
|
|
'01-node',
|
|
async (_testPath, t, port) => {
|
|
{
|
|
const res = await fetch(`http://localhost:${port}////?foo=bar`, {
|
|
redirect: 'manual',
|
|
});
|
|
|
|
validateResponseHeaders(t, res);
|
|
|
|
const body = await res.text();
|
|
t.is(res.status, 301);
|
|
t.is(res.headers.get('location'), `http://localhost:${port}/?foo=bar`);
|
|
t.is(body, 'Redirecting to /?foo=bar (301)\n');
|
|
}
|
|
|
|
{
|
|
const res = await fetch(`http://localhost:${port}///api////date.js`, {
|
|
method: 'POST',
|
|
redirect: 'manual',
|
|
});
|
|
|
|
validateResponseHeaders(t, res);
|
|
|
|
const body = await res.text();
|
|
t.is(res.status, 200);
|
|
t.truthy(
|
|
body.startsWith('January') ||
|
|
body.startsWith('February') ||
|
|
body.startsWith('March') ||
|
|
body.startsWith('April') ||
|
|
body.startsWith('May') ||
|
|
body.startsWith('June') ||
|
|
body.startsWith('July') ||
|
|
body.startsWith('August') ||
|
|
body.startsWith('September') ||
|
|
body.startsWith('October') ||
|
|
body.startsWith('November') ||
|
|
body.startsWith('December')
|
|
);
|
|
}
|
|
},
|
|
{ skipDeploy: true }
|
|
)
|
|
);
|
|
|
|
test(
|
|
'[now dev] 18-marko',
|
|
testFixtureStdio(
|
|
'18-marko',
|
|
async testPath => {
|
|
await testPath(200, '/', /Marko Starter/m);
|
|
},
|
|
{ skipDeploy: true }
|
|
)
|
|
);
|
|
|
|
test(
|
|
'[now dev] 19-mithril',
|
|
testFixtureStdio(
|
|
'19-mithril',
|
|
async testPath => {
|
|
await testPath(200, '/', /Mithril on Vercel/m);
|
|
},
|
|
{ skipDeploy: true }
|
|
)
|
|
);
|
|
|
|
test(
|
|
'[now dev] 20-riot',
|
|
testFixtureStdio(
|
|
'20-riot',
|
|
async testPath => {
|
|
await testPath(200, '/', /Riot on Vercel/m);
|
|
},
|
|
{ skipDeploy: true }
|
|
)
|
|
);
|
|
|
|
test(
|
|
'[now dev] 21-charge',
|
|
testFixtureStdio(
|
|
'21-charge',
|
|
async testPath => {
|
|
await testPath(200, '/', /Welcome to my new Charge site/m);
|
|
},
|
|
{ skipDeploy: true }
|
|
)
|
|
);
|
|
|
|
test(
|
|
'[now dev] 22-brunch',
|
|
testFixtureStdio(
|
|
'22-brunch',
|
|
async testPath => {
|
|
await testPath(200, '/', /Bon Appétit./m);
|
|
},
|
|
{ skipDeploy: true }
|
|
)
|
|
);
|
|
|
|
test(
|
|
'[now dev] 23-docusaurus',
|
|
testFixtureStdio(
|
|
'23-docusaurus',
|
|
async testPath => {
|
|
await testPath(200, '/', /My Site/m);
|
|
},
|
|
{ skipDeploy: true }
|
|
)
|
|
);
|
|
|
|
test('[now dev] 24-ember', async t => {
|
|
if (shouldSkip(t, '24-ember', '>^6.14.0 || ^8.10.0 || >=9.10.0')) return;
|
|
|
|
const tester = await testFixtureStdio(
|
|
'24-ember',
|
|
async testPath => {
|
|
await testPath(200, '/', /HelloWorld/m);
|
|
},
|
|
{ skipDeploy: true }
|
|
);
|
|
|
|
await tester(t);
|
|
});
|
|
|
|
test(
|
|
'[now dev] temporary directory listing',
|
|
testFixtureStdio(
|
|
'temporary-directory-listing',
|
|
async (_testPath, t, port) => {
|
|
const directory = fixture('temporary-directory-listing');
|
|
await fs.unlink(join(directory, 'index.txt')).catch(() => null);
|
|
|
|
await sleep(ms('20s'));
|
|
|
|
const firstResponse = await fetch(`http://localhost:${port}`);
|
|
validateResponseHeaders(t, firstResponse);
|
|
const body = await firstResponse.text();
|
|
t.is(firstResponse.status, 404, `Received instead: ${body}`);
|
|
|
|
await fs.writeFile(join(directory, 'index.txt'), 'hello');
|
|
|
|
for (let i = 0; i < 20; i++) {
|
|
const response = await fetch(`http://localhost:${port}`);
|
|
validateResponseHeaders(t, response);
|
|
|
|
if (response.status === 200) {
|
|
const body = await response.text();
|
|
t.is(body, 'hello');
|
|
}
|
|
|
|
await sleep(ms('1s'));
|
|
}
|
|
},
|
|
{ skipDeploy: true }
|
|
)
|
|
);
|
|
|
|
test('[now dev] add a `package.json` to trigger `@now/static-build`', async t => {
|
|
const directory = fixture('trigger-static-build');
|
|
|
|
await fs.unlink(join(directory, 'package.json')).catch(() => null);
|
|
|
|
await fs.unlink(join(directory, 'public', 'index.txt')).catch(() => null);
|
|
|
|
await fs.rmdir(join(directory, 'public')).catch(() => null);
|
|
|
|
const tester = testFixtureStdio(
|
|
'trigger-static-build',
|
|
async (_testPath, t, port) => {
|
|
{
|
|
const response = await fetch(`http://localhost:${port}`);
|
|
validateResponseHeaders(t, response);
|
|
const body = await response.text();
|
|
t.is(body.trim(), 'hello:index.txt');
|
|
}
|
|
|
|
const rnd = Math.random().toString();
|
|
const pkg = {
|
|
private: true,
|
|
scripts: { build: `mkdir -p public && echo ${rnd} > public/index.txt` },
|
|
};
|
|
|
|
await fs.writeFile(join(directory, 'package.json'), JSON.stringify(pkg));
|
|
|
|
// Wait until file events have been processed
|
|
await sleep(ms('2s'));
|
|
|
|
{
|
|
const response = await fetch(`http://localhost:${port}`);
|
|
validateResponseHeaders(t, response);
|
|
const body = await response.text();
|
|
t.is(body.trim(), rnd);
|
|
}
|
|
},
|
|
{ skipDeploy: true }
|
|
);
|
|
|
|
await tester(t);
|
|
});
|
|
|
|
test('[now dev] no build matches warning', async t => {
|
|
const directory = fixture('no-build-matches');
|
|
const { dev } = await testFixture(directory, {
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
});
|
|
|
|
try {
|
|
// start `now dev` detached in child_process
|
|
dev.unref();
|
|
|
|
dev.stderr.setEncoding('utf8');
|
|
await new Promise(resolve => {
|
|
dev.stderr.on('data', str => {
|
|
if (str.includes('did not match any source files')) {
|
|
resolve();
|
|
}
|
|
});
|
|
});
|
|
|
|
t.pass();
|
|
} finally {
|
|
await dev.kill('SIGTERM');
|
|
}
|
|
});
|
|
|
|
test(
|
|
'[now dev] do not recursivly check the path',
|
|
testFixtureStdio('handle-filesystem-missing', async testPath => {
|
|
await testPath(200, '/', /hello/m);
|
|
await testPath(404, '/favicon.txt');
|
|
})
|
|
);
|
|
|
|
test('[now dev] render warning for empty cwd dir', async t => {
|
|
const directory = fixture('empty');
|
|
const { dev, port } = await testFixture(directory, {
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
});
|
|
|
|
try {
|
|
dev.unref();
|
|
|
|
// Monitor `stderr` for the warning
|
|
dev.stderr.setEncoding('utf8');
|
|
await new Promise(resolve => {
|
|
dev.stderr.on('data', str => {
|
|
if (
|
|
str.includes(
|
|
'There are no files (or only files starting with a dot) inside your deployment'
|
|
)
|
|
) {
|
|
resolve();
|
|
}
|
|
});
|
|
});
|
|
|
|
// Issue a request to ensure a 404 response
|
|
await sleep(ms('3s'));
|
|
const response = await fetch(`http://localhost:${port}`);
|
|
validateResponseHeaders(t, response);
|
|
t.is(response.status, 404);
|
|
} finally {
|
|
await dev.kill('SIGTERM');
|
|
}
|
|
});
|
|
|
|
test('[now dev] do not rebuild for changes in the output directory', async t => {
|
|
const directory = fixture('output-is-source');
|
|
|
|
// Pack the builder and set it in the now.json
|
|
const builder = await getPackedBuilderPath('now-static-build');
|
|
|
|
await fs.writeFile(
|
|
join(directory, 'now.json'),
|
|
JSON.stringify({
|
|
builds: [
|
|
{
|
|
src: 'package.json',
|
|
use: `file://${builder}`,
|
|
config: { zeroConfig: true },
|
|
},
|
|
],
|
|
})
|
|
);
|
|
|
|
const { dev, port } = await testFixture(directory, {
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
});
|
|
|
|
try {
|
|
dev.unref();
|
|
|
|
let stderr = [];
|
|
const start = Date.now();
|
|
|
|
dev.stderr.on('data', str => stderr.push(str));
|
|
|
|
while (stderr.join('').includes('Ready') === false) {
|
|
await sleep(ms('3s'));
|
|
|
|
if (Date.now() - start > ms('30s')) {
|
|
console.log('stderr:', stderr.join(''));
|
|
break;
|
|
}
|
|
}
|
|
|
|
const resp1 = await fetch(`http://localhost:${port}`);
|
|
const text1 = await resp1.text();
|
|
t.is(text1.trim(), 'hello first', stderr.join(''));
|
|
|
|
await fs.writeFile(join(directory, 'public', 'index.html'), 'hello second');
|
|
|
|
await sleep(ms('3s'));
|
|
|
|
const resp2 = await fetch(`http://localhost:${port}`);
|
|
const text2 = await resp2.text();
|
|
t.is(text2.trim(), 'hello second', stderr.join(''));
|
|
} finally {
|
|
await dev.kill('SIGTERM');
|
|
}
|
|
});
|
|
|
|
test(
|
|
'[now dev] 25-nextjs-src-dir',
|
|
testFixtureStdio('25-nextjs-src-dir', async testPath => {
|
|
await testPath(200, '/', /Next.js \+ Node.js API/m);
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[now dev] 26-nextjs-secrets',
|
|
testFixtureStdio(
|
|
'26-nextjs-secrets',
|
|
async testPath => {
|
|
await testPath(200, '/api/user', /runtime/m);
|
|
await testPath(200, '/', /buildtime/m);
|
|
},
|
|
{ skipDeploy: true }
|
|
)
|
|
);
|
|
|
|
test(
|
|
'[now dev] 27-zero-config-env',
|
|
testFixtureStdio(
|
|
'27-zero-config-env',
|
|
async testPath => {
|
|
await testPath(200, '/api/print', /build-and-runtime/m);
|
|
await testPath(200, '/', /build-and-runtime/m);
|
|
},
|
|
{ skipDeploy: true }
|
|
)
|
|
);
|
|
|
|
test(
|
|
'[now dev] Use `@now/python` with Flask requirements.txt',
|
|
testFixtureStdio('python-flask', async testPath => {
|
|
const name = 'Alice';
|
|
const year = new Date().getFullYear();
|
|
await testPath(200, `/api/user?name=${name}`, new RegExp(`Hello ${name}`));
|
|
await testPath(200, `/api/date`, new RegExp(`Current date is ${year}`));
|
|
await testPath(200, `/api/date.py`, new RegExp(`Current date is ${year}`));
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[now dev] Use runtime from the functions property',
|
|
testFixtureStdio(
|
|
'custom-runtime',
|
|
async testPath => {
|
|
await testPath(200, `/api/user`, /Hello, from Bash!/m);
|
|
await testPath(200, `/api/user.sh`, /Hello, from Bash!/m);
|
|
},
|
|
{ skipDeploy: true }
|
|
)
|
|
);
|