diff --git a/products/02-iac-drift-detection/saas/tests/unit/ingestion.test.ts b/products/02-iac-drift-detection/saas/tests/unit/ingestion.test.ts new file mode 100644 index 0000000..8ff6548 --- /dev/null +++ b/products/02-iac-drift-detection/saas/tests/unit/ingestion.test.ts @@ -0,0 +1,65 @@ +import { describe, it, expect } from 'vitest'; + +// Mock withTenant for unit tests +const mockQuery = vi.fn(); +const mockClient = { + query: mockQuery, +}; + +vi.mock('../../src/data/db.js', () => ({ + pool: {}, + withTenant: vi.fn(async (_tenantId: string, fn: any) => fn(mockClient)), +})); + +import { vi } from 'vitest'; + +describe('Ingestion Endpoint', () => { + it('validates nonce format (UUID v4)', () => { + const validNonce = '550e8400-e29b-41d4-a716-446655440000'; + const invalidNonce = 'not-a-uuid'; + const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + + expect(uuidRegex.test(validNonce)).toBe(true); + expect(uuidRegex.test(invalidNonce)).toBe(false); + }); + + it('rejects duplicate nonces', () => { + const seen = new Set(); + const nonce = '550e8400-e29b-41d4-a716-446655440000'; + + expect(seen.has(nonce)).toBe(false); + seen.add(nonce); + expect(seen.has(nonce)).toBe(true); // Duplicate detected + }); +}); + +describe('Drift Report Schema', () => { + it('validates severity levels', () => { + const validSeverities = ['critical', 'high', 'medium', 'low']; + const invalid = 'extreme'; + + for (const s of validSeverities) { + expect(['critical', 'high', 'medium', 'low'].includes(s)).toBe(true); + } + expect(['critical', 'high', 'medium', 'low'].includes(invalid)).toBe(false); + }); + + it('requires tenant_id in report', () => { + const report = { stack_name: 'prod', resources: [] }; + expect(report).not.toHaveProperty('tenant_id'); + + const validReport = { ...report, tenant_id: 'abc-123' }; + expect(validReport).toHaveProperty('tenant_id'); + }); +}); + +describe('RLS withTenant', () => { + it('sets and resets tenant context', async () => { + const { withTenant } = await import('../../src/data/db.js'); + + await withTenant('tenant-1', async (client: any) => { + // Inside the transaction, tenant context should be set + expect(client.query).toBeDefined(); + }); + }); +}); diff --git a/products/03-alert-intelligence/tests/unit/notifications.test.ts b/products/03-alert-intelligence/tests/unit/notifications.test.ts new file mode 100644 index 0000000..be1acdb --- /dev/null +++ b/products/03-alert-intelligence/tests/unit/notifications.test.ts @@ -0,0 +1,56 @@ +import { describe, it, expect } from 'vitest'; + +describe('Notification Dispatcher', () => { + const SEVERITY_ORDER: Record = { critical: 5, high: 4, medium: 3, low: 2, info: 1 }; + + it('dispatches to channels meeting severity threshold', () => { + const incidentSeverity = 'high'; + const channelMinSeverity = 'medium'; + + const incidentLevel = SEVERITY_ORDER[incidentSeverity] ?? 0; + const minLevel = SEVERITY_ORDER[channelMinSeverity] ?? 0; + + expect(incidentLevel >= minLevel).toBe(true); + }); + + it('skips channels below severity threshold', () => { + const incidentSeverity = 'low'; + const channelMinSeverity = 'high'; + + const incidentLevel = SEVERITY_ORDER[incidentSeverity] ?? 0; + const minLevel = SEVERITY_ORDER[channelMinSeverity] ?? 0; + + expect(incidentLevel >= minLevel).toBe(false); + }); + + it('critical always dispatches', () => { + const incidentLevel = SEVERITY_ORDER['critical']!; + for (const [, minLevel] of Object.entries(SEVERITY_ORDER)) { + expect(incidentLevel >= minLevel).toBe(true); + } + }); + + it('info only dispatches to info channels', () => { + const incidentLevel = SEVERITY_ORDER['info']!; + expect(incidentLevel >= SEVERITY_ORDER['info']!).toBe(true); + expect(incidentLevel >= SEVERITY_ORDER['low']!).toBe(false); + }); +}); + +describe('Slack Block Kit Builder', () => { + it('maps severity to emoji', () => { + const emojiMap: Record = { + critical: '🔴', high: '🟠', medium: '🟡', low: 'đŸ”ĩ', info: 'â„šī¸', + }; + + expect(emojiMap['critical']).toBe('🔴'); + expect(emojiMap['high']).toBe('🟠'); + expect(emojiMap['unknown']).toBeUndefined(); + }); + + it('includes action buttons', () => { + const actions = ['view_incident', 'ack_incident', 'suppress_incident']; + expect(actions).toHaveLength(3); + expect(actions).toContain('ack_incident'); + }); +}); diff --git a/products/04-lightweight-idp/tests/unit/search.test.ts b/products/04-lightweight-idp/tests/unit/search.test.ts new file mode 100644 index 0000000..e7d02c2 --- /dev/null +++ b/products/04-lightweight-idp/tests/unit/search.test.ts @@ -0,0 +1,48 @@ +import { describe, it, expect } from 'vitest'; + +describe('Meilisearch Fallback', () => { + it('ILIKE pattern escapes special characters', () => { + const query = 'test%query'; + const escaped = `%${query}%`; + // In real code, we'd need to escape % and _ in user input + expect(escaped).toContain(query); + }); + + it('search results include fallback flag when Meili is down', () => { + const fallbackResult = { hits: [], total: 0, query: 'test', fallback: true }; + expect(fallbackResult.fallback).toBe(true); + }); +}); + +describe('Service CRUD Validation', () => { + it('rejects empty service name', () => { + const name = ''; + expect(name.length >= 1).toBe(false); + }); + + it('accepts valid service name', () => { + const name = 'auth-service'; + expect(name.length >= 1 && name.length <= 200).toBe(true); + }); + + it('validates lifecycle values', () => { + const valid = ['active', 'deprecated', 'decommissioned']; + expect(valid.includes('active')).toBe(true); + expect(valid.includes('deleted')).toBe(false); + }); + + it('validates tier values', () => { + const valid = ['critical', 'high', 'medium', 'low']; + expect(valid.includes('critical')).toBe(true); + expect(valid.includes('ultra')).toBe(false); + }); +}); + +describe('Discovery Staged Updates', () => { + it('only allows apply or reject actions', () => { + const validActions = ['apply', 'reject']; + expect(validActions.includes('apply')).toBe(true); + expect(validActions.includes('reject')).toBe(true); + expect(validActions.includes('delete')).toBe(false); + }); +}); diff --git a/products/05-aws-cost-anomaly/tests/unit/ingestion.test.ts b/products/05-aws-cost-anomaly/tests/unit/ingestion.test.ts new file mode 100644 index 0000000..e815738 --- /dev/null +++ b/products/05-aws-cost-anomaly/tests/unit/ingestion.test.ts @@ -0,0 +1,44 @@ +import { describe, it, expect } from 'vitest'; + +describe('Cost Ingestion Validation', () => { + it('rejects negative hourly cost', () => { + const cost = -5.00; + expect(cost >= 0).toBe(false); + }); + + it('accepts zero cost', () => { + const cost = 0; + expect(cost >= 0).toBe(true); + }); + + it('batch size limited to 100', () => { + const events = Array.from({ length: 101 }, (_, i) => ({ account_id: 'a', resource_type: 'ec2', hourly_cost: i })); + expect(events.length <= 100).toBe(false); + + const validBatch = events.slice(0, 100); + expect(validBatch.length <= 100).toBe(true); + }); +}); + +describe('Anomaly Snooze', () => { + it('accepts 1-168 hour range', () => { + expect(1 >= 1 && 1 <= 168).toBe(true); + expect(168 >= 1 && 168 <= 168).toBe(true); + expect(0 >= 1).toBe(false); + expect(169 <= 168).toBe(false); + }); +}); + +describe('Optimistic Locking', () => { + it('detects version conflict', () => { + const currentVersion = 5; + const expectedVersion = 4; // Stale read + expect(currentVersion === expectedVersion).toBe(false); + }); + + it('allows update when versions match', () => { + const currentVersion = 5; + const expectedVersion = 5; + expect(currentVersion === expectedVersion).toBe(true); + }); +}); diff --git a/products/06-runbook-automation/saas/tests/unit/api.test.ts b/products/06-runbook-automation/saas/tests/unit/api.test.ts new file mode 100644 index 0000000..6d4859b --- /dev/null +++ b/products/06-runbook-automation/saas/tests/unit/api.test.ts @@ -0,0 +1,63 @@ +import { describe, it, expect } from 'vitest'; + +describe('Runbook API Validation', () => { + it('rejects empty runbook name', () => { + const name = ''; + expect(name.length >= 1).toBe(false); + }); + + it('accepts valid runbook name', () => { + const name = 'restart-ecs-service'; + expect(name.length >= 1 && name.length <= 200).toBe(true); + }); + + it('requires yaml_content', () => { + const body = { name: 'test', description: 'desc' }; + expect(body).not.toHaveProperty('yaml_content'); + }); +}); + +describe('Approval Decisions', () => { + it('only allows approve or reject', () => { + const valid = ['approve', 'reject']; + expect(valid.includes('approve')).toBe(true); + expect(valid.includes('reject')).toBe(true); + expect(valid.includes('maybe')).toBe(false); + }); + + it('reason is optional and max 500 chars', () => { + const shortReason = 'Looks safe'; + const longReason = 'x'.repeat(501); + expect(shortReason.length <= 500).toBe(true); + expect(longReason.length <= 500).toBe(false); + }); +}); + +describe('Execution Status Machine', () => { + const validStatuses = ['pending', 'running', 'awaiting_approval', 'completed', 'failed', 'aborted']; + + it('starts in pending', () => { + expect(validStatuses[0]).toBe('pending'); + }); + + it('validates all status values', () => { + for (const s of validStatuses) { + expect(validStatuses.includes(s)).toBe(true); + } + expect(validStatuses.includes('cancelled')).toBe(false); + }); +}); + +describe('Slack Signature Verification', () => { + it('rejects stale timestamps (>5 min)', () => { + const now = Math.floor(Date.now() / 1000); + const staleTs = now - 301; + expect(Math.abs(now - staleTs) > 300).toBe(true); + }); + + it('accepts fresh timestamps', () => { + const now = Math.floor(Date.now() / 1000); + const freshTs = now - 10; + expect(Math.abs(now - freshTs) > 300).toBe(false); + }); +});