// // InsulinDeliveryStoreTests.swift // LoopKitHostedTests // // Created by Darin Krauss on 10/22/20. // Copyright © 2020 LoopKit Authors. All rights reserved. // import XCTest import HealthKit @testable import LoopKit class InsulinDeliveryStoreTestsBase: PersistenceControllerTestCase { internal let entry1 = DoseEntry(type: .basal, startDate: Date(timeIntervalSinceNow: -.minutes(6)), endDate: Date(timeIntervalSinceNow: -.minutes(5.5)), value: 1.8, unit: .unitsPerHour, deliveredUnits: 0.015, syncIdentifier: "4B14522E-A7B5-4E73-B76B-5043CD7176B0", scheduledBasalRate: nil) internal let entry2 = DoseEntry(type: .tempBasal, startDate: Date(timeIntervalSinceNow: -.minutes(2)), endDate: Date(timeIntervalSinceNow: -.minutes(1.5)), value: 2.4, unit: .unitsPerHour, deliveredUnits: 0.02, syncIdentifier: "A1F8E29B-33D6-4B38-B4CD-D84F14744871", scheduledBasalRate: HKQuantity(unit: .internationalUnitsPerHour, doubleValue: 1.8)) internal let entry3 = DoseEntry(type: .bolus, startDate: Date(timeIntervalSinceNow: -.minutes(4)), endDate: Date(timeIntervalSinceNow: -.minutes(3.5)), value: 1.0, unit: .units, deliveredUnits: nil, syncIdentifier: "1A1D6192-1521-4469-B962-1B82C4534BB1", scheduledBasalRate: nil) internal let device = HKDevice(name: UUID().uuidString, manufacturer: UUID().uuidString, model: UUID().uuidString, hardwareVersion: UUID().uuidString, firmwareVersion: UUID().uuidString, softwareVersion: UUID().uuidString, localIdentifier: UUID().uuidString, udiDeviceIdentifier: UUID().uuidString) var healthStore: HKHealthStoreMock! var insulinDeliveryStore: InsulinDeliveryStore! var authorizationStatus: HKAuthorizationStatus = .notDetermined override func setUp() { super.setUp() healthStore = HKHealthStoreMock() healthStore.authorizationStatus = authorizationStatus insulinDeliveryStore = InsulinDeliveryStore(healthStore: healthStore, cacheStore: cacheStore, cacheLength: .hours(1), provenanceIdentifier: HKSource.default().bundleIdentifier) let semaphore = DispatchSemaphore(value: 0) cacheStore.onReady { error in XCTAssertNil(error) semaphore.signal() } semaphore.wait() } override func tearDown() { let semaphore = DispatchSemaphore(value: 0) insulinDeliveryStore.purgeAllDoseEntries(healthKitPredicate: HKQuery.predicateForObjects(from: HKSource.default())) { error in XCTAssertNil(error) semaphore.signal() } semaphore.wait() insulinDeliveryStore = nil healthStore = nil super.tearDown() } } class InsulinDeliveryStoreTestsAuthorized: InsulinDeliveryStoreTestsBase { override func setUp() { authorizationStatus = .sharingAuthorized super.setUp() } func testObserverQueryStartup() { // Check that an observer query is registered when authorization is already determined. XCTAssertFalse(insulinDeliveryStore.authorizationRequired); let observerQueryCreated = expectation(description: "observer query created") insulinDeliveryStore.createObserverQuery = { (sampleType, predicate, updateHandler) -> HKObserverQuery in let observerQuery = HKObserverQueryMock(sampleType: sampleType, predicate: predicate, updateHandler: updateHandler) observerQueryCreated.fulfill() return observerQuery } waitForExpectations(timeout: 2) } } class InsulinDeliveryStoreTests: InsulinDeliveryStoreTestsBase { // MARK: - HealthKitSampleStore func testHealthKitQueryAnchorPersistence() { var observerQuery: HKObserverQueryMock? = nil var anchoredObjectQuery: HKAnchoredObjectQueryMock? = nil XCTAssert(insulinDeliveryStore.authorizationRequired); XCTAssertNil(insulinDeliveryStore.observerQuery); let observerQueryCreated = expectation(description: "observer query created") insulinDeliveryStore.createObserverQuery = { (sampleType, predicate, updateHandler) -> HKObserverQuery in observerQuery = HKObserverQueryMock(sampleType: sampleType, predicate: predicate, updateHandler: updateHandler) observerQueryCreated.fulfill() return observerQuery! } let authorizationCompletion = expectation(description: "authorization completion") insulinDeliveryStore.authorize { (result) in authorizationCompletion.fulfill() } waitForExpectations(timeout: 2) XCTAssertNotNil(observerQuery) let anchoredObjectQueryCreationExpectation = expectation(description: "anchored object query creation") insulinDeliveryStore.createAnchoredObjectQuery = { (sampleType, predicate, anchor, limit, resultsHandler) -> HKAnchoredObjectQuery in anchoredObjectQuery = HKAnchoredObjectQueryMock(type: sampleType, predicate: predicate, anchor: anchor, limit: limit, resultsHandler: resultsHandler) anchoredObjectQueryCreationExpectation.fulfill() return anchoredObjectQuery! } let observerQueryCompletionExpectation = expectation(description: "observer query completion") let observerQueryCompletionHandler = { observerQueryCompletionExpectation.fulfill() } // This simulates a signal marking the arrival of new HK Data. observerQuery!.updateHandler(observerQuery!, observerQueryCompletionHandler, nil) wait(for: [anchoredObjectQueryCreationExpectation], timeout: 10) // Trigger results handler for anchored object query let returnedAnchor = HKQueryAnchor(fromValue: 5) anchoredObjectQuery!.resultsHandler(anchoredObjectQuery!, [], [], returnedAnchor, nil) // Wait for observerQueryCompletionExpectation waitForExpectations(timeout: 10) XCTAssertNotNil(insulinDeliveryStore.queryAnchor) // Allow any managedObjectContext tasks to complete, like storing the anchor cacheStore.managedObjectContext.performAndWait {} // Create a new glucose store, and ensure it uses the last query anchor let newInsulinDeliveryStore = InsulinDeliveryStore(healthStore: healthStore, cacheStore: cacheStore, provenanceIdentifier: HKSource.default().bundleIdentifier) let newAuthorizationCompletion = expectation(description: "authorization completion") observerQuery = nil let newObserverQueryCreationExpectation = expectation(description: "new observer query created") newInsulinDeliveryStore.createObserverQuery = { (sampleType, predicate, updateHandler) -> HKObserverQuery in observerQuery = HKObserverQueryMock(sampleType: sampleType, predicate: predicate, updateHandler: updateHandler) newObserverQueryCreationExpectation.fulfill() return observerQuery! } newInsulinDeliveryStore.authorize { (result) in newAuthorizationCompletion.fulfill() } waitForExpectations(timeout: 10) anchoredObjectQuery = nil let newAnchoredObjectQueryCreationExpectation = expectation(description: "new anchored object query creation") newInsulinDeliveryStore.createAnchoredObjectQuery = { (sampleType, predicate, anchor, limit, resultsHandler) -> HKAnchoredObjectQuery in anchoredObjectQuery = HKAnchoredObjectQueryMock(type: sampleType, predicate: predicate, anchor: anchor, limit: limit, resultsHandler: resultsHandler) newAnchoredObjectQueryCreationExpectation.fulfill() return anchoredObjectQuery! } // This simulates a signal marking the arrival of new HK Data. observerQuery!.updateHandler(observerQuery!, {}, nil) waitForExpectations(timeout: 10) // Assert new glucose store is querying with the last anchor that our HealthKit mock returned XCTAssertEqual(returnedAnchor, anchoredObjectQuery?.anchor) anchoredObjectQuery!.resultsHandler(anchoredObjectQuery!, [], [], returnedAnchor, nil) } // MARK: - Fetching func testGetDoseEntries() { let addDoseEntriesCompletion = expectation(description: "addDoseEntries") insulinDeliveryStore.addDoseEntries([entry1, entry2, entry3], from: device, syncVersion: 2) { result in switch result { case .failure(let error): XCTFail("Unexpected failure: \(error)") case .success: break } addDoseEntriesCompletion.fulfill() } waitForExpectations(timeout: 10) let getDoseEntries1Completion = expectation(description: "getDoseEntries1") insulinDeliveryStore.getDoseEntries() { result in switch result { case .failure(let error): XCTFail("Unexpected failure: \(error)") case .success(let entries): XCTAssertEqual(entries.count, 3) XCTAssertEqual(entries[0].type, self.entry1.type) XCTAssertEqual(entries[0].startDate, self.entry1.startDate) XCTAssertEqual(entries[0].endDate, self.entry1.endDate) XCTAssertEqual(entries[0].value, 0.015) XCTAssertEqual(entries[0].unit, .units) XCTAssertNil(entries[0].deliveredUnits) XCTAssertEqual(entries[0].description, self.entry1.description) XCTAssertEqual(entries[0].syncIdentifier, self.entry1.syncIdentifier) XCTAssertEqual(entries[0].scheduledBasalRate, self.entry1.scheduledBasalRate) XCTAssertEqual(entries[1].type, self.entry3.type) XCTAssertEqual(entries[1].startDate, self.entry3.startDate) XCTAssertEqual(entries[1].endDate, self.entry3.endDate) XCTAssertEqual(entries[1].value, self.entry3.value) XCTAssertEqual(entries[1].unit, self.entry3.unit) XCTAssertEqual(entries[1].deliveredUnits, self.entry3.deliveredUnits) XCTAssertEqual(entries[1].description, self.entry3.description) XCTAssertEqual(entries[1].syncIdentifier, self.entry3.syncIdentifier) XCTAssertEqual(entries[1].scheduledBasalRate, self.entry3.scheduledBasalRate) XCTAssertEqual(entries[2].type, self.entry2.type) XCTAssertEqual(entries[2].startDate, self.entry2.startDate) XCTAssertEqual(entries[2].endDate, self.entry2.endDate) XCTAssertEqual(entries[2].value, self.entry2.value) XCTAssertEqual(entries[2].unit, self.entry2.unit) XCTAssertEqual(entries[2].deliveredUnits, self.entry2.deliveredUnits) XCTAssertEqual(entries[2].description, self.entry2.description) XCTAssertEqual(entries[2].syncIdentifier, self.entry2.syncIdentifier) XCTAssertEqual(entries[2].scheduledBasalRate, self.entry2.scheduledBasalRate) } getDoseEntries1Completion.fulfill() } waitForExpectations(timeout: 10) let getDoseEntries2Completion = expectation(description: "getDoseEntries2") insulinDeliveryStore.getDoseEntries(start: Date(timeIntervalSinceNow: -.minutes(3.75)), end: Date(timeIntervalSinceNow: -.minutes(1.75))) { result in switch result { case .failure(let error): XCTFail("Unexpected failure: \(error)") case .success(let entries): XCTAssertEqual(entries.count, 2) XCTAssertEqual(entries[0].type, self.entry3.type) XCTAssertEqual(entries[0].startDate, self.entry3.startDate) XCTAssertEqual(entries[0].endDate, self.entry3.endDate) XCTAssertEqual(entries[0].value, self.entry3.value) XCTAssertEqual(entries[0].unit, self.entry3.unit) XCTAssertEqual(entries[0].deliveredUnits, self.entry3.deliveredUnits) XCTAssertEqual(entries[0].description, self.entry3.description) XCTAssertEqual(entries[0].syncIdentifier, self.entry3.syncIdentifier) XCTAssertEqual(entries[0].scheduledBasalRate, self.entry3.scheduledBasalRate) XCTAssertEqual(entries[1].type, self.entry2.type) XCTAssertEqual(entries[1].startDate, self.entry2.startDate) XCTAssertEqual(entries[1].endDate, self.entry2.endDate) XCTAssertEqual(entries[1].value, self.entry2.value) XCTAssertEqual(entries[1].unit, self.entry2.unit) XCTAssertEqual(entries[1].deliveredUnits, self.entry2.deliveredUnits) XCTAssertEqual(entries[1].description, self.entry2.description) XCTAssertEqual(entries[1].syncIdentifier, self.entry2.syncIdentifier) XCTAssertEqual(entries[1].scheduledBasalRate, self.entry2.scheduledBasalRate) } getDoseEntries2Completion.fulfill() } waitForExpectations(timeout: 10) let purgeCachedInsulinDeliveryObjectsCompletion = expectation(description: "purgeCachedInsulinDeliveryObjects") insulinDeliveryStore.purgeCachedInsulinDeliveryObjects() { error in XCTAssertNil(error) purgeCachedInsulinDeliveryObjectsCompletion.fulfill() } waitForExpectations(timeout: 10) let getDoseEntries3Completion = expectation(description: "getDoseEntries3") insulinDeliveryStore.getDoseEntries() { result in switch result { case .failure(let error): XCTFail("Unexpected failure: \(error)") case .success(let entries): XCTAssertEqual(entries.count, 0) } getDoseEntries3Completion.fulfill() } waitForExpectations(timeout: 10) } func testLastBasalEndDate() { let getLastImmutableBasalEndDate1Completion = expectation(description: "getLastImmutableBasalEndDate1") insulinDeliveryStore.getLastImmutableBasalEndDate() { result in switch result { case .failure(let error): XCTFail("Unexpected failure: \(error)") case .success(let lastBasalEndDate): XCTAssertEqual(lastBasalEndDate, .distantPast) } getLastImmutableBasalEndDate1Completion.fulfill() } waitForExpectations(timeout: 10) let addDoseEntriesCompletion = expectation(description: "addDoseEntries") insulinDeliveryStore.addDoseEntries([entry1, entry2, entry3], from: device, syncVersion: 2) { result in switch result { case .failure(let error): XCTFail("Unexpected failure: \(error)") case .success: break } addDoseEntriesCompletion.fulfill() } waitForExpectations(timeout: 10) let getLastImmutableBasalEndDate2Completion = expectation(description: "getLastImmutableBasalEndDate2") insulinDeliveryStore.getLastImmutableBasalEndDate() { result in switch result { case .failure(let error): XCTFail("Unexpected failure: \(error)") case .success(let lastBasalEndDate): XCTAssertEqual(lastBasalEndDate, self.entry2.endDate) } getLastImmutableBasalEndDate2Completion.fulfill() } waitForExpectations(timeout: 10) let purgeCachedInsulinDeliveryObjectsCompletion = expectation(description: "purgeCachedInsulinDeliveryObjects") insulinDeliveryStore.purgeCachedInsulinDeliveryObjects() { error in XCTAssertNil(error) purgeCachedInsulinDeliveryObjectsCompletion.fulfill() } waitForExpectations(timeout: 10) let getLastImmutableBasalEndDate3Completion = expectation(description: "getLastImmutableBasalEndDate3") insulinDeliveryStore.getLastImmutableBasalEndDate() { result in switch result { case .failure(let error): XCTFail("Unexpected failure: \(error)") case .success(let lastBasalEndDate): XCTAssertEqual(lastBasalEndDate, .distantPast) } getLastImmutableBasalEndDate3Completion.fulfill() } waitForExpectations(timeout: 10) } // MARK: - Modification func testAddDoseEntries() { let addDoseEntries1Completion = expectation(description: "addDoseEntries1") insulinDeliveryStore.addDoseEntries([entry1, entry2, entry3], from: device, syncVersion: 2) { result in switch result { case .failure(let error): XCTFail("Unexpected failure: \(error)") case .success: break } addDoseEntries1Completion.fulfill() } waitForExpectations(timeout: 10) let getDoseEntries1Completion = expectation(description: "getDoseEntries1") insulinDeliveryStore.getDoseEntries() { result in switch result { case .failure(let error): XCTFail("Unexpected failure: \(error)") case .success(let entries): XCTAssertEqual(entries.count, 3) XCTAssertEqual(entries[0].type, self.entry1.type) XCTAssertEqual(entries[0].startDate, self.entry1.startDate) XCTAssertEqual(entries[0].endDate, self.entry1.endDate) XCTAssertEqual(entries[0].value, 0.015) XCTAssertEqual(entries[0].unit, .units) XCTAssertNil(entries[0].deliveredUnits) XCTAssertEqual(entries[0].description, self.entry1.description) XCTAssertEqual(entries[0].syncIdentifier, self.entry1.syncIdentifier) XCTAssertEqual(entries[0].scheduledBasalRate, self.entry1.scheduledBasalRate) XCTAssertEqual(entries[1].type, self.entry3.type) XCTAssertEqual(entries[1].startDate, self.entry3.startDate) XCTAssertEqual(entries[1].endDate, self.entry3.endDate) XCTAssertEqual(entries[1].value, self.entry3.value) XCTAssertEqual(entries[1].unit, self.entry3.unit) XCTAssertEqual(entries[1].deliveredUnits, self.entry3.deliveredUnits) XCTAssertEqual(entries[1].description, self.entry3.description) XCTAssertEqual(entries[1].syncIdentifier, self.entry3.syncIdentifier) XCTAssertEqual(entries[1].scheduledBasalRate, self.entry3.scheduledBasalRate) XCTAssertEqual(entries[2].type, self.entry2.type) XCTAssertEqual(entries[2].startDate, self.entry2.startDate) XCTAssertEqual(entries[2].endDate, self.entry2.endDate) XCTAssertEqual(entries[2].value, self.entry2.value) XCTAssertEqual(entries[2].unit, self.entry2.unit) XCTAssertEqual(entries[2].deliveredUnits, self.entry2.deliveredUnits) XCTAssertEqual(entries[2].description, self.entry2.description) XCTAssertEqual(entries[2].syncIdentifier, self.entry2.syncIdentifier) XCTAssertEqual(entries[2].scheduledBasalRate, self.entry2.scheduledBasalRate) } getDoseEntries1Completion.fulfill() } waitForExpectations(timeout: 10) let addDoseEntries2Completion = expectation(description: "addDoseEntries2") insulinDeliveryStore.addDoseEntries([entry3, entry1, entry2], from: device, syncVersion: 2) { result in switch result { case .failure(let error): XCTFail("Unexpected failure: \(error)") case .success: break } addDoseEntries2Completion.fulfill() } waitForExpectations(timeout: 10) let getDoseEntries2Completion = expectation(description: "getDoseEntries2Completion") insulinDeliveryStore.getDoseEntries() { result in switch result { case .failure(let error): XCTFail("Unexpected failure: \(error)") case .success(let entries): XCTAssertEqual(entries.count, 3) XCTAssertEqual(entries[0].type, self.entry1.type) XCTAssertEqual(entries[0].startDate, self.entry1.startDate) XCTAssertEqual(entries[0].endDate, self.entry1.endDate) XCTAssertEqual(entries[0].value, 0.015) XCTAssertEqual(entries[0].unit, .units) XCTAssertNil(entries[0].deliveredUnits) XCTAssertEqual(entries[0].description, self.entry1.description) XCTAssertEqual(entries[0].syncIdentifier, self.entry1.syncIdentifier) XCTAssertEqual(entries[0].scheduledBasalRate, self.entry1.scheduledBasalRate) XCTAssertEqual(entries[1].type, self.entry3.type) XCTAssertEqual(entries[1].startDate, self.entry3.startDate) XCTAssertEqual(entries[1].endDate, self.entry3.endDate) XCTAssertEqual(entries[1].value, self.entry3.value) XCTAssertEqual(entries[1].unit, self.entry3.unit) XCTAssertEqual(entries[1].deliveredUnits, self.entry3.deliveredUnits) XCTAssertEqual(entries[1].description, self.entry3.description) XCTAssertEqual(entries[1].syncIdentifier, self.entry3.syncIdentifier) XCTAssertEqual(entries[1].scheduledBasalRate, self.entry3.scheduledBasalRate) XCTAssertEqual(entries[2].type, self.entry2.type) XCTAssertEqual(entries[2].startDate, self.entry2.startDate) XCTAssertEqual(entries[2].endDate, self.entry2.endDate) XCTAssertEqual(entries[2].value, self.entry2.value) XCTAssertEqual(entries[2].unit, self.entry2.unit) XCTAssertEqual(entries[2].deliveredUnits, self.entry2.deliveredUnits) XCTAssertEqual(entries[2].description, self.entry2.description) XCTAssertEqual(entries[2].syncIdentifier, self.entry2.syncIdentifier) XCTAssertEqual(entries[2].scheduledBasalRate, self.entry2.scheduledBasalRate) } getDoseEntries2Completion.fulfill() } waitForExpectations(timeout: 10) } func testAddDoseEntriesEmpty() { let addDoseEntriesCompletion = expectation(description: "addDoseEntries") insulinDeliveryStore.addDoseEntries([], from: device, syncVersion: 2) { result in switch result { case .failure(let error): XCTFail("Unexpected failure: \(error)") case .success: break } addDoseEntriesCompletion.fulfill() } waitForExpectations(timeout: 10) } func testAddDoseEntriesNotification() { let doseEntriesDidChangeCompletion = expectation(description: "doseEntriesDidChange") let observer = NotificationCenter.default.addObserver(forName: InsulinDeliveryStore.doseEntriesDidChange, object: insulinDeliveryStore, queue: nil) { notification in doseEntriesDidChangeCompletion.fulfill() } let addDoseEntriesCompletion = expectation(description: "addDoseEntries") insulinDeliveryStore.addDoseEntries([entry1, entry2, entry3], from: device, syncVersion: 2) { result in switch result { case .failure(let error): XCTFail("Unexpected failure: \(error)") case .success: break } addDoseEntriesCompletion.fulfill() } wait(for: [doseEntriesDidChangeCompletion, addDoseEntriesCompletion], timeout: 10, enforceOrder: true) NotificationCenter.default.removeObserver(observer) } func testManuallyEnteredDoses() { let manualEntry = DoseEntry(type: .bolus, startDate: Date(timeIntervalSinceNow: -.minutes(15)), endDate: Date(timeIntervalSinceNow: -.minutes(10)), value: 3.0, unit: .units, syncIdentifier: "C0AB1CBD-6B36-4113-9D49-709A022B2451", manuallyEntered: true) let addDoseEntriesCompletion = expectation(description: "addDoseEntries") insulinDeliveryStore.addDoseEntries([entry1, manualEntry, entry2, entry3], from: device, syncVersion: 2) { result in switch result { case .failure(let error): XCTFail("Unexpected failure: \(error)") case .success: break } addDoseEntriesCompletion.fulfill() } waitForExpectations(timeout: 10) let getManuallyEnteredDoses1Completion = expectation(description: "getManuallyEnteredDoses1") insulinDeliveryStore.getManuallyEnteredDoses(since: .distantPast) { result in switch result { case .failure(let error): XCTFail("Unexpected failure: \(error)") case .success(let entries): XCTAssertEqual(entries.count, 1) XCTAssertEqual(entries[0], manualEntry) } getManuallyEnteredDoses1Completion.fulfill() } waitForExpectations(timeout: 10) let getManuallyEnteredDoses2Completion = expectation(description: "getManuallyEnteredDoses2") insulinDeliveryStore.getManuallyEnteredDoses(since: Date(timeIntervalSinceNow: -.minutes(12))) { result in switch result { case .failure(let error): XCTFail("Unexpected failure: \(error)") case .success(let entries): XCTAssertEqual(entries.count, 0) } getManuallyEnteredDoses2Completion.fulfill() } waitForExpectations(timeout: 10) let deleteAllManuallyEnteredDoses1Completion = expectation(description: "deleteAllManuallyEnteredDoses1") insulinDeliveryStore.deleteAllManuallyEnteredDoses(since: Date(timeIntervalSinceNow: -.minutes(12))) { error in XCTAssertNil(error) deleteAllManuallyEnteredDoses1Completion.fulfill() } waitForExpectations(timeout: 10) let getManuallyEnteredDoses3Completion = expectation(description: "getManuallyEnteredDoses3") insulinDeliveryStore.getManuallyEnteredDoses(since: .distantPast) { result in switch result { case .failure(let error): XCTFail("Unexpected failure: \(error)") case .success(let entries): XCTAssertEqual(entries.count, 1) XCTAssertEqual(entries[0], manualEntry) } getManuallyEnteredDoses3Completion.fulfill() } waitForExpectations(timeout: 10) let deleteAllManuallyEnteredDose2Completion = expectation(description: "deleteAllManuallyEnteredDose2") insulinDeliveryStore.deleteAllManuallyEnteredDoses(since: Date(timeIntervalSinceNow: -.minutes(20))) { error in XCTAssertNil(error) deleteAllManuallyEnteredDose2Completion.fulfill() } waitForExpectations(timeout: 10) let getManuallyEnteredDoses4Completion = expectation(description: "getManuallyEnteredDoses4") insulinDeliveryStore.getManuallyEnteredDoses(since: .distantPast) { result in switch result { case .failure(let error): XCTFail("Unexpected failure: \(error)") case .success(let entries): XCTAssertEqual(entries.count, 0) } getManuallyEnteredDoses4Completion.fulfill() } waitForExpectations(timeout: 10) } // MARK: - Cache Management func testEarliestCacheDate() { XCTAssertEqual(insulinDeliveryStore.earliestCacheDate.timeIntervalSinceNow, -.hours(1), accuracy: 1) } func testPurgeAllDoseEntries() { let addDoseEntriesCompletion = expectation(description: "addDoseEntries") insulinDeliveryStore.addDoseEntries([entry1, entry2, entry3], from: device, syncVersion: 2) { result in switch result { case .failure(let error): XCTFail("Unexpected failure: \(error)") case .success: break } addDoseEntriesCompletion.fulfill() } waitForExpectations(timeout: 10) let getDoseEntries1Completion = expectation(description: "getDoseEntries1") insulinDeliveryStore.getDoseEntries() { result in switch result { case .failure(let error): XCTFail("Unexpected failure: \(error)") case .success(let entries): XCTAssertEqual(entries.count, 3) } getDoseEntries1Completion.fulfill() } waitForExpectations(timeout: 10) let purgeAllDoseEntriesCompletion = expectation(description: "purgeAllDoseEntries") insulinDeliveryStore.purgeAllDoseEntries(healthKitPredicate: HKQuery.predicateForObjects(from: HKSource.default())) { error in XCTAssertNil(error) purgeAllDoseEntriesCompletion.fulfill() } waitForExpectations(timeout: 10) let getDoseEntries2Completion = expectation(description: "getDoseEntries2") insulinDeliveryStore.getDoseEntries() { result in switch result { case .failure(let error): XCTFail("Unexpected failure: \(error)") case .success(let entries): XCTAssertEqual(entries.count, 0) } getDoseEntries2Completion.fulfill() } waitForExpectations(timeout: 10) } func testPurgeExpiredGlucoseObjects() { let expiredEntry = DoseEntry(type: .bolus, startDate: Date(timeIntervalSinceNow: -.hours(3)), endDate: Date(timeIntervalSinceNow: -.hours(2)), value: 3.0, unit: .units, deliveredUnits: nil, syncIdentifier: "7530B8CA-827A-4DE8-ADE3-9E10FF80A4A9", scheduledBasalRate: nil) let addDoseEntriesCompletion = expectation(description: "addDoseEntries") insulinDeliveryStore.addDoseEntries([entry1, entry2, entry3, expiredEntry], from: device, syncVersion: 2) { result in switch result { case .failure(let error): XCTFail("Unexpected failure: \(error)") case .success: break } addDoseEntriesCompletion.fulfill() } waitForExpectations(timeout: 10) let getDoseEntriesCompletion = expectation(description: "getDoseEntries") insulinDeliveryStore.getDoseEntries() { result in switch result { case .failure(let error): XCTFail("Unexpected failure: \(error)") case .success(let entries): XCTAssertEqual(entries.count, 3) } getDoseEntriesCompletion.fulfill() } waitForExpectations(timeout: 10) } func testPurgeCachedInsulinDeliveryObjects() { let addDoseEntriesCompletion = expectation(description: "addDoseEntries") insulinDeliveryStore.addDoseEntries([entry1, entry2, entry3], from: device, syncVersion: 2) { result in switch result { case .failure(let error): XCTFail("Unexpected failure: \(error)") case .success: break } addDoseEntriesCompletion.fulfill() } waitForExpectations(timeout: 10) let getDoseEntries1Completion = expectation(description: "getDoseEntries1") insulinDeliveryStore.getDoseEntries() { result in switch result { case .failure(let error): XCTFail("Unexpected failure: \(error)") case .success(let entries): XCTAssertEqual(entries.count, 3) } getDoseEntries1Completion.fulfill() } waitForExpectations(timeout: 10) let purgeCachedInsulinDeliveryObjects1Completion = expectation(description: "purgeCachedInsulinDeliveryObjects1") insulinDeliveryStore.purgeCachedInsulinDeliveryObjects(before: Date(timeIntervalSinceNow: -.minutes(5))) { error in XCTAssertNil(error) purgeCachedInsulinDeliveryObjects1Completion.fulfill() } waitForExpectations(timeout: 10) let getDoseEntries2Completion = expectation(description: "getDoseEntries2") insulinDeliveryStore.getDoseEntries() { result in switch result { case .failure(let error): XCTFail("Unexpected failure: \(error)") case .success(let entries): XCTAssertEqual(entries.count, 2) } getDoseEntries2Completion.fulfill() } waitForExpectations(timeout: 10) let purgeCachedInsulinDeliveryObjects2Completion = expectation(description: "purgeCachedInsulinDeliveryObjects2") insulinDeliveryStore.purgeCachedInsulinDeliveryObjects() { error in XCTAssertNil(error) purgeCachedInsulinDeliveryObjects2Completion.fulfill() } waitForExpectations(timeout: 10) let getDoseEntries3Completion = expectation(description: "getDoseEntries3") insulinDeliveryStore.getDoseEntries() { result in switch result { case .failure(let error): XCTFail("Unexpected failure: \(error)") case .success(let entries): XCTAssertEqual(entries.count, 0) } getDoseEntries3Completion.fulfill() } waitForExpectations(timeout: 10) } func testPurgeCachedInsulinDeliveryObjectsNotification() { let addDoseEntriesCompletion = expectation(description: "addDoseEntries") insulinDeliveryStore.addDoseEntries([entry1, entry2, entry3], from: device, syncVersion: 2) { result in switch result { case .failure(let error): XCTFail("Unexpected failure: \(error)") case .success: break } addDoseEntriesCompletion.fulfill() } waitForExpectations(timeout: 10) let doseEntriesDidChangeCompletion = expectation(description: "doseEntriesDidChange") let observer = NotificationCenter.default.addObserver(forName: InsulinDeliveryStore.doseEntriesDidChange, object: insulinDeliveryStore, queue: nil) { notification in doseEntriesDidChangeCompletion.fulfill() } let purgeCachedInsulinDeliveryObjectsCompletion = expectation(description: "purgeCachedInsulinDeliveryObjects") insulinDeliveryStore.purgeCachedInsulinDeliveryObjects() { error in XCTAssertNil(error) purgeCachedInsulinDeliveryObjectsCompletion.fulfill() } wait(for: [doseEntriesDidChangeCompletion, purgeCachedInsulinDeliveryObjectsCompletion], timeout: 10, enforceOrder: true) NotificationCenter.default.removeObserver(observer) } } class InsulinDeliveryStoreQueryAnchorTests: XCTestCase { var rawValue: InsulinDeliveryStore.QueryAnchor.RawValue = [ "modificationCounter": Int64(123) ] func testInitializerDefault() { let queryAnchor = InsulinDeliveryStore.QueryAnchor() XCTAssertEqual(queryAnchor.modificationCounter, 0) } func testInitializerRawValue() { let queryAnchor = InsulinDeliveryStore.QueryAnchor(rawValue: rawValue) XCTAssertNotNil(queryAnchor) XCTAssertEqual(queryAnchor?.modificationCounter, 123) } func testInitializerRawValueMissingModificationCounter() { rawValue["modificationCounter"] = nil XCTAssertNil(InsulinDeliveryStore.QueryAnchor(rawValue: rawValue)) } func testInitializerRawValueInvalidModificationCounter() { rawValue["modificationCounter"] = "123" XCTAssertNil(InsulinDeliveryStore.QueryAnchor(rawValue: rawValue)) } func testRawValueWithDefault() { let rawValue = InsulinDeliveryStore.QueryAnchor().rawValue XCTAssertEqual(rawValue.count, 1) XCTAssertEqual(rawValue["modificationCounter"] as? Int64, Int64(0)) } func testRawValueWithNonDefault() { var queryAnchor = InsulinDeliveryStore.QueryAnchor() queryAnchor.modificationCounter = 123 let rawValue = queryAnchor.rawValue XCTAssertEqual(rawValue.count, 1) XCTAssertEqual(rawValue["modificationCounter"] as? Int64, Int64(123)) } } class InsulinDeliveryStoreQueryTests: PersistenceControllerTestCase { let insulinModel = WalshInsulinModel(actionDuration: .hours(4)) 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]]]) 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]]]) var insulinDeliveryStore: InsulinDeliveryStore! var completion: XCTestExpectation! var queryAnchor: InsulinDeliveryStore.QueryAnchor! var limit: Int! override func setUp() { super.setUp() insulinDeliveryStore = InsulinDeliveryStore(healthStore: HKHealthStoreMock(), storeSamplesToHealthKit: false, cacheStore: cacheStore, observationEnabled: false, provenanceIdentifier: HKSource.default().bundleIdentifier) completion = expectation(description: "Completion") queryAnchor = InsulinDeliveryStore.QueryAnchor() limit = Int.max } override func tearDown() { limit = nil queryAnchor = nil completion = nil insulinDeliveryStore = nil super.tearDown() } func testDoseEmptyWithDefaultQueryAnchor() { insulinDeliveryStore.executeDoseQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in switch result { case .failure(let error): XCTFail("Unexpected failure: \(error)") case .success(let anchor, let created, let deleted): XCTAssertEqual(anchor.modificationCounter, 0) XCTAssertEqual(created.count, 0) XCTAssertEqual(deleted.count, 0) } self.completion.fulfill() } wait(for: [completion], timeout: 2, enforceOrder: true) } func testDoseEmptyWithMissingQueryAnchor() { queryAnchor = nil insulinDeliveryStore.executeDoseQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in switch result { case .failure(let error): XCTFail("Unexpected failure: \(error)") case .success(let anchor, let created, let deleted): XCTAssertEqual(anchor.modificationCounter, 0) XCTAssertEqual(created.count, 0) XCTAssertEqual(deleted.count, 0) } self.completion.fulfill() } wait(for: [completion], timeout: 2, enforceOrder: true) } func testDoseEmptyWithNonDefaultQueryAnchor() { queryAnchor.modificationCounter = 1 insulinDeliveryStore.executeDoseQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in switch result { case .failure(let error): XCTFail("Unexpected failure: \(error)") case .success(let anchor, let created, let deleted): XCTAssertEqual(anchor.modificationCounter, 1) XCTAssertEqual(created.count, 0) XCTAssertEqual(deleted.count, 0) } self.completion.fulfill() } wait(for: [completion], timeout: 2, enforceOrder: true) } func testDoseDataWithUnusedQueryAnchor() { let doseData = [DoseDatum(), DoseDatum(deleted: true), DoseDatum()] addDoseData(doseData) insulinDeliveryStore.executeDoseQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in switch result { case .failure(let error): XCTFail("Unexpected failure: \(error)") case .success(let anchor, let created, let deleted): XCTAssertEqual(anchor.modificationCounter, 3) XCTAssertEqual(created.count, 2) XCTAssertEqual(deleted.count, 1) if created.count >= 2 { XCTAssertEqual(created[0].syncIdentifier, doseData[0].syncIdentifier) XCTAssertEqual(created[1].syncIdentifier, doseData[2].syncIdentifier) } if deleted.count >= 1 { XCTAssertEqual(deleted[0].syncIdentifier, doseData[1].syncIdentifier) } } self.completion.fulfill() } wait(for: [completion], timeout: 2, enforceOrder: true) } func testDoseDataWithStaleQueryAnchor() { let doseData = [DoseDatum(), DoseDatum(deleted: true), DoseDatum()] addDoseData(doseData) queryAnchor.modificationCounter = 2 insulinDeliveryStore.executeDoseQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in switch result { case .failure(let error): XCTFail("Unexpected failure: \(error)") case .success(let anchor, let created, let deleted): XCTAssertEqual(anchor.modificationCounter, 3) XCTAssertEqual(created.count, 1) XCTAssertEqual(deleted.count, 0) if created.count >= 1 { XCTAssertEqual(created[0].syncIdentifier, doseData[2].syncIdentifier) } } self.completion.fulfill() } wait(for: [completion], timeout: 2, enforceOrder: true) } func testDoseDataWithCurrentQueryAnchor() { let doseData = [DoseDatum(), DoseDatum(deleted: true), DoseDatum()] addDoseData(doseData) queryAnchor.modificationCounter = 3 insulinDeliveryStore.executeDoseQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in switch result { case .failure(let error): XCTFail("Unexpected failure: \(error)") case .success(let anchor, let created, let deleted): XCTAssertEqual(anchor.modificationCounter, 3) XCTAssertEqual(created.count, 0) XCTAssertEqual(deleted.count, 0) } self.completion.fulfill() } wait(for: [completion], timeout: 2, enforceOrder: true) } func testDoseDataWithLimitCoveredByData() { let doseData = [DoseDatum(), DoseDatum(deleted: true), DoseDatum()] addDoseData(doseData) limit = 2 insulinDeliveryStore.executeDoseQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in switch result { case .failure(let error): XCTFail("Unexpected failure: \(error)") case .success(let anchor, let created, let deleted): XCTAssertEqual(anchor.modificationCounter, 2) XCTAssertEqual(created.count, 1) XCTAssertEqual(deleted.count, 1) if created.count >= 1 { XCTAssertEqual(created[0].syncIdentifier, doseData[0].syncIdentifier) } if deleted.count >= 1 { XCTAssertEqual(deleted[0].syncIdentifier, doseData[1].syncIdentifier) } } self.completion.fulfill() } wait(for: [completion], timeout: 2, enforceOrder: true) } private func addDoseData(_ doseData: [DoseDatum]) { cacheStore.managedObjectContext.performAndWait { for doseDatum in doseData { let object = CachedInsulinDeliveryObject(context: self.cacheStore.managedObjectContext) object.provenanceIdentifier = HKSource.default().bundleIdentifier object.hasLoopKitOrigin = true object.startDate = Date().addingTimeInterval(-.minutes(15)) object.endDate = Date().addingTimeInterval(-.minutes(5)) object.syncIdentifier = doseDatum.syncIdentifier object.value = 1.0 object.reason = .bolus object.createdAt = Date() object.deletedAt = doseDatum.deleted ? Date() : nil object.manuallyEntered = false object.isSuspend = false self.cacheStore.save() } } } private struct DoseDatum { let syncIdentifier: String let deleted: Bool init(deleted: Bool = false) { self.syncIdentifier = UUID().uuidString self.deleted = deleted } } }