Javier Cicchelli 9bcdaa697b [Setup] Basic project structure (#1)
This PR contains all the work related to setting up this project as required to implement the [Assignment](https://repo.rock-n-code.com/rock-n-code/deep-linking-assignment/wiki/Assignment) on top, as intended.

To summarise this work:
- [x] created a new **Xcode** project;
- [x] cloned the `Wikipedia` app and inserted it into the **Xcode** project;
- [x] created the `Locations` app and also, its `Libraries` package;
- [x] created the `Shared` package to share dependencies between the apps;
- [x] added a `Makefile` file and implemented some **environment** and **help** commands.

Co-authored-by: Javier Cicchelli <javier@rock-n-code.com>
Reviewed-on: rock-n-code/deep-linking-assignment#1
2023-04-08 18:37:13 +00:00

319 lines
13 KiB
Swift

import XCTest
@testable import WMF
final class LocationManagerTests: XCTestCase {
private var mockCLLocationManager: MockCLLocationManager!
private var mockDevice: MockUIDevice!
private var locationManager: LocationManager!
private var delegate: TestLocationManagerDelegate!
override func setUp() {
super.setUp()
mockCLLocationManager = MockCLLocationManager()
mockCLLocationManager.simulate(authorizationStatus: .authorizedAlways)
mockDevice = MockUIDevice(orientation: .unknown)
locationManager = LocationManager(
locationManager: mockCLLocationManager,
device: mockDevice
)
delegate = TestLocationManagerDelegate()
locationManager.delegate = delegate
}
// MARK: - LocationManager tests
func testFineLocationManager() {
let clLocationManager = CLLocationManager()
_ = LocationManager(locationManager: clLocationManager, configuration: .fine)
XCTAssertEqual(clLocationManager.distanceFilter, 1)
XCTAssertEqual(clLocationManager.desiredAccuracy, kCLLocationAccuracyBest)
XCTAssertEqual(clLocationManager.activityType, .fitness)
}
func testCoarseLocationManager() {
let clLocationManager = CLLocationManager()
_ = LocationManager(locationManager: clLocationManager, configuration: .coarse)
XCTAssertEqual(clLocationManager.distanceFilter, 1000)
XCTAssertEqual(clLocationManager.desiredAccuracy, kCLLocationAccuracyKilometer)
XCTAssertEqual(clLocationManager.activityType, .fitness)
}
func testStartMonitoring() {
locationManager.startMonitoringLocation()
XCTAssertEqual(locationManager.isUpdating, true)
XCTAssertEqual(mockCLLocationManager.isUpdatingLocation, true)
XCTAssertEqual(mockCLLocationManager.isUpdatingHeading, true)
}
func testStartLocationWithoutPermission() {
mockCLLocationManager.simulate(authorizationStatus: .denied)
locationManager.startMonitoringLocation()
XCTAssertEqual(locationManager.isUpdating, false)
XCTAssertEqual(mockCLLocationManager.isUpdatingLocation, false)
XCTAssertEqual(mockCLLocationManager.isUpdatingHeading, false)
mockCLLocationManager.simulate(authorizationStatus: .restricted)
locationManager.startMonitoringLocation()
XCTAssertEqual(locationManager.isUpdating, false)
XCTAssertEqual(mockCLLocationManager.isUpdatingLocation, false)
XCTAssertEqual(mockCLLocationManager.isUpdatingHeading, false)
}
func testStopMonitoring() {
locationManager.startMonitoringLocation()
locationManager.stopMonitoringLocation()
XCTAssertEqual(locationManager.isUpdating, false)
XCTAssertEqual(mockCLLocationManager.isUpdatingLocation, false)
XCTAssertEqual(mockCLLocationManager.isUpdatingHeading, false)
}
// MARK: - Authorization
func testStartMonitoringCalledWhenAuthorizationSuccessfull() {
mockCLLocationManager.simulate(authorizationStatus: .notDetermined)
locationManager.startMonitoringLocation()
XCTAssertEqual(mockCLLocationManager.isRequestedForAuthorization, true)
XCTAssertEqual(locationManager.isUpdating, false)
XCTAssertEqual(mockCLLocationManager.isUpdatingLocation, false)
XCTAssertEqual(mockCLLocationManager.isUpdatingHeading, false)
// Simulate the user allowing Location Services.
mockCLLocationManager.simulate(authorizationStatus: .authorizedAlways)
XCTAssertEqual(locationManager.isUpdating, true)
XCTAssertEqual(mockCLLocationManager.isUpdatingLocation, true)
XCTAssertEqual(mockCLLocationManager.isUpdatingHeading, true)
}
func testStartMonitoringCalledWhenAuthorizationDeniedAndThenAuthorized() {
mockCLLocationManager.simulate(authorizationStatus: .denied)
locationManager.startMonitoringLocation()
XCTAssertEqual(locationManager.isUpdating, false)
XCTAssertEqual(mockCLLocationManager.isUpdatingLocation, false)
XCTAssertEqual(mockCLLocationManager.isUpdatingHeading, false)
// Simulate the user allowing Location Services.
mockCLLocationManager.simulate(authorizationStatus: .authorizedAlways)
XCTAssertEqual(locationManager.isUpdating, true)
XCTAssertEqual(mockCLLocationManager.isUpdatingLocation, true)
XCTAssertEqual(mockCLLocationManager.isUpdatingHeading, true)
}
func testAuthorizedStatus() {
// Test authorizedAlways status.
mockCLLocationManager.simulate(authorizationStatus: .authorizedAlways)
XCTAssertTrue(locationManager.authorizationStatus.isAuthorized)
// Test when in use status.
mockCLLocationManager.simulate(authorizationStatus: .authorizedWhenInUse)
XCTAssertEqual(locationManager.authorizationStatus, .authorizedWhenInUse)
XCTAssertTrue(locationManager.authorizationStatus.isAuthorized)
// Test notDetermined status.
mockCLLocationManager.simulate(authorizationStatus: .notDetermined)
XCTAssertEqual(locationManager.authorizationStatus, .notDetermined)
XCTAssertFalse(locationManager.authorizationStatus.isAuthorized)
// Test denied status.
mockCLLocationManager.simulate(authorizationStatus: .denied)
XCTAssertEqual(locationManager.authorizationStatus, .denied)
XCTAssertFalse(locationManager.authorizationStatus.isAuthorized)
// Test restricted status.
mockCLLocationManager.simulate(authorizationStatus: .restricted)
XCTAssertEqual(locationManager.authorizationStatus, .restricted)
XCTAssertFalse(locationManager.authorizationStatus.isAuthorized)
}
// MARK: - LocationManagerDelegate tests
func testUpdateLocation() {
locationManager.startMonitoringLocation()
let location = CLLocation(latitude: 10, longitude: 20)
mockCLLocationManager.simulateUpdate(location: location)
XCTAssertEqual(locationManager.location, location)
XCTAssertEqual(delegate.location, location)
}
func testExistingLocationIsUsedWhenAvailable() {
// When the location has already been fetched, new instances of CLLocationManager contain
// the last known location in their `location` variable even before `startUpdatingLocation()`
// is called.
let location = CLLocation(latitude: 10, longitude: 20)
mockCLLocationManager.simulateUpdate(location: location)
let locationManager = LocationManager(locationManager: mockCLLocationManager)
// `locationManager.startMonitoringLocation()` is not called.
XCTAssertEqual(locationManager.location, location)
}
func testUpdateHeading() {
locationManager.startMonitoringLocation()
let heading = MockCLHeading(headingAccuracy: 10)
mockCLLocationManager.simulateUpdate(heading: heading)
XCTAssertEqual(locationManager.heading, heading)
XCTAssertEqual(delegate.heading, heading)
}
func testExistingHeadingIsUsedWhenAvailable() {
// When the heading has already been fetched, new instances of CLLocationManager contain
// the last known heading in their `heading` variable even before `startUpdatingHeading()`
// is called.
let heading = MockCLHeading(headingAccuracy: 10)
mockCLLocationManager.simulateUpdate(heading: heading)
let locationManager = LocationManager(locationManager: mockCLLocationManager)
// `locationManager.startMonitoringLocation()` is not called.
XCTAssertEqual(locationManager.heading, heading)
}
func testStopUpdating() {
locationManager.startMonitoringLocation()
// Simulate the values are updated while monitoring.
let location1 = CLLocation(latitude: 10, longitude: 20)
mockCLLocationManager.simulateUpdate(location: location1)
let heading1 = MockCLHeading(headingAccuracy: 10)
mockCLLocationManager.simulateUpdate(heading: heading1)
locationManager.stopMonitoringLocation()
// Simulate the values are updated while not monitoring.
let location2 = CLLocation(latitude: 100, longitude: 200)
mockCLLocationManager.simulateUpdate(location: location2)
let heading2 = MockCLHeading(headingAccuracy: 100)
mockCLLocationManager.simulateUpdate(heading: heading2)
// Check the values are not updated.
XCTAssertEqual(locationManager.heading, heading1)
XCTAssertEqual(delegate.heading, heading1)
XCTAssertEqual(locationManager.location, location1)
XCTAssertEqual(delegate.location, location1)
// Check the error is not propagated when the monitoring is stopped.
let error = NSError(domain: "org.wikimedia.wikipedia.test", code: -1, userInfo: nil)
mockCLLocationManager.simulate(error: error)
XCTAssertNil(delegate.error)
}
func testReceivingError() {
locationManager.startMonitoringLocation()
let error = NSError(domain: "org.wikimedia.wikipedia.test", code: -1, userInfo: nil)
mockCLLocationManager.simulate(error: error)
XCTAssertEqual((delegate.error as NSError?), error)
}
func testAuthorizedStateChangesArePropagated() {
mockCLLocationManager.simulate(authorizationStatus: .denied)
XCTAssertEqual(delegate.authorized, false)
mockCLLocationManager.simulate(authorizationStatus: .authorizedAlways)
XCTAssertEqual(delegate.authorized, true)
}
// MARK: - Test heading
func testDeviceHeadingUpdates() {
locationManager.startMonitoringLocation()
mockDevice.simulateUpdate(orientation: .portrait)
XCTAssertEqual(mockCLLocationManager.headingOrientation, .portrait)
mockDevice.simulateUpdate(orientation: .landscapeLeft)
XCTAssertEqual(mockCLLocationManager.headingOrientation, .landscapeLeft)
// The device orientation updates should not be propagated when the monitoring is stopped.
locationManager.stopMonitoringLocation()
mockDevice.simulateUpdate(orientation: .portrait)
XCTAssertNotEqual(mockCLLocationManager.headingOrientation, .portrait)
}
func test_UIDevice_BeingEndGeneratingDeviceOrientation_IsCalled() {
locationManager.startMonitoringLocation()
XCTAssertEqual(mockDevice.beginGeneratingDeviceOrientationCount, 1)
XCTAssertEqual(mockDevice.endGeneratingDeviceOrientationCount, 0)
// Verify `startMonitoringLocation()` is idempotent.
locationManager.startMonitoringLocation()
locationManager.startMonitoringLocation()
locationManager.startMonitoringLocation()
XCTAssertEqual(mockDevice.beginGeneratingDeviceOrientationCount, 1)
XCTAssertEqual(mockDevice.endGeneratingDeviceOrientationCount, 0)
locationManager.stopMonitoringLocation()
XCTAssertEqual(mockDevice.beginGeneratingDeviceOrientationCount, 1)
XCTAssertEqual(mockDevice.endGeneratingDeviceOrientationCount, 1)
// Verify `stopMonitoringLocation()` is idempotent.
locationManager.stopMonitoringLocation()
locationManager.stopMonitoringLocation()
locationManager.stopMonitoringLocation()
XCTAssertEqual(mockDevice.beginGeneratingDeviceOrientationCount, 1)
XCTAssertEqual(mockDevice.endGeneratingDeviceOrientationCount, 1)
}
func testMonitoringStopsWhenDeallocated() {
// Start the monitoring first
locationManager.startMonitoringLocation()
XCTAssertEqual(mockCLLocationManager.isUpdatingLocation, true)
XCTAssertEqual(mockCLLocationManager.isUpdatingHeading, true)
mockDevice.simulateUpdate(orientation: .portrait)
XCTAssertEqual(mockCLLocationManager.headingOrientation, .portrait)
// Deallocate
locationManager = nil
XCTAssertEqual(mockCLLocationManager.isUpdatingLocation, false)
XCTAssertEqual(mockCLLocationManager.isUpdatingHeading, false)
// The device orientation updates should not be propagated
// when `locationManager` is deallocated.
mockDevice.simulateUpdate(orientation: .landscapeLeft)
XCTAssertNotEqual(mockCLLocationManager.headingOrientation, .landscapeLeft)
}
}
/// A test implementation of `LocationManagerDelegate`.
private final class TestLocationManagerDelegate: LocationManagerDelegate {
private(set) var heading: CLHeading?
private(set) var location: CLLocation?
private(set) var error: Error?
private(set) var authorized: Bool?
func locationManager(_ locationManager: LocationManagerProtocol, didReceive error: Error) {
self.error = error
}
func locationManager(_ locationManager: LocationManagerProtocol, didUpdate heading: CLHeading) {
self.heading = heading
}
func locationManager(_ locationManager: LocationManagerProtocol, didUpdate location: CLLocation) {
self.location = location
}
func locationManager(_ locationManager: LocationManagerProtocol, didUpdateAuthorized authorized: Bool) {
self.authorized = authorized
}
}