Skip to content

Add integration tests for Passwordless.AspNetCore #83

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

Merged
merged 16 commits into from
Nov 27, 2023
Merged
Show file tree
Hide file tree
Changes from 14 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: 14 additions & 0 deletions Passwordless.sln
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Passwordless.Example", "exa
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Passwordless.AspNetIdentity.Example", "examples\Passwordless.AspNetIdentity.Example\Passwordless.AspNetIdentity.Example.csproj", "{F9487727-715D-442F-BE2F-7FB9931606C2}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Passwordless.Tests.Infra", "tests\Passwordless.Tests.Infra\Passwordless.Tests.Infra.csproj", "{92D2ED44-AC6F-4E10-8300-7E5BFC4890EE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Passwordless.AspNetCore.Tests.Dummy", "tests\Passwordless.AspNetCore.Tests.Dummy\Passwordless.AspNetCore.Tests.Dummy.csproj", "{9E15DF1C-BD60-4373-9905-C86C16A06553}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -62,6 +66,14 @@ Global
{F9487727-715D-442F-BE2F-7FB9931606C2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F9487727-715D-442F-BE2F-7FB9931606C2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F9487727-715D-442F-BE2F-7FB9931606C2}.Release|Any CPU.Build.0 = Release|Any CPU
{92D2ED44-AC6F-4E10-8300-7E5BFC4890EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{92D2ED44-AC6F-4E10-8300-7E5BFC4890EE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{92D2ED44-AC6F-4E10-8300-7E5BFC4890EE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{92D2ED44-AC6F-4E10-8300-7E5BFC4890EE}.Release|Any CPU.Build.0 = Release|Any CPU
{9E15DF1C-BD60-4373-9905-C86C16A06553}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9E15DF1C-BD60-4373-9905-C86C16A06553}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9E15DF1C-BD60-4373-9905-C86C16A06553}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9E15DF1C-BD60-4373-9905-C86C16A06553}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -73,5 +85,7 @@ Global
{F64C850E-9923-43F1-BC84-432AFBBA4425} = {8FC08940-3E9D-4AE5-AB1D-940B4D5DC0E6}
{15D70E7A-D222-4C60-93B2-06570A2BE5F2} = {6EFECBD2-2BF5-473D-A7C3-A1F3A3F1816A}
{F9487727-715D-442F-BE2F-7FB9931606C2} = {6EFECBD2-2BF5-473D-A7C3-A1F3A3F1816A}
{92D2ED44-AC6F-4E10-8300-7E5BFC4890EE} = {8FC08940-3E9D-4AE5-AB1D-940B4D5DC0E6}
{9E15DF1C-BD60-4373-9905-C86C16A06553} = {8FC08940-3E9D-4AE5-AB1D-940B4D5DC0E6}
EndGlobalSection
EndGlobal
2 changes: 1 addition & 1 deletion examples/Passwordless.AspNetIdentity.Example/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

// Execute our migrations to generate our `example.db` file with all the required tables.
using var scope = app.Services.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<PasswordlessContext>();
using var dbContext = scope.ServiceProvider.GetRequiredService<PasswordlessContext>();
dbContext.Database.Migrate();

// Configure the HTTP request pipeline.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Passwordless.AspNetCore\Passwordless.AspNetCore.csproj" />
</ItemGroup>

<ItemGroup>
<None Remove="Properties\launchSettings.json" />
</ItemGroup>

</Project>
57 changes: 57 additions & 0 deletions tests/Passwordless.AspNetCore.Tests.Dummy/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace Passwordless.AspNetCore.Tests.Dummy;

public partial class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<PasswordlessDbContext>();

builder.Services.AddIdentity<IdentityUser, IdentityRole>(o =>
{
o.User.RequireUniqueEmail = true;
})
.AddEntityFrameworkStores<PasswordlessDbContext>()
.AddPasswordless(_ => { });

var app = builder.Build();

app.MapPasswordless(new PasswordlessEndpointOptions
{
GroupPrefix = "",
RegisterPath = "/register",
LoginPath = "/login"
});

app.Run();
}
}

public partial class Program
{
public class PasswordlessDbContext : IdentityDbContext<IdentityUser, IdentityRole, string>
{
public PasswordlessDbContext(DbContextOptions options) : base(options)
{
Database.EnsureCreated();
}

protected override void OnConfiguring(DbContextOptionsBuilder builder)
{
// For some reason, this is required
var connection = new SqliteConnection("DataSource=:memory:");
connection.Open();

builder.UseSqlite(connection);
}
}
}
141 changes: 141 additions & 0 deletions tests/Passwordless.AspNetCore.Tests/EndpointTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using FluentAssertions;
using Passwordless.AspNetCore.Tests.Infra;
using Xunit;
using Xunit.Abstractions;

namespace Passwordless.AspNetCore.Tests;

public class EndpointTests : AppTestBase
{
public EndpointTests(TestAppFixture app, ITestOutputHelper testOutput)
: base(app, testOutput)
{
}

[Fact]
public async Task I_can_define_a_register_endpoint()
{
// Arrange
using var http = await App.CreateClientAsync();

// Act
using var request = new HttpRequestMessage(HttpMethod.Post, "/register");
request.Content = new StringContent(
// lang=json
"""
{
"email": "[email protected]",
"username": "test",
"displayName": "Test User"
}
""",
Encoding.UTF8,
"application/json"
);

using var response = await http.SendAsync(request);
var responseJson = await response.Content.ReadFromJsonAsync<JsonElement>();

// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
responseJson.GetProperty("token").GetString().Should().StartWith("register_");
}

[Fact]
public async Task I_can_define_a_register_endpoint_and_it_will_reject_invalid_registration_attempts()
{
// Arrange
using var http = await App.CreateClientAsync();

// Act
using var request = new HttpRequestMessage(HttpMethod.Post, "/register");
request.Content = new StringContent(
// lang=json
"""
{
"email": null
}
""",
Encoding.UTF8,
"application/json"
);

using var response = await http.SendAsync(request);

// Assert
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
}

[Fact]
public async Task I_can_define_a_register_endpoint_and_it_will_reject_duplicate_registration_attempts()
{
// Arrange
using var http = await App.CreateClientAsync();

// Act
var responses = new List<HttpResponseMessage>();
for (var i = 0; i < 5; i++)
{
using var request = new HttpRequestMessage(HttpMethod.Post, "/register");
request.Content = new StringContent(
// lang=json
"""
{
"email": "[email protected]",
"username": "test",
"displayName": "Test User"
}
""",
Encoding.UTF8,
"application/json"
);

var response = await http.SendAsync(request);
responses.Add(response);
}

// Assert
responses.Take(1).Should().OnlyContain(r => r.StatusCode == HttpStatusCode.OK);
responses.Skip(1).Should().OnlyContain(r => r.StatusCode == HttpStatusCode.BadRequest);
}

[Fact(Skip = "Bug: this currently does not return 400 status code")]
public async Task I_can_define_a_signin_endpoint_and_it_will_reject_invalid_signin_attempts()
{
// Arrange
using var http = await App.CreateClientAsync();

const string token =
"verify_" +
"k8Qg4kXVl8D2aunn__jMT7td5endUueS9zEG8zIsu0lqQjfFAQXcABPX_wlDNbBlTNiB2SQ5MjQ0ZmUzYS0wOGExLTRlMTctOTMwZS1i" +
"YWZhNmM0OWJiOGWucGFzc2tleV9zaWduaW7AwMDAwMDA2SQ3NGUxMzFjOS0yNDZhLTRmNzYtYjIxMS1jNzBkZWQ1Mjg2YzLX_wlDJIBl" +
"TNgJv2FkbWluY29uc29sZTAxLmxlc3NwYXNzd29yZC5kZXbZJ2h0dHBzOi8vYWRtaW5jb25zb2xlMDEubGVzc3Bhc3N3b3JkLmRldsOy" +
"Q2hyb21lLCBXaW5kb3dzIDEwolVBqXRlc3Rlc3RzZcQghR4WgXh0HvbrT27GvP0Pkk4HmfL2b0ucVVSRlDElp_fOeb02NQ";

// Act
using var request = new HttpRequestMessage(HttpMethod.Post, "/login");
request.Content = new StringContent(
// lang=json
$$"""
{
"token": "{{token}}"
}
""",
Encoding.UTF8,
"application/json"
);

using var response = await http.SendAsync(request);

// Assert
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
}
}
26 changes: 26 additions & 0 deletions tests/Passwordless.AspNetCore.Tests/Infra/AppTestBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System;
using Xunit;
using Xunit.Abstractions;

namespace Passwordless.AspNetCore.Tests.Infra;

[Collection(TestAppFixture.Collection.Name)]
public abstract class AppTestBase : IDisposable
{
protected TestAppFixture App { get; }

protected ITestOutputHelper TestOutput { get; }

protected AppTestBase(TestAppFixture app, ITestOutputHelper testOutput)
{
App = app;
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(App.GetLogs());
}
}
55 changes: 55 additions & 0 deletions tests/Passwordless.AspNetCore.Tests/Infra/TestAppFixture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Passwordless.AspNetCore.Tests.Dummy;
using Passwordless.Tests.Infra;
using Xunit;

namespace Passwordless.AspNetCore.Tests.Infra;

// xUnit can't initialize fixture from another assembly, so we have to wrap it
public partial class TestAppFixture : IAsyncLifetime
{
private readonly TestApi _api = new();

public async Task InitializeAsync() => await _api.InitializeAsync();

public async Task<HttpClient> CreateClientAsync(Action<IServiceCollection>? configure = null)
{
var app = await _api.CreateAppAsync();

return new WebApplicationFactory<Program>()
.WithWebHostBuilder(c => c
.ConfigureTestServices(s =>
{
s.Configure<PasswordlessAspNetCoreOptions>(o =>
{
o.ApiUrl = app.ApiUrl;
o.ApiSecret = app.ApiSecret;
o.ApiKey = app.ApiKey;
});

configure?.Invoke(s);
})
).CreateClient();
}

public string GetLogs() => _api.GetLogs();

public async Task DisposeAsync()
{
await _api.DisposeAsync();
}
}

public partial class TestAppFixture
{
[CollectionDefinition(Name)]
public class Collection : ICollectionFixture<TestAppFixture>
{
public const string Name = nameof(TestAppFixture);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,25 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Autofac" Version="7.1.0" />
<PackageReference Include="AutoFixture" Version="4.18.0" />
<Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0" PrivateAssets="all" />
<PackageReference Include="GitHubActionsTestLogger" Version="2.3.3" PrivateAssets="all" />
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="Moq" Version="4.20.69" />
<PackageReference Include="xunit" Version="2.6.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.4" PrivateAssets="all" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Passwordless.AspNetCore\Passwordless.AspNetCore.csproj" />
<ProjectReference Include="..\Passwordless.AspNetCore.Tests.Dummy\Passwordless.AspNetCore.Tests.Dummy.csproj" />
<ProjectReference Include="..\Passwordless.Tests.Infra\Passwordless.Tests.Infra.csproj" />
</ItemGroup>

</Project>
Loading