feat(release): prepare 1.1.0 candidate

This commit is contained in:
Yun Chan 2026-08-29 18:33:45 +09:00
parent 5a34f66981
commit 5205dcdfa9
736 changed files with 115667 additions and 12203 deletions

View 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);
}
}
}