mirror of
https://github.com/LukeHagar/vercel.git
synced 2025-12-10 21:07:48 +00:00
This PR is a follow-up to #8039, which provides an intuitive syntax for writing unit tests for interactive CLI commands. The heart of this is the new `await expect(stream).toOutput(test)` custom Jest matcher, which is intended for use with the mock Client `stdout` and `stderr` stream properties. The `test` is a string that will wait for the stream to output via "data" events until a match is found, or it will timeout (after 3 seconds by default). The timeout error has nice Jest-style formatting so that you can easily identify what was output: <img width="553" alt="Screen Shot 2022-06-29 at 10 33 06 PM" src="https://user-images.githubusercontent.com/71256/176600324-cb1ebecb-e891-42d9-bdc9-4864d3594a8c.png"> Below is an example of a unit test that was added for an interactive `vc login` session: ```typescript it('should allow login via email', async () => { const user = useUser(); const exitCodePromise = login(client); // Wait for login interactive prompt await expect(client.stderr).toOutput(`> Log in to Vercel`); // Move down to "Email" option client.stdin.write('\x1B[B'); // Down arrow client.stdin.write('\x1B[B'); // Down arrow client.stdin.write('\x1B[B'); // Down arrow client.stdin.write('\r'); // Return key // Wait for email input prompt await expect(client.stderr).toOutput('> Enter your email address:'); // Write user email address into prompt client.stdin.write(`${user.email}\n`); // Wait for login success message await expect(client.stderr).toOutput( `Success! Email authentication complete for ${user.email}` ); // Assert that the `login()` command returned 0 exit code await expect(exitCodePromise).resolves.toEqual(0); }); ``` **Note:** as a consequence of this PR, prompts are now written to stderr instead of stdout, so this change _may_ be considered a breaking change, in which case we should tag it for major release.
179 lines
5.9 KiB
TypeScript
179 lines
5.9 KiB
TypeScript
import fs from 'fs-extra';
|
|
import path from 'path';
|
|
import pull from '../../../src/commands/pull';
|
|
import { setupFixture } from '../../helpers/setup-fixture';
|
|
import { client } from '../../mocks/client';
|
|
import { defaultProject, useProject } from '../../mocks/project';
|
|
import { useTeams } from '../../mocks/team';
|
|
import { useUser } from '../../mocks/user';
|
|
|
|
describe('pull', () => {
|
|
it('should handle pulling', async () => {
|
|
const cwd = setupFixture('vercel-pull-next');
|
|
useUser();
|
|
useTeams('team_dummy');
|
|
useProject({
|
|
...defaultProject,
|
|
id: 'vercel-pull-next',
|
|
name: 'vercel-pull-next',
|
|
});
|
|
client.setArgv('pull', cwd);
|
|
const exitCodePromise = pull(client);
|
|
await expect(client.stderr).toOutput(
|
|
'Downloading "development" Environment Variables for Project vercel-pull-next'
|
|
);
|
|
await expect(client.stderr).toOutput(
|
|
`Created .vercel${path.sep}.env.development.local file`
|
|
);
|
|
await expect(client.stderr).toOutput(
|
|
`Downloaded project settings to .vercel${path.sep}project.json`
|
|
);
|
|
await expect(exitCodePromise).resolves.toEqual(0);
|
|
|
|
const rawDevEnv = await fs.readFile(
|
|
path.join(cwd, '.vercel', '.env.development.local')
|
|
);
|
|
const devFileHasDevEnv = rawDevEnv.toString().includes('SPECIAL_FLAG');
|
|
expect(devFileHasDevEnv).toBeTruthy();
|
|
});
|
|
|
|
it('should fail with message to pull without a link and without --env', async () => {
|
|
client.stdin.isTTY = false;
|
|
|
|
const cwd = setupFixture('vercel-pull-unlinked');
|
|
useUser();
|
|
useTeams('team_dummy');
|
|
|
|
client.setArgv('pull', cwd);
|
|
const exitCodePromise = pull(client);
|
|
await expect(client.stderr).toOutput(
|
|
'Command `vercel pull` requires confirmation. Use option "--yes" to confirm.'
|
|
);
|
|
await expect(exitCodePromise).resolves.toEqual(1);
|
|
});
|
|
|
|
it('should fail without message to pull without a link and with --env', async () => {
|
|
const cwd = setupFixture('vercel-pull-next');
|
|
useUser();
|
|
useTeams('team_dummy');
|
|
|
|
client.setArgv('pull', cwd, '--yes');
|
|
const exitCodePromise = pull(client);
|
|
await expect(client.stderr).not.toOutput(
|
|
'Command `vercel pull` requires confirmation. Use option "--yes" to confirm.'
|
|
);
|
|
await expect(exitCodePromise).resolves.toEqual(1);
|
|
});
|
|
|
|
it('should handle pulling with env vars (headless mode)', async () => {
|
|
try {
|
|
process.env.VERCEL_PROJECT_ID = 'vercel-pull-next';
|
|
process.env.VERCEL_ORG_ID = 'team_dummy';
|
|
|
|
const cwd = setupFixture('vercel-pull-next');
|
|
|
|
// Remove the `.vercel` dir to ensure that the `pull`
|
|
// command creates a new one based on env vars
|
|
await fs.remove(path.join(cwd, '.vercel'));
|
|
|
|
useUser();
|
|
useTeams('team_dummy');
|
|
useProject({
|
|
...defaultProject,
|
|
id: 'vercel-pull-next',
|
|
name: 'vercel-pull-next',
|
|
});
|
|
client.setArgv('pull', cwd);
|
|
const exitCodePromise = pull(client);
|
|
await expect(client.stderr).toOutput(
|
|
'Downloading "development" Environment Variables for Project vercel-pull-next'
|
|
);
|
|
await expect(client.stderr).toOutput(
|
|
`Created .vercel${path.sep}.env.development.local file`
|
|
);
|
|
await expect(client.stderr).toOutput(
|
|
`Downloaded project settings to .vercel${path.sep}project.json`
|
|
);
|
|
await expect(exitCodePromise).resolves.toEqual(0);
|
|
|
|
const config = await fs.readJSON(path.join(cwd, '.vercel/project.json'));
|
|
expect(config).toMatchInlineSnapshot(`
|
|
Object {
|
|
"orgId": "team_dummy",
|
|
"projectId": "vercel-pull-next",
|
|
"settings": Object {},
|
|
}
|
|
`);
|
|
} finally {
|
|
delete process.env.VERCEL_PROJECT_ID;
|
|
delete process.env.VERCEL_ORG_ID;
|
|
}
|
|
});
|
|
|
|
it('should handle --environment=preview flag', async () => {
|
|
const cwd = setupFixture('vercel-pull-next');
|
|
useUser();
|
|
useTeams('team_dummy');
|
|
useProject({
|
|
...defaultProject,
|
|
id: 'vercel-pull-next',
|
|
name: 'vercel-pull-next',
|
|
});
|
|
client.setArgv('pull', '--environment=preview', cwd);
|
|
const exitCodePromise = pull(client);
|
|
await expect(client.stderr).toOutput(
|
|
'Downloading "preview" Environment Variables for Project vercel-pull-next'
|
|
);
|
|
await expect(client.stderr).toOutput(
|
|
`Created .vercel${path.sep}.env.preview.local file`
|
|
);
|
|
await expect(client.stderr).toOutput(
|
|
`Downloaded project settings to .vercel${path.sep}project.json`
|
|
);
|
|
await expect(exitCodePromise).resolves.toEqual(0);
|
|
|
|
const rawPreviewEnv = await fs.readFile(
|
|
path.join(cwd, '.vercel', '.env.preview.local')
|
|
);
|
|
const previewFileHasPreviewEnv = rawPreviewEnv
|
|
.toString()
|
|
.includes('REDIS_CONNECTION_STRING');
|
|
expect(previewFileHasPreviewEnv).toBeTruthy();
|
|
});
|
|
|
|
it('should handle --environment=production flag', async () => {
|
|
const cwd = setupFixture('vercel-pull-next');
|
|
useUser();
|
|
useTeams('team_dummy');
|
|
useProject({
|
|
...defaultProject,
|
|
id: 'vercel-pull-next',
|
|
name: 'vercel-pull-next',
|
|
});
|
|
client.setArgv('pull', '--environment=production', cwd);
|
|
const exitCodePromise = pull(client);
|
|
await expect(client.stderr).toOutput(
|
|
'Downloading "production" Environment Variables for Project vercel-pull-next'
|
|
);
|
|
await expect(client.stderr).toOutput(
|
|
`Created .vercel${path.sep}.env.production.local file`
|
|
);
|
|
await expect(client.stderr).toOutput(
|
|
`Downloaded project settings to .vercel${path.sep}project.json`
|
|
);
|
|
await expect(exitCodePromise).resolves.toEqual(0);
|
|
|
|
const rawProdEnv = await fs.readFile(
|
|
path.join(cwd, '.vercel', '.env.production.local')
|
|
);
|
|
const previewFileHasPreviewEnv1 = rawProdEnv
|
|
.toString()
|
|
.includes('REDIS_CONNECTION_STRING');
|
|
expect(previewFileHasPreviewEnv1).toBeTruthy();
|
|
const previewFileHasPreviewEnv2 = rawProdEnv
|
|
.toString()
|
|
.includes('SQL_CONNECTION_STRING');
|
|
expect(previewFileHasPreviewEnv2).toBeTruthy();
|
|
});
|
|
});
|