Files
location-spoofer/Tests/PaopaoLocationSpooferTests/LocationActionCoordinatorTests.swift
T

133 lines
3.8 KiB
Swift

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<Void, Never>] = []
private var waitUntilBlockedContinuation: CheckedContinuation<Void, Never>?
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()
}
}