From ba6c7f372faba32d89630d03eaf23a9a49643cb5 Mon Sep 17 00:00:00 2001 From: Bill Gestrich <3207996+gestrich@users.noreply.github.com> Date: Sun, 19 Feb 2023 11:30:46 -0500 Subject: [PATCH 1/5] Remote Action Models --- Loop.xcodeproj/project.pbxproj | 8 +- Loop/Managers/DeviceDataManager.swift | 260 +++++++++--------- Loop/Managers/NotificationManager.swift | 5 + Loop/Managers/RemoteDataServicesManager.swift | 8 + Loop/Models/LoopConstants.swift | 4 + Loop/Models/RemoteCommand.swift | 74 +---- LoopTests/Models/RemoteActionTests.swift | 108 ++++++++ LoopTests/Models/RemoteCommandTests.swift | 225 --------------- 8 files changed, 275 insertions(+), 417 deletions(-) create mode 100644 LoopTests/Models/RemoteActionTests.swift delete mode 100644 LoopTests/Models/RemoteCommandTests.swift diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index 6ddb07b1c9..f42ed34510 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -412,7 +412,7 @@ A9DF02CB24F72B9E00B7C988 /* CriticalEventLogTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9DF02CA24F72B9E00B7C988 /* CriticalEventLogTests.swift */; }; A9DFAFB324F0415E00950D1E /* CarbBackfillRequestUserInfoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9DFAFB224F0415E00950D1E /* CarbBackfillRequestUserInfoTests.swift */; }; A9DFAFB524F048A000950D1E /* WatchHistoricalCarbsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9DFAFB424F048A000950D1E /* WatchHistoricalCarbsTests.swift */; }; - A9E8A80528A7CAC000C0A8A4 /* RemoteCommandTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E8A80428A7CAC000C0A8A4 /* RemoteCommandTests.swift */; }; + A9E8A80528A7CAC000C0A8A4 /* RemoteActionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E8A80428A7CAC000C0A8A4 /* RemoteActionTests.swift */; }; A9F5F1F5251050EC00E7C8A4 /* ZipArchiveTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9F5F1F4251050EC00E7C8A4 /* ZipArchiveTests.swift */; }; A9F66FC3247F451500096EA7 /* UIDevice+Loop.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9F66FC2247F451500096EA7 /* UIDevice+Loop.swift */; }; A9F703732489BC8500C98AD8 /* CarbStore+SimulatedCoreData.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9F703722489BC8500C98AD8 /* CarbStore+SimulatedCoreData.swift */; }; @@ -1407,7 +1407,7 @@ A9DF02CA24F72B9E00B7C988 /* CriticalEventLogTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CriticalEventLogTests.swift; sourceTree = ""; }; A9DFAFB224F0415E00950D1E /* CarbBackfillRequestUserInfoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbBackfillRequestUserInfoTests.swift; sourceTree = ""; }; A9DFAFB424F048A000950D1E /* WatchHistoricalCarbsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchHistoricalCarbsTests.swift; sourceTree = ""; }; - A9E8A80428A7CAC000C0A8A4 /* RemoteCommandTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteCommandTests.swift; sourceTree = ""; }; + A9E8A80428A7CAC000C0A8A4 /* RemoteActionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteActionTests.swift; sourceTree = ""; }; A9F5F1F4251050EC00E7C8A4 /* ZipArchiveTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZipArchiveTests.swift; sourceTree = ""; }; A9F66FC2247F451500096EA7 /* UIDevice+Loop.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIDevice+Loop.swift"; sourceTree = ""; }; A9F703722489BC8500C98AD8 /* CarbStore+SimulatedCoreData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CarbStore+SimulatedCoreData.swift"; sourceTree = ""; }; @@ -2803,7 +2803,7 @@ C19008FF252271BB00721625 /* SimpleBolusCalculatorTests.swift */, A9C1719625366F780053BCBD /* WatchHistoricalGlucoseTest.swift */, A9BD28E6272226B40071DF15 /* TestLocalizedError.swift */, - A9E8A80428A7CAC000C0A8A4 /* RemoteCommandTests.swift */, + A9E8A80428A7CAC000C0A8A4 /* RemoteActionTests.swift */, ); path = Models; sourceTree = ""; @@ -4121,7 +4121,7 @@ A96DAC2A2838EF8A00D94E38 /* DiagnosticLogTests.swift in Sources */, A9DAE7D02332D77F006AE942 /* LoopTests.swift in Sources */, E93E86B024DDE1BD00FF40C8 /* MockGlucoseStore.swift in Sources */, - A9E8A80528A7CAC000C0A8A4 /* RemoteCommandTests.swift in Sources */, + A9E8A80528A7CAC000C0A8A4 /* RemoteActionTests.swift in Sources */, 1DFE9E172447B6270082C280 /* UserNotificationAlertSchedulerTests.swift in Sources */, B4BC56382518DEA900373647 /* CGMStatusHUDViewModelTests.swift in Sources */, C1900900252271BB00721625 /* SimpleBolusCalculatorTests.swift in Sources */, diff --git a/Loop/Managers/DeviceDataManager.swift b/Loop/Managers/DeviceDataManager.swift index 762eb0408b..5d67b62549 100644 --- a/Loop/Managers/DeviceDataManager.swift +++ b/Loop/Managers/DeviceDataManager.swift @@ -812,6 +812,18 @@ extension DeviceDataManager { self.loopManager.updateRemoteRecommendation() } } + + func enactBolus(units: Double, activationType: BolusActivationType) async throws { + return try await withCheckedThrowingContinuation { continuation in + enactBolus(units: units, activationType: activationType) { error in + if let error = error { + continuation.resume(throwing: error) + return + } + continuation.resume() + } + } + } var pumpManagerStatus: PumpManagerStatus? { return pumpManager?.status @@ -1341,8 +1353,15 @@ extension Notification.Name { // MARK: - Remote Notification Handling extension DeviceDataManager { + func handleRemoteNotification(_ notification: [String: AnyObject]) { - + Task { + await handleRemoteNotification(notification) + } + } + + func handleRemoteNotification(_ notification: [String: AnyObject]) async { + defer { log.default("Remote Notification: Finished handling") } @@ -1351,9 +1370,6 @@ extension DeviceDataManager { log.error("Remote Notification: Overrides not enabled.") return } - - let defaultServiceIdentifier = "NightscoutService" - let serviceIdentifer = notification["serviceIdentifier"] as? String ?? defaultServiceIdentifier if let expirationStr = notification["expiration"] as? String { let formatter = ISO8601DateFormatter() @@ -1362,7 +1378,7 @@ extension DeviceDataManager { let nowDate = Date() guard nowDate < expiration else { let expiredInterval = nowDate.timeIntervalSince(expiration) - NotificationManager.sendRemoteCommandExpiredNotification(timeExpired: expiredInterval) + await NotificationManager.sendRemoteCommandExpiredNotification(timeExpired: expiredInterval) log.error("Remote Notification: Expired: %{public}@", String(describing: notification)) return } @@ -1372,156 +1388,144 @@ extension DeviceDataManager { } } - let command: RemoteCommand + let action: RemoteAction do { - command = try RemoteCommand.createRemoteCommand(notification: notification, allowedPresets: loopManager.settings.overridePresets, defaultAbsorptionTime: carbStore.defaultAbsorptionTimes.medium).get() + action = try RemoteAction.createRemoteAction(notification: notification).get() } catch { log.error("Remote Notification: Parse Error: %{public}@", String(describing: error)) return } - - switch command { - - case .temporaryScheduleOverride(let remoteOverride): - log.default("Remote Notification: Enacting temporary override: %{public}@", String(describing: remoteOverride)) - activateRemoteOverride(remoteOverride) - case .cancelTemporaryOverride: - log.default("Remote Notification: Canceling temporary override") - activateRemoteOverride(nil) - case .bolusEntry(let bolusAmount): - log.default("Remote Notification: Enacting bolus entry: %{public}@", String(describing: bolusAmount)) - - //Remote bolus requires validation from its remote source - - let validationResult = remoteDataServicesManager.validatePushNotificationSource(notification, serviceIdentifier: serviceIdentifer) - - switch validationResult { - case .success(): - log.info("Remote Notification: Validation successful") - case .failure(let error): - NotificationManager.sendRemoteBolusFailureNotification(for: error, amount: bolusAmount) - log.error("Remote Notification: Could not validate notification: %{public}@", String(describing: notification)) - return - } - - guard let maxBolusAmount = loopManager.settings.maximumBolus else { - NotificationManager.sendRemoteBolusFailureNotification(for: RemoteCommandError.missingMaxBolus, amount: bolusAmount) - log.error("Remote Notification: No max bolus detected. Aborting...") - return - } - - guard bolusAmount.isLessThanOrEqualTo(maxBolusAmount) else { - NotificationManager.sendRemoteBolusFailureNotification(for: RemoteCommandError.exceedsMaxBolus, amount: bolusAmount) - log.error("Remote Notification: Bolus exceeds maximum allowed. Aborting...") - return - } - - self.enactRemoteBolus(units: bolusAmount) { error in - if let error = error { - self.log.error("Remote Notification: Error adding bolus: %{public}@", String(describing: error)) - NotificationManager.sendRemoteBolusFailureNotification(for: error, amount: bolusAmount) - } else { - NotificationManager.sendRemoteBolusNotification(amount: bolusAmount) - } - } - case .carbsEntry(let candidateCarbEntry): - log.default("Remote Notification: Adding carbs entry: %{public}@", String(describing: candidateCarbEntry)) - - let candidateCarbsInGrams = candidateCarbEntry.quantity.doubleValue(for: .gram()) - - //Remote carb entry requires validation from its remote source - let validationResult = remoteDataServicesManager.validatePushNotificationSource(notification, serviceIdentifier: serviceIdentifer) - switch validationResult { - case .success(): - log.info("Remote Notification: Validation successful") - case .failure(let error): - NotificationManager.sendRemoteCarbEntryFailureNotification(for: error, amountInGrams: candidateCarbsInGrams) - log.error("Remote Notification: Could not validate notification: %{public}@", String(describing: notification)) - return + + log.default("Remote Notification: Handling action %{public}@", String(describing: action)) + + switch action { + case .temporaryScheduleOverride(let overrideAction): + do { + try await handleRemoteOverrideAction(overrideAction) + } catch { + log.error("Remote Notification: Override Action Error: %{public}@", String(describing: error)) } - - guard candidateCarbsInGrams > 0.0 else { - NotificationManager.sendRemoteCarbEntryFailureNotification(for: RemoteCommandError.invalidCarbs, amountInGrams: candidateCarbsInGrams) - log.error("Remote Notification: Invalid carb entry amount. Aborting...") - return + case .cancelTemporaryOverride(let overrideCancelAction): + do { + try await handleRemoteOverrideCancelAction(overrideCancelAction) + } catch { + log.error("Remote Notification: Override Action Cancel Error: %{public}@", String(describing: error)) } - - guard candidateCarbsInGrams <= LoopConstants.maxCarbEntryQuantity.doubleValue(for: .gram()) else { - NotificationManager.sendRemoteCarbEntryFailureNotification(for: RemoteCommandError.exceedsMaxCarbs, amountInGrams: candidateCarbsInGrams) - log.error("Remote Notification: Carbs higher than maximum. Aborting...") - return + case .bolusEntry(let bolusAction): + do { + try validatePushNotificationSource(notification: notification) + try await handleRemoteBolusAction(bolusAction) + } catch { + await NotificationManager.sendRemoteBolusFailureNotification(for: error, amount: bolusAction.amountInUnits) + log.error("Remote Notification: Bolus Action Error: %{public}@", String(describing: notification)) } - - addRemoteCarbEntry(candidateCarbEntry) { carbEntryAddResult in - switch carbEntryAddResult { - case .success(let completedCarbEntry): - NotificationManager.sendRemoteCarbEntryNotification(amountInGrams: completedCarbEntry.quantity.doubleValue(for: .gram())) - case .failure(let error): - self.log.error("Remote Notification: Error adding carb entry: %{public}@", String(describing: error)) - NotificationManager.sendRemoteCarbEntryFailureNotification(for: error, amountInGrams: candidateCarbsInGrams) - } + case .carbsEntry(let carbAction): + do { + try validatePushNotificationSource(notification: notification) + try await handleRemoteCarbAction(carbAction) + } catch { + await NotificationManager.sendRemoteCarbEntryFailureNotification(for: error, amountInGrams: carbAction.amountInGrams) + log.error("Remote Notification: Carb Action Error: %{public}@", String(describing: notification)) } } } - func activateRemoteOverride(_ remoteOverride: TemporaryScheduleOverride?) { + func validatePushNotificationSource(notification: [String: AnyObject]) throws { + + let defaultServiceIdentifier = "NightscoutService" + let serviceIdentifer = notification["serviceIdentifier"] as? String ?? defaultServiceIdentifier + + let validationResult = remoteDataServicesManager.validatePushNotificationSource(notification, serviceIdentifier: serviceIdentifer) + switch validationResult { + case .success(): + log.info("Remote Notification: Validation successful") + case .failure(let error): + throw error + } + } + + //Remote Overrides + + func handleRemoteOverrideAction(_ action: RemoteOverrideAction) async throws { + let remoteOverride = try action.toValidOverride(allowedPresets: loopManager.settings.overridePresets) + await activateRemoteOverride(remoteOverride) + } + + func handleRemoteOverrideCancelAction(_ cancelAction: RemoteOverrideCancelAction) async throws { + await activateRemoteOverride(nil) + } + + func activateRemoteOverride(_ remoteOverride: TemporaryScheduleOverride?) async { loopManager.mutateSettings { settings in settings.scheduleOverride = remoteOverride } - self.triggerBackgroundUpload(for: .overrides) + await triggerBackgroundUpload(for: .overrides) } - func addRemoteCarbEntry(_ carbEntry: NewCarbEntry, completion: @escaping (_ result: CarbStoreResult) -> Void) { - - var backgroundTask: UIBackgroundTaskIdentifier? - backgroundTask = UIApplication.shared.beginBackgroundTask(withName: "Add Remote Carb Entry") { - guard let backgroundTask = backgroundTask else {return} - UIApplication.shared.endBackgroundTask(backgroundTask) - self.log.error("Add Remote Carb Entry background task expired") - } - - carbStore.addCarbEntry(carbEntry) { result in - self.triggerBackgroundUpload(for: .carb) - if let backgroundTask = backgroundTask { - UIApplication.shared.endBackgroundTask(backgroundTask) - } - completion(result) - self.analyticsServicesManager.didAddCarbs(source: "Remote", amount: carbEntry.quantity.doubleValue(for: .gram())) - } + //Remote Bolus + + func handleRemoteBolusAction(_ bolusCommand: RemoteBolusAction) async throws { + let validBolusAmount = try bolusCommand.toValidBolusAmount(maximumBolus: loopManager.settings.maximumBolus) + try await self.enactBolus(units: validBolusAmount, activationType: .manualNoRecommendation) + await triggerBackgroundUpload(for: .dose) + self.analyticsServicesManager.didBolus(source: "Remote", units: validBolusAmount) } - func enactRemoteBolus(units: Double, completion: @escaping (_ error: Error?) -> Void = { _ in }) { - - var backgroundTask: UIBackgroundTaskIdentifier? - backgroundTask = UIApplication.shared.beginBackgroundTask(withName: "Enact Remote Bolus") { - guard let backgroundTask = backgroundTask else {return} - UIApplication.shared.endBackgroundTask(backgroundTask) - self.log.error("Enact Remote Bolus background task expired") - } + //Remote Carb Entry + + func handleRemoteCarbAction(_ carbCommand: RemoteCarbAction) async throws { + let candidateCarbEntry = try carbCommand.toValidCarbEntry(defaultAbsorptionTime: carbStore.defaultAbsorptionTimes.medium, + minAbsorptionTime: LoopConstants.minCarbAbsorptionTime, + maxAbsorptionTime: LoopConstants.maxCarbAbsorptionTime, + maxCarbEntryQuantity: LoopConstants.maxCarbEntryQuantity.doubleValue(for: .gram()), + maxCarbEntryPastTime: LoopConstants.maxCarbEntryPastTime, + maxCarbEntryFutureTime: LoopConstants.maxCarbEntryFutureTime + ) - self.enactBolus(units: units, activationType: .manualNoRecommendation) { error in - self.triggerBackgroundUpload(for: .dose) - if let backgroundTask = backgroundTask { - UIApplication.shared.endBackgroundTask(backgroundTask) + let _ = try await addRemoteCarbEntry(candidateCarbEntry) + await triggerBackgroundUpload(for: .carb) + } + + //Can't add this concurrency wrapper method to LoopKit due to the minimum iOS version + func addRemoteCarbEntry(_ carbEntry: NewCarbEntry) async throws -> StoredCarbEntry { + return try await withCheckedThrowingContinuation { continuation in + carbStore.addCarbEntry(carbEntry) { result in + switch result { + case .success(let storedCarbEntry): + self.analyticsServicesManager.didAddCarbs(source: "Remote", amount: carbEntry.quantity.doubleValue(for: .gram())) + continuation.resume(returning: storedCarbEntry) + case .failure(let error): + continuation.resume(throwing: error) + } } - completion(error) - self.analyticsServicesManager.didBolus(source: "Remote", units: units) } } - func triggerBackgroundUpload(for triggeringType: RemoteDataType) { - + //Background Uploads + + func triggerBackgroundUpload(for triggeringType: RemoteDataType) async { + let backgroundTask = await beginBackgroundTask(name: "Remote Data Upload") + await remoteDataServicesManager.triggerUpload(for: triggeringType) + await endBackgroundTask(backgroundTask) + + } + + func beginBackgroundTask(name: String) async -> UIBackgroundTaskIdentifier? { var backgroundTask: UIBackgroundTaskIdentifier? - backgroundTask = UIApplication.shared.beginBackgroundTask(withName: "Remote Data Upload") { + backgroundTask = await UIApplication.shared.beginBackgroundTask(withName: name) { guard let backgroundTask = backgroundTask else {return} - UIApplication.shared.endBackgroundTask(backgroundTask) - self.log.error("Remote Data Upload background task expired") - } - - self.remoteDataServicesManager.triggerUpload(for: triggeringType) { - if let backgroundTask = backgroundTask { - UIApplication.shared.endBackgroundTask(backgroundTask) + Task { + await UIApplication.shared.endBackgroundTask(backgroundTask) } + + self.log.error("Background Task Expired: %{public}@", name) } + + return backgroundTask + } + + func endBackgroundTask(_ backgroundTask: UIBackgroundTaskIdentifier?) async { + guard let backgroundTask else {return} + await UIApplication.shared.endBackgroundTask(backgroundTask) } } diff --git a/Loop/Managers/NotificationManager.swift b/Loop/Managers/NotificationManager.swift index 1c92ffb7f2..b74be9ef72 100644 --- a/Loop/Managers/NotificationManager.swift +++ b/Loop/Managers/NotificationManager.swift @@ -78,6 +78,7 @@ extension NotificationManager { // MARK: - Notifications + @MainActor static func sendRemoteCommandExpiredNotification(timeExpired: TimeInterval) { let notification = UNMutableNotificationContent() @@ -134,6 +135,7 @@ extension NotificationManager { UNUserNotificationCenter.current().add(request) } + @MainActor static func sendRemoteBolusNotification(amount: Double) { let notification = UNMutableNotificationContent() let quantityFormatter = QuantityFormatter() @@ -157,6 +159,7 @@ extension NotificationManager { UNUserNotificationCenter.current().add(request) } + @MainActor static func sendRemoteBolusFailureNotification(for error: Error, amount: Double) { let notification = UNMutableNotificationContent() let quantityFormatter = QuantityFormatter() @@ -178,6 +181,7 @@ extension NotificationManager { UNUserNotificationCenter.current().add(request) } + @MainActor static func sendRemoteCarbEntryNotification(amountInGrams: Double) { let notification = UNMutableNotificationContent() @@ -198,6 +202,7 @@ extension NotificationManager { UNUserNotificationCenter.current().add(request) } + @MainActor static func sendRemoteCarbEntryFailureNotification(for error: Error, amountInGrams: Double) { let notification = UNMutableNotificationContent() diff --git a/Loop/Managers/RemoteDataServicesManager.swift b/Loop/Managers/RemoteDataServicesManager.swift index ccd17c7051..6b37ce6c9a 100644 --- a/Loop/Managers/RemoteDataServicesManager.swift +++ b/Loop/Managers/RemoteDataServicesManager.swift @@ -209,6 +209,14 @@ final class RemoteDataServicesManager { completion() } } + + func triggerUpload(for triggeringType: RemoteDataType) async { + return await withCheckedContinuation { continuation in + triggerUpload(for: triggeringType) { + continuation.resume(returning: ()) + } + } + } } extension RemoteDataServicesManager { diff --git a/Loop/Models/LoopConstants.swift b/Loop/Models/LoopConstants.swift index 1f439c08ef..fa76fdcfa6 100644 --- a/Loop/Models/LoopConstants.swift +++ b/Loop/Models/LoopConstants.swift @@ -21,7 +21,11 @@ enum LoopConstants { static let validManualGlucoseEntryRange = HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 10)...HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 600) + static let minCarbAbsorptionTime = TimeInterval(minutes: 30) static let maxCarbAbsorptionTime = TimeInterval(hours: 8) + + static let maxCarbEntryPastTime = TimeInterval(hours: (-12)) + static let maxCarbEntryFutureTime = TimeInterval(hours: 1) // MARK - Display settings diff --git a/Loop/Models/RemoteCommand.swift b/Loop/Models/RemoteCommand.swift index 52196b5a82..5806c308a1 100644 --- a/Loop/Models/RemoteCommand.swift +++ b/Loop/Models/RemoteCommand.swift @@ -8,78 +8,34 @@ import Foundation import LoopKit -import HealthKit - -public enum RemoteCommandError: LocalizedError { - case expired - case invalidOTP - case missingMaxBolus - case exceedsMaxBolus - case exceedsMaxCarbs - case invalidCarbs - - public var errorDescription: String? { - get { - switch self { - case .expired: - return NSLocalizedString("Expired", comment: "Remote command error description: expired.") - case .invalidOTP: - return NSLocalizedString("Invalid OTP", comment: "Remote command error description: invalid OTP.") - case .missingMaxBolus: - return NSLocalizedString("Missing maximum allowed bolus in settings", comment: "Remote command error description: missing maximum bolus in settings.") - case .exceedsMaxBolus: - return NSLocalizedString("Exceeds maximum allowed bolus in settings", comment: "Remote command error description: bolus exceeds maximum bolus in settings.") - case .exceedsMaxCarbs: - return NSLocalizedString("Exceeds maximum allowed carbs", comment: "Remote command error description: carbs exceed maximum amount.") - case .invalidCarbs: - return NSLocalizedString("Invalid carb amount", comment: "Remote command error description: invalid carb amount.") - } - } - } -} - - -enum RemoteCommand { - case temporaryScheduleOverride(TemporaryScheduleOverride) - case cancelTemporaryOverride - case bolusEntry(Double) - case carbsEntry(NewCarbEntry) -} - // Push Notifications -extension RemoteCommand { - static func createRemoteCommand(notification: [String: Any], allowedPresets: [TemporaryScheduleOverridePreset], defaultAbsorptionTime: TimeInterval, nowDate: Date = Date()) -> Result { +extension RemoteAction { + static func createRemoteAction(notification: [String: Any]) -> Result { if let overrideName = notification["override-name"] as? String, - let preset = allowedPresets.first(where: { $0.name == overrideName }), let remoteAddress = notification["remote-address"] as? String { - var override = preset.createOverride(enactTrigger: .remote(remoteAddress)) + var overrideTime: TimeInterval? = nil if let overrideDurationMinutes = notification["override-duration-minutes"] as? Double { - override.duration = .finite(TimeInterval(minutes: overrideDurationMinutes)) + overrideTime = TimeInterval(minutes: overrideDurationMinutes) } - return .success(.temporaryScheduleOverride(override)) - } else if let _ = notification["cancel-temporary-override"] as? String { - return .success(.cancelTemporaryOverride) + return .success(.temporaryScheduleOverride(RemoteOverrideAction(name: overrideName, durationTime: overrideTime, remoteAddress: remoteAddress))) + } else if let _ = notification["cancel-temporary-override"] as? String, + let remoteAddress = notification["remote-address"] as? String + { + return .success(.cancelTemporaryOverride(RemoteOverrideCancelAction(remoteAddress: remoteAddress))) } else if let bolusValue = notification["bolus-entry"] as? Double { - return .success(.bolusEntry(bolusValue)) + return .success(.bolusEntry(RemoteBolusAction(amountInUnits: bolusValue))) } else if let carbsValue = notification["carbs-entry"] as? Double { - - let minAbsorptionTime = TimeInterval(hours: 0.5) - let maxAbsorptionTime = LoopConstants.maxCarbAbsorptionTime - var absorptionTime = defaultAbsorptionTime + var absorptionTime: TimeInterval? = nil if let absorptionOverrideInHours = notification["absorption-time"] as? Double { absorptionTime = TimeInterval(hours: absorptionOverrideInHours) } - if absorptionTime < minAbsorptionTime || absorptionTime > maxAbsorptionTime { - return .failure(RemoteCommandParseError.invalidAbsorptionSeconds(absorptionTime)) - } - - let quantity = HKQuantity(unit: .gram(), doubleValue: carbsValue) + var foodType = notification["food-type"] as? String ?? nil - var startDate = nowDate + var startDate: Date? = nil if let notificationStartTimeString = notification["start-time"] as? String { let formatter = ISO8601DateFormatter() formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] @@ -90,8 +46,7 @@ extension RemoteCommand { } } - let newEntry = NewCarbEntry(quantity: quantity, startDate: startDate, foodType: "", absorptionTime: absorptionTime) - return .success(.carbsEntry(newEntry)) + return .success(.carbsEntry(RemoteCarbAction(amountInGrams: carbsValue, absorptionTime: absorptionTime, foodType: foodType, startDate: startDate))) } else { return .failure(RemoteCommandParseError.unhandledNotication("\(notification)")) } @@ -99,7 +54,6 @@ extension RemoteCommand { enum RemoteCommandParseError: LocalizedError { case invalidStartTime(String) - case invalidAbsorptionSeconds(Double) case unhandledNotication(String) } } diff --git a/LoopTests/Models/RemoteActionTests.swift b/LoopTests/Models/RemoteActionTests.swift new file mode 100644 index 0000000000..395ff0e944 --- /dev/null +++ b/LoopTests/Models/RemoteActionTests.swift @@ -0,0 +1,108 @@ +// +// RemoteActionTests.swift +// LoopTests +// +// Created by Bill Gestrich on 8/13/22. +// Copyright © 2022 LoopKit Authors. All rights reserved. +// + +import XCTest +import HealthKit +@testable import Loop +import LoopKit + +class RemoteActionTests: XCTestCase { + + override func setUpWithError() throws { + } + + override func tearDownWithError() throws { + } + + + //MARK: Carb Entry Command + + func testParseCarbEntryNotification_ValidPayload_Succeeds() throws { + + //Arrange + let expectedStartDateString = "2022-08-14T03:08:00.000Z" + let expectedCarbsInGrams = 15.0 + let expectedDate = dateFormatter().date(from: expectedStartDateString)! + let expectedAbsorptionTimeInHours = 3.0 + let expectedFoodType = "🍕" + let otp = 12345 + let notification: [String: Any] = [ + "carbs-entry":expectedCarbsInGrams, + "absorption-time": expectedAbsorptionTimeInHours, + "food-type": expectedFoodType, + "otp": otp, + "start-time": expectedStartDateString + ] + + //Act + let action = try RemoteAction.createRemoteAction(notification: notification).get() + + //Assert + guard case .carbsEntry(let carbEntry) = action else { + XCTFail("Incorrect case") + return + } + XCTAssertEqual(carbEntry.startDate, expectedDate) + XCTAssertEqual(carbEntry.absorptionTime, TimeInterval(hours: expectedAbsorptionTimeInHours)) + XCTAssertEqual(carbEntry.amountInGrams, expectedCarbsInGrams) + XCTAssertEqual(expectedFoodType, carbEntry.foodType) + } + + func testParseCarbEntryNotification_MissingCreatedDate_Succeeds() throws { + + //Arrange + let expectedCarbsInGrams = 15.0 + let expectedAbsorptionTimeInHours = 3.0 + let otp = 12345 + let notification: [String: Any] = [ + "carbs-entry":expectedCarbsInGrams, + "absorption-time": expectedAbsorptionTimeInHours, + "otp": otp + ] + + //Act + let action = try RemoteAction.createRemoteAction(notification: notification).get() + + //Assert + guard case .carbsEntry(let carbEntry) = action else { + XCTFail("Incorrect case") + return + } + + XCTAssertEqual(carbEntry.startDate, nil) + XCTAssertEqual(carbEntry.absorptionTime, TimeInterval(hours: expectedAbsorptionTimeInHours)) + XCTAssertEqual(carbEntry.amountInGrams, expectedCarbsInGrams) + } + + func testParseCarbEntryNotification_InvalidCreatedDate_Fails() throws { + + //Arrange + let expectedCarbsInGrams = 15.0 + let expectedAbsorptionTimeInHours = 3.0 + let otp = 12345 + let notification: [String: Any] = [ + "carbs-entry": expectedCarbsInGrams, + "absorption-time":expectedAbsorptionTimeInHours, + "otp": otp, + "start-time": "invalid-date-string" + ] + + //Act + Assert + XCTAssertThrowsError(try RemoteAction.createRemoteAction(notification: notification).get()) + } + + + //MARK: Utils + + func dateFormatter() -> ISO8601DateFormatter { + let formatter = ISO8601DateFormatter() + formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + return formatter + } + +} diff --git a/LoopTests/Models/RemoteCommandTests.swift b/LoopTests/Models/RemoteCommandTests.swift deleted file mode 100644 index 58cd8c650c..0000000000 --- a/LoopTests/Models/RemoteCommandTests.swift +++ /dev/null @@ -1,225 +0,0 @@ -// -// RemoteCommandTests.swift -// LoopTests -// -// Created by Bill Gestrich on 8/13/22. -// Copyright © 2022 LoopKit Authors. All rights reserved. -// - -import XCTest -import HealthKit -@testable import Loop - -class RemoteCommandTests: XCTestCase { - - override func setUpWithError() throws { - } - - override func tearDownWithError() throws { - } - - - //MARK: Carb Entry Command - - func testParseCarbEntryNotification_ValidPayload_Succeeds() throws { - - //Arrange - let expectedStartDateString = "2022-08-14T03:08:00.000Z" - let expectedCarbsInGrams = 15.0 - let expectedDate = dateFormatter().date(from: expectedStartDateString)! - let expectedAbsorptionTimeInHours = 3.0 - let otp = 12345 - let notification: [String: Any] = [ - "carbs-entry":expectedCarbsInGrams, - "absorption-time": expectedAbsorptionTimeInHours, - "otp": otp, - "start-time": expectedStartDateString - ] - - //Act - let command = try RemoteCommand.createRemoteCommand(notification: notification, allowedPresets: [], defaultAbsorptionTime: TimeInterval(hours: 4.0)).get() - - //Assert - guard case .carbsEntry(let carbEntry) = command else { - XCTFail("Incorrect case") - return - } - XCTAssertEqual(carbEntry.startDate, expectedDate) - XCTAssertEqual(carbEntry.absorptionTime, TimeInterval(hours: expectedAbsorptionTimeInHours)) - XCTAssertEqual(carbEntry.quantity, HKQuantity(unit: .gram(), doubleValue: expectedCarbsInGrams)) - } - - func testParseCarbEntryNotification_MissingCreatedDate_Succeeds() throws { - - //Arrange - let expectedStartDate = Date() - let expectedCarbsInGrams = 15.0 - let expectedAbsorptionTimeInHours = 3.0 - let otp = 12345 - let notification: [String: Any] = [ - "carbs-entry":expectedCarbsInGrams, - "absorption-time": expectedAbsorptionTimeInHours, - "otp": otp - ] - - //Act - let command = try RemoteCommand.createRemoteCommand(notification: notification, allowedPresets: [], defaultAbsorptionTime: TimeInterval(hours: 4.0), nowDate: expectedStartDate).get() - - //Assert - guard case .carbsEntry(let carbEntry) = command else { - XCTFail("Incorrect case") - return - } - - XCTAssertEqual(carbEntry.startDate, expectedStartDate) - XCTAssertEqual(carbEntry.absorptionTime, TimeInterval(hours: expectedAbsorptionTimeInHours)) - XCTAssertEqual(carbEntry.quantity, HKQuantity(unit: .gram(), doubleValue: expectedCarbsInGrams)) - } - - func testParseCarbEntryNotification_InvalidCreatedDate_Fails() throws { - - //Arrange - let expectedCarbsInGrams = 15.0 - let expectedAbsorptionTimeInHours = 3.0 - let otp = 12345 - let notification: [String: Any] = [ - "carbs-entry": expectedCarbsInGrams, - "absorption-time":expectedAbsorptionTimeInHours, - "otp": otp, - "start-time": "invalid-date-string" - ] - - //Act + Assert - XCTAssertThrowsError(try RemoteCommand.createRemoteCommand(notification: notification, allowedPresets: [], defaultAbsorptionTime: TimeInterval(hours: 4.0)).get()) - } - - func testParseCarbEntryNotification_MissingAbsorptionHours_UsesDefaultAbsorption() throws { - - //Arrange - let expectedStartDateString = "2022-08-14T03:08:00.000Z" - let expectedCarbsInGrams = 15.0 - let expectedDate = dateFormatter().date(from: expectedStartDateString)! - let expectedAbsorptionTimeInHours = 4.0 - let otp = 12345 - let notification: [String: Any] = [ - "carbs-entry": expectedCarbsInGrams, - "otp": otp, - "start-time": expectedStartDateString - ] - - //Act - let command = try RemoteCommand.createRemoteCommand(notification: notification, allowedPresets: [], defaultAbsorptionTime: TimeInterval(hours: expectedAbsorptionTimeInHours)).get() - - //Assert - guard case .carbsEntry(let carbEntry) = command else { - XCTFail("Incorrect case") - return - } - XCTAssertEqual(carbEntry.startDate, expectedDate) - XCTAssertEqual(carbEntry.absorptionTime, TimeInterval(hours: expectedAbsorptionTimeInHours)) - XCTAssertEqual(carbEntry.quantity, HKQuantity(unit: .gram(), doubleValue: expectedCarbsInGrams)) - } - - - func testParseCarbEntryNotification_AtMinAbsorptionHours_Succeeds() throws { - - //Arrange - let expectedStartDateString = "2022-08-14T03:08:00.000Z" - let expectedCarbsInGrams = 15.0 - let expectedDate = dateFormatter().date(from: expectedStartDateString)! - let expectedAbsorptionTimeInHours = 0.5 - let otp = 12345 - let notification: [String: Any] = [ - "carbs-entry": expectedCarbsInGrams, - "absorption-time":expectedAbsorptionTimeInHours, - "otp": otp, - "start-time": expectedStartDateString - ] - - //Act - let command = try RemoteCommand.createRemoteCommand(notification: notification, allowedPresets: [], defaultAbsorptionTime: TimeInterval(hours: 4.0)).get() - - //Assert - guard case .carbsEntry(let carbEntry) = command else { - XCTFail("Incorrect case") - return - } - XCTAssertEqual(carbEntry.startDate, expectedDate) - XCTAssertEqual(carbEntry.absorptionTime, TimeInterval(hours: expectedAbsorptionTimeInHours)) - XCTAssertEqual(carbEntry.quantity, HKQuantity(unit: .gram(), doubleValue: expectedCarbsInGrams)) - } - - func testParseCarbEntryNotification_BelowMinAbsorptionHours_Fails() throws { - - //Arrange - let expectedStartDateString = "2022-08-14T03:08:00.000Z" - let expectedCarbsInGrams = 15.0 - let expectedAbsorptionTimeInHours = 0.4 - let otp = 12345 - let notification: [String: Any] = [ - "carbs-entry": expectedCarbsInGrams, - "absorption-time":expectedAbsorptionTimeInHours, - "otp": otp, - "start-time": expectedStartDateString - ] - - //Act + Assert - XCTAssertThrowsError(try RemoteCommand.createRemoteCommand(notification: notification, allowedPresets: [], defaultAbsorptionTime: TimeInterval(hours: 4.0)).get()) - } - - func testParseCarbEntryNotification_AtMaxAbsorptionHours_Succeeds() throws { - - //Arrange - let expectedStartDateString = "2022-08-14T03:08:00.000Z" - let expectedCarbsInGrams = 15.0 - let expectedDate = dateFormatter().date(from: expectedStartDateString)! - let expectedAbsorptionTimeInHours = 8.0 - let otp = 12345 - let notification: [String: Any] = [ - "carbs-entry": expectedCarbsInGrams, - "absorption-time":expectedAbsorptionTimeInHours, - "otp": otp, - "start-time": expectedStartDateString - ] - - //Act - let command = try RemoteCommand.createRemoteCommand(notification: notification, allowedPresets: [], defaultAbsorptionTime: TimeInterval(hours: 4.0)).get() - - //Assert - guard case .carbsEntry(let carbEntry) = command else { - XCTFail("Incorrect case") - return - } - XCTAssertEqual(carbEntry.startDate, expectedDate) - XCTAssertEqual(carbEntry.absorptionTime, TimeInterval(hours: expectedAbsorptionTimeInHours)) - XCTAssertEqual(carbEntry.quantity, HKQuantity(unit: .gram(), doubleValue: expectedCarbsInGrams)) - } - - func testParseCarbEntryNotification_AboveMaxAbsorptionHours_Fails() throws { - - //Arrange - let expectedStartDateString = "2022-08-14T03:08:00.000Z" - let expectedCarbsInGrams = 15.0 - let expectedAbsorptionTimeInHours = 8.1 - let otp = 12345 - let notification: [String: Any] = [ - "carbs-entry": expectedCarbsInGrams, - "absorption-time":expectedAbsorptionTimeInHours, - "otp": otp, - "start-time": expectedStartDateString - ] - - //Act + Assert - XCTAssertThrowsError(try RemoteCommand.createRemoteCommand(notification: notification, allowedPresets: [], defaultAbsorptionTime: TimeInterval(hours: 4.0)).get()) - } - - - //MARK: Utils - - func dateFormatter() -> ISO8601DateFormatter { - let formatter = ISO8601DateFormatter() - formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] - return formatter - } - -} From 6f321afba1f02861ab695f99883eb51337419394 Mon Sep 17 00:00:00 2001 From: Bill Gestrich <3207996+gestrich@users.noreply.github.com> Date: Tue, 21 Feb 2023 18:22:18 -0500 Subject: [PATCH 2/5] Move RemoteAction validation to extensions in Loop target --- Loop.xcodeproj/project.pbxproj | 42 +- Loop/Models/LoopConstants.swift | 1 + Loop/Models/Remote/RemoteBolusAction.swift | 46 ++ Loop/Models/Remote/RemoteCarbAction.swift | 73 ++++ Loop/Models/Remote/RemoteOverrideAction.swift | 57 +++ .../{ => Remote}/RemoteActionTests.swift | 0 .../Remote/RemoteBolusActionTests.swift | 101 +++++ .../Models/Remote/RemoteCarbActionTests.swift | 402 ++++++++++++++++++ .../Remote/RemoteOverrideActionTests.swift | 152 +++++++ 9 files changed, 873 insertions(+), 1 deletion(-) create mode 100644 Loop/Models/Remote/RemoteBolusAction.swift create mode 100644 Loop/Models/Remote/RemoteCarbAction.swift create mode 100644 Loop/Models/Remote/RemoteOverrideAction.swift rename LoopTests/Models/{ => Remote}/RemoteActionTests.swift (100%) create mode 100644 LoopTests/Models/Remote/RemoteBolusActionTests.swift create mode 100644 LoopTests/Models/Remote/RemoteCarbActionTests.swift create mode 100644 LoopTests/Models/Remote/RemoteOverrideActionTests.swift diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index f42ed34510..ddf4178b2f 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -389,6 +389,12 @@ A98556852493F901000FD662 /* AlertStore+SimulatedCoreData.swift in Sources */ = {isa = PBXBuildFile; fileRef = A98556842493F901000FD662 /* AlertStore+SimulatedCoreData.swift */; }; A987CD4924A58A0100439ADC /* ZipArchive.swift in Sources */ = {isa = PBXBuildFile; fileRef = A987CD4824A58A0100439ADC /* ZipArchive.swift */; }; A999D40624663D18004C89D4 /* PumpManagerError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A999D40524663D18004C89D4 /* PumpManagerError.swift */; }; + A99A114229A581F4007919CE /* RemoteBolusAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = A99A114129A581F4007919CE /* RemoteBolusAction.swift */; }; + A99A114429A5829A007919CE /* RemoteCarbAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = A99A114329A5829A007919CE /* RemoteCarbAction.swift */; }; + A99A114629A582A2007919CE /* RemoteOverrideAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = A99A114529A582A2007919CE /* RemoteOverrideAction.swift */; }; + A99A114E29A5879D007919CE /* RemoteBolusActionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A99A114B29A5879C007919CE /* RemoteBolusActionTests.swift */; }; + A99A114F29A5879D007919CE /* RemoteCarbActionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A99A114C29A5879C007919CE /* RemoteCarbActionTests.swift */; }; + A99A115029A5879D007919CE /* RemoteOverrideActionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A99A114D29A5879C007919CE /* RemoteOverrideActionTests.swift */; }; A9A056B324B93C62007CF06D /* CriticalEventLogExportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9A056B224B93C62007CF06D /* CriticalEventLogExportView.swift */; }; A9A056B524B94123007CF06D /* CriticalEventLogExportViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9A056B424B94123007CF06D /* CriticalEventLogExportViewModel.swift */; }; A9A63F8E246B271600588D5B /* NSTimeInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = 439897341CD2F7DE00223065 /* NSTimeInterval.swift */; }; @@ -1386,6 +1392,12 @@ A98556842493F901000FD662 /* AlertStore+SimulatedCoreData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AlertStore+SimulatedCoreData.swift"; sourceTree = ""; }; A987CD4824A58A0100439ADC /* ZipArchive.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZipArchive.swift; sourceTree = ""; }; A999D40524663D18004C89D4 /* PumpManagerError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PumpManagerError.swift; sourceTree = ""; }; + A99A114129A581F4007919CE /* RemoteBolusAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteBolusAction.swift; sourceTree = ""; }; + A99A114329A5829A007919CE /* RemoteCarbAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteCarbAction.swift; sourceTree = ""; }; + A99A114529A582A2007919CE /* RemoteOverrideAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteOverrideAction.swift; sourceTree = ""; }; + A99A114B29A5879C007919CE /* RemoteBolusActionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteBolusActionTests.swift; sourceTree = ""; }; + A99A114C29A5879C007919CE /* RemoteCarbActionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteCarbActionTests.swift; sourceTree = ""; }; + A99A114D29A5879C007919CE /* RemoteOverrideActionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteOverrideActionTests.swift; sourceTree = ""; }; A9A056B224B93C62007CF06D /* CriticalEventLogExportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CriticalEventLogExportView.swift; sourceTree = ""; }; A9A056B424B94123007CF06D /* CriticalEventLogExportViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CriticalEventLogExportViewModel.swift; sourceTree = ""; }; A9B607AF247F000F00792BE4 /* UserNotifications+Loop.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserNotifications+Loop.swift"; sourceTree = ""; }; @@ -2020,6 +2032,7 @@ 43757D131C06F26C00910CB9 /* Models */ = { isa = PBXGroup; children = ( + A99A114029A581D6007919CE /* Remote */, 43511CDD21FD80AD00566C63 /* RetrospectiveCorrection */, A9FB75F0252BE320004C7D3F /* BolusDosingDecision.swift */, C17824A41E1AD4D100D9D25C /* ManualBolusRecommendation.swift */, @@ -2786,6 +2799,27 @@ path = Shortcuts; sourceTree = ""; }; + A99A114029A581D6007919CE /* Remote */ = { + isa = PBXGroup; + children = ( + A99A114129A581F4007919CE /* RemoteBolusAction.swift */, + A99A114329A5829A007919CE /* RemoteCarbAction.swift */, + A99A114529A582A2007919CE /* RemoteOverrideAction.swift */, + ); + path = Remote; + sourceTree = ""; + }; + A99A114A29A58789007919CE /* Remote */ = { + isa = PBXGroup; + children = ( + A9E8A80428A7CAC000C0A8A4 /* RemoteActionTests.swift */, + A99A114B29A5879C007919CE /* RemoteBolusActionTests.swift */, + A99A114C29A5879C007919CE /* RemoteCarbActionTests.swift */, + A99A114D29A5879C007919CE /* RemoteOverrideActionTests.swift */, + ); + path = Remote; + sourceTree = ""; + }; A9E6DFE4246A0418005B1A1C /* Extensions */ = { isa = PBXGroup; children = ( @@ -2797,13 +2831,13 @@ A9E6DFED246A0460005B1A1C /* Models */ = { isa = PBXGroup; children = ( + A99A114A29A58789007919CE /* Remote */, A9DFAFB224F0415E00950D1E /* CarbBackfillRequestUserInfoTests.swift */, A963B279252CEBAE0062AA12 /* SetBolusUserInfoTests.swift */, A9DFAFB424F048A000950D1E /* WatchHistoricalCarbsTests.swift */, C19008FF252271BB00721625 /* SimpleBolusCalculatorTests.swift */, A9C1719625366F780053BCBD /* WatchHistoricalGlucoseTest.swift */, A9BD28E6272226B40071DF15 /* TestLocalizedError.swift */, - A9E8A80428A7CAC000C0A8A4 /* RemoteActionTests.swift */, ); path = Models; sourceTree = ""; @@ -3786,6 +3820,7 @@ C17824A01E19CF9800D9D25C /* GlucoseThresholdTableViewController.swift in Sources */, 4372E487213C86240068E043 /* SampleValue.swift in Sources */, 437CEEE41CDE5C0A003C8C80 /* UIImage.swift in Sources */, + A99A114229A581F4007919CE /* RemoteBolusAction.swift in Sources */, C1201E2C23ECDBD0002DA84A /* WatchContextRequestUserInfo.swift in Sources */, 1D49795824E7289700948F05 /* ServicesViewModel.swift in Sources */, 1D4A3E2D2478628500FD601B /* StoredAlert+CoreDataClass.swift in Sources */, @@ -3845,6 +3880,7 @@ 1D6B1B6726866D89009AC446 /* AlertPermissionsChecker.swift in Sources */, 4F08DE8F1E7BB871006741EA /* CollectionType+Loop.swift in Sources */, A9F703772489D8AA00C98AD8 /* PersistentDeviceLog+SimulatedCoreData.swift in Sources */, + A99A114629A582A2007919CE /* RemoteOverrideAction.swift in Sources */, E9B080B1253BDA6300BAD8F8 /* UserDefaults+LoopIntents.swift in Sources */, C1AF062329426300002C1B19 /* ManualGlucoseEntryRow.swift in Sources */, C148CEE724FD91BD00711B3B /* DeliveryUncertaintyAlertManager.swift in Sources */, @@ -3892,6 +3928,7 @@ A9C62D842331700E00535612 /* DiagnosticLog+Subsystem.swift in Sources */, 895FE0952201234000FCF18A /* OverrideSelectionViewController.swift in Sources */, C1EF747228D6A44A00C8C083 /* CrashRecoveryManager.swift in Sources */, + A99A114429A5829A007919CE /* RemoteCarbAction.swift in Sources */, A9F66FC3247F451500096EA7 /* UIDevice+Loop.swift in Sources */, 439706E622D2E84900C81566 /* PredictionSettingTableViewCell.swift in Sources */, 430D85891F44037000AF2D4F /* HUDViewTableViewCell.swift in Sources */, @@ -4092,10 +4129,12 @@ buildActionMask = 2147483647; files = ( A9DF02CB24F72B9E00B7C988 /* CriticalEventLogTests.swift in Sources */, + A99A114F29A5879D007919CE /* RemoteCarbActionTests.swift in Sources */, B44251B3252350CE00605937 /* ChartAxisValuesStaticGeneratorTests.swift in Sources */, 1D80313D24746274002810DF /* AlertStoreTests.swift in Sources */, C1777A6625A125F100595963 /* ManualEntryDoseViewModelTests.swift in Sources */, C16B984026B4898800256B05 /* DoseEnactorTests.swift in Sources */, + A99A115029A5879D007919CE /* RemoteOverrideActionTests.swift in Sources */, A9A63F8E246B271600588D5B /* NSTimeInterval.swift in Sources */, A9DFAFB324F0415E00950D1E /* CarbBackfillRequestUserInfoTests.swift in Sources */, A963B27A252CEBAE0062AA12 /* SetBolusUserInfoTests.swift in Sources */, @@ -4113,6 +4152,7 @@ B4CAD8792549D2540057946B /* LoopCompletionFreshnessTests.swift in Sources */, 1D8D55BC252274650044DBB6 /* BolusEntryViewModelTests.swift in Sources */, A91E4C2124F867A700BE9213 /* StoredAlertTests.swift in Sources */, + A99A114E29A5879D007919CE /* RemoteBolusActionTests.swift in Sources */, 1DA7A84224476EAD008257F0 /* AlertManagerTests.swift in Sources */, A91E4C2324F86F1000BE9213 /* CriticalEventLogExportManagerTests.swift in Sources */, E9C58A7324DB4A2700487A17 /* LoopDataManagerTests.swift in Sources */, diff --git a/Loop/Models/LoopConstants.swift b/Loop/Models/LoopConstants.swift index fa76fdcfa6..a62fc13849 100644 --- a/Loop/Models/LoopConstants.swift +++ b/Loop/Models/LoopConstants.swift @@ -27,6 +27,7 @@ enum LoopConstants { static let maxCarbEntryPastTime = TimeInterval(hours: (-12)) static let maxCarbEntryFutureTime = TimeInterval(hours: 1) + static let maxOverrideDurationTime = TimeInterval(hours: 24) // MARK - Display settings diff --git a/Loop/Models/Remote/RemoteBolusAction.swift b/Loop/Models/Remote/RemoteBolusAction.swift new file mode 100644 index 0000000000..e75d66d73b --- /dev/null +++ b/Loop/Models/Remote/RemoteBolusAction.swift @@ -0,0 +1,46 @@ +// +// RemoteBolusAction.swift +// Loop +// +// Created by Bill Gestrich on 2/21/23. +// Copyright © 2023 LoopKit Authors. All rights reserved. +// + +import LoopKit + +extension RemoteBolusAction { + public func toValidBolusAmount(maximumBolus: Double?) throws -> Double { + + guard amountInUnits > 0 else { + throw RemoteBolusActionError.invalidBolus + } + + guard let maxBolusAmount = maximumBolus else { + throw RemoteBolusActionError.missingMaxBolus + } + + guard amountInUnits <= maxBolusAmount else { + throw RemoteBolusActionError.exceedsMaxBolus + } + + return amountInUnits + } +} + +public enum RemoteBolusActionError: LocalizedError { + + case invalidBolus + case missingMaxBolus + case exceedsMaxBolus + + public var errorDescription: String? { + switch self { + case .invalidBolus: + return NSLocalizedString("Invalid Bolus Amount", comment: "Remote command error description: invalid bolus amount.") + case .missingMaxBolus: + return NSLocalizedString("Missing maximum allowed bolus in settings", comment: "Remote command error description: missing maximum bolus in settings.") + case .exceedsMaxBolus: + return NSLocalizedString("Exceeds maximum allowed bolus in settings", comment: "Remote command error description: bolus exceeds maximum bolus in settings.") + } + } +} diff --git a/Loop/Models/Remote/RemoteCarbAction.swift b/Loop/Models/Remote/RemoteCarbAction.swift new file mode 100644 index 0000000000..143843eea0 --- /dev/null +++ b/Loop/Models/Remote/RemoteCarbAction.swift @@ -0,0 +1,73 @@ +// +// RemoteCarbAction.swift +// Loop +// +// Created by Bill Gestrich on 2/21/23. +// Copyright © 2023 LoopKit Authors. All rights reserved. +// + +import LoopKit +import HealthKit + +extension RemoteCarbAction { + + public func toValidCarbEntry(defaultAbsorptionTime: TimeInterval, + minAbsorptionTime: TimeInterval, + maxAbsorptionTime: TimeInterval, + maxCarbEntryQuantity: Double, + maxCarbEntryPastTime: TimeInterval, + maxCarbEntryFutureTime: TimeInterval, + nowDate: Date = Date()) throws -> NewCarbEntry { + + let absorptionTime = absorptionTime ?? defaultAbsorptionTime + if absorptionTime < minAbsorptionTime || absorptionTime > maxAbsorptionTime { + throw RemoteCarbActionError.invalidAbsorptionTime(absorptionTime) + } + + guard amountInGrams > 0.0 else { + throw RemoteCarbActionError.invalidCarbs + } + + guard amountInGrams <= maxCarbEntryQuantity else { + throw RemoteCarbActionError.exceedsMaxCarbs + } + + if let startDate = startDate { + let maxStartDate = nowDate.addingTimeInterval(maxCarbEntryFutureTime) + let minStartDate = nowDate.addingTimeInterval(maxCarbEntryPastTime) + guard startDate <= maxStartDate && startDate >= minStartDate else { + throw RemoteCarbActionError.invalidStartDate(startDate) + } + } + + let quantity = HKQuantity(unit: .gram(), doubleValue: amountInGrams) + return NewCarbEntry(quantity: quantity, startDate: startDate ?? nowDate, foodType: foodType, absorptionTime: absorptionTime) + } +} + +enum RemoteCarbActionError: LocalizedError { + + case invalidAbsorptionTime(TimeInterval) + case invalidStartDate(Date) + case exceedsMaxCarbs + case invalidCarbs + + public var errorDescription: String? { + switch self { + case .exceedsMaxCarbs: + return NSLocalizedString("Exceeds maximum allowed carbs", comment: "Remote command error description: carbs exceed maximum amount.") + case .invalidCarbs: + return NSLocalizedString("Invalid carb amount", comment: "Remote command error description: invalid carb amount.") + case .invalidAbsorptionTime(let absorptionTime): + return String(format: NSLocalizedString("Invalid absorption time: %d hours", comment: "Remote command error description: invalid absorption time."), absorptionTime.hours) + case .invalidStartDate(let startDate): + return String(format: NSLocalizedString("Start time is out of range: %@", comment: "Remote command error description: invalid start time is out of range."), Self.dateFormatter.string(from: startDate)) + } + } + + static var dateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.timeStyle = .medium + return formatter + }() +} diff --git a/Loop/Models/Remote/RemoteOverrideAction.swift b/Loop/Models/Remote/RemoteOverrideAction.swift new file mode 100644 index 0000000000..04d3f5fe8f --- /dev/null +++ b/Loop/Models/Remote/RemoteOverrideAction.swift @@ -0,0 +1,57 @@ +// +// RemoteOverrideAction.swift +// Loop +// +// Created by Bill Gestrich on 2/21/23. +// Copyright © 2023 LoopKit Authors. All rights reserved. +// + +import LoopKit + +extension RemoteOverrideAction { + + public func toValidOverride(allowedPresets: [TemporaryScheduleOverridePreset]) throws -> TemporaryScheduleOverride { + guard let preset = allowedPresets.first(where: { $0.name == name }) else { + throw RemoteOverrideActionError.unknownPreset(name) + } + + var remoteOverride = preset.createOverride(enactTrigger: .remote(remoteAddress)) + + if let durationTime = durationTime { + + guard durationTime <= LoopConstants.maxOverrideDurationTime else { + throw RemoteOverrideActionError.durationExceedsMax(LoopConstants.maxOverrideDurationTime) + } + + guard durationTime >= 0 else { + throw RemoteOverrideActionError.negativeDuration + } + + if durationTime == 0 { + remoteOverride.duration = .indefinite + } else { + remoteOverride.duration = .finite(durationTime) + } + } + + return remoteOverride + } +} + +public enum RemoteOverrideActionError: LocalizedError { + + case unknownPreset(String) + case durationExceedsMax(TimeInterval) + case negativeDuration + + public var errorDescription: String? { + switch self { + case .unknownPreset(let presetName): + return String(format: NSLocalizedString("Unknown preset: %1$@", comment: "Remote command error description: unknown preset (1: preset name)."), presetName) + case .durationExceedsMax(let maxDurationTime): + return String(format: NSLocalizedString("Duration exceeds: %1$.1f hours", comment: "Remote command error description: duration exceed max (1: max duration in hours)."), maxDurationTime.hours) + case .negativeDuration: + return String(format: NSLocalizedString("Negative duration not allowed", comment: "Remote command error description: negative duration error.")) + } + } +} diff --git a/LoopTests/Models/RemoteActionTests.swift b/LoopTests/Models/Remote/RemoteActionTests.swift similarity index 100% rename from LoopTests/Models/RemoteActionTests.swift rename to LoopTests/Models/Remote/RemoteActionTests.swift diff --git a/LoopTests/Models/Remote/RemoteBolusActionTests.swift b/LoopTests/Models/Remote/RemoteBolusActionTests.swift new file mode 100644 index 0000000000..d101bc3e13 --- /dev/null +++ b/LoopTests/Models/Remote/RemoteBolusActionTests.swift @@ -0,0 +1,101 @@ +// +// RemoteBolusActionTests.swift +// LoopKitTests +// +// Created by Bill Gestrich on 1/14/23. +// Copyright © 2023 LoopKit Authors. All rights reserved. +// + +import XCTest +@testable import Loop +import LoopKit + +final class RemoteBolusActionTests: XCTestCase { + + override func setUpWithError() throws { + } + + override func tearDownWithError() throws { + } + + func testToValidBolusAtMaxAmount_Succeeds() throws { + + //Arrange + let maxBolusAmount = 10.0 + let bolusAmount = maxBolusAmount + let action = RemoteBolusAction(amountInUnits: bolusAmount) + + //Act + let validatedBolusAmount = try action.toValidBolusAmount(maximumBolus: 10.0) + + //Assert + XCTAssertEqual(validatedBolusAmount, bolusAmount) + + } + + func testToValidBolusAmount_AboveMaxAmount_Fails() throws { + + //Arrange + let maxBolusAmount = 10.0 + let bolusAmount = maxBolusAmount + 0.1 + let action = RemoteBolusAction(amountInUnits: bolusAmount) + + //Act + var thrownError: Error? = nil + do { + let _ = try action.toValidBolusAmount(maximumBolus: maxBolusAmount) + } catch { + thrownError = error + } + + //Assert + guard let validationError = thrownError as? RemoteBolusActionError, case .exceedsMaxBolus = validationError else { + XCTFail("Unexpected type \(thrownError.debugDescription)") + return + } + } + + func testToValidBolusAmount_AtZero_Fails() throws { + + //Arrange + let bolusAmount = 0.0 + let action = RemoteBolusAction(amountInUnits: bolusAmount) + + //Act + var thrownError: Error? = nil + do { + let _ = try action.toValidBolusAmount(maximumBolus: 10.0) + } catch { + thrownError = error + } + + //Assert + guard let validationError = thrownError as? RemoteBolusActionError, case .invalidBolus = validationError else { + XCTFail("Unexpected type \(thrownError.debugDescription)") + return + } + } + + func testToValidBolusAmount_NegativeAmount_Fails() throws { + + //Arrange + let bolusAmount = -1.0 + let action = RemoteBolusAction(amountInUnits: bolusAmount) + + //Act + var thrownError: Error? = nil + do { + let _ = try action.toValidBolusAmount(maximumBolus: 10.0) + } catch { + thrownError = error + } + + //Assert + guard let validationError = thrownError as? RemoteBolusActionError, case .invalidBolus = validationError else { + XCTFail("Unexpected type \(thrownError.debugDescription)") + return + } + } + + +} diff --git a/LoopTests/Models/Remote/RemoteCarbActionTests.swift b/LoopTests/Models/Remote/RemoteCarbActionTests.swift new file mode 100644 index 0000000000..80b8647dd1 --- /dev/null +++ b/LoopTests/Models/Remote/RemoteCarbActionTests.swift @@ -0,0 +1,402 @@ +// +// RemoteCarbActionTests.swift +// LoopTests +// +// Created by Bill Gestrich on 1/14/23. +// Copyright © 2022 LoopKit Authors. All rights reserved. +// + +import XCTest +import HealthKit +@testable import Loop +import LoopKit + +class RemoteCarbActionTests: XCTestCase { + + override func setUpWithError() throws { + } + + override func tearDownWithError() throws { + } + + + func testToValidCarbEntry_Succeeds() throws { + + //Arrange + let expectedCarbsInGrams = 15.0 + let expectedDate = Date() + let expectedAbsorptionTime = TimeInterval(hours: 4.0) + let foodType = "🍕" + + let action = RemoteCarbAction(amountInGrams: expectedCarbsInGrams, absorptionTime: expectedAbsorptionTime, foodType: foodType, startDate: expectedDate) + + //Act + let carbEntry = try action.toValidCarbEntry(defaultAbsorptionTime: TimeInterval(hours: 3.0), + minAbsorptionTime: TimeInterval(hours: 0.5), + maxAbsorptionTime: TimeInterval(hours: 5.0), + maxCarbEntryQuantity: expectedCarbsInGrams, + maxCarbEntryPastTime: .hours(-12), + maxCarbEntryFutureTime: .hours(1) + ) + + //Assert + XCTAssertEqual(carbEntry.startDate, expectedDate) + XCTAssertEqual(carbEntry.absorptionTime, expectedAbsorptionTime) + XCTAssertEqual(carbEntry.quantity, HKQuantity(unit: .gram(), doubleValue: expectedCarbsInGrams)) + XCTAssertEqual(carbEntry.foodType, foodType) + } + + func testToValidCarbEntry_MissingAbsorptionHours_UsesDefaultAbsorption() throws { + + //Arrange + let defaultAbsorptionTime = TimeInterval(hours: 4.0) + let action = RemoteCarbAction(amountInGrams: 15.0, startDate: Date()) + + //Act + let carbEntry = try action.toValidCarbEntry(defaultAbsorptionTime: defaultAbsorptionTime, + minAbsorptionTime: TimeInterval(hours: 0.5), + maxAbsorptionTime: TimeInterval(hours: 5.0), + maxCarbEntryQuantity: 200, + maxCarbEntryPastTime: .hours(-12), + maxCarbEntryFutureTime: .hours(1)) + + //Assert + XCTAssertEqual(carbEntry.absorptionTime, defaultAbsorptionTime) + } + + func testToValidCarbEntry_AtMinAbsorptionHours_Succeeds() throws { + + //Arrange + let minAbsorptionTime = TimeInterval(hours: 0.5) + let action = RemoteCarbAction(amountInGrams: 15.0, + absorptionTime: minAbsorptionTime, + startDate: Date()) + + //Act + let carbEntry = try action.toValidCarbEntry(defaultAbsorptionTime: minAbsorptionTime, + minAbsorptionTime: minAbsorptionTime, + maxAbsorptionTime: TimeInterval(hours: 5.0), + maxCarbEntryQuantity: 200, + maxCarbEntryPastTime: .hours(-12), + maxCarbEntryFutureTime: .hours(1)) + + //Assert + XCTAssertEqual(carbEntry.absorptionTime, minAbsorptionTime) + } + + func testToValidCarbEntry_BelowMinAbsorptionHours_Fails() throws { + + //Arrange + let minAbsorptionTime = TimeInterval(hours: 0.5) + let aborptionOverrideTime = TimeInterval(hours: 0.4) + let action = RemoteCarbAction(amountInGrams: 15.0, + absorptionTime: aborptionOverrideTime, + startDate: Date()) + + //Act + var thrownError: Error? = nil + do { + let _ = try action.toValidCarbEntry(defaultAbsorptionTime: minAbsorptionTime, + minAbsorptionTime: minAbsorptionTime, + maxAbsorptionTime: TimeInterval(hours: 5.0), + maxCarbEntryQuantity: 200, + maxCarbEntryPastTime: .hours(-12), + maxCarbEntryFutureTime: .hours(1) + ) + } catch { + thrownError = error + } + + //Assert + guard let validationError = thrownError as? RemoteCarbActionError, case .invalidAbsorptionTime = validationError else { + XCTFail("Unexpected type \(thrownError.debugDescription)") + return + } + } + + func testToValidCarbEntry_AtMaxAbsorptionHours_Succeeds() throws { + + //Arrange + let maxAbsorptionTime = TimeInterval(hours: 5.0) + let action = RemoteCarbAction(amountInGrams: 15.0, + absorptionTime: maxAbsorptionTime, + startDate: Date()) + + //Act + let carbEntry = try action.toValidCarbEntry(defaultAbsorptionTime: maxAbsorptionTime, + minAbsorptionTime: TimeInterval(hours: 0.5), + maxAbsorptionTime: maxAbsorptionTime, + maxCarbEntryQuantity: 200, + maxCarbEntryPastTime: .hours(-12), + maxCarbEntryFutureTime: .hours(1) + ) + + //Assert + XCTAssertEqual(carbEntry.absorptionTime, maxAbsorptionTime) + } + + func testToValidCarbEntry_AboveMaxAbsorptionHours_Fails() throws { + + //Arrange + let maxAbsorptionTime = TimeInterval(hours: 5.0) + let absorptionTime = TimeInterval(hours: 5.1) + let action = RemoteCarbAction(amountInGrams: 15.0, + absorptionTime: absorptionTime, + startDate: Date()) + + //Act + var thrownError: Error? = nil + do { + let _ = try action.toValidCarbEntry(defaultAbsorptionTime: maxAbsorptionTime, + minAbsorptionTime: TimeInterval(hours: 0.5), + maxAbsorptionTime: maxAbsorptionTime, + maxCarbEntryQuantity: 200, + maxCarbEntryPastTime: .hours(-12), + maxCarbEntryFutureTime: .hours(1) + ) + } catch { + thrownError = error + } + + //Assert + guard let validationError = thrownError as? RemoteCarbActionError, case .invalidAbsorptionTime = validationError else { + XCTFail("Unexpected type \(thrownError.debugDescription)") + return + } + } + + func testToValidCarbEntry_AtMinStartTime_Succeeds() throws { + + //Arrange + let maxCarbEntryPastTime = TimeInterval(hours: -12) + let nowDate = Date() + let startDate = nowDate.addingTimeInterval(maxCarbEntryPastTime) + let action = RemoteCarbAction(amountInGrams: 15.0, + absorptionTime: TimeInterval(hours: 5.0), + startDate: startDate) + + //Act + let carbEntry = try action.toValidCarbEntry(defaultAbsorptionTime: TimeInterval(hours: 3.0), + minAbsorptionTime: TimeInterval(hours: 0.5), + maxAbsorptionTime: TimeInterval(hours: 5.0), + maxCarbEntryQuantity: 200, + maxCarbEntryPastTime: maxCarbEntryPastTime, + maxCarbEntryFutureTime: .hours(1), + nowDate: nowDate + ) + + //Assert + XCTAssertEqual(carbEntry.startDate, startDate) + } + + func testToValidCarbEntry_BeforeMinStartTime_Fails() throws { + + //Arrange + let maxCarbEntryPastTime = TimeInterval(hours: -12) + let nowDate = Date() + let startDate = nowDate.addingTimeInterval(maxCarbEntryPastTime - 1) + let action = RemoteCarbAction(amountInGrams: 15.0, + absorptionTime: TimeInterval(hours: 5.0), + startDate: startDate) + + //Act + var thrownError: Error? = nil + do { + let _ = try action.toValidCarbEntry(defaultAbsorptionTime: TimeInterval(hours: 3.0), + minAbsorptionTime: TimeInterval(hours: 0.5), + maxAbsorptionTime: TimeInterval(hours: 5.0), + maxCarbEntryQuantity: 200, + maxCarbEntryPastTime: maxCarbEntryPastTime, + maxCarbEntryFutureTime: .hours(1) + ) + } catch { + thrownError = error + } + + //Assert + guard let validationError = thrownError as? RemoteCarbActionError, case .invalidStartDate = validationError else { + XCTFail("Unexpected type \(thrownError.debugDescription)") + return + } + } + + func testToValidCarbEntry_AtMaxStartTime_Succeeds() throws { + + //Arrange + let maxCarbEntryFutureTime = TimeInterval(hours: 1) + let nowDate = Date() + let startDate = nowDate.addingTimeInterval(maxCarbEntryFutureTime) + let action = RemoteCarbAction(amountInGrams: 15.0, + absorptionTime: TimeInterval(hours: 5.0), + startDate: startDate) + + //Act + let carbEntry = try action.toValidCarbEntry(defaultAbsorptionTime: TimeInterval(hours: 3.0), + minAbsorptionTime: TimeInterval(hours: 0.5), + maxAbsorptionTime: TimeInterval(hours: 5.0), + maxCarbEntryQuantity: 200, + maxCarbEntryPastTime: .hours(-12), + maxCarbEntryFutureTime: maxCarbEntryFutureTime, + nowDate: nowDate + ) + + //Assert + XCTAssertEqual(carbEntry.startDate, startDate) + } + + func testToValidCarbEntry_AfterMaxStartTime_Fails() throws { + + //Arrange + let maxCarbEntryFutureTime = TimeInterval(hours: 1) + let nowDate = Date() + let startDate = nowDate.addingTimeInterval(maxCarbEntryFutureTime + 1) + let action = RemoteCarbAction(amountInGrams: 15.0, + absorptionTime: TimeInterval(hours: 5.0), + startDate: startDate) + + //Act + var thrownError: Error? = nil + do { + let _ = try action.toValidCarbEntry(defaultAbsorptionTime: TimeInterval(hours: 3.0), + minAbsorptionTime: TimeInterval(hours: 0.5), + maxAbsorptionTime: TimeInterval(hours: 5.0), + maxCarbEntryQuantity: 200, + maxCarbEntryPastTime: .hours(-12), + maxCarbEntryFutureTime: maxCarbEntryFutureTime + ) + } catch { + thrownError = error + } + + //Assert + guard let validationError = thrownError as? RemoteCarbActionError, case .invalidStartDate = validationError else { + XCTFail("Unexpected type \(thrownError.debugDescription)") + return + } + } + + func testToValidCarbEntry_AtMaxCarbs_Succeeds() throws { + + let maxCarbsAmount = 200.0 + let carbsAmount = maxCarbsAmount + + //Arrange + let action = RemoteCarbAction(amountInGrams: carbsAmount, + absorptionTime: TimeInterval(hours: 5.0), + startDate: Date()) + + //Act + let carbEntry = try action.toValidCarbEntry(defaultAbsorptionTime: TimeInterval(hours: 3.0), + minAbsorptionTime: TimeInterval(hours: 0.5), + maxAbsorptionTime: TimeInterval(hours: 5.0), + maxCarbEntryQuantity: maxCarbsAmount, + maxCarbEntryPastTime: .hours(-12), + maxCarbEntryFutureTime: TimeInterval(hours: 1) + ) + + //Assert + XCTAssertEqual(carbEntry.quantity, HKQuantity(unit: .gram(), doubleValue: carbsAmount)) + } + + func testToValidCarbEntry_AboveMaxCarbs_Fails() throws { + + let maxCarbsAmount = 200.0 + let carbsAmount = maxCarbsAmount + 1 + + //Arrange + let action = RemoteCarbAction(amountInGrams: carbsAmount, + absorptionTime: TimeInterval(hours: 5.0), + startDate: Date()) + + //Act + var thrownError: Error? = nil + do { + let _ = try action.toValidCarbEntry(defaultAbsorptionTime: TimeInterval(hours: 3.0), + minAbsorptionTime: TimeInterval(hours: 0.5), + maxAbsorptionTime: TimeInterval(hours: 5.0), + maxCarbEntryQuantity: 200, + maxCarbEntryPastTime: .hours(-12), + maxCarbEntryFutureTime: .hours(1.0) + ) + } catch { + thrownError = error + } + + //Assert + guard let validationError = thrownError as? RemoteCarbActionError, case .exceedsMaxCarbs = validationError else { + XCTFail("Unexpected type \(thrownError.debugDescription)") + return + } + } + + func testToValidCarbEntry_NegativeCarbs_Fails() throws { + + let carbsAmount = -1.0 + + //Arrange + let action = RemoteCarbAction(amountInGrams: carbsAmount, + absorptionTime: TimeInterval(hours: 5.0), + startDate: Date()) + + //Act + var thrownError: Error? = nil + do { + let _ = try action.toValidCarbEntry(defaultAbsorptionTime: TimeInterval(hours: 3.0), + minAbsorptionTime: TimeInterval(hours: 0.5), + maxAbsorptionTime: TimeInterval(hours: 5.0), + maxCarbEntryQuantity: 200, + maxCarbEntryPastTime: .hours(-12), + maxCarbEntryFutureTime: .hours(1.0) + ) + } catch { + thrownError = error + } + + //Assert + guard let validationError = thrownError as? RemoteCarbActionError, case .invalidCarbs = validationError else { + XCTFail("Unexpected type \(thrownError.debugDescription)") + return + } + } + + func testToValidCarbEntry_ZeroCarbs_Fails() throws { + + let carbsAmount = 0.0 + + //Arrange + let action = RemoteCarbAction(amountInGrams: carbsAmount, + absorptionTime: TimeInterval(hours: 5.0), + startDate: Date()) + + //Act + var thrownError: Error? = nil + do { + let _ = try action.toValidCarbEntry(defaultAbsorptionTime: TimeInterval(hours: 3.0), + minAbsorptionTime: TimeInterval(hours: 0.5), + maxAbsorptionTime: TimeInterval(hours: 5.0), + maxCarbEntryQuantity: 200, + maxCarbEntryPastTime: .hours(-12), + maxCarbEntryFutureTime: .hours(1.0) + ) + } catch { + thrownError = error + } + + //Assert + guard let validationError = thrownError as? RemoteCarbActionError, case .invalidCarbs = validationError else { + XCTFail("Unexpected type \(thrownError.debugDescription)") + return + } + } + +} + + +//MARK: Utils + +func dateFormatter() -> ISO8601DateFormatter { + let formatter = ISO8601DateFormatter() + formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + return formatter +} + diff --git a/LoopTests/Models/Remote/RemoteOverrideActionTests.swift b/LoopTests/Models/Remote/RemoteOverrideActionTests.swift new file mode 100644 index 0000000000..1015c71192 --- /dev/null +++ b/LoopTests/Models/Remote/RemoteOverrideActionTests.swift @@ -0,0 +1,152 @@ +// +// RemoteOverrideActionTests.swift +// LoopKitTests +// +// Created by Bill Gestrich on 1/14/23. +// Copyright © 2023 LoopKit Authors. All rights reserved. +// + +import XCTest +@testable import Loop +import LoopKit + +final class RemoteOverrideActionTests: XCTestCase { + + override func setUpWithError() throws { + + } + + override func tearDownWithError() throws { + + } + + func testToValidOverride_Succeeds() throws { + + //Arrange + let durationTime = TimeInterval(hours: 1.0) + let remoteAddress = "1234-54321" + let overrideName = "My-Override" + let action = RemoteOverrideAction(name: overrideName, durationTime: durationTime, remoteAddress: remoteAddress) + let presets = [TemporaryScheduleOverridePreset(symbol: "", name: overrideName, settings: .init(targetRange: .none), duration: .indefinite)] + + //Act + let validOverride = try action.toValidOverride(allowedPresets: presets) + + //Assert + XCTAssertEqual(validOverride.duration, .finite(durationTime)) + switch validOverride.enactTrigger { + case .remote(let triggerAddress): + XCTAssertEqual(triggerAddress, remoteAddress) + default: + XCTFail("Unexpected trigger trigger type") + } + } + + func testToValidOverride_WhenOverrideNotInPresets_Fails() throws { + + //Arrange + let action = RemoteOverrideAction(name: "Unknown-Override", durationTime: TimeInterval(hours: 1.0), remoteAddress: "1234-54321") + let presets = [TemporaryScheduleOverridePreset(symbol: "", name: "My-Override", settings: .init(targetRange: .none), duration: .indefinite)] + + //Act + var thrownError: Error? = nil + do { + let _ = try action.toValidOverride(allowedPresets: presets) + } catch { + thrownError = error + } + + //Assert + guard let validationError = thrownError as? RemoteOverrideActionError, case .unknownPreset = validationError else { + XCTFail("Unexpected type \(thrownError.debugDescription)") + return + } + } + + func testToValidOverride_WhenNoDuration_YieldsIndefiniteOverride() throws { + + //Arrange + let action = RemoteOverrideAction(name: "My-Override", durationTime: nil, remoteAddress: "1234-54321") + let presets = [TemporaryScheduleOverridePreset(symbol: "", name: "My-Override", settings: .init(targetRange: .none), duration: .indefinite)] + + //Act + let validOverride = try action.toValidOverride(allowedPresets: presets) + + //Assert + XCTAssertEqual(validOverride.duration, .indefinite) + } + + func testToValidOverride_WhenDurationZero_YieldsIndefiniteOverride() throws { + + //Arrange + let action = RemoteOverrideAction(name: "My-Override", durationTime: TimeInterval(hours: 0), remoteAddress: "1234-54321") + let presets = [TemporaryScheduleOverridePreset(symbol: "", name: "My-Override", settings: .init(targetRange: .none), duration: .indefinite)] + + //Act + let validOverride = try action.toValidOverride(allowedPresets: presets) + + //Assert + XCTAssertEqual(validOverride.duration, .indefinite) + } + + func testToValidOverride_WhenNegativeDuration_Fails() throws { + + //Arrange + let action = RemoteOverrideAction(name: "My-Override", durationTime: TimeInterval(hours: -1.0), remoteAddress: "1234-54321") + let presets = [TemporaryScheduleOverridePreset(symbol: "", name: "My-Override", settings: .init(targetRange: .none), duration: .indefinite)] + + //Act + var thrownError: Error? = nil + do { + let _ = try action.toValidOverride(allowedPresets: presets) + } catch { + thrownError = error + } + + //Assert + guard let validationError = thrownError as? RemoteOverrideActionError, case .negativeDuration = validationError else { + XCTFail("Unexpected type \(thrownError.debugDescription)") + return + } + } + + //Limit to 24 hour duration + + func testToValidOverride_WhenAtMaxDuration_Succeeds() throws { + + //Arrange + let duration = TimeInterval(hours: 24) + let action = RemoteOverrideAction(name: "My-Override", durationTime: duration, remoteAddress: "1234-54321") + let presets = [TemporaryScheduleOverridePreset(symbol: "", name: "My-Override", settings: .init(targetRange: .none), duration: .indefinite)] + + //Act + let validOverride = try action.toValidOverride(allowedPresets: presets) + + //Assert + XCTAssertEqual(validOverride.duration, .finite(duration)) + + } + + func testToValidOverride_WhenAtMaxDuration_Fails() throws { + + //Arrange + let duration = TimeInterval(hours: 24) + 1 + let action = RemoteOverrideAction(name: "My-Override", durationTime: duration, remoteAddress: "1234-54321") + let presets = [TemporaryScheduleOverridePreset(symbol: "", name: "My-Override", settings: .init(targetRange: .none), duration: .indefinite)] + + //Act + var thrownError: Error? = nil + do { + let _ = try action.toValidOverride(allowedPresets: presets) + } catch { + thrownError = error + } + + //Assert + guard let validationError = thrownError as? RemoteOverrideActionError, case .durationExceedsMax = validationError else { + XCTFail("Unexpected type \(thrownError.debugDescription)") + return + } + } + +} From ab04d4493b063dbaa0047061fe215a01c7fc1df7 Mon Sep 17 00:00:00 2001 From: Bill Gestrich <3207996+gestrich@users.noreply.github.com> Date: Tue, 21 Feb 2023 18:52:28 -0500 Subject: [PATCH 3/5] Use Action verbiage rather than RemoteAction --- Loop.xcodeproj/project.pbxproj | 56 +++++++++---------- Loop/Managers/DeviceDataManager.swift | 20 +++---- ...oteBolusAction.swift => BolusAction.swift} | 16 +++--- ...emoteCarbAction.swift => CarbAction.swift} | 18 +++--- ...rrideAction.swift => OverrideAction.swift} | 16 +++--- Loop/Models/RemoteCommand.swift | 12 ++-- ...tionTests.swift => BolusActionTests.swift} | 18 +++--- ...ctionTests.swift => CarbActionTests.swift} | 46 +++++++-------- ...nTests.swift => OverrideActionTests.swift} | 24 ++++---- ...onTests.swift => RemoteCommandTests.swift} | 10 ++-- 10 files changed, 118 insertions(+), 118 deletions(-) rename Loop/Models/Remote/{RemoteBolusAction.swift => BolusAction.swift} (72%) rename Loop/Models/Remote/{RemoteCarbAction.swift => CarbAction.swift} (84%) rename Loop/Models/Remote/{RemoteOverrideAction.swift => OverrideAction.swift} (75%) rename LoopTests/Models/Remote/{RemoteBolusActionTests.swift => BolusActionTests.swift} (74%) rename LoopTests/Models/Remote/{RemoteCarbActionTests.swift => CarbActionTests.swift} (89%) rename LoopTests/Models/Remote/{RemoteOverrideActionTests.swift => OverrideActionTests.swift} (76%) rename LoopTests/Models/Remote/{RemoteActionTests.swift => RemoteCommandTests.swift} (89%) diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index ddf4178b2f..e4e95f380e 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -389,12 +389,12 @@ A98556852493F901000FD662 /* AlertStore+SimulatedCoreData.swift in Sources */ = {isa = PBXBuildFile; fileRef = A98556842493F901000FD662 /* AlertStore+SimulatedCoreData.swift */; }; A987CD4924A58A0100439ADC /* ZipArchive.swift in Sources */ = {isa = PBXBuildFile; fileRef = A987CD4824A58A0100439ADC /* ZipArchive.swift */; }; A999D40624663D18004C89D4 /* PumpManagerError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A999D40524663D18004C89D4 /* PumpManagerError.swift */; }; - A99A114229A581F4007919CE /* RemoteBolusAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = A99A114129A581F4007919CE /* RemoteBolusAction.swift */; }; - A99A114429A5829A007919CE /* RemoteCarbAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = A99A114329A5829A007919CE /* RemoteCarbAction.swift */; }; - A99A114629A582A2007919CE /* RemoteOverrideAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = A99A114529A582A2007919CE /* RemoteOverrideAction.swift */; }; - A99A114E29A5879D007919CE /* RemoteBolusActionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A99A114B29A5879C007919CE /* RemoteBolusActionTests.swift */; }; - A99A114F29A5879D007919CE /* RemoteCarbActionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A99A114C29A5879C007919CE /* RemoteCarbActionTests.swift */; }; - A99A115029A5879D007919CE /* RemoteOverrideActionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A99A114D29A5879C007919CE /* RemoteOverrideActionTests.swift */; }; + A99A114229A581F4007919CE /* BolusAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = A99A114129A581F4007919CE /* BolusAction.swift */; }; + A99A114429A5829A007919CE /* CarbAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = A99A114329A5829A007919CE /* CarbAction.swift */; }; + A99A114629A582A2007919CE /* OverrideAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = A99A114529A582A2007919CE /* OverrideAction.swift */; }; + A99A114E29A5879D007919CE /* BolusActionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A99A114B29A5879C007919CE /* BolusActionTests.swift */; }; + A99A114F29A5879D007919CE /* CarbActionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A99A114C29A5879C007919CE /* CarbActionTests.swift */; }; + A99A115029A5879D007919CE /* OverrideActionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A99A114D29A5879C007919CE /* OverrideActionTests.swift */; }; A9A056B324B93C62007CF06D /* CriticalEventLogExportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9A056B224B93C62007CF06D /* CriticalEventLogExportView.swift */; }; A9A056B524B94123007CF06D /* CriticalEventLogExportViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9A056B424B94123007CF06D /* CriticalEventLogExportViewModel.swift */; }; A9A63F8E246B271600588D5B /* NSTimeInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = 439897341CD2F7DE00223065 /* NSTimeInterval.swift */; }; @@ -418,7 +418,7 @@ A9DF02CB24F72B9E00B7C988 /* CriticalEventLogTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9DF02CA24F72B9E00B7C988 /* CriticalEventLogTests.swift */; }; A9DFAFB324F0415E00950D1E /* CarbBackfillRequestUserInfoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9DFAFB224F0415E00950D1E /* CarbBackfillRequestUserInfoTests.swift */; }; A9DFAFB524F048A000950D1E /* WatchHistoricalCarbsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9DFAFB424F048A000950D1E /* WatchHistoricalCarbsTests.swift */; }; - A9E8A80528A7CAC000C0A8A4 /* RemoteActionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E8A80428A7CAC000C0A8A4 /* RemoteActionTests.swift */; }; + A9E8A80528A7CAC000C0A8A4 /* RemoteCommandTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E8A80428A7CAC000C0A8A4 /* RemoteCommandTests.swift */; }; A9F5F1F5251050EC00E7C8A4 /* ZipArchiveTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9F5F1F4251050EC00E7C8A4 /* ZipArchiveTests.swift */; }; A9F66FC3247F451500096EA7 /* UIDevice+Loop.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9F66FC2247F451500096EA7 /* UIDevice+Loop.swift */; }; A9F703732489BC8500C98AD8 /* CarbStore+SimulatedCoreData.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9F703722489BC8500C98AD8 /* CarbStore+SimulatedCoreData.swift */; }; @@ -1392,12 +1392,12 @@ A98556842493F901000FD662 /* AlertStore+SimulatedCoreData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AlertStore+SimulatedCoreData.swift"; sourceTree = ""; }; A987CD4824A58A0100439ADC /* ZipArchive.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZipArchive.swift; sourceTree = ""; }; A999D40524663D18004C89D4 /* PumpManagerError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PumpManagerError.swift; sourceTree = ""; }; - A99A114129A581F4007919CE /* RemoteBolusAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteBolusAction.swift; sourceTree = ""; }; - A99A114329A5829A007919CE /* RemoteCarbAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteCarbAction.swift; sourceTree = ""; }; - A99A114529A582A2007919CE /* RemoteOverrideAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteOverrideAction.swift; sourceTree = ""; }; - A99A114B29A5879C007919CE /* RemoteBolusActionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteBolusActionTests.swift; sourceTree = ""; }; - A99A114C29A5879C007919CE /* RemoteCarbActionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteCarbActionTests.swift; sourceTree = ""; }; - A99A114D29A5879C007919CE /* RemoteOverrideActionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteOverrideActionTests.swift; sourceTree = ""; }; + A99A114129A581F4007919CE /* BolusAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusAction.swift; sourceTree = ""; }; + A99A114329A5829A007919CE /* CarbAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbAction.swift; sourceTree = ""; }; + A99A114529A582A2007919CE /* OverrideAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideAction.swift; sourceTree = ""; }; + A99A114B29A5879C007919CE /* BolusActionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BolusActionTests.swift; sourceTree = ""; }; + A99A114C29A5879C007919CE /* CarbActionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CarbActionTests.swift; sourceTree = ""; }; + A99A114D29A5879C007919CE /* OverrideActionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OverrideActionTests.swift; sourceTree = ""; }; A9A056B224B93C62007CF06D /* CriticalEventLogExportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CriticalEventLogExportView.swift; sourceTree = ""; }; A9A056B424B94123007CF06D /* CriticalEventLogExportViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CriticalEventLogExportViewModel.swift; sourceTree = ""; }; A9B607AF247F000F00792BE4 /* UserNotifications+Loop.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserNotifications+Loop.swift"; sourceTree = ""; }; @@ -1419,7 +1419,7 @@ A9DF02CA24F72B9E00B7C988 /* CriticalEventLogTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CriticalEventLogTests.swift; sourceTree = ""; }; A9DFAFB224F0415E00950D1E /* CarbBackfillRequestUserInfoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbBackfillRequestUserInfoTests.swift; sourceTree = ""; }; A9DFAFB424F048A000950D1E /* WatchHistoricalCarbsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchHistoricalCarbsTests.swift; sourceTree = ""; }; - A9E8A80428A7CAC000C0A8A4 /* RemoteActionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteActionTests.swift; sourceTree = ""; }; + A9E8A80428A7CAC000C0A8A4 /* RemoteCommandTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteCommandTests.swift; sourceTree = ""; }; A9F5F1F4251050EC00E7C8A4 /* ZipArchiveTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZipArchiveTests.swift; sourceTree = ""; }; A9F66FC2247F451500096EA7 /* UIDevice+Loop.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIDevice+Loop.swift"; sourceTree = ""; }; A9F703722489BC8500C98AD8 /* CarbStore+SimulatedCoreData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CarbStore+SimulatedCoreData.swift"; sourceTree = ""; }; @@ -2802,9 +2802,9 @@ A99A114029A581D6007919CE /* Remote */ = { isa = PBXGroup; children = ( - A99A114129A581F4007919CE /* RemoteBolusAction.swift */, - A99A114329A5829A007919CE /* RemoteCarbAction.swift */, - A99A114529A582A2007919CE /* RemoteOverrideAction.swift */, + A99A114129A581F4007919CE /* BolusAction.swift */, + A99A114329A5829A007919CE /* CarbAction.swift */, + A99A114529A582A2007919CE /* OverrideAction.swift */, ); path = Remote; sourceTree = ""; @@ -2812,10 +2812,10 @@ A99A114A29A58789007919CE /* Remote */ = { isa = PBXGroup; children = ( - A9E8A80428A7CAC000C0A8A4 /* RemoteActionTests.swift */, - A99A114B29A5879C007919CE /* RemoteBolusActionTests.swift */, - A99A114C29A5879C007919CE /* RemoteCarbActionTests.swift */, - A99A114D29A5879C007919CE /* RemoteOverrideActionTests.swift */, + A9E8A80428A7CAC000C0A8A4 /* RemoteCommandTests.swift */, + A99A114B29A5879C007919CE /* BolusActionTests.swift */, + A99A114C29A5879C007919CE /* CarbActionTests.swift */, + A99A114D29A5879C007919CE /* OverrideActionTests.swift */, ); path = Remote; sourceTree = ""; @@ -3820,7 +3820,7 @@ C17824A01E19CF9800D9D25C /* GlucoseThresholdTableViewController.swift in Sources */, 4372E487213C86240068E043 /* SampleValue.swift in Sources */, 437CEEE41CDE5C0A003C8C80 /* UIImage.swift in Sources */, - A99A114229A581F4007919CE /* RemoteBolusAction.swift in Sources */, + A99A114229A581F4007919CE /* BolusAction.swift in Sources */, C1201E2C23ECDBD0002DA84A /* WatchContextRequestUserInfo.swift in Sources */, 1D49795824E7289700948F05 /* ServicesViewModel.swift in Sources */, 1D4A3E2D2478628500FD601B /* StoredAlert+CoreDataClass.swift in Sources */, @@ -3880,7 +3880,7 @@ 1D6B1B6726866D89009AC446 /* AlertPermissionsChecker.swift in Sources */, 4F08DE8F1E7BB871006741EA /* CollectionType+Loop.swift in Sources */, A9F703772489D8AA00C98AD8 /* PersistentDeviceLog+SimulatedCoreData.swift in Sources */, - A99A114629A582A2007919CE /* RemoteOverrideAction.swift in Sources */, + A99A114629A582A2007919CE /* OverrideAction.swift in Sources */, E9B080B1253BDA6300BAD8F8 /* UserDefaults+LoopIntents.swift in Sources */, C1AF062329426300002C1B19 /* ManualGlucoseEntryRow.swift in Sources */, C148CEE724FD91BD00711B3B /* DeliveryUncertaintyAlertManager.swift in Sources */, @@ -3928,7 +3928,7 @@ A9C62D842331700E00535612 /* DiagnosticLog+Subsystem.swift in Sources */, 895FE0952201234000FCF18A /* OverrideSelectionViewController.swift in Sources */, C1EF747228D6A44A00C8C083 /* CrashRecoveryManager.swift in Sources */, - A99A114429A5829A007919CE /* RemoteCarbAction.swift in Sources */, + A99A114429A5829A007919CE /* CarbAction.swift in Sources */, A9F66FC3247F451500096EA7 /* UIDevice+Loop.swift in Sources */, 439706E622D2E84900C81566 /* PredictionSettingTableViewCell.swift in Sources */, 430D85891F44037000AF2D4F /* HUDViewTableViewCell.swift in Sources */, @@ -4129,12 +4129,12 @@ buildActionMask = 2147483647; files = ( A9DF02CB24F72B9E00B7C988 /* CriticalEventLogTests.swift in Sources */, - A99A114F29A5879D007919CE /* RemoteCarbActionTests.swift in Sources */, + A99A114F29A5879D007919CE /* CarbActionTests.swift in Sources */, B44251B3252350CE00605937 /* ChartAxisValuesStaticGeneratorTests.swift in Sources */, 1D80313D24746274002810DF /* AlertStoreTests.swift in Sources */, C1777A6625A125F100595963 /* ManualEntryDoseViewModelTests.swift in Sources */, C16B984026B4898800256B05 /* DoseEnactorTests.swift in Sources */, - A99A115029A5879D007919CE /* RemoteOverrideActionTests.swift in Sources */, + A99A115029A5879D007919CE /* OverrideActionTests.swift in Sources */, A9A63F8E246B271600588D5B /* NSTimeInterval.swift in Sources */, A9DFAFB324F0415E00950D1E /* CarbBackfillRequestUserInfoTests.swift in Sources */, A963B27A252CEBAE0062AA12 /* SetBolusUserInfoTests.swift in Sources */, @@ -4152,7 +4152,7 @@ B4CAD8792549D2540057946B /* LoopCompletionFreshnessTests.swift in Sources */, 1D8D55BC252274650044DBB6 /* BolusEntryViewModelTests.swift in Sources */, A91E4C2124F867A700BE9213 /* StoredAlertTests.swift in Sources */, - A99A114E29A5879D007919CE /* RemoteBolusActionTests.swift in Sources */, + A99A114E29A5879D007919CE /* BolusActionTests.swift in Sources */, 1DA7A84224476EAD008257F0 /* AlertManagerTests.swift in Sources */, A91E4C2324F86F1000BE9213 /* CriticalEventLogExportManagerTests.swift in Sources */, E9C58A7324DB4A2700487A17 /* LoopDataManagerTests.swift in Sources */, @@ -4161,7 +4161,7 @@ A96DAC2A2838EF8A00D94E38 /* DiagnosticLogTests.swift in Sources */, A9DAE7D02332D77F006AE942 /* LoopTests.swift in Sources */, E93E86B024DDE1BD00FF40C8 /* MockGlucoseStore.swift in Sources */, - A9E8A80528A7CAC000C0A8A4 /* RemoteActionTests.swift in Sources */, + A9E8A80528A7CAC000C0A8A4 /* RemoteCommandTests.swift in Sources */, 1DFE9E172447B6270082C280 /* UserNotificationAlertSchedulerTests.swift in Sources */, B4BC56382518DEA900373647 /* CGMStatusHUDViewModelTests.swift in Sources */, C1900900252271BB00721625 /* SimpleBolusCalculatorTests.swift in Sources */, diff --git a/Loop/Managers/DeviceDataManager.swift b/Loop/Managers/DeviceDataManager.swift index 5d67b62549..53f421f7be 100644 --- a/Loop/Managers/DeviceDataManager.swift +++ b/Loop/Managers/DeviceDataManager.swift @@ -1388,10 +1388,10 @@ extension DeviceDataManager { } } - let action: RemoteAction + let action: Action do { - action = try RemoteAction.createRemoteAction(notification: notification).get() + action = try RemoteCommand.createRemoteAction(notification: notification).get() } catch { log.error("Remote Notification: Parse Error: %{public}@", String(describing: error)) return @@ -1402,20 +1402,20 @@ extension DeviceDataManager { switch action { case .temporaryScheduleOverride(let overrideAction): do { - try await handleRemoteOverrideAction(overrideAction) + try await handleOverrideAction(overrideAction) } catch { log.error("Remote Notification: Override Action Error: %{public}@", String(describing: error)) } case .cancelTemporaryOverride(let overrideCancelAction): do { - try await handleRemoteOverrideCancelAction(overrideCancelAction) + try await handleOverrideCancelAction(overrideCancelAction) } catch { log.error("Remote Notification: Override Action Cancel Error: %{public}@", String(describing: error)) } case .bolusEntry(let bolusAction): do { try validatePushNotificationSource(notification: notification) - try await handleRemoteBolusAction(bolusAction) + try await handleBolusAction(bolusAction) } catch { await NotificationManager.sendRemoteBolusFailureNotification(for: error, amount: bolusAction.amountInUnits) log.error("Remote Notification: Bolus Action Error: %{public}@", String(describing: notification)) @@ -1423,7 +1423,7 @@ extension DeviceDataManager { case .carbsEntry(let carbAction): do { try validatePushNotificationSource(notification: notification) - try await handleRemoteCarbAction(carbAction) + try await handleCarbAction(carbAction) } catch { await NotificationManager.sendRemoteCarbEntryFailureNotification(for: error, amountInGrams: carbAction.amountInGrams) log.error("Remote Notification: Carb Action Error: %{public}@", String(describing: notification)) @@ -1447,12 +1447,12 @@ extension DeviceDataManager { //Remote Overrides - func handleRemoteOverrideAction(_ action: RemoteOverrideAction) async throws { + func handleOverrideAction(_ action: OverrideAction) async throws { let remoteOverride = try action.toValidOverride(allowedPresets: loopManager.settings.overridePresets) await activateRemoteOverride(remoteOverride) } - func handleRemoteOverrideCancelAction(_ cancelAction: RemoteOverrideCancelAction) async throws { + func handleOverrideCancelAction(_ cancelAction: OverrideCancelAction) async throws { await activateRemoteOverride(nil) } @@ -1463,7 +1463,7 @@ extension DeviceDataManager { //Remote Bolus - func handleRemoteBolusAction(_ bolusCommand: RemoteBolusAction) async throws { + func handleBolusAction(_ bolusCommand: BolusAction) async throws { let validBolusAmount = try bolusCommand.toValidBolusAmount(maximumBolus: loopManager.settings.maximumBolus) try await self.enactBolus(units: validBolusAmount, activationType: .manualNoRecommendation) await triggerBackgroundUpload(for: .dose) @@ -1472,7 +1472,7 @@ extension DeviceDataManager { //Remote Carb Entry - func handleRemoteCarbAction(_ carbCommand: RemoteCarbAction) async throws { + func handleCarbAction(_ carbCommand: CarbAction) async throws { let candidateCarbEntry = try carbCommand.toValidCarbEntry(defaultAbsorptionTime: carbStore.defaultAbsorptionTimes.medium, minAbsorptionTime: LoopConstants.minCarbAbsorptionTime, maxAbsorptionTime: LoopConstants.maxCarbAbsorptionTime, diff --git a/Loop/Models/Remote/RemoteBolusAction.swift b/Loop/Models/Remote/BolusAction.swift similarity index 72% rename from Loop/Models/Remote/RemoteBolusAction.swift rename to Loop/Models/Remote/BolusAction.swift index e75d66d73b..054b618089 100644 --- a/Loop/Models/Remote/RemoteBolusAction.swift +++ b/Loop/Models/Remote/BolusAction.swift @@ -1,5 +1,5 @@ // -// RemoteBolusAction.swift +// BolusAction.swift // Loop // // Created by Bill Gestrich on 2/21/23. @@ -8,32 +8,32 @@ import LoopKit -extension RemoteBolusAction { - public func toValidBolusAmount(maximumBolus: Double?) throws -> Double { +extension BolusAction { + func toValidBolusAmount(maximumBolus: Double?) throws -> Double { guard amountInUnits > 0 else { - throw RemoteBolusActionError.invalidBolus + throw BolusActionError.invalidBolus } guard let maxBolusAmount = maximumBolus else { - throw RemoteBolusActionError.missingMaxBolus + throw BolusActionError.missingMaxBolus } guard amountInUnits <= maxBolusAmount else { - throw RemoteBolusActionError.exceedsMaxBolus + throw BolusActionError.exceedsMaxBolus } return amountInUnits } } -public enum RemoteBolusActionError: LocalizedError { +enum BolusActionError: LocalizedError { case invalidBolus case missingMaxBolus case exceedsMaxBolus - public var errorDescription: String? { + var errorDescription: String? { switch self { case .invalidBolus: return NSLocalizedString("Invalid Bolus Amount", comment: "Remote command error description: invalid bolus amount.") diff --git a/Loop/Models/Remote/RemoteCarbAction.swift b/Loop/Models/Remote/CarbAction.swift similarity index 84% rename from Loop/Models/Remote/RemoteCarbAction.swift rename to Loop/Models/Remote/CarbAction.swift index 143843eea0..fd00ed9d6a 100644 --- a/Loop/Models/Remote/RemoteCarbAction.swift +++ b/Loop/Models/Remote/CarbAction.swift @@ -1,5 +1,5 @@ // -// RemoteCarbAction.swift +// CarbAction.swift // Loop // // Created by Bill Gestrich on 2/21/23. @@ -9,9 +9,9 @@ import LoopKit import HealthKit -extension RemoteCarbAction { +extension CarbAction { - public func toValidCarbEntry(defaultAbsorptionTime: TimeInterval, + func toValidCarbEntry(defaultAbsorptionTime: TimeInterval, minAbsorptionTime: TimeInterval, maxAbsorptionTime: TimeInterval, maxCarbEntryQuantity: Double, @@ -21,22 +21,22 @@ extension RemoteCarbAction { let absorptionTime = absorptionTime ?? defaultAbsorptionTime if absorptionTime < minAbsorptionTime || absorptionTime > maxAbsorptionTime { - throw RemoteCarbActionError.invalidAbsorptionTime(absorptionTime) + throw CarbActionError.invalidAbsorptionTime(absorptionTime) } guard amountInGrams > 0.0 else { - throw RemoteCarbActionError.invalidCarbs + throw CarbActionError.invalidCarbs } guard amountInGrams <= maxCarbEntryQuantity else { - throw RemoteCarbActionError.exceedsMaxCarbs + throw CarbActionError.exceedsMaxCarbs } if let startDate = startDate { let maxStartDate = nowDate.addingTimeInterval(maxCarbEntryFutureTime) let minStartDate = nowDate.addingTimeInterval(maxCarbEntryPastTime) guard startDate <= maxStartDate && startDate >= minStartDate else { - throw RemoteCarbActionError.invalidStartDate(startDate) + throw CarbActionError.invalidStartDate(startDate) } } @@ -45,14 +45,14 @@ extension RemoteCarbAction { } } -enum RemoteCarbActionError: LocalizedError { +enum CarbActionError: LocalizedError { case invalidAbsorptionTime(TimeInterval) case invalidStartDate(Date) case exceedsMaxCarbs case invalidCarbs - public var errorDescription: String? { + var errorDescription: String? { switch self { case .exceedsMaxCarbs: return NSLocalizedString("Exceeds maximum allowed carbs", comment: "Remote command error description: carbs exceed maximum amount.") diff --git a/Loop/Models/Remote/RemoteOverrideAction.swift b/Loop/Models/Remote/OverrideAction.swift similarity index 75% rename from Loop/Models/Remote/RemoteOverrideAction.swift rename to Loop/Models/Remote/OverrideAction.swift index 04d3f5fe8f..28ea26e383 100644 --- a/Loop/Models/Remote/RemoteOverrideAction.swift +++ b/Loop/Models/Remote/OverrideAction.swift @@ -1,5 +1,5 @@ // -// RemoteOverrideAction.swift +// OverrideAction.swift // Loop // // Created by Bill Gestrich on 2/21/23. @@ -8,11 +8,11 @@ import LoopKit -extension RemoteOverrideAction { +extension OverrideAction { - public func toValidOverride(allowedPresets: [TemporaryScheduleOverridePreset]) throws -> TemporaryScheduleOverride { + func toValidOverride(allowedPresets: [TemporaryScheduleOverridePreset]) throws -> TemporaryScheduleOverride { guard let preset = allowedPresets.first(where: { $0.name == name }) else { - throw RemoteOverrideActionError.unknownPreset(name) + throw OverrideActionError.unknownPreset(name) } var remoteOverride = preset.createOverride(enactTrigger: .remote(remoteAddress)) @@ -20,11 +20,11 @@ extension RemoteOverrideAction { if let durationTime = durationTime { guard durationTime <= LoopConstants.maxOverrideDurationTime else { - throw RemoteOverrideActionError.durationExceedsMax(LoopConstants.maxOverrideDurationTime) + throw OverrideActionError.durationExceedsMax(LoopConstants.maxOverrideDurationTime) } guard durationTime >= 0 else { - throw RemoteOverrideActionError.negativeDuration + throw OverrideActionError.negativeDuration } if durationTime == 0 { @@ -38,13 +38,13 @@ extension RemoteOverrideAction { } } -public enum RemoteOverrideActionError: LocalizedError { +enum OverrideActionError: LocalizedError { case unknownPreset(String) case durationExceedsMax(TimeInterval) case negativeDuration - public var errorDescription: String? { + var errorDescription: String? { switch self { case .unknownPreset(let presetName): return String(format: NSLocalizedString("Unknown preset: %1$@", comment: "Remote command error description: unknown preset (1: preset name)."), presetName) diff --git a/Loop/Models/RemoteCommand.swift b/Loop/Models/RemoteCommand.swift index 5806c308a1..b927188d48 100644 --- a/Loop/Models/RemoteCommand.swift +++ b/Loop/Models/RemoteCommand.swift @@ -10,8 +10,8 @@ import Foundation import LoopKit // Push Notifications -extension RemoteAction { - static func createRemoteAction(notification: [String: Any]) -> Result { +struct RemoteCommand { + static func createRemoteAction(notification: [String: Any]) -> Result { if let overrideName = notification["override-name"] as? String, let remoteAddress = notification["remote-address"] as? String { @@ -19,13 +19,13 @@ extension RemoteAction { if let overrideDurationMinutes = notification["override-duration-minutes"] as? Double { overrideTime = TimeInterval(minutes: overrideDurationMinutes) } - return .success(.temporaryScheduleOverride(RemoteOverrideAction(name: overrideName, durationTime: overrideTime, remoteAddress: remoteAddress))) + return .success(.temporaryScheduleOverride(OverrideAction(name: overrideName, durationTime: overrideTime, remoteAddress: remoteAddress))) } else if let _ = notification["cancel-temporary-override"] as? String, let remoteAddress = notification["remote-address"] as? String { - return .success(.cancelTemporaryOverride(RemoteOverrideCancelAction(remoteAddress: remoteAddress))) + return .success(.cancelTemporaryOverride(OverrideCancelAction(remoteAddress: remoteAddress))) } else if let bolusValue = notification["bolus-entry"] as? Double { - return .success(.bolusEntry(RemoteBolusAction(amountInUnits: bolusValue))) + return .success(.bolusEntry(BolusAction(amountInUnits: bolusValue))) } else if let carbsValue = notification["carbs-entry"] as? Double { var absorptionTime: TimeInterval? = nil @@ -46,7 +46,7 @@ extension RemoteAction { } } - return .success(.carbsEntry(RemoteCarbAction(amountInGrams: carbsValue, absorptionTime: absorptionTime, foodType: foodType, startDate: startDate))) + return .success(.carbsEntry(CarbAction(amountInGrams: carbsValue, absorptionTime: absorptionTime, foodType: foodType, startDate: startDate))) } else { return .failure(RemoteCommandParseError.unhandledNotication("\(notification)")) } diff --git a/LoopTests/Models/Remote/RemoteBolusActionTests.swift b/LoopTests/Models/Remote/BolusActionTests.swift similarity index 74% rename from LoopTests/Models/Remote/RemoteBolusActionTests.swift rename to LoopTests/Models/Remote/BolusActionTests.swift index d101bc3e13..2129ea68b4 100644 --- a/LoopTests/Models/Remote/RemoteBolusActionTests.swift +++ b/LoopTests/Models/Remote/BolusActionTests.swift @@ -1,5 +1,5 @@ // -// RemoteBolusActionTests.swift +// BolusActionTests.swift // LoopKitTests // // Created by Bill Gestrich on 1/14/23. @@ -10,7 +10,7 @@ import XCTest @testable import Loop import LoopKit -final class RemoteBolusActionTests: XCTestCase { +final class BolusActionTests: XCTestCase { override func setUpWithError() throws { } @@ -23,7 +23,7 @@ final class RemoteBolusActionTests: XCTestCase { //Arrange let maxBolusAmount = 10.0 let bolusAmount = maxBolusAmount - let action = RemoteBolusAction(amountInUnits: bolusAmount) + let action = BolusAction(amountInUnits: bolusAmount) //Act let validatedBolusAmount = try action.toValidBolusAmount(maximumBolus: 10.0) @@ -38,7 +38,7 @@ final class RemoteBolusActionTests: XCTestCase { //Arrange let maxBolusAmount = 10.0 let bolusAmount = maxBolusAmount + 0.1 - let action = RemoteBolusAction(amountInUnits: bolusAmount) + let action = BolusAction(amountInUnits: bolusAmount) //Act var thrownError: Error? = nil @@ -49,7 +49,7 @@ final class RemoteBolusActionTests: XCTestCase { } //Assert - guard let validationError = thrownError as? RemoteBolusActionError, case .exceedsMaxBolus = validationError else { + guard let validationError = thrownError as? BolusActionError, case .exceedsMaxBolus = validationError else { XCTFail("Unexpected type \(thrownError.debugDescription)") return } @@ -59,7 +59,7 @@ final class RemoteBolusActionTests: XCTestCase { //Arrange let bolusAmount = 0.0 - let action = RemoteBolusAction(amountInUnits: bolusAmount) + let action = BolusAction(amountInUnits: bolusAmount) //Act var thrownError: Error? = nil @@ -70,7 +70,7 @@ final class RemoteBolusActionTests: XCTestCase { } //Assert - guard let validationError = thrownError as? RemoteBolusActionError, case .invalidBolus = validationError else { + guard let validationError = thrownError as? BolusActionError, case .invalidBolus = validationError else { XCTFail("Unexpected type \(thrownError.debugDescription)") return } @@ -80,7 +80,7 @@ final class RemoteBolusActionTests: XCTestCase { //Arrange let bolusAmount = -1.0 - let action = RemoteBolusAction(amountInUnits: bolusAmount) + let action = BolusAction(amountInUnits: bolusAmount) //Act var thrownError: Error? = nil @@ -91,7 +91,7 @@ final class RemoteBolusActionTests: XCTestCase { } //Assert - guard let validationError = thrownError as? RemoteBolusActionError, case .invalidBolus = validationError else { + guard let validationError = thrownError as? BolusActionError, case .invalidBolus = validationError else { XCTFail("Unexpected type \(thrownError.debugDescription)") return } diff --git a/LoopTests/Models/Remote/RemoteCarbActionTests.swift b/LoopTests/Models/Remote/CarbActionTests.swift similarity index 89% rename from LoopTests/Models/Remote/RemoteCarbActionTests.swift rename to LoopTests/Models/Remote/CarbActionTests.swift index 80b8647dd1..277051252a 100644 --- a/LoopTests/Models/Remote/RemoteCarbActionTests.swift +++ b/LoopTests/Models/Remote/CarbActionTests.swift @@ -1,5 +1,5 @@ // -// RemoteCarbActionTests.swift +// CarbActionTests.swift // LoopTests // // Created by Bill Gestrich on 1/14/23. @@ -11,7 +11,7 @@ import HealthKit @testable import Loop import LoopKit -class RemoteCarbActionTests: XCTestCase { +class CarbActionTests: XCTestCase { override func setUpWithError() throws { } @@ -28,7 +28,7 @@ class RemoteCarbActionTests: XCTestCase { let expectedAbsorptionTime = TimeInterval(hours: 4.0) let foodType = "🍕" - let action = RemoteCarbAction(amountInGrams: expectedCarbsInGrams, absorptionTime: expectedAbsorptionTime, foodType: foodType, startDate: expectedDate) + let action = CarbAction(amountInGrams: expectedCarbsInGrams, absorptionTime: expectedAbsorptionTime, foodType: foodType, startDate: expectedDate) //Act let carbEntry = try action.toValidCarbEntry(defaultAbsorptionTime: TimeInterval(hours: 3.0), @@ -50,7 +50,7 @@ class RemoteCarbActionTests: XCTestCase { //Arrange let defaultAbsorptionTime = TimeInterval(hours: 4.0) - let action = RemoteCarbAction(amountInGrams: 15.0, startDate: Date()) + let action = CarbAction(amountInGrams: 15.0, startDate: Date()) //Act let carbEntry = try action.toValidCarbEntry(defaultAbsorptionTime: defaultAbsorptionTime, @@ -68,7 +68,7 @@ class RemoteCarbActionTests: XCTestCase { //Arrange let minAbsorptionTime = TimeInterval(hours: 0.5) - let action = RemoteCarbAction(amountInGrams: 15.0, + let action = CarbAction(amountInGrams: 15.0, absorptionTime: minAbsorptionTime, startDate: Date()) @@ -89,7 +89,7 @@ class RemoteCarbActionTests: XCTestCase { //Arrange let minAbsorptionTime = TimeInterval(hours: 0.5) let aborptionOverrideTime = TimeInterval(hours: 0.4) - let action = RemoteCarbAction(amountInGrams: 15.0, + let action = CarbAction(amountInGrams: 15.0, absorptionTime: aborptionOverrideTime, startDate: Date()) @@ -108,7 +108,7 @@ class RemoteCarbActionTests: XCTestCase { } //Assert - guard let validationError = thrownError as? RemoteCarbActionError, case .invalidAbsorptionTime = validationError else { + guard let validationError = thrownError as? CarbActionError, case .invalidAbsorptionTime = validationError else { XCTFail("Unexpected type \(thrownError.debugDescription)") return } @@ -118,7 +118,7 @@ class RemoteCarbActionTests: XCTestCase { //Arrange let maxAbsorptionTime = TimeInterval(hours: 5.0) - let action = RemoteCarbAction(amountInGrams: 15.0, + let action = CarbAction(amountInGrams: 15.0, absorptionTime: maxAbsorptionTime, startDate: Date()) @@ -140,7 +140,7 @@ class RemoteCarbActionTests: XCTestCase { //Arrange let maxAbsorptionTime = TimeInterval(hours: 5.0) let absorptionTime = TimeInterval(hours: 5.1) - let action = RemoteCarbAction(amountInGrams: 15.0, + let action = CarbAction(amountInGrams: 15.0, absorptionTime: absorptionTime, startDate: Date()) @@ -159,7 +159,7 @@ class RemoteCarbActionTests: XCTestCase { } //Assert - guard let validationError = thrownError as? RemoteCarbActionError, case .invalidAbsorptionTime = validationError else { + guard let validationError = thrownError as? CarbActionError, case .invalidAbsorptionTime = validationError else { XCTFail("Unexpected type \(thrownError.debugDescription)") return } @@ -171,7 +171,7 @@ class RemoteCarbActionTests: XCTestCase { let maxCarbEntryPastTime = TimeInterval(hours: -12) let nowDate = Date() let startDate = nowDate.addingTimeInterval(maxCarbEntryPastTime) - let action = RemoteCarbAction(amountInGrams: 15.0, + let action = CarbAction(amountInGrams: 15.0, absorptionTime: TimeInterval(hours: 5.0), startDate: startDate) @@ -195,7 +195,7 @@ class RemoteCarbActionTests: XCTestCase { let maxCarbEntryPastTime = TimeInterval(hours: -12) let nowDate = Date() let startDate = nowDate.addingTimeInterval(maxCarbEntryPastTime - 1) - let action = RemoteCarbAction(amountInGrams: 15.0, + let action = CarbAction(amountInGrams: 15.0, absorptionTime: TimeInterval(hours: 5.0), startDate: startDate) @@ -214,7 +214,7 @@ class RemoteCarbActionTests: XCTestCase { } //Assert - guard let validationError = thrownError as? RemoteCarbActionError, case .invalidStartDate = validationError else { + guard let validationError = thrownError as? CarbActionError, case .invalidStartDate = validationError else { XCTFail("Unexpected type \(thrownError.debugDescription)") return } @@ -226,7 +226,7 @@ class RemoteCarbActionTests: XCTestCase { let maxCarbEntryFutureTime = TimeInterval(hours: 1) let nowDate = Date() let startDate = nowDate.addingTimeInterval(maxCarbEntryFutureTime) - let action = RemoteCarbAction(amountInGrams: 15.0, + let action = CarbAction(amountInGrams: 15.0, absorptionTime: TimeInterval(hours: 5.0), startDate: startDate) @@ -250,7 +250,7 @@ class RemoteCarbActionTests: XCTestCase { let maxCarbEntryFutureTime = TimeInterval(hours: 1) let nowDate = Date() let startDate = nowDate.addingTimeInterval(maxCarbEntryFutureTime + 1) - let action = RemoteCarbAction(amountInGrams: 15.0, + let action = CarbAction(amountInGrams: 15.0, absorptionTime: TimeInterval(hours: 5.0), startDate: startDate) @@ -269,7 +269,7 @@ class RemoteCarbActionTests: XCTestCase { } //Assert - guard let validationError = thrownError as? RemoteCarbActionError, case .invalidStartDate = validationError else { + guard let validationError = thrownError as? CarbActionError, case .invalidStartDate = validationError else { XCTFail("Unexpected type \(thrownError.debugDescription)") return } @@ -281,7 +281,7 @@ class RemoteCarbActionTests: XCTestCase { let carbsAmount = maxCarbsAmount //Arrange - let action = RemoteCarbAction(amountInGrams: carbsAmount, + let action = CarbAction(amountInGrams: carbsAmount, absorptionTime: TimeInterval(hours: 5.0), startDate: Date()) @@ -304,7 +304,7 @@ class RemoteCarbActionTests: XCTestCase { let carbsAmount = maxCarbsAmount + 1 //Arrange - let action = RemoteCarbAction(amountInGrams: carbsAmount, + let action = CarbAction(amountInGrams: carbsAmount, absorptionTime: TimeInterval(hours: 5.0), startDate: Date()) @@ -323,7 +323,7 @@ class RemoteCarbActionTests: XCTestCase { } //Assert - guard let validationError = thrownError as? RemoteCarbActionError, case .exceedsMaxCarbs = validationError else { + guard let validationError = thrownError as? CarbActionError, case .exceedsMaxCarbs = validationError else { XCTFail("Unexpected type \(thrownError.debugDescription)") return } @@ -334,7 +334,7 @@ class RemoteCarbActionTests: XCTestCase { let carbsAmount = -1.0 //Arrange - let action = RemoteCarbAction(amountInGrams: carbsAmount, + let action = CarbAction(amountInGrams: carbsAmount, absorptionTime: TimeInterval(hours: 5.0), startDate: Date()) @@ -353,7 +353,7 @@ class RemoteCarbActionTests: XCTestCase { } //Assert - guard let validationError = thrownError as? RemoteCarbActionError, case .invalidCarbs = validationError else { + guard let validationError = thrownError as? CarbActionError, case .invalidCarbs = validationError else { XCTFail("Unexpected type \(thrownError.debugDescription)") return } @@ -364,7 +364,7 @@ class RemoteCarbActionTests: XCTestCase { let carbsAmount = 0.0 //Arrange - let action = RemoteCarbAction(amountInGrams: carbsAmount, + let action = CarbAction(amountInGrams: carbsAmount, absorptionTime: TimeInterval(hours: 5.0), startDate: Date()) @@ -383,7 +383,7 @@ class RemoteCarbActionTests: XCTestCase { } //Assert - guard let validationError = thrownError as? RemoteCarbActionError, case .invalidCarbs = validationError else { + guard let validationError = thrownError as? CarbActionError, case .invalidCarbs = validationError else { XCTFail("Unexpected type \(thrownError.debugDescription)") return } diff --git a/LoopTests/Models/Remote/RemoteOverrideActionTests.swift b/LoopTests/Models/Remote/OverrideActionTests.swift similarity index 76% rename from LoopTests/Models/Remote/RemoteOverrideActionTests.swift rename to LoopTests/Models/Remote/OverrideActionTests.swift index 1015c71192..fe9f5d7fb3 100644 --- a/LoopTests/Models/Remote/RemoteOverrideActionTests.swift +++ b/LoopTests/Models/Remote/OverrideActionTests.swift @@ -1,5 +1,5 @@ // -// RemoteOverrideActionTests.swift +// OverrideActionTests.swift // LoopKitTests // // Created by Bill Gestrich on 1/14/23. @@ -10,7 +10,7 @@ import XCTest @testable import Loop import LoopKit -final class RemoteOverrideActionTests: XCTestCase { +final class OverrideActionTests: XCTestCase { override func setUpWithError() throws { @@ -26,7 +26,7 @@ final class RemoteOverrideActionTests: XCTestCase { let durationTime = TimeInterval(hours: 1.0) let remoteAddress = "1234-54321" let overrideName = "My-Override" - let action = RemoteOverrideAction(name: overrideName, durationTime: durationTime, remoteAddress: remoteAddress) + let action = OverrideAction(name: overrideName, durationTime: durationTime, remoteAddress: remoteAddress) let presets = [TemporaryScheduleOverridePreset(symbol: "", name: overrideName, settings: .init(targetRange: .none), duration: .indefinite)] //Act @@ -45,7 +45,7 @@ final class RemoteOverrideActionTests: XCTestCase { func testToValidOverride_WhenOverrideNotInPresets_Fails() throws { //Arrange - let action = RemoteOverrideAction(name: "Unknown-Override", durationTime: TimeInterval(hours: 1.0), remoteAddress: "1234-54321") + let action = OverrideAction(name: "Unknown-Override", durationTime: TimeInterval(hours: 1.0), remoteAddress: "1234-54321") let presets = [TemporaryScheduleOverridePreset(symbol: "", name: "My-Override", settings: .init(targetRange: .none), duration: .indefinite)] //Act @@ -57,7 +57,7 @@ final class RemoteOverrideActionTests: XCTestCase { } //Assert - guard let validationError = thrownError as? RemoteOverrideActionError, case .unknownPreset = validationError else { + guard let validationError = thrownError as? OverrideActionError, case .unknownPreset = validationError else { XCTFail("Unexpected type \(thrownError.debugDescription)") return } @@ -66,7 +66,7 @@ final class RemoteOverrideActionTests: XCTestCase { func testToValidOverride_WhenNoDuration_YieldsIndefiniteOverride() throws { //Arrange - let action = RemoteOverrideAction(name: "My-Override", durationTime: nil, remoteAddress: "1234-54321") + let action = OverrideAction(name: "My-Override", durationTime: nil, remoteAddress: "1234-54321") let presets = [TemporaryScheduleOverridePreset(symbol: "", name: "My-Override", settings: .init(targetRange: .none), duration: .indefinite)] //Act @@ -79,7 +79,7 @@ final class RemoteOverrideActionTests: XCTestCase { func testToValidOverride_WhenDurationZero_YieldsIndefiniteOverride() throws { //Arrange - let action = RemoteOverrideAction(name: "My-Override", durationTime: TimeInterval(hours: 0), remoteAddress: "1234-54321") + let action = OverrideAction(name: "My-Override", durationTime: TimeInterval(hours: 0), remoteAddress: "1234-54321") let presets = [TemporaryScheduleOverridePreset(symbol: "", name: "My-Override", settings: .init(targetRange: .none), duration: .indefinite)] //Act @@ -92,7 +92,7 @@ final class RemoteOverrideActionTests: XCTestCase { func testToValidOverride_WhenNegativeDuration_Fails() throws { //Arrange - let action = RemoteOverrideAction(name: "My-Override", durationTime: TimeInterval(hours: -1.0), remoteAddress: "1234-54321") + let action = OverrideAction(name: "My-Override", durationTime: TimeInterval(hours: -1.0), remoteAddress: "1234-54321") let presets = [TemporaryScheduleOverridePreset(symbol: "", name: "My-Override", settings: .init(targetRange: .none), duration: .indefinite)] //Act @@ -104,7 +104,7 @@ final class RemoteOverrideActionTests: XCTestCase { } //Assert - guard let validationError = thrownError as? RemoteOverrideActionError, case .negativeDuration = validationError else { + guard let validationError = thrownError as? OverrideActionError, case .negativeDuration = validationError else { XCTFail("Unexpected type \(thrownError.debugDescription)") return } @@ -116,7 +116,7 @@ final class RemoteOverrideActionTests: XCTestCase { //Arrange let duration = TimeInterval(hours: 24) - let action = RemoteOverrideAction(name: "My-Override", durationTime: duration, remoteAddress: "1234-54321") + let action = OverrideAction(name: "My-Override", durationTime: duration, remoteAddress: "1234-54321") let presets = [TemporaryScheduleOverridePreset(symbol: "", name: "My-Override", settings: .init(targetRange: .none), duration: .indefinite)] //Act @@ -131,7 +131,7 @@ final class RemoteOverrideActionTests: XCTestCase { //Arrange let duration = TimeInterval(hours: 24) + 1 - let action = RemoteOverrideAction(name: "My-Override", durationTime: duration, remoteAddress: "1234-54321") + let action = OverrideAction(name: "My-Override", durationTime: duration, remoteAddress: "1234-54321") let presets = [TemporaryScheduleOverridePreset(symbol: "", name: "My-Override", settings: .init(targetRange: .none), duration: .indefinite)] //Act @@ -143,7 +143,7 @@ final class RemoteOverrideActionTests: XCTestCase { } //Assert - guard let validationError = thrownError as? RemoteOverrideActionError, case .durationExceedsMax = validationError else { + guard let validationError = thrownError as? OverrideActionError, case .durationExceedsMax = validationError else { XCTFail("Unexpected type \(thrownError.debugDescription)") return } diff --git a/LoopTests/Models/Remote/RemoteActionTests.swift b/LoopTests/Models/Remote/RemoteCommandTests.swift similarity index 89% rename from LoopTests/Models/Remote/RemoteActionTests.swift rename to LoopTests/Models/Remote/RemoteCommandTests.swift index 395ff0e944..9b9163b55b 100644 --- a/LoopTests/Models/Remote/RemoteActionTests.swift +++ b/LoopTests/Models/Remote/RemoteCommandTests.swift @@ -1,5 +1,5 @@ // -// RemoteActionTests.swift +// RemoteCommandTests.swift // LoopTests // // Created by Bill Gestrich on 8/13/22. @@ -11,7 +11,7 @@ import HealthKit @testable import Loop import LoopKit -class RemoteActionTests: XCTestCase { +class RemoteCommandTests: XCTestCase { override func setUpWithError() throws { } @@ -40,7 +40,7 @@ class RemoteActionTests: XCTestCase { ] //Act - let action = try RemoteAction.createRemoteAction(notification: notification).get() + let action = try RemoteCommand.createRemoteAction(notification: notification).get() //Assert guard case .carbsEntry(let carbEntry) = action else { @@ -66,7 +66,7 @@ class RemoteActionTests: XCTestCase { ] //Act - let action = try RemoteAction.createRemoteAction(notification: notification).get() + let action = try RemoteCommand.createRemoteAction(notification: notification).get() //Assert guard case .carbsEntry(let carbEntry) = action else { @@ -93,7 +93,7 @@ class RemoteActionTests: XCTestCase { ] //Act + Assert - XCTAssertThrowsError(try RemoteAction.createRemoteAction(notification: notification).get()) + XCTAssertThrowsError(try RemoteCommand.createRemoteAction(notification: notification).get()) } From 302323aa3a56aca014c974a6f03522b82e0b3fce Mon Sep 17 00:00:00 2001 From: Bill Gestrich <3207996+gestrich@users.noreply.github.com> Date: Tue, 21 Feb 2023 19:08:35 -0500 Subject: [PATCH 4/5] update naming --- Loop/Managers/DeviceDataManager.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Loop/Managers/DeviceDataManager.swift b/Loop/Managers/DeviceDataManager.swift index 53f421f7be..1adda7bd85 100644 --- a/Loop/Managers/DeviceDataManager.swift +++ b/Loop/Managers/DeviceDataManager.swift @@ -1452,7 +1452,7 @@ extension DeviceDataManager { await activateRemoteOverride(remoteOverride) } - func handleOverrideCancelAction(_ cancelAction: OverrideCancelAction) async throws { + func handleOverrideCancelAction(_ action: OverrideCancelAction) async throws { await activateRemoteOverride(nil) } @@ -1463,8 +1463,8 @@ extension DeviceDataManager { //Remote Bolus - func handleBolusAction(_ bolusCommand: BolusAction) async throws { - let validBolusAmount = try bolusCommand.toValidBolusAmount(maximumBolus: loopManager.settings.maximumBolus) + func handleBolusAction(_ action: BolusAction) async throws { + let validBolusAmount = try action.toValidBolusAmount(maximumBolus: loopManager.settings.maximumBolus) try await self.enactBolus(units: validBolusAmount, activationType: .manualNoRecommendation) await triggerBackgroundUpload(for: .dose) self.analyticsServicesManager.didBolus(source: "Remote", units: validBolusAmount) @@ -1472,8 +1472,8 @@ extension DeviceDataManager { //Remote Carb Entry - func handleCarbAction(_ carbCommand: CarbAction) async throws { - let candidateCarbEntry = try carbCommand.toValidCarbEntry(defaultAbsorptionTime: carbStore.defaultAbsorptionTimes.medium, + func handleCarbAction(_ action: CarbAction) async throws { + let candidateCarbEntry = try action.toValidCarbEntry(defaultAbsorptionTime: carbStore.defaultAbsorptionTimes.medium, minAbsorptionTime: LoopConstants.minCarbAbsorptionTime, maxAbsorptionTime: LoopConstants.maxCarbAbsorptionTime, maxCarbEntryQuantity: LoopConstants.maxCarbEntryQuantity.doubleValue(for: .gram()), From db2ef8d7c9b6ef75358e735557fad25743eec9ce Mon Sep 17 00:00:00 2001 From: Bill Gestrich <3207996+gestrich@users.noreply.github.com> Date: Wed, 22 Feb 2023 18:52:43 -0500 Subject: [PATCH 5/5] Request background processing time for entire asynchronous execution --- Loop/Managers/DeviceDataManager.swift | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/Loop/Managers/DeviceDataManager.swift b/Loop/Managers/DeviceDataManager.swift index 1adda7bd85..93199f863d 100644 --- a/Loop/Managers/DeviceDataManager.swift +++ b/Loop/Managers/DeviceDataManager.swift @@ -1356,7 +1356,9 @@ extension DeviceDataManager { func handleRemoteNotification(_ notification: [String: AnyObject]) { Task { + let backgroundTask = await beginBackgroundTask(name: "Remote Data Upload") await handleRemoteNotification(notification) + await endBackgroundTask(backgroundTask) } } @@ -1458,7 +1460,7 @@ extension DeviceDataManager { func activateRemoteOverride(_ remoteOverride: TemporaryScheduleOverride?) async { loopManager.mutateSettings { settings in settings.scheduleOverride = remoteOverride } - await triggerBackgroundUpload(for: .overrides) + await remoteDataServicesManager.triggerUpload(for: .overrides) } //Remote Bolus @@ -1466,7 +1468,7 @@ extension DeviceDataManager { func handleBolusAction(_ action: BolusAction) async throws { let validBolusAmount = try action.toValidBolusAmount(maximumBolus: loopManager.settings.maximumBolus) try await self.enactBolus(units: validBolusAmount, activationType: .manualNoRecommendation) - await triggerBackgroundUpload(for: .dose) + await remoteDataServicesManager.triggerUpload(for: .dose) self.analyticsServicesManager.didBolus(source: "Remote", units: validBolusAmount) } @@ -1482,7 +1484,7 @@ extension DeviceDataManager { ) let _ = try await addRemoteCarbEntry(candidateCarbEntry) - await triggerBackgroundUpload(for: .carb) + await remoteDataServicesManager.triggerUpload(for: .carb) } //Can't add this concurrency wrapper method to LoopKit due to the minimum iOS version @@ -1502,13 +1504,6 @@ extension DeviceDataManager { //Background Uploads - func triggerBackgroundUpload(for triggeringType: RemoteDataType) async { - let backgroundTask = await beginBackgroundTask(name: "Remote Data Upload") - await remoteDataServicesManager.triggerUpload(for: triggeringType) - await endBackgroundTask(backgroundTask) - - } - func beginBackgroundTask(name: String) async -> UIBackgroundTaskIdentifier? { var backgroundTask: UIBackgroundTaskIdentifier? backgroundTask = await UIApplication.shared.beginBackgroundTask(withName: name) {