GlucoseStorageTests.swift 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  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. // Then verify deletion
  86. let archivedEntries = try await coreDataStack.fetchEntitiesAsync(
  87. ofType: DeletedGlucoseStored.self,
  88. onContext: testContext,
  89. predicate: NSPredicate(format: "glucose == 140"),
  90. key: "date",
  91. ascending: false
  92. ) as? [DeletedGlucoseStored]
  93. #expect(archivedEntries?.isEmpty == false, "Should have archived entries after deletion")
  94. }
  95. @Test("Get glucose not yet uploaded to Nightscout") func testGetGlucoseNotYetUploadedToNightscout() async throws {
  96. // Given
  97. let testGlucose = [
  98. BloodGlucose(direction: BloodGlucose.Direction.flat, date: 123, dateString: Date(), glucose: 160)
  99. ]
  100. try await storage.storeGlucose(testGlucose)
  101. // When
  102. let notUploadedEntries = try await storage.getGlucoseNotYetUploadedToNightscout()
  103. // Then
  104. #expect(!notUploadedEntries.isEmpty, "Should have entries not uploaded to NS")
  105. #expect(notUploadedEntries[0].glucose == 160, "Glucose value should match")
  106. }
  107. @Test("Get manual glucose not yet uploaded to Nightscout") func testGetManualGlucoseNotYetUploadedToNightscout() async throws {
  108. // Given
  109. storage.addManualGlucose(glucose: 180)
  110. // When
  111. let notUploadedEntries = try await storage.getManualGlucoseNotYetUploadedToNightscout()
  112. // Then
  113. #expect(!notUploadedEntries.isEmpty, "Should have manual entries not uploaded to NS")
  114. let entry = notUploadedEntries[0]
  115. #expect(entry.glucose == "180", "Glucose value should match")
  116. #expect(entry.glucoseType == "Manual", "Type should be mbg for manual entries")
  117. #expect(entry.eventType == .capillaryGlucose, "Type should be capillaryGlucose")
  118. }
  119. @Test(
  120. "Test glucose alarms",
  121. .enabled(if: false, "Flaky test, disabled while investigating")
  122. ) func testGlucoseAlarms() async throws {
  123. // Given
  124. let lowGlucose = [
  125. BloodGlucose(direction: BloodGlucose.Direction.flat, date: 123, dateString: Date(), glucose: 55)
  126. ]
  127. let highGlucose = [
  128. BloodGlucose(direction: BloodGlucose.Direction.flat, date: 123, dateString: Date(), glucose: 271)
  129. ]
  130. let normalGlucose = [
  131. BloodGlucose(direction: BloodGlucose.Direction.flat, date: 123, dateString: Date(), glucose: 100)
  132. ]
  133. // When - Test low glucose
  134. try await storage.storeGlucose(lowGlucose)
  135. var storedEntries = try await coreDataStack.fetchEntitiesAsync(
  136. ofType: GlucoseStored.self,
  137. onContext: testContext,
  138. predicate: NSPredicate(format: "glucose == 55"),
  139. key: "date",
  140. ascending: false
  141. ) as? [GlucoseStored]
  142. // Then
  143. #expect(storedEntries?.first?.glucose == 55, "Low glucose value should match")
  144. #expect(storage.alarm == .low, "Should trigger low glucose alarm") // default low limit is 72 mg/dL
  145. // When - Test high glucose
  146. try await storage.storeGlucose(highGlucose)
  147. storedEntries = try await coreDataStack.fetchEntitiesAsync(
  148. ofType: GlucoseStored.self,
  149. onContext: testContext,
  150. predicate: NSPredicate(format: "glucose == 271"),
  151. key: "date",
  152. ascending: false
  153. ) as? [GlucoseStored]
  154. // Then
  155. #expect(storedEntries?.first?.glucose == 271, "High glucose value should match")
  156. #expect(storage.alarm == .high, "Should trigger high glucose alarm") // default high limit is 270 mg/dL
  157. // When - Test normal glucose
  158. try await storage.storeGlucose(normalGlucose)
  159. storedEntries = try await coreDataStack.fetchEntitiesAsync(
  160. ofType: GlucoseStored.self,
  161. onContext: testContext,
  162. predicate: NSPredicate(format: "glucose == 100"),
  163. key: "date",
  164. ascending: false
  165. ) as? [GlucoseStored]
  166. // Then
  167. #expect(storedEntries?.first?.glucose == 100, "Normal glucose value should match")
  168. #expect(storage.alarm == nil, "Should not trigger any alarm")
  169. }
  170. }