Skip to content

Commit

Permalink
Implement tests (#67)
Browse files Browse the repository at this point in the history
  • Loading branch information
Tyrrrz authored Nov 8, 2023
1 parent 56608d4 commit 1c1dbb9
Show file tree
Hide file tree
Showing 11 changed files with 313 additions and 147 deletions.
14 changes: 9 additions & 5 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,14 +84,18 @@ jobs:

# Run tests
test:
runs-on: ${{ matrix.os }}
permissions:
contents: read

strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
os:
- ubuntu-latest
# Windows runners don't support Linux Docker containers (needed for tests),
# so we currently cannot run tests on Windows.
# - windows-latest

runs-on: ${{ matrix.os }}
permissions:
contents: read

steps:
- name: Checkout
Expand Down
14 changes: 0 additions & 14 deletions tests/Passwordless.Tests/ApiFactAttribute.cs

This file was deleted.

172 changes: 172 additions & 0 deletions tests/Passwordless.Tests/Fixtures/TestApiFixture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
using System.Net;
using System.Net.Http.Json;
using System.Text;
using System.Text.Json;
using DotNet.Testcontainers.Builders;
using DotNet.Testcontainers.Containers;
using DotNet.Testcontainers.Networks;
using Microsoft.Extensions.DependencyInjection;
using Testcontainers.MsSql;
using Xunit;

namespace Passwordless.Tests.Fixtures;

public class TestApiFixture : IAsyncLifetime
{
private readonly HttpClient _http = new();

private readonly INetwork _network;
private readonly MsSqlContainer _databaseContainer;
private readonly IContainer _apiContainer;

private readonly MemoryStream _databaseContainerStdOut = new();
private readonly MemoryStream _databaseContainerStdErr = new();
private readonly MemoryStream _apiContainerStdOut = new();
private readonly MemoryStream _apiContainerStdErr = new();

private string PublicApiUrl => $"http://localhost:{_apiContainer.GetMappedPublicPort(80)}";

public TestApiFixture()
{
const string managementKey = "yourStrong(!)ManagementKey";
const string databaseHost = "database";

_network = new NetworkBuilder()
.Build();

_databaseContainer = new MsSqlBuilder()
.WithImage("mcr.microsoft.com/mssql/server:2022-latest")
.WithNetwork(_network)
.WithNetworkAliases(databaseHost)
.WithOutputConsumer(
Consume.RedirectStdoutAndStderrToStream(_databaseContainerStdOut, _databaseContainerStdErr)
)
.Build();

_apiContainer = new ContainerBuilder()
// https://github.com/passwordless/passwordless-server/pkgs/container/passwordless-test-api
// TODO: replace with ':stable' after the next release of the server.
.WithImage("ghcr.io/passwordless/passwordless-test-api:latest")
.WithNetwork(_network)
// Run in development environment to execute migrations
.WithEnvironment("ASPNETCORE_ENVIRONMENT", "Development")
.WithEnvironment("ConnectionStrings__sqlite:api", "")
.WithEnvironment("ConnectionStrings__mssql:api",
$"Server={databaseHost},{MsSqlBuilder.MsSqlPort};" +
"Database=Passwordless;" +
$"User Id={MsSqlBuilder.DefaultUsername};" +
$"Password={MsSqlBuilder.DefaultPassword};" +
"Trust Server Certificate=true;" +
"Trusted_Connection=false;"
)
.WithEnvironment("PasswordlessManagement__ManagementKey", managementKey)
.WithPortBinding(80, true)
// Wait until the API is launched, has performed migrations, and is ready to accept requests
.WithWaitStrategy(Wait
.ForUnixContainer()
.UntilHttpRequestIsSucceeded(r => r
.ForPath("/")
.ForStatusCode(HttpStatusCode.OK)
)
)
.WithOutputConsumer(
Consume.RedirectStdoutAndStderrToStream(_apiContainerStdOut, _apiContainerStdErr)
)
.Build();

_http.DefaultRequestHeaders.Add("ManagementKey", managementKey);
}

public async Task InitializeAsync()
{
await _network.CreateAsync();
await _databaseContainer.StartAsync();
await _apiContainer.StartAsync();
}

public async Task<IPasswordlessClient> CreateClientAsync()
{
using var response = await _http.PostAsJsonAsync(
$"{PublicApiUrl}/admin/apps/app{Guid.NewGuid():N}/create",
new { AdminEmail = "[email protected]", EventLoggingIsEnabled = true }
);

if (!response.IsSuccessStatusCode)
{
throw new InvalidOperationException(
$"Failed to create an app. " +
$"Status code: {(int)response.StatusCode}. " +
$"Response body: {await response.Content.ReadAsStringAsync()}."
);
}

var responseContent = await response.Content.ReadFromJsonAsync<JsonElement>();
var apiKey = responseContent.GetProperty("apiKey1").GetString();
var apiSecret = responseContent.GetProperty("apiSecret1").GetString();

var services = new ServiceCollection();

services.AddPasswordlessSdk(options =>
{
options.ApiUrl = PublicApiUrl;
options.ApiKey = apiKey;
options.ApiSecret = apiSecret ??
throw new InvalidOperationException("Cannot extract API Secret from the response.");
});

return services.BuildServiceProvider().GetRequiredService<IPasswordlessClient>();
}

public string GetLogs()
{
var databaseContainerStdOutText = Encoding.UTF8.GetString(
_databaseContainerStdOut.ToArray()
);

var databaseContainerStdErrText = Encoding.UTF8.GetString(
_databaseContainerStdErr.ToArray()
);

var apiContainerStdOutText = Encoding.UTF8.GetString(
_apiContainerStdOut.ToArray()
);

var apiContainerStdErrText = Encoding.UTF8.GetString(
_apiContainerStdErr.ToArray()
);

// API logs are typically more relevant, so put them first
return
$"""
# API container STDOUT:
{apiContainerStdOutText}
# API container STDERR:
{apiContainerStdErrText}
# Database container STDOUT:
{databaseContainerStdOutText}
# Database container STDERR:
{databaseContainerStdErrText}
""";
}

public async Task DisposeAsync()
{
await _apiContainer.DisposeAsync();
await _databaseContainer.DisposeAsync();
await _network.DisposeAsync();

_databaseContainerStdOut.Dispose();
_databaseContainerStdErr.Dispose();
_apiContainerStdOut.Dispose();
_apiContainerStdErr.Dispose();

_http.Dispose();
}
}
8 changes: 8 additions & 0 deletions tests/Passwordless.Tests/Fixtures/TestApiFixtureCollection.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using Xunit;

namespace Passwordless.Tests.Fixtures;

[CollectionDefinition(nameof(TestApiFixtureCollection))]
public class TestApiFixtureCollection : ICollectionFixture<TestApiFixture>
{
}
26 changes: 26 additions & 0 deletions tests/Passwordless.Tests/Infra/ApiTestBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using Passwordless.Tests.Fixtures;
using Xunit;
using Xunit.Abstractions;

namespace Passwordless.Tests.Infra;

[Collection(nameof(TestApiFixtureCollection))]
public abstract class ApiTestBase : IDisposable
{
protected TestApiFixture Api { get; }

protected ITestOutputHelper TestOutput { get; }

protected ApiTestBase(TestApiFixture api, ITestOutputHelper testOutput)
{
Api = api;
TestOutput = testOutput;
}

public void Dispose()
{
// Ideally we should route the logs in realtime, but it's a bit tedious
// with the way the TestContainers library is designed.
TestOutput.WriteLine(Api.GetLogs());
}
}
10 changes: 7 additions & 3 deletions tests/Passwordless.Tests/Passwordless.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,22 @@
<PropertyGroup>
<IncludeNetCoreAppTargets Condition="'$(IncludeNetCoreAppTargets)' == ''">true</IncludeNetCoreAppTargets>
<IncludeNetFrameworkTargets Condition="'$(IncludeNetFrameworkTargets)' == ''">$([MSBuild]::IsOsPlatform('Windows'))</IncludeNetFrameworkTargets>
</PropertyGroup>

<PropertyGroup>
<TargetFrameworks Condition="$(IncludeNetCoreAppTargets)">$(TargetFrameworks);net6.0;net7.0</TargetFrameworks>
<TargetFrameworks Condition="$(IncludeNetFrameworkTargets)">$(TargetFrameworks);net462</TargetFrameworks>
<TargetFrameworks Condition="$(IncludePreview)">$(TargetFrameworks);$(CurrentPreviewTfm)</TargetFrameworks>
</PropertyGroup>

<ItemGroup>
<Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="GitHubActionsTestLogger" Version="2.3.3" PrivateAssets="all" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
<PackageReference Include="Testcontainers" Version="3.5.0" />
<PackageReference Include="Testcontainers.MsSql" Version="3.5.0" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" PrivateAssets="all" />
<PackageReference Include="coverlet.collector" Version="3.2.0" PrivateAssets="all" />
Expand Down
Loading

0 comments on commit 1c1dbb9

Please sign in to comment.