Skip to content
Open
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
5 changes: 5 additions & 0 deletions tests/e2e/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
node_modules/
test-results/
playwright-report/
blob-report/
.playwright/
114 changes: 114 additions & 0 deletions tests/e2e/ability-filter-dropdowns.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { test, expect } from './fixtures/caldera-auth';

/**
* Tests for the tactic -> technique -> procedure cascading dropdown filters.
*/
test.describe('Ability Filter Dropdowns', () => {
test('should display the tactic filter dropdown', async ({ manxPage }) => {
const tacticSelect = manxPage.locator('#tactic-filter');
await expect(tacticSelect).toBeVisible();
});

test('tactic dropdown should show "Select a tactic" placeholder', async ({ manxPage }) => {
const placeholder = manxPage.locator('#tactic-filter option[disabled][selected]');
await expect(placeholder).toHaveText('Select a tactic');
});

test('should display the technique filter dropdown', async ({ manxPage }) => {
// Technique select does not have an id in all versions; locate by structure
const selects = manxPage.locator('.select.is-small select');
const count = await selects.count();
// There should be 4 dropdowns: session, tactic, technique, procedure
expect(count).toBeGreaterThanOrEqual(4);
});

test('technique dropdown should show "Select a technique" placeholder', async ({ manxPage }) => {
const placeholder = manxPage.locator('option[disabled][selected]').filter({ hasText: 'Select a technique' });
await expect(placeholder).toBeAttached();
});

test('should display the procedure filter dropdown', async ({ manxPage }) => {
const procedureSelect = manxPage.locator('#procedure-filter');
await expect(procedureSelect).toBeVisible();
});

test('procedure dropdown should show "Select a procedure" placeholder', async ({ manxPage }) => {
const placeholder = manxPage.locator('#procedure-filter option[disabled][selected]');
await expect(placeholder).toHaveText('Select a procedure');
});

test('all four dropdowns should be in a flex row layout', async ({ manxPage }) => {
const flexRow = manxPage.locator('.is-flex.is-flex-direction-row.is-justify-content-space-around');
await expect(flexRow).toBeVisible();
const dropdowns = flexRow.locator('.select.is-small');
const count = await dropdowns.count();
expect(count).toBe(4);
});

test('tactic dropdown should be empty until a session is selected', async ({ manxPage }) => {
const tacticOptions = manxPage.locator('#tactic-filter option:not([disabled])');
const count = await tacticOptions.count();
expect(count).toBe(0);
});

test('technique dropdown should be empty until a tactic is selected', async ({ manxPage }) => {
const selects = manxPage.locator('.select.is-small select');
// The third select is technique (0-indexed: session=0, tactic=1, technique=2)
if (await selects.count() >= 3) {
const techniqueOptions = selects.nth(2).locator('option:not([disabled])');
const count = await techniqueOptions.count();
expect(count).toBe(0);
}
});

test('procedure dropdown should be empty until a technique is selected', async ({ manxPage }) => {
const procedureOptions = manxPage.locator('#procedure-filter option:not([disabled])');
const count = await procedureOptions.count();
expect(count).toBe(0);
});

test('selecting a session should populate the tactic dropdown', async ({ manxPage }) => {
await manxPage.waitForTimeout(4000);
const sessionOptions = manxPage.locator('#session-id option:not([disabled])');
const sessionCount = await sessionOptions.count();

if (sessionCount > 0) {
const value = await sessionOptions.first().getAttribute('value');
if (value) {
await manxPage.locator('#session-id').selectOption(value);
await manxPage.waitForTimeout(3000);

const tacticOptions = manxPage.locator('#tactic-filter option:not([disabled])');
const tacticCount = await tacticOptions.count();
// Tactics should now have options (if the API returned data)
expect(tacticCount).toBeGreaterThanOrEqual(0);
}
}
});

test('changing session should reset tactic, technique, and procedure selections', async ({ manxPage }) => {
await manxPage.waitForTimeout(4000);
const sessionOptions = manxPage.locator('#session-id option:not([disabled])');
const sessionCount = await sessionOptions.count();

if (sessionCount > 1) {
// Select first session
const value1 = await sessionOptions.first().getAttribute('value');
if (value1) {
await manxPage.locator('#session-id').selectOption(value1);
await manxPage.waitForTimeout(2000);
}

// Select second session - should reset filters
const value2 = await sessionOptions.nth(1).getAttribute('value');
if (value2) {
await manxPage.locator('#session-id').selectOption(value2);
await manxPage.waitForTimeout(1000);

// Tactic should be reset to placeholder
const tacticValue = await manxPage.locator('#tactic-filter').inputValue();
expect(tacticValue).toBe('');
}
}
});
});
115 changes: 115 additions & 0 deletions tests/e2e/agent-deployment-display.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { test, expect } from './fixtures/caldera-auth';

/**
* Tests for agent deployment command display and the ability/procedure
* selection workflow.
*/
test.describe('Agent Deployment Display', () => {
test('should display deployment reference text', async ({ manxPage }) => {
await expect(
manxPage.locator('text=To deploy a Manx agent')
).toBeVisible();
});

test('should mention the Agents tab for deployment', async ({ manxPage }) => {
await expect(
manxPage.locator('text=Agents tab')
).toBeVisible();
});

test('should describe Manx as a GoLang agent', async ({ manxPage }) => {
await expect(
manxPage.locator('text=written in GoLang')
).toBeVisible();
});

test('should mention TCP contact point', async ({ manxPage }) => {
await expect(manxPage.locator('i').filter({ hasText: 'contact point' })).toBeVisible();
});

test('should mention the terminal tool', async ({ manxPage }) => {
await expect(manxPage.locator('i').filter({ hasText: 'terminal' })).toBeVisible();
});

test('procedure selection should populate the hidden command element', async ({ manxPage }) => {
await manxPage.waitForTimeout(4000);

const sessionOptions = manxPage.locator('#session-id option:not([disabled])');
if (await sessionOptions.count() === 0) {
test.skip();
return;
}

// Select a session
const sessionValue = await sessionOptions.first().getAttribute('value');
if (!sessionValue) { test.skip(); return; }
await manxPage.locator('#session-id').selectOption(sessionValue);
await manxPage.waitForTimeout(3000);

// If tactics are available, walk the cascade
const tacticOptions = manxPage.locator('#tactic-filter option:not([disabled])');
if (await tacticOptions.count() === 0) {
test.skip();
return;
}

const tacticText = await tacticOptions.first().textContent();
if (tacticText) {
await manxPage.locator('#tactic-filter').selectOption({ label: tacticText });
await manxPage.waitForTimeout(1000);
}

// Select a technique if available
const selects = manxPage.locator('.select.is-small select');
const techniqueSelect = selects.nth(2);
const techniqueOptions = techniqueSelect.locator('option:not([disabled])');
if (await techniqueOptions.count() > 0) {
const techValue = await techniqueOptions.first().getAttribute('value');
if (techValue) {
await techniqueSelect.selectOption(techValue);
await manxPage.waitForTimeout(1000);
}
}

// Select a procedure if available
const procedureOptions = manxPage.locator('#procedure-filter option:not([disabled])');
if (await procedureOptions.count() > 0) {
const procValue = await procedureOptions.first().getAttribute('value');
if (procValue) {
await manxPage.locator('#procedure-filter').selectOption(procValue);
await manxPage.waitForTimeout(2000);

// The hidden command element should now contain the procedure command
const cmdEl = manxPage.locator('#xterminal-command');
const cmdText = await cmdEl.textContent();
// If the ability matched the session platform, a command should be set
if (cmdText) {
expect(cmdText.length).toBeGreaterThanOrEqual(0);
}
}
}
});

test('selecting a procedure should inject command into terminal input', async ({ manxPage }) => {
// This test verifies the setCommand() function in the Vue component
// pushes the command text into the xterm terminal.
// Since we need a full cascade, we skip if no sessions are available.
await manxPage.waitForTimeout(4000);

const sessionOptions = manxPage.locator('#session-id option:not([disabled])');
if (await sessionOptions.count() === 0) {
test.skip();
return;
}

// The xterm terminal should still be interactive after procedure selection
const terminal = manxPage.locator('#xterminal');
await terminal.click();
const textarea = manxPage.locator('.xterm-helper-textarea');
if (await textarea.count() > 0) {
// Type something to verify terminal is still functional
await textarea.type('echo verify');
await manxPage.waitForTimeout(500);
}
});
});
83 changes: 83 additions & 0 deletions tests/e2e/api-endpoints.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { test, expect } from './fixtures/caldera-auth';

/**
* Tests that the Manx REST API endpoints respond correctly.
* These are lightweight contract tests verifying the UI's backend is wired up.
*/
test.describe('Manx API Endpoints', () => {
test('GET /plugin/manx/sessions should return JSON', async ({ authedPage }) => {
const response = await authedPage.request.get('/plugin/manx/sessions');
expect(response.status()).toBe(200);
const contentType = response.headers()['content-type'];
expect(contentType).toContain('application/json');
});

test('GET /plugin/manx/sessions should return a sessions array', async ({ authedPage }) => {
const response = await authedPage.request.get('/plugin/manx/sessions');
const body = await response.json();
// Response should contain a sessions key (magma format) or be an array (legacy)
if (body.sessions) {
expect(Array.isArray(body.sessions)).toBe(true);
} else {
expect(Array.isArray(body)).toBe(true);
}
});

test('POST /plugin/manx/sessions should return session list', async ({ authedPage }) => {
const response = await authedPage.request.post('/plugin/manx/sessions');
expect(response.status()).toBe(200);
const body = await response.json();
expect(body).toBeTruthy();
});

test('POST /plugin/manx/history should accept paw parameter', async ({ authedPage }) => {
const response = await authedPage.request.post('/plugin/manx/history', {
data: { paw: 'nonexistent-paw' },
});
expect(response.status()).toBe(200);
const body = await response.json();
expect(Array.isArray(body)).toBe(true);
// Non-existent paw should return empty history
expect(body).toHaveLength(0);
});

test('POST /plugin/manx/ability should accept paw parameter', async ({ authedPage }) => {
const response = await authedPage.request.post('/plugin/manx/ability', {
data: { paw: 'nonexistent-paw' },
});
// Should not crash; may return empty abilities
expect([200, 400, 500]).toContain(response.status());
});

test('session objects should have required fields', async ({ authedPage }) => {
const response = await authedPage.request.get('/plugin/manx/sessions');
const body = await response.json();
const sessions = body.sessions || body;

if (Array.isArray(sessions) && sessions.length > 0) {
const session = sessions[0];
expect(session).toHaveProperty('id');
expect(session).toHaveProperty('info');
}
});

test('history entries should have cmd and paw fields', async ({ authedPage }) => {
// Get a session first to find a real paw
const sessResponse = await authedPage.request.get('/plugin/manx/sessions');
const sessBody = await sessResponse.json();
const sessions = sessBody.sessions || sessBody;

if (Array.isArray(sessions) && sessions.length > 0) {
const paw = sessions[0].info;
const histResponse = await authedPage.request.post('/plugin/manx/history', {
data: { paw },
});
const history = await histResponse.json();

if (Array.isArray(history) && history.length > 0) {
expect(history[0]).toHaveProperty('cmd');
expect(history[0]).toHaveProperty('paw');
}
}
});
});
Loading
Loading