feat(deck-builder): /deck_code mint + /deck resolve with 3-min in-memory TTL
Adds the portal pair (shadowverse-portal.com deck-builder endpoints) as anonymous routes on the app server. The translation middleware learns a new [NoWireEncryption] attribute that skips both AES calls but keeps the rest of the msgpack + base64 + envelope pipeline intact, matching prod's portal wire profile observed in data_dumps/traffic_prod_deckcode.ndjson. Storage is a 3-minute IMemoryCache — codes are anonymous-global, 4-char lowercase alphanumeric (matches the shortest prod sample). Foil bit is stripped on mint to match prod's normalize-on-encode behaviour. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
56
SVSim.UnitTests/Services/DeckCodeServiceTests.cs
Normal file
56
SVSim.UnitTests/Services/DeckCodeServiceTests.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using SVSim.EmulatedEntrypoint.Models.Dtos.Responses.DeckBuilder;
|
||||
using SVSim.EmulatedEntrypoint.Services;
|
||||
|
||||
namespace SVSim.UnitTests.Services;
|
||||
|
||||
public class DeckCodeServiceTests
|
||||
{
|
||||
private static DeckCodeService NewService(out IMemoryCache cache, IRandom? random = null)
|
||||
{
|
||||
cache = new MemoryCache(new MemoryCacheOptions());
|
||||
return new DeckCodeService(cache, random ?? new SystemRandom());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Mint_returns_4char_lowercase_alphanumeric_code()
|
||||
{
|
||||
var svc = NewService(out _);
|
||||
|
||||
var code = svc.Mint(new DeckPayload { Clan = "1", CardID = new() { 100211010 } });
|
||||
|
||||
Assert.That(code, Has.Length.EqualTo(4));
|
||||
Assert.That(code, Does.Match("^[a-z0-9]+$"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Resolve_returns_payload_when_code_unexpired()
|
||||
{
|
||||
var svc = NewService(out _);
|
||||
var original = new DeckPayload { Clan = "4", CardID = new() { 100414020, 100414020 } };
|
||||
|
||||
var code = svc.Mint(original);
|
||||
var resolved = svc.TryResolve(code);
|
||||
|
||||
Assert.That(resolved, Is.SameAs(original));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Resolve_returns_null_for_unknown_code()
|
||||
{
|
||||
var svc = NewService(out _);
|
||||
|
||||
Assert.That(svc.TryResolve("nope"), Is.Null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Resolve_returns_null_after_cache_eviction()
|
||||
{
|
||||
// Don't sleep for the 3-minute TTL — drop the entry directly to simulate expiry.
|
||||
var svc = NewService(out var cache);
|
||||
var code = svc.Mint(new DeckPayload { Clan = "1", CardID = new() { 100211010 } });
|
||||
cache.Remove(DeckCodeService.CacheKey(code));
|
||||
|
||||
Assert.That(svc.TryResolve(code), Is.Null);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user