Radicade Public Code

This commit is contained in:
JogmansonReal
2026-10-16 23:33:13 -07:00
commit 55175acdf6
832 changed files with 363261 additions and 0 deletions
+332
View File
@@ -0,0 +1,332 @@
using System.Security.Claims;
using System.Text.Json;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using RadicadeServer.API.Keepsakes;
using RadicadeServer.DB;
using RadicadeServer.RecNet.Keepsakes;
using Xunit;
namespace RadicadeServer.Tests;
/// <summary>
/// Keepsake rooms: which rooms the server tells the client it may place keepsakes in.
///
/// The client's room-role permissions have no keepsake entry and its room DTO carries no keepsake
/// field, so the room's <c>KeepsakeRoomListId</c> - the thing it reports as missing with
/// "This room does not have keepsake permissions." - can only arrive with
/// <c>GET /api/keepsakes/rooms/{id}</c>. A configured room therefore answers with the room fields
/// even while it holds nothing, and an unconfigured one keeps the old NO CONTENT.
/// </summary>
public class KeepsakeRoomConfigTests
{
private const long Room = 25;
private static RecDBContext NewDb()
=> new RecDBContext(TestDbOptions.Create()
.UseInMemoryDatabase(Guid.NewGuid().ToString()).Options);
private static KeepsakesAPI CreateApi(RecDBContext? db)
{
var identity = new ClaimsIdentity("TestAuth");
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, "2"));
var context = new DefaultHttpContext { User = new ClaimsPrincipal(identity) };
return new KeepsakesAPI(db!) { ControllerContext = new ControllerContext { HttpContext = context } };
}
/// <summary>
/// Runs one endpoint call against a config. The seam is process-wide and the endpoint reads it
/// per call, so the override only has to be in place for the call itself.
/// </summary>
private static async Task<T> WithConfigAsync<T>(KeepsakeRoomConfig config, Func<Task<T>> call)
{
var previous = KeepsakeRoomConfig.Override;
KeepsakeRoomConfig.Override = config;
try
{
return await call();
}
finally
{
KeepsakeRoomConfig.Override = previous;
}
}
[Fact]
public void NothingConfiguredIsNothingConfigured()
{
var config = KeepsakeRoomConfig.Parse(null);
Assert.False(config.Any);
Assert.False(config.IsKeepsakeRoom(Room));
Assert.Equal(KeepsakeRoomConfig.DefaultRoomListId, config.RoomListId);
Assert.Equal(KeepsakeRoomConfig.DefaultRoomLimit, config.RoomLimit);
}
[Fact]
public void ExplicitEnvironmentRoomsReplaceAllRoomsFromFile()
{
var config = KeepsakeRoomConfig.Parse("""{"AllRooms":true}""", rooms: "25");
Assert.True(config.IsKeepsakeRoom(25));
Assert.False(config.IsKeepsakeRoom(26));
}
[Theory]
[InlineData(0)]
[InlineData(-1)]
[InlineData(4294967297L)]
public void InvalidRoomLimitsUseDefault(long limit)
{
var config = KeepsakeRoomConfig.Parse($$"""{"RoomLimit":{{limit}}}""");
Assert.Equal(KeepsakeRoomConfig.DefaultRoomLimit, config.RoomLimit);
}
[Fact]
public void AllRoomsFlagMakesEveryRoomAKeepsakeRoom()
{
var config = KeepsakeRoomConfig.Parse("""{ "AllRooms": true }""");
Assert.True(config.Any);
Assert.True(config.IsKeepsakeRoom(1));
Assert.True(config.IsKeepsakeRoom(Room));
// No useful list of "every room" exists, so the payload advertises the room asked about.
Assert.Equal(new[] { Room }, config.AdvertisedRooms(Room));
}
[Fact]
public void RoomIdsAcceptNumbersAndStrings()
{
var config = KeepsakeRoomConfig.Parse("""{ "RoomIds": [3, "25"], "RoomListId": 7, "RoomLimit": 5 }""");
Assert.True(config.IsKeepsakeRoom(3));
Assert.True(config.IsKeepsakeRoom(Room));
Assert.False(config.IsKeepsakeRoom(26));
Assert.Equal(7, config.RoomListId);
Assert.Equal(5, config.RoomLimit);
Assert.Equal(new long[] { 3, Room }, config.AdvertisedRooms(Room));
}
[Fact]
public void ABareArrayIsAListOfRooms()
{
var config = KeepsakeRoomConfig.Parse("[3, 25]");
Assert.True(config.IsKeepsakeRoom(3));
Assert.True(config.IsKeepsakeRoom(Room));
}
/// <summary>
/// A half-written file is "nothing configured", not a crash and not an enabled server: this
/// switch decides whether rooms may hold keepsakes, so a typo must not turn it on.
/// </summary>
[Fact]
public void AMalformedFileConfiguresNothing()
{
var config = KeepsakeRoomConfig.Parse("{ \"RoomIds\": [3,");
Assert.False(config.Any);
Assert.False(config.IsKeepsakeRoom(3));
}
/// <summary>
/// The environment names the rooms INSTEAD of the file: an operator reaching for
/// <c>RADICADE_KEEPSAKE_ROOMS=*</c> means "all of them", not "all of them plus this list".
/// </summary>
[Fact]
public void TheEnvironmentReplacesTheFilesRooms()
{
var replaced = KeepsakeRoomConfig.Parse("""{ "RoomIds": [3] }""", rooms: "25;26");
Assert.False(replaced.IsKeepsakeRoom(3));
Assert.True(replaced.IsKeepsakeRoom(25));
Assert.True(replaced.IsKeepsakeRoom(26));
var all = KeepsakeRoomConfig.Parse("""{ "RoomIds": [3] }""", rooms: "*");
Assert.True(all.AllRooms);
Assert.True(all.IsKeepsakeRoom(9999));
}
[Fact]
public void TheEnvironmentCarriesTheListIdAndLimit()
{
var config = KeepsakeRoomConfig.Parse(null, rooms: "25", roomListId: "9", roomLimit: "12");
Assert.Equal(9, config.RoomListId);
Assert.Equal(12, config.RoomLimit);
}
/// <summary>
/// A keepsake room answers with the room fields even while it holds nothing, and the answer
/// still carries the three instance lists the client parses.
/// </summary>
[Fact]
public async Task AConfiguredRoomAnswersTheRoomFields()
{
await using var db = NewDb();
var api = CreateApi(db);
var result = await WithConfigAsync(
KeepsakeRoomConfig.Parse("""{ "RoomIds": [25], "RoomListId": 7, "RoomLimit": 5 }"""),
() => api.Room(Room));
var ok = Assert.IsType<OkObjectResult>(result);
var json = JsonSerializer.SerializeToElement(ok.Value);
Assert.Equal(Room, json.GetProperty("KeepsakeRoomId").GetInt64());
Assert.Equal(7, json.GetProperty("KeepsakeRoomListId").GetInt64());
Assert.Equal(5, json.GetProperty("KeepsakeRoomLimit").GetInt32());
Assert.Equal(new long[] { Room }, json.GetProperty("KeepsakeRooms").EnumerateArray().Select(e => e.GetInt64()).ToArray());
// The list the client resolves has to contain the room it is loading.
var list = Assert.Single(json.GetProperty("KeepsakeRoomLists").EnumerateArray());
Assert.Equal(7, list.GetProperty("KeepsakeRoomListId").GetInt64());
Assert.Equal(new long[] { Room }, list.GetProperty("KeepsakeRooms").EnumerateArray().Select(e => e.GetProperty("RoomId").GetInt64()).ToArray());
// Nothing is placed, so there is nothing for the keepsake loader to resolve - which is
// what keeps this answer from being the body the client null-references over.
Assert.Empty(json.GetProperty("KeepsakeInstances").EnumerateArray());
Assert.Empty(json.GetProperty("KeepsakeCollectionRecords").EnumerateArray());
// The room names the progression event its keepsakes belong to: that id is what the client
// looks up among the active events before it offers a keepsake to place at all.
Assert.Equal(
new[] { KeepsakeRoomConfig.DefaultEventId },
json.GetProperty("KeepsakeProgressionEventIds").EnumerateArray().Select(e => e.GetInt64()).ToArray());
}
[Fact]
public async Task AnUnconfiguredRoomStillAnswersNoContent()
{
await using var db = NewDb();
var api = CreateApi(db);
var result = await WithConfigAsync(KeepsakeRoomConfig.Parse("""{ "RoomIds": [999] }"""), () => api.Room(Room));
Assert.IsType<NoContentResult>(result);
}
/// <summary>A room that already holds a keepsake keeps its body whether or not it is listed -
/// while the feature itself is on, which is forced here because a server with no keepsake room
/// configured is a server with keepsakes disabled.</summary>
[Fact]
public async Task ARoomWithAPlacedKeepsakeKeepsItsBody()
{
await using var db = NewDb();
await db.KeepsakeInstances.AddAsync(new KeepsakeInstance
{
KeepsakeInstanceId = Guid.Parse("a75f7547-79eb-47c6-8986-6767abcb4f92"),
RoomId = Room,
KeepsakeCategoryId = 0,
PlacedByAccountId = 2,
PlacedAt = DateTime.UtcNow
});
await db.SaveChangesAsync();
var api = CreateApi(db);
var previousFeature = KeepsakeFeature.Override;
KeepsakeFeature.Override = true;
try
{
var result = await WithConfigAsync(KeepsakeRoomConfig.Parse(null), () => api.Room(Room));
var ok = Assert.IsType<OkObjectResult>(result);
var json = JsonSerializer.SerializeToElement(ok.Value);
Assert.Single(json.GetProperty("KeepsakeInstances").EnumerateArray());
Assert.False(json.TryGetProperty("KeepsakeRoomListId", out _));
}
finally
{
KeepsakeFeature.Override = previousFeature;
}
}
/// <summary>
/// ...and with the feature OFF the same room answers NO CONTENT, keepsake and all. That is the
/// point of the switch: a disabled server does not describe keepsakes to a client at all, so a
/// room holding a placed keepsake is not the exception that still talks about one.
/// </summary>
[Fact]
public async Task TheFeatureSwitchHidesEvenPlacedKeepsakes()
{
await using var db = NewDb();
await db.KeepsakeInstances.AddAsync(new KeepsakeInstance
{
KeepsakeInstanceId = Guid.NewGuid(),
RoomId = Room,
KeepsakeCategoryId = 0,
PlacedByAccountId = 2,
PlacedAt = DateTime.UtcNow
});
await db.SaveChangesAsync();
var previousFeature = KeepsakeFeature.Override;
KeepsakeFeature.Override = false;
try
{
var result = await WithConfigAsync(KeepsakeRoomConfig.Parse("""{ "AllRooms": true }"""),
() => CreateApi(db).Room(Room));
Assert.IsType<NoContentResult>(result);
}
finally
{
KeepsakeFeature.Override = previousFeature;
}
}
/// <summary>
/// The file this repository ships gives every room the permission. That answer is the one the
/// client reads off `GET /api/keepsakes/rooms/{id}` as "this room can hold keepsakes", so a
/// checkout that shipped it switched off would keep the MakerPen banner the feature exists to
/// clear. Narrowing it is an operator edit - `AllRooms: false` plus the ids in `RoomIds`.
/// </summary>
[Fact]
public void TheShippedConfigEnablesKeepsakesAsRequested()
{
var path = ShippedConfigPath();
Assert.NotNull(path);
var config = KeepsakeRoomConfig.Parse(File.ReadAllText(path!));
Assert.True(config.Any);
Assert.True(config.AllRooms);
Assert.True(config.IsKeepsakeRoom(25));
Assert.True(config.RoomLimit > 0);
Assert.True(config.EventId > 0);
}
/// <summary>
/// The feature switch follows that config: no keepsake room configured means the whole feature
/// is off, which is the state the room, the catalog, the event and the client's own feature flag
/// all read - see <see cref="KeepsakeFeature"/>. RADICADE_KEEPSAKES is the operator's override of
/// exactly that inference.
/// </summary>
[Fact]
public void NoKeepsakeRoomMeansTheFeatureIsOff()
{
Assert.False(KeepsakeFeature.EnabledFor(KeepsakeRoomConfig.Parse(null), environment: null));
Assert.False(KeepsakeFeature.EnabledFor(KeepsakeRoomConfig.Parse(null), environment: "off"));
Assert.True(KeepsakeFeature.EnabledFor(KeepsakeRoomConfig.Parse("""{ "AllRooms": true }"""), environment: null));
Assert.True(KeepsakeFeature.EnabledFor(KeepsakeRoomConfig.Parse("""{ "RoomIds": [25] }"""), environment: null));
Assert.False(KeepsakeFeature.EnabledFor(KeepsakeRoomConfig.Parse("""{ "AllRooms": true }"""), environment: "off"));
Assert.True(KeepsakeFeature.EnabledFor(KeepsakeRoomConfig.Parse(null), environment: "on"));
}
/// <summary>
/// The checked-in config, found from the test binaries rather than from the working directory:
/// the test host's cwd is a bin folder, so the file the repository ships would otherwise go
/// unread and this guard would silently pass.
/// </summary>
private static string? ShippedConfigPath()
{
var directory = new DirectoryInfo(AppContext.BaseDirectory);
for (var depth = 0; depth < 6 && directory != null; depth++, directory = directory.Parent)
{
var candidate = Path.Join(directory.FullName, "Server", "RadicadeServer-Config", KeepsakeRoomConfig.FileName);
if (File.Exists(candidate)) return candidate;
}
return null;
}
}