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/StreamingIntegrationTests.s...

204 lines
9.6 KiB

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=<trackId> 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/<id>/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)
}
}