import Foundation import MusicShared import Network import Testing @testable import Music @MainActor struct StreamingServerTests { // MARK: - Helpers /// Creates a StreamingServer backed by an in-memory database on port 0 /// (OS-assigned) with a known API key for testing. private func makeServer() throws -> StreamingServer { let db = try DatabaseService(inMemory: true) return StreamingServer(db: db, apiKey: "test-key-12345", port: 0) } /// Performs a simple HTTP GET using NWConnection and returns the full /// response (headers + body) as raw Data, then splits out just the body. private func httpGet( host: String, port: Int, path: String, headers: [String: String] = [:] ) async throws -> (statusCode: Int, body: Data) { try await withCheckedThrowingContinuation { continuation in let connection = NWConnection( host: NWEndpoint.Host(host), port: NWEndpoint.Port(rawValue: UInt16(port))!, using: .tcp ) connection.stateUpdateHandler = { state in if case .ready = state { // Build the HTTP request var request = "GET \(path) HTTP/1.1\r\nHost: \(host)\r\nConnection: close\r\n" for (key, value) in headers { request += "\(key): \(value)\r\n" } request += "\r\n" connection.send(content: Data(request.utf8), completion: .contentProcessed { _ in }) // Receive the full response (Connection: close ensures everything arrives) connection.receiveMessage { data, _, _, error in defer { connection.cancel() } if let error { continuation.resume(throwing: error) return } guard let data else { continuation.resume(returning: (statusCode: 0, body: Data())) return } // Parse the status code from the first line let responseString = String(data: data, encoding: .utf8) ?? "" let firstLine = responseString.split(separator: "\r\n").first ?? "" let parts = firstLine.split(separator: " ") let statusCode = parts.count >= 2 ? Int(parts[1]) ?? 0 : 0 // Extract body after the header/body separator if let range = data.range(of: Data("\r\n\r\n".utf8)) { continuation.resume(returning: (statusCode: statusCode, body: Data(data[range.upperBound...]))) } else { continuation.resume(returning: (statusCode: statusCode, body: Data())) } } } else if case .failed(let error) = state { continuation.resume(throwing: error) } } connection.start(queue: .main) } } // MARK: - Tests // 1. Sends GET /auth with a valid Bearer key. // 2. Expects a 200 status code. // 3. Decodes the body as AuthResponse and verifies protocolVersion matches. @Test(.timeLimit(.minutes(1))) func authEndpointAcceptsValidKey() async throws { let server = try makeServer() try await server.start() defer { server.stop() } let port = server.actualPort! let (statusCode, body) = try await httpGet( host: "127.0.0.1", port: port, path: "/auth", headers: ["Authorization": "Bearer test-key-12345"] ) #expect(statusCode == 200) let authResponse = try JSONDecoder().decode(AuthResponse.self, from: body) #expect(authResponse.protocolVersion == StreamingConstants.protocolVersion) #expect(!authResponse.hostName.isEmpty) } // 1. Sends GET /auth WITHOUT an Authorization header. // 2. Expects a 401 Unauthorized status code. @Test(.timeLimit(.minutes(1))) func authEndpointRejectsNoKey() async throws { let server = try makeServer() try await server.start() defer { server.stop() } let port = server.actualPort! let (statusCode, _) = try await httpGet( host: "127.0.0.1", port: port, path: "/auth" // No Authorization header ) #expect(statusCode == 401) } // 1. Sends GET /db with a valid key using URLSession (binary response // requires proper HTTP framing that NWConnection.receiveMessage lacks). // 2. Expects a 200 status code. // 3. Verifies the response body starts with the SQLite magic header "SQLite format 3". @Test(.timeLimit(.minutes(1))) func dbEndpointReturnsDatabaseFile() async throws { let server = try makeServer() try await server.start() defer { server.stop() } let port = server.actualPort! var request = URLRequest(url: URL(string: "http://127.0.0.1:\(port)/db")!) request.setValue("Bearer test-key-12345", forHTTPHeaderField: "Authorization") let (data, response) = try await URLSession.shared.data(for: request) let httpResponse = try #require(response as? HTTPURLResponse) #expect(httpResponse.statusCode == 200) // SQLite files start with "SQLite format 3\0" (16 bytes) let header = String(data: data.prefix(16), encoding: .utf8) ?? "" #expect(header.hasPrefix("SQLite format 3")) } }