diff --git a/src/OpenFeature.Contrib.Providers.Flagd/FlagdProvider.cs b/src/OpenFeature.Contrib.Providers.Flagd/FlagdProvider.cs index 68123788..d04a65fa 100644 --- a/src/OpenFeature.Contrib.Providers.Flagd/FlagdProvider.cs +++ b/src/OpenFeature.Contrib.Providers.Flagd/FlagdProvider.cs @@ -71,7 +71,7 @@ public FlagdProvider(FlagdConfig config) if (_config.ResolverType == ResolverType.IN_PROCESS) { - var jsonSchemaValidator = new JsonSchemaValidator(null, _config.Logger); + var jsonSchemaValidator = new JsonSchemaValidator(_config.Logger); _resolver = new InProcessResolver(_config, jsonSchemaValidator); } else diff --git a/src/OpenFeature.Contrib.Providers.Flagd/OpenFeature.Contrib.Providers.Flagd.csproj b/src/OpenFeature.Contrib.Providers.Flagd/OpenFeature.Contrib.Providers.Flagd.csproj index d0c63bc5..71a70ee4 100644 --- a/src/OpenFeature.Contrib.Providers.Flagd/OpenFeature.Contrib.Providers.Flagd.csproj +++ b/src/OpenFeature.Contrib.Providers.Flagd/OpenFeature.Contrib.Providers.Flagd.csproj @@ -29,6 +29,10 @@ + + + + diff --git a/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/FlagdJsonSchemaEmbeddedResourceReader.cs b/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/FlagdJsonSchemaEmbeddedResourceReader.cs new file mode 100644 index 00000000..737588cd --- /dev/null +++ b/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/FlagdJsonSchemaEmbeddedResourceReader.cs @@ -0,0 +1,47 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +#nullable enable + +namespace OpenFeature.Contrib.Providers.Flagd.Resolver.InProcess; + +internal sealed class FlagdJsonSchemaEmbeddedResourceReader : IFlagdJsonSchemaProvider +{ + const string TargetingJsonResourceName = "OpenFeature.Contrib.Providers.Flagd.Resources.targeting.json"; + const string FlagJsonResourceName = "OpenFeature.Contrib.Providers.Flagd.Resources.flags.json"; + + public Task ReadTargetingSchemaAsync(CancellationToken cancellationToken = default) + { + return this.ReadAsStringAsync(TargetingJsonResourceName, cancellationToken); + } + + public Task ReadFlagSchemaAsync(CancellationToken cancellationToken = default) + { + return this.ReadAsStringAsync(FlagJsonResourceName, cancellationToken); + } + + private async Task ReadAsStringAsync(string resourceName, CancellationToken cancellationToken = default) + { + var assembly = typeof(FlagdJsonSchemaEmbeddedResourceReader).Assembly; + if (assembly == null) + { + throw new InvalidOperationException($"Unable to locate assembly for {nameof(FlagdJsonSchemaEmbeddedResourceReader)}."); + } + + using var stream = assembly.GetManifestResourceStream(resourceName); + if (stream == null) + { + throw new InvalidOperationException($"Embedded resource not found: '{resourceName}'."); + } + + using var streamReader = new StreamReader(stream); + +#if NET8_0_OR_GREATER + return await streamReader.ReadToEndAsync(cancellationToken).ConfigureAwait(false); +#else + return await streamReader.ReadToEndAsync().ConfigureAwait(false); +#endif + } +} diff --git a/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/IFlagdJsonSchemaProvider.cs b/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/IFlagdJsonSchemaProvider.cs new file mode 100644 index 00000000..36403692 --- /dev/null +++ b/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/IFlagdJsonSchemaProvider.cs @@ -0,0 +1,13 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace OpenFeature.Contrib.Providers.Flagd.Resolver.InProcess; + +#nullable enable + +internal interface IFlagdJsonSchemaProvider +{ + Task ReadTargetingSchemaAsync(CancellationToken cancellationToken = default); + + Task ReadFlagSchemaAsync(CancellationToken cancellationToken = default); +} diff --git a/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/IJsonSchemaValidator.cs b/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/IJsonSchemaValidator.cs new file mode 100644 index 00000000..65f5dd06 --- /dev/null +++ b/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/IJsonSchemaValidator.cs @@ -0,0 +1,12 @@ +using System.Threading; +using System.Threading.Tasks; + +#nullable enable + +namespace OpenFeature.Contrib.Providers.Flagd.Resolver.InProcess; + +internal interface IJsonSchemaValidator +{ + Task InitializeAsync(CancellationToken cancellationToken = default); + void Validate(string configuration); +} diff --git a/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/JsonSchemaValidator.cs b/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/JsonSchemaValidator.cs index 7bee383e..dba5e1cf 100644 --- a/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/JsonSchemaValidator.cs +++ b/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/JsonSchemaValidator.cs @@ -1,5 +1,4 @@ using System; -using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -8,62 +7,29 @@ namespace OpenFeature.Contrib.Providers.Flagd.Resolver.InProcess; -internal interface IJsonSchemaValidator -{ - Task InitializeAsync(CancellationToken cancellationToken = default); - void Validate(string configuration); -} - internal class JsonSchemaValidator : IJsonSchemaValidator { - private readonly HttpClient _client; private readonly ILogger _logger; + private readonly IFlagdJsonSchemaProvider _flagdJsonSchemaProvider; + private JsonSchema _validator; - internal JsonSchemaValidator(HttpClient client, ILogger logger) + internal JsonSchemaValidator(ILogger logger) + : this(logger, new FlagdJsonSchemaEmbeddedResourceReader()) { - if (client == null) - { - client = new HttpClient - { - BaseAddress = new Uri("https://flagd.dev"), - }; - } + } - _client = client; - _logger = logger; + internal JsonSchemaValidator(ILogger logger, IFlagdJsonSchemaProvider flagdJsonSchemaProvider) + { + this._logger = logger; + this._flagdJsonSchemaProvider = flagdJsonSchemaProvider; } public async Task InitializeAsync(CancellationToken cancellationToken = default) { try { - var targetingTask = _client.GetAsync("/schema/v0/targeting.json", cancellationToken); - var flagTask = _client.GetAsync("/schema/v0/flags.json", cancellationToken); - - await Task.WhenAll(targetingTask, flagTask).ConfigureAwait(false); - - var targeting = targetingTask.Result; - var flag = flagTask.Result; - - if (!targeting.IsSuccessStatusCode) - { - _logger.LogWarning("Unable to retrieve Flagd targeting JSON Schema, status code: {StatusCode}", targeting.StatusCode); - return; - } - - if (!flag.IsSuccessStatusCode) - { - _logger.LogWarning("Unable to retrieve Flagd flags JSON Schema, status code: {StatusCode}", flag.StatusCode); - return; - } - -#if NET5_0_OR_GREATER - var targetingJson = await targeting.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); -#else - var targetingJson = await targeting.Content.ReadAsStringAsync().ConfigureAwait(false); -#endif - + var targetingJson = await this._flagdJsonSchemaProvider.ReadTargetingSchemaAsync(cancellationToken).ConfigureAwait(false); var targetingSchema = await JsonSchema.FromJsonAsync(targetingJson, "targeting.json", schema => { var schemaResolver = new JsonSchemaResolver(schema, new SystemTextJsonSchemaGeneratorSettings()); @@ -72,11 +38,7 @@ public async Task InitializeAsync(CancellationToken cancellationToken = default) return resolver; }, cancellationToken).ConfigureAwait(false); -#if NET5_0_OR_GREATER - var flagJson = await flag.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); -#else - var flagJson = await flag.Content.ReadAsStringAsync().ConfigureAwait(false); -#endif + var flagJson = await this._flagdJsonSchemaProvider.ReadFlagSchemaAsync(cancellationToken).ConfigureAwait(false); var flagSchema = await JsonSchema.FromJsonAsync(flagJson, "flags.json", schema => { var schemaResolver = new JsonSchemaResolver(schema, new SystemTextJsonSchemaGeneratorSettings()); diff --git a/test/OpenFeature.Contrib.Providers.Flagd.Test/JsonSchemaValidatorTests.cs b/test/OpenFeature.Contrib.Providers.Flagd.Test/JsonSchemaValidatorTests.cs index 967387f6..f2bca90b 100644 --- a/test/OpenFeature.Contrib.Providers.Flagd.Test/JsonSchemaValidatorTests.cs +++ b/test/OpenFeature.Contrib.Providers.Flagd.Test/JsonSchemaValidatorTests.cs @@ -1,11 +1,10 @@ using System; -using System.Net; -using System.Net.Http; -using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Testing; +using NSubstitute; +using NSubstitute.ExceptionExtensions; using OpenFeature.Contrib.Providers.Flagd.Resolver.InProcess; using Xunit; @@ -13,56 +12,12 @@ namespace OpenFeature.Contrib.Providers.Flagd.Test; public class JsonSchemaValidatorTests { - private readonly string _targetingSchemaJson = @" - { - ""$id"": ""https://example.com/example.schema.json"", - ""$schema"": ""https://json-schema.org/draft/2020-12/schema"", - ""description"": ""A sample Schema"", - ""properties"": { - ""id"": { - ""description"": ""The unique identifier"", - ""type"": ""integer"" - } - }, - ""title"": ""Targeting"", - ""type"": ""object"" - } - "; - - private readonly string _flagsSchemaJson = @" - { - ""$id"": ""https://example.com/example2.schema.json"", - ""$schema"": ""https://json-schema.org/draft/2020-12/schema"", - ""description"": ""A 2nd Sample Schema"", - ""properties"": { - ""name"": { - ""description"": ""The name"", - ""type"": ""string"" - } - }, - ""title"": ""Flags"", - ""type"": ""object"" - } - "; - [Fact] public async Task InitializeFetchesFlagSchema() { // Arrange - var targetingResponse = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent(_targetingSchemaJson, Encoding.UTF8, "application/json") - }; - var flagsResponse = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent(_flagsSchemaJson, Encoding.UTF8, "application/json") - }; - var httpClient = new HttpClient(new MockHttpMessageHandler(targetingResponse, flagsResponse)) - { - BaseAddress = new Uri("https://example.com") - }; var logger = new FakeLogger(); - var validator = new JsonSchemaValidator(httpClient, logger); + var validator = new JsonSchemaValidator(logger); // Act await validator.InitializeAsync(); @@ -73,86 +28,15 @@ public async Task InitializeFetchesFlagSchema() } [Fact] - public async Task InitializeFailsOnTargetingSchemaLogsWarning() + public async Task InitializeWhenReadTargetingSchemaAsyncThrowsLogsError() { // Arrange - var targetingResponse = new HttpResponseMessage(HttpStatusCode.InternalServerError); - var flagsResponse = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent(_flagsSchemaJson, Encoding.UTF8, "application/json") - }; - var httpClient = new HttpClient(new MockHttpMessageHandler(targetingResponse, flagsResponse)) - { - BaseAddress = new Uri("https://example.com") - }; var logger = new FakeLogger(); - var validator = new JsonSchemaValidator(httpClient, logger); - - // Act - await validator.InitializeAsync(); - - // Assert - var logs = logger.Collector.GetSnapshot(); - Assert.Single(logs); - Assert.Multiple(() => - { - var actual = logs[0]; - Assert.Equal(LogLevel.Warning, actual.Level); - Assert.Equal("Unable to retrieve Flagd targeting JSON Schema, status code: InternalServerError", actual.Message); - }); - } + var failingSchemaProvider = Substitute.For(); + var validator = new JsonSchemaValidator(logger, failingSchemaProvider); - [Fact] - public async Task InitializeFailsOnFlagsSchemaLogsWarning() - { - // Arrange - var targetingResponse = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent(_targetingSchemaJson, Encoding.UTF8, "application/json") - }; - var flagsResponse = new HttpResponseMessage(HttpStatusCode.InternalServerError) - { - Content = new StringContent(_flagsSchemaJson, Encoding.UTF8, "application/json") - }; - var httpClient = new HttpClient(new MockHttpMessageHandler(targetingResponse, flagsResponse)) - { - BaseAddress = new Uri("https://example.com") - }; - var logger = new FakeLogger(); - var validator = new JsonSchemaValidator(httpClient, logger); - - // Act - await validator.InitializeAsync(); - - // Assert - var logs = logger.Collector.GetSnapshot(); - Assert.Single(logs); - Assert.Multiple(() => - { - var actual = logs[0]; - Assert.Equal(LogLevel.Warning, actual.Level); - Assert.Equal("Unable to retrieve Flagd flags JSON Schema, status code: InternalServerError", actual.Message); - }); - } - - [Fact] - public async Task InitializeInvalidJsonSchemaLogsError() - { - // Arrange - var targetingResponse = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent(@"", Encoding.UTF8, "application/json") - }; - var flagsResponse = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent(_flagsSchemaJson, Encoding.UTF8, "application/json") - }; - var httpClient = new HttpClient(new MockHttpMessageHandler(targetingResponse, flagsResponse)) - { - BaseAddress = new Uri("https://example.com") - }; - var logger = new FakeLogger(); - var validator = new JsonSchemaValidator(httpClient, logger); + failingSchemaProvider.ReadTargetingSchemaAsync(Arg.Any()) + .Throws(new Exception("Simulated failure")); // Act await validator.InitializeAsync(); @@ -169,59 +53,21 @@ public async Task InitializeInvalidJsonSchemaLogsError() } [Fact] - public async Task ValidateSchemaNoWarnings() + public async Task InitializeWhenReadFlagSchemaAsyncThrowsLogsError() { // Arrange - var targetingResponse = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent(_targetingSchemaJson, Encoding.UTF8, "application/json") - }; - var flagsResponse = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent(_flagsSchemaJson, Encoding.UTF8, "application/json") - }; - var httpClient = new HttpClient(new MockHttpMessageHandler(targetingResponse, flagsResponse)) - { - BaseAddress = new Uri("https://example.com") - }; var logger = new FakeLogger(); - var validator = new JsonSchemaValidator(httpClient, logger); + var failingSchemaProvider = Substitute.For(); + var validator = new JsonSchemaValidator(logger, failingSchemaProvider); - await validator.InitializeAsync(); - - // Act - var configuration = @"{""$schema"":""https://example.com/example2.schema.jsonhttps://example.com/example2.schema.json"",""name"":""test""}"; - validator.Validate(configuration); + failingSchemaProvider.ReadTargetingSchemaAsync(Arg.Any()) + .Returns("{$id\": \"https://flagd.dev/schema/v0/targeting.json\"}"); - // Assert - var logs = logger.Collector.GetSnapshot(); - Assert.Empty(logs); - } - - [Fact] - public async Task ValidateSchemaInvalidJsonWarning() - { - // Arrange - var targetingResponse = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent(_targetingSchemaJson, Encoding.UTF8, "application/json") - }; - var flagsResponse = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent(_flagsSchemaJson, Encoding.UTF8, "application/json") - }; - var httpClient = new HttpClient(new MockHttpMessageHandler(targetingResponse, flagsResponse)) - { - BaseAddress = new Uri("https://example.com") - }; - var logger = new FakeLogger(); - var validator = new JsonSchemaValidator(httpClient, logger); - - await validator.InitializeAsync(); + failingSchemaProvider.ReadFlagSchemaAsync(Arg.Any()) + .Throws(new Exception("Simulated failure")); // Act - var configuration = @"{""$schema"":""https://example.com/example2.schema.jsonhttps://example.com/example2.schema.json"",""name"":15}"; - validator.Validate(configuration); + await validator.InitializeAsync(); // Assert var logs = logger.Collector.GetSnapshot(); @@ -229,8 +75,8 @@ public async Task ValidateSchemaInvalidJsonWarning() Assert.Multiple(() => { var actual = logs[0]; - Assert.Equal(LogLevel.Warning, actual.Level); - Assert.StartsWith("Validating Flagd configuration resulted in Schema Validation errors", actual.Message); + Assert.Equal(LogLevel.Error, actual.Level); + Assert.Equal("Unable to retrieve Flagd flags and targeting JSON Schemas", actual.Message); }); } @@ -238,12 +84,8 @@ public async Task ValidateSchemaInvalidJsonWarning() public void WhenNotInitializedThenValidateSchemaNoWarnings() { // Arrange - var httpClient = new HttpClient(new MockHttpMessageHandler(null, null)) - { - BaseAddress = new Uri("https://example.com") - }; var logger = new FakeLogger(); - var validator = new JsonSchemaValidator(httpClient, logger); + var validator = new JsonSchemaValidator(logger); // Act var configuration = @"{""$schema"":""https://example.com/example2.schema.jsonhttps://example.com/example2.schema.json"",""name"":""test""}"; @@ -254,30 +96,3 @@ public void WhenNotInitializedThenValidateSchemaNoWarnings() Assert.Empty(logs); } } - -public class MockHttpMessageHandler : HttpMessageHandler -{ - private readonly HttpResponseMessage _targetingResponse; - private readonly HttpResponseMessage _flagsResponse; - - public MockHttpMessageHandler(HttpResponseMessage targetingResponse, HttpResponseMessage flagsResponse) - { - _targetingResponse = targetingResponse; - _flagsResponse = flagsResponse; - } - - protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - if (request.RequestUri.PathAndQuery.Contains("/schema/v0/targeting.json")) - { - return Task.FromResult(_targetingResponse); - } - - if (request.RequestUri.PathAndQuery.Contains("/schema/v0/flags.json")) - { - return Task.FromResult(_flagsResponse); - } - - throw new NotImplementedException("HttpMessageHandler not implemented"); - } -} diff --git a/test/OpenFeature.Contrib.Providers.Flagd.Test/Resolver/InProcess/FlagdJsonSchemaEmbeddedResourceReaderTests.cs b/test/OpenFeature.Contrib.Providers.Flagd.Test/Resolver/InProcess/FlagdJsonSchemaEmbeddedResourceReaderTests.cs new file mode 100644 index 00000000..0cf68670 --- /dev/null +++ b/test/OpenFeature.Contrib.Providers.Flagd.Test/Resolver/InProcess/FlagdJsonSchemaEmbeddedResourceReaderTests.cs @@ -0,0 +1,33 @@ +using System.Text.Json; +using System.Threading.Tasks; +using OpenFeature.Contrib.Providers.Flagd.Resolver.InProcess; +using Xunit; + +namespace OpenFeature.Contrib.Providers.Flagd.Test.Resolver.InProcess; + +public class FlagdJsonSchemaEmbeddedResourceReaderTests +{ + [Fact] + public async Task ReadTargetingSchemaAsyncReturnsJson() + { + var reader = new FlagdJsonSchemaEmbeddedResourceReader(); + + var schema = await reader.ReadTargetingSchemaAsync(); + + Assert.False(string.IsNullOrWhiteSpace(schema)); + + JsonDocument.Parse(schema); // Will throw if not valid JSON + } + + [Fact] + public async Task ReadFlagSchemaAsyncReturnsJson() + { + var reader = new FlagdJsonSchemaEmbeddedResourceReader(); + + var schema = await reader.ReadFlagSchemaAsync(); + + Assert.False(string.IsNullOrWhiteSpace(schema)); + + JsonDocument.Parse(schema); // Will throw if not valid JSON + } +}