205 lines
7.1 KiB
Swift
205 lines
7.1 KiB
Swift
//
|
|
// 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
|
|
}
|
|
}
|