diff --git a/src/test/queue-configuration.test.ts b/src/test/queue-configuration.test.ts new file mode 100644 index 00000000..134f66bc --- /dev/null +++ b/src/test/queue-configuration.test.ts @@ -0,0 +1,358 @@ +import { getConfig } from '../config' + +// Mock dependencies +jest.mock('../config') + +const mockGetConfig = getConfig as jest.MockedFunction + +describe('Queue Configuration Tests', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + describe('Queue Configuration Values', () => { + it('should have correct default configuration values', () => { + mockGetConfig.mockReturnValue({ + isMultitenant: false, + databaseURL: 'postgres://test:test@localhost:5432/test', + multitenantDatabaseUrl: 'postgres://test:test@localhost:5433/test', + pgQueueConnectionURL: undefined, + pgQueueArchiveCompletedAfterSeconds: 3600, + pgQueueDeleteAfterDays: 7, + pgQueueDeleteAfterHours: undefined, + pgQueueRetentionDays: 7, + pgQueueEnableWorkers: true, + pgQueueReadWriteTimeout: 30000, + pgQueueConcurrentTasksPerQueue: 5, + pgQueueMaxConnections: 10, + logLevel: 'info', + logflareApiKey: undefined, + logflareSourceToken: undefined, + logflareEnabled: false, + } as any) + + const config = mockGetConfig() + + expect(config.isMultitenant).toBe(false) + expect(config.databaseURL).toBe('postgres://test:test@localhost:5432/test') + expect(config.pgQueueEnableWorkers).toBe(true) + expect(config.pgQueueMaxConnections).toBe(10) + expect(config.pgQueueConcurrentTasksPerQueue).toBe(5) + expect(config.pgQueueReadWriteTimeout).toBe(30000) + expect(config.pgQueueArchiveCompletedAfterSeconds).toBe(3600) + expect(config.pgQueueDeleteAfterDays).toBe(7) + expect(config.pgQueueRetentionDays).toBe(7) + }) + + it('should handle multitenant configuration', () => { + mockGetConfig.mockReturnValue({ + isMultitenant: true, + databaseURL: 'postgres://test:test@localhost:5432/test', + multitenantDatabaseUrl: 'postgres://test:test@localhost:5433/test', + pgQueueConnectionURL: 'postgres://queue:queue@localhost:5434/queue', + pgQueueArchiveCompletedAfterSeconds: 3600, + pgQueueDeleteAfterDays: 7, + pgQueueDeleteAfterHours: undefined, + pgQueueRetentionDays: 7, + pgQueueEnableWorkers: true, + pgQueueReadWriteTimeout: 30000, + pgQueueConcurrentTasksPerQueue: 5, + pgQueueMaxConnections: 10, + logLevel: 'info', + logflareApiKey: undefined, + logflareSourceToken: undefined, + logflareEnabled: false, + } as any) + + const config = mockGetConfig() + + expect(config.isMultitenant).toBe(true) + expect(config.multitenantDatabaseUrl).toBe('postgres://test:test@localhost:5433/test') + expect(config.pgQueueConnectionURL).toBe('postgres://queue:queue@localhost:5434/queue') + }) + + it('should handle custom queue connection URL', () => { + mockGetConfig.mockReturnValue({ + isMultitenant: false, + databaseURL: 'postgres://test:test@localhost:5432/test', + multitenantDatabaseUrl: 'postgres://test:test@localhost:5433/test', + pgQueueConnectionURL: 'postgres://queue:queue@localhost:5434/queue', + pgQueueArchiveCompletedAfterSeconds: 3600, + pgQueueDeleteAfterDays: 7, + pgQueueDeleteAfterHours: undefined, + pgQueueRetentionDays: 7, + pgQueueEnableWorkers: true, + pgQueueReadWriteTimeout: 30000, + pgQueueConcurrentTasksPerQueue: 5, + pgQueueMaxConnections: 10, + logLevel: 'info', + logflareApiKey: undefined, + logflareSourceToken: undefined, + logflareEnabled: false, + } as any) + + const config = mockGetConfig() + + expect(config.pgQueueConnectionURL).toBe('postgres://queue:queue@localhost:5434/queue') + }) + + it('should handle delete after hours configuration', () => { + mockGetConfig.mockReturnValue({ + isMultitenant: false, + databaseURL: 'postgres://test:test@localhost:5432/test', + multitenantDatabaseUrl: 'postgres://test:test@localhost:5433/test', + pgQueueConnectionURL: undefined, + pgQueueArchiveCompletedAfterSeconds: 3600, + pgQueueDeleteAfterDays: undefined, + pgQueueDeleteAfterHours: 24, + pgQueueRetentionDays: 7, + pgQueueEnableWorkers: true, + pgQueueReadWriteTimeout: 30000, + pgQueueConcurrentTasksPerQueue: 5, + pgQueueMaxConnections: 10, + logLevel: 'info', + logflareApiKey: undefined, + logflareSourceToken: undefined, + logflareEnabled: false, + } as any) + + const config = mockGetConfig() + + expect(config.pgQueueDeleteAfterDays).toBeUndefined() + expect(config.pgQueueDeleteAfterHours).toBe(24) + }) + + it('should handle disabled workers configuration', () => { + mockGetConfig.mockReturnValue({ + isMultitenant: false, + databaseURL: 'postgres://test:test@localhost:5432/test', + multitenantDatabaseUrl: 'postgres://test:test@localhost:5433/test', + pgQueueConnectionURL: undefined, + pgQueueArchiveCompletedAfterSeconds: 3600, + pgQueueDeleteAfterDays: 7, + pgQueueDeleteAfterHours: undefined, + pgQueueRetentionDays: 7, + pgQueueEnableWorkers: false, + pgQueueReadWriteTimeout: 30000, + pgQueueConcurrentTasksPerQueue: 5, + pgQueueMaxConnections: 10, + logLevel: 'info', + logflareApiKey: undefined, + logflareSourceToken: undefined, + logflareEnabled: false, + } as any) + + const config = mockGetConfig() + + expect(config.pgQueueEnableWorkers).toBe(false) + }) + }) + + describe('Queue Configuration Validation', () => { + it('should validate required configuration fields', () => { + mockGetConfig.mockReturnValue({ + isMultitenant: false, + databaseURL: 'postgres://test:test@localhost:5432/test', + multitenantDatabaseUrl: 'postgres://test:test@localhost:5433/test', + pgQueueConnectionURL: undefined, + pgQueueArchiveCompletedAfterSeconds: 3600, + pgQueueDeleteAfterDays: 7, + pgQueueDeleteAfterHours: undefined, + pgQueueRetentionDays: 7, + pgQueueEnableWorkers: true, + pgQueueReadWriteTimeout: 30000, + pgQueueConcurrentTasksPerQueue: 5, + pgQueueMaxConnections: 10, + logLevel: 'info', + logflareApiKey: undefined, + logflareSourceToken: undefined, + logflareEnabled: false, + } as any) + + const config = mockGetConfig() + + // Required fields + expect(config.databaseURL).toBeDefined() + expect(config.pgQueueEnableWorkers).toBeDefined() + expect(config.pgQueueMaxConnections).toBeDefined() + expect(config.pgQueueConcurrentTasksPerQueue).toBeDefined() + expect(config.pgQueueReadWriteTimeout).toBeDefined() + expect(config.pgQueueArchiveCompletedAfterSeconds).toBeDefined() + expect(config.pgQueueRetentionDays).toBeDefined() + }) + + it('should validate optional configuration fields', () => { + mockGetConfig.mockReturnValue({ + isMultitenant: false, + databaseURL: 'postgres://test:test@localhost:5432/test', + multitenantDatabaseUrl: 'postgres://test:test@localhost:5433/test', + pgQueueConnectionURL: undefined, + pgQueueArchiveCompletedAfterSeconds: 3600, + pgQueueDeleteAfterDays: 7, + pgQueueDeleteAfterHours: undefined, + pgQueueRetentionDays: 7, + pgQueueEnableWorkers: true, + pgQueueReadWriteTimeout: 30000, + pgQueueConcurrentTasksPerQueue: 5, + pgQueueMaxConnections: 10, + logLevel: 'info', + logflareApiKey: undefined, + logflareSourceToken: undefined, + logflareEnabled: false, + } as any) + + const config = mockGetConfig() + + // Optional fields + expect(config.pgQueueConnectionURL).toBeUndefined() + expect(config.pgQueueDeleteAfterHours).toBeUndefined() + expect(config.multitenantDatabaseUrl).toBeDefined() + }) + + it('should validate multitenant configuration requirements', () => { + mockGetConfig.mockReturnValue({ + isMultitenant: true, + databaseURL: 'postgres://test:test@localhost:5432/test', + multitenantDatabaseUrl: 'postgres://test:test@localhost:5433/test', + pgQueueConnectionURL: undefined, + pgQueueArchiveCompletedAfterSeconds: 3600, + pgQueueDeleteAfterDays: 7, + pgQueueDeleteAfterHours: undefined, + pgQueueRetentionDays: 7, + pgQueueEnableWorkers: true, + pgQueueReadWriteTimeout: 30000, + pgQueueConcurrentTasksPerQueue: 5, + pgQueueMaxConnections: 10, + logLevel: 'info', + logflareApiKey: undefined, + logflareSourceToken: undefined, + logflareEnabled: false, + } as any) + + const config = mockGetConfig() + + expect(config.isMultitenant).toBe(true) + expect(config.multitenantDatabaseUrl).toBeDefined() + expect(config.multitenantDatabaseUrl).toBe('postgres://test:test@localhost:5433/test') + }) + }) + + describe('Queue Configuration Types', () => { + it('should validate configuration value types', () => { + mockGetConfig.mockReturnValue({ + isMultitenant: false, + databaseURL: 'postgres://test:test@localhost:5432/test', + multitenantDatabaseUrl: 'postgres://test:test@localhost:5433/test', + pgQueueConnectionURL: undefined, + pgQueueArchiveCompletedAfterSeconds: 3600, + pgQueueDeleteAfterDays: 7, + pgQueueDeleteAfterHours: undefined, + pgQueueRetentionDays: 7, + pgQueueEnableWorkers: true, + pgQueueReadWriteTimeout: 30000, + pgQueueConcurrentTasksPerQueue: 5, + pgQueueMaxConnections: 10, + logLevel: 'info', + logflareApiKey: undefined, + logflareSourceToken: undefined, + logflareEnabled: false, + } as any) + + const config = mockGetConfig() + + // Boolean types + expect(typeof config.isMultitenant).toBe('boolean') + expect(typeof config.pgQueueEnableWorkers).toBe('boolean') + expect(typeof config.logflareEnabled).toBe('boolean') + + // String types + expect(typeof config.databaseURL).toBe('string') + expect(typeof config.multitenantDatabaseUrl).toBe('string') + expect(typeof config.logLevel).toBe('string') + + // Number types + expect(typeof config.pgQueueMaxConnections).toBe('number') + expect(typeof config.pgQueueConcurrentTasksPerQueue).toBe('number') + expect(typeof config.pgQueueReadWriteTimeout).toBe('number') + expect(typeof config.pgQueueArchiveCompletedAfterSeconds).toBe('number') + expect(typeof config.pgQueueDeleteAfterDays).toBe('number') + expect(typeof config.pgQueueRetentionDays).toBe('number') + + // Undefined types + expect(config.pgQueueConnectionURL).toBeUndefined() + expect(config.pgQueueDeleteAfterHours).toBeUndefined() + expect(config.logflareApiKey).toBeUndefined() + expect(config.logflareSourceToken).toBeUndefined() + }) + }) + + describe('Queue Configuration Ranges', () => { + it('should validate configuration value ranges', () => { + mockGetConfig.mockReturnValue({ + isMultitenant: false, + databaseURL: 'postgres://test:test@localhost:5432/test', + multitenantDatabaseUrl: 'postgres://test:test@localhost:5433/test', + pgQueueConnectionURL: undefined, + pgQueueArchiveCompletedAfterSeconds: 3600, + pgQueueDeleteAfterDays: 7, + pgQueueDeleteAfterHours: undefined, + pgQueueRetentionDays: 7, + pgQueueEnableWorkers: true, + pgQueueReadWriteTimeout: 30000, + pgQueueConcurrentTasksPerQueue: 5, + pgQueueMaxConnections: 10, + logLevel: 'info', + logflareApiKey: undefined, + logflareSourceToken: undefined, + logflareEnabled: false, + } as any) + + const config = mockGetConfig() + + // Positive numbers + expect(config.pgQueueMaxConnections).toBeGreaterThan(0) + expect(config.pgQueueConcurrentTasksPerQueue).toBeGreaterThan(0) + expect(config.pgQueueReadWriteTimeout).toBeGreaterThan(0) + expect(config.pgQueueArchiveCompletedAfterSeconds).toBeGreaterThan(0) + expect(config.pgQueueDeleteAfterDays).toBeGreaterThan(0) + expect(config.pgQueueRetentionDays).toBeGreaterThan(0) + + // Reasonable ranges + expect(config.pgQueueMaxConnections).toBeLessThanOrEqual(100) + expect(config.pgQueueConcurrentTasksPerQueue).toBeLessThanOrEqual(50) + expect(config.pgQueueReadWriteTimeout).toBeLessThanOrEqual(300000) + expect(config.pgQueueArchiveCompletedAfterSeconds).toBeLessThanOrEqual(86400) + expect(config.pgQueueDeleteAfterDays).toBeLessThanOrEqual(365) + expect(config.pgQueueRetentionDays).toBeLessThanOrEqual(365) + }) + }) + + describe('Queue Configuration Environment Variables', () => { + it('should validate environment variable names', () => { + const envVarNames = [ + 'DATABASE_URL', + 'MULTITENANT_DATABASE_URL', + 'PG_QUEUE_CONNECTION_URL', + 'PG_QUEUE_ARCHIVE_COMPLETED_AFTER_SECONDS', + 'PG_QUEUE_DELETE_AFTER_DAYS', + 'PG_QUEUE_DELETE_AFTER_HOURS', + 'PG_QUEUE_RETENTION_DAYS', + 'PG_QUEUE_ENABLE_WORKERS', + 'PG_QUEUE_READ_WRITE_TIMEOUT', + 'PG_QUEUE_CONCURRENT_TASKS_PER_QUEUE', + 'PG_QUEUE_MAX_CONNECTIONS', + 'LOG_LEVEL', + 'LOGFLARE_API_KEY', + 'LOGFLARE_SOURCE_TOKEN', + 'LOGFLARE_ENABLED', + ] + + envVarNames.forEach(envVar => { + expect(envVar).toBeDefined() + expect(typeof envVar).toBe('string') + expect(envVar.length).toBeGreaterThan(0) + expect(envVar).toMatch(/^[A-Z_]+$/) + }) + }) + }) +}) diff --git a/src/test/queue-payload-validation.test.ts b/src/test/queue-payload-validation.test.ts new file mode 100644 index 00000000..e66142d6 --- /dev/null +++ b/src/test/queue-payload-validation.test.ts @@ -0,0 +1,369 @@ +import { getConfig } from '../config' + +// Mock dependencies +jest.mock('../config') + +const mockGetConfig = getConfig as jest.MockedFunction + +describe('Queue Payload Validation Tests', () => { + beforeEach(() => { + jest.clearAllMocks() + + // Mock config with default values + mockGetConfig.mockReturnValue({ + isMultitenant: false, + databaseURL: 'postgres://test:test@localhost:5432/test', + multitenantDatabaseUrl: 'postgres://test:test@localhost:5433/test', + pgQueueConnectionURL: undefined, + pgQueueArchiveCompletedAfterSeconds: 3600, + pgQueueDeleteAfterDays: 7, + pgQueueDeleteAfterHours: undefined, + pgQueueRetentionDays: 7, + pgQueueEnableWorkers: true, + pgQueueReadWriteTimeout: 30000, + pgQueueConcurrentTasksPerQueue: 5, + pgQueueMaxConnections: 10, + logLevel: 'info', + logflareApiKey: undefined, + logflareSourceToken: undefined, + logflareEnabled: false, + } as any) + }) + + describe('Webhook Event Payload Validation', () => { + it('should validate webhook event payload structure', () => { + const validWebhookPayload = { + event: { + $version: 'v1', + type: 'object.created', + payload: { + bucketId: 'test-bucket', + name: 'test-file.jpg', + reqId: 'test-req-id', + }, + applyTime: Date.now(), + }, + tenant: { + ref: 'test-tenant', + }, + sentAt: new Date().toISOString(), + } + + // Validate required fields + expect(validWebhookPayload.event).toBeDefined() + expect(validWebhookPayload.event.$version).toBe('v1') + expect(validWebhookPayload.event.type).toBe('object.created') + expect(validWebhookPayload.event.payload).toBeDefined() + expect(validWebhookPayload.event.payload.bucketId).toBe('test-bucket') + expect(validWebhookPayload.event.payload.name).toBe('test-file.jpg') + expect(validWebhookPayload.tenant).toBeDefined() + expect(validWebhookPayload.tenant.ref).toBe('test-tenant') + expect(validWebhookPayload.sentAt).toBeDefined() + }) + + it('should validate webhook event types', () => { + const eventTypes = [ + 'object.created', + 'object.updated', + 'object.deleted', + 'bucket.created', + 'bucket.deleted', + ] + + eventTypes.forEach(eventType => { + const payload = { + event: { + $version: 'v1', + type: eventType, + payload: { + bucketId: 'test-bucket', + name: 'test-file.jpg', + }, + applyTime: Date.now(), + }, + tenant: { + ref: 'test-tenant', + }, + sentAt: new Date().toISOString(), + } + + expect(payload.event.type).toBe(eventType) + expect(typeof payload.event.type).toBe('string') + }) + }) + + it('should validate webhook event payload properties', () => { + const payload = { + event: { + $version: 'v1', + type: 'object.created', + payload: { + bucketId: 'test-bucket', + name: 'test-file.jpg', + reqId: 'test-req-id', + size: 1024, + contentType: 'image/jpeg', + }, + applyTime: Date.now(), + }, + tenant: { + ref: 'test-tenant', + }, + sentAt: new Date().toISOString(), + } + + expect(payload.event.payload.bucketId).toBeDefined() + expect(payload.event.payload.name).toBeDefined() + expect(payload.event.payload.reqId).toBeDefined() + expect(payload.event.payload.size).toBeDefined() + expect(payload.event.payload.contentType).toBeDefined() + }) + }) + + describe('Object Admin Delete Payload Validation', () => { + it('should validate object admin delete payload structure', () => { + const validPayload = { + bucketId: 'test-bucket', + objectName: 'test-file.jpg', + tenant: { + ref: 'test-tenant', + host: 'localhost', + }, + } + + expect(validPayload.bucketId).toBeDefined() + expect(validPayload.objectName).toBeDefined() + expect(validPayload.tenant).toBeDefined() + expect(validPayload.tenant.ref).toBeDefined() + expect(validPayload.tenant.host).toBeDefined() + }) + + it('should validate object admin delete all before payload', () => { + const validPayload = { + bucketId: 'test-bucket', + before: new Date().toISOString(), + tenant: { + ref: 'test-tenant', + host: 'localhost', + }, + } + + expect(validPayload.bucketId).toBeDefined() + expect(validPayload.before).toBeDefined() + expect(validPayload.tenant).toBeDefined() + expect(validPayload.tenant.ref).toBeDefined() + expect(validPayload.tenant.host).toBeDefined() + }) + }) + + describe('Migration Event Payload Validation', () => { + it('should validate run migrations payload structure', () => { + const validPayload = { + tenantId: 'test-tenant', + tenant: { + ref: 'test-tenant', + host: 'localhost', + }, + singletonKey: 'test-tenant', + } + + expect(validPayload.tenantId).toBeDefined() + expect(validPayload.tenant).toBeDefined() + expect(validPayload.tenant.ref).toBeDefined() + expect(validPayload.tenant.host).toBeDefined() + expect(validPayload.singletonKey).toBeDefined() + }) + + it('should validate reset migrations payload structure', () => { + const validPayload = { + tenantId: 'test-tenant', + tenant: { + ref: 'test-tenant', + host: 'localhost', + }, + untilMigration: '0001-initialmigration', + markCompletedTillMigration: '0001-initialmigration', + } + + expect(validPayload.tenantId).toBeDefined() + expect(validPayload.tenant).toBeDefined() + expect(validPayload.tenant.ref).toBeDefined() + expect(validPayload.tenant.host).toBeDefined() + expect(validPayload.untilMigration).toBeDefined() + expect(validPayload.markCompletedTillMigration).toBeDefined() + }) + }) + + describe('PgBoss Event Payload Validation', () => { + it('should validate move jobs payload structure', () => { + const validPayload = { + fromQueue: 'old-queue', + toQueue: 'new-queue', + deleteJobsFromOriginalQueue: true, + tenant: { + ref: 'test-tenant', + host: 'localhost', + }, + } + + expect(validPayload.fromQueue).toBeDefined() + expect(validPayload.toQueue).toBeDefined() + expect(validPayload.deleteJobsFromOriginalQueue).toBeDefined() + expect(validPayload.tenant).toBeDefined() + expect(validPayload.tenant.ref).toBeDefined() + expect(validPayload.tenant.host).toBeDefined() + }) + + it('should validate upgrade pgboss v10 payload structure', () => { + const validPayload = { + tenant: { + ref: 'test-tenant', + host: 'localhost', + }, + } + + expect(validPayload.tenant).toBeDefined() + expect(validPayload.tenant.ref).toBeDefined() + expect(validPayload.tenant.host).toBeDefined() + }) + }) + + describe('Backup Object Event Payload Validation', () => { + it('should validate backup object payload structure', () => { + const validPayload = { + bucketId: 'test-bucket', + objectName: 'test-file.jpg', + tenant: { + ref: 'test-tenant', + host: 'localhost', + }, + backupUrl: 'https://backup.example.com/test-file.jpg', + } + + expect(validPayload.bucketId).toBeDefined() + expect(validPayload.objectName).toBeDefined() + expect(validPayload.tenant).toBeDefined() + expect(validPayload.tenant.ref).toBeDefined() + expect(validPayload.tenant.host).toBeDefined() + expect(validPayload.backupUrl).toBeDefined() + }) + }) + + describe('JWKS Event Payload Validation', () => { + it('should validate jwks create signing secret payload structure', () => { + const validPayload = { + tenantId: 'test-tenant', + tenant: { + ref: 'test-tenant', + host: 'localhost', + }, + keyId: 'test-key-id', + } + + expect(validPayload.tenantId).toBeDefined() + expect(validPayload.tenant).toBeDefined() + expect(validPayload.tenant.ref).toBeDefined() + expect(validPayload.tenant.host).toBeDefined() + expect(validPayload.keyId).toBeDefined() + }) + }) + + describe('Queue Event Payload Type Validation', () => { + it('should validate string types in payloads', () => { + const payload = { + bucketId: 'test-bucket', + objectName: 'test-file.jpg', + tenantId: 'test-tenant', + fromQueue: 'old-queue', + toQueue: 'new-queue', + } + + expect(typeof payload.bucketId).toBe('string') + expect(typeof payload.objectName).toBe('string') + expect(typeof payload.tenantId).toBe('string') + expect(typeof payload.fromQueue).toBe('string') + expect(typeof payload.toQueue).toBe('string') + }) + + it('should validate boolean types in payloads', () => { + const payload = { + deleteJobsFromOriginalQueue: true, + includeMetadata: true, + } + + expect(typeof payload.deleteJobsFromOriginalQueue).toBe('boolean') + expect(typeof payload.includeMetadata).toBe('boolean') + }) + + it('should validate number types in payloads', () => { + const payload = { + size: 1024, + applyTime: Date.now(), + priority: 10, + retryLimit: 3, + } + + expect(typeof payload.size).toBe('number') + expect(typeof payload.applyTime).toBe('number') + expect(typeof payload.priority).toBe('number') + expect(typeof payload.retryLimit).toBe('number') + }) + + it('should validate object types in payloads', () => { + const payload = { + tenant: { + ref: 'test-tenant', + host: 'localhost', + }, + event: { + $version: 'v1', + type: 'object.created', + payload: { + bucketId: 'test-bucket', + name: 'test-file.jpg', + }, + applyTime: Date.now(), + }, + } + + expect(typeof payload.tenant).toBe('object') + expect(typeof payload.event).toBe('object') + expect(payload.tenant.ref).toBeDefined() + expect(payload.event.$version).toBeDefined() + }) + }) + + describe('Queue Event Payload Required Fields', () => { + it('should validate required fields for all event types', () => { + const eventTypes = [ + 'webhook', + 'object-admin-delete', + 'object-admin-delete-all-before', + 'tenants-migrations-v2', + 'backup-object', + 'tenants-migrations-reset-v2', + 'jwks-create-signing-secret', + 'upgrade-pg-boss-v10', + 'move-jobs', + ] + + eventTypes.forEach(eventType => { + expect(eventType).toBeDefined() + expect(typeof eventType).toBe('string') + expect(eventType.length).toBeGreaterThan(0) + }) + }) + + it('should validate tenant structure in all payloads', () => { + const tenant = { + ref: 'test-tenant', + host: 'localhost', + } + + expect(tenant.ref).toBeDefined() + expect(tenant.host).toBeDefined() + expect(typeof tenant.ref).toBe('string') + expect(typeof tenant.host).toBe('string') + }) + }) +}) diff --git a/src/test/queue-simple.test.ts b/src/test/queue-simple.test.ts new file mode 100644 index 00000000..fb7c7445 --- /dev/null +++ b/src/test/queue-simple.test.ts @@ -0,0 +1,288 @@ +import { Queue } from '@internal/queue' +import { QueueDB } from '@internal/queue/database' +import { getConfig } from '../config' + +// Mock all dependencies +jest.mock('../config') +jest.mock('@internal/queue') +jest.mock('@internal/queue/database') +jest.mock('pg-boss') +jest.mock('pg') + +const mockGetConfig = getConfig as jest.MockedFunction + +describe('Queue Simple Tests', () => { + beforeEach(() => { + jest.clearAllMocks() + + // Mock config with all required values + mockGetConfig.mockReturnValue({ + isMultitenant: false, + databaseURL: 'postgres://test:test@localhost:5432/test', + multitenantDatabaseUrl: 'postgres://test:test@localhost:5433/test', + pgQueueConnectionURL: undefined, + pgQueueArchiveCompletedAfterSeconds: 3600, + pgQueueDeleteAfterDays: 7, + pgQueueDeleteAfterHours: undefined, + pgQueueRetentionDays: 7, + pgQueueEnableWorkers: true, + pgQueueReadWriteTimeout: 30000, + pgQueueConcurrentTasksPerQueue: 5, + pgQueueMaxConnections: 10, + logLevel: 'info', + logflareApiKey: undefined, + logflareSourceToken: undefined, + logflareEnabled: false, + } as any) + }) + + describe('Queue Configuration', () => { + it('should have correct configuration values', () => { + const config = mockGetConfig() + + expect(config.isMultitenant).toBe(false) + expect(config.databaseURL).toBe('postgres://test:test@localhost:5432/test') + expect(config.pgQueueEnableWorkers).toBe(true) + expect(config.pgQueueMaxConnections).toBe(10) + }) + + it('should handle multitenant configuration', () => { + mockGetConfig.mockReturnValue({ + isMultitenant: true, + databaseURL: 'postgres://test:test@localhost:5432/test', + multitenantDatabaseUrl: 'postgres://test:test@localhost:5433/test', + pgQueueConnectionURL: undefined, + pgQueueArchiveCompletedAfterSeconds: 3600, + pgQueueDeleteAfterDays: 7, + pgQueueDeleteAfterHours: undefined, + pgQueueRetentionDays: 7, + pgQueueEnableWorkers: true, + pgQueueReadWriteTimeout: 30000, + pgQueueConcurrentTasksPerQueue: 5, + pgQueueMaxConnections: 10, + logLevel: 'info', + logflareApiKey: undefined, + logflareSourceToken: undefined, + logflareEnabled: false, + } as any) + + const config = mockGetConfig() + expect(config.isMultitenant).toBe(true) + expect(config.multitenantDatabaseUrl).toBe('postgres://test:test@localhost:5433/test') + }) + }) + + describe('QueueDB Configuration', () => { + it('should create QueueDB with correct configuration', () => { + const config = { + min: 0, + max: 10, + connectionString: 'postgres://test:test@localhost:5432/test', + statement_timeout: 30000, + } + + // Mock QueueDB constructor + const mockQueueDB = jest.fn() + jest.doMock('@internal/queue/database', () => ({ + QueueDB: mockQueueDB, + })) + + expect(mockQueueDB).toBeDefined() + }) + + it('should handle connection string configuration', () => { + const config = { + min: 0, + max: 10, + connectionString: 'postgres://test:test@localhost:5432/test', + statement_timeout: 30000, + } + + expect(config.connectionString).toBe('postgres://test:test@localhost:5432/test') + expect(config.statement_timeout).toBe(30000) + }) + }) + + describe('Queue Event Names', () => { + it('should have correct queue names', () => { + // Test queue names without importing the actual classes + const expectedQueueNames = [ + 'webhooks', + 'object-admin-delete', + 'object-admin-delete-all-before', + 'tenants-migrations-v2', + 'backup-object', + 'tenants-migrations-reset-v2', + 'jwks-create-signing-secret', + 'upgrade-pg-boss-v10', + 'move-jobs', + ] + + expectedQueueNames.forEach(name => { + expect(name).toBeDefined() + expect(typeof name).toBe('string') + expect(name.length).toBeGreaterThan(0) + }) + }) + }) + + describe('Queue Event Payloads', () => { + it('should have correct payload structure for webhook', () => { + const webhookPayload = { + event: { + $version: 'v1', + type: 'object.created', + payload: { + bucketId: 'test-bucket', + name: 'test-file.jpg', + reqId: 'test-req-id', + }, + applyTime: Date.now(), + }, + tenant: { + ref: 'test-tenant', + }, + sentAt: new Date().toISOString(), + } + + expect(webhookPayload.event.$version).toBe('v1') + expect(webhookPayload.event.type).toBe('object.created') + expect(webhookPayload.tenant.ref).toBe('test-tenant') + expect(webhookPayload.sentAt).toBeDefined() + }) + + it('should have correct payload structure for object admin delete', () => { + const objectAdminDeletePayload = { + bucketId: 'test-bucket', + objectName: 'test-file.jpg', + tenant: { + ref: 'test-tenant', + host: 'localhost', + }, + } + + expect(objectAdminDeletePayload.bucketId).toBe('test-bucket') + expect(objectAdminDeletePayload.objectName).toBe('test-file.jpg') + expect(objectAdminDeletePayload.tenant.ref).toBe('test-tenant') + expect(objectAdminDeletePayload.tenant.host).toBe('localhost') + }) + + it('should have correct payload structure for migrations', () => { + const migrationsPayload = { + tenantId: 'test-tenant', + tenant: { + ref: 'test-tenant', + host: 'localhost', + }, + singletonKey: 'test-tenant', + } + + expect(migrationsPayload.tenantId).toBe('test-tenant') + expect(migrationsPayload.tenant.ref).toBe('test-tenant') + expect(migrationsPayload.singletonKey).toBe('test-tenant') + }) + + it('should have correct payload structure for move jobs', () => { + const moveJobsPayload = { + fromQueue: 'old-queue', + toQueue: 'new-queue', + deleteJobsFromOriginalQueue: true, + tenant: { + ref: 'test-tenant', + host: 'localhost', + }, + } + + expect(moveJobsPayload.fromQueue).toBe('old-queue') + expect(moveJobsPayload.toQueue).toBe('new-queue') + expect(moveJobsPayload.deleteJobsFromOriginalQueue).toBe(true) + expect(moveJobsPayload.tenant.ref).toBe('test-tenant') + }) + }) + + describe('Queue Send Options', () => { + it('should have correct send options structure', () => { + const sendOptions = { + expireInHours: 2, + retryLimit: 3, + retryDelay: 5, + priority: 10, + singletonKey: 'test-tenant', + } + + expect(sendOptions.expireInHours).toBe(2) + expect(sendOptions.retryLimit).toBe(3) + expect(sendOptions.retryDelay).toBe(5) + expect(sendOptions.priority).toBe(10) + expect(sendOptions.singletonKey).toBe('test-tenant') + }) + + it('should have correct queue options structure', () => { + const queueOptions = { + name: 'test-queue', + policy: 'exactly_once', + } + + expect(queueOptions.name).toBe('test-queue') + expect(queueOptions.policy).toBe('exactly_once') + }) + + it('should have correct worker options structure', () => { + const workerOptions = { + includeMetadata: true, + } + + expect(workerOptions.includeMetadata).toBe(true) + }) + }) + + describe('Queue Event Versions', () => { + it('should have correct version for all events', () => { + const version = 'v1' + expect(version).toBe('v1') + }) + }) + + describe('Queue Configuration Validation', () => { + it('should validate required configuration fields', () => { + const config = mockGetConfig() + + expect(config.databaseURL).toBeDefined() + expect(config.pgQueueEnableWorkers).toBeDefined() + expect(config.pgQueueMaxConnections).toBeDefined() + expect(config.pgQueueConcurrentTasksPerQueue).toBeDefined() + }) + + it('should handle optional configuration fields', () => { + const config = mockGetConfig() + + expect(config.pgQueueConnectionURL).toBeUndefined() + expect(config.pgQueueDeleteAfterHours).toBeUndefined() + }) + + it('should validate multitenant configuration', () => { + mockGetConfig.mockReturnValue({ + isMultitenant: true, + databaseURL: 'postgres://test:test@localhost:5432/test', + multitenantDatabaseUrl: 'postgres://test:test@localhost:5433/test', + pgQueueConnectionURL: undefined, + pgQueueArchiveCompletedAfterSeconds: 3600, + pgQueueDeleteAfterDays: 7, + pgQueueDeleteAfterHours: undefined, + pgQueueRetentionDays: 7, + pgQueueEnableWorkers: true, + pgQueueReadWriteTimeout: 30000, + pgQueueConcurrentTasksPerQueue: 5, + pgQueueMaxConnections: 10, + logLevel: 'info', + logflareApiKey: undefined, + logflareSourceToken: undefined, + logflareEnabled: false, + } as any) + + const config = mockGetConfig() + expect(config.isMultitenant).toBe(true) + expect(config.multitenantDatabaseUrl).toBeDefined() + }) + }) +})