// // CoreLocationProviderTests.swift // suixinkanTests // import CoreLocation import XCTest @testable import suixinkan /// Core Location fallback 的授权、定位与逆地理编码测试。 @MainActor final class CoreLocationProviderTests: XCTestCase { func testPrivacyAgreementIsRequiredBeforeLocation() async { let context = makeContext(privacyAccepted: false) await assertLocationError(.privacyNotAccepted) { _ = try await context.provider.requestCoordinate() } XCTAssertEqual(context.manager.requestLocationCallCount, 0) } func testDeniedAuthorizationReturnsPermissionError() async { let context = makeContext(authorizationStatus: .denied) await assertLocationError(.permissionDenied) { _ = try await context.provider.requestCoordinate() } XCTAssertEqual(context.manager.requestLocationCallCount, 0) } func testNotDeterminedAuthorizationContinuesAfterGrant() async throws { let coordinate = CLLocationCoordinate2D(latitude: 30.2741, longitude: 120.1551) let context = makeContext( authorizationStatus: .notDetermined, requestedAuthorizationStatus: .authorizedWhenInUse, locationResult: .success(CLLocation( latitude: coordinate.latitude, longitude: coordinate.longitude )) ) let result = try await context.provider.requestCoordinate( desiredAccuracy: kCLLocationAccuracyHundredMeters ) XCTAssertEqual(context.manager.requestAuthorizationCallCount, 1) XCTAssertEqual(context.manager.requestLocationCallCount, 1) XCTAssertEqual(context.manager.desiredAccuracy, kCLLocationAccuracyHundredMeters) XCTAssertEqual(result.latitude, coordinate.latitude, accuracy: 0.000_001) XCTAssertEqual(result.longitude, coordinate.longitude, accuracy: 0.000_001) } func testLocationFailureUsesExistingBusinessError() async { let context = makeContext(locationResult: .failure) await assertLocationError(.locationFailed) { _ = try await context.provider.requestCoordinate() } } func testSnapshotContainsReverseGeocodedAddress() async throws { let context = makeContext( locationResult: .success(CLLocation(latitude: 31.2304, longitude: 121.4737)), geocodedAddress: "上海市黄浦区" ) let snapshot = try await context.provider.requestSnapshot() XCTAssertEqual(snapshot.latitude, 31.2304, accuracy: 0.000_001) XCTAssertEqual(snapshot.longitude, 121.4737, accuracy: 0.000_001) XCTAssertEqual(snapshot.address, "上海市黄浦区") XCTAssertEqual(context.geocoder.requestedCoordinates.count, 1) } func testSnapshotFallsBackToEmptyAddressWhenGeocoderHasNoResult() async throws { let context = makeContext( locationResult: .success(CLLocation(latitude: 39.9042, longitude: 116.4074)), geocodedAddress: "" ) let snapshot = try await context.provider.requestSnapshot() XCTAssertEqual(snapshot.address, "") } private func makeContext( privacyAccepted: Bool = true, authorizationStatus: CLAuthorizationStatus = .authorizedWhenInUse, requestedAuthorizationStatus: CLAuthorizationStatus = .authorizedWhenInUse, locationResult: FakeCoreLocationManager.LocationResult = .success( CLLocation(latitude: 39.9042, longitude: 116.4074) ), geocodedAddress: String = "北京市东城区" ) -> TestContext { let suiteName = "CoreLocationProviderTests.\(UUID().uuidString)" let defaults = UserDefaults(suiteName: suiteName)! defaults.removePersistentDomain(forName: suiteName) let appStore = AppStore(defaults: defaults) appStore.session.privacyAgreementAccepted = privacyAccepted let manager = FakeCoreLocationManager( authorizationStatus: authorizationStatus, requestedAuthorizationStatus: requestedAuthorizationStatus, locationResult: locationResult ) let geocoder = FakeLocationAddressGeocoder(address: geocodedAddress) let provider = CoreLocationProvider( appStore: appStore, manager: manager, geocoder: geocoder ) return TestContext(provider: provider, manager: manager, geocoder: geocoder) } private func assertLocationError( _ expected: LocationProviderError, operation: () async throws -> Void ) async { do { try await operation() XCTFail("Expected \(expected)") } catch { XCTAssertEqual(error as? LocationProviderError, expected) } } } /// Core Location fallback 测试依赖集合。 @MainActor private struct TestContext { let provider: CoreLocationProvider let manager: FakeCoreLocationManager let geocoder: FakeLocationAddressGeocoder } /// 可控的 `CLLocationManager` 替身。 @MainActor private final class FakeCoreLocationManager: CoreLocationManaging { /// 单次定位请求的模拟结果。 enum LocationResult { case success(CLLocation) case failure } var authorizationStatus: CLAuthorizationStatus var desiredAccuracy: CLLocationAccuracy = kCLLocationAccuracyBest weak var delegate: CLLocationManagerDelegate? private(set) var requestAuthorizationCallCount = 0 private(set) var requestLocationCallCount = 0 private let requestedAuthorizationStatus: CLAuthorizationStatus private let locationResult: LocationResult init( authorizationStatus: CLAuthorizationStatus, requestedAuthorizationStatus: CLAuthorizationStatus, locationResult: LocationResult ) { self.authorizationStatus = authorizationStatus self.requestedAuthorizationStatus = requestedAuthorizationStatus self.locationResult = locationResult } func requestWhenInUseAuthorization() { requestAuthorizationCallCount += 1 authorizationStatus = requestedAuthorizationStatus delegate?.locationManagerDidChangeAuthorization?(CLLocationManager()) } func requestLocation() { requestLocationCallCount += 1 let callbackManager = CLLocationManager() switch locationResult { case let .success(location): delegate?.locationManager?(callbackManager, didUpdateLocations: [location]) case .failure: delegate?.locationManager?( callbackManager, didFailWithError: CLError(.locationUnknown) ) } } } /// 可控的逆地理编码替身。 @MainActor private final class FakeLocationAddressGeocoder: LocationAddressGeocoding { private(set) var requestedCoordinates: [CLLocationCoordinate2D] = [] private let address: String init(address: String) { self.address = address } func reverseGeocode(latitude: Double, longitude: Double) async -> String { requestedCoordinates.append(CLLocationCoordinate2D( latitude: latitude, longitude: longitude )) return address } }