Experiment with setting up system test docker services
This commit is contained in:
@@ -11,6 +11,8 @@ public class JwtOptions
|
|||||||
public string MetadataUrl { get; set; } = "";
|
public string MetadataUrl { get; set; } = "";
|
||||||
|
|
||||||
public string? NameClaimType { get; set; }
|
public string? NameClaimType { get; set; }
|
||||||
|
|
||||||
|
public bool AllowHttpMetadataUrl { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class JwtOptionsValidator : AbstractValidator<JwtOptions>
|
public class JwtOptionsValidator : AbstractValidator<JwtOptions>
|
||||||
|
|||||||
@@ -18,14 +18,16 @@ public static class DependencyInjectionExtensions
|
|||||||
/// Adds all the WebApi related services to the Dependency Injection container.
|
/// Adds all the WebApi related services to the Dependency Injection container.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="services"></param>
|
/// <param name="services"></param>
|
||||||
public static void AddWebApiServices(this IServiceCollection services, IConfiguration configuration)
|
/// <param name="configuration"></param>
|
||||||
|
/// <param name="environment"></param>
|
||||||
|
public static void AddWebApiServices(this IServiceCollection services, IConfiguration configuration, IHostEnvironment environment)
|
||||||
{
|
{
|
||||||
services
|
services
|
||||||
.AddMiscellaneousServices()
|
.AddMiscellaneousServices()
|
||||||
.AddOpenApi()
|
.AddOpenApi()
|
||||||
.AddApiVersioning()
|
.AddApiVersioning()
|
||||||
.AddOtel()
|
.AddOtel()
|
||||||
.AddAuthenticationAndAuthorization()
|
.AddAuthenticationAndAuthorization(environment)
|
||||||
.AddDbContext(configuration);
|
.AddDbContext(configuration);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,7 +115,7 @@ public static class DependencyInjectionExtensions
|
|||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IServiceCollection AddAuthenticationAndAuthorization(this IServiceCollection services)
|
private static IServiceCollection AddAuthenticationAndAuthorization(this IServiceCollection services, IHostEnvironment environment)
|
||||||
{
|
{
|
||||||
services.AddOptions<JwtOptions>()
|
services.AddOptions<JwtOptions>()
|
||||||
.BindConfiguration(JwtOptions.SectionName)
|
.BindConfiguration(JwtOptions.SectionName)
|
||||||
@@ -134,6 +136,8 @@ public static class DependencyInjectionExtensions
|
|||||||
{
|
{
|
||||||
o.TokenValidationParameters.NameClaimType = jwtOptions.Value.NameClaimType;
|
o.TokenValidationParameters.NameClaimType = jwtOptions.Value.NameClaimType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
o.RequireHttpsMetadata = !jwtOptions.Value.AllowHttpMetadataUrl && !environment.IsDevelopment();
|
||||||
});
|
});
|
||||||
|
|
||||||
services.AddAuthorizationBuilder()
|
services.AddAuthorizationBuilder()
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ internal static class StartupExtensions
|
|||||||
{
|
{
|
||||||
builder.Configuration.AddEnvironmentVariables("Vegasco_");
|
builder.Configuration.AddEnvironmentVariables("Vegasco_");
|
||||||
|
|
||||||
builder.Services.AddWebApiServices(builder.Configuration);
|
builder.Services.AddWebApiServices(builder.Configuration, builder.Environment);
|
||||||
|
|
||||||
WebApplication app = builder.Build();
|
WebApplication app = builder.Build();
|
||||||
return app;
|
return app;
|
||||||
|
|||||||
166
tests/WebApi.Tests.System/ComposeService.cs
Normal file
166
tests/WebApi.Tests.System/ComposeService.cs
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
using Ductus.FluentDocker.Services;
|
||||||
|
using Ductus.FluentDocker.Services.Extensions;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
|
||||||
|
namespace WebApi.Tests.System;
|
||||||
|
|
||||||
|
public abstract class ComposeService : IDisposable
|
||||||
|
{
|
||||||
|
public string ServiceName { get; init; }
|
||||||
|
public string ServiceInternalPort { get; init; }
|
||||||
|
public string ServiceInternalProtocol { get; init; }
|
||||||
|
public string ServiceInternalPortAndProtocol => $"{ServiceInternalPort}/{ServiceInternalProtocol}";
|
||||||
|
|
||||||
|
private readonly ICompositeService _dockerService;
|
||||||
|
private readonly bool _isTestRunningInContainer;
|
||||||
|
|
||||||
|
private IContainerService? _container;
|
||||||
|
private bool _hasCheckedForContainer;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Not null, if <see cref="ContainerExists"/> is true.
|
||||||
|
/// </summary>
|
||||||
|
public IContainerService? Container
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_hasCheckedForContainer)
|
||||||
|
{
|
||||||
|
return _container;
|
||||||
|
}
|
||||||
|
|
||||||
|
_container ??= _dockerService.Containers.First(x => x.Name == ServiceName);
|
||||||
|
_hasCheckedForContainer = true;
|
||||||
|
|
||||||
|
return _container;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[MemberNotNullWhen(returnValue: true, nameof(Container))]
|
||||||
|
public bool ContainerExists => Container is not null;
|
||||||
|
|
||||||
|
public ComposeService(
|
||||||
|
ICompositeService dockerService,
|
||||||
|
bool isTestRunningInContainer,
|
||||||
|
string serviceName,
|
||||||
|
string serviceInternalPort,
|
||||||
|
string serviceInternalProtocol = "tcp")
|
||||||
|
{
|
||||||
|
_dockerService = dockerService;
|
||||||
|
_isTestRunningInContainer = isTestRunningInContainer;
|
||||||
|
ServiceName = serviceName;
|
||||||
|
ServiceInternalPort = serviceInternalPort;
|
||||||
|
ServiceInternalProtocol = serviceInternalProtocol;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? GetServiceUrl()
|
||||||
|
{
|
||||||
|
if (!ContainerExists)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _isTestRunningInContainer
|
||||||
|
? GetServiceUrlWhenRunningInsideContainer()
|
||||||
|
: GetUrlFromOutsideContainer(Container, ServiceInternalPortAndProtocol);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetServiceUrlWhenRunningInsideContainer()
|
||||||
|
{
|
||||||
|
return $"http://{ServiceName}:{ServiceInternalPort}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetUrlFromOutsideContainer(IContainerService container, string portAndProto)
|
||||||
|
{
|
||||||
|
var ipEndpoint = container.ToHostExposedEndpoint(portAndProto);
|
||||||
|
return $"http://{ipEndpoint.Address}:{ipEndpoint.Port}";
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (disposing)
|
||||||
|
{
|
||||||
|
_container?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Dispose(true);
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class AppContainer : ComposeService
|
||||||
|
{
|
||||||
|
public AppContainer(ICompositeService dockerService, bool isTestRunningInContainer)
|
||||||
|
: base(dockerService, isTestRunningInContainer, "app", "8080")
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class LoginContainer : ComposeService
|
||||||
|
{
|
||||||
|
public LoginContainer(ICompositeService dockerService, bool isTestRunningInContainer)
|
||||||
|
: base(dockerService, isTestRunningInContainer, "login", "8080")
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public sealed class TestAppContainer : IDisposable
|
||||||
|
{
|
||||||
|
private IContainerService? _testApplicationContainer;
|
||||||
|
private bool _hasCheckedForThisContainer;
|
||||||
|
public IContainerService? Container
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (!_hasCheckedForThisContainer)
|
||||||
|
{
|
||||||
|
_testApplicationContainer = _dockerHost.GetRunningContainers()
|
||||||
|
.FirstOrDefault(x => x.Id.StartsWith(Environment.MachineName));
|
||||||
|
|
||||||
|
if (_testApplicationContainer is not null)
|
||||||
|
{
|
||||||
|
// If the test is running inside a container (i.e. usually in a pipeline), we do not want to mess with the container, just release the resources held by this program
|
||||||
|
_testApplicationContainer.RemoveOnDispose = false;
|
||||||
|
_testApplicationContainer.StopOnDispose = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_hasCheckedForThisContainer = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _testApplicationContainer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool _hasCheckedIfTestRunInContainer;
|
||||||
|
private bool _isTestRunningInContainer;
|
||||||
|
public bool IsTestRunningInContainer
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (!_hasCheckedIfTestRunInContainer)
|
||||||
|
{
|
||||||
|
_isTestRunningInContainer = Container is not null;
|
||||||
|
_hasCheckedIfTestRunInContainer = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _isTestRunningInContainer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private readonly IHostService _dockerHost;
|
||||||
|
|
||||||
|
public TestAppContainer(IHostService dockerHost)
|
||||||
|
{
|
||||||
|
_dockerHost = dockerHost;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_testApplicationContainer?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
13
tests/WebApi.Tests.System/Constants.cs
Normal file
13
tests/WebApi.Tests.System/Constants.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
namespace WebApi.Tests.System;
|
||||||
|
|
||||||
|
public static class Constants
|
||||||
|
{
|
||||||
|
public static class Login
|
||||||
|
{
|
||||||
|
public const string ClientId = "vegasco";
|
||||||
|
public const string ClientSecret = "siIgnkijkkIxeQ9BDNwnGGUb60S53QZh";
|
||||||
|
public const string Username = "test.user";
|
||||||
|
public const string Password = "T3sttest.";
|
||||||
|
public const string Realm = "development";
|
||||||
|
}
|
||||||
|
}
|
||||||
301
tests/WebApi.Tests.System/DockerComposeFixture.cs
Normal file
301
tests/WebApi.Tests.System/DockerComposeFixture.cs
Normal file
@@ -0,0 +1,301 @@
|
|||||||
|
using Ductus.FluentDocker.Builders;
|
||||||
|
using Ductus.FluentDocker.Extensions;
|
||||||
|
using Ductus.FluentDocker.Model.Common;
|
||||||
|
using Ductus.FluentDocker.Model.Compose;
|
||||||
|
using Ductus.FluentDocker.Services;
|
||||||
|
using Ductus.FluentDocker.Services.Extensions;
|
||||||
|
|
||||||
|
namespace WebApi.Tests.System;
|
||||||
|
|
||||||
|
public sealed class DockerComposeFixture : IDisposable
|
||||||
|
{
|
||||||
|
private const string ComposeFileName = "compose.system.yaml";
|
||||||
|
|
||||||
|
private const string AppServiceName = "app";
|
||||||
|
private const string AppServiceInternalPort = "8080";
|
||||||
|
private const string AppServiceInternalPortAndProtocol = $"{AppServiceInternalPort}/tcp";
|
||||||
|
|
||||||
|
private const string LoginServiceName = "login";
|
||||||
|
private const string LoginServiceInternalPort = "8080";
|
||||||
|
private const string LoginServiceInternalPortAndProtocol = $"{LoginServiceInternalPort}/tcp";
|
||||||
|
|
||||||
|
private static readonly string ComposeFilePath = Path.GetFullPath(Path.Combine("../../..", ComposeFileName));
|
||||||
|
|
||||||
|
private readonly ICompositeService _dockerService;
|
||||||
|
|
||||||
|
|
||||||
|
private bool _hasCheckedForThisContainer;
|
||||||
|
private bool _hasCheckedIfTestRunInContainer;
|
||||||
|
|
||||||
|
private IHostService? _host;
|
||||||
|
|
||||||
|
private bool _isTestRunningInContainer;
|
||||||
|
private INetworkService? _networkService;
|
||||||
|
private IContainerService? _testApplicationContainer;
|
||||||
|
|
||||||
|
public DockerComposeFixture()
|
||||||
|
{
|
||||||
|
_dockerService = GetDockerComposeServices();
|
||||||
|
_dockerService.Start();
|
||||||
|
AttachDockerNetworksIfRunningInContainer();
|
||||||
|
}
|
||||||
|
|
||||||
|
private IContainerService? _appContainer;
|
||||||
|
public IContainerService AppContainer
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
_appContainer ??= _dockerService.Containers.First(x => x.Name == AppServiceName);
|
||||||
|
return _appContainer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private IContainerService? _loginContainer;
|
||||||
|
public IContainerService LoginContainer
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
_loginContainer ??= _dockerService.Containers.First(x => x.Name == LoginServiceName);
|
||||||
|
return _loginContainer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public IContainerService? TestApplicationContainer
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (!_hasCheckedForThisContainer)
|
||||||
|
{
|
||||||
|
_testApplicationContainer = DockerHost.GetRunningContainers()
|
||||||
|
.FirstOrDefault(x => x.Id.StartsWith(Environment.MachineName));
|
||||||
|
|
||||||
|
if (_testApplicationContainer is not null)
|
||||||
|
{
|
||||||
|
// If the test is running inside a container (i.e. usually in a pipeline), we do not want to mess with the container, just release the resources held by this program
|
||||||
|
_testApplicationContainer.RemoveOnDispose = false;
|
||||||
|
_testApplicationContainer.StopOnDispose = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_hasCheckedForThisContainer = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _testApplicationContainer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public IHostService DockerHost
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var hosts = new Hosts().Discover();
|
||||||
|
_host = hosts.FirstOrDefault(x => x.IsNative) ??
|
||||||
|
hosts.FirstOrDefault(x => x.Name == "default") ??
|
||||||
|
hosts.FirstOrDefault();
|
||||||
|
|
||||||
|
if (_host is null) throw new InvalidOperationException("No docker host found");
|
||||||
|
|
||||||
|
return _host;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsTestRunningInContainer
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (!_hasCheckedIfTestRunInContainer)
|
||||||
|
{
|
||||||
|
_isTestRunningInContainer = TestApplicationContainer is not null;
|
||||||
|
_hasCheckedIfTestRunInContainer = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _isTestRunningInContainer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_networkService?.Dispose();
|
||||||
|
|
||||||
|
_testApplicationContainer?.Dispose();
|
||||||
|
_appContainer?.Dispose();
|
||||||
|
_loginContainer?.Dispose();
|
||||||
|
|
||||||
|
// Kill container because otherwise the _dockerService.Dispose() takes much longer
|
||||||
|
KillDockerComposeServices();
|
||||||
|
|
||||||
|
_dockerService.Dispose();
|
||||||
|
|
||||||
|
_host?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private ICompositeService GetDockerComposeServices()
|
||||||
|
{
|
||||||
|
var services = new Builder()
|
||||||
|
.UseContainer()
|
||||||
|
.UseCompose()
|
||||||
|
.AssumeComposeVersion(ComposeVersion.V2)
|
||||||
|
.FromFile((TemplateString)ComposeFilePath)
|
||||||
|
.ForceBuild()
|
||||||
|
.RemoveOrphans()
|
||||||
|
.Wait("app", WaitForApplicationToListenToRequests)
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int WaitForApplicationToListenToRequests(IContainerService container, int iteration)
|
||||||
|
{
|
||||||
|
const int maxTryCount = 15;
|
||||||
|
ArgumentOutOfRangeException.ThrowIfGreaterThan(iteration, maxTryCount);
|
||||||
|
|
||||||
|
var isStarted = container.Logs().ReadToEnd().Reverse().Any(x => x.Contains("Now listening on:"));
|
||||||
|
return isStarted ? 0 : 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AttachDockerNetworksIfRunningInContainer()
|
||||||
|
{
|
||||||
|
if (!IsTestRunningInContainer) return;
|
||||||
|
|
||||||
|
var randomNetworkName = Guid.NewGuid().ToString("N");
|
||||||
|
_networkService = DockerHost.CreateNetwork(randomNetworkName, removeOnDispose: true);
|
||||||
|
|
||||||
|
_networkService.Attach(AppContainer, true, AppServiceName);
|
||||||
|
_networkService.Attach(TestApplicationContainer, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetAppUrl()
|
||||||
|
{
|
||||||
|
return IsTestRunningInContainer
|
||||||
|
? GetAppUrlWhenRunningInsideContainer()
|
||||||
|
: GetUrlFromOutsideContainer(AppContainer, AppServiceInternalPortAndProtocol);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetAppUrlWhenRunningInsideContainer()
|
||||||
|
{
|
||||||
|
return $"http://{AppServiceName}:{AppServiceInternalPort}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetLoginUrl()
|
||||||
|
{
|
||||||
|
return IsTestRunningInContainer
|
||||||
|
? GetLoginUrlWhenRunningInsideContainer()
|
||||||
|
: GetUrlFromOutsideContainer(LoginContainer, LoginServiceInternalPortAndProtocol);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetLoginUrlWhenRunningInsideContainer()
|
||||||
|
{
|
||||||
|
return $"http://{LoginServiceName}:{LoginServiceInternalPort}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetUrlFromOutsideContainer(IContainerService container, string portAndProto)
|
||||||
|
{
|
||||||
|
var ipEndpoint = container.ToHostExposedEndpoint(portAndProto);
|
||||||
|
return $"http://{ipEndpoint.Address}:{ipEndpoint.Port}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void KillDockerComposeServices()
|
||||||
|
{
|
||||||
|
foreach (var container in _dockerService.Containers) container.Remove(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public sealed class DockerComposeFixture2 : IDisposable
|
||||||
|
{
|
||||||
|
private const string ComposeFileName = "compose.system.yaml";
|
||||||
|
|
||||||
|
private static readonly string ComposeFilePath = Path.GetFullPath(Path.Combine("../../..", ComposeFileName));
|
||||||
|
|
||||||
|
private readonly ICompositeService _dockerService;
|
||||||
|
|
||||||
|
private IHostService? _host;
|
||||||
|
|
||||||
|
private INetworkService? _networkService;
|
||||||
|
|
||||||
|
public AppContainer AppContainer { get; init; }
|
||||||
|
public LoginContainer LoginContainer { get; init; }
|
||||||
|
public TestAppContainer TestApplicationContainer { get; init; }
|
||||||
|
|
||||||
|
public DockerComposeFixture2()
|
||||||
|
{
|
||||||
|
_dockerService = GetDockerComposeServices();
|
||||||
|
_dockerService.Start();
|
||||||
|
|
||||||
|
TestApplicationContainer = new TestAppContainer(DockerHost);
|
||||||
|
AppContainer = new AppContainer(_dockerService, TestApplicationContainer.IsTestRunningInContainer);
|
||||||
|
LoginContainer = new LoginContainer(_dockerService, TestApplicationContainer.IsTestRunningInContainer);
|
||||||
|
|
||||||
|
AttachDockerNetworksIfRunningInContainer();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IHostService DockerHost
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var hosts = new Hosts().Discover();
|
||||||
|
_host = hosts.FirstOrDefault(x => x.IsNative) ??
|
||||||
|
hosts.FirstOrDefault(x => x.Name == "default") ??
|
||||||
|
hosts.FirstOrDefault();
|
||||||
|
|
||||||
|
if (_host is null) throw new InvalidOperationException("No docker host found");
|
||||||
|
|
||||||
|
return _host;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_networkService?.Dispose();
|
||||||
|
|
||||||
|
TestApplicationContainer.Dispose();
|
||||||
|
AppContainer.Dispose();
|
||||||
|
LoginContainer.Dispose();
|
||||||
|
|
||||||
|
// Kill container because otherwise the _dockerService.Dispose() takes much longer
|
||||||
|
KillDockerComposeServices();
|
||||||
|
|
||||||
|
_dockerService.Dispose();
|
||||||
|
|
||||||
|
_host?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private ICompositeService GetDockerComposeServices()
|
||||||
|
{
|
||||||
|
var services = new Builder()
|
||||||
|
.UseContainer()
|
||||||
|
.UseCompose()
|
||||||
|
.AssumeComposeVersion(ComposeVersion.V2)
|
||||||
|
.FromFile((TemplateString)ComposeFilePath)
|
||||||
|
.ForceBuild()
|
||||||
|
.RemoveOrphans()
|
||||||
|
.Wait("app", WaitForApplicationToListenToRequests)
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int WaitForApplicationToListenToRequests(IContainerService container, int iteration)
|
||||||
|
{
|
||||||
|
const int maxTryCount = 15;
|
||||||
|
ArgumentOutOfRangeException.ThrowIfGreaterThan(iteration, maxTryCount);
|
||||||
|
|
||||||
|
var isStarted = container.Logs().ReadToEnd().Reverse().Any(x => x.Contains("Now listening on:"));
|
||||||
|
return isStarted ? 0 : 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AttachDockerNetworksIfRunningInContainer()
|
||||||
|
{
|
||||||
|
if (!TestApplicationContainer.IsTestRunningInContainer) return;
|
||||||
|
|
||||||
|
var randomNetworkName = Guid.NewGuid().ToString("N");
|
||||||
|
_networkService = DockerHost.CreateNetwork(randomNetworkName, removeOnDispose: true);
|
||||||
|
|
||||||
|
_networkService.Attach(AppContainer.Container!, true, AppContainer.ServiceName);
|
||||||
|
_networkService.Attach(TestApplicationContainer.Container, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void KillDockerComposeServices()
|
||||||
|
{
|
||||||
|
foreach (var container in _dockerService.Containers) container.Remove(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
7
tests/WebApi.Tests.System/SharedTestCollection.cs
Normal file
7
tests/WebApi.Tests.System/SharedTestCollection.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace WebApi.Tests.System;
|
||||||
|
|
||||||
|
[CollectionDefinition(Name)]
|
||||||
|
public class SharedTestCollection : ICollectionFixture<SharedTestContext>
|
||||||
|
{
|
||||||
|
public const string Name = nameof(SharedTestCollection);
|
||||||
|
}
|
||||||
11
tests/WebApi.Tests.System/SharedTestContext.cs
Normal file
11
tests/WebApi.Tests.System/SharedTestContext.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
namespace WebApi.Tests.System;
|
||||||
|
|
||||||
|
public sealed class SharedTestContext : IDisposable
|
||||||
|
{
|
||||||
|
public DockerComposeFixture2 DockerComposeFixture { get; set; } = new();
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
DockerComposeFixture.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
57
tests/WebApi.Tests.System/Test.cs
Normal file
57
tests/WebApi.Tests.System/Test.cs
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace WebApi.Tests.System;
|
||||||
|
|
||||||
|
[Collection(SharedTestCollection.Name)]
|
||||||
|
public class Test
|
||||||
|
{
|
||||||
|
private readonly SharedTestContext _context;
|
||||||
|
|
||||||
|
public Test(SharedTestContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
//[Fact]
|
||||||
|
public async Task Test1()
|
||||||
|
{
|
||||||
|
var loginUrl = _context.DockerComposeFixture.LoginContainer.GetServiceUrl();
|
||||||
|
var baseUrl = new Uri(loginUrl!, UriKind.Absolute);
|
||||||
|
var relativeUrl = new Uri($"/realms/{Constants.Login.Realm}/protocol/openid-connect/token", UriKind.Relative);
|
||||||
|
var uri = new Uri(baseUrl, relativeUrl);
|
||||||
|
var request = new HttpRequestMessage(HttpMethod.Post, uri);
|
||||||
|
|
||||||
|
var data = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ "grant_type", "password" },
|
||||||
|
{ "audience", Constants.Login.ClientId },
|
||||||
|
{ "username", Constants.Login.Username },
|
||||||
|
{ "password", Constants.Login.Password }
|
||||||
|
};
|
||||||
|
request.Content = new FormUrlEncodedContent(data);
|
||||||
|
|
||||||
|
request.Headers.Authorization = new AuthenticationHeaderValue("Basic",
|
||||||
|
Convert.ToBase64String(Encoding.UTF8.GetBytes($"{Constants.Login.ClientId}:{Constants.Login.ClientSecret}")));
|
||||||
|
|
||||||
|
using var client = new HttpClient();
|
||||||
|
using var response = await client.SendAsync(request);
|
||||||
|
|
||||||
|
var content = await response.Content.ReadAsStringAsync();
|
||||||
|
var tokenResponse = JsonSerializer.Deserialize<TokenResponse>(content);
|
||||||
|
|
||||||
|
var appUrl = _context.DockerComposeFixture.AppContainer.GetServiceUrl();
|
||||||
|
baseUrl = new Uri(appUrl!, UriKind.Absolute);
|
||||||
|
relativeUrl = new Uri("/v1/cars", UriKind.Relative);
|
||||||
|
uri = new Uri(baseUrl, relativeUrl);
|
||||||
|
|
||||||
|
request = new HttpRequestMessage(HttpMethod.Get, uri);
|
||||||
|
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokenResponse!.AccessToken);
|
||||||
|
|
||||||
|
using var response2 = await client.SendAsync(request);
|
||||||
|
|
||||||
|
var content2 = await response2.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
9
tests/WebApi.Tests.System/TokenResponse.cs
Normal file
9
tests/WebApi.Tests.System/TokenResponse.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace WebApi.Tests.System;
|
||||||
|
|
||||||
|
public class TokenResponse
|
||||||
|
{
|
||||||
|
[JsonPropertyName("access_token")]
|
||||||
|
public required string AccessToken { get; init; }
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="coverlet.collector" Version="6.0.0" />
|
<PackageReference Include="coverlet.collector" Version="6.0.0" />
|
||||||
|
<PackageReference Include="Ductus.FluentDocker" Version="2.10.59" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
||||||
<PackageReference Include="xunit" Version="2.5.3" />
|
<PackageReference Include="xunit" Version="2.5.3" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
|
||||||
|
|||||||
@@ -3,9 +3,12 @@ services:
|
|||||||
build: ../../src/WebApi
|
build: ../../src/WebApi
|
||||||
environment:
|
environment:
|
||||||
Vegasco_ConnectionStrings__Default: "Host=db;Port=5432;Database=postgres;Username=postgres;Password=postgres"
|
Vegasco_ConnectionStrings__Default: "Host=db;Port=5432;Database=postgres;Username=postgres;Password=postgres"
|
||||||
Vegasco_JWT__Issuer:
|
Vegasco_JWT__MetadataUrl: http://login:8080/realms/development/.well-known/openid-configuration
|
||||||
Vegasco_JWT__Authority:
|
Vegasco_JWT__ValidAudience: vegasco
|
||||||
Vegasco_JWT__Audience:
|
Vegasco_JWT__NameClaimType: name
|
||||||
|
Vegasco_JWT__AllowHttpMetadataUrl: "true"
|
||||||
|
ports:
|
||||||
|
- "8080"
|
||||||
depends_on:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
@@ -38,12 +41,12 @@ services:
|
|||||||
KC_DB_PASSWORD: keycloak
|
KC_DB_PASSWORD: keycloak
|
||||||
KEYCLOAK_ADMIN: admin
|
KEYCLOAK_ADMIN: admin
|
||||||
KEYCLOAK_ADMIN_PASSWORD: admin1!
|
KEYCLOAK_ADMIN_PASSWORD: admin1!
|
||||||
KC_HOSTNAME: http://localhost:12345/
|
KC_HOSTNAME_STRICT: false
|
||||||
KC_HEALTH_ENABLED: true
|
KC_HEALTH_ENABLED: true
|
||||||
KC_METRICS_ENABLED: true
|
KC_METRICS_ENABLED: true
|
||||||
KC_HTTP_ENABLED: true
|
KC_HTTP_ENABLED: true
|
||||||
ports:
|
ports:
|
||||||
- 12345:8080
|
- "8080"
|
||||||
volumes:
|
volumes:
|
||||||
- ./test-realm.json:/opt/keycloak/data/import/test-realm.json:ro
|
- ./test-realm.json:/opt/keycloak/data/import/test-realm.json:ro
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|||||||
@@ -667,6 +667,7 @@
|
|||||||
"enabled" : true,
|
"enabled" : true,
|
||||||
"alwaysDisplayInConsole" : false,
|
"alwaysDisplayInConsole" : false,
|
||||||
"clientAuthenticatorType" : "client-secret",
|
"clientAuthenticatorType" : "client-secret",
|
||||||
|
"secret" : "siIgnkijkkIxeQ9BDNwnGGUb60S53QZh",
|
||||||
"redirectUris" : [ "*" ],
|
"redirectUris" : [ "*" ],
|
||||||
"webOrigins" : [ ],
|
"webOrigins" : [ ],
|
||||||
"notBefore" : 0,
|
"notBefore" : 0,
|
||||||
@@ -674,15 +675,17 @@
|
|||||||
"consentRequired" : false,
|
"consentRequired" : false,
|
||||||
"standardFlowEnabled" : true,
|
"standardFlowEnabled" : true,
|
||||||
"implicitFlowEnabled" : false,
|
"implicitFlowEnabled" : false,
|
||||||
"directAccessGrantsEnabled" : false,
|
"directAccessGrantsEnabled" : true,
|
||||||
"serviceAccountsEnabled" : false,
|
"serviceAccountsEnabled" : false,
|
||||||
"publicClient" : true,
|
"publicClient" : false,
|
||||||
"frontchannelLogout" : true,
|
"frontchannelLogout" : true,
|
||||||
"protocol" : "openid-connect",
|
"protocol" : "openid-connect",
|
||||||
"attributes" : {
|
"attributes" : {
|
||||||
"oidc.ciba.grant.enabled" : "false",
|
"oidc.ciba.grant.enabled" : "false",
|
||||||
|
"client.secret.creation.time" : "1723219692",
|
||||||
"backchannel.logout.session.required" : "true",
|
"backchannel.logout.session.required" : "true",
|
||||||
"post.logout.redirect.uris" : "*",
|
"post.logout.redirect.uris" : "*",
|
||||||
|
"display.on.consent.screen" : "false",
|
||||||
"oauth2.device.authorization.grant.enabled" : "false",
|
"oauth2.device.authorization.grant.enabled" : "false",
|
||||||
"backchannel.logout.revoke.offline.tokens" : "false"
|
"backchannel.logout.revoke.offline.tokens" : "false"
|
||||||
},
|
},
|
||||||
@@ -1279,7 +1282,7 @@
|
|||||||
"subType" : "authenticated",
|
"subType" : "authenticated",
|
||||||
"subComponents" : { },
|
"subComponents" : { },
|
||||||
"config" : {
|
"config" : {
|
||||||
"allowed-protocol-mapper-types" : [ "saml-user-property-mapper", "oidc-address-mapper", "oidc-full-name-mapper", "oidc-usermodel-attribute-mapper", "oidc-usermodel-property-mapper", "saml-user-attribute-mapper", "saml-role-list-mapper", "oidc-sha256-pairwise-sub-mapper" ]
|
"allowed-protocol-mapper-types" : [ "oidc-usermodel-attribute-mapper", "oidc-full-name-mapper", "oidc-address-mapper", "oidc-sha256-pairwise-sub-mapper", "oidc-usermodel-property-mapper", "saml-user-attribute-mapper", "saml-role-list-mapper", "saml-user-property-mapper" ]
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
"id" : "b099d087-5954-460d-902f-def7799cb005",
|
"id" : "b099d087-5954-460d-902f-def7799cb005",
|
||||||
@@ -1297,7 +1300,7 @@
|
|||||||
"subType" : "anonymous",
|
"subType" : "anonymous",
|
||||||
"subComponents" : { },
|
"subComponents" : { },
|
||||||
"config" : {
|
"config" : {
|
||||||
"allowed-protocol-mapper-types" : [ "oidc-usermodel-attribute-mapper", "saml-user-attribute-mapper", "oidc-usermodel-property-mapper", "oidc-sha256-pairwise-sub-mapper", "oidc-full-name-mapper", "oidc-address-mapper", "saml-role-list-mapper", "saml-user-property-mapper" ]
|
"allowed-protocol-mapper-types" : [ "oidc-sha256-pairwise-sub-mapper", "oidc-address-mapper", "saml-user-property-mapper", "saml-role-list-mapper", "oidc-usermodel-property-mapper", "oidc-full-name-mapper", "oidc-usermodel-attribute-mapper", "saml-user-attribute-mapper" ]
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
"id" : "e86226d6-0944-4c08-b809-d72e6c7991c4",
|
"id" : "e86226d6-0944-4c08-b809-d72e6c7991c4",
|
||||||
|
|||||||
Reference in New Issue
Block a user