PumpHistoryStorageTests.swift 11 KB

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