You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
Music/MusicTests/StreamingServerTests.swift

147 lines
5.6 KiB

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"))
}
}