diff --git a/Sources/DevConfiguration/Core/CodableValueRepresentation.swift b/Sources/DevConfiguration/Core/CodableValueRepresentation.swift index 1cdb608..3847f41 100644 --- a/Sources/DevConfiguration/Core/CodableValueRepresentation.swift +++ b/Sources/DevConfiguration/Core/CodableValueRepresentation.swift @@ -118,6 +118,28 @@ public struct CodableValueRepresentation: Sendable { } + /// Converts encoded `Data` into the appropriate ``ConfigContent`` for this representation. + /// + /// For string-backed representations, this converts the data to a string using the representation's encoding and + /// returns it as ``ConfigContent/string(_:)``. For data-backed representations, this returns the data's bytes as + /// ``ConfigContent/bytes(_:)``. + /// + /// - Parameter data: The encoded data to convert. + /// - Returns: The ``ConfigContent`` representing the encoded data. + /// - Throws: ``StringEncodingError`` if the data cannot be converted to a string using the expected encoding. + func encodeToContent(_ data: Data) throws -> ConfigContent { + switch kind { + case .string(let encoding): + guard let string = String(data: data, encoding: encoding) else { + throw StringEncodingError(encoding: encoding) + } + return .string(string) + case .data: + return .bytes(Array(data)) + } + } + + /// Watches for raw data changes from the reader based on this representation. /// /// Each time the underlying configuration value changes, `onUpdate` is called with the new raw data (or `nil` if the @@ -154,3 +176,10 @@ public struct CodableValueRepresentation: Sendable { } } } + + +/// An error thrown when encoded data cannot be converted to a string using the expected encoding. +struct StringEncodingError: Error { + /// The string encoding that failed. + let encoding: String.Encoding +} diff --git a/Sources/DevConfiguration/Core/ConfigVariableContent.swift b/Sources/DevConfiguration/Core/ConfigVariableContent.swift index 94cd678..5dffea7 100644 --- a/Sources/DevConfiguration/Core/ConfigVariableContent.swift +++ b/Sources/DevConfiguration/Core/ConfigVariableContent.swift @@ -65,6 +65,9 @@ public struct ConfigVariableContent: Sendable where Value: Sendable { _ line: UInt, _ continuation: AsyncStream.Continuation ) async throws -> Void + + /// Encodes a value into a ``ConfigContent`` for registration. + let encode: @Sendable (_ value: Value) throws -> ConfigContent } @@ -99,7 +102,8 @@ extension ConfigVariableContent where Value == Bool { continuation.yield(value) } } - } + }, + encode: { .bool($0) } ) } } @@ -134,7 +138,8 @@ extension ConfigVariableContent where Value == [Bool] { continuation.yield(value) } } - } + }, + encode: { .boolArray($0) } ) } } @@ -169,7 +174,8 @@ extension ConfigVariableContent where Value == Float64 { continuation.yield(value) } } - } + }, + encode: { .double($0) } ) } } @@ -204,7 +210,8 @@ extension ConfigVariableContent where Value == [Float64] { continuation.yield(value) } } - } + }, + encode: { .doubleArray($0) } ) } } @@ -239,7 +246,8 @@ extension ConfigVariableContent where Value == Int { continuation.yield(value) } } - } + }, + encode: { .int($0) } ) } } @@ -274,7 +282,8 @@ extension ConfigVariableContent where Value == [Int] { continuation.yield(value) } } - } + }, + encode: { .intArray($0) } ) } } @@ -309,7 +318,8 @@ extension ConfigVariableContent where Value == String { continuation.yield(value) } } - } + }, + encode: { .string($0) } ) } } @@ -344,7 +354,8 @@ extension ConfigVariableContent where Value == [String] { continuation.yield(value) } } - } + }, + encode: { .stringArray($0) } ) } } @@ -379,7 +390,8 @@ extension ConfigVariableContent where Value == [UInt8] { continuation.yield(value) } } - } + }, + encode: { .bytes($0) } ) } } @@ -420,7 +432,8 @@ extension ConfigVariableContent where Value == [[UInt8]] { continuation.yield(value) } } - } + }, + encode: { .byteChunkArray($0) } ) } } @@ -467,7 +480,8 @@ extension ConfigVariableContent { continuation.yield(value) } } - } + }, + encode: { .string($0.rawValue) } ) } @@ -510,7 +524,8 @@ extension ConfigVariableContent { continuation.yield(value) } } - } + }, + encode: { .stringArray($0.map(\.rawValue)) } ) } @@ -552,7 +567,8 @@ extension ConfigVariableContent { continuation.yield(value) } } - } + }, + encode: { .string($0.description) } ) } @@ -595,7 +611,8 @@ extension ConfigVariableContent { continuation.yield(value) } } - } + }, + encode: { .stringArray($0.map(\.description)) } ) } } @@ -642,7 +659,8 @@ extension ConfigVariableContent { continuation.yield(value) } } - } + }, + encode: { .int($0.rawValue) } ) } @@ -685,7 +703,8 @@ extension ConfigVariableContent { continuation.yield(value) } } - } + }, + encode: { .intArray($0.map(\.rawValue)) } ) } @@ -727,7 +746,8 @@ extension ConfigVariableContent { continuation.yield(value) } } - } + }, + encode: { .int($0.configInt) } ) } @@ -770,7 +790,8 @@ extension ConfigVariableContent { continuation.yield(value) } } - } + }, + encode: { .intArray($0.map(\.configInt)) } ) } } @@ -905,6 +926,11 @@ extension ConfigVariableContent { } continuation.yield(defaultValue) } + }, + encode: { (value) in + let resolvedEncoder = encoder ?? JSONEncoder() + let data = try resolvedEncoder.encode(value) + return try representation.encodeToContent(data) } ) } diff --git a/Sources/DevConfiguration/Core/ConfigVariableReader.swift b/Sources/DevConfiguration/Core/ConfigVariableReader.swift index 628d34e..50f0ecd 100644 --- a/Sources/DevConfiguration/Core/ConfigVariableReader.swift +++ b/Sources/DevConfiguration/Core/ConfigVariableReader.swift @@ -7,6 +7,7 @@ import Configuration import DevFoundation +import OSLog /// Provides access to configuration values queried by a `ConfigVariable`. /// @@ -53,6 +54,12 @@ public struct ConfigVariableReader { /// The event bus used to post diagnostic events like ``ConfigVariableDecodingFailedEvent``. public let eventBus: EventBus + /// The variables that have been registered with this reader, keyed by their configuration key. + private(set) var registeredVariables: [ConfigKey: RegisteredConfigVariable] = [:] + + /// The logger used for registration diagnostics. + private static let logger = Logger(subsystem: "DevConfiguration", category: "ConfigVariableReader") + /// Creates a new `ConfigVariableReader` with the specified providers and the default telemetry access reporter. /// @@ -87,6 +94,44 @@ public struct ConfigVariableReader { } +// MARK: - Registration + +extension ConfigVariableReader { + /// Registers a configuration variable with this reader. + /// + /// Registration records the variable's key, default value, secrecy, and metadata in a non-generic form so that the + /// reader can provide information about all registered variables without needing their generic type parameters. + /// + /// Registration is intended to be performed during setup, before the reader is shared with other components. If a + /// variable with the same key has already been registered, the new registration overwrites the previous one, a + /// warning is logged, and an assertion failure is triggered. + /// + /// - Parameter variable: The configuration variable to register. + public mutating func register(_ variable: ConfigVariable) { + let defaultContent: ConfigContent + do { + defaultContent = try variable.content.encode(variable.defaultValue) + } catch { + assertionFailure("Failed to encode default value for config variable '\(variable.key)': \(error)") + Self.logger.error("Failed to encode default value for config variable '\(variable.key)': \(error)") + return + } + + if registeredVariables[variable.key] != nil { + assertionFailure("Config variable '\(variable.key)' is already registered") + Self.logger.error("Config variable '\(variable.key)' is already registered; overwriting") + } + + registeredVariables[variable.key] = RegisteredConfigVariable( + key: variable.key, + defaultContent: defaultContent, + secrecy: variable.secrecy, + metadata: variable.metadata + ) + } +} + + // MARK: - Value Access extension ConfigVariableReader { diff --git a/Sources/DevConfiguration/Core/RegisteredConfigVariable.swift b/Sources/DevConfiguration/Core/RegisteredConfigVariable.swift new file mode 100644 index 0000000..0f16524 --- /dev/null +++ b/Sources/DevConfiguration/Core/RegisteredConfigVariable.swift @@ -0,0 +1,42 @@ +// +// RegisteredConfigVariable.swift +// DevConfiguration +// +// Created by Prachi Gauriar on 3/5/2026. +// + +import Configuration + +/// A non-generic representation of a registered ``ConfigVariable``. +/// +/// `RegisteredConfigVariable` stores the type-erased information from a ``ConfigVariable`` so that registered variables +/// can be stored in homogeneous collections. It captures the variable's key, its default value as a ``ConfigContent``, +/// its secrecy setting, and any attached metadata. +@dynamicMemberLookup +struct RegisteredConfigVariable: Sendable { + /// The configuration key used to look up this variable's value. + let key: ConfigKey + + /// The variable's default value represented as a ``ConfigContent``. + let defaultContent: ConfigContent + + /// Whether this value should be treated as a secret. + let secrecy: ConfigVariableSecrecy + + /// The configuration variable's metadata. + let metadata: ConfigVariableMetadata + + + /// Provides dynamic member lookup access to metadata properties. + /// + /// This subscript enables dot-syntax access to metadata properties, mirroring the access pattern on + /// ``ConfigVariable``. + /// + /// - Parameter keyPath: A keypath to a property on `ConfigVariableMetadata`. + /// - Returns: The value of the metadata property. + subscript( + dynamicMember keyPath: KeyPath + ) -> MetadataValue { + metadata[keyPath: keyPath] + } +} diff --git a/Tests/DevConfigurationTests/Testing Support/MockCodableConfig.swift b/Tests/DevConfigurationTests/Testing Support/MockCodableConfig.swift new file mode 100644 index 0000000..e63cd76 --- /dev/null +++ b/Tests/DevConfigurationTests/Testing Support/MockCodableConfig.swift @@ -0,0 +1,11 @@ +// +// MockCodableConfig.swift +// DevConfiguration +// +// Created by Prachi Gauriar on 3/5/2026. +// + +struct MockCodableConfig: Codable, Hashable, Sendable { + let variant: String + let count: Int +} diff --git a/Tests/DevConfigurationTests/Testing Support/MockConfigIntValue.swift b/Tests/DevConfigurationTests/Testing Support/MockConfigIntValue.swift new file mode 100644 index 0000000..5e1607b --- /dev/null +++ b/Tests/DevConfigurationTests/Testing Support/MockConfigIntValue.swift @@ -0,0 +1,18 @@ +// +// MockConfigIntValue.swift +// DevConfiguration +// +// Created by Prachi Gauriar on 3/5/2026. +// + +import Configuration + +struct MockConfigIntValue: ExpressibleByConfigInt, Hashable, Sendable { + let intValue: Int + var configInt: Int { intValue } + var description: String { "\(intValue)" } + + init?(configInt: Int) { + self.intValue = configInt + } +} diff --git a/Tests/DevConfigurationTests/Testing Support/MockConfigStringValue.swift b/Tests/DevConfigurationTests/Testing Support/MockConfigStringValue.swift new file mode 100644 index 0000000..29fdb06 --- /dev/null +++ b/Tests/DevConfigurationTests/Testing Support/MockConfigStringValue.swift @@ -0,0 +1,17 @@ +// +// MockConfigStringValue.swift +// DevConfiguration +// +// Created by Prachi Gauriar on 3/5/2026. +// + +import Configuration + +struct MockConfigStringValue: ExpressibleByConfigString, Hashable, Sendable { + let stringValue: String + var description: String { stringValue } + + init?(configString: String) { + self.stringValue = configString + } +} diff --git a/Tests/DevConfigurationTests/Testing Support/MockIntEnum.swift b/Tests/DevConfigurationTests/Testing Support/MockIntEnum.swift new file mode 100644 index 0000000..ce918fa --- /dev/null +++ b/Tests/DevConfigurationTests/Testing Support/MockIntEnum.swift @@ -0,0 +1,13 @@ +// +// MockIntEnum.swift +// DevConfiguration +// +// Created by Prachi Gauriar on 3/5/2026. +// + +enum MockIntEnum: Int, CaseIterable, Sendable { + case one = 1 + case two = 2 + case three = 3 + case four = 4 +} diff --git a/Tests/DevConfigurationTests/Testing Support/MockStringEnum.swift b/Tests/DevConfigurationTests/Testing Support/MockStringEnum.swift new file mode 100644 index 0000000..0c62060 --- /dev/null +++ b/Tests/DevConfigurationTests/Testing Support/MockStringEnum.swift @@ -0,0 +1,13 @@ +// +// MockStringEnum.swift +// DevConfiguration +// +// Created by Prachi Gauriar on 3/5/2026. +// + +enum MockStringEnum: String, CaseIterable, Sendable { + case alpha + case bravo + case charlie + case delta +} diff --git a/Tests/DevConfigurationTests/Testing Support/TestConfigVariableMetadataKeys.swift b/Tests/DevConfigurationTests/Testing Support/TestConfigVariableMetadataKeys.swift new file mode 100644 index 0000000..985cd1c --- /dev/null +++ b/Tests/DevConfigurationTests/Testing Support/TestConfigVariableMetadataKeys.swift @@ -0,0 +1,72 @@ +// +// TestConfigVariableMetadataKeys.swift +// DevConfiguration +// +// Created by Prachi Gauriar on 3/5/2026. +// + +@testable import DevConfiguration + +// MARK: - Metadata Keys + +enum MetadataEnum: String, CaseIterable, Sendable { + case valueA + case valueB +} + + +struct EnumMetadataKey: ConfigVariableMetadataKey { + static let defaultValue = MetadataEnum.valueA + static let keyDisplayText = "EnumKey" +} + + +struct IntMetadataKey: ConfigVariableMetadataKey { + static let defaultValue = 0 + static let keyDisplayText = "IntKey" +} + + +struct OptionalEnumMetadataKey: ConfigVariableMetadataKey { + static let defaultValue: MetadataEnum? = nil + static let keyDisplayText = "OptionalEnumKey" +} + + +struct OptionalIntMetadataKey: ConfigVariableMetadataKey { + static let defaultValue: Int? = nil + static let keyDisplayText = "OptionalIntKey" +} + + +struct StringMetadataKey: ConfigVariableMetadataKey { + static let defaultValue: String? = nil + static let keyDisplayText = "StringKey" +} + + +struct TestProjectMetadataKey: ConfigVariableMetadataKey { + static let defaultValue: String? = nil + static let keyDisplayText = "TestProject" +} + + +struct TestTeamMetadataKey: ConfigVariableMetadataKey { + static let defaultValue: String? = nil + static let keyDisplayText = "TestTeam" +} + + +// MARK: - ConfigVariableMetadata Extensions + +extension ConfigVariableMetadata { + var testProject: String? { + get { self[TestProjectMetadataKey.self] } + set { self[TestProjectMetadataKey.self] = newValue } + } + + var testTeam: String? { + get { self[TestTeamMetadataKey.self] } + set { self[TestTeamMetadataKey.self] = newValue } + } +} diff --git a/Tests/DevConfigurationTests/Testing Support/UnencodableValue.swift b/Tests/DevConfigurationTests/Testing Support/UnencodableValue.swift new file mode 100644 index 0000000..27a40bc --- /dev/null +++ b/Tests/DevConfigurationTests/Testing Support/UnencodableValue.swift @@ -0,0 +1,8 @@ +// +// UnencodableValue.swift +// DevConfiguration +// +// Created by Prachi Gauriar on 3/5/2026. +// + +struct UnencodableValue: Sendable {} diff --git a/Tests/DevConfigurationTests/Unit Tests/Access Reporting/ConfigVariableDecodingFailedEventTests.swift b/Tests/DevConfigurationTests/Unit Tests/Access Reporting/ConfigVariableDecodingFailedEventTests.swift new file mode 100644 index 0000000..2bc43b0 --- /dev/null +++ b/Tests/DevConfigurationTests/Unit Tests/Access Reporting/ConfigVariableDecodingFailedEventTests.swift @@ -0,0 +1,32 @@ +// +// ConfigVariableDecodingFailedEventTests.swift +// DevConfiguration +// +// Created by Prachi Gauriar on 3/5/2026. +// + +import Configuration +import DevConfiguration +import DevTesting +import Testing + +struct ConfigVariableDecodingFailedEventTests: RandomValueGenerating { + var randomNumberGenerator = makeRandomNumberGenerator() + + + @Test + mutating func initStoresParameters() { + // set up + let key = AbsoluteConfigKey(randomConfigKey()) + let targetType: Any.Type = String.self + let error = MockError(id: randomAlphanumericString()) + + // exercise + let event = ConfigVariableDecodingFailedEvent(key: key, targetType: targetType, error: error) + + // expect + #expect(event.key == key) + #expect(event.targetType is String.Type) + #expect(event.error as? MockError == error) + } +} diff --git a/Tests/DevConfigurationTests/Unit Tests/Core/CodableValueRepresentationEncodeTests.swift b/Tests/DevConfigurationTests/Unit Tests/Core/CodableValueRepresentationEncodeTests.swift new file mode 100644 index 0000000..5038834 --- /dev/null +++ b/Tests/DevConfigurationTests/Unit Tests/Core/CodableValueRepresentationEncodeTests.swift @@ -0,0 +1,73 @@ +// +// CodableValueRepresentationEncodeTests.swift +// DevConfiguration +// +// Created by Prachi Gauriar on 3/5/2026. +// + +import Configuration +import DevTesting +import Foundation +import Testing + +@testable import DevConfiguration + +struct CodableValueRepresentationEncodeTests: RandomValueGenerating { + var randomNumberGenerator = makeRandomNumberGenerator() + + + @Test + mutating func encodeToContentWithStringRepresentation() throws { + // set up + let string = randomAlphanumericString() + let data = string.data(using: .utf8)! + + // exercise + let content = try CodableValueRepresentation.string().encodeToContent(data) + + // expect + #expect(content == .string(string)) + } + + + @Test + mutating func encodeToContentWithDataRepresentation() throws { + // set up + let bytes = randomBytes() + let data = Data(bytes) + + // exercise + let content = try CodableValueRepresentation.data.encodeToContent(data) + + // expect + #expect(content == .bytes(bytes)) + } + + + @Test + func encodeToContentThrowsStringEncodingErrorForInvalidEncoding() throws { + // set up — create data that is invalid for ASCII encoding (bytes > 127) + let data = Data([0xFF, 0xFE]) + + // exercise and expect + #expect(throws: StringEncodingError.self) { + try CodableValueRepresentation.string(encoding: .ascii).encodeToContent(data) + } + } + + + @Test + func stringEncodingErrorContainsEncoding() throws { + // set up + let data = Data([0xFF, 0xFE]) + + // exercise + do { + _ = try CodableValueRepresentation.string(encoding: .ascii).encodeToContent(data) + Issue.record("Expected StringEncodingError to be thrown") + } catch let error as StringEncodingError { + // expect + #expect(error.encoding == .ascii) + } + } +} diff --git a/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableContentEncodeTests.swift b/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableContentEncodeTests.swift new file mode 100644 index 0000000..6599066 --- /dev/null +++ b/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableContentEncodeTests.swift @@ -0,0 +1,324 @@ +// +// ConfigVariableContentEncodeTests.swift +// DevConfiguration +// +// Created by Prachi Gauriar on 3/5/2026. +// + +import Configuration +import DevTesting +import Foundation +import Testing + +@testable import DevConfiguration + +struct ConfigVariableContentEncodeTests: RandomValueGenerating { + var randomNumberGenerator = makeRandomNumberGenerator() + + + // MARK: - Primitive Content + + @Test + mutating func encodeBool() throws { + // set up + let value = randomBool() + + // exercise + let content = try ConfigVariableContent.bool.encode(value) + + // expect + #expect(content == .bool(value)) + } + + + @Test + mutating func encodeBoolArray() throws { + // set up + let value = randomBoolArray() + + // exercise + let content = try ConfigVariableContent<[Bool]>.boolArray.encode(value) + + // expect + #expect(content == .boolArray(value)) + } + + + @Test + mutating func encodeFloat64() throws { + // set up + let value = randomFloat64(in: -100_000 ... 100_000) + + // exercise + let content = try ConfigVariableContent.float64.encode(value) + + // expect + #expect(content == .double(value)) + } + + + @Test + mutating func encodeFloat64Array() throws { + // set up + let value = randomFloat64Array() + + // exercise + let content = try ConfigVariableContent<[Float64]>.float64Array.encode(value) + + // expect + #expect(content == .doubleArray(value)) + } + + + @Test + mutating func encodeInt() throws { + // set up + let value = randomInt(in: .min ... .max) + + // exercise + let content = try ConfigVariableContent.int.encode(value) + + // expect + #expect(content == .int(value)) + } + + + @Test + mutating func encodeIntArray() throws { + // set up + let value = randomIntArray() + + // exercise + let content = try ConfigVariableContent<[Int]>.intArray.encode(value) + + // expect + #expect(content == .intArray(value)) + } + + + @Test + mutating func encodeString() throws { + // set up + let value = randomAlphanumericString() + + // exercise + let content = try ConfigVariableContent.string.encode(value) + + // expect + #expect(content == .string(value)) + } + + + @Test + mutating func encodeStringArray() throws { + // set up + let value = randomStringArray() + + // exercise + let content = try ConfigVariableContent<[String]>.stringArray.encode(value) + + // expect + #expect(content == .stringArray(value)) + } + + + @Test + mutating func encodeBytes() throws { + // set up + let value = randomBytes() + + // exercise + let content = try ConfigVariableContent<[UInt8]>.bytes.encode(value) + + // expect + #expect(content == .bytes(value)) + } + + + @Test + mutating func encodeByteChunkArray() throws { + // set up + let value = randomByteChunkArray() + + // exercise + let content = try ConfigVariableContent<[[UInt8]]>.byteChunkArray.encode(value) + + // expect + #expect(content == .byteChunkArray(value)) + } + + + // MARK: - String-Convertible Content + + @Test + mutating func encodeRawRepresentableString() throws { + // set up + let value = MockStringEnum.allCases.randomElement(using: &randomNumberGenerator)! + + // exercise + let content = try ConfigVariableContent.rawRepresentableString().encode(value) + + // expect + #expect(content == .string(value.rawValue)) + } + + + @Test + mutating func encodeRawRepresentableStringArray() throws { + // set up + let value = Array(count: randomInt(in: 1 ... 5)) { + MockStringEnum.allCases.randomElement(using: &randomNumberGenerator)! + } + + // exercise + let content = try ConfigVariableContent<[MockStringEnum]>.rawRepresentableStringArray().encode(value) + + // expect + #expect(content == .stringArray(value.map(\.rawValue))) + } + + + @Test + mutating func encodeExpressibleByConfigString() throws { + // set up + let value = MockConfigStringValue(configString: randomAlphanumericString())! + + // exercise + let content = try ConfigVariableContent.expressibleByConfigString().encode(value) + + // expect + #expect(content == .string(value.description)) + } + + + @Test + mutating func encodeExpressibleByConfigStringArray() throws { + // set up + let value = Array(count: randomInt(in: 1 ... 5)) { + MockConfigStringValue(configString: randomAlphanumericString())! + } + + // exercise + let content = + try ConfigVariableContent<[MockConfigStringValue]>.expressibleByConfigStringArray().encode(value) + + // expect + #expect(content == .stringArray(value.map(\.description))) + } + + + // MARK: - Int-Convertible Content + + @Test + mutating func encodeRawRepresentableInt() throws { + // set up + let value = MockIntEnum.allCases.randomElement(using: &randomNumberGenerator)! + + // exercise + let content = try ConfigVariableContent.rawRepresentableInt().encode(value) + + // expect + #expect(content == .int(value.rawValue)) + } + + + @Test + mutating func encodeRawRepresentableIntArray() throws { + // set up + let value = Array(count: randomInt(in: 1 ... 5)) { + MockIntEnum.allCases.randomElement(using: &randomNumberGenerator)! + } + + // exercise + let content = try ConfigVariableContent<[MockIntEnum]>.rawRepresentableIntArray().encode(value) + + // expect + #expect(content == .intArray(value.map(\.rawValue))) + } + + + @Test + mutating func encodeExpressibleByConfigInt() throws { + // set up + let value = MockConfigIntValue(configInt: randomInt(in: .min ... .max))! + + // exercise + let content = try ConfigVariableContent.expressibleByConfigInt().encode(value) + + // expect + #expect(content == .int(value.configInt)) + } + + + @Test + mutating func encodeExpressibleByConfigIntArray() throws { + // set up + let value = Array(count: randomInt(in: 1 ... 5)) { + MockConfigIntValue(configInt: randomInt(in: .min ... .max))! + } + + // exercise + let content = + try ConfigVariableContent<[MockConfigIntValue]>.expressibleByConfigIntArray().encode(value) + + // expect + #expect(content == .intArray(value.map(\.configInt))) + } + + + // MARK: - Codable Content + + @Test + mutating func encodeJSONWithStringRepresentation() throws { + // set up + let value = MockCodableConfig(variant: randomAlphanumericString(), count: randomInt(in: 0 ... 1000)) + + // exercise + let content = try ConfigVariableContent.json(representation: .string()).encode(value) + + // expect — decode the encoded string back to verify round-trip correctness + guard case .string(let jsonString) = content else { + Issue.record("Expected .string content, got \(content)") + return + } + let decoded = try JSONDecoder().decode(MockCodableConfig.self, from: Data(jsonString.utf8)) + #expect(decoded == value) + } + + + @Test + mutating func encodeJSONWithDataRepresentation() throws { + // set up + let value = MockCodableConfig(variant: randomAlphanumericString(), count: randomInt(in: 0 ... 1000)) + + // exercise + let content = try ConfigVariableContent.json(representation: .data).encode(value) + + // expect — decode the encoded bytes back to verify round-trip correctness + guard case .bytes(let bytes) = content else { + Issue.record("Expected .bytes content, got \(content)") + return + } + let decoded = try JSONDecoder().decode(MockCodableConfig.self, from: Data(bytes)) + #expect(decoded == value) + } + + + @Test + mutating func encodePropertyListWithExplicitEncoder() throws { + // set up + let value = MockCodableConfig(variant: randomAlphanumericString(), count: randomInt(in: 0 ... 1000)) + let encoder = PropertyListEncoder() + + // exercise + let content = try ConfigVariableContent.propertyList(encoder: encoder).encode(value) + + // expect — decode the encoded bytes back to verify round-trip correctness + guard case .bytes(let bytes) = content else { + Issue.record("Expected .bytes content, got \(content)") + return + } + let decoded = try PropertyListDecoder().decode(MockCodableConfig.self, from: Data(bytes)) + #expect(decoded == value) + } +} diff --git a/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableMetadataTests.swift b/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableMetadataTests.swift index 070d601..1f19464 100644 --- a/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableMetadataTests.swift +++ b/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableMetadataTests.swift @@ -114,41 +114,3 @@ struct ConfigVariableMetadataTests: RandomValueGenerating { #expect(OptionalEnumMetadataKey.displayText(for: nil) == nil) } } - - -// MARK: - Test Metadata Keys - -private enum MetadataEnum: String, CaseIterable, Sendable { - case valueA - case valueB -} - - -private struct EnumMetadataKey: ConfigVariableMetadataKey { - static let defaultValue = MetadataEnum.valueA - static let keyDisplayText = "EnumKey" -} - - -private struct IntMetadataKey: ConfigVariableMetadataKey { - static let defaultValue = 0 - static let keyDisplayText = "IntKey" -} - - -private struct OptionalEnumMetadataKey: ConfigVariableMetadataKey { - static let defaultValue: MetadataEnum? = nil - static let keyDisplayText = "OptionalEnumKey" -} - - -private struct OptionalIntMetadataKey: ConfigVariableMetadataKey { - static let defaultValue: Int? = nil - static let keyDisplayText = "OptionalIntKey" -} - - -private struct StringMetadataKey: ConfigVariableMetadataKey { - static let defaultValue: String? = nil - static let keyDisplayText = "StringKey" -} diff --git a/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReaderArrayTests.swift b/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReaderArrayTests.swift new file mode 100644 index 0000000..dbe5473 --- /dev/null +++ b/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReaderArrayTests.swift @@ -0,0 +1,367 @@ +// +// ConfigVariableReaderArrayTests.swift +// DevConfiguration +// +// Created by Prachi Gauriar on 2/16/26. +// + +import Configuration +import DevFoundation +import DevTesting +import Foundation +import Testing + +@testable import DevConfiguration + +struct ConfigVariableReaderArrayTests: RandomValueGenerating { + var randomNumberGenerator = makeRandomNumberGenerator() + + /// A mutable provider for testing. + let provider = MutableInMemoryProvider(initialValues: [:]) + + /// The event bus for testing event posting. + let eventBus = EventBus() + + /// The reader under test. + lazy var reader: ConfigVariableReader = { + ConfigVariableReader(providers: [provider], eventBus: eventBus) + }() + + /// Sets a value in the provider for the given key with a random `isSecret` flag. + private mutating func setProviderValue(_ content: ConfigContent, forKey key: ConfigKey) { + provider.setValue( + .init(content, isSecret: randomBool()), + forKey: .init(key) + ) + } + + + // MARK: - [Bool] tests + + @Test + mutating func valueForBoolArrayReturnsProviderValue() { + // set up + let key = randomConfigKey() + let expectedValue = randomBoolArray() + let defaultValue = randomBoolArray() + let variable = ConfigVariable<[Bool]>(key: key, defaultValue: defaultValue) + setProviderValue(.boolArray(expectedValue), forKey: key) + + // exercise + let result = reader.value(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func fetchValueForBoolArrayReturnsProviderValue() async throws { + // set up + let key = randomConfigKey() + let expectedValue = randomBoolArray() + let defaultValue = randomBoolArray() + let variable = ConfigVariable<[Bool]>(key: key, defaultValue: defaultValue) + setProviderValue(.boolArray(expectedValue), forKey: key) + + // exercise + let result = try await reader.fetchValue(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func watchValueForBoolArrayReceivesUpdates() async throws { + // set up + let key = randomConfigKey() + let initialValue = randomBoolArray() + let updatedValue = randomBoolArray() + let defaultValue = randomBoolArray() + let isSecret = randomBool() + let provider = provider + let variable = ConfigVariable<[Bool]>(key: key, defaultValue: defaultValue) + setProviderValue(.boolArray(initialValue), forKey: key) + + // exercise and expect + try await reader.watchValue(for: variable) { updates in + var iterator = updates.makeAsyncIterator() + + let value1 = await iterator.next() + #expect(value1 == initialValue) + + provider.setValue( + .init(.boolArray(updatedValue), isSecret: isSecret), + forKey: .init(key) + ) + + let value2 = await iterator.next() + #expect(value2 == updatedValue) + } + } + + + // MARK: - [Int] tests + + @Test + mutating func valueForIntArrayReturnsProviderValue() { + // set up + let key = randomConfigKey() + let expectedValue = randomIntArray() + let defaultValue = randomIntArray() + let variable = ConfigVariable<[Int]>(key: key, defaultValue: defaultValue) + setProviderValue(.intArray(expectedValue), forKey: key) + + // exercise + let result = reader.value(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func fetchValueForIntArrayReturnsProviderValue() async throws { + // set up + let key = randomConfigKey() + let expectedValue = randomIntArray() + let defaultValue = randomIntArray() + let variable = ConfigVariable<[Int]>(key: key, defaultValue: defaultValue) + setProviderValue(.intArray(expectedValue), forKey: key) + + // exercise + let result = try await reader.fetchValue(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func watchValueForIntArrayReceivesUpdates() async throws { + // set up + let key = randomConfigKey() + let initialValue = randomIntArray() + let updatedValue = randomIntArray() + let defaultValue = randomIntArray() + let isSecret = randomBool() + let provider = provider + let variable = ConfigVariable<[Int]>(key: key, defaultValue: defaultValue) + setProviderValue(.intArray(initialValue), forKey: key) + + // exercise and expect + try await reader.watchValue(for: variable) { updates in + var iterator = updates.makeAsyncIterator() + + let value1 = await iterator.next() + #expect(value1 == initialValue) + + provider.setValue( + .init(.intArray(updatedValue), isSecret: isSecret), + forKey: .init(key) + ) + + let value2 = await iterator.next() + #expect(value2 == updatedValue) + } + } + + + // MARK: - [Float64] tests + + @Test + mutating func valueForFloat64ArrayReturnsProviderValue() { + // set up + let key = randomConfigKey() + let expectedValue = randomFloat64Array() + let defaultValue = randomFloat64Array() + let variable = ConfigVariable<[Float64]>(key: key, defaultValue: defaultValue) + setProviderValue(.doubleArray(expectedValue), forKey: key) + + // exercise + let result = reader.value(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func fetchValueForFloat64ArrayReturnsProviderValue() async throws { + // set up + let key = randomConfigKey() + let expectedValue = randomFloat64Array() + let defaultValue = randomFloat64Array() + let variable = ConfigVariable<[Float64]>(key: key, defaultValue: defaultValue) + setProviderValue(.doubleArray(expectedValue), forKey: key) + + // exercise + let result = try await reader.fetchValue(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func watchValueForFloat64ArrayReceivesUpdates() async throws { + // set up + let key = randomConfigKey() + let initialValue = randomFloat64Array() + let updatedValue = randomFloat64Array() + let defaultValue = randomFloat64Array() + let isSecret = randomBool() + let provider = provider + let variable = ConfigVariable<[Float64]>(key: key, defaultValue: defaultValue) + setProviderValue(.doubleArray(initialValue), forKey: key) + + // exercise and expect + try await reader.watchValue(for: variable) { updates in + var iterator = updates.makeAsyncIterator() + + let value1 = await iterator.next() + #expect(value1 == initialValue) + + provider.setValue( + .init(.doubleArray(updatedValue), isSecret: isSecret), + forKey: .init(key) + ) + + let value2 = await iterator.next() + #expect(value2 == updatedValue) + } + } + + + // MARK: - [String] tests + + @Test + mutating func valueForStringArrayReturnsProviderValue() { + // set up + let key = randomConfigKey() + let expectedValue = randomStringArray() + let defaultValue = randomStringArray() + let variable = ConfigVariable<[String]>(key: key, defaultValue: defaultValue) + setProviderValue(.stringArray(expectedValue), forKey: key) + + // exercise + let result = reader.value(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func fetchValueForStringArrayReturnsProviderValue() async throws { + // set up + let key = randomConfigKey() + let expectedValue = randomStringArray() + let defaultValue = randomStringArray() + let variable = ConfigVariable<[String]>(key: key, defaultValue: defaultValue) + setProviderValue(.stringArray(expectedValue), forKey: key) + + // exercise + let result = try await reader.fetchValue(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func watchValueForStringArrayReceivesUpdates() async throws { + // set up + let key = randomConfigKey() + let initialValue = randomStringArray() + let updatedValue = randomStringArray() + let defaultValue = randomStringArray() + let isSecret = randomBool() + let provider = provider + let variable = ConfigVariable<[String]>(key: key, defaultValue: defaultValue) + setProviderValue(.stringArray(initialValue), forKey: key) + + // exercise and expect + try await reader.watchValue(for: variable) { updates in + var iterator = updates.makeAsyncIterator() + + let value1 = await iterator.next() + #expect(value1 == initialValue) + + provider.setValue( + .init(.stringArray(updatedValue), isSecret: isSecret), + forKey: .init(key) + ) + + let value2 = await iterator.next() + #expect(value2 == updatedValue) + } + } + + + // MARK: - [[UInt8]] tests + + @Test + mutating func valueForByteChunkArrayReturnsProviderValue() { + // set up + let key = randomConfigKey() + let expectedValue = randomByteChunkArray() + let defaultValue = randomByteChunkArray() + let variable = ConfigVariable<[[UInt8]]>(key: key, defaultValue: defaultValue) + setProviderValue(.byteChunkArray(expectedValue), forKey: key) + + // exercise + let result = reader.value(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func fetchValueForByteChunkArrayReturnsProviderValue() async throws { + // set up + let key = randomConfigKey() + let expectedValue = randomByteChunkArray() + let defaultValue = randomByteChunkArray() + let variable = ConfigVariable<[[UInt8]]>(key: key, defaultValue: defaultValue) + setProviderValue(.byteChunkArray(expectedValue), forKey: key) + + // exercise + let result = try await reader.fetchValue(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func watchValueForByteChunkArrayReceivesUpdates() async throws { + // set up + let key = randomConfigKey() + let initialValue = randomByteChunkArray() + let updatedValue = randomByteChunkArray() + let defaultValue = randomByteChunkArray() + let isSecret = randomBool() + let provider = provider + let variable = ConfigVariable<[[UInt8]]>(key: key, defaultValue: defaultValue) + setProviderValue(.byteChunkArray(initialValue), forKey: key) + + // exercise and expect + try await reader.watchValue(for: variable) { updates in + var iterator = updates.makeAsyncIterator() + + let value1 = await iterator.next() + #expect(value1 == initialValue) + + provider.setValue( + .init(.byteChunkArray(updatedValue), isSecret: isSecret), + forKey: .init(key) + ) + + let value2 = await iterator.next() + #expect(value2 == updatedValue) + } + } +} diff --git a/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReaderCodableTests.swift b/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReaderCodableTests.swift new file mode 100644 index 0000000..9d011bc --- /dev/null +++ b/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReaderCodableTests.swift @@ -0,0 +1,426 @@ +// +// ConfigVariableReaderCodableTests.swift +// DevConfiguration +// +// Created by Prachi Gauriar on 2/16/26. +// + +import Configuration +import DevFoundation +import DevTesting +import Foundation +import Testing + +@testable import DevConfiguration + +struct ConfigVariableReaderCodableTests: RandomValueGenerating { + var randomNumberGenerator = makeRandomNumberGenerator() + + /// A mutable provider for testing. + let provider = MutableInMemoryProvider(initialValues: [:]) + + /// The event bus for testing event posting. + let eventBus = EventBus() + + /// The reader under test. + lazy var reader: ConfigVariableReader = { + ConfigVariableReader(providers: [provider], eventBus: eventBus) + }() + + /// Sets a value in the provider for the given key with a random `isSecret` flag. + private mutating func setProviderValue(_ content: ConfigContent, forKey key: ConfigKey) { + provider.setValue( + .init(content, isSecret: randomBool()), + forKey: .init(key) + ) + } + + + // MARK: - JSON Codable tests + + @Test + mutating func valueForJSONReturnsProviderValue() { + // set up + let key = randomConfigKey() + let expectedValue = MockCodableConfig(variant: randomAlphanumericString(), count: randomInt(in: 1 ... 100)) + let defaultValue = MockCodableConfig(variant: randomAlphanumericString(), count: randomInt(in: 1 ... 100)) + let variable = ConfigVariable(key: key, defaultValue: defaultValue, content: .json()) + let jsonData = try! JSONEncoder().encode(expectedValue) + let jsonString = String(data: jsonData, encoding: .utf8)! + setProviderValue(.string(jsonString), forKey: key) + + // exercise + let result = reader.value(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func valueForJSONReturnsDefaultWhenKeyNotFound() { + // set up + let key = randomConfigKey() + let defaultValue = MockCodableConfig(variant: randomAlphanumericString(), count: randomInt(in: 1 ... 100)) + let variable = ConfigVariable(key: key, defaultValue: defaultValue, content: .json()) + + // exercise + let result = reader.value(for: variable) + + // expect + #expect(result == defaultValue) + } + + + @Test + mutating func valueForJSONReturnsDefaultWhenDecodingFails() { + // set up + let key = randomConfigKey() + let defaultValue = MockCodableConfig(variant: randomAlphanumericString(), count: randomInt(in: 1 ... 100)) + let variable = ConfigVariable(key: key, defaultValue: defaultValue, content: .json()) + setProviderValue(.string("not valid json"), forKey: key) + + // exercise + let result = reader.value(for: variable) + + // expect + #expect(result == defaultValue) + } + + + @Test + mutating func valueForJSONPostsDecodingFailedEventWhenDecodingFails() async throws { + // set up + let observer = ContextualBusEventObserver(context: ()) + eventBus.addObserver(observer) + + let key = randomConfigKey() + let defaultValue = MockCodableConfig(variant: randomAlphanumericString(), count: randomInt(in: 1 ... 100)) + let variable = ConfigVariable(key: key, defaultValue: defaultValue, content: .json()) + setProviderValue(.string("not valid json"), forKey: key) + + let (eventStream, continuation) = AsyncStream.makeStream() + observer.addHandler(for: ConfigVariableDecodingFailedEvent.self) { (event, _) in + continuation.yield(event) + } + + // exercise + _ = reader.value(for: variable) + + // expect + let postedEvent = try #require(await eventStream.first { _ in true }) + #expect(postedEvent.key == AbsoluteConfigKey(variable.key)) + #expect(postedEvent.targetType is MockCodableConfig.Type) + } + + + @Test + mutating func fetchValueForJSONReturnsProviderValue() async throws { + // set up + let key = randomConfigKey() + let expectedValue = MockCodableConfig(variant: randomAlphanumericString(), count: randomInt(in: 1 ... 100)) + let defaultValue = MockCodableConfig(variant: randomAlphanumericString(), count: randomInt(in: 1 ... 100)) + let variable = ConfigVariable(key: key, defaultValue: defaultValue, content: .json()) + let jsonData = try! JSONEncoder().encode(expectedValue) + let jsonString = String(data: jsonData, encoding: .utf8)! + setProviderValue(.string(jsonString), forKey: key) + + // exercise + let result = try await reader.fetchValue(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func fetchValueForJSONReturnsDefaultWhenKeyNotFound() async throws { + // set up + let key = randomConfigKey() + let defaultValue = MockCodableConfig(variant: randomAlphanumericString(), count: randomInt(in: 1 ... 100)) + let variable = ConfigVariable(key: key, defaultValue: defaultValue, content: .json()) + + // exercise + let result = try await reader.fetchValue(for: variable) + + // expect + #expect(result == defaultValue) + } + + + @Test + mutating func watchValueForJSONReceivesUpdates() async throws { + // set up + let key = randomConfigKey() + let initialValue = MockCodableConfig(variant: randomAlphanumericString(), count: randomInt(in: 1 ... 100)) + let updatedValue = MockCodableConfig(variant: randomAlphanumericString(), count: randomInt(in: 1 ... 100)) + let defaultValue = MockCodableConfig(variant: randomAlphanumericString(), count: randomInt(in: 1 ... 100)) + let isSecret = randomBool() + let provider = provider + let variable = ConfigVariable(key: key, defaultValue: defaultValue, content: .json()) + let encoder = JSONEncoder() + let initialJSON = String(data: try! encoder.encode(initialValue), encoding: .utf8)! + let updatedJSON = String(data: try! encoder.encode(updatedValue), encoding: .utf8)! + setProviderValue(.string(initialJSON), forKey: key) + + // exercise and expect + try await reader.watchValue(for: variable) { updates in + var iterator = updates.makeAsyncIterator() + + let value1 = await iterator.next() + #expect(value1 == initialValue) + + provider.setValue( + .init(.string(updatedJSON), isSecret: isSecret), + forKey: .init(key) + ) + + let value2 = await iterator.next() + #expect(value2 == updatedValue) + } + } + + + @Test + mutating func subscriptJSONReturnsProviderValue() { + // set up + let key = randomConfigKey() + let expectedValue = MockCodableConfig(variant: randomAlphanumericString(), count: randomInt(in: 1 ... 100)) + let defaultValue = MockCodableConfig(variant: randomAlphanumericString(), count: randomInt(in: 1 ... 100)) + let variable = ConfigVariable(key: key, defaultValue: defaultValue, content: .json()) + let jsonData = try! JSONEncoder().encode(expectedValue) + let jsonString = String(data: jsonData, encoding: .utf8)! + setProviderValue(.string(jsonString), forKey: key) + + // exercise + let result = reader[variable] + + // expect + #expect(result == expectedValue) + } + + + // MARK: - JSON Codable Error Path Tests + + @Test + mutating func fetchValueForJSONReturnsDefaultWhenDecodingFails() async throws { + // set up + let key = randomConfigKey() + let defaultValue = MockCodableConfig( + variant: randomAlphanumericString(), + count: randomInt(in: 1 ... 100) + ) + let variable = ConfigVariable(key: key, defaultValue: defaultValue, content: .json()) + setProviderValue(.string("not valid json"), forKey: key) + + // exercise + let result = try await reader.fetchValue(for: variable) + + // expect + #expect(result == defaultValue) + } + + + @Test + mutating func fetchValueForJSONPostsDecodingFailedEventWhenDecodingFails() async throws { + // set up + let observer = ContextualBusEventObserver(context: ()) + eventBus.addObserver(observer) + + let key = randomConfigKey() + let defaultValue = MockCodableConfig( + variant: randomAlphanumericString(), + count: randomInt(in: 1 ... 100) + ) + let variable = ConfigVariable(key: key, defaultValue: defaultValue, content: .json()) + setProviderValue(.string("not valid json"), forKey: key) + + let (eventStream, continuation) = AsyncStream.makeStream() + observer.addHandler(for: ConfigVariableDecodingFailedEvent.self) { (event, _) in + continuation.yield(event) + } + + // exercise + _ = try await reader.fetchValue(for: variable) + + // expect + let postedEvent = try #require(await eventStream.first { _ in true }) + #expect(postedEvent.key == AbsoluteConfigKey(variable.key)) + #expect(postedEvent.targetType is MockCodableConfig.Type) + } + + + @Test + mutating func watchValueForJSONYieldsDefaultWhenDecodingFails() async throws { + // set up + let key = randomConfigKey() + let defaultValue = MockCodableConfig( + variant: randomAlphanumericString(), + count: randomInt(in: 1 ... 100) + ) + let validValue = MockCodableConfig( + variant: randomAlphanumericString(), + count: randomInt(in: 1 ... 100) + ) + let isSecret = randomBool() + let provider = provider + let variable = ConfigVariable(key: key, defaultValue: defaultValue, content: .json()) + let validJSON = String(data: try! JSONEncoder().encode(validValue), encoding: .utf8)! + setProviderValue(.string("not valid json"), forKey: key) + + // exercise and expect + try await reader.watchValue(for: variable) { updates in + var iterator = updates.makeAsyncIterator() + + let value1 = await iterator.next() + #expect(value1 == defaultValue) + + provider.setValue( + .init(.string(validJSON), isSecret: isSecret), + forKey: .init(key) + ) + + let value2 = await iterator.next() + #expect(value2 == validValue) + } + } + + + @Test + mutating func watchValueForJSONYieldsDefaultWhenKeyNotFound() async throws { + // set up + let key = randomConfigKey() + let defaultValue = MockCodableConfig( + variant: randomAlphanumericString(), + count: randomInt(in: 1 ... 100) + ) + let validValue = MockCodableConfig( + variant: randomAlphanumericString(), + count: randomInt(in: 1 ... 100) + ) + let isSecret = randomBool() + let provider = provider + let variable = ConfigVariable(key: key, defaultValue: defaultValue, content: .json()) + let validJSON = String(data: try! JSONEncoder().encode(validValue), encoding: .utf8)! + + // exercise and expect + try await reader.watchValue(for: variable) { updates in + var iterator = updates.makeAsyncIterator() + + let value1 = await iterator.next() + #expect(value1 == defaultValue) + + provider.setValue( + .init(.string(validJSON), isSecret: isSecret), + forKey: .init(key) + ) + + let value2 = await iterator.next() + #expect(value2 == validValue) + } + } + + + // MARK: - Property List Codable Tests + + @Test + mutating func valueForPropertyListReturnsProviderValue() { + // set up + let key = randomConfigKey() + let expectedValue = MockCodableConfig( + variant: randomAlphanumericString(), + count: randomInt(in: 1 ... 100) + ) + let defaultValue = MockCodableConfig( + variant: randomAlphanumericString(), + count: randomInt(in: 1 ... 100) + ) + let variable = ConfigVariable( + key: key, + defaultValue: defaultValue, + content: .propertyList(decoder: PropertyListDecoder()) + ) + let plistData = try! PropertyListEncoder().encode(expectedValue) + setProviderValue(.bytes(Array(plistData)), forKey: key) + + // exercise + let result = reader.value(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func fetchValueForPropertyListReturnsProviderValue() async throws { + // set up + let key = randomConfigKey() + let expectedValue = MockCodableConfig( + variant: randomAlphanumericString(), + count: randomInt(in: 1 ... 100) + ) + let defaultValue = MockCodableConfig( + variant: randomAlphanumericString(), + count: randomInt(in: 1 ... 100) + ) + let variable = ConfigVariable( + key: key, + defaultValue: defaultValue, + content: .propertyList(decoder: PropertyListDecoder()) + ) + let plistData = try! PropertyListEncoder().encode(expectedValue) + setProviderValue(.bytes(Array(plistData)), forKey: key) + + // exercise + let result = try await reader.fetchValue(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func watchValueForPropertyListReceivesUpdates() async throws { + // set up + let key = randomConfigKey() + let initialValue = MockCodableConfig( + variant: randomAlphanumericString(), + count: randomInt(in: 1 ... 100) + ) + let updatedValue = MockCodableConfig( + variant: randomAlphanumericString(), + count: randomInt(in: 1 ... 100) + ) + let defaultValue = MockCodableConfig( + variant: randomAlphanumericString(), + count: randomInt(in: 1 ... 100) + ) + let isSecret = randomBool() + let provider = provider + let variable = ConfigVariable( + key: key, + defaultValue: defaultValue, + content: .propertyList(decoder: PropertyListDecoder()) + ) + let encoder = PropertyListEncoder() + let initialPlist = try! encoder.encode(initialValue) + let updatedPlist = try! encoder.encode(updatedValue) + setProviderValue(.bytes(Array(initialPlist)), forKey: key) + + // exercise and expect + try await reader.watchValue(for: variable) { updates in + var iterator = updates.makeAsyncIterator() + + let value1 = await iterator.next() + #expect(value1 == initialValue) + + provider.setValue( + .init(.bytes(Array(updatedPlist)), isSecret: isSecret), + forKey: .init(key) + ) + + let value2 = await iterator.next() + #expect(value2 == updatedValue) + } + } +} diff --git a/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReaderConfigExpressionTests.swift b/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReaderConfigExpressionTests.swift new file mode 100644 index 0000000..a4e296c --- /dev/null +++ b/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReaderConfigExpressionTests.swift @@ -0,0 +1,329 @@ +// +// ConfigVariableReaderConfigExpressionTests.swift +// DevConfiguration +// +// Created by Prachi Gauriar on 2/16/26. +// + +import Configuration +import DevFoundation +import DevTesting +import Foundation +import Testing + +@testable import DevConfiguration + +struct ConfigVariableReaderConfigExpressionTests: RandomValueGenerating { + var randomNumberGenerator = makeRandomNumberGenerator() + + /// A mutable provider for testing. + let provider = MutableInMemoryProvider(initialValues: [:]) + + /// The event bus for testing event posting. + let eventBus = EventBus() + + /// The reader under test. + lazy var reader: ConfigVariableReader = { + ConfigVariableReader(providers: [provider], eventBus: eventBus) + }() + + /// Sets a value in the provider for the given key with a random `isSecret` flag. + private mutating func setProviderValue(_ content: ConfigContent, forKey key: ConfigKey) { + provider.setValue( + .init(content, isSecret: randomBool()), + forKey: .init(key) + ) + } + + + // MARK: - ExpressibleByConfigString tests + + @Test + mutating func valueForExpressibleByConfigStringReturnsProviderValue() { + // set up + let key = randomConfigKey() + let expectedValue = MockConfigStringValue(configString: randomAlphanumericString())! + let defaultValue = MockConfigStringValue(configString: randomAlphanumericString())! + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + setProviderValue(.string(expectedValue.description), forKey: key) + + // exercise + let result = reader.value(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func fetchValueForExpressibleByConfigStringReturnsProviderValue() async throws { + // set up + let key = randomConfigKey() + let expectedValue = MockConfigStringValue(configString: randomAlphanumericString())! + let defaultValue = MockConfigStringValue(configString: randomAlphanumericString())! + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + setProviderValue(.string(expectedValue.description), forKey: key) + + // exercise + let result = try await reader.fetchValue(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func watchValueForExpressibleByConfigStringReceivesUpdates() async throws { + // set up + let key = randomConfigKey() + let initialValue = MockConfigStringValue(configString: randomAlphanumericString())! + let updatedValue = MockConfigStringValue(configString: randomAlphanumericString())! + let defaultValue = MockConfigStringValue(configString: randomAlphanumericString())! + let isSecret = randomBool() + let provider = provider + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + setProviderValue(.string(initialValue.description), forKey: key) + + // exercise and expect + try await reader.watchValue(for: variable) { updates in + var iterator = updates.makeAsyncIterator() + + let value1 = await iterator.next() + #expect(value1 == initialValue) + + provider.setValue( + .init(.string(updatedValue.description), isSecret: isSecret), + forKey: .init(key) + ) + + let value2 = await iterator.next() + #expect(value2 == updatedValue) + } + } + + + // MARK: - [ExpressibleByConfigString] tests + + @Test + mutating func valueForExpressibleByConfigStringArrayReturnsProviderValue() { + // set up + let key = randomConfigKey() + let expectedValue = Array(count: randomInt(in: 1 ... 5)) { + MockConfigStringValue(configString: randomAlphanumericString())! + } + let defaultValue = Array(count: randomInt(in: 1 ... 5)) { + MockConfigStringValue(configString: randomAlphanumericString())! + } + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + setProviderValue(.stringArray(expectedValue.map(\.description)), forKey: key) + + // exercise + let result = reader.value(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func fetchValueForExpressibleByConfigStringArrayReturnsProviderValue() async throws { + // set up + let key = randomConfigKey() + let expectedValue = Array(count: randomInt(in: 1 ... 5)) { + MockConfigStringValue(configString: randomAlphanumericString())! + } + let defaultValue = Array(count: randomInt(in: 1 ... 5)) { + MockConfigStringValue(configString: randomAlphanumericString())! + } + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + setProviderValue(.stringArray(expectedValue.map(\.description)), forKey: key) + + // exercise + let result = try await reader.fetchValue(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func watchValueForExpressibleByConfigStringArrayReceivesUpdates() async throws { + // set up + let key = randomConfigKey() + let initialValue = Array(count: randomInt(in: 1 ... 5)) { + MockConfigStringValue(configString: randomAlphanumericString())! + } + let updatedValue = Array(count: randomInt(in: 1 ... 5)) { + MockConfigStringValue(configString: randomAlphanumericString())! + } + let defaultValue = Array(count: randomInt(in: 1 ... 5)) { + MockConfigStringValue(configString: randomAlphanumericString())! + } + let isSecret = randomBool() + let provider = provider + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + setProviderValue(.stringArray(initialValue.map(\.description)), forKey: key) + + // exercise and expect + try await reader.watchValue(for: variable) { updates in + var iterator = updates.makeAsyncIterator() + + let value1 = await iterator.next() + #expect(value1 == initialValue) + + provider.setValue( + .init(.stringArray(updatedValue.map(\.description)), isSecret: isSecret), + forKey: .init(key) + ) + + let value2 = await iterator.next() + #expect(value2 == updatedValue) + } + } + + + // MARK: - ExpressibleByConfigInt tests + + @Test + mutating func valueForExpressibleByConfigIntReturnsProviderValue() { + // set up + let key = randomConfigKey() + let expectedValue = MockConfigIntValue(configInt: randomInt(in: .min ... .max))! + let defaultValue = MockConfigIntValue(configInt: randomInt(in: .min ... .max))! + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + setProviderValue(.int(expectedValue.configInt), forKey: key) + + // exercise + let result = reader.value(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func fetchValueForExpressibleByConfigIntReturnsProviderValue() async throws { + // set up + let key = randomConfigKey() + let expectedValue = MockConfigIntValue(configInt: randomInt(in: .min ... .max))! + let defaultValue = MockConfigIntValue(configInt: randomInt(in: .min ... .max))! + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + setProviderValue(.int(expectedValue.configInt), forKey: key) + + // exercise + let result = try await reader.fetchValue(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func watchValueForExpressibleByConfigIntReceivesUpdates() async throws { + // set up + let key = randomConfigKey() + let initialValue = MockConfigIntValue(configInt: randomInt(in: .min ... .max))! + let updatedValue = MockConfigIntValue(configInt: randomInt(in: .min ... .max))! + let defaultValue = MockConfigIntValue(configInt: randomInt(in: .min ... .max))! + let isSecret = randomBool() + let provider = provider + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + setProviderValue(.int(initialValue.configInt), forKey: key) + + // exercise and expect + try await reader.watchValue(for: variable) { updates in + var iterator = updates.makeAsyncIterator() + + let value1 = await iterator.next() + #expect(value1 == initialValue) + + provider.setValue( + .init(.int(updatedValue.configInt), isSecret: isSecret), + forKey: .init(key) + ) + + let value2 = await iterator.next() + #expect(value2 == updatedValue) + } + } + + + // MARK: - [ExpressibleByConfigInt] tests + + @Test + mutating func valueForExpressibleByConfigIntArrayReturnsProviderValue() { + // set up + let key = randomConfigKey() + let expectedValue = Array(count: randomInt(in: 1 ... 5)) { + MockConfigIntValue(configInt: randomInt(in: .min ... .max))! + } + let defaultValue = Array(count: randomInt(in: 1 ... 5)) { + MockConfigIntValue(configInt: randomInt(in: .min ... .max))! + } + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + setProviderValue(.intArray(expectedValue.map(\.configInt)), forKey: key) + + // exercise + let result = reader.value(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func fetchValueForExpressibleByConfigIntArrayReturnsProviderValue() async throws { + // set up + let key = randomConfigKey() + let expectedValue = Array(count: randomInt(in: 1 ... 5)) { + MockConfigIntValue(configInt: randomInt(in: .min ... .max))! + } + let defaultValue = Array(count: randomInt(in: 1 ... 5)) { + MockConfigIntValue(configInt: randomInt(in: .min ... .max))! + } + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + setProviderValue(.intArray(expectedValue.map(\.configInt)), forKey: key) + + // exercise + let result = try await reader.fetchValue(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func watchValueForExpressibleByConfigIntArrayReceivesUpdates() async throws { + // set up + let key = randomConfigKey() + let initialValue = Array(count: randomInt(in: 1 ... 5)) { + MockConfigIntValue(configInt: randomInt(in: .min ... .max))! + } + let updatedValue = Array(count: randomInt(in: 1 ... 5)) { + MockConfigIntValue(configInt: randomInt(in: .min ... .max))! + } + let defaultValue = Array(count: randomInt(in: 1 ... 5)) { + MockConfigIntValue(configInt: randomInt(in: .min ... .max))! + } + let isSecret = randomBool() + let provider = provider + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + setProviderValue(.intArray(initialValue.map(\.configInt)), forKey: key) + + // exercise and expect + try await reader.watchValue(for: variable) { updates in + var iterator = updates.makeAsyncIterator() + + let value1 = await iterator.next() + #expect(value1 == initialValue) + + provider.setValue( + .init(.intArray(updatedValue.map(\.configInt)), isSecret: isSecret), + forKey: .init(key) + ) + + let value2 = await iterator.next() + #expect(value2 == updatedValue) + } + } +} diff --git a/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReaderDataRepresentationTests.swift b/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReaderDataRepresentationTests.swift new file mode 100644 index 0000000..1de3ed1 --- /dev/null +++ b/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReaderDataRepresentationTests.swift @@ -0,0 +1,141 @@ +// +// ConfigVariableReaderDataRepresentationTests.swift +// DevConfiguration +// +// Created by Prachi Gauriar on 2/16/26. +// + +import Configuration +import DevFoundation +import DevTesting +import Foundation +import Testing + +@testable import DevConfiguration + +struct ConfigVariableReaderDataRepresentationTests: RandomValueGenerating { + var randomNumberGenerator = makeRandomNumberGenerator() + + /// A mutable provider for testing. + let provider = MutableInMemoryProvider(initialValues: [:]) + + /// The event bus for testing event posting. + let eventBus = EventBus() + + /// The reader under test. + lazy var reader: ConfigVariableReader = { + ConfigVariableReader(providers: [provider], eventBus: eventBus) + }() + + /// Sets a value in the provider for the given key with a random `isSecret` flag. + private mutating func setProviderValue(_ content: ConfigContent, forKey key: ConfigKey) { + provider.setValue( + .init(content, isSecret: randomBool()), + forKey: .init(key) + ) + } + + + // MARK: - JSON Data Representation Tests + + @Test + mutating func valueForJSONWithDataRepresentationReturnsProviderValue() { + // set up + let key = randomConfigKey() + let expectedValue = MockCodableConfig( + variant: randomAlphanumericString(), + count: randomInt(in: 1 ... 100) + ) + let defaultValue = MockCodableConfig( + variant: randomAlphanumericString(), + count: randomInt(in: 1 ... 100) + ) + let variable = ConfigVariable( + key: key, + defaultValue: defaultValue, + content: .json(representation: .data) + ) + let jsonData = try! JSONEncoder().encode(expectedValue) + setProviderValue(.bytes(Array(jsonData)), forKey: key) + + // exercise + let result = reader.value(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func fetchValueForJSONWithDataRepresentationReturnsProviderValue() async throws { + // set up + let key = randomConfigKey() + let expectedValue = MockCodableConfig( + variant: randomAlphanumericString(), + count: randomInt(in: 1 ... 100) + ) + let defaultValue = MockCodableConfig( + variant: randomAlphanumericString(), + count: randomInt(in: 1 ... 100) + ) + let variable = ConfigVariable( + key: key, + defaultValue: defaultValue, + content: .json(representation: .data) + ) + let jsonData = try! JSONEncoder().encode(expectedValue) + setProviderValue(.bytes(Array(jsonData)), forKey: key) + + // exercise + let result = try await reader.fetchValue(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func watchValueForJSONWithDataRepresentationReceivesUpdates() async throws { + // set up + let key = randomConfigKey() + let initialValue = MockCodableConfig( + variant: randomAlphanumericString(), + count: randomInt(in: 1 ... 100) + ) + let updatedValue = MockCodableConfig( + variant: randomAlphanumericString(), + count: randomInt(in: 1 ... 100) + ) + let defaultValue = MockCodableConfig( + variant: randomAlphanumericString(), + count: randomInt(in: 1 ... 100) + ) + let isSecret = randomBool() + let provider = provider + let variable = ConfigVariable( + key: key, + defaultValue: defaultValue, + content: .json(representation: .data) + ) + let encoder = JSONEncoder() + let initialJSON = try! encoder.encode(initialValue) + let updatedJSON = try! encoder.encode(updatedValue) + setProviderValue(.bytes(Array(initialJSON)), forKey: key) + + // exercise and expect + try await reader.watchValue(for: variable) { updates in + var iterator = updates.makeAsyncIterator() + + let value1 = await iterator.next() + #expect(value1 == initialValue) + + provider.setValue( + .init(.bytes(Array(updatedJSON)), isSecret: isSecret), + forKey: .init(key) + ) + + let value2 = await iterator.next() + #expect(value2 == updatedValue) + } + } +} diff --git a/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReaderRawRepresentableTests.swift b/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReaderRawRepresentableTests.swift new file mode 100644 index 0000000..3f12480 --- /dev/null +++ b/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReaderRawRepresentableTests.swift @@ -0,0 +1,405 @@ +// +// ConfigVariableReaderRawRepresentableTests.swift +// DevConfiguration +// +// Created by Prachi Gauriar on 2/16/26. +// + +import Configuration +import DevFoundation +import DevTesting +import Foundation +import Testing + +@testable import DevConfiguration + +struct ConfigVariableReaderRawRepresentableTests: RandomValueGenerating { + var randomNumberGenerator = makeRandomNumberGenerator() + + /// A mutable provider for testing. + let provider = MutableInMemoryProvider(initialValues: [:]) + + /// The event bus for testing event posting. + let eventBus = EventBus() + + /// The reader under test. + lazy var reader: ConfigVariableReader = { + ConfigVariableReader(providers: [provider], eventBus: eventBus) + }() + + /// Sets a value in the provider for the given key with a random `isSecret` flag. + private mutating func setProviderValue(_ content: ConfigContent, forKey key: ConfigKey) { + provider.setValue( + .init(content, isSecret: randomBool()), + forKey: .init(key) + ) + } + + + // MARK: - RawRepresentable tests + + @Test + mutating func valueForRawRepresentableStringReturnsProviderValue() { + // set up + let key = randomConfigKey() + let expectedValue = randomCase(of: MockStringEnum.self)! + var defaultValue: MockStringEnum + repeat { defaultValue = randomCase(of: MockStringEnum.self)! } while defaultValue == expectedValue + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + setProviderValue(.string(expectedValue.rawValue), forKey: key) + + // exercise + let result = reader.value(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func valueForRawRepresentableStringReturnsDefaultWhenKeyNotFound() { + // set up + let key = randomConfigKey() + let defaultValue = randomCase(of: MockStringEnum.self)! + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + + // exercise + let result = reader.value(for: variable) + + // expect + #expect(result == defaultValue) + } + + + @Test + mutating func fetchValueForRawRepresentableStringReturnsProviderValue() async throws { + // set up + let key = randomConfigKey() + let expectedValue = randomCase(of: MockStringEnum.self)! + var defaultValue: MockStringEnum + repeat { defaultValue = randomCase(of: MockStringEnum.self)! } while defaultValue == expectedValue + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + setProviderValue(.string(expectedValue.rawValue), forKey: key) + + // exercise + let result = try await reader.fetchValue(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func fetchValueForRawRepresentableStringReturnsDefaultWhenKeyNotFound() async throws { + // set up + let key = randomConfigKey() + let defaultValue = randomCase(of: MockStringEnum.self)! + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + + // exercise + let result = try await reader.fetchValue(for: variable) + + // expect + #expect(result == defaultValue) + } + + + @Test + mutating func watchValueForRawRepresentableStringReceivesUpdates() async throws { + // set up + let key = randomConfigKey() + let initialValue = randomCase(of: MockStringEnum.self)! + var differentValue: MockStringEnum + repeat { differentValue = randomCase(of: MockStringEnum.self)! } while differentValue == initialValue + let updatedValue = differentValue + let defaultValue = randomCase(of: MockStringEnum.self)! + let isSecret = randomBool() + let provider = provider + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + setProviderValue(.string(initialValue.rawValue), forKey: key) + + // exercise and expect + try await reader.watchValue(for: variable) { updates in + var iterator = updates.makeAsyncIterator() + + let value1 = await iterator.next() + #expect(value1 == initialValue) + + provider.setValue( + .init(.string(updatedValue.rawValue), isSecret: isSecret), + forKey: .init(key) + ) + + let value2 = await iterator.next() + #expect(value2 == updatedValue) + } + } + + + @Test + mutating func subscriptRawRepresentableStringReturnsProviderValue() { + // set up + let key = randomConfigKey() + let expectedValue = randomCase(of: MockStringEnum.self)! + var defaultValue: MockStringEnum + repeat { defaultValue = randomCase(of: MockStringEnum.self)! } while defaultValue == expectedValue + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + setProviderValue(.string(expectedValue.rawValue), forKey: key) + + // exercise + let result = reader[variable] + + // expect + #expect(result == expectedValue) + } + + + // MARK: - [RawRepresentable] tests + + @Test + mutating func valueForRawRepresentableStringArrayReturnsProviderValue() { + // set up + let key = randomConfigKey() + let expectedValue = Array(count: randomInt(in: 1 ... 5)) { randomCase(of: MockStringEnum.self)! } + let defaultValue = Array(count: randomInt(in: 1 ... 5)) { randomCase(of: MockStringEnum.self)! } + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + setProviderValue(.stringArray(expectedValue.map(\.rawValue)), forKey: key) + + // exercise + let result = reader.value(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func fetchValueForRawRepresentableStringArrayReturnsProviderValue() async throws { + // set up + let key = randomConfigKey() + let expectedValue = Array(count: randomInt(in: 1 ... 5)) { randomCase(of: MockStringEnum.self)! } + let defaultValue = Array(count: randomInt(in: 1 ... 5)) { randomCase(of: MockStringEnum.self)! } + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + setProviderValue(.stringArray(expectedValue.map(\.rawValue)), forKey: key) + + // exercise + let result = try await reader.fetchValue(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func watchValueForRawRepresentableStringArrayReceivesUpdates() async throws { + // set up + let key = randomConfigKey() + let initialValue = Array(count: randomInt(in: 1 ... 5)) { randomCase(of: MockStringEnum.self)! } + let updatedValue = Array(count: randomInt(in: 1 ... 5)) { randomCase(of: MockStringEnum.self)! } + let defaultValue = Array(count: randomInt(in: 1 ... 5)) { randomCase(of: MockStringEnum.self)! } + let isSecret = randomBool() + let provider = provider + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + setProviderValue(.stringArray(initialValue.map(\.rawValue)), forKey: key) + + // exercise and expect + try await reader.watchValue(for: variable) { updates in + var iterator = updates.makeAsyncIterator() + + let value1 = await iterator.next() + #expect(value1 == initialValue) + + provider.setValue( + .init(.stringArray(updatedValue.map(\.rawValue)), isSecret: isSecret), + forKey: .init(key) + ) + + let value2 = await iterator.next() + #expect(value2 == updatedValue) + } + } + + + // MARK: - RawRepresentable tests + + @Test + mutating func valueForRawRepresentableIntReturnsProviderValue() { + // set up + let key = randomConfigKey() + let expectedValue = randomCase(of: MockIntEnum.self)! + var defaultValue: MockIntEnum + repeat { defaultValue = randomCase(of: MockIntEnum.self)! } while defaultValue == expectedValue + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + setProviderValue(.int(expectedValue.rawValue), forKey: key) + + // exercise + let result = reader.value(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func valueForRawRepresentableIntReturnsDefaultWhenKeyNotFound() { + // set up + let key = randomConfigKey() + let defaultValue = randomCase(of: MockIntEnum.self)! + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + + // exercise + let result = reader.value(for: variable) + + // expect + #expect(result == defaultValue) + } + + + @Test + mutating func fetchValueForRawRepresentableIntReturnsProviderValue() async throws { + // set up + let key = randomConfigKey() + let expectedValue = randomCase(of: MockIntEnum.self)! + var defaultValue: MockIntEnum + repeat { defaultValue = randomCase(of: MockIntEnum.self)! } while defaultValue == expectedValue + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + setProviderValue(.int(expectedValue.rawValue), forKey: key) + + // exercise + let result = try await reader.fetchValue(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func fetchValueForRawRepresentableIntReturnsDefaultWhenKeyNotFound() async throws { + // set up + let key = randomConfigKey() + let defaultValue = randomCase(of: MockIntEnum.self)! + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + + // exercise + let result = try await reader.fetchValue(for: variable) + + // expect + #expect(result == defaultValue) + } + + + @Test + mutating func watchValueForRawRepresentableIntReceivesUpdates() async throws { + // set up + let key = randomConfigKey() + let initialValue = randomCase(of: MockIntEnum.self)! + var differentValue: MockIntEnum + repeat { differentValue = randomCase(of: MockIntEnum.self)! } while differentValue == initialValue + let updatedValue = differentValue + let defaultValue = randomCase(of: MockIntEnum.self)! + let isSecret = randomBool() + let provider = provider + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + setProviderValue(.int(initialValue.rawValue), forKey: key) + + // exercise and expect + try await reader.watchValue(for: variable) { updates in + var iterator = updates.makeAsyncIterator() + + let value1 = await iterator.next() + #expect(value1 == initialValue) + + provider.setValue( + .init(.int(updatedValue.rawValue), isSecret: isSecret), + forKey: .init(key) + ) + + let value2 = await iterator.next() + #expect(value2 == updatedValue) + } + } + + + @Test + mutating func subscriptRawRepresentableIntReturnsProviderValue() { + // set up + let key = randomConfigKey() + let expectedValue = randomCase(of: MockIntEnum.self)! + var defaultValue: MockIntEnum + repeat { defaultValue = randomCase(of: MockIntEnum.self)! } while defaultValue == expectedValue + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + setProviderValue(.int(expectedValue.rawValue), forKey: key) + + // exercise + let result = reader[variable] + + // expect + #expect(result == expectedValue) + } + + + // MARK: - [RawRepresentable] tests + + @Test + mutating func valueForRawRepresentableIntArrayReturnsProviderValue() { + // set up + let key = randomConfigKey() + let expectedValue = Array(count: randomInt(in: 1 ... 5)) { randomCase(of: MockIntEnum.self)! } + let defaultValue = Array(count: randomInt(in: 1 ... 5)) { randomCase(of: MockIntEnum.self)! } + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + setProviderValue(.intArray(expectedValue.map(\.rawValue)), forKey: key) + + // exercise + let result = reader.value(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func fetchValueForRawRepresentableIntArrayReturnsProviderValue() async throws { + // set up + let key = randomConfigKey() + let expectedValue = Array(count: randomInt(in: 1 ... 5)) { randomCase(of: MockIntEnum.self)! } + let defaultValue = Array(count: randomInt(in: 1 ... 5)) { randomCase(of: MockIntEnum.self)! } + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + setProviderValue(.intArray(expectedValue.map(\.rawValue)), forKey: key) + + // exercise + let result = try await reader.fetchValue(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func watchValueForRawRepresentableIntArrayReceivesUpdates() async throws { + // set up + let key = randomConfigKey() + let initialValue = Array(count: randomInt(in: 1 ... 5)) { randomCase(of: MockIntEnum.self)! } + let updatedValue = Array(count: randomInt(in: 1 ... 5)) { randomCase(of: MockIntEnum.self)! } + let defaultValue = Array(count: randomInt(in: 1 ... 5)) { randomCase(of: MockIntEnum.self)! } + let isSecret = randomBool() + let provider = provider + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + setProviderValue(.intArray(initialValue.map(\.rawValue)), forKey: key) + + // exercise and expect + try await reader.watchValue(for: variable) { updates in + var iterator = updates.makeAsyncIterator() + + let value1 = await iterator.next() + #expect(value1 == initialValue) + + provider.setValue( + .init(.intArray(updatedValue.map(\.rawValue)), isSecret: isSecret), + forKey: .init(key) + ) + + let value2 = await iterator.next() + #expect(value2 == updatedValue) + } + } +} diff --git a/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReaderRegistrationTests.swift b/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReaderRegistrationTests.swift new file mode 100644 index 0000000..7ebfd6b --- /dev/null +++ b/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReaderRegistrationTests.swift @@ -0,0 +1,112 @@ +// +// ConfigVariableReaderRegistrationTests.swift +// DevConfiguration +// +// Created by Prachi Gauriar on 3/5/2026. +// + +import Configuration +import DevFoundation +import DevTesting +import Foundation +import Testing + +@testable import DevConfiguration + +struct ConfigVariableReaderRegistrationTests: RandomValueGenerating { + var randomNumberGenerator = makeRandomNumberGenerator() + + + @Test + mutating func registerStoresVariableWithCorrectProperties() { + // set up + var reader = ConfigVariableReader(providers: [InMemoryProvider(values: [:])], eventBus: EventBus()) + + var metadata = ConfigVariableMetadata() + metadata[TestTeamMetadataKey.self] = randomAlphanumericString() + + let key = randomConfigKey() + let defaultValue = randomInt(in: .min ... .max) + let secrecy = randomConfigVariableSecrecy() + let variable = ConfigVariable(key: key, defaultValue: defaultValue, secrecy: secrecy) + .metadata(\.testTeam, metadata[TestTeamMetadataKey.self]) + + // exercise + reader.register(variable) + + // expect + let registered = reader.registeredVariables[key] + #expect(registered != nil) + #expect(registered?.key == key) + #expect(registered?.defaultContent == .int(defaultValue)) + #expect(registered?.secrecy == secrecy) + #expect(registered?.testTeam == metadata[TestTeamMetadataKey.self]) + } + + + @Test + mutating func registerMultipleVariablesStoresAll() { + // set up + var reader = ConfigVariableReader(providers: [InMemoryProvider(values: [:])], eventBus: EventBus()) + let key1 = randomConfigKey() + let key2 = randomConfigKey() + let variable1 = ConfigVariable(key: key1, defaultValue: randomBool()) + let variable2 = ConfigVariable(key: key2, defaultValue: randomAlphanumericString()) + + // exercise + reader.register(variable1) + reader.register(variable2) + + // expect + #expect(reader.registeredVariables.count == 2) + #expect(reader.registeredVariables[key1] != nil) + #expect(reader.registeredVariables[key2] != nil) + } + + + #if os(macOS) + @Test + func registerDuplicateKeyHalts() async { + await #expect(processExitsWith: .failure) { + var reader = ConfigVariableReader( + providers: [InMemoryProvider(values: [:])], + eventBus: EventBus() + ) + let variable1 = ConfigVariable(key: "duplicate.key", defaultValue: 1) + let variable2 = ConfigVariable(key: "duplicate.key", defaultValue: 2) + + reader.register(variable1) + reader.register(variable2) + } + } + + + @Test + func registerWithEncodeFailureHalts() async { + await #expect(processExitsWith: .failure) { + var reader = ConfigVariableReader( + providers: [InMemoryProvider(values: [:])], + eventBus: EventBus() + ) + let variable = ConfigVariable( + key: "encode.failure", + defaultValue: UnencodableValue(), + content: ConfigVariableContent( + isAutoSecret: false, + read: { _, _, _, defaultValue, _, _, _ in defaultValue }, + fetch: { _, _, _, defaultValue, _, _, _ in defaultValue }, + startWatching: { _, _, _, _, _, _, _, _ in }, + encode: { _ in + throw EncodingError.invalidValue( + "", + .init(codingPath: [], debugDescription: "") + ) + } + ) + ) + + reader.register(variable) + } + } + #endif +} diff --git a/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReaderScalarTests.swift b/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReaderScalarTests.swift new file mode 100644 index 0000000..1116bef --- /dev/null +++ b/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReaderScalarTests.swift @@ -0,0 +1,461 @@ +// +// ConfigVariableReaderScalarTests.swift +// DevConfiguration +// +// Created by Prachi Gauriar on 2/16/26. +// + +import Configuration +import DevFoundation +import DevTesting +import Foundation +import Testing + +@testable import DevConfiguration + +struct ConfigVariableReaderScalarTests: RandomValueGenerating { + var randomNumberGenerator = makeRandomNumberGenerator() + + /// A mutable provider for testing. + let provider = MutableInMemoryProvider(initialValues: [:]) + + /// The event bus for testing event posting. + let eventBus = EventBus() + + /// The reader under test. + lazy var reader: ConfigVariableReader = { + ConfigVariableReader(providers: [provider], eventBus: eventBus) + }() + + /// Sets a value in the provider for the given key with a random `isSecret` flag. + private mutating func setProviderValue(_ content: ConfigContent, forKey key: ConfigKey) { + provider.setValue( + .init(content, isSecret: randomBool()), + forKey: .init(key) + ) + } + + + // MARK: - Bool tests + + @Test + mutating func valueForBoolReturnsProviderValue() { + // set up + let key = randomConfigKey() + let expectedValue = randomBool() + let defaultValue = !expectedValue + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + setProviderValue(.bool(expectedValue), forKey: key) + + // exercise + let result = reader.value(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func valueForBoolReturnsDefaultWhenKeyNotFound() { + // set up + let key = randomConfigKey() + let defaultValue = randomBool() + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + + // exercise + let result = reader.value(for: variable) + + // expect + #expect(result == defaultValue) + } + + + @Test + mutating func fetchValueForBoolReturnsProviderValue() async throws { + // set up + let key = randomConfigKey() + let expectedValue = randomBool() + let defaultValue = !expectedValue + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + setProviderValue(.bool(expectedValue), forKey: key) + + // exercise + let result = try await reader.fetchValue(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func fetchValueForBoolReturnsDefaultWhenKeyNotFound() async throws { + // set up + let key = randomConfigKey() + let defaultValue = randomBool() + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + + // exercise + let result = try await reader.fetchValue(for: variable) + + // expect + #expect(result == defaultValue) + } + + + @Test + mutating func watchValueForBoolReceivesUpdates() async throws { + // set up + let key = randomConfigKey() + let initialValue = randomBool() + let updatedValue = !initialValue + let defaultValue = randomBool() + let isSecret = randomBool() + let provider = provider + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + setProviderValue(.bool(initialValue), forKey: key) + + // exercise and expect + try await reader.watchValue(for: variable) { updates in + var iterator = updates.makeAsyncIterator() + + let value1 = await iterator.next() + #expect(value1 == initialValue) + + provider.setValue( + .init(.bool(updatedValue), isSecret: isSecret), + forKey: .init(key) + ) + + let value2 = await iterator.next() + #expect(value2 == updatedValue) + } + } + + + @Test + mutating func subscriptBoolReturnsProviderValue() { + // set up + let key = randomConfigKey() + let expectedValue = randomBool() + let defaultValue = !expectedValue + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + setProviderValue(.bool(expectedValue), forKey: key) + + // exercise + let result = reader[variable] + + // expect + #expect(result == expectedValue) + } + + + // MARK: - Int tests + + @Test + mutating func valueForIntReturnsProviderValue() { + // set up + let key = randomConfigKey() + let expectedValue = randomInt(in: .min ... .max) + let defaultValue = randomInt(in: .min ... .max) + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + setProviderValue(.int(expectedValue), forKey: key) + + // exercise + let result = reader.value(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func fetchValueForIntReturnsProviderValue() async throws { + // set up + let key = randomConfigKey() + let expectedValue = randomInt(in: .min ... .max) + let defaultValue = randomInt(in: .min ... .max) + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + setProviderValue(.int(expectedValue), forKey: key) + + // exercise + let result = try await reader.fetchValue(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func watchValueForIntReceivesUpdates() async throws { + // set up + let key = randomConfigKey() + let initialValue = randomInt(in: .min ... .max) + let updatedValue = randomInt(in: .min ... .max) + let defaultValue = randomInt(in: .min ... .max) + let isSecret = randomBool() + let provider = provider + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + setProviderValue(.int(initialValue), forKey: key) + + // exercise and expect + try await reader.watchValue(for: variable) { updates in + var iterator = updates.makeAsyncIterator() + + let value1 = await iterator.next() + #expect(value1 == initialValue) + + provider.setValue( + .init(.int(updatedValue), isSecret: isSecret), + forKey: .init(key) + ) + + let value2 = await iterator.next() + #expect(value2 == updatedValue) + } + } + + + // MARK: - Float64 tests + + @Test + mutating func valueForFloat64ReturnsProviderValue() { + // set up + let key = randomConfigKey() + let expectedValue = randomFloat64(in: -100_000 ... 100_000) + let defaultValue = randomFloat64(in: -100_000 ... 100_000) + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + setProviderValue(.double(expectedValue), forKey: key) + + // exercise + let result = reader.value(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func fetchValueForFloat64ReturnsProviderValue() async throws { + // set up + let key = randomConfigKey() + let expectedValue = randomFloat64(in: -100_000 ... 100_000) + let defaultValue = randomFloat64(in: -100_000 ... 100_000) + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + setProviderValue(.double(expectedValue), forKey: key) + + // exercise + let result = try await reader.fetchValue(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func watchValueForFloat64ReceivesUpdates() async throws { + // set up + let key = randomConfigKey() + let initialValue = randomFloat64(in: -100_000 ... 100_000) + let updatedValue = randomFloat64(in: -100_000 ... 100_000) + let defaultValue = randomFloat64(in: -100_000 ... 100_000) + let isSecret = randomBool() + let provider = provider + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + setProviderValue(.double(initialValue), forKey: key) + + // exercise and expect + try await reader.watchValue(for: variable) { updates in + var iterator = updates.makeAsyncIterator() + + let value1 = await iterator.next() + #expect(value1 == initialValue) + + provider.setValue( + .init(.double(updatedValue), isSecret: isSecret), + forKey: .init(key) + ) + + let value2 = await iterator.next() + #expect(value2 == updatedValue) + } + } + + + // MARK: - String tests + + @Test + mutating func valueForStringReturnsProviderValue() { + // set up + let key = randomConfigKey() + let expectedValue = randomAlphanumericString() + let defaultValue = randomAlphanumericString() + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + setProviderValue(.string(expectedValue), forKey: key) + + // exercise + let result = reader.value(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func valueForStringReturnsDefaultWhenKeyNotFound() { + // set up + let key = randomConfigKey() + let defaultValue = randomAlphanumericString() + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + + // exercise + let result = reader.value(for: variable) + + // expect + #expect(result == defaultValue) + } + + + @Test + mutating func fetchValueForStringReturnsProviderValue() async throws { + // set up + let key = randomConfigKey() + let expectedValue = randomAlphanumericString() + let defaultValue = randomAlphanumericString() + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + setProviderValue(.string(expectedValue), forKey: key) + + // exercise + let result = try await reader.fetchValue(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func fetchValueForStringReturnsDefaultWhenKeyNotFound() async throws { + // set up + let key = randomConfigKey() + let defaultValue = randomAlphanumericString() + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + + // exercise + let result = try await reader.fetchValue(for: variable) + + // expect + #expect(result == defaultValue) + } + + + @Test + mutating func watchValueForStringReceivesUpdates() async throws { + // set up + let key = randomConfigKey() + let initialValue = randomAlphanumericString() + let updatedValue = randomAlphanumericString() + let defaultValue = randomAlphanumericString() + let isSecret = randomBool() + let provider = provider + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + setProviderValue(.string(initialValue), forKey: key) + + // exercise and expect + try await reader.watchValue(for: variable) { updates in + var iterator = updates.makeAsyncIterator() + + let value1 = await iterator.next() + #expect(value1 == initialValue) + + provider.setValue( + .init(.string(updatedValue), isSecret: isSecret), + forKey: .init(key) + ) + + let value2 = await iterator.next() + #expect(value2 == updatedValue) + } + } + + + @Test + mutating func subscriptStringReturnsProviderValue() { + // set up + let key = randomConfigKey() + let expectedValue = randomAlphanumericString() + let defaultValue = randomAlphanumericString() + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + setProviderValue(.string(expectedValue), forKey: key) + + // exercise + let result = reader[variable] + + // expect + #expect(result == expectedValue) + } + + + // MARK: - [UInt8] tests + + @Test + mutating func valueForBytesReturnsProviderValue() { + // set up + let key = randomConfigKey() + let expectedValue = randomBytes() + let defaultValue = randomBytes() + let variable = ConfigVariable<[UInt8]>(key: key, defaultValue: defaultValue) + setProviderValue(.bytes(expectedValue), forKey: key) + + // exercise + let result = reader.value(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func fetchValueForBytesReturnsProviderValue() async throws { + // set up + let key = randomConfigKey() + let expectedValue = randomBytes() + let defaultValue = randomBytes() + let variable = ConfigVariable<[UInt8]>(key: key, defaultValue: defaultValue) + setProviderValue(.bytes(expectedValue), forKey: key) + + // exercise + let result = try await reader.fetchValue(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func watchValueForBytesReceivesUpdates() async throws { + // set up + let key = randomConfigKey() + let initialValue = randomBytes() + let updatedValue = randomBytes() + let defaultValue = randomBytes() + let isSecret = randomBool() + let provider = provider + let variable = ConfigVariable<[UInt8]>(key: key, defaultValue: defaultValue) + setProviderValue(.bytes(initialValue), forKey: key) + + // exercise and expect + try await reader.watchValue(for: variable) { updates in + var iterator = updates.makeAsyncIterator() + + let value1 = await iterator.next() + #expect(value1 == initialValue) + + provider.setValue( + .init(.bytes(updatedValue), isSecret: isSecret), + forKey: .init(key) + ) + + let value2 = await iterator.next() + #expect(value2 == updatedValue) + } + } +} diff --git a/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReaderTests.swift b/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReaderTests.swift index 812bb09..4d6086f 100644 --- a/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReaderTests.swift +++ b/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReaderTests.swift @@ -27,14 +27,6 @@ struct ConfigVariableReaderTests: RandomValueGenerating { ConfigVariableReader(providers: [provider], eventBus: eventBus) }() - /// Sets a value in the provider for the given key with a random `isSecret` flag. - private mutating func setProviderValue(_ content: ConfigContent, forKey key: ConfigKey) { - provider.setValue( - .init(content, isSecret: randomBool()), - forKey: .init(key) - ) - } - // MARK: - isSecret @@ -132,1936 +124,29 @@ struct ConfigVariableReaderTests: RandomValueGenerating { } - // MARK: - Bool tests - - @Test - mutating func valueForBoolReturnsProviderValue() { - // set up - let key = randomConfigKey() - let expectedValue = randomBool() - let defaultValue = !expectedValue - let variable = ConfigVariable(key: key, defaultValue: defaultValue) - setProviderValue(.bool(expectedValue), forKey: key) - - // exercise - let result = reader.value(for: variable) - - // expect - #expect(result == expectedValue) - } - - - @Test - mutating func valueForBoolReturnsDefaultWhenKeyNotFound() { - // set up - let key = randomConfigKey() - let defaultValue = randomBool() - let variable = ConfigVariable(key: key, defaultValue: defaultValue) - - // exercise - let result = reader.value(for: variable) - - // expect - #expect(result == defaultValue) - } - - - @Test - mutating func fetchValueForBoolReturnsProviderValue() async throws { - // set up - let key = randomConfigKey() - let expectedValue = randomBool() - let defaultValue = !expectedValue - let variable = ConfigVariable(key: key, defaultValue: defaultValue) - setProviderValue(.bool(expectedValue), forKey: key) - - // exercise - let result = try await reader.fetchValue(for: variable) - - // expect - #expect(result == expectedValue) - } - - - @Test - mutating func fetchValueForBoolReturnsDefaultWhenKeyNotFound() async throws { - // set up - let key = randomConfigKey() - let defaultValue = randomBool() - let variable = ConfigVariable(key: key, defaultValue: defaultValue) - - // exercise - let result = try await reader.fetchValue(for: variable) - - // expect - #expect(result == defaultValue) - } - + // MARK: - Event Bus Integration @Test - mutating func watchValueForBoolReceivesUpdates() async throws { + mutating func valuePostsAccessSucceededEventWhenFound() async throws { // set up - let key = randomConfigKey() - let initialValue = randomBool() - let updatedValue = !initialValue - let defaultValue = randomBool() - let isSecret = randomBool() - let provider = provider - let variable = ConfigVariable(key: key, defaultValue: defaultValue) - setProviderValue(.bool(initialValue), forKey: key) - - // exercise and expect - try await reader.watchValue(for: variable) { updates in - var iterator = updates.makeAsyncIterator() - - let value1 = await iterator.next() - #expect(value1 == initialValue) - - provider.setValue( - .init(.bool(updatedValue), isSecret: isSecret), - forKey: .init(key) - ) - - let value2 = await iterator.next() - #expect(value2 == updatedValue) - } - } - + let observer = ContextualBusEventObserver(context: ()) + eventBus.addObserver(observer) - @Test - mutating func subscriptBoolReturnsProviderValue() { - // set up let key = randomConfigKey() let expectedValue = randomBool() - let defaultValue = !expectedValue - let variable = ConfigVariable(key: key, defaultValue: defaultValue) - setProviderValue(.bool(expectedValue), forKey: key) - - // exercise - let result = reader[variable] - - // expect - #expect(result == expectedValue) - } - - - // MARK: - Int tests - - @Test - mutating func valueForIntReturnsProviderValue() { - // set up - let key = randomConfigKey() - let expectedValue = randomInt(in: .min ... .max) - let defaultValue = randomInt(in: .min ... .max) - let variable = ConfigVariable(key: key, defaultValue: defaultValue) - setProviderValue(.int(expectedValue), forKey: key) - - // exercise - let result = reader.value(for: variable) - - // expect - #expect(result == expectedValue) - } - - - @Test - mutating func fetchValueForIntReturnsProviderValue() async throws { - // set up - let key = randomConfigKey() - let expectedValue = randomInt(in: .min ... .max) - let defaultValue = randomInt(in: .min ... .max) - let variable = ConfigVariable(key: key, defaultValue: defaultValue) - setProviderValue(.int(expectedValue), forKey: key) - - // exercise - let result = try await reader.fetchValue(for: variable) - - // expect - #expect(result == expectedValue) - } - - - @Test - mutating func watchValueForIntReceivesUpdates() async throws { - // set up - let key = randomConfigKey() - let initialValue = randomInt(in: .min ... .max) - let updatedValue = randomInt(in: .min ... .max) - let defaultValue = randomInt(in: .min ... .max) - let isSecret = randomBool() - let provider = provider - let variable = ConfigVariable(key: key, defaultValue: defaultValue) - setProviderValue(.int(initialValue), forKey: key) - - // exercise and expect - try await reader.watchValue(for: variable) { updates in - var iterator = updates.makeAsyncIterator() - - let value1 = await iterator.next() - #expect(value1 == initialValue) - - provider.setValue( - .init(.int(updatedValue), isSecret: isSecret), - forKey: .init(key) - ) - - let value2 = await iterator.next() - #expect(value2 == updatedValue) - } - } - - - // MARK: - Float64 tests - - @Test - mutating func valueForFloat64ReturnsProviderValue() { - // set up - let key = randomConfigKey() - let expectedValue = randomFloat64(in: -100_000 ... 100_000) - let defaultValue = randomFloat64(in: -100_000 ... 100_000) - let variable = ConfigVariable(key: key, defaultValue: defaultValue) - setProviderValue(.double(expectedValue), forKey: key) - - // exercise - let result = reader.value(for: variable) - - // expect - #expect(result == expectedValue) - } - - - @Test - mutating func fetchValueForFloat64ReturnsProviderValue() async throws { - // set up - let key = randomConfigKey() - let expectedValue = randomFloat64(in: -100_000 ... 100_000) - let defaultValue = randomFloat64(in: -100_000 ... 100_000) - let variable = ConfigVariable(key: key, defaultValue: defaultValue) - setProviderValue(.double(expectedValue), forKey: key) - - // exercise - let result = try await reader.fetchValue(for: variable) - - // expect - #expect(result == expectedValue) - } - - - @Test - mutating func watchValueForFloat64ReceivesUpdates() async throws { - // set up - let key = randomConfigKey() - let initialValue = randomFloat64(in: -100_000 ... 100_000) - let updatedValue = randomFloat64(in: -100_000 ... 100_000) - let defaultValue = randomFloat64(in: -100_000 ... 100_000) - let isSecret = randomBool() - let provider = provider - let variable = ConfigVariable(key: key, defaultValue: defaultValue) - setProviderValue(.double(initialValue), forKey: key) - - // exercise and expect - try await reader.watchValue(for: variable) { updates in - var iterator = updates.makeAsyncIterator() - - let value1 = await iterator.next() - #expect(value1 == initialValue) - - provider.setValue( - .init(.double(updatedValue), isSecret: isSecret), - forKey: .init(key) - ) - - let value2 = await iterator.next() - #expect(value2 == updatedValue) - } - } - - - // MARK: - [Bool] tests - - @Test - mutating func valueForBoolArrayReturnsProviderValue() { - // set up - let key = randomConfigKey() - let expectedValue = randomBoolArray() - let defaultValue = randomBoolArray() - let variable = ConfigVariable<[Bool]>(key: key, defaultValue: defaultValue) - setProviderValue(.boolArray(expectedValue), forKey: key) - - // exercise - let result = reader.value(for: variable) - - // expect - #expect(result == expectedValue) - } - - - @Test - mutating func fetchValueForBoolArrayReturnsProviderValue() async throws { - // set up - let key = randomConfigKey() - let expectedValue = randomBoolArray() - let defaultValue = randomBoolArray() - let variable = ConfigVariable<[Bool]>(key: key, defaultValue: defaultValue) - setProviderValue(.boolArray(expectedValue), forKey: key) - - // exercise - let result = try await reader.fetchValue(for: variable) - - // expect - #expect(result == expectedValue) - } - - - @Test - mutating func watchValueForBoolArrayReceivesUpdates() async throws { - // set up - let key = randomConfigKey() - let initialValue = randomBoolArray() - let updatedValue = randomBoolArray() - let defaultValue = randomBoolArray() - let isSecret = randomBool() - let provider = provider - let variable = ConfigVariable<[Bool]>(key: key, defaultValue: defaultValue) - setProviderValue(.boolArray(initialValue), forKey: key) - - // exercise and expect - try await reader.watchValue(for: variable) { updates in - var iterator = updates.makeAsyncIterator() - - let value1 = await iterator.next() - #expect(value1 == initialValue) - - provider.setValue( - .init(.boolArray(updatedValue), isSecret: isSecret), - forKey: .init(key) - ) - - let value2 = await iterator.next() - #expect(value2 == updatedValue) - } - } - - - // MARK: - [Int] tests - - @Test - mutating func valueForIntArrayReturnsProviderValue() { - // set up - let key = randomConfigKey() - let expectedValue = randomIntArray() - let defaultValue = randomIntArray() - let variable = ConfigVariable<[Int]>(key: key, defaultValue: defaultValue) - setProviderValue(.intArray(expectedValue), forKey: key) - - // exercise - let result = reader.value(for: variable) - - // expect - #expect(result == expectedValue) - } - - - @Test - mutating func fetchValueForIntArrayReturnsProviderValue() async throws { - // set up - let key = randomConfigKey() - let expectedValue = randomIntArray() - let defaultValue = randomIntArray() - let variable = ConfigVariable<[Int]>(key: key, defaultValue: defaultValue) - setProviderValue(.intArray(expectedValue), forKey: key) - - // exercise - let result = try await reader.fetchValue(for: variable) - - // expect - #expect(result == expectedValue) - } - - - @Test - mutating func watchValueForIntArrayReceivesUpdates() async throws { - // set up - let key = randomConfigKey() - let initialValue = randomIntArray() - let updatedValue = randomIntArray() - let defaultValue = randomIntArray() - let isSecret = randomBool() - let provider = provider - let variable = ConfigVariable<[Int]>(key: key, defaultValue: defaultValue) - setProviderValue(.intArray(initialValue), forKey: key) - - // exercise and expect - try await reader.watchValue(for: variable) { updates in - var iterator = updates.makeAsyncIterator() - - let value1 = await iterator.next() - #expect(value1 == initialValue) - - provider.setValue( - .init(.intArray(updatedValue), isSecret: isSecret), - forKey: .init(key) - ) - - let value2 = await iterator.next() - #expect(value2 == updatedValue) - } - } - - - // MARK: - [Float64] tests - - @Test - mutating func valueForFloat64ArrayReturnsProviderValue() { - // set up - let key = randomConfigKey() - let expectedValue = randomFloat64Array() - let defaultValue = randomFloat64Array() - let variable = ConfigVariable<[Float64]>(key: key, defaultValue: defaultValue) - setProviderValue(.doubleArray(expectedValue), forKey: key) - - // exercise - let result = reader.value(for: variable) - - // expect - #expect(result == expectedValue) - } - - - @Test - mutating func fetchValueForFloat64ArrayReturnsProviderValue() async throws { - // set up - let key = randomConfigKey() - let expectedValue = randomFloat64Array() - let defaultValue = randomFloat64Array() - let variable = ConfigVariable<[Float64]>(key: key, defaultValue: defaultValue) - setProviderValue(.doubleArray(expectedValue), forKey: key) - - // exercise - let result = try await reader.fetchValue(for: variable) - - // expect - #expect(result == expectedValue) - } - - - @Test - mutating func watchValueForFloat64ArrayReceivesUpdates() async throws { - // set up - let key = randomConfigKey() - let initialValue = randomFloat64Array() - let updatedValue = randomFloat64Array() - let defaultValue = randomFloat64Array() - let isSecret = randomBool() - let provider = provider - let variable = ConfigVariable<[Float64]>(key: key, defaultValue: defaultValue) - setProviderValue(.doubleArray(initialValue), forKey: key) - - // exercise and expect - try await reader.watchValue(for: variable) { updates in - var iterator = updates.makeAsyncIterator() - - let value1 = await iterator.next() - #expect(value1 == initialValue) - - provider.setValue( - .init(.doubleArray(updatedValue), isSecret: isSecret), - forKey: .init(key) - ) + let variable = ConfigVariable(key: key, defaultValue: !expectedValue) + provider.setValue( + .init(.bool(expectedValue), isSecret: randomBool()), + forKey: .init(variable.key) + ) - let value2 = await iterator.next() - #expect(value2 == updatedValue) + let (eventStream, continuation) = AsyncStream.makeStream() + observer.addHandler(for: ConfigVariableAccessSucceededEvent.self) { (event, _) in + continuation.yield(event) } - } - - - // MARK: - String tests - - @Test - mutating func valueForStringReturnsProviderValue() { - // set up - let key = randomConfigKey() - let expectedValue = randomAlphanumericString() - let defaultValue = randomAlphanumericString() - let variable = ConfigVariable(key: key, defaultValue: defaultValue) - setProviderValue(.string(expectedValue), forKey: key) - - // exercise - let result = reader.value(for: variable) - - // expect - #expect(result == expectedValue) - } - - - @Test - mutating func valueForStringReturnsDefaultWhenKeyNotFound() { - // set up - let key = randomConfigKey() - let defaultValue = randomAlphanumericString() - let variable = ConfigVariable(key: key, defaultValue: defaultValue) // exercise - let result = reader.value(for: variable) - - // expect - #expect(result == defaultValue) - } - - - @Test - mutating func fetchValueForStringReturnsProviderValue() async throws { - // set up - let key = randomConfigKey() - let expectedValue = randomAlphanumericString() - let defaultValue = randomAlphanumericString() - let variable = ConfigVariable(key: key, defaultValue: defaultValue) - setProviderValue(.string(expectedValue), forKey: key) - - // exercise - let result = try await reader.fetchValue(for: variable) - - // expect - #expect(result == expectedValue) - } - - - @Test - mutating func fetchValueForStringReturnsDefaultWhenKeyNotFound() async throws { - // set up - let key = randomConfigKey() - let defaultValue = randomAlphanumericString() - let variable = ConfigVariable(key: key, defaultValue: defaultValue) - - // exercise - let result = try await reader.fetchValue(for: variable) - - // expect - #expect(result == defaultValue) - } - - - @Test - mutating func watchValueForStringReceivesUpdates() async throws { - // set up - let key = randomConfigKey() - let initialValue = randomAlphanumericString() - let updatedValue = randomAlphanumericString() - let defaultValue = randomAlphanumericString() - let isSecret = randomBool() - let provider = provider - let variable = ConfigVariable(key: key, defaultValue: defaultValue) - setProviderValue(.string(initialValue), forKey: key) - - // exercise and expect - try await reader.watchValue(for: variable) { updates in - var iterator = updates.makeAsyncIterator() - - let value1 = await iterator.next() - #expect(value1 == initialValue) - - provider.setValue( - .init(.string(updatedValue), isSecret: isSecret), - forKey: .init(key) - ) - - let value2 = await iterator.next() - #expect(value2 == updatedValue) - } - } - - - @Test - mutating func subscriptStringReturnsProviderValue() { - // set up - let key = randomConfigKey() - let expectedValue = randomAlphanumericString() - let defaultValue = randomAlphanumericString() - let variable = ConfigVariable(key: key, defaultValue: defaultValue) - setProviderValue(.string(expectedValue), forKey: key) - - // exercise - let result = reader[variable] - - // expect - #expect(result == expectedValue) - } - - - // MARK: - [String] tests - - @Test - mutating func valueForStringArrayReturnsProviderValue() { - // set up - let key = randomConfigKey() - let expectedValue = randomStringArray() - let defaultValue = randomStringArray() - let variable = ConfigVariable<[String]>(key: key, defaultValue: defaultValue) - setProviderValue(.stringArray(expectedValue), forKey: key) - - // exercise - let result = reader.value(for: variable) - - // expect - #expect(result == expectedValue) - } - - - @Test - mutating func fetchValueForStringArrayReturnsProviderValue() async throws { - // set up - let key = randomConfigKey() - let expectedValue = randomStringArray() - let defaultValue = randomStringArray() - let variable = ConfigVariable<[String]>(key: key, defaultValue: defaultValue) - setProviderValue(.stringArray(expectedValue), forKey: key) - - // exercise - let result = try await reader.fetchValue(for: variable) - - // expect - #expect(result == expectedValue) - } - - - @Test - mutating func watchValueForStringArrayReceivesUpdates() async throws { - // set up - let key = randomConfigKey() - let initialValue = randomStringArray() - let updatedValue = randomStringArray() - let defaultValue = randomStringArray() - let isSecret = randomBool() - let provider = provider - let variable = ConfigVariable<[String]>(key: key, defaultValue: defaultValue) - setProviderValue(.stringArray(initialValue), forKey: key) - - // exercise and expect - try await reader.watchValue(for: variable) { updates in - var iterator = updates.makeAsyncIterator() - - let value1 = await iterator.next() - #expect(value1 == initialValue) - - provider.setValue( - .init(.stringArray(updatedValue), isSecret: isSecret), - forKey: .init(key) - ) - - let value2 = await iterator.next() - #expect(value2 == updatedValue) - } - } - - - // MARK: - [UInt8] tests - - @Test - mutating func valueForBytesReturnsProviderValue() { - // set up - let key = randomConfigKey() - let expectedValue = randomBytes() - let defaultValue = randomBytes() - let variable = ConfigVariable<[UInt8]>(key: key, defaultValue: defaultValue) - setProviderValue(.bytes(expectedValue), forKey: key) - - // exercise - let result = reader.value(for: variable) - - // expect - #expect(result == expectedValue) - } - - - @Test - mutating func fetchValueForBytesReturnsProviderValue() async throws { - // set up - let key = randomConfigKey() - let expectedValue = randomBytes() - let defaultValue = randomBytes() - let variable = ConfigVariable<[UInt8]>(key: key, defaultValue: defaultValue) - setProviderValue(.bytes(expectedValue), forKey: key) - - // exercise - let result = try await reader.fetchValue(for: variable) - - // expect - #expect(result == expectedValue) - } - - - @Test - mutating func watchValueForBytesReceivesUpdates() async throws { - // set up - let key = randomConfigKey() - let initialValue = randomBytes() - let updatedValue = randomBytes() - let defaultValue = randomBytes() - let isSecret = randomBool() - let provider = provider - let variable = ConfigVariable<[UInt8]>(key: key, defaultValue: defaultValue) - setProviderValue(.bytes(initialValue), forKey: key) - - // exercise and expect - try await reader.watchValue(for: variable) { updates in - var iterator = updates.makeAsyncIterator() - - let value1 = await iterator.next() - #expect(value1 == initialValue) - - provider.setValue( - .init(.bytes(updatedValue), isSecret: isSecret), - forKey: .init(key) - ) - - let value2 = await iterator.next() - #expect(value2 == updatedValue) - } - } - - - // MARK: - [[UInt8]] tests - - @Test - mutating func valueForByteChunkArrayReturnsProviderValue() { - // set up - let key = randomConfigKey() - let expectedValue = randomByteChunkArray() - let defaultValue = randomByteChunkArray() - let variable = ConfigVariable<[[UInt8]]>(key: key, defaultValue: defaultValue) - setProviderValue(.byteChunkArray(expectedValue), forKey: key) - - // exercise - let result = reader.value(for: variable) - - // expect - #expect(result == expectedValue) - } - - - @Test - mutating func fetchValueForByteChunkArrayReturnsProviderValue() async throws { - // set up - let key = randomConfigKey() - let expectedValue = randomByteChunkArray() - let defaultValue = randomByteChunkArray() - let variable = ConfigVariable<[[UInt8]]>(key: key, defaultValue: defaultValue) - setProviderValue(.byteChunkArray(expectedValue), forKey: key) - - // exercise - let result = try await reader.fetchValue(for: variable) - - // expect - #expect(result == expectedValue) - } - - - @Test - mutating func watchValueForByteChunkArrayReceivesUpdates() async throws { - // set up - let key = randomConfigKey() - let initialValue = randomByteChunkArray() - let updatedValue = randomByteChunkArray() - let defaultValue = randomByteChunkArray() - let isSecret = randomBool() - let provider = provider - let variable = ConfigVariable<[[UInt8]]>(key: key, defaultValue: defaultValue) - setProviderValue(.byteChunkArray(initialValue), forKey: key) - - // exercise and expect - try await reader.watchValue(for: variable) { updates in - var iterator = updates.makeAsyncIterator() - - let value1 = await iterator.next() - #expect(value1 == initialValue) - - provider.setValue( - .init(.byteChunkArray(updatedValue), isSecret: isSecret), - forKey: .init(key) - ) - - let value2 = await iterator.next() - #expect(value2 == updatedValue) - } - } - - - // MARK: - RawRepresentable tests - - @Test - mutating func valueForRawRepresentableStringReturnsProviderValue() { - // set up - let key = randomConfigKey() - let expectedValue = randomCase(of: MockStringEnum.self)! - var defaultValue: MockStringEnum - repeat { defaultValue = randomCase(of: MockStringEnum.self)! } while defaultValue == expectedValue - let variable = ConfigVariable(key: key, defaultValue: defaultValue) - setProviderValue(.string(expectedValue.rawValue), forKey: key) - - // exercise - let result = reader.value(for: variable) - - // expect - #expect(result == expectedValue) - } - - - @Test - mutating func valueForRawRepresentableStringReturnsDefaultWhenKeyNotFound() { - // set up - let key = randomConfigKey() - let defaultValue = randomCase(of: MockStringEnum.self)! - let variable = ConfigVariable(key: key, defaultValue: defaultValue) - - // exercise - let result = reader.value(for: variable) - - // expect - #expect(result == defaultValue) - } - - - @Test - mutating func fetchValueForRawRepresentableStringReturnsProviderValue() async throws { - // set up - let key = randomConfigKey() - let expectedValue = randomCase(of: MockStringEnum.self)! - var defaultValue: MockStringEnum - repeat { defaultValue = randomCase(of: MockStringEnum.self)! } while defaultValue == expectedValue - let variable = ConfigVariable(key: key, defaultValue: defaultValue) - setProviderValue(.string(expectedValue.rawValue), forKey: key) - - // exercise - let result = try await reader.fetchValue(for: variable) - - // expect - #expect(result == expectedValue) - } - - - @Test - mutating func fetchValueForRawRepresentableStringReturnsDefaultWhenKeyNotFound() async throws { - // set up - let key = randomConfigKey() - let defaultValue = randomCase(of: MockStringEnum.self)! - let variable = ConfigVariable(key: key, defaultValue: defaultValue) - - // exercise - let result = try await reader.fetchValue(for: variable) - - // expect - #expect(result == defaultValue) - } - - - @Test - mutating func watchValueForRawRepresentableStringReceivesUpdates() async throws { - // set up - let key = randomConfigKey() - let initialValue = randomCase(of: MockStringEnum.self)! - var differentValue: MockStringEnum - repeat { differentValue = randomCase(of: MockStringEnum.self)! } while differentValue == initialValue - let updatedValue = differentValue - let defaultValue = randomCase(of: MockStringEnum.self)! - let isSecret = randomBool() - let provider = provider - let variable = ConfigVariable(key: key, defaultValue: defaultValue) - setProviderValue(.string(initialValue.rawValue), forKey: key) - - // exercise and expect - try await reader.watchValue(for: variable) { updates in - var iterator = updates.makeAsyncIterator() - - let value1 = await iterator.next() - #expect(value1 == initialValue) - - provider.setValue( - .init(.string(updatedValue.rawValue), isSecret: isSecret), - forKey: .init(key) - ) - - let value2 = await iterator.next() - #expect(value2 == updatedValue) - } - } - - - @Test - mutating func subscriptRawRepresentableStringReturnsProviderValue() { - // set up - let key = randomConfigKey() - let expectedValue = randomCase(of: MockStringEnum.self)! - var defaultValue: MockStringEnum - repeat { defaultValue = randomCase(of: MockStringEnum.self)! } while defaultValue == expectedValue - let variable = ConfigVariable(key: key, defaultValue: defaultValue) - setProviderValue(.string(expectedValue.rawValue), forKey: key) - - // exercise - let result = reader[variable] - - // expect - #expect(result == expectedValue) - } - - - // MARK: - [RawRepresentable] tests - - @Test - mutating func valueForRawRepresentableStringArrayReturnsProviderValue() { - // set up - let key = randomConfigKey() - let expectedValue = Array(count: randomInt(in: 1 ... 5)) { randomCase(of: MockStringEnum.self)! } - let defaultValue = Array(count: randomInt(in: 1 ... 5)) { randomCase(of: MockStringEnum.self)! } - let variable = ConfigVariable(key: key, defaultValue: defaultValue) - setProviderValue(.stringArray(expectedValue.map(\.rawValue)), forKey: key) - - // exercise - let result = reader.value(for: variable) - - // expect - #expect(result == expectedValue) - } - - - @Test - mutating func fetchValueForRawRepresentableStringArrayReturnsProviderValue() async throws { - // set up - let key = randomConfigKey() - let expectedValue = Array(count: randomInt(in: 1 ... 5)) { randomCase(of: MockStringEnum.self)! } - let defaultValue = Array(count: randomInt(in: 1 ... 5)) { randomCase(of: MockStringEnum.self)! } - let variable = ConfigVariable(key: key, defaultValue: defaultValue) - setProviderValue(.stringArray(expectedValue.map(\.rawValue)), forKey: key) - - // exercise - let result = try await reader.fetchValue(for: variable) - - // expect - #expect(result == expectedValue) - } - - - @Test - mutating func watchValueForRawRepresentableStringArrayReceivesUpdates() async throws { - // set up - let key = randomConfigKey() - let initialValue = Array(count: randomInt(in: 1 ... 5)) { randomCase(of: MockStringEnum.self)! } - let updatedValue = Array(count: randomInt(in: 1 ... 5)) { randomCase(of: MockStringEnum.self)! } - let defaultValue = Array(count: randomInt(in: 1 ... 5)) { randomCase(of: MockStringEnum.self)! } - let isSecret = randomBool() - let provider = provider - let variable = ConfigVariable(key: key, defaultValue: defaultValue) - setProviderValue(.stringArray(initialValue.map(\.rawValue)), forKey: key) - - // exercise and expect - try await reader.watchValue(for: variable) { updates in - var iterator = updates.makeAsyncIterator() - - let value1 = await iterator.next() - #expect(value1 == initialValue) - - provider.setValue( - .init(.stringArray(updatedValue.map(\.rawValue)), isSecret: isSecret), - forKey: .init(key) - ) - - let value2 = await iterator.next() - #expect(value2 == updatedValue) - } - } - - - // MARK: - ExpressibleByConfigString tests - - @Test - mutating func valueForExpressibleByConfigStringReturnsProviderValue() { - // set up - let key = randomConfigKey() - let expectedValue = MockConfigStringValue(configString: randomAlphanumericString())! - let defaultValue = MockConfigStringValue(configString: randomAlphanumericString())! - let variable = ConfigVariable(key: key, defaultValue: defaultValue) - setProviderValue(.string(expectedValue.description), forKey: key) - - // exercise - let result = reader.value(for: variable) - - // expect - #expect(result == expectedValue) - } - - - @Test - mutating func fetchValueForExpressibleByConfigStringReturnsProviderValue() async throws { - // set up - let key = randomConfigKey() - let expectedValue = MockConfigStringValue(configString: randomAlphanumericString())! - let defaultValue = MockConfigStringValue(configString: randomAlphanumericString())! - let variable = ConfigVariable(key: key, defaultValue: defaultValue) - setProviderValue(.string(expectedValue.description), forKey: key) - - // exercise - let result = try await reader.fetchValue(for: variable) - - // expect - #expect(result == expectedValue) - } - - - @Test - mutating func watchValueForExpressibleByConfigStringReceivesUpdates() async throws { - // set up - let key = randomConfigKey() - let initialValue = MockConfigStringValue(configString: randomAlphanumericString())! - let updatedValue = MockConfigStringValue(configString: randomAlphanumericString())! - let defaultValue = MockConfigStringValue(configString: randomAlphanumericString())! - let isSecret = randomBool() - let provider = provider - let variable = ConfigVariable(key: key, defaultValue: defaultValue) - setProviderValue(.string(initialValue.description), forKey: key) - - // exercise and expect - try await reader.watchValue(for: variable) { updates in - var iterator = updates.makeAsyncIterator() - - let value1 = await iterator.next() - #expect(value1 == initialValue) - - provider.setValue( - .init(.string(updatedValue.description), isSecret: isSecret), - forKey: .init(key) - ) - - let value2 = await iterator.next() - #expect(value2 == updatedValue) - } - } - - - // MARK: - [ExpressibleByConfigString] tests - - @Test - mutating func valueForExpressibleByConfigStringArrayReturnsProviderValue() { - // set up - let key = randomConfigKey() - let expectedValue = Array(count: randomInt(in: 1 ... 5)) { - MockConfigStringValue(configString: randomAlphanumericString())! - } - let defaultValue = Array(count: randomInt(in: 1 ... 5)) { - MockConfigStringValue(configString: randomAlphanumericString())! - } - let variable = ConfigVariable(key: key, defaultValue: defaultValue) - setProviderValue(.stringArray(expectedValue.map(\.description)), forKey: key) - - // exercise - let result = reader.value(for: variable) - - // expect - #expect(result == expectedValue) - } - - - @Test - mutating func fetchValueForExpressibleByConfigStringArrayReturnsProviderValue() async throws { - // set up - let key = randomConfigKey() - let expectedValue = Array(count: randomInt(in: 1 ... 5)) { - MockConfigStringValue(configString: randomAlphanumericString())! - } - let defaultValue = Array(count: randomInt(in: 1 ... 5)) { - MockConfigStringValue(configString: randomAlphanumericString())! - } - let variable = ConfigVariable(key: key, defaultValue: defaultValue) - setProviderValue(.stringArray(expectedValue.map(\.description)), forKey: key) - - // exercise - let result = try await reader.fetchValue(for: variable) - - // expect - #expect(result == expectedValue) - } - - - @Test - mutating func watchValueForExpressibleByConfigStringArrayReceivesUpdates() async throws { - // set up - let key = randomConfigKey() - let initialValue = Array(count: randomInt(in: 1 ... 5)) { - MockConfigStringValue(configString: randomAlphanumericString())! - } - let updatedValue = Array(count: randomInt(in: 1 ... 5)) { - MockConfigStringValue(configString: randomAlphanumericString())! - } - let defaultValue = Array(count: randomInt(in: 1 ... 5)) { - MockConfigStringValue(configString: randomAlphanumericString())! - } - let isSecret = randomBool() - let provider = provider - let variable = ConfigVariable(key: key, defaultValue: defaultValue) - setProviderValue(.stringArray(initialValue.map(\.description)), forKey: key) - - // exercise and expect - try await reader.watchValue(for: variable) { updates in - var iterator = updates.makeAsyncIterator() - - let value1 = await iterator.next() - #expect(value1 == initialValue) - - provider.setValue( - .init(.stringArray(updatedValue.map(\.description)), isSecret: isSecret), - forKey: .init(key) - ) - - let value2 = await iterator.next() - #expect(value2 == updatedValue) - } - } - - - // MARK: - RawRepresentable tests - - @Test - mutating func valueForRawRepresentableIntReturnsProviderValue() { - // set up - let key = randomConfigKey() - let expectedValue = randomCase(of: MockIntEnum.self)! - var defaultValue: MockIntEnum - repeat { defaultValue = randomCase(of: MockIntEnum.self)! } while defaultValue == expectedValue - let variable = ConfigVariable(key: key, defaultValue: defaultValue) - setProviderValue(.int(expectedValue.rawValue), forKey: key) - - // exercise - let result = reader.value(for: variable) - - // expect - #expect(result == expectedValue) - } - - - @Test - mutating func valueForRawRepresentableIntReturnsDefaultWhenKeyNotFound() { - // set up - let key = randomConfigKey() - let defaultValue = randomCase(of: MockIntEnum.self)! - let variable = ConfigVariable(key: key, defaultValue: defaultValue) - - // exercise - let result = reader.value(for: variable) - - // expect - #expect(result == defaultValue) - } - - - @Test - mutating func fetchValueForRawRepresentableIntReturnsProviderValue() async throws { - // set up - let key = randomConfigKey() - let expectedValue = randomCase(of: MockIntEnum.self)! - var defaultValue: MockIntEnum - repeat { defaultValue = randomCase(of: MockIntEnum.self)! } while defaultValue == expectedValue - let variable = ConfigVariable(key: key, defaultValue: defaultValue) - setProviderValue(.int(expectedValue.rawValue), forKey: key) - - // exercise - let result = try await reader.fetchValue(for: variable) - - // expect - #expect(result == expectedValue) - } - - - @Test - mutating func fetchValueForRawRepresentableIntReturnsDefaultWhenKeyNotFound() async throws { - // set up - let key = randomConfigKey() - let defaultValue = randomCase(of: MockIntEnum.self)! - let variable = ConfigVariable(key: key, defaultValue: defaultValue) - - // exercise - let result = try await reader.fetchValue(for: variable) - - // expect - #expect(result == defaultValue) - } - - - @Test - mutating func watchValueForRawRepresentableIntReceivesUpdates() async throws { - // set up - let key = randomConfigKey() - let initialValue = randomCase(of: MockIntEnum.self)! - var differentValue: MockIntEnum - repeat { differentValue = randomCase(of: MockIntEnum.self)! } while differentValue == initialValue - let updatedValue = differentValue - let defaultValue = randomCase(of: MockIntEnum.self)! - let isSecret = randomBool() - let provider = provider - let variable = ConfigVariable(key: key, defaultValue: defaultValue) - setProviderValue(.int(initialValue.rawValue), forKey: key) - - // exercise and expect - try await reader.watchValue(for: variable) { updates in - var iterator = updates.makeAsyncIterator() - - let value1 = await iterator.next() - #expect(value1 == initialValue) - - provider.setValue( - .init(.int(updatedValue.rawValue), isSecret: isSecret), - forKey: .init(key) - ) - - let value2 = await iterator.next() - #expect(value2 == updatedValue) - } - } - - - @Test - mutating func subscriptRawRepresentableIntReturnsProviderValue() { - // set up - let key = randomConfigKey() - let expectedValue = randomCase(of: MockIntEnum.self)! - var defaultValue: MockIntEnum - repeat { defaultValue = randomCase(of: MockIntEnum.self)! } while defaultValue == expectedValue - let variable = ConfigVariable(key: key, defaultValue: defaultValue) - setProviderValue(.int(expectedValue.rawValue), forKey: key) - - // exercise - let result = reader[variable] - - // expect - #expect(result == expectedValue) - } - - - // MARK: - [RawRepresentable] tests - - @Test - mutating func valueForRawRepresentableIntArrayReturnsProviderValue() { - // set up - let key = randomConfigKey() - let expectedValue = Array(count: randomInt(in: 1 ... 5)) { randomCase(of: MockIntEnum.self)! } - let defaultValue = Array(count: randomInt(in: 1 ... 5)) { randomCase(of: MockIntEnum.self)! } - let variable = ConfigVariable(key: key, defaultValue: defaultValue) - setProviderValue(.intArray(expectedValue.map(\.rawValue)), forKey: key) - - // exercise - let result = reader.value(for: variable) - - // expect - #expect(result == expectedValue) - } - - - @Test - mutating func fetchValueForRawRepresentableIntArrayReturnsProviderValue() async throws { - // set up - let key = randomConfigKey() - let expectedValue = Array(count: randomInt(in: 1 ... 5)) { randomCase(of: MockIntEnum.self)! } - let defaultValue = Array(count: randomInt(in: 1 ... 5)) { randomCase(of: MockIntEnum.self)! } - let variable = ConfigVariable(key: key, defaultValue: defaultValue) - setProviderValue(.intArray(expectedValue.map(\.rawValue)), forKey: key) - - // exercise - let result = try await reader.fetchValue(for: variable) - - // expect - #expect(result == expectedValue) - } - - - @Test - mutating func watchValueForRawRepresentableIntArrayReceivesUpdates() async throws { - // set up - let key = randomConfigKey() - let initialValue = Array(count: randomInt(in: 1 ... 5)) { randomCase(of: MockIntEnum.self)! } - let updatedValue = Array(count: randomInt(in: 1 ... 5)) { randomCase(of: MockIntEnum.self)! } - let defaultValue = Array(count: randomInt(in: 1 ... 5)) { randomCase(of: MockIntEnum.self)! } - let isSecret = randomBool() - let provider = provider - let variable = ConfigVariable(key: key, defaultValue: defaultValue) - setProviderValue(.intArray(initialValue.map(\.rawValue)), forKey: key) - - // exercise and expect - try await reader.watchValue(for: variable) { updates in - var iterator = updates.makeAsyncIterator() - - let value1 = await iterator.next() - #expect(value1 == initialValue) - - provider.setValue( - .init(.intArray(updatedValue.map(\.rawValue)), isSecret: isSecret), - forKey: .init(key) - ) - - let value2 = await iterator.next() - #expect(value2 == updatedValue) - } - } - - - // MARK: - ExpressibleByConfigInt tests - - @Test - mutating func valueForExpressibleByConfigIntReturnsProviderValue() { - // set up - let key = randomConfigKey() - let expectedValue = MockConfigIntValue(configInt: randomInt(in: .min ... .max))! - let defaultValue = MockConfigIntValue(configInt: randomInt(in: .min ... .max))! - let variable = ConfigVariable(key: key, defaultValue: defaultValue) - setProviderValue(.int(expectedValue.configInt), forKey: key) - - // exercise - let result = reader.value(for: variable) - - // expect - #expect(result == expectedValue) - } - - - @Test - mutating func fetchValueForExpressibleByConfigIntReturnsProviderValue() async throws { - // set up - let key = randomConfigKey() - let expectedValue = MockConfigIntValue(configInt: randomInt(in: .min ... .max))! - let defaultValue = MockConfigIntValue(configInt: randomInt(in: .min ... .max))! - let variable = ConfigVariable(key: key, defaultValue: defaultValue) - setProviderValue(.int(expectedValue.configInt), forKey: key) - - // exercise - let result = try await reader.fetchValue(for: variable) - - // expect - #expect(result == expectedValue) - } - - - @Test - mutating func watchValueForExpressibleByConfigIntReceivesUpdates() async throws { - // set up - let key = randomConfigKey() - let initialValue = MockConfigIntValue(configInt: randomInt(in: .min ... .max))! - let updatedValue = MockConfigIntValue(configInt: randomInt(in: .min ... .max))! - let defaultValue = MockConfigIntValue(configInt: randomInt(in: .min ... .max))! - let isSecret = randomBool() - let provider = provider - let variable = ConfigVariable(key: key, defaultValue: defaultValue) - setProviderValue(.int(initialValue.configInt), forKey: key) - - // exercise and expect - try await reader.watchValue(for: variable) { updates in - var iterator = updates.makeAsyncIterator() - - let value1 = await iterator.next() - #expect(value1 == initialValue) - - provider.setValue( - .init(.int(updatedValue.configInt), isSecret: isSecret), - forKey: .init(key) - ) - - let value2 = await iterator.next() - #expect(value2 == updatedValue) - } - } - - - // MARK: - [ExpressibleByConfigInt] tests - - @Test - mutating func valueForExpressibleByConfigIntArrayReturnsProviderValue() { - // set up - let key = randomConfigKey() - let expectedValue = Array(count: randomInt(in: 1 ... 5)) { - MockConfigIntValue(configInt: randomInt(in: .min ... .max))! - } - let defaultValue = Array(count: randomInt(in: 1 ... 5)) { - MockConfigIntValue(configInt: randomInt(in: .min ... .max))! - } - let variable = ConfigVariable(key: key, defaultValue: defaultValue) - setProviderValue(.intArray(expectedValue.map(\.configInt)), forKey: key) - - // exercise - let result = reader.value(for: variable) - - // expect - #expect(result == expectedValue) - } - - - @Test - mutating func fetchValueForExpressibleByConfigIntArrayReturnsProviderValue() async throws { - // set up - let key = randomConfigKey() - let expectedValue = Array(count: randomInt(in: 1 ... 5)) { - MockConfigIntValue(configInt: randomInt(in: .min ... .max))! - } - let defaultValue = Array(count: randomInt(in: 1 ... 5)) { - MockConfigIntValue(configInt: randomInt(in: .min ... .max))! - } - let variable = ConfigVariable(key: key, defaultValue: defaultValue) - setProviderValue(.intArray(expectedValue.map(\.configInt)), forKey: key) - - // exercise - let result = try await reader.fetchValue(for: variable) - - // expect - #expect(result == expectedValue) - } - - - @Test - mutating func watchValueForExpressibleByConfigIntArrayReceivesUpdates() async throws { - // set up - let key = randomConfigKey() - let initialValue = Array(count: randomInt(in: 1 ... 5)) { - MockConfigIntValue(configInt: randomInt(in: .min ... .max))! - } - let updatedValue = Array(count: randomInt(in: 1 ... 5)) { - MockConfigIntValue(configInt: randomInt(in: .min ... .max))! - } - let defaultValue = Array(count: randomInt(in: 1 ... 5)) { - MockConfigIntValue(configInt: randomInt(in: .min ... .max))! - } - let isSecret = randomBool() - let provider = provider - let variable = ConfigVariable(key: key, defaultValue: defaultValue) - setProviderValue(.intArray(initialValue.map(\.configInt)), forKey: key) - - // exercise and expect - try await reader.watchValue(for: variable) { updates in - var iterator = updates.makeAsyncIterator() - - let value1 = await iterator.next() - #expect(value1 == initialValue) - - provider.setValue( - .init(.intArray(updatedValue.map(\.configInt)), isSecret: isSecret), - forKey: .init(key) - ) - - let value2 = await iterator.next() - #expect(value2 == updatedValue) - } - } - - - // MARK: - JSON Codable tests - - @Test - mutating func valueForJSONReturnsProviderValue() { - // set up - let key = randomConfigKey() - let expectedValue = MockCodableConfig(variant: randomAlphanumericString(), count: randomInt(in: 1 ... 100)) - let defaultValue = MockCodableConfig(variant: randomAlphanumericString(), count: randomInt(in: 1 ... 100)) - let variable = ConfigVariable(key: key, defaultValue: defaultValue, content: .json()) - let jsonData = try! JSONEncoder().encode(expectedValue) - let jsonString = String(data: jsonData, encoding: .utf8)! - setProviderValue(.string(jsonString), forKey: key) - - // exercise - let result = reader.value(for: variable) - - // expect - #expect(result == expectedValue) - } - - - @Test - mutating func valueForJSONReturnsDefaultWhenKeyNotFound() { - // set up - let key = randomConfigKey() - let defaultValue = MockCodableConfig(variant: randomAlphanumericString(), count: randomInt(in: 1 ... 100)) - let variable = ConfigVariable(key: key, defaultValue: defaultValue, content: .json()) - - // exercise - let result = reader.value(for: variable) - - // expect - #expect(result == defaultValue) - } - - - @Test - mutating func valueForJSONReturnsDefaultWhenDecodingFails() { - // set up - let key = randomConfigKey() - let defaultValue = MockCodableConfig(variant: randomAlphanumericString(), count: randomInt(in: 1 ... 100)) - let variable = ConfigVariable(key: key, defaultValue: defaultValue, content: .json()) - setProviderValue(.string("not valid json"), forKey: key) - - // exercise - let result = reader.value(for: variable) - - // expect - #expect(result == defaultValue) - } - - - @Test - mutating func valueForJSONPostsDecodingFailedEventWhenDecodingFails() async throws { - // set up - let observer = ContextualBusEventObserver(context: ()) - eventBus.addObserver(observer) - - let key = randomConfigKey() - let defaultValue = MockCodableConfig(variant: randomAlphanumericString(), count: randomInt(in: 1 ... 100)) - let variable = ConfigVariable(key: key, defaultValue: defaultValue, content: .json()) - setProviderValue(.string("not valid json"), forKey: key) - - let (eventStream, continuation) = AsyncStream.makeStream() - observer.addHandler(for: ConfigVariableDecodingFailedEvent.self) { (event, _) in - continuation.yield(event) - } - - // exercise - _ = reader.value(for: variable) - - // expect - let postedEvent = try #require(await eventStream.first { _ in true }) - #expect(postedEvent.key == AbsoluteConfigKey(variable.key)) - #expect(postedEvent.targetType is MockCodableConfig.Type) - } - - - @Test - mutating func fetchValueForJSONReturnsProviderValue() async throws { - // set up - let key = randomConfigKey() - let expectedValue = MockCodableConfig(variant: randomAlphanumericString(), count: randomInt(in: 1 ... 100)) - let defaultValue = MockCodableConfig(variant: randomAlphanumericString(), count: randomInt(in: 1 ... 100)) - let variable = ConfigVariable(key: key, defaultValue: defaultValue, content: .json()) - let jsonData = try! JSONEncoder().encode(expectedValue) - let jsonString = String(data: jsonData, encoding: .utf8)! - setProviderValue(.string(jsonString), forKey: key) - - // exercise - let result = try await reader.fetchValue(for: variable) - - // expect - #expect(result == expectedValue) - } - - - @Test - mutating func fetchValueForJSONReturnsDefaultWhenKeyNotFound() async throws { - // set up - let key = randomConfigKey() - let defaultValue = MockCodableConfig(variant: randomAlphanumericString(), count: randomInt(in: 1 ... 100)) - let variable = ConfigVariable(key: key, defaultValue: defaultValue, content: .json()) - - // exercise - let result = try await reader.fetchValue(for: variable) - - // expect - #expect(result == defaultValue) - } - - - @Test - mutating func watchValueForJSONReceivesUpdates() async throws { - // set up - let key = randomConfigKey() - let initialValue = MockCodableConfig(variant: randomAlphanumericString(), count: randomInt(in: 1 ... 100)) - let updatedValue = MockCodableConfig(variant: randomAlphanumericString(), count: randomInt(in: 1 ... 100)) - let defaultValue = MockCodableConfig(variant: randomAlphanumericString(), count: randomInt(in: 1 ... 100)) - let isSecret = randomBool() - let provider = provider - let variable = ConfigVariable(key: key, defaultValue: defaultValue, content: .json()) - let encoder = JSONEncoder() - let initialJSON = String(data: try! encoder.encode(initialValue), encoding: .utf8)! - let updatedJSON = String(data: try! encoder.encode(updatedValue), encoding: .utf8)! - setProviderValue(.string(initialJSON), forKey: key) - - // exercise and expect - try await reader.watchValue(for: variable) { updates in - var iterator = updates.makeAsyncIterator() - - let value1 = await iterator.next() - #expect(value1 == initialValue) - - provider.setValue( - .init(.string(updatedJSON), isSecret: isSecret), - forKey: .init(key) - ) - - let value2 = await iterator.next() - #expect(value2 == updatedValue) - } - } - - - @Test - mutating func subscriptJSONReturnsProviderValue() { - // set up - let key = randomConfigKey() - let expectedValue = MockCodableConfig(variant: randomAlphanumericString(), count: randomInt(in: 1 ... 100)) - let defaultValue = MockCodableConfig(variant: randomAlphanumericString(), count: randomInt(in: 1 ... 100)) - let variable = ConfigVariable(key: key, defaultValue: defaultValue, content: .json()) - let jsonData = try! JSONEncoder().encode(expectedValue) - let jsonString = String(data: jsonData, encoding: .utf8)! - setProviderValue(.string(jsonString), forKey: key) - - // exercise - let result = reader[variable] - - // expect - #expect(result == expectedValue) - } - - - // MARK: - JSON Codable Error Path Tests - - @Test - mutating func fetchValueForJSONReturnsDefaultWhenDecodingFails() async throws { - // set up - let key = randomConfigKey() - let defaultValue = MockCodableConfig( - variant: randomAlphanumericString(), - count: randomInt(in: 1 ... 100) - ) - let variable = ConfigVariable(key: key, defaultValue: defaultValue, content: .json()) - setProviderValue(.string("not valid json"), forKey: key) - - // exercise - let result = try await reader.fetchValue(for: variable) - - // expect - #expect(result == defaultValue) - } - - - @Test - mutating func fetchValueForJSONPostsDecodingFailedEventWhenDecodingFails() async throws { - // set up - let observer = ContextualBusEventObserver(context: ()) - eventBus.addObserver(observer) - - let key = randomConfigKey() - let defaultValue = MockCodableConfig( - variant: randomAlphanumericString(), - count: randomInt(in: 1 ... 100) - ) - let variable = ConfigVariable(key: key, defaultValue: defaultValue, content: .json()) - setProviderValue(.string("not valid json"), forKey: key) - - let (eventStream, continuation) = AsyncStream.makeStream() - observer.addHandler(for: ConfigVariableDecodingFailedEvent.self) { (event, _) in - continuation.yield(event) - } - - // exercise - _ = try await reader.fetchValue(for: variable) - - // expect - let postedEvent = try #require(await eventStream.first { _ in true }) - #expect(postedEvent.key == AbsoluteConfigKey(variable.key)) - #expect(postedEvent.targetType is MockCodableConfig.Type) - } - - - @Test - mutating func watchValueForJSONYieldsDefaultWhenDecodingFails() async throws { - // set up - let key = randomConfigKey() - let defaultValue = MockCodableConfig( - variant: randomAlphanumericString(), - count: randomInt(in: 1 ... 100) - ) - let validValue = MockCodableConfig( - variant: randomAlphanumericString(), - count: randomInt(in: 1 ... 100) - ) - let isSecret = randomBool() - let provider = provider - let variable = ConfigVariable(key: key, defaultValue: defaultValue, content: .json()) - let validJSON = String(data: try! JSONEncoder().encode(validValue), encoding: .utf8)! - setProviderValue(.string("not valid json"), forKey: key) - - // exercise and expect - try await reader.watchValue(for: variable) { updates in - var iterator = updates.makeAsyncIterator() - - let value1 = await iterator.next() - #expect(value1 == defaultValue) - - provider.setValue( - .init(.string(validJSON), isSecret: isSecret), - forKey: .init(key) - ) - - let value2 = await iterator.next() - #expect(value2 == validValue) - } - } - - - @Test - mutating func watchValueForJSONYieldsDefaultWhenKeyNotFound() async throws { - // set up - let key = randomConfigKey() - let defaultValue = MockCodableConfig( - variant: randomAlphanumericString(), - count: randomInt(in: 1 ... 100) - ) - let validValue = MockCodableConfig( - variant: randomAlphanumericString(), - count: randomInt(in: 1 ... 100) - ) - let isSecret = randomBool() - let provider = provider - let variable = ConfigVariable(key: key, defaultValue: defaultValue, content: .json()) - let validJSON = String(data: try! JSONEncoder().encode(validValue), encoding: .utf8)! - - // exercise and expect - try await reader.watchValue(for: variable) { updates in - var iterator = updates.makeAsyncIterator() - - let value1 = await iterator.next() - #expect(value1 == defaultValue) - - provider.setValue( - .init(.string(validJSON), isSecret: isSecret), - forKey: .init(key) - ) - - let value2 = await iterator.next() - #expect(value2 == validValue) - } - } - - - // MARK: - Property List Codable Tests - - @Test - mutating func valueForPropertyListReturnsProviderValue() { - // set up - let key = randomConfigKey() - let expectedValue = MockCodableConfig( - variant: randomAlphanumericString(), - count: randomInt(in: 1 ... 100) - ) - let defaultValue = MockCodableConfig( - variant: randomAlphanumericString(), - count: randomInt(in: 1 ... 100) - ) - let variable = ConfigVariable( - key: key, - defaultValue: defaultValue, - content: .propertyList(decoder: PropertyListDecoder()) - ) - let plistData = try! PropertyListEncoder().encode(expectedValue) - setProviderValue(.bytes(Array(plistData)), forKey: key) - - // exercise - let result = reader.value(for: variable) - - // expect - #expect(result == expectedValue) - } - - - @Test - mutating func fetchValueForPropertyListReturnsProviderValue() async throws { - // set up - let key = randomConfigKey() - let expectedValue = MockCodableConfig( - variant: randomAlphanumericString(), - count: randomInt(in: 1 ... 100) - ) - let defaultValue = MockCodableConfig( - variant: randomAlphanumericString(), - count: randomInt(in: 1 ... 100) - ) - let variable = ConfigVariable( - key: key, - defaultValue: defaultValue, - content: .propertyList(decoder: PropertyListDecoder()) - ) - let plistData = try! PropertyListEncoder().encode(expectedValue) - setProviderValue(.bytes(Array(plistData)), forKey: key) - - // exercise - let result = try await reader.fetchValue(for: variable) - - // expect - #expect(result == expectedValue) - } - - - @Test - mutating func watchValueForPropertyListReceivesUpdates() async throws { - // set up - let key = randomConfigKey() - let initialValue = MockCodableConfig( - variant: randomAlphanumericString(), - count: randomInt(in: 1 ... 100) - ) - let updatedValue = MockCodableConfig( - variant: randomAlphanumericString(), - count: randomInt(in: 1 ... 100) - ) - let defaultValue = MockCodableConfig( - variant: randomAlphanumericString(), - count: randomInt(in: 1 ... 100) - ) - let isSecret = randomBool() - let provider = provider - let variable = ConfigVariable( - key: key, - defaultValue: defaultValue, - content: .propertyList(decoder: PropertyListDecoder()) - ) - let encoder = PropertyListEncoder() - let initialPlist = try! encoder.encode(initialValue) - let updatedPlist = try! encoder.encode(updatedValue) - setProviderValue(.bytes(Array(initialPlist)), forKey: key) - - // exercise and expect - try await reader.watchValue(for: variable) { updates in - var iterator = updates.makeAsyncIterator() - - let value1 = await iterator.next() - #expect(value1 == initialValue) - - provider.setValue( - .init(.bytes(Array(updatedPlist)), isSecret: isSecret), - forKey: .init(key) - ) - - let value2 = await iterator.next() - #expect(value2 == updatedValue) - } - } - - - // MARK: - JSON Data Representation Tests - - @Test - mutating func valueForJSONWithDataRepresentationReturnsProviderValue() { - // set up - let key = randomConfigKey() - let expectedValue = MockCodableConfig( - variant: randomAlphanumericString(), - count: randomInt(in: 1 ... 100) - ) - let defaultValue = MockCodableConfig( - variant: randomAlphanumericString(), - count: randomInt(in: 1 ... 100) - ) - let variable = ConfigVariable( - key: key, - defaultValue: defaultValue, - content: .json(representation: .data) - ) - let jsonData = try! JSONEncoder().encode(expectedValue) - setProviderValue(.bytes(Array(jsonData)), forKey: key) - - // exercise - let result = reader.value(for: variable) - - // expect - #expect(result == expectedValue) - } - - - @Test - mutating func fetchValueForJSONWithDataRepresentationReturnsProviderValue() async throws { - // set up - let key = randomConfigKey() - let expectedValue = MockCodableConfig( - variant: randomAlphanumericString(), - count: randomInt(in: 1 ... 100) - ) - let defaultValue = MockCodableConfig( - variant: randomAlphanumericString(), - count: randomInt(in: 1 ... 100) - ) - let variable = ConfigVariable( - key: key, - defaultValue: defaultValue, - content: .json(representation: .data) - ) - let jsonData = try! JSONEncoder().encode(expectedValue) - setProviderValue(.bytes(Array(jsonData)), forKey: key) - - // exercise - let result = try await reader.fetchValue(for: variable) - - // expect - #expect(result == expectedValue) - } - - - @Test - mutating func watchValueForJSONWithDataRepresentationReceivesUpdates() async throws { - // set up - let key = randomConfigKey() - let initialValue = MockCodableConfig( - variant: randomAlphanumericString(), - count: randomInt(in: 1 ... 100) - ) - let updatedValue = MockCodableConfig( - variant: randomAlphanumericString(), - count: randomInt(in: 1 ... 100) - ) - let defaultValue = MockCodableConfig( - variant: randomAlphanumericString(), - count: randomInt(in: 1 ... 100) - ) - let isSecret = randomBool() - let provider = provider - let variable = ConfigVariable( - key: key, - defaultValue: defaultValue, - content: .json(representation: .data) - ) - let encoder = JSONEncoder() - let initialJSON = try! encoder.encode(initialValue) - let updatedJSON = try! encoder.encode(updatedValue) - setProviderValue(.bytes(Array(initialJSON)), forKey: key) - - // exercise and expect - try await reader.watchValue(for: variable) { updates in - var iterator = updates.makeAsyncIterator() - - let value1 = await iterator.next() - #expect(value1 == initialValue) - - provider.setValue( - .init(.bytes(Array(updatedJSON)), isSecret: isSecret), - forKey: .init(key) - ) - - let value2 = await iterator.next() - #expect(value2 == updatedValue) - } - } - - - // MARK: - Event Bus Integration - - @Test - mutating func valuePostsAccessSucceededEventWhenFound() async throws { - // set up - let observer = ContextualBusEventObserver(context: ()) - eventBus.addObserver(observer) - - let key = randomConfigKey() - let expectedValue = randomBool() - let variable = ConfigVariable(key: key, defaultValue: !expectedValue) - provider.setValue( - .init(.bool(expectedValue), isSecret: randomBool()), - forKey: .init(variable.key) - ) - - let (eventStream, continuation) = AsyncStream.makeStream() - observer.addHandler(for: ConfigVariableAccessSucceededEvent.self) { (event, _) in - continuation.yield(event) - } - - // exercise - _ = reader.value(for: variable) + _ = reader.value(for: variable) // expect let postedEvent = try #require(await eventStream.first { _ in true }) @@ -2069,56 +154,3 @@ struct ConfigVariableReaderTests: RandomValueGenerating { #expect(postedEvent.value.content == .bool(expectedValue)) } } - - -// MARK: - MockCodableConfig - -private struct MockCodableConfig: Codable, Hashable, Sendable { - let variant: String - let count: Int -} - - -// MARK: - MockStringEnum - -private enum MockStringEnum: String, CaseIterable, Sendable { - case alpha - case bravo - case charlie - case delta -} - - -// MARK: - MockIntEnum - -private enum MockIntEnum: Int, CaseIterable, Sendable { - case one = 1 - case two = 2 - case three = 3 - case four = 4 -} - - -// MARK: - MockConfigStringValue - -private struct MockConfigStringValue: ExpressibleByConfigString, Hashable, Sendable { - let stringValue: String - var description: String { stringValue } - - init?(configString: String) { - self.stringValue = configString - } -} - - -// MARK: - MockConfigIntValue - -private struct MockConfigIntValue: ExpressibleByConfigInt, Hashable, Sendable { - let intValue: Int - var configInt: Int { intValue } - var description: String { "\(intValue)" } - - init?(configInt: Int) { - self.intValue = configInt - } -} diff --git a/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableTests.swift b/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableTests.swift index 9b0f5c4..89571a6 100644 --- a/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableTests.swift +++ b/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableTests.swift @@ -82,30 +82,3 @@ struct ConfigVariableTests: RandomValueGenerating { #expect(variable.testProject == project) } } - - -// MARK: - Test Metadata Keys - -private struct TestProjectMetadataKey: ConfigVariableMetadataKey { - static let defaultValue: String? = nil - static let keyDisplayText = "TestProject" -} - - -private struct TestTeamMetadataKey: ConfigVariableMetadataKey { - static let defaultValue: String? = nil - static let keyDisplayText = "TestTeam" -} - - -extension ConfigVariableMetadata { - fileprivate var testProject: String? { - get { self[TestProjectMetadataKey.self] } - set { self[TestProjectMetadataKey.self] = newValue } - } - - fileprivate var testTeam: String? { - get { self[TestTeamMetadataKey.self] } - set { self[TestTeamMetadataKey.self] = newValue } - } -} diff --git a/Tests/DevConfigurationTests/Unit Tests/Core/RegisteredConfigVariableTests.swift b/Tests/DevConfigurationTests/Unit Tests/Core/RegisteredConfigVariableTests.swift new file mode 100644 index 0000000..16118ad --- /dev/null +++ b/Tests/DevConfigurationTests/Unit Tests/Core/RegisteredConfigVariableTests.swift @@ -0,0 +1,50 @@ +// +// RegisteredConfigVariableTests.swift +// DevConfiguration +// +// Created by Prachi Gauriar on 3/5/2026. +// + +import Configuration +import DevTesting +import Testing + +@testable import DevConfiguration + +struct RegisteredConfigVariableTests: RandomValueGenerating { + var randomNumberGenerator = makeRandomNumberGenerator() + + + @Test + mutating func dynamicMemberLookupReturnsMetadataValue() { + // set up + var metadata = ConfigVariableMetadata() + let project = randomAlphanumericString() + metadata[TestProjectMetadataKey.self] = project + + let variable = RegisteredConfigVariable( + key: randomConfigKey(), + defaultContent: randomConfigContent(), + secrecy: randomConfigVariableSecrecy(), + metadata: metadata + ) + + // expect + #expect(variable.testProject == project) + } + + + @Test + mutating func dynamicMemberLookupReturnsDefaultWhenNotSet() { + // set up + let variable = RegisteredConfigVariable( + key: randomConfigKey(), + defaultContent: randomConfigContent(), + secrecy: randomConfigVariableSecrecy(), + metadata: ConfigVariableMetadata() + ) + + // expect + #expect(variable.testProject == nil) + } +}