using System.Security.Cryptography; using System.Text; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using WishNinja.Configuration; using WishNinja.Data; using WishNinja.Data.Entities; using WishNinja.Services; using Xunit; namespace WishNinja.Tests; /// /// Covers the token-validation half of the invite lifecycle (expiry + single-use). Account /// creation in AcceptInviteAsync requires the full Identity stack and is exercised manually / /// in integration, not here. /// public class InviteServiceTests : IDisposable { private readonly TestDb _db = new(); private readonly InviteService _svc; private const string AdminId = "admin-1"; public InviteServiceTests() { using (var ctx = _db.CreateDbContext()) { ctx.Users.Add(new ApplicationUser { Id = AdminId, UserName = "admin@x", DisplayName = "Admin" }); ctx.SaveChanges(); } _svc = new InviteService( _db, userManager: null!, // unused by GetValidInviteAsync emailSender: null!, // unused by GetValidInviteAsync options: Options.Create(new WishNinjaOptions()), logger: NullLogger.Instance); } private static string Hash(string raw) => Convert.ToBase64String(SHA256.HashData(Encoding.UTF8.GetBytes(raw))); private void SeedInvite(string rawToken, DateTimeOffset expiresAt, DateTimeOffset? acceptedAt = null) { using var ctx = _db.CreateDbContext(); ctx.Invites.Add(new Invite { Email = "invitee@x", TokenHash = Hash(rawToken), Role = "User", InvitedByUserId = AdminId, ExpiresAt = expiresAt, AcceptedAt = acceptedAt, }); ctx.SaveChanges(); } [Fact] public async Task Valid_token_returns_invite() { SeedInvite("good-token", DateTimeOffset.UtcNow.AddHours(1)); var invite = await _svc.GetValidInviteAsync("good-token"); Assert.NotNull(invite); Assert.Equal("invitee@x", invite!.Email); } [Fact] public async Task Expired_token_returns_null() { SeedInvite("old-token", DateTimeOffset.UtcNow.AddHours(-1)); Assert.Null(await _svc.GetValidInviteAsync("old-token")); } [Fact] public async Task Accepted_token_returns_null() { SeedInvite("used-token", DateTimeOffset.UtcNow.AddHours(1), acceptedAt: DateTimeOffset.UtcNow.AddMinutes(-5)); Assert.Null(await _svc.GetValidInviteAsync("used-token")); } [Fact] public async Task Unknown_token_returns_null() { SeedInvite("real-token", DateTimeOffset.UtcNow.AddHours(1)); Assert.Null(await _svc.GetValidInviteAsync("wrong-token")); Assert.Null(await _svc.GetValidInviteAsync("")); } public void Dispose() => _db.Dispose(); }