Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion Sources/Extensions/Value+Semi-Equatable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,3 @@ extension Value {
return false
}
}

12 changes: 12 additions & 0 deletions Sources/Protocols/Logger.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Logger.swift

import Foundation

public protocol Logger {
func log(_ error: ToggleError)
}

public enum ToggleError: Error {
case invalidValueType(Variable, Value, ValueProvider)
case insecureValue(Variable, ValueProvider)
}
16 changes: 12 additions & 4 deletions Sources/ToggleManager/ToggleManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ final public class ToggleManager: ObservableObject {
let cache = ValueCache<Variable, Value>()
var subjectsRefs = [Variable: CurrentValueSubject<Value, Never>]()
let options: [ToggleManagerOptions]
var logger: Logger?

@Published var hasOverrides: Bool = false

Expand All @@ -44,6 +45,7 @@ final public class ToggleManager: ObservableObject {
valueProviders: [ValueProvider] = [],
datasourceUrl: URL,
cipherConfiguration: CipherConfiguration? = nil,
logger: Logger? = nil,
options: [ToggleManagerOptions] = []) throws {
self.mutableValueProvider = mutableValueProvider
self.valueProviders = valueProviders
Expand All @@ -53,6 +55,7 @@ final public class ToggleManager: ObservableObject {
self.hasOverrides = !mutableValueProvider.variables.isEmpty
}
self.options = options
self.logger = logger
}
}

Expand All @@ -79,11 +82,14 @@ extension ToggleManager {
private func fetchValueFromProviders(for variable: Variable) -> Value {
let defaultValue: Value? = defaultValueProvider.optionalValue(for: variable)

if let value = mutableValueProvider?.value(for: variable), isValueValid(value: value, defaultValue: defaultValue) {
return value
if let mutableValueProvider, let value = mutableValueProvider.value(for: variable) {
if isValueValid(value: value, defaultValue: defaultValue, variableName: variable, provider: mutableValueProvider) {
return value
}
}

for provider in valueProviders {
if let value = provider.value(for: variable), isValueValid(value: value, defaultValue: defaultValue) {
if let value = provider.value(for: variable), isValueValid(value: value, defaultValue: defaultValue, variableName: variable, provider: provider) {
return value
}
}
Expand All @@ -102,12 +108,14 @@ extension ToggleManager {
return !options.contains(.noCaching)
}

private func isValueValid(value: Value, defaultValue: Value?) -> Bool {
private func isValueValid(value: Value, defaultValue: Value?, variableName: Variable, provider: ValueProvider) -> Bool {
if shouldCheckInvalidValueTypes, let defaultValue, !(value ~= defaultValue) {
logger?.log(ToggleError.invalidValueType(variableName, value, provider))
return false
}

if shouldCheckInvalidSecureValues, ((try? readValue(for: value)) == nil) {
logger?.log(ToggleError.insecureValue(variableName, provider))
return false
}

Expand Down
262 changes: 262 additions & 0 deletions Tests/Suites/ToggleManager/ToggleManager+ErrorLoggingTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
// ToggleManager+ErrorLogging.swift

@testable import Toggles
import XCTest

final class ToggleManager_ErrorLoggingTests: XCTestCase {
private var toggleManager: ToggleManager!
private var mockErrorLogger: myErrorLogger = myErrorLogger()

override func tearDown() {
toggleManager.removeOverrides()
toggleManager = nil
mockErrorLogger = myErrorLogger()
super.tearDown()
}

final private class myErrorLogger: Logger {
public var loggedMessages: [ToggleError] = []

func log(_ error: ToggleError) {
loggedMessages.append(error)
}
}

func test_fallbackToDefaultValue_WhenOtherValueProviderValueIsADifferentType_isLogged() throws {
let url = Bundle.toggles.url(forResource: "TestDatasource", withExtension: "json")!
let invalidValue = Value.string("hello world")
let valueProvider = MockSingleValueProvider(value: invalidValue)
toggleManager = try ToggleManager(valueProviders: [MockSingleValueProvider(value: invalidValue)], datasourceUrl: url, logger: mockErrorLogger, options: [.skipInvalidValueTypes])
let variable = "integer_toggle"
XCTAssertEqual(toggleManager.value(for: variable), Value.int(42))

XCTAssertTrue(mockErrorLogger.loggedMessages.contains { error in
if case .invalidValueType(let varName, let value, let provider) = error {
return varName == variable &&
value == invalidValue &&
provider.name == valueProvider.name
}
return false
})

XCTAssertEqual(mockErrorLogger.loggedMessages.count, 1)
}

func test_fallbackToDefaultValue_WhenMultipleOtherValueProviderValuesAreDifferentTypes_isLogged() throws {
let url = Bundle.toggles.url(forResource: "TestDatasource", withExtension: "json")!
let invalidValue1 = Value.string("hello world")
let invalidValue2 = Value.bool(true)
toggleManager = try ToggleManager(valueProviders: [MockSingleValueProvider(value: invalidValue1), MockSingleValueProvider(value: invalidValue2)], datasourceUrl: url, logger: mockErrorLogger, options: [.skipInvalidValueTypes])
let variable = "integer_toggle"
XCTAssertEqual(toggleManager.value(for: variable), Value.int(42))

XCTAssertTrue(mockErrorLogger.loggedMessages.contains { error in
if case .invalidValueType(let varName, let value, let provider) = error {
return varName == variable &&
value == invalidValue1 &&
provider.name == "SingleValue (mock)"
}
return false
})

XCTAssertTrue(mockErrorLogger.loggedMessages.contains { error in
if case .invalidValueType(let varName, let value, let provider) = error {
return varName == variable &&
value == invalidValue2 &&
provider.name == "SingleValue (mock)"
}
return false
})
XCTAssertEqual(mockErrorLogger.loggedMessages.count, 2)
}

func test_fallbackToOtherValidValue_WhenOneValueProviderValueIsADifferentType_isLogged() throws {
let url = Bundle.toggles.url(forResource: "TestDatasource", withExtension: "json")!
let invalidValue = Value.string("hello world")
toggleManager = try ToggleManager(valueProviders: [MockSingleValueProvider(value: invalidValue), MockSingleValueProvider(value: .int(333))], datasourceUrl: url, logger: mockErrorLogger, options: [.skipInvalidValueTypes])
let variable = "integer_toggle"
XCTAssertEqual(toggleManager.value(for: variable), Value.int(333))

XCTAssertTrue(mockErrorLogger.loggedMessages.contains { error in
if case .invalidValueType(let varName, let value, let provider) = error {
return varName == variable &&
value == invalidValue &&
provider.name == "SingleValue (mock)"
}
return false
})

XCTAssertEqual(mockErrorLogger.loggedMessages.count, 1)
}

func test_fallbackToDefaultValue_WhenOtherValueProviderHasAnInvalidSecureValue_isLogged() throws {
let url = Bundle.toggles.url(forResource: "TestDatasource", withExtension: "json")!
let invalidValue = Value.secure("my unencrypted string")
toggleManager = try ToggleManager(valueProviders: [MockSingleValueProvider(value: invalidValue)],
datasourceUrl: url,
cipherConfiguration: CipherConfiguration.chaChaPoly,
logger: mockErrorLogger,
options: [.skipInvalidSecureValues])

let variable = "secure_toggle"
let value = try toggleManager.readValue(for: .secure("YXe+Ev76FbdwCeDCVpZNZ1RItWZwKTLXF3/Yi+x62n3JWbvPo6YK"))
XCTAssertEqual(toggleManager.value(for: variable), value)

XCTAssertTrue(mockErrorLogger.loggedMessages.contains { error in
if case .insecureValue(let varName, let provider) = error {
return varName == variable &&
provider.name == "SingleValue (mock)"
}
return false
})

XCTAssertEqual(mockErrorLogger.loggedMessages.count, 1)
}

func test_fallbackToDefaultValue_WhenInMemoryValueProviderHasAPresetInvalidSecureValue_isLogged() throws {
let url = Bundle.toggles.url(forResource: "TestDatasource", withExtension: "json")!
let variable = "secure_toggle"
let invalidValue = Value.secure("my unencrypted string")
let inMemoryValueProvider = InMemoryValueProvider(datasource: [variable : invalidValue])

toggleManager = try ToggleManager(mutableValueProvider: inMemoryValueProvider,
datasourceUrl: url,
cipherConfiguration: CipherConfiguration.chaChaPoly,
logger: mockErrorLogger,
options: [.skipInvalidSecureValues])

let value = try toggleManager.readValue(for: .secure("YXe+Ev76FbdwCeDCVpZNZ1RItWZwKTLXF3/Yi+x62n3JWbvPo6YK"))
XCTAssertEqual(toggleManager.value(for: variable), value)

XCTAssertTrue(mockErrorLogger.loggedMessages.contains { error in
if case .insecureValue(let varName, let provider) = error {
return varName == variable &&
provider.name == "InMemory"
}
return false
})

XCTAssertEqual(mockErrorLogger.loggedMessages.count, 1)
}

func test_fallbackToOtherSecureValue_WhenOtherValueProviderHasAnInvalidSecureValue_isLogged() throws {
let url = Bundle.toggles.url(forResource: "TestDatasource", withExtension: "json")!
let invalidValue = Value.secure("my unencrypted string")
toggleManager = try ToggleManager(valueProviders: [MockSingleValueProvider(value: invalidValue), MockSingleValueProvider(value: .secure("SwYDP6aCtI6L1jkJqE3vdzni/6V/CR9PDvpXG3bbF7t48iWyhjxx"))],
datasourceUrl: url,
cipherConfiguration: CipherConfiguration.chaChaPoly,
logger: mockErrorLogger,
options: [.skipInvalidSecureValues])

let variable = "secure_toggle"
let value = try toggleManager.readValue(for: .secure("SwYDP6aCtI6L1jkJqE3vdzni/6V/CR9PDvpXG3bbF7t48iWyhjxx"))
XCTAssertEqual(toggleManager.value(for: variable), value)

XCTAssertTrue(mockErrorLogger.loggedMessages.contains { error in
if case .insecureValue(let varName, let provider) = error {
return varName == variable &&
provider.name == "SingleValue (mock)"
}
return false
})

XCTAssertEqual(mockErrorLogger.loggedMessages.count, 1)
}

func test_fallbackToOtherSecureValue_WhenInMemoryValueProviderHasAnInvalidSecureValue_isLogged() throws {
let url = Bundle.toggles.url(forResource: "TestDatasource", withExtension: "json")!
let variable = "secure_toggle"
let invalidValue = Value.secure("my unencrypted string")
let inMemoryValueProvider = InMemoryValueProvider(datasource: [variable : invalidValue])

toggleManager = try ToggleManager(mutableValueProvider: inMemoryValueProvider,
valueProviders: [MockSingleValueProvider(value: .secure("SwYDP6aCtI6L1jkJqE3vdzni/6V/CR9PDvpXG3bbF7t48iWyhjxx"))],
datasourceUrl: url,
cipherConfiguration: CipherConfiguration.chaChaPoly,
logger: mockErrorLogger,
options: [.skipInvalidSecureValues])

let value = try toggleManager.readValue(for: .secure("SwYDP6aCtI6L1jkJqE3vdzni/6V/CR9PDvpXG3bbF7t48iWyhjxx"))
XCTAssertEqual(toggleManager.value(for: variable), value)

XCTAssertTrue(mockErrorLogger.loggedMessages.contains { error in
if case .insecureValue(let varName, let provider) = error {
return varName == variable &&
provider.name == "InMemory"
}
return false
})

XCTAssertEqual(mockErrorLogger.loggedMessages.count, 1)
}

func test_fallbackToDefaultValue_WhenMultipleOtherValueProvidersHaveInvalidSecureValues_isLogged() throws {
let url = Bundle.toggles.url(forResource: "TestDatasource", withExtension: "json")!
let invalidValue1 = Value.secure("my unencrypted string")
let invalidValue2 = Value.secure("hello world")
toggleManager = try ToggleManager(valueProviders: [MockSingleValueProvider(value: invalidValue1), MockSingleValueProvider(value: invalidValue2)],
datasourceUrl: url,
cipherConfiguration: CipherConfiguration.chaChaPoly,
logger: mockErrorLogger,
options: [.skipInvalidSecureValues])

let variable = "secure_toggle"
let value = try toggleManager.readValue(for: .secure("YXe+Ev76FbdwCeDCVpZNZ1RItWZwKTLXF3/Yi+x62n3JWbvPo6YK"))
XCTAssertEqual(toggleManager.value(for: variable), value)

XCTAssertTrue(mockErrorLogger.loggedMessages.contains { error in
if case .insecureValue(let varName, let provider) = error {
return varName == variable &&
provider.name == "SingleValue (mock)"
}
return false
})

XCTAssertTrue(mockErrorLogger.loggedMessages.contains { error in
if case .insecureValue(let varName, let provider) = error {
return varName == variable &&
provider.name == "SingleValue (mock)"
}
return false
})

XCTAssertEqual(mockErrorLogger.loggedMessages.count, 2)
}

func test_fallbackWhenValueProvidersContainsInvalidSecureValueAndDifferentTypes_AndNoFallBackWhenValueIsNotInDefaultValueProvider_isLogged() throws {
let url = Bundle.toggles.url(forResource: "TestDatasource", withExtension: "json")!
let invalidValue1 = Value.string("hello world")
let invalidValue2 = Value.secure("my unencrypted string")
toggleManager = try ToggleManager(valueProviders: [MockSingleValueProvider(value: invalidValue1), MockSingleValueProvider(value: invalidValue2)],
datasourceUrl: url,
cipherConfiguration: CipherConfiguration.chaChaPoly,
logger: mockErrorLogger,
options: [.skipInvalidValueTypes, .skipInvalidSecureValues])

let variableNotInDefaultProvider = "string_toggle_new"
XCTAssertEqual(toggleManager.value(for: variableNotInDefaultProvider), .string("hello world"))

let secureVariable = "secure_toggle"
let value = try toggleManager.readValue(for: .secure("YXe+Ev76FbdwCeDCVpZNZ1RItWZwKTLXF3/Yi+x62n3JWbvPo6YK"))
XCTAssertEqual(toggleManager.value(for: secureVariable), value)

XCTAssertTrue(mockErrorLogger.loggedMessages.contains { error in
if case .invalidValueType(let varName, let value, let provider) = error {
return varName == secureVariable &&
value == invalidValue1 &&
provider.name == "SingleValue (mock)"
}
return false
})

XCTAssertTrue(mockErrorLogger.loggedMessages.contains { error in
if case .insecureValue(let varName, let provider) = error {
return varName == secureVariable &&
provider.name == "SingleValue (mock)"
}
return false
})

XCTAssertEqual(mockErrorLogger.loggedMessages.count, 2)
}
}