185 lines
10 KiB
C#
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());
|
|
}
|
|
}
|