Skip to content

Commit

Permalink
Merge pull request #41 from lbroudoux/feat/add_oidc_support
Browse files Browse the repository at this point in the history
Add contract testing for OIDC/OAuth2 protected resources
  • Loading branch information
lbroudoux authored Jan 30, 2025
2 parents 462bbcf + ba7b1e6 commit af189ad
Show file tree
Hide file tree
Showing 7 changed files with 469 additions and 0 deletions.
40 changes: 40 additions & 0 deletions src/Microcks.Testcontainers/Model/OAuth2AuthorizedClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//
// Copyright The Microcks Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License")
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
//

using System.Text.Json.Serialization;

namespace Microcks.Testcontainers.Model;

#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member

/// <summary>
/// Persisted information for an OAuth2 authorization/authentication done before launching a test.
/// </summary>
public class OAuth2AuthorizedClient
{
[JsonPropertyName("principalName")]
public string PrincipalName { get; set; }

[JsonPropertyName("tokenUri")]
public string TokenUri { get; set; }

[JsonPropertyName("scopes")]
public string Scopes { get; set; }

[JsonPropertyName("grantType")]
public OAuth2GrantType GrantType { get; set; }
}
95 changes: 95 additions & 0 deletions src/Microcks.Testcontainers/Model/OAuth2ClientContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,104 @@
//
//

using System.Text.Json.Serialization;

namespace Microcks.Testcontainers.Model;

#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member

/// <summary>
/// A volatile OAuth2 client context usually associated with a Test request.
/// </summary>
public class OAuth2ClientContext
{
[JsonPropertyName("clientId")]
public string ClientId { get; set; }

[JsonPropertyName("clientSecret")]
public string ClientSecret { get; set; }

[JsonPropertyName("tokenUri")]
public string TokenUri { get; set; }

[JsonPropertyName("scopes")]
public string Scopes { get; set; }

[JsonPropertyName("username")]
public string Username { get; set; }

[JsonPropertyName("password")]
public string Password { get; set; }

[JsonPropertyName("refreshToken")]
public string RefreshToken { get; set; }

[JsonPropertyName("grantType")]
public OAuth2GrantType GrantType { get; set; }
}

/// <summary>
/// A Builder to create OAuth2ClientContext using a fluid APi.
/// </summary>
public class OAuth2ClientContextBuilder
{
private readonly OAuth2ClientContext _context;

public OAuth2ClientContextBuilder()
{
_context = new OAuth2ClientContext();
}

public OAuth2ClientContextBuilder WithClientId(string clientId)
{
_context.ClientId = clientId;
return this;
}

public OAuth2ClientContextBuilder WithClientSecret(string clientSecret)
{
_context.ClientSecret = clientSecret;
return this;
}

public OAuth2ClientContextBuilder WithTokenUri(string tokenUri)
{
_context.TokenUri = tokenUri;
return this;
}

public OAuth2ClientContextBuilder WithScopes(string scopes)
{
_context.Scopes = scopes;
return this;
}

public OAuth2ClientContextBuilder WithUsername(string username)
{
_context.Username = username;
return this;
}

public OAuth2ClientContextBuilder WithPassword(string password)
{
_context.Password = password;
return this;
}

public OAuth2ClientContextBuilder WithRefreshToken(string refreshToken)
{
_context.RefreshToken = refreshToken;
return this;
}

public OAuth2ClientContextBuilder WithGrantType(OAuth2GrantType grantType)
{
_context.GrantType = grantType;
return this;
}

public OAuth2ClientContext Build()
{
return _context;
}
}
33 changes: 33 additions & 0 deletions src/Microcks.Testcontainers/Model/OAuth2GrantType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//
// Copyright The Microcks Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License")
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
//

using System.Text.Json.Serialization;

namespace Microcks.Testcontainers.Model;

#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member

/// <summary>
/// Enumeration for the different supported grants/flows of OAuth2.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum OAuth2GrantType
{
PASSWORD,
CLIENT_CREDENTIALS,
REFRESH_TOKEN
}
3 changes: 3 additions & 0 deletions src/Microcks.Testcontainers/Model/TestResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,7 @@ public class TestResult

[JsonPropertyName("operationsHeaders")]
public Dictionary<string, List<Header>> OperationsHeaders { get; set; }

[JsonPropertyName("authorizedClient")]
public OAuth2AuthorizedClient AuthorizedClient { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<PackageReference Include="RestAssured.Net" Version="4.6.0" />
<PackageReference Include="Confluent.Kafka" Version="2.6.1" />
<PackageReference Include="TestContainers.Kafka" Version="4.1.0" />
<PackageReference Include="TestContainers.Keycloak" Version="4.1.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
<PackageReference Include="xunit" Version="2.9.2" />
</ItemGroup>
Expand All @@ -30,6 +31,9 @@
<None Update="microcks-repository.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="myrealm-realm.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="subdir\weather-forecast-openapi.yaml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
//
// Copyright The Microcks Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License")
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
//

using DotNet.Testcontainers.Builders;
using DotNet.Testcontainers.Containers;
using DotNet.Testcontainers.Networks;
using Microcks.Testcontainers;
using Microcks.Testcontainers.Model;
using System;
using System.Net;
using Testcontainers.Keycloak;

namespace Testcontainers.Microcks.Tests;

public sealed class MicrocksContractTestingFunctionalityWithOAuth2Tests : IAsyncLifetime
{
private readonly INetwork _network = new NetworkBuilder().Build();
private readonly MicrocksContainer _microcksContainer;
private readonly IContainer _goodImpl;
private readonly KeycloakContainer _keycloak;

private static readonly string GOOD_PASTRY_IMAGE = "quay.io/microcks/contract-testing-demo:02";

public MicrocksContractTestingFunctionalityWithOAuth2Tests()
{
_microcksContainer = new MicrocksBuilder()
.WithNetwork(_network)
.Build();

_goodImpl = new ContainerBuilder()
.WithImage(GOOD_PASTRY_IMAGE)
.WithNetwork(_network)
.WithNetworkAliases("good-impl")
.WithWaitStrategy(Wait.ForUnixContainer().UntilMessageIsLogged(".*Example app listening on port 3002.*"))
.Build();

_keycloak = new KeycloakBuilder()
.WithImage("quay.io/keycloak/keycloak:26.0.0")
.WithNetwork(_network)
.WithNetworkAliases("keycloak")
.WithCommand("--import-realm")
.WithResourceMapping("./myrealm-realm.json", "/opt/keycloak/data/import")
.Build();
}

public Task DisposeAsync()
{
return Task.WhenAll(
_microcksContainer.DisposeAsync().AsTask(),
_keycloak.DisposeAsync().AsTask(),
_goodImpl.DisposeAsync().AsTask(),
_network.DisposeAsync().AsTask()
);
}

public Task InitializeAsync()
{
_microcksContainer.Started +=
(_, _) => _microcksContainer.ImportAsMainArtifact("apipastries-openapi.yaml");

return Task.WhenAll(
_microcksContainer.StartAsync(),
_keycloak.StartAsync(),
_goodImpl.StartAsync()
);
}

[Fact]
public void ShouldConfigRetrieval()
{
var uriBuilder = new UriBuilder(_microcksContainer.GetHttpEndpoint())
{
Path = "/api/keycloak/config"
};

Given()
.When()
.Get(uriBuilder.ToString())
.Then()
.StatusCode(HttpStatusCode.OK);
}

[Fact]
public async Task ShouldReturnSuccess_WhenGoodImplementation()
{
// Switch endpoint to good implementation
var testRequest = new TestRequest
{
ServiceId = "API Pastries:0.0.1",
RunnerType = TestRunnerType.OPEN_API_SCHEMA,
TestEndpoint = "http://good-impl:3002",
Timeout = TimeSpan.FromMilliseconds(2000),
oAuth2Context = new OAuth2ClientContextBuilder()
.WithClientId("myrealm-serviceaccount")
.WithClientSecret("ab54d329-e435-41ae-a900-ec6b3fe15c54")
.WithTokenUri("http://keycloak:8080/realms/myrealm/protocol/openid-connect/token")
.WithGrantType(OAuth2GrantType.CLIENT_CREDENTIALS)
.Build()
};
TestResult testResult = await _microcksContainer.TestEndpointAsync(testRequest);
Assert.True(testResult.Success);
Assert.Equal("http://good-impl:3002", testResult.TestedEndpoint);
Assert.Equal(3, testResult.TestCaseResults.Count);
Assert.Empty(testResult.TestCaseResults[0].TestStepResults[0].Message);

// Ensure test has used a valid OAuth2 client
Assert.NotNull(testResult.AuthorizedClient);
Assert.Equal("myrealm-serviceaccount", testResult.AuthorizedClient.PrincipalName);
Assert.Equal("http://keycloak:8080/realms/myrealm/protocol/openid-connect/token", testResult.AuthorizedClient.TokenUri);
Assert.Equal("openid profile email", testResult.AuthorizedClient.Scopes);
}
}
Loading

0 comments on commit af189ad

Please sign in to comment.