import { randomUUID, createHash } from 'node:crypto'; import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { dirname, join } from 'node:path'; import { Readable } from 'node:stream'; import { ForbiddenException, INestApplication, NotFoundException, ValidationPipe, VersioningType, } from '@nestjs/common'; import { Test } from '@nestjs/testing'; import { AppModule } from '../../src/app.module'; import { AssetsController } from '../../src/modules/assets/assets.controller'; import { AssetDownloadQueryDto } from '../../src/modules/assets/assets.dto'; 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 { LibraryController } from '../../src/modules/library/library.controller'; import { LibraryTracksQueryDto } from '../../src/modules/library/library.dto'; 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; } async function streamToBuffer(stream: NodeJS.ReadableStream): Promise { const chunks: Buffer[] = []; for await (const chunkValue of stream) { chunks.push( Buffer.isBuffer(chunkValue) ? chunkValue : Buffer.from(chunkValue), ); } return Buffer.concat(chunks); } 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 }) => { const device = devices.get(where.id) ?? null; if (!device) { return null; } if (where.id && typeof where.id === 'string') { return device; } return device; }), 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; const assetMatches = where?.primaryAudioAssetId?.not ? track.primaryAudioAssetId != null : true; return userMatches && statusMatches && assetMatches; }) .sort((lhs, rhs) => lhs.createdAt.getTime() - rhs.createdAt.getTime()) .map((track) => ({ ...track, primaryAudioAsset: track.primaryAudioAssetId ? audioAssets.get(track.primaryAudioAssetId) ?? null : null, })); }), 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 assetsController: AssetsController; let healthController: HealthController; let devicesController: DevicesController; let libraryController: LibraryController; 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(); assetsController = moduleRef.get(AssetsController); healthController = moduleRef.get(HealthController); devicesController = moduleRef.get(DevicesController); libraryController = moduleRef.get(LibraryController); 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('downloads audio asset bytes for the owning device user', async () => { const registerResponse = await devicesController.register({ platform: 'IPHONE', deviceName: 'Playback iPhone', appVersion: '0.1.0', }); const assetId = randomUUID(); const trackId = randomUUID(); const bytes = sampleMp3Bytes('owner-download'); const storageKey = join( 'users', prismaState.defaultUser.id, 'audio', 'owner-download.mp3', ); prismaState.audioAssets.set(assetId, { id: assetId, userId: prismaState.defaultUser.id, trackId, sha256: sha256Hex(bytes), storageKey, originalFilename: 'owner-download.mp3', mimeType: 'audio/mpeg', fileExtension: 'mp3', fileSizeBytes: BigInt(bytes.length), durationMs: 180000, sourceDeviceId: registerResponse.deviceId, createdAt: new Date('2026-05-29T08:00:00.000Z'), }); const filePath = join(storageRoot, storageKey); await mkdir(dirname(filePath), { recursive: true }); await writeFile(filePath, bytes); const headers = new Map(); const responseMock = { setHeader(name: string, value: string) { headers.set(name.toLowerCase(), String(value)); }, } as any; const streamable = await assetsController.download( assetId, { deviceId: registerResponse.deviceId }, responseMock, ); const downloadedBytes = await streamToBuffer(streamable.getStream()); expect(downloadedBytes.equals(bytes)).toBe(true); expect(headers.get('content-type')).toBe('audio/mpeg'); expect(headers.get('content-length')).toBe(String(bytes.length)); }); it('rejects unauthorized asset download requests for another user asset', async () => { const registerResponse = await devicesController.register({ platform: 'IPHONE', deviceName: 'Playback iPhone', appVersion: '0.1.0', }); const assetId = randomUUID(); const otherUserId = randomUUID(); prismaState.audioAssets.set(assetId, { id: assetId, userId: otherUserId, trackId: randomUUID(), sha256: 'sha-other', storageKey: join('users', otherUserId, 'audio', 'other.mp3'), originalFilename: 'other.mp3', mimeType: 'audio/mpeg', fileExtension: 'mp3', fileSizeBytes: BigInt(10), durationMs: 180000, sourceDeviceId: randomUUID(), createdAt: new Date('2026-05-29T08:00:00.000Z'), }); await expect( assetsController.download( assetId, { deviceId: registerResponse.deviceId }, { setHeader() {} } as any, ), ).rejects.toBeInstanceOf(ForbiddenException); }); it('handles missing audio asset files cleanly', async () => { const registerResponse = await devicesController.register({ platform: 'IPHONE', deviceName: 'Playback iPhone', appVersion: '0.1.0', }); const assetId = randomUUID(); prismaState.audioAssets.set(assetId, { id: assetId, userId: prismaState.defaultUser.id, trackId: randomUUID(), sha256: 'sha-missing-file', storageKey: join( 'users', prismaState.defaultUser.id, 'audio', 'missing-file.mp3', ), originalFilename: 'missing-file.mp3', mimeType: 'audio/mpeg', fileExtension: 'mp3', fileSizeBytes: BigInt(10), durationMs: 180000, sourceDeviceId: registerResponse.deviceId, createdAt: new Date('2026-05-29T08:00:00.000Z'), }); await expect( assetsController.download( assetId, { deviceId: registerResponse.deviceId }, { setHeader() {} } as any, ), ).rejects.toBeInstanceOf(NotFoundException); }); it('rejects an invalid asset download device id query', async () => { const validationPipe = new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true, }); await expect( validationPipe.transform( { deviceId: 'not-a-uuid' }, { type: 'query', metatype: AssetDownloadQueryDto, data: '', }, ), ).rejects.toMatchObject({ response: { message: expect.arrayContaining(['deviceId must be a UUID']), }, }); }); it('returns remote library metadata for the requesting device owner', async () => { const primaryDevice = await devicesController.register({ platform: 'IPHONE', deviceName: 'iPhone', appVersion: '0.1.0', }); const secondUserId = randomUUID(); const secondaryDeviceId = randomUUID(); const primaryTrackId = randomUUID(); const primaryAssetId = randomUUID(); const secondaryTrackId = randomUUID(); const secondaryAssetId = randomUUID(); prismaState.devices.set(secondaryDeviceId, { id: secondaryDeviceId, userId: secondUserId, platform: 'IPHONE', deviceName: 'Other iPhone', appVersion: '0.1.0', installTokenHash: 'other-device-hash', lastSeenAt: new Date(), createdAt: new Date(), updatedAt: new Date(), }); prismaState.audioAssets.set(primaryAssetId, { id: primaryAssetId, userId: prismaState.defaultUser.id, trackId: primaryTrackId, sha256: 'sha-default', storageKey: 'users/default/audio/sha-default.mp3', originalFilename: 'default.mp3', mimeType: 'audio/mpeg', fileExtension: 'mp3', fileSizeBytes: BigInt(42), durationMs: 245000, sourceDeviceId: primaryDevice.deviceId, createdAt: new Date('2026-05-29T08:00:00.000Z'), }); prismaState.audioAssets.set(secondaryAssetId, { id: secondaryAssetId, userId: secondUserId, trackId: secondaryTrackId, sha256: 'sha-other-user', storageKey: 'users/other/audio/sha-other-user.mp3', originalFilename: 'other.mp3', mimeType: 'audio/mpeg', fileExtension: 'mp3', fileSizeBytes: BigInt(24), durationMs: 180000, sourceDeviceId: secondaryDeviceId, createdAt: new Date('2026-05-29T08:01:00.000Z'), }); prismaState.tracks.set(primaryTrackId, { id: primaryTrackId, userId: prismaState.defaultUser.id, primaryAudioAssetId: primaryAssetId, artworkAssetId: null, title: 'Default User Track', artist: 'Velody', album: null, albumArtist: null, genre: null, discNumber: null, trackNumber: null, year: null, durationMs: 245000, status: 'ACTIVE', deletedAt: null, createdAt: new Date('2026-05-29T08:00:00.000Z'), updatedAt: new Date('2026-05-29T08:02:00.000Z'), }); prismaState.tracks.set(secondaryTrackId, { id: secondaryTrackId, userId: secondUserId, primaryAudioAssetId: secondaryAssetId, artworkAssetId: null, title: 'Other User Track', artist: 'Elsewhere', album: null, albumArtist: null, genre: null, discNumber: null, trackNumber: null, year: null, durationMs: 180000, status: 'ACTIVE', deletedAt: null, createdAt: new Date('2026-05-29T08:01:00.000Z'), updatedAt: new Date('2026-05-29T08:03:00.000Z'), }); const response = await libraryController.getTracks({ deviceId: primaryDevice.deviceId, }); expect(response).toEqual({ tracks: [ { trackId: primaryTrackId, title: 'Default User Track', artist: 'Velody', durationSeconds: 245, sha256: 'sha-default', assetId: primaryAssetId, createdAt: '2026-05-29T08:00:00.000Z', updatedAt: '2026-05-29T08:02:00.000Z', }, ], }); }); it('rejects an invalid remote library device id query', async () => { const validationPipe = new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true, }); await expect( validationPipe.transform( { deviceId: 'not-a-uuid' }, { type: 'query', metatype: LibraryTracksQueryDto, data: '', }, ), ).rejects.toMatchObject({ response: { message: expect.arrayContaining(['deviceId must be a UUID']), }, }); }); 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); }); });