import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import MockAdapter from 'axios-mock-adapter'; import { apiClient } from '@/api/client'; import { deleteC2Config, executeC2, getC2Config, getC2Tasks, importC2, listCallbackHistory, listCallbacks, putC2Config, testC2Config, } from '@/api/c2'; let mock: MockAdapter; beforeEach(() => { mock = new MockAdapter(apiClient); }); afterEach(() => { mock.restore(); }); describe('getC2Config', () => { it('returns config on 200', async () => { mock.onGet('/engagements/1/c2-config').reply(200, { has_token: true, url: 'https://mythic.lab:7443', verify_tls: true, }); const result = await getC2Config(1); expect(result).toEqual({ has_token: true, url: 'https://mythic.lab:7443', verify_tls: true }); }); it('returns null on 404', async () => { mock.onGet('/engagements/1/c2-config').reply(404); const result = await getC2Config(1); expect(result).toBeNull(); }); it('throws on other errors', async () => { mock.onGet('/engagements/1/c2-config').reply(503); await expect(getC2Config(1)).rejects.toThrow(); }); }); describe('putC2Config', () => { it('sends PUT to correct URL with body', async () => { mock.onPut('/engagements/2/c2-config').reply(200, { has_token: true, url: 'https://mythic.lab:7443', verify_tls: false, }); const result = await putC2Config(2, { url: 'https://mythic.lab:7443', api_token: 'secret', verify_tls: false, }); expect(result.has_token).toBe(true); expect(result.verify_tls).toBe(false); const req = mock.history['put'][0]; expect(req.url).toBe('/engagements/2/c2-config'); const body = JSON.parse(req.data as string); expect(body.api_token).toBe('secret'); expect(body.verify_tls).toBe(false); }); }); describe('deleteC2Config', () => { it('sends DELETE to correct URL', async () => { mock.onDelete('/engagements/3/c2-config').reply(204); await expect(deleteC2Config(3)).resolves.toBeUndefined(); expect(mock.history['delete'][0].url).toBe('/engagements/3/c2-config'); }); }); describe('testC2Config', () => { it('sends POST and returns test result', async () => { mock.onPost('/engagements/1/c2-config/test').reply(200, { ok: true, error: null }); const result = await testC2Config(1); expect(result.ok).toBe(true); expect(result.error).toBeNull(); expect(mock.history['post'][0].url).toBe('/engagements/1/c2-config/test'); }); it('returns error message when connection fails', async () => { mock .onPost('/engagements/1/c2-config/test') .reply(200, { ok: false, error: 'Connection refused' }); const result = await testC2Config(1); expect(result.ok).toBe(false); expect(result.error).toBe('Connection refused'); }); }); describe('listCallbacks', () => { it('sends GET and returns callbacks', async () => { mock.onGet('/engagements/1/c2/callbacks').reply(200, { callbacks: [ { display_id: 1, active: true, host: 'WIN-TARGET', user: 'administrator', domain: 'lab.local', last_checkin: '2026-06-10T10:00:00', }, ], }); const result = await listCallbacks(1); expect(result.callbacks).toHaveLength(1); expect(result.callbacks[0].display_id).toBe(1); expect(result.callbacks[0].host).toBe('WIN-TARGET'); expect(mock.history['get'][0].url).toBe('/engagements/1/c2/callbacks'); }); }); describe('executeC2', () => { it('sends POST with callback_display_id and commands', async () => { mock.onPost('/simulations/5/c2/execute').reply(200, { tasks: [ { id: 1, mythic_task_display_id: 42, command: 'whoami', status: 'submitted', completed: false }, ], }); const result = await executeC2(5, { callback_display_id: 1, commands: ['whoami'], }); expect(result.tasks).toHaveLength(1); expect(result.tasks[0].command).toBe('whoami'); const req = mock.history['post'][0]; expect(req.url).toBe('/simulations/5/c2/execute'); const body = JSON.parse(req.data as string); expect(body.callback_display_id).toBe(1); expect(body.commands).toEqual(['whoami']); }); }); describe('getC2Tasks', () => { it('GET /simulations/:id/c2/tasks returns tasks list', async () => { mock.onGet('/simulations/7/c2/tasks').reply(200, { tasks: [ { id: 1, mythic_task_display_id: 10, callback_display_id: 1, command: 'whoami', params: null, status: 'completed', completed: true, output: 'NT AUTHORITY\\SYSTEM', mapping_applied: true, source: 'mimic', created_at: '2026-06-10T10:00:00', completed_at: '2026-06-10T10:00:05', }, ], }); const result = await getC2Tasks(7); expect(result.tasks).toHaveLength(1); expect(result.tasks[0].status).toBe('completed'); expect(result.tasks[0].output).toBe('NT AUTHORITY\\SYSTEM'); expect(mock.history['get'][0].url).toBe('/simulations/7/c2/tasks'); }); }); describe('listCallbackHistory', () => { it('GET with page/page_size params', async () => { mock.onGet('/engagements/1/c2/callbacks/2/history').reply(200, { tasks: [], total: 0, page: 1, page_size: 25, }); const result = await listCallbackHistory(1, 2, { page: 1, pageSize: 25 }); expect(result.total).toBe(0); const req = mock.history['get'][0]; expect(req.url).toBe('/engagements/1/c2/callbacks/2/history'); expect(req.params).toMatchObject({ page: 1, page_size: 25 }); }); }); describe('importC2', () => { it('POST /simulations/:id/c2/import with task_display_ids', async () => { mock.onPost('/simulations/7/c2/import').reply(200, { imported: 3, skipped: 1 }); const result = await importC2(7, { callback_display_id: 2, task_display_ids: [10, 11, 12, 13], }); expect(result.imported).toBe(3); expect(result.skipped).toBe(1); const req = mock.history['post'][0]; expect(req.url).toBe('/simulations/7/c2/import'); const body = JSON.parse(req.data as string); expect(body.callback_display_id).toBe(2); expect(body.task_display_ids).toEqual([10, 11, 12, 13]); }); });