From eb0bbf998cb694ea7d8ab5c84c7bf78b91cef430 Mon Sep 17 00:00:00 2001 From: Philip Conrad Date: Thu, 18 Jul 2024 18:28:22 -0400 Subject: [PATCH] High-level Batch Query API Support (#58) This commit adds new "high-level" Batch Query methods, and tests to prove that they work. It also incidentally required wiring up several new "OPA Types" for wrapping the lower-level error and result types generated by Speakeasy. The new wrapper types all render as JSON objects when stringified, which has proven much more helpful for debugging than the default behavior. Exception handling has been refactored across the board to conform to C# norms a bit better. Signed-off-by: Philip Conrad --- Styra/Opa/OpaClient.cs | 390 ++++++++++- test/SmokeTest.Tests/EOPAContainerFixture.cs | 84 ++- test/SmokeTest.Tests/HighLevelTest.cs | 256 ++++++- test/SmokeTest.Tests/OPAContainerFixture.cs | 70 +- test/SmokeTest.Tests/OpenApiTest.cs | 670 ++++++++++--------- test/SmokeTest.Tests/Usings.cs | 6 +- 6 files changed, 1021 insertions(+), 455 deletions(-) diff --git a/Styra/Opa/OpaClient.cs b/Styra/Opa/OpaClient.cs index 1c43aaf..6643bdf 100644 --- a/Styra/Opa/OpaClient.cs +++ b/Styra/Opa/OpaClient.cs @@ -1,14 +1,195 @@ -namespace Styra.Opa; - -using OpenApi; -using OpenApi.Models.Requests; -using OpenApi.Models.Components; -using System.Collections.Generic; +using System.Collections.Generic; using System.Threading.Tasks; -using System.Text.Json.Serialization; using System; -using System.IO; using Newtonsoft.Json; +using Styra.Opa.OpenApi.Models.Components; +using Styra.Opa.OpenApi; +using Styra.Opa.OpenApi.Models.Requests; +using Styra.Opa.OpenApi.Models.Errors; + +namespace Styra.Opa; + +public class OpaResult +{ + /// + /// If decision logging is enabled, this field contains a string that uniquely identifies the decision. The identifier will be included in the decision log event for this decision. Callers can use the identifier for correlation purposes. + /// + [JsonProperty("decision_id")] + public string? DecisionId { get; set; } + + /// + /// If query metrics are enabled, this field contains query performance metrics collected during the parse, compile, and evaluation steps. + /// + [JsonProperty("metrics")] + public Dictionary? Metrics { get; set; } + + /// + /// Provenance information can be requested on individual API calls and are returned inline with the API response. To obtain provenance information on an API call, specify the `provenance=true` query parameter when executing the API call. + /// + [JsonProperty("provenance")] + public Provenance? Provenance { get; set; } + + /// + /// The base or virtual document referred to by the URL path. If the path is undefined, this key will be omitted. + /// + [JsonProperty("result")] + public Result? Result { get; set; } + + /// + /// The HTTP status code for the request. Limited to "200" or "500". + /// + [JsonProperty("http_status_code")] + public string? HttpStatusCode { get; set; } + + public OpaResult() { } + + public OpaResult(ResponsesSuccessfulPolicyResponse resp) + { + DecisionId = resp.DecisionId; + Metrics = resp.Metrics; + Provenance = resp.Provenance; + Result = resp.Result; + HttpStatusCode = resp.HttpStatusCode; + } + + public OpaResult(SuccessfulPolicyResponse resp) + { + DecisionId = resp.DecisionId; + Metrics = resp.Metrics; + Provenance = resp.Provenance; + Result = resp.Result; + } + + public static explicit operator OpaResult(ResponsesSuccessfulPolicyResponse e) => new OpaResult(e); + public static explicit operator OpaResult(SuccessfulPolicyResponse e) => new OpaResult(e); + + public override string ToString() + { + return JsonConvert.SerializeObject(this); + } +} + +public class OpaError +{ + /// + /// The short-form category of error, such as "internal_error", "invalid_policy_or_data", etc. + /// + [JsonProperty("code")] + public string Code { get; set; } = default!; + + /// + /// If decision logging is enabled, this field contains a string that uniquely identifies the decision. The identifier will be included in the decision log event for this decision. Callers can use the identifier for correlation purposes. + /// + [JsonProperty("decision_id")] + public string? DecisionId { get; set; } + + /// + /// The long-form error message from the OPA instance, describing what went wrong. + /// + [JsonProperty("message")] + public string Message { get; set; } = default!; + + /// + /// The HTTP status code for the request. Limited to "200" or "500". + /// + [JsonProperty("http_status_code")] + public string? HttpStatusCode { get; set; } + + public OpaError() + { + + } + public OpaError(Styra.Opa.OpenApi.Models.Components.ServerError err) + { + Code = err.Code; + DecisionId = err.DecisionId; + Message = err.Message; + HttpStatusCode = err.HttpStatusCode; + } + public OpaError(Styra.Opa.OpenApi.Models.Errors.ServerError err) + { + Code = err.Code; + DecisionId = err.DecisionId; + Message = err.Message; + } + + public static explicit operator OpaError(OpenApi.Models.Components.ServerError e) => new OpaError(e); + public static explicit operator OpaError(OpenApi.Models.Errors.ServerError e) => new OpaError(e); + + public override string ToString() + { + return JsonConvert.SerializeObject(this); + } +} + +public class OpaBatchInputs : Dictionary> +{ + public override string ToString() + { + return JsonConvert.SerializeObject(this); + } +} + +public class OpaBatchResults : Dictionary +{ + public override string ToString() + { + return JsonConvert.SerializeObject(this); + } +} + +public class OpaBatchErrors : Dictionary +{ + public override string ToString() + { + return JsonConvert.SerializeObject(this); + } +} + +// Used for converting inputs for the Batch Query API, and converting result types +// into useful higher-level types. +public static class DictionaryExtensions +{ + public static Dictionary ToOpaBatchInputRaw(this Dictionary> inputs) + { + var opaBatchInputs = new Dictionary(); + foreach (var kvp in inputs) + { + opaBatchInputs[kvp.Key] = Input.CreateMapOfAny(kvp.Value); + } + return opaBatchInputs; + } + + public static OpaBatchErrors ToOpaBatchErrors(this Dictionary errors) + { + var opaBatchErrors = new OpaBatchErrors(); + foreach (var kvp in errors) + { + opaBatchErrors[kvp.Key] = new OpaError(kvp.Value); + } + return opaBatchErrors; + } + + public static OpaBatchErrors ToOpaBatchErrors(this Dictionary errors) + { + var opaBatchErrors = new OpaBatchErrors(); + foreach (var kvp in errors) + { + opaBatchErrors[kvp.Key] = new OpaError(kvp.Value); + } + return opaBatchErrors; + } + + public static OpaBatchResults ToOpaBatchResults(this Dictionary responses) + { + var opaBatchResults = new OpaBatchResults(); + foreach (var kvp in responses) + { + opaBatchResults[kvp.Key] = (OpaResult)kvp.Value; + } + return opaBatchResults; + } +} /// /// OpaClient provides high-level convenience APIs for interacting with an OPA server. @@ -21,6 +202,11 @@ public class OpaClient // Default values to use when creating the SDK instance. private string sdkServerUrl = "http://localhost:8181"; + // Internal: Records whether or not to go to fallback mode immediately for + // batched queries. It is switched over to false as soon as it gets a 404 + // from an OPA server. + private bool opaSupportsBatchQueryAPI = true; + // Values to use when generating requests. private bool policyRequestPretty = false; private bool policyRequestProvenance = false; @@ -114,7 +300,7 @@ public async Task check(string path, Dictionary input) } /// - /// Evaluate the server's default policy, then coerce the result to type T. + /// Evaluate a policy, then coerce the result to type T. /// /// The rule to evaluate. (Example: "app/rbac") /// Result, as an instance of T, or null in the case of a query failure. @@ -124,7 +310,7 @@ public async Task check(string path, Dictionary input) } /// - /// Evaluate the server's default policy, using the provided input boolean value, then coerce the result to type T. + /// Evaluate a policy, using the provided input boolean value, then coerce the result to type T. /// /// The input boolean value OPA will use for evaluating the rule. /// The rule to evaluate. (Example: "app/rbac") @@ -135,7 +321,7 @@ public async Task check(string path, Dictionary input) } /// - /// Evaluate the server's default policy, using the provided input floating-point number, then coerce the result to type T. + /// Evaluate a policy, using the provided input floating-point number, then coerce the result to type T. /// /// The input floating-point number OPA will use for evaluating the rule. /// The rule to evaluate. (Example: "app/rbac") @@ -146,7 +332,7 @@ public async Task check(string path, Dictionary input) } /// - /// Evaluate the server's default policy, using the provided input string, then coerce the result to type T. + /// Evaluate a policy, using the provided input string, then coerce the result to type T. /// /// The input string OPA will use for evaluating the rule. /// The rule to evaluate. (Example: "app/rbac") @@ -157,7 +343,7 @@ public async Task check(string path, Dictionary input) } /// - /// Evaluate the server's default policy, using the provided input boolean value, then coerce the result to type T. + /// Evaluate a policy, using the provided input boolean value, then coerce the result to type T. /// /// The input List value OPA will use for evaluating the rule. /// The rule to evaluate. (Example: "app/rbac") @@ -169,7 +355,7 @@ public async Task check(string path, Dictionary input) } /// - /// Evaluate the server's default policy, using the provided input map, then coerce the result to type T. + /// Evaluate a policy, using the provided input map, then coerce the result to type T. /// /// The input Dictionary OPA will use for evaluating the rule. /// The rule to evaluate. (Example: "app/rbac") @@ -182,29 +368,14 @@ public async Task check(string path, Dictionary input) /// private async Task queryMachinery(string path, Input input) { - ExecutePolicyWithInputRequest req = new ExecutePolicyWithInputRequest() - { - Path = path, - RequestBody = new ExecutePolicyWithInputRequestBody() - { - Input = input - }, - Pretty = policyRequestPretty, - Provenance = policyRequestProvenance, - Explain = policyRequestExplain, - Metrics = policyRequestMetrics, - Instrument = policyRequestInstrument, - StrictBuiltinErrors = policyRequestStrictBuiltinErrors, - }; - ExecutePolicyWithInputResponse res; try { - res = await opa.ExecutePolicyWithInputAsync(req); + res = await evalPolicySingle(path, input); } catch (Exception e) { - string msg = string.Format("executing policy at '{0}' with failed due to exception '{1}'", path, e); + var msg = string.Format("executing policy at '{0}' with failed due to exception '{1}'", path, e); throw new OpaException(msg, e); } @@ -315,7 +486,7 @@ public async Task check(string path, Dictionary input) } catch (Exception e) { - string msg = string.Format("executing server default policy failed due to exception '{0}'", e); + var msg = string.Format("executing server default policy failed due to exception '{0}'", e); throw new OpaException(msg, e); } @@ -355,4 +526,159 @@ public async Task check(string path, Dictionary input) // from or is, we return the appropriate null type for T. return default; } + + /// + /// Evaluate a policy, using the provided map of query inputs. Results will + /// be returned in an identically-structured pair of maps, one for + /// successful evals, and one for errors. In the event that the OPA server + /// does not support the /v1/batch/data endpoint, this method will fall back + /// to performing sequential queries against the OPA server. + /// + /// The rule to evaluate. (Example: "app/rbac") + /// The input Dictionary OPA will use for evaluating the rule. The keys are arbitrary ID strings, the values are the input values intended for each query. + /// A pair of mappings, between string keys, and SuccessfulPolicyResponses, or ServerErrors. + public async Task<(OpaBatchResults, OpaBatchErrors)> evaluateBatch(string path, Dictionary> inputs) + { + return await queryMachineryBatch(path, inputs); + } + + /// + private async Task<(OpaBatchResults, OpaBatchErrors)> queryMachineryBatch(string path, Dictionary> inputs) + { + OpaBatchResults successResults = new(); + OpaBatchErrors failureResults = new(); + + // Attempt using the /v1/batch/data endpoint. If we ever receive a 404, then it's a vanilla OPA instance, and we should skip straight to fallback mode. + if (opaSupportsBatchQueryAPI) + { + var req = new ExecuteBatchPolicyWithInputRequest() + { + Path = path, + RequestBody = new ExecuteBatchPolicyWithInputRequestBody() + { + Inputs = inputs.ToOpaBatchInputRaw(), + }, + Pretty = policyRequestPretty, + Provenance = policyRequestProvenance, + Explain = policyRequestExplain, + Metrics = policyRequestMetrics, + Instrument = policyRequestInstrument, + StrictBuiltinErrors = policyRequestStrictBuiltinErrors, + }; + + // Launch query. The all-errors case is handled in the exception handler block. + ExecuteBatchPolicyWithInputResponse res; + try + { + res = await opa.ExecuteBatchPolicyWithInputAsync(req); + switch (res.StatusCode) + { + // All-success case. + case 200: + successResults = res.BatchSuccessfulPolicyEvaluation!.Responses!.ToOpaBatchResults(); // Should not be null here. + return (successResults, failureResults); + // Mixed results case. + case 207: + var mixedResponses = res.BatchMixedResults?.Responses!; // Should not be null here. + foreach (var (key, value) in mixedResponses) + { + switch (value.Type.ToString()) + { + case "200": + successResults.Add(key, (OpaResult)value.ResponsesSuccessfulPolicyResponse!); + break; + case "500": + failureResults.Add(key, (OpaError)value.ServerError!); // Should not be null. + break; + } + } + + return (successResults, failureResults); + default: + // TODO: Throw exception if we reach the end of this block without a successful return. + // This *should* never happen. It means we didn't return from the batch or fallback handler blocks earlier. + throw new Exception("Impossible error"); + } + } + catch (ClientError ce) + { + throw ce; // Rethrow for the caller to deal with. Request was malformed. + } + catch (BatchServerError bse) + { + failureResults = bse.Responses!.ToOpaBatchErrors(); // Should not be null here. + return (successResults, failureResults); + } + catch (SDKException se) when (se.StatusCode == 404) + { + // We know we've got an issue now. + opaSupportsBatchQueryAPI = false; + // Fall-through to the "unsupported" case. + } + } + // Implicitly rethrow all other exceptions. + + // Fall back to sequential queries against the OPA instance. + if (!opaSupportsBatchQueryAPI) + { + foreach (var (key, value) in inputs) + { + try + { + var res = await evalPolicySingle(path, Input.CreateMapOfAny(value)); + successResults.Add(key, (OpaResult)res.SuccessfulPolicyResponse!); + } + catch (ClientError ce) + { + throw ce; // Rethrow for the caller to deal with. Request was malformed. + } + catch (Styra.Opa.OpenApi.Models.Errors.ServerError se) + { + failureResults.Add(key, (OpaError)se); + } + // Implicitly rethrow all other exceptions. + } + + // If we have the mixed case, add the HttpStatusCode fields. + if (successResults.Count > 0 && failureResults.Count > 0) + { + // Modifying the dictionary element while iterating is a language feature since 2020, apparently. + // Ref: https://github.com/dotnet/runtime/pull/34667 + foreach (var key in successResults.Keys) + { + successResults[key].HttpStatusCode = "200"; + } + foreach (var key in failureResults.Keys) + { + failureResults[key].HttpStatusCode = "500"; + } + } + + return (successResults, failureResults); + } + + // This *should* never happen. It means we didn't return from the batch or fallback handler blocks earlier. + throw new Exception("Impossible error"); + } + + /// + private async Task evalPolicySingle(string path, Input input) + { + var req = new ExecutePolicyWithInputRequest() + { + Path = path, + RequestBody = new ExecutePolicyWithInputRequestBody() + { + Input = input + }, + Pretty = policyRequestPretty, + Provenance = policyRequestProvenance, + Explain = policyRequestExplain, + Metrics = policyRequestMetrics, + Instrument = policyRequestInstrument, + StrictBuiltinErrors = policyRequestStrictBuiltinErrors, + }; + + return await opa.ExecutePolicyWithInputAsync(req); + } } diff --git a/test/SmokeTest.Tests/EOPAContainerFixture.cs b/test/SmokeTest.Tests/EOPAContainerFixture.cs index 0b85b2f..d8f6383 100644 --- a/test/SmokeTest.Tests/EOPAContainerFixture.cs +++ b/test/SmokeTest.Tests/EOPAContainerFixture.cs @@ -1,59 +1,55 @@ -using DotNet.Testcontainers.Containers; -using Docker.DotNet.Models; -using System.Runtime.InteropServices; - -namespace SmokeTest.Tests; +namespace SmokeTest.Tests; public class EOPAContainerFixture : IAsyncLifetime { - // Note: We disable this warning because we control when/how the constructor - // will be invoked for this class. + // Note: We disable this warning because we control when/how the constructor + // will be invoked for this class. #pragma warning disable CS8618 - private IContainer _container; -#pragma warning restore CS8618 + private IContainer _container; +#pragma warning restore CS8618 - public async Task InitializeAsync() - { - string[] startupFiles = { + public async Task InitializeAsync() + { + string[] startupFiles = { "testdata/policy.rego", "testdata/weird_name.rego", "testdata/simple/system.rego", "testdata/condfail.rego", "testdata/data.json" }; - string[] opaCmd = { "run", "--server", "--addr=0.0.0.0:8181", "--disable-telemetry" }; - string[] startupCommand = new List().Concat(opaCmd).Concat(startupFiles).ToArray(); + string[] opaCmd = { "run", "--server", "--addr=0.0.0.0:8181", "--disable-telemetry" }; + var startupCommand = new List().Concat(opaCmd).Concat(startupFiles).ToArray(); - // Create a new instance of a container. - IContainer container = new ContainerBuilder() - .WithImage("ghcr.io/styrainc/enterprise-opa:1.23.0") - .WithEnvironment("EOPA_LICENSE_TOKEN", Environment.GetEnvironmentVariable("EOPA_LICENSE_TOKEN")) - .WithEnvironment("EOPA_LICENSE_KEY", Environment.GetEnvironmentVariable("EOPA_LICENSE_KEY")) - // Bind port 8181 of the container to a random port on the host. - .WithPortBinding(8181, true) - .WithCommand(startupCommand) - // Map our policy and data files into the container instance. - .WithResourceMapping(new DirectoryInfo("testdata"), "/testdata/") - // Wait until the HTTP endpoint of the container is available. - .WithWaitStrategy(Wait.ForUnixContainer().UntilHttpRequestIsSucceeded(r => r.ForPort(8181).ForPath("/health"))) - //.WithOutputConsumer(Consume.RedirectStdoutAndStderrToConsole()) // DEBUG - // Build the container configuration. - .Build(); + // Create a new instance of a container. + var container = new ContainerBuilder() + .WithImage("ghcr.io/styrainc/enterprise-opa:1.23.0") + .WithEnvironment("EOPA_LICENSE_TOKEN", Environment.GetEnvironmentVariable("EOPA_LICENSE_TOKEN")) + .WithEnvironment("EOPA_LICENSE_KEY", Environment.GetEnvironmentVariable("EOPA_LICENSE_KEY")) + // Bind port 8181 of the container to a random port on the host. + .WithPortBinding(8181, true) + .WithCommand(startupCommand) + // Map our policy and data files into the container instance. + .WithResourceMapping(new DirectoryInfo("testdata"), "/testdata/") + // Wait until the HTTP endpoint of the container is available. + .WithWaitStrategy(Wait.ForUnixContainer().UntilHttpRequestIsSucceeded(r => r.ForPort(8181).ForPath("/health"))) + //.WithOutputConsumer(Consume.RedirectStdoutAndStderrToConsole()) // DEBUG + // Build the container configuration. + .Build(); - // Start the container. - await container.StartAsync() - .ConfigureAwait(false); - // DEBUG: - // var (stderr, stdout) = await container.GetLogsAsync(default); - // Console.WriteLine("STDERR: {0}", stderr); - // Console.WriteLine("STDOUT: {0}", stdout); + // Start the container. + await container.StartAsync() + .ConfigureAwait(false); + // DEBUG: + // var (stderr, stdout) = await container.GetLogsAsync(default); + // Console.WriteLine("STDERR: {0}", stderr); + // Console.WriteLine("STDOUT: {0}", stdout); - _container = container; - } - public async Task DisposeAsync() - { - await _container.DisposeAsync(); - } + _container = container; + } + public async Task DisposeAsync() + { + await _container.DisposeAsync(); + } - // Expose the container for tests - public IContainer GetContainer() => _container; + // Expose the container for tests + public IContainer GetContainer() => _container; } diff --git a/test/SmokeTest.Tests/HighLevelTest.cs b/test/SmokeTest.Tests/HighLevelTest.cs index 0080966..680fea4 100644 --- a/test/SmokeTest.Tests/HighLevelTest.cs +++ b/test/SmokeTest.Tests/HighLevelTest.cs @@ -1,20 +1,32 @@ -using Styra.Opa; +using Styra.Opa; +using Styra.Opa.OpenApi.Models.Components; namespace SmokeTest.Tests; -public class HighLevelTest : IClassFixture +public class HighLevelTest : IClassFixture, IClassFixture { - public IContainer _container; + public IContainer _containerOpa; + public IContainer _containerEopa; - public HighLevelTest(OPAContainerFixture fixture) + public HighLevelTest(OPAContainerFixture opaFixture, EOPAContainerFixture eopaFixture) { - _container = fixture.GetContainer(); + _containerOpa = opaFixture.GetContainer(); + _containerEopa = eopaFixture.GetContainer(); } private OpaClient GetOpaClient() { // Construct the request URI by specifying the scheme, hostname, assigned random host port, and the endpoint "uuid". - var requestUri = new UriBuilder(Uri.UriSchemeHttp, _container.Hostname, _container.GetMappedPublicPort(8181)).Uri; + var requestUri = new UriBuilder(Uri.UriSchemeHttp, _containerOpa.Hostname, _containerOpa.GetMappedPublicPort(8181)).Uri; + + // Send an HTTP GET request to the specified URI and retrieve the response as a string. + return new OpaClient(serverUrl: requestUri.ToString()); + } + + private OpaClient GetEOpaClient() + { + // Construct the request URI by specifying the scheme, hostname, assigned random host port, and the endpoint "uuid". + var requestUri = new UriBuilder(Uri.UriSchemeHttp, _containerEopa.Hostname, _containerEopa.GetMappedPublicPort(8181)).Uri; // Send an HTTP GET request to the specified URI and retrieve the response as a string. return new OpaClient(serverUrl: requestUri.ToString()); @@ -92,11 +104,241 @@ public async Task EvaluateDefaultTest() { var client = GetOpaClient(); - Dictionary? res = await client.evaluateDefault>( + var res = await client.evaluateDefault>( new Dictionary() { { "hello", "world" }, }); Assert.Equal(new Dictionary() { { "hello", "world" } }, res?.GetValueOrDefault("echo", "")); } + + [Fact] + public async Task RBACBatchAllSuccessTest() + { + var client = GetEOpaClient(); + + var goodInput = new Dictionary() { + { "user", "alice" }, + { "action", "read" }, + { "object", "id123" }, + { "type", "dog" } + }; + + var (successes, failures) = await client.evaluateBatch("app/rbac/allow", new Dictionary>() { + {"AAA", goodInput }, + {"BBB", goodInput }, + {"CCC", goodInput }, + }); + + var expSuccess = new OpaResult() { Result = Result.CreateBoolean(true) }; + + // Assert that the successes dictionary has all expected elements, and the + // failures dictionary is empty. + Assert.Equivalent(new Dictionary() { + { "AAA", expSuccess }, + { "BBB", expSuccess }, + { "CCC", expSuccess }, + }, successes); + Assert.Empty(failures); + } + + [Fact] + public async Task RBACBatchMixedTest() + { + var client = GetEOpaClient(); + + var goodInput = new Dictionary() { + { "x", new List {1, 1, 3} }, + { "y", new List {1, 1, 1} }, + }; + + var badInput = new Dictionary() { + { "x", new List {1, 1, 3} }, + { "y", new List {1, 2, 1} }, + }; + + var (successes, failures) = await client.evaluateBatch("testmod/condfail", new Dictionary>() { + {"AAA", badInput }, + {"BBB", goodInput }, + {"CCC", badInput }, + }); + + var expSuccess = new OpaResult() + { + HttpStatusCode = "200", + Result = Result.CreateMapOfAny( + new Dictionary() { + {"p", new Dictionary() { { "1", 2 }, { "3", 4 } } } + } + ) + }; + var expError = new OpaError() + { + Code = "internal_error", + DecisionId = null, + HttpStatusCode = "500", + Message = "object insert conflict" + }; + + // Assert that the failures dictionary has all expected elements, and the + // successes dictionary is empty. + Assert.Equivalent(new OpaBatchResults() { { "BBB", expSuccess } }, successes); + Assert.Equivalent(new Dictionary() { + { "AAA", expError }, + { "CCC", expError }, + }, failures); + + } + + [Fact] + public async Task RBACBatchAllFailuresTest() + { + var client = GetEOpaClient(); + + var badInput = new Dictionary() { + { "x", new List {1, 1, 3} }, + { "y", new List {1, 2, 1} }, + }; + + var (successes, failures) = await client.evaluateBatch("testmod/condfail", new Dictionary>() { + {"AAA", badInput }, + {"BBB", badInput }, + {"CCC", badInput }, + }); + + var expError = new OpaError() + { + Code = "internal_error", + DecisionId = null, + Message = "object insert conflict" + }; + + // Assert that the failures dictionary has all expected elements, and the + // successes dictionary is empty. + Assert.Empty(successes); + Assert.Equivalent(new Dictionary() { + { "AAA", expError }, + { "BBB", expError }, + { "CCC", expError }, + }, failures); + + } + + [Fact] + public async Task RBACBatchAllSuccessFallbackTest() + { + var client = GetOpaClient(); + + var goodInput = new Dictionary() { + { "x", new List {1, 1, 3} }, + { "y", new List {1, 1, 1} }, + }; + + var (successes, failures) = await client.evaluateBatch("testmod/condfail", new Dictionary>() { + {"AAA", goodInput }, + {"BBB", goodInput }, + {"CCC", goodInput }, + }); + + var expSuccess = new OpaResult() + { + Result = Result.CreateMapOfAny( + new Dictionary() { + {"p", new Dictionary() { { "1", 2 }, { "3", 4 } } } + } + ) + }; + + // Assert that the failures dictionary has all expected elements, and the + // successes dictionary is empty. + Assert.Equivalent(new OpaBatchResults() { + { "BBB", expSuccess }, + { "AAA", expSuccess }, + { "CCC", expSuccess } + }, successes); + Assert.Empty(failures); + + } + + [Fact] + public async Task RBACBatchMixedFallbackTest() + { + var client = GetOpaClient(); + + var goodInput = new Dictionary() { + { "x", new List {1, 1, 3} }, + { "y", new List {1, 1, 1} }, + }; + + var badInput = new Dictionary() { + { "x", new List {1, 1, 3} }, + { "y", new List {1, 2, 1} }, + }; + + var (successes, failures) = await client.evaluateBatch("testmod/condfail", new Dictionary>() { + {"AAA", badInput }, + {"BBB", goodInput }, + {"CCC", badInput }, + }); + + var expSuccess = new OpaResult() + { + HttpStatusCode = "200", + Result = Result.CreateMapOfAny( + new Dictionary() { + {"p", new Dictionary() { { "1", 2 }, { "3", 4 } } } + } + ) + }; + var expError = new OpaError() + { + Code = "internal_error", + DecisionId = null, + HttpStatusCode = "500", + Message = "error(s) occurred while evaluating query" // Note: different error message for OPA mode. + }; + + // Assert that the failures dictionary has all expected elements, and the + // successes dictionary is empty. + Assert.Equivalent(new OpaBatchResults() { { "BBB", expSuccess } }, successes); + Assert.Equivalent(new Dictionary() { + { "AAA", expError }, + { "CCC", expError }, + }, failures); + + } + + [Fact] + public async Task RBACBatchAllFailuresFallbackTest() + { + var client = GetOpaClient(); + + var badInput = new Dictionary() { + { "x", new List {1, 1, 3} }, + { "y", new List {1, 2, 1} }, + }; + + var (successes, failures) = await client.evaluateBatch("testmod/condfail", new Dictionary>() { + {"AAA", badInput }, + {"BBB", badInput }, + {"CCC", badInput }, + }); + + var expError = new OpaError() + { + Code = "internal_error", + DecisionId = null, + Message = "error(s) occurred while evaluating query" // Note: different error message for OPA mode. + }; + + // Assert that the failures dictionary has all expected elements, and the + // successes dictionary is empty. + Assert.Empty(successes); + Assert.Equivalent(new Dictionary() { + { "AAA", expError }, + { "BBB", expError }, + { "CCC", expError }, + }, failures); + + } } \ No newline at end of file diff --git a/test/SmokeTest.Tests/OPAContainerFixture.cs b/test/SmokeTest.Tests/OPAContainerFixture.cs index d5cb074..dc0238e 100644 --- a/test/SmokeTest.Tests/OPAContainerFixture.cs +++ b/test/SmokeTest.Tests/OPAContainerFixture.cs @@ -1,49 +1,47 @@ -using DotNet.Testcontainers.Containers; -using Docker.DotNet.Models; - -namespace SmokeTest.Tests; +namespace SmokeTest.Tests; public class OPAContainerFixture : IAsyncLifetime { - // Note: We disable this warning because we control when/how the constructor - // will be invoked for this class. + // Note: We disable this warning because we control when/how the constructor + // will be invoked for this class. #pragma warning disable CS8618 - private IContainer _container; -#pragma warning restore CS8618 + private IContainer _container; +#pragma warning restore CS8618 - public async Task InitializeAsync() - { - string[] startupFiles = { + public async Task InitializeAsync() + { + string[] startupFiles = { "testdata/policy.rego", "testdata/weird_name.rego", "testdata/simple/system.rego", + "testdata/condfail.rego", "testdata/data.json" }; - string[] opaCmd = { "run", "--server" }; - string[] startupCommand = new List().Concat(opaCmd).Concat(startupFiles).ToArray(); + string[] opaCmd = { "run", "--server" }; + var startupCommand = new List().Concat(opaCmd).Concat(startupFiles).ToArray(); - // Create a new instance of a container. - IContainer container = new ContainerBuilder() - .WithImage("openpolicyagent/opa:latest") - // Bind port 8181 of the container to a random port on the host. - .WithPortBinding(8181, true) - .WithCommand(startupCommand) - // Map our policy and data files into the container instance. - .WithResourceMapping(new DirectoryInfo("testdata"), "/testdata/") - // Wait until the HTTP endpoint of the container is available. - .WithWaitStrategy(Wait.ForUnixContainer().UntilHttpRequestIsSucceeded(r => r.ForPort(8181).ForPath("/health"))) - // Build the container configuration. - .Build(); + // Create a new instance of a container. + var container = new ContainerBuilder() + .WithImage("openpolicyagent/opa:latest") + // Bind port 8181 of the container to a random port on the host. + .WithPortBinding(8181, true) + .WithCommand(startupCommand) + // Map our policy and data files into the container instance. + .WithResourceMapping(new DirectoryInfo("testdata"), "/testdata/") + // Wait until the HTTP endpoint of the container is available. + .WithWaitStrategy(Wait.ForUnixContainer().UntilHttpRequestIsSucceeded(r => r.ForPort(8181).ForPath("/health"))) + // Build the container configuration. + .Build(); - // Start the container. - await container.StartAsync() - .ConfigureAwait(false); - _container = container; - } - public async Task DisposeAsync() - { - await _container.DisposeAsync(); - } + // Start the container. + await container.StartAsync() + .ConfigureAwait(false); + _container = container; + } + public async Task DisposeAsync() + { + await _container.DisposeAsync(); + } - // Expose the container for tests - public IContainer GetContainer() => _container; + // Expose the container for tests + public IContainer GetContainer() => _container; } diff --git a/test/SmokeTest.Tests/OpenApiTest.cs b/test/SmokeTest.Tests/OpenApiTest.cs index 64fa5dc..49b146a 100644 --- a/test/SmokeTest.Tests/OpenApiTest.cs +++ b/test/SmokeTest.Tests/OpenApiTest.cs @@ -1,331 +1,339 @@ -using Styra.Opa.OpenApi; -using Styra.Opa.OpenApi.Models.Requests; -using Styra.Opa.OpenApi.Models.Components; -using Styra.Opa.OpenApi.Models.Errors; - -namespace SmokeTest.Tests; - -public class OpenApiTest : IClassFixture, IClassFixture -{ - public IContainer _containerOpa; - public IContainer _containerEopa; - - public OpenApiTest(OPAContainerFixture opaFixture, EOPAContainerFixture eopaFixture) - { - _containerOpa = opaFixture.GetContainer(); - _containerEopa = eopaFixture.GetContainer(); - } - - private OpaApiClient GetOpaApiClient() - { - // Construct the request URI by specifying the scheme, hostname, assigned random host port, and the endpoint "uuid". - var requestUri = new UriBuilder(Uri.UriSchemeHttp, _containerOpa.Hostname, _containerOpa.GetMappedPublicPort(8181)).Uri; - - // Send an HTTP GET request to the specified URI and retrieve the response as a string. - return new OpaApiClient(serverIndex: 0, serverUrl: requestUri.ToString()); - } - - private OpaApiClient GetEOpaApiClient() - { - // Construct the request URI by specifying the scheme, hostname, assigned random host port, and the endpoint "uuid". - var requestUri = new UriBuilder(Uri.UriSchemeHttp, _containerEopa.Hostname, _containerEopa.GetMappedPublicPort(8181)).Uri; - - // Send an HTTP GET request to the specified URI and retrieve the response as a string. - return new OpaApiClient(serverIndex: 0, serverUrl: requestUri.ToString()); - } - - [Fact] - public async Task OpenApiClientRBACTestcontainersTest() - { - var client = GetOpaApiClient(); - - // Exercise the low-level OPA C# SDK. - ExecutePolicyWithInputRequest req = new ExecutePolicyWithInputRequest() - { - Path = "app/rbac", - RequestBody = new ExecutePolicyWithInputRequestBody() - { - Input = Input.CreateMapOfAny( - new Dictionary() { - { "user", "alice" }, - { "action", "read" }, - { "object", "id123" }, - { "type", "dog" }, - }), - }, - }; - - var res = await client.ExecutePolicyWithInputAsync(req); - var resultMap = res.SuccessfulPolicyResponse?.Result?.MapOfAny; - - // Ensure we got back the expected fields from the eval. - Assert.Equal(true, resultMap?.GetValueOrDefault("allow", false)); - Assert.Equal(true, resultMap?.GetValueOrDefault("user_is_admin", false)); - Assert.Equal(new List(), resultMap?.GetValueOrDefault("user_is_granted")); - } - - [Fact] - public async Task OpenApiClientEncodedPathTest() - { - var client = GetOpaApiClient(); - - // Exercise the low-level OPA C# SDK. - ExecutePolicyRequest req = new ExecutePolicyRequest() - { - Path = "this/is%2fallowed/pkg", - }; - - var res = await client.ExecutePolicyAsync(req); - var resultMap = res.SuccessfulPolicyResponse?.Result?.MapOfAny; - - Assert.Equal(true, resultMap?.GetValueOrDefault("allow", false)); - } - - [Fact] - public async Task OpenApiClientEvaluateDefaultTest() - { - var client = GetOpaApiClient(); - - // Note(philip): Due to how the API is generated, we have to fire off - // requests directly-- there's no building of requests in advance for later - // launching. - var res = await client.ExecuteDefaultPolicyWithInputAsync(Input.CreateMapOfAny( - new Dictionary() { - { "hello", "world" }, - })); - var resultMap = res.Result?.MapOfAny; - - // Ensure we got back the expected fields from the eval. - Assert.Equal("this is the default path", resultMap?.GetValueOrDefault("msg", "")); - Assert.Equal(new Dictionary() { { "hello", "world" } }, resultMap?.GetValueOrDefault("echo", "")); - } - - [Fact] - public async Task OpenApiClientBatchPolicyNoInputTest() - { - // Currently, this API only exists in Enterprise OPA. - var client = GetEOpaApiClient(); - - ExecuteBatchPolicyWithInputRequest req = new ExecuteBatchPolicyWithInputRequest() - { - Path = "app/rbac", - RequestBody = new ExecuteBatchPolicyWithInputRequestBody() - { - Inputs = new Dictionary() { }, - } - }; - - var res = await client.ExecuteBatchPolicyWithInputAsync(req); - - // Assert we get the expected "success" response - Assert.Equal(200, res.StatusCode); - - // Assert no responses. - Assert.NotNull(res.BatchSuccessfulPolicyEvaluation); - Assert.Null(res.BatchSuccessfulPolicyEvaluation?.Responses); - } - - [Fact] - public async Task OpenApiClientBatchPolicyAllSuccessTest() - { - // Currently, this API only exists in Enterprise OPA. - var client = GetEOpaApiClient(); - - ExecuteBatchPolicyWithInputRequest req = new ExecuteBatchPolicyWithInputRequest() - { - Path = "app/rbac", - RequestBody = new ExecuteBatchPolicyWithInputRequestBody() - { - Inputs = new Dictionary() { - {"AAA", Input.CreateMapOfAny( - new Dictionary() { - { "user", "alice" }, - { "action", "read" }, - { "object", "id123" }, - { "type", "dog" }, - }) - }, - {"BBB", Input.CreateMapOfAny( - new Dictionary() { - { "user", "eve" }, - { "action", "write" }, - { "object", "id123" }, - { "type", "dog" }, - }) - }, - } - } - }; - - var res = await client.ExecuteBatchPolicyWithInputAsync(req); - - // Assert we get the expected "success" response - Assert.Equal(200, res.StatusCode); - - var responsesMap = res.BatchSuccessfulPolicyEvaluation?.Responses; - { - var resp = responsesMap?.GetValueOrDefault("AAA"); - Assert.NotNull(resp); - Assert.Equal(true, resp?.Result?.MapOfAny?.GetValueOrDefault("allow")); - } - { - var resp = responsesMap?.GetValueOrDefault("BBB"); - Assert.Equal(false, resp?.Result?.MapOfAny?.GetValueOrDefault("allow")); - } - } - - [Fact] - public async Task OpenApiClientBatchPolicyMixedTest() - { - // Currently, this API only exists in Enterprise OPA. - var client = GetEOpaApiClient(); - - ExecuteBatchPolicyWithInputRequest req = new ExecuteBatchPolicyWithInputRequest() - { - Path = "testmod/condfail", - RequestBody = new ExecuteBatchPolicyWithInputRequestBody() - { - Inputs = new Dictionary() { - {"AAA", Input.CreateMapOfAny( - new Dictionary() { - { "x", new List {1, 1, 3} }, - { "y", new List {1, 1, 1} }, - }) - }, - {"BBB", Input.CreateMapOfAny( - new Dictionary() { - { "x", new List {1, 1, 3} }, - { "y", new List {1, 2, 1} }, - }) - }, - {"CCC", Input.CreateMapOfAny( - new Dictionary() { - { "x", new List {1, 1, 3} }, - { "y", new List {1, 1, 1} }, - }) - } - }, - } - }; - - var res = await client.ExecuteBatchPolicyWithInputAsync(req); - - // Assert we get the expected "success" response - Assert.Equal(207, res.StatusCode); - - var responsesMap = res.BatchMixedResults?.Responses; - { - var resp = responsesMap?.GetValueOrDefault("AAA"); - Assert.NotNull(resp); - Assert.Equivalent(new Dictionary() { { "1", 2 }, { "3", 4 } }, resp?.ResponsesSuccessfulPolicyResponse?.Result?.MapOfAny?.GetValueOrDefault("p")); - Assert.Equal("200", resp?.ResponsesSuccessfulPolicyResponse?.HttpStatusCode); - } - { - var resp = responsesMap?.GetValueOrDefault("BBB"); - Assert.NotNull(resp?.ServerError); - Assert.Equal("internal_error", resp?.ServerError?.Code); - Assert.Equal("500", resp?.ServerError?.HttpStatusCode); - Assert.Equal("object insert conflict", resp?.ServerError?.Message); - } - { - var resp = responsesMap?.GetValueOrDefault("CCC"); - Assert.NotNull(resp); - Assert.Equivalent(new Dictionary() { { "1", 2 }, { "3", 4 } }, resp?.ResponsesSuccessfulPolicyResponse?.Result?.MapOfAny?.GetValueOrDefault("p")); - Assert.Equal("200", resp?.ResponsesSuccessfulPolicyResponse?.HttpStatusCode); - } - } - - [Fact] - public async Task OpenApiClientBatchPolicyAllFailureTest() - { - // Currently, this API only exists in Enterprise OPA. - var client = GetEOpaApiClient(); - - ExecuteBatchPolicyWithInputRequest req = new ExecuteBatchPolicyWithInputRequest() - { - Path = "testmod/condfail", - RequestBody = new ExecuteBatchPolicyWithInputRequestBody() - { - Inputs = new Dictionary() { - {"AAA", Input.CreateMapOfAny( - new Dictionary() { - { "x", new List {1, 1, 3} }, - { "y", new List {1, 2, 1} }, - }) - }, - {"BBB", Input.CreateMapOfAny( - new Dictionary() { - { "x", new List {1, 1, 3} }, - { "y", new List {1, 2, 1} }, - }) - }, - {"CCC", Input.CreateMapOfAny( - new Dictionary() { - { "x", new List {1, 1, 3} }, - { "y", new List {1, 2, 1} }, - }) - } - }, - } - }; - - // We populate this variable later in the catch block, otherwise this would - // not be a terribly good idea. - Dictionary responsesMap = null!; - try - { - var res = await client.ExecuteBatchPolicyWithInputAsync(req); - } - catch (Exception ex) - { - if (ex is ClientError ce) - { - Assert.Fail(String.Format("ClientError: {0}, Message: {1}", ce.Code, ce.Message)); - } - else if (ex is BatchServerError bse) - { - Assert.NotNull(bse.Responses); - responsesMap = bse.Responses; - } - else if (ex is SDKException sdke) - { - Assert.Fail(String.Format("SDKException: {0}, Message: {1}", sdke.Body, sdke.Message)); - } - else - { - Assert.Fail(String.Format("Unknown Error: {0}, Message: {1}", ex, ex.Message)); - } - } - - { - var resp = responsesMap?.GetValueOrDefault("AAA"); - Assert.NotNull(resp); - Assert.Equal("internal_error", resp?.Code); - if (resp!.Message.Contains("eval_conflict_error")) - { - Assert.Fail("Test failure due to OPA Fallback mode. Please check the EOPA license environment variables."); - } - Assert.Contains("object insert conflict", resp?.Message); - } - { - var resp = responsesMap?.GetValueOrDefault("BBB"); - Assert.NotNull(resp); - Assert.Equal("internal_error", resp?.Code); - if (resp!.Message.Contains("eval_conflict_error")) - { - Assert.Fail("Test failure due to OPA Fallback mode. Please check the EOPA license environment variables."); - } - Assert.Equal("object insert conflict", resp?.Message); - } - { - var resp = responsesMap?.GetValueOrDefault("CCC"); - Assert.NotNull(resp); - Assert.Equal("internal_error", resp?.Code); - if (resp!.Message.Contains("eval_conflict_error")) - { - Assert.Fail("Test failure due to OPA Fallback mode. Please check the EOPA license environment variables."); - } - Assert.Equal("object insert conflict", resp?.Message); - } - } -} +using Styra.Opa.OpenApi; +using Styra.Opa.OpenApi.Models.Components; +using Styra.Opa.OpenApi.Models.Errors; +using Styra.Opa.OpenApi.Models.Requests; + +namespace SmokeTest.Tests; + +public class OpenApiTest : IClassFixture, IClassFixture +{ + public IContainer _containerOpa; + public IContainer _containerEopa; + + public OpenApiTest(OPAContainerFixture opaFixture, EOPAContainerFixture eopaFixture) + { + _containerOpa = opaFixture.GetContainer(); + _containerEopa = eopaFixture.GetContainer(); + } + + private OpaApiClient GetOpaApiClient() + { + // Construct the request URI by specifying the scheme, hostname, assigned random host port, and the endpoint "uuid". + var requestUri = new UriBuilder(Uri.UriSchemeHttp, _containerOpa.Hostname, _containerOpa.GetMappedPublicPort(8181)).Uri; + + // Send an HTTP GET request to the specified URI and retrieve the response as a string. + return new OpaApiClient(serverIndex: 0, serverUrl: requestUri.ToString()); + } + + private OpaApiClient GetEOpaApiClient() + { + // Construct the request URI by specifying the scheme, hostname, assigned random host port, and the endpoint "uuid". + var requestUri = new UriBuilder(Uri.UriSchemeHttp, _containerEopa.Hostname, _containerEopa.GetMappedPublicPort(8181)).Uri; + + // Send an HTTP GET request to the specified URI and retrieve the response as a string. + return new OpaApiClient(serverIndex: 0, serverUrl: requestUri.ToString()); + } + + [Fact] + public async Task OpenApiClientRBACTestcontainersTest() + { + var client = GetOpaApiClient(); + + // Exercise the low-level OPA C# SDK. + var req = new ExecutePolicyWithInputRequest() + { + Path = "app/rbac", + RequestBody = new ExecutePolicyWithInputRequestBody() + { + Input = Input.CreateMapOfAny( + new Dictionary() { + { "user", "alice" }, + { "action", "read" }, + { "object", "id123" }, + { "type", "dog" }, + }), + }, + }; + + var res = await client.ExecutePolicyWithInputAsync(req); + var resultMap = res.SuccessfulPolicyResponse?.Result?.MapOfAny; + + // Ensure we got back the expected fields from the eval. + Assert.Equal(true, resultMap?.GetValueOrDefault("allow", false)); + Assert.Equal(true, resultMap?.GetValueOrDefault("user_is_admin", false)); + Assert.Equal(new List(), resultMap?.GetValueOrDefault("user_is_granted")); + } + + [Fact] + public async Task OpenApiClientEncodedPathTest() + { + var client = GetOpaApiClient(); + + // Exercise the low-level OPA C# SDK. + var req = new ExecutePolicyRequest() + { + Path = "this/is%2fallowed/pkg", + }; + + var res = await client.ExecutePolicyAsync(req); + var resultMap = res.SuccessfulPolicyResponse?.Result?.MapOfAny; + + Assert.Equal(true, resultMap?.GetValueOrDefault("allow", false)); + } + + [Fact] + public async Task OpenApiClientEvaluateDefaultTest() + { + var client = GetOpaApiClient(); + + // Note(philip): Due to how the API is generated, we have to fire off + // requests directly-- there's no building of requests in advance for later + // launching. + var res = await client.ExecuteDefaultPolicyWithInputAsync(Input.CreateMapOfAny( + new Dictionary() { + { "hello", "world" }, + })); + var resultMap = res.Result?.MapOfAny; + + // Ensure we got back the expected fields from the eval. + Assert.Equal("this is the default path", resultMap?.GetValueOrDefault("msg", "")); + Assert.Equal(new Dictionary() { { "hello", "world" } }, resultMap?.GetValueOrDefault("echo", "")); + } + + [Fact] + public async Task OpenApiClientBatchPolicyNoInputTest() + { + // Currently, this API only exists in Enterprise OPA. + var client = GetEOpaApiClient(); + + var req = new ExecuteBatchPolicyWithInputRequest() + { + Path = "app/rbac", + RequestBody = new ExecuteBatchPolicyWithInputRequestBody() + { + Inputs = new Dictionary() { }, + } + }; + + var res = await client.ExecuteBatchPolicyWithInputAsync(req); + + // Assert we get the expected "success" response + Assert.Equal(200, res.StatusCode); + + // Assert no responses. + Assert.NotNull(res.BatchSuccessfulPolicyEvaluation); + Assert.Null(res.BatchSuccessfulPolicyEvaluation?.Responses); + } + + [Fact] + public async Task OpenApiClientBatchPolicyAllSuccessTest() + { + // Currently, this API only exists in Enterprise OPA. + var client = GetEOpaApiClient(); + + var req = new ExecuteBatchPolicyWithInputRequest() + { + Path = "app/rbac", + RequestBody = new ExecuteBatchPolicyWithInputRequestBody() + { + Inputs = new Dictionary() { + {"AAA", Input.CreateMapOfAny( + new Dictionary() { + { "user", "alice" }, + { "action", "read" }, + { "object", "id123" }, + { "type", "dog" }, + }) + }, + {"BBB", Input.CreateMapOfAny( + new Dictionary() { + { "user", "eve" }, + { "action", "write" }, + { "object", "id123" }, + { "type", "dog" }, + }) + }, + } + } + }; + + var res = await client.ExecuteBatchPolicyWithInputAsync(req); + + // Assert we get the expected "success" response + Assert.Equal(200, res.StatusCode); + + var responsesMap = res.BatchSuccessfulPolicyEvaluation?.Responses; + { + var resp = responsesMap?.GetValueOrDefault("AAA"); + Assert.NotNull(resp); + Assert.Equal(true, resp?.Result?.MapOfAny?.GetValueOrDefault("allow")); + } + + { + var resp = responsesMap?.GetValueOrDefault("BBB"); + Assert.Equal(false, resp?.Result?.MapOfAny?.GetValueOrDefault("allow")); + } + } + + [Fact] + public async Task OpenApiClientBatchPolicyMixedTest() + { + // Currently, this API only exists in Enterprise OPA. + var client = GetEOpaApiClient(); + + var req = new ExecuteBatchPolicyWithInputRequest() + { + Path = "testmod/condfail", + RequestBody = new ExecuteBatchPolicyWithInputRequestBody() + { + Inputs = new Dictionary() { + {"AAA", Input.CreateMapOfAny( + new Dictionary() { + { "x", new List {1, 1, 3} }, + { "y", new List {1, 1, 1} }, + }) + }, + {"BBB", Input.CreateMapOfAny( + new Dictionary() { + { "x", new List {1, 1, 3} }, + { "y", new List {1, 2, 1} }, + }) + }, + {"CCC", Input.CreateMapOfAny( + new Dictionary() { + { "x", new List {1, 1, 3} }, + { "y", new List {1, 1, 1} }, + }) + } + }, + } + }; + + var res = await client.ExecuteBatchPolicyWithInputAsync(req); + + // Assert we get the expected "success" response + Assert.Equal(207, res.StatusCode); + + var responsesMap = res.BatchMixedResults?.Responses; + { + var resp = responsesMap?.GetValueOrDefault("AAA"); + Assert.NotNull(resp); + Assert.Equivalent(new Dictionary() { { "1", 2 }, { "3", 4 } }, resp?.ResponsesSuccessfulPolicyResponse?.Result?.MapOfAny?.GetValueOrDefault("p")); + Assert.Equal("200", resp?.ResponsesSuccessfulPolicyResponse?.HttpStatusCode); + } + + { + var resp = responsesMap?.GetValueOrDefault("BBB"); + Assert.NotNull(resp?.ServerError); + Assert.Equal("internal_error", resp?.ServerError?.Code); + Assert.Equal("500", resp?.ServerError?.HttpStatusCode); + Assert.Equal("object insert conflict", resp?.ServerError?.Message); + } + + { + var resp = responsesMap?.GetValueOrDefault("CCC"); + Assert.NotNull(resp); + Assert.Equivalent(new Dictionary() { { "1", 2 }, { "3", 4 } }, resp?.ResponsesSuccessfulPolicyResponse?.Result?.MapOfAny?.GetValueOrDefault("p")); + Assert.Equal("200", resp?.ResponsesSuccessfulPolicyResponse?.HttpStatusCode); + } + } + + [Fact] + public async Task OpenApiClientBatchPolicyAllFailureTest() + { + // Currently, this API only exists in Enterprise OPA. + var client = GetEOpaApiClient(); + + var req = new ExecuteBatchPolicyWithInputRequest() + { + Path = "testmod/condfail", + RequestBody = new ExecuteBatchPolicyWithInputRequestBody() + { + Inputs = new Dictionary() { + {"AAA", Input.CreateMapOfAny( + new Dictionary() { + { "x", new List {1, 1, 3} }, + { "y", new List {1, 2, 1} }, + }) + }, + {"BBB", Input.CreateMapOfAny( + new Dictionary() { + { "x", new List {1, 1, 3} }, + { "y", new List {1, 2, 1} }, + }) + }, + {"CCC", Input.CreateMapOfAny( + new Dictionary() { + { "x", new List {1, 1, 3} }, + { "y", new List {1, 2, 1} }, + }) + } + }, + } + }; + + // We populate this variable later in the catch block, otherwise this would + // not be a terribly good idea. + Dictionary responsesMap = null!; + try + { + var res = await client.ExecuteBatchPolicyWithInputAsync(req); + } + catch (Exception ex) + { + if (ex is ClientError ce) + { + Assert.Fail(string.Format("ClientError: {0}, Message: {1}", ce.Code, ce.Message)); + } + else if (ex is BatchServerError bse) + { + Assert.NotNull(bse.Responses); + responsesMap = bse.Responses; + } + else if (ex is SDKException sdke) + { + Assert.Fail(string.Format("SDKException: {0}, Message: {1}", sdke.Body, sdke.Message)); + } + else + { + Assert.Fail(string.Format("Unknown Error: {0}, Message: {1}", ex, ex.Message)); + } + } + + { + var resp = responsesMap?.GetValueOrDefault("AAA"); + Assert.NotNull(resp); + Assert.Equal("internal_error", resp?.Code); + if (resp!.Message.Contains("eval_conflict_error")) + { + Assert.Fail("Test failure due to OPA Fallback mode. Please check the EOPA license environment variables."); + } + + Assert.Contains("object insert conflict", resp?.Message); + } + + { + var resp = responsesMap?.GetValueOrDefault("BBB"); + Assert.NotNull(resp); + Assert.Equal("internal_error", resp?.Code); + if (resp!.Message.Contains("eval_conflict_error")) + { + Assert.Fail("Test failure due to OPA Fallback mode. Please check the EOPA license environment variables."); + } + + Assert.Equal("object insert conflict", resp?.Message); + } + + { + var resp = responsesMap?.GetValueOrDefault("CCC"); + Assert.NotNull(resp); + Assert.Equal("internal_error", resp?.Code); + if (resp!.Message.Contains("eval_conflict_error")) + { + Assert.Fail("Test failure due to OPA Fallback mode. Please check the EOPA license environment variables."); + } + + Assert.Equal("object insert conflict", resp?.Message); + } + } +} diff --git a/test/SmokeTest.Tests/Usings.cs b/test/SmokeTest.Tests/Usings.cs index c0ba6e0..4350e55 100644 --- a/test/SmokeTest.Tests/Usings.cs +++ b/test/SmokeTest.Tests/Usings.cs @@ -1,7 +1,3 @@ -global using Xunit; -global using DotNet.Testcontainers; +global using Xunit; global using DotNet.Testcontainers.Builders; global using DotNet.Testcontainers.Containers; -global using DotNet.Testcontainers.Images; -global using DotNet.Testcontainers.Networks; -global using Newtonsoft.Json;