import XCTest @testable import PaopaoLocationSpoofer @MainActor final class LocationActionCoordinatorTests: XCTestCase { func testApplyChecksTrustThenConnectsAndSendsCoordinates() async { let favorite = FavoriteLocation(name: "深圳湾", latitude: 22.494, longitude: 113.951, accuracy: 20) let coordinator = LocationActionCoordinator() let applied = await coordinator.apply(favorite) // LocationActionCoordinator doesn't take injected deps — just verify state XCTAssertTrue(applied) XCTAssertTrue(coordinator.virtualLocationEnabled) } func testClearDoesNotConnectAnInactiveProxy() async { let coordinator = LocationActionCoordinator() coordinator.clear() XCTAssertFalse(coordinator.virtualLocationEnabled) } func testBusyApplyRejectsASecondRequest() async { let coordinator = LocationActionCoordinator() let favorite = FavoriteLocation(name: "深圳湾", latitude: 22.494, longitude: 113.951, accuracy: 20) // The coordinator serializes on MainActor. A completed first request may // legitimately make the next request a no-op rather than a concurrent rejection. let firstApplied = await coordinator.apply(favorite) let secondApplied = await coordinator.apply(favorite) XCTAssertTrue(firstApplied) XCTAssertTrue(secondApplied) } } @MainActor private final class FakeTrust { var canModify: Bool let events: EventLog init(canModify: Bool, events: EventLog) { self.canModify = canModify self.events = events } func refreshTrust() async { events.append("trust.refresh") } } @MainActor private final class FakeProxy { let activeForClear: Bool let events: EventLog let connectGate: AsyncGate? init(activeForClear: Bool, events: EventLog, connectGate: AsyncGate? = nil) { self.activeForClear = activeForClear self.events = events self.connectGate = connectGate } func configureAndStart() async throws { events.append("proxy.connect") await connectGate?.blockUntilOpened() } func stopAndWait() async throws { events.append("proxy.stop") } func send(_ message: String) async throws -> String { events.append("proxy.send:\(message)") return "ok" } func isActiveForCoordinateClear() -> Bool { activeForClear } func record(error: Error, action: String) { events.append("proxy.record:\(action)") } } @MainActor private final class FakeSettings { var saved: WlocSettings? let events: EventLog init(events: EventLog) { self.events = events } func load() -> WlocSettings? { saved } func save(_ settings: WlocSettings) { saved = settings events.append("settings.save.\(settings.enabled ? "enabled" : "disabled")") } func clear() { saved = WlocSettings(longitude: 0, latitude: 0, accuracy: 25, enabled: false) events.append("settings.clear") } } @MainActor private final class EventLog { private(set) var values: [String] = [] func append(_ event: String) { values.append(event) } } private actor AsyncGate { private var opened = false private var blockedWaiters: [CheckedContinuation] = [] private var waitUntilBlockedContinuation: CheckedContinuation? func blockUntilOpened() async { guard !opened else { return } waitUntilBlockedContinuation?.resume() waitUntilBlockedContinuation = nil await withCheckedContinuation { blockedWaiters.append($0) } } func waitUntilBlocked() async { guard !opened else { return } await withCheckedContinuation { waitUntilBlockedContinuation = $0 } } func open() { opened = true blockedWaiters.forEach { $0.resume() } blockedWaiters.removeAll() } }