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
+ }
+}