using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Http; using Microsoft.EntityFrameworkCore; using Moq; using System.Security.Claims; using ROLAC.API.Data; using ROLAC.API.Data.Interceptors; using ROLAC.API.DTOs.Payee; using ROLAC.API.Entities; using ROLAC.API.Services; using ROLAC.API.Services.Logging; using ROLAC.API.Services.Security; using ROLAC.API.Services.Storage; using Xunit; namespace ROLAC.API.Tests.Services; public class Payee1099ServiceTests { // Minimal in-memory IFileStorage (mirrors the ExpenseServiceTests fake). private sealed class FakeStorage : IFileStorage { public Dictionary Files = new(); public Task SaveAsync(Stream c, string p, CancellationToken ct = default) { using var ms = new MemoryStream(); c.CopyTo(ms); Files[p] = ms.ToArray(); return Task.FromResult(p); } public Task OpenReadAsync(string p, CancellationToken ct = default) => Task.FromResult(Files.TryGetValue(p, out var b) ? new MemoryStream(b) : null); public Task DeleteAsync(string p, CancellationToken ct = default) { Files.Remove(p); return Task.CompletedTask; } } private static (Payee1099Service svc, AppDbContext db) Build() { var httpContext = new DefaultHttpContext { User = new(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, "test-user") })) }; var accessorMock = new Mock(); accessorMock.Setup(x => x.HttpContext).Returns(httpContext); var db = new AppDbContext(new DbContextOptionsBuilder() .UseInMemoryDatabase(Guid.NewGuid().ToString()) .AddInterceptors(new AuditSaveChangesInterceptor(new CurrentUserAccessor(accessorMock.Object))).Options); var tin = new TinProtector(DataProtectionProvider.Create("ROLAC.Tests")); return (new Payee1099Service(db, tin, new FakeStorage()), db); } [Fact] public async Task Create_encrypts_tin_and_stores_last4_only_in_clear() { var (svc, db) = Build(); var id = await svc.CreateAsync(new SavePayee1099Request { LegalName = "Pat Player", TinType = "SSN", Tin = "123-45-6789", W9Status = "OnFile" }); var saved = await db.Payee1099s.FindAsync(id); Assert.NotNull(saved); Assert.Equal("6789", saved!.TinLast4); Assert.NotNull(saved.TinEncrypted); Assert.DoesNotContain("123-45-6789", saved.TinEncrypted!); Assert.Equal("123-45-6789", await svc.RevealTinAsync(id)); } [Fact] public async Task Update_with_null_tin_keeps_existing_ciphertext() { var (svc, db) = Build(); var id = await svc.CreateAsync(new SavePayee1099Request { LegalName = "X", Tin = "11-2223333" }); var before = (await db.Payee1099s.FindAsync(id))!.TinEncrypted; await svc.UpdateAsync(id, new SavePayee1099Request { LegalName = "X renamed", Tin = null }); var after = await db.Payee1099s.FindAsync(id); Assert.Equal("X renamed", after!.LegalName); Assert.Equal(before, after.TinEncrypted); Assert.Equal("3333", after.TinLast4); } [Fact] public async Task List_dto_masks_tin_to_last4() { var (svc, _) = Build(); await svc.CreateAsync(new SavePayee1099Request { LegalName = "Y", Tin = "999-88-7777" }); var list = await svc.GetAllAsync(includeInactive: true); var item = Assert.Single(list); Assert.Equal("7777", item.TinLast4); } [Fact] public async Task Delete_is_soft_and_hides_from_list() { var (svc, _) = Build(); var id = await svc.CreateAsync(new SavePayee1099Request { LegalName = "Z" }); await svc.DeleteAsync(id); Assert.Empty(await svc.GetAllAsync(includeInactive: true)); } [Fact] public async Task SaveW9_records_document_and_round_trips_bytes() { var (svc, _) = Build(); var id = await svc.CreateAsync(new SavePayee1099Request { LegalName = "W9 Payee" }); var bytes = new byte[] { 1, 2, 3, 4, 5 }; await svc.SaveW9Async(id, new MemoryStream(bytes), "w9.pdf"); var dto = await svc.GetByIdAsync(id); Assert.NotNull(dto); Assert.True(dto!.HasW9Document); var opened = await svc.OpenW9Async(id); Assert.NotNull(opened); Assert.Equal("application/pdf", opened!.Value.contentType); using var ms = new MemoryStream(); await opened.Value.stream.CopyToAsync(ms); Assert.Equal(bytes, ms.ToArray()); } }