using Microsoft.EntityFrameworkCore; using WishNinja.Data; using WishNinja.Data.Entities; using WishNinja.Services; using Xunit; namespace WishNinja.Tests; public class WishlistServiceTests : IDisposable { private readonly TestDb _db = new(); private readonly WishlistService _svc; private const string OwnerId = "owner-1"; private const string FriendId = "friend-1"; private const string OutsiderId = "outsider-1"; public WishlistServiceTests() { _svc = new WishlistService(_db); Seed(); } private void Seed() { using var ctx = _db.CreateDbContext(); ctx.Users.AddRange( new ApplicationUser { Id = OwnerId, UserName = "owner@x", DisplayName = "Owner" }, new ApplicationUser { Id = FriendId, UserName = "friend@x", DisplayName = "Friend" }, new ApplicationUser { Id = OutsiderId, UserName = "out@x", DisplayName = "Outsider" }); var list = new Wishlist { Id = 1, OwnerId = OwnerId, Title = "Birthday", Visibility = WishlistVisibility.AllMembers, Items = { new WishlistItem { Id = 10, Name = "Book", Quantity = 1 }, new WishlistItem { Id = 11, Name = "Mug", Quantity = 3 }, }, }; ctx.Wishlists.Add(list); ctx.SaveChanges(); } // --- The defining rule: the owner never sees claim data -------------------------------------- [Fact] public async Task Owner_view_never_exposes_claims() { // Friend claims both items. await _svc.ClaimAsync(10, FriendId, 1, "got it"); await _svc.ClaimAsync(11, FriendId, 2, null); var view = await _svc.GetDetailAsync(1, OwnerId); Assert.NotNull(view); Assert.True(view!.IsViewerOwner); Assert.All(view.Items, iv => { Assert.True(iv.IsViewerOwner); Assert.Equal(0, iv.ClaimedQuantity); Assert.False(iv.ClaimedByViewer); Assert.Empty(iv.OtherClaims); // Remaining always equals the wanted quantity for the owner — no leakage. Assert.Equal(iv.Item.Quantity, iv.RemainingQuantity); }); } [Fact] public async Task NonOwner_view_shows_claims() { await _svc.ClaimAsync(11, FriendId, 2, "two mugs"); // Outsider (also a member) sees the claim made by Friend. var outsiderView = await _svc.GetDetailAsync(1, OutsiderId); var mug = outsiderView!.Items.Single(i => i.Item.Id == 11); Assert.False(outsiderView.IsViewerOwner); Assert.Equal(2, mug.ClaimedQuantity); Assert.False(mug.ClaimedByViewer); Assert.Single(mug.OtherClaims); Assert.Equal("Friend", mug.OtherClaims[0].ClaimedByDisplayName); // Friend sees their own claim flagged. var friendView = await _svc.GetDetailAsync(1, FriendId); var friendMug = friendView!.Items.Single(i => i.Item.Id == 11); Assert.True(friendMug.ClaimedByViewer); Assert.Equal(1, friendMug.RemainingQuantity); // 3 wanted - 2 claimed } // --- Claim mutation rules -------------------------------------------------------------------- [Fact] public async Task Owner_cannot_claim_their_own_item() { var result = await _svc.ClaimAsync(10, OwnerId, 1, null); Assert.False(result.Succeeded); } [Fact] public async Task Claim_cannot_exceed_remaining_quantity() { var first = await _svc.ClaimAsync(11, FriendId, 2, null); Assert.True(first.Succeeded); // Only 1 of 3 remains; outsider asking for 2 should fail. var second = await _svc.ClaimAsync(11, OutsiderId, 2, null); Assert.False(second.Succeeded); // But claiming the last 1 succeeds. var third = await _svc.ClaimAsync(11, OutsiderId, 1, null); Assert.True(third.Succeeded); } [Fact] public async Task Unclaim_frees_the_quantity() { await _svc.ClaimAsync(10, FriendId, 1, null); var blocked = await _svc.ClaimAsync(10, OutsiderId, 1, null); Assert.False(blocked.Succeeded); // single-quantity item already taken await _svc.UnclaimAsync(10, FriendId); var nowOk = await _svc.ClaimAsync(10, OutsiderId, 1, null); Assert.True(nowOk.Succeeded); } // --- Visibility ------------------------------------------------------------------------------ [Fact] public async Task SpecificUsers_visibility_blocks_unshared_users() { using (var ctx = _db.CreateDbContext()) { var list = ctx.Wishlists.Single(w => w.Id == 1); list.Visibility = WishlistVisibility.SpecificUsers; ctx.WishlistShares.Add(new WishlistShare { WishlistId = 1, UserId = FriendId }); ctx.SaveChanges(); } Assert.NotNull(await _svc.GetDetailAsync(1, FriendId)); // explicitly shared Assert.Null(await _svc.GetDetailAsync(1, OutsiderId)); // not shared Assert.NotNull(await _svc.GetDetailAsync(1, OwnerId)); // owner always } public void Dispose() => _db.Dispose(); }