mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-26 07:29:00 -07:00
333 lines
13 KiB
C#
333 lines
13 KiB
C#
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;
|
|
}
|
|
}
|