diff --git a/packages/polling-contracts/src/__tests__/watermark-cas.test.ts b/packages/polling-contracts/src/__tests__/watermark-cas.test.ts index 1d8237c..3eef3ca 100644 --- a/packages/polling-contracts/src/__tests__/watermark-cas.test.ts +++ b/packages/polling-contracts/src/__tests__/watermark-cas.test.ts @@ -166,14 +166,14 @@ describe('Watermark CAS Race Conditions', () => { expect(coreDataResult2).toBe(false); }); - it('should verify IndexedDB CAS returns success boolean', async () => { - // Simulate IndexedDB transaction - const idbResult = await simulateIndexedDBWatermarkUpdate(null, testJwtIds[0]); - expect(idbResult).toBe(true); + it('should verify storage CAS returns success boolean', async () => { + // Simulate storage transaction + const storageResult = await simulateStorageWatermarkUpdate(null, testJwtIds[0]); + expect(storageResult).toBe(true); // Attempt to update with same condition - const idbResult2 = await simulateIndexedDBWatermarkUpdate(null, testJwtIds[0]); - expect(idbResult2).toBe(false); + const storageResult2 = await simulateStorageWatermarkUpdate(null, testJwtIds[0]); + expect(storageResult2).toBe(false); }); }); }); @@ -251,7 +251,7 @@ async function simulateCoreDataWatermarkUpdate( return false; } -async function simulateIndexedDBWatermarkUpdate( +async function simulateStorageWatermarkUpdate( expectedWatermark: string | null, newWatermark: string ): Promise { diff --git a/src/__tests__/platform-config.test.ts b/src/__tests__/platform-config.test.ts new file mode 100644 index 0000000..d74429f --- /dev/null +++ b/src/__tests__/platform-config.test.ts @@ -0,0 +1,289 @@ +/** + * Platform Configuration Tests + * + * Tests for TimeSafari-specific platform configurations including + * Android and iOS platform settings. + * + * @author Matthew Raymer + * @version 1.0.0 + */ + +import { + TimeSafariAndroidConfigManager, + TimeSafariAndroidConfig +} from '../android/timesafari-android-config'; + +import { + TimeSafariIOSConfigManager, + TimeSafariIOSConfig +} from '../ios/timesafari-ios-config'; + +describe('Platform Configuration', () => { + describe('Android Configuration', () => { + let androidConfig: TimeSafariAndroidConfigManager; + + beforeEach(() => { + androidConfig = new TimeSafariAndroidConfigManager(); + }); + + test('should initialize with default configuration', () => { + expect(androidConfig).toBeDefined(); + + const notificationChannels = androidConfig.getAllNotificationChannels(); + expect(notificationChannels).toHaveLength(5); + expect(notificationChannels[0].id).toBe('timesafari_community_updates'); + expect(notificationChannels[0].name).toBe('TimeSafari Community Updates'); + expect(notificationChannels[0].importance).toBe('default'); + }); + + test('should get notification channels configuration', () => { + const channels = androidConfig.getAllNotificationChannels(); + expect(channels).toHaveLength(5); + + const communityChannel = channels.find(c => c.id === 'timesafari_community_updates'); + expect(communityChannel).toBeDefined(); + expect(communityChannel?.name).toBe('TimeSafari Community Updates'); + expect(communityChannel?.importance).toBe('default'); + expect(communityChannel?.sound).toBe('default'); + expect(communityChannel?.enableVibration).toBe(true); + }); + + test('should get required permissions', () => { + const permissions = androidConfig.getRequiredPermissions(); + expect(permissions.some(p => p.name === 'android.permission.POST_NOTIFICATIONS')).toBe(true); + expect(permissions.some(p => p.name === 'android.permission.SCHEDULE_EXACT_ALARM')).toBe(true); + expect(permissions.some(p => p.name === 'android.permission.WAKE_LOCK')).toBe(true); + expect(permissions.some(p => p.name === 'android.permission.RECEIVE_BOOT_COMPLETED')).toBe(true); + }); + + test('should get battery optimization settings', () => { + const batterySettings = androidConfig.getBatteryOptimizationConfig(); + expect(batterySettings.exemptPackages).toContain('com.timesafari.dailynotification'); + expect(batterySettings.whitelistRequestMessage).toBeDefined(); + expect(batterySettings.optimizationCheckInterval).toBeGreaterThan(0); + }); + + test('should get WorkManager constraints', () => { + const constraints = androidConfig.getWorkManagerConstraints(); + expect(constraints.networkType).toBe('connected'); + expect(constraints.requiresBatteryNotLow).toBe(false); + expect(constraints.requiresCharging).toBe(false); + expect(constraints.requiresDeviceIdle).toBe(false); + expect(constraints.requiresStorageNotLow).toBe(true); + }); + + test('should update configuration', () => { + const updates: Partial = { + notificationChannels: [ + { + id: 'custom_channel', + name: 'Custom Channel', + description: 'Custom notification channel', + importance: 'default', + enableLights: false, + enableVibration: false, + lightColor: '#000000', + sound: null, + showBadge: true, + bypassDnd: false, + lockscreenVisibility: 'public' + } + ] + }; + + androidConfig.updateConfig(updates); + + const channels = androidConfig.getAllNotificationChannels(); + expect(channels).toHaveLength(1); + expect(channels[0].id).toBe('custom_channel'); + }); + + test('should validate configuration', () => { + const validation = androidConfig.validateConfig(); + expect(validation.valid).toBe(true); + expect(validation.errors).toEqual([]); + }); + + test('should handle configuration updates', () => { + const updates: Partial = { + notificationChannels: [ + { + id: 'test_channel', + name: 'Test Channel', + description: 'Test channel', + importance: 'default', + enableLights: false, + enableVibration: true, + lightColor: '#000000', + sound: 'default', + showBadge: true, + bypassDnd: false, + lockscreenVisibility: 'public' + } + ] + }; + + androidConfig.updateConfig(updates); + const validation = androidConfig.validateConfig(); + + expect(validation.valid).toBe(true); + }); + }); + + describe('iOS Configuration', () => { + let iosConfig: TimeSafariIOSConfigManager; + + beforeEach(() => { + iosConfig = new TimeSafariIOSConfigManager(); + }); + + test('should initialize with default configuration', () => { + expect(iosConfig).toBeDefined(); + + const notificationCategories = iosConfig.getAllNotificationCategories(); + expect(notificationCategories).toHaveLength(4); + expect(notificationCategories[0].identifier).toBe('TIMESAFARI_COMMUNITY_UPDATE'); + expect(notificationCategories[0].actions).toHaveLength(3); + }); + + test('should get notification categories configuration', () => { + const categories = iosConfig.getAllNotificationCategories(); + expect(categories).toHaveLength(4); + + const communityCategory = categories.find(c => c.identifier === 'TIMESAFARI_COMMUNITY_UPDATE'); + expect(communityCategory).toBeDefined(); + expect(communityCategory?.actions).toHaveLength(3); + expect(communityCategory?.intentIdentifiers).toEqual([]); + }); + + test('should get background tasks', () => { + const backgroundTasks = iosConfig.getAllBackgroundTasks(); + expect(backgroundTasks).toHaveLength(2); + expect(backgroundTasks[0].identifier).toBe('com.timesafari.dailynotification.fetch'); + expect(backgroundTasks[1].identifier).toBe('com.timesafari.dailynotification.notify'); + }); + + test('should update configuration', () => { + const updates: Partial = { + notificationCategories: [ + { + identifier: 'custom_category', + actions: [ + { + identifier: 'CUSTOM_ACTION', + title: 'Custom Action', + options: { + foreground: true, + destructive: false, + authenticationRequired: false + } + } + ], + intentIdentifiers: ['CUSTOM_INTENT'], + hiddenPreviewsBodyPlaceholder: 'Custom placeholder', + categorySummaryFormat: '%u more', + options: { + customDismissAction: false, + allowInCarPlay: true, + allowAnnouncement: false, + showTitle: true, + showSubtitle: true, + showBody: true + } + } + ] + }; + + iosConfig.updateConfig(updates); + + const categories = iosConfig.getAllNotificationCategories(); + expect(categories).toHaveLength(1); + expect(categories[0].identifier).toBe('custom_category'); + }); + + test('should validate configuration', () => { + const validation = iosConfig.validateConfig(); + expect(validation.valid).toBe(true); + expect(validation.errors).toEqual([]); + }); + + test('should handle configuration updates', () => { + const updates: Partial = { + notificationCategories: [ + { + identifier: 'test_category', + actions: [ + { + identifier: 'TEST_ACTION', + title: 'Test Action', + options: { + foreground: false, + destructive: false, + authenticationRequired: false + } + } + ], + intentIdentifiers: [], + hiddenPreviewsBodyPlaceholder: 'Test', + categorySummaryFormat: '%u more', + options: { + customDismissAction: false, + allowInCarPlay: true, + allowAnnouncement: false, + showTitle: true, + showSubtitle: true, + showBody: true + } + } + ] + }; + + iosConfig.updateConfig(updates); + const validation = iosConfig.validateConfig(); + + expect(validation.valid).toBe(true); + }); + }); + + describe('Cross-Platform Consistency', () => { + test('should have consistent notification channel/category structure', () => { + const androidConfig = new TimeSafariAndroidConfigManager(); + const iosConfig = new TimeSafariIOSConfigManager(); + + const androidChannels = androidConfig.getAllNotificationChannels(); + const iosCategories = iosConfig.getAllNotificationCategories(); + + // Both should have notification types (may have different counts) + expect(androidChannels.length).toBeGreaterThan(0); + expect(iosCategories.length).toBeGreaterThan(0); + + // Both should have community updates as the first item + expect(androidChannels[0].id).toBe('timesafari_community_updates'); + expect(iosCategories[0].identifier).toBe('TIMESAFARI_COMMUNITY_UPDATE'); + }); + + test('should have consistent permission requirements', () => { + const androidConfig = new TimeSafariAndroidConfigManager(); + const iosConfig = new TimeSafariIOSConfigManager(); + + const androidPermissions = androidConfig.getRequiredPermissions(); + const iosCategories = iosConfig.getAllNotificationCategories(); + + // Both should require notification permissions + expect(androidPermissions.some(p => p.name === 'android.permission.POST_NOTIFICATIONS')).toBe(true); + expect(iosCategories.length).toBeGreaterThan(0); + }); + + test('should have consistent background task support', () => { + const androidConfig = new TimeSafariAndroidConfigManager(); + const iosConfig = new TimeSafariIOSConfigManager(); + + const androidConstraints = androidConfig.getWorkManagerConstraints(); + const iosBackgroundTasks = iosConfig.getAllBackgroundTasks(); + + // Both should support background processing + expect(androidConstraints.networkType).toBe('connected'); + expect(iosBackgroundTasks.length).toBeGreaterThan(0); + }); + }); +}); \ No newline at end of file diff --git a/src/__tests__/service-integration.test.ts b/src/__tests__/service-integration.test.ts new file mode 100644 index 0000000..0daeb5b --- /dev/null +++ b/src/__tests__/service-integration.test.ts @@ -0,0 +1,415 @@ +/** + * Service Integration Tests + * + * Tests for TimeSafari service integration layer including + * DailyNotificationService, DatabaseIntegrationService, and PlatformServiceMixin. + * + * @author Matthew Raymer + * @version 1.0.0 + */ + +import { + DailyNotificationService, + DailyNotificationServiceConfig +} from '../services/DailyNotificationService'; + +import { + DatabaseIntegrationService, + DatabaseIntegrationConfig +} from '../services/DatabaseIntegrationService'; + +import { + TimeSafariDailyNotificationMixin, + TimeSafariDailyNotificationExample, + WithTimeSafariDailyNotifications +} from '../utils/PlatformServiceMixin'; + +import { + TimeSafariStorageAdapterImpl, + LocalStorageStorage +} from '../timesafari-storage-adapter'; + +// Mock Vue for testing (not used in simplified version) + +// Note: Native platform implementations are handled by the service layer +// No mocking needed as we're testing the service integration, not the native implementations + +describe('Service Integration', () => { + let dailyNotificationService: DailyNotificationService; + let databaseIntegrationService: DatabaseIntegrationService; + let storageAdapter: TimeSafariStorageAdapterImpl; + + beforeEach(() => { + // Reset mocks + jest.clearAllMocks(); + + // Create fresh instances + dailyNotificationService = DailyNotificationService.getInstance(); + databaseIntegrationService = DatabaseIntegrationService.getInstance(); + storageAdapter = new TimeSafariStorageAdapterImpl(new LocalStorageStorage()); + }); + + afterEach(() => { + // Clean up localStorage + localStorage.clear(); + }); + + describe('DailyNotificationService', () => { + test('should initialize with configuration', async () => { + const config: DailyNotificationServiceConfig = { + activeDid: 'did:example:test-123', + storageAdapter: storageAdapter, + platform: 'android', + logging: { + level: 'info', + enableStructuredLogging: true, + enableEventIds: true, + enablePerformanceMetrics: true + } + }; + + await dailyNotificationService.initialize(config); + + const status = dailyNotificationService.getStatus(); + expect(status.initialized).toBe(true); + expect(status.activeDid).toBe('did:example:test-123'); + expect(status.platform).toBe('android'); + }); + + test('should schedule daily notification', async () => { + const config: DailyNotificationServiceConfig = { + activeDid: 'did:example:test-123', + storageAdapter: storageAdapter, + platform: 'android' + }; + + await dailyNotificationService.initialize(config); + + const options = { + title: 'Test Notification', + body: 'Test notification body', + time: '09:00', + channel: 'timesafari_community_updates' + }; + + await dailyNotificationService.scheduleDailyNotification(options); + + // Verify notification was scheduled (native platform handles this) + // The service logs indicate successful scheduling + }); + + test('should fetch community data', async () => { + const config: DailyNotificationServiceConfig = { + activeDid: 'did:example:test-123', + storageAdapter: storageAdapter, + platform: 'android' + }; + + await dailyNotificationService.initialize(config); + + // This would normally call the community service + // For testing, we'll just verify the service is initialized + const status = dailyNotificationService.getStatus(); + expect(status.initialized).toBe(true); + }); + + test('should provide service status', async () => { + const config: DailyNotificationServiceConfig = { + activeDid: 'did:example:test-123', + storageAdapter: storageAdapter, + platform: 'android' + }; + + await dailyNotificationService.initialize(config); + + const status = dailyNotificationService.getStatus(); + expect(status).toHaveProperty('initialized'); + expect(status).toHaveProperty('activeDid'); + expect(status).toHaveProperty('platform'); + expect(status).toHaveProperty('circuitBreakerState'); + expect(status).toHaveProperty('totalRequests'); + expect(status).toHaveProperty('successfulRequests'); + expect(status).toHaveProperty('failedRequests'); + expect(status).toHaveProperty('uptime'); + }); + + test('should handle circuit breaker', async () => { + const config: DailyNotificationServiceConfig = { + activeDid: 'did:example:test-123', + storageAdapter: storageAdapter, + platform: 'android', + circuitBreaker: { + failureThreshold: 2, + recoveryTimeout: 1000, + monitoringPeriod: 1000, + halfOpenMaxCalls: 1 + } + }; + + await dailyNotificationService.initialize(config); + + const circuitBreakerStatus = dailyNotificationService.getCircuitBreakerStatus(); + expect(circuitBreakerStatus.state).toBe('closed'); + expect(circuitBreakerStatus.failures).toBe(0); + }); + + test('should shutdown gracefully', async () => { + const config: DailyNotificationServiceConfig = { + activeDid: 'did:example:test-123', + storageAdapter: storageAdapter, + platform: 'android' + }; + + await dailyNotificationService.initialize(config); + await dailyNotificationService.shutdown(); + + const status = dailyNotificationService.getStatus(); + expect(status.initialized).toBe(false); + }); + }); + + describe('DatabaseIntegrationService', () => { + test('should initialize with configuration', async () => { + const config: DatabaseIntegrationConfig = { + platform: 'android', + storageAdapter: storageAdapter, + database: { + name: 'test_db', + version: 1, + tables: [], + migrations: [] + }, + watermark: { + enabled: true, + tableName: 'watermarks', + columnName: 'value', + initialValue: 0, + updateInterval: 1000 + } + }; + + await databaseIntegrationService.initialize(config); + + // Service should be initialized without errors + expect(databaseIntegrationService).toBeDefined(); + }); + + test('should store notification data', async () => { + const config: DatabaseIntegrationConfig = { + platform: 'android', + storageAdapter: storageAdapter + }; + + await databaseIntegrationService.initialize(config); + + const notificationData = { + id: 'test-notification-123', + title: 'Test Notification', + body: 'Test notification body', + scheduledTime: Date.now(), + channel: 'timesafari_community_updates', + activeDid: 'did:example:test-123', + metadata: { test: true } + }; + + await databaseIntegrationService.storeNotificationData(notificationData); + + // Verify data was stored + const retrieved = await storageAdapter.retrieve('notification_test-notification-123'); + expect(retrieved).toBeDefined(); + expect((retrieved as { title: string; body: string }).title).toBe(notificationData.title); + expect((retrieved as { title: string; body: string }).body).toBe(notificationData.body); + }); + + test('should retrieve notification data', async () => { + const config: DatabaseIntegrationConfig = { + platform: 'android', + storageAdapter: storageAdapter + }; + + await databaseIntegrationService.initialize(config); + + const notificationData = { + id: 'test-notification-456', + title: 'Test Notification 2', + body: 'Test notification body 2', + scheduledTime: Date.now(), + channel: 'timesafari_project_notifications', + activeDid: 'did:example:test-123' + }; + + await databaseIntegrationService.storeNotificationData(notificationData); + + const retrieved = await databaseIntegrationService.retrieveNotificationData('test-notification-456'); + expect(retrieved).toBeDefined(); + expect((retrieved as { title: string }).title).toBe(notificationData.title); + }); + + test('should manage watermark', async () => { + const config: DatabaseIntegrationConfig = { + platform: 'android', + storageAdapter: storageAdapter, + watermark: { + enabled: true, + tableName: 'watermarks', + columnName: 'value', + initialValue: 0, + updateInterval: 1000 + } + }; + + await databaseIntegrationService.initialize(config); + + const newWatermark = 12345; + await databaseIntegrationService.updateWatermark(newWatermark); + + const currentWatermark = await databaseIntegrationService.getWatermark(); + expect(currentWatermark).toBe(newWatermark); + }); + + test('should provide database statistics', async () => { + const config: DatabaseIntegrationConfig = { + platform: 'android', + storageAdapter: storageAdapter + }; + + await databaseIntegrationService.initialize(config); + + const stats = await databaseIntegrationService.getDatabaseStats(); + expect(stats).toHaveProperty('totalNotifications'); + expect(stats).toHaveProperty('totalStorageSize'); + expect(stats).toHaveProperty('watermarkValue'); + expect(stats).toHaveProperty('lastUpdated'); + }); + + test('should cleanup expired data', async () => { + const config: DatabaseIntegrationConfig = { + platform: 'android', + storageAdapter: storageAdapter + }; + + await databaseIntegrationService.initialize(config); + + // Store some data with short TTL + await storageAdapter.store('test-expired', { data: 'test' }, 1); // 1 second TTL + + // Wait for expiration + await new Promise(resolve => setTimeout(resolve, 1100)); + + await databaseIntegrationService.cleanupExpiredData(); + + // Verify expired data was cleaned up + const retrieved = await storageAdapter.retrieve('test-expired'); + expect(retrieved).toBeNull(); + }); + + test('should shutdown gracefully', async () => { + const config: DatabaseIntegrationConfig = { + platform: 'android', + storageAdapter: storageAdapter + }; + + await databaseIntegrationService.initialize(config); + await databaseIntegrationService.shutdown(); + + // Service should be shut down without errors + expect(databaseIntegrationService).toBeDefined(); + }); + }); + + describe('PlatformServiceMixin', () => { + test('should create mixin instance', () => { + const mixin = new TimeSafariDailyNotificationMixin(); + expect(mixin).toBeDefined(); + expect(mixin.dailyNotificationService).toBeDefined(); + }); + + test('should have required methods', () => { + const mixin = new TimeSafariDailyNotificationMixin(); + + expect(typeof mixin.initializeDailyNotifications).toBe('function'); + expect(typeof mixin.scheduleDailyNotification).toBe('function'); + expect(typeof mixin.fetchCommunityData).toBe('function'); + expect(typeof mixin.getServiceStatus).toBe('function'); + expect(typeof mixin.shutdownDailyNotifications).toBe('function'); + }); + + test('should have service status methods', () => { + const mixin = new TimeSafariDailyNotificationMixin(); + + expect(mixin.isServiceReady()).toBe(false); + expect(mixin.getServiceStatusData()).toBe(null); + expect(mixin.getServiceError()).toBe(null); + }); + + test('should create example component', () => { + const example = new TimeSafariDailyNotificationExample(); + expect(example).toBeDefined(); + expect(example.dailyNotificationService).toBeDefined(); + }); + + test('should create decorator example component', () => { + class TestComponent { + [key: string]: unknown; + } + const DecoratedComponent = WithTimeSafariDailyNotifications(TestComponent); + const decoratorExample = new DecoratedComponent(); + expect(decoratorExample).toBeDefined(); + expect((decoratorExample as { dailyNotificationService: unknown }).dailyNotificationService).toBeDefined(); + }); + }); + + describe('Service Integration Flow', () => { + test('should complete full service integration flow', async () => { + // Initialize DailyNotificationService + const dailyConfig: DailyNotificationServiceConfig = { + activeDid: 'did:example:integration-test', + storageAdapter: storageAdapter, + platform: 'android' + }; + + await dailyNotificationService.initialize(dailyConfig); + + // Initialize DatabaseIntegrationService + const dbConfig: DatabaseIntegrationConfig = { + platform: 'android', + storageAdapter: storageAdapter + }; + + await databaseIntegrationService.initialize(dbConfig); + + // Schedule a notification + await dailyNotificationService.scheduleDailyNotification({ + title: 'Integration Test Notification', + body: 'Testing full service integration', + time: '10:00', + channel: 'timesafari_community_updates' + }); + + // Store notification data + await databaseIntegrationService.storeNotificationData({ + id: 'integration-test-123', + title: 'Integration Test Notification', + body: 'Testing full service integration', + scheduledTime: Date.now(), + channel: 'timesafari_community_updates', + activeDid: 'did:example:integration-test' + }); + + // Verify services are working + const dailyStatus = dailyNotificationService.getStatus(); + const dbStats = await databaseIntegrationService.getDatabaseStats(); + + expect(dailyStatus.initialized).toBe(true); + expect(dbStats.totalNotifications).toBeGreaterThanOrEqual(0); + + // Shutdown services + await dailyNotificationService.shutdown(); + await databaseIntegrationService.shutdown(); + + // Get status after shutdown + const finalStatus = dailyNotificationService.getStatus(); + expect(finalStatus.initialized).toBe(false); + }); + }); +}); diff --git a/src/__tests__/timesafari-integration.test.ts b/src/__tests__/timesafari-integration.test.ts new file mode 100644 index 0000000..5bda58c --- /dev/null +++ b/src/__tests__/timesafari-integration.test.ts @@ -0,0 +1,287 @@ +/** + * TimeSafari Integration Tests + * + * Tests for TimeSafari-specific integration features including + * privacy-preserving claims, storage adapters, and community features. + * + * @author Matthew Raymer + * @version 1.0.0 + */ + +import { + TimeSafariIntegrationService +} from '../timesafari-integration'; +import { + TimeSafariStorageAdapterImpl, + StorageFactory, + LocalStorageStorage +} from '../timesafari-storage-adapter'; +import { + TimeSafariCommunityIntegrationService +} from '../timesafari-community-integration'; + +// Mock fetch for testing +global.fetch = jest.fn(); + +describe('TimeSafari Integration', () => { + let integrationService: TimeSafariIntegrationService; + let storageAdapter: TimeSafariStorageAdapterImpl; + let communityService: TimeSafariCommunityIntegrationService; + + beforeEach(() => { + // Reset mocks + jest.clearAllMocks(); + + // Create fresh instances + integrationService = TimeSafariIntegrationService.getInstance(); + storageAdapter = new TimeSafariStorageAdapterImpl(new LocalStorageStorage()); + communityService = TimeSafariCommunityIntegrationService.getInstance(); + }); + + afterEach(() => { + // Clean up localStorage + localStorage.clear(); + }); + + describe('TimeSafariIntegrationService', () => { + test('should initialize with configuration', async () => { + const config = { + activeDid: 'did:example:test-123', + storageAdapter: storageAdapter, + endorserApiBaseUrl: 'https://test.endorser.ch/api/v1' + }; + + await integrationService.initialize(config); + + // Service should be initialized without errors + expect(integrationService).toBeDefined(); + }); + + test('should generate sample DID payloads', () => { + const payloads = integrationService.generateSampleDidPayloads(); + + expect(payloads).toHaveLength(2); + expect(payloads[0].type).toBe('notification_content'); + expect(payloads[1].type).toBe('callback_event'); + expect(payloads[0].verificationSteps).toContain('Extract DID from payload'); + }); + + test('should return data retention policy', () => { + const policy = integrationService.getDataRetentionPolicy(); + + expect(policy.fields).toBeDefined(); + expect(policy.redactionMethods).toBeDefined(); + expect(policy.storageLocations).toBeDefined(); + expect(policy.fields.activeDid.retention).toBe('session'); + expect(policy.fields.notificationContent.retention).toBe('7_days'); + }); + + test('should verify DID signatures (placeholder)', async () => { + const isValid = await integrationService.verifyDidSignature( + 'test-payload', + 'test-signature' + ); + + // Should return false when Veramo stack is not available + expect(isValid).toBe(false); + }); + }); + + describe('TimeSafariStorageAdapterImpl', () => { + test('should store and retrieve data', async () => { + const testData = { message: 'Hello TimeSafari' }; + + await storageAdapter.store('test-key', testData); + const retrieved = await storageAdapter.retrieve('test-key'); + + expect(retrieved).toEqual(testData); + }); + + test('should handle TTL expiration', async () => { + const testData = { message: 'Expires soon' }; + + // Store with 1 second TTL + await storageAdapter.store('ttl-key', testData, 1); + + // Should be available immediately + const retrieved = await storageAdapter.retrieve('ttl-key'); + expect(retrieved).toEqual(testData); + + // Wait for expiration + await new Promise(resolve => setTimeout(resolve, 1100)); + + // Should be null after expiration + const expired = await storageAdapter.retrieve('ttl-key'); + expect(expired).toBeNull(); + }); + + test('should remove data', async () => { + const testData = { message: 'To be removed' }; + + await storageAdapter.store('remove-key', testData); + await storageAdapter.remove('remove-key'); + + const retrieved = await storageAdapter.retrieve('remove-key'); + expect(retrieved).toBeNull(); + }); + + test('should clear all data', async () => { + await storageAdapter.store('key1', { data: 1 }); + await storageAdapter.store('key2', { data: 2 }); + + await storageAdapter.clear(); + + const retrieved1 = await storageAdapter.retrieve('key1'); + const retrieved2 = await storageAdapter.retrieve('key2'); + + expect(retrieved1).toBeNull(); + expect(retrieved2).toBeNull(); + }); + + test('should provide storage statistics', async () => { + await storageAdapter.store('stats1', { data: 1 }); + await storageAdapter.store('stats2', { data: 2 }); + + const stats = await storageAdapter.getStats(); + + expect(stats.totalKeys).toBe(2); + expect(stats.validKeys).toBe(2); + expect(stats.expiredKeys).toBe(0); + expect(stats.prefix).toBe('timesafari_notifications'); + }); + }); + + describe('TimeSafariCommunityIntegrationService', () => { + test('should initialize with configuration', async () => { + const config = { + maxRequestsPerMinute: 10, + maxRequestsPerHour: 100, + burstLimit: 5, + initialBackoffMs: 500, + maxBackoffMs: 10000, + basePollingIntervalMs: 60000, + maxPollingIntervalMs: 300000 + }; + + await communityService.initialize(config); + + // Service should be initialized without errors + expect(communityService).toBeDefined(); + }); + + test('should provide rate limit status', () => { + const status = communityService.getRateLimitStatus(); + + expect(status).toHaveProperty('requestsLastMinute'); + expect(status).toHaveProperty('requestsLastHour'); + expect(status).toHaveProperty('burstRequests'); + expect(status).toHaveProperty('canMakeRequest'); + expect(status).toHaveProperty('waitTime'); + }); + + test('should provide backoff status', () => { + const status = communityService.getBackoffStatus(); + + expect(status).toHaveProperty('failureCount'); + expect(status).toHaveProperty('currentDelay'); + expect(status).toHaveProperty('isInBackoff'); + expect(status).toHaveProperty('remainingBackoffTime'); + }); + + test('should provide polling status', () => { + const status = communityService.getPollingStatus(); + + expect(status).toHaveProperty('currentInterval'); + expect(status).toHaveProperty('consecutiveFailures'); + expect(status).toHaveProperty('consecutiveSuccesses'); + expect(status).toHaveProperty('adaptivePolling'); + }); + + test('should get optimized polling interval', () => { + const interval = communityService.getOptimizedPollingInterval(); + + expect(typeof interval).toBe('number'); + expect(interval).toBeGreaterThan(0); + }); + }); + + describe('StorageFactory', () => { + test('should create android storage', () => { + const storage = StorageFactory.createStorage('android'); + + expect(storage).toBeDefined(); + expect(typeof storage.getItem).toBe('function'); + expect(typeof storage.setItem).toBe('function'); + expect(typeof storage.removeItem).toBe('function'); + }); + + test('should create android storage', () => { + const storage = StorageFactory.createStorage('android'); + + expect(storage).toBeDefined(); + expect(typeof storage.getItem).toBe('function'); + expect(typeof storage.setItem).toBe('function'); + expect(typeof storage.removeItem).toBe('function'); + }); + + test('should create ios storage', () => { + const storage = StorageFactory.createStorage('ios'); + + expect(storage).toBeDefined(); + expect(typeof storage.getItem).toBe('function'); + expect(typeof storage.setItem).toBe('function'); + expect(typeof storage.removeItem).toBe('function'); + }); + + test('should throw error for unsupported platform', () => { + expect(() => { + StorageFactory.createStorage('unsupported' as 'android' | 'ios' | 'electron'); + }).toThrow('Unsupported platform: unsupported'); + }); + }); + + describe('Integration Flow', () => { + test('should complete full integration flow', async () => { + // Initialize services + const config = { + activeDid: 'did:example:integration-test', + storageAdapter: storageAdapter, + endorserApiBaseUrl: 'https://test.endorser.ch/api/v1' + }; + + await integrationService.initialize(config); + await communityService.initialize({ + maxRequestsPerMinute: 30, + maxRequestsPerHour: 1000 + }); + + // Store sample data + await storageAdapter.store('integration-test', { + timestamp: Date.now(), + data: 'integration test data' + }); + + // Retrieve data + const retrieved = await storageAdapter.retrieve('integration-test'); + expect(retrieved).toBeDefined(); + expect((retrieved as { data: string }).data).toBe('integration test data'); + + // Get status from all services + const rateLimitStatus = communityService.getRateLimitStatus(); + const backoffStatus = communityService.getBackoffStatus(); + const pollingStatus = communityService.getPollingStatus(); + + expect(rateLimitStatus.canMakeRequest).toBe(true); + expect(backoffStatus.isInBackoff).toBe(false); + expect(pollingStatus.currentInterval).toBeGreaterThan(0); + + // Generate sample payloads + const payloads = integrationService.generateSampleDidPayloads(); + expect(payloads.length).toBeGreaterThan(0); + + // Get data retention policy + const policy = integrationService.getDataRetentionPolicy(); + expect(policy.fields).toBeDefined(); + }); + }); +});