126 lines
5.6 KiB
Swift
126 lines
5.6 KiB
Swift
import XCTest
|
|
@testable import MusicBridge
|
|
|
|
@MainActor
|
|
final class ResilienceTests: XCTestCase {
|
|
private func connection(_ token: String) throws -> Connection {
|
|
let url = try XCTUnwrap(Bundle(for: Self.self).url(forResource: "tls-fixture", withExtension: "json"))
|
|
let fixtures = try JSONDecoder().decode([String: [String: String]].self, from: Data(contentsOf: url))
|
|
guard let fixture = fixtures["valid"] else { throw XCTSkip("Start tools/tls_fixture.py first.") }
|
|
return try Connection(endpoint: XCTUnwrap(fixture["endpoint"]), fingerprint: XCTUnwrap(fixture["fingerprint"]), token: token)
|
|
}
|
|
|
|
private func store(_ token: String) throws -> PlayerStore {
|
|
let saved = try connection(token)
|
|
return PlayerStore(storage: ConnectionStorage(load: { saved }, save: { _ in }, delete: {}),
|
|
retryUnit: 20_000_000, artworkRetryUnit: 20_000_000, discoveryEnabled: false)
|
|
}
|
|
|
|
private func eventually(_ condition: () -> Bool, file: StaticString = #filePath, line: UInt = #line) async throws {
|
|
for _ in 0..<200 {
|
|
if condition() { return }
|
|
try await Task.sleep(nanoseconds: 20_000_000)
|
|
}
|
|
XCTFail("Condition did not become true", file: file, line: line)
|
|
}
|
|
|
|
func testRecoversAfterTemporaryServerFailureAndForegroundReturn() async throws {
|
|
let store = try store("recover")
|
|
defer { store.setActive(false) }
|
|
store.setActive(true)
|
|
try await eventually { store.connected }
|
|
store.setActive(false)
|
|
XCTAssertFalse(store.connected)
|
|
XCTAssertNil(store.state)
|
|
store.setActive(true)
|
|
try await eventually { store.connected }
|
|
}
|
|
|
|
func testRevokedAccessStopsRetriesAndSurvivesForegroundReturn() async throws {
|
|
let store = try store("revoked")
|
|
defer { store.setActive(false) }
|
|
store.setActive(true)
|
|
try await eventually { store.needsPairing }
|
|
XCTAssertFalse(store.connected)
|
|
store.setActive(false); store.setActive(true)
|
|
try await Task.sleep(nanoseconds: 100_000_000)
|
|
XCTAssertEqual(store.status, "Нужно повторное сопряжение")
|
|
XCTAssertTrue(store.needsPairing)
|
|
XCTAssertFalse(store.connected)
|
|
}
|
|
|
|
func testArtworkRetriesAfter404WithoutTrackChange() async throws {
|
|
let store = try store("artwork")
|
|
defer { store.setActive(false) }
|
|
store.setActive(true)
|
|
try await eventually { store.artwork != nil }
|
|
XCTAssertEqual(store.state?.artworkId, String(repeating: "A", count: 64))
|
|
XCTAssertNil(store.artworkError)
|
|
}
|
|
|
|
func testArtworkFailureStopsAtBoundedRetryAndAllowsManualRetry() async throws {
|
|
let store = try store("artwork-fail")
|
|
defer { store.setActive(false) }
|
|
store.setActive(true)
|
|
try await eventually { store.artworkError?.contains("вручную") == true }
|
|
XCTAssertTrue(store.connected)
|
|
store.retryArtwork()
|
|
try await eventually { store.artworkError?.contains("Повторяем") == true }
|
|
store.setActive(false)
|
|
XCTAssertNil(store.artworkError)
|
|
}
|
|
|
|
func testLateArtworkDoesNotReplaceNewTrackImage() async throws {
|
|
let store = try store("late-art")
|
|
defer { store.setActive(false) }
|
|
store.setActive(true)
|
|
try await eventually { store.artwork?.size.width == 2 }
|
|
try await Task.sleep(nanoseconds: 600_000_000)
|
|
XCTAssertEqual(store.state?.artworkId, String(repeating: "B", count: 64))
|
|
XCTAssertEqual(store.artwork?.size.width, 2)
|
|
}
|
|
|
|
func testStreamUpdatesDoNotEraseRejectedCommandMessage() async throws {
|
|
let store = try store("command-reject")
|
|
defer { store.setActive(false) }
|
|
store.setActive(true)
|
|
try await eventually { store.connected }
|
|
await store.send("next")
|
|
try await Task.sleep(nanoseconds: 200_000_000)
|
|
XCTAssertEqual(store.error, "Плеер не поддерживает эту команду.")
|
|
XCTAssertFalse(store.busy)
|
|
}
|
|
|
|
func testInterruptedPairingDoesNotSaveLateResponse() async throws {
|
|
let target = try connection("")
|
|
var saved = false
|
|
let store = PlayerStore(storage: ConnectionStorage(load: { nil }, save: { _ in saved = true }, delete: {}), discoveryEnabled: false)
|
|
store.setActive(true)
|
|
let task = Task { await store.pair(endpoint: target.endpoint.absoluteString, fingerprint: target.fingerprint, code: "33333333") }
|
|
try await eventually { store.pairing }
|
|
try await Task.sleep(nanoseconds: 80_000_000)
|
|
store.setActive(false)
|
|
await task.value
|
|
XCTAssertFalse(saved)
|
|
XCTAssertNil(store.connection)
|
|
XCTAssertFalse(store.pairing)
|
|
XCTAssertTrue(store.error?.contains("прервано") == true)
|
|
}
|
|
|
|
func testRedirectIsRejectedAndPairingServerCodesArePreserved() async throws {
|
|
let client = BridgeClient(try connection("redirect"))
|
|
defer { client.close() }
|
|
do { _ = try await client.state(); XCTFail("Followed redirect") }
|
|
catch {
|
|
guard case BridgeError.http(status: 302, code: _) = error else { return XCTFail("Unexpected error: \(error)") }
|
|
}
|
|
for (code, expected) in [("11111111", "invalid_code"), ("22222222", "pairing_closed")] {
|
|
do { _ = try await client.pair(code: code); XCTFail("Accepted invalid pairing") }
|
|
catch {
|
|
guard case BridgeError.http(status: 403, code: let actual) = error else { return XCTFail("Unexpected error: \(error)") }
|
|
XCTAssertEqual(actual, expected)
|
|
}
|
|
}
|
|
}
|
|
}
|