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

98 lines
4.5 KiB

import Testing
import Foundation
import MusicShared
import Network
@testable import Music
@MainActor
struct RemoteLibraryDisplayTests {
// Reproduces the reported remote-mode bug: "connect + DB download seem fine,
// but no tracks show up". This exercises the exact step enterRemoteMode performs
// after the download pointing a LibraryViewModel at the downloaded DB and
// asserts the view model's `tracks` array actually populates.
@Test(.timeLimit(.minutes(1)))
func remoteLibraryViewModelPopulatesTracksAfterDownload() async throws {
// 1. Create a host database and insert three tracks (the "host library").
let tempDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: tempDir) }
let hostDBPath = tempDir.appendingPathComponent("host.sqlite").path
let hostDB = try DatabaseService(path: hostDBPath)
for i in 1...3 {
var t = Track.fixture(fileURL: "/song\(i).mp3", title: "Song \(i)")
try hostDB.insert(&t)
}
// 2. Start the HostServer configured with the DB so GET /db serves a backup copy.
let server = HostServer(dbPath: hostDBPath)
server.configure(player: nil, db: hostDB)
try server.start()
try await Task.sleep(for: .milliseconds(200))
let port = server.actualPort!
defer { server.stop() }
// 3. Download the DB over HTTP and write the body to disk, exactly as
// RemoteClient.handleDBData does when a remote connects to a host.
let body = try await httpGet(host: "127.0.0.1", port: port, path: "/db")
let downloadedPath = tempDir.appendingPathComponent("remote_db.sqlite").path
try body.write(to: URL(fileURLWithPath: downloadedPath))
// 4. Point a LibraryViewModel at the downloaded DB this is precisely what
// MusicApp.enterRemoteMode() does after a successful connect.
let remoteDB = try DatabaseService(path: downloadedPath)
let library = LibraryViewModel(db: remoteDB)
// 5. The LibraryViewModel loads via an async GRDB ValueObservation, so poll
// briefly for the observation to deliver its initial value.
try await waitUntil { library.tracks.count == 3 }
// 6. The remote library MUST display all three downloaded tracks. If this
// fails with 0 tracks, it reproduces the "no tracks after connect" bug.
#expect(library.tracks.count == 3)
}
// MARK: - Helpers
/// Polls `condition` on the main actor until it becomes true or the timeout elapses.
private func waitUntil(
timeout: Duration = .seconds(3),
_ condition: @MainActor () -> Bool
) async throws {
let deadline = ContinuousClock.now + timeout
while ContinuousClock.now < deadline {
if condition() { return }
try await Task.sleep(for: .milliseconds(50))
}
}
/// Performs a simple HTTP GET using NWConnection and returns the response body.
private func httpGet(host: String, port: UInt16, path: String) async throws -> Data {
try await withCheckedThrowingContinuation { continuation in
let connection = NWConnection(
host: NWEndpoint.Host(host),
port: NWEndpoint.Port(rawValue: port)!,
using: .tcp
)
connection.stateUpdateHandler = { state in
if case .ready = state {
let request = "GET \(path) HTTP/1.1\r\nHost: \(host)\r\nConnection: close\r\n\r\n"
connection.send(content: Data(request.utf8), completion: .contentProcessed { _ in })
connection.receiveMessage { data, _, _, error in
if let error {
continuation.resume(throwing: error)
} else if let data, let range = data.range(of: Data("\r\n\r\n".utf8)) {
continuation.resume(returning: Data(data[range.upperBound...]))
} else {
continuation.resume(returning: data ?? Data())
}
connection.cancel()
}
} else if case .failed(let error) = state {
continuation.resume(throwing: error)
}
}
connection.start(queue: .main)
}
}
}