diff --git a/Sources/XyoClient/Witness/Location/Generic/Coordinates.swift b/Sources/XyoClient/Witness/Location/Generic/Coordinates.swift new file mode 100644 index 0000000..fe2f17e --- /dev/null +++ b/Sources/XyoClient/Witness/Location/Generic/Coordinates.swift @@ -0,0 +1,53 @@ +import CoreLocation +import Foundation + +struct CoordinatesStruct: Encodable { + + var accuracy: Double? + var altitude: Double? + var altitudeAccuracy: Double? + var heading: Double? + var latitude: Double + var longitude: Double + var speed: Double? + + init( + accuracy: Double?, + altitude: Double?, + altitudeAccuracy: Double?, + heading: Double?, + latitude: Double, + longitude: Double, + speed: Double? + ) { + self.accuracy = accuracy + self.altitude = altitude + self.altitudeAccuracy = altitudeAccuracy + self.heading = heading + self.latitude = latitude + self.longitude = longitude + self.speed = speed + } + + enum CodingKeys: String, CodingKey { + case accuracy + case altitude + case altitudeAccuracy + case heading + case latitude + case longitude + case speed + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(self.accuracy, forKey: .accuracy) + try container.encode(self.altitude, forKey: .altitude) + try container.encode(self.altitudeAccuracy, forKey: .altitudeAccuracy) + try container.encode(self.heading, forKey: .heading) + try container.encode(self.latitude, forKey: .latitude) + try container.encode(self.longitude, forKey: .longitude) + try container.encode(self.speed, forKey: .speed) + } +} diff --git a/Sources/XyoClient/Witness/Location/Generic/CurrentLocation.swift b/Sources/XyoClient/Witness/Location/Generic/CurrentLocation.swift new file mode 100644 index 0000000..626e9a0 --- /dev/null +++ b/Sources/XyoClient/Witness/Location/Generic/CurrentLocation.swift @@ -0,0 +1,28 @@ +import CoreLocation +import Foundation + +struct CurrentLocationStruct: Encodable { + + var coords: CoordinatesStruct + var timestamp: Date + + init( + coords: CoordinatesStruct, + timestamp: Date + ) { + self.coords = coords + self.timestamp = timestamp + } + + enum CodingKeys: String, CodingKey { + case coords + case timestamp + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(self.coords, forKey: .coords) + try container.encode(Int(self.timestamp.timeIntervalSince1970 * 1000), forKey: .timestamp) + } +} diff --git a/Sources/XyoClient/Witness/Location/Generic/LocationPayload.swift b/Sources/XyoClient/Witness/Location/Generic/LocationPayload.swift new file mode 100644 index 0000000..5548e04 --- /dev/null +++ b/Sources/XyoClient/Witness/Location/Generic/LocationPayload.swift @@ -0,0 +1,37 @@ +import CoreLocation + +open class LocationPayload: Payload { + + public static let schema: String = "network.xyo.location" + + var location: CLLocation + + public init(_ location: CLLocation) { + self.location = location + super.init(LocationPayload.schema) + } + + enum CodingKeys: String, CodingKey { + case currentLocation + case schema + } + + override open func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.schema, forKey: .schema) + + let coords: CoordinatesStruct = CoordinatesStruct( + accuracy: self.location.horizontalAccuracy, + altitude: self.location.altitude, + altitudeAccuracy: self.location.altitude, + heading: self.location.course, + latitude: self.location.coordinate.latitude, + longitude: self.location.coordinate.longitude, + speed: self.location.speed + ) + let timestamp = self.location.timestamp + let currentLocation: CurrentLocationStruct = CurrentLocationStruct( + coords: coords, timestamp: timestamp) + try container.encode(currentLocation, forKey: .currentLocation) + } +} diff --git a/Sources/XyoClient/Witness/Location/LocationService.swift b/Sources/XyoClient/Witness/Location/LocationService.swift index 3308ed3..50a406b 100644 --- a/Sources/XyoClient/Witness/Location/LocationService.swift +++ b/Sources/XyoClient/Witness/Location/LocationService.swift @@ -1,7 +1,7 @@ import CoreLocation import Foundation -public class LocationService: NSObject, CLLocationManagerDelegate { +public class LocationService: NSObject, CLLocationManagerDelegate, LocationServiceProtocol { private let locationManager = CLLocationManager() private var locationCompletion: ((Result) -> Void)? diff --git a/Sources/XyoClient/Witness/Location/LocationServiceProtocol.swift b/Sources/XyoClient/Witness/Location/LocationServiceProtocol.swift new file mode 100644 index 0000000..d346554 --- /dev/null +++ b/Sources/XyoClient/Witness/Location/LocationServiceProtocol.swift @@ -0,0 +1,7 @@ +import CoreLocation +import Foundation + +protocol LocationServiceProtocol { + func requestAuthorization() + func requestLocation(completion: @escaping (Result) -> Void) +} diff --git a/Sources/XyoClient/Witness/Location/LocationWitness.swift b/Sources/XyoClient/Witness/Location/LocationWitness.swift index 6bd4584..875879e 100644 --- a/Sources/XyoClient/Witness/Location/LocationWitness.swift +++ b/Sources/XyoClient/Witness/Location/LocationWitness.swift @@ -2,7 +2,26 @@ import CoreLocation import Foundation open class LocationWitness: WitnessModuleAsync { - private let locationService = LocationService() + private var _locationService: LocationServiceProtocol? + + private var locationService: LocationServiceProtocol { + if let service = _locationService { + return service + } else { + let initialized = LocationService() + self._locationService = initialized + return initialized + } + } + + override init(account: AccountInstance? = nil) { + super.init(account: account) + } + + convenience init(locationService: LocationServiceProtocol) { + self.init(account: nil) + self._locationService = locationService + } override open func observe(completion: @escaping ([Payload]?, Error?) -> Void) { locationService.requestAuthorization() @@ -10,8 +29,9 @@ open class LocationWitness: WitnessModuleAsync { DispatchQueue.main.async { switch result { case .success(let location): - let payload = LocationPayload(location) - completion([payload], nil) + let iosLocationPayload = IosLocationPayload(location) + let locationPayload = LocationPayload(location) + completion([iosLocationPayload, locationPayload], nil) case .failure(let error): completion(nil, error) } diff --git a/Sources/XyoClient/Witness/Location/LocationCoordinatePayloadStruct.swift b/Sources/XyoClient/Witness/Location/iOS/IosLocationCoordinatePayloadStruct.swift similarity index 90% rename from Sources/XyoClient/Witness/Location/LocationCoordinatePayloadStruct.swift rename to Sources/XyoClient/Witness/Location/iOS/IosLocationCoordinatePayloadStruct.swift index e1d8e54..c828828 100644 --- a/Sources/XyoClient/Witness/Location/LocationCoordinatePayloadStruct.swift +++ b/Sources/XyoClient/Witness/Location/iOS/IosLocationCoordinatePayloadStruct.swift @@ -1,7 +1,7 @@ import CoreLocation import Foundation -struct LocationCoordinatePayloadStruct: Encodable { +struct IosLocationCoordinatePayloadStruct: Encodable { var coordinate: CLLocationCoordinate2D diff --git a/Sources/XyoClient/Witness/Location/LocationFloorPayloadStruct.swift b/Sources/XyoClient/Witness/Location/iOS/IosLocationFloorPayloadStruct.swift similarity index 88% rename from Sources/XyoClient/Witness/Location/LocationFloorPayloadStruct.swift rename to Sources/XyoClient/Witness/Location/iOS/IosLocationFloorPayloadStruct.swift index 19aee5a..9b21a99 100644 --- a/Sources/XyoClient/Witness/Location/LocationFloorPayloadStruct.swift +++ b/Sources/XyoClient/Witness/Location/iOS/IosLocationFloorPayloadStruct.swift @@ -1,7 +1,7 @@ import CoreLocation import Foundation -struct LocationFloorPayloadStruct: Encodable { +struct IosLocationFloorPayloadStruct: Encodable { var floor: CLFloor diff --git a/Sources/XyoClient/Witness/Location/LocationPayload.swift b/Sources/XyoClient/Witness/Location/iOS/IosLocationPayload.swift similarity index 82% rename from Sources/XyoClient/Witness/Location/LocationPayload.swift rename to Sources/XyoClient/Witness/Location/iOS/IosLocationPayload.swift index 223d281..ba044b0 100644 --- a/Sources/XyoClient/Witness/Location/LocationPayload.swift +++ b/Sources/XyoClient/Witness/Location/iOS/IosLocationPayload.swift @@ -1,12 +1,14 @@ import CoreLocation -open class LocationPayload: Payload { +open class IosLocationPayload: Payload { + + public static let schema: String = "network.xyo.location.ios" var location: CLLocation public init(_ location: CLLocation) { self.location = location - super.init("network.xyo.location.ios") + super.init(IosLocationPayload.schema) } enum CodingKeys: String, CodingKey { @@ -31,7 +33,7 @@ open class LocationPayload: Payload { try container.encode(self.location.altitude, forKey: .altitude) try container.encode( - LocationCoordinatePayloadStruct(self.location.coordinate), forKey: .coordinate) + IosLocationCoordinatePayloadStruct(self.location.coordinate), forKey: .coordinate) try container.encode(self.location.course, forKey: .course) if #available(iOS 13.4, *) { try container.encode(self.location.courseAccuracy, forKey: .courseAccuracy) @@ -41,13 +43,13 @@ open class LocationPayload: Payload { } if let floor = self.location.floor { try container.encode( - LocationFloorPayloadStruct(floor), forKey: .floor) + IosLocationFloorPayloadStruct(floor), forKey: .floor) } try container.encode(self.location.horizontalAccuracy, forKey: .horizontalAccuracy) if #available(iOS 15.0, *) { if let sourceInformation = self.location.sourceInformation { try container.encode( - LocationSourceInformationPayloadStruct(sourceInformation), + IosLocationSourceInformationPayloadStruct(sourceInformation), forKey: .sourceInformation) } } diff --git a/Sources/XyoClient/Witness/Location/LocationSourceInformationPayloadStruct.swift b/Sources/XyoClient/Witness/Location/iOS/IosLocationSourceInformationPayloadStruct.swift similarity index 92% rename from Sources/XyoClient/Witness/Location/LocationSourceInformationPayloadStruct.swift rename to Sources/XyoClient/Witness/Location/iOS/IosLocationSourceInformationPayloadStruct.swift index 0d4af13..ec4c604 100644 --- a/Sources/XyoClient/Witness/Location/LocationSourceInformationPayloadStruct.swift +++ b/Sources/XyoClient/Witness/Location/iOS/IosLocationSourceInformationPayloadStruct.swift @@ -2,7 +2,7 @@ import CoreLocation import Foundation @available(iOS 15.0, *) -struct LocationSourceInformationPayloadStruct: Encodable { +struct IosLocationSourceInformationPayloadStruct: Encodable { var sourceInformation: CLLocationSourceInformation diff --git a/Tests/XyoClientTests/AccountServices.swift b/Tests/XyoClientTests/AccountServices.swift index 426e529..0001a3b 100644 --- a/Tests/XyoClientTests/AccountServices.swift +++ b/Tests/XyoClientTests/AccountServices.swift @@ -17,7 +17,7 @@ class AccountServicesTests: XCTestCase { func testGetNamedAccount_CreatesAndReturnsNewAccount_WhenNoExistingAccount() { // Act - let account = AccountServices.getNamedAccount(name: "testAccount") + let account = AccountServices.getNamedAccount(name: "testAccount1") // Assert XCTAssertNotNil(account) @@ -27,9 +27,9 @@ class AccountServicesTests: XCTestCase { func testGetNamedAccount_ReturnsExistingAccount_WhenAccountExists() { // Act // Initial attempt create account - let accountA = AccountServices.getNamedAccount(name: "testAccount") + let accountA = AccountServices.getNamedAccount(name: "testAccount2") // Subsequent ones retrieve account - let accountB = AccountServices.getNamedAccount(name: "testAccount") + let accountB = AccountServices.getNamedAccount(name: "testAccount2") // Asserts let addressA = accountA.address diff --git a/Tests/XyoClientTests/CoreDataPreviousHashStoreTests.swift b/Tests/XyoClientTests/CoreDataPreviousHashStoreTests.swift index 05180cf..832235b 100644 --- a/Tests/XyoClientTests/CoreDataPreviousHashStoreTests.swift +++ b/Tests/XyoClientTests/CoreDataPreviousHashStoreTests.swift @@ -22,8 +22,8 @@ final class CoreDataPreviousHashStoreTests: XCTestCase { } func testSetItemAndGetItem() { - let address = "0x123456" - let hash = "0xabcdef" + let address = "2d0fb5708b9d68bfaa96c6e426cbc66a341f117d" + let hash = "fb2b2ed349278d35b2cf32ec719227cf2a0f099f3a3305307bce15362eca32b9" // Set an item store.setItem(address: address, previousHash: hash) @@ -34,8 +34,8 @@ final class CoreDataPreviousHashStoreTests: XCTestCase { } func testRemoveItem() { - let address = "0x123456" - let hash = "0xabcdef" + let address = "f90b9ad30ea94d3df17d51c727c416b46faf18b6" + let hash = "8a76ed3fa2507859e43f24ea0e6c03acb1782281429294bb8123b6d9e73f1710" // Set an item store.setItem(address: address, previousHash: hash) @@ -49,9 +49,9 @@ final class CoreDataPreviousHashStoreTests: XCTestCase { } func testUpdateItem() { - let address = "0x123456" - let initialHash = "0xabcdef" - let updatedHash = "0x123abc" + let address = "85e7a0494c1feb184a80d64aca7bef07d8efd960" + let initialHash = "6c509659288d86d4961906299692239d40e5e3a8834ab89a473d9031e50703e0" + let updatedHash = "c2842590d989afae0bf2970b31d0323f97fe68c71a1c9d13bf275bbed13cf92c" // Set an initial item store.setItem(address: address, previousHash: initialHash) @@ -65,7 +65,7 @@ final class CoreDataPreviousHashStoreTests: XCTestCase { } func testGetItemNonExistentAddress() { - let address = "0x789abc" + let address = "f8ede235dbc41c06936d46a26d9038a58ba254a1" // Try to get an item that doesn't exist let retrievedHash = store.getItem(address: address) diff --git a/Tests/XyoClientTests/Witness/LocationWitness.swift b/Tests/XyoClientTests/Witness/LocationWitness.swift new file mode 100644 index 0000000..27a5fc3 --- /dev/null +++ b/Tests/XyoClientTests/Witness/LocationWitness.swift @@ -0,0 +1,51 @@ +import CoreLocation +import XCTest + +@testable import XyoClient + +private class MockLocationService: LocationServiceProtocol { + var didRequestAuthorization = false + var simulatedResult: Result? + + func requestAuthorization() { + didRequestAuthorization = true + } + + func requestLocation(completion: @escaping (Result) -> Void) { + if let result = simulatedResult { + completion(result) + } + } +} + +@available(iOS 13.0, *) +final class LocationWitnessTests: XCTestCase { + static var allTests = [ + ( + "observe:returnsMultipleLocationPayloads", + testLocationWitness_observe_returnsMultipleLocationPayloads + ) + ] + + @available(iOS 15, *) + func testLocationWitness_observe_returnsMultipleLocationPayloads() async throws { + let locationServiceMock = MockLocationService() + let lattitiude: Double = 1 + let longitude: Double = 2 + locationServiceMock.simulatedResult = .success(CLLocation(latitude: lattitiude, longitude: longitude)) + let sut = LocationWitness(locationService: locationServiceMock) + let results = try await sut.observe() + XCTAssertEqual(results.count, 2) + let locationPayload = try XCTUnwrap( + results.compactMap { $0 as? LocationPayload }.first, "Missing location payload.") + XCTAssertEqual(locationPayload.schema, LocationPayload.schema) + XCTAssertEqual(locationPayload.location.coordinate.latitude, lattitiude) + XCTAssertEqual(locationPayload.location.coordinate.longitude, longitude) + let iosLocationPayload = try XCTUnwrap( + results.compactMap { $0 as? IosLocationPayload }.first, "Missing iOS location payload.") + XCTAssertEqual(iosLocationPayload.schema, IosLocationPayload.schema) + XCTAssertEqual(iosLocationPayload.location.coordinate.latitude, lattitiude) + XCTAssertEqual(iosLocationPayload.location.coordinate.longitude, longitude) + + } +}