CryptoSDK phased rollout feature

This commit is contained in:
Andy Uhnak
2023-02-14 17:03:25 +00:00
parent 292ea0c347
commit 81cdb68ce4
21 changed files with 463 additions and 84 deletions

View File

@@ -92,14 +92,8 @@ class CommonConfiguration: NSObject, Configurable {
sdkOptions.enableNewClientInformationFeature = RiotSettings.shared.enableClientInformationFeature
if sdkOptions.isCryptoSDKAvailable {
let isEnabled = RiotSettings.shared.enableCryptoSDK
MXLog.debug("[CommonConfiguration] Crypto SDK is \(isEnabled ? "enabled" : "disabled")")
sdkOptions.enableCryptoSDK = isEnabled
sdkOptions.enableStartupProgress = isEnabled
} else {
MXLog.debug("[CommonConfiguration] Crypto SDK is not available)")
}
// Configure Crypto SDK feature deciding which crypto module to use
sdkOptions.cryptoSDKFeature = CryptoSDKFeature.shared
}
private func makeASCIIUserAgent() -> String? {

View File

@@ -1,42 +0,0 @@
//
// Copyright 2023 New Vector Ltd
//
// Licensed 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
/// Configuration for enabling / disabling Matrix Crypto SDK
@objcMembers class CryptoSDKConfiguration: NSObject {
static let shared = CryptoSDKConfiguration()
func enable() {
guard MXSDKOptions.sharedInstance().isCryptoSDKAvailable else {
return
}
RiotSettings.shared.enableCryptoSDK = true
MXSDKOptions.sharedInstance().enableCryptoSDK = true
MXSDKOptions.sharedInstance().enableStartupProgress = true
MXLog.debug("[CryptoSDKConfiguration] enabling Crypto SDK")
}
func disable() {
RiotSettings.shared.enableCryptoSDK = false
MXSDKOptions.sharedInstance().enableCryptoSDK = false
MXSDKOptions.sharedInstance().enableStartupProgress = false
MXLog.debug("[CryptoSDKConfiguration] disabling Crypto SDK")
}
}

View File

@@ -70,7 +70,7 @@ abstract_target 'RiotPods' do
pod 'WeakDictionary', '~> 2.0'
# PostHog for analytics
pod 'PostHog', '~> 1.4.4'
pod 'PostHog', '~> 2.0.0'
pod 'Sentry', '~> 7.15.0'
pod 'AnalyticsEvents', :git => 'https://github.com/matrix-org/matrix-analytics-events.git', :branch => 'release/swift', :inhibit_warnings => false
# pod 'AnalyticsEvents', :path => '../matrix-analytics-events/AnalyticsEvents.podspec'

View File

@@ -57,7 +57,7 @@ PODS:
- OLMKit/olmcpp (= 3.2.12)
- OLMKit/olmc (3.2.12)
- OLMKit/olmcpp (3.2.12)
- PostHog (1.4.4)
- PostHog (2.0.0)
- ReadMoreTextView (3.0.1)
- Realm (10.27.0):
- Realm/Headers (= 10.27.0)
@@ -105,7 +105,7 @@ DEPENDENCIES:
- MatrixSDK (= 0.25.2)
- MatrixSDK/JingleCallStack (= 0.25.2)
- OLMKit
- PostHog (~> 1.4.4)
- PostHog (~> 2.0.0)
- ReadMoreTextView (~> 3.0.1)
- Reusable (~> 4.1)
- Sentry (~> 7.15.0)
@@ -199,7 +199,7 @@ SPEC CHECKSUMS:
MatrixSDK: 354274127d163af37bdc55093ab96deea1be6a40
MatrixSDKCrypto: e1ef22aae76b5a6f030ace21a47be83864f4ff44
OLMKit: da115f16582e47626616874e20f7bb92222c7a51
PostHog: 4b6321b521569092d4ef3a02238d9435dbaeb99f
PostHog: 660ec6c9d80cec17b685e148f17f6785a88b597d
ReadMoreTextView: 19147adf93abce6d7271e14031a00303fe28720d
Realm: 9ca328bd7e700cc19703799785e37f77d1a130f2
Reusable: 6bae6a5e8aa793c9c441db0213c863a64bce9136
@@ -217,6 +217,6 @@ SPEC CHECKSUMS:
zxcvbn-ios: fef98b7c80f1512ff0eec47ac1fa399fc00f7e3c
ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb
PODFILE CHECKSUM: 20544e99d9acfdfbc4bf98b21d20c1496b9d6fe9
PODFILE CHECKSUM: a160b10da6c4728f70275f07b53ef1502c794d4e
COCOAPODS: 1.11.3

View File

@@ -0,0 +1,97 @@
//
// Copyright 2023 New Vector Ltd
//
// Licensed 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
/// An implementation of `MXCryptoV2Feature` which uses `UserDefaults` to persist the enabled status
/// of `CryptoSDK`, and which uses feature flags to control rollout availability.
///
/// The implementation uses both remote and local feature flags to control the availability of `CryptoSDK`.
/// Whilst remote is more convenient in that it allows changes to the rollout without new app releases,
/// it is not available to all users because it requires data tracking user consent. Remote therefore
/// represents the safer, albeit limited rollout strategy, whereas the local feature flags allows eventually
/// targetting all users, but each target change requires new app release.
///
/// Additionally users can manually enable this feature from the settings if they are not already in the
/// feature group.
@objc class CryptoSDKFeature: NSObject, MXCryptoV2Feature {
@objc static let shared = CryptoSDKFeature()
var isEnabled: Bool {
RiotSettings.shared.enableCryptoSDK
}
private static let FeatureName = "ios-crypto-sdk"
private let remoteFeature: RemoteFeaturesClientProtocol
private let localFeature: PhasedRolloutFeature
init(remoteFeature: RemoteFeaturesClientProtocol = PostHogAnalyticsClient.shared) {
self.remoteFeature = remoteFeature
self.localFeature = PhasedRolloutFeature(
name: Self.FeatureName,
// Local feature is currently set to 0% target, and all availability is fully controlled
// by the remote feature. Once the remote is fully rolled out, target for local feature will
// be gradually increased.
targetPercentage: 0.0
)
}
func enable() {
RiotSettings.shared.enableCryptoSDK = true
Analytics.shared.trackCryptoSDKEnabled()
MXLog.debug("[CryptoSDKFeature] Crypto SDK enabled")
}
func enableIfAvailable(forUserId userId: String!) {
guard !isEnabled else {
MXLog.debug("[CryptoSDKFeature] enableIfAvailable: Feature is already enabled")
return
}
guard let userId else {
MXLog.failure("[CryptoSDKFeature] enableIfAvailable: Missing user id")
return
}
guard isFeatureEnabled(userId: userId) else {
MXLog.debug("[CryptoSDKFeature] enableIfAvailable: Feature is currently not available for this user")
return
}
MXLog.debug("[CryptoSDKFeature] enableIfAvailable: Feature has become available for this user and will be enabled")
enable()
}
@objc func canManuallyEnable(forUserId userId: String!) -> Bool {
guard let userId else {
MXLog.failure("[CryptoSDKFeature] canManuallyEnable: Missing user id")
return false
}
// User can manually enable only if not already within the automatic feature group
return !isFeatureEnabled(userId: userId)
}
@objc func reset() {
RiotSettings.shared.enableCryptoSDK = false
MXLog.debug("[CryptoSDKFeature] Crypto SDK disabled")
}
private func isFeatureEnabled(userId: String) -> Bool {
remoteFeature.isFeatureEnabled(Self.FeatureName) || localFeature.isEnabled(userId: userId)
}
}

View File

@@ -0,0 +1,50 @@
//
// Copyright 2023 New Vector Ltd
//
// Licensed 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
import CryptoKit
/// Object encapsulating an experiment with an arbitrary number of variants, and a method to deterministically
/// and uniformly assign a variant to user. Variants do not carry any implicit semantics, they are plain numbers
/// to be interpreted by the caller of the experiment.
struct Experiment {
let name: String
let variants: UInt
/// Get the assigned variant from the total number of variants and for a given `userId`
///
/// This variant is chosen deterministically (the same `userId` and experiment `name` will yield the same variant)
/// and uniformly (multiple users are distributed roughly evenly among the variants).
func variant(userId: String) -> UInt {
// Combine user id with experiment name to avoid identical variant
// for the same user in different experiments
let data = (userId + name).data(using: .utf8) ?? Data()
// Get the first 8 bytes and map to decimal number (UInt64 = 8 bytes)
let decimal = digest(for: data)
.prefix(8)
.reduce(0) { $0 << 8 | UInt64($1) }
// Compress the decimal into a set number of variants using modulo
return UInt(decimal % UInt64(variants))
}
private func digest(for data: Data) -> SHA256.Digest {
var sha = SHA256()
sha.update(data: data)
return sha.finalize()
}
}

View File

@@ -0,0 +1,46 @@
//
// Copyright 2023 New Vector Ltd
//
// Licensed 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
/// Object enabling a phased rollout of features depending on the `userId` and `targetPercentage`.
///
/// The feature uses an experiment under the hood with 100 variants representing 100%.
/// Each userId is deterministically and uniformly assigned a variant, and depending
/// on whether this falls below or above the `targetPercentage` threshold, the feature
/// is considered enabled or disabled.
struct PhasedRolloutFeature {
private let experiment: Experiment
private let targetPercentage: Double
init(name: String, targetPercentage: Double) {
self.experiment = .init(
name: name,
// 100 variants where each variant represents a single percentage
variants: 100
)
self.targetPercentage = targetPercentage
}
func isEnabled(userId: String) -> Bool {
// Get a bucket number between 0-99
let variant = experiment.variant(userId: userId)
// Convert to a percentage
let percentage = Double(variant) / 100
// Consider enabled if falls below rollout target
return percentage < targetPercentage
}
}

View File

@@ -0,0 +1,22 @@
//
// Copyright 2023 New Vector Ltd
//
// Licensed 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
/// A protocol representing a remote features client
protocol RemoteFeaturesClientProtocol {
func isFeatureEnabled(_ feature: String) -> Bool
}

View File

@@ -44,7 +44,7 @@ import AnalyticsEvents
static let shared = Analytics()
/// The analytics client to send events with.
private var client: AnalyticsClientProtocol = PostHogAnalyticsClient()
private var client: AnalyticsClientProtocol = PostHogAnalyticsClient.shared
/// The monitoring client to track crashes, issues and performance
private var monitoringClient = SentryMonitoringClient()

View File

@@ -25,6 +25,8 @@ class PostHogAnalyticsClient: AnalyticsClientProtocol {
/// Any user properties to be included with the next captured event.
private(set) var pendingUserProperties: AnalyticsEvent.UserProperties?
static let shared = PostHogAnalyticsClient()
var isRunning: Bool { postHog?.enabled ?? false }
func start() {
@@ -102,3 +104,9 @@ class PostHogAnalyticsClient: AnalyticsClientProtocol {
return properties
}
}
extension PostHogAnalyticsClient: RemoteFeaturesClientProtocol {
func isFeatureEnabled(_ feature: String) -> Bool {
postHog?.isFeatureEnabled(feature) == true
}
}

View File

@@ -2184,7 +2184,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
[self clearCache];
// Reset Crypto SDK configuration (labs flag for which crypto module to use)
[CryptoSDKConfiguration.shared disable];
[CryptoSDKFeature.shared reset];
// Reset key backup banner preferences
[SecureBackupBannerPreferences.shared reset];

View File

@@ -588,7 +588,7 @@ ChangePasswordCoordinatorBridgePresenterDelegate>
if (BuildSettings.settingsScreenShowLabSettings)
{
Section *sectionLabs = [Section sectionWithTag:SECTION_TAG_LABS];
if (MXSDKOptions.sharedInstance.isCryptoSDKAvailable)
if ([CryptoSDKFeature.shared canManuallyEnableForUserId:self.mainSession.myUserId])
{
[sectionLabs addRowWithTag:LABS_ENABLE_CRYPTO_SDK];
}
@@ -2589,20 +2589,17 @@ ChangePasswordCoordinatorBridgePresenterDelegate>
cell = labelAndSwitchCell;
}
else
else if (row == LABS_ENABLE_CRYPTO_SDK)
{
if (row == LABS_ENABLE_CRYPTO_SDK)
{
MXKTableViewCellWithLabelAndSwitch *labelAndSwitchCell = [self getLabelAndSwitchCell:tableView forIndexPath:indexPath];
BOOL isEnabled = MXSDKOptions.sharedInstance.enableCryptoSDK;
labelAndSwitchCell.mxkLabel.text = isEnabled ? VectorL10n.settingsLabsDisableCryptoSdk : VectorL10n.settingsLabsEnableCryptoSdk;
labelAndSwitchCell.mxkSwitch.on = isEnabled;
[labelAndSwitchCell.mxkSwitch setEnabled:!isEnabled];
labelAndSwitchCell.mxkSwitch.onTintColor = ThemeService.shared.theme.tintColor;
[labelAndSwitchCell.mxkSwitch addTarget:self action:@selector(enableCryptoSDKFeature:) forControlEvents:UIControlEventTouchUpInside];
cell = labelAndSwitchCell;
}
MXKTableViewCellWithLabelAndSwitch *labelAndSwitchCell = [self getLabelAndSwitchCell:tableView forIndexPath:indexPath];
BOOL isEnabled = MXSDKOptions.sharedInstance.enableCryptoSDK;
labelAndSwitchCell.mxkLabel.text = isEnabled ? VectorL10n.settingsLabsDisableCryptoSdk : VectorL10n.settingsLabsEnableCryptoSdk;
labelAndSwitchCell.mxkSwitch.on = isEnabled;
[labelAndSwitchCell.mxkSwitch setEnabled:!isEnabled];
labelAndSwitchCell.mxkSwitch.onTintColor = ThemeService.shared.theme.tintColor;
[labelAndSwitchCell.mxkSwitch addTarget:self action:@selector(enableCryptoSDKFeature:) forControlEvents:UIControlEventTouchUpInside];
cell = labelAndSwitchCell;
}
}
else if (section == SECTION_TAG_SECURITY)
@@ -3391,10 +3388,7 @@ ChangePasswordCoordinatorBridgePresenterDelegate>
}]];
[confirmationAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n continue] style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) {
[CryptoSDKConfiguration.shared enable];
[Analytics.shared trackCryptoSDKEnabled];
[CryptoSDKFeature.shared enable];
[[AppDelegate theDelegate] reloadMatrixSessions:YES];
}]];

View File

@@ -200,11 +200,14 @@ class NotificationService: UNNotificationServiceExtension {
MXLog.debug("[NotificationService] setup: MXBackgroundSyncService init: BEFORE")
self.logMemory()
NotificationService.backgroundSyncService = MXBackgroundSyncService(withCredentials: userAccount.mxCredentials, persistTokenDataHandler: { persistTokenDataHandler in
MXKAccountManager.shared().readAndWriteCredentials(persistTokenDataHandler)
}, unauthenticatedHandler: { error, softLogout, refreshTokenAuth, completion in
userAccount.handleUnauthenticatedWithError(error, isSoftLogout: softLogout, isRefreshTokenAuth: refreshTokenAuth, andCompletion: completion)
})
NotificationService.backgroundSyncService = MXBackgroundSyncService(
withCredentials: userAccount.mxCredentials,
isCryptoSDKEnabled: isCryptoSDKEnabled,
persistTokenDataHandler: { persistTokenDataHandler in
MXKAccountManager.shared().readAndWriteCredentials(persistTokenDataHandler)
}, unauthenticatedHandler: { error, softLogout, refreshTokenAuth, completion in
userAccount.handleUnauthenticatedWithError(error, isSoftLogout: softLogout, isRefreshTokenAuth: refreshTokenAuth, andCompletion: completion)
})
MXLog.debug("[NotificationService] setup: MXBackgroundSyncService init: AFTER")
self.logMemory()
}
@@ -219,10 +222,10 @@ class NotificationService: UNNotificationServiceExtension {
/// Determine whether we have switched from using crypto v1 to v2 or vice versa which will require
/// rebuilding `MXBackgroundSyncService`
private func hasChangedCryptoSDK() -> Bool {
guard isCryptoSDKEnabled != RiotSettings.shared.enableCryptoSDK else {
guard isCryptoSDKEnabled != MXSDKOptions.sharedInstance().enableCryptoSDK else {
return false
}
isCryptoSDKEnabled = RiotSettings.shared.enableCryptoSDK
isCryptoSDKEnabled = MXSDKOptions.sharedInstance().enableCryptoSDK
return true
}

View File

@@ -45,6 +45,7 @@ targets:
- path: ../Config/BuildSettings.swift
- path: ../Riot/Utils/DataProtectionHelper.swift
- path: ../Config/CommonConfiguration.swift
- path: ../Riot/Experiments/
- path: ../Riot/Managers/PushNotification/PushNotificationStore.swift
- path: ../Riot/Modules/SetPinCode/PinCodePreferences.swift
- path: ../Riot/Managers/KeyValueStorage/Extensions/Keychain.swift

View File

@@ -52,6 +52,7 @@ targets:
- path: ../Riot/Categories/MXRoom+Riot.m
- path: ../Config/Configurable.swift
- path: ../Config/CommonConfiguration.swift
- path: ../Riot/Experiments/
- path: ../Riot/Utils/UserNameColorGenerator.swift
- path: ../Riot/Categories/MXRoomSummary+Riot.m
- path: ../Riot/Managers/EncryptionKeyManager/EncryptionKeyManager.swift

View File

@@ -0,0 +1,79 @@
//
// Copyright 2023 New Vector Ltd
//
// Licensed 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
import XCTest
@testable import Element
class CryptoSDKFeatureTests: XCTestCase {
class RemoteFeatureClient: RemoteFeaturesClientProtocol {
var isEnabled = false
func isFeatureEnabled(_ feature: String) -> Bool {
isEnabled
}
}
var remote: RemoteFeatureClient!
var feature: CryptoSDKFeature!
override func setUp() {
RiotSettings.shared.enableCryptoSDK = false
remote = RemoteFeatureClient()
feature = CryptoSDKFeature(remoteFeature: remote)
}
override func tearDown() {
RiotSettings.shared.enableCryptoSDK = false
}
func test_disabledByDefault() {
XCTAssertFalse(feature.isEnabled)
}
func test_enable() {
feature.enable()
XCTAssertTrue(feature.isEnabled)
}
func test_enableIfAvailable_remainsEnabledWhenRemoteClientDisabled() {
feature.enable()
remote.isEnabled = false
feature.enableIfAvailable(forUserId: "alice")
XCTAssertTrue(feature.isEnabled)
}
func test_enableIfAvailable_notEnabledIfRemoteFeatureDisabled() {
remote.isEnabled = false
feature.enableIfAvailable(forUserId: "alice")
XCTAssertFalse(feature.isEnabled)
}
func test_canManuallyEnable() {
remote.isEnabled = false
XCTAssertTrue(feature.canManuallyEnable(forUserId: "alice"))
remote.isEnabled = true
XCTAssertFalse(feature.canManuallyEnable(forUserId: "alice"))
}
func test_reset() {
feature.enable()
feature.reset()
XCTAssertFalse(RiotSettings.shared.enableCryptoSDK)
}
}

View File

@@ -0,0 +1,71 @@
//
// Copyright 2023 New Vector Ltd
//
// Licensed 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
import XCTest
@testable import Element
class ExperimentTests: XCTestCase {
private func randomUserId() -> String {
return "user_" + UUID().uuidString.prefix(6)
}
func test_singleVariant() {
let experiment = Experiment(name: "single", variants: 1)
for _ in 0 ..< 1000 {
let variant = experiment.variant(userId: randomUserId())
XCTAssertEqual(variant, 0)
}
}
func test_twoVariants() {
let experiment = Experiment(name: "two", variants: 2)
var variants = Set<UInt>()
for _ in 0 ..< 1000 {
let variant = experiment.variant(userId: randomUserId())
variants.insert(variant)
}
// We perform the test by collecting all assigned variants for 1000 users
// and ensuring we only encounter variants 0 and 1
XCTAssertEqual(variants.count, 2)
XCTAssertTrue(variants.contains(0))
XCTAssertTrue(variants.contains(1))
XCTAssertFalse(variants.contains(2))
}
func test_manyVariants() {
let experiment = Experiment(name: "many", variants: 5)
var variants = Set<UInt>()
for _ in 0 ..< 10000 {
let variant = experiment.variant(userId: randomUserId())
variants.insert(variant)
}
// We perform the test by collecting all assigned variants for 10000 users
// and ensuring we only encounter variants between 0 and 4
XCTAssertEqual(variants.count, 5)
XCTAssertTrue(variants.contains(0))
XCTAssertTrue(variants.contains(1))
XCTAssertTrue(variants.contains(2))
XCTAssertTrue(variants.contains(3))
XCTAssertTrue(variants.contains(4))
XCTAssertFalse(variants.contains(5))
}
}

View File

@@ -0,0 +1,55 @@
//
// Copyright 2023 New Vector Ltd
//
// Licensed 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
import XCTest
@testable import Element
class PhasedRolloutFeatureTests: XCTestCase {
private func randomUserId() -> String {
return "user_" + UUID().uuidString.prefix(6)
}
func test_allDisabled() {
let feature = PhasedRolloutFeature(name: "disabled", targetPercentage: 0)
for _ in 0 ..< 1000 {
let isEnabled = feature.isEnabled(userId: randomUserId())
XCTAssertFalse(isEnabled)
}
}
func test_allEnabled() {
let feature = PhasedRolloutFeature(name: "enabled", targetPercentage: 1)
for _ in 0 ..< 1000 {
let isEnabled = feature.isEnabled(userId: randomUserId())
XCTAssertTrue(isEnabled)
}
}
func test_someEnabled() {
let feature = PhasedRolloutFeature(name: "some", targetPercentage: 0.5)
var statuses = Set<Bool>()
for _ in 0 ..< 1000 {
let isEnabled = feature.isEnabled(userId: randomUserId())
statuses.insert(isEnabled)
}
// We test by checking that we encountered both enabled and disabled cases
XCTAssertTrue(statuses.contains(true))
XCTAssertTrue(statuses.contains(false))
}
}

View File

@@ -60,8 +60,6 @@ targets:
- path: .
- path: ../Config/Configurable.swift
- path: ../Config/BuildSettings.swift
- path: ../Config/AppConfiguration.swift
- path: ../Config/CommonConfiguration.swift
- path: ../Riot/Categories/Bundle.swift
- path: ../Riot/Managers/AppInfo/AppInfo.swift
- path: ../Riot/Managers/AppInfo/AppVersion.swift

View File

@@ -45,6 +45,7 @@ targets:
- path: ../Riot/Categories/Bundle.swift
- path: ../Riot/Categories/MXEvent.swift
- path: ../Config/CommonConfiguration.swift
- path: ../Riot/Experiments/
- path: ../Config/BuildSettings.swift
- path: ../Config/Configurable.swift
- path: ../Riot/Managers/Settings/RiotSettings.swift

View File

@@ -0,0 +1 @@
CryptoV2: CryptoSDK phased rollout feature