UNPKG

wgc

Version:

The official CLI tool to manage the GraphQL Federation Platform Cosmo

348 lines 13.2 kB
import { Command } from 'commander'; import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; import { createClient, createRouterTransport } from '@connectrpc/connect'; import { EnumStatusCode } from '@wundergraph/cosmo-connect/dist/common/common_pb'; import { PlatformService, } from '@wundergraph/cosmo-connect/dist/platform/v1/platform_pb'; import inquirer from 'inquirer'; import DeleteClientsCommand from '../src/commands/clients/commands/delete.js'; vi.mock('inquirer', () => ({ default: { prompt: vi.fn(), }, })); function createMockTransport(previewResponse, deleteResponse, onPreviewDeleteClient, onDeleteClient) { return createRouterTransport(({ service }) => { service(PlatformService, { previewDeleteClient: (req) => { onPreviewDeleteClient === null || onPreviewDeleteClient === void 0 ? void 0 : onPreviewDeleteClient(req); return previewResponse; }, deleteClient: (req) => { onDeleteClient === null || onDeleteClient === void 0 ? void 0 : onDeleteClient(req); return deleteResponse; }, }); }); } async function runDelete({ previewResponse, deleteResponse, args = [], onPreviewDeleteClient, onDeleteClient, }) { const client = { platform: createClient(PlatformService, createMockTransport(previewResponse, deleteResponse, onPreviewDeleteClient, onDeleteClient)), }; const program = new Command(); program.exitOverride(); program.addCommand(DeleteClientsCommand({ client })); await program.parseAsync(['delete', 'web', '--graph-name', 'mygraph', ...args], { from: 'user' }); } function getJsonOutput(logSpy) { const call = logSpy.mock.calls.find(([arg]) => { try { JSON.parse(String(arg)); return true; } catch { return false; } }); if (!call) { throw new Error('No JSON output found in console.log calls'); } return JSON.parse(String(call[0])); } const successPreview = { response: { code: EnumStatusCode.OK }, client: { id: 'client-id', name: 'web', createdAt: '2026-04-23T10:00:00.000Z', lastUpdatedAt: '', createdBy: 'a@b.com', lastUpdatedBy: '', }, }; const successDelete = { response: { code: EnumStatusCode.OK }, client: successPreview.client, deletedOperationsCount: 0, }; describe('stdout', () => { let logSpy; let stderrSpy; let exitSpy; const promptMock = vi.mocked(inquirer.prompt); beforeEach(() => { logSpy = vi.spyOn(console, 'log').mockImplementation(() => { }); stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true); exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => { throw new Error('process.exit'); }); promptMock.mockReset(); }); afterEach(() => { process.exitCode = undefined; }); test('uses default namespace', async () => { let previewNamespace = ''; let deleteNamespace = ''; await runDelete({ previewResponse: successPreview, deleteResponse: successDelete, onPreviewDeleteClient: (req) => { previewNamespace = req.namespace; }, onDeleteClient: (req) => { deleteNamespace = req.namespace; }, }); expect(previewNamespace).toBe('default'); expect(deleteNamespace).toBe('default'); }); test('deletes client without prompt when no operations exist', async () => { let deleteCalled = false; await runDelete({ previewResponse: successPreview, deleteResponse: successDelete, onDeleteClient: () => { deleteCalled = true; }, }); expect(promptMock).not.toHaveBeenCalled(); expect(deleteCalled).toBe(true); expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('Deleted 0 related persisted operation(s).')); }); test('prompts before deleting client with operations', async () => { let deleteCalled = false; promptMock.mockResolvedValue({ confirmDeletion: true }); await runDelete({ previewResponse: { ...successPreview, persistedOperationsCount: 2, }, deleteResponse: { ...successDelete, deletedOperationsCount: 2, }, onDeleteClient: () => { deleteCalled = true; }, }); expect(promptMock).toHaveBeenCalledWith(expect.objectContaining({ name: 'confirmDeletion', type: 'confirm' })); expect(deleteCalled).toBe(true); expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('Deleted 2 related persisted operation(s).')); }); test('prompts before deleting client with traffic', async () => { let deleteCalled = false; promptMock.mockResolvedValue({ confirmDeletion: true }); await runDelete({ previewResponse: { ...successPreview, persistedOperationsCount: 0, hasTraffic: true, }, deleteResponse: successDelete, onDeleteClient: () => { deleteCalled = true; }, }); expect(promptMock).toHaveBeenCalledWith(expect.objectContaining({ message: expect.stringContaining('One or more operations have traffic.'), })); expect(deleteCalled).toBe(true); }); test('stops when deletion is not confirmed', async () => { let deleteCalled = false; promptMock.mockResolvedValue({ confirmDeletion: false }); await runDelete({ previewResponse: { ...successPreview, persistedOperationsCount: 1, }, deleteResponse: successDelete, onDeleteClient: () => { deleteCalled = true; }, }); expect(deleteCalled).toBe(false); expect(process.exitCode).toBe(1); }); test('force skips confirmation', async () => { let deleteCalled = false; await runDelete({ previewResponse: { ...successPreview, persistedOperationsCount: 1, }, deleteResponse: successDelete, args: ['--force'], onDeleteClient: () => { deleteCalled = true; }, }); expect(promptMock).not.toHaveBeenCalled(); expect(deleteCalled).toBe(true); }); test('fails on preview rpc error', async () => { await expect(runDelete({ previewResponse: { response: { code: EnumStatusCode.ERR_NOT_FOUND, details: 'not found' }, persistedOperationsCount: 0, }, deleteResponse: successDelete, })).rejects.toThrow(); expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('not found')); expect(stderrSpy).toHaveBeenCalled(); expect(exitSpy).toHaveBeenCalled(); }); test('fails on delete rpc error', async () => { await expect(runDelete({ previewResponse: successPreview, deleteResponse: { response: { code: EnumStatusCode.ERR, details: 'boom' }, deletedOperationsCount: 0, }, })).rejects.toThrow(); expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('boom')); expect(stderrSpy).toHaveBeenCalled(); expect(exitSpy).toHaveBeenCalled(); }); }); describe('json output', () => { let logSpy; let stderrSpy; let exitSpy; const promptMock = vi.mocked(inquirer.prompt); beforeEach(() => { logSpy = vi.spyOn(console, 'log').mockImplementation(() => { }); stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true); exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => { throw new Error('process.exit'); }); promptMock.mockReset(); }); afterEach(() => { process.exitCode = undefined; }); test('prints delete result as json', async () => { await runDelete({ previewResponse: successPreview, deleteResponse: { ...successDelete, deletedOperationsCount: 1, }, args: ['--json'], }); const output = getJsonOutput(logSpy); expect(output).toEqual({ status: 'success', client: { ...successPreview.client, persistedOperationsCount: 0, hasTraffic: false, }, deletedOperationsCount: 1, }); expect(output).not.toHaveProperty('url'); expect(promptMock).not.toHaveBeenCalled(); }); test('prints json error instead of prompting when operations exist without force', async () => { let deleteCalled = false; await runDelete({ previewResponse: { ...successPreview, persistedOperationsCount: 1, }, deleteResponse: successDelete, args: ['--json'], onDeleteClient: () => { deleteCalled = true; }, }); expect(getJsonOutput(logSpy)).toEqual({ status: 'error', code: EnumStatusCode.ERR, message: 'Could not delete client.', details: "Client 'web' has 1 persisted operation(s).", url: 'https://cosmo.wundergraph.com/default/graph/mygraph/operations?clientNames=web', hasTraffic: false, operationsCount: 1, }); expect(deleteCalled).toBe(false); expect(promptMock).not.toHaveBeenCalled(); expect(process.exitCode).toBe(1); expect(stderrSpy).not.toHaveBeenCalled(); expect(exitSpy).not.toHaveBeenCalled(); }); test('prints json error instead of prompting when traffic exists without force', async () => { let deleteCalled = false; await runDelete({ previewResponse: { ...successPreview, persistedOperationsCount: 1, hasTraffic: true, }, deleteResponse: successDelete, args: ['--json'], onDeleteClient: () => { deleteCalled = true; }, }); const studioUrl = 'https://cosmo.wundergraph.com/default/graph/mygraph/operations?clientNames=web'; expect(getJsonOutput(logSpy)).toEqual({ status: 'error', code: EnumStatusCode.ERR, message: 'Could not delete client.', details: `Client 'web' has 1 persisted operation(s). One or more operations have traffic. See details:\n${studioUrl}\n`, url: studioUrl, hasTraffic: true, operationsCount: 1, }); expect(deleteCalled).toBe(false); expect(promptMock).not.toHaveBeenCalled(); expect(process.exitCode).toBe(1); expect(stderrSpy).not.toHaveBeenCalled(); expect(exitSpy).not.toHaveBeenCalled(); }); test('encodes special characters in clientName for studio URL', async () => { await runDelete({ previewResponse: { ...successPreview, client: { ...successPreview.client, name: 'weird name&with#chars' }, persistedOperationsCount: 1, }, deleteResponse: successDelete, args: ['--json'], }); expect(getJsonOutput(logSpy).url).toBe('https://cosmo.wundergraph.com/default/graph/mygraph/operations?clientNames=weird+name%26with%23chars'); }); test('uses organizationSlug from preview response in URL', async () => { await runDelete({ previewResponse: { ...successPreview, persistedOperationsCount: 1, organizationSlug: 'acme', }, deleteResponse: successDelete, args: ['--json'], }); expect(getJsonOutput(logSpy).url).toBe('https://cosmo.wundergraph.com/acme/default/graph/mygraph/operations?clientNames=web'); }); test('prints preview rpc error as json', async () => { await runDelete({ previewResponse: { response: { code: EnumStatusCode.ERR_NOT_FOUND, details: 'not found' }, persistedOperationsCount: 0, }, deleteResponse: successDelete, args: ['--json'], }); expect(getJsonOutput(logSpy)).toEqual({ status: 'error', code: EnumStatusCode.ERR_NOT_FOUND, message: 'Could not delete client.', details: 'not found', }); expect(process.exitCode).toBe(1); expect(stderrSpy).not.toHaveBeenCalled(); expect(exitSpy).not.toHaveBeenCalled(); }); }); //# sourceMappingURL=clients-delete.test.js.map