import { randomUUID, createHash } from 'node:crypto'; import { mkdtemp, readFile, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { Readable } from 'node:stream'; import { INestApplication, ValidationPipe, VersioningType } from '@nestjs/common'; import { Test } from '@nestjs/testing'; import { AppModule } from '../../src/app.module'; import { AppConfigService } from '../../src/modules/config/config.service'; import { DevicesController } from '../../src/modules/devices/devices.controller'; import { HealthController } from '../../src/modules/health/health.controller'; import { SyncController } from '../../src/modules/sync/sync.controller'; import { UploadsController } from '../../src/modules/uploads/uploads.controller'; import { UploadsService } from '../../src/modules/uploads/uploads.service'; import { PrismaService } from '../../src/infrastructure/database/prisma.service'; function sampleMp3Bytes(seed: string): Buffer { return Buffer.concat([ Buffer.from('ID3', 'ascii'), Buffer.from([0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21]), Buffer.from(seed, 'utf8'), ]); } function sha256Hex(data: Buffer): string { return createHash('sha256').update(data).digest('hex'); } function createUploadRequest(data: Buffer): any { const request = Readable.from([data]) as any; request.headers = { 'content-type': 'audio/mpeg', 'content-length': String(data.length), }; return request; } function createPrismaMock() { const users = new Map(); const devices = new Map(); const tracks = new Map(); const audioAssets = new Map(); const uploadSessions = new Map(); const libraryEvents = new Map(); let nextLibraryEventId = 1n; const defaultUser = { id: randomUUID(), slug: 'default-owner', displayName: 'Default Owner', isDefault: true, createdAt: new Date(), updatedAt: new Date(), }; users.set(defaultUser.id, defaultUser); const prismaMock: any = { $queryRawUnsafe: jest.fn().mockResolvedValue([{ '?column?': 1 }]), $transaction: jest.fn().mockImplementation(async (callback: any) => callback(prismaMock)), user: { upsert: jest.fn().mockResolvedValue(defaultUser), }, device: { create: jest.fn().mockImplementation(async ({ data }) => { const record = { id: randomUUID(), createdAt: new Date(), updatedAt: new Date(), ...data, }; devices.set(record.id, record); return record; }), findUnique: jest.fn().mockImplementation(async ({ where }) => { return devices.get(where.id) ?? null; }), update: jest.fn().mockImplementation(async ({ where, data }) => { const current = devices.get(where.id); const updated = { ...current, ...data, updatedAt: new Date(), }; devices.set(where.id, updated); return updated; }), }, track: { findMany: jest.fn().mockImplementation(async ({ where }) => { return [...tracks.values()] .filter((track) => { const userMatches = where?.userId ? track.userId === where.userId : true; const statusMatches = where?.status ? track.status === where.status : true; return userMatches && statusMatches; }) .sort((lhs, rhs) => lhs.createdAt.getTime() - rhs.createdAt.getTime()); }), findUnique: jest.fn().mockImplementation(async ({ where }) => { return tracks.get(where.id) ?? null; }), create: jest.fn().mockImplementation(async ({ data }) => { const now = new Date(); const record = { id: randomUUID(), primaryAudioAssetId: null, artworkAssetId: null, albumArtist: null, genre: null, discNumber: null, trackNumber: null, year: null, deletedAt: null, createdAt: now, updatedAt: now, ...data, }; tracks.set(record.id, record); return record; }), update: jest.fn().mockImplementation(async ({ where, data }) => { const current = tracks.get(where.id); const updated = { ...current, ...data, updatedAt: new Date(), }; tracks.set(where.id, updated); return updated; }), }, audioAsset: { findUnique: jest.fn().mockImplementation(async ({ where }) => { if (where.id) { return audioAssets.get(where.id) ?? null; } const composite = where.userId_sha256; if (!composite) { return null; } return ( [...audioAssets.values()].find( (asset) => asset.userId === composite.userId && asset.sha256 === composite.sha256, ) ?? null ); }), create: jest.fn().mockImplementation(async ({ data }) => { const record = { id: randomUUID(), bitRateKbps: null, sampleRateHz: null, channels: null, createdAt: new Date(), ...data, }; audioAssets.set(record.id, record); return record; }), update: jest.fn().mockImplementation(async ({ where, data }) => { const current = audioAssets.get(where.id); const updated = { ...current, ...data, }; audioAssets.set(where.id, updated); return updated; }), }, uploadSession: { create: jest.fn().mockImplementation(async ({ data }) => { const now = new Date(); const record = { createdAt: now, updatedAt: now, completedAt: null, finalizedAt: null, trackId: null, audioAssetId: null, ...data, }; uploadSessions.set(record.id, record); return record; }), findUnique: jest.fn().mockImplementation(async ({ where }) => { return uploadSessions.get(where.id) ?? null; }), update: jest.fn().mockImplementation(async ({ where, data }) => { const current = uploadSessions.get(where.id); const updated = { ...current, ...data, updatedAt: new Date(), }; uploadSessions.set(where.id, updated); return updated; }), }, libraryEvent: { create: jest.fn().mockImplementation(async ({ data }) => { const record = { id: nextLibraryEventId, payloadVersion: 1, createdAt: new Date(), ...data, }; libraryEvents.set(record.id, record); nextLibraryEventId += 1n; return record; }), findFirst: jest.fn().mockImplementation(async ({ where }) => { const filteredEvents = [...libraryEvents.values()].filter((event) => where?.userId ? event.userId === where.userId : true, ); return filteredEvents.sort((lhs, rhs) => Number(rhs.id - lhs.id))[0] ?? null; }), }, }; return { prismaMock, state: { defaultUser, devices, tracks, audioAssets, uploadSessions, libraryEvents, }, }; } describe('Velody API wiring (e2e)', () => { let app: INestApplication; let healthController: HealthController; let devicesController: DevicesController; let syncController: SyncController; let uploadsController: UploadsController; let uploadsService: UploadsService; let prismaState: ReturnType['state']; let storageRoot: string; beforeEach(async () => { const { prismaMock, state } = createPrismaMock(); prismaState = state; storageRoot = await mkdtemp(join(tmpdir(), 'velody-e2e-')); const moduleRef = await Test.createTestingModule({ imports: [AppModule], }) .overrideProvider(AppConfigService) .useValue({ appVersion: '0.1.0', maxUploadSizeBytes: 1024 * 1024 * 1024, storageRoot, }) .overrideProvider(PrismaService) .useValue(prismaMock) .compile(); app = moduleRef.createNestApplication(); app.setGlobalPrefix('api'); app.enableVersioning({ type: VersioningType.URI }); app.useGlobalPipes( new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true, }), ); await app.init(); healthController = moduleRef.get(HealthController); devicesController = moduleRef.get(DevicesController); syncController = moduleRef.get(SyncController); uploadsController = moduleRef.get(UploadsController); uploadsService = moduleRef.get(UploadsService); }); afterEach(async () => { if (app) { await app.close(); } await rm(storageRoot, { recursive: true, force: true }); }); it('returns health information', async () => { const response = await healthController.getHealth(); expect(response.database.status).toBe('up'); expect(response.storage.status).toBe('up'); expect(response.version).toBe('0.1.0'); }); it('registers a device and accepts heartbeat', async () => { const registerResponse = await devicesController.register({ platform: 'MACOS', deviceName: 'Diya MacBook Pro', appVersion: '0.1.0', }); expect(registerResponse.deviceId).toBeDefined(); expect(registerResponse.bootstrapToken).toBeDefined(); const heartbeatResponse = await devicesController.heartbeat({ deviceId: registerResponse.deviceId, appVersion: '0.1.1', }); expect(heartbeatResponse.ok).toBe(true); }); it('returns sync bootstrap and changes payloads', async () => { const bootstrapResponse = await syncController.bootstrap(); const changesResponse = await syncController.changes({ after: '0' }); expect(bootstrapResponse.tracks).toEqual([]); expect(changesResponse.events).toEqual([]); expect(changesResponse.nextCursor).toBe('0'); }); it('supports the MP3 upload pipeline through the Nest app wiring', async () => { const registerResponse = await devicesController.register({ platform: 'MACOS', deviceName: 'Upload Mac', appVersion: '0.1.0', }); const bytes = sampleMp3Bytes('e2e-upload'); const sha256 = sha256Hex(bytes); const prepareResponse = await uploadsController.prepare({ deviceId: registerResponse.deviceId, sha256, originalFilename: 'e2e-upload.mp3', sizeBytes: bytes.length, }); expect(prepareResponse.status).toBe('upload_required'); const uploadResponse = await uploadsService.uploadFile( prepareResponse.uploadId!, createUploadRequest(bytes), ); expect(uploadResponse.status).toBe('COMPLETED'); const finalizeResponse = await uploadsController.finalize( prepareResponse.uploadId!, { title: 'Uploaded Track', artist: 'Velody', album: 'Milestone 6', durationMs: 222000, }, ); expect(finalizeResponse.trackId).toBeDefined(); expect(finalizeResponse.assetId).toBeDefined(); const storedBytes = await readFile( join(storageRoot, 'users', prismaState.defaultUser.id, 'audio', `${sha256}.mp3`), ); expect(storedBytes.equals(bytes)).toBe(true); const duplicatePrepare = await uploadsController.prepare({ deviceId: registerResponse.deviceId, sha256, originalFilename: 'e2e-upload.mp3', sizeBytes: bytes.length, }); expect(duplicatePrepare.status).toBe('exists'); expect(prismaState.audioAssets.size).toBe(1); expect(prismaState.libraryEvents.size).toBe(1); }); });