test(gift): claim Card/Sleeve presents; reject unsupported types
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -379,4 +379,154 @@ public class GiftControllerTests
|
|||||||
{ "71478626", "71478627", "71478628", "71478629", "71478630" }));
|
{ "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.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user