using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using RadicadeServer.Admin;
using RadicadeServer.DB;
using RadicadeServer.RecNet.Auth;
using RadicadeServer.RecNet.Enums;
using RadicadeServer.RecNet.Rooms;
using RadicadeServer.Utils;
using Xunit;
namespace RadicadeServer.Tests;
///
/// IsDeveloperOwned is the flag that files a room under the client's Dev Rooms row, and
/// the room payload never carried it at all - so every room read as "not developer-owned" and a
/// developer's own rooms were missing from that list. It is a property of the room, stamped when
/// the room is made (a player makes a room by CLONING a template - there is no separate create
/// route) from whether the maker holds the developer role.
///
/// These pin the three ways it can come out wrong: stamped from nothing (always false), stamped
/// on a dorm (a dev's dorm is a home, not a project), and stamping the room row while the room
/// the client is handed still defaults to false.
///
[Collection(DbFactoryCollection.Name)]
public class RoomDeveloperOwnedTests
{
private const long DeveloperId = 2;
private const long PlainPlayerId = 3;
private const long MakerRoomTemplateId = 23;
private const long DormTemplateId = 1;
private sealed class TestDbContextFactory : IDbContextFactory
{
private readonly DbContextOptions options;
public TestDbContextFactory()
{
options = TestDbOptions.Create()
.UseInMemoryDatabase(Guid.NewGuid().ToString())
.Options;
}
public RecDBContext CreateDbContext() => new(options);
}
///
/// The shape a player's create-room flow runs against: a clonable template (room 23 is the
/// Maker Room the client clones) and the per-player dorm template. Room 1 is what
/// GetOrCreateDorm clones, so it has to be a dorm for the dorm case to be real.
///
private static async Task<(TestDbContextFactory Factory, RecDBContext Db)> TemplatesAsync(bool withDeveloperProfile = true)
{
var factory = new TestDbContextFactory();
RoomUtils.DbFactory = factory;
var db = factory.CreateDbContext();
db.Rooms.Add(new RoomData
{
RoomId = MakerRoomTemplateId,
Name = "MakerRoom",
CloningAllowed = true,
Accessibility = Accessibility.Public,
CreatorAccountId = 1,
MaxPlayers = 8,
CreatedAt = DateTime.UtcNow,
});
db.SubRooms.Add(new SubRoom
{
RoomId = MakerRoomTemplateId,
Name = "Home",
UnitySceneId = SceneLocations.MakerRoom,
Accessibility = Accessibility.Public,
MaxPlayers = 8,
DataBlob = "",
});
db.Rooms.Add(new RoomData
{
RoomId = DormTemplateId,
Name = "DormRoom",
IsDorm = true,
CloningAllowed = true,
Accessibility = Accessibility.Private,
CreatorAccountId = 1,
MaxPlayers = 4,
CreatedAt = DateTime.UtcNow,
});
db.SubRooms.Add(new SubRoom
{
RoomId = DormTemplateId,
Name = "DormRoom",
UnitySceneId = "76d98498-60a1-430c-ab76-b54a29b7a163",
Accessibility = Accessibility.Private,
MaxPlayers = 4,
DataBlob = "",
});
db.Profiles.Add(new Profile
{
accountId = DeveloperId,
role = withDeveloperProfile ? "developer" : null,
username = "devplayer",
displayName = "Dev Player",
profileImage = DBConfig.DefaultProfileImage,
});
await db.SaveChangesAsync();
return (factory, db);
}
[Fact]
public async Task ARoomMadeByADeveloperIsDeveloperOwned()
{
var (_, db) = await TemplatesAsync();
var made = await RoomUtils.CloneRoom(MakerRoomTemplateId, DeveloperId, "My Dev Room");
Assert.NotNull(made);
Assert.Equal(DeveloperId, made!.CreatorAccountId);
Assert.True(made.IsDeveloperOwned);
// The room the client is handed and the row it came from have to agree - the response is
// what the Dev Rooms row is built from, and the row is what the next read answers with.
Assert.True(db.Rooms.Single(x => x.Name == "My Dev Room").IsDeveloperOwned);
}
[Fact]
public async Task ARoomMadeByAPlainPlayerIsNotDeveloperOwned()
{
var (_, db) = await TemplatesAsync();
var made = await RoomUtils.CloneRoom(MakerRoomTemplateId, PlainPlayerId, "Plain Room");
Assert.NotNull(made);
Assert.False(made!.IsDeveloperOwned);
Assert.False(db.Rooms.Single(x => x.Name == "Plain Room").IsDeveloperOwned);
}
[Fact]
public async Task ADevelopersDormIsNotDeveloperOwned()
{
var (_, db) = await TemplatesAsync();
var dorm = await RoomUtils.GetOrCreateDorm(DeveloperId);
Assert.NotNull(dorm);
Assert.True(dorm!.IsDorm);
// The dorm is cloned by the same code path that creates rooms, so without the dorm guard
// a developer's own dorm came back developer-owned.
Assert.False(dorm.IsDeveloperOwned);
Assert.False(db.Rooms.Single(x => x.IsDorm && x.CreatorAccountId == DeveloperId).IsDeveloperOwned);
}
[Fact]
public async Task TheRoomTheClientReadsCarriesTheFlag()
{
await TemplatesAsync();
await RoomUtils.CloneRoom(MakerRoomTemplateId, DeveloperId, "Read Back");
var read = await RoomUtils.GetRoom("Read Back");
Assert.NotNull(read);
Assert.True(read!.IsDeveloperOwned);
// The 2023 client reads room payloads by their declared names (Program.cs pins the
// serializer to no naming policy), the same PascalCase the real API answers with.
var json = JsonSerializer.Serialize(read);
Assert.Contains("\"IsDeveloperOwned\":true", json);
}
[Fact]
public void APayloadWithoutTheFieldReadsAsFalse()
{
// Every room that predates the column, and every room an importer does not say otherwise
// about, serializes the field explicitly - a missing property is what makes the client's
// parser fall back to its own default.
var json = JsonSerializer.Serialize(new FullRoom { Name = "Imported" });
Assert.Contains("\"IsDeveloperOwned\":false", json);
}
// ---- The admin create route, the path every import uses -------------------------------
private const string Secret = "test-bot-secret";
private static async Task CreateRoomAsync(RecDBContext db, long newOwner, string json)
{
var api = new AdminRoomAPI(db);
var original = DBConfig.AdminApiKey;
DBConfig.AdminApiKey = Secret;
try
{
return await api.CreateRoom(Secret, newOwner, JsonNode.Parse(json)!);
}
finally
{
DBConfig.AdminApiKey = original;
}
}
[Fact]
public async Task AnImportedRoomWithNoOpinionTakesTheOwnersRoles()
{
var factory = new TestDbContextFactory();
var db = factory.CreateDbContext();
db.Profiles.Add(new Profile
{
accountId = DeveloperId,
role = "developer",
username = "devplayer",
displayName = "Dev Player",
profileImage = DBConfig.DefaultProfileImage,
});
await db.SaveChangesAsync();
Assert.IsType(await CreateRoomAsync(db, DeveloperId, """{ "Name": "DerivedDevRoom" }"""));
Assert.True(db.Rooms.Single(x => x.Name == "DerivedDevRoom").IsDeveloperOwned);
}
[Fact]
public async Task AnImportedRoomThatSaysItIsDeveloperOwnedIsBelieved()
{
var factory = new TestDbContextFactory();
var db = factory.CreateDbContext();
// A captured RRO room is developer-owned on the real service even though it is recreated
// here under a local (non-developer) owner, so an explicit value wins.
Assert.IsType(await CreateRoomAsync(db, PlainPlayerId,
"""{ "Name": "ImportedRRO", "IsRRO": true, "IsDeveloperOwned": true }"""));
Assert.True(db.Rooms.Single(x => x.Name == "ImportedRRO").IsDeveloperOwned);
}
[Fact]
public async Task ANonDeveloperOwnerMakesAPlainRoom()
{
var factory = new TestDbContextFactory();
var db = factory.CreateDbContext();
Assert.IsType(await CreateRoomAsync(db, PlainPlayerId,
"""{ "Name": "PlayerRoom", "IsDeveloperOwned": false }"""));
Assert.False(db.Rooms.Single(x => x.Name == "PlayerRoom").IsDeveloperOwned);
}
}