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.
98 lines
4.5 KiB
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)
|
|
}
|
|
}
|
|
}
|
|
|