test(gift): claim Card/Sleeve presents; reject unsupported types

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-06-09 20:58:53 -04:00
parent 742058403c
commit c1eec9057a

View File

@@ -379,4 +379,154 @@ public class GiftControllerTests
{ "71478626", "71478627", "71478628", "71478629", "71478630" }));
}
}
[Test]
public async Task GiftReceive_with_card_reward_grants_card_via_inventory_service()
{
using var factory = new SVSimTestFactory();
await factory.SeedGlobalsAsync();
long viewerId = await factory.SeedViewerAsync(tutorialState: 100);
long cardId = await factory.SeedCardAsync();
// Seed a single non-tutorial Card present.
using (var seedScope = factory.Services.CreateScope())
{
var ctx = seedScope.ServiceProvider.GetRequiredService<SVSimDbContext>();
ctx.ViewerPresents.Add(new ViewerPresent
{
ViewerId = viewerId,
PresentId = "card-gift-001",
Status = PresentStatus.Unclaimed,
RewardType = 5, // UserGoodsType.Card
RewardDetailId = cardId,
RewardCount = 1,
Message = "Test card grant",
CreatedAt = DateTime.UtcNow,
Source = "test",
});
await ctx.SaveChangesAsync();
}
using var client = factory.CreateAuthenticatedClient(viewerId);
var json = $$"""{"present_id_array":["card-gift-001"],"state":1,{{BaseAuthBlock}}}""";
var response = await client.PostAsync("/gift/receive_gift",
new StringContent(json, Encoding.UTF8, "application/json"));
var bodyStr = await response.Content.ReadAsStringAsync();
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK), bodyStr);
// Verify the card landed in the viewer's collection.
using var verifyScope = factory.Services.CreateScope();
var ctx2 = verifyScope.ServiceProvider.GetRequiredService<SVSimDbContext>();
var owned = await ctx2.Viewers.AsNoTracking()
.Include(v => v.Cards).ThenInclude(c => c.Card)
.FirstAsync(v => v.Id == viewerId);
Assert.That(owned.Cards.Any(c => c.Card.Id == cardId && c.Count > 0), Is.True,
"Card reward_type=5 must round-trip through the gift mapper and InventoryService.GrantAsync.");
}
[Test]
public async Task GiftReceive_with_sleeve_reward_grants_sleeve_via_inventory_service()
{
using var factory = new SVSimTestFactory();
await factory.SeedGlobalsAsync();
long viewerId = await factory.SeedViewerAsync(tutorialState: 100);
const int sleeveId = 700100;
using (var seedScope = factory.Services.CreateScope())
{
var ctx = seedScope.ServiceProvider.GetRequiredService<SVSimDbContext>();
ctx.Sleeves.Add(new SleeveEntry { Id = sleeveId });
await ctx.SaveChangesAsync();
ctx.ViewerPresents.Add(new ViewerPresent
{
ViewerId = viewerId,
PresentId = "sleeve-gift-001",
Status = PresentStatus.Unclaimed,
RewardType = 6, // UserGoodsType.Sleeve
RewardDetailId = sleeveId,
RewardCount = 1,
Message = "Test sleeve grant",
CreatedAt = DateTime.UtcNow,
Source = "test",
});
await ctx.SaveChangesAsync();
}
using var client = factory.CreateAuthenticatedClient(viewerId);
var json = $$"""{"present_id_array":["sleeve-gift-001"],"state":1,{{BaseAuthBlock}}}""";
var response = await client.PostAsync("/gift/receive_gift",
new StringContent(json, Encoding.UTF8, "application/json"));
var bodyStr = await response.Content.ReadAsStringAsync();
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK), bodyStr);
using var verifyScope = factory.Services.CreateScope();
var ctx2 = verifyScope.ServiceProvider.GetRequiredService<SVSimDbContext>();
var owned = await ctx2.Viewers.AsNoTracking()
.Include(v => v.Sleeves)
.FirstAsync(v => v.Id == viewerId);
Assert.That(owned.Sleeves.Any(s => s.Id == sleeveId), Is.True,
"Sleeve reward_type=6 must round-trip through the gift mapper and InventoryService.GrantAsync.");
}
[Test]
public async Task GiftReceive_with_unsupported_reward_type_does_not_grant()
{
using var factory = new SVSimTestFactory();
await factory.SeedGlobalsAsync();
long viewerId = await factory.SeedViewerAsync(tutorialState: 100);
// RewardType 11 = SpotCard, which GiftRewardTypes.IsSupported rejects, causing
// WireRewardTypeToUserGoodsType to throw InvalidOperationException before CommitAsync.
using (var seedScope = factory.Services.CreateScope())
{
var ctx = seedScope.ServiceProvider.GetRequiredService<SVSimDbContext>();
ctx.ViewerPresents.Add(new ViewerPresent
{
ViewerId = viewerId,
PresentId = "bad-gift-001",
Status = PresentStatus.Unclaimed,
RewardType = 11,
RewardDetailId = 123,
RewardCount = 1,
Message = "Bad type",
CreatedAt = DateTime.UtcNow,
Source = "test",
});
await ctx.SaveChangesAsync();
}
using var client = factory.CreateAuthenticatedClient(viewerId);
var json = $$"""{"present_id_array":["bad-gift-001"],"state":1,{{BaseAuthBlock}}}""";
// TestServer propagates unhandled controller exceptions through HttpClient.PostAsync
// (because Program.cs has no app.UseExceptionHandler middleware). Catch the exception
// here; the critical assertion is that the DB row was never transitioned.
bool threw = false;
try
{
var response = await client.PostAsync("/gift/receive_gift",
new StringContent(json, Encoding.UTF8, "application/json"));
// If exception handling middleware is added later, the response will be 500 — accept both.
Assert.That((int)response.StatusCode, Is.GreaterThanOrEqualTo(500),
"Unsupported reward_type must not return a success status code.");
}
catch (Exception ex) when (ex.Message.Contains("Unsupported gift reward_type") ||
ex.InnerException?.Message.Contains("Unsupported gift reward_type") == true)
{
threw = true;
}
// Either path (thrown exception or 5xx) is acceptable; what matters is the DB row stayed Unclaimed.
using var verifyScope = factory.Services.CreateScope();
var ctx2 = verifyScope.ServiceProvider.GetRequiredService<SVSimDbContext>();
var present = await ctx2.ViewerPresents.AsNoTracking()
.FirstAsync(p => p.PresentId == "bad-gift-001");
Assert.That(present.Status, Is.EqualTo(PresentStatus.Unclaimed),
"Failed claim must NOT transition the row — it's still claimable once the producer is fixed.");
// Confirm the exception did propagate (not swallowed into a silent 200).
Assert.That(threw, Is.True,
"InvalidOperationException for unsupported reward_type must propagate — not be silently swallowed into a 200.");
}
}