diff --git a/.github/workflows/pull-request.yaml b/.github/workflows/pull-request.yaml
new file mode 100644
index 0000000..1765622
--- /dev/null
+++ b/.github/workflows/pull-request.yaml
@@ -0,0 +1,46 @@
+name: Checks
+
+on:
+ workflow_dispatch: {}
+ pull_request: {}
+
+# When a new revision is pushed to a PR, cancel all in-progress CI runs for that
+# PR. See https://docs.github.com/en/actions/using-jobs/using-concurrency
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ build:
+ name: Build
+ runs-on: ubuntu-latest
+ steps:
+ - name: Tune GitHub-hosted runner network
+ uses: smorimoto/tune-github-hosted-runner-network@v1
+ - uses: actions/checkout@v4
+ - name: Setup dotnet
+ uses: actions/setup-dotnet@v3
+ with:
+ dotnet-version: "6.0.x"
+ - name: Install dependencies
+ run: dotnet restore
+ - name: Build
+ run: dotnet build
+
+ smoke-test:
+ name: Smoke Test
+ runs-on: ubuntu-latest
+ steps:
+ - name: Tune GitHub-hosted runner network
+ uses: smorimoto/tune-github-hosted-runner-network@v1
+ - uses: actions/checkout@v4
+ - name: Setup dotnet
+ uses: actions/setup-dotnet@v3
+ with:
+ dotnet-version: "6.0.x"
+ - name: Install dependencies
+ working-directory: test
+ run: dotnet restore
+ - name: Test
+ working-directory: test
+ run: dotnet test
diff --git a/Api/Api.csproj b/Api/Api.csproj
index 0341a93..91a1f87 100644
--- a/Api/Api.csproj
+++ b/Api/Api.csproj
@@ -10,12 +10,12 @@
true
1591
https://github.com/StyraInc/opa-csharp
+ true
-
diff --git a/test/SmokeTest.Tests/OPAEvalRBACTest.cs b/test/SmokeTest.Tests/OPAEvalRBACTest.cs
new file mode 100644
index 0000000..7cb0bbe
--- /dev/null
+++ b/test/SmokeTest.Tests/OPAEvalRBACTest.cs
@@ -0,0 +1,67 @@
+using Api;
+using Api.Models.Requests;
+using Api.Models.Components;
+
+namespace SmokeTest.Tests;
+
+public class OPAEvalRBACTest
+{
+ [Fact]
+ public async Task HelloTestContainers()
+ {
+ // Read in the test data files.
+ var policy = System.IO.File.ReadAllBytes(Path.Combine("testdata", "policy.rego"));
+ var data = System.IO.File.ReadAllBytes(Path.Combine("testdata", "data.json"));
+
+ // 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("run", "--server", "policy.rego", "data.json")
+ // Map our policy and data files into the container instance.
+ .WithResourceMapping(policy, "policy.rego")
+ .WithResourceMapping(data, "data.json")
+ // 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);
+
+ // Create a new instance of HttpClient to send HTTP requests.
+ var httpClient = new HttpClient();
+
+ // 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;
+
+ // Send an HTTP GET request to the specified URI and retrieve the response as a string.
+ var sdk = new Opa(serverIndex: 0, serverUrl: requestUri.ToString());
+
+ // 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 = Task.Run(() => sdk.ExecutePolicyWithInputAsync(req)).GetAwaiter().GetResult();
+ var resultMap = res.SuccessfulPolicyEvaluation?.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"));
+ }
+}
\ No newline at end of file
diff --git a/test/SmokeTest.Tests/SmokeTest.Tests.csproj b/test/SmokeTest.Tests/SmokeTest.Tests.csproj
new file mode 100644
index 0000000..a8238cc
--- /dev/null
+++ b/test/SmokeTest.Tests/SmokeTest.Tests.csproj
@@ -0,0 +1,39 @@
+
+
+
+ net6.0
+ enable
+ enable
+
+ false
+
+
+
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/SmokeTest.Tests/Usings.cs b/test/SmokeTest.Tests/Usings.cs
new file mode 100644
index 0000000..6c9d8e7
--- /dev/null
+++ b/test/SmokeTest.Tests/Usings.cs
@@ -0,0 +1,6 @@
+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;
\ No newline at end of file
diff --git a/test/SmokeTest.Tests/testdata/data.json b/test/SmokeTest.Tests/testdata/data.json
new file mode 100644
index 0000000..88ac41b
--- /dev/null
+++ b/test/SmokeTest.Tests/testdata/data.json
@@ -0,0 +1,63 @@
+{
+ "user_roles": {
+ "alice": [
+ "admin"
+ ],
+ "bob": [
+ "employee",
+ "billing"
+ ],
+ "eve": [
+ "customer"
+ ]
+ },
+ "role_grants": {
+ "customer": [
+ {
+ "action": "read",
+ "type": "dog"
+ },
+ {
+ "action": "read",
+ "type": "cat"
+ },
+ {
+ "action": "adopt",
+ "type": "dog"
+ },
+ {
+ "action": "adopt",
+ "type": "cat"
+ }
+ ],
+ "employee": [
+ {
+ "action": "read",
+ "type": "dog"
+ },
+ {
+ "action": "read",
+ "type": "cat"
+ },
+ {
+ "action": "update",
+ "type": "dog"
+ },
+ {
+ "action": "update",
+ "type": "cat"
+ }
+ ],
+ "billing": [
+ {
+ "action": "read",
+ "type": "finance"
+ },
+ {
+ "action": "update",
+ "type": "finance"
+ }
+ ]
+ }
+}
+
diff --git a/test/SmokeTest.Tests/testdata/policy.rego b/test/SmokeTest.Tests/testdata/policy.rego
new file mode 100644
index 0000000..35686b0
--- /dev/null
+++ b/test/SmokeTest.Tests/testdata/policy.rego
@@ -0,0 +1,52 @@
+# Role-based Access Control (RBAC)
+# --------------------------------
+#
+# This example defines an RBAC model for a Pet Store API. The Pet Store API allows
+# users to look at pets, adopt them, update their stats, and so on. The policy
+# controls which users can perform actions on which resources. The policy implements
+# a classic Role-based Access Control model where users are assigned to roles and
+# roles are granted the ability to perform some action(s) on some type of resource.
+#
+# This example shows how to:
+#
+# * Define an RBAC model in Rego that interprets role mappings represented in JSON.
+# * Iterate/search across JSON data structures (e.g., role mappings)
+#
+# For more information see:
+#
+# * Rego comparison to other systems: https://www.openpolicyagent.org/docs/latest/comparison-to-other-systems/
+# * Rego Iteration: https://www.openpolicyagent.org/docs/latest/#iteration
+
+package app.rbac
+
+import rego.v1
+
+# By default, deny requests.
+default allow := false
+
+# Allow admins to do anything.
+allow if user_is_admin
+
+# Allow the action if the user is granted permission to perform the action.
+allow if {
+ # Find grants for the user.
+ some grant in user_is_granted
+
+ # Check if the grant permits the action.
+ input.action == grant.action
+ input.type == grant.type
+}
+
+# user_is_admin is true if "admin" is among the user's roles as per data.user_roles
+user_is_admin if "admin" in data.user_roles[input.user]
+
+# user_is_granted is a set of grants for the user identified in the request.
+# The `grant` will be contained if the set `user_is_granted` for every...
+user_is_granted contains grant if {
+ # `role` assigned an element of the user_roles for this user...
+ some role in data.user_roles[input.user]
+
+ # `grant` assigned a single grant from the grants list for 'role'...
+ some grant in data.role_grants[role]
+}
+
diff --git a/test/test.sln b/test/test.sln
new file mode 100644
index 0000000..d6630b6
--- /dev/null
+++ b/test/test.sln
@@ -0,0 +1,22 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.0.31903.59
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SmokeTest.Tests", "SmokeTest.Tests\SmokeTest.Tests.csproj", "{32BE0D80-A8E6-49A3-822B-C73768A1423D}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {32BE0D80-A8E6-49A3-822B-C73768A1423D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {32BE0D80-A8E6-49A3-822B-C73768A1423D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {32BE0D80-A8E6-49A3-822B-C73768A1423D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {32BE0D80-A8E6-49A3-822B-C73768A1423D}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+EndGlobal