feat(user-mypage): UserMyPageController + DTOs + persistence tests
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
51
SVSim.EmulatedEntrypoint/Controllers/UserMyPageController.cs
Normal file
51
SVSim.EmulatedEntrypoint/Controllers/UserMyPageController.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SVSim.Database;
|
||||
using SVSim.Database.Models;
|
||||
using SVSim.EmulatedEntrypoint.Models.Dtos.UserMyPage;
|
||||
|
||||
namespace SVSim.EmulatedEntrypoint.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// /user_mypage/* — viewer-scoped MyPage configuration writes. Separate from the
|
||||
/// <c>/mypage/*</c> family because the wire URL family is distinct.
|
||||
/// </summary>
|
||||
[Route("user_mypage")]
|
||||
public sealed class UserMyPageController : SVSimController
|
||||
{
|
||||
private readonly SVSimDbContext _db;
|
||||
|
||||
public UserMyPageController(SVSimDbContext db) => _db = db;
|
||||
|
||||
[HttpPost("update")]
|
||||
public async Task<ActionResult<UserMyPageUpdateResponse>> Update(
|
||||
[FromBody] UserMyPageUpdateRequest request,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (!TryGetViewerId(out var viewerId)) return Unauthorized();
|
||||
|
||||
var viewer = await _db.Viewers
|
||||
.Include(v => v.MyPageBgRotation)
|
||||
.FirstOrDefaultAsync(v => v.Id == viewerId, ct);
|
||||
if (viewer is null) return NotFound();
|
||||
|
||||
viewer.MyPageBgSelectType = request.SelectType;
|
||||
viewer.MyPageBgId = ParseIdOrZero(request.MyPageId);
|
||||
|
||||
viewer.MyPageBgRotation.Clear();
|
||||
for (int slot = 0; slot < request.MyPageIdList.Count; slot++)
|
||||
{
|
||||
viewer.MyPageBgRotation.Add(new MyPageBgRotationEntry
|
||||
{
|
||||
Slot = slot,
|
||||
BgId = ParseIdOrZero(request.MyPageIdList[slot]),
|
||||
});
|
||||
}
|
||||
|
||||
await _db.SaveChangesAsync(ct);
|
||||
return new UserMyPageUpdateResponse();
|
||||
}
|
||||
|
||||
private static int ParseIdOrZero(string s) =>
|
||||
int.TryParse(s, out var n) ? n : 0;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using MessagePack;
|
||||
|
||||
namespace SVSim.EmulatedEntrypoint.Models.Dtos.UserMyPage;
|
||||
|
||||
/// <summary>
|
||||
/// Body of <c>POST /user_mypage/update</c>. Client task: <c>MyPageSettingUpdateTask</c>
|
||||
/// (Shadowverse_Code_2026-05-23/Wizard/MyPageSettingUpdateTask.cs). Note that
|
||||
/// <c>select_type</c> is the only int on the wire — id fields are strings.
|
||||
/// </summary>
|
||||
[MessagePackObject]
|
||||
public sealed class UserMyPageUpdateRequest
|
||||
{
|
||||
/// <summary>BGType enum: 0=Deck, 1=CustomBG, 2=RandomBG. Client sends as an int.</summary>
|
||||
[JsonPropertyName("select_type")]
|
||||
[Key("select_type")]
|
||||
public int SelectType { get; set; }
|
||||
|
||||
/// <summary>Chosen BG id when SelectType=CustomBG; empty or "0" otherwise.</summary>
|
||||
[JsonPropertyName("mypage_id")]
|
||||
[Key("mypage_id")]
|
||||
public string MyPageId { get; set; } = "0";
|
||||
|
||||
/// <summary>Saved rotation pool, in slot order; client sends the full list on every call.</summary>
|
||||
[JsonPropertyName("mypage_id_list")]
|
||||
[Key("mypage_id_list")]
|
||||
public List<string> MyPageIdList { get; set; } = new();
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using MessagePack;
|
||||
|
||||
namespace SVSim.EmulatedEntrypoint.Models.Dtos.UserMyPage;
|
||||
|
||||
/// <summary>
|
||||
/// Empty response payload. The client's <c>MyPageSettingUpdateTask.Parse()</c> is the default
|
||||
/// pass-through; server just acknowledges.
|
||||
/// </summary>
|
||||
[MessagePackObject(true)]
|
||||
public sealed class UserMyPageUpdateResponse
|
||||
{
|
||||
}
|
||||
149
SVSim.UnitTests/Controllers/UserMyPageControllerTests.cs
Normal file
149
SVSim.UnitTests/Controllers/UserMyPageControllerTests.cs
Normal file
@@ -0,0 +1,149 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using SVSim.Database;
|
||||
using SVSim.Database.Models;
|
||||
using SVSim.UnitTests.Infrastructure;
|
||||
|
||||
namespace SVSim.UnitTests.Controllers;
|
||||
|
||||
public class UserMyPageControllerTests
|
||||
{
|
||||
private static StringContent JsonBody(string json) => new(json, Encoding.UTF8, "application/json");
|
||||
|
||||
private static async Task<Viewer> LoadViewerWithRotation(SVSimTestFactory factory, long viewerId)
|
||||
{
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var ctx = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
return await ctx.Viewers
|
||||
.Include(v => v.MyPageBgRotation)
|
||||
.AsNoTracking()
|
||||
.FirstAsync(v => v.Id == viewerId);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Update_persists_select_type_and_single_bg()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
long viewerId = await factory.SeedViewerAsync();
|
||||
using var client = factory.CreateAuthenticatedClient(viewerId);
|
||||
|
||||
var body = JsonBody("""
|
||||
{"select_type":1,"mypage_id":"1213410310","mypage_id_list":[]}
|
||||
""");
|
||||
var response = await client.PostAsync("/user_mypage/update", body);
|
||||
var raw = await response.Content.ReadAsStringAsync();
|
||||
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK), raw);
|
||||
|
||||
var viewer = await LoadViewerWithRotation(factory, viewerId);
|
||||
Assert.That(viewer.MyPageBgSelectType, Is.EqualTo(1));
|
||||
Assert.That(viewer.MyPageBgId, Is.EqualTo(1213410310));
|
||||
Assert.That(viewer.MyPageBgRotation, Is.Empty);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Update_persists_rotation_pool_in_slot_order()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
long viewerId = await factory.SeedViewerAsync();
|
||||
using var client = factory.CreateAuthenticatedClient(viewerId);
|
||||
|
||||
var body = JsonBody("""
|
||||
{"select_type":2,"mypage_id":"0","mypage_id_list":["1211410310","1212410310","1213410310"]}
|
||||
""");
|
||||
var response = await client.PostAsync("/user_mypage/update", body);
|
||||
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
|
||||
|
||||
var viewer = await LoadViewerWithRotation(factory, viewerId);
|
||||
var pool = viewer.MyPageBgRotation.OrderBy(r => r.Slot).Select(r => r.BgId).ToList();
|
||||
Assert.That(pool, Is.EqualTo(new[] { 1211410310, 1212410310, 1213410310 }));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Update_overwrites_previous_rotation_atomically()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
long viewerId = await factory.SeedViewerAsync();
|
||||
|
||||
// Seed a 5-entry rotation directly.
|
||||
using (var seedScope = factory.Services.CreateScope())
|
||||
{
|
||||
var ctx = seedScope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
var viewer = await ctx.Viewers.Include(v => v.MyPageBgRotation).FirstAsync(v => v.Id == viewerId);
|
||||
for (int slot = 0; slot < 5; slot++)
|
||||
{
|
||||
viewer.MyPageBgRotation.Add(new MyPageBgRotationEntry { Slot = slot, BgId = 9000 + slot });
|
||||
}
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
|
||||
using var client = factory.CreateAuthenticatedClient(viewerId);
|
||||
var body = JsonBody("""
|
||||
{"select_type":2,"mypage_id":"0","mypage_id_list":["1001","1002","1003"]}
|
||||
""");
|
||||
var response = await client.PostAsync("/user_mypage/update", body);
|
||||
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
|
||||
|
||||
var viewer2 = await LoadViewerWithRotation(factory, viewerId);
|
||||
var pool = viewer2.MyPageBgRotation.OrderBy(r => r.Slot).Select(r => r.BgId).ToList();
|
||||
Assert.That(pool, Is.EqualTo(new[] { 1001, 1002, 1003 }),
|
||||
"old slots 3-4 should have been deleted, not orphaned");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Update_with_empty_mypage_id_falls_back_to_zero()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
long viewerId = await factory.SeedViewerAsync();
|
||||
using var client = factory.CreateAuthenticatedClient(viewerId);
|
||||
|
||||
var body = JsonBody("""
|
||||
{"select_type":0,"mypage_id":"","mypage_id_list":[]}
|
||||
""");
|
||||
var response = await client.PostAsync("/user_mypage/update", body);
|
||||
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
|
||||
|
||||
var viewer = await LoadViewerWithRotation(factory, viewerId);
|
||||
Assert.That(viewer.MyPageBgSelectType, Is.EqualTo(0));
|
||||
Assert.That(viewer.MyPageBgId, Is.EqualTo(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Update_with_unparseable_mypage_id_falls_back_to_zero()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
long viewerId = await factory.SeedViewerAsync();
|
||||
using var client = factory.CreateAuthenticatedClient(viewerId);
|
||||
|
||||
var body = JsonBody("""
|
||||
{"select_type":0,"mypage_id":"garbage","mypage_id_list":[]}
|
||||
""");
|
||||
var response = await client.PostAsync("/user_mypage/update", body);
|
||||
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
|
||||
|
||||
var viewer = await LoadViewerWithRotation(factory, viewerId);
|
||||
Assert.That(viewer.MyPageBgId, Is.EqualTo(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Update_returns_envelope_with_empty_data_payload()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
long viewerId = await factory.SeedViewerAsync();
|
||||
using var client = factory.CreateAuthenticatedClient(viewerId);
|
||||
|
||||
var body = JsonBody("""
|
||||
{"select_type":0,"mypage_id":"0","mypage_id_list":[]}
|
||||
""");
|
||||
var response = await client.PostAsync("/user_mypage/update", body);
|
||||
var raw = await response.Content.ReadAsStringAsync();
|
||||
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK), raw);
|
||||
|
||||
// Test path bypasses the translation middleware (gated on UnityPlayer UA), so the
|
||||
// controller's literal return value is what comes back. An empty class serializes to "{}".
|
||||
Assert.That(raw, Is.EqualTo("{}"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user