mirror of
https://github.com/LukeHagar/vercel.git
synced 2025-12-08 04:22:09 +00:00
1352 lines
38 KiB
TypeScript
Vendored
1352 lines
38 KiB
TypeScript
Vendored
import ms from 'ms';
|
|
import path from 'path';
|
|
import { once } from 'node:events';
|
|
import { URL, parse as parseUrl } from 'url';
|
|
import semVer from 'semver';
|
|
import { Readable } from 'stream';
|
|
import { homedir } from 'os';
|
|
import { runNpmInstall } from '@vercel/build-utils';
|
|
import { execCli } from './helpers/exec';
|
|
import fetch, { RequestInit, RequestInfo } from 'node-fetch';
|
|
import retry from 'async-retry';
|
|
import fs from 'fs-extra';
|
|
import { logo } from '../src/util/pkg-name';
|
|
import sleep from '../src/util/sleep';
|
|
import humanizePath from '../src/util/humanize-path';
|
|
import pkg from '../package.json';
|
|
import { fetchTokenWithRetry } from '../../../test/lib/deployment/now-deploy';
|
|
import waitForPrompt from './helpers/wait-for-prompt';
|
|
import { getNewTmpDir, listTmpDirs } from './helpers/get-tmp-dir';
|
|
import getGlobalDir from './helpers/get-global-dir';
|
|
import {
|
|
setupE2EFixture,
|
|
prepareE2EFixtures,
|
|
} from './helpers/setup-e2e-fixture';
|
|
import formatOutput from './helpers/format-output';
|
|
import type http from 'http';
|
|
import type { NowJson, DeploymentLike } from './helpers/types';
|
|
|
|
const TEST_TIMEOUT = 3 * 60 * 1000;
|
|
jest.setTimeout(TEST_TIMEOUT);
|
|
|
|
const binaryPath = path.resolve(__dirname, `../scripts/start.js`);
|
|
|
|
const deployHelpMessage = `${logo} vercel [options] <command | path>`;
|
|
let session = 'temp-session';
|
|
let secretName: string | undefined;
|
|
|
|
function fetchTokenInformation(token: string, 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}", status: ${
|
|
res.status
|
|
}, id: ${res.headers.get('x-vercel-id')}`
|
|
);
|
|
}
|
|
|
|
const data = await res.json();
|
|
|
|
return data.user;
|
|
},
|
|
{ retries, factor: 1 }
|
|
);
|
|
}
|
|
|
|
const context: {
|
|
deployment: string | undefined;
|
|
} = {
|
|
deployment: undefined,
|
|
};
|
|
|
|
let token: string | undefined;
|
|
let email: string | undefined;
|
|
let contextName: string | undefined;
|
|
|
|
function mockLoginApi(req: http.IncomingMessage, res: http.ServerResponse) {
|
|
const { url = '/', method } = req;
|
|
let { pathname = '/', query = {} } = parseUrl(url, true);
|
|
// eslint-disable-next-line no-console
|
|
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 if (method === 'GET' && pathname === '/v2/user') {
|
|
res.end(JSON.stringify({ user: { email } }));
|
|
} else {
|
|
res.statusCode = 405;
|
|
res.end(JSON.stringify({ code: 'method_not_allowed' }));
|
|
}
|
|
}
|
|
|
|
const pickUrl = (stdout: string) => {
|
|
const lines = stdout.split('\n');
|
|
return lines[lines.length - 1];
|
|
};
|
|
|
|
const waitForDeployment = async (href: RequestInfo) => {
|
|
// eslint-disable-next-line no-console
|
|
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);
|
|
}
|
|
};
|
|
|
|
let loginApiUrl = '';
|
|
const loginApiServer = require('http')
|
|
.createServer(mockLoginApi)
|
|
.listen(0, () => {
|
|
const { port } = loginApiServer.address();
|
|
loginApiUrl = `http://localhost:${port}`;
|
|
// eslint-disable-next-line no-console
|
|
console.log(`[mock-login-server] Listening on ${loginApiUrl}`);
|
|
});
|
|
|
|
const apiFetch = (url: string, { headers, ...options }: RequestInit = {}) => {
|
|
return fetch(`https://api.vercel.com${url}`, {
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
...(headers || {}),
|
|
},
|
|
...options,
|
|
});
|
|
};
|
|
|
|
const createUser = async () => {
|
|
await retry(
|
|
async () => {
|
|
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 }
|
|
);
|
|
};
|
|
|
|
function getConfigAuthPath() {
|
|
return path.join(getGlobalDir(), 'auth.json');
|
|
}
|
|
|
|
beforeAll(async () => {
|
|
try {
|
|
await createUser();
|
|
|
|
if (!contextName) {
|
|
throw new Error('Shared state "contextName" not set.');
|
|
}
|
|
await prepareE2EFixtures(contextName, binaryPath);
|
|
|
|
if (!email) {
|
|
throw new Error('Shared state "email" not set.');
|
|
}
|
|
await fs.remove(getConfigAuthPath());
|
|
const loginOutput = await execCli(binaryPath, [
|
|
'login',
|
|
email,
|
|
'--api',
|
|
loginApiUrl,
|
|
]);
|
|
|
|
expect(loginOutput.exitCode, formatOutput(loginOutput)).toBe(0);
|
|
expect(loginOutput.stderr).toMatch(/You are now logged in\./gm);
|
|
|
|
const auth = await fs.readJSON(getConfigAuthPath());
|
|
expect(auth.token).toBe(token);
|
|
} catch (err) {
|
|
// eslint-disable-next-line no-console
|
|
console.log('Failed test suite `beforeAll`');
|
|
// eslint-disable-next-line no-console
|
|
console.log(err);
|
|
|
|
// force test suite to actually stop
|
|
process.exit(1);
|
|
}
|
|
});
|
|
|
|
afterAll(async () => {
|
|
delete process.env.ENABLE_EXPERIMENTAL_COREPACK;
|
|
|
|
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 execCli(binaryPath, ['logout']);
|
|
}
|
|
|
|
const allTmpDirs = listTmpDirs();
|
|
for (const tmpDir of allTmpDirs) {
|
|
// eslint-disable-next-line no-console
|
|
console.log('Removing temp dir: ', tmpDir.name);
|
|
tmpDir.removeCallback();
|
|
}
|
|
});
|
|
|
|
async function clearAuthConfig() {
|
|
const configPath = getConfigAuthPath();
|
|
if (fs.existsSync(configPath)) {
|
|
await fs.writeFile(configPath, JSON.stringify({}));
|
|
}
|
|
}
|
|
|
|
test('[vc projects] should create a project successfully', async () => {
|
|
const projectName = `vc-projects-add-${
|
|
Math.random().toString(36).split('.')[1]
|
|
}`;
|
|
|
|
const vc = execCli(binaryPath, ['project', 'add', projectName]);
|
|
|
|
await waitForPrompt(vc, `Success! Project ${projectName} added`);
|
|
|
|
const { exitCode, stdout, stderr } = await vc;
|
|
expect(exitCode, formatOutput({ stdout, stderr })).toBe(0);
|
|
|
|
// creating the same project again should succeed
|
|
const vc2 = execCli(binaryPath, ['project', 'add', projectName]);
|
|
|
|
await waitForPrompt(vc2, `Success! Project ${projectName} added`);
|
|
|
|
const { exitCode: exitCode2 } = await vc;
|
|
expect(exitCode2).toBe(0);
|
|
});
|
|
|
|
test('deploy with metadata containing "=" in the value', async () => {
|
|
const target = await setupE2EFixture('static-v2-meta');
|
|
|
|
const { exitCode, stdout, stderr } = await execCli(binaryPath, [
|
|
target,
|
|
'--yes',
|
|
'--meta',
|
|
'someKey==',
|
|
]);
|
|
|
|
expect(exitCode, formatOutput({ stdout, stderr })).toBe(0);
|
|
|
|
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();
|
|
expect(deployment.meta.someKey).toBe('=');
|
|
});
|
|
|
|
test('print the deploy help message', async () => {
|
|
const { stderr, stdout, exitCode } = await execCli(binaryPath, ['help']);
|
|
|
|
expect(exitCode, formatOutput({ stdout, stderr })).toBe(2);
|
|
expect(stderr).toContain(deployHelpMessage);
|
|
expect(stderr).not.toContain('ExperimentalWarning');
|
|
});
|
|
|
|
test('output the version', async () => {
|
|
const { stdout, stderr, exitCode } = await execCli(binaryPath, ['--version']);
|
|
|
|
const version = stdout.trim();
|
|
|
|
expect(exitCode, formatOutput({ stdout, stderr })).toBe(0);
|
|
expect(semVer.valid(version)).toBeTruthy();
|
|
expect(version).toBe(pkg.version);
|
|
});
|
|
|
|
test('should add secret with hyphen prefix', async () => {
|
|
const target = await setupE2EFixture('build-secret');
|
|
const key = 'mysecret';
|
|
const value = '-foo_bar';
|
|
|
|
let secretCall = await execCli(
|
|
binaryPath,
|
|
['secrets', 'add', '--', key, value],
|
|
{
|
|
cwd: target,
|
|
}
|
|
);
|
|
|
|
expect(secretCall.exitCode, formatOutput(secretCall)).toBe(0);
|
|
|
|
let targetCall = await execCli(binaryPath, ['--yes'], {
|
|
cwd: target,
|
|
});
|
|
|
|
expect(targetCall.exitCode, formatOutput(targetCall)).toBe(0);
|
|
const { host } = new URL(targetCall.stdout);
|
|
const response = await fetch(`https://${host}`);
|
|
expect(response.status).toBe(200);
|
|
expect(await response.text()).toBe(`${value}\n`);
|
|
});
|
|
|
|
test('login with unregistered user', async () => {
|
|
const { stdout, stderr, exitCode } = await execCli(binaryPath, [
|
|
'login',
|
|
`${session}@${session}.com`,
|
|
]);
|
|
|
|
const goal = `Error: Please sign up: https://vercel.com/signup`;
|
|
const lines = stderr.trim().split('\n');
|
|
const last = lines[lines.length - 1];
|
|
|
|
expect(exitCode, formatOutput({ stdout, stderr })).toBe(1);
|
|
expect(last).toContain(goal);
|
|
});
|
|
|
|
test('ignore files specified in .nowignore', async () => {
|
|
const directory = await setupE2EFixture('nowignore');
|
|
|
|
const args = ['--debug', '--public', '--name', session, '--yes'];
|
|
const targetCall = await execCli(binaryPath, args, {
|
|
cwd: directory,
|
|
});
|
|
|
|
const { host } = new URL(targetCall.stdout);
|
|
const ignoredFile = await fetch(`https://${host}/ignored.txt`);
|
|
expect(ignoredFile.status).toBe(404);
|
|
|
|
const presentFile = await fetch(`https://${host}/index.txt`);
|
|
expect(presentFile.status).toBe(200);
|
|
});
|
|
|
|
test('ignore files specified in .nowignore via allowlist', async () => {
|
|
const directory = await setupE2EFixture('nowignore-allowlist');
|
|
|
|
const args = ['--debug', '--public', '--name', session, '--yes'];
|
|
const targetCall = await execCli(binaryPath, args, {
|
|
cwd: directory,
|
|
});
|
|
|
|
const { host } = new URL(targetCall.stdout);
|
|
const ignoredFile = await fetch(`https://${host}/ignored.txt`);
|
|
expect(ignoredFile.status).toBe(404);
|
|
|
|
const presentFile = await fetch(`https://${host}/index.txt`);
|
|
expect(presentFile.status).toBe(200);
|
|
});
|
|
|
|
test('list the scopes', async () => {
|
|
const { stdout, stderr, exitCode } = await execCli(binaryPath, [
|
|
'teams',
|
|
'ls',
|
|
]);
|
|
|
|
expect(exitCode, formatOutput({ stdout, stderr })).toBe(0);
|
|
|
|
const include = new RegExp(`✔ ${contextName}\\s+${email}`);
|
|
expect(stderr).toMatch(include);
|
|
});
|
|
|
|
test('domains inspect', async () => {
|
|
const domainName = `inspect-${contextName}-${Math.random()
|
|
.toString()
|
|
.slice(2, 8)}.org`;
|
|
|
|
const directory = await setupE2EFixture('static-multiple-files');
|
|
const projectName = Math.random().toString().slice(2);
|
|
|
|
const output = await execCli(binaryPath, [
|
|
directory,
|
|
`--name=${projectName}`,
|
|
'--yes',
|
|
'--public',
|
|
]);
|
|
|
|
expect(output.exitCode, formatOutput(output)).toBe(0);
|
|
|
|
{
|
|
// Add a domain that can be inspected
|
|
const result = await execCli(binaryPath, [
|
|
`domains`,
|
|
`add`,
|
|
domainName,
|
|
projectName,
|
|
]);
|
|
|
|
expect(result.exitCode, formatOutput(result)).toBe(0);
|
|
}
|
|
|
|
const { exitCode, stdout, stderr } = await execCli(binaryPath, [
|
|
'domains',
|
|
'inspect',
|
|
domainName,
|
|
]);
|
|
|
|
expect(stderr).toContain(`Renewal Price`);
|
|
expect(exitCode, formatOutput({ stdout, stderr })).toBe(0);
|
|
|
|
{
|
|
// Remove the domain again
|
|
const result = await execCli(binaryPath, [`domains`, `rm`, domainName], {
|
|
input: 'y',
|
|
});
|
|
|
|
expect(result.exitCode, formatOutput(result)).toBe(0);
|
|
}
|
|
});
|
|
|
|
// eslint-disable-next-line jest/no-disabled-tests
|
|
test('try to purchase a domain', async () => {
|
|
if (process.env.VERCEL_TOKEN || process.env.NOW_TOKEN) {
|
|
// eslint-disable-next-line no-console
|
|
console.log(
|
|
'Skipping test `try to purchase a domain` because a personal VERCEL_TOKEN was provided.'
|
|
);
|
|
return;
|
|
}
|
|
|
|
const stream = new Readable();
|
|
stream._read = () => {};
|
|
|
|
const { stderr, stdout, exitCode } = await execCli(
|
|
binaryPath,
|
|
['domains', 'buy', `${session}-test.com`],
|
|
{
|
|
input: stream,
|
|
env: {
|
|
FORCE_TTY: '1',
|
|
},
|
|
}
|
|
);
|
|
|
|
expect(exitCode, formatOutput({ stdout, stderr })).toBe(1);
|
|
expect(stderr).toMatch(
|
|
/Error: Could not purchase domain\. Please add a payment method using/
|
|
);
|
|
});
|
|
|
|
test('try to transfer-in a domain with "--code" option', async () => {
|
|
const { stderr, stdout, exitCode } = await execCli(binaryPath, [
|
|
'domains',
|
|
'transfer-in',
|
|
'--code',
|
|
'xyz',
|
|
`${session}-test.com`,
|
|
]);
|
|
|
|
expect(stderr).toContain(
|
|
`Error: The domain "${session}-test.com" is not transferable.`
|
|
);
|
|
expect(exitCode, formatOutput({ stdout, stderr })).toBe(1);
|
|
});
|
|
|
|
test('try to move an invalid domain', async () => {
|
|
const { stderr, stdout, exitCode } = await execCli(binaryPath, [
|
|
'domains',
|
|
'move',
|
|
`${session}-invalid-test.org`,
|
|
`${session}-invalid-user`,
|
|
]);
|
|
|
|
expect(stderr).toContain(`Error: Domain not found under `);
|
|
expect(exitCode, formatOutput({ stdout, stderr })).toBe(1);
|
|
});
|
|
|
|
/*
|
|
* 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 execCli(
|
|
binaryPath,
|
|
['alias', hosts.deployment, hosts.alias],
|
|
);
|
|
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 execCli(
|
|
binaryPath,
|
|
['alias', 'rm', context.wildcardAlias, '--yes'],
|
|
);
|
|
console.log(stderr);
|
|
console.log(stdout);
|
|
console.log(exitCode);
|
|
t.is(exitCode, 0);
|
|
t.true(stdout.startsWith(goal));
|
|
});
|
|
*/
|
|
|
|
test('ensure we render a warning for deployments with no files', async () => {
|
|
const directory = await setupE2EFixture('empty-directory');
|
|
|
|
const { stderr, stdout, exitCode } = await execCli(binaryPath, [
|
|
directory,
|
|
'--public',
|
|
'--name',
|
|
session,
|
|
'--yes',
|
|
'--force',
|
|
]);
|
|
|
|
// Ensure the warning is printed
|
|
expect(stderr).toMatch(/There are no files inside your deployment/);
|
|
|
|
// Test if the output is really a URL
|
|
const { href, host } = new URL(stdout);
|
|
expect(host.split('-')[0]).toBe(session);
|
|
|
|
if (host) {
|
|
context.deployment = host;
|
|
}
|
|
|
|
// Ensure the exit code is right
|
|
expect(exitCode, formatOutput({ stdout, stderr })).toBe(0);
|
|
|
|
// Send a test request to the deployment
|
|
const res = await fetch(href);
|
|
expect(res.status).toBe(404);
|
|
});
|
|
|
|
test('output logs with "short" output', async () => {
|
|
if (!context.deployment) {
|
|
throw new Error('Shared state "context.deployment" not set.');
|
|
}
|
|
|
|
const { stderr, stdout, exitCode } = await execCli(binaryPath, [
|
|
'logs',
|
|
context.deployment,
|
|
]);
|
|
|
|
expect(stderr).toContain(`Fetched deployment "${context.deployment}"`);
|
|
|
|
// "short" format includes timestamps
|
|
expect(
|
|
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)/
|
|
)
|
|
).toBeTruthy();
|
|
|
|
expect(exitCode, formatOutput({ stdout, stderr })).toBe(0);
|
|
});
|
|
|
|
test('output logs with "raw" output', async () => {
|
|
if (!context.deployment) {
|
|
throw new Error('Shared state "context.deployment" not set.');
|
|
}
|
|
|
|
const { stderr, stdout, exitCode } = await execCli(binaryPath, [
|
|
'logs',
|
|
context.deployment,
|
|
'--output',
|
|
'raw',
|
|
]);
|
|
|
|
expect(stderr).toContain(`Fetched deployment "${context.deployment}"`);
|
|
|
|
// "raw" format does not include timestamps
|
|
expect(null).toBe(
|
|
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)/
|
|
)
|
|
);
|
|
|
|
expect(exitCode, formatOutput({ stdout, stderr })).toBe(0);
|
|
});
|
|
|
|
test('ensure we render a prompt when deploying home directory', async () => {
|
|
const directory = homedir();
|
|
|
|
const { stderr, stdout, exitCode } = await execCli(
|
|
binaryPath,
|
|
[directory, '--public', '--name', session, '--force'],
|
|
{
|
|
input: 'N\n',
|
|
}
|
|
);
|
|
|
|
// Ensure the exit code is right
|
|
expect(exitCode, formatOutput({ stdout, stderr })).toBe(0);
|
|
|
|
expect(stderr).toContain(
|
|
'You are deploying your home directory. Do you want to continue?'
|
|
);
|
|
expect(stderr).toContain('Canceled');
|
|
});
|
|
|
|
test('ensure the `scope` property works with email', async () => {
|
|
const directory = await setupE2EFixture('config-scope-property-email');
|
|
|
|
const { stderr, stdout, exitCode } = await execCli(binaryPath, [
|
|
directory,
|
|
'--public',
|
|
'--name',
|
|
session,
|
|
'--force',
|
|
'--yes',
|
|
]);
|
|
|
|
// Ensure we're deploying under the right scope
|
|
expect(stderr).toContain(session);
|
|
|
|
// Ensure the exit code is right
|
|
expect(exitCode, formatOutput({ stdout, stderr })).toBe(0);
|
|
|
|
// Test if the output is really a URL
|
|
const { href, host } = new URL(stdout);
|
|
expect(host.split('-')[0]).toBe(session);
|
|
|
|
// Send a test request to the deployment
|
|
const response = await fetch(href);
|
|
const contentType = response.headers.get('content-type');
|
|
|
|
expect(contentType).toBe('text/html; charset=utf-8');
|
|
});
|
|
|
|
test('ensure the `scope` property works with username', async () => {
|
|
const directory = await setupE2EFixture('config-scope-property-username');
|
|
|
|
const { stderr, stdout, exitCode } = await execCli(binaryPath, [
|
|
directory,
|
|
'--public',
|
|
'--name',
|
|
session,
|
|
'--force',
|
|
'--yes',
|
|
]);
|
|
|
|
// Ensure we're deploying under the right scope
|
|
expect(stderr).toContain(contextName);
|
|
|
|
// Ensure the exit code is right
|
|
expect(exitCode, formatOutput({ stdout, stderr })).toBe(0);
|
|
|
|
// Test if the output is really a URL
|
|
const { href, host } = new URL(stdout);
|
|
expect(host.split('-')[0]).toBe(session);
|
|
|
|
// Send a test request to the deployment
|
|
const response = await fetch(href);
|
|
const contentType = response.headers.get('content-type');
|
|
|
|
expect(contentType).toBe('text/html; charset=utf-8');
|
|
});
|
|
|
|
test('try to create a builds deployments with wrong now.json', async () => {
|
|
const directory = await setupE2EFixture('builds-wrong');
|
|
|
|
const { stderr, stdout, exitCode } = await execCli(binaryPath, [
|
|
directory,
|
|
'--public',
|
|
'--yes',
|
|
]);
|
|
|
|
// Ensure the exit code is right
|
|
expect(exitCode, formatOutput({ stdout, stderr })).toBe(1);
|
|
expect(stderr).toContain(
|
|
'Error: Invalid now.json - should NOT have additional property `builder`. Did you mean `builds`?'
|
|
);
|
|
expect(stderr).toContain(
|
|
'https://vercel.com/docs/concepts/projects/project-configuration'
|
|
);
|
|
});
|
|
|
|
test('try to create a builds deployments with wrong vercel.json', async () => {
|
|
const directory = await setupE2EFixture('builds-wrong-vercel');
|
|
|
|
const { stderr, stdout, exitCode } = await execCli(binaryPath, [
|
|
directory,
|
|
'--public',
|
|
'--yes',
|
|
]);
|
|
|
|
expect(exitCode, formatOutput({ stdout, stderr })).toBe(1);
|
|
expect(stderr).toContain(
|
|
'Error: Invalid vercel.json - should NOT have additional property `fake`. Please remove it.'
|
|
);
|
|
expect(stderr).toContain(
|
|
'https://vercel.com/docs/concepts/projects/project-configuration'
|
|
);
|
|
});
|
|
|
|
test('try to create a builds deployments with wrong `build.env` property', async () => {
|
|
const directory = await setupE2EFixture('builds-wrong-build-env');
|
|
|
|
const { exitCode, stdout, stderr } = await execCli(
|
|
binaryPath,
|
|
['--public', '--yes'],
|
|
{
|
|
cwd: directory,
|
|
}
|
|
);
|
|
|
|
expect(exitCode, formatOutput({ stdout, stderr })).toBe(1);
|
|
expect(stderr).toContain(
|
|
'Error: Invalid vercel.json - should NOT have additional property `build.env`. Did you mean `{ "build": { "env": {"name": "value"} } }`?'
|
|
);
|
|
expect(stderr).toContain(
|
|
'https://vercel.com/docs/concepts/projects/project-configuration'
|
|
);
|
|
});
|
|
|
|
test('create a builds deployments with no actual builds', async () => {
|
|
const directory = await setupE2EFixture('builds-no-list');
|
|
|
|
const { exitCode, stdout, stderr } = await execCli(binaryPath, [
|
|
directory,
|
|
'--public',
|
|
'--name',
|
|
session,
|
|
'--force',
|
|
'--yes',
|
|
]);
|
|
|
|
// Ensure the exit code is right
|
|
expect(exitCode, formatOutput({ stdout, stderr })).toBe(0);
|
|
|
|
// Test if the output is really a URL
|
|
const { host } = new URL(stdout);
|
|
expect(host.split('-')[0]).toBe(session);
|
|
});
|
|
|
|
test('create a staging deployment', async () => {
|
|
const directory = await setupE2EFixture('static-deployment');
|
|
|
|
const args = ['--debug', '--public', '--name', session];
|
|
const targetCall = await execCli(binaryPath, [
|
|
directory,
|
|
'--target=staging',
|
|
...args,
|
|
'--yes',
|
|
]);
|
|
|
|
expect(targetCall.stderr).toMatch(/Setting target to staging/gm);
|
|
expect(targetCall.stdout).toMatch(/https:\/\//gm);
|
|
expect(targetCall.exitCode, formatOutput(targetCall)).toBe(0);
|
|
|
|
const { host } = new URL(targetCall.stdout);
|
|
const deployment = await apiFetch(
|
|
`/v10/now/deployments/unknown?url=${host}`
|
|
).then(resp => resp.json());
|
|
expect(deployment.target).toBe('staging');
|
|
});
|
|
|
|
test('create a production deployment', async () => {
|
|
const directory = await setupE2EFixture('static-deployment');
|
|
|
|
const args = ['--debug', '--public', '--name', session];
|
|
const targetCall = await execCli(binaryPath, [
|
|
directory,
|
|
'--target=production',
|
|
...args,
|
|
'--yes',
|
|
]);
|
|
|
|
expect(targetCall.exitCode, formatOutput(targetCall)).toBe(0);
|
|
expect(targetCall.stderr).toMatch(/`--prod` option instead/gm);
|
|
expect(targetCall.stderr).toMatch(/Setting target to production/gm);
|
|
expect(targetCall.stderr).toMatch(/Inspect: https:\/\/vercel.com\//gm);
|
|
expect(targetCall.stdout).toMatch(/https:\/\//gm);
|
|
|
|
const { host: targetHost } = new URL(targetCall.stdout);
|
|
const targetDeployment = await apiFetch(
|
|
`/v10/now/deployments/unknown?url=${targetHost}`
|
|
).then(resp => resp.json());
|
|
expect(targetDeployment.target).toBe('production');
|
|
|
|
const call = await execCli(binaryPath, [directory, '--prod', ...args]);
|
|
|
|
expect(call.exitCode, formatOutput(call)).toBe(0);
|
|
expect(call.stderr).toMatch(/Setting target to production/gm);
|
|
expect(call.stdout).toMatch(/https:\/\//gm);
|
|
|
|
const { host } = new URL(call.stdout);
|
|
const deployment = await apiFetch(
|
|
`/v10/now/deployments/unknown?url=${host}`
|
|
).then(resp => resp.json());
|
|
expect(deployment.target).toBe('production');
|
|
});
|
|
|
|
test('try to deploy non-existing path', async () => {
|
|
const goal = `Error: Could not find “${humanizePath(
|
|
path.join(process.cwd(), session)
|
|
)}”`;
|
|
|
|
const { stderr, stdout, exitCode } = await execCli(binaryPath, [
|
|
session,
|
|
'--yes',
|
|
]);
|
|
|
|
expect(exitCode, formatOutput({ stdout, stderr })).toBe(1);
|
|
expect(stderr.trim().endsWith(goal), `should end with "${goal}"`).toBe(true);
|
|
});
|
|
|
|
test('try to deploy with non-existing team', async () => {
|
|
const target = await setupE2EFixture('static-deployment');
|
|
const goal = `Error: The specified scope does not exist`;
|
|
|
|
const { stderr, stdout, exitCode } = await execCli(binaryPath, [
|
|
target,
|
|
'--scope',
|
|
session,
|
|
'--yes',
|
|
]);
|
|
|
|
expect(exitCode, formatOutput({ stdout, stderr })).toBe(1);
|
|
expect(stderr).toContain(goal);
|
|
});
|
|
|
|
test('initialize example "angular"', async () => {
|
|
const cwd = getNewTmpDir();
|
|
const goal = '> Success! Initialized "angular" example in';
|
|
|
|
const { exitCode, stdout, stderr } = await execCli(
|
|
binaryPath,
|
|
['init', 'angular'],
|
|
{ cwd }
|
|
);
|
|
|
|
expect(exitCode, formatOutput({ stdout, stderr })).toBe(0);
|
|
expect(stderr).toContain(goal);
|
|
|
|
expect(
|
|
fs.existsSync(path.join(cwd, 'angular', 'package.json')),
|
|
'package.json'
|
|
).toBe(true);
|
|
expect(
|
|
fs.existsSync(path.join(cwd, 'angular', 'tsconfig.json')),
|
|
'tsconfig.json'
|
|
).toBe(true);
|
|
expect(
|
|
fs.existsSync(path.join(cwd, 'angular', 'angular.json')),
|
|
'angular.json'
|
|
).toBe(true);
|
|
});
|
|
|
|
test('fail to add a domain without a project', async () => {
|
|
const output = await execCli(binaryPath, [
|
|
'domains',
|
|
'add',
|
|
'my-domain.vercel.app',
|
|
]);
|
|
expect(output.exitCode, formatOutput(output)).toBe(1);
|
|
expect(output.stderr).toMatch(/expects two arguments/gm);
|
|
});
|
|
|
|
test('try to revert a deployment and assign the automatic aliases', async () => {
|
|
const firstDeployment = await setupE2EFixture('now-revert-alias-1');
|
|
const secondDeployment = await setupE2EFixture('now-revert-alias-2');
|
|
|
|
const { name } = JSON.parse(
|
|
fs.readFileSync(path.join(firstDeployment, 'now.json')).toString()
|
|
) as NowJson;
|
|
expect(name).toBeTruthy();
|
|
|
|
const url = `https://${name}.user.vercel.app`;
|
|
|
|
{
|
|
const { exitCode, stdout, stderr } = await execCli(binaryPath, [
|
|
firstDeployment,
|
|
'--yes',
|
|
]);
|
|
const deploymentUrl = stdout;
|
|
|
|
expect(exitCode, formatOutput({ stdout, stderr })).toBe(0);
|
|
|
|
await waitForDeployment(deploymentUrl);
|
|
await sleep(20000);
|
|
|
|
const result = await fetch(url).then(r => r.json());
|
|
|
|
expect(result.name).toBe('now-revert-alias-1');
|
|
}
|
|
|
|
{
|
|
const { exitCode, stdout, stderr } = await execCli(binaryPath, [
|
|
secondDeployment,
|
|
'--yes',
|
|
]);
|
|
const deploymentUrl = stdout;
|
|
|
|
expect(exitCode, formatOutput({ stdout, stderr })).toBe(0);
|
|
|
|
await waitForDeployment(deploymentUrl);
|
|
await sleep(20000);
|
|
await fetch(url);
|
|
await sleep(5000);
|
|
|
|
const result = await fetch(url).then(r => r.json());
|
|
|
|
expect(result.name).toBe('now-revert-alias-2');
|
|
}
|
|
|
|
{
|
|
const { exitCode, stdout, stderr } = await execCli(binaryPath, [
|
|
firstDeployment,
|
|
'--yes',
|
|
]);
|
|
const deploymentUrl = stdout;
|
|
|
|
expect(exitCode, formatOutput({ stdout, stderr })).toBe(0);
|
|
|
|
await waitForDeployment(deploymentUrl);
|
|
await sleep(20000);
|
|
await fetch(url);
|
|
await sleep(5000);
|
|
|
|
const result = await fetch(url).then(r => r.json());
|
|
|
|
expect(result.name).toBe('now-revert-alias-1');
|
|
}
|
|
});
|
|
|
|
test('whoami', async () => {
|
|
const { exitCode, stdout, stderr } = await execCli(binaryPath, ['whoami']);
|
|
|
|
expect(exitCode, formatOutput({ stdout, stderr })).toBe(0);
|
|
expect(stdout).toBe(contextName);
|
|
});
|
|
|
|
test('[vercel dev] fails when dev script calls vercel dev recursively', async () => {
|
|
const deploymentPath = await setupE2EFixture('now-dev-fail-dev-script');
|
|
const { exitCode, stdout, stderr } = await execCli(binaryPath, [
|
|
'dev',
|
|
deploymentPath,
|
|
]);
|
|
|
|
expect(exitCode, formatOutput({ stdout, stderr })).toBe(1);
|
|
expect(stderr).toContain('must not recursively invoke itself');
|
|
});
|
|
|
|
test('[vercel dev] fails when development command calls vercel dev recursively', async () => {
|
|
expect.assertions(0);
|
|
|
|
const dir = await setupE2EFixture('dev-fail-on-recursion-command');
|
|
const dev = execCli(binaryPath, ['dev', '--yes'], {
|
|
cwd: dir,
|
|
});
|
|
|
|
try {
|
|
await waitForPrompt(dev, 'must not recursively invoke itself', 10000);
|
|
} finally {
|
|
const onClose = once(dev, 'close');
|
|
dev.kill();
|
|
await onClose;
|
|
}
|
|
});
|
|
|
|
test('`vercel rm` removes a deployment', async () => {
|
|
const directory = await setupE2EFixture('static-deployment');
|
|
|
|
let host;
|
|
|
|
{
|
|
const { exitCode, stdout, stderr } = await execCli(binaryPath, [
|
|
directory,
|
|
'--public',
|
|
'--name',
|
|
session,
|
|
'--force',
|
|
'--yes',
|
|
]);
|
|
expect(exitCode, formatOutput({ stdout, stderr })).toBe(0);
|
|
host = new URL(stdout).host;
|
|
}
|
|
|
|
{
|
|
const { exitCode, stdout, stderr } = await execCli(binaryPath, [
|
|
'rm',
|
|
host,
|
|
'--yes',
|
|
]);
|
|
|
|
expect(stderr).toContain(host);
|
|
expect(exitCode, formatOutput({ stdout, stderr })).toBe(0);
|
|
}
|
|
});
|
|
|
|
test('`vercel rm` should fail with unexpected option', async () => {
|
|
const output = await execCli(binaryPath, [
|
|
'rm',
|
|
'example.example.com',
|
|
'--fake',
|
|
]);
|
|
|
|
expect(output.exitCode, formatOutput(output)).toBe(1);
|
|
expect(output.stderr).toMatch(
|
|
/Error: unknown or unexpected option: --fake/gm
|
|
);
|
|
});
|
|
|
|
test('`vercel rm` 404 exits quickly', async () => {
|
|
const start = Date.now();
|
|
const { exitCode, stderr, stdout } = await execCli(binaryPath, [
|
|
'rm',
|
|
'this.is.a.deployment.that.does.not.exist.example.com',
|
|
]);
|
|
|
|
const delta = Date.now() - start;
|
|
|
|
// "does not exist" case is exit code 1, similar to Unix `rm`
|
|
expect(exitCode, formatOutput({ stdout, stderr })).toBe(1);
|
|
expect(
|
|
stderr.includes(
|
|
'Could not find any deployments or projects matching "this.is.a.deployment.that.does.not.exist.example.com"'
|
|
)
|
|
).toBeTruthy();
|
|
|
|
// "quickly" meaning < 5 seconds, because it used to hang from a previous bug
|
|
expect(delta < 5000).toBeTruthy();
|
|
});
|
|
|
|
test('render build errors', async () => {
|
|
const deploymentPath = await setupE2EFixture('failing-build');
|
|
const output = await execCli(binaryPath, [deploymentPath, '--yes']);
|
|
|
|
expect(output.exitCode, formatOutput(output)).toBe(1);
|
|
expect(output.stderr).toMatch(/Command "npm run build" exited with 1/gm);
|
|
});
|
|
|
|
test('invalid deployment, projects and alias names', async () => {
|
|
const check = async (...args: string[]) => {
|
|
const output = await execCli(binaryPath, args);
|
|
expect(output.exitCode, formatOutput(output)).toBe(1);
|
|
expect(output.stderr).toMatch(/The provided argument/gm);
|
|
};
|
|
|
|
await Promise.all([
|
|
check('alias', '/', 'test'),
|
|
check('alias', 'test', '/'),
|
|
check('rm', '/'),
|
|
check('ls', '/'),
|
|
]);
|
|
});
|
|
|
|
test('vercel certs ls', async () => {
|
|
const output = await execCli(binaryPath, ['certs', 'ls']);
|
|
|
|
expect(output.exitCode, formatOutput(output)).toBe(0);
|
|
expect(output.stderr).toMatch(/certificates? found under/gm);
|
|
});
|
|
|
|
test('vercel certs ls --next=123456', async () => {
|
|
const output = await execCli(binaryPath, ['certs', 'ls', '--next=123456']);
|
|
|
|
expect(output.exitCode, formatOutput(output)).toBe(0);
|
|
expect(output.stderr).toMatch(/No certificates found under/gm);
|
|
});
|
|
|
|
test('vercel hasOwnProperty not a valid subcommand', async () => {
|
|
const output = await execCli(binaryPath, ['hasOwnProperty']);
|
|
|
|
expect(output.exitCode, formatOutput(output)).toBe(1);
|
|
expect(
|
|
output.stderr.endsWith(
|
|
`Error: Could not find “${humanizePath(
|
|
path.join(process.cwd(), 'hasOwnProperty')
|
|
)}”`
|
|
)
|
|
).toEqual(true);
|
|
});
|
|
|
|
test('create zero-config deployment', async () => {
|
|
const fixturePath = await setupE2EFixture('zero-config-next-js');
|
|
const output = await execCli(binaryPath, [
|
|
fixturePath,
|
|
'--force',
|
|
'--public',
|
|
'--yes',
|
|
]);
|
|
|
|
expect(output.exitCode, formatOutput(output)).toBe(0);
|
|
|
|
const { host } = new URL(output.stdout);
|
|
const response = await apiFetch(`/v10/now/deployments/unkown?url=${host}`);
|
|
|
|
const text = await response.text();
|
|
|
|
expect(response.status).toBe(200);
|
|
const data = JSON.parse(text) as DeploymentLike;
|
|
|
|
expect(data.error).toBe(undefined);
|
|
|
|
const validBuilders = data.builds.every(
|
|
build => !build.use.endsWith('@canary')
|
|
);
|
|
|
|
expect(validBuilders).toBe(true);
|
|
});
|
|
|
|
test('next unsupported functions config shows warning link', async () => {
|
|
const fixturePath = await setupE2EFixture(
|
|
'zero-config-next-js-functions-warning'
|
|
);
|
|
const output = await execCli(binaryPath, [
|
|
fixturePath,
|
|
'--force',
|
|
'--public',
|
|
'--yes',
|
|
]);
|
|
|
|
expect(output.exitCode, formatOutput(output)).toBe(0);
|
|
expect(output.stderr).toMatch(
|
|
/Ignoring function property `runtime`\. When using Next\.js, only `memory` and `maxDuration` can be used\./gm
|
|
);
|
|
expect(output.stderr).toMatch(
|
|
/Learn More: https:\/\/vercel\.link\/functions-property-next/gm
|
|
);
|
|
});
|
|
|
|
test('vercel secret add', async () => {
|
|
secretName = `my-secret-${Date.now().toString(36)}`;
|
|
const value = 'https://my-secret-endpoint.com';
|
|
|
|
const output = await execCli(binaryPath, [
|
|
'secret',
|
|
'add',
|
|
secretName,
|
|
value,
|
|
]);
|
|
expect(output.exitCode, formatOutput(output)).toBe(0);
|
|
});
|
|
|
|
test('vercel secret ls', async () => {
|
|
const output = await execCli(binaryPath, ['secret', 'ls']);
|
|
expect(output.exitCode, formatOutput(output)).toBe(0);
|
|
expect(output.stderr).toMatch(/Secrets found under/gm);
|
|
});
|
|
|
|
test('vercel secret ls --test-warning', async () => {
|
|
const output = await execCli(binaryPath, ['secret', 'ls', '--test-warning']);
|
|
expect(output.exitCode, formatOutput(output)).toBe(0);
|
|
expect(output.stderr).toMatch(/Test warning message./gm);
|
|
expect(output.stderr).toMatch(/Learn more: https:\/\/vercel.com/gm);
|
|
expect(output.stderr).toMatch(/No secrets found under/gm);
|
|
});
|
|
|
|
test('vercel secret rename', async () => {
|
|
if (!secretName) {
|
|
throw new Error('Shared state "secretName" not set.');
|
|
}
|
|
|
|
const nextName = `renamed-secret-${Date.now().toString(36)}`;
|
|
const output = await execCli(binaryPath, [
|
|
'secret',
|
|
'rename',
|
|
secretName,
|
|
nextName,
|
|
]);
|
|
expect(output.exitCode, formatOutput(output)).toBe(0);
|
|
|
|
secretName = nextName;
|
|
});
|
|
|
|
test('vercel secret rm', async () => {
|
|
if (!secretName) {
|
|
throw new Error('Shared state "secretName" not set.');
|
|
}
|
|
|
|
const output = await execCli(binaryPath, ['secret', 'rm', secretName, '-y']);
|
|
expect(output.exitCode, formatOutput(output)).toBe(0);
|
|
});
|
|
|
|
test('deploy a Lambda with 128MB of memory', async () => {
|
|
const directory = await setupE2EFixture('lambda-with-128-memory');
|
|
const output = await execCli(binaryPath, [directory, '--yes']);
|
|
|
|
expect(output.exitCode, formatOutput(output)).toBe(0);
|
|
|
|
const { host: url } = new URL(output.stdout);
|
|
const response = await fetch('https://' + url + '/api/memory');
|
|
|
|
expect(response.status).toBe(200);
|
|
|
|
// It won't be exactly 128MB,
|
|
// so we just compare if it is lower than 450MB
|
|
const { memory } = await response.json();
|
|
expect(memory).toBe(128);
|
|
});
|
|
|
|
test('fail to deploy a Lambda with an incorrect value for of memory', async () => {
|
|
const directory = await setupE2EFixture('lambda-with-123-memory');
|
|
const output = await execCli(binaryPath, [directory, '--yes']);
|
|
|
|
expect(output.exitCode, formatOutput(output)).toBe(1);
|
|
expect(output.stderr).toMatch(/Serverless Functions.+memory/gm);
|
|
expect(output.stderr).toMatch(/Learn More/gm);
|
|
});
|
|
|
|
test('deploy a Lambda with 3 seconds of maxDuration', async () => {
|
|
const directory = await setupE2EFixture('lambda-with-3-second-timeout');
|
|
const output = await execCli(binaryPath, [directory, '--yes']);
|
|
|
|
expect(output.exitCode, formatOutput(output)).toBe(0);
|
|
|
|
const url = new URL(output.stdout);
|
|
|
|
// Should time out
|
|
url.pathname = '/api/wait-for/5';
|
|
const response1 = await fetch(url.href);
|
|
expect(response1.status).toBe(504);
|
|
|
|
// Should not time out
|
|
url.pathname = '/api/wait-for/1';
|
|
const response2 = await fetch(url.href);
|
|
expect(response2.status).toBe(200);
|
|
});
|
|
|
|
test('fail to deploy a Lambda with an incorrect value for maxDuration', async () => {
|
|
const directory = await setupE2EFixture('lambda-with-1000-second-timeout');
|
|
const output = await execCli(binaryPath, [directory, '--yes']);
|
|
|
|
expect(output.exitCode, formatOutput(output)).toBe(1);
|
|
expect(output.stderr).toMatch(
|
|
/maxDuration must be between \d+ second and \d+ seconds/gm
|
|
);
|
|
});
|
|
|
|
test('invalid `--token`', async () => {
|
|
const output = await execCli(binaryPath, ['--token', 'he\nl,o.']);
|
|
|
|
expect(output.exitCode, formatOutput(output)).toBe(1);
|
|
expect(output.stderr).toContain(
|
|
'Error: You defined "--token", but its contents are invalid. Must not contain: "\\n", ",", "."'
|
|
);
|
|
});
|
|
|
|
test('deploy a Lambda with a specific runtime', async () => {
|
|
const directory = await setupE2EFixture('lambda-with-php-runtime');
|
|
const output = await execCli(binaryPath, [directory, '--public', '--yes']);
|
|
|
|
expect(output.exitCode, formatOutput(output)).toBe(0);
|
|
|
|
const url = new URL(output.stdout);
|
|
const res = await fetch(`${url}/api/test`);
|
|
const text = await res.text();
|
|
expect(text).toBe('Hello from PHP');
|
|
});
|
|
|
|
test('fail to deploy a Lambda with a specific runtime but without a locked version', async () => {
|
|
const directory = await setupE2EFixture('lambda-with-invalid-runtime');
|
|
const output = await execCli(binaryPath, [directory, '--yes']);
|
|
|
|
expect(output.exitCode, formatOutput(output)).toBe(1);
|
|
expect(output.stderr).toMatch(
|
|
/Function Runtimes must have a valid version/gim
|
|
);
|
|
});
|
|
|
|
test('use build-env', async () => {
|
|
const directory = await setupE2EFixture('build-env');
|
|
|
|
const { exitCode, stdout, stderr } = await execCli(binaryPath, [
|
|
directory,
|
|
'--public',
|
|
'--yes',
|
|
]);
|
|
|
|
// Ensure the exit code is right
|
|
expect(exitCode, formatOutput({ stdout, stderr })).toBe(0);
|
|
|
|
// 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();
|
|
expect(content.trim()).toBe('bar');
|
|
});
|
|
|
|
test('should invoke CLI extension', async () => {
|
|
const fixture = path.join(__dirname, 'fixtures/e2e/cli-extension');
|
|
|
|
// Ensure the `.bin` is populated in the fixture
|
|
await runNpmInstall(fixture);
|
|
|
|
const output = await execCli(binaryPath, ['mywhoami'], { cwd: fixture });
|
|
const formatted = formatOutput(output);
|
|
expect(output.stdout, formatted).toContain('Hello from a CLI extension!');
|
|
expect(output.stdout, formatted).toContain('VERCEL_API: http://127.0.0.1:');
|
|
expect(output.stdout, formatted).toContain(`Username: ${contextName}`);
|
|
});
|
|
|
|
test('should pass through exit code for CLI extension', async () => {
|
|
const fixture = path.join(__dirname, 'fixtures/e2e/cli-extension-exit-code');
|
|
|
|
// Ensure the `.bin` is populated in the fixture
|
|
await runNpmInstall(fixture);
|
|
|
|
const output = await execCli(binaryPath, ['fail'], {
|
|
cwd: fixture,
|
|
reject: false,
|
|
});
|
|
expect(output.exitCode).toEqual(6);
|
|
});
|
|
|
|
// NOTE: Order matters here. This must be the last test in the file.
|
|
test('default command should prompt login with empty auth.json', async () => {
|
|
await clearAuthConfig();
|
|
const output = await execCli(binaryPath);
|
|
expect(output.stderr, formatOutput(output)).toBeTruthy();
|
|
expect(output.stderr).toContain(
|
|
'Error: No existing credentials found. Please run `vercel login` or pass "--token"'
|
|
);
|
|
});
|