feat(item-acquire-history): controller + DTOs
Add ItemAcquireHistoryController (POST /item_acquire_history/info) with its three DTOs and two integration tests (ordering + empty-viewer). The endpoint reads ViewerAcquireHistory rows written by InventoryTransaction.CommitAsync, ordered newest-first, capped at 300. Tests access doc.RootElement.histories directly (no envelope wrapper in the test path — middleware skips non-UnityPlayer UA). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SVSim.Database;
|
||||
using SVSim.EmulatedEntrypoint.Models.Dtos.ItemAcquireHistory;
|
||||
|
||||
namespace SVSim.EmulatedEntrypoint.Controllers;
|
||||
|
||||
[Route("item_acquire_history")]
|
||||
public sealed class ItemAcquireHistoryController : SVSimController
|
||||
{
|
||||
private const string WireDateFormat = "yyyy-MM-dd HH:mm:ss";
|
||||
private const int PageSize = 300;
|
||||
|
||||
private readonly SVSimDbContext _db;
|
||||
|
||||
public ItemAcquireHistoryController(SVSimDbContext db) => _db = db;
|
||||
|
||||
[HttpPost("info")]
|
||||
public async Task<ActionResult<ItemAcquireHistoryInfoResponse>> Info(
|
||||
[FromBody] ItemAcquireHistoryInfoRequest _,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (!TryGetViewerId(out var viewerId)) return Unauthorized();
|
||||
|
||||
var rows = await _db.ViewerAcquireHistory
|
||||
.Where(h => h.ViewerId == viewerId)
|
||||
.OrderByDescending(h => h.AcquireTime)
|
||||
.ThenByDescending(h => h.Id)
|
||||
.Take(PageSize)
|
||||
.AsNoTracking()
|
||||
.ToListAsync(ct);
|
||||
|
||||
return new ItemAcquireHistoryInfoResponse
|
||||
{
|
||||
Histories = rows.Select(h => new ItemAcquireHistoryEntryDto
|
||||
{
|
||||
RewardType = h.RewardType.ToString(),
|
||||
RewardDetailId = h.RewardDetailId.ToString(),
|
||||
RewardCount = h.RewardCount.ToString(),
|
||||
AcquireType = h.AcquireType.ToString(),
|
||||
AcquireTime = h.AcquireTime.ToString(WireDateFormat),
|
||||
Message = h.Message,
|
||||
}).ToList(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using MessagePack;
|
||||
|
||||
namespace SVSim.EmulatedEntrypoint.Models.Dtos.ItemAcquireHistory;
|
||||
|
||||
/// <summary>
|
||||
/// One row in the <c>/item_acquire_history/info</c> response. All numeric fields ship as
|
||||
/// decimal strings to match the prod capture.
|
||||
/// </summary>
|
||||
[MessagePackObject]
|
||||
public sealed class ItemAcquireHistoryEntryDto
|
||||
{
|
||||
[JsonPropertyName("reward_type")]
|
||||
[Key("reward_type")]
|
||||
public string RewardType { get; set; } = "0";
|
||||
|
||||
[JsonPropertyName("reward_detail_id")]
|
||||
[Key("reward_detail_id")]
|
||||
public string RewardDetailId { get; set; } = "0";
|
||||
|
||||
[JsonPropertyName("reward_count")]
|
||||
[Key("reward_count")]
|
||||
public string RewardCount { get; set; } = "0";
|
||||
|
||||
[JsonPropertyName("acquire_type")]
|
||||
[Key("acquire_type")]
|
||||
public string AcquireType { get; set; } = "0";
|
||||
|
||||
[JsonPropertyName("acquire_time")]
|
||||
[Key("acquire_time")]
|
||||
public string AcquireTime { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("message")]
|
||||
[Key("message")]
|
||||
public string Message { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using MessagePack;
|
||||
|
||||
namespace SVSim.EmulatedEntrypoint.Models.Dtos.ItemAcquireHistory;
|
||||
|
||||
/// <summary>
|
||||
/// Empty request body. The endpoint takes no parameters; this DTO exists so model binding
|
||||
/// resolves the envelope correctly.
|
||||
/// </summary>
|
||||
[MessagePackObject(true)]
|
||||
public sealed class ItemAcquireHistoryInfoRequest
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using MessagePack;
|
||||
|
||||
namespace SVSim.EmulatedEntrypoint.Models.Dtos.ItemAcquireHistory;
|
||||
|
||||
[MessagePackObject]
|
||||
public sealed class ItemAcquireHistoryInfoResponse
|
||||
{
|
||||
[JsonPropertyName("histories")]
|
||||
[Key("histories")]
|
||||
public List<ItemAcquireHistoryEntryDto> Histories { get; set; } = new();
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using SVSim.Database;
|
||||
using SVSim.Database.Models;
|
||||
using SVSim.Database.Services.Inventory;
|
||||
using SVSim.UnitTests.Infrastructure;
|
||||
|
||||
namespace SVSim.UnitTests.Controllers;
|
||||
|
||||
public class ItemAcquireHistoryControllerTests
|
||||
{
|
||||
private static StringContent JsonBody(string json) => new(json, Encoding.UTF8, "application/json");
|
||||
|
||||
[Test]
|
||||
public async Task Info_returns_history_in_newest_first_order_for_the_authenticated_viewer()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
long viewerA = await factory.SeedViewerAsync(steamId: 76_561_198_000_000_001UL);
|
||||
long viewerB = await factory.SeedViewerAsync(steamId: 76_561_198_000_000_002UL);
|
||||
|
||||
using var seedScope = factory.Services.CreateScope();
|
||||
var ctx = seedScope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
var baseTime = new DateTime(2026, 6, 9, 12, 0, 0, DateTimeKind.Utc);
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
ctx.ViewerAcquireHistory.Add(new ViewerAcquireHistoryEntry
|
||||
{
|
||||
ViewerId = viewerA,
|
||||
RewardType = 9,
|
||||
RewardDetailId = 0,
|
||||
RewardCount = i + 1,
|
||||
AcquireType = (int)GrantSource.DailyBonus,
|
||||
Message = "Daily Bonus",
|
||||
AcquireTime = baseTime.AddMinutes(i),
|
||||
});
|
||||
}
|
||||
ctx.ViewerAcquireHistory.Add(new ViewerAcquireHistoryEntry
|
||||
{
|
||||
ViewerId = viewerB,
|
||||
RewardType = 9, RewardDetailId = 0, RewardCount = 99,
|
||||
AcquireType = (int)GrantSource.PackOpen, Message = "x", AcquireTime = baseTime,
|
||||
});
|
||||
await ctx.SaveChangesAsync();
|
||||
|
||||
using var client = factory.CreateAuthenticatedClient(viewerA);
|
||||
var response = await client.PostAsync("/item_acquire_history/info",
|
||||
JsonBody("""{"viewer_id":"0","steam_id":0,"steam_session_ticket":""}"""));
|
||||
var raw = await response.Content.ReadAsStringAsync();
|
||||
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK), raw);
|
||||
|
||||
// Tests bypass the translation middleware (no UnityPlayer UA), so the response is
|
||||
// the raw controller JSON — no {data_headers, data} envelope.
|
||||
using var doc = JsonDocument.Parse(raw);
|
||||
var histories = doc.RootElement.GetProperty("histories");
|
||||
|
||||
Assert.That(histories.GetArrayLength(), Is.EqualTo(5));
|
||||
// Newest first: i=4 was AcquireTime+4min → reward_count = "5"
|
||||
Assert.That(histories[0].GetProperty("reward_count").GetString(), Is.EqualTo("5"));
|
||||
Assert.That(histories[4].GetProperty("reward_count").GetString(), Is.EqualTo("1"));
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
Assert.That(histories[i].GetProperty("acquire_type").GetString(),
|
||||
Is.EqualTo("1"), $"histories[{i}].acquire_type should be DailyBonus=1");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Info_returns_empty_array_for_viewer_with_no_history()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
long viewerId = await factory.SeedViewerAsync();
|
||||
|
||||
using var client = factory.CreateAuthenticatedClient(viewerId);
|
||||
var response = await client.PostAsync("/item_acquire_history/info",
|
||||
JsonBody("""{"viewer_id":"0","steam_id":0,"steam_session_ticket":""}"""));
|
||||
var raw = await response.Content.ReadAsStringAsync();
|
||||
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK), raw);
|
||||
|
||||
using var doc = JsonDocument.Parse(raw);
|
||||
var histories = doc.RootElement.GetProperty("histories");
|
||||
Assert.That(histories.GetArrayLength(), Is.EqualTo(0));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user