mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-25 23:18:56 -07:00
Radicade Public Code
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user