Adding VehicleService implementation and tests

This commit is contained in:
Selim Mustafaev 2025-01-26 00:25:35 +03:00
parent 3cc2ef2101
commit ff82b4b755
25 changed files with 796 additions and 63 deletions

View File

@ -64,6 +64,7 @@
7A4927D52CCE438600851C01 /* OptionalBinding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A4927D42CCE438600851C01 /* OptionalBinding.swift */; };
7A530B7A24001D3300CBFE6E /* CheckController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A530B7924001D3300CBFE6E /* CheckController.swift */; };
7A530B7E24017FEE00CBFE6E /* VehicleCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A530B7D24017FEE00CBFE6E /* VehicleCell.swift */; };
7A54BFD32D43B95E00176D6D /* DbUpdatePolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A54BFD22D43B95E00176D6D /* DbUpdatePolicy.swift */; };
7A599C362C18AC7F00D47C18 /* ApiError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A599C352C18AC7F00D47C18 /* ApiError.swift */; };
7A599C392C18B22900D47C18 /* FbRefreshTokenModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A599C382C18B22900D47C18 /* FbRefreshTokenModel.swift */; };
7A599C3B2C18B36A00D47C18 /* FbVerifyTokenModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A599C3A2C18B36A00D47C18 /* FbVerifyTokenModel.swift */; };
@ -334,6 +335,7 @@
7A530B7924001D3300CBFE6E /* CheckController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckController.swift; sourceTree = "<group>"; };
7A530B7D24017FEE00CBFE6E /* VehicleCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleCell.swift; sourceTree = "<group>"; };
7A530B7F2401803A00CBFE6E /* Vehicle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Vehicle.swift; sourceTree = "<group>"; };
7A54BFD22D43B95E00176D6D /* DbUpdatePolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DbUpdatePolicy.swift; sourceTree = "<group>"; };
7A599C352C18AC7F00D47C18 /* ApiError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiError.swift; sourceTree = "<group>"; };
7A599C382C18B22900D47C18 /* FbRefreshTokenModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FbRefreshTokenModel.swift; sourceTree = "<group>"; };
7A599C3A2C18B36A00D47C18 /* FbVerifyTokenModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FbVerifyTokenModel.swift; sourceTree = "<group>"; };
@ -468,9 +470,21 @@
7AFBE8CD2C308B53003C491D /* ACMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ACMessageView.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
7A54BFD52D43D7E100176D6D /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
Extensions/TestError.swift,
"Extensions/VehicleDto+Presets.swift",
"Extensions/VehicleEventDto+Presets.swift",
);
target = 7A2E6FA22C42B3AD00C40DA7 /* AutoCatCoreTests */;
};
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
7A2E6FA42C42B3AD00C40DA7 /* AutoCatCoreTests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = AutoCatCoreTests; sourceTree = "<group>"; };
7AB587232C42D27F00FA7B66 /* AutoCatTests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = AutoCatTests; sourceTree = "<group>"; };
7AB587232C42D27F00FA7B66 /* AutoCatTests */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (7A54BFD52D43D7E100176D6D /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = AutoCatTests; sourceTree = "<group>"; };
/* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */
@ -973,6 +987,7 @@
7A45FB372C27073700618694 /* StorageService.swift */,
7AB587332C42D3FA00FA7B66 /* StorageService+Notes.swift */,
7AA514DF2D0B75B3001CAC50 /* StorageService+Events.swift */,
7A54BFD22D43B95E00176D6D /* DbUpdatePolicy.swift */,
);
path = StorageService;
sourceTree = "<group>";
@ -1479,6 +1494,7 @@
7A6B65B32CFB0DB500AABA6B /* NullifyDate.swift in Sources */,
7A7097C22C9EC139007CFDCA /* ServiceContainer.swift in Sources */,
7A7097C62C9EC77A007CFDCA /* ServicePropertyWrapper.swift in Sources */,
7A54BFD32D43B95E00176D6D /* DbUpdatePolicy.swift in Sources */,
7A5D84BE2C1AE44700C2209B /* VehiclePhoto.swift in Sources */,
7A64A2262C1A32C800284124 /* AudioRecordDto.swift in Sources */,
7A761C09267E8EE40005F28F /* Base64FS.swift in Sources */,

View File

@ -43,7 +43,9 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
let settingsService = SettingsService(defaults: .standard)
container.register(SettingsServiceProtocol.self, instance: settingsService)
container.register(ApiServiceProtocol.self, instance: ApiService())
let apiService = ApiService()
container.register(ApiServiceProtocol.self, instance: apiService)
let locationService = LocationService(
geocoder: CLGeocoder(),
@ -53,10 +55,13 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
container.register(LocationServiceProtocol.self, instance: locationService)
container.register(StorageServiceProtocol.self,
instance: try await StorageService(settingsService: settingsService))
let storageService = try await StorageService(settingsService: settingsService)
container.register(StorageServiceProtocol.self, instance: storageService)
container.register(VehicleServiceProtocol.self, instance: VehicleService())
let vehicleService = VehicleService(apiService: apiService,
storageService: storageService,
locationService: locationService)
container.register(VehicleServiceProtocol.self, instance: vehicleService)
}
func setupRootController(scene: UIScene) {

View File

@ -135,7 +135,7 @@ class EventsViewModel: ACHudContainer {
await wrapWithToast { [weak self] in
guard let self else { return }
let vehicle = try await apiOperation()
try await storageService.updateVehicleIfExists(dto: vehicle)
try await storageService.updateVehicle(dto: vehicle, policy: .ifExists)
self.vehicle = vehicle
}
}

View File

@ -110,7 +110,7 @@ final class HistoryViewModel: ACHudContainer {
func checkNewNumber(_ number: String) async {
await wrapWithToast { [weak self] in
try await self?.vehicleService.checkAndStore(number: number)
try await self?.vehicleService.check(number: number)
}
}
}

View File

@ -74,7 +74,7 @@ class NotesViewModel: ACHudContainer {
await wrapWithToast { [weak self] in
guard let self else { return }
let vehicle = try await apiOp()
try await storageService.updateVehicleIfExists(dto: vehicle)
try await storageService.updateVehicle(dto: vehicle, policy: .ifExists)
self.vehicle = vehicle
}
}

View File

@ -93,7 +93,7 @@ class ReportViewModel: ACHudContainer {
func checkGB() async {
await wrapWithToast {
self.vehicle = try await self.apiService.checkVehicleGb(by: self.vehicle.getNumber())
try await self.storageService.updateVehicleIfExists(dto: self.vehicle)
try await self.storageService.updateVehicle(dto: self.vehicle, policy: .ifExists)
}
}

View File

@ -37,6 +37,14 @@ public struct VehicleDto: Sendable, Equatable {
public var synchronized: Bool = true
public init() { }
public init(number: String) {
self.number = number
self.addedDate = Date().timeIntervalSince1970
self.updatedDate = self.addedDate
self.synchronized = false
}
}
extension VehicleDto: Identifiable {

View File

@ -25,5 +25,6 @@ public protocol ApiServiceProtocol: Sendable {
func getRegions() async throws -> [VehicleRegion]
func getYears() async throws -> [Int]
func checkVehicle(by number: String, notes: [VehicleNoteDto], events: [VehicleEventDto], force: Bool) async throws -> VehicleDto
func checkVehicleGb(by number: String) async throws -> VehicleDto
}

View File

@ -18,6 +18,8 @@ public final class LocationService {
private var eventTask: Task<VehicleEventDto,Error>?
private(set) public var lastEvent: VehicleEventDto?
public init(geocoder: GeocoderProtocol,
locationManager: SwiftLocationProtocol,
settingsService: SettingsServiceProtocol) {
@ -52,6 +54,10 @@ public final class LocationService {
return VehicleEventDto(lat: coordinate.latitude, lon: coordinate.longitude, addedBy: settingsService.user.email)
}
func setLastEvent(_ event: VehicleEventDto) {
lastEvent = event
}
}
extension LocationService: LocationServiceProtocol {
@ -77,10 +83,30 @@ extension LocationService: LocationServiceProtocol {
let task = Task {
let location = try await requestLocation()
eventTask = nil
lastEvent = location
return location
}
eventTask = task
return try await task.value
}
}
public func getRecentLocation() async throws -> VehicleEventDto {
var event: VehicleEventDto
if let lastEvent, Date().timeIntervalSince1970 - lastEvent.date < 100 {
event = lastEvent
} else {
event = try await requestCurrentLocation()
}
event.address = try? await getAddressForLocation(latitude: event.latitude, longitude: event.longitude)
return event
}
public func resetLastEvent() {
lastEvent = nil
}
}

View File

@ -6,9 +6,16 @@
// Copyright © 2024 Selim Mustafaev. All rights reserved.
//
import Mockable
@MainActor
public protocol LocationServiceProtocol {
@Mockable
public protocol LocationServiceProtocol: Sendable {
var lastEvent: VehicleEventDto? { get }
func getAddressForLocation(latitude: Double, longitude: Double) async throws -> String
func requestCurrentLocation() async throws -> VehicleEventDto
func getRecentLocation() async throws -> VehicleEventDto
func resetLastEvent()
}

View File

@ -0,0 +1,13 @@
//
// DbUpdatePolicy.swift
// AutoCatCore
//
// Created by Selim Mustafaev on 24.01.2025.
// Copyright © 2025 Selim Mustafaev. All rights reserved.
//
public enum DbUpdatePolicy: CaseIterable {
case always
case ifExists
}

View File

@ -43,15 +43,25 @@ public actor StorageService: StorageServiceProtocol {
}
}
public func updateVehicleIfExists(dto: VehicleDto) async throws {
@discardableResult
public func updateVehicle(dto: VehicleDto, policy: DbUpdatePolicy) async throws -> Bool {
guard realm.object(ofType: Vehicle.self, forPrimaryKey: dto.getNumber()) != nil else {
return
let shouldUpdate = switch policy {
case .always:
true
case .ifExists:
realm.object(ofType: Vehicle.self, forPrimaryKey: dto.getNumber()) != nil
}
guard shouldUpdate else {
return false
}
try await realm.asyncWrite {
realm.add(Vehicle(dto: dto), update: .all)
}
return true
}
public func loadVehicle(number: String) async throws -> VehicleDto {

View File

@ -18,7 +18,8 @@ public protocol StorageServiceProtocol: Sendable {
// Vehicles
func loadVehicles() async -> [VehicleDto]
func loadVehicle(number: String) async throws -> VehicleDto
func updateVehicleIfExists(dto: VehicleDto) async throws
@discardableResult
func updateVehicle(dto: VehicleDto, policy: DbUpdatePolicy) async throws -> Bool
// Notes
func addNote(text: String, to number: String) async throws -> VehicleDto

View File

@ -6,19 +6,82 @@
// Copyright © 2025 Selim Mustafaev. All rights reserved.
//
public struct VehicleWithErrors: Sendable {
public var vehicle: VehicleDto
public var errors: [Error]
}
public final class VehicleService {
public init() {}
let apiService: ApiServiceProtocol
let storageService: StorageServiceProtocol
let locationService: LocationServiceProtocol
public init(apiService: ApiServiceProtocol,
storageService: StorageServiceProtocol,
locationService: LocationServiceProtocol) {
self.apiService = apiService
self.storageService = storageService
self.locationService = locationService
}
}
extension VehicleService: VehicleServiceProtocol {
public func check(number: String) async throws -> VehicleDto {
func check(number: String,
forceUpdate: Bool,
trackLocation: Bool,
dbUpdatePolicy: DbUpdatePolicy) async throws -> VehicleWithErrors {
VehicleDto()
var vehicle = (try? await storageService.loadVehicle(number: number)) ?? VehicleDto(number: number)
var errors: [Error] = []
let events = vehicle.events
let notes = vehicle.notes
async let locationTask = trackLocation ? locationService.getRecentLocation() : nil
async let vehicleTask = apiService.checkVehicle(by: number, notes: notes, events: events, force: forceUpdate)
do {
vehicle = try await vehicleTask
} catch {
errors.append(error)
}
public func checkAndStore(number: String) async throws {
if trackLocation {
do {
if let event = try await locationTask {
vehicle.events.append(event)
vehicle.synchronized = false
if !vehicle.unrecognized {
vehicle = try await apiService.add(event: event, to: number)
}
}
} catch {
errors.append(error)
}
}
await locationService.resetLastEvent()
try await storageService.updateVehicle(dto: vehicle, policy: dbUpdatePolicy)
return VehicleWithErrors(vehicle: vehicle, errors: errors)
}
public func check(number: String) async throws -> VehicleWithErrors {
try await check(number: number, forceUpdate: false, trackLocation: true, dbUpdatePolicy: .always)
}
public func updateHistory(number: String) async throws -> VehicleWithErrors {
try await check(number: number, forceUpdate: true, trackLocation: false, dbUpdatePolicy: .always)
}
public func updateSearch(number: String) async throws -> VehicleWithErrors {
try await check(number: number, forceUpdate: true, trackLocation: false, dbUpdatePolicy: .ifExists)
}
}

View File

@ -8,6 +8,7 @@
public protocol VehicleServiceProtocol: Sendable {
func check(number: String) async throws -> VehicleDto
func checkAndStore(number: String) async throws
func check(number: String) async throws -> VehicleWithErrors
func updateHistory(number: String) async throws -> VehicleWithErrors
func updateSearch(number: String) async throws -> VehicleWithErrors
}

View File

@ -174,6 +174,7 @@ struct LocationServiceTests {
#expect(event.latitude == latitude)
#expect(event.longitude == longitude)
#expect(locationService.lastEvent != nil)
}
@Test("Get location: no location")
@ -190,6 +191,8 @@ struct LocationServiceTests {
await #expect(throws: LocationError.generic) {
_ = try await locationService.requestCurrentLocation()
}
#expect(locationService.lastEvent == nil)
}
@Test("Get location: parallel requests")
@ -222,4 +225,106 @@ struct LocationServiceTests {
#expect(event3.latitude == latitude)
#expect(event3.longitude == longitude)
}
@Test("Get recent location (request new)", arguments: [true, false])
func getRecentLocationNew(hasAddress: Bool) async throws {
let placemark = CLPlacemark(location: location, name: address, postalAddress: nil)
given(locationManagerMock)
.authorizationStatus
.willReturn(.authorizedWhenInUse)
given(locationManagerMock)
.requestLocation(accuracy: .any, timeout: .any)
.willReturn(.didUpdateLocations([location]))
given(geocoderMock)
.reverseGeocodeLocation(.any)
.willReturn(hasAddress ? [placemark] : [])
let event = try await locationService.getRecentLocation()
verify(locationManagerMock)
.requestLocation(accuracy: .any, timeout: .any)
.called(.once)
#expect(event.latitude == latitude)
#expect(event.longitude == longitude)
#expect(locationService.lastEvent != nil)
#expect(event.address == (hasAddress ? address : nil))
}
@Test("Get recent location (return existing)", arguments: [true, false])
func getRecentLocationExisting(hasAddress: Bool) async throws {
let placemark = CLPlacemark(location: location, name: address, postalAddress: nil)
given(locationManagerMock)
.authorizationStatus
.willReturn(.authorizedWhenInUse)
given(geocoderMock)
.reverseGeocodeLocation(.any)
.willReturn(hasAddress ? [placemark] : [])
locationService.setLastEvent(VehicleEventDto(lat: latitude, lon: longitude, addedBy: nil))
let event = try await locationService.getRecentLocation()
verify(locationManagerMock)
.requestLocation(accuracy: .any, timeout: .any)
.called(.never)
#expect(event.latitude == latitude)
#expect(event.longitude == longitude)
#expect(locationService.lastEvent != nil)
#expect(event.address == (hasAddress ? address : nil))
}
@Test("Get recent location (update existing)", arguments: [true, false])
func getRecentLocationUpdateExisting(hasAddress: Bool) async throws {
let placemark = CLPlacemark(location: location, name: address, postalAddress: nil)
given(locationManagerMock)
.authorizationStatus
.willReturn(.authorizedWhenInUse)
given(locationManagerMock)
.requestLocation(accuracy: .any, timeout: .any)
.willReturn(.didUpdateLocations([location]))
given(geocoderMock)
.reverseGeocodeLocation(.any)
.willReturn(hasAddress ? [placemark] : [])
// Set existing date which is too old and should not be used
var existingEvent = VehicleEventDto(lat: 0, lon: 0, addedBy: nil)
existingEvent.date = try #require(Calendar.current.date(byAdding: .day, value: -1, to: Date())).timeIntervalSince1970
locationService.setLastEvent(existingEvent)
let event = try await locationService.getRecentLocation()
verify(locationManagerMock)
.requestLocation(accuracy: .any, timeout: .any)
.called(.once)
#expect(event.latitude == latitude)
#expect(event.longitude == longitude)
#expect(locationService.lastEvent != nil)
#expect(event.address == (hasAddress ? address : nil))
}
@Test("Reset last event")
func resetLastEvent() {
locationService.setLastEvent(VehicleEventDto(lat: latitude, lon: longitude, addedBy: nil))
locationService.resetLastEvent()
#expect(locationService.lastEvent == nil)
}
}

View File

@ -64,4 +64,29 @@ struct StorageServiceTests {
_ = try await storageService.loadVehicle(number: nonExistingVehicleNumber)
}
}
@Test("Update existing vehicle", arguments: DbUpdatePolicy.allCases)
func updateVehicle(policy: DbUpdatePolicy) async throws {
let vehicle = VehicleDto(number: existingVehicleNumber)
let updated = try await storageService.updateVehicle(dto: vehicle, policy: policy)
#expect(updated == true)
}
@Test("Update non-existent vehicle", arguments: DbUpdatePolicy.allCases)
func updateNonExistentVehicle(policy: DbUpdatePolicy) async throws {
let vehicle = VehicleDto(number: nonExistingVehicleNumber)
let updated = try await storageService.updateVehicle(dto: vehicle, policy: policy)
switch policy {
case .always:
#expect(updated == true)
case .ifExists:
#expect(updated == false)
}
}
}

View File

@ -0,0 +1,424 @@
//
// VehicleServiceTests.swift
// AutoCatCoreTests
//
// Created by Selim Mustafaev on 24.01.2025.
// Copyright © 2025 Selim Mustafaev. All rights reserved.
//
import Testing
import Mockable
import AutoCatCore
struct VehicleServiceTests {
let apiServiceMock = MockApiServiceProtocol()
let storageServiceMock = MockStorageServiceProtocol()
let locationServiceMock: MockLocationServiceProtocol
let vehicleService: VehicleServiceProtocol
let existingVehicleNumber = "А123АА761"
let nonExistingVehicleNumber = "А999АА761"
let testVin = "1234567890"
init () async throws {
self.locationServiceMock = await .init()
self.vehicleService = VehicleService(apiService: apiServiceMock,
storageService: storageServiceMock,
locationService: locationServiceMock)
}
@Test("Check vehicle (all throws)")
func checkVehicleAllThrows() async throws {
given(storageServiceMock)
.loadVehicle(number: .any)
.willThrow(TestError.generic)
given(locationServiceMock)
.getRecentLocation()
.willThrow(TestError.generic)
given(apiServiceMock)
.checkVehicle(by: .any, notes: .any, events: .any, force: .any)
.willThrow(TestError.generic)
given(locationServiceMock)
.resetLastEvent()
.willReturn()
given(storageServiceMock)
.updateVehicle(dto: .any, policy: .any)
.willThrow(TestError.generic)
await #expect(throws: TestError.generic) {
_ = try await vehicleService.check(number: existingVehicleNumber)
}
}
@Test("Check vehicle (all but DB update throws)")
func checkVehicleDbUpdate() async throws {
given(storageServiceMock)
.loadVehicle(number: .any)
.willThrow(TestError.generic)
given(locationServiceMock)
.getRecentLocation()
.willThrow(TestError.generic)
given(apiServiceMock)
.checkVehicle(by: .any, notes: .any, events: .any, force: .any)
.willThrow(TestError.generic)
given(locationServiceMock)
.resetLastEvent()
.willReturn()
given(storageServiceMock)
.updateVehicle(dto: .any, policy: .any)
.willReturn(true)
let result = try await vehicleService.check(number: existingVehicleNumber)
#expect(result.vehicle.unrecognized)
#expect(result.vehicle.events.isEmpty)
#expect(result.errors.count == 2)
}
@Test("Check vehicle (existing unrecognized vehicle)")
func checkVehicleExistingVehicle() async throws {
var vehicle = VehicleDto(number: existingVehicleNumber)
vehicle.vin1 = testVin
given(storageServiceMock)
.loadVehicle(number: .any)
.willReturn(vehicle)
given(locationServiceMock)
.getRecentLocation()
.willThrow(TestError.generic)
given(apiServiceMock)
.checkVehicle(by: .any, notes: .any, events: .any, force: .any)
.willThrow(TestError.generic)
given(locationServiceMock)
.resetLastEvent()
.willReturn()
given(storageServiceMock)
.updateVehicle(dto: .any, policy: .any)
.willReturn(true)
let result = try await vehicleService.check(number: existingVehicleNumber)
#expect(result.vehicle.vin1 == testVin)
#expect(result.errors.count == 2)
}
@Test("Check vehicle (location received)")
func checkVehicleLocationReceived() async throws {
var vehicle = VehicleDto(number: existingVehicleNumber)
vehicle.vin1 = testVin
given(storageServiceMock)
.loadVehicle(number: .any)
.willReturn(vehicle)
given(locationServiceMock)
.getRecentLocation()
.willReturn(.valid)
given(apiServiceMock)
.checkVehicle(by: .any, notes: .any, events: .any, force: .any)
.willThrow(TestError.generic)
given(locationServiceMock)
.resetLastEvent()
.willReturn()
given(storageServiceMock)
.updateVehicle(dto: .any, policy: .any)
.willReturn(true)
let result = try await vehicleService.check(number: VehicleDto.validNumber)
#expect(result.vehicle.vin1 == testVin)
#expect(result.errors.count == 1)
#expect(result.vehicle.events.count == 1)
#expect(result.vehicle.events.first?.latitude == VehicleEventDto.validLatitude)
#expect(result.vehicle.events.first?.longitude == VehicleEventDto.validLongitude)
}
@Test("Check vehicle (existing normal vehicle)")
func checkVehicleExistingNormalVehicle() async throws {
given(storageServiceMock)
.loadVehicle(number: .any)
.willReturn(.normal)
given(locationServiceMock)
.getRecentLocation()
.willReturn(.valid)
given(apiServiceMock)
.checkVehicle(by: .any, notes: .any, events: .any, force: .any)
.willThrow(TestError.generic)
given(locationServiceMock)
.resetLastEvent()
.willReturn()
given(storageServiceMock)
.updateVehicle(dto: .any, policy: .any)
.willReturn(true)
given(apiServiceMock)
.add(event: .any, to: .any)
.willThrow(TestError.generic)
let result = try await vehicleService.check(number: VehicleDto.validNumber)
#expect(result.vehicle.number == VehicleDto.validNumber)
#expect(result.errors.count == 2)
#expect(result.vehicle.events.count == 1)
#expect(result.vehicle.events.first?.latitude == VehicleEventDto.validLatitude)
#expect(result.vehicle.events.first?.longitude == VehicleEventDto.validLongitude)
}
@Test("Check vehicle (existing normal vehicle, add event)")
func checkVehicleExistingNormalVehicleAddEvent() async throws {
given(storageServiceMock)
.loadVehicle(number: .any)
.willReturn(.normal)
given(locationServiceMock)
.getRecentLocation()
.willReturn(.valid)
given(apiServiceMock)
.checkVehicle(by: .any, notes: .any, events: .any, force: .any)
.willThrow(TestError.generic)
given(locationServiceMock)
.resetLastEvent()
.willReturn()
given(storageServiceMock)
.updateVehicle(dto: .any, policy: .any)
.willReturn(true)
given(apiServiceMock)
.add(event: .any, to: .any)
.willReturn(.normal.addEvent(.valid))
let result = try await vehicleService.check(number: VehicleDto.validNumber)
#expect(result.vehicle.number == VehicleDto.validNumber)
#expect(result.errors.count == 1)
#expect(result.vehicle.events.count == 1)
#expect(result.vehicle.events.first?.latitude == VehicleEventDto.validLatitude)
#expect(result.vehicle.events.first?.longitude == VehicleEventDto.validLongitude)
}
@Test("Check vehicle (with server check working)")
func checkVehicleServerCheck() async throws {
given(storageServiceMock)
.loadVehicle(number: .any)
.willReturn(.normal)
given(locationServiceMock)
.getRecentLocation()
.willReturn(.valid)
given(apiServiceMock)
.checkVehicle(by: .any, notes: .any, events: .any, force: .any)
.willReturn(.normal)
given(locationServiceMock)
.resetLastEvent()
.willReturn()
given(storageServiceMock)
.updateVehicle(dto: .any, policy: .any)
.willReturn(true)
given(apiServiceMock)
.add(event: .any, to: .any)
.willReturn(.normal.addEvent(.valid))
let result = try await vehicleService.check(number: VehicleDto.validNumber)
#expect(result.vehicle.number == VehicleDto.validNumber)
#expect(result.errors.count == 0)
#expect(result.vehicle.events.count == 1)
#expect(result.vehicle.events.first?.latitude == VehicleEventDto.validLatitude)
#expect(result.vehicle.events.first?.longitude == VehicleEventDto.validLongitude)
}
@Test("Check")
func check() async throws {
let vehicle: VehicleDto = .normal
given(storageServiceMock)
.loadVehicle(number: .any)
.willReturn(.normal)
given(locationServiceMock)
.getRecentLocation()
.willReturn(.valid)
given(apiServiceMock)
.checkVehicle(by: .any, notes: .any, events: .any, force: .any)
.willReturn(vehicle)
given(locationServiceMock)
.resetLastEvent()
.willReturn()
given(storageServiceMock)
.updateVehicle(dto: .any, policy: .any)
.willReturn(true)
given(apiServiceMock)
.add(event: .any, to: .any)
.willReturn(vehicle.addEvent(.valid))
let result = try await vehicleService.check(number: vehicle.number)
verify(apiServiceMock)
.checkVehicle(by: .any, notes: .any, events: .any, force: .value(false))
.called(.once)
verify(locationServiceMock)
.getRecentLocation()
.called(.once)
verify(apiServiceMock)
.add(event: .any, to: .any)
.called(.once)
verify(storageServiceMock)
.updateVehicle(dto: .any, policy: .value(.always))
.called(.once)
#expect(result.vehicle.number == vehicle.number)
#expect(result.errors.count == 0)
#expect(result.vehicle.events.count == 1)
#expect(result.vehicle.events.first?.latitude == VehicleEventDto.validLatitude)
#expect(result.vehicle.events.first?.longitude == VehicleEventDto.validLongitude)
}
@Test("Update (history)")
func updateHistory() async throws {
let vehicle: VehicleDto = .normal
given(storageServiceMock)
.loadVehicle(number: .any)
.willReturn(.normal)
given(locationServiceMock)
.getRecentLocation()
.willReturn(.valid)
given(apiServiceMock)
.checkVehicle(by: .any, notes: .any, events: .any, force: .any)
.willReturn(vehicle)
given(locationServiceMock)
.resetLastEvent()
.willReturn()
given(storageServiceMock)
.updateVehicle(dto: .any, policy: .any)
.willReturn(true)
given(apiServiceMock)
.add(event: .any, to: .any)
.willReturn(vehicle.addEvent(.valid))
let result = try await vehicleService.updateHistory(number: vehicle.number)
verify(apiServiceMock)
.checkVehicle(by: .any, notes: .any, events: .any, force: .value(true))
.called(.once)
verify(locationServiceMock)
.getRecentLocation()
.called(.never)
verify(apiServiceMock)
.add(event: .any, to: .any)
.called(.never)
verify(storageServiceMock)
.updateVehicle(dto: .any, policy: .value(.always))
.called(.once)
#expect(result.vehicle.number == vehicle.number)
#expect(result.errors.count == 0)
#expect(result.vehicle.events.count == 0)
}
@Test("Update (search)")
func updateSearch() async throws {
let vehicle: VehicleDto = .normal
given(storageServiceMock)
.loadVehicle(number: .any)
.willReturn(.normal)
given(locationServiceMock)
.getRecentLocation()
.willReturn(.valid)
given(apiServiceMock)
.checkVehicle(by: .any, notes: .any, events: .any, force: .any)
.willReturn(vehicle)
given(locationServiceMock)
.resetLastEvent()
.willReturn()
given(storageServiceMock)
.updateVehicle(dto: .any, policy: .any)
.willReturn(true)
given(apiServiceMock)
.add(event: .any, to: .any)
.willReturn(vehicle.addEvent(.valid))
let result = try await vehicleService.updateSearch(number: vehicle.number)
verify(apiServiceMock)
.checkVehicle(by: .any, notes: .any, events: .any, force: .value(true))
.called(.once)
verify(locationServiceMock)
.getRecentLocation()
.called(.never)
verify(apiServiceMock)
.add(event: .any, to: .any)
.called(.never)
verify(storageServiceMock)
.updateVehicle(dto: .any, policy: .value(.ifExists))
.called(.once)
#expect(result.vehicle.number == vehicle.number)
#expect(result.errors.count == 0)
#expect(result.vehicle.events.count == 0)
}
}

View File

@ -21,7 +21,7 @@ struct EventsTests {
var viewModel: EventsViewModel
lazy var vehicleWithEvent: VehicleDto = .normal.addEvent(.valid)
lazy var unrecognizedVehicleWithEvent: VehicleDto = .unrecognized.addEvent(.valid)
lazy var unrecognizedVehicleWithEvent: VehicleDto = .unrecognizedVehicle.addEvent(.valid)
init() {
@ -56,14 +56,14 @@ struct EventsTests {
.willReturn(vehicleWithEvent)
given(storageServiceMock)
.updateVehicleIfExists(dto: .value(updatedVehicle))
.willReturn()
.updateVehicle(dto: .value(updatedVehicle), policy: .value(.ifExists))
.willReturn(true)
given(storageServiceMock)
.add(event: .value(.valid), to: .value(VehicleDto.validNumber))
.willReturn(unrecognizedVehicleWithEvent)
viewModel = makeViewModel(vehicle: isUnrecognized ? .unrecognized : .normal)
viewModel = makeViewModel(vehicle: isUnrecognized ? .unrecognizedVehicle : .normal)
await viewModel.addEvent(.valid)
verify(apiServiceMock)
@ -71,7 +71,7 @@ struct EventsTests {
.called(isUnrecognized ? .never : .once)
verify(storageServiceMock)
.updateVehicleIfExists(dto: .any)
.updateVehicle(dto: .any, policy: .any)
.called(isUnrecognized ? .never : .once)
verify(storageServiceMock)
@ -89,19 +89,19 @@ struct EventsTests {
mutating func deleteEvent(isUnrecognized: Bool) async throws {
let vehicleWithEvent: VehicleDto = isUnrecognized ? unrecognizedVehicleWithEvent : vehicleWithEvent
let vehicle: VehicleDto = isUnrecognized ? .unrecognized : .normal
let vehicle: VehicleDto = isUnrecognized ? .unrecognizedVehicle : .normal
given(apiServiceMock)
.remove(event: .value(VehicleEventDto.validId))
.willReturn(.normal)
given(storageServiceMock)
.updateVehicleIfExists(dto: .value(vehicle))
.willReturn()
.updateVehicle(dto: .value(vehicle), policy: .value(.ifExists))
.willReturn(true)
given(storageServiceMock)
.remove(event: .value(VehicleEventDto.validId), from: .value(VehicleDto.validNumber))
.willReturn(.unrecognized)
.willReturn(.unrecognizedVehicle)
viewModel = makeViewModel(vehicle: vehicleWithEvent)
await viewModel.deleteEvent(VehicleEventDto.valid.viewModel)
@ -111,7 +111,7 @@ struct EventsTests {
.called(isUnrecognized ? .never : .once)
verify(storageServiceMock)
.updateVehicleIfExists(dto: .any)
.updateVehicle(dto: .any, policy: .any)
.called(isUnrecognized ? .never : .once)
verify(storageServiceMock)

View File

@ -18,3 +18,10 @@ enum TestError: LocalizedError {
}
}
}
extension Error {
static var testError: Error {
return TestError.generic
}
}

View File

@ -11,6 +11,7 @@ import AutoCatCore
extension VehicleDto {
static let validNumber: String = "А123АА761"
static let validNumber2: String = "А456АА761"
static var normal: VehicleDto {
var vehicle = VehicleDto()
@ -19,7 +20,14 @@ extension VehicleDto {
return vehicle
}
static var unrecognized: VehicleDto {
static var normal2: VehicleDto {
var vehicle = VehicleDto()
vehicle.number = validNumber2
vehicle.brand = VehicleBrandDto()
return vehicle
}
static var unrecognizedVehicle: VehicleDto {
var vehicle = VehicleDto()
vehicle.number = validNumber
return vehicle

View File

@ -8,7 +8,6 @@
import CoreLocation
import AutoCatCore
@testable import AutoCat
extension VehicleEventDto {
@ -34,15 +33,4 @@ extension VehicleEventDto {
event.address = testAddress
return event
}
var viewModel: EventModel {
EventModel(
id: id,
date: "",
coordinate: CLLocationCoordinate2DMake(latitude, longitude),
address: address ?? "",
isMe: true
)
}
}

View File

@ -0,0 +1,25 @@
//
// VehicleEventDto+ViewModel.swift
// AutoCatTests
//
// Created by Selim Mustafaev on 25.01.2025.
// Copyright © 2025 Selim Mustafaev. All rights reserved.
//
@testable import AutoCat
import AutoCatCore
import CoreLocation
extension VehicleEventDto {
var viewModel: EventModel {
EventModel(
id: id,
date: "",
coordinate: CLLocationCoordinate2DMake(latitude, longitude),
address: address ?? "",
isMe: true
)
}
}

View File

@ -23,7 +23,7 @@ final class NotesTests {
let noteTextModified = "Test note text modified"
lazy var vehicleWithNote: VehicleDto = .normal.addNote(text: noteText)
lazy var unrecognizedVehicleWithNote: VehicleDto = .unrecognized.addNote(text: noteText)
lazy var unrecognizedVehicleWithNote: VehicleDto = .unrecognizedVehicle.addNote(text: noteText)
init() {
storageServiceMock = MockStorageServiceProtocol()
@ -44,8 +44,8 @@ final class NotesTests {
.willReturn(vehicleWithNote)
given(storageServiceMock)
.updateVehicleIfExists(dto: .any)
.willReturn()
.updateVehicle(dto: .any, policy: .any)
.willReturn(true)
viewModel.vehicle = .normal
@ -56,7 +56,7 @@ final class NotesTests {
verify(storageServiceMock)
.addNote(text: .any, to: .any).called(.never)
.updateVehicleIfExists(dto: .any).called(.once)
.updateVehicle(dto: .any, policy: .any).called(.once)
#expect(viewModel.vehicle.notes.contains { $0.text == noteText })
#expect(viewModel.hud == nil)
@ -69,7 +69,7 @@ final class NotesTests {
.addNote(text: .any, to: .any)
.willReturn(vehicleWithNote)
viewModel.vehicle = .unrecognized
viewModel.vehicle = .unrecognizedVehicle
await viewModel.addNote(text: noteText)
@ -78,7 +78,7 @@ final class NotesTests {
verify(storageServiceMock)
.addNote(text: .any, to: .any).called(.once)
.updateVehicleIfExists(dto: .any).called(.never)
.updateVehicle(dto: .any, policy: .any).called(.never)
#expect(viewModel.vehicle.notes.contains { $0.text == noteText })
#expect(viewModel.hud == nil)
@ -95,8 +95,8 @@ final class NotesTests {
.willReturn(modifiedVehicle)
given(storageServiceMock)
.updateVehicleIfExists(dto: .any)
.willReturn()
.updateVehicle(dto: .any, policy: .any)
.willReturn(true)
viewModel.vehicle = vehicleWithNote
@ -107,7 +107,7 @@ final class NotesTests {
verify(storageServiceMock)
.editNote(id: .any, text: .any, for: .any).called(.never)
.updateVehicleIfExists(dto: .any).called(.once)
.updateVehicle(dto: .any, policy: .any).called(.once)
#expect(viewModel.vehicle.notes.contains { $0.text == noteTextModified })
#expect(viewModel.hud == nil)
@ -116,12 +116,12 @@ final class NotesTests {
@Test("Edit note (unrecognized vehicle)")
func editNoteUnrecognized() async throws {
let vehicle: VehicleDto = .unrecognized.addNote(text: noteText)
let vehicle: VehicleDto = .unrecognizedVehicle.addNote(text: noteText)
let noteId = try #require(vehicle.notes.first?.id)
given(storageServiceMock)
.editNote(id: .value(noteId), text: .value(noteTextModified), for: .any)
.willReturn(.unrecognized.addNote(text: noteTextModified, id: noteId))
.willReturn(.unrecognizedVehicle.addNote(text: noteTextModified, id: noteId))
viewModel.vehicle = vehicle
@ -132,7 +132,7 @@ final class NotesTests {
verify(storageServiceMock)
.editNote(id: .any, text: .any, for: .any).called(.once)
.updateVehicleIfExists(dto: .any).called(.never)
.updateVehicle(dto: .any, policy: .any).called(.never)
#expect(viewModel.vehicle.notes.contains { $0.text == noteTextModified })
#expect(viewModel.hud == nil)
@ -148,8 +148,8 @@ final class NotesTests {
.willReturn(.normal)
given(storageServiceMock)
.updateVehicleIfExists(dto: .any)
.willReturn()
.updateVehicle(dto: .any, policy: .any)
.willReturn(true)
viewModel.vehicle = vehicleWithNote
@ -160,7 +160,7 @@ final class NotesTests {
verify(storageServiceMock)
.deleteNote(id: .any, for: .any).called(.never)
.updateVehicleIfExists(dto: .any).called(.once)
.updateVehicle(dto: .any, policy: .any).called(.once)
#expect(!viewModel.vehicle.notes.contains { $0.text == noteText })
#expect(viewModel.hud == nil)
@ -173,7 +173,7 @@ final class NotesTests {
given(storageServiceMock)
.deleteNote(id: .value(noteId), for: .any)
.willReturn(.unrecognized)
.willReturn(.unrecognizedVehicle)
viewModel.vehicle = unrecognizedVehicleWithNote
@ -184,7 +184,7 @@ final class NotesTests {
verify(storageServiceMock)
.deleteNote(id: .value(noteId), for: .any).called(.once)
.updateVehicleIfExists(dto: .any).called(.never)
.updateVehicle(dto: .any, policy: .any).called(.never)
#expect(!viewModel.vehicle.notes.contains { $0.text == noteText })
#expect(viewModel.hud == nil)

View File

@ -110,8 +110,8 @@ class ReportTests {
.willReturn(updatedVehicle)
given(storageServiceMock)
.updateVehicleIfExists(dto: .value(updatedVehicle))
.willReturn()
.updateVehicle(dto: .value(updatedVehicle), policy: .value(.ifExists))
.willReturn(true)
given(storageServiceMock)
.loadVehicle(number: .value(existingVehicleNumber))
@ -125,7 +125,7 @@ class ReportTests {
.called(.once)
verify(storageServiceMock)
.updateVehicleIfExists(dto: .value(updatedVehicle))
.updateVehicle(dto: .value(updatedVehicle), policy: .value(.ifExists))
.called(.once)
verify(storageServiceMock)