package gs.config.v0.audit import cats.data.NonEmptyList import cats.effect.IO import gs.config.v0.AuditedConfiguration import gs.config.v0.ConfigError import gs.config.v0.ConfigKey import gs.config.v0.ConfigName import gs.config.v0.Configurable import gs.config.v0.Configuration import gs.config.v0.GsSuite import gs.config.v0.source.ConfigSource import gs.config.v0.source.MemoryConfigSource import java.time.Instant import java.time.LocalDate class AuditedConfigurationTests extends GsSuite: import AuditedConfigurationTests.* given CanEqual[LocalDate, LocalDate] = CanEqual.derived given CanEqual[Instant, Instant] = CanEqual.derived iotest( "should not return values, but should record attempts to find, when no config exists" ) { for config <- Configuration .audited(ConfigSource.inMemory[IO](Map.empty)) .build() string <- config.getValue(Keys.KString) int <- config.getValue(Keys.KInt) long <- config.getValue(Keys.KLong) bool <- config.getValue(Keys.KBool) localDate <- config.getValue(Keys.KLocalDate) instant <- config.getValue(Keys.KInstant) manifest <- config.manifest.snapshot() yield assertEquals(string, Left(ConfigError.MissingValue(Names.KString))) assertEquals(int, Left(ConfigError.MissingValue(Names.KInt))) assertEquals(long, Left(ConfigError.MissingValue(Names.KLong))) assertEquals(bool, Left(ConfigError.MissingValue(Names.KBool))) assertEquals(localDate, Left(ConfigError.MissingValue(Names.KLocalDate))) assertEquals(instant, Left(ConfigError.MissingValue(Names.KInstant))) assertMissing(config, Names.KString, manifest) assertMissing(config, Names.KInt, manifest) assertMissing(config, Names.KLong, manifest) assertMissing(config, Names.KBool, manifest) assertMissing(config, Names.KLocalDate, manifest) assertMissing(config, Names.KInstant, manifest) } iotest( "should not return values, but should record attempts to find, when no config exists, across multiple sources" ) { for config <- Configuration .audited(ConfigSource.inMemory[IO](Map.empty)) .withMemorySource(Map.empty) .withSource(ConfigSource.empty[IO]) .build() string <- config.getValue(Keys.KString) manifest <- config.manifest.snapshot() yield assertEquals(string, Left(ConfigError.MissingValue(Names.KString))) assertEquals( manifest.get(Names.KString), Some( List( ConfigQueryResult.Failure( sources = config.sources.toList.map(_.name), error = ConfigError.MissingValue(Names.KString) ) ) ) ) } iotest("should find and audit a string value") { testFound(Keys.KString, "test") } iotest("should find and audit a boolan value") { testFound(Keys.KBool, true) } iotest("should find and audit an integer value") { testFound(Keys.KInt, 11) } iotest("should find and audit a long value") { testFound(Keys.KLong, 33L) } iotest("should find and audit a local date value") { testFound(Keys.KLocalDate, LocalDate.now()) } iotest("should find and audit an instant value") { testFound(Keys.KInstant, Instant.now()) } iotest("should find a value in the first source, skipping the next sources") { val key = Keys.KString val expectedValue = "value" for config <- Configuration .audited( ConfigSource .inMemory[IO](Map(key.name.unwrap() -> expectedValue.toString())) ) .withSource(ConfigSource.empty[IO]) .build() value <- config.getValue(key) manifest <- config.manifest.snapshot() yield assertEquals(value, Right(expectedValue)) assertSuccess(config, key.name, manifest, expectedValue.toString()) } iotest("should instantiate for memory and environment sources") { for c1 <- AuditedConfiguration.forSource[IO](emptyMemorySource()).build() c2 <- AuditedConfiguration.forEnvironmentSource[IO].build() c3 <- AuditedConfiguration.forMemorySource[IO](Map.empty).build() c4 <- AuditedConfiguration.forSources[IO]( NonEmptyList(emptyMemorySource(), List(emptyMemorySource())) ) c5 <- AuditedConfiguration .forEnvironmentSource[IO] .withMemorySource(Map.empty) .withEnvironmentSource() .withSource(emptyMemorySource()) .build() c6 <- Configuration.auditedEnvironmentOnly[IO] yield assertEquals(c1.sources.size, 1) assertEquals(c2.sources.size, 1) assertEquals(c3.sources.size, 1) assertEquals(c4.sources.size, 2) assertEquals(c5.sources.size, 4) assertEquals(c6.sources.size, 1) } iotest("should audit the use of default values") { val name = Names.KString val defaultValue = "value" val key = ConfigKey.WithDefaultValue(name, defaultValue) for config <- Configuration .audited(ConfigSource.inMemory[IO](Map.empty)) .withMemorySource(Map.empty) .build() value <- config.getValue(key) manifest <- config.manifest.snapshot() yield assertEquals(value, Right(defaultValue)) assertEquals( manifest.get(name), Some( List(ConfigQueryResult.UsedDefault(config.sources.toList.map(_.name))) ) ) } iotest("should detect and audit parsing failures") { val name = Names.KInt val rawValue = "not-an-integer" val key = ConfigKey.Required[Int](name) for config <- Configuration .audited(ConfigSource.inMemory[IO](Map(name.unwrap() -> rawValue))) .withMemorySource(Map.empty) .build() value <- config.getValue(key) manifest <- config.manifest.snapshot() yield val expectedError = ConfigError.CannotParseValue( configName = name, candidateValue = rawValue, source = config.sources.head.name ) assertEquals(value, Left(expectedError)) assertEquals( manifest.get(name), Some( List( ConfigQueryResult.Failure( sources = List(config.sources.head.name), error = expectedError ) ) ) ) } iotest("should detect and audit parsing failures - EitherT") { val name = Names.KInt val rawValue = "not-an-integer" val key = ConfigKey.Required[Int](name) for config <- Configuration .audited(ConfigSource.inMemory[IO](Map(name.unwrap() -> rawValue))) .withMemorySource(Map.empty) .build() value <- config.getValueT(key).value manifest <- config.manifest.snapshot() yield val expectedError = ConfigError.CannotParseValue( configName = name, candidateValue = rawValue, source = config.sources.head.name ) assertEquals(value, Left(expectedError)) assertEquals( manifest.get(name), Some( List( ConfigQueryResult.Failure( sources = List(config.sources.head.name), error = expectedError ) ) ) ) } iotest("should audit multiple accesses of the same key") { val key = Keys.KString val expectedValue = "value" for config <- Configuration .audited( ConfigSource .inMemory[IO](Map(key.name.unwrap() -> expectedValue.toString())) ) .build() v1 <- config.getValue(key) v2 <- config.getValue(key) v3 <- config.getValue(key) manifest <- config.manifest.snapshot() yield assertEquals(v1, Right(expectedValue)) assertEquals(v2, Right(expectedValue)) assertEquals(v3, Right(expectedValue)) assertEquals( manifest.get(key.name), Some( List( ConfigQueryResult .Success(Some(config.sources.head.name), expectedValue), ConfigQueryResult .Success(Some(config.sources.head.name), expectedValue), ConfigQueryResult.Success( Some(config.sources.head.name), expectedValue ) ) ) ) } private def emptyMemorySource(): MemoryConfigSource[IO] = MemoryConfigSource(Map.empty) private def testFound[A: Configurable]( key: ConfigKey[A], expectedValue: A ): IO[Any] = for config <- Configuration .audited( ConfigSource .inMemory[IO](Map(key.name.unwrap() -> expectedValue.toString())) ) .build() value <- config.getValue(key) manifest <- config.manifest.snapshot() yield assertEquals(value, Right(expectedValue)) assertSuccess(config, key.name, manifest, expectedValue.toString()) private def assertMissing( config: AuditedConfiguration[IO], name: ConfigName, manifest: Map[ConfigName, List[ConfigQueryResult]] ): Unit = assertEquals( manifest.get(name), Some( List( ConfigQueryResult.Failure( sources = List(config.sources.head.name), error = ConfigError.MissingValue(name) ) ) ) ) private def assertSuccess( config: AuditedConfiguration[IO], name: ConfigName, manifest: Map[ConfigName, List[ConfigQueryResult]], expectedRawValue: String ): Unit = assertEquals( manifest.get(name), Some( List( ConfigQueryResult.Success( Some(config.sources.head.name), expectedRawValue ) ) ) ) object AuditedConfigurationTests: object Names: val KString: ConfigName = ConfigName("string") val KInt: ConfigName = ConfigName("int") val KLong: ConfigName = ConfigName("long") val KBool: ConfigName = ConfigName("bool") val KLocalDate: ConfigName = ConfigName("localdate") val KInstant: ConfigName = ConfigName("instant") end Names object Keys: val KString: ConfigKey[String] = ConfigKey.Required[String](Names.KString) val KInt: ConfigKey[Int] = ConfigKey.Required[Int](Names.KInt) val KLong: ConfigKey[Long] = ConfigKey.Required[Long](Names.KLong) val KBool: ConfigKey[Boolean] = ConfigKey.Required[Boolean](Names.KBool) val KLocalDate: ConfigKey[LocalDate] = ConfigKey.Required[LocalDate](Names.KLocalDate) val KInstant: ConfigKey[Instant] = ConfigKey.Required[Instant](Names.KInstant) end Keys end AuditedConfigurationTests