mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 20:53:06 +00:00
fix composite PartitionKey leaking into event TenantId + add MCP integration test suite
MapToEnrollmentEvent was returning the composite PartitionKey ({TenantId}_{SessionId})
as TenantId instead of extracting the actual tenant ID. Now prefers the stored TenantId
property with fallback extraction from the composite key.
Also adds a Vitest-based integration test suite (39 tests) covering all MCP tool endpoints:
- Session tools (search, get, events, analysis, CVE, blocked devices)
- Admin tools (metrics, geographic, platform, usage, audit, ops, reports)
- Raw data tools (tables, raw sessions/events, backend logs)
- Read-only verification (PUT/DELETE rejection)
- Error handling (404, 400, empty results)
Run with: AUTOPILOT_API_TOKEN="eyJ..." npm test
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1511,11 +1511,21 @@ namespace AutopilotMonitor.Functions.Services
|
||||
/// </summary>
|
||||
private EnrollmentEvent MapToEnrollmentEvent(TableEntity entity)
|
||||
{
|
||||
// Events table uses composite PartitionKey: "{TenantId}_{SessionId}".
|
||||
// Prefer the stored TenantId property; fall back to extracting from the composite key.
|
||||
var partitionKey = entity.PartitionKey ?? string.Empty;
|
||||
var tenantId = entity.GetString("TenantId");
|
||||
if (string.IsNullOrEmpty(tenantId) && partitionKey.Contains('_'))
|
||||
{
|
||||
var separatorIndex = partitionKey.IndexOf('_');
|
||||
tenantId = partitionKey.Substring(0, separatorIndex);
|
||||
}
|
||||
|
||||
return new EnrollmentEvent
|
||||
{
|
||||
EventId = entity.GetString("EventId") ?? string.Empty,
|
||||
SessionId = entity.GetString("SessionId") ?? string.Empty,
|
||||
TenantId = entity.PartitionKey,
|
||||
TenantId = tenantId ?? partitionKey,
|
||||
Timestamp = DateTime.SpecifyKind(
|
||||
entity.GetDateTimeOffset("Timestamp")?.UtcDateTime
|
||||
?? entity.GetDateTime("Timestamp")
|
||||
|
||||
+1196
-1
File diff suppressed because it is too large
Load Diff
@@ -11,7 +11,9 @@
|
||||
"start": "node dist/index.js",
|
||||
"dev": "tsx src/index.ts",
|
||||
"docker:build": "docker build -f Dockerfile -t autopilotmonitor-mcp:local ../../../",
|
||||
"docker:run": "docker run --rm -p 8080:8080 -e AUTOPILOT_API_URL=https://autopilotmonitor-api.azurewebsites.net autopilotmonitor-mcp:local"
|
||||
"docker:run": "docker run --rm -p 8080:8080 -e AUTOPILOT_API_URL=https://autopilotmonitor-api.azurewebsites.net autopilotmonitor-mcp:local",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.80.0",
|
||||
@@ -25,6 +27,7 @@
|
||||
"@types/express": "^5.0.6",
|
||||
"@types/node": "^22.0.0",
|
||||
"tsx": "^4.0.0",
|
||||
"typescript": "^5.0.0"
|
||||
"typescript": "^5.0.0",
|
||||
"vitest": "^4.1.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Shared test helpers for MCP integration tests.
|
||||
*
|
||||
* These tests call the real backend API — they require a valid Bearer token
|
||||
* provided via the AUTOPILOT_API_TOKEN environment variable.
|
||||
*
|
||||
* Usage:
|
||||
* AUTOPILOT_API_TOKEN="eyJ..." npm test
|
||||
*/
|
||||
|
||||
const BASE_URL = process.env.AUTOPILOT_API_URL ?? 'https://autopilotmonitor-api.azurewebsites.net';
|
||||
const TOKEN = process.env.AUTOPILOT_API_TOKEN ?? '';
|
||||
|
||||
export function getToken(): string {
|
||||
if (!TOKEN) {
|
||||
throw new Error(
|
||||
'AUTOPILOT_API_TOKEN environment variable is required.\n' +
|
||||
'Get a token from the browser (F12 → Network → copy Authorization header value) and run:\n' +
|
||||
' AUTOPILOT_API_TOKEN="eyJ..." npm test',
|
||||
);
|
||||
}
|
||||
return TOKEN;
|
||||
}
|
||||
|
||||
export function getBaseUrl(): string {
|
||||
return BASE_URL;
|
||||
}
|
||||
|
||||
interface ApiFetchOptions extends RequestInit {
|
||||
expectStatus?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lightweight fetch wrapper for integration tests.
|
||||
* Returns parsed JSON on success, throws on unexpected status.
|
||||
*/
|
||||
export async function apiFetch<T = unknown>(path: string, options: ApiFetchOptions = {}): Promise<T> {
|
||||
const { expectStatus, ...fetchOpts } = options;
|
||||
const url = `${BASE_URL}${path}`;
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${getToken()}`,
|
||||
'X-Client-Source': 'mcp-test',
|
||||
...((fetchOpts.headers as Record<string, string>) ?? {}),
|
||||
};
|
||||
|
||||
const res = await fetch(url, { ...fetchOpts, headers, signal: AbortSignal.timeout(25_000) });
|
||||
|
||||
if (expectStatus !== undefined) {
|
||||
if (res.status !== expectStatus) {
|
||||
const body = await res.text().catch(() => '(no body)');
|
||||
throw new Error(`Expected status ${expectStatus}, got ${res.status}: ${body.slice(0, 300)}`);
|
||||
}
|
||||
// For non-2xx expected statuses, try to parse the JSON body anyway
|
||||
const text = await res.text();
|
||||
try {
|
||||
return JSON.parse(text) as T;
|
||||
} catch {
|
||||
return text as unknown as T;
|
||||
}
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
const body = await res.text().catch(() => '(no body)');
|
||||
throw new Error(`API ${res.status}: ${body.slice(0, 500)}`);
|
||||
}
|
||||
|
||||
return res.json() as Promise<T>;
|
||||
}
|
||||
|
||||
/** Build query string from params, skipping null/undefined values. */
|
||||
export function buildQuery(params: Record<string, string | number | boolean | undefined | null>): string {
|
||||
const p = new URLSearchParams();
|
||||
for (const [k, v] of Object.entries(params)) {
|
||||
if (v != null && v !== '') p.set(k, String(v));
|
||||
}
|
||||
const s = p.toString();
|
||||
return s ? `?${s}` : '';
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* Integration tests for MCP admin and metrics tools.
|
||||
* Tests against the live backend — requires AUTOPILOT_API_TOKEN env var.
|
||||
*/
|
||||
import { describe, it, expect, beforeAll } from 'vitest';
|
||||
import { apiFetch, getToken } from './helpers.js';
|
||||
|
||||
beforeAll(() => {
|
||||
getToken();
|
||||
});
|
||||
|
||||
describe('get_metrics', () => {
|
||||
it('should return global enrollment metrics', async () => {
|
||||
const data = await apiFetch<any>(`/api/global/metrics/summary?days=30`);
|
||||
expect(data.success).toBe(true);
|
||||
expect(Array.isArray(data.summary)).toBe(true);
|
||||
if (data.summary.length > 0) {
|
||||
expect(data.summary[0]).toHaveProperty('tenantId');
|
||||
expect(data.summary[0]).toHaveProperty('totalSessions');
|
||||
expect(data.summary[0]).toHaveProperty('failureRate');
|
||||
}
|
||||
});
|
||||
|
||||
it('should return app metrics', async () => {
|
||||
const data = await apiFetch<any>(`/api/global/metrics/app?days=30`);
|
||||
expect(data.success).toBe(true);
|
||||
expect(data).toHaveProperty('totalApps');
|
||||
expect(data).toHaveProperty('slowestApps');
|
||||
expect(data).toHaveProperty('topFailingApps');
|
||||
});
|
||||
});
|
||||
|
||||
describe('get_usage_metrics', () => {
|
||||
it('should return platform usage overview', async () => {
|
||||
const data = await apiFetch<any>(`/api/global/metrics/usage`);
|
||||
expect(data).toHaveProperty('sessions');
|
||||
expect(data).toHaveProperty('tenants');
|
||||
expect(data).toHaveProperty('users');
|
||||
expect(data.sessions).toHaveProperty('total');
|
||||
expect(data.tenants).toHaveProperty('total');
|
||||
});
|
||||
});
|
||||
|
||||
describe('get_geographic_metrics', () => {
|
||||
it('should return geographic distribution (by country)', async () => {
|
||||
const data = await apiFetch<any>(`/api/global/metrics/geographic?groupBy=country&days=30`);
|
||||
expect(data.success).toBe(true);
|
||||
expect(Array.isArray(data.locations)).toBe(true);
|
||||
if (data.locations.length > 0) {
|
||||
const loc = data.locations[0];
|
||||
expect(loc).toHaveProperty('locationKey');
|
||||
expect(loc).toHaveProperty('sessionCount');
|
||||
expect(loc).toHaveProperty('successRate');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('get_geographic_sessions', () => {
|
||||
it('should return sessions for a country', async () => {
|
||||
const data = await apiFetch<any>(`/api/global/metrics/geographic/sessions?country=DE&days=7`);
|
||||
expect(data.success).toBe(true);
|
||||
expect(Array.isArray(data.sessions)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('get_platform_metrics', () => {
|
||||
it('should return agent performance metrics', async () => {
|
||||
const data = await apiFetch<any>(`/api/global/metrics/platform`);
|
||||
expect(data).toHaveProperty('sessionsAnalyzed');
|
||||
expect(data).toHaveProperty('cpu');
|
||||
expect(data).toHaveProperty('memory');
|
||||
expect(data).toHaveProperty('network');
|
||||
expect(data.cpu).toHaveProperty('avgPercent');
|
||||
expect(data.memory).toHaveProperty('avgWorkingSetMB');
|
||||
});
|
||||
});
|
||||
|
||||
describe('get_api_usage', () => {
|
||||
it('should return daily usage summary', async () => {
|
||||
const data = await apiFetch<any>(`/api/global/metrics/mcp-usage/daily`);
|
||||
expect(Array.isArray(data.summaries)).toBe(true);
|
||||
if (data.summaries.length > 0) {
|
||||
expect(data.summaries[0]).toHaveProperty('date');
|
||||
expect(data.summaries[0]).toHaveProperty('totalRequests');
|
||||
}
|
||||
});
|
||||
|
||||
it('should return global per-record usage', async () => {
|
||||
const data = await apiFetch<any>(`/api/global/metrics/mcp-usage`);
|
||||
// Response structure depends on implementation, just verify no error
|
||||
expect(data).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('get_audit_logs', () => {
|
||||
it('should return audit log entries (cross-tenant)', async () => {
|
||||
const data = await apiFetch<any>(`/api/global/audit/logs`);
|
||||
expect(data.success).toBe(true);
|
||||
expect(Array.isArray(data.logs)).toBe(true);
|
||||
if (data.logs.length > 0) {
|
||||
const log = data.logs[0];
|
||||
expect(log).toHaveProperty('action');
|
||||
expect(log).toHaveProperty('entityType');
|
||||
expect(log).toHaveProperty('performedBy');
|
||||
expect(log).toHaveProperty('timestamp');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('get_ops_events', () => {
|
||||
it('should return ops events', async () => {
|
||||
const data = await apiFetch<any>(`/api/global/ops-events?maxResults=5`);
|
||||
expect(data.success).toBe(true);
|
||||
expect(Array.isArray(data.events)).toBe(true);
|
||||
});
|
||||
|
||||
it('should filter by category', async () => {
|
||||
const data = await apiFetch<any>(`/api/global/ops-events?category=Security&maxResults=5`);
|
||||
expect(data.success).toBe(true);
|
||||
for (const evt of data.events) {
|
||||
expect(evt.category).toBe('Security');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('list_session_reports', () => {
|
||||
it('should return session reports', async () => {
|
||||
const data = await apiFetch<any>(`/api/global/session-reports`);
|
||||
expect(data.success).toBe(true);
|
||||
expect(Array.isArray(data.reports)).toBe(true);
|
||||
if (data.reports.length > 0) {
|
||||
expect(data.reports[0]).toHaveProperty('reportId');
|
||||
expect(data.reports[0]).toHaveProperty('sessionId');
|
||||
expect(data.reports[0]).toHaveProperty('tenantId');
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* Integration tests for MCP raw data tools (tables, raw sessions/events, backend logs).
|
||||
* Tests against the live backend — requires AUTOPILOT_API_TOKEN env var.
|
||||
*/
|
||||
import { describe, it, expect, beforeAll } from 'vitest';
|
||||
import { apiFetch, getToken } from './helpers.js';
|
||||
|
||||
let knownTenantId: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
getToken();
|
||||
// Discover a tenant with data
|
||||
const data = await apiFetch<any>(`/api/global/search/sessions?limit=1`);
|
||||
knownTenantId = data.sessions[0]?.tenantId ?? '';
|
||||
});
|
||||
|
||||
describe('list_tables', () => {
|
||||
it('should return list of Azure Table Storage tables', async () => {
|
||||
const data = await apiFetch<any>(`/api/global/raw/tables`);
|
||||
expect(data).toHaveProperty('tables');
|
||||
expect(Array.isArray(data.tables)).toBe(true);
|
||||
expect(data.tables.length).toBeGreaterThan(10);
|
||||
expect(data.tables).toContain('Sessions');
|
||||
expect(data.tables).toContain('Events');
|
||||
expect(data.tables).toContain('TenantConfiguration');
|
||||
});
|
||||
});
|
||||
|
||||
describe('query_table', () => {
|
||||
it('should query TenantConfiguration table', async () => {
|
||||
const data = await apiFetch<any>(`/api/global/raw/tables/TenantConfiguration?limit=2`);
|
||||
expect(data).toHaveProperty('entities');
|
||||
expect(Array.isArray(data.entities)).toBe(true);
|
||||
if (data.entities.length > 0) {
|
||||
expect(data.entities[0]).toHaveProperty('PartitionKey');
|
||||
expect(data.entities[0]).toHaveProperty('RowKey');
|
||||
}
|
||||
});
|
||||
|
||||
it('should return error for non-existent table', async () => {
|
||||
const data = await apiFetch<any>(
|
||||
`/api/global/raw/tables/ZZZ_NonExistent_Table`,
|
||||
{ expectStatus: 404 },
|
||||
);
|
||||
// Should be a structured error
|
||||
expect(data).toBeDefined();
|
||||
});
|
||||
|
||||
it('should filter by partitionKey', async () => {
|
||||
if (!knownTenantId) return;
|
||||
const data = await apiFetch<any>(
|
||||
`/api/global/raw/tables/Sessions?partitionKey=${knownTenantId}&limit=2`,
|
||||
);
|
||||
expect(data).toHaveProperty('entities');
|
||||
for (const entity of data.entities) {
|
||||
expect(entity.PartitionKey).toBe(knownTenantId);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('query_raw_sessions', () => {
|
||||
it('should return raw sessions (cross-tenant)', async () => {
|
||||
const data = await apiFetch<any>(`/api/global/raw/sessions?limit=3`);
|
||||
expect(data).toHaveProperty('sessions');
|
||||
expect(Array.isArray(data.sessions)).toBe(true);
|
||||
expect(data.sessions.length).toBeLessThanOrEqual(3);
|
||||
});
|
||||
|
||||
it('should filter by status', async () => {
|
||||
const data = await apiFetch<any>(`/api/global/raw/sessions?status=Succeeded&limit=2`);
|
||||
for (const s of data.sessions) {
|
||||
expect(s.status).toBe('Succeeded');
|
||||
}
|
||||
});
|
||||
|
||||
it('should support field projection', async () => {
|
||||
const data = await apiFetch<any>(
|
||||
`/api/global/raw/sessions?fields=sessionId,status&limit=2`,
|
||||
);
|
||||
expect(data.sessions.length).toBeGreaterThan(0);
|
||||
const s = data.sessions[0];
|
||||
expect(s).toHaveProperty('sessionId');
|
||||
expect(s).toHaveProperty('status');
|
||||
});
|
||||
});
|
||||
|
||||
describe('query_raw_events', () => {
|
||||
it('should return events for a tenant', async () => {
|
||||
if (!knownTenantId) return;
|
||||
const data = await apiFetch<any>(
|
||||
`/api/raw/events?tenantId=${knownTenantId}&eventType=enrollment_complete&limit=2`,
|
||||
);
|
||||
expect(data).toHaveProperty('events');
|
||||
expect(Array.isArray(data.events)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('query_backend_logs', () => {
|
||||
it('should execute KQL query (POST)', async () => {
|
||||
const data = await apiFetch<any>(`/api/global/raw/logs`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
query: 'traces | take 1',
|
||||
timespan: 'PT1H',
|
||||
}),
|
||||
});
|
||||
expect(data).toHaveProperty('tables');
|
||||
expect(Array.isArray(data.tables)).toBe(true);
|
||||
expect(data.tables[0]).toHaveProperty('columns');
|
||||
expect(data.tables[0]).toHaveProperty('rows');
|
||||
});
|
||||
});
|
||||
|
||||
describe('read-only verification', () => {
|
||||
it('should reject PUT on session endpoint', async () => {
|
||||
// Verify the API doesn't accept mutation methods on read endpoints
|
||||
try {
|
||||
await apiFetch(`/api/sessions/00000000-0000-0000-0000-000000000001`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ status: 'Failed' }),
|
||||
});
|
||||
// If it doesn't throw, the endpoint exists for PUT — that would be unexpected
|
||||
expect.fail('PUT should not succeed on a read endpoint');
|
||||
} catch (e: any) {
|
||||
// Expected: 404 (no route) or 405 (method not allowed) or similar
|
||||
expect(e.message).toMatch(/API (404|405|400)/);
|
||||
}
|
||||
});
|
||||
|
||||
it('should reject DELETE on session events endpoint', async () => {
|
||||
try {
|
||||
await apiFetch(`/api/sessions/00000000-0000-0000-0000-000000000001/events`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
expect.fail('DELETE should not succeed on a read endpoint');
|
||||
} catch (e: any) {
|
||||
expect(e.message).toMatch(/API (404|405|400)/);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* Integration tests for MCP session tools.
|
||||
* Tests against the live backend — requires AUTOPILOT_API_TOKEN env var.
|
||||
*/
|
||||
import { describe, it, expect, beforeAll } from 'vitest';
|
||||
import { apiFetch, buildQuery, getToken } from './helpers.js';
|
||||
|
||||
// Shared state discovered during tests
|
||||
let knownSessionId: string;
|
||||
let knownTenantId: string;
|
||||
|
||||
beforeAll(() => {
|
||||
getToken(); // Fail fast if no token
|
||||
});
|
||||
|
||||
describe('search_sessions', () => {
|
||||
it('should return sessions (cross-tenant, no filters)', async () => {
|
||||
const data = await apiFetch<any>(`/api/global/search/sessions?limit=3`);
|
||||
expect(data.success).toBe(true);
|
||||
expect(Array.isArray(data.sessions)).toBe(true);
|
||||
expect(data.sessions.length).toBeGreaterThan(0);
|
||||
expect(data.sessions.length).toBeLessThanOrEqual(3);
|
||||
|
||||
// Store for subsequent tests
|
||||
knownSessionId = data.sessions[0].sessionId;
|
||||
knownTenantId = data.sessions[0].tenantId;
|
||||
|
||||
// Validate session shape
|
||||
const s = data.sessions[0];
|
||||
expect(s).toHaveProperty('sessionId');
|
||||
expect(s).toHaveProperty('tenantId');
|
||||
expect(s).toHaveProperty('status');
|
||||
expect(s).toHaveProperty('serialNumber');
|
||||
});
|
||||
|
||||
it('should filter by status', async () => {
|
||||
const data = await apiFetch<any>(`/api/global/search/sessions?status=Failed&limit=2`);
|
||||
expect(data.success).toBe(true);
|
||||
for (const s of data.sessions) {
|
||||
expect(s.status).toBe('Failed');
|
||||
}
|
||||
});
|
||||
|
||||
it('should return empty for impossible filter', async () => {
|
||||
const data = await apiFetch<any>(
|
||||
`/api/global/search/sessions?serialNumber=ZZZZZ_NONEXISTENT_99999&limit=1`,
|
||||
);
|
||||
expect(data.success).toBe(true);
|
||||
expect(data.sessions).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('search_sessions_by_event', () => {
|
||||
it('should find sessions with enrollment_complete events', async () => {
|
||||
const data = await apiFetch<any>(
|
||||
`/api/global/search/sessions-by-event?eventType=enrollment_complete&limit=3`,
|
||||
);
|
||||
expect(data.success).toBe(true);
|
||||
expect(Array.isArray(data.sessions)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return empty for unknown event type', async () => {
|
||||
const data = await apiFetch<any>(
|
||||
`/api/global/search/sessions-by-event?eventType=totally_fake_event&limit=1`,
|
||||
);
|
||||
expect(data.success).toBe(true);
|
||||
expect(data.count).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('get_session', () => {
|
||||
it('should return session details', async () => {
|
||||
const data = await apiFetch<any>(`/api/sessions/${knownSessionId}`);
|
||||
expect(data.success).toBe(true);
|
||||
expect(data.session).toBeDefined();
|
||||
expect(data.session.sessionId).toBe(knownSessionId);
|
||||
});
|
||||
|
||||
it('should return 404 for non-existent session', async () => {
|
||||
const data = await apiFetch<any>(
|
||||
`/api/sessions/00000000-0000-0000-0000-000000000000`,
|
||||
{ expectStatus: 404 },
|
||||
);
|
||||
expect(data.success).toBe(false);
|
||||
});
|
||||
|
||||
it('should reject invalid UUID format', async () => {
|
||||
const data = await apiFetch<any>(
|
||||
`/api/sessions/not-a-guid`,
|
||||
{ expectStatus: 400 },
|
||||
);
|
||||
expect(data.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('get_session_events', () => {
|
||||
it('should return events for a session', async () => {
|
||||
const data = await apiFetch<any>(`/api/sessions/${knownSessionId}/events`);
|
||||
expect(data.success).toBe(true);
|
||||
expect(data.sessionId).toBe(knownSessionId);
|
||||
expect(Array.isArray(data.events)).toBe(true);
|
||||
expect(data.count).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should have correct tenantId (not composite key)', async () => {
|
||||
const data = await apiFetch<any>(`/api/sessions/${knownSessionId}/events`);
|
||||
expect(data.events.length).toBeGreaterThan(0);
|
||||
const event = data.events[0];
|
||||
// TenantId should NOT contain underscore + sessionId (composite key bug)
|
||||
expect(event.tenantId).not.toContain('_');
|
||||
expect(event.tenantId).toHaveLength(36); // UUID length
|
||||
});
|
||||
|
||||
it('should return events for cross-tenant session without tenantId (Global Admin fallback)', async () => {
|
||||
// This tests the cross-tenant fix we deployed
|
||||
const data = await apiFetch<any>(`/api/sessions/${knownSessionId}/events`);
|
||||
expect(data.success).toBe(true);
|
||||
expect(data.count).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should return 0 events for non-existent session (not an error)', async () => {
|
||||
// A valid GUID that doesn't exist should return success with 0 events
|
||||
const data = await apiFetch<any>(
|
||||
`/api/sessions/00000000-0000-0000-0000-000000000001/events`,
|
||||
);
|
||||
expect(data.success).toBe(true);
|
||||
expect(data.count).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('get_session_analysis', () => {
|
||||
it('should return analysis results', async () => {
|
||||
const data = await apiFetch<any>(`/api/sessions/${knownSessionId}/analysis`);
|
||||
expect(data.success).toBe(true);
|
||||
expect(data.sessionId).toBe(knownSessionId);
|
||||
expect(Array.isArray(data.results)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('search_sessions_by_cve', () => {
|
||||
it('should return results (possibly empty) for a CVE search', async () => {
|
||||
const data = await apiFetch<any>(
|
||||
`/api/global/search/sessions-by-cve?cveId=CVE-2024-21447&limit=3`,
|
||||
);
|
||||
expect(data.success).toBe(true);
|
||||
expect(Array.isArray(data.sessions)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return empty for nonexistent CVE', async () => {
|
||||
const data = await apiFetch<any>(
|
||||
`/api/global/search/sessions-by-cve?cveId=CVE-9999-99999&limit=1`,
|
||||
);
|
||||
expect(data.success).toBe(true);
|
||||
expect(data.count).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('list_blocked_devices', () => {
|
||||
it('should return blocked devices list (cross-tenant)', async () => {
|
||||
const data = await apiFetch<any>(`/api/global/devices/blocked`);
|
||||
expect(data.success).toBe(true);
|
||||
expect(Array.isArray(data.blocked)).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
testTimeout: 30_000,
|
||||
hookTimeout: 10_000,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user