release: PaopaoLocationSpoofer v1.0.0

- iOS 虚拟定位工具,基于本地 HTTP 代理 MITM 方案
- MapKit 原生地图体验,支持搜索、收藏、实时定位
- 完整的设置引导流程(证书安装、WiFi 代理配置、环境验证)
- 支持 iOS 15+,SwiftUI 构建
This commit is contained in:
xweiba
2026-08-05 15:45:24 +08:00
commit c8aba2be78
75 changed files with 7339 additions and 0 deletions
@@ -0,0 +1,17 @@
import XCTest
@testable import PaopaoLocationSpoofer
final class CertificateAuthorityStoreTests: XCTestCase {
func testEnsureCreatesOnceAndThenReusesExistingPair() throws {
let directory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
defer { try? FileManager.default.removeItem(at: directory) }
var generations = 0
let store = CertificateAuthorityStore(directory: directory) {
generations += 1
return CertificateAuthority(certPEM: "cert", keyPEM: "key")
}
XCTAssertEqual(try store.ensure(), CertificateAuthority(certPEM: "cert", keyPEM: "key"))
XCTAssertEqual(try store.ensure(), CertificateAuthority(certPEM: "cert", keyPEM: "key"))
XCTAssertEqual(generations, 1)
}
}
@@ -0,0 +1,17 @@
import XCTest
@testable import PaopaoLocationSpoofer
final class CertificateTrustStateTests: XCTestCase {
func testActionStateBusyAndStatusText() {
XCTAssertTrue(LocationActionState.applyingLocation.isBusy)
XCTAssertEqual(LocationActionState.idle.statusTitle, "")
XCTAssertEqual(LocationActionState.failed("permission denied").statusTitle, "failed")
XCTAssertFalse(LocationActionState.idle.isBusy)
}
func testCertificateReadinessAllowsOnlyTrustedState() {
XCTAssertTrue(CertificateTrustState.trusted.canModify)
XCTAssertFalse(CertificateTrustState.unavailable.canModify)
XCTAssertFalse(CertificateTrustState.checking.canModify)
}
}
@@ -0,0 +1,9 @@
import XCTest
@testable import PaopaoLocationSpoofer
final class CertificateTrustVerifierTests: XCTestCase {
func testVerifierMapsFailedProbeToUnavailable() async {
let verifier = CertificateTrustVerifier(probe: { _, _ in false })
XCTAssertEqual(await verifier.verify(url: URL(string: "https://127.0.0.1:1/health")!, leafHash: "x"), .unavailable)
}
}
@@ -0,0 +1,20 @@
import XCTest
@testable import PaopaoLocationSpoofer
final class FavoriteLocationStoreTests: XCTestCase {
func testSavingFavoriteSelectsItAndPersistsAcrossStoreInstances() {
let suite = "FavoriteLocationStoreTests.\(UUID().uuidString)"
let defaults = UserDefaults(suiteName: suite)!
defer { defaults.removePersistentDomain(forName: suite) }
let store = FavoriteLocationStore(defaults: defaults)
let favorite = store.save(name: "深圳湾", latitude: 22.494, longitude: 113.951, accuracy: 20)
XCTAssertEqual(store.selectedFavoriteID, favorite.id)
XCTAssertEqual(FavoriteLocationStore(defaults: defaults).selectedFavorite?.name, "深圳湾")
}
func testMapConfigurationNeverRequestsRealUserLocation() {
XCTAssertFalse(MapConfiguration.default.showsUserLocation)
XCTAssertFalse(MapConfiguration.default.allowsCurrentLocationRequest)
}
}
@@ -0,0 +1,138 @@
import XCTest
@testable import PaopaoLocationSpoofer
@MainActor
final class LocationActionCoordinatorTests: XCTestCase {
func testApplyChecksTrustThenConnectsAndSendsCoordinates() async {
let events = EventLog()
let trust = FakeTrust(canModify: true, events: events)
let proxy = FakeProxy(activeForClear: false, events: events)
let settings = FakeSettings(events: events)
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 events = EventLog()
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)
let first = Task { await coordinator.apply(favorite) }
let secondApplied = await coordinator.apply(favorite)
// Should reject while busy
XCTAssertFalse(secondApplied)
let firstApplied = await first.value
// First one might succeed or fail depending on proxy state; just check no crash
_ = firstApplied
}
}
@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()
}
}
@@ -0,0 +1,210 @@
import XCTest
import CoreLocation
import MapKit
@testable import PaopaoLocationSpoofer
@MainActor
final class MapLocationStateTests: XCTestCase {
private let initial = CLLocationCoordinate2D(latitude: 22.544577, longitude: 113.94114)
func testStaleRealtimeResultDoesNotReplaceNewerMapPan() {
let state = MapLocationState(initialCoordinate: initial)
let request = state.beginRealtimeIntent()
state.selectUserMapCenter(.init(latitude: 31.23, longitude: 121.47))
let accepted = state.acceptRealtimeLocation(
.init(latitude: 39.90, longitude: 116.40),
intent: request,
focus: true
)
XCTAssertFalse(accepted)
XCTAssertEqual(state.selection.coordinate.latitude, 31.23, accuracy: 0.000001)
XCTAssertEqual(state.realtimeCoordinate?.latitude ?? 0, 39.90, accuracy: 0.000001)
}
func testSearchAndFavoriteSelectionsOwnTheirNamesAndRevision() {
let state = MapLocationState(initialCoordinate: initial)
let originalRevision = state.selection.revision
let favoriteID = UUID()
state.selectSearchResult(.init(latitude: 31.23, longitude: 121.47), name: "外滩")
XCTAssertGreaterThan(state.selection.revision, originalRevision)
XCTAssertEqual(state.selection.source, .search)
XCTAssertEqual(state.displayName, "外滩")
state.selectFavorite(.init(latitude: 39.90, longitude: 116.40), id: favoriteID, name: "公司")
XCTAssertEqual(state.selection.source, .favorite(favoriteID))
XCTAssertEqual(state.displayName, "公司")
state.updateViewport(distanceMeters: 300_000)
XCTAssertEqual(state.displayName, "公司")
}
func testPanTapAndRealtimeClearOldFavoriteOwnership() {
let state = MapLocationState(initialCoordinate: initial)
state.selectFavorite(initial, id: UUID(), name: "旧收藏")
state.selectMapTap(.init(latitude: 23, longitude: 114))
XCTAssertEqual(state.selection.source, .mapTap)
XCTAssertNil(state.selection.explicitName)
state.selectUserMapCenter(.init(latitude: 24, longitude: 115))
XCTAssertEqual(state.selection.source, .userPan)
let intent = state.beginRealtimeIntent()
XCTAssertTrue(state.acceptRealtimeLocation(.init(latitude: 25, longitude: 116), intent: intent, focus: true))
XCTAssertEqual(state.selection.source, .realtime)
}
func testUnchangedUserCenterDoesNotClearExplicitSearchName() {
let state = MapLocationState(initialCoordinate: initial)
state.selectSearchResult(initial, name: "深圳湾")
let revision = state.selection.revision
let returnedRevision = state.selectUserMapCenter(initial)
XCTAssertEqual(returnedRevision, revision)
XCTAssertEqual(state.selection.source, .search)
XCTAssertEqual(state.displayName, "深圳湾")
}
func testRepeatedFocusCommandsHaveUniqueIDs() {
let state = MapLocationState(initialCoordinate: initial)
state.focusSelection(distanceMeters: 200)
let first = state.cameraCommand
state.focusSelection(distanceMeters: 200)
let second = state.cameraCommand
XCTAssertNotNil(first)
XCTAssertNotNil(second)
XCTAssertNotEqual(first?.id, second?.id)
}
func testZoomCommandDoesNotChangeSelectedCoordinate() {
let state = MapLocationState(initialCoordinate: initial)
let before = state.selection
state.zoom(by: 0.5)
XCTAssertEqual(state.selection.coordinate.latitude, before.coordinate.latitude, accuracy: 0.000001)
XCTAssertEqual(state.selection.coordinate.longitude, before.coordinate.longitude, accuracy: 0.000001)
guard case .zoom(let factor) = state.cameraCommand?.kind else {
return XCTFail("Expected zoom command")
}
XCTAssertEqual(factor, 0.5)
}
func testPlaceLabelChangesWithViewportDistance() {
let place = MapPlaceDescriptor(
pointOfInterest: "深圳湾体育中心",
streetAddress: "滨海大道 3001 号",
road: "滨海大道",
neighborhood: "粤海街道",
district: "南山区",
city: "深圳市",
province: "广东省",
country: "中国"
)
XCTAssertEqual(place.displayName(viewportMeters: 300), "深圳湾体育中心")
XCTAssertEqual(place.displayName(viewportMeters: 2_500), "滨海大道")
XCTAssertEqual(place.displayName(viewportMeters: 8_000), "粤海街道")
XCTAssertEqual(place.displayName(viewportMeters: 15_000), "南山区 · 深圳市")
XCTAssertEqual(place.displayName(viewportMeters: 300_000), "深圳市 · 广东省")
}
func testDistrictFallbackStillChangesBetweenNeighborhoodAndCityZoom() {
let place = MapPlaceDescriptor(
pointOfInterest: "深圳湾公园",
streetAddress: "望海路 1 号",
road: "望海路",
district: "南山区",
city: "深圳市",
province: "广东省",
country: "中国"
)
XCTAssertEqual(place.displayName(viewportMeters: 8_000), "南山区")
XCTAssertEqual(place.displayName(viewportMeters: 15_000), "南山区 · 深圳市")
XCTAssertEqual(place.displayName(viewportMeters: 300_000), "深圳市 · 广东省")
}
func testPlaceLabelFallsBackAcrossMissingLevels() {
let place = MapPlaceDescriptor(
pointOfInterest: nil,
streetAddress: nil,
neighborhood: "科技园社区",
district: nil,
city: "深圳市",
province: "广东省",
country: "中国"
)
XCTAssertEqual(place.displayName(viewportMeters: 300), "科技园社区")
XCTAssertEqual(place.displayName(viewportMeters: 15_000), "深圳市")
XCTAssertEqual(place.displayName(viewportMeters: 300_000), "深圳市 · 广东省")
}
func testStaleGeocodeCannotReplaceCurrentDescriptor() {
let state = MapLocationState(initialCoordinate: initial)
let staleRevision = state.selection.revision
state.selectUserMapCenter(.init(latitude: 31.23, longitude: 121.47))
let accepted = state.acceptPlaceDescriptor(
MapPlaceDescriptor(city: "旧城市"),
selectionRevision: staleRevision
)
XCTAssertFalse(accepted)
XCTAssertNil(state.placeDescriptor)
}
func testNativeRealtimeUpdateDoesNotMoveCurrentSelection() {
let state = MapLocationState(initialCoordinate: initial)
state.selectSearchResult(.init(latitude: 31.23, longitude: 121.47), name: "外滩")
let revision = state.selection.revision
state.updateRealtimeLocation(CLLocation(latitude: 30.42, longitude: 114.25))
XCTAssertEqual(state.realtimeCoordinate?.latitude ?? 0, 30.42, accuracy: 0.000001)
XCTAssertEqual(state.selection.coordinate.latitude, 31.23, accuracy: 0.000001)
XCTAssertEqual(state.selection.revision, revision)
XCTAssertEqual(state.selection.source, .search)
}
func testRealtimeIntentCanImmediatelyAcceptNativeLocation() {
let state = MapLocationState(initialCoordinate: initial)
let nativeLocation = CLLocation(latitude: 30.42, longitude: 114.25)
state.updateRealtimeLocation(nativeLocation)
let intent = state.beginRealtimeIntent()
XCTAssertTrue(state.acceptRealtimeLocation(nativeLocation.coordinate, intent: intent, focus: true))
XCTAssertEqual(state.selection.source, .realtime)
XCTAssertEqual(state.selection.coordinate.latitude, 30.42, accuracy: 0.000001)
guard case let .focus(coordinate, distance) = state.cameraCommand?.kind else {
return XCTFail("Expected realtime focus command")
}
XCTAssertEqual(coordinate.latitude, 30.42, accuracy: 0.000001)
XCTAssertEqual(distance, 200)
}
func testZoomMathScalesBothAxesInTheSameDirection() {
let span = MKCoordinateSpan(latitudeDelta: 0.2, longitudeDelta: 0.1)
let zoomedIn = MapZoomMath.scaledSpan(span, factor: 0.5)
XCTAssertEqual(zoomedIn.latitudeDelta, 0.1, accuracy: 0.000001)
XCTAssertEqual(zoomedIn.longitudeDelta, 0.05, accuracy: 0.000001)
let zoomedOut = MapZoomMath.scaledSpan(span, factor: 2)
XCTAssertEqual(zoomedOut.latitudeDelta, 0.4, accuracy: 0.000001)
XCTAssertEqual(zoomedOut.longitudeDelta, 0.2, accuracy: 0.000001)
}
func testViewportScaleLabelUsesReadableMetricUnits() {
XCTAssertEqual(MapZoomMath.viewportScaleLabel(distanceMeters: 180), "180 m")
XCTAssertEqual(MapZoomMath.viewportScaleLabel(distanceMeters: 2_500), "2.5 km")
XCTAssertEqual(MapZoomMath.viewportScaleLabel(distanceMeters: 126_000), "126 km")
}
}
@@ -0,0 +1,185 @@
import CoreLocation
import XCTest
@testable import PaopaoLocationSpoofer
@MainActor
final class RealtimeLocationManagerTests: XCTestCase {
func testFreshCachedLocationReturnsImmediatelyWithoutRequestingAgain() async {
let driver = FakeRealtimeLocationDriver()
driver.location = CLLocation(
coordinate: .init(latitude: 30.42, longitude: 114.25),
altitude: 0,
horizontalAccuracy: 12,
verticalAccuracy: 10,
timestamp: Date()
)
let manager = RealtimeLocationManager(driver: driver, oneShotTimeoutNanoseconds: 1_000_000_000)
let coordinate = await manager.requestLocation()
XCTAssertEqual(coordinate?.latitude ?? 0, 30.42, accuracy: 0.000001)
XCTAssertEqual(driver.requestLocationCallCount, 0)
XCTAssertEqual(driver.startUpdatingCallCount, 0)
XCTAssertFalse(manager.isRequesting)
}
func testContinuationIsInstalledBeforeOneShotRequest() async {
let driver = FakeRealtimeLocationDriver()
let manager = RealtimeLocationManager(driver: driver, timeoutNanoseconds: 1_000_000_000)
driver.onRequestLocation = {
driver.emit(CLLocation(latitude: 22.54, longitude: 113.94))
}
let coordinate = await manager.requestLocation()
XCTAssertEqual(coordinate?.latitude ?? 0, 22.54, accuracy: 0.000001)
XCTAssertFalse(manager.isRequesting)
}
func testUndeterminedAuthorizationWaitsBeforeRequestingLocation() async {
let driver = FakeRealtimeLocationDriver()
driver.authorizationStatus = .notDetermined
let manager = RealtimeLocationManager(driver: driver, timeoutNanoseconds: 1_000_000_000)
let request = Task { await manager.requestLocation() }
while !manager.isRequesting { await Task.yield() }
XCTAssertEqual(driver.requestAuthorizationCallCount, 1)
XCTAssertEqual(driver.requestLocationCallCount, 0)
driver.emitAuthorization(.authorizedWhenInUse)
await Task.yield()
XCTAssertEqual(driver.requestLocationCallCount, 1)
driver.emit(CLLocation(latitude: 22.54, longitude: 113.94))
let coordinate = await request.value
XCTAssertEqual(coordinate?.latitude ?? 0, 22.54, accuracy: 0.000001)
}
func testOverlappingRequestIsRejectedWithoutReplacingFirstContinuation() async {
let driver = FakeRealtimeLocationDriver()
let manager = RealtimeLocationManager(driver: driver, timeoutNanoseconds: 1_000_000_000)
let first = Task { await manager.requestLocation() }
while !manager.isRequesting { await Task.yield() }
let second = await manager.requestLocation()
XCTAssertNil(second)
driver.emit(CLLocation(latitude: 31.23, longitude: 121.47))
let firstCoordinate = await first.value
XCTAssertEqual(firstCoordinate?.longitude ?? 0, 121.47, accuracy: 0.000001)
}
func testOneShotTimeoutTransitionsToContinuousFallback() async {
let driver = FakeRealtimeLocationDriver()
let manager = RealtimeLocationManager(driver: driver, timeoutNanoseconds: 5_000_000)
let request = Task { await manager.requestLocation() }
try? await Task.sleep(nanoseconds: 20_000_000)
XCTAssertEqual(driver.startUpdatingCallCount, 1)
driver.emit(CLLocation(latitude: 39.90, longitude: 116.40))
let coordinate = await request.value
XCTAssertEqual(coordinate?.latitude ?? 0, 39.90, accuracy: 0.000001)
XCTAssertEqual(driver.stopUpdatingCallCount, 1)
}
func testInvalidAccuracyCannotCompleteRequest() async {
let driver = FakeRealtimeLocationDriver()
let manager = RealtimeLocationManager(driver: driver, timeoutNanoseconds: 1_000_000_000)
let request = Task { await manager.requestLocation() }
while !manager.isRequesting { await Task.yield() }
driver.emit(CLLocation(
coordinate: .init(latitude: 22.54, longitude: 113.94),
altitude: 0,
horizontalAccuracy: -1,
verticalAccuracy: 10,
timestamp: Date()
))
await Task.yield()
XCTAssertTrue(manager.isRequesting)
XCTAssertNil(manager.location)
driver.emit(CLLocation(latitude: 31.23, longitude: 121.47))
let coordinate = await request.value
XCTAssertEqual(coordinate?.longitude ?? 0, 121.47, accuracy: 0.000001)
}
func testDeniedLocationErrorFinishesWithoutStartingFallback() async {
let driver = FakeRealtimeLocationDriver()
let manager = RealtimeLocationManager(driver: driver, timeoutNanoseconds: 1_000_000_000)
let request = Task { await manager.requestLocation() }
while !manager.isRequesting { await Task.yield() }
driver.emitError(NSError(domain: kCLErrorDomain, code: CLError.denied.rawValue))
let coordinate = await request.value
XCTAssertNil(coordinate)
XCTAssertEqual(driver.startUpdatingCallCount, 0)
XCTAssertFalse(manager.isRequesting)
}
func testOldTimestampCannotCompleteNewRequest() async {
let driver = FakeRealtimeLocationDriver()
let manager = RealtimeLocationManager(driver: driver, timeoutNanoseconds: 1_000_000_000)
let request = Task { await manager.requestLocation() }
while !manager.isRequesting { await Task.yield() }
driver.emit(CLLocation(
coordinate: .init(latitude: 1, longitude: 2),
altitude: 0,
horizontalAccuracy: 10,
verticalAccuracy: 10,
timestamp: Date(timeIntervalSinceNow: -60)
))
await Task.yield()
XCTAssertTrue(manager.isRequesting)
driver.emit(CLLocation(latitude: 22.54, longitude: 113.94))
let coordinate = await request.value
XCTAssertEqual(coordinate?.latitude ?? 0, 22.54, accuracy: 0.000001)
}
}
@MainActor
private final class FakeRealtimeLocationDriver: RealtimeLocationDriving {
var location: CLLocation?
var authorizationStatus: CLAuthorizationStatus = .authorizedWhenInUse
weak var delegate: CLLocationManagerDelegate?
var onRequestLocation: (() -> Void)?
private(set) var requestAuthorizationCallCount = 0
private(set) var requestLocationCallCount = 0
private(set) var startUpdatingCallCount = 0
private(set) var stopUpdatingCallCount = 0
func requestWhenInUseAuthorization() {
requestAuthorizationCallCount += 1
}
func requestLocation() {
requestLocationCallCount += 1
onRequestLocation?()
}
func startUpdatingLocation() {
startUpdatingCallCount += 1
}
func stopUpdatingLocation() {
stopUpdatingCallCount += 1
}
func emitAuthorization(_ status: CLAuthorizationStatus) {
authorizationStatus = status
delegate?.locationManagerDidChangeAuthorization?(CLLocationManager())
}
func emit(_ location: CLLocation) {
delegate?.locationManager?(CLLocationManager(), didUpdateLocations: [location])
}
func emitError(_ error: Error) {
delegate?.locationManager?(CLLocationManager(), didFailWithError: error)
}
}