Files
SpotiFLAC-Mobile/ios/RunnerTests/RunnerTests.swift
T
zarzet 857d146c55 fix(ios): isolate download progress subscriptions
Give every subscription its own cursor, payload, and worker queue so an old blocked waiter cannot advance a replacement listener. Serialize lifecycle handling and guard cancellation.

Add a restart regression with an injected waiter; verified with Thread Sanitizer.
2026-09-06 14:03:49 +07:00

104 lines
3.4 KiB
Swift

import Foundation
import XCTest
@testable import Runner
class RunnerTests: XCTestCase {
func testProgressRestartDoesNotAcceptCancelledWaiterState() {
let oldStarted = expectation(description: "old waiter started")
let replacementDelivered = expectation(description: "replacement snapshot")
let replacementAdvanced = expectation(description: "replacement owns cursor")
let oldReleased = expectation(description: "old waiter returned")
let releaseOld = DispatchSemaphore(value: 0)
let lock = NSLock()
var calls = 0
var invalidCursors = [Int64]()
let stream = DownloadProgressSubscription(interval: 0.01) { sequence, _ in
lock.lock()
calls += 1
let call = calls
if (call <= 2 && sequence != 0) || (call > 2 && sequence != 2) {
invalidCursors.append(sequence)
}
lock.unlock()
if call == 1 {
oldStarted.fulfill()
_ = releaseOld.wait(timeout: .now() + 3)
oldReleased.fulfill()
return "{\"seq\":99,\"items\":{}}"
}
if call == 2 { return "{\"seq\":2,\"reset\":true,\"items\":{}}" }
if call == 3 { replacementAdvanced.fulfill() }
return ""
}
stream.start { _ in XCTFail("Cancelled listener received an event") }
wait(for: [oldStarted], timeout: 2)
stream.stop()
stream.start { event in
XCTAssertEqual((event as? [String: Any])?["seq"] as? Int, 2)
replacementDelivered.fulfill()
releaseOld.signal()
}
wait(for: [replacementDelivered, oldReleased, replacementAdvanced], timeout: 2)
stream.stop()
lock.lock()
XCTAssertTrue(invalidCursors.isEmpty, "Unexpected cursors: \(invalidCursors)")
lock.unlock()
}
func testParsesOAuthCallback() {
let route = ExtensionCallbackParser.parse(
URL(string: "spotiflac://callback?code=auth-code&state=spotify-web")!
)
XCTAssertEqual(
route,
ExtensionCallbackRoute(
code: "auth-code",
state: "spotify-web",
isSessionGrant: false
)
)
}
func testParsesSignedSessionGrant() {
let route = ExtensionCallbackParser.parse(
URL(
string:
"spotiflac://session-grant?grant=session-token&state=provider"
)!
)
XCTAssertEqual(
route,
ExtensionCallbackRoute(
code: "session-token",
state: "provider",
isSessionGrant: true
)
)
}
func testRejectsUntrustedOrIncompleteCallbacks() {
XCTAssertNil(
ExtensionCallbackParser.parse(
URL(string: "https://callback?code=auth&state=provider")!
)
)
XCTAssertNil(
ExtensionCallbackParser.parse(
URL(string: "spotiflac://unknown?code=auth&state=provider")!
)
)
XCTAssertNil(
ExtensionCallbackParser.parse(
URL(string: "spotiflac://callback?code=auth")!
)
)
XCTAssertNil(
ExtensionCallbackParser.parse(
URL(string: "spotiflac://callback?state=provider")!
)
)
}
}