From f9a971a546766f0de5f1ddd66ee5c99c5ab2d369 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Tue, 9 Jun 2026 14:49:43 -0400 Subject: [PATCH] feat(item-acquire-history): controller + DTOs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../ItemAcquireHistoryController.cs | 46 ++++++++++ .../ItemAcquireHistoryEntryDto.cs | 36 ++++++++ .../ItemAcquireHistoryInfoRequest.cs | 12 +++ .../ItemAcquireHistoryInfoResponse.cs | 12 +++ .../ItemAcquireHistoryControllerTests.cs | 85 +++++++++++++++++++ 5 files changed, 191 insertions(+) create mode 100644 SVSim.EmulatedEntrypoint/Controllers/ItemAcquireHistoryController.cs create mode 100644 SVSim.EmulatedEntrypoint/Models/Dtos/ItemAcquireHistory/ItemAcquireHistoryEntryDto.cs create mode 100644 SVSim.EmulatedEntrypoint/Models/Dtos/ItemAcquireHistory/ItemAcquireHistoryInfoRequest.cs create mode 100644 SVSim.EmulatedEntrypoint/Models/Dtos/ItemAcquireHistory/ItemAcquireHistoryInfoResponse.cs create mode 100644 SVSim.UnitTests/Controllers/ItemAcquireHistoryControllerTests.cs diff --git a/SVSim.EmulatedEntrypoint/Controllers/ItemAcquireHistoryController.cs b/SVSim.EmulatedEntrypoint/Controllers/ItemAcquireHistoryController.cs new file mode 100644 index 0000000..55728f4 --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Controllers/ItemAcquireHistoryController.cs @@ -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> 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(), + }; + } +} diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/ItemAcquireHistory/ItemAcquireHistoryEntryDto.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/ItemAcquireHistory/ItemAcquireHistoryEntryDto.cs new file mode 100644 index 0000000..0c7f7b5 --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/ItemAcquireHistory/ItemAcquireHistoryEntryDto.cs @@ -0,0 +1,36 @@ +using System.Text.Json.Serialization; +using MessagePack; + +namespace SVSim.EmulatedEntrypoint.Models.Dtos.ItemAcquireHistory; + +/// +/// One row in the /item_acquire_history/info response. All numeric fields ship as +/// decimal strings to match the prod capture. +/// +[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; +} diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/ItemAcquireHistory/ItemAcquireHistoryInfoRequest.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/ItemAcquireHistory/ItemAcquireHistoryInfoRequest.cs new file mode 100644 index 0000000..20dfad1 --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/ItemAcquireHistory/ItemAcquireHistoryInfoRequest.cs @@ -0,0 +1,12 @@ +using MessagePack; + +namespace SVSim.EmulatedEntrypoint.Models.Dtos.ItemAcquireHistory; + +/// +/// Empty request body. The endpoint takes no parameters; this DTO exists so model binding +/// resolves the envelope correctly. +/// +[MessagePackObject(true)] +public sealed class ItemAcquireHistoryInfoRequest +{ +} diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/ItemAcquireHistory/ItemAcquireHistoryInfoResponse.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/ItemAcquireHistory/ItemAcquireHistoryInfoResponse.cs new file mode 100644 index 0000000..6087529 --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/ItemAcquireHistory/ItemAcquireHistoryInfoResponse.cs @@ -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 Histories { get; set; } = new(); +} diff --git a/SVSim.UnitTests/Controllers/ItemAcquireHistoryControllerTests.cs b/SVSim.UnitTests/Controllers/ItemAcquireHistoryControllerTests.cs new file mode 100644 index 0000000..c37f1cb --- /dev/null +++ b/SVSim.UnitTests/Controllers/ItemAcquireHistoryControllerTests.cs @@ -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(); + 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)); + } +}