Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 73 additions & 2 deletions packages/catalyst/src/cli/commands/logs.spec.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
import { Command } from 'commander';
import Conf from 'conf';
import { http, HttpResponse } from 'msw';
import { afterEach, beforeAll, describe, expect, MockInstance, test, vi } from 'vitest';
import { afterAll, afterEach, beforeAll, describe, expect, MockInstance, test, vi } from 'vitest';

import { server } from '../../../tests/mocks/node';
import { consola } from '../lib/logger';
import { mkTempDir } from '../lib/mk-temp-dir';
import { getProjectConfig, ProjectConfigSchema } from '../lib/project-config';
import { program } from '../program';

import { logs, parseSSEEvent, tailLogs } from './logs';

let exitMock: MockInstance;
let stdoutWriteMock: MockInstance;

let tmpDir: string;
let cleanup: () => Promise<void>;
let config: Conf<ProjectConfigSchema>;

const projectUuid = '6b202364-10f3-11f1-8bc7-fe9b9d8b14ab';
const storeHash = 'test-store';
const accessToken = 'test-token';
Expand Down Expand Up @@ -69,15 +76,28 @@ const callTailLogs = async (format: Parameters<typeof tailLogs>[4], events?: str
});
};

beforeAll(() => {
beforeAll(async () => {
consola.mockTypes(() => vi.fn());
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
exitMock = vi.spyOn(process, 'exit').mockImplementation(() => null as never);
stdoutWriteMock = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);

[tmpDir, cleanup] = await mkTempDir();

vi.spyOn(process, 'cwd').mockReturnValue(tmpDir);

config = getProjectConfig();
});

afterEach(() => {
vi.clearAllMocks();
config.delete('storeHash');
config.delete('accessToken');
config.delete('projectUuid');
});

afterAll(async () => {
await cleanup();
});

describe('command configuration', () => {
Expand Down Expand Up @@ -486,6 +506,39 @@ describe('retry and reconnect', () => {
});
});

describe('credential resolution', () => {
test('falls back to project.json for storeHash and accessToken', async () => {
config.set('storeHash', storeHash);
config.set('accessToken', accessToken);

server.use(createOneShotLogHandler([`data: ${JSON.stringify(validLogEvent)}\n\n`]));

await program.parseAsync(['node', 'catalyst', 'logs', 'tail', '--project-uuid', projectUuid]);

expect(consola.info).toHaveBeenCalledWith('Tailing logs...');
expect(consola.log).toHaveBeenCalledWith(expect.stringContaining('hello world'));
});

test('exits with error when no credentials are provided', async () => {
const savedStoreHash = process.env.CATALYST_STORE_HASH;
const savedAccessToken = process.env.CATALYST_ACCESS_TOKEN;

delete process.env.CATALYST_STORE_HASH;
delete process.env.CATALYST_ACCESS_TOKEN;

await program.parseAsync(['node', 'catalyst', 'logs', 'tail', '--project-uuid', projectUuid]);

if (savedStoreHash !== undefined) process.env.CATALYST_STORE_HASH = savedStoreHash;
if (savedAccessToken !== undefined) process.env.CATALYST_ACCESS_TOKEN = savedAccessToken;

expect(consola.error).toHaveBeenCalledWith('Missing credentials.');
expect(consola.info).toHaveBeenCalledWith(
'Run `catalyst auth login`, or provide --store-hash and --access-token flags (or set CATALYST_STORE_HASH and CATALYST_ACCESS_TOKEN environment variables).',
);
expect(exitMock).toHaveBeenCalledWith(1);
});
});

describe('query subcommand', () => {
test('exits with error as not yet implemented', async () => {
await program.parseAsync([
Expand All @@ -502,6 +555,24 @@ describe('query subcommand', () => {
expect(consola.error).toHaveBeenCalledWith('The query command is not yet implemented.');
expect(exitMock).toHaveBeenCalledWith(1);
});

test('exits with missing credentials error when none are provided', async () => {
const savedStoreHash = process.env.CATALYST_STORE_HASH;
const savedAccessToken = process.env.CATALYST_ACCESS_TOKEN;

delete process.env.CATALYST_STORE_HASH;
delete process.env.CATALYST_ACCESS_TOKEN;

await expect(program.parseAsync(['node', 'catalyst', 'logs', 'query'])).rejects.toThrow(
'Missing credentials',
);

if (savedStoreHash !== undefined) process.env.CATALYST_STORE_HASH = savedStoreHash;
if (savedAccessToken !== undefined) process.env.CATALYST_ACCESS_TOKEN = savedAccessToken;

expect(consola.error).toHaveBeenCalledWith('Missing credentials.');
expect(exitMock).toHaveBeenCalledWith(1);
});
});

describe('program integration', () => {
Expand Down
30 changes: 16 additions & 14 deletions packages/catalyst/src/cli/commands/logs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { colorize } from 'consola/utils';
import { z } from 'zod';

import { consola } from '../lib/logger';
import { getProjectConfig } from '../lib/project-config';
import { resolveCredentials } from '../lib/resolve-credentials';
import {
accessTokenOption,
apiHostOption,
Expand Down Expand Up @@ -245,8 +247,8 @@ Examples:
# Tail logs as raw JSON (useful for piping to other tools)
$ catalyst logs tail --format json`,
)
.addOption(storeHashOption().makeOptionMandatory())
.addOption(accessTokenOption().makeOptionMandatory())
.addOption(storeHashOption())
.addOption(accessTokenOption())
.addOption(apiHostOption())
.addOption(projectUuidOption())
.addOption(
Expand All @@ -256,17 +258,14 @@ Examples:
)
.action(async (options) => {
try {
await telemetry.identify(options.storeHash);
const config = getProjectConfig();
const { storeHash, accessToken } = resolveCredentials(options, config);

await telemetry.identify(storeHash);

const projectUuid = resolveProjectUuid(options);

await tailLogs(
projectUuid,
options.storeHash,
options.accessToken,
options.apiHost,
options.format,
);
await tailLogs(projectUuid, storeHash, accessToken, options.apiHost, options.format);
} catch (error) {
consola.error(error);
process.exit(1);
Expand All @@ -281,12 +280,15 @@ const query = new Command('query')
Example:
$ catalyst logs query`,
)
.addOption(storeHashOption().makeOptionMandatory())
.addOption(accessTokenOption().makeOptionMandatory())
.addOption(storeHashOption())
.addOption(accessTokenOption())
.addOption(apiHostOption())
.addOption(projectUuidOption())
// eslint-disable-next-line @typescript-eslint/no-unused-vars
.action((_options) => {
.action((options) => {
const config = getProjectConfig();

resolveCredentials(options, config);

Comment thread
chanceaclark marked this conversation as resolved.
consola.error('The query command is not yet implemented.');
process.exit(1);
});
Expand Down
6 changes: 3 additions & 3 deletions packages/catalyst/src/cli/lib/shared-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ export const storeHashOption = () =>
new Option(
'--store-hash <hash>',
'BigCommerce store hash. Can be found in the URL of your store Control Panel.',
).env('BIGCOMMERCE_STORE_HASH');
).env('CATALYST_STORE_HASH');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the other env vars in these options also be prefixed with CATALYST_?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Included CATALYST_PROJECT_UUID


export const accessTokenOption = () =>
new Option(
'--access-token <token>',
'BigCommerce access token. Can be found after creating a store-level API account.',
).env('BIGCOMMERCE_ACCESS_TOKEN');
).env('CATALYST_ACCESS_TOKEN');

export const apiHostOption = () =>
new Option('--api-host <host>', 'BigCommerce API host. The default is api.bigcommerce.com.')
Expand All @@ -24,7 +24,7 @@ export const projectUuidOption = () =>
new Option(
'--project-uuid <uuid>',
'BigCommerce infrastructure project UUID. Can be found via the BigCommerce API (GET /v3/infrastructure/projects).',
).env('BIGCOMMERCE_PROJECT_UUID');
).env('CATALYST_PROJECT_UUID');

export const resolveProjectUuid = (options: { projectUuid?: string }) => {
const config = getProjectConfig();
Expand Down
Loading