Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement tests #67

Merged
merged 13 commits into from
Nov 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 = "foo@bar.com", 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();
}
}
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
Loading