Files
vercel/packages/cli/test/integration.js
Nathan Rajlich 465129e62e [cli] Remove support for single file deployments (#6652)
Deploying a single file has printed a deprecation warning for a long time. Let's finally remove that behavior.
2021-12-08 19:45:06 +00:00

3577 lines
94 KiB
JavaScript
Vendored
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import ms from 'ms';
import path from 'path';
import { URL, parse as parseUrl } from 'url';
import test from 'ava';
import semVer from 'semver';
import { Readable } from 'stream';
import { homedir } from 'os';
import _execa from 'execa';
import XDGAppPaths from 'xdg-app-paths';
import fetch from 'node-fetch';
import tmp from 'tmp-promise';
import retry from 'async-retry';
import fs, {
writeFile,
readFile,
remove,
copy,
ensureDir,
exists,
} from 'fs-extra';
import logo from '../src/util/output/logo';
import sleep from '../src/util/sleep';
import pkg from '../package';
import prepareFixtures from './helpers/prepare';
import { fetchTokenWithRetry } from '../../../test/lib/deployment/now-deploy';
// log command when running `execa`
function execa(file, args, options) {
console.log(`$ vercel ${args.join(' ')}`);
return _execa(file, args, options);
}
function fixture(name) {
const directory = path.join(__dirname, 'fixtures', 'integration', name);
const config = path.join(directory, 'project.json');
// We need to remove it, otherwise we can't re-use fixtures
if (fs.existsSync(config)) {
fs.unlinkSync(config);
}
return directory;
}
const binaryPath = path.resolve(__dirname, `../scripts/start.js`);
const example = name =>
path.join(__dirname, '..', '..', '..', 'examples', name);
const deployHelpMessage = `${logo} vercel [options] <command | path>`;
let session = 'temp-session';
const isCanary = pkg.version.includes('canary');
const pickUrl = stdout => {
const lines = stdout.split('\n');
return lines[lines.length - 1];
};
const createFile = dest => fs.closeSync(fs.openSync(dest, 'w'));
const waitForDeployment = async href => {
console.log(`waiting for ${href} to become ready...`);
const start = Date.now();
const max = ms('4m');
const inspectorText = '<title>Deployment Overview';
// eslint-disable-next-line
while (true) {
const response = await fetch(href, { redirect: 'manual' });
const text = await response.text();
if (response.status === 200 && !text.includes(inspectorText)) {
break;
}
const current = Date.now();
if (current - start > max || response.status >= 500) {
throw new Error(
`Waiting for "${href}" failed since it took longer than 4 minutes.\n` +
`Received status ${response.status}:\n"${text}"`
);
}
await sleep(2000);
}
};
function fetchTokenInformation(token, retries = 3) {
const url = `https://api.vercel.com/v2/user`;
const headers = { Authorization: `Bearer ${token}` };
return retry(
async () => {
const res = await fetch(url, { headers });
if (!res.ok) {
throw new Error(
`Failed to fetch ${url}, received status ${res.status}`
);
}
const data = await res.json();
return data.user;
},
{ retries, factor: 1 }
);
}
function formatOutput({ stderr, stdout }) {
return `
-----
Stderr:
${stderr}
-----
Stdout:
${stdout}
-----
`;
}
// AVA's `t.context` can only be set before the tests,
// but we want to set it within as well
const context = {};
const defaultOptions = { reject: false };
const defaultArgs = [];
let token;
let email;
let contextName;
let tmpDir;
let globalDir = XDGAppPaths('com.vercel.cli').dataDirs()[0];
if (!process.env.CI) {
tmpDir = tmp.dirSync({
// This ensures the directory gets
// deleted even if it has contents
unsafeCleanup: true,
});
globalDir = path.join(tmpDir.name, 'com.vercel.tests');
defaultArgs.push('-Q', globalDir);
console.log(
'No CI detected, adding defaultArgs to avoid polluting user settings',
defaultArgs
);
}
function mockLoginApi(req, res) {
const { url = '/', method } = req;
let { pathname = '/', query = {} } = parseUrl(url, true);
console.log(`[mock-login-server] ${method} ${pathname}`);
const securityCode = 'Bears Beets Battlestar Galactica';
res.setHeader('content-type', 'application/json');
if (
method === 'POST' &&
pathname === '/registration' &&
query.mode === 'login'
) {
res.end(JSON.stringify({ token, securityCode }));
} else if (
method === 'GET' &&
pathname === '/registration/verify' &&
query.email === email
) {
res.end(JSON.stringify({ token }));
} else {
res.statusCode = 405;
res.end(JSON.stringify({ code: 'method_not_allowed' }));
}
}
let loginApiUrl = '';
const loginApiServer = require('http')
.createServer(mockLoginApi)
.listen(0, () => {
const { port } = loginApiServer.address();
loginApiUrl = `http://localhost:${port}`;
console.log(`[mock-login-server] Listening on ${loginApiUrl}`);
});
const execute = (args, options) =>
execa(binaryPath, [...defaultArgs, ...args], {
...defaultOptions,
...options,
});
const apiFetch = (url, { headers, ...options } = {}) => {
return fetch(`https://api.vercel.com${url}`, {
headers: {
Authorization: `Bearer ${token}`,
...(headers || {}),
},
...options,
});
};
const waitForPrompt = (cp, assertion) =>
new Promise((resolve, reject) => {
console.log('Waiting for prompt...');
setTimeout(() => reject(new Error('timeout in waitForPrompt')), 60000);
const listener = chunk => {
console.log('> ' + chunk);
if (assertion(chunk)) {
cp.stdout.off && cp.stdout.off('data', listener);
cp.stderr.off && cp.stderr.off('data', listener);
resolve();
}
};
cp.stdout.on('data', listener);
cp.stderr.on('data', listener);
});
const getDeploymentBuildsByUrl = async url => {
const hostRes = await apiFetch(`/v10/now/deployments/get?url=${url}`);
const { id } = await hostRes.json();
const buildsRes = await apiFetch(`/v10/now/deployments/${id}/builds`);
const { builds } = await buildsRes.json();
return builds;
};
const createUser = async () => {
await retry(
async () => {
if (!fs.existsSync(globalDir)) {
console.log('Creating global config directory ', globalDir);
await ensureDir(globalDir);
} else {
console.log('Found global config directory ', globalDir);
}
token = await fetchTokenWithRetry();
await fs.writeJSON(getConfigAuthPath(), { token });
const user = await fetchTokenInformation(token);
email = user.email;
contextName = user.username;
session = Math.random().toString(36).split('.')[1];
},
{ retries: 3, factor: 1 }
);
};
const getConfigAuthPath = () => path.join(globalDir, 'auth.json');
async function setupProject(process, projectName, overrides) {
await waitForPrompt(process, chunk => /Set up [^?]+\?/.test(chunk));
process.stdin.write('yes\n');
await waitForPrompt(process, chunk => /Which scope [^?]+\?/.test(chunk));
process.stdin.write('\n');
await waitForPrompt(process, chunk =>
chunk.includes('Link to existing project?')
);
process.stdin.write('no\n');
await waitForPrompt(process, chunk =>
chunk.includes('Whats your projects name?')
);
process.stdin.write(`${projectName}\n`);
await waitForPrompt(process, chunk =>
chunk.includes('In which directory is your code located?')
);
process.stdin.write('\n');
await waitForPrompt(process, chunk =>
chunk.includes('Want to override the settings?')
);
if (overrides) {
process.stdin.write('yes\n');
const { buildCommand, outputDirectory, devCommand } = overrides;
await waitForPrompt(process, chunk =>
chunk.includes(
'Which settings would you like to overwrite (select multiple)?'
)
);
process.stdin.write('a\n'); // 'a' means select all
await waitForPrompt(process, chunk =>
chunk.includes(`What's your Build Command?`)
);
process.stdin.write(`${buildCommand ?? ''}\n`);
await waitForPrompt(process, chunk =>
chunk.includes(`What's your Output Directory?`)
);
process.stdin.write(`${outputDirectory ?? ''}\n`);
await waitForPrompt(process, chunk =>
chunk.includes(`What's your Development Command?`)
);
process.stdin.write(`${devCommand ?? ''}\n`);
} else {
process.stdin.write('no\n');
}
await waitForPrompt(process, chunk => chunk.includes('Linked to'));
}
test.before(async () => {
try {
await createUser();
await prepareFixtures(contextName, binaryPath);
} catch (err) {
console.log('Failed `test.before`');
console.log(err);
}
});
test.after.always(async () => {
if (loginApiServer) {
// Stop mock server
loginApiServer.close();
}
// Make sure the token gets revoked unless it's passed in via environment
if (!process.env.VERCEL_TOKEN) {
await execa(binaryPath, ['logout', ...defaultArgs]);
}
if (tmpDir) {
// Remove config directory entirely
tmpDir.removeCallback();
}
});
test('default command should prompt login with empty auth.json', async t => {
await fs.writeFile(getConfigAuthPath(), JSON.stringify({}));
try {
await execa(binaryPath, [...defaultArgs]);
t.fail();
} catch (err) {
t.true(
err.stderr.includes(
'Error! No existing credentials found. Please run `vercel login` or pass "--token"'
)
);
}
});
test('login', async t => {
t.timeout(ms('1m'));
await fs.remove(getConfigAuthPath());
const loginOutput = await execa(binaryPath, [
'login',
email,
'--api',
loginApiUrl,
...defaultArgs,
]);
t.is(loginOutput.exitCode, 0, formatOutput(loginOutput));
t.regex(
loginOutput.stderr,
/You are now logged in\./gm,
formatOutput(loginOutput)
);
const auth = await fs.readJSON(getConfigAuthPath());
t.is(auth.token, token);
});
test('deploy using only now.json with `redirects` defined', async t => {
const target = fixture('redirects-v2');
const { exitCode, stderr, stdout } = await execa(
binaryPath,
[target, ...defaultArgs, '--confirm'],
{
reject: false,
}
);
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
const url = stdout;
const res = await fetch(`${url}/foo/bar`, { redirect: 'manual' });
const location = res.headers.get('location');
t.is(location, 'https://example.com/foo/bar');
});
test('deploy using --local-config flag v2', async t => {
const target = fixture('local-config-v2');
const configPath = path.join(target, 'now-test.json');
const { exitCode, stderr, stdout } = await execa(
binaryPath,
[
'deploy',
target,
'--local-config',
configPath,
...defaultArgs,
'--confirm',
],
{
reject: false,
}
);
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
const { host } = new URL(stdout);
t.regex(host, /secondary/gm, `Expected "secondary" but received "${host}"`);
const testRes = await fetch(`https://${host}/test-${contextName}.html`);
const testText = await testRes.text();
t.is(testText, '<h1>hello test</h1>');
const anotherTestRes = await fetch(`https://${host}/another-test`);
const anotherTestText = await anotherTestRes.text();
t.is(anotherTestText, testText);
const mainRes = await fetch(`https://${host}/main-${contextName}.html`);
t.is(mainRes.status, 404, 'Should not deploy/build main now.json');
const anotherMainRes = await fetch(`https://${host}/another-main`);
t.is(anotherMainRes.status, 404, 'Should not deploy/build main now.json');
});
test('deploy using --local-config flag above target', async t => {
const root = fixture('local-config-above-target');
const target = path.join(root, 'dir');
const { exitCode, stderr, stdout } = await execa(
binaryPath,
[
'deploy',
target,
'--local-config',
'./now-root.json',
...defaultArgs,
'--confirm',
],
{
cwd: root,
reject: false,
}
);
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
const { host } = new URL(stdout);
const testRes = await fetch(`https://${host}/index.html`);
const testText = await testRes.text();
t.is(testText, '<h1>hello index</h1>');
const anotherTestRes = await fetch(`https://${host}/another.html`);
const anotherTestText = await anotherTestRes.text();
t.is(anotherTestText, '<h1>hello another</h1>');
t.regex(host, /root-level/gm, `Expected "root-level" but received "${host}"`);
});
test('Deploy `api-env` fixture and test `vercel env` command', async t => {
const target = fixture('api-env');
async function vcLink() {
const { exitCode, stderr, stdout } = await execa(
binaryPath,
['link', '--confirm', ...defaultArgs],
{
reject: false,
cwd: target,
}
);
console.log({ stdout });
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
}
async function vcEnvLsIsEmpty() {
const { exitCode, stderr, stdout } = await execa(
binaryPath,
['env', 'ls', ...defaultArgs],
{
reject: false,
cwd: target,
}
);
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
t.regex(stderr, /No Environment Variables found in Project/gm);
}
async function vcEnvAddWithPrompts() {
const vc = execa(binaryPath, ['env', 'add', ...defaultArgs], {
reject: false,
cwd: target,
});
await waitForPrompt(vc, chunk =>
chunk.includes('Whats the name of the variable?')
);
vc.stdin.write('MY_NEW_ENV_VAR\n');
await waitForPrompt(
vc,
chunk =>
chunk.includes('Whats the value of') &&
chunk.includes('MY_NEW_ENV_VAR')
);
vc.stdin.write('my plaintext value\n');
await waitForPrompt(
vc,
chunk =>
chunk.includes('which Environments') && chunk.includes('MY_NEW_ENV_VAR')
);
vc.stdin.write('a\n'); // select all
const { exitCode, stderr, stdout } = await vc;
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
}
async function vcEnvAddFromStdin() {
const vc = execa(
binaryPath,
['env', 'add', 'MY_STDIN_VAR', 'development', ...defaultArgs],
{
reject: false,
cwd: target,
}
);
vc.stdin.end('{"expect":"quotes"}');
const { exitCode, stderr, stdout } = await vc;
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
}
async function vcEnvAddFromStdinPreview() {
const vc = execa(
binaryPath,
['env', 'add', 'MY_PREVIEW', 'preview', ...defaultArgs],
{
reject: false,
cwd: target,
}
);
vc.stdin.end('preview-no-branch');
const { exitCode, stderr, stdout } = await vc;
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
}
async function vcEnvAddFromStdinPreviewWithBranch() {
const vc = execa(
binaryPath,
['env', 'add', 'MY_PREVIEW', 'preview', 'staging', ...defaultArgs],
{
reject: false,
cwd: target,
}
);
vc.stdin.end('preview-with-branch');
const { exitCode, stderr, stdout } = await vc;
t.is(exitCode, 1, formatOutput({ stderr, stdout }));
t.regex(stderr, /does not have a connected Git repository/gm);
}
async function vcEnvLsIncludesVar() {
const { exitCode, stderr, stdout } = await execa(
binaryPath,
['env', 'ls', ...defaultArgs],
{
reject: false,
cwd: target,
}
);
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
t.regex(stderr, /Environment Variables found in Project/gm);
console.log(stdout);
const lines = stdout.split('\n');
const plaintextEnvs = lines.filter(line => line.includes('MY_NEW_ENV_VAR'));
t.is(plaintextEnvs.length, 1);
t.regex(plaintextEnvs[0], /Production, Preview, Development/gm);
const stdinEnvs = lines.filter(line => line.includes('MY_STDIN_VAR'));
t.is(stdinEnvs.length, 1);
t.regex(stdinEnvs[0], /Development/gm);
const previewEnvs = lines.filter(line => line.includes('MY_PREVIEW'));
t.is(previewEnvs.length, 1);
t.regex(previewEnvs[0], /Encrypted .* Preview /gm);
}
// we create a "legacy" env variable that contains a decryptable secret
// to check that vc env pull and vc dev work correctly with decryptable secrets
async function createEnvWithDecryptableSecret() {
console.log('creating an env variable with a decryptable secret');
const name = `my-secret${Math.floor(Math.random() * 10000)}`;
const res = await apiFetch('/v2/now/secrets', {
method: 'POST',
body: JSON.stringify({
name,
value: 'decryptable value',
decryptable: true,
}),
});
t.is(res.status, 200);
const json = await res.json();
const link = require(path.join(target, '.vercel/project.json'));
const resEnv = await apiFetch(`/v4/projects/${link.projectId}/env`, {
method: 'POST',
body: JSON.stringify({
key: 'MY_DECRYPTABLE_SECRET_ENV',
value: json.uid,
target: ['development'],
type: 'secret',
}),
});
t.is(resEnv.status, 200);
}
async function vcEnvPull() {
const { exitCode, stderr, stdout } = await execa(
binaryPath,
['env', 'pull', '-y', ...defaultArgs],
{
reject: false,
cwd: target,
}
);
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
t.regex(stderr, /Created .env file/gm);
const contents = fs.readFileSync(path.join(target, '.env'), 'utf8');
t.true(contents.startsWith('# Created by Vercel CLI\n'));
const lines = new Set(contents.split('\n'));
t.true(lines.has('MY_NEW_ENV_VAR="my plaintext value"'));
t.true(lines.has('MY_STDIN_VAR="{"expect":"quotes"}"'));
t.true(lines.has('MY_DECRYPTABLE_SECRET_ENV="decryptable value"'));
t.false(lines.has('MY_PREVIEW'));
}
async function vcEnvPullOverwrite() {
const { exitCode, stderr, stdout } = await execa(
binaryPath,
['env', 'pull', ...defaultArgs],
{
reject: false,
cwd: target,
}
);
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
t.regex(stderr, /Overwriting existing .env file/gm);
t.regex(stderr, /Updated .env file/gm);
}
async function vcEnvPullConfirm() {
fs.writeFileSync(path.join(target, '.env'), 'hahaha');
const vc = execa(binaryPath, ['env', 'pull', ...defaultArgs], {
reject: false,
cwd: target,
});
await waitForPrompt(vc, chunk =>
chunk.includes('Found existing file ".env". Do you want to overwrite?')
);
vc.stdin.end('y\n');
const { exitCode, stderr, stdout } = await vc;
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
}
async function vcDeployWithVar() {
const { exitCode, stderr, stdout } = await execa(
binaryPath,
[...defaultArgs],
{
reject: false,
cwd: target,
}
);
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
const { host } = new URL(stdout);
const apiUrl = `https://${host}/api/get-env`;
console.log({ apiUrl });
const apiRes = await fetch(apiUrl);
t.is(apiRes.status, 200, formatOutput({ stderr, stdout }));
const apiJson = await apiRes.json();
t.is(apiJson['MY_NEW_ENV_VAR'], 'my plaintext value');
const homeUrl = `https://${host}`;
console.log({ homeUrl });
const homeRes = await fetch(homeUrl);
t.is(homeRes.status, 200, formatOutput({ stderr, stdout }));
const homeJson = await homeRes.json();
t.is(homeJson['MY_NEW_ENV_VAR'], 'my plaintext value');
}
async function vcDevWithEnv() {
const vc = execa(binaryPath, ['dev', ...defaultArgs], {
reject: false,
cwd: target,
});
let localhost = undefined;
await waitForPrompt(vc, chunk => {
if (chunk.includes('Ready! Available at')) {
localhost = /(https?:[^\s]+)/g.exec(chunk);
return true;
}
return false;
});
const apiUrl = `${localhost[0]}/api/get-env`;
const apiRes = await fetch(apiUrl);
t.is(apiRes.status, 200);
const apiJson = await apiRes.json();
t.is(apiJson['MY_NEW_ENV_VAR'], 'my plaintext value');
t.is(apiJson['MY_DECRYPTABLE_SECRET_ENV'], 'decryptable value');
const homeUrl = localhost[0];
const homeRes = await fetch(homeUrl);
const homeJson = await homeRes.json();
t.is(homeJson['MY_NEW_ENV_VAR'], 'my plaintext value');
t.is(homeJson['MY_DECRYPTABLE_SECRET_ENV'], 'decryptable value');
vc.kill('SIGTERM', { forceKillAfterTimeout: 2000 });
const { exitCode, stderr, stdout } = await vc;
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
}
async function vcDevAndFetchCloudVars() {
const vc = execa(binaryPath, ['dev', ...defaultArgs], {
reject: false,
cwd: target,
});
let localhost = undefined;
await waitForPrompt(vc, chunk => {
if (chunk.includes('Ready! Available at')) {
localhost = /(https?:[^\s]+)/g.exec(chunk);
return true;
}
return false;
});
const apiUrl = `${localhost[0]}/api/get-env`;
const apiRes = await fetch(apiUrl);
t.is(apiRes.status, 200);
const apiJson = await apiRes.json();
t.is(apiJson['MY_NEW_ENV_VAR'], 'my plaintext value');
t.is(apiJson['MY_STDIN_VAR'], '{"expect":"quotes"}');
t.is(apiJson['MY_DECRYPTABLE_SECRET_ENV'], 'decryptable value');
const homeUrl = localhost[0];
const homeRes = await fetch(homeUrl);
const homeJson = await homeRes.json();
t.is(homeJson['MY_NEW_ENV_VAR'], 'my plaintext value');
t.is(homeJson['MY_STDIN_VAR'], '{"expect":"quotes"}');
t.is(homeJson['MY_DECRYPTABLE_SECRET_ENV'], 'decryptable value');
// system env vars are automatically exposed
t.is(apiJson['VERCEL'], '1');
t.is(homeJson['VERCEL'], '1');
vc.kill('SIGTERM', { forceKillAfterTimeout: 2000 });
const { exitCode, stderr, stdout } = await vc;
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
}
async function enableAutoExposeSystemEnvs() {
const link = require(path.join(target, '.vercel/project.json'));
const res = await apiFetch(`/v2/projects/${link.projectId}`, {
method: 'PATCH',
body: JSON.stringify({ autoExposeSystemEnvs: true }),
});
t.is(res.status, 200);
if (res.status === 200) {
console.log(
`Set autoExposeSystemEnvs=true for project ${link.projectId}`
);
}
}
async function vcEnvPullFetchSystemVars() {
const { exitCode, stderr, stdout } = await execa(
binaryPath,
['env', 'pull', '-y', ...defaultArgs],
{
reject: false,
cwd: target,
}
);
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
const contents = fs.readFileSync(path.join(target, '.env'), 'utf8');
const lines = new Set(contents.split('\n'));
t.true(lines.has('VERCEL="1"'));
t.true(lines.has('VERCEL_URL=""'));
t.true(lines.has('VERCEL_ENV="development"'));
t.true(lines.has('VERCEL_GIT_PROVIDER=""'));
t.true(lines.has('VERCEL_GIT_REPO_SLUG=""'));
}
async function vcDevAndFetchSystemVars() {
const vc = execa(binaryPath, ['dev', ...defaultArgs], {
reject: false,
cwd: target,
});
let localhost = undefined;
await waitForPrompt(vc, chunk => {
if (chunk.includes('Ready! Available at')) {
localhost = /(https?:[^\s]+)/g.exec(chunk);
return true;
}
return false;
});
const apiUrl = `${localhost[0]}/api/get-env`;
const apiRes = await fetch(apiUrl);
const localhostNoProtocol = localhost[0].slice('http://'.length);
const apiJson = await apiRes.json();
t.is(apiJson['VERCEL'], '1');
t.is(apiJson['VERCEL_URL'], localhostNoProtocol);
t.is(apiJson['VERCEL_ENV'], 'development');
t.is(apiJson['VERCEL_REGION'], 'dev1');
t.is(apiJson['VERCEL_GIT_PROVIDER'], '');
t.is(apiJson['VERCEL_GIT_REPO_SLUG'], '');
const homeUrl = localhost[0];
const homeRes = await fetch(homeUrl);
const homeJson = await homeRes.json();
t.is(homeJson['VERCEL'], '1');
t.is(homeJson['VERCEL_URL'], localhostNoProtocol);
t.is(homeJson['VERCEL_ENV'], 'development');
t.is(homeJson['VERCEL_REGION'], undefined);
t.is(homeJson['VERCEL_GIT_PROVIDER'], '');
t.is(homeJson['VERCEL_GIT_REPO_SLUG'], '');
vc.kill('SIGTERM', { forceKillAfterTimeout: 2000 });
const { exitCode, stderr, stdout } = await vc;
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
}
async function vcEnvRemove() {
const vc = execa(binaryPath, ['env', 'rm', '-y', ...defaultArgs], {
reject: false,
cwd: target,
});
await waitForPrompt(vc, chunk =>
chunk.includes('Whats the name of the variable?')
);
vc.stdin.write('MY_PREVIEW\n');
const { exitCode, stderr, stdout } = await vc;
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
}
async function vcEnvRemoveWithArgs() {
const { exitCode, stderr, stdout } = await execa(
binaryPath,
['env', 'rm', 'MY_STDIN_VAR', 'development', '-y', ...defaultArgs],
{
reject: false,
cwd: target,
}
);
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
const {
exitCode: exitCode3,
stderr: stderr3,
stdout: stdout3,
} = await execa(
binaryPath,
[
'env',
'rm',
'MY_DECRYPTABLE_SECRET_ENV',
'development',
'-y',
...defaultArgs,
],
{
reject: false,
cwd: target,
}
);
t.is(exitCode3, 0, formatOutput({ stderr3, stdout3 }));
}
async function vcEnvRemoveWithNameOnly() {
const { exitCode, stderr, stdout } = await execa(
binaryPath,
['env', 'rm', 'MY_NEW_ENV_VAR', '-y', ...defaultArgs],
{
reject: false,
cwd: target,
}
);
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
}
await vcLink();
await vcEnvLsIsEmpty();
await vcEnvAddWithPrompts();
await vcEnvAddFromStdin();
await vcEnvAddFromStdinPreview();
await vcEnvAddFromStdinPreviewWithBranch();
await vcEnvLsIncludesVar();
await createEnvWithDecryptableSecret();
await vcEnvPull();
await vcEnvPullOverwrite();
await vcEnvPullConfirm();
await vcDeployWithVar();
await vcDevWithEnv();
fs.unlinkSync(path.join(target, '.env'));
await vcDevAndFetchCloudVars();
await enableAutoExposeSystemEnvs();
await vcEnvPullFetchSystemVars();
fs.unlinkSync(path.join(target, '.env'));
await vcDevAndFetchSystemVars();
await vcEnvRemove();
await vcEnvRemoveWithArgs();
await vcEnvRemoveWithNameOnly();
await vcEnvLsIsEmpty();
});
test('[vc projects] should create a project successfully', async t => {
const projectName = `vc-projects-add-${
Math.random().toString(36).split('.')[1]
}`;
const vc = execa(binaryPath, [
'projects',
'add',
projectName,
...defaultArgs,
]);
await waitForPrompt(vc, chunk =>
chunk.includes(`Success! Project ${projectName} added`)
);
const { exitCode, stderr, stdout } = await vc;
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
// creating the same project again should succeed
const vc2 = execa(binaryPath, [
'projects',
'add',
projectName,
...defaultArgs,
]);
await waitForPrompt(vc2, chunk =>
chunk.includes(`Success! Project ${projectName} added`)
);
const { exitCode: exitCode2, stderr: stderr2, stdout: stdout2 } = await vc;
t.is(exitCode2, 0, formatOutput({ stderr2, stdout2 }));
});
test('deploy with metadata containing "=" in the value', async t => {
const target = fixture('static-v2-meta');
const { exitCode, stderr, stdout } = await execa(
binaryPath,
[target, ...defaultArgs, '--confirm', '--meta', 'someKey=='],
{ reject: false }
);
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
const { host } = new URL(stdout);
const res = await fetch(
`https://api.vercel.com/v12/now/deployments/get?url=${host}`,
{ headers: { authorization: `Bearer ${token}` } }
);
const deployment = await res.json();
t.is(deployment.meta.someKey, '=');
});
test('print the deploy help message', async t => {
const { stderr, stdout, exitCode } = await execa(
binaryPath,
['help', ...defaultArgs],
{
reject: false,
}
);
console.log(stderr);
console.log(stdout);
console.log(exitCode);
t.is(exitCode, 2);
t.true(stderr.includes(deployHelpMessage), `Received:\n${stderr}\n${stdout}`);
t.false(
stderr.includes('ExperimentalWarning'),
`Received:\n${stderr}\n${stdout}`
);
});
test('output the version', async t => {
const { stdout, stderr, exitCode } = await execa(
binaryPath,
['--version', ...defaultArgs],
{
reject: false,
}
);
console.log(stderr);
console.log(stdout);
console.log(exitCode);
const version = stdout.trim();
t.is(exitCode, 0);
t.truthy(semVer.valid(version));
t.is(version, pkg.version);
});
test('should error with suggestion for secrets subcommand', async t => {
const target = fixture('subdirectory-secret');
const { exitCode, stderr, stdout } = await execa(
binaryPath,
['secret', 'add', 'key', 'value', ...defaultArgs],
{
cwd: target,
reject: false,
}
);
console.log(stderr);
console.log(stdout);
console.log(exitCode);
t.is(exitCode, 1);
t.regex(
stderr,
/secrets/gm,
`Expected "secrets" suggestion but received "${stderr}"`
);
});
test('should add secret with hyphen prefix', async t => {
const target = fixture('build-secret');
const key = 'mysecret';
const value = '-foo_bar';
let secretCall = await execa(
binaryPath,
['secrets', 'add', ...defaultArgs, key, value],
{
cwd: target,
reject: false,
}
);
t.is(
secretCall.exitCode,
0,
formatOutput({ stderr: secretCall.stderr, stdout: secretCall.stdout })
);
let targetCall = await execa(binaryPath, [...defaultArgs, '--confirm'], {
cwd: target,
reject: false,
});
t.is(
targetCall.exitCode,
0,
formatOutput({ stderr: targetCall.stderr, stdout: targetCall.stdout })
);
const { host } = new URL(targetCall.stdout);
const response = await fetch(`https://${host}`);
t.is(
response.status,
200,
formatOutput({ stderr: targetCall.stderr, stdout: targetCall.stdout })
);
t.is(
await response.text(),
`${value}\n`,
formatOutput({ stderr: targetCall.stderr, stdout: targetCall.stdout })
);
});
test('login with unregistered user', async t => {
const { stdout, stderr, exitCode } = await execa(
binaryPath,
['login', `${session}@${session}.com`, ...defaultArgs],
{
reject: false,
}
);
console.log(stderr);
console.log(stdout);
console.log(exitCode);
const goal = `Error! Please sign up: https://vercel.com/signup`;
const lines = stderr.trim().split('\n');
const last = lines[lines.length - 1];
t.is(exitCode, 1);
t.true(last.includes(goal));
});
test('ignore files specified in .nowignore', async t => {
const directory = fixture('nowignore');
const args = [
'--debug',
'--public',
'--name',
session,
...defaultArgs,
'--confirm',
];
const targetCall = await execa(binaryPath, args, {
cwd: directory,
reject: false,
});
console.log(targetCall.stderr);
console.log(targetCall.stdout);
console.log(targetCall.exitCode);
const { host } = new URL(targetCall.stdout);
const ignoredFile = await fetch(`https://${host}/ignored.txt`);
t.is(ignoredFile.status, 404);
const presentFile = await fetch(`https://${host}/index.txt`);
t.is(presentFile.status, 200);
});
test('ignore files specified in .nowignore via allowlist', async t => {
const directory = fixture('nowignore-allowlist');
const args = [
'--debug',
'--public',
'--name',
session,
...defaultArgs,
'--confirm',
];
const targetCall = await execa(binaryPath, args, {
cwd: directory,
reject: false,
});
console.log(targetCall.stderr);
console.log(targetCall.stdout);
console.log(targetCall.exitCode);
const { host } = new URL(targetCall.stdout);
const ignoredFile = await fetch(`https://${host}/ignored.txt`);
t.is(ignoredFile.status, 404);
const presentFile = await fetch(`https://${host}/index.txt`);
t.is(presentFile.status, 200);
});
test('list the scopes', async t => {
const { stdout, stderr, exitCode } = await execa(
binaryPath,
['teams', 'ls', ...defaultArgs],
{
reject: false,
}
);
console.log(stderr);
console.log(stdout);
console.log(exitCode);
t.is(exitCode, 0);
const include = new RegExp(`${contextName}\\s+${email}`);
t.true(
include.test(stdout),
`Expected: ${include}\n\nReceived instead:\n${stdout}\n${stderr}`
);
});
test('list the payment methods', async t => {
const { stdout, stderr, exitCode } = await execa(
binaryPath,
['billing', 'ls', ...defaultArgs],
{
reject: false,
}
);
console.log(stderr);
console.log(stdout);
console.log(exitCode);
t.is(exitCode, 0);
t.true(stdout.startsWith(`> 0 cards found under ${contextName}`));
});
test('domains inspect', async t => {
const domainName = `inspect-${contextName}-${Math.random()
.toString()
.slice(2, 8)}.org`;
const directory = fixture('static-multiple-files');
const projectName = Math.random().toString().slice(2);
const output = await execute([
directory,
`-V`,
`2`,
`--name=${projectName}`,
'--confirm',
'--public',
]);
t.is(output.exitCode, 0, formatOutput(output));
{
// Add a domain that can be inspected
const result = await execa(
binaryPath,
[`domains`, `add`, domainName, projectName, ...defaultArgs],
{ reject: false }
);
t.is(result.exitCode, 0, formatOutput(result));
}
const { stderr, stdout, exitCode } = await execa(
binaryPath,
['domains', 'inspect', domainName, ...defaultArgs],
{
reject: false,
}
);
t.true(stderr.includes(`Renewal Price`));
t.is(exitCode, 0, formatOutput({ stdout, stderr }));
{
// Remove the domain again
const result = await execa(
binaryPath,
[`domains`, `rm`, domainName, ...defaultArgs],
{ reject: false, input: 'y' }
);
t.is(result.exitCode, 0, formatOutput(result));
}
});
test('try to purchase a domain', async t => {
if (process.env.VERCEL_TOKEN || process.env.NOW_TOKEN) {
console.log(
'Skipping test `try to purchase a domain` because a personal VERCEL_TOKEN was provided.'
);
t.pass();
return;
}
const stream = new Readable();
stream._read = () => {};
setTimeout(async () => {
await sleep(ms('1s'));
stream.push('y');
await sleep(ms('1s'));
stream.push('y');
stream.push(null);
}, ms('1s'));
const { stderr, stdout, exitCode } = await execa(
binaryPath,
['domains', 'buy', `${session}-test.org`, ...defaultArgs],
{
reject: false,
input: stream,
}
);
console.log(stderr);
console.log(stdout);
console.log(exitCode);
t.is(exitCode, 1);
t.true(
stderr.includes(
`Error! Could not purchase domain. Please add a payment method using \`vercel billing add\`.`
)
);
});
test('try to transfer-in a domain with "--code" option', async t => {
const { stderr, stdout, exitCode } = await execa(
binaryPath,
[
'domains',
'transfer-in',
'--code',
'xyz',
`${session}-test.org`,
...defaultArgs,
],
{
reject: false,
}
);
console.log(stderr);
console.log(stdout);
console.log(exitCode);
t.true(
stderr.includes(
`Error! The domain "${session}-test.org" is not transferable.`
)
);
t.is(exitCode, 1);
});
test('try to move an invalid domain', async t => {
const { stderr, stdout, exitCode } = await execa(
binaryPath,
[
'domains',
'move',
`${session}-invalid-test.org`,
`${session}-invalid-user`,
...defaultArgs,
],
{
reject: false,
}
);
console.log(stderr);
console.log(stdout);
console.log(exitCode);
t.true(stderr.includes(`Error! Domain not found under `));
t.is(exitCode, 1);
});
test('try to set default without existing payment method', async t => {
const { stderr, stdout, exitCode } = await execa(
binaryPath,
['billing', 'set-default', ...defaultArgs],
{
reject: false,
}
);
console.log(stderr);
console.log(stdout);
console.log(exitCode);
t.is(exitCode, 0);
t.true(stderr.includes('You have no credit cards to choose from'));
});
test('try to remove a non-existing payment method', async t => {
const { stderr, stdout, exitCode } = await execa(
binaryPath,
['billing', 'rm', 'card_d2j32d9382jr928rd', ...defaultArgs],
{
reject: false,
}
);
console.log(stderr);
console.log(stdout);
console.log(exitCode);
t.is(exitCode, 0);
t.true(
stderr.includes(
`You have no credit cards to choose from to delete under ${contextName}`
)
);
});
/*
* Disabled 2 tests because these temp users don't have certs
test('create wildcard alias for deployment', async t => {
const hosts = {
deployment: context.deployment,
alias: `*.${contextName}.now.sh`,
};
const { stdout, stderr, exitCode } = await execa(
binaryPath,
['alias', hosts.deployment, hosts.alias, ...defaultArgs],
{
reject: false,
}
);
console.log(stderr);
console.log(stdout);
console.log(exitCode);
const goal = `> Success! ${hosts.alias} now points to https://${hosts.deployment}`;
t.is(exitCode, 0);
t.true(stdout.startsWith(goal));
// Send a test request to the alias
// Retries to make sure we consider the time it takes to update
const response = await retry(
async () => {
const response = await fetch(`https://test.${contextName}.now.sh`);
if (response.ok) {
return response;
}
throw new Error(`Error: Returned code ${response.status}`);
},
{ retries: 3 }
);
const content = await response.text();
t.true(response.ok);
t.true(content.includes(contextName));
context.wildcardAlias = hosts.alias;
});
test('remove the wildcard alias', async t => {
const goal = `> Success! Alias ${context.wildcardAlias} removed`;
const { stdout, stderr, exitCode } = await execa(
binaryPath,
['alias', 'rm', context.wildcardAlias, '--yes', ...defaultArgs],
{
reject: false,
}
);
console.log(stderr);
console.log(stdout);
console.log(exitCode);
t.is(exitCode, 0);
t.true(stdout.startsWith(goal));
});
*/
test('ensure username in list is right', async t => {
const { stdout, stderr, exitCode } = await execa(
binaryPath,
['ls', ...defaultArgs],
{
reject: false,
}
);
console.log(stderr);
console.log(stdout);
console.log(exitCode);
// Ensure the exit code is right
t.is(exitCode, 0);
const line = stdout
.split('\n')
.find(line => line.includes('.now.sh') || line.includes('.vercel.app'));
const columns = line.split(/\s+/);
// Ensure username column have username
t.truthy(columns.pop().includes(contextName));
});
test('ensure we render a warning for deployments with no files', async t => {
const directory = fixture('empty-directory');
const { stderr, stdout, exitCode } = await execa(
binaryPath,
[
directory,
'--public',
'--name',
session,
...defaultArgs,
'--confirm',
'--force',
],
{
reject: false,
}
);
console.log(stderr);
console.log(stdout);
console.log(exitCode);
// Ensure the warning is printed
t.regex(stderr, /There are no files inside your deployment/);
// Test if the output is really a URL
const { href, host } = new URL(stdout);
t.is(host.split('-')[0], session);
if (host) {
context.deployment = host;
}
// Ensure the exit code is right
t.is(exitCode, 0);
// Send a test request to the deployment
const res = await fetch(href);
t.is(res.status, 404);
});
test('output logs with "short" output', async t => {
const { stderr, stdout, exitCode } = await execa(
binaryPath,
['logs', context.deployment, ...defaultArgs],
{
reject: false,
}
);
console.log(stderr);
console.log(stdout);
console.log(exitCode);
t.true(
stderr.includes(`Fetched deployment "${context.deployment}"`),
formatOutput({ stderr, stdout })
);
// "short" format includes timestamps
t.truthy(
stdout.match(
/\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z)/
),
formatOutput({ stderr, stdout })
);
t.is(exitCode, 0);
});
test('output logs with "raw" output', async t => {
const { stderr, stdout, exitCode } = await execa(
binaryPath,
['logs', context.deployment, ...defaultArgs, '--output', 'raw'],
{
reject: false,
}
);
console.log(stderr);
console.log(stdout);
console.log(exitCode);
t.true(
stderr.includes(`Fetched deployment "${context.deployment}"`),
formatOutput({ stderr, stdout })
);
// "raw" format does not include timestamps
t.is(
null,
stdout.match(
/\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z)/
)
);
t.is(exitCode, 0);
});
test('ensure we render a prompt when deploying home directory', async t => {
const directory = homedir();
const { stderr, stdout, exitCode } = await execa(
binaryPath,
[directory, '--public', '--name', session, ...defaultArgs, '--force'],
{
reject: false,
input: 'N',
}
);
console.log(stderr);
console.log(stdout);
console.log(exitCode);
// Ensure the exit code is right
t.is(exitCode, 0);
t.true(
stdout.includes(
'You are deploying your home directory. Do you want to continue? [y/N]'
)
);
t.true(stderr.includes('Aborted'));
});
test('ensure the `scope` property works with email', async t => {
const directory = fixture('config-scope-property-email');
const { stderr, stdout, exitCode } = await execa(
binaryPath,
[
directory,
'--public',
'--name',
session,
...defaultArgs,
'--force',
'--confirm',
],
{
reject: false,
}
);
console.log(stderr);
console.log(stdout);
console.log(exitCode);
// Ensure we're deploying under the right scope
t.true(stderr.includes(session));
// Ensure the exit code is right
t.is(exitCode, 0);
// Test if the output is really a URL
const { href, host } = new URL(stdout);
t.is(host.split('-')[0], session);
// Send a test request to the deployment
const response = await fetch(href);
const contentType = response.headers.get('content-type');
t.is(contentType, 'text/html; charset=utf-8');
});
test('ensure the `scope` property works with username', async t => {
const directory = fixture('config-scope-property-username');
const { stderr, stdout, exitCode } = await execa(
binaryPath,
[
directory,
'--public',
'--name',
session,
...defaultArgs,
'--force',
'--confirm',
],
{
reject: false,
}
);
console.log(stderr);
console.log(stdout);
console.log(exitCode);
// Ensure we're deploying under the right scope
t.true(stderr.includes(contextName));
// Ensure the exit code is right
t.is(exitCode, 0);
// Test if the output is really a URL
const { href, host } = new URL(stdout);
t.is(host.split('-')[0], session);
// Send a test request to the deployment
const response = await fetch(href);
const contentType = response.headers.get('content-type');
t.is(contentType, 'text/html; charset=utf-8');
});
test('try to create a builds deployments with wrong now.json', async t => {
const directory = fixture('builds-wrong');
const { stderr, stdout, exitCode } = await execa(
binaryPath,
[directory, '--public', ...defaultArgs, '--confirm'],
{
reject: false,
}
);
console.log(stderr);
console.log(stdout);
console.log(exitCode);
// Ensure the exit code is right
t.is(exitCode, 1);
t.true(
stderr.includes(
'Error! Invalid now.json - should NOT have additional property `builder`. Did you mean `builds`?'
)
);
t.true(stderr.includes('https://vercel.com/docs/configuration'));
});
test('try to create a builds deployments with wrong vercel.json', async t => {
const directory = fixture('builds-wrong-vercel');
const { stderr, stdout, exitCode } = await execa(
binaryPath,
[directory, '--public', ...defaultArgs, '--confirm'],
{
reject: false,
}
);
console.log(stderr);
console.log(stdout);
console.log(exitCode);
t.is(exitCode, 1);
t.true(
stderr.includes(
'Error! Invalid vercel.json - should NOT have additional property `fake`. Please remove it.'
)
);
t.true(stderr.includes('https://vercel.com/docs/configuration'));
});
test('try to create a builds deployments with wrong `build.env` property', async t => {
const directory = fixture('builds-wrong-build-env');
const { stderr, stdout, exitCode } = await execa(
binaryPath,
['--public', ...defaultArgs, '--confirm'],
{
cwd: directory,
reject: false,
}
);
t.is(exitCode, 1, formatOutput({ stdout, stderr }));
t.true(
stderr.includes(
'Error! Invalid vercel.json - should NOT have additional property `build.env`. Did you mean `{ "build": { "env": {"name": "value"} } }`?'
),
formatOutput({ stdout, stderr })
);
t.true(
stderr.includes('https://vercel.com/docs/configuration'),
formatOutput({ stdout, stderr })
);
});
test('create a builds deployments with no actual builds', async t => {
const directory = fixture('builds-no-list');
const { stdout, stderr, exitCode } = await execa(
binaryPath,
[
directory,
'--public',
'--name',
session,
...defaultArgs,
'--force',
'--confirm',
],
{
reject: false,
}
);
console.log(stderr);
console.log(stdout);
console.log(exitCode);
// Ensure the exit code is right
t.is(exitCode, 0);
// Test if the output is really a URL
const { host } = new URL(stdout);
t.is(host.split('-')[0], session);
});
test('create a staging deployment', async t => {
const directory = fixture('static-deployment');
const args = ['--debug', '--public', '--name', session, ...defaultArgs];
const targetCall = await execa(binaryPath, [
directory,
'--target=staging',
...args,
'--confirm',
]);
console.log(targetCall.stderr);
console.log(targetCall.stdout);
console.log(targetCall.exitCode);
t.regex(
targetCall.stderr,
/Setting target to staging/gm,
formatOutput(targetCall)
);
t.regex(targetCall.stdout, /https:\/\//gm);
t.is(targetCall.exitCode, 0, formatOutput(targetCall));
const { host } = new URL(targetCall.stdout);
const deployment = await apiFetch(
`/v10/now/deployments/unknown?url=${host}`
).then(resp => resp.json());
t.is(deployment.target, 'staging', JSON.stringify(deployment, null, 2));
});
test('create a production deployment', async t => {
const directory = fixture('static-deployment');
const args = ['--debug', '--public', '--name', session, ...defaultArgs];
const targetCall = await execa(binaryPath, [
directory,
'--target=production',
...args,
'--confirm',
]);
console.log(targetCall.stderr);
console.log(targetCall.stdout);
console.log(targetCall.exitCode);
t.is(targetCall.exitCode, 0, formatOutput(targetCall));
t.regex(
targetCall.stderr,
/`--prod` option instead/gm,
formatOutput(targetCall)
);
t.regex(
targetCall.stderr,
/Setting target to production/gm,
formatOutput(targetCall)
);
t.regex(
targetCall.stderr,
/Inspect: https:\/\/vercel.com\//gm,
formatOutput(targetCall)
);
t.regex(targetCall.stdout, /https:\/\//gm);
const { host: targetHost } = new URL(targetCall.stdout);
const targetDeployment = await apiFetch(
`/v10/now/deployments/unknown?url=${targetHost}`
).then(resp => resp.json());
t.is(
targetDeployment.target,
'production',
JSON.stringify(targetDeployment, null, 2)
);
const call = await execa(binaryPath, [directory, '--prod', ...args]);
console.log(call.stderr);
console.log(call.stdout);
console.log(call.exitCode);
t.is(call.exitCode, 0, formatOutput(call));
t.regex(
call.stderr,
/Setting target to production/gm,
formatOutput(targetCall)
);
t.regex(call.stdout, /https:\/\//gm);
const { host } = new URL(call.stdout);
const deployment = await apiFetch(
`/v10/now/deployments/unknown?url=${host}`
).then(resp => resp.json());
t.is(deployment.target, 'production', JSON.stringify(deployment, null, 2));
});
test('use build-env', async t => {
const directory = fixture('build-env');
const { stdout, stderr, exitCode } = await execa(
binaryPath,
[directory, '--public', ...defaultArgs, '--confirm'],
{
reject: false,
}
);
// Ensure the exit code is right
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
// Test if the output is really a URL
const deploymentUrl = pickUrl(stdout);
const { href } = new URL(deploymentUrl);
await waitForDeployment(href);
// get the content
const response = await fetch(href);
const content = await response.text();
t.is(content.trim(), 'bar');
});
test('use `--debug` CLI flag', async t => {
const directory = fixture('build-env-debug');
const { stderr, stdout, exitCode } = await execa(
binaryPath,
[
directory,
'--public',
'--name',
session,
'--debug',
...defaultArgs,
'--confirm',
],
{
reject: false,
}
);
console.log(stderr);
console.log(stdout);
console.log(exitCode);
// Ensure the exit code is right
t.is(exitCode, 0, `Received:\n"${stderr}"\n"${stdout}"`);
// Test if the output is really a URL
const deploymentUrl = pickUrl(stdout);
const { href, host } = new URL(deploymentUrl);
t.is(host.split('-')[0], session);
await waitForDeployment(href);
// get the content
const response = await fetch(href);
const content = await response.text();
t.is(content.trim(), 'off');
});
test('try to deploy non-existing path', async t => {
const goal = `Error! The specified file or directory "${session}" does not exist.`;
const { stderr, stdout, exitCode } = await execa(
binaryPath,
[session, ...defaultArgs, '--confirm'],
{
reject: false,
}
);
console.log(stderr);
console.log(stdout);
console.log(exitCode);
t.is(exitCode, 1);
t.true(stderr.trim().endsWith(goal));
});
test('try to deploy with non-existing team', async t => {
const target = fixture('static-deployment');
const goal = `Error! The specified scope does not exist`;
const { stderr, stdout, exitCode } = await execa(
binaryPath,
[target, '--scope', session, ...defaultArgs, '--confirm'],
{
reject: false,
}
);
console.log(stderr);
console.log(stdout);
console.log(exitCode);
t.is(exitCode, 1);
t.true(stderr.includes(goal));
});
const verifyExampleAngular = (cwd, dir) =>
fs.existsSync(path.join(cwd, dir, 'package.json')) &&
fs.existsSync(path.join(cwd, dir, 'tsconfig.json')) &&
fs.existsSync(path.join(cwd, dir, 'angular.json'));
const verifyExampleAmp = (cwd, dir) =>
fs.existsSync(path.join(cwd, dir, 'index.html')) &&
fs.existsSync(path.join(cwd, dir, 'logo.png')) &&
fs.existsSync(path.join(cwd, dir, 'favicon.png'));
test('initialize example "angular"', async t => {
tmpDir = tmp.dirSync({ unsafeCleanup: true });
const cwd = tmpDir.name;
const goal = '> Success! Initialized "angular" example in';
const { stdout, stderr, exitCode } = await execute(['init', 'angular'], {
cwd,
});
t.is(exitCode, 0, formatOutput({ stdout, stderr }));
t.true(stderr.includes(goal), formatOutput({ stdout, stderr }));
t.true(
verifyExampleAngular(cwd, 'angular'),
formatOutput({ stdout, stderr })
);
});
test('initialize example ("angular") to specified directory', async t => {
tmpDir = tmp.dirSync({ unsafeCleanup: true });
const cwd = tmpDir.name;
const goal = '> Success! Initialized "angular" example in';
const { stdout, stderr, exitCode } = await execute(
['init', 'angular', 'ang'],
{
cwd,
}
);
t.is(exitCode, 0, formatOutput({ stdout, stderr }));
t.true(stderr.includes(goal), formatOutput({ stdout, stderr }));
t.true(verifyExampleAngular(cwd, 'ang'), formatOutput({ stdout, stderr }));
});
test('initialize selected example ("amp")', async t => {
tmpDir = tmp.dirSync({ unsafeCleanup: true });
const cwd = tmpDir.name;
const goal = '> Success! Initialized "amp" example in';
const { stdout, stderr, exitCode } = await execute(['init'], {
cwd,
input: '\n',
});
t.is(exitCode, 0, formatOutput({ stdout, stderr }));
t.true(stderr.includes(goal), formatOutput({ stdout, stderr }));
t.true(verifyExampleAmp(cwd, 'amp'), formatOutput({ stdout, stderr }));
});
test('initialize example to existing directory with "-f"', async t => {
tmpDir = tmp.dirSync({ unsafeCleanup: true });
const cwd = tmpDir.name;
const goal = '> Success! Initialized "angular" example in';
await ensureDir(path.join(cwd, 'angular'));
createFile(path.join(cwd, 'angular', '.gitignore'));
const { stdout, stderr, exitCode } = await execute(
['init', 'angular', '-f'],
{
cwd,
}
);
t.is(exitCode, 0, formatOutput({ stdout, stderr }));
t.true(stderr.includes(goal), formatOutput({ stdout, stderr }));
t.true(
verifyExampleAngular(cwd, 'angular'),
formatOutput({ stdout, stderr })
);
});
test('try to initialize example to existing directory', async t => {
tmpDir = tmp.dirSync({ unsafeCleanup: true });
const cwd = tmpDir.name;
const goal =
'Error! Destination path "angular" already exists and is not an empty directory. You may use `--force` or `-f` to override it.';
await ensureDir(path.join(cwd, 'angular'));
createFile(path.join(cwd, 'angular', '.gitignore'));
const { stdout, stderr, exitCode } = await execute(['init', 'angular'], {
cwd,
input: '\n',
});
t.is(exitCode, 1, formatOutput({ stdout, stderr }));
t.true(stderr.includes(goal), formatOutput({ stdout, stderr }));
});
test('try to initialize misspelled example (noce) in non-tty', async t => {
tmpDir = tmp.dirSync({ unsafeCleanup: true });
const cwd = tmpDir.name;
const goal =
'Error! No example found for noce, run `vercel init` to see the list of available examples.';
const { stdout, stderr, exitCode } = await execute(['init', 'noce'], { cwd });
console.log(stderr);
console.log(stdout);
console.log(exitCode);
t.is(exitCode, 1, formatOutput({ stdout, stderr }));
t.true(stderr.includes(goal), formatOutput({ stdout, stderr }));
});
test('try to initialize example "example-404"', async t => {
tmpDir = tmp.dirSync({ unsafeCleanup: true });
const cwd = tmpDir.name;
const goal =
'Error! No example found for example-404, run `vercel init` to see the list of available examples.';
const { stdout, stderr, exitCode } = await execute(['init', 'example-404'], {
cwd,
});
t.is(exitCode, 1, formatOutput({ stdout, stderr }));
t.true(stderr.includes(goal), formatOutput({ stdout, stderr }));
});
test('try to revert a deployment and assign the automatic aliases', async t => {
const firstDeployment = fixture('now-revert-alias-1');
const secondDeployment = fixture('now-revert-alias-2');
const { name } = JSON.parse(
fs.readFileSync(path.join(firstDeployment, 'now.json'))
);
const url = `https://${name}.user.vercel.app`;
{
const {
stdout: deploymentUrl,
stderr,
exitCode,
} = await execute([firstDeployment, '--confirm']);
t.is(exitCode, 0, formatOutput({ stderr, stdout: deploymentUrl }));
await waitForDeployment(deploymentUrl);
await sleep(20000);
const result = await fetch(url).then(r => r.json());
t.is(
result.name,
'now-revert-alias-1',
`[First run] Received ${result.name} instead on ${url} (${deploymentUrl})`
);
}
{
const {
stdout: deploymentUrl,
stderr,
exitCode,
} = await execute([secondDeployment, '--confirm']);
t.is(exitCode, 0, formatOutput({ stderr, stdout: deploymentUrl }));
await waitForDeployment(deploymentUrl);
await sleep(20000);
await fetch(url);
await sleep(5000);
const result = await fetch(url).then(r => r.json());
t.is(
result.name,
'now-revert-alias-2',
`[Second run] Received ${result.name} instead on ${url} (${deploymentUrl})`
);
}
{
const {
stdout: deploymentUrl,
stderr,
exitCode,
} = await execute([firstDeployment, '--confirm']);
t.is(exitCode, 0, formatOutput({ stderr, stdout: deploymentUrl }));
await waitForDeployment(deploymentUrl);
await sleep(20000);
await fetch(url);
await sleep(5000);
const result = await fetch(url).then(r => r.json());
t.is(
result.name,
'now-revert-alias-1',
`[Third run] Received ${result.name} instead on ${url} (${deploymentUrl})`
);
}
});
test('whoami', async t => {
const { exitCode, stdout, stderr } = await execute(['whoami']);
console.log(stderr);
console.log(stdout);
console.log(exitCode);
t.is(exitCode, 0);
t.is(stdout, contextName, formatOutput({ stdout, stderr }));
});
test('[vercel dev] fails when dev script calls vercel dev recursively', async t => {
const deploymentPath = fixture('now-dev-fail-dev-script');
const { exitCode, stderr } = await execute(['dev', deploymentPath]);
t.is(exitCode, 1);
t.true(
stderr.includes('must not recursively invoke itself'),
`Received instead: "${stderr}"`
);
});
test('[vercel dev] fails when development commad calls vercel dev recursively', async t => {
const dir = fixture('dev-fail-on-recursion-command');
const projectName = `dev-fail-on-recursion-command-${
Math.random().toString(36).split('.')[1]
}`;
const dev = execa(binaryPath, ['dev', ...defaultArgs], {
cwd: dir,
reject: false,
});
await setupProject(dev, projectName, {
devCommand: `${binaryPath} dev`,
});
const { exitCode, stderr } = await dev;
t.is(exitCode, 1);
t.true(
stderr.includes('must not recursively invoke itself'),
`Received instead: "${stderr}"`
);
});
test('[vercel build] fails when build commad calls vercel build recursively', async t => {
const dir = fixture('build-fail-on-recursion-command');
const projectName = `build-fail-on-recursion-command-${
Math.random().toString(36).split('.')[1]
}`;
const build = execa(binaryPath, ['build', ...defaultArgs], {
cwd: dir,
reject: false,
});
await waitForPrompt(build, chunk =>
chunk.includes('No Project Settings found locally')
);
build.stdin.write('yes\n');
await setupProject(build, projectName, {
buildCommand: `${binaryPath} build`,
});
const { exitCode, stderr } = await build;
t.is(exitCode, 1);
t.true(
stderr.includes('must not recursively invoke itself'),
`Received instead: "${stderr}"`
);
});
test('[vercel build] fails when build script calls vercel build recursively', async t => {
const dir = fixture('build-fail-on-recursion-script');
const projectName = `build-fail-on-recursion-script-${
Math.random().toString(36).split('.')[1]
}`;
const build = execa(binaryPath, ['build', ...defaultArgs], {
cwd: dir,
reject: false,
});
await waitForPrompt(build, chunk =>
chunk.includes('No Project Settings found locally')
);
build.stdin.write('yes\n');
await setupProject(build, projectName);
const { exitCode, stderr } = await build;
t.is(exitCode, 1);
t.true(
stderr.includes('must not recursively invoke itself'),
`Received instead: "${stderr}"`
);
});
test('`vercel rm` removes a deployment', async t => {
const directory = fixture('static-deployment');
const { stdout } = await execa(
binaryPath,
[
directory,
'--public',
'--name',
session,
...defaultArgs,
'-V',
2,
'--force',
'--confirm',
],
{
reject: false,
}
);
const { host } = new URL(stdout);
const { exitCode, stdout: stdoutRemove } = await execute([
'rm',
host,
'--yes',
]);
t.truthy(stdoutRemove.includes(host));
t.is(exitCode, 0);
});
test('`vercel rm` should fail with unexpected option', async t => {
const output = await execute(['rm', 'example.example.com', '--fake']);
t.is(output.exitCode, 1, formatOutput(output));
t.regex(
output.stderr,
/Error! unknown or unexpected option: --fake/gm,
formatOutput(output)
);
});
test('`vercel rm` 404 exits quickly', async t => {
const start = Date.now();
const { exitCode, stderr, stdout } = await execute([
'rm',
'this.is.a.deployment.that.does.not.exist.example.com',
]);
console.log(stderr);
console.log(stdout);
console.log(exitCode);
const delta = Date.now() - start;
// "does not exist" case is exit code 1, similar to Unix `rm`
t.is(exitCode, 1);
t.truthy(
stderr.includes(
'Could not find any deployments or projects matching "this.is.a.deployment.that.does.not.exist.example.com"'
)
);
// "quickly" meaning < 5 seconds, because it used to hang from a previous bug
t.truthy(delta < 5000);
});
test('render build errors', async t => {
const deploymentPath = fixture('failing-build');
const output = await execute([deploymentPath, '--confirm']);
console.log(output.stderr);
console.log(output.stdout);
console.log(output.exitCode);
t.is(output.exitCode, 1, formatOutput(output));
t.regex(
output.stderr,
/Command "yarn run build" exited with 1/gm,
formatOutput(output)
);
});
test('invalid deployment, projects and alias names', async t => {
const check = async (...args) => {
const output = await execute(args);
console.log(output.stderr);
console.log(output.stdout);
console.log(output.exitCode);
const print = `\`${args.join(' ')}\`\n${formatOutput(output)}`;
t.is(output.exitCode, 1, print);
t.regex(output.stderr, /The provided argument/gm, print);
};
await Promise.all([
check('alias', '/', 'test'),
check('alias', 'test', '/'),
check('rm', '/'),
check('ls', '/'),
]);
});
test('vercel certs ls', async t => {
const output = await execute(['certs', 'ls']);
console.log(output.stderr);
console.log(output.stdout);
console.log(output.exitCode);
t.is(output.exitCode, 0, formatOutput(output));
t.regex(output.stderr, /certificates? found under/gm, formatOutput(output));
});
test('vercel certs ls --next=123456', async t => {
const output = await execute(['certs', 'ls', '--next=123456']);
console.log(output.stderr);
console.log(output.stdout);
console.log(output.exitCode);
t.is(output.exitCode, 0, formatOutput(output));
t.regex(output.stderr, /No certificates found under/gm, formatOutput(output));
});
test('vercel hasOwnProperty not a valid subcommand', async t => {
const output = await execute(['hasOwnProperty']);
console.log(output.stderr);
console.log(output.stdout);
console.log(output.exitCode);
t.is(output.exitCode, 1, formatOutput(output));
t.regex(
output.stderr,
/The specified file or directory "hasOwnProperty" does not exist/gm,
formatOutput(output)
);
});
test('create zero-config deployment', async t => {
const fixturePath = fixture('zero-config-next-js');
const output = await execute([
fixturePath,
'--force',
'--public',
'--confirm',
]);
console.log('isCanary', isCanary);
console.log(output.stderr);
console.log(output.stdout);
console.log(output.exitCode);
t.is(output.exitCode, 0, formatOutput(output));
const { host } = new URL(output.stdout);
const response = await apiFetch(`/v10/now/deployments/unkown?url=${host}`);
const text = await response.text();
t.is(response.status, 200, text);
const data = JSON.parse(text);
t.is(data.error, undefined, JSON.stringify(data, null, 2));
const validBuilders = data.builds.every(build =>
isCanary ? build.use.endsWith('@canary') : !build.use.endsWith('@canary')
);
t.true(
validBuilders,
'Builders are not valid: ' + JSON.stringify(data, null, 2)
);
});
test('next unsupported functions config shows warning link', async t => {
const fixturePath = fixture('zero-config-next-js-functions-warning');
const output = await execute([
fixturePath,
'--force',
'--public',
'--confirm',
]);
console.log('isCanary', isCanary);
console.log(output.stderr);
console.log(output.stdout);
console.log(output.exitCode);
t.is(output.exitCode, 0, formatOutput(output));
t.regex(
output.stderr,
/Ignoring function property `runtime`\. When using Next\.js, only `memory` and `maxDuration` can be used\./gm,
formatOutput(output)
);
t.regex(
output.stderr,
/Learn More: https:\/\/vercel\.link\/functions-property-next/gm,
formatOutput(output)
);
});
test('vercel secret add', async t => {
context.secretName = `my-secret-${Date.now().toString(36)}`;
const value = 'https://my-secret-endpoint.com';
const output = await execute(['secret', 'add', context.secretName, value]);
console.log(output.stderr);
console.log(output.stdout);
console.log(output.exitCode);
t.is(output.exitCode, 0, formatOutput(output));
});
test('vercel secret ls', async t => {
const output = await execute(['secret', 'ls']);
console.log(output.stderr);
console.log(output.stdout);
console.log(output.exitCode);
t.is(output.exitCode, 0, formatOutput(output));
t.regex(output.stdout, /Secrets found under/gm, formatOutput(output));
t.regex(output.stdout, new RegExp(), formatOutput(output));
});
test('vercel secret ls --test-warning', async t => {
const output = await execute(['secret', 'ls', '--test-warning']);
t.is(output.exitCode, 0, formatOutput(output));
t.regex(output.stderr, /Test warning message./gm, formatOutput(output));
t.regex(
output.stderr,
/Learn more: https:\/\/vercel.com/gm,
formatOutput(output)
);
t.regex(output.stdout, /No secrets found under/gm, formatOutput(output));
});
test('vercel secret rename', async t => {
const nextName = `renamed-secret-${Date.now().toString(36)}`;
const output = await execute([
'secret',
'rename',
context.secretName,
nextName,
]);
console.log(output.stderr);
console.log(output.stdout);
console.log(output.exitCode);
t.is(output.exitCode, 0, formatOutput(output));
context.secretName = nextName;
});
test('vercel secret rm', async t => {
const output = await execute(['secret', 'rm', context.secretName, '-y']);
console.log(output.stderr);
console.log(output.stdout);
console.log(output.exitCode);
t.is(output.exitCode, 0, formatOutput(output));
});
test('deploy a Lambda with 128MB of memory', async t => {
const directory = fixture('lambda-with-128-memory');
const output = await execute([directory, '--confirm']);
t.is(output.exitCode, 0, formatOutput(output));
const { host: url } = new URL(output.stdout);
const response = await fetch('https://' + url + '/api/memory');
t.is(response.status, 200, url);
// It won't be exactly 128MB,
// so we just compare if it is lower than 450MB
const { memory } = await response.json();
t.is(memory, 128, `Lambda has ${memory} bytes of memory`);
});
test('fail to deploy a Lambda with an incorrect value for of memory', async t => {
const directory = fixture('lambda-with-200-memory');
const output = await execute([directory, '--confirm']);
t.is(output.exitCode, 1, formatOutput(output));
t.regex(output.stderr, /steps of 64/gm, formatOutput(output));
t.regex(output.stderr, /Learn More/gm, formatOutput(output));
});
test('deploy a Lambda with 3 seconds of maxDuration', async t => {
const directory = fixture('lambda-with-3-second-timeout');
const output = await execute([directory, '--confirm']);
t.is(output.exitCode, 0, formatOutput(output));
const url = new URL(output.stdout);
// Should time out
url.pathname = '/api/wait-for/5';
const response1 = await fetch(url.href);
t.is(
response1.status,
504,
`Expected 504 status, got ${response1.status}: ${url}`
);
// Should not time out
url.pathname = '/api/wait-for/1';
const response2 = await fetch(url.href);
t.is(
response2.status,
200,
`Expected 200 status, got ${response1.status}: ${url}`
);
});
test('fail to deploy a Lambda with an incorrect value for maxDuration', async t => {
const directory = fixture('lambda-with-1000-second-timeout');
const output = await execute([directory, '--confirm']);
t.is(output.exitCode, 1, formatOutput(output));
t.regex(
output.stderr,
/maxDuration must be between 1 second and 10 seconds/gm,
formatOutput(output)
);
});
test('invalid `--token`', async t => {
const output = await execute(['--token', 'he\nl,o.']);
t.is(output.exitCode, 1, formatOutput(output));
t.true(
output.stderr.includes(
'Error! You defined "--token", but its contents are invalid. Must not contain: "\\n", ",", "."'
)
);
});
test('deploy a Lambda with a specific runtime', async t => {
const directory = fixture('lambda-with-php-runtime');
const output = await execute([directory, '--public', '--confirm']);
t.is(output.exitCode, 0, formatOutput(output));
const { host: url } = new URL(output.stdout);
const builds = await getDeploymentBuildsByUrl(url);
const build = builds.find(b => b.use && b.use.includes('php')) || builds[0];
t.is(build.use, 'vercel-php@0.1.0', JSON.stringify(build, null, 2));
});
test('fail to deploy a Lambda with a specific runtime but without a locked version', async t => {
const directory = fixture('lambda-with-invalid-runtime');
const output = await execute([directory, '--confirm']);
t.is(output.exitCode, 1, formatOutput(output));
t.regex(
output.stderr,
/Function Runtimes must have a valid version/gim,
formatOutput(output)
);
});
test('fail to add a domain without a project', async t => {
const output = await execute(['domains', 'add', 'my-domain.vercel.app']);
t.is(output.exitCode, 1, formatOutput(output));
t.regex(output.stderr, /expects two arguments/gm, formatOutput(output));
});
test('change user', async t => {
t.timeout(ms('1m'));
const { stdout: prevUser } = await execute(['whoami']);
// Delete the current token
await execute(['logout', '--debug'], { stdio: 'inherit' });
await createUser();
await execute(['login', email, '--api', loginApiUrl, '--debug'], {
stdio: 'inherit',
});
const auth = await fs.readJSON(getConfigAuthPath());
t.is(auth.token, token);
const { stdout: nextUser } = await execute(['whoami']);
console.log('prev user', prevUser);
console.log('next user', nextUser);
t.is(typeof prevUser, 'string', prevUser);
t.is(typeof nextUser, 'string', nextUser);
t.not(prevUser, nextUser, JSON.stringify({ prevUser, nextUser }));
});
test('assign a domain to a project', async t => {
const domain = `project-domain.${contextName}.vercel.app`;
const directory = fixture('static-deployment');
const deploymentOutput = await execute([directory, '--public', '--confirm']);
t.is(deploymentOutput.exitCode, 0, formatOutput(deploymentOutput));
const host = deploymentOutput.stdout.trim().replace('https://', '');
const deployment = await apiFetch(
`/v10/now/deployments/unknown?url=${host}`
).then(resp => resp.json());
t.is(typeof deployment.name, 'string', JSON.stringify(deployment, null, 2));
const project = deployment.name;
const output = await execute(['domains', 'add', domain, project, '--force']);
t.is(output.exitCode, 0, formatOutput(output));
const removeResponse = await execute(['rm', project, '-y']);
t.is(removeResponse.exitCode, 0, formatOutput(removeResponse));
});
test('ensure `github` and `scope` are not sent to the API', async t => {
const directory = fixture('github-and-scope-config');
const output = await execute([directory, '--confirm']);
t.is(output.exitCode, 0, formatOutput(output));
});
test('should show prompts to set up project during first deploy', async t => {
const dir = fixture('project-link-deploy');
const projectName = `project-link-deploy-${
Math.random().toString(36).split('.')[1]
}`;
// remove previously linked project if it exists
await remove(path.join(dir, '.vercel'));
const now = execa(binaryPath, [dir, ...defaultArgs]);
await setupProject(now, projectName, {
buildCommand: `mkdir -p o && echo '<h1>custom hello</h1>' > o/index.html`,
outputDirectory: 'o',
});
const output = await now;
// Ensure the exit code is right
t.is(output.exitCode, 0, formatOutput(output));
// Ensure .gitignore is created
const gitignore = await readFile(path.join(dir, '.gitignore'), 'utf8');
t.is(gitignore, '.vercel\n.output\n');
// Ensure .vercel/project.json and .vercel/README.txt are created
t.is(
await exists(path.join(dir, '.vercel', 'project.json')),
true,
'project.json should be created'
);
t.is(
await exists(path.join(dir, '.vercel', 'README.txt')),
true,
'README.txt should be created'
);
// Send a test request to the deployment
const response = await fetch(new URL(output.stdout).href);
const text = await response.text();
t.is(text.includes('<h1>custom hello</h1>'), true, text);
// Ensure that `vc dev` also uses the configured build command
// and output directory
let stderr = '';
const port = 58351;
const dev = execa(binaryPath, ['dev', '--listen', port, dir, ...defaultArgs]);
dev.stderr.setEncoding('utf8');
try {
dev.stdout.pipe(process.stdout);
dev.stderr.pipe(process.stderr);
await new Promise((resolve, reject) => {
dev.once('exit', (code, signal) => {
reject(`"vc dev" failed with ${signal || code}`);
});
dev.stderr.on('data', data => {
stderr += data;
if (stderr.includes('Ready! Available at')) {
resolve();
}
});
});
const res2 = await fetch(`http://localhost:${port}/`);
const text2 = await res2.text();
t.is(text2.includes('<h1>custom hello</h1>'), true, text2);
} finally {
process.kill(dev.pid, 'SIGTERM');
}
});
test('should prefill "project name" prompt with folder name', async t => {
const projectName = `static-deployment-${
Math.random().toString(36).split('.')[1]
}`;
const src = fixture('static-deployment');
// remove previously linked project if it exists
await remove(path.join(src, '.vercel'));
const directory = path.join(src, '../', projectName);
await copy(src, directory);
const now = execa(binaryPath, [directory, ...defaultArgs]);
await waitForPrompt(now, chunk => /Set up and deploy [^?]+\?/.test(chunk));
now.stdin.write('yes\n');
await waitForPrompt(now, chunk =>
chunk.includes('Which scope do you want to deploy to?')
);
now.stdin.write('\n');
await waitForPrompt(now, chunk =>
chunk.includes('Link to existing project?')
);
now.stdin.write('no\n');
await waitForPrompt(now, chunk =>
chunk.includes(`Whats your projects name? (${projectName})`)
);
now.stdin.write(`\n`);
await waitForPrompt(now, chunk =>
chunk.includes('In which directory is your code located?')
);
now.stdin.write('\n');
await waitForPrompt(now, chunk =>
chunk.includes('Want to override the settings?')
);
now.stdin.write('no\n');
const output = await now;
t.is(output.exitCode, 0, formatOutput(output));
});
test('should prefill "project name" prompt with --name', async t => {
const directory = fixture('static-deployment');
const projectName = `static-deployment-${
Math.random().toString(36).split('.')[1]
}`;
// remove previously linked project if it exists
await remove(path.join(directory, '.vercel'));
const now = execa(binaryPath, [
directory,
'--name',
projectName,
...defaultArgs,
]);
let isDeprecated = false;
await waitForPrompt(now, chunk => {
if (chunk.includes('The "--name" option is deprecated')) {
isDeprecated = true;
}
return /Set up and deploy [^?]+\?/.test(chunk);
});
now.stdin.write('yes\n');
t.is(isDeprecated, true);
await waitForPrompt(now, chunk =>
chunk.includes('Which scope do you want to deploy to?')
);
now.stdin.write('\n');
await waitForPrompt(now, chunk =>
chunk.includes('Link to existing project?')
);
now.stdin.write('no\n');
await waitForPrompt(now, chunk =>
chunk.includes(`Whats your projects name? (${projectName})`)
);
now.stdin.write(`\n`);
await waitForPrompt(now, chunk =>
chunk.includes('In which directory is your code located?')
);
now.stdin.write('\n');
await waitForPrompt(now, chunk =>
chunk.includes('Want to override the settings?')
);
now.stdin.write('no\n');
const output = await now;
t.is(output.exitCode, 0, formatOutput(output));
});
test('should prefill "project name" prompt with now.json `name`', async t => {
const directory = fixture('static-deployment');
const projectName = `static-deployment-${
Math.random().toString(36).split('.')[1]
}`;
// remove previously linked project if it exists
await remove(path.join(directory, '.vercel'));
await fs.writeFile(
path.join(directory, 'vercel.json'),
JSON.stringify({
name: projectName,
})
);
const now = execa(binaryPath, [directory, ...defaultArgs]);
let isDeprecated = false;
now.stderr.on('data', data => {
if (
data
.toString()
.includes('The `name` property in vercel.json is deprecated')
) {
isDeprecated = true;
}
});
await waitForPrompt(now, chunk => {
return /Set up and deploy [^?]+\?/.test(chunk);
});
now.stdin.write('yes\n');
await waitForPrompt(now, chunk =>
chunk.includes('Which scope do you want to deploy to?')
);
now.stdin.write('\n');
await waitForPrompt(now, chunk =>
chunk.includes('Link to existing project?')
);
now.stdin.write('no\n');
await waitForPrompt(now, chunk =>
chunk.includes(`Whats your projects name? (${projectName})`)
);
now.stdin.write(`\n`);
await waitForPrompt(now, chunk =>
chunk.includes('In which directory is your code located?')
);
now.stdin.write('\n');
await waitForPrompt(now, chunk =>
chunk.includes('Want to override the settings?')
);
now.stdin.write('no\n');
const output = await now;
t.is(output.exitCode, 0, formatOutput(output));
t.is(isDeprecated, true);
// clean up
await remove(path.join(directory, 'vercel.json'));
});
test('deploy with unknown `VERCEL_PROJECT_ID` should fail', async t => {
const directory = fixture('static-deployment');
const user = await fetchTokenInformation(token);
const output = await execute([directory], {
env: {
VERCEL_ORG_ID: user.id,
VERCEL_PROJECT_ID: 'asdf',
},
});
t.is(output.exitCode, 1, formatOutput(output));
t.is(output.stderr.includes('Project not found'), true, formatOutput(output));
});
test('deploy with `VERCEL_ORG_ID` but without `VERCEL_PROJECT_ID` should fail', async t => {
const directory = fixture('static-deployment');
const user = await fetchTokenInformation(token);
const output = await execute([directory], {
env: { VERCEL_ORG_ID: user.id },
});
t.is(output.exitCode, 1, formatOutput(output));
t.is(
output.stderr.includes(
'You specified `VERCEL_ORG_ID` but you forgot to specify `VERCEL_PROJECT_ID`. You need to specify both to deploy to a custom project.'
),
true,
formatOutput(output)
);
});
test('deploy with `VERCEL_PROJECT_ID` but without `VERCEL_ORG_ID` should fail', async t => {
const directory = fixture('static-deployment');
const output = await execute([directory], {
env: { VERCEL_PROJECT_ID: 'asdf' },
});
t.is(output.exitCode, 1, formatOutput(output));
t.is(
output.stderr.includes(
'You specified `VERCEL_PROJECT_ID` but you forgot to specify `VERCEL_ORG_ID`. You need to specify both to deploy to a custom project.'
),
true,
formatOutput(output)
);
});
test('deploy with `VERCEL_ORG_ID` and `VERCEL_PROJECT_ID`', async t => {
const directory = fixture('static-deployment');
// generate `.vercel`
await execute([directory, '--confirm']);
const link = require(path.join(directory, '.vercel/project.json'));
await remove(path.join(directory, '.vercel'));
const output = await execute([directory], {
env: {
VERCEL_ORG_ID: link.orgId,
VERCEL_PROJECT_ID: link.projectId,
},
});
t.is(output.exitCode, 0, formatOutput(output));
t.is(output.stdout.includes('Linked to'), false);
});
test('deploy shows notice when project in `.vercel` does not exists', async t => {
const directory = fixture('static-deployment');
// overwrite .vercel with unexisting project
await ensureDir(path.join(directory, '.vercel'));
await writeFile(
path.join(directory, '.vercel/project.json'),
JSON.stringify({
orgId: 'asdf',
projectId: 'asdf',
})
);
const now = execute([directory]);
let detectedNotice = false;
// kill after first prompt
await waitForPrompt(now, chunk => {
detectedNotice =
detectedNotice ||
chunk.includes(
'Your Project was either deleted, transferred to a new Team, or you dont have access to it anymore'
);
return /Set up and deploy [^?]+\?/.test(chunk);
});
now.stdin.write('no\n');
t.is(detectedNotice, true, 'did not detect notice');
});
test('use `rootDirectory` from project when deploying', async t => {
const directory = fixture('project-root-directory');
const firstResult = await execute([directory, '--confirm', '--public']);
t.is(firstResult.exitCode, 0, formatOutput(firstResult));
const { host: firstHost } = new URL(firstResult.stdout);
const response = await apiFetch(`/v12/now/deployments/get?url=${firstHost}`);
t.is(response.status, 200);
const { projectId } = await response.json();
t.is(typeof projectId, 'string', projectId);
const projectResponse = await apiFetch(`/v2/projects/${projectId}`, {
method: 'PATCH',
body: JSON.stringify({
rootDirectory: 'src',
}),
});
console.log('response', await projectResponse.text());
t.is(projectResponse.status, 200);
const secondResult = await execute([directory, '--public']);
t.is(secondResult.exitCode, 0, formatOutput(secondResult));
const pageResponse1 = await fetch(secondResult.stdout);
t.is(pageResponse1.status, 200);
t.regex(await pageResponse1.text(), /I am a website/gm);
// Ensures that the `now.json` file has been applied
const pageResponse2 = await fetch(`${secondResult.stdout}/i-do-exist`);
t.is(pageResponse2.status, 200);
t.regex(await pageResponse2.text(), /I am a website/gm);
await apiFetch(`/v2/projects/${projectId}`, {
method: 'DELETE',
});
});
test('vercel deploy with unknown `VERCEL_ORG_ID` or `VERCEL_PROJECT_ID` should error', async t => {
const output = await execute(['deploy'], {
env: { VERCEL_ORG_ID: 'asdf', VERCEL_PROJECT_ID: 'asdf' },
});
t.is(output.exitCode, 1, formatOutput(output));
t.is(output.stderr.includes('Project not found'), true, formatOutput(output));
});
test('vercel env with unknown `VERCEL_ORG_ID` or `VERCEL_PROJECT_ID` should error', async t => {
const output = await execute(['env'], {
env: { VERCEL_ORG_ID: 'asdf', VERCEL_PROJECT_ID: 'asdf' },
});
t.is(output.exitCode, 1, formatOutput(output));
t.is(output.stderr.includes('Project not found'), true, formatOutput(output));
});
test('whoami with `VERCEL_ORG_ID` should favor `--scope` and should error', async t => {
const user = await fetchTokenInformation(token);
const output = await execute(['whoami', '--scope', 'asdf'], {
env: { VERCEL_ORG_ID: user.id },
});
t.is(output.exitCode, 1, formatOutput(output));
t.is(
output.stderr.includes('The specified scope does not exist'),
true,
formatOutput(output)
);
});
test('whoami with local .vercel scope', async t => {
const directory = fixture('static-deployment');
const user = await fetchTokenInformation(token);
// create local .vercel
await ensureDir(path.join(directory, '.vercel'));
await fs.writeFile(
path.join(directory, '.vercel', 'project.json'),
JSON.stringify({ orgId: user.id, projectId: 'xxx' })
);
const output = await execute(['whoami'], {
cwd: directory,
});
t.is(output.exitCode, 0, formatOutput(output));
t.is(output.stdout.includes(contextName), true, formatOutput(output));
// clean up
await remove(path.join(directory, '.vercel'));
});
test('deploys with only now.json and README.md', async t => {
const directory = fixture('deploy-with-only-readme-now-json');
const { exitCode, stderr, stdout } = await execa(
binaryPath,
[...defaultArgs, '--confirm'],
{
cwd: directory,
reject: false,
}
);
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
const { host } = new URL(stdout);
const res = await fetch(`https://${host}/README.md`);
const text = await res.text();
t.regex(text, /readme contents/);
});
test('deploys with only vercel.json and README.md', async t => {
const directory = fixture('deploy-with-only-readme-vercel-json');
const { exitCode, stderr, stdout } = await execa(
binaryPath,
[...defaultArgs, '--confirm'],
{
cwd: directory,
reject: false,
}
);
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
const { host } = new URL(stdout);
const res = await fetch(`https://${host}/README.md`);
const text = await res.text();
t.regex(text, /readme contents/);
});
test('reject conflicting `vercel.json` and `now.json` files', async t => {
const directory = fixture('conflicting-now-json-vercel-json');
const { exitCode, stderr, stdout } = await execa(
binaryPath,
[...defaultArgs, '--confirm'],
{
cwd: directory,
reject: false,
}
);
t.is(exitCode, 1, formatOutput({ stderr, stdout }));
t.true(
stderr.includes(
'Cannot use both a `vercel.json` and `now.json` file. Please delete the `now.json` file.'
),
formatOutput({ stderr, stdout })
);
});
test('`vc --debug project ls` should output the projects listing', async t => {
const { exitCode, stderr, stdout } = await execa(
binaryPath,
[...defaultArgs, '--debug', 'project', 'ls'],
{
reject: false,
}
);
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
t.true(
stdout.includes('> Projects found under'),
formatOutput({ stderr, stdout })
);
});
test('deploy gatsby twice and print cached directories', async t => {
const directory = example('gatsby');
const packageJsonPath = path.join(directory, 'package.json');
const packageJsonOriginal = await readFile(packageJsonPath, 'utf8');
const pkg = JSON.parse(packageJsonOriginal);
async function tryDeploy(cwd) {
await execa(binaryPath, [...defaultArgs, '--public', '--confirm'], {
cwd,
stdio: 'inherit',
reject: true,
});
t.true(true);
}
// Deploy once to populate the cache
await tryDeploy(directory);
// Wait because the cache is not available right away
// See https://codeburst.io/quick-explanation-of-the-s3-consistency-model-6c9f325e3f82
await sleep(60000);
// Update build script to ensure cached files were restored in the next deploy
pkg.scripts.build = `ls -lA && ls .cache && ls public && ${pkg.scripts.build}`;
await writeFile(packageJsonPath, JSON.stringify(pkg));
try {
await tryDeploy(directory);
} finally {
await writeFile(packageJsonPath, packageJsonOriginal);
}
});
test('reject deploying with wrong team .vercel config', async t => {
const directory = fixture('unauthorized-vercel-config');
const { exitCode, stderr, stdout } = await execa(
binaryPath,
[...defaultArgs, '--confirm'],
{
cwd: directory,
reject: false,
}
);
t.is(exitCode, 1, formatOutput({ stderr, stdout }));
t.true(
stderr.includes(
'Could not retrieve Project Settings. To link your Project, remove the `.vercel` directory and deploy again.'
),
formatOutput({ stderr, stdout })
);
});
test('[vc link] should show prompts to set up project', async t => {
const dir = fixture('project-link-zeroconf');
const projectName = `project-link-zeroconf-${
Math.random().toString(36).split('.')[1]
}`;
// remove previously linked project if it exists
await remove(path.join(dir, '.vercel'));
const vc = execa(binaryPath, ['link', ...defaultArgs], { cwd: dir });
await setupProject(vc, projectName, {
buildCommand: `mkdir -p o && echo '<h1>custom hello</h1>' > o/index.html`,
outputDirectory: 'o',
});
const output = await vc;
// Ensure the exit code is right
t.is(output.exitCode, 0, formatOutput(output));
// Ensure .gitignore is created
const gitignore = await readFile(path.join(dir, '.gitignore'), 'utf8');
t.is(gitignore, '.vercel\n.output\n');
// Ensure .vercel/project.json and .vercel/README.txt are created
t.is(
await exists(path.join(dir, '.vercel', 'project.json')),
true,
'project.json should be created'
);
t.is(
await exists(path.join(dir, '.vercel', 'README.txt')),
true,
'README.txt should be created'
);
});
test('[vc link --confirm] should not show prompts and autolink', async t => {
const dir = fixture('project-link-confirm');
// remove previously linked project if it exists
await remove(path.join(dir, '.vercel'));
const { exitCode, stderr, stdout } = await execa(
binaryPath,
['link', '--confirm', ...defaultArgs],
{ cwd: dir, reject: false }
);
// Ensure the exit code is right
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
// Ensure the message is correct pattern
t.regex(stderr, /Linked to /m);
// Ensure .gitignore is created
const gitignore = await readFile(path.join(dir, '.gitignore'), 'utf8');
t.is(gitignore, '.vercel\n.output\n');
// Ensure .vercel/project.json and .vercel/README.txt are created
t.is(
await exists(path.join(dir, '.vercel', 'project.json')),
true,
'project.json should be created'
);
t.is(
await exists(path.join(dir, '.vercel', 'README.txt')),
true,
'README.txt should be created'
);
});
test('[vc link] should not duplicate paths in .gitignore', async t => {
const dir = fixture('project-link-gitignore');
// remove previously linked project if it exists
await remove(path.join(dir, '.vercel'));
const { exitCode, stderr, stdout } = await execa(
binaryPath,
['link', '--confirm', ...defaultArgs],
{ cwd: dir, reject: false }
);
// Ensure the exit code is right
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
// Ensure the message is correct pattern
t.regex(stderr, /Linked to /m);
// Ensure .gitignore is created
const gitignore = await readFile(path.join(dir, '.gitignore'), 'utf8');
t.is(gitignore, '.output\n.vercel\n');
});
test('[vc dev] should show prompts to set up project', async t => {
const dir = fixture('project-link-dev');
const port = 58352;
const projectName = `project-link-dev-${
Math.random().toString(36).split('.')[1]
}`;
// remove previously linked project if it exists
await remove(path.join(dir, '.vercel'));
const dev = execa(binaryPath, ['dev', '--listen', port, ...defaultArgs], {
cwd: dir,
});
await setupProject(dev, projectName, {
buildCommand: `mkdir -p o && echo '<h1>custom hello</h1>' > o/index.html`,
outputDirectory: 'o',
});
// Ensure .gitignore is created
const gitignore = await readFile(path.join(dir, '.gitignore'), 'utf8');
t.is(gitignore, '.vercel\n.output\n');
// Ensure .vercel/project.json and .vercel/README.txt are created
t.is(
await exists(path.join(dir, '.vercel', 'project.json')),
true,
'project.json should be created'
);
t.is(
await exists(path.join(dir, '.vercel', 'README.txt')),
true,
'README.txt should be created'
);
await waitForPrompt(dev, chunk => chunk.includes('Ready! Available at'));
// Ensure that `vc dev` also works
try {
const response = await fetch(`http://localhost:${port}/`);
const text = await response.text();
t.is(text.includes('<h1>custom hello</h1>'), true, text);
} finally {
process.kill(dev.pid, 'SIGTERM');
}
});
test('[vc link] should show project prompts but not framework when `builds` defined', async t => {
const dir = fixture('project-link-legacy');
const projectName = `project-link-legacy-${
Math.random().toString(36).split('.')[1]
}`;
// remove previously linked project if it exists
await remove(path.join(dir, '.vercel'));
const vc = execa(binaryPath, ['link', ...defaultArgs], { cwd: dir });
await waitForPrompt(vc, chunk => /Set up [^?]+\?/.test(chunk));
vc.stdin.write('yes\n');
await waitForPrompt(vc, chunk =>
chunk.includes('Which scope should contain your project?')
);
vc.stdin.write('\n');
await waitForPrompt(vc, chunk => chunk.includes('Link to existing project?'));
vc.stdin.write('no\n');
await waitForPrompt(vc, chunk =>
chunk.includes('Whats your projects name?')
);
vc.stdin.write(`${projectName}\n`);
await waitForPrompt(vc, chunk =>
chunk.includes('In which directory is your code located?')
);
vc.stdin.write('\n');
await waitForPrompt(vc, chunk => chunk.includes('Linked to'));
const output = await vc;
// Ensure the exit code is right
t.is(output.exitCode, 0, formatOutput(output));
// Ensure .gitignore is created
const gitignore = await readFile(path.join(dir, '.gitignore'), 'utf8');
t.is(gitignore, '.vercel\n.output\n');
// Ensure .vercel/project.json and .vercel/README.txt are created
t.is(
await exists(path.join(dir, '.vercel', 'project.json')),
true,
'project.json should be created'
);
t.is(
await exists(path.join(dir, '.vercel', 'README.txt')),
true,
'README.txt should be created'
);
});
test('[vc dev] should send the platform proxy request headers to frontend dev server ', async t => {
const dir = fixture('dev-proxy-headers-and-env');
const port = 58353;
const projectName = `dev-proxy-headers-and-env-${
Math.random().toString(36).split('.')[1]
}`;
// remove previously linked project if it exists
await remove(path.join(dir, '.vercel'));
const dev = execa(binaryPath, ['dev', '--listen', port, ...defaultArgs], {
cwd: dir,
});
await setupProject(dev, projectName, {
buildCommand: `mkdir -p o && echo '<h1>custom hello</h1>' > o/index.html`,
outputDirectory: 'o',
devCommand: 'node server.js',
});
await waitForPrompt(dev, chunk => chunk.includes('Ready! Available at'));
// Ensure that `vc dev` also works
try {
const response = await fetch(`http://localhost:${port}/`);
const body = await response.json();
t.is(body.headers['x-vercel-deployment-url'], `localhost:${port}`);
t.is(body.env.NOW_REGION, 'dev1');
} finally {
process.kill(dev.pid, 'SIGTERM');
}
});
test('[vc link] should support the `--project` flag', async t => {
const projectName = 'link-project-flag';
const directory = fixture('static-deployment');
const [user, output] = await Promise.all([
fetchTokenInformation(token),
execute(['link', '--confirm', '--project', projectName, directory]),
]);
t.is(output.exitCode, 0, formatOutput(output));
t.true(
output.stderr.includes(`Linked to ${user.username}/${projectName}`),
formatOutput(output)
);
});