Need to fix index load issues
This commit is contained in:
@@ -1,10 +1,13 @@
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
using MessagePack;
|
using MessagePack;
|
||||||
|
using MessagePack.Resolvers;
|
||||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||||
using Microsoft.Extensions.Primitives;
|
using Microsoft.Extensions.Primitives;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json.Linq;
|
||||||
using SVSim.Database.Models;
|
using SVSim.Database.Models;
|
||||||
using SVSim.EmulatedEntrypoint.Constants;
|
using SVSim.EmulatedEntrypoint.Constants;
|
||||||
using SVSim.EmulatedEntrypoint.Extensions;
|
using SVSim.EmulatedEntrypoint.Extensions;
|
||||||
@@ -23,6 +26,16 @@ public class ShadowverseTranslationMiddleware : IMiddleware
|
|||||||
private readonly IActionDescriptorCollectionProvider _actionDescriptorCollectionProvider;
|
private readonly IActionDescriptorCollectionProvider _actionDescriptorCollectionProvider;
|
||||||
private readonly ShadowverseSessionService _sessionService;
|
private readonly ShadowverseSessionService _sessionService;
|
||||||
|
|
||||||
|
// Serialization policy MUST match what AddJsonOptions configured on the controllers, or the
|
||||||
|
// model binder won't find the snake_case keys we write into the synthetic request body and
|
||||||
|
// every request 400s with empty ModelState. WhenWritingNull is irrelevant for request
|
||||||
|
// serialization but kept here for symmetry.
|
||||||
|
private static readonly JsonSerializerOptions ControllerJsonOptions = new()
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||||
|
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||||
|
};
|
||||||
|
|
||||||
public ShadowverseTranslationMiddleware(IActionDescriptorCollectionProvider actionDescriptorCollectionProvider, ShadowverseSessionService sessionService)
|
public ShadowverseTranslationMiddleware(IActionDescriptorCollectionProvider actionDescriptorCollectionProvider, ShadowverseSessionService sessionService)
|
||||||
{
|
{
|
||||||
_actionDescriptorCollectionProvider = actionDescriptorCollectionProvider;
|
_actionDescriptorCollectionProvider = actionDescriptorCollectionProvider;
|
||||||
@@ -58,9 +71,12 @@ public class ShadowverseTranslationMiddleware : IMiddleware
|
|||||||
|
|
||||||
// Decrypt incoming data.
|
// Decrypt incoming data.
|
||||||
requestBytes = Encryption.Decrypt(requestBytes, udid);
|
requestBytes = Encryption.Decrypt(requestBytes, udid);
|
||||||
object? data = MessagePackSerializer.Deserialize(endpointDescriptor.Parameters.FirstOrDefault().ParameterType,
|
Type requestType = endpointDescriptor.Parameters.FirstOrDefault().ParameterType;
|
||||||
requestBytes);
|
object? data = MessagePackSerializer.Deserialize(requestType, requestBytes);
|
||||||
string json = JsonConvert.SerializeObject(data);
|
// Re-serialize via System.Text.Json with the SAME options the controllers use, so the
|
||||||
|
// model binder sees snake_case keys it can match. Using JsonConvert here writes the
|
||||||
|
// CLR property names (PascalCase) and every property silently binds to default → 400.
|
||||||
|
string json = JsonSerializer.Serialize(data, requestType, ControllerJsonOptions);
|
||||||
StringContent newStream = new StringContent(json, Encoding.UTF8, "application/json");
|
StringContent newStream = new StringContent(json, Encoding.UTF8, "application/json");
|
||||||
context.Request.Body = newStream.ReadAsStream();
|
context.Request.Body = newStream.ReadAsStream();
|
||||||
context.Request.Headers.ContentType = new StringValues("application/json");
|
context.Request.Headers.ContentType = new StringValues("application/json");
|
||||||
@@ -68,19 +84,20 @@ public class ShadowverseTranslationMiddleware : IMiddleware
|
|||||||
await next.Invoke(context);
|
await next.Invoke(context);
|
||||||
|
|
||||||
Viewer? viewer = context.GetViewer();
|
Viewer? viewer = context.GetViewer();
|
||||||
|
|
||||||
// Grab the response object
|
// Read the controller's JSON response body. System.Text.Json was configured with
|
||||||
Type responseType = ((ControllerActionDescriptor)endpointDescriptor).MethodInfo.ReturnType;
|
// SnakeCaseLower + WhenWritingNull, so the JSON keys are already in the wire shape and
|
||||||
if (responseType.IsGenericType && responseType.GetGenericTypeDefinition() == typeof(Task<>))
|
// null/optional properties have been omitted. Parse to a JToken tree to preserve that
|
||||||
{
|
// "absent vs null" information — going back through a typed DTO via JsonConvert would
|
||||||
responseType = responseType.GetGenericArguments()[0];
|
// re-introduce nulls for missing properties and they'd reach the client as msgpack Nil.
|
||||||
}
|
|
||||||
using MemoryStream responseBytesStream = new MemoryStream();
|
using MemoryStream responseBytesStream = new MemoryStream();
|
||||||
context.Response.Body.Seek(0, SeekOrigin.Begin);
|
context.Response.Body.Seek(0, SeekOrigin.Begin);
|
||||||
await context.Response.Body.CopyToAsync(responseBytesStream);
|
await context.Response.Body.CopyToAsync(responseBytesStream);
|
||||||
byte[] responseBytes = responseBytesStream.ToArray();
|
string responseJson = Encoding.UTF8.GetString(responseBytesStream.ToArray());
|
||||||
object? responseData = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(responseBytes), responseType);
|
object? responseData = string.IsNullOrEmpty(responseJson)
|
||||||
|
? null
|
||||||
|
: ConvertJsonTreeToPlainObject(JToken.Parse(responseJson));
|
||||||
|
|
||||||
// Wrap the response in a datawrapper
|
// Wrap the response in a datawrapper
|
||||||
DataWrapper wrappedResponseData = new DataWrapper
|
DataWrapper wrappedResponseData = new DataWrapper
|
||||||
{
|
{
|
||||||
@@ -88,8 +105,14 @@ public class ShadowverseTranslationMiddleware : IMiddleware
|
|||||||
DataHeaders = new DataHeaders
|
DataHeaders = new DataHeaders
|
||||||
{
|
{
|
||||||
Servertime = DateTime.UtcNow.Ticks,
|
Servertime = DateTime.UtcNow.Ticks,
|
||||||
Sid =
|
// SID intentionally empty. See docs/api-spec/common/envelope.md §"SID
|
||||||
context.Request.Headers[NetworkConstants.SessionIdHeaderName].FirstOrDefault(),
|
// rotation" — the client's SessionId is a hash-on-read property, so echoing
|
||||||
|
// the request's SID poisons its backing field and the next request hashes
|
||||||
|
// the hash, missing our SID→UDID dict and crashing decryption. To rotate
|
||||||
|
// sessions in the future, use the "stable-prefix + counter" pattern from
|
||||||
|
// that doc (Option B), and pre-hash the rotated value to index the map by
|
||||||
|
// what the client will actually send back on the next request.
|
||||||
|
Sid = "",
|
||||||
// TODO error handling
|
// TODO error handling
|
||||||
ResultCode = 1,
|
ResultCode = 1,
|
||||||
ShortUdid = viewer.ShortUdid,
|
ShortUdid = viewer.ShortUdid,
|
||||||
@@ -97,10 +120,39 @@ public class ShadowverseTranslationMiddleware : IMiddleware
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Convert the response into a messagepack, encrypt it
|
// Convert the response into a messagepack, encrypt it. ContractlessStandardResolver
|
||||||
byte[] packedData = MessagePackSerializer.Serialize<DataWrapper>(wrappedResponseData);
|
// walks the DataWrapper's typed properties (DataHeaders) AND the boxed object/list/
|
||||||
|
// primitive tree under Data — emitting only the keys present in the dictionary.
|
||||||
|
var msgPackOptions = MessagePackSerializerOptions.Standard
|
||||||
|
.WithResolver(ContractlessStandardResolver.Instance);
|
||||||
|
byte[] packedData = MessagePackSerializer.Serialize(wrappedResponseData, msgPackOptions);
|
||||||
packedData = Encryption.Encrypt(packedData, udid);
|
packedData = Encryption.Encrypt(packedData, udid);
|
||||||
await originalResponsebody.WriteAsync(Encoding.UTF8.GetBytes(Convert.ToBase64String(packedData)));
|
await originalResponsebody.WriteAsync(Encoding.UTF8.GetBytes(Convert.ToBase64String(packedData)));
|
||||||
context.Response.Body = originalResponsebody;
|
context.Response.Body = originalResponsebody;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Walks a parsed JSON tree into the plain CLR shape MessagePack-CSharp's contractless
|
||||||
|
/// resolver understands: objects → <c>Dictionary<string, object?></c>, arrays →
|
||||||
|
/// <c>List<object?></c>, scalars unboxed to their nearest primitive. Crucially, JSON
|
||||||
|
/// objects that lacked a key DON'T get one in the dictionary — preserving "absent" as a
|
||||||
|
/// distinct state from "null" all the way to the msgpack writer.
|
||||||
|
/// </summary>
|
||||||
|
internal static object? ConvertJsonTreeToPlainObject(JToken? token)
|
||||||
|
{
|
||||||
|
if (token is null || token.Type == JTokenType.Null) return null;
|
||||||
|
return token.Type switch
|
||||||
|
{
|
||||||
|
JTokenType.Object => token.Children<JProperty>()
|
||||||
|
.ToDictionary(p => p.Name, p => ConvertJsonTreeToPlainObject(p.Value)),
|
||||||
|
JTokenType.Array => token.Children().Select(ConvertJsonTreeToPlainObject).ToList(),
|
||||||
|
JTokenType.Integer => token.Value<long>(),
|
||||||
|
JTokenType.Float => token.Value<double>(),
|
||||||
|
JTokenType.String => token.Value<string>(),
|
||||||
|
JTokenType.Boolean => token.Value<bool>(),
|
||||||
|
JTokenType.Date => token.Value<DateTime>(),
|
||||||
|
JTokenType.Bytes => token.Value<byte[]>(),
|
||||||
|
_ => token.ToString()
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -48,13 +48,20 @@ public class GameStartResponse
|
|||||||
[Key("transition_account_data")]
|
[Key("transition_account_data")]
|
||||||
public List<TransitionAccountData> TransitionAccountData { get; set; } = new();
|
public List<TransitionAccountData> TransitionAccountData { get; set; } = new();
|
||||||
|
|
||||||
// INTENTIONALLY OMITTED: `rewrite_viewer_id` and `account_delete_reservation_status`.
|
/// <summary>
|
||||||
// Both are presence-checked by the client via `Keys.Contains(...)` + `.ToInt()` with no
|
/// When present, client overwrites <c>Certification.ViewerId</c> with this value. Optional
|
||||||
// null guard. MessagePack-CSharp writes [Key] properties unconditionally (null → Nil),
|
/// — leave null to omit. The serialization pipeline (JSON + msgpack via the translation
|
||||||
// and the System.Text.Json `WhenWritingNull` ignore only affects the plain-JSON path.
|
/// middleware) drops null properties end-to-end, so the client sees the key as absent.
|
||||||
// So including these as nullable properties is a guaranteed NRE on the encrypted client
|
/// </summary>
|
||||||
// path. We don't need them — the client tolerates their absence — so don't declare them.
|
[Key("rewrite_viewer_id")]
|
||||||
// Re-add only if we have a real value to send.
|
public long? RewriteViewerId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Presence indicates the user has applied for account deletion (value ignored by client at
|
||||||
|
/// this stage). Optional — leave null to omit.
|
||||||
|
/// </summary>
|
||||||
|
[Key("account_delete_reservation_status")]
|
||||||
|
public int? AccountDeleteReservationStatus { get; set; }
|
||||||
|
|
||||||
// --- Agreement / consent state (all required) ---
|
// --- Agreement / consent state (all required) ---
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using SVSim.Database;
|
using SVSim.Database;
|
||||||
@@ -22,10 +23,16 @@ public class Program
|
|||||||
|
|
||||||
builder.Services.AddControllers().AddJsonOptions(opt =>
|
builder.Services.AddControllers().AddJsonOptions(opt =>
|
||||||
{
|
{
|
||||||
|
// Wire-format congruence: the encrypted msgpack path uses snake_case [Key("...")]
|
||||||
|
// names; the plain-JSON path runs through System.Text.Json. Match them by using
|
||||||
|
// SnakeCaseLower naming policy here so both paths emit identical key names — and
|
||||||
|
// so the translation middleware can hand JSON keys straight through to msgpack
|
||||||
|
// without per-property name remapping.
|
||||||
|
opt.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower;
|
||||||
// Production omits null/optional fields entirely; the client uses
|
// Production omits null/optional fields entirely; the client uses
|
||||||
// `Keys.Contains(name)` as a presence check and calls `.ToInt()` (etc.) on the
|
// `Keys.Contains(name)` as a presence check and calls `.ToInt()` (etc.) on the
|
||||||
// value without a null guard. Emitting `"key":null` makes Contains return true and
|
// value without a null guard. Emitting `"key":null` makes Contains return true and
|
||||||
// crashes the client. Match prod by dropping nulls during serialization.
|
// crashes the client. Drop nulls during serialization so missing == absent.
|
||||||
opt.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
|
opt.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
|
||||||
});
|
});
|
||||||
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
|
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
|
||||||
|
|||||||
@@ -28,6 +28,10 @@
|
|||||||
</Content>
|
</Content>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<InternalsVisibleTo Include="SVSim.UnitTests" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\SVSim.Database\SVSim.Database.csproj" />
|
<ProjectReference Include="..\SVSim.Database\SVSim.Database.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Encodings.Web;
|
using System.Text.Encodings.Web;
|
||||||
|
using System.Text.Json;
|
||||||
using Microsoft.AspNetCore.Authentication;
|
using Microsoft.AspNetCore.Authentication;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Newtonsoft.Json;
|
|
||||||
using SVSim.Database.Enums;
|
using SVSim.Database.Enums;
|
||||||
using SVSim.Database.Models;
|
using SVSim.Database.Models;
|
||||||
using SVSim.Database.Repositories.Viewer;
|
using SVSim.Database.Repositories.Viewer;
|
||||||
@@ -16,6 +16,14 @@ namespace SVSim.EmulatedEntrypoint.Security.SteamSessionAuthentication;
|
|||||||
|
|
||||||
public class SteamSessionAuthenticationHandler : AuthenticationHandler<SteamAuthenticationHandlerOptions>
|
public class SteamSessionAuthenticationHandler : AuthenticationHandler<SteamAuthenticationHandlerOptions>
|
||||||
{
|
{
|
||||||
|
// Must mirror the controller-side JSON options — the translation middleware rewrites the
|
||||||
|
// request body in snake_case, and we have to read it back the same way or every property
|
||||||
|
// binds to null and we NRE downstream against the Steam ticket.
|
||||||
|
private static readonly JsonSerializerOptions RequestJsonOptions = new()
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
|
||||||
|
};
|
||||||
|
|
||||||
private readonly SteamSessionService _sessionService;
|
private readonly SteamSessionService _sessionService;
|
||||||
private readonly IViewerRepository _viewerRepository;
|
private readonly IViewerRepository _viewerRepository;
|
||||||
public SteamSessionAuthenticationHandler(IOptionsMonitor<SteamAuthenticationHandlerOptions> options, ILoggerFactory logger, UrlEncoder encoder, SteamSessionService sessionService, IViewerRepository viewerRepository) : base(options, logger, encoder)
|
public SteamSessionAuthenticationHandler(IOptionsMonitor<SteamAuthenticationHandlerOptions> options, ILoggerFactory logger, UrlEncoder encoder, SteamSessionService sessionService, IViewerRepository viewerRepository) : base(options, logger, encoder)
|
||||||
@@ -48,13 +56,21 @@ public class SteamSessionAuthenticationHandler : AuthenticationHandler<SteamAuth
|
|||||||
|
|
||||||
// Convert bytes to json
|
// Convert bytes to json
|
||||||
string requestString = Encoding.UTF8.GetString(requestBytes);
|
string requestString = Encoding.UTF8.GetString(requestBytes);
|
||||||
BaseRequest? requestJson = JsonConvert.DeserializeObject<BaseRequest>(requestString);
|
BaseRequest? requestJson;
|
||||||
|
try
|
||||||
if (requestJson is null)
|
{
|
||||||
|
requestJson = JsonSerializer.Deserialize<BaseRequest>(requestString, RequestJsonOptions);
|
||||||
|
}
|
||||||
|
catch (JsonException)
|
||||||
{
|
{
|
||||||
return AuthenticateResult.Fail("Invalid request body.");
|
return AuthenticateResult.Fail("Invalid request body.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (requestJson is null || string.IsNullOrEmpty(requestJson.SteamSessionTicket))
|
||||||
|
{
|
||||||
|
return AuthenticateResult.Fail("Invalid request body.");
|
||||||
|
}
|
||||||
|
|
||||||
// Check steam session validity
|
// Check steam session validity
|
||||||
bool sessionIsValid = _sessionService.IsTicketValidForUser(requestJson.SteamSessionTicket, requestJson.SteamId);
|
bool sessionIsValid = _sessionService.IsTicketValidForUser(requestJson.SteamSessionTicket, requestJson.SteamId);
|
||||||
if (!sessionIsValid)
|
if (!sessionIsValid)
|
||||||
|
|||||||
@@ -19,6 +19,14 @@ public class SteamSessionService : IDisposable
|
|||||||
/// <returns>whether the ticket is valid for the given steamid</returns>
|
/// <returns>whether the ticket is valid for the given steamid</returns>
|
||||||
public bool IsTicketValidForUser(string ticket, ulong steamId)
|
public bool IsTicketValidForUser(string ticket, ulong steamId)
|
||||||
{
|
{
|
||||||
|
if (string.IsNullOrEmpty(ticket))
|
||||||
|
{
|
||||||
|
// Caller already shouldn't pass null/empty here, but a misshaped request body
|
||||||
|
// (e.g. wrong casing) used to NRE on the ConcurrentDictionary lookup below.
|
||||||
|
// Fail cleanly so the auth pipeline returns 401 instead of crashing the request.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (_validatedSessionTickets.TryGetValue(ticket, out ulong storedSteamId))
|
if (_validatedSessionTickets.TryGetValue(ticket, out ulong storedSteamId))
|
||||||
{
|
{
|
||||||
return storedSteamId == steamId;
|
return storedSteamId == steamId;
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ namespace SVSim.UnitTests.Controllers;
|
|||||||
public class CheckControllerTests
|
public class CheckControllerTests
|
||||||
{
|
{
|
||||||
private const string BaseRequestJson =
|
private const string BaseRequestJson =
|
||||||
"""{"viewerId":"0","steamId":0,"steamSessionTicket":""}""";
|
"""{"viewer_id":"0","steam_id":0,"steam_session_ticket":""}""";
|
||||||
|
|
||||||
private const string GameStartRequestJson =
|
private const string GameStartRequestJson =
|
||||||
"""{"viewerId":"0","steamId":0,"steamSessionTicket":"","appType":0,"campaignData":"","campaignSign":"","campaignUser":0}""";
|
"""{"viewer_id":"0","steam_id":0,"steam_session_ticket":"","app_type":0,"campaign_data":"","campaign_sign":"","campaign_user":0}""";
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public async Task SpecialTitle_returns_default_title_id()
|
public async Task SpecialTitle_returns_default_title_id()
|
||||||
@@ -31,7 +31,7 @@ public class CheckControllerTests
|
|||||||
|
|
||||||
var body = await response.Content.ReadAsStringAsync();
|
var body = await response.Content.ReadAsStringAsync();
|
||||||
using var doc = JsonDocument.Parse(body);
|
using var doc = JsonDocument.Parse(body);
|
||||||
Assert.That(doc.RootElement.GetProperty("titleImageId").GetString(), Is.EqualTo("0"));
|
Assert.That(doc.RootElement.GetProperty("title_image_id").GetString(), Is.EqualTo("0"));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -51,30 +51,30 @@ public class CheckControllerTests
|
|||||||
var root = doc.RootElement;
|
var root = doc.RootElement;
|
||||||
|
|
||||||
// now_tutorial_step is a STRING on the wire (prod sends "100"); client calls .ToInt().
|
// now_tutorial_step is a STRING on the wire (prod sends "100"); client calls .ToInt().
|
||||||
Assert.That(root.GetProperty("nowTutorialStep").GetString(), Is.EqualTo("100"),
|
Assert.That(root.GetProperty("now_tutorial_step").GetString(), Is.EqualTo("100"),
|
||||||
"RegisterViewer's seed-config default sets tutorial_state=100 (tutorial complete).");
|
"RegisterViewer's seed-config default sets tutorial_state=100 (tutorial complete).");
|
||||||
Assert.That(root.GetProperty("tosState").GetInt32(), Is.EqualTo(1));
|
Assert.That(root.GetProperty("tos_state").GetInt32(), Is.EqualTo(1));
|
||||||
Assert.That(root.GetProperty("policyState").GetInt32(), Is.EqualTo(1));
|
Assert.That(root.GetProperty("policy_state").GetInt32(), Is.EqualTo(1));
|
||||||
Assert.That(root.GetProperty("korAuthorityState").GetInt32(), Is.EqualTo(0));
|
Assert.That(root.GetProperty("kor_authority_state").GetInt32(), Is.EqualTo(0));
|
||||||
Assert.That(root.GetProperty("tosId").GetInt32(), Is.EqualTo(1));
|
Assert.That(root.GetProperty("tos_id").GetInt32(), Is.EqualTo(1));
|
||||||
Assert.That(root.GetProperty("policyId").GetInt32(), Is.EqualTo(1));
|
Assert.That(root.GetProperty("policy_id").GetInt32(), Is.EqualTo(1));
|
||||||
Assert.That(root.GetProperty("korAuthorityId").GetInt32(), Is.EqualTo(0));
|
Assert.That(root.GetProperty("kor_authority_id").GetInt32(), Is.EqualTo(0));
|
||||||
|
|
||||||
// Prod-shape fields (not strictly read by GameStartCheckTask.Parse but sent by prod).
|
// Prod-shape fields (not strictly read by GameStartCheckTask.Parse but sent by prod).
|
||||||
Assert.That(root.GetProperty("nowViewerId").GetInt64(), Is.GreaterThan(0));
|
Assert.That(root.GetProperty("now_viewer_id").GetInt64(), Is.GreaterThan(0));
|
||||||
Assert.That(root.GetProperty("nowName").GetString(), Is.Not.Empty);
|
Assert.That(root.GetProperty("now_name").GetString(), Is.Not.Empty);
|
||||||
Assert.That(root.GetProperty("nowRank").ValueKind, Is.EqualTo(JsonValueKind.Object));
|
Assert.That(root.GetProperty("now_rank").ValueKind, Is.EqualTo(JsonValueKind.Object));
|
||||||
|
|
||||||
// Steam connection should round-trip into transition_account_data — all three fields
|
// Steam connection should round-trip into transition_account_data — all three fields
|
||||||
// serialized as strings (matches prod wire shape).
|
// serialized as strings (matches prod wire shape).
|
||||||
var transitions = root.GetProperty("transitionAccountData");
|
var transitions = root.GetProperty("transition_account_data");
|
||||||
Assert.That(transitions.ValueKind, Is.EqualTo(JsonValueKind.Array));
|
Assert.That(transitions.ValueKind, Is.EqualTo(JsonValueKind.Array));
|
||||||
Assert.That(transitions.GetArrayLength(), Is.EqualTo(1),
|
Assert.That(transitions.GetArrayLength(), Is.EqualTo(1),
|
||||||
"Seeded viewer has exactly one Steam social account connection.");
|
"Seeded viewer has exactly one Steam social account connection.");
|
||||||
Assert.That(transitions[0].GetProperty("socialAccountType").GetString(),
|
Assert.That(transitions[0].GetProperty("social_account_type").GetString(),
|
||||||
Is.EqualTo(((int)SVSim.Database.Enums.SocialAccountType.Steam).ToString()));
|
Is.EqualTo(((int)SVSim.Database.Enums.SocialAccountType.Steam).ToString()));
|
||||||
Assert.That(transitions[0].GetProperty("socialAccountId").GetString(), Is.Not.Empty);
|
Assert.That(transitions[0].GetProperty("social_account_id").GetString(), Is.Not.Empty);
|
||||||
Assert.That(transitions[0].GetProperty("connectedViewerId").GetString(), Is.Not.Empty);
|
Assert.That(transitions[0].GetProperty("connected_viewer_id").GetString(), Is.Not.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -97,9 +97,9 @@ public class CheckControllerTests
|
|||||||
|
|
||||||
using var doc = JsonDocument.Parse(body);
|
using var doc = JsonDocument.Parse(body);
|
||||||
var root = doc.RootElement;
|
var root = doc.RootElement;
|
||||||
Assert.That(root.TryGetProperty("rewriteViewerId", out _), Is.False,
|
Assert.That(root.TryGetProperty("rewrite_viewer_id", out _), Is.False,
|
||||||
"rewrite_viewer_id must NOT be present in the response — client NREs on null .ToInt().");
|
"rewrite_viewer_id must NOT be present in the response — client NREs on null .ToInt().");
|
||||||
Assert.That(root.TryGetProperty("accountDeleteReservationStatus", out _), Is.False,
|
Assert.That(root.TryGetProperty("account_delete_reservation_status", out _), Is.False,
|
||||||
"account_delete_reservation_status must NOT be present — presence triggers client behavior.");
|
"account_delete_reservation_status must NOT be present — presence triggers client behavior.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ namespace SVSim.UnitTests.Controllers;
|
|||||||
public class DeckControllerTests
|
public class DeckControllerTests
|
||||||
{
|
{
|
||||||
private static string DeckFormatRequestJson(Format f) =>
|
private static string DeckFormatRequestJson(Format f) =>
|
||||||
$$"""{"viewerId":"0","steamId":0,"steamSessionTicket":"","deckFormat":{{(int)f}}}""";
|
$$"""{"viewer_id":"0","steam_id":0,"steam_session_ticket":"","deck_format":{{(int)f}}}""";
|
||||||
|
|
||||||
private static StringContent JsonBody(string json) => new(json, Encoding.UTF8, "application/json");
|
private static StringContent JsonBody(string json) => new(json, Encoding.UTF8, "application/json");
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ public class DeckControllerTests
|
|||||||
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK), body);
|
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK), body);
|
||||||
|
|
||||||
using var doc = JsonDocument.Parse(body);
|
using var doc = JsonDocument.Parse(body);
|
||||||
var decks = doc.RootElement.GetProperty("userDeckList");
|
var decks = doc.RootElement.GetProperty("user_deck_list");
|
||||||
Assert.That(decks.GetArrayLength(), Is.EqualTo(2),
|
Assert.That(decks.GetArrayLength(), Is.EqualTo(2),
|
||||||
"Only Rotation-format decks should be returned for a Rotation request.");
|
"Only Rotation-format decks should be returned for a Rotation request.");
|
||||||
var names = Enumerable.Range(0, decks.GetArrayLength())
|
var names = Enumerable.Range(0, decks.GetArrayLength())
|
||||||
@@ -72,7 +72,7 @@ public class DeckControllerTests
|
|||||||
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK), body);
|
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK), body);
|
||||||
|
|
||||||
using var doc = JsonDocument.Parse(body);
|
using var doc = JsonDocument.Parse(body);
|
||||||
var decks = doc.RootElement.GetProperty("userDeckList");
|
var decks = doc.RootElement.GetProperty("user_deck_list");
|
||||||
Assert.That(decks.GetArrayLength(), Is.EqualTo(1));
|
Assert.That(decks.GetArrayLength(), Is.EqualTo(1));
|
||||||
Assert.That(decks[0].GetProperty("name").GetString(), Is.EqualTo("Unlimited Deck"));
|
Assert.That(decks[0].GetProperty("name").GetString(), Is.EqualTo("Unlimited Deck"));
|
||||||
}
|
}
|
||||||
@@ -90,7 +90,7 @@ public class DeckControllerTests
|
|||||||
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK), body);
|
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK), body);
|
||||||
|
|
||||||
using var doc = JsonDocument.Parse(body);
|
using var doc = JsonDocument.Parse(body);
|
||||||
var decks = doc.RootElement.GetProperty("userDeckList");
|
var decks = doc.RootElement.GetProperty("user_deck_list");
|
||||||
Assert.That(decks.GetArrayLength(), Is.EqualTo(0));
|
Assert.That(decks.GetArrayLength(), Is.EqualTo(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,7 +108,7 @@ public class DeckControllerTests
|
|||||||
|
|
||||||
var body = await response.Content.ReadAsStringAsync();
|
var body = await response.Content.ReadAsStringAsync();
|
||||||
using var doc = JsonDocument.Parse(body);
|
using var doc = JsonDocument.Parse(body);
|
||||||
Assert.That(doc.RootElement.GetProperty("emptyDeckNum").GetInt32(), Is.EqualTo(1));
|
Assert.That(doc.RootElement.GetProperty("empty_deck_num").GetInt32(), Is.EqualTo(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -127,7 +127,7 @@ public class DeckControllerTests
|
|||||||
|
|
||||||
var body = await response.Content.ReadAsStringAsync();
|
var body = await response.Content.ReadAsStringAsync();
|
||||||
using var doc = JsonDocument.Parse(body);
|
using var doc = JsonDocument.Parse(body);
|
||||||
Assert.That(doc.RootElement.GetProperty("emptyDeckNum").GetInt32(), Is.EqualTo(3),
|
Assert.That(doc.RootElement.GetProperty("empty_deck_num").GetInt32(), Is.EqualTo(3),
|
||||||
"Algorithm must return the smallest free slot, not just one past the highest used.");
|
"Algorithm must return the smallest free slot, not just one past the highest used.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,10 +142,10 @@ public class DeckControllerTests
|
|||||||
using var client = factory.CreateAuthenticatedClient(viewerId);
|
using var client = factory.CreateAuthenticatedClient(viewerId);
|
||||||
|
|
||||||
var updateJson = $$"""
|
var updateJson = $$"""
|
||||||
{"viewerId":"0","steamId":0,"steamSessionTicket":"",
|
{"viewer_id":"0","steam_id":0,"steam_session_ticket":"",
|
||||||
"deckNo":1,"classId":{{classId}},"leaderSkinId":{{leaderSkinId}},
|
"deck_no":1,"class_id":{{classId}},"leader_skin_id":{{leaderSkinId}},
|
||||||
"isRandomLeaderSkin":false,"sleeveId":{{sleeveId}},"deckName":"Fresh Deck",
|
"is_random_leader_skin":false,"sleeve_id":{{sleeveId}},"deck_name":"Fresh Deck",
|
||||||
"isDelete":0,"deckFormat":0}
|
"is_delete":0,"deck_format":0}
|
||||||
""";
|
""";
|
||||||
var response = await client.PostAsync("/deck/update", JsonBody(updateJson));
|
var response = await client.PostAsync("/deck/update", JsonBody(updateJson));
|
||||||
|
|
||||||
@@ -170,10 +170,10 @@ public class DeckControllerTests
|
|||||||
using var client = factory.CreateAuthenticatedClient(viewerId);
|
using var client = factory.CreateAuthenticatedClient(viewerId);
|
||||||
|
|
||||||
var updateJson = $$"""
|
var updateJson = $$"""
|
||||||
{"viewerId":"0","steamId":0,"steamSessionTicket":"",
|
{"viewer_id":"0","steam_id":0,"steam_session_ticket":"",
|
||||||
"deckNo":1,"classId":{{classId}},"leaderSkinId":{{leaderSkinId}},
|
"deck_no":1,"class_id":{{classId}},"leader_skin_id":{{leaderSkinId}},
|
||||||
"isRandomLeaderSkin":false,"sleeveId":{{sleeveId}},"deckName":"Renamed",
|
"is_random_leader_skin":false,"sleeve_id":{{sleeveId}},"deck_name":"Renamed",
|
||||||
"isDelete":0,"deckFormat":0}
|
"is_delete":0,"deck_format":0}
|
||||||
""";
|
""";
|
||||||
await client.PostAsync("/deck/update", JsonBody(updateJson));
|
await client.PostAsync("/deck/update", JsonBody(updateJson));
|
||||||
|
|
||||||
@@ -194,10 +194,10 @@ public class DeckControllerTests
|
|||||||
using var client = factory.CreateAuthenticatedClient(viewerId);
|
using var client = factory.CreateAuthenticatedClient(viewerId);
|
||||||
|
|
||||||
var deleteJson = $$"""
|
var deleteJson = $$"""
|
||||||
{"viewerId":"0","steamId":0,"steamSessionTicket":"",
|
{"viewer_id":"0","steam_id":0,"steam_session_ticket":"",
|
||||||
"deckNo":1,"classId":{{classId}},"leaderSkinId":{{leaderSkinId}},
|
"deck_no":1,"class_id":{{classId}},"leader_skin_id":{{leaderSkinId}},
|
||||||
"isRandomLeaderSkin":false,"sleeveId":{{sleeveId}},"deckName":null,
|
"is_random_leader_skin":false,"sleeve_id":{{sleeveId}},"deck_name":null,
|
||||||
"isDelete":1,"deckFormat":0}
|
"is_delete":1,"deck_format":0}
|
||||||
""";
|
""";
|
||||||
var response = await client.PostAsync("/deck/update", JsonBody(deleteJson));
|
var response = await client.PostAsync("/deck/update", JsonBody(deleteJson));
|
||||||
|
|
||||||
@@ -219,16 +219,16 @@ public class DeckControllerTests
|
|||||||
using var client = factory.CreateAuthenticatedClient(viewerId);
|
using var client = factory.CreateAuthenticatedClient(viewerId);
|
||||||
|
|
||||||
var updateJson = $$"""
|
var updateJson = $$"""
|
||||||
{"viewerId":"0","steamId":0,"steamSessionTicket":"",
|
{"viewer_id":"0","steam_id":0,"steam_session_ticket":"",
|
||||||
"deckNo":2,"classId":{{classId}},"leaderSkinId":{{leaderSkinId}},
|
"deck_no":2,"class_id":{{classId}},"leader_skin_id":{{leaderSkinId}},
|
||||||
"isRandomLeaderSkin":false,"sleeveId":{{sleeveId}},"deckName":"Second",
|
"is_random_leader_skin":false,"sleeve_id":{{sleeveId}},"deck_name":"Second",
|
||||||
"isDelete":0,"deckFormat":0}
|
"is_delete":0,"deck_format":0}
|
||||||
""";
|
""";
|
||||||
var response = await client.PostAsync("/deck/update", JsonBody(updateJson));
|
var response = await client.PostAsync("/deck/update", JsonBody(updateJson));
|
||||||
|
|
||||||
var body = await response.Content.ReadAsStringAsync();
|
var body = await response.Content.ReadAsStringAsync();
|
||||||
using var doc = JsonDocument.Parse(body);
|
using var doc = JsonDocument.Parse(body);
|
||||||
var decks = doc.RootElement.GetProperty("userDeckList");
|
var decks = doc.RootElement.GetProperty("user_deck_list");
|
||||||
Assert.That(decks.GetArrayLength(), Is.EqualTo(2),
|
Assert.That(decks.GetArrayLength(), Is.EqualTo(2),
|
||||||
"/deck/update should hand back the full refreshed list, saving the client a follow-up.");
|
"/deck/update should hand back the full refreshed list, saving the client a follow-up.");
|
||||||
var names = Enumerable.Range(0, decks.GetArrayLength())
|
var names = Enumerable.Range(0, decks.GetArrayLength())
|
||||||
@@ -247,12 +247,12 @@ public class DeckControllerTests
|
|||||||
await factory.SeedDeckAsync(viewerId, Format.Rotation, 1, name: "Old Name");
|
await factory.SeedDeckAsync(viewerId, Format.Rotation, 1, name: "Old Name");
|
||||||
using var client = factory.CreateAuthenticatedClient(viewerId);
|
using var client = factory.CreateAuthenticatedClient(viewerId);
|
||||||
|
|
||||||
var json = """{"viewerId":"0","steamId":0,"steamSessionTicket":"","deckNo":1,"deckName":"New Name","deckFormat":0}""";
|
var json = """{"viewer_id":"0","steam_id":0,"steam_session_ticket":"","deck_no":1,"deck_name":"New Name","deck_format":0}""";
|
||||||
var response = await client.PostAsync("/deck/update_name", JsonBody(json));
|
var response = await client.PostAsync("/deck/update_name", JsonBody(json));
|
||||||
|
|
||||||
var body = await response.Content.ReadAsStringAsync();
|
var body = await response.Content.ReadAsStringAsync();
|
||||||
using var doc = JsonDocument.Parse(body);
|
using var doc = JsonDocument.Parse(body);
|
||||||
Assert.That(doc.RootElement.GetProperty("userDeck").GetProperty("name").GetString(),
|
Assert.That(doc.RootElement.GetProperty("user_deck").GetProperty("name").GetString(),
|
||||||
Is.EqualTo("New Name"));
|
Is.EqualTo("New Name"));
|
||||||
|
|
||||||
using var scope = factory.Services.CreateScope();
|
using var scope = factory.Services.CreateScope();
|
||||||
@@ -276,12 +276,12 @@ public class DeckControllerTests
|
|||||||
}
|
}
|
||||||
using var client = factory.CreateAuthenticatedClient(viewerId);
|
using var client = factory.CreateAuthenticatedClient(viewerId);
|
||||||
|
|
||||||
var json = $$"""{"viewerId":"0","steamId":0,"steamSessionTicket":"","deckNo":1,"sleeveId":{{sleeveId}},"deckFormat":0}""";
|
var json = $$"""{"viewer_id":"0","steam_id":0,"steam_session_ticket":"","deckNo":1,"sleeve_id":{{sleeveId}},"deckFormat":0}""";
|
||||||
var response = await client.PostAsync("/deck/update_sleeve", JsonBody(json));
|
var response = await client.PostAsync("/deck/update_sleeve", JsonBody(json));
|
||||||
|
|
||||||
var body = await response.Content.ReadAsStringAsync();
|
var body = await response.Content.ReadAsStringAsync();
|
||||||
using var doc = JsonDocument.Parse(body);
|
using var doc = JsonDocument.Parse(body);
|
||||||
Assert.That(doc.RootElement.GetProperty("userDeck").GetProperty("sleeveId").GetInt32(),
|
Assert.That(doc.RootElement.GetProperty("user_deck").GetProperty("sleeve_id").GetInt32(),
|
||||||
Is.EqualTo(sleeveId));
|
Is.EqualTo(sleeveId));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -299,14 +299,14 @@ public class DeckControllerTests
|
|||||||
}
|
}
|
||||||
using var client = factory.CreateAuthenticatedClient(viewerId);
|
using var client = factory.CreateAuthenticatedClient(viewerId);
|
||||||
|
|
||||||
var json = $$"""{"viewerId":"0","steamId":0,"steamSessionTicket":"","deckNo":1,"leaderSkinId":{{skinId}},"deckFormat":0}""";
|
var json = $$"""{"viewer_id":"0","steam_id":0,"steam_session_ticket":"","deck_no":1,"leader_skin_id":{{skinId}},"deck_format":0}""";
|
||||||
var response = await client.PostAsync("/deck/update_leader_skin", JsonBody(json));
|
var response = await client.PostAsync("/deck/update_leader_skin", JsonBody(json));
|
||||||
|
|
||||||
var body = await response.Content.ReadAsStringAsync();
|
var body = await response.Content.ReadAsStringAsync();
|
||||||
using var doc = JsonDocument.Parse(body);
|
using var doc = JsonDocument.Parse(body);
|
||||||
var userDeck = doc.RootElement.GetProperty("userDeck");
|
var userDeck = doc.RootElement.GetProperty("user_deck");
|
||||||
Assert.That(userDeck.GetProperty("leaderSkinId").GetInt32(), Is.EqualTo(skinId));
|
Assert.That(userDeck.GetProperty("leader_skin_id").GetInt32(), Is.EqualTo(skinId));
|
||||||
Assert.That(userDeck.GetProperty("isRandomLeaderSkin").GetInt32(), Is.EqualTo(0),
|
Assert.That(userDeck.GetProperty("is_random_leader_skin").GetInt32(), Is.EqualTo(0),
|
||||||
"Selecting a specific leader skin clears the random-skin flag.");
|
"Selecting a specific leader skin clears the random-skin flag.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -325,17 +325,17 @@ public class DeckControllerTests
|
|||||||
using var client = factory.CreateAuthenticatedClient(viewerId);
|
using var client = factory.CreateAuthenticatedClient(viewerId);
|
||||||
|
|
||||||
var json =
|
var json =
|
||||||
$$"""{"viewerId":"0","steamId":0,"steamSessionTicket":"","deckNo":1,"deckFormat":0,"leaderSkinIdList":[{{string.Join(',', pool)}}]}""";
|
$$"""{"viewer_id":"0","steam_id":0,"steam_session_ticket":"","deck_no":1,"deck_format":0,"leader_skin_id_list":[{{string.Join(',', pool)}}]}""";
|
||||||
var response = await client.PostAsync("/deck/update_random_leader_skin", JsonBody(json));
|
var response = await client.PostAsync("/deck/update_random_leader_skin", JsonBody(json));
|
||||||
|
|
||||||
var body = await response.Content.ReadAsStringAsync();
|
var body = await response.Content.ReadAsStringAsync();
|
||||||
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK), body);
|
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK), body);
|
||||||
|
|
||||||
using var doc = JsonDocument.Parse(body);
|
using var doc = JsonDocument.Parse(body);
|
||||||
var userDeck = doc.RootElement.GetProperty("userDeck");
|
var userDeck = doc.RootElement.GetProperty("user_deck");
|
||||||
Assert.That(pool, Contains.Item(userDeck.GetProperty("leaderSkinId").GetInt32()),
|
Assert.That(pool, Contains.Item(userDeck.GetProperty("leader_skin_id").GetInt32()),
|
||||||
"Chosen skin must come from the supplied pool.");
|
"Chosen skin must come from the supplied pool.");
|
||||||
Assert.That(userDeck.GetProperty("isRandomLeaderSkin").GetInt32(), Is.EqualTo(1));
|
Assert.That(userDeck.GetProperty("is_random_leader_skin").GetInt32(), Is.EqualTo(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -347,7 +347,7 @@ public class DeckControllerTests
|
|||||||
using var client = factory.CreateAuthenticatedClient(viewerId);
|
using var client = factory.CreateAuthenticatedClient(viewerId);
|
||||||
|
|
||||||
var json =
|
var json =
|
||||||
"""{"viewerId":"0","steamId":0,"steamSessionTicket":"","deckNo":1,"deckFormat":0,"leaderSkinIdList":[]}""";
|
"""{"viewer_id":"0","steam_id":0,"steam_session_ticket":"","deck_no":1,"deck_format":0,"leader_skin_id_list":[]}""";
|
||||||
var response = await client.PostAsync("/deck/update_random_leader_skin", JsonBody(json));
|
var response = await client.PostAsync("/deck/update_random_leader_skin", JsonBody(json));
|
||||||
|
|
||||||
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest));
|
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest));
|
||||||
@@ -365,7 +365,7 @@ public class DeckControllerTests
|
|||||||
using var client = factory.CreateAuthenticatedClient(viewerId);
|
using var client = factory.CreateAuthenticatedClient(viewerId);
|
||||||
|
|
||||||
var json =
|
var json =
|
||||||
"""{"viewerId":"0","steamId":0,"steamSessionTicket":"","deckOrder":[2,1],"deckFormat":0}""";
|
"""{"viewer_id":"0","steam_id":0,"steam_session_ticket":"","deck_order":[2,1],"deck_format":0}""";
|
||||||
var response = await client.PostAsync("/deck/update_order", JsonBody(json));
|
var response = await client.PostAsync("/deck/update_order", JsonBody(json));
|
||||||
|
|
||||||
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
|
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
|
||||||
@@ -382,7 +382,7 @@ public class DeckControllerTests
|
|||||||
using var client = factory.CreateAuthenticatedClient(viewerId);
|
using var client = factory.CreateAuthenticatedClient(viewerId);
|
||||||
|
|
||||||
var json =
|
var json =
|
||||||
"""{"viewerId":"0","steamId":0,"steamSessionTicket":"","deckNoList":[1,3],"deckFormat":0}""";
|
"""{"viewer_id":"0","steam_id":0,"steam_session_ticket":"","deck_no_list":[1,3],"deck_format":0}""";
|
||||||
var response = await client.PostAsync("/deck/delete_deck_list", JsonBody(json));
|
var response = await client.PostAsync("/deck/delete_deck_list", JsonBody(json));
|
||||||
|
|
||||||
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
|
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
|
||||||
@@ -401,7 +401,7 @@ public class DeckControllerTests
|
|||||||
long viewerId = await factory.SeedViewerAsync();
|
long viewerId = await factory.SeedViewerAsync();
|
||||||
using var client = factory.CreateAuthenticatedClient(viewerId);
|
using var client = factory.CreateAuthenticatedClient(viewerId);
|
||||||
|
|
||||||
var json = """{"viewerId":"0","steamId":0,"steamSessionTicket":"","deckNo":1,"classId":1}""";
|
var json = """{"viewer_id":"0","steam_id":0,"steam_session_ticket":"","deck_no":1,"class_id":1}""";
|
||||||
var response = await client.PostAsync("/deck/set_deck_redis", JsonBody(json));
|
var response = await client.PostAsync("/deck/set_deck_redis", JsonBody(json));
|
||||||
|
|
||||||
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
|
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ namespace SVSim.UnitTests.Controllers;
|
|||||||
public class LoadControllerTests
|
public class LoadControllerTests
|
||||||
{
|
{
|
||||||
private const string IndexRequestJson =
|
private const string IndexRequestJson =
|
||||||
"""{"viewerId":"0","steamId":0,"steamSessionTicket":"","carrier":"steam","cardMasterHash":""}""";
|
"""{"viewer_id":"0","steam_id":0,"steam_session_ticket":"","carrier":"steam","card_master_hash":""}""";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// JSON keys (camelCased C# property names) for fields the client reads unconditionally.
|
/// JSON keys (camelCased C# property names) for fields the client reads unconditionally.
|
||||||
@@ -25,17 +25,17 @@ public class LoadControllerTests
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private static readonly string[] RequiredIndexKeys =
|
private static readonly string[] RequiredIndexKeys =
|
||||||
{
|
{
|
||||||
"userTutorial", "userInfo", "userCurrency", "userItems",
|
"user_tutorial", "user_info", "user_currency", "user_items",
|
||||||
"userRotationDecks", "userUnlimitedDecks", "userMyRotationDecks",
|
"user_rotation_decks", "user_unlimited_decks", "user_my_rotation_decks",
|
||||||
"userCards", "userClasses", "sleeves", "userEmblems",
|
"user_cards", "user_classes", "sleeves", "user_emblems",
|
||||||
"userDegrees", "leaderSkins", "myPageBackgrounds",
|
"user_degrees", "leader_skins", "my_page_backgrounds",
|
||||||
"userRankInfo", "userRankedMatches", "dailyLoginBonus", "arenaConfig",
|
"user_rank_info", "user_ranked_matches", "daily_login_bonus", "arena_config",
|
||||||
"redEtherOverrides", "maintenanceCards", "arenaInfos", "rankInfo",
|
"red_ether_overrides", "maintenance_cards", "arena_infos", "rank_info",
|
||||||
"classExp", "loadingTipCardExclusions", "defaultSettings",
|
"class_exp", "loading_tip_card_exclusions", "default_settings",
|
||||||
"unlimitedBanList", "rotationSets",
|
"unlimited_ban_list", "rotation_sets",
|
||||||
"reprintedCards", "spotCards", "featureMaintenances",
|
"reprinted_cards", "spot_cards", "feature_maintenances",
|
||||||
"specialCrystalInfos", "openBattlefieldIds", "lootBoxRegulations",
|
"special_crystal_infos", "open_battlefield_ids", "loot_box_regulations",
|
||||||
"gatheringInfo", "userConfig", "deckFormat", "cardSetIdForResourceDlView"
|
"gathering_info", "user_config", "deck_format", "card_set_id_for_resource_dl_view"
|
||||||
};
|
};
|
||||||
|
|
||||||
private static async Task<JsonElement> PostIndexAndReadBody(SVSimTestFactory factory, long viewerId)
|
private static async Task<JsonElement> PostIndexAndReadBody(SVSimTestFactory factory, long viewerId)
|
||||||
@@ -100,7 +100,7 @@ public class LoadControllerTests
|
|||||||
|
|
||||||
var root = await PostIndexAndReadBody(factory, viewerId);
|
var root = await PostIndexAndReadBody(factory, viewerId);
|
||||||
|
|
||||||
Assert.That(root.GetProperty("userRankInfo").ValueKind, Is.EqualTo(JsonValueKind.Array));
|
Assert.That(root.GetProperty("user_rank_info").ValueKind, Is.EqualTo(JsonValueKind.Array));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -114,7 +114,7 @@ public class LoadControllerTests
|
|||||||
|
|
||||||
var root = await PostIndexAndReadBody(factory, viewerId);
|
var root = await PostIndexAndReadBody(factory, viewerId);
|
||||||
|
|
||||||
Assert.That(root.GetProperty("userRankInfo").GetArrayLength(), Is.EqualTo(5));
|
Assert.That(root.GetProperty("user_rank_info").GetArrayLength(), Is.EqualTo(5));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -127,7 +127,7 @@ public class LoadControllerTests
|
|||||||
|
|
||||||
var root = await PostIndexAndReadBody(factory, viewerId);
|
var root = await PostIndexAndReadBody(factory, viewerId);
|
||||||
|
|
||||||
Assert.That(root.GetProperty("rotationSets").GetArrayLength(),
|
Assert.That(root.GetProperty("rotation_sets").GetArrayLength(),
|
||||||
Is.GreaterThanOrEqualTo(2));
|
Is.GreaterThanOrEqualTo(2));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,12 +141,12 @@ public class LoadControllerTests
|
|||||||
|
|
||||||
var root = await PostIndexAndReadBody(factory, viewerId);
|
var root = await PostIndexAndReadBody(factory, viewerId);
|
||||||
|
|
||||||
foreach (var key in new[] { "userRotationDecks", "userUnlimitedDecks", "userMyRotationDecks" })
|
foreach (var key in new[] { "user_rotation_decks", "user_unlimited_decks", "user_my_rotation_decks" })
|
||||||
{
|
{
|
||||||
var container = root.GetProperty(key);
|
var container = root.GetProperty(key);
|
||||||
Assert.That(container.ValueKind, Is.EqualTo(JsonValueKind.Object),
|
Assert.That(container.ValueKind, Is.EqualTo(JsonValueKind.Object),
|
||||||
$"{key} should be the UserFormatDeckInfo object wrapper, not a raw array.");
|
$"{key} should be the UserFormatDeckInfo object wrapper, not a raw array.");
|
||||||
var inner = container.GetProperty("userDecks");
|
var inner = container.GetProperty("user_decks");
|
||||||
Assert.That(inner.ValueKind, Is.EqualTo(JsonValueKind.Array));
|
Assert.That(inner.ValueKind, Is.EqualTo(JsonValueKind.Array));
|
||||||
Assert.That(inner.GetArrayLength(), Is.EqualTo(0),
|
Assert.That(inner.GetArrayLength(), Is.EqualTo(0),
|
||||||
$"{key}.userDecks must be an empty array for a deckless viewer, not null.");
|
$"{key}.userDecks must be an empty array for a deckless viewer, not null.");
|
||||||
|
|||||||
@@ -14,10 +14,10 @@ namespace SVSim.UnitTests.Controllers;
|
|||||||
public class PracticeControllerTests
|
public class PracticeControllerTests
|
||||||
{
|
{
|
||||||
private const string BaseRequestJson =
|
private const string BaseRequestJson =
|
||||||
"""{"viewerId":"0","steamId":0,"steamSessionTicket":""}""";
|
"""{"viewer_id":"0","steam_id":0,"steam_session_ticket":""}""";
|
||||||
|
|
||||||
private static string DeckFormatRequestJson(Format f) =>
|
private static string DeckFormatRequestJson(Format f) =>
|
||||||
$$"""{"viewerId":"0","steamId":0,"steamSessionTicket":"","deckFormat":{{(int)f}}}""";
|
$$"""{"viewer_id":"0","steam_id":0,"steam_session_ticket":"","deck_format":{{(int)f}}}""";
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public async Task Info_returns_non_empty_opponent_array()
|
public async Task Info_returns_non_empty_opponent_array()
|
||||||
@@ -36,7 +36,7 @@ public class PracticeControllerTests
|
|||||||
Assert.That(doc.RootElement.ValueKind, Is.EqualTo(JsonValueKind.Array),
|
Assert.That(doc.RootElement.ValueKind, Is.EqualTo(JsonValueKind.Array),
|
||||||
"/practice/info returns a bare array (no wrapper object) per spec.");
|
"/practice/info returns a bare array (no wrapper object) per spec.");
|
||||||
Assert.That(doc.RootElement.GetArrayLength(), Is.GreaterThan(0));
|
Assert.That(doc.RootElement.GetArrayLength(), Is.GreaterThan(0));
|
||||||
Assert.That(doc.RootElement[0].GetProperty("practiceId").GetInt32(), Is.GreaterThan(0));
|
Assert.That(doc.RootElement[0].GetProperty("practice_id").GetInt32(), Is.GreaterThan(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -55,8 +55,8 @@ public class PracticeControllerTests
|
|||||||
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK), body);
|
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK), body);
|
||||||
|
|
||||||
using var doc = JsonDocument.Parse(body);
|
using var doc = JsonDocument.Parse(body);
|
||||||
var rotation = doc.RootElement.GetProperty("userDeckRotation");
|
var rotation = doc.RootElement.GetProperty("user_deck_rotation");
|
||||||
var unlimited = doc.RootElement.GetProperty("userDeckUnlimited");
|
var unlimited = doc.RootElement.GetProperty("user_deck_unlimited");
|
||||||
Assert.That(rotation.GetArrayLength(), Is.EqualTo(1));
|
Assert.That(rotation.GetArrayLength(), Is.EqualTo(1));
|
||||||
Assert.That(rotation[0].GetProperty("name").GetString(), Is.EqualTo("Rotation Deck"));
|
Assert.That(rotation[0].GetProperty("name").GetString(), Is.EqualTo("Rotation Deck"));
|
||||||
Assert.That(unlimited.GetArrayLength(), Is.EqualTo(1));
|
Assert.That(unlimited.GetArrayLength(), Is.EqualTo(1));
|
||||||
@@ -77,8 +77,8 @@ public class PracticeControllerTests
|
|||||||
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK), body);
|
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK), body);
|
||||||
|
|
||||||
using var doc = JsonDocument.Parse(body);
|
using var doc = JsonDocument.Parse(body);
|
||||||
Assert.That(doc.RootElement.GetProperty("userDeckRotation").GetArrayLength(), Is.EqualTo(0));
|
Assert.That(doc.RootElement.GetProperty("user_deck_rotation").GetArrayLength(), Is.EqualTo(0));
|
||||||
Assert.That(doc.RootElement.GetProperty("userDeckUnlimited").GetArrayLength(), Is.EqualTo(0));
|
Assert.That(doc.RootElement.GetProperty("user_deck_unlimited").GetArrayLength(), Is.EqualTo(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -104,7 +104,7 @@ public class PracticeControllerTests
|
|||||||
// recoveryData is an opaque JSON blob serialized to string by the client; the server
|
// recoveryData is an opaque JSON blob serialized to string by the client; the server
|
||||||
// is supposed to accept it without validation. Anything goes.
|
// is supposed to accept it without validation. Anything goes.
|
||||||
var finishJson =
|
var finishJson =
|
||||||
"""{"viewerId":"0","steamId":0,"steamSessionTicket":"","deckNo":1,"isWin":1,"evolveCount":2,"totalTurn":5,"enemyClassId":3,"difficulty":1,"deckFormat":0,"classId":1,"recoveryData":"{\"opaque\":\"blob\"}"}""";
|
"""{"viewer_id":"0","steam_id":0,"steam_session_ticket":"","deck_no":1,"is_win":1,"evolve_count":2,"total_turn":5,"enemy_class_id":3,"difficulty":1,"deck_format":0,"class_id":1,"recovery_data":"{\"opaque\":\"blob\"}"}""";
|
||||||
|
|
||||||
var response = await client.PostAsync("/practice/finish",
|
var response = await client.PostAsync("/practice/finish",
|
||||||
new StringContent(finishJson, Encoding.UTF8, "application/json"));
|
new StringContent(finishJson, Encoding.UTF8, "application/json"));
|
||||||
@@ -113,8 +113,8 @@ public class PracticeControllerTests
|
|||||||
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK), body);
|
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK), body);
|
||||||
|
|
||||||
using var doc = JsonDocument.Parse(body);
|
using var doc = JsonDocument.Parse(body);
|
||||||
Assert.That(doc.RootElement.GetProperty("getClassExperience").GetInt32(), Is.EqualTo(0));
|
Assert.That(doc.RootElement.GetProperty("get_class_experience").GetInt32(), Is.EqualTo(0));
|
||||||
Assert.That(doc.RootElement.GetProperty("classExperience").GetInt32(), Is.EqualTo(0));
|
Assert.That(doc.RootElement.GetProperty("class_experience").GetInt32(), Is.EqualTo(0));
|
||||||
Assert.That(doc.RootElement.GetProperty("rewardList").GetArrayLength(), Is.EqualTo(0));
|
Assert.That(doc.RootElement.GetProperty("reward_list").GetArrayLength(), Is.EqualTo(0));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
138
SVSim.UnitTests/Infrastructure/WireSerializationTests.cs
Normal file
138
SVSim.UnitTests/Infrastructure/WireSerializationTests.cs
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using MessagePack;
|
||||||
|
using MessagePack.Resolvers;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using SVSim.EmulatedEntrypoint.Middlewares;
|
||||||
|
using SVSim.EmulatedEntrypoint.Models.Dtos;
|
||||||
|
using SVSim.EmulatedEntrypoint.Models.Dtos.Responses;
|
||||||
|
|
||||||
|
namespace SVSim.UnitTests.Infrastructure;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Exercises the JSON → JTree → msgpack pipeline that runs inside
|
||||||
|
/// <see cref="ShadowverseTranslationMiddleware"/>. The middleware itself is hard to unit-test
|
||||||
|
/// in isolation because it depends on HttpContext + the encryption layer; instead we re-run
|
||||||
|
/// the exact same transformation steps the middleware would and assert on the resulting
|
||||||
|
/// msgpack bytes converted back to JSON.
|
||||||
|
///
|
||||||
|
/// What this guards: that a controller returning an object with mixed null/non-null optional
|
||||||
|
/// fields produces a msgpack map containing ONLY the non-null fields, with snake_case keys.
|
||||||
|
/// That's the load-bearing property — the Unity client's <c>Keys.Contains</c> + <c>.ToInt()</c>
|
||||||
|
/// idiom NREs the moment the wire serializes a present-but-null field.
|
||||||
|
/// </summary>
|
||||||
|
public class WireSerializationTests
|
||||||
|
{
|
||||||
|
private static readonly JsonSerializerOptions ControllerJsonOptions = new()
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||||
|
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly MessagePackSerializerOptions MsgPackOptions =
|
||||||
|
MessagePackSerializerOptions.Standard.WithResolver(ContractlessStandardResolver.Instance);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reproduces what the middleware does to a controller's response object: serialize via
|
||||||
|
/// our JSON options (snake_case, omit null) → parse JSON tree → convert to plain CLR tree
|
||||||
|
/// → msgpack-serialize → msgpack-to-JSON. Returns the JSON the client would see.
|
||||||
|
/// </summary>
|
||||||
|
private static JsonDocument RoundTripThroughWirePipeline<T>(T response)
|
||||||
|
{
|
||||||
|
var json = JsonSerializer.Serialize(response, ControllerJsonOptions);
|
||||||
|
var tree = ShadowverseTranslationMiddleware.ConvertJsonTreeToPlainObject(JToken.Parse(json));
|
||||||
|
var msgPackBytes = MessagePackSerializer.Serialize(tree, MsgPackOptions);
|
||||||
|
var msgPackJson = MessagePackSerializer.ConvertToJson(msgPackBytes, MsgPackOptions);
|
||||||
|
return JsonDocument.Parse(msgPackJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void GameStartResponse_with_null_optional_fields_omits_them_from_msgpack()
|
||||||
|
{
|
||||||
|
var response = new GameStartResponse
|
||||||
|
{
|
||||||
|
NowViewerId = 42,
|
||||||
|
NowName = "Tester",
|
||||||
|
NowTutorialStep = "100",
|
||||||
|
IsSetTransitionPassword = true,
|
||||||
|
NowRank = new Dictionary<string, string> { { "1", "RankName_010" } },
|
||||||
|
TransitionAccountData = new List<TransitionAccountData>
|
||||||
|
{
|
||||||
|
new() { SocialAccountId = "76561198000000001", SocialAccountType = "5", ConnectedViewerId = "42" }
|
||||||
|
},
|
||||||
|
// Both deliberately left at null — must NOT appear in the msgpack output.
|
||||||
|
RewriteViewerId = null,
|
||||||
|
AccountDeleteReservationStatus = null,
|
||||||
|
TosState = 1, PolicyState = 1, KorAuthorityState = 0,
|
||||||
|
TosId = 1, PolicyId = 1, KorAuthorityId = 0
|
||||||
|
};
|
||||||
|
|
||||||
|
using var doc = RoundTripThroughWirePipeline(response);
|
||||||
|
var root = doc.RootElement;
|
||||||
|
|
||||||
|
Assert.That(root.TryGetProperty("rewrite_viewer_id", out _), Is.False,
|
||||||
|
"Null `rewrite_viewer_id` must not appear in msgpack output — client NREs on Keys.Contains+.ToInt() against Nil.");
|
||||||
|
Assert.That(root.TryGetProperty("account_delete_reservation_status", out _), Is.False,
|
||||||
|
"Null `account_delete_reservation_status` must not appear (presence triggers client behavior).");
|
||||||
|
|
||||||
|
// Sanity: required fields still there, with snake_case keys.
|
||||||
|
Assert.That(root.GetProperty("now_viewer_id").GetInt64(), Is.EqualTo(42));
|
||||||
|
Assert.That(root.GetProperty("now_name").GetString(), Is.EqualTo("Tester"));
|
||||||
|
Assert.That(root.GetProperty("now_tutorial_step").GetString(), Is.EqualTo("100"));
|
||||||
|
Assert.That(root.GetProperty("tos_state").GetInt64(), Is.EqualTo(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void GameStartResponse_with_set_optional_field_emits_it_in_msgpack()
|
||||||
|
{
|
||||||
|
// Mirror image of the omission test: when an optional field IS set, it must reach the
|
||||||
|
// client. Otherwise we'd have papered over the bug by accidentally dropping everything.
|
||||||
|
var response = new GameStartResponse
|
||||||
|
{
|
||||||
|
NowViewerId = 42,
|
||||||
|
NowName = "Tester",
|
||||||
|
NowTutorialStep = "100",
|
||||||
|
IsSetTransitionPassword = false,
|
||||||
|
RewriteViewerId = 999, // explicitly set
|
||||||
|
AccountDeleteReservationStatus = 1,
|
||||||
|
TosState = 1, PolicyState = 1, KorAuthorityState = 0,
|
||||||
|
TosId = 1, PolicyId = 1, KorAuthorityId = 0
|
||||||
|
};
|
||||||
|
|
||||||
|
using var doc = RoundTripThroughWirePipeline(response);
|
||||||
|
var root = doc.RootElement;
|
||||||
|
|
||||||
|
Assert.That(root.GetProperty("rewrite_viewer_id").GetInt64(), Is.EqualTo(999));
|
||||||
|
Assert.That(root.GetProperty("account_delete_reservation_status").GetInt64(), Is.EqualTo(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void GameStartResponse_nested_transition_account_data_round_trips_with_snake_case_keys()
|
||||||
|
{
|
||||||
|
// The nested list of TransitionAccountData entries needs to keep its per-entry shape
|
||||||
|
// (snake_case keys, three string fields) after going JSON → tree → msgpack → JSON.
|
||||||
|
var response = new GameStartResponse
|
||||||
|
{
|
||||||
|
NowViewerId = 42,
|
||||||
|
NowName = "Tester",
|
||||||
|
NowTutorialStep = "100",
|
||||||
|
TransitionAccountData = new List<TransitionAccountData>
|
||||||
|
{
|
||||||
|
new() { SocialAccountId = "111", SocialAccountType = "5", ConnectedViewerId = "42" }
|
||||||
|
},
|
||||||
|
TosState = 1, PolicyState = 1, KorAuthorityState = 0,
|
||||||
|
TosId = 1, PolicyId = 1, KorAuthorityId = 0
|
||||||
|
};
|
||||||
|
|
||||||
|
using var doc = RoundTripThroughWirePipeline(response);
|
||||||
|
var transitions = doc.RootElement.GetProperty("transition_account_data");
|
||||||
|
Assert.That(transitions.ValueKind, Is.EqualTo(JsonValueKind.Array));
|
||||||
|
Assert.That(transitions.GetArrayLength(), Is.EqualTo(1));
|
||||||
|
|
||||||
|
var entry = transitions[0];
|
||||||
|
Assert.That(entry.GetProperty("social_account_id").GetString(), Is.EqualTo("111"));
|
||||||
|
Assert.That(entry.GetProperty("social_account_type").GetString(), Is.EqualTo("5"));
|
||||||
|
Assert.That(entry.GetProperty("connected_viewer_id").GetString(), Is.EqualTo("42"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,7 +30,7 @@ public class RoutingSmokeTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
private const string ValidBaseRequestJson =
|
private const string ValidBaseRequestJson =
|
||||||
"""{"viewerId":"0","steamId":0,"steamSessionTicket":""}""";
|
"""{"viewer_id":"0","steam_id":0,"steam_session_ticket":""}""";
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public async Task CheckSpecialTitle_resolves_to_CheckController()
|
public async Task CheckSpecialTitle_resolves_to_CheckController()
|
||||||
@@ -48,7 +48,7 @@ public class RoutingSmokeTests
|
|||||||
var body = await response.Content.ReadAsStringAsync();
|
var body = await response.Content.ReadAsStringAsync();
|
||||||
// Plain-JSON path uses camelCase (System.Text.Json default); MessagePack [Key] only applies
|
// Plain-JSON path uses camelCase (System.Text.Json default); MessagePack [Key] only applies
|
||||||
// to the Unity-UA encrypted path through ShadowverseTranslationMiddleware.
|
// to the Unity-UA encrypted path through ShadowverseTranslationMiddleware.
|
||||||
Assert.That(body, Does.Contain("\"titleImageId\":\"0\""),
|
Assert.That(body, Does.Contain("\"title_image_id\":\"0\""),
|
||||||
"SpecialTitleCheck should return the built-in title id \"0\".");
|
"SpecialTitleCheck should return the built-in title id \"0\".");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user