Skip to content

Commit 1c1dbb9

Browse files
authored
Implement tests (#67)
1 parent 56608d4 commit 1c1dbb9

File tree

11 files changed

+313
-147
lines changed

11 files changed

+313
-147
lines changed

.github/workflows/main.yml

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -84,14 +84,18 @@ jobs:
8484

8585
# Run tests
8686
test:
87-
runs-on: ${{ matrix.os }}
88-
permissions:
89-
contents: read
90-
9187
strategy:
9288
fail-fast: false
9389
matrix:
94-
os: [ubuntu-latest, windows-latest]
90+
os:
91+
- ubuntu-latest
92+
# Windows runners don't support Linux Docker containers (needed for tests),
93+
# so we currently cannot run tests on Windows.
94+
# - windows-latest
95+
96+
runs-on: ${{ matrix.os }}
97+
permissions:
98+
contents: read
9599

96100
steps:
97101
- name: Checkout

tests/Passwordless.Tests/ApiFactAttribute.cs

Lines changed: 0 additions & 14 deletions
This file was deleted.
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
using System.Net;
2+
using System.Net.Http.Json;
3+
using System.Text;
4+
using System.Text.Json;
5+
using DotNet.Testcontainers.Builders;
6+
using DotNet.Testcontainers.Containers;
7+
using DotNet.Testcontainers.Networks;
8+
using Microsoft.Extensions.DependencyInjection;
9+
using Testcontainers.MsSql;
10+
using Xunit;
11+
12+
namespace Passwordless.Tests.Fixtures;
13+
14+
public class TestApiFixture : IAsyncLifetime
15+
{
16+
private readonly HttpClient _http = new();
17+
18+
private readonly INetwork _network;
19+
private readonly MsSqlContainer _databaseContainer;
20+
private readonly IContainer _apiContainer;
21+
22+
private readonly MemoryStream _databaseContainerStdOut = new();
23+
private readonly MemoryStream _databaseContainerStdErr = new();
24+
private readonly MemoryStream _apiContainerStdOut = new();
25+
private readonly MemoryStream _apiContainerStdErr = new();
26+
27+
private string PublicApiUrl => $"http://localhost:{_apiContainer.GetMappedPublicPort(80)}";
28+
29+
public TestApiFixture()
30+
{
31+
const string managementKey = "yourStrong(!)ManagementKey";
32+
const string databaseHost = "database";
33+
34+
_network = new NetworkBuilder()
35+
.Build();
36+
37+
_databaseContainer = new MsSqlBuilder()
38+
.WithImage("mcr.microsoft.com/mssql/server:2022-latest")
39+
.WithNetwork(_network)
40+
.WithNetworkAliases(databaseHost)
41+
.WithOutputConsumer(
42+
Consume.RedirectStdoutAndStderrToStream(_databaseContainerStdOut, _databaseContainerStdErr)
43+
)
44+
.Build();
45+
46+
_apiContainer = new ContainerBuilder()
47+
// https://github.com/passwordless/passwordless-server/pkgs/container/passwordless-test-api
48+
// TODO: replace with ':stable' after the next release of the server.
49+
.WithImage("ghcr.io/passwordless/passwordless-test-api:latest")
50+
.WithNetwork(_network)
51+
// Run in development environment to execute migrations
52+
.WithEnvironment("ASPNETCORE_ENVIRONMENT", "Development")
53+
.WithEnvironment("ConnectionStrings__sqlite:api", "")
54+
.WithEnvironment("ConnectionStrings__mssql:api",
55+
$"Server={databaseHost},{MsSqlBuilder.MsSqlPort};" +
56+
"Database=Passwordless;" +
57+
$"User Id={MsSqlBuilder.DefaultUsername};" +
58+
$"Password={MsSqlBuilder.DefaultPassword};" +
59+
"Trust Server Certificate=true;" +
60+
"Trusted_Connection=false;"
61+
)
62+
.WithEnvironment("PasswordlessManagement__ManagementKey", managementKey)
63+
.WithPortBinding(80, true)
64+
// Wait until the API is launched, has performed migrations, and is ready to accept requests
65+
.WithWaitStrategy(Wait
66+
.ForUnixContainer()
67+
.UntilHttpRequestIsSucceeded(r => r
68+
.ForPath("/")
69+
.ForStatusCode(HttpStatusCode.OK)
70+
)
71+
)
72+
.WithOutputConsumer(
73+
Consume.RedirectStdoutAndStderrToStream(_apiContainerStdOut, _apiContainerStdErr)
74+
)
75+
.Build();
76+
77+
_http.DefaultRequestHeaders.Add("ManagementKey", managementKey);
78+
}
79+
80+
public async Task InitializeAsync()
81+
{
82+
await _network.CreateAsync();
83+
await _databaseContainer.StartAsync();
84+
await _apiContainer.StartAsync();
85+
}
86+
87+
public async Task<IPasswordlessClient> CreateClientAsync()
88+
{
89+
using var response = await _http.PostAsJsonAsync(
90+
$"{PublicApiUrl}/admin/apps/app{Guid.NewGuid():N}/create",
91+
new { AdminEmail = "[email protected]", EventLoggingIsEnabled = true }
92+
);
93+
94+
if (!response.IsSuccessStatusCode)
95+
{
96+
throw new InvalidOperationException(
97+
$"Failed to create an app. " +
98+
$"Status code: {(int)response.StatusCode}. " +
99+
$"Response body: {await response.Content.ReadAsStringAsync()}."
100+
);
101+
}
102+
103+
var responseContent = await response.Content.ReadFromJsonAsync<JsonElement>();
104+
var apiKey = responseContent.GetProperty("apiKey1").GetString();
105+
var apiSecret = responseContent.GetProperty("apiSecret1").GetString();
106+
107+
var services = new ServiceCollection();
108+
109+
services.AddPasswordlessSdk(options =>
110+
{
111+
options.ApiUrl = PublicApiUrl;
112+
options.ApiKey = apiKey;
113+
options.ApiSecret = apiSecret ??
114+
throw new InvalidOperationException("Cannot extract API Secret from the response.");
115+
});
116+
117+
return services.BuildServiceProvider().GetRequiredService<IPasswordlessClient>();
118+
}
119+
120+
public string GetLogs()
121+
{
122+
var databaseContainerStdOutText = Encoding.UTF8.GetString(
123+
_databaseContainerStdOut.ToArray()
124+
);
125+
126+
var databaseContainerStdErrText = Encoding.UTF8.GetString(
127+
_databaseContainerStdErr.ToArray()
128+
);
129+
130+
var apiContainerStdOutText = Encoding.UTF8.GetString(
131+
_apiContainerStdOut.ToArray()
132+
);
133+
134+
var apiContainerStdErrText = Encoding.UTF8.GetString(
135+
_apiContainerStdErr.ToArray()
136+
);
137+
138+
// API logs are typically more relevant, so put them first
139+
return
140+
$"""
141+
# API container STDOUT:
142+
143+
{apiContainerStdOutText}
144+
145+
# API container STDERR:
146+
147+
{apiContainerStdErrText}
148+
149+
# Database container STDOUT:
150+
151+
{databaseContainerStdOutText}
152+
153+
# Database container STDERR:
154+
155+
{databaseContainerStdErrText}
156+
""";
157+
}
158+
159+
public async Task DisposeAsync()
160+
{
161+
await _apiContainer.DisposeAsync();
162+
await _databaseContainer.DisposeAsync();
163+
await _network.DisposeAsync();
164+
165+
_databaseContainerStdOut.Dispose();
166+
_databaseContainerStdErr.Dispose();
167+
_apiContainerStdOut.Dispose();
168+
_apiContainerStdErr.Dispose();
169+
170+
_http.Dispose();
171+
}
172+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
using Xunit;
2+
3+
namespace Passwordless.Tests.Fixtures;
4+
5+
[CollectionDefinition(nameof(TestApiFixtureCollection))]
6+
public class TestApiFixtureCollection : ICollectionFixture<TestApiFixture>
7+
{
8+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using Passwordless.Tests.Fixtures;
2+
using Xunit;
3+
using Xunit.Abstractions;
4+
5+
namespace Passwordless.Tests.Infra;
6+
7+
[Collection(nameof(TestApiFixtureCollection))]
8+
public abstract class ApiTestBase : IDisposable
9+
{
10+
protected TestApiFixture Api { get; }
11+
12+
protected ITestOutputHelper TestOutput { get; }
13+
14+
protected ApiTestBase(TestApiFixture api, ITestOutputHelper testOutput)
15+
{
16+
Api = api;
17+
TestOutput = testOutput;
18+
}
19+
20+
public void Dispose()
21+
{
22+
// Ideally we should route the logs in realtime, but it's a bit tedious
23+
// with the way the TestContainers library is designed.
24+
TestOutput.WriteLine(Api.GetLogs());
25+
}
26+
}

tests/Passwordless.Tests/Passwordless.Tests.csproj

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,22 @@
44
<PropertyGroup>
55
<IncludeNetCoreAppTargets Condition="'$(IncludeNetCoreAppTargets)' == ''">true</IncludeNetCoreAppTargets>
66
<IncludeNetFrameworkTargets Condition="'$(IncludeNetFrameworkTargets)' == ''">$([MSBuild]::IsOsPlatform('Windows'))</IncludeNetFrameworkTargets>
7-
</PropertyGroup>
8-
9-
<PropertyGroup>
107
<TargetFrameworks Condition="$(IncludeNetCoreAppTargets)">$(TargetFrameworks);net6.0;net7.0</TargetFrameworks>
118
<TargetFrameworks Condition="$(IncludeNetFrameworkTargets)">$(TargetFrameworks);net462</TargetFrameworks>
129
<TargetFrameworks Condition="$(IncludePreview)">$(TargetFrameworks);$(CurrentPreviewTfm)</TargetFrameworks>
1310
</PropertyGroup>
1411

1512
<ItemGroup>
13+
<Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
14+
</ItemGroup>
15+
16+
<ItemGroup>
17+
<PackageReference Include="FluentAssertions" Version="6.12.0" />
1618
<PackageReference Include="GitHubActionsTestLogger" Version="2.3.3" PrivateAssets="all" />
1719
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
1820
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
21+
<PackageReference Include="Testcontainers" Version="3.5.0" />
22+
<PackageReference Include="Testcontainers.MsSql" Version="3.5.0" />
1923
<PackageReference Include="xunit" Version="2.4.2" />
2024
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" PrivateAssets="all" />
2125
<PackageReference Include="coverlet.collector" Version="3.2.0" PrivateAssets="all" />

0 commit comments

Comments
 (0)