import 'package:flutter_test/flutter_test.dart'; import 'package:integrations/integrations.dart'; // ── Test doubles ───────────────────────────────────────────────────────────── class _AlwaysHealthyCheck implements IntegrationHealthCheck { @override Future check() async => const HealthCheckResult.healthy(); } class _AlwaysUnhealthyCheck implements IntegrationHealthCheck { @override Future check() async => const HealthCheckResult.unhealthy(message: 'Connection refused'); } class _EchoWebhookHandler implements WebhookHandler { final String _handledSource; _EchoWebhookHandler(this._handledSource); @override bool canHandle(WebhookEvent event) => event.source == _handledSource; @override Future handle(WebhookEvent event) async => WebhookResult.processed(message: 'handled ${event.eventType}'); } void main() { // ── ApiException ──────────────────────────────────────────────────────── group('ApiException', () { test('stores statusCode, message, and body', () { const ex = ApiException(statusCode: 404, message: 'Not found', body: '{}'); expect(ex.statusCode, 404); expect(ex.message, 'Not found'); expect(ex.body, '{}'); }); test('body defaults to empty string', () { const ex = ApiException(statusCode: 500, message: 'Server error'); expect(ex.body, ''); }); test('toString includes statusCode and message', () { const ex = ApiException(statusCode: 401, message: 'Unauthorized'); expect(ex.toString(), contains('401')); expect(ex.toString(), contains('Unauthorized')); }); test('is an Exception', () { const ex = ApiException(statusCode: 400, message: 'Bad request'); expect(ex, isA()); }); }); // ── RetryPolicy ───────────────────────────────────────────────────────── group('NoRetryPolicy', () { const policy = NoRetryPolicy(); test('maxAttempts is 0', () => expect(policy.maxAttempts, 0)); test('delayFor returns zero', () => expect(policy.delayFor(1), Duration.zero)); test('shouldRetry always returns false', () { const ex = ApiException(statusCode: 503, message: 'Service unavailable'); expect(policy.shouldRetry(ex), isFalse); }); }); group('ExponentialBackoffRetryPolicy', () { const policy = ExponentialBackoffRetryPolicy( maxAttempts: 3, initialDelay: Duration(milliseconds: 100), maxDelay: Duration(seconds: 10), ); test('maxAttempts is 3', () => expect(policy.maxAttempts, 3)); test('delay doubles with each attempt', () { expect(policy.delayFor(1), const Duration(milliseconds: 100)); expect(policy.delayFor(2), const Duration(milliseconds: 200)); expect(policy.delayFor(3), const Duration(milliseconds: 400)); }); test('delay is capped at maxDelay', () { const bigPolicy = ExponentialBackoffRetryPolicy( maxAttempts: 10, initialDelay: Duration(seconds: 5), maxDelay: Duration(seconds: 10), ); expect(bigPolicy.delayFor(5).inSeconds, lessThanOrEqualTo(10)); }); test('shouldRetry returns true for 5xx errors', () { const ex500 = ApiException(statusCode: 500, message: 'Internal server error'); const ex503 = ApiException(statusCode: 503, message: 'Service unavailable'); expect(policy.shouldRetry(ex500), isTrue); expect(policy.shouldRetry(ex503), isTrue); }); test('shouldRetry returns true for network-level errors (statusCode -1)', () { const ex = ApiException(statusCode: -1, message: 'Network error'); expect(policy.shouldRetry(ex), isTrue); }); test('shouldRetry returns false for 4xx client errors', () { const ex = ApiException(statusCode: 404, message: 'Not found'); expect(policy.shouldRetry(ex), isFalse); }); test('shouldRetry returns false for 2xx (should not happen but safe)', () { const ex = ApiException(statusCode: 200, message: 'OK'); expect(policy.shouldRetry(ex), isFalse); }); }); // ── RateLimiter ───────────────────────────────────────────────────────── group('NoRateLimiter', () { test('acquire completes immediately', () async { const limiter = NoRateLimiter(); await expectLater(limiter.acquire(), completes); }); test('dispose does not throw', () { const limiter = NoRateLimiter(); expect(() => limiter.dispose(), returnsNormally); }); }); group('TokenBucketRateLimiter', () { test('allows up to maxRequests without delay', () async { final limiter = TokenBucketRateLimiter(maxRequests: 3); // All three should complete without waiting. await limiter.acquire(); await limiter.acquire(); await limiter.acquire(); limiter.dispose(); }); test('dispose does not throw', () { final limiter = TokenBucketRateLimiter(maxRequests: 5); expect(() => limiter.dispose(), returnsNormally); }); }); // ── HealthCheckResult ──────────────────────────────────────────────────── group('HealthCheckResult', () { test('healthy constructor sets isHealthy true and default message', () { const result = HealthCheckResult.healthy(); expect(result.isHealthy, isTrue); expect(result.message, 'OK'); expect(result.latency, isNull); }); test('healthy constructor accepts custom message and latency', () { const result = HealthCheckResult.healthy( message: 'Ping 42ms', latency: Duration(milliseconds: 42), ); expect(result.isHealthy, isTrue); expect(result.message, 'Ping 42ms'); expect(result.latency, const Duration(milliseconds: 42)); }); test('unhealthy constructor sets isHealthy false', () { const result = HealthCheckResult.unhealthy(message: 'Timeout'); expect(result.isHealthy, isFalse); expect(result.message, 'Timeout'); expect(result.latency, isNull); }); test('toString includes healthy status and message', () { const result = HealthCheckResult.healthy(message: 'OK'); expect(result.toString(), contains('true')); expect(result.toString(), contains('OK')); }); }); // ── IntegrationHealthCheck ─────────────────────────────────────────────── group('IntegrationHealthCheck', () { test('healthy implementation returns healthy result', () async { final check = _AlwaysHealthyCheck(); final result = await check.check(); expect(result.isHealthy, isTrue); }); test('unhealthy implementation returns unhealthy result', () async { final check = _AlwaysUnhealthyCheck(); final result = await check.check(); expect(result.isHealthy, isFalse); expect(result.message, 'Connection refused'); }); }); // ── WebhookEvent ───────────────────────────────────────────────────────── group('WebhookEvent', () { test('stores all fields', () { final now = DateTime(2026, 7, 11); final event = WebhookEvent( eventType: 'order.created', source: 'woocommerce', payload: {'id': 42}, receivedAt: now, ); expect(event.eventType, 'order.created'); expect(event.source, 'woocommerce'); expect(event.payload['id'], 42); expect(event.receivedAt, now); }); }); // ── WebhookResult ──────────────────────────────────────────────────────── group('WebhookResult', () { test('processed constructor sets correct status', () { const result = WebhookResult.processed(message: 'done'); expect(result.status, WebhookProcessingStatus.processed); expect(result.message, 'done'); }); test('ignored constructor sets correct status', () { const result = WebhookResult.ignored(); expect(result.status, WebhookProcessingStatus.ignored); expect(result.message, isNull); }); test('failed constructor sets correct status', () { const result = WebhookResult.failed(message: 'error'); expect(result.status, WebhookProcessingStatus.failed); expect(result.message, 'error'); }); }); // ── WebhookHandler ─────────────────────────────────────────────────────── group('WebhookHandler', () { final handler = _EchoWebhookHandler('woocommerce'); final now = DateTime(2026, 7, 11); test('canHandle returns true for matching source', () { final event = WebhookEvent( eventType: 'product.updated', source: 'woocommerce', payload: {}, receivedAt: now, ); expect(handler.canHandle(event), isTrue); }); test('canHandle returns false for non-matching source', () { final event = WebhookEvent( eventType: 'payment.completed', source: 'square', payload: {}, receivedAt: now, ); expect(handler.canHandle(event), isFalse); }); test('handle returns processed result', () async { final event = WebhookEvent( eventType: 'order.created', source: 'woocommerce', payload: {}, receivedAt: now, ); final result = await handler.handle(event); expect(result.status, WebhookProcessingStatus.processed); expect(result.message, contains('order.created')); }); }); // ── ChannelAdapter value objects ───────────────────────────────────────── group('ChannelProduct', () { test('stores required and optional fields', () { const product = ChannelProduct( id: 'p1', name: 'Test Product', price: 9.99, status: 'publish', description: 'A test product', sku: 'SKU-001', imageUrl: 'https://example.com/img.jpg', category: 'Candles', ); expect(product.id, 'p1'); expect(product.name, 'Test Product'); expect(product.price, 9.99); expect(product.status, 'publish'); expect(product.description, 'A test product'); expect(product.sku, 'SKU-001'); expect(product.imageUrl, 'https://example.com/img.jpg'); expect(product.category, 'Candles'); }); test('optional fields default to null', () { const product = ChannelProduct(id: 'p2', name: 'Minimal', price: 0, status: 'draft'); expect(product.description, isNull); expect(product.sku, isNull); expect(product.imageUrl, isNull); expect(product.category, isNull); }); }); group('ChannelOrder', () { test('stores required and optional fields', () { const order = ChannelOrder( id: 'o1', status: 'processing', total: 29.99, customerName: 'Jane Doe', customerEmail: 'jane@example.com', items: [ChannelOrderItem(productId: 'p1', name: 'Candle', quantity: 2, unitPrice: 14.99)], ); expect(order.id, 'o1'); expect(order.status, 'processing'); expect(order.total, 29.99); expect(order.customerName, 'Jane Doe'); expect(order.customerEmail, 'jane@example.com'); expect(order.items.length, 1); expect(order.items.first.productId, 'p1'); }); test('items defaults to empty list', () { const order = ChannelOrder(id: 'o2', status: 'pending', total: 0); expect(order.items, isEmpty); }); }); group('ChannelOrderItem', () { test('stores all fields', () { const item = ChannelOrderItem( productId: 'p1', name: 'Wax Melt', quantity: 3, unitPrice: 5.00, ); expect(item.productId, 'p1'); expect(item.name, 'Wax Melt'); expect(item.quantity, 3); expect(item.unitPrice, 5.00); }); }); group('ChannelInventoryCount', () { test('stores required fields', () { const count = ChannelInventoryCount(productId: 'p1', quantity: 10); expect(count.productId, 'p1'); expect(count.quantity, 10); expect(count.sku, isNull); }); test('stores optional sku', () { const count = ChannelInventoryCount(productId: 'p1', sku: 'SKU-001', quantity: 5); expect(count.sku, 'SKU-001'); }); }); }