diff --git a/src/xcode/ENA/ENA.xcodeproj/project.pbxproj b/src/xcode/ENA/ENA.xcodeproj/project.pbxproj index 355ac7af061..48c0dd53e0a 100644 --- a/src/xcode/ENA/ENA.xcodeproj/project.pbxproj +++ b/src/xcode/ENA/ENA.xcodeproj/project.pbxproj @@ -375,6 +375,7 @@ ABC6B7ED255AEF77000A1AC0 /* RiskProviderAndNewKeyPackagesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC6B7E8255AEF74000A1AC0 /* RiskProviderAndNewKeyPackagesTests.swift */; }; ABD2F634254C533200DC1958 /* KeyPackageDownload.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABD2F633254C533200DC1958 /* KeyPackageDownload.swift */; }; ABDA2792251CE308006BAE84 /* DMServerEnvironmentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABDA2791251CE308006BAE84 /* DMServerEnvironmentViewController.swift */; }; + ABFCE98A255C32EF0075FF13 /* AppConfigMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABFCE989255C32EE0075FF13 /* AppConfigMetadata.swift */; }; B103193224E18A0A00DD02EF /* DMMenuItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = B103193124E18A0A00DD02EF /* DMMenuItem.swift */; }; B10F9B8B249961BC00C418F4 /* DynamicTypeLabelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10F9B89249961B500C418F4 /* DynamicTypeLabelTests.swift */; }; B10F9B8C249961CE00C418F4 /* UIFont+DynamicTypeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B163D11424993F64001A322C /* UIFont+DynamicTypeTests.swift */; }; @@ -979,6 +980,7 @@ ABC6B7E8255AEF74000A1AC0 /* RiskProviderAndNewKeyPackagesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RiskProviderAndNewKeyPackagesTests.swift; sourceTree = ""; }; ABD2F633254C533200DC1958 /* KeyPackageDownload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyPackageDownload.swift; sourceTree = ""; }; ABDA2791251CE308006BAE84 /* DMServerEnvironmentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DMServerEnvironmentViewController.swift; sourceTree = ""; }; + ABFCE989255C32EE0075FF13 /* AppConfigMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppConfigMetadata.swift; sourceTree = ""; }; B102BDC22460410600CD55A2 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; B103193124E18A0A00DD02EF /* DMMenuItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DMMenuItem.swift; sourceTree = ""; }; B10F9B89249961B500C418F4 /* DynamicTypeLabelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DynamicTypeLabelTests.swift; sourceTree = ""; }; @@ -1448,6 +1450,7 @@ A16714BA248D18D20031B111 /* SummaryMetadata.swift */, 354E305824EFF26E00526C9F /* Country.swift */, 941ADDB12518C3FB00E421D9 /* ENSettingEuTracingViewModel.swift */, + ABFCE989255C32EE0075FF13 /* AppConfigMetadata.swift */, 94B255A52551B7C800649B4C /* WarnOthersReminder.swift */, 94427A4F25502B8900C36BE6 /* WarnOthersNotificationsTimeInterval.swift */, 948AFE662553DC5B0019579A /* WarnOthersRemindable.swift */, @@ -3435,6 +3438,7 @@ A3816086250633D7002286E9 /* RequiresDismissConfirmation.swift in Sources */, 51D420B424583ABB00AD70CA /* AppStoryboard.swift in Sources */, 4026C2DC24852B7600926FB4 /* AppInformationViewController+LegalModel.swift in Sources */, + ABFCE98A255C32EF0075FF13 /* AppConfigMetadata.swift in Sources */, 01C6ABF42527273E0052814D /* String+Insertion.swift in Sources */, EE278B30245F2C8A008B06F9 /* FriendsInviteController.swift in Sources */, 710ABB27247533FA00948792 /* DynamicTableViewController.swift in Sources */, diff --git a/src/xcode/ENA/ENA/Source/Client/__tests__/Mocks/CachingHTTPClientMock.swift b/src/xcode/ENA/ENA/Source/Client/__tests__/Mocks/CachingHTTPClientMock.swift index 222b4934e2f..880a2894e3c 100644 --- a/src/xcode/ENA/ENA/Source/Client/__tests__/Mocks/CachingHTTPClientMock.swift +++ b/src/xcode/ENA/ENA/Source/Client/__tests__/Mocks/CachingHTTPClientMock.swift @@ -27,6 +27,12 @@ final class CachingHTTPClientMock: CachingHTTPClient { return config }() + static let staticAppConfigMetadata: AppConfigMetadata = { + let bundle = Bundle(for: CachingHTTPClientMock.self) + let configMetadata = AppConfigMetadata(lastAppConfigETag: "\"SomeETag\"", lastAppConfigFetch: .distantPast, appConfig: CachingHTTPClientMock.staticAppConfig) + return configMetadata + }() + // MARK: AppConfigurationFetching var onFetchAppConfiguration: ((String?, @escaping CachingHTTPClient.AppConfigResultHandler) -> Void)? diff --git a/src/xcode/ENA/ENA/Source/Developer Menu/Features/DMStoreViewController.swift b/src/xcode/ENA/ENA/Source/Developer Menu/Features/DMStoreViewController.swift index f42b8da58b1..b59d026b4f3 100644 --- a/src/xcode/ENA/ENA/Source/Developer Menu/Features/DMStoreViewController.swift +++ b/src/xcode/ENA/ENA/Source/Developer Menu/Features/DMStoreViewController.swift @@ -42,13 +42,13 @@ final class DMStoreViewController: UITableViewController { store.previousRiskLevel?.description ?? "" }, DMStoreItem(attribute: "lastAppConfigETag") { store in - store.lastAppConfigETag?.description ?? "" + store.appConfigMetadata?.lastAppConfigETag.description ?? "" }, DMStoreItem(attribute: "lastAppConfigFetch") { store in - store.lastAppConfigFetch?.description ?? "" + store.appConfigMetadata?.lastAppConfigFetch.description ?? "" }, DMStoreItem(attribute: "appConfig") { store in - store.appConfig?.debugDescription ?? "" + store.appConfigMetadata?.appConfig.debugDescription ?? "" } ] }() diff --git a/src/xcode/ENA/ENA/Source/Models/Exposure/AppConfigMetadata.swift b/src/xcode/ENA/ENA/Source/Models/Exposure/AppConfigMetadata.swift new file mode 100644 index 00000000000..6abef3c536e --- /dev/null +++ b/src/xcode/ENA/ENA/Source/Models/Exposure/AppConfigMetadata.swift @@ -0,0 +1,73 @@ +// +// Corona-Warn-App +// +// SAP SE and all other contributors +// copyright owners license this file to you under the Apache +// License, Version 2.0 (the "License"); you may not use this +// file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +import Foundation + +struct AppConfigMetadata: Codable, Equatable { + + // MARK: - Init + + init( + lastAppConfigETag: String, + lastAppConfigFetch: Date, + appConfig: SAP_Internal_ApplicationConfiguration + ) { + self.lastAppConfigETag = lastAppConfigETag + self.lastAppConfigFetch = lastAppConfigFetch + self.appConfig = appConfig + } + + // MARK: - Protocol Codable + + enum CodingKeys: String, CodingKey { + case lastAppConfigETag + case lastAppConfigFetch + case appConfig + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + lastAppConfigETag = try container.decode(String.self, forKey: .lastAppConfigETag) + lastAppConfigFetch = try container.decode(Date.self, forKey: .lastAppConfigFetch) + + let appConfigData = try container.decode(Data.self, forKey: .appConfig) + appConfig = try SAP_Internal_ApplicationConfiguration(serializedData: appConfigData) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(lastAppConfigETag, forKey: .lastAppConfigETag) + try container.encode(lastAppConfigFetch, forKey: .lastAppConfigFetch) + + let appConfigData = try appConfig.serializedData() + try container.encode(appConfigData, forKey: .appConfig) + } + + // MARK: - Internal + + var lastAppConfigETag: String + var lastAppConfigFetch: Date + var appConfig: SAP_Internal_ApplicationConfiguration + + mutating func refeshLastAppConfigFetchDate() { + lastAppConfigFetch = Date() + } +} diff --git a/src/xcode/ENA/ENA/Source/Services/Exposure Transaction/ExposureDetectionExecutor.swift b/src/xcode/ENA/ENA/Source/Services/Exposure Transaction/ExposureDetectionExecutor.swift index a5f33b931e8..69b560a8f5a 100644 --- a/src/xcode/ENA/ENA/Source/Services/Exposure Transaction/ExposureDetectionExecutor.swift +++ b/src/xcode/ENA/ENA/Source/Services/Exposure Transaction/ExposureDetectionExecutor.swift @@ -76,9 +76,7 @@ final class ExposureDetectionExecutor: ExposureDetectionDelegate { downloadedPackagesStore.open() // Clear the app config - store.appConfig = nil - store.lastAppConfigETag = nil - store.lastAppConfigFetch = nil + store.appConfigMetadata = nil } } diff --git a/src/xcode/ENA/ENA/Source/Services/Exposure Transaction/__tests__/ExposureDetectionExecutorTests.swift b/src/xcode/ENA/ENA/Source/Services/Exposure Transaction/__tests__/ExposureDetectionExecutorTests.swift index 89bdc9071f7..c6d141c0128 100644 --- a/src/xcode/ENA/ENA/Source/Services/Exposure Transaction/__tests__/ExposureDetectionExecutorTests.swift +++ b/src/xcode/ENA/ENA/Source/Services/Exposure Transaction/__tests__/ExposureDetectionExecutorTests.swift @@ -9,6 +9,14 @@ import XCTest final class ExposureDetectionExecutorTests: XCTestCase { + private var dummyAppConfigMetadata: AppConfigMetadata { + AppConfigMetadata( + lastAppConfigETag: "ETag", + lastAppConfigFetch: Date(), + appConfig: SAP_Internal_ApplicationConfiguration() + ) + } + // MARK: - Write Downloaded Package Tests func testWriteDownloadedPackage() throws { @@ -139,7 +147,7 @@ final class ExposureDetectionExecutorTests: XCTestCase { try packageStore.set(country: "DE", day: "SomeDay", etag: nil, package: package) let store = MockTestStore() - store.appConfig = SAP_Internal_ApplicationConfiguration() + store.appConfigMetadata = dummyAppConfigMetadata let sut = ExposureDetectionExecutor.makeWith( packageStore: packageStore, @@ -153,7 +161,7 @@ final class ExposureDetectionExecutorTests: XCTestCase { ) XCTAssertNotEqual(packageStore.allDays(country: "DE").count, 0) - XCTAssertNotNil(store.appConfig) + XCTAssertNotNil(store.appConfigMetadata) _ = sut.exposureDetection( exposureDetection, @@ -162,9 +170,7 @@ final class ExposureDetectionExecutorTests: XCTestCase { completion: { _ in XCTAssertEqual(packageStore.allDays(country: "DE").count, 0) - XCTAssertNil(store.appConfig) - XCTAssertNil(store.lastAppConfigETag) - XCTAssertNil(store.lastAppConfigFetch) + XCTAssertNil(store.appConfigMetadata) completionExpectation.fulfill() } diff --git a/src/xcode/ENA/ENA/Source/Services/Risk/Provider/Helper/CachedAppConfiguration.swift b/src/xcode/ENA/ENA/Source/Services/Risk/Provider/Helper/CachedAppConfiguration.swift index 220e2437b5b..e327454137c 100644 --- a/src/xcode/ENA/ENA/Source/Services/Risk/Provider/Helper/CachedAppConfiguration.swift +++ b/src/xcode/ENA/ENA/Source/Services/Risk/Provider/Helper/CachedAppConfiguration.swift @@ -32,63 +32,75 @@ final class CachedAppConfiguration { deviceTimeCheck: DeviceTimeCheckProtocol? = nil, configurationDidChange: (() -> Void)? = nil ) { + Log.debug("CachedAppConfiguration init called", log: .appConfig) + self.client = client self.store = store self.configurationDidChange = configurationDidChange self.deviceTimeCheck = deviceTimeCheck ?? DeviceTimeCheck(store: store) - guard shouldFetch() else { return } - // edge case: if no app config is cached, omit a potentially existing ETag to force fetch a new configuration - let etag = store.appConfig == nil ? nil : store.lastAppConfigETag + guard shouldFetch() else { return } // check for updated or fetch initial app configuration - fetchConfig(with: etag) + fetchConfig(with: store.appConfigMetadata?.lastAppConfigETag) } private func fetchConfig(with etag: String?, completion: Completion? = nil) { + Log.debug("fetchConfig called with etag:\(etag ?? "nil")", log: .appConfig) + client.fetchAppConfiguration(etag: etag) { [weak self] result in guard let self = self else { return } switch result.0 /* fyi, `result.1` would be the server time */{ case .success(let response): - self.store.lastAppConfigETag = response.eTag - self.store.appConfig = response.config - self.completeOnMain(completion: completion, result: .success(response.config)) + let configMetadata = AppConfigMetadata( + lastAppConfigETag: response.eTag ?? "\"ReloadMe\"", + lastAppConfigFetch: Date(), + appConfig: response.config + ) + + // Skip processing of config if it didn't change. + guard self.store.appConfigMetadata?.lastAppConfigETag != configMetadata.lastAppConfigETag else { + Log.debug("Skip processing app config, because it didn't change", log: .appConfig) + self.completeOnMain(completion: completion, result: .success(response.config)) + return + } - // keep track of last successful fetch - self.store.lastAppConfigFetch = Date() + Log.debug("Persist new app configuration", log: .appConfig) + self.store.appConfigMetadata = configMetadata // update revokation list - let revokationList = self.store.appConfig?.revokationEtags ?? [] + let revokationList = self.store.appConfigMetadata?.appConfig.revokationEtags ?? [] self.packageStore?.revokationList = revokationList // for future package-operations // validate currently stored key packages do { try self.packageStore?.validateCachedKeyPackages(revokationList: revokationList) } catch { - Log.error("Error while removing invalidated key packages.", log: .localData, error: error) + Log.error("Error while removing invalidated key packages.", log: .appConfig, error: error) // no further action - yet } + self.completeOnMain(completion: completion, result: .success(response.config)) + self.configurationDidChange?() case .failure(let error): switch error { - case CachedAppConfiguration.CacheError.notModified where self.store.appConfig != nil: - Log.error("config not modified", log: .api) + case CachedAppConfiguration.CacheError.notModified where self.store.appConfigMetadata != nil: + Log.error("Config not modified", log: .appConfig) // server is not modified and we have a cached config - guard let config = self.store.appConfig else { + guard var appConfigMetadata = self.store.appConfigMetadata else { fatalError("App configuration cache broken!") // in `where` we trust } - self.completeOnMain(completion: completion, result: .success(config)) // server response HTTP 304 is considered a 'successful fetch' - self.store.lastAppConfigFetch = Date() - default: - // ensure reset - self.store.lastAppConfigETag = nil - self.store.lastAppConfigFetch = nil + Log.debug("Update lastAppConfigFetchDate of persisted app configuration", log: .appConfig) + appConfigMetadata.refeshLastAppConfigFetchDate() + self.store.appConfigMetadata = appConfigMetadata + self.completeOnMain(completion: completion, result: .success(appConfigMetadata.appConfig)) + default: self.completeOnMain(completion: completion, result: .failure(error)) } } @@ -117,16 +129,18 @@ extension CachedAppConfiguration: AppConfigurationProviding { fileprivate static let timestampKey = "LastAppConfigFetch" func appConfiguration(forceFetch: Bool = false, completion: @escaping Completion) { + Log.debug("Request app configuration forceFetch: \(forceFetch)", log: .appConfig) + let force = shouldFetch() || forceFetch - if let cachedVersion = store.appConfig, !force { - Log.debug("[App Config] fetching cached app configuration", log: .localData) + if let cachedVersion = store.appConfigMetadata, !force { + Log.debug("fetching cached app configuration", log: .appConfig) // use the cached version - completeOnMain(completion: completion, result: .success(cachedVersion)) + completeOnMain(completion: completion, result: .success(cachedVersion.appConfig)) } else { - Log.debug("[App Config] fetching fresh app configuration", log: .localData) + Log.debug("fetching fresh app configuration. forceFetch: \(forceFetch), force: \(force)", log: .appConfig) // fetch a new one - fetchConfig(with: store.lastAppConfigETag, completion: completion) + fetchConfig(with: store.appConfigMetadata?.lastAppConfigETag, completion: completion) } } @@ -139,14 +153,19 @@ extension CachedAppConfiguration: AppConfigurationProviding { /// which does not easily return response headers. This requires further refactoring of `URLSession+Convenience.swift`. /// - Returns: `true` is a network call should be done; `false` if cache should be used private func shouldFetch() -> Bool { - if store.appConfig == nil { return true } + Log.debug("shouldFetch called", log: .appConfig) + + if store.appConfigMetadata == nil { + Log.debug("store.appConfigMetadata is nil", log: .appConfig) + return true + } // naïve cache control - guard let lastFetch = store.lastAppConfigFetch else { - Log.debug("[Cache-Control] no last config fetch timestamp stored", log: .localData) + guard let lastFetch = store.appConfigMetadata?.lastAppConfigFetch else { + Log.debug("no last config fetch timestamp stored", log: .appConfig) return true } - Log.debug("[Cache-Control] timestamp >= 300s? \(abs(lastFetch.distance(to: Date())) >= 300)", log: .localData) + Log.debug("timestamp >= 300s? \(abs(lastFetch.distance(to: Date())) >= 300)", log: .appConfig) return abs(lastFetch.distance(to: Date())) >= 300 } } diff --git a/src/xcode/ENA/ENA/Source/Services/Risk/Provider/Helper/DeviceTimeCheck.swift b/src/xcode/ENA/ENA/Source/Services/Risk/Provider/Helper/DeviceTimeCheck.swift index 629f4b71a03..9f7a0cf8b57 100644 --- a/src/xcode/ENA/ENA/Source/Services/Risk/Provider/Helper/DeviceTimeCheck.swift +++ b/src/xcode/ENA/ENA/Source/Services/Risk/Provider/Helper/DeviceTimeCheck.swift @@ -39,7 +39,7 @@ final class DeviceTimeCheck: DeviceTimeCheckProtocol { deviceTime: deviceTime ), isDeviceTimeCheckKillSwitchActive: self.isDeviceTimeCheckKillSwitchActive( - config: self.store.appConfig + config: self.store.appConfigMetadata?.appConfig ) ) } diff --git a/src/xcode/ENA/ENA/Source/Services/Risk/Provider/Helper/__tests__/CachedAppConfigurationTests.swift b/src/xcode/ENA/ENA/Source/Services/Risk/Provider/Helper/__tests__/CachedAppConfigurationTests.swift index 8eaae0cd9e7..639b6a40c73 100644 --- a/src/xcode/ENA/ENA/Source/Services/Risk/Provider/Helper/__tests__/CachedAppConfigurationTests.swift +++ b/src/xcode/ENA/ENA/Source/Services/Risk/Provider/Helper/__tests__/CachedAppConfigurationTests.swift @@ -15,8 +15,7 @@ final class CachedAppConfigurationTests: XCTestCase { fetchedFromClientExpectation.assertForOverFulfill = true let store = MockTestStore() - XCTAssertNil(store.appConfig) - XCTAssertNil(store.lastAppConfigETag) + XCTAssertNil(store.appConfigMetadata) let client = CachingHTTPClientMock(store: store) let expectedConfig = SAP_Internal_ApplicationConfiguration() @@ -46,8 +45,7 @@ final class CachedAppConfigurationTests: XCTestCase { completionExpectation.fulfill() } - XCTAssertNotNil(store.appConfig) - XCTAssertNotNil(store.lastAppConfigETag) + XCTAssertNotNil(store.appConfigMetadata) // Should not trigger another call (expectation) to the actual client or a new risk calculation // Remember: `expectedFulfillmentCount = 1` @@ -55,7 +53,7 @@ final class CachedAppConfigurationTests: XCTestCase { switch response { case .success(let config): XCTAssertEqual(config, expectedConfig) - XCTAssertEqual(config, store.appConfig) + XCTAssertEqual(config, store.appConfigMetadata?.appConfig) case .failure(let error): XCTFail(error.localizedDescription) } @@ -67,15 +65,19 @@ final class CachedAppConfigurationTests: XCTestCase { func testCacheDecay() throws { let outdatedConfig = SAP_Internal_ApplicationConfiguration() - let updatedConfig = CachingHTTPClientMock.staticAppConfig + let updatedConfigMetaData = CachingHTTPClientMock.staticAppConfigMetadata let store = MockTestStore() - store.appConfig = outdatedConfig - store.lastAppConfigFetch = 297.secondsAgo // close to the assumed 300 seconds default decay + let appConfigMetadata = AppConfigMetadata( + lastAppConfigETag: "\"OldETag\"", + lastAppConfigFetch: 297.secondsAgo ?? Date(), // close to the assumed 300 seconds default decay + appConfig: outdatedConfig + ) + store.appConfigMetadata = appConfigMetadata let client = CachingHTTPClientMock(store: store) - let lastFetch = try XCTUnwrap(store.lastAppConfigFetch) + let lastFetch = try XCTUnwrap(store.appConfigMetadata?.lastAppConfigFetch) XCTAssertLessThan(Date().timeIntervalSince(lastFetch), 300) let fetchedFromClientExpectation = expectation(description: "configuration fetched from client") @@ -83,9 +85,9 @@ final class CachedAppConfigurationTests: XCTestCase { fetchedFromClientExpectation.assertForOverFulfill = true client.onFetchAppConfiguration = { _, completeWith in - store.appConfig = updatedConfig + store.appConfigMetadata = updatedConfigMetaData - let config = AppConfigurationFetchingResponse(updatedConfig, "etag") + let config = AppConfigurationFetchingResponse(updatedConfigMetaData.appConfig, "\"NewETag\"") completeWith((.success(config), nil)) fetchedFromClientExpectation.fulfill() } @@ -111,7 +113,7 @@ final class CachedAppConfigurationTests: XCTestCase { completionExpectation.fulfill() } - XCTAssertEqual(store.appConfig, outdatedConfig) + XCTAssertEqual(store.appConfigMetadata?.appConfig, outdatedConfig) // ensure cache decay sleep(5) @@ -121,8 +123,8 @@ final class CachedAppConfigurationTests: XCTestCase { cache.appConfiguration { response in switch response { case .success(let config): - XCTAssertEqual(config, updatedConfig) - XCTAssertEqual(config, store.appConfig) + XCTAssertEqual(config, updatedConfigMetaData.appConfig) + XCTAssertEqual(config, store.appConfigMetadata?.appConfig) case .failure(let error): XCTFail(error.localizedDescription) } @@ -134,8 +136,7 @@ final class CachedAppConfigurationTests: XCTestCase { func testFetch_nothingCached() throws { let store = MockTestStore() - store.appConfig = nil - store.lastAppConfigETag = nil + store.appConfigMetadata = nil let client = CachingHTTPClientMock(store: store) @@ -150,8 +151,7 @@ final class CachedAppConfigurationTests: XCTestCase { }) cache.appConfiguration { response in - XCTAssertNotNil(store.appConfig) - XCTAssertNotNil(store.lastAppConfigETag) + XCTAssertNotNil(store.appConfigMetadata) switch response { case .success(let config): @@ -165,18 +165,15 @@ final class CachedAppConfigurationTests: XCTestCase { waitForExpectations(timeout: .medium) } - func testCacheNotModfied_invalidCache() throws { + func testCacheExpired_invalidCache() throws { let fetchedFromClientExpectation = expectation(description: "configuration fetched from client") fetchedFromClientExpectation.expectedFulfillmentCount = 1 let store = MockTestStore() - store.lastAppConfigETag = "etag" - store.appConfig = nil + store.appConfigMetadata = CachingHTTPClientMock.staticAppConfigMetadata let client = CachingHTTPClientMock(store: store) - client.onFetchAppConfiguration = { etag, completeWith in - XCTAssertNil(etag, "ETag should be reset!") - + client.onFetchAppConfiguration = { _, completeWith in let config = CachingHTTPClientMock.staticAppConfig let response = AppConfigurationFetchingResponse(config, "etag_2") completeWith((.success(response), nil)) @@ -194,8 +191,8 @@ final class CachedAppConfigurationTests: XCTestCase { cache.appConfiguration { response in switch response { case .success(let config): - XCTAssertEqual(config, store.appConfig) - XCTAssertEqual("etag_2", store.lastAppConfigETag) + XCTAssertEqual(config, store.appConfigMetadata?.appConfig) + XCTAssertEqual("etag_2", store.appConfigMetadata?.lastAppConfigETag) case .failure(let error): XCTFail("Expected no error, got: \(error)") } @@ -210,8 +207,7 @@ final class CachedAppConfigurationTests: XCTestCase { fetchedFromClientExpectation.expectedFulfillmentCount = 1 let store = MockTestStore() - store.lastAppConfigETag = "etag" - store.appConfig = SAP_Internal_ApplicationConfiguration() + store.appConfigMetadata = CachingHTTPClientMock.staticAppConfigMetadata let client = CachingHTTPClientMock(store: store) client.onFetchAppConfiguration = { _, completeWith in @@ -231,7 +227,8 @@ final class CachedAppConfigurationTests: XCTestCase { cache.appConfiguration { response in switch response { case .success(let config): - XCTAssertEqual(config, store.appConfig) + XCTAssertEqual(config, store.appConfigMetadata?.appConfig) + XCTAssertEqual("\"SomeETag\"", store.appConfigMetadata?.lastAppConfigETag) case .failure(let error): XCTFail("Expected no error, got: \(error)") } @@ -248,13 +245,11 @@ final class CachedAppConfigurationTests: XCTestCase { fetchedFromClientExpectation.expectedFulfillmentCount = 2 let store = MockTestStore() - XCTAssertNil(store.appConfig) - XCTAssertNil(store.lastAppConfigETag) - + XCTAssertNil(store.appConfigMetadata) + let client = CachingHTTPClientMock(store: store) client.onFetchAppConfiguration = { _, completeWith in - XCTAssertNil(store.appConfig) - XCTAssertNil(store.lastAppConfigETag) + XCTAssertNil(store.appConfigMetadata) completeWith((.failure(CachedAppConfiguration.CacheError.notModified), nil)) fetchedFromClientExpectation.fulfill() @@ -270,8 +265,7 @@ final class CachedAppConfigurationTests: XCTestCase { }) cache.appConfiguration { response in - XCTAssertNil(store.appConfig) - XCTAssertNil(store.lastAppConfigETag) + XCTAssertNil(store.appConfigMetadata) switch response { case .success: @@ -294,7 +288,7 @@ final class CachedAppConfigurationTests: XCTestCase { } -private extension Int { +extension Int { /// A date n seconds ago var secondsAgo: Date? { diff --git a/src/xcode/ENA/ENA/Source/Services/Risk/Provider/Helper/__tests__/DeviceTimeCheckTests.swift b/src/xcode/ENA/ENA/Source/Services/Risk/Provider/Helper/__tests__/DeviceTimeCheckTests.swift index 207ae223db7..9be63f938fc 100644 --- a/src/xcode/ENA/ENA/Source/Services/Risk/Provider/Helper/__tests__/DeviceTimeCheckTests.swift +++ b/src/xcode/ENA/ENA/Source/Services/Risk/Provider/Helper/__tests__/DeviceTimeCheckTests.swift @@ -9,7 +9,7 @@ final class DeviceTimeCheckTest: XCTestCase { func test_WHEN_CorrectDeviceTime_THEN_DeviceTimeIsCorrectIsSavedToStore() { let fakeStore = MockTestStore() - fakeStore.appConfig = makeAppConfig(killSwitchIsOn: false) + fakeStore.appConfigMetadata = makeAppConfig(killSwitchIsOn: false) let serverTime = Date() let deviceTime = Date() @@ -26,7 +26,7 @@ final class DeviceTimeCheckTest: XCTestCase { func test_WHEN_DeviceTimeIs2HoursInThePast_THEN_DeviceTimeIsCorrectIsSavedToStore() { let fakeStore = MockTestStore() - fakeStore.appConfig = makeAppConfig(killSwitchIsOn: false) + fakeStore.appConfigMetadata = makeAppConfig(killSwitchIsOn: false) let twoHourIntevall: Double = 2 * 60 * 60 let serverTime = Date() @@ -44,7 +44,7 @@ final class DeviceTimeCheckTest: XCTestCase { func test_WHEN_DeviceTimeIsOn2HoursInTheFuture_THEN_DeviceTimeIsCorrectIsSavedToStore() { let fakeStore = MockTestStore() - fakeStore.appConfig = makeAppConfig(killSwitchIsOn: false) + fakeStore.appConfigMetadata = makeAppConfig(killSwitchIsOn: false) let twoHourIntevall: Double = 2 * 60 * 60 let serverTime = Date() @@ -62,7 +62,7 @@ final class DeviceTimeCheckTest: XCTestCase { func test_WHEN_DeviceTimeMoreThen2HoursInThePast_THEN_DeviceTimeIsNOTCorrectIsSavedToStore() { let fakeStore = MockTestStore() - fakeStore.appConfig = makeAppConfig(killSwitchIsOn: false) + fakeStore.appConfigMetadata = makeAppConfig(killSwitchIsOn: false) let twoHourOneSecondIntevall: Double = 2 * 60 * 60 + 1 let serverTime = Date() @@ -80,7 +80,7 @@ final class DeviceTimeCheckTest: XCTestCase { func test_WHEN_DeviceTimeMoreThen2HoursInTheFuture_THEN_DeviceTimeIsNOTCorrectIsSavedToStore() { let fakeStore = MockTestStore() - fakeStore.appConfig = makeAppConfig(killSwitchIsOn: false) + fakeStore.appConfigMetadata = makeAppConfig(killSwitchIsOn: false) let twoHourOneSecondIntevall: Double = 2 * 60 * 60 + 1 let serverTime = Date() @@ -98,7 +98,7 @@ final class DeviceTimeCheckTest: XCTestCase { func test_WHEN_DeviceTimeMoreThen2HoursInTheFuture_AND_KillSwitchIsActive_THEN_DeviceTimeIsCorrectIsSavedToStore() { let fakeStore = MockTestStore() - fakeStore.appConfig = makeAppConfig(killSwitchIsOn: true) + fakeStore.appConfigMetadata = makeAppConfig(killSwitchIsOn: true) let serverTime = Date() guard let deviceTime = Calendar.current.date(byAdding: .minute, value: 121, to: serverTime) else { @@ -128,7 +128,7 @@ final class DeviceTimeCheckTest: XCTestCase { XCTAssertFalse(fakeStore.wasDeviceTimeErrorShown) } - private func makeAppConfig(killSwitchIsOn: Bool) -> SAP_Internal_ApplicationConfiguration { + private func makeAppConfig(killSwitchIsOn: Bool) -> AppConfigMetadata { var killSwitchFeature = SAP_Internal_AppFeature() killSwitchFeature.label = "disable-device-time-check" killSwitchFeature.value = killSwitchIsOn ? 1 : 0 @@ -139,6 +139,8 @@ final class DeviceTimeCheckTest: XCTestCase { var fakeAppConfig = SAP_Internal_ApplicationConfiguration() fakeAppConfig.appFeatures = fakeAppFeatures - return fakeAppConfig + let configMetadata = AppConfigMetadata(lastAppConfigETag: "\"SomeETag\"", lastAppConfigFetch: Date(), appConfig: fakeAppConfig) + + return configMetadata } } diff --git a/src/xcode/ENA/ENA/Source/Services/Risk/Provider/RiskProvider.swift b/src/xcode/ENA/ENA/Source/Services/Risk/Provider/RiskProvider.swift index e59b849ed8b..3d496df0c43 100644 --- a/src/xcode/ENA/ENA/Source/Services/Risk/Provider/RiskProvider.swift +++ b/src/xcode/ENA/ENA/Source/Services/Risk/Provider/RiskProvider.swift @@ -88,12 +88,15 @@ extension RiskProvider: RiskProviding { } /// Called by consumers to request the risk level. This method triggers the risk level process. + /// The completion is only used for the background fetch. Please use a consumer to get state updates. func requestRisk(userInitiated: Bool, ignoreCachedSummary: Bool = false, completion: Completion? = nil) { Log.info("RiskProvider: Request risk was called. UserInitiated: \(userInitiated), ignoreCachedSummary: \(ignoreCachedSummary)", log: .riskDetection) guard activityState == .idle else { Log.info("RiskProvider: Risk detection is allready running. Don't start new risk detection", log: .riskDetection) targetQueue.async { + // This completion callback only affects the background fetch. + // (Since at the moment the background fetch is the only one using the completion) completion?(.failure(.riskProviderIsRunning)) } return diff --git a/src/xcode/ENA/ENA/Source/Services/__tests__/Mocks/MockTestStore.swift b/src/xcode/ENA/ENA/Source/Services/__tests__/Mocks/MockTestStore.swift index d680040fc37..cdf8bf9f26f 100644 --- a/src/xcode/ENA/ENA/Source/Services/__tests__/Mocks/MockTestStore.swift +++ b/src/xcode/ENA/ENA/Source/Services/__tests__/Mocks/MockTestStore.swift @@ -58,7 +58,5 @@ final class MockTestStore: Store, AppConfigCaching { // MARK: - AppConfigCaching - var lastAppConfigETag: String? - var lastAppConfigFetch: Date? - var appConfig: SAP_Internal_ApplicationConfiguration? + var appConfigMetadata: AppConfigMetadata? } diff --git a/src/xcode/ENA/ENA/Source/Workers/Logging.swift b/src/xcode/ENA/ENA/Source/Workers/Logging.swift index 1042cb1b971..7c56fe95d49 100644 --- a/src/xcode/ENA/ENA/Source/Workers/Logging.swift +++ b/src/xcode/ENA/ENA/Source/Workers/Logging.swift @@ -13,6 +13,8 @@ extension OSLog { static let localData = OSLog(subsystem: subsystem, category: "localdata") /// Risk Detection static let riskDetection = OSLog(subsystem: subsystem, category: "riskdetection") + /// Risk Detection + static let appConfig = OSLog(subsystem: subsystem, category: "appconfig") } enum Log { diff --git a/src/xcode/ENA/ENA/Source/Workers/Store/SecureStore.swift b/src/xcode/ENA/ENA/Source/Workers/Store/SecureStore.swift index ecb34e9cd02..1ea20e1d7de 100644 --- a/src/xcode/ENA/ENA/Source/Workers/Store/SecureStore.swift +++ b/src/xcode/ENA/ENA/Source/Workers/Store/SecureStore.swift @@ -275,26 +275,12 @@ extension SecureStore { } extension SecureStore: AppConfigCaching { - var lastAppConfigETag: String? { - get { kvStore["lastAppConfigETag"] as String? ?? nil } - set { kvStore["lastAppConfigETag"] = newValue } - } - - var lastAppConfigFetch: Date? { - get { kvStore["lastAppConfigFetch"] as Date? ?? nil } - set { kvStore["lastAppConfigFetch"] = newValue } - } - - var appConfig: SAP_Internal_ApplicationConfiguration? { - get { - guard let data = kvStore["SAP_Internal_ApplicationConfiguration"] else { return nil } - return try? SAP_Internal_ApplicationConfiguration(serializedData: data) - } - set { kvStore["SAP_Internal_ApplicationConfiguration"] = try? newValue?.serializedData() } + var appConfigMetadata: AppConfigMetadata? { + get { kvStore["appConfigMetadata"] as AppConfigMetadata? ?? nil } + set { kvStore["appConfigMetadata"] = newValue } } } - extension SecureStore { static let keychainDatabaseKey = "secureStoreDatabaseKey" diff --git a/src/xcode/ENA/ENA/Source/Workers/Store/Store.swift b/src/xcode/ENA/ENA/Source/Workers/Store/Store.swift index b3356586141..33eca47376c 100644 --- a/src/xcode/ENA/ENA/Source/Workers/Store/Store.swift +++ b/src/xcode/ENA/ENA/Source/Workers/Store/Store.swift @@ -130,9 +130,7 @@ protocol StoreProtocol: AnyObject { } protocol AppConfigCaching: AnyObject { - var lastAppConfigETag: String? { get set } - var lastAppConfigFetch: Date? { get set } - var appConfig: SAP_Internal_ApplicationConfiguration? { get set } + var appConfigMetadata: AppConfigMetadata? { get set } } /// Convenience protocol diff --git a/src/xcode/ENA/ENA/Source/Workers/Store/__tests__/StoreTests.swift b/src/xcode/ENA/ENA/Source/Workers/Store/__tests__/StoreTests.swift index 375c8c107fc..85b6651a254 100644 --- a/src/xcode/ENA/ENA/Source/Workers/Store/__tests__/StoreTests.swift +++ b/src/xcode/ENA/ENA/Source/Workers/Store/__tests__/StoreTests.swift @@ -238,15 +238,13 @@ final class StoreTests: XCTestCase { func testConfigCaching() throws { let store = SecureStore(subDirectory: "test", serverEnvironment: ServerEnvironment()) - XCTAssertNil(store.appConfig) - XCTAssertNil(store.lastAppConfigETag) + XCTAssertNil(store.appConfigMetadata) let tag = "fake_\(Int.random(in: 100...999))" - store.lastAppConfigETag = tag - XCTAssertEqual(store.lastAppConfigETag, tag) - let config = CachingHTTPClientMock.staticAppConfig - store.appConfig = config - XCTAssertEqual(store.appConfig, config) + let appConfigMetadata = AppConfigMetadata(lastAppConfigETag: tag, lastAppConfigFetch: Date(), appConfig: config) + + store.appConfigMetadata = appConfigMetadata + XCTAssertEqual(store.appConfigMetadata, appConfigMetadata) } }