Files
2026-10-29 11:34:35 +08:00

185 lines
10 KiB
C#

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<WeidianShopEntity>().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<WeidianShopEntity>().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<AppException>(() => c.Service.UpdateWeidianCookie(new() { ShopId = "shop", Cookie = "attack" }));
await Assert.ThrowsAsync<AppException>(() => c.Service.SaveWeidian(new() { LoginName = "login", ShopId = "shop", Notes = "attack" }, false));
await Assert.ThrowsAsync<AppException>(() => c.Service.DeleteWeidian(new() { ShopId = "shop" }));
Assert.Equal("secret", (await c.Db.Select<WeidianShopEntity>().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<AppException>(() => c.Service.UpdateWeidianCookie(new() { ShopId = "shop", Cookie = "attack" }));
await c.Db.Update<WeidianShopEntity>().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<AppException>(() => c.Service.SaveWeidian(new() { LoginName = "login", ShopId = "shop", SubUserName = "new" }, false));
await Assert.ThrowsAsync<AppException>(() => c.Service.DeleteWeidian(new() { ShopId = "shop" }));
Assert.Equal("allowed", (await c.Db.Select<WeidianShopEntity>().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<WeidianShopEntity>().Where(s => s.UserName == "owner").FirstAsync()).WdToken);
await c.Service.DeleteWeidian(new() { ShopId = "shop" });
Assert.Equal("secret", Assert.Single(await c.Db.Select<WeidianShopEntity>().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<AppException>(() => c.Service.SaveWeidian(new() { LoginName = "same", Notes = "bad" }, false));
Assert.All(await c.Db.Select<WeidianShopEntity>().ToListAsync(), s => Assert.Equal("", s.Notes));
}
[Fact]
public async Task ValidationAndAnonymousRequestsDoNotWrite()
{
using var c = new ShopTestContext();
await Assert.ThrowsAsync<ValidationException>(() => c.Service.SaveWeidian(new(), true));
await Assert.ThrowsAsync<ValidationException>(() => c.Service.UpdateWeidianCookie(new() { ShopId = "shop", WdToken = "only" }));
await Assert.ThrowsAsync<ValidationException>(() => c.Service.DeleteWeidian(new()));
c.UserName = null;
await Assert.ThrowsAsync<UnauthorizedAccessException>(() => c.Service.SaveWeidian(new() { LoginName = "x" }, true));
await Assert.ThrowsAsync<UnauthorizedAccessException>(() => c.Service.GetPddClients());
Assert.Equal(0, await c.Db.Select<WeidianShopEntity>().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<ValidationException>(() => c.Service.GetAdminShops(new() { Page = page, PageSize = size }));
await Assert.ThrowsAsync<ValidationException>(() => 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<AppException>(() => 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<ShopProductQuotaEntity>().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<PddShopEntity>().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<Exception>(() => c.Service.GetAdminShops(new()));
var quota = await c.Db.Select<ShopProductQuotaEntity>().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<AppException>(() => 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<AppException>(() => 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<AppException>(() => c.Service.GetPddClients());
}
}