using System.ComponentModel.DataAnnotations; using System.Text.Json; using MAX.Framework.Commons; using Store.Shops.Dtos; using Store.Shops.Entities; namespace Store.Shops.Tests; public sealed class ShopManagementTests { [Fact] public async Task CreateThenImportAgain_UpdatesWithoutDuplicating_AndNullPreservesFields() { using var c = new ShopTestContext(); await c.Service.SaveWeidian(new() { LoginName = "login", ShopId = "shop", Notes = "keep", Cookie = "old" }, true); await c.Service.SaveWeidian(new() { LoginName = "login", ShopId = "shop", Cookie = "new" }, true); var row = Assert.Single(await c.Db.Select().ToListAsync()); Assert.Equal("keep", row.Notes); Assert.Equal("new", row.Cookie); Assert.Equal("owner", row.UserName); await c.Service.SaveWeidian(new() { LoginName = "login", ShopId = "shop", Notes = "", NewShopId = "new-id" }, false); row = await c.Db.Select().FirstAsync(); Assert.Equal("", row.Notes); Assert.Equal("new-id", row.ShopId); } [Fact] public async Task WrongOwnerCannotUpdateCookieOrDelete_EvenWithSameShopId() { using var c = new ShopTestContext(); await c.Db.Insert(new WeidianShopEntity { LoginName = "login", ShopId = "shop", UserName = "other", Cookie = "secret" }).ExecuteAffrowsAsync(); await Assert.ThrowsAsync(() => c.Service.UpdateWeidianCookie(new() { ShopId = "shop", Cookie = "attack" })); await Assert.ThrowsAsync(() => c.Service.SaveWeidian(new() { LoginName = "login", ShopId = "shop", Notes = "attack" }, false)); await Assert.ThrowsAsync(() => c.Service.DeleteWeidian(new() { ShopId = "shop" })); Assert.Equal("secret", (await c.Db.Select().FirstAsync()).Cookie); } [Fact] public async Task AssignedAccountMatchesWholeName_AndCannotReassignOrDelete() { using var c = new ShopTestContext(); await c.Db.Insert(new WeidianShopEntity { LoginName = "login", ShopId = "shop", UserName = "other", SubUserName = "coowner,owner2" }).ExecuteAffrowsAsync(); await Assert.ThrowsAsync(() => c.Service.UpdateWeidianCookie(new() { ShopId = "shop", Cookie = "attack" })); await c.Db.Update().Where(s => s.ShopId == "shop").Set(s => s.SubUserName, "first,owner,last").ExecuteAffrowsAsync(); await c.Service.UpdateWeidianCookie(new() { ShopId = "shop", Cookie = "allowed" }); await Assert.ThrowsAsync(() => c.Service.SaveWeidian(new() { LoginName = "login", ShopId = "shop", SubUserName = "new" }, false)); await Assert.ThrowsAsync(() => c.Service.DeleteWeidian(new() { ShopId = "shop" })); Assert.Equal("allowed", (await c.Db.Select().FirstAsync()).Cookie); } [Fact] public async Task CookieUpdateOnlyChangesAuthorizedRows_AndOwnerCanDelete() { using var c = new ShopTestContext(); await c.Db.Insert(new[] { new WeidianShopEntity { ShopId = "shop", UserName = "owner", WdToken = "keep" }, new WeidianShopEntity { ShopId = "shop", UserName = "other", Cookie = "secret" } }).ExecuteAffrowsAsync(); await c.Service.UpdateWeidianCookie(new() { ShopId = "shop", Cookie = "new", LoginStatus = "online" }); Assert.Equal("keep", (await c.Db.Select().Where(s => s.UserName == "owner").FirstAsync()).WdToken); await c.Service.DeleteWeidian(new() { ShopId = "shop" }); Assert.Equal("secret", Assert.Single(await c.Db.Select().ToListAsync()).Cookie); } [Fact] public async Task AmbiguousLoginIsRejectedWithoutChangingEitherShop() { using var c = new ShopTestContext(); await c.Service.SaveWeidian(new() { LoginName = "same", ShopId = "one" }, true); await c.Service.SaveWeidian(new() { LoginName = "same", ShopId = "two" }, true); await Assert.ThrowsAsync(() => c.Service.SaveWeidian(new() { LoginName = "same", Notes = "bad" }, false)); Assert.All(await c.Db.Select().ToListAsync(), s => Assert.Equal("", s.Notes)); } [Fact] public async Task ValidationAndAnonymousRequestsDoNotWrite() { using var c = new ShopTestContext(); await Assert.ThrowsAsync(() => c.Service.SaveWeidian(new(), true)); await Assert.ThrowsAsync(() => c.Service.UpdateWeidianCookie(new() { ShopId = "shop", WdToken = "only" })); await Assert.ThrowsAsync(() => c.Service.DeleteWeidian(new())); c.UserName = null; await Assert.ThrowsAsync(() => c.Service.SaveWeidian(new() { LoginName = "x" }, true)); await Assert.ThrowsAsync(() => c.Service.GetPddClients()); Assert.Equal(0, await c.Db.Select().CountAsync()); } [Theory] [InlineData(0, 50)] [InlineData(1, 0)] [InlineData(1, 501)] [InlineData(int.MaxValue, 500)] public async Task InvalidPaginationIsRejected(int page, int size) { using var c = new ShopTestContext(); await Assert.ThrowsAsync(() => c.Service.GetAdminShops(new() { Page = page, PageSize = size })); await Assert.ThrowsAsync(() => c.Service.GetShopClientInfo(new() { Page = page, PageSize = size })); } [Fact] public async Task ClientInfoScopesCountAndRowsBeforePagination() { using var c = new ShopTestContext(); await c.Db.Insert(new[] { new PddShopEntity { ShopId = "one", UserName = "owner" }, new PddShopEntity { ShopId = "two", UserName = "other", SubUserName = "owner" }, new PddShopEntity { ShopId = "three", UserName = "other", SubUserName = "owner2" } }).ExecuteAffrowsAsync(); foreach (var id in new[] { "one", "two", "three" }) await c.Db.Insert(new ShopApiAuthEntity { ShopId = id, AppKey = id, AppSecret = "secret" }).ExecuteAffrowsAsync(); var page = await c.Service.GetShopClientInfo(new() { PageSize = 1 }); Assert.Equal(2, page.Total); Assert.Equal("two", Assert.Single(page.Items).ShopId); Assert.Empty((await c.Service.GetShopClientInfo(new() { ShopIds = ["three"] })).Items); c.UserName = "admin"; Assert.Empty((await c.Service.GetShopClientInfo(new())).Items); } [Fact] public async Task AdminListResetsOldMonthOnceAndKeepsCurrentQuota() { using var c = new ShopTestContext(); await Assert.ThrowsAsync(() => c.Service.GetAdminShops(new())); c.UserName = "admin"; await c.Db.Insert(new PddShopEntity { ShopId = "old", ShopAccount = "test", LoginPass = "password", Cookie = "secret" }).ExecuteAffrowsAsync(); await c.Db.Insert(new ShopProductQuotaEntity { ShopId = "old", AddProductMonth = DateTime.Now.Month % 12 + 1, AddProductRemaining = 2 }).ExecuteAffrowsAsync(); var page = await c.Service.GetAdminShops(new() { SearchName = "test" }); Assert.Equal(1, page.Total); Assert.Equal(300, Assert.Single(page.Items).AddProductRemaining); var json = JsonSerializer.Serialize(page); Assert.DoesNotContain("LoginPass", json); Assert.DoesNotContain("Cookie", json); await c.Db.Update().Where(s => s.ShopId == "old").Set(s => s.AddProductRemaining, 17).ExecuteAffrowsAsync(); Assert.Equal(17, Assert.Single((await c.Service.GetAdminShops(new())).Items).AddProductRemaining); Assert.Equal(17, (await c.Db.Select().FirstAsync()).AddProductRemaining); } [Fact] public async Task QuotaAndShopUpdatesRollbackTogether() { using var c = new ShopTestContext { UserName = "admin" }; await c.Db.Insert(new PddShopEntity { ShopId = "shop" }).ExecuteAffrowsAsync(); await c.Db.Insert(new ShopProductQuotaEntity { ShopId = "shop", AddProductMonth = 0, AddProductRemaining = 7 }).ExecuteAffrowsAsync(); c.Db.Ado.ExecuteNonQuery("CREATE TRIGGER reject_shop_update BEFORE UPDATE ON pdd_shops BEGIN SELECT RAISE(ABORT, 'test failure'); END"); await Assert.ThrowsAnyAsync(() => c.Service.GetAdminShops(new())); var quota = await c.Db.Select().FirstAsync(); Assert.Equal(0, quota.AddProductMonth); Assert.Equal(7, quota.AddProductRemaining); } [Fact] public async Task TokenRequiresShopAccess_AndRetriesStructuredFailure() { using var c = new ShopTestContext(); await Assert.ThrowsAsync(() => c.Service.GetPddToken(new() { MallId = "mall" })); Assert.Equal(0, c.Handler.Calls); await c.Db.Insert(new PddShopEntity { MallId = "mall&x=1", UserName = "owner" }).ExecuteAffrowsAsync(); c.Handler.Responses.Enqueue("""{"code":1,"msg":"failed"}"""); var token = await c.Service.GetPddToken(new() { MallId = "mall&x=1" }); Assert.Equal("test-token", token.GetProperty("accessToken").GetString()); Assert.Equal(2, c.Handler.Calls); Assert.Contains("mall%26x%3D1", c.Handler.LastUri!.AbsoluteUri); } [Fact] public async Task TokenMalformedResponsesFailWithoutLeakingUpstreamContent() { using var c = new ShopTestContext(); c.Handler.Responses.Enqueue("private upstream error"); c.Handler.Responses.Enqueue("""{"code":3}"""); var error = await Assert.ThrowsAsync(() => c.Gateway.GetToken("mall", default)); Assert.DoesNotContain("private", error.Message); Assert.Equal(2, c.Handler.Calls); } [Fact] public async Task PddClientsComeFromConfiguration() { using var c = new ShopTestContext(); Assert.Equal("test-client", Assert.Single(await c.Service.GetPddClients()).ClientId); c.Configuration["Shops:PddClients:0:ClientSecret"] = ""; await Assert.ThrowsAsync(() => c.Service.GetPddClients()); } }