UNPKG

autotel

Version:
362 lines (300 loc) 10.3 kB
/** * Integration tests for autotel * * Tests end-to-end flows with all components working together */ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { init } from './init'; import { trace } from './functional'; import { track, resetEventQueue } from './track'; import { Event, resetEvents } from './event'; import { flush, shutdown } from './shutdown'; import { resetMetrics } from './metric'; import { resetConfig } from './config'; import { EventAttributes, EventSubscriber } from './event-subscriber'; // Test adapter that collects events class TestAdapter implements EventSubscriber { name = 'test-adapter'; events: Array<{ type: string; name: string; attributes?: EventAttributes }> = []; async trackEvent(name: string, attributes?: EventAttributes): Promise<void> { this.events.push({ type: 'event', name, attributes }); } async trackFunnelStep( funnel: string, status: string, attributes?: EventAttributes, ): Promise<void> { this.events.push({ type: 'funnel', name: `${funnel}.${status}`, attributes, }); } async trackOutcome( operation: string, status: string, attributes?: EventAttributes, ): Promise<void> { this.events.push({ type: 'outcome', name: `${operation}.${status}`, attributes, }); } async trackValue( metric: string, value: number, attributes?: EventAttributes, ): Promise<void> { this.events.push({ type: 'value', name: metric, attributes: { ...attributes, value }, }); } async shutdown(): Promise<void> { // Cleanup if needed } reset(): void { this.events = []; } } describe('Integration Test Suite', () => { let testAdapter: TestAdapter; beforeEach(() => { // Reset all global state between tests resetEventQueue(); resetEvents(); resetMetrics(); resetConfig(); testAdapter = new TestAdapter(); }); afterEach(async () => { // Clean up after each test await shutdown(); }); describe('Full stack initialization', () => { it('should initialize all components together', () => { init({ service: 'test-app', subscribers: [testAdapter], version: '1.0.0', environment: 'test', }); expect(true).toBe(true); // Should not throw }); }); describe('Tracing + Events integration', () => { it('should correlate traces with events events', async () => { init({ service: 'user-service', subscribers: [testAdapter], }); // Create trace function const createUser = trace(async (userId: string, plan: string) => { // Track events event inside trace function track('user.signup', { userId, plan }); return { id: userId, plan }; }); // Execute const result = await createUser('user123', 'pro'); // Should return expected result expect(result).toEqual({ id: 'user123', plan: 'pro' }); // Flush to send events await flush(); // Events event should be tracked // (traceId correlation happens automatically) const event = testAdapter.events.filter((e) => e.name === 'user.signup'); expect(event.length).toBeGreaterThan(0); }); it('should handle nested trace functions', async () => { init({ service: 'order-service', subscribers: [testAdapter], }); const validateOrder = trace(async (orderId: string) => { track('order.validated', { orderId }); return true; }); const processPayment = trace(async (orderId: string, amount: number) => { track('payment.processed', { orderId, amount }); return { success: true }; }); const createOrder = trace(async (orderId: string, amount: number) => { await validateOrder(orderId); await processPayment(orderId, amount); track('order.completed', { orderId, amount }); return { orderId, status: 'completed' }; }); await createOrder('order123', 99.99); await flush(); // All events should be tracked expect(testAdapter.events.length).toBeGreaterThanOrEqual(3); expect(testAdapter.events.map((e) => e.name)).toContain( 'order.validated', ); expect(testAdapter.events.map((e) => e.name)).toContain( 'payment.processed', ); expect(testAdapter.events.map((e) => e.name)).toContain( 'order.completed', ); }); }); describe('Events class integration', () => { it('should track all events event types', async () => { const event = new Event('checkout', { subscribers: [testAdapter], }); // Track different event types event.trackEvent('checkout.started', { cartValue: 149.99 }); event.trackFunnelStep('checkout', 'started', { userId: '123' }); event.trackFunnelStep('checkout', 'started', { userId: '123', step: 'payment_info', }); event.trackFunnelStep('checkout', 'completed', { userId: '123' }); event.trackOutcome('payment.process', 'success', { amount: 149.99 }); event.trackValue('revenue', 149.99, { currency: 'USD' }); // Flush adapters await event.flush(); // All events should be captured expect(testAdapter.events.length).toBe(6); expect(testAdapter.events.map((e) => e.type)).toEqual([ 'event', 'funnel', 'funnel', 'funnel', 'outcome', 'value', ]); }); }); describe('Error handling integration', () => { it('should handle adapter failures gracefully', async () => { const failingAdapter: EventSubscriber = { name: 'failing-adapter', trackEvent: async () => { throw new Error('Adapter error'); }, trackFunnelStep: async () => {}, trackOutcome: async () => {}, trackValue: async () => {}, }; const event = new Event('test', { subscribers: [failingAdapter, testAdapter], // Mix failing and working }); // Should not throw even though one adapter fails event.trackEvent('test.event', { foo: 'bar' }); await event.flush(); // Working adapter should still receive events expect(testAdapter.events.length).toBeGreaterThan(0); }); it('should handle circuit breaker opening', async () => { let callCount = 0; const unreliableAdapter: EventSubscriber = { name: 'unreliable-adapter', trackEvent: async () => { callCount++; // Fail first 5 times to trip circuit breaker if (callCount <= 5) { throw new Error('Service unavailable'); } }, trackFunnelStep: async () => {}, trackOutcome: async () => {}, trackValue: async () => {}, }; const event = new Event('test', { subscribers: [unreliableAdapter], }); // Send events one at a time to allow circuit breaker to open for (let i = 0; i < 10; i++) { event.trackEvent(`event.${i}`, { index: i }); // Wait a bit to allow circuit breaker to process await new Promise((resolve) => setTimeout(resolve, 10)); } await event.flush(); // Circuit should open after 5 failures // Remaining calls should be fast-failed expect(callCount).toBeLessThanOrEqual(6); // 5 failures + maybe 1 half-open test }); }); describe('Input validation integration', () => { it('should sanitize sensitive data across all components', async () => { init({ service: 'auth-service', subscribers: [testAdapter], }); // Track with sensitive data track('user.login', { email: 'user@example.com', password: 'secret123', // Should be redacted apiKey: 'abc123', // Should be redacted userId: '123', // Should NOT be redacted }); await flush(); const event = testAdapter.events.find((e) => e.name === 'user.login'); expect(event?.attributes?.email).toBe('user@example.com'); expect(event?.attributes?.password).toBe('[REDACTED]'); expect(event?.attributes?.apiKey).toBe('[REDACTED]'); expect(event?.attributes?.userId).toBe('123'); }); it('should validate event names', () => { const event = new Event('test', { subscribers: [testAdapter], }); // Invalid event names should throw expect(() => { event.trackEvent('', { foo: 'bar' }); }).toThrow(); expect(() => { event.trackEvent('invalid event name', { foo: 'bar' }); }).toThrow(); }); }); describe('Graceful shutdown integration', () => { it('should shutdown all components cleanly', async () => { init({ service: 'test-service', subscribers: [testAdapter], }); track('test.event', { foo: 'bar' }); await shutdown(); // Events should be flushed expect(testAdapter.events.length).toBeGreaterThan(0); }); }); describe('Performance under load', () => { it('should handle high event volume', async () => { init({ service: 'high-volume-service', subscribers: [testAdapter], }); // Send 1000 events for (let i = 0; i < 1000; i++) { track(`event.${i}`, { index: i }); } await flush({ timeout: 10_000 }); // 10s timeout for high-volume test // All events should be captured expect(testAdapter.events.length).toBeGreaterThanOrEqual(1000); }); it('should batch events efficiently', async () => { init({ service: 'batch-test', subscribers: [testAdapter], }); const startTime = Date.now(); // Track 500 events for (let i = 0; i < 500; i++) { track(`event.${i}`, { index: i }); } await flush({ timeout: 10_000 }); // 10s timeout for high-volume test const duration = Date.now() - startTime; // Should complete in reasonable time (batching improves performance) // This is a sanity check - actual threshold depends on hardware expect(duration).toBeLessThan(10_000); // 10 seconds (increased for CI/CD reliability) }); }); });