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.
147 lines
5.6 KiB
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"))
|
|
}
|
|
}
|
|
|