Add TUnit-based unit and integration tests for backend
Set up YesChef.Api.UnitTests and YesChef.Api.IntegrationTests projects running on TUnit + Microsoft.Testing.Platform. Integration tests use a single Postgres 17 Testcontainer per session and clone a migrated template database per test (`CREATE DATABASE … TEMPLATE …`) so tests remain fully isolated and run in parallel without replaying migrations each time. Test-author DX is built around fluent entity builders, a TestDataFactory for common scenarios, and a two-level base hierarchy (IntegrationTest / AuthenticatedIntegrationTest) whose `[Before(Test)]` hooks stand up the per-test database, app factory, default user, and authenticated HttpClient — leaving each test body focused on the action under test. Adds src/backend/global.json to opt `dotnet test` into MTP mode on the .NET 10 SDK, and updates CLAUDE.md with how to run the tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using YesChef.Api.Auth;
|
||||
using YesChef.Api.Entities;
|
||||
|
||||
namespace YesChef.Api.UnitTests.Auth;
|
||||
|
||||
public class JwtTokenServiceTests
|
||||
{
|
||||
private const string Secret = "this-is-a-test-secret-that-is-definitely-long-enough-for-hs256";
|
||||
|
||||
private static JwtTokenService BuildService(string secret = Secret)
|
||||
{
|
||||
var config = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(new Dictionary<string, string?> { ["Jwt:Secret"] = secret })
|
||||
.Build();
|
||||
return new JwtTokenService(config);
|
||||
}
|
||||
|
||||
private static JwtSecurityToken Decode(string token) =>
|
||||
new JwtSecurityTokenHandler().ReadJwtToken(token);
|
||||
|
||||
[Test]
|
||||
public async Task GenerateToken_includes_user_id_and_name_claims()
|
||||
{
|
||||
var service = BuildService();
|
||||
var user = new User { Id = 42, Name = "alice", PasswordHash = "x" };
|
||||
|
||||
var jwt = Decode(service.GenerateToken(user));
|
||||
|
||||
await Assert.That(jwt.Claims.First(c => c.Type == ClaimTypes.NameIdentifier).Value).IsEqualTo("42");
|
||||
await Assert.That(jwt.Claims.First(c => c.Type == ClaimTypes.Name).Value).IsEqualTo("alice");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GenerateToken_expires_in_about_30_days()
|
||||
{
|
||||
var service = BuildService();
|
||||
var user = new User { Id = 1, Name = "bob", PasswordHash = "x" };
|
||||
|
||||
var jwt = Decode(service.GenerateToken(user));
|
||||
|
||||
var expectedExpiry = DateTime.UtcNow.AddDays(30);
|
||||
var delta = (jwt.ValidTo - expectedExpiry).Duration();
|
||||
await Assert.That(delta).IsLessThan(TimeSpan.FromMinutes(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GenerateToken_signs_with_hs256_using_configured_secret()
|
||||
{
|
||||
var service = BuildService();
|
||||
var user = new User { Id = 7, Name = "carol", PasswordHash = "x" };
|
||||
|
||||
var token = service.GenerateToken(user);
|
||||
|
||||
var validator = new JwtSecurityTokenHandler();
|
||||
var parameters = new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuer = false,
|
||||
ValidateAudience = false,
|
||||
ValidateLifetime = true,
|
||||
ValidateIssuerSigningKey = true,
|
||||
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Secret))
|
||||
};
|
||||
|
||||
var principal = validator.ValidateToken(token, parameters, out var validatedToken);
|
||||
|
||||
await Assert.That(principal.Identity!.IsAuthenticated).IsTrue();
|
||||
await Assert.That(((JwtSecurityToken)validatedToken).SignatureAlgorithm)
|
||||
.IsEqualTo(SecurityAlgorithms.HmacSha256);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GenerateToken_with_different_secret_fails_validation()
|
||||
{
|
||||
var service = BuildService();
|
||||
var token = service.GenerateToken(new User { Id = 1, Name = "x", PasswordHash = "x" });
|
||||
|
||||
var validator = new JwtSecurityTokenHandler();
|
||||
var parameters = new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuer = false,
|
||||
ValidateAudience = false,
|
||||
ValidateLifetime = true,
|
||||
IssuerSigningKey = new SymmetricSecurityKey(
|
||||
Encoding.UTF8.GetBytes("a-completely-different-secret-of-sufficient-length"))
|
||||
};
|
||||
|
||||
await Assert.That(() => validator.ValidateToken(token, parameters, out _))
|
||||
.ThrowsExactly<SecurityTokenSignatureKeyNotFoundException>();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user