Files
ios/MusicBridgeTests/ResilienceTests.swift
T

126 lines
5.6 KiB
Swift
Raw Normal View History

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)
}
}
}
}