GlucoseStorageTests.swift 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. import CoreData
  2. import Foundation
  3. import Swinject
  4. import Testing
  5. @testable import Trio
  6. @Suite("GlucoseStorage Tests", .serialized) struct GlucoseStorageTests: Injectable {
  7. @Injected() var storage: GlucoseStorage!
  8. let resolver: Resolver
  9. var coreDataStack: CoreDataStack!
  10. var testContext: NSManagedObjectContext!
  11. init() async throws {
  12. // Create test context
  13. // As we are only using this single test context to initialize our in-memory DeterminationStorage we need to perform the Unit Tests serialized
  14. coreDataStack = try await CoreDataStack.createForTests()
  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 Storage
  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, "GlucoseStorage should be injected")
  32. // Verify it's the correct type
  33. #expect(storage is BaseGlucoseStorage, "Storage should be of type BaseGlucoseStorage")
  34. }
  35. @Test("Store and retrieve glucose entries") func testStoreAndRetrieveGlucose() async throws {
  36. // Given
  37. let testGlucose = [
  38. BloodGlucose(direction: BloodGlucose.Direction.flat, date: 123, dateString: Date(), glucose: 126)
  39. ]
  40. // When
  41. try await storage.storeGlucose(testGlucose)
  42. // Then verify stored entries
  43. let storedEntries = try await coreDataStack.fetchEntitiesAsync(
  44. ofType: GlucoseStored.self,
  45. onContext: testContext,
  46. predicate: NSPredicate(format: "glucose == 126"),
  47. key: "date",
  48. ascending: false
  49. ) as? [GlucoseStored]
  50. #expect(storedEntries?.isEmpty == false, "Should have stored entries")
  51. #expect(storedEntries?.count == 1, "Should have exactly one entry")
  52. #expect(storedEntries?[0].glucose == 126, "Glucose value should match")
  53. #expect(storedEntries?[0].direction == "Flat", "Direction should match")
  54. }
  55. @Test("Delete glucose entry") func testDeleteGlucose() async throws {
  56. // Given
  57. let testGlucose = [
  58. BloodGlucose(direction: BloodGlucose.Direction.flat, date: 123, dateString: Date(), glucose: 140)
  59. ]
  60. try await storage.storeGlucose(testGlucose)
  61. // Get the stored entry's ObjectID
  62. let storedEntries = try await coreDataStack.fetchEntitiesAsync(
  63. ofType: GlucoseStored.self,
  64. onContext: testContext,
  65. predicate: NSPredicate(format: "glucose == 140"),
  66. key: "date",
  67. ascending: false
  68. ) as? [GlucoseStored]
  69. guard let objectID = storedEntries?.first?.objectID else {
  70. throw TestError("Failed to get stored entry's ObjectID")
  71. }
  72. #expect(storedEntries.isNotNilNotEmpty == true, "Should have exactly one (test) entry")
  73. // When
  74. await storage.deleteGlucose(objectID)
  75. // Then verify deletion
  76. let remainingEntries = try await coreDataStack.fetchEntitiesAsync(
  77. ofType: GlucoseStored.self,
  78. onContext: testContext,
  79. predicate: NSPredicate(format: "glucose == 140"),
  80. key: "date",
  81. ascending: false
  82. ) as? [GlucoseStored]
  83. #expect(remainingEntries?.isEmpty == true, "Should have no entries after deletion")
  84. // Finally verify that it stored a copy
  85. let archivedEntries = try await coreDataStack.fetchEntitiesAsync(
  86. ofType: DeletedGlucoseStored.self,
  87. onContext: testContext,
  88. predicate: NSPredicate(format: "glucose == 140"),
  89. key: "date",
  90. ascending: false
  91. ) as? [DeletedGlucoseStored]
  92. #expect(archivedEntries?.isEmpty == false, "Should have archived entries after deletion")
  93. }
  94. @Test("Get glucose not yet uploaded to Nightscout") func testGetGlucoseNotYetUploadedToNightscout() async throws {
  95. // Given
  96. let testGlucose = [
  97. BloodGlucose(direction: BloodGlucose.Direction.flat, date: 123, dateString: Date(), glucose: 160)
  98. ]
  99. try await storage.storeGlucose(testGlucose)
  100. // When
  101. let notUploadedEntries = try await storage.getGlucoseNotYetUploadedToNightscout()
  102. // Then
  103. #expect(!notUploadedEntries.isEmpty, "Should have entries not uploaded to NS")
  104. #expect(notUploadedEntries[0].glucose == 160, "Glucose value should match")
  105. }
  106. @Test(
  107. "Test glucose alarms",
  108. .enabled(if: false, "Flaky test, disabled while investigating")
  109. ) func testGlucoseAlarms() async throws {
  110. // Given
  111. let lowGlucose = [
  112. BloodGlucose(direction: BloodGlucose.Direction.flat, date: 123, dateString: Date(), glucose: 55)
  113. ]
  114. let highGlucose = [
  115. BloodGlucose(direction: BloodGlucose.Direction.flat, date: 123, dateString: Date(), glucose: 271)
  116. ]
  117. let normalGlucose = [
  118. BloodGlucose(direction: BloodGlucose.Direction.flat, date: 123, dateString: Date(), glucose: 100)
  119. ]
  120. // When - Test low glucose
  121. try await storage.storeGlucose(lowGlucose)
  122. var storedEntries = try await coreDataStack.fetchEntitiesAsync(
  123. ofType: GlucoseStored.self,
  124. onContext: testContext,
  125. predicate: NSPredicate(format: "glucose == 55"),
  126. key: "date",
  127. ascending: false
  128. ) as? [GlucoseStored]
  129. // Then
  130. #expect(storedEntries?.first?.glucose == 55, "Low glucose value should match")
  131. #expect(storage.alarm == .low, "Should trigger low glucose alarm") // default low limit is 72 mg/dL
  132. // When - Test high glucose
  133. try await storage.storeGlucose(highGlucose)
  134. storedEntries = try await coreDataStack.fetchEntitiesAsync(
  135. ofType: GlucoseStored.self,
  136. onContext: testContext,
  137. predicate: NSPredicate(format: "glucose == 271"),
  138. key: "date",
  139. ascending: false
  140. ) as? [GlucoseStored]
  141. // Then
  142. #expect(storedEntries?.first?.glucose == 271, "High glucose value should match")
  143. #expect(storage.alarm == .high, "Should trigger high glucose alarm") // default high limit is 270 mg/dL
  144. // When - Test normal glucose
  145. try await storage.storeGlucose(normalGlucose)
  146. storedEntries = try await coreDataStack.fetchEntitiesAsync(
  147. ofType: GlucoseStored.self,
  148. onContext: testContext,
  149. predicate: NSPredicate(format: "glucose == 100"),
  150. key: "date",
  151. ascending: false
  152. ) as? [GlucoseStored]
  153. // Then
  154. #expect(storedEntries?.first?.glucose == 100, "Normal glucose value should match")
  155. #expect(storage.alarm == nil, "Should not trigger any alarm")
  156. }
  157. /* Commenting out while we don't have getGlucoseStatus defined
  158. @Test("getGlucoseStatus returns correct deltas for 0/5/15/30m readings") func testGetGlucoseStatusFourPoints() async throws {
  159. let now = Date()
  160. // Prepare 4 readings: at 0, 5, 15, and 30 minutes ago
  161. let specs: [(offset: TimeInterval, value: Int)] = [
  162. (0, 100), // now
  163. (5 * 60, 110), // 5m ago
  164. (15 * 60, 120), // 15m ago
  165. (30 * 60, 130) // 30m ago
  166. ]
  167. // Insert them into CoreData so that our fetch predicate picks them up
  168. for (offset, value) in specs {
  169. await testContext.perform {
  170. let glucoseToStore = GlucoseStored(context: testContext)
  171. glucoseToStore.id = UUID()
  172. glucoseToStore.date = now.addingTimeInterval(-offset)
  173. glucoseToStore.glucose = Int16(value)
  174. }
  175. }
  176. try testContext.save()
  177. // Call the method under test
  178. let status = try await storage.getGlucoseStatus()
  179. #expect(status != nil, "Expected non‐nil status")
  180. // “Now” glucose is the 0m reading
  181. #expect(status!.glucose == 100)
  182. // lastDelta: only the 5m point: (100–110)/5*5 = –10
  183. #expect(status!.delta == -10)
  184. // shortAvgDelta: average of 5m and 15m windows:
  185. // 5m window: (100–110)/5*5 = –10
  186. // 15m window: (100–120)/15*5 ≈ –6.6667 → –6.67
  187. // avg ≈ (–10 + –6.67)/2 = –8.333… → rounded to –8.33
  188. #expect(status!.shortAvgDelta == -8.33)
  189. // longAvgDelta: only the 30m window: (100–130)/30*5 = –5
  190. #expect(status!.longAvgDelta == -5)
  191. }*/
  192. }