feat(release): prepare 1.1.0 candidate
This commit is contained in:
parent
5a34f66981
commit
5205dcdfa9
736 changed files with 115667 additions and 12203 deletions
118
apps/api-server.Tests/AuthSecurityTests.cs
Normal file
118
apps/api-server.Tests/AuthSecurityTests.cs
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
using D3ROVoice.Api.Data;
|
||||
using D3ROVoice.Api.Dtos;
|
||||
using D3ROVoice.Api.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Xunit;
|
||||
|
||||
namespace D3ROVoice.Api.Tests;
|
||||
|
||||
public sealed class AuthSecurityTests
|
||||
{
|
||||
private static IConfiguration TestConfiguration() => new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
["JWT_SECRET"] = "test-only-jwt-secret-0123456789-abcdef",
|
||||
["JWT_ISSUER"] = "https://issuer.test",
|
||||
["JWT_AUDIENCE"] = "d3ro-admin-test"
|
||||
})
|
||||
.Build();
|
||||
|
||||
[Fact]
|
||||
public async Task BootstrapStoresPbkdf2HashAndRejectsASecondAdministrator()
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<AppDbContext>()
|
||||
.UseSqlite("Data Source=:memory:")
|
||||
.Options;
|
||||
await using var db = new AppDbContext(options);
|
||||
await db.Database.OpenConnectionAsync();
|
||||
await db.Database.EnsureCreatedAsync();
|
||||
var service = new AuthService(db, TestConfiguration());
|
||||
|
||||
var created = await service.RegisterAsync(
|
||||
new RegisterDto("ADMIN@EXAMPLE.COM", "correct-horse-battery-staple"));
|
||||
|
||||
var stored = await db.Users.SingleAsync();
|
||||
Assert.Equal("admin@example.com", stored.Email);
|
||||
Assert.Equal("SuperAdmin", stored.Role);
|
||||
Assert.StartsWith("AQAAAA", stored.PasswordHash);
|
||||
Assert.DoesNotContain("correct-horse", stored.PasswordHash, StringComparison.Ordinal);
|
||||
Assert.Equal("SuperAdmin", created.Role);
|
||||
Assert.True(created.ExpiresAt > DateTime.UtcNow.AddHours(7));
|
||||
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() => service.RegisterAsync(
|
||||
new RegisterDto("second@example.com", "another-correct-password")));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoginRejectsWrongPasswordAndReusesNoLegacyHashScheme()
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<AppDbContext>()
|
||||
.UseSqlite("Data Source=:memory:")
|
||||
.Options;
|
||||
await using var db = new AppDbContext(options);
|
||||
await db.Database.OpenConnectionAsync();
|
||||
await db.Database.EnsureCreatedAsync();
|
||||
var service = new AuthService(db, TestConfiguration());
|
||||
await service.RegisterAsync(
|
||||
new RegisterDto("admin@example.com", "correct-horse-battery-staple"));
|
||||
|
||||
await Assert.ThrowsAsync<UnauthorizedAccessException>(() => service.LoginAsync(
|
||||
new LoginDto("admin@example.com", "wrong-password-value")));
|
||||
await Assert.ThrowsAsync<UnauthorizedAccessException>(() => service.LoginAsync(
|
||||
new LoginDto("missing@example.com", "wrong-password-value")));
|
||||
|
||||
var login = await service.LoginAsync(
|
||||
new LoginDto("ADMIN@example.com", "correct-horse-battery-staple"));
|
||||
Assert.Equal("admin@example.com", login.Email);
|
||||
Assert.False(string.IsNullOrWhiteSpace(login.Token));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConcurrentBootstrapAllowsExactlyOneAdministrator()
|
||||
{
|
||||
var dbPath = Path.Combine(
|
||||
Path.GetTempPath(),
|
||||
$"d3ro-auth-security-{Guid.NewGuid():N}.db");
|
||||
var options = new DbContextOptionsBuilder<AppDbContext>()
|
||||
.UseSqlite($"Data Source={dbPath};Pooling=False")
|
||||
.Options;
|
||||
|
||||
try
|
||||
{
|
||||
await using (var setup = new AppDbContext(options))
|
||||
{
|
||||
await setup.Database.EnsureCreatedAsync();
|
||||
}
|
||||
|
||||
async Task<bool> TryBootstrapAsync(int index)
|
||||
{
|
||||
await using var db = new AppDbContext(options);
|
||||
var service = new AuthService(db, TestConfiguration());
|
||||
try
|
||||
{
|
||||
await service.RegisterAsync(new RegisterDto(
|
||||
$"admin-{index}@example.com",
|
||||
$"correct-horse-battery-{index}-staple"));
|
||||
return true;
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
var outcomes = await Task.WhenAll(TryBootstrapAsync(1), TryBootstrapAsync(2));
|
||||
Assert.Single(outcomes, result => result);
|
||||
|
||||
await using (var verification = new AppDbContext(options))
|
||||
{
|
||||
Assert.Equal(1, await verification.Users.CountAsync(user => user.IsActive));
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(dbPath)) File.Delete(dbPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue