kell_creations/kell_creations_apps/packages/integrations/test/integrations_test.dart

367 lines
13 KiB
Dart

import 'package:flutter_test/flutter_test.dart';
import 'package:integrations/integrations.dart';
// ── Test doubles ─────────────────────────────────────────────────────────────
class _AlwaysHealthyCheck implements IntegrationHealthCheck {
@override
Future<HealthCheckResult> check() async => const HealthCheckResult.healthy();
}
class _AlwaysUnhealthyCheck implements IntegrationHealthCheck {
@override
Future<HealthCheckResult> 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<WebhookResult> 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<Exception>());
});
});
// ── 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');
});
});
}