import { describe, it, expect, vi, beforeEach } from 'vitest'; import { createBackupCommand, createRestoreCommand } from '../../src/commands/backup.js'; const mockClient = { get: vi.fn(), post: vi.fn(), put: vi.fn(), delete: vi.fn(), }; const log = vi.fn(); describe('backup command', () => { beforeEach(() => { vi.resetAllMocks(); }); it('creates backup command', () => { const cmd = createBackupCommand({ client: mockClient as never, log }); expect(cmd.name()).toBe('backup'); }); it('shows status when enabled', async () => { mockClient.get.mockResolvedValue({ enabled: true, repoUrl: 'ssh://git@10.0.0.194:2222/michal/mcp-backup.git', gitReachable: true, lastSyncAt: new Date().toISOString(), lastPushAt: null, lastError: null, pendingCount: 0, }); const cmd = createBackupCommand({ client: mockClient as never, log }); await cmd.parseAsync([], { from: 'user' }); expect(mockClient.get).toHaveBeenCalledWith('/api/v1/backup/status'); expect(log).toHaveBeenCalledWith(expect.stringContaining('ssh://git@10.0.0.194:2222/michal/mcp-backup.git')); expect(log).toHaveBeenCalledWith(expect.stringContaining('synced')); }); it('shows disabled when not configured', async () => { mockClient.get.mockResolvedValue({ enabled: false, repoUrl: null, gitReachable: false, lastSyncAt: null, lastPushAt: null, lastError: null, pendingCount: 0, }); const cmd = createBackupCommand({ client: mockClient as never, log }); await cmd.parseAsync([], { from: 'user' }); expect(log).toHaveBeenCalledWith(expect.stringContaining('disabled')); }); it('shows pending count', async () => { mockClient.get.mockResolvedValue({ enabled: true, repoUrl: 'ssh://git@host/repo.git', gitReachable: true, lastSyncAt: null, lastPushAt: null, lastError: null, pendingCount: 5, }); const cmd = createBackupCommand({ client: mockClient as never, log }); await cmd.parseAsync([], { from: 'user' }); expect(log).toHaveBeenCalledWith(expect.stringContaining('5 changes pending')); }); it('shows SSH public key', async () => { mockClient.get.mockResolvedValue({ publicKey: 'ssh-ed25519 AAAA... mcpd@mcpctl.local' }); const cmd = createBackupCommand({ client: mockClient as never, log }); await cmd.parseAsync(['key'], { from: 'user' }); expect(mockClient.get).toHaveBeenCalledWith('/api/v1/backup/key'); expect(log).toHaveBeenCalledWith('ssh-ed25519 AAAA... mcpd@mcpctl.local'); }); it('shows commit log', async () => { mockClient.get.mockResolvedValue({ entries: [ { hash: 'abc1234567890', date: '2026-03-08T10:00:00Z', author: 'mcpd ', message: 'Update server grafana', manual: false }, { hash: 'def4567890123', date: '2026-03-07T09:00:00Z', author: 'Michal ', message: 'Manual fix', manual: true }, ], }); const cmd = createBackupCommand({ client: mockClient as never, log }); await cmd.parseAsync(['log'], { from: 'user' }); expect(mockClient.get).toHaveBeenCalledWith('/api/v1/backup/log?limit=20'); // Header expect(log).toHaveBeenCalledWith(expect.stringContaining('COMMIT')); // Entries expect(log).toHaveBeenCalledWith(expect.stringContaining('abc1234')); expect(log).toHaveBeenCalledWith(expect.stringContaining('[manual]')); }); }); describe('restore command', () => { beforeEach(() => { vi.resetAllMocks(); }); it('creates restore command', () => { const cmd = createRestoreCommand({ client: mockClient as never, log }); expect(cmd.name()).toBe('restore'); }); it('lists restore points', async () => { mockClient.get.mockResolvedValue({ entries: [ { hash: 'abc1234567890', date: '2026-03-08T10:00:00Z', author: 'mcpd ', message: 'Sync' }, ], }); const cmd = createRestoreCommand({ client: mockClient as never, log }); await cmd.parseAsync(['list'], { from: 'user' }); expect(mockClient.get).toHaveBeenCalledWith('/api/v1/backup/log?limit=30'); expect(log).toHaveBeenCalledWith(expect.stringContaining('abc1234')); }); it('shows restore diff preview', async () => { mockClient.post.mockResolvedValue({ targetCommit: 'abc1234567890', targetDate: '2026-03-08T10:00:00Z', targetMessage: 'Snapshot', added: ['servers/new.yaml'], removed: ['servers/old.yaml'], modified: ['projects/default.yaml'], }); const cmd = createRestoreCommand({ client: mockClient as never, log }); await cmd.parseAsync(['diff', 'abc1234'], { from: 'user' }); expect(mockClient.post).toHaveBeenCalledWith('/api/v1/backup/restore/preview', { commit: 'abc1234' }); expect(log).toHaveBeenCalledWith(expect.stringContaining('+ servers/new.yaml')); expect(log).toHaveBeenCalledWith(expect.stringContaining('- servers/old.yaml')); expect(log).toHaveBeenCalledWith(expect.stringContaining('~ projects/default.yaml')); }); it('requires --force for restore', async () => { mockClient.post.mockResolvedValue({ targetCommit: 'abc1234567890', targetDate: '2026-03-08T10:00:00Z', targetMessage: 'Snapshot', added: ['servers/new.yaml'], removed: [], modified: [], }); const cmd = createRestoreCommand({ client: mockClient as never, log }); await cmd.parseAsync(['to', 'abc1234'], { from: 'user' }); // Should show preview but NOT call restore endpoint expect(mockClient.post).toHaveBeenCalledWith('/api/v1/backup/restore/preview', { commit: 'abc1234' }); expect(mockClient.post).not.toHaveBeenCalledWith('/api/v1/backup/restore', expect.anything()); expect(log).toHaveBeenCalledWith(expect.stringContaining('--force')); }); it('executes restore with --force', async () => { // First call: preview, second call: restore mockClient.post .mockResolvedValueOnce({ targetCommit: 'abc1234567890', targetDate: '2026-03-08T10:00:00Z', targetMessage: 'Snapshot', added: ['servers/new.yaml'], removed: [], modified: [], }) .mockResolvedValueOnce({ branchName: 'timeline/20260308-100000', applied: 1, deleted: 0, errors: [], }); const cmd = createRestoreCommand({ client: mockClient as never, log }); await cmd.parseAsync(['to', 'abc1234', '--force'], { from: 'user' }); expect(mockClient.post).toHaveBeenCalledWith('/api/v1/backup/restore', { commit: 'abc1234' }); expect(log).toHaveBeenCalledWith(expect.stringContaining('1 applied')); expect(log).toHaveBeenCalledWith(expect.stringContaining('timeline/20260308-100000')); }); it('reports restore errors', async () => { mockClient.post .mockResolvedValueOnce({ targetCommit: 'abc1234567890', targetDate: '2026-03-08T10:00:00Z', targetMessage: 'Snapshot', added: [], removed: [], modified: ['servers/broken.yaml'], }) .mockResolvedValueOnce({ branchName: 'timeline/20260308-100000', applied: 0, deleted: 0, errors: ['Failed to apply servers/broken.yaml: invalid YAML'], }); const cmd = createRestoreCommand({ client: mockClient as never, log }); await cmd.parseAsync(['to', 'abc1234', '--force'], { from: 'user' }); expect(log).toHaveBeenCalledWith('Errors:'); expect(log).toHaveBeenCalledWith(expect.stringContaining('invalid YAML')); }); });