InsulinDeliveryStoreTests.swift 45 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020
  1. //
  2. // InsulinDeliveryStoreTests.swift
  3. // LoopKitHostedTests
  4. //
  5. // Created by Darin Krauss on 10/22/20.
  6. // Copyright © 2020 LoopKit Authors. All rights reserved.
  7. //
  8. import XCTest
  9. import HealthKit
  10. @testable import LoopKit
  11. class InsulinDeliveryStoreTestsBase: PersistenceControllerTestCase {
  12. internal let entry1 = DoseEntry(type: .basal,
  13. startDate: Date(timeIntervalSinceNow: -.minutes(6)),
  14. endDate: Date(timeIntervalSinceNow: -.minutes(5.5)),
  15. value: 1.8,
  16. unit: .unitsPerHour,
  17. deliveredUnits: 0.015,
  18. syncIdentifier: "4B14522E-A7B5-4E73-B76B-5043CD7176B0",
  19. scheduledBasalRate: nil)
  20. internal let entry2 = DoseEntry(type: .tempBasal,
  21. startDate: Date(timeIntervalSinceNow: -.minutes(2)),
  22. endDate: Date(timeIntervalSinceNow: -.minutes(1.5)),
  23. value: 2.4,
  24. unit: .unitsPerHour,
  25. deliveredUnits: 0.02,
  26. syncIdentifier: "A1F8E29B-33D6-4B38-B4CD-D84F14744871",
  27. scheduledBasalRate: HKQuantity(unit: .internationalUnitsPerHour, doubleValue: 1.8))
  28. internal let entry3 = DoseEntry(type: .bolus,
  29. startDate: Date(timeIntervalSinceNow: -.minutes(4)),
  30. endDate: Date(timeIntervalSinceNow: -.minutes(3.5)),
  31. value: 1.0,
  32. unit: .units,
  33. deliveredUnits: nil,
  34. syncIdentifier: "1A1D6192-1521-4469-B962-1B82C4534BB1",
  35. scheduledBasalRate: nil)
  36. internal let device = HKDevice(name: UUID().uuidString,
  37. manufacturer: UUID().uuidString,
  38. model: UUID().uuidString,
  39. hardwareVersion: UUID().uuidString,
  40. firmwareVersion: UUID().uuidString,
  41. softwareVersion: UUID().uuidString,
  42. localIdentifier: UUID().uuidString,
  43. udiDeviceIdentifier: UUID().uuidString)
  44. var mockHealthStore: HKHealthStoreMock!
  45. var insulinDeliveryStore: InsulinDeliveryStore!
  46. var hkSampleStore: HealthKitSampleStore!
  47. var authorizationStatus: HKAuthorizationStatus = .notDetermined
  48. override func setUp() {
  49. super.setUp()
  50. mockHealthStore = HKHealthStoreMock()
  51. hkSampleStore = HealthKitSampleStore(healthStore: mockHealthStore, type: HealthKitSampleStore.insulinQuantityType)
  52. hkSampleStore.observerQueryType = MockHKObserverQuery.self
  53. hkSampleStore.anchoredObjectQueryType = MockHKAnchoredObjectQuery.self
  54. mockHealthStore.authorizationStatus = authorizationStatus
  55. insulinDeliveryStore = InsulinDeliveryStore(healthKitSampleStore: hkSampleStore,
  56. cacheStore: cacheStore,
  57. cacheLength: .hours(1),
  58. provenanceIdentifier: HKSource.default().bundleIdentifier)
  59. let semaphore = DispatchSemaphore(value: 0)
  60. cacheStore.onReady { error in
  61. XCTAssertNil(error)
  62. semaphore.signal()
  63. }
  64. semaphore.wait()
  65. }
  66. override func tearDown() {
  67. let semaphore = DispatchSemaphore(value: 0)
  68. insulinDeliveryStore.purgeAllDoseEntries(healthKitPredicate: HKQuery.predicateForObjects(from: HKSource.default())) { error in
  69. XCTAssertNil(error)
  70. semaphore.signal()
  71. }
  72. semaphore.wait()
  73. insulinDeliveryStore = nil
  74. mockHealthStore = nil
  75. super.tearDown()
  76. }
  77. }
  78. class InsulinDeliveryStoreTestsAuthorized: InsulinDeliveryStoreTestsBase {
  79. override func setUp() {
  80. authorizationStatus = .sharingAuthorized
  81. super.setUp()
  82. }
  83. func testObserverQueryStartup() {
  84. // Check that an observer query is registered when authorization is already determined.
  85. XCTAssertFalse(hkSampleStore.authorizationRequired);
  86. mockHealthStore.observerQueryStartedExpectation = expectation(description: "observer query started")
  87. waitForExpectations(timeout: 2)
  88. XCTAssertNotNil(mockHealthStore.observerQuery)
  89. }
  90. }
  91. class InsulinDeliveryStoreTests: InsulinDeliveryStoreTestsBase {
  92. // MARK: - HealthKitSampleStore
  93. func testHealthKitQueryAnchorPersistence() {
  94. XCTAssert(hkSampleStore.authorizationRequired);
  95. XCTAssertNil(hkSampleStore.observerQuery);
  96. mockHealthStore.observerQueryStartedExpectation = expectation(description: "observer query started")
  97. mockHealthStore.authorizationStatus = .sharingAuthorized
  98. hkSampleStore.authorizationIsDetermined()
  99. waitForExpectations(timeout: 3)
  100. XCTAssertNotNil(mockHealthStore.observerQuery)
  101. mockHealthStore.anchorQueryStartedExpectation = expectation(description: "anchored object query started")
  102. let observerQueryCompletionExpectation = expectation(description: "observer query completion")
  103. let observerQueryCompletionHandler = {
  104. observerQueryCompletionExpectation.fulfill()
  105. }
  106. let mockObserverQuery = mockHealthStore.observerQuery as! MockHKObserverQuery
  107. // This simulates a signal marking the arrival of new HK Data.
  108. mockObserverQuery.updateHandler?(mockObserverQuery, observerQueryCompletionHandler, nil)
  109. wait(for: [mockHealthStore.anchorQueryStartedExpectation!])
  110. let currentAnchor = HKQueryAnchor(fromValue: 5)
  111. let mockAnchoredObjectQuery = mockHealthStore.anchoredObjectQuery as! MockHKAnchoredObjectQuery
  112. mockAnchoredObjectQuery.resultsHandler?(mockAnchoredObjectQuery, [], [], currentAnchor, nil)
  113. // Wait for observerQueryCompletionExpectation
  114. waitForExpectations(timeout: 3)
  115. XCTAssertNotNil(hkSampleStore.queryAnchor)
  116. cacheStore.managedObjectContext.performAndWait {}
  117. // Create a new carb store, and ensure it uses the last query anchor
  118. let newSampleStore = HealthKitSampleStore(healthStore: mockHealthStore, type: HealthKitSampleStore.insulinQuantityType)
  119. newSampleStore.observerQueryType = MockHKObserverQuery.self
  120. newSampleStore.anchoredObjectQueryType = MockHKAnchoredObjectQuery.self
  121. // Create a new glucose store, and ensure it uses the last query anchor
  122. let _ = InsulinDeliveryStore(healthKitSampleStore: newSampleStore,
  123. cacheStore: cacheStore,
  124. provenanceIdentifier: HKSource.default().bundleIdentifier)
  125. mockHealthStore.observerQueryStartedExpectation = expectation(description: "new observer query started")
  126. mockHealthStore.authorizationStatus = .sharingAuthorized
  127. newSampleStore.authorizationIsDetermined()
  128. // Wait for observerQueryCompletionExpectation
  129. waitForExpectations(timeout: 3)
  130. mockHealthStore.anchorQueryStartedExpectation = expectation(description: "new anchored object query started")
  131. let mockObserverQuery2 = mockHealthStore.observerQuery as! MockHKObserverQuery
  132. // This simulates a signal marking the arrival of new HK Data.
  133. mockObserverQuery2.updateHandler?(mockObserverQuery2, {}, nil)
  134. // Wait for anchorQueryStartedExpectation
  135. waitForExpectations(timeout: 3)
  136. // Assert new carb store is querying with the last anchor that our HealthKit mock returned
  137. let mockAnchoredObjectQuery2 = mockHealthStore.anchoredObjectQuery as! MockHKAnchoredObjectQuery
  138. XCTAssertEqual(currentAnchor, mockAnchoredObjectQuery2.anchor)
  139. mockAnchoredObjectQuery2.resultsHandler?(mockAnchoredObjectQuery2, [], [], currentAnchor, nil)
  140. }
  141. // MARK: - Fetching
  142. func testGetDoseEntries() {
  143. let addDoseEntriesCompletion = expectation(description: "addDoseEntries")
  144. insulinDeliveryStore.addDoseEntries([entry1, entry2, entry3], from: device, syncVersion: 2) { result in
  145. switch result {
  146. case .failure(let error):
  147. XCTFail("Unexpected failure: \(error)")
  148. case .success:
  149. break
  150. }
  151. addDoseEntriesCompletion.fulfill()
  152. }
  153. waitForExpectations(timeout: 10)
  154. let getDoseEntries1Completion = expectation(description: "getDoseEntries1")
  155. insulinDeliveryStore.getDoseEntries() { result in
  156. switch result {
  157. case .failure(let error):
  158. XCTFail("Unexpected failure: \(error)")
  159. case .success(let entries):
  160. XCTAssertEqual(entries.count, 3)
  161. XCTAssertEqual(entries[0].type, self.entry1.type)
  162. XCTAssertEqual(entries[0].startDate, self.entry1.startDate)
  163. XCTAssertEqual(entries[0].endDate, self.entry1.endDate)
  164. XCTAssertEqual(entries[0].value, 0.015)
  165. XCTAssertEqual(entries[0].unit, .units)
  166. XCTAssertNil(entries[0].deliveredUnits)
  167. XCTAssertEqual(entries[0].description, self.entry1.description)
  168. XCTAssertEqual(entries[0].syncIdentifier, self.entry1.syncIdentifier)
  169. XCTAssertEqual(entries[0].scheduledBasalRate, self.entry1.scheduledBasalRate)
  170. XCTAssertEqual(entries[1].type, self.entry3.type)
  171. XCTAssertEqual(entries[1].startDate, self.entry3.startDate)
  172. XCTAssertEqual(entries[1].endDate, self.entry3.endDate)
  173. XCTAssertEqual(entries[1].value, self.entry3.value)
  174. XCTAssertEqual(entries[1].unit, self.entry3.unit)
  175. XCTAssertEqual(entries[1].deliveredUnits, self.entry3.deliveredUnits)
  176. XCTAssertEqual(entries[1].description, self.entry3.description)
  177. XCTAssertEqual(entries[1].syncIdentifier, self.entry3.syncIdentifier)
  178. XCTAssertEqual(entries[1].scheduledBasalRate, self.entry3.scheduledBasalRate)
  179. XCTAssertEqual(entries[2].type, self.entry2.type)
  180. XCTAssertEqual(entries[2].startDate, self.entry2.startDate)
  181. XCTAssertEqual(entries[2].endDate, self.entry2.endDate)
  182. XCTAssertEqual(entries[2].value, self.entry2.value)
  183. XCTAssertEqual(entries[2].unit, self.entry2.unit)
  184. XCTAssertEqual(entries[2].deliveredUnits, self.entry2.deliveredUnits)
  185. XCTAssertEqual(entries[2].description, self.entry2.description)
  186. XCTAssertEqual(entries[2].syncIdentifier, self.entry2.syncIdentifier)
  187. XCTAssertEqual(entries[2].scheduledBasalRate, self.entry2.scheduledBasalRate)
  188. }
  189. getDoseEntries1Completion.fulfill()
  190. }
  191. waitForExpectations(timeout: 10)
  192. let getDoseEntries2Completion = expectation(description: "getDoseEntries2")
  193. insulinDeliveryStore.getDoseEntries(start: Date(timeIntervalSinceNow: -.minutes(3.75)), end: Date(timeIntervalSinceNow: -.minutes(1.75))) { result in
  194. switch result {
  195. case .failure(let error):
  196. XCTFail("Unexpected failure: \(error)")
  197. case .success(let entries):
  198. XCTAssertEqual(entries.count, 2)
  199. XCTAssertEqual(entries[0].type, self.entry3.type)
  200. XCTAssertEqual(entries[0].startDate, self.entry3.startDate)
  201. XCTAssertEqual(entries[0].endDate, self.entry3.endDate)
  202. XCTAssertEqual(entries[0].value, self.entry3.value)
  203. XCTAssertEqual(entries[0].unit, self.entry3.unit)
  204. XCTAssertEqual(entries[0].deliveredUnits, self.entry3.deliveredUnits)
  205. XCTAssertEqual(entries[0].description, self.entry3.description)
  206. XCTAssertEqual(entries[0].syncIdentifier, self.entry3.syncIdentifier)
  207. XCTAssertEqual(entries[0].scheduledBasalRate, self.entry3.scheduledBasalRate)
  208. XCTAssertEqual(entries[1].type, self.entry2.type)
  209. XCTAssertEqual(entries[1].startDate, self.entry2.startDate)
  210. XCTAssertEqual(entries[1].endDate, self.entry2.endDate)
  211. XCTAssertEqual(entries[1].value, self.entry2.value)
  212. XCTAssertEqual(entries[1].unit, self.entry2.unit)
  213. XCTAssertEqual(entries[1].deliveredUnits, self.entry2.deliveredUnits)
  214. XCTAssertEqual(entries[1].description, self.entry2.description)
  215. XCTAssertEqual(entries[1].syncIdentifier, self.entry2.syncIdentifier)
  216. XCTAssertEqual(entries[1].scheduledBasalRate, self.entry2.scheduledBasalRate)
  217. }
  218. getDoseEntries2Completion.fulfill()
  219. }
  220. waitForExpectations(timeout: 10)
  221. let purgeCachedInsulinDeliveryObjectsCompletion = expectation(description: "purgeCachedInsulinDeliveryObjects")
  222. insulinDeliveryStore.purgeCachedInsulinDeliveryObjects() { error in
  223. XCTAssertNil(error)
  224. purgeCachedInsulinDeliveryObjectsCompletion.fulfill()
  225. }
  226. waitForExpectations(timeout: 10)
  227. let getDoseEntries3Completion = expectation(description: "getDoseEntries3")
  228. insulinDeliveryStore.getDoseEntries() { result in
  229. switch result {
  230. case .failure(let error):
  231. XCTFail("Unexpected failure: \(error)")
  232. case .success(let entries):
  233. XCTAssertEqual(entries.count, 0)
  234. }
  235. getDoseEntries3Completion.fulfill()
  236. }
  237. waitForExpectations(timeout: 10)
  238. }
  239. func testLastBasalEndDate() {
  240. let getLastImmutableBasalEndDate1Completion = expectation(description: "getLastImmutableBasalEndDate1")
  241. insulinDeliveryStore.getLastImmutableBasalEndDate() { result in
  242. switch result {
  243. case .failure(let error):
  244. XCTFail("Unexpected failure: \(error)")
  245. case .success(let lastBasalEndDate):
  246. XCTAssertEqual(lastBasalEndDate, .distantPast)
  247. }
  248. getLastImmutableBasalEndDate1Completion.fulfill()
  249. }
  250. waitForExpectations(timeout: 10)
  251. let addDoseEntriesCompletion = expectation(description: "addDoseEntries")
  252. insulinDeliveryStore.addDoseEntries([entry1, entry2, entry3], from: device, syncVersion: 2) { result in
  253. switch result {
  254. case .failure(let error):
  255. XCTFail("Unexpected failure: \(error)")
  256. case .success:
  257. break
  258. }
  259. addDoseEntriesCompletion.fulfill()
  260. }
  261. waitForExpectations(timeout: 10)
  262. let getLastImmutableBasalEndDate2Completion = expectation(description: "getLastImmutableBasalEndDate2")
  263. insulinDeliveryStore.getLastImmutableBasalEndDate() { result in
  264. switch result {
  265. case .failure(let error):
  266. XCTFail("Unexpected failure: \(error)")
  267. case .success(let lastBasalEndDate):
  268. XCTAssertEqual(lastBasalEndDate, self.entry2.endDate)
  269. }
  270. getLastImmutableBasalEndDate2Completion.fulfill()
  271. }
  272. waitForExpectations(timeout: 10)
  273. let purgeCachedInsulinDeliveryObjectsCompletion = expectation(description: "purgeCachedInsulinDeliveryObjects")
  274. insulinDeliveryStore.purgeCachedInsulinDeliveryObjects() { error in
  275. XCTAssertNil(error)
  276. purgeCachedInsulinDeliveryObjectsCompletion.fulfill()
  277. }
  278. waitForExpectations(timeout: 10)
  279. let getLastImmutableBasalEndDate3Completion = expectation(description: "getLastImmutableBasalEndDate3")
  280. insulinDeliveryStore.getLastImmutableBasalEndDate() { result in
  281. switch result {
  282. case .failure(let error):
  283. XCTFail("Unexpected failure: \(error)")
  284. case .success(let lastBasalEndDate):
  285. XCTAssertEqual(lastBasalEndDate, .distantPast)
  286. }
  287. getLastImmutableBasalEndDate3Completion.fulfill()
  288. }
  289. waitForExpectations(timeout: 10)
  290. }
  291. // MARK: - Modification
  292. func testAddDoseEntries() {
  293. let addDoseEntries1Completion = expectation(description: "addDoseEntries1")
  294. insulinDeliveryStore.addDoseEntries([entry1, entry2, entry3], from: device, syncVersion: 2) { result in
  295. switch result {
  296. case .failure(let error):
  297. XCTFail("Unexpected failure: \(error)")
  298. case .success:
  299. break
  300. }
  301. addDoseEntries1Completion.fulfill()
  302. }
  303. waitForExpectations(timeout: 10)
  304. let getDoseEntries1Completion = expectation(description: "getDoseEntries1")
  305. insulinDeliveryStore.getDoseEntries() { result in
  306. switch result {
  307. case .failure(let error):
  308. XCTFail("Unexpected failure: \(error)")
  309. case .success(let entries):
  310. XCTAssertEqual(entries.count, 3)
  311. XCTAssertEqual(entries[0].type, self.entry1.type)
  312. XCTAssertEqual(entries[0].startDate, self.entry1.startDate)
  313. XCTAssertEqual(entries[0].endDate, self.entry1.endDate)
  314. XCTAssertEqual(entries[0].value, 0.015)
  315. XCTAssertEqual(entries[0].unit, .units)
  316. XCTAssertNil(entries[0].deliveredUnits)
  317. XCTAssertEqual(entries[0].description, self.entry1.description)
  318. XCTAssertEqual(entries[0].syncIdentifier, self.entry1.syncIdentifier)
  319. XCTAssertEqual(entries[0].scheduledBasalRate, self.entry1.scheduledBasalRate)
  320. XCTAssertEqual(entries[1].type, self.entry3.type)
  321. XCTAssertEqual(entries[1].startDate, self.entry3.startDate)
  322. XCTAssertEqual(entries[1].endDate, self.entry3.endDate)
  323. XCTAssertEqual(entries[1].value, self.entry3.value)
  324. XCTAssertEqual(entries[1].unit, self.entry3.unit)
  325. XCTAssertEqual(entries[1].deliveredUnits, self.entry3.deliveredUnits)
  326. XCTAssertEqual(entries[1].description, self.entry3.description)
  327. XCTAssertEqual(entries[1].syncIdentifier, self.entry3.syncIdentifier)
  328. XCTAssertEqual(entries[1].scheduledBasalRate, self.entry3.scheduledBasalRate)
  329. XCTAssertEqual(entries[2].type, self.entry2.type)
  330. XCTAssertEqual(entries[2].startDate, self.entry2.startDate)
  331. XCTAssertEqual(entries[2].endDate, self.entry2.endDate)
  332. XCTAssertEqual(entries[2].value, self.entry2.value)
  333. XCTAssertEqual(entries[2].unit, self.entry2.unit)
  334. XCTAssertEqual(entries[2].deliveredUnits, self.entry2.deliveredUnits)
  335. XCTAssertEqual(entries[2].description, self.entry2.description)
  336. XCTAssertEqual(entries[2].syncIdentifier, self.entry2.syncIdentifier)
  337. XCTAssertEqual(entries[2].scheduledBasalRate, self.entry2.scheduledBasalRate)
  338. }
  339. getDoseEntries1Completion.fulfill()
  340. }
  341. waitForExpectations(timeout: 10)
  342. let addDoseEntries2Completion = expectation(description: "addDoseEntries2")
  343. insulinDeliveryStore.addDoseEntries([entry3, entry1, entry2], from: device, syncVersion: 2) { result in
  344. switch result {
  345. case .failure(let error):
  346. XCTFail("Unexpected failure: \(error)")
  347. case .success:
  348. break
  349. }
  350. addDoseEntries2Completion.fulfill()
  351. }
  352. waitForExpectations(timeout: 10)
  353. let getDoseEntries2Completion = expectation(description: "getDoseEntries2Completion")
  354. insulinDeliveryStore.getDoseEntries() { result in
  355. switch result {
  356. case .failure(let error):
  357. XCTFail("Unexpected failure: \(error)")
  358. case .success(let entries):
  359. XCTAssertEqual(entries.count, 3)
  360. XCTAssertEqual(entries[0].type, self.entry1.type)
  361. XCTAssertEqual(entries[0].startDate, self.entry1.startDate)
  362. XCTAssertEqual(entries[0].endDate, self.entry1.endDate)
  363. XCTAssertEqual(entries[0].value, 0.015)
  364. XCTAssertEqual(entries[0].unit, .units)
  365. XCTAssertNil(entries[0].deliveredUnits)
  366. XCTAssertEqual(entries[0].description, self.entry1.description)
  367. XCTAssertEqual(entries[0].syncIdentifier, self.entry1.syncIdentifier)
  368. XCTAssertEqual(entries[0].scheduledBasalRate, self.entry1.scheduledBasalRate)
  369. XCTAssertEqual(entries[1].type, self.entry3.type)
  370. XCTAssertEqual(entries[1].startDate, self.entry3.startDate)
  371. XCTAssertEqual(entries[1].endDate, self.entry3.endDate)
  372. XCTAssertEqual(entries[1].value, self.entry3.value)
  373. XCTAssertEqual(entries[1].unit, self.entry3.unit)
  374. XCTAssertEqual(entries[1].deliveredUnits, self.entry3.deliveredUnits)
  375. XCTAssertEqual(entries[1].description, self.entry3.description)
  376. XCTAssertEqual(entries[1].syncIdentifier, self.entry3.syncIdentifier)
  377. XCTAssertEqual(entries[1].scheduledBasalRate, self.entry3.scheduledBasalRate)
  378. XCTAssertEqual(entries[2].type, self.entry2.type)
  379. XCTAssertEqual(entries[2].startDate, self.entry2.startDate)
  380. XCTAssertEqual(entries[2].endDate, self.entry2.endDate)
  381. XCTAssertEqual(entries[2].value, self.entry2.value)
  382. XCTAssertEqual(entries[2].unit, self.entry2.unit)
  383. XCTAssertEqual(entries[2].deliveredUnits, self.entry2.deliveredUnits)
  384. XCTAssertEqual(entries[2].description, self.entry2.description)
  385. XCTAssertEqual(entries[2].syncIdentifier, self.entry2.syncIdentifier)
  386. XCTAssertEqual(entries[2].scheduledBasalRate, self.entry2.scheduledBasalRate)
  387. }
  388. getDoseEntries2Completion.fulfill()
  389. }
  390. waitForExpectations(timeout: 10)
  391. }
  392. func testAddDoseEntriesEmpty() {
  393. let addDoseEntriesCompletion = expectation(description: "addDoseEntries")
  394. insulinDeliveryStore.addDoseEntries([], from: device, syncVersion: 2) { result in
  395. switch result {
  396. case .failure(let error):
  397. XCTFail("Unexpected failure: \(error)")
  398. case .success:
  399. break
  400. }
  401. addDoseEntriesCompletion.fulfill()
  402. }
  403. waitForExpectations(timeout: 10)
  404. }
  405. func testAddDoseEntriesNotification() {
  406. let doseEntriesDidChangeCompletion = expectation(description: "doseEntriesDidChange")
  407. let observer = NotificationCenter.default.addObserver(forName: InsulinDeliveryStore.doseEntriesDidChange, object: insulinDeliveryStore, queue: nil) { notification in
  408. doseEntriesDidChangeCompletion.fulfill()
  409. }
  410. let addDoseEntriesCompletion = expectation(description: "addDoseEntries")
  411. insulinDeliveryStore.addDoseEntries([entry1, entry2, entry3], from: device, syncVersion: 2) { result in
  412. switch result {
  413. case .failure(let error):
  414. XCTFail("Unexpected failure: \(error)")
  415. case .success:
  416. break
  417. }
  418. addDoseEntriesCompletion.fulfill()
  419. }
  420. wait(for: [doseEntriesDidChangeCompletion, addDoseEntriesCompletion], timeout: 10, enforceOrder: true)
  421. NotificationCenter.default.removeObserver(observer)
  422. }
  423. func testManuallyEnteredDoses() {
  424. let manualEntry = DoseEntry(type: .bolus,
  425. startDate: Date(timeIntervalSinceNow: -.minutes(15)),
  426. endDate: Date(timeIntervalSinceNow: -.minutes(10)),
  427. value: 3.0,
  428. unit: .units,
  429. syncIdentifier: "C0AB1CBD-6B36-4113-9D49-709A022B2451",
  430. manuallyEntered: true)
  431. let addDoseEntriesCompletion = expectation(description: "addDoseEntries")
  432. insulinDeliveryStore.addDoseEntries([entry1, manualEntry, entry2, entry3], from: device, syncVersion: 2) { result in
  433. switch result {
  434. case .failure(let error):
  435. XCTFail("Unexpected failure: \(error)")
  436. case .success:
  437. break
  438. }
  439. addDoseEntriesCompletion.fulfill()
  440. }
  441. waitForExpectations(timeout: 10)
  442. let getManuallyEnteredDoses1Completion = expectation(description: "getManuallyEnteredDoses1")
  443. insulinDeliveryStore.getManuallyEnteredDoses(since: .distantPast) { result in
  444. switch result {
  445. case .failure(let error):
  446. XCTFail("Unexpected failure: \(error)")
  447. case .success(let entries):
  448. XCTAssertEqual(entries.count, 1)
  449. XCTAssertEqual(entries[0], manualEntry)
  450. }
  451. getManuallyEnteredDoses1Completion.fulfill()
  452. }
  453. waitForExpectations(timeout: 10)
  454. let getManuallyEnteredDoses2Completion = expectation(description: "getManuallyEnteredDoses2")
  455. insulinDeliveryStore.getManuallyEnteredDoses(since: Date(timeIntervalSinceNow: -.minutes(12))) { result in
  456. switch result {
  457. case .failure(let error):
  458. XCTFail("Unexpected failure: \(error)")
  459. case .success(let entries):
  460. XCTAssertEqual(entries.count, 0)
  461. }
  462. getManuallyEnteredDoses2Completion.fulfill()
  463. }
  464. waitForExpectations(timeout: 10)
  465. let deleteAllManuallyEnteredDoses1Completion = expectation(description: "deleteAllManuallyEnteredDoses1")
  466. insulinDeliveryStore.deleteAllManuallyEnteredDoses(since: Date(timeIntervalSinceNow: -.minutes(12))) { error in
  467. XCTAssertNil(error)
  468. deleteAllManuallyEnteredDoses1Completion.fulfill()
  469. }
  470. waitForExpectations(timeout: 10)
  471. let getManuallyEnteredDoses3Completion = expectation(description: "getManuallyEnteredDoses3")
  472. insulinDeliveryStore.getManuallyEnteredDoses(since: .distantPast) { result in
  473. switch result {
  474. case .failure(let error):
  475. XCTFail("Unexpected failure: \(error)")
  476. case .success(let entries):
  477. XCTAssertEqual(entries.count, 1)
  478. XCTAssertEqual(entries[0], manualEntry)
  479. }
  480. getManuallyEnteredDoses3Completion.fulfill()
  481. }
  482. waitForExpectations(timeout: 10)
  483. let deleteAllManuallyEnteredDose2Completion = expectation(description: "deleteAllManuallyEnteredDose2")
  484. insulinDeliveryStore.deleteAllManuallyEnteredDoses(since: Date(timeIntervalSinceNow: -.minutes(20))) { error in
  485. XCTAssertNil(error)
  486. deleteAllManuallyEnteredDose2Completion.fulfill()
  487. }
  488. waitForExpectations(timeout: 10)
  489. let getManuallyEnteredDoses4Completion = expectation(description: "getManuallyEnteredDoses4")
  490. insulinDeliveryStore.getManuallyEnteredDoses(since: .distantPast) { result in
  491. switch result {
  492. case .failure(let error):
  493. XCTFail("Unexpected failure: \(error)")
  494. case .success(let entries):
  495. XCTAssertEqual(entries.count, 0)
  496. }
  497. getManuallyEnteredDoses4Completion.fulfill()
  498. }
  499. waitForExpectations(timeout: 10)
  500. }
  501. // MARK: - Cache Management
  502. func testEarliestCacheDate() {
  503. XCTAssertEqual(insulinDeliveryStore.earliestCacheDate.timeIntervalSinceNow, -.hours(1), accuracy: 1)
  504. }
  505. func testPurgeAllDoseEntries() {
  506. let addDoseEntriesCompletion = expectation(description: "addDoseEntries")
  507. insulinDeliveryStore.addDoseEntries([entry1, entry2, entry3], from: device, syncVersion: 2) { result in
  508. switch result {
  509. case .failure(let error):
  510. XCTFail("Unexpected failure: \(error)")
  511. case .success:
  512. break
  513. }
  514. addDoseEntriesCompletion.fulfill()
  515. }
  516. waitForExpectations(timeout: 10)
  517. let getDoseEntries1Completion = expectation(description: "getDoseEntries1")
  518. insulinDeliveryStore.getDoseEntries() { result in
  519. switch result {
  520. case .failure(let error):
  521. XCTFail("Unexpected failure: \(error)")
  522. case .success(let entries):
  523. XCTAssertEqual(entries.count, 3)
  524. }
  525. getDoseEntries1Completion.fulfill()
  526. }
  527. waitForExpectations(timeout: 10)
  528. let purgeAllDoseEntriesCompletion = expectation(description: "purgeAllDoseEntries")
  529. insulinDeliveryStore.purgeAllDoseEntries(healthKitPredicate: HKQuery.predicateForObjects(from: HKSource.default())) { error in
  530. XCTAssertNil(error)
  531. purgeAllDoseEntriesCompletion.fulfill()
  532. }
  533. waitForExpectations(timeout: 10)
  534. let getDoseEntries2Completion = expectation(description: "getDoseEntries2")
  535. insulinDeliveryStore.getDoseEntries() { result in
  536. switch result {
  537. case .failure(let error):
  538. XCTFail("Unexpected failure: \(error)")
  539. case .success(let entries):
  540. XCTAssertEqual(entries.count, 0)
  541. }
  542. getDoseEntries2Completion.fulfill()
  543. }
  544. waitForExpectations(timeout: 10)
  545. }
  546. func testPurgeExpiredGlucoseObjects() {
  547. let expiredEntry = DoseEntry(type: .bolus,
  548. startDate: Date(timeIntervalSinceNow: -.hours(3)),
  549. endDate: Date(timeIntervalSinceNow: -.hours(2)),
  550. value: 3.0,
  551. unit: .units,
  552. deliveredUnits: nil,
  553. syncIdentifier: "7530B8CA-827A-4DE8-ADE3-9E10FF80A4A9",
  554. scheduledBasalRate: nil)
  555. let addDoseEntriesCompletion = expectation(description: "addDoseEntries")
  556. insulinDeliveryStore.addDoseEntries([entry1, entry2, entry3, expiredEntry], from: device, syncVersion: 2) { result in
  557. switch result {
  558. case .failure(let error):
  559. XCTFail("Unexpected failure: \(error)")
  560. case .success:
  561. break
  562. }
  563. addDoseEntriesCompletion.fulfill()
  564. }
  565. waitForExpectations(timeout: 10)
  566. let getDoseEntriesCompletion = expectation(description: "getDoseEntries")
  567. insulinDeliveryStore.getDoseEntries() { result in
  568. switch result {
  569. case .failure(let error):
  570. XCTFail("Unexpected failure: \(error)")
  571. case .success(let entries):
  572. XCTAssertEqual(entries.count, 3)
  573. }
  574. getDoseEntriesCompletion.fulfill()
  575. }
  576. waitForExpectations(timeout: 10)
  577. }
  578. func testPurgeCachedInsulinDeliveryObjects() {
  579. let addDoseEntriesCompletion = expectation(description: "addDoseEntries")
  580. insulinDeliveryStore.addDoseEntries([entry1, entry2, entry3], from: device, syncVersion: 2) { result in
  581. switch result {
  582. case .failure(let error):
  583. XCTFail("Unexpected failure: \(error)")
  584. case .success:
  585. break
  586. }
  587. addDoseEntriesCompletion.fulfill()
  588. }
  589. waitForExpectations(timeout: 10)
  590. let getDoseEntries1Completion = expectation(description: "getDoseEntries1")
  591. insulinDeliveryStore.getDoseEntries() { result in
  592. switch result {
  593. case .failure(let error):
  594. XCTFail("Unexpected failure: \(error)")
  595. case .success(let entries):
  596. XCTAssertEqual(entries.count, 3)
  597. }
  598. getDoseEntries1Completion.fulfill()
  599. }
  600. waitForExpectations(timeout: 10)
  601. let purgeCachedInsulinDeliveryObjects1Completion = expectation(description: "purgeCachedInsulinDeliveryObjects1")
  602. insulinDeliveryStore.purgeCachedInsulinDeliveryObjects(before: Date(timeIntervalSinceNow: -.minutes(5))) { error in
  603. XCTAssertNil(error)
  604. purgeCachedInsulinDeliveryObjects1Completion.fulfill()
  605. }
  606. waitForExpectations(timeout: 10)
  607. let getDoseEntries2Completion = expectation(description: "getDoseEntries2")
  608. insulinDeliveryStore.getDoseEntries() { result in
  609. switch result {
  610. case .failure(let error):
  611. XCTFail("Unexpected failure: \(error)")
  612. case .success(let entries):
  613. XCTAssertEqual(entries.count, 2)
  614. }
  615. getDoseEntries2Completion.fulfill()
  616. }
  617. waitForExpectations(timeout: 10)
  618. let purgeCachedInsulinDeliveryObjects2Completion = expectation(description: "purgeCachedInsulinDeliveryObjects2")
  619. insulinDeliveryStore.purgeCachedInsulinDeliveryObjects() { error in
  620. XCTAssertNil(error)
  621. purgeCachedInsulinDeliveryObjects2Completion.fulfill()
  622. }
  623. waitForExpectations(timeout: 10)
  624. let getDoseEntries3Completion = expectation(description: "getDoseEntries3")
  625. insulinDeliveryStore.getDoseEntries() { result in
  626. switch result {
  627. case .failure(let error):
  628. XCTFail("Unexpected failure: \(error)")
  629. case .success(let entries):
  630. XCTAssertEqual(entries.count, 0)
  631. }
  632. getDoseEntries3Completion.fulfill()
  633. }
  634. waitForExpectations(timeout: 10)
  635. }
  636. func testPurgeCachedInsulinDeliveryObjectsNotification() {
  637. let addDoseEntriesCompletion = expectation(description: "addDoseEntries")
  638. insulinDeliveryStore.addDoseEntries([entry1, entry2, entry3], from: device, syncVersion: 2) { result in
  639. switch result {
  640. case .failure(let error):
  641. XCTFail("Unexpected failure: \(error)")
  642. case .success:
  643. break
  644. }
  645. addDoseEntriesCompletion.fulfill()
  646. }
  647. waitForExpectations(timeout: 10)
  648. let doseEntriesDidChangeCompletion = expectation(description: "doseEntriesDidChange")
  649. let observer = NotificationCenter.default.addObserver(forName: InsulinDeliveryStore.doseEntriesDidChange, object: insulinDeliveryStore, queue: nil) { notification in
  650. doseEntriesDidChangeCompletion.fulfill()
  651. }
  652. let purgeCachedInsulinDeliveryObjectsCompletion = expectation(description: "purgeCachedInsulinDeliveryObjects")
  653. insulinDeliveryStore.purgeCachedInsulinDeliveryObjects() { error in
  654. XCTAssertNil(error)
  655. purgeCachedInsulinDeliveryObjectsCompletion.fulfill()
  656. }
  657. wait(for: [doseEntriesDidChangeCompletion, purgeCachedInsulinDeliveryObjectsCompletion], timeout: 10, enforceOrder: true)
  658. NotificationCenter.default.removeObserver(observer)
  659. }
  660. }
  661. class InsulinDeliveryStoreQueryAnchorTests: XCTestCase {
  662. var rawValue: InsulinDeliveryStore.QueryAnchor.RawValue = [
  663. "modificationCounter": Int64(123)
  664. ]
  665. func testInitializerDefault() {
  666. let queryAnchor = InsulinDeliveryStore.QueryAnchor()
  667. XCTAssertEqual(queryAnchor.modificationCounter, 0)
  668. }
  669. func testInitializerRawValue() {
  670. let queryAnchor = InsulinDeliveryStore.QueryAnchor(rawValue: rawValue)
  671. XCTAssertNotNil(queryAnchor)
  672. XCTAssertEqual(queryAnchor?.modificationCounter, 123)
  673. }
  674. func testInitializerRawValueMissingModificationCounter() {
  675. rawValue["modificationCounter"] = nil
  676. XCTAssertNil(InsulinDeliveryStore.QueryAnchor(rawValue: rawValue))
  677. }
  678. func testInitializerRawValueInvalidModificationCounter() {
  679. rawValue["modificationCounter"] = "123"
  680. XCTAssertNil(InsulinDeliveryStore.QueryAnchor(rawValue: rawValue))
  681. }
  682. func testRawValueWithDefault() {
  683. let rawValue = InsulinDeliveryStore.QueryAnchor().rawValue
  684. XCTAssertEqual(rawValue.count, 1)
  685. XCTAssertEqual(rawValue["modificationCounter"] as? Int64, Int64(0))
  686. }
  687. func testRawValueWithNonDefault() {
  688. var queryAnchor = InsulinDeliveryStore.QueryAnchor()
  689. queryAnchor.modificationCounter = 123
  690. let rawValue = queryAnchor.rawValue
  691. XCTAssertEqual(rawValue.count, 1)
  692. XCTAssertEqual(rawValue["modificationCounter"] as? Int64, Int64(123))
  693. }
  694. }
  695. class InsulinDeliveryStoreQueryTests: PersistenceControllerTestCase {
  696. let insulinModel = WalshInsulinModel(actionDuration: .hours(4))
  697. let basalProfile = BasalRateSchedule(rawValue: ["timeZone": -28800, "items": [["value": 0.75, "startTime": 0.0], ["value": 0.8, "startTime": 10800.0], ["value": 0.85, "startTime": 32400.0], ["value": 1.0, "startTime": 68400.0]]])
  698. let insulinSensitivitySchedule = InsulinSensitivitySchedule(rawValue: ["unit": "mg/dL", "timeZone": -28800, "items": [["value": 40.0, "startTime": 0.0], ["value": 35.0, "startTime": 21600.0], ["value": 40.0, "startTime": 57600.0]]])
  699. var insulinDeliveryStore: InsulinDeliveryStore!
  700. var completion: XCTestExpectation!
  701. var queryAnchor: InsulinDeliveryStore.QueryAnchor!
  702. var limit: Int!
  703. override func setUp() {
  704. super.setUp()
  705. insulinDeliveryStore = InsulinDeliveryStore(cacheStore: cacheStore,
  706. provenanceIdentifier: HKSource.default().bundleIdentifier)
  707. completion = expectation(description: "Completion")
  708. queryAnchor = InsulinDeliveryStore.QueryAnchor()
  709. limit = Int.max
  710. }
  711. override func tearDown() {
  712. limit = nil
  713. queryAnchor = nil
  714. completion = nil
  715. insulinDeliveryStore = nil
  716. super.tearDown()
  717. }
  718. func testDoseEmptyWithDefaultQueryAnchor() {
  719. insulinDeliveryStore.executeDoseQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  720. switch result {
  721. case .failure(let error):
  722. XCTFail("Unexpected failure: \(error)")
  723. case .success(let anchor, let created, let deleted):
  724. XCTAssertEqual(anchor.modificationCounter, 0)
  725. XCTAssertEqual(created.count, 0)
  726. XCTAssertEqual(deleted.count, 0)
  727. }
  728. self.completion.fulfill()
  729. }
  730. wait(for: [completion], timeout: 2, enforceOrder: true)
  731. }
  732. func testDoseEmptyWithMissingQueryAnchor() {
  733. queryAnchor = nil
  734. insulinDeliveryStore.executeDoseQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  735. switch result {
  736. case .failure(let error):
  737. XCTFail("Unexpected failure: \(error)")
  738. case .success(let anchor, let created, let deleted):
  739. XCTAssertEqual(anchor.modificationCounter, 0)
  740. XCTAssertEqual(created.count, 0)
  741. XCTAssertEqual(deleted.count, 0)
  742. }
  743. self.completion.fulfill()
  744. }
  745. wait(for: [completion], timeout: 2, enforceOrder: true)
  746. }
  747. func testDoseEmptyWithNonDefaultQueryAnchor() {
  748. queryAnchor.modificationCounter = 1
  749. insulinDeliveryStore.executeDoseQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  750. switch result {
  751. case .failure(let error):
  752. XCTFail("Unexpected failure: \(error)")
  753. case .success(let anchor, let created, let deleted):
  754. XCTAssertEqual(anchor.modificationCounter, 1)
  755. XCTAssertEqual(created.count, 0)
  756. XCTAssertEqual(deleted.count, 0)
  757. }
  758. self.completion.fulfill()
  759. }
  760. wait(for: [completion], timeout: 2, enforceOrder: true)
  761. }
  762. func testDoseDataWithUnusedQueryAnchor() {
  763. let doseData = [DoseDatum(), DoseDatum(deleted: true), DoseDatum()]
  764. addDoseData(doseData)
  765. insulinDeliveryStore.executeDoseQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  766. switch result {
  767. case .failure(let error):
  768. XCTFail("Unexpected failure: \(error)")
  769. case .success(let anchor, let created, let deleted):
  770. XCTAssertEqual(anchor.modificationCounter, 3)
  771. XCTAssertEqual(created.count, 2)
  772. XCTAssertEqual(deleted.count, 1)
  773. if created.count >= 2 {
  774. XCTAssertEqual(created[0].syncIdentifier, doseData[0].syncIdentifier)
  775. XCTAssertEqual(created[1].syncIdentifier, doseData[2].syncIdentifier)
  776. }
  777. if deleted.count >= 1 {
  778. XCTAssertEqual(deleted[0].syncIdentifier, doseData[1].syncIdentifier)
  779. }
  780. }
  781. self.completion.fulfill()
  782. }
  783. wait(for: [completion], timeout: 2, enforceOrder: true)
  784. }
  785. func testDoseDataWithStaleQueryAnchor() {
  786. let doseData = [DoseDatum(), DoseDatum(deleted: true), DoseDatum()]
  787. addDoseData(doseData)
  788. queryAnchor.modificationCounter = 2
  789. insulinDeliveryStore.executeDoseQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  790. switch result {
  791. case .failure(let error):
  792. XCTFail("Unexpected failure: \(error)")
  793. case .success(let anchor, let created, let deleted):
  794. XCTAssertEqual(anchor.modificationCounter, 3)
  795. XCTAssertEqual(created.count, 1)
  796. XCTAssertEqual(deleted.count, 0)
  797. if created.count >= 1 {
  798. XCTAssertEqual(created[0].syncIdentifier, doseData[2].syncIdentifier)
  799. }
  800. }
  801. self.completion.fulfill()
  802. }
  803. wait(for: [completion], timeout: 2, enforceOrder: true)
  804. }
  805. func testDoseDataWithCurrentQueryAnchor() {
  806. let doseData = [DoseDatum(), DoseDatum(deleted: true), DoseDatum()]
  807. addDoseData(doseData)
  808. queryAnchor.modificationCounter = 3
  809. insulinDeliveryStore.executeDoseQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  810. switch result {
  811. case .failure(let error):
  812. XCTFail("Unexpected failure: \(error)")
  813. case .success(let anchor, let created, let deleted):
  814. XCTAssertEqual(anchor.modificationCounter, 3)
  815. XCTAssertEqual(created.count, 0)
  816. XCTAssertEqual(deleted.count, 0)
  817. }
  818. self.completion.fulfill()
  819. }
  820. wait(for: [completion], timeout: 2, enforceOrder: true)
  821. }
  822. func testDoseDataWithLimitCoveredByData() {
  823. let doseData = [DoseDatum(), DoseDatum(deleted: true), DoseDatum()]
  824. addDoseData(doseData)
  825. limit = 2
  826. insulinDeliveryStore.executeDoseQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  827. switch result {
  828. case .failure(let error):
  829. XCTFail("Unexpected failure: \(error)")
  830. case .success(let anchor, let created, let deleted):
  831. XCTAssertEqual(anchor.modificationCounter, 2)
  832. XCTAssertEqual(created.count, 1)
  833. XCTAssertEqual(deleted.count, 1)
  834. if created.count >= 1 {
  835. XCTAssertEqual(created[0].syncIdentifier, doseData[0].syncIdentifier)
  836. }
  837. if deleted.count >= 1 {
  838. XCTAssertEqual(deleted[0].syncIdentifier, doseData[1].syncIdentifier)
  839. }
  840. }
  841. self.completion.fulfill()
  842. }
  843. wait(for: [completion], timeout: 2, enforceOrder: true)
  844. }
  845. private func addDoseData(_ doseData: [DoseDatum]) {
  846. cacheStore.managedObjectContext.performAndWait {
  847. for doseDatum in doseData {
  848. let object = CachedInsulinDeliveryObject(context: self.cacheStore.managedObjectContext)
  849. object.provenanceIdentifier = HKSource.default().bundleIdentifier
  850. object.hasLoopKitOrigin = true
  851. object.startDate = Date().addingTimeInterval(-.minutes(15))
  852. object.endDate = Date().addingTimeInterval(-.minutes(5))
  853. object.syncIdentifier = doseDatum.syncIdentifier
  854. object.value = 1.0
  855. object.reason = .bolus
  856. object.createdAt = Date()
  857. object.deletedAt = doseDatum.deleted ? Date() : nil
  858. object.manuallyEntered = false
  859. object.isSuspend = false
  860. self.cacheStore.save()
  861. }
  862. }
  863. }
  864. private struct DoseDatum {
  865. let syncIdentifier: String
  866. let deleted: Bool
  867. init(deleted: Bool = false) {
  868. self.syncIdentifier = UUID().uuidString
  869. self.deleted = deleted
  870. }
  871. }
  872. }