PumpHistoryStorageTests.swift 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. import CoreData
  2. import Foundation
  3. import Swinject
  4. import Testing
  5. @testable import LoopKit
  6. @testable import Trio
  7. @Suite("PumpHistoryStorage Tests") struct PumpHistoryStorageTests: Injectable {
  8. @Injected() var storage: PumpHistoryStorage!
  9. let resolver: Resolver
  10. let coreDataStack = CoreDataStack.createForTests()
  11. let testContext: NSManagedObjectContext
  12. typealias PumpEvent = PumpEventStored.EventType
  13. init() {
  14. // Create test context
  15. testContext = coreDataStack.newTaskContext()
  16. // Create assembler with test assembly
  17. let assembler = Assembler([
  18. StorageAssembly(),
  19. ServiceAssembly(),
  20. APSAssembly(),
  21. NetworkAssembly(),
  22. UIAssembly(),
  23. SecurityAssembly(),
  24. TestAssembly(testContext: testContext) // Add our test assembly last to override PumpHistoryStorage
  25. ])
  26. resolver = assembler.resolver
  27. injectServices(resolver)
  28. }
  29. @Test("Storage is correctly initialized") func testStorageInitialization() {
  30. // Verify storage exists
  31. #expect(storage != nil, "PumpHistoryStorage should be injected")
  32. // Verify it's the correct type
  33. #expect(
  34. storage is BasePumpHistoryStorage, "Storage should be of type BasePumpHistoryStorage"
  35. )
  36. // Verify we can access the update publisher
  37. #expect(storage.updatePublisher != nil, "Update publisher should be available")
  38. }
  39. @Test("Test read and delete using generic CoreDataStack functions") func testFetchAndDeletePumpEvents() async throws {
  40. // Given
  41. let date = Date()
  42. // Insert mock entry
  43. let events: [LoopKit.NewPumpEvent] = [
  44. LoopKit.NewPumpEvent(
  45. date: date,
  46. dose: LoopKit.DoseEntry(
  47. type: .bolus,
  48. startDate: date,
  49. value: 0.5,
  50. unit: .units,
  51. deliveredUnits: nil,
  52. description: nil,
  53. syncIdentifier: nil,
  54. scheduledBasalRate: nil,
  55. insulinType: .lyumjev,
  56. automatic: false,
  57. manuallyEntered: false,
  58. isMutable: false
  59. ),
  60. raw: Data(),
  61. title: "Test Bolus for Fetch",
  62. type: .bolus
  63. )
  64. ]
  65. // Store test event
  66. try await storage.storePumpEvents(events)
  67. // When - Fetch events with our generic fetch function
  68. let fetchedEvents = try await coreDataStack.fetchEntitiesAsync(
  69. ofType: PumpEventStored.self,
  70. onContext: testContext,
  71. predicate: NSPredicate(
  72. format: "type == %@ AND timestamp == %@",
  73. PumpEvent.bolus.rawValue,
  74. date as NSDate
  75. ),
  76. key: "timestamp",
  77. ascending: false
  78. )
  79. guard let fetchedEvents = fetchedEvents as? [PumpEventStored] else { return }
  80. // Then
  81. #expect(fetchedEvents.count == 1, "Should have found exactly one event")
  82. let fetchedEvent = fetchedEvents.first
  83. #expect(fetchedEvent?.type == PumpEvent.bolus.rawValue, "Should be a bolus event")
  84. #expect(fetchedEvent?.bolus?.amount as? Decimal == 0.5, "Bolus amount should be 0.5")
  85. #expect(
  86. abs(fetchedEvent?.timestamp?.timeIntervalSince(date) ?? 1) < 1,
  87. "Timestamp should match"
  88. )
  89. // When - Delete event
  90. if let fetchedEvent = fetchedEvent {
  91. await coreDataStack.deleteObject(identifiedBy: fetchedEvent.objectID)
  92. }
  93. // Then - Verify deletion
  94. let eventsAfterDeletion = try await coreDataStack.fetchEntitiesAsync(
  95. ofType: PumpEventStored.self,
  96. onContext: testContext,
  97. predicate: NSPredicate(
  98. format: "type == %@ AND timestamp == %@",
  99. PumpEvent.bolus.rawValue,
  100. date as NSDate
  101. ),
  102. key: "timestamp",
  103. ascending: false
  104. )
  105. guard let eventsAfterDeletion = eventsAfterDeletion as? [PumpEventStored] else { return }
  106. #expect(eventsAfterDeletion.isEmpty, "Should have no events after deletion")
  107. }
  108. @Test("Test store function in PumpHistoryStorage") func testStorePumpEvents() async throws {
  109. // Given
  110. let date = Date()
  111. let tenMinAgo = date.addingTimeInterval(-10.minutes.timeInterval)
  112. let halfHourInFuture = date.addingTimeInterval(30.minutes.timeInterval)
  113. // Get initial entries to compare to final entries later
  114. let initialEntries = try await testContext.perform {
  115. try testContext.fetch(PumpEventStored.fetchRequest())
  116. }
  117. // Create 2 test events, 1 bolus + 1 temp basal event
  118. let events: [LoopKit.NewPumpEvent] = [
  119. // SMB
  120. LoopKit.NewPumpEvent(
  121. date: tenMinAgo,
  122. dose: LoopKit.DoseEntry(
  123. type: .bolus,
  124. startDate: tenMinAgo,
  125. value: 0.4,
  126. unit: .units,
  127. deliveredUnits: nil,
  128. description: nil,
  129. syncIdentifier: nil,
  130. scheduledBasalRate: nil,
  131. insulinType: .lyumjev,
  132. automatic: true,
  133. manuallyEntered: false,
  134. isMutable: false
  135. ),
  136. raw: Data(),
  137. title: "Test Bolus",
  138. type: .bolus
  139. ),
  140. // Temp Basal event
  141. LoopKit.NewPumpEvent(
  142. date: date,
  143. dose: LoopKit.DoseEntry(
  144. type: .tempBasal,
  145. startDate: date,
  146. endDate: halfHourInFuture,
  147. value: 1.2,
  148. unit: .unitsPerHour,
  149. deliveredUnits: nil,
  150. description: nil,
  151. syncIdentifier: nil,
  152. scheduledBasalRate: nil,
  153. insulinType: .lyumjev,
  154. automatic: true,
  155. manuallyEntered: false,
  156. isMutable: true
  157. ),
  158. raw: Data(),
  159. title: "Test Temp Basal",
  160. type: .tempBasal
  161. )
  162. ]
  163. // When
  164. // Store in our in-memory PumphistoryStorage
  165. try await storage.storePumpEvents(events)
  166. // Then
  167. // Fetch all events after storing
  168. let finalEntries = try await testContext.perform {
  169. try testContext.fetch(PumpEventStored.fetchRequest())
  170. }
  171. // Verify there were no initial entries
  172. #expect(initialEntries.isEmpty, "There should be no initial entries")
  173. // Verify count increased by 2
  174. #expect(finalEntries.count == initialEntries.count + 2, "Should have added 2 new events")
  175. // Verify bolus event
  176. let bolusEvent = finalEntries.first {
  177. $0.type == PumpEvent.bolus.rawValue &&
  178. abs($0.timestamp?.timeIntervalSince(tenMinAgo) ?? 1) < 1
  179. }
  180. #expect(bolusEvent != nil, "Should have found bolus event")
  181. #expect(bolusEvent?.bolus?.amount as? Decimal == 0.4, "Bolus amount should be 0.4")
  182. #expect(bolusEvent?.isUploadedToNS == false, "Should not be uploaded to NS")
  183. #expect(bolusEvent?.isUploadedToHealth == false, "Should not be uploaded to Health")
  184. #expect(bolusEvent?.isUploadedToTidepool == false, "Should not be uploaded to Tidepool")
  185. #expect(bolusEvent?.bolus?.isSMB == true, "Should be a SMB")
  186. #expect(bolusEvent?.bolus?.isExternal == false, "Should not be external insulin")
  187. // Verify temp basal event
  188. let tempBasalEvent = finalEntries.first {
  189. $0.type == PumpEvent.tempBasal.rawValue &&
  190. abs($0.timestamp?.timeIntervalSince(date) ?? 1) < 1
  191. }
  192. #expect(tempBasalEvent != nil, "Should have found temp basal event")
  193. #expect(tempBasalEvent?.tempBasal?.rate as? Decimal == 1.2, "Temp basal rate should be 1.2")
  194. #expect(tempBasalEvent?.tempBasal?.duration == 30, "Temp basal duration should be 30 minutes")
  195. #expect(tempBasalEvent?.isUploadedToNS == false, "Should not be uploaded to NS")
  196. #expect(tempBasalEvent?.isUploadedToHealth == false, "Should not be uploaded to Health")
  197. #expect(bolusEvent?.isUploadedToTidepool == false, "Should not be uploaded to Tidepool")
  198. }
  199. @Test("Test store function for manual boluses") func testStorePumpEventsWithManualBoluses() async throws {
  200. // Given
  201. let date = Date()
  202. // Insert mock entry
  203. let events: [LoopKit.NewPumpEvent] = [
  204. LoopKit.NewPumpEvent(
  205. date: date,
  206. dose: LoopKit.DoseEntry(
  207. type: .bolus,
  208. startDate: date,
  209. value: 4,
  210. unit: .units,
  211. deliveredUnits: nil,
  212. description: nil,
  213. syncIdentifier: nil,
  214. scheduledBasalRate: nil,
  215. insulinType: .lyumjev,
  216. automatic: false,
  217. manuallyEntered: false,
  218. isMutable: false
  219. ),
  220. raw: Data(),
  221. title: "Test Bolus",
  222. type: .bolus
  223. )
  224. ]
  225. // Store test event and wait for storage to complete the task
  226. try await storage.storePumpEvents(events)
  227. // When - Fetch events with our generic fetch function
  228. let fetchedEvents = try await coreDataStack.fetchEntitiesAsync(
  229. ofType: PumpEventStored.self,
  230. onContext: testContext,
  231. predicate: NSPredicate(
  232. format: "type == %@ AND timestamp == %@",
  233. PumpEvent.bolus.rawValue,
  234. date as NSDate
  235. ),
  236. key: "timestamp",
  237. ascending: false
  238. )
  239. guard let fetchedEvents = fetchedEvents as? [PumpEventStored] else { return }
  240. // Then
  241. #expect(fetchedEvents.count == 1, "Should have found exactly one event")
  242. let fetchedEvent = fetchedEvents.first
  243. #expect(fetchedEvent?.type == PumpEvent.bolus.rawValue, "Should be a bolus event")
  244. #expect(fetchedEvent?.bolus?.amount as? Decimal == 4, "Bolus amount should be 4 U")
  245. #expect(
  246. abs(fetchedEvent?.timestamp?.timeIntervalSince(date) ?? 1) < 1,
  247. "Timestamp should match"
  248. )
  249. #expect(fetchedEvent?.bolus?.isSMB == false, "Should not be a SMB")
  250. #expect(fetchedEvent?.bolus?.isExternal == false, "Should not be external Insulin")
  251. #expect(fetchedEvent?.isUploadedToNS == false, "Should not be uploaded to NS")
  252. #expect(fetchedEvent?.isUploadedToHealth == false, "Should not be uploaded to Health")
  253. #expect(fetchedEvent?.isUploadedToTidepool == false, "Should not be uploaded to Tidepool")
  254. }
  255. }