import Testing import Foundation import MusicShared @testable import Music @MainActor struct StreamingIntegrationTests { static let testAPIKey = "integration-test-key" // Full flow: start server, authenticate, download DB, verify track is present. // Steps: // 1. Create an in-memory DB and insert a test track // 2. Start StreamingServer on a random port // 3. Authenticate via GET /auth // 4. Download DB via GET /db // 5. Save downloaded DB to disk and verify the track is present @Test(.timeLimit(.minutes(1))) func fullConnectionFlow() async throws { let db = try DatabaseService(inMemory: true) var track = Track.fixture(id: nil, fileURL: "/tmp/test.mp3", title: "Test Song") try db.insert(&track) let server = StreamingServer(db: db, apiKey: Self.testAPIKey, port: 0) try await server.start() defer { server.stop() } let port = try #require(server.actualPort) let baseURL = "http://127.0.0.1:\(port)" // 3. Authenticate var authReq = URLRequest(url: URL(string: "\(baseURL)/auth")!) authReq.setValue("Bearer \(Self.testAPIKey)", forHTTPHeaderField: "Authorization") let (authData, authResp) = try await URLSession.shared.data(for: authReq) let authHTTP = try #require(authResp as? HTTPURLResponse) #expect(authHTTP.statusCode == 200) let authResponse = try JSONDecoder().decode(AuthResponse.self, from: authData) #expect(authResponse.protocolVersion == StreamingConstants.protocolVersion) // 4. Download DB var dbReq = URLRequest(url: URL(string: "\(baseURL)/db")!) dbReq.setValue("Bearer \(Self.testAPIKey)", forHTTPHeaderField: "Authorization") let (dbData, dbResp) = try await URLSession.shared.data(for: dbReq) let dbHTTP = try #require(dbResp as? HTTPURLResponse) #expect(dbHTTP.statusCode == 200) #expect(dbData.count > 0) // 5. Verify downloaded DB contains the track let tempPath = FileManager.default.temporaryDirectory .appendingPathComponent("integration_test_\(UUID().uuidString).sqlite").path defer { try? FileManager.default.removeItem(atPath: tempPath) } try dbData.write(to: URL(fileURLWithPath: tempPath)) let downloadedDb = try DatabaseService(path: tempPath) let tracks = try downloadedDb.fetchTracks(search: "", sortColumn: "title", ascending: true) #expect(tracks.count == 1) #expect(tracks[0].title == "Test Song") } // Verifies that requests without auth get 401. @Test(.timeLimit(.minutes(1))) func unauthenticatedRequestsRejected() async throws { let db = try DatabaseService(inMemory: true) let server = StreamingServer(db: db, apiKey: Self.testAPIKey, port: 0) try await server.start() defer { server.stop() } let port = try #require(server.actualPort) let request = URLRequest(url: URL(string: "http://127.0.0.1:\(port)/auth")!) let (_, response) = try await URLSession.shared.data(for: request) let httpResponse = try #require(response as? HTTPURLResponse) #expect(httpResponse.statusCode == 401) } // Verifies the /tracks/:trackId/file endpoint serves audio data. @Test(.timeLimit(.minutes(1))) func fileEndpointServesTrack() async throws { // 1. Create DB with a test track pointing to a real audio file let db = try DatabaseService(inMemory: true) let fixtureURL = try TestFixtures.shortMP3URL() var track = Track.fixture(id: nil, fileURL: fixtureURL.path, title: "Stream Test") try db.insert(&track) let trackId = try #require(track.id) // 2. Start server let server = StreamingServer(db: db, apiKey: Self.testAPIKey, port: 0) try await server.start() defer { server.stop() } let port = try #require(server.actualPort) let baseURL = "http://127.0.0.1:\(port)" // 3. Request via Bearer auth var bearerReq = URLRequest(url: URL(string: "\(baseURL)/file?id=\(trackId)")!) bearerReq.setValue("Bearer \(Self.testAPIKey)", forHTTPHeaderField: "Authorization") let (bearerData, bearerResp) = try await URLSession.shared.data(for: bearerReq) let bearerHTTP = try #require(bearerResp as? HTTPURLResponse) #expect(bearerHTTP.statusCode == 200) #expect(bearerData.count > 0) // 4. Request via token query param let tokenURL = URL(string: "\(baseURL)/file?id=\(trackId)&token=\(Self.testAPIKey)")! let (tokenData, tokenResp) = try await URLSession.shared.data(for: URLRequest(url: tokenURL)) let tokenHTTP = try #require(tokenResp as? HTTPURLResponse) #expect(tokenHTTP.statusCode == 200) #expect(tokenData.count == bearerData.count) // 5. Unauthenticated request should be 401 let noAuthURL = URL(string: "\(baseURL)/file?id=\(trackId)")! let (_, noAuthResp) = try await URLSession.shared.data(for: URLRequest(url: noAuthURL)) let noAuthHTTP = try #require(noAuthResp as? HTTPURLResponse) #expect(noAuthHTTP.statusCode == 401) } // Verifies that wrong API key gets 401. @Test(.timeLimit(.minutes(1))) func wrongApiKeyRejected() async throws { let db = try DatabaseService(inMemory: true) let server = StreamingServer(db: db, apiKey: Self.testAPIKey, port: 0) try await server.start() defer { server.stop() } let port = try #require(server.actualPort) var request = URLRequest(url: URL(string: "http://127.0.0.1:\(port)/auth")!) request.setValue("Bearer wrong-key", forHTTPHeaderField: "Authorization") let (_, response) = try await URLSession.shared.data(for: request) let httpResponse = try #require(response as? HTTPURLResponse) #expect(httpResponse.statusCode == 401) } // Reproduces the real-world "File not found on disk" (HTTP 404) bug. // // The production scanner (ScannerService) stores `fileURL` as // `url.absoluteString` — e.g. "file:///Users/.../song.m4a" — WITH the // "file://" scheme and percent-encoding. StreamingServer reconstructed it // with `URL(fileURLWithPath:)`, which treats that whole string as a raw // (relative) path, prepends the CWD, and skips percent-decoding, so the file // is never found on disk. The earlier `fileEndpointServesTrack` test masked // this because it stored `fixtureURL.path` (a bare path) instead. // // Steps: // 1. Create a DB with a track whose fileURL is stored EXACTLY as the scanner // stores it: `fixtureURL.absoluteString` (not `.path`). // 2. Start the streaming server. // 3. Request GET /file?id= with a valid token. // 4. Expect HTTP 200 with the file's bytes. Before the fix this returns 404 // with body {"error":{"message":"File not found on disk"}}. @Test(.timeLimit(.minutes(1))) func fileEndpointServesTrackStoredAsAbsoluteString() async throws { // 1. Insert a track using the production storage format (absoluteString). let db = try DatabaseService(inMemory: true) let fixtureURL = try TestFixtures.shortMP3URL() var track = Track.fixture(id: nil, fileURL: fixtureURL.absoluteString, title: "Abs Stream Test") try db.insert(&track) let trackId = try #require(track.id) // 2. Start the server. let server = StreamingServer(db: db, apiKey: Self.testAPIKey, port: 0) try await server.start() defer { server.stop() } let port = try #require(server.actualPort) let baseURL = "http://127.0.0.1:\(port)" // 3. Request the file via the token query parameter. let url = URL(string: "\(baseURL)/file?id=\(trackId)&token=\(Self.testAPIKey)")! let (data, resp) = try await URLSession.shared.data(for: URLRequest(url: url)) let http = try #require(resp as? HTTPURLResponse) // 4. Must serve the bytes, not 404. #expect(http.statusCode == 200) #expect(data.count > 0) } // Same root cause as above, exercised through the HLS path (SegmenterCache // also used URL(fileURLWithPath:) on the stored fileURL). // // Steps: // 1. Insert a track whose fileURL is stored as `absoluteString`. // 2. Start the server. // 3. Request GET /tracks//segments/0.mp3 with a valid token. // 4. Expect HTTP 200 with segment bytes (before the fix the segmenter cannot // open the file, so this returns a 404/error). @Test(.timeLimit(.minutes(1))) func segmentEndpointServesTrackStoredAsAbsoluteString() async throws { // 1. Insert a track using the production storage format (absoluteString). let db = try DatabaseService(inMemory: true) let fixtureURL = try TestFixtures.shortMP3URL() var track = Track.fixture(id: nil, fileURL: fixtureURL.absoluteString, title: "Abs HLS Test") try db.insert(&track) let trackId = try #require(track.id) // 2. Start the server. let server = StreamingServer(db: db, apiKey: Self.testAPIKey, port: 0) try await server.start() defer { server.stop() } let port = try #require(server.actualPort) let baseURL = "http://127.0.0.1:\(port)" // 3. Request the first HLS segment via the token query parameter. let url = URL(string: "\(baseURL)/tracks/\(trackId)/segments/0.mp3?token=\(Self.testAPIKey)")! let (data, resp) = try await URLSession.shared.data(for: URLRequest(url: url)) let http = try #require(resp as? HTTPURLResponse) // 4. Must serve segment bytes, not 404. #expect(http.statusCode == 200) #expect(data.count > 0) } }