wgc
Version:
The official CLI tool to manage the GraphQL Federation Platform Cosmo
396 lines • 18.2 kB
JavaScript
import { Command } from 'commander';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { create } from '@bufbuild/protobuf';
import { createClient, createRouterTransport } from '@connectrpc/connect';
import { FederatedGraphSchema, SubgraphSchema, PlatformService, } from '@wundergraph/cosmo-connect/dist/platform/v1/platform_pb';
import { EnumStatusCode } from '@wundergraph/cosmo-connect/dist/common/common_pb';
import DemoCommand from '../../src/commands/demo/index.js';
import { waitForKeyPress } from '../../src/utils.js';
import * as demoUtil from '../../src/commands/demo/util.js';
vi.mock('../../src/commands/auth/utils.js', async (importOriginal) => {
const mod = await importOriginal();
return { ...mod, checkAuth: vi.fn().mockResolvedValue(undefined) };
});
vi.mock('../../src/commands/demo/util.js', async (importOriginal) => {
const mod = await importOriginal();
return {
...mod,
prepareSupportingData: vi.fn(),
checkDockerReadiness: vi.fn(),
publishAllPlugins: vi.fn(),
runRouterContainer: vi.fn(),
getDemoLogPath: vi.fn(),
captureOnboardingEvent: vi.fn(),
};
});
vi.mock('../../src/utils.js', async (importOriginal) => {
const mod = await importOriginal();
return { ...mod, waitForKeyPress: vi.fn() };
});
function createMockTransport(overrides = {}) {
return createRouterTransport(({ service }) => {
service(PlatformService, {
whoAmI: () => ({
response: { code: EnumStatusCode.OK },
userEmail: 'test@example.com',
organizationName: 'TestOrg',
}),
getOnboarding: () => ({
response: { code: EnumStatusCode.OK },
enabled: true,
}),
getFederatedGraphByName: () => ({
response: { code: EnumStatusCode.ERR_NOT_FOUND, details: 'not found' },
}),
createFederatedGraph: () => ({
response: { code: EnumStatusCode.OK },
}),
deleteFederatedGraph: () => ({
response: { code: EnumStatusCode.OK },
}),
deleteFederatedSubgraph: () => ({
response: { code: EnumStatusCode.OK },
}),
createFederatedGraphToken: () => ({
response: { code: EnumStatusCode.OK },
token: 'test-token',
}),
deleteRouterToken: () => ({
response: { code: EnumStatusCode.OK },
}),
...overrides,
});
});
}
function runDemo(overrides = {}) {
const client = {
platform: createClient(PlatformService, createMockTransport(overrides)),
};
const program = new Command();
program.addCommand(DemoCommand({ client }));
return program.parseAsync(['demo'], { from: 'user' });
}
// Queues responses for upcoming waitForKeyPress calls. Call these in the order the command will prompt.
const keys = {
enter: () => vi.mocked(waitForKeyPress).mockResolvedValueOnce(undefined),
press: (key) => vi.mocked(waitForKeyPress).mockImplementationOnce(async (keyMap) => {
const entry = keyMap[key];
if (typeof entry !== 'function') {
throw new TypeError(`waitForKeyPress was not given a function handler for '${key}'`);
}
await entry();
}),
};
describe('Demo command', () => {
let exitSpy;
beforeEach(() => {
// Silence the demo command's logo, welcome banner, and spinner output so CI logs stay readable.
vi.spyOn(console, 'log').mockImplementation(() => { });
vi.spyOn(console, 'error').mockImplementation(() => { });
vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {
throw new Error('process.exit');
});
vi.mocked(demoUtil.prepareSupportingData).mockResolvedValue('/tmp/cosmo-demo');
vi.mocked(demoUtil.checkDockerReadiness).mockResolvedValue(undefined);
vi.mocked(demoUtil.publishAllPlugins).mockResolvedValue({ error: null });
vi.mocked(demoUtil.runRouterContainer).mockResolvedValue({ error: null });
vi.mocked(demoUtil.getDemoLogPath).mockReturnValue('/tmp/demo.log');
});
describe('happy path', () => {
it('fresh setup: creates graph, publishes plugins, starts router', async () => {
keys.enter();
await runDemo();
expect(demoUtil.prepareSupportingData).toHaveBeenCalledOnce();
expect(demoUtil.checkDockerReadiness).toHaveBeenCalledOnce();
expect(demoUtil.publishAllPlugins).toHaveBeenCalledOnce();
expect(demoUtil.runRouterContainer).toHaveBeenCalledOnce();
expect(demoUtil.runRouterContainer).toHaveBeenCalledWith(expect.objectContaining({ routerToken: 'test-token' }));
});
it('existing graph: continues with existing graph', async () => {
const overrides = {
getFederatedGraphByName: () => ({
response: { code: EnumStatusCode.OK },
graph: create(FederatedGraphSchema, {
name: 'demo',
namespace: 'default',
routingURL: 'http://localhost:3002/graphql',
}),
subgraphs: [],
}),
};
keys.enter();
keys.enter();
await runDemo(overrides);
expect(demoUtil.publishAllPlugins).toHaveBeenCalledOnce();
expect(demoUtil.runRouterContainer).toHaveBeenCalledOnce();
});
it('existing graph: deletes graph and exits', async () => {
const overrides = {
getFederatedGraphByName: () => ({
response: { code: EnumStatusCode.OK },
graph: create(FederatedGraphSchema, {
name: 'demo',
namespace: 'default',
routingURL: 'http://localhost:3002/graphql',
}),
subgraphs: [create(SubgraphSchema, { name: 'products', namespace: 'default' })],
}),
};
keys.enter();
keys.press('d');
await expect(runDemo(overrides)).rejects.toThrow('process.exit');
expect(exitSpy).toHaveBeenCalledWith(0);
expect(demoUtil.publishAllPlugins).not.toHaveBeenCalled();
expect(demoUtil.runRouterContainer).not.toHaveBeenCalled();
});
});
describe('non-recoverable errors', () => {
it('exits when whoAmI RPC fails', async () => {
const overrides = {
whoAmI: () => ({
response: { code: EnumStatusCode.ERR, details: 'Unauthorized' },
}),
};
await expect(runDemo(overrides)).rejects.toThrow('process.exit');
expect(exitSpy).toHaveBeenCalledWith(1);
expect(demoUtil.publishAllPlugins).not.toHaveBeenCalled();
});
it('exits when user is not an organization owner', async () => {
const overrides = {
getOnboarding: () => ({
response: { code: EnumStatusCode.OK },
enabled: false,
}),
};
keys.enter();
await expect(runDemo(overrides)).rejects.toThrow('process.exit');
expect(exitSpy).toHaveBeenCalledWith(1);
expect(demoUtil.publishAllPlugins).not.toHaveBeenCalled();
});
it('exits when docker is unavailable', async () => {
vi.mocked(demoUtil.checkDockerReadiness).mockImplementationOnce(() => {
process.exit(1);
});
await expect(runDemo()).rejects.toThrow('process.exit');
expect(exitSpy).toHaveBeenCalledWith(1);
expect(demoUtil.publishAllPlugins).not.toHaveBeenCalled();
});
it('exits when github fetch fails', async () => {
vi.mocked(demoUtil.prepareSupportingData).mockImplementationOnce(() => {
process.exit(1);
});
await expect(runDemo()).rejects.toThrow('process.exit');
expect(exitSpy).toHaveBeenCalledWith(1);
expect(demoUtil.checkDockerReadiness).not.toHaveBeenCalled();
});
});
describe('retrying failures', () => {
it('user retries after plugin publishing fails', async () => {
vi.mocked(demoUtil.publishAllPlugins)
.mockResolvedValueOnce({ error: new Error('build failed') })
.mockResolvedValueOnce({ error: null });
keys.enter();
keys.press('r');
await runDemo();
expect(demoUtil.publishAllPlugins).toHaveBeenCalledTimes(2);
expect(demoUtil.runRouterContainer).toHaveBeenCalledOnce();
});
it('user retries after router fails to start', async () => {
vi.mocked(demoUtil.runRouterContainer)
.mockResolvedValueOnce({ error: new Error('container exited') })
.mockResolvedValueOnce({ error: null });
keys.enter();
keys.press('r');
await runDemo();
expect(demoUtil.runRouterContainer).toHaveBeenCalledTimes(2);
});
it('user retries after graph lookup fails', async () => {
const getGraphFn = vi
.fn()
.mockReturnValueOnce({ response: { code: EnumStatusCode.ERR, details: 'service unavailable' } })
.mockReturnValue({ response: { code: EnumStatusCode.ERR_NOT_FOUND, details: 'not found' } });
keys.enter();
keys.press('r');
await runDemo({ getFederatedGraphByName: getGraphFn });
expect(getGraphFn).toHaveBeenCalledTimes(2);
expect(demoUtil.runRouterContainer).toHaveBeenCalledOnce();
});
});
describe('event tracking', () => {
it('fires completed events on happy path', async () => {
keys.enter();
await runDemo();
expect(demoUtil.captureOnboardingEvent).toHaveBeenCalledWith({
name: 'onboarding_step_completed',
properties: { step_name: 'init', entry_source: 'wgc' },
});
expect(demoUtil.captureOnboardingEvent).toHaveBeenCalledWith({
name: 'onboarding_step_completed',
properties: { step_name: 'check_onboarding', entry_source: 'wgc' },
});
expect(demoUtil.captureOnboardingEvent).toHaveBeenCalledWith({
name: 'onboarding_step_completed',
properties: { step_name: 'create_federated_graph', entry_source: 'wgc' },
});
});
it('fires delete_federated_graph completed when user deletes existing graph', async () => {
const overrides = {
getFederatedGraphByName: () => ({
response: { code: EnumStatusCode.OK },
graph: create(FederatedGraphSchema, {
name: 'demo',
namespace: 'default',
routingURL: 'http://localhost:3002/graphql',
}),
subgraphs: [create(SubgraphSchema, { name: 'products', namespace: 'default' })],
}),
};
keys.enter();
keys.press('d');
await expect(runDemo(overrides)).rejects.toThrow('process.exit');
expect(demoUtil.captureOnboardingEvent).toHaveBeenCalledWith({
name: 'onboarding_step_completed',
properties: { step_name: 'delete_federated_graph', entry_source: 'wgc' },
});
});
it('fires init failed when whoAmI RPC fails', async () => {
const overrides = {
whoAmI: () => ({
response: { code: EnumStatusCode.ERR, details: 'Unauthorized' },
}),
};
await expect(runDemo(overrides)).rejects.toThrow('process.exit');
expect(demoUtil.captureOnboardingEvent).toHaveBeenCalledWith({
name: 'onboarding_step_failed',
properties: {
step_name: 'init',
entry_source: 'wgc',
error_category: 'resource',
error_message: expect.any(String),
},
});
});
it('fires check_onboarding failed when user is not org owner', async () => {
const overrides = {
getOnboarding: () => ({
response: { code: EnumStatusCode.OK },
enabled: false,
}),
};
keys.enter();
await expect(runDemo(overrides)).rejects.toThrow('process.exit');
expect(demoUtil.captureOnboardingEvent).toHaveBeenCalledWith({
name: 'onboarding_step_failed',
properties: {
step_name: 'check_onboarding',
entry_source: 'wgc',
error_category: 'resource',
error_message: expect.any(String),
},
});
});
it('fires check_onboarding failed when getOnboarding RPC errors', async () => {
const getOnboardingFn = vi
.fn()
.mockReturnValueOnce({ response: { code: EnumStatusCode.ERR, details: 'rpc error' } })
.mockReturnValue({ response: { code: EnumStatusCode.OK }, enabled: true });
keys.enter();
keys.press('Enter');
await runDemo({ getOnboarding: getOnboardingFn });
expect(demoUtil.captureOnboardingEvent).toHaveBeenCalledWith({
name: 'onboarding_step_failed',
properties: {
step_name: 'check_onboarding',
entry_source: 'wgc',
error_category: 'resource',
error_message: expect.any(String),
},
});
});
it('fires delete_federated_graph failed when graph deletion fails', async () => {
const overrides = {
getFederatedGraphByName: () => ({
response: { code: EnumStatusCode.OK },
graph: create(FederatedGraphSchema, {
name: 'demo',
namespace: 'default',
routingURL: 'http://localhost:3002/graphql',
}),
subgraphs: [create(SubgraphSchema, { name: 'products', namespace: 'default' })],
}),
deleteFederatedGraph: () => ({
response: { code: EnumStatusCode.ERR, details: 'deletion failed' },
}),
};
keys.enter();
keys.press('d');
keys.press('Enter');
await expect(runDemo(overrides)).rejects.toThrow('process.exit');
expect(demoUtil.captureOnboardingEvent).toHaveBeenCalledWith({
name: 'onboarding_step_failed',
properties: {
step_name: 'delete_federated_graph',
entry_source: 'wgc',
error_category: 'resource',
error_message: expect.any(String),
},
});
});
it('fires run_router_send_metrics failed when deleteRouterToken RPC fails', async () => {
const deleteRouterTokenFn = vi
.fn()
.mockReturnValueOnce({ response: { code: EnumStatusCode.ERR, details: 'token error' } })
.mockReturnValue({ response: { code: EnumStatusCode.OK } });
keys.enter();
keys.press('r');
await runDemo({ deleteRouterToken: deleteRouterTokenFn });
expect(demoUtil.captureOnboardingEvent).toHaveBeenCalledWith({
name: 'onboarding_step_failed',
properties: {
step_name: 'run_router_send_metrics',
entry_source: 'wgc',
error_category: 'router',
error_message: expect.any(String),
},
});
});
it('fires run_router_send_metrics failed when createFederatedGraphToken RPC fails', async () => {
const createFederatedGraphTokenFn = vi
.fn()
.mockReturnValueOnce({ response: { code: EnumStatusCode.ERR, details: 'create token error' } })
.mockReturnValue({ response: { code: EnumStatusCode.OK }, token: 'test-token' });
keys.enter();
keys.press('r');
await runDemo({ createFederatedGraphToken: createFederatedGraphTokenFn });
expect(demoUtil.captureOnboardingEvent).toHaveBeenCalledWith({
name: 'onboarding_step_failed',
properties: {
step_name: 'run_router_send_metrics',
entry_source: 'wgc',
error_category: 'router',
error_message: expect.any(String),
},
});
});
it('fires run_router_send_metrics failed when router exits with error', async () => {
vi.mocked(demoUtil.runRouterContainer)
.mockResolvedValueOnce({ error: new Error('container exited') })
.mockResolvedValueOnce({ error: null });
keys.enter();
keys.press('r');
await runDemo();
expect(demoUtil.captureOnboardingEvent).toHaveBeenCalledWith({
name: 'onboarding_step_failed',
properties: {
step_name: 'run_router_send_metrics',
entry_source: 'wgc',
error_category: 'router',
error_message: expect.any(String),
},
});
});
});
});
//# sourceMappingURL=command.test.js.map