mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-25 23:18:44 -07:00
fix: 2023 room instances, publish, role permissions and co-owner invites
- Room roles: list the creator as Creator (255), not CoOwner (30). The
client takes the local role from the Roles list and only lets a player
edit roles below their own (Creator excepted), so the creator could not
edit Co-Owner and Creator permissions. Enforce the role ladder on the
server (RoomRoleRules), let invitees accept (PUT roles/{me} role=30) and
reject (PUT roles/{me}/invite role=0), send message Types 62/60/61, and
push RoomUpdate after every role change.
- Permissions: persist PUT subrooms/{sub}/permissions cells in a new
RoomRolePermissions table, serve them from photon_access_token (plus a
roomserver/ twin) and push PhotonAccessToken to the sub-room.
- Publish: player-owned rooms are never IsRRO on the wire; the client
hides Publish and every room setting for an RRO.
- Sub-rooms: wire SubRoomId = OrderIndex + 1 (the client will not load a
sub-room with id 0), new rooms always get a named Home sub-room, and a
startup pass repairs rooms without one.
- Instance browser: per-room Redis presence index, live players only,
private instances for the people allowed in, real IsFull.
- Room/sub-room settings: creator or accepted co-owner everywhere.
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text.Json;
|
||||
using DorkNet.Server.Data;
|
||||
using DorkNet.Server.Data.Entities;
|
||||
using DorkNet.Server.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace DorkNet.Server.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// "Publish room" in the 2023 client: RoomModel.TryPublishRoom →
|
||||
/// <c>PUT roomserver/rooms/{id}/accessibility</c> with form
|
||||
/// <c>accessibility=1</c> (boxed Int32, NLDBPDCNNCF_NestedType_OMHIIEAKHCH),
|
||||
/// typed <c>Task<FGCPNAACHIK></c>. The button only exists when
|
||||
/// get_CanPublishRoom holds: role ≥ CoOwner, not a dorm, <b>not IsRRO</b>,
|
||||
/// not already public. A clone of a Rec Room Original template used to be
|
||||
/// advertised as IsRRO and so could never be published.
|
||||
/// </summary>
|
||||
public sealed class RoomPublish2023Tests : IClassFixture<DorkNetServerFactory>
|
||||
{
|
||||
private readonly DorkNetServerFactory _factory;
|
||||
|
||||
public RoomPublish2023Tests(DorkNetServerFactory factory) => _factory = factory;
|
||||
|
||||
[Fact]
|
||||
public void Only_system_owned_originals_are_RRO_on_the_wire()
|
||||
{
|
||||
Assert.True(RoomService.WireIsRRO(new RoomEntity { IsAGRoom = true, CreatorPlayerId = PlayerService.SystemAccountId }));
|
||||
Assert.False(RoomService.WireIsRRO(new RoomEntity { IsAGRoom = true, CreatorPlayerId = 4242 }));
|
||||
Assert.False(RoomService.WireIsRRO(new RoomEntity { IsAGRoom = false, CreatorPlayerId = PlayerService.SystemAccountId }));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Clone_of_an_original_is_not_RRO_and_can_be_published_by_owner_and_co_owner()
|
||||
{
|
||||
using var setup = Client("api");
|
||||
var owner = await GameClientSessionFactory.CreateAsync(setup, _factory.ApexDomain);
|
||||
var coOwner = await GameClientSessionFactory.CreateAsync(setup, _factory.ApexDomain);
|
||||
var moderator = await GameClientSessionFactory.CreateAsync(setup, _factory.ApexDomain);
|
||||
|
||||
var templateId = 9_100_000 + Random.Shared.Next(1, 99_999);
|
||||
await using (var scope = _factory.Services.CreateAsyncScope())
|
||||
{
|
||||
var db = scope.ServiceProvider.GetRequiredService<DorkNetDbContext>();
|
||||
db.Rooms.Add(new RoomEntity
|
||||
{
|
||||
Id = templateId,
|
||||
Name = $"Tmpl{Guid.NewGuid():N}"[..18],
|
||||
CreatorPlayerId = PlayerService.SystemAccountId,
|
||||
IsAGRoom = true,
|
||||
Accessibility = 1,
|
||||
CloningAllowed = false,
|
||||
ImageName = "image_RecCenter.png",
|
||||
LocationReplicationId = "a75f7547-79eb-47c6-8986-6767abcb4f92",
|
||||
});
|
||||
db.RoomScenes.Add(new RoomSceneEntity { RoomId = templateId, OrderIndex = 0, Name = "Home" });
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
using var ownerRooms = Client("rooms", owner);
|
||||
var template = await GetJsonAsync(ownerRooms, $"/roomserver/rooms/{templateId}");
|
||||
Assert.True(template.GetProperty("IsRRO").GetBoolean());
|
||||
|
||||
using var cloneResp = await ownerRooms.PostAsync($"/roomserver/rooms/{templateId}/clone",
|
||||
new FormUrlEncodedContent(new Dictionary<string, string> { ["name"] = $"Pub{Guid.NewGuid():N}"[..20] }));
|
||||
var cloneText = await cloneResp.Content.ReadAsStringAsync();
|
||||
Assert.True(cloneResp.IsSuccessStatusCode, cloneText);
|
||||
var clone = JsonDocument.Parse(cloneText).RootElement.GetProperty("value").Clone();
|
||||
var cloneId = clone.GetProperty("RoomId").GetInt64();
|
||||
Assert.False(clone.GetProperty("IsRRO").GetBoolean());
|
||||
Assert.Equal(0, clone.GetProperty("Accessibility").GetInt32());
|
||||
|
||||
await using (var scope = _factory.Services.CreateAsyncScope())
|
||||
{
|
||||
var db = scope.ServiceProvider.GetRequiredService<DorkNetDbContext>();
|
||||
db.RoomRoles.Add(new RoomRoleEntity { RoomId = cloneId, PlayerId = coOwner.PlayerId, Role = RoomRoleRules.StoredCoOwner, Accepted = true });
|
||||
db.RoomRoles.Add(new RoomRoleEntity { RoomId = cloneId, PlayerId = moderator.PlayerId, Role = RoomRoleRules.StoredModerator, Accepted = true });
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
// Publish as the owner: full room details back, now public, still not RRO.
|
||||
var published = await PutAccessibilityAsync(ownerRooms, cloneId, "1");
|
||||
Assert.Equal(1, published.GetProperty("Accessibility").GetInt32());
|
||||
Assert.False(published.GetProperty("IsRRO").GetBoolean());
|
||||
Assert.True(published.TryGetProperty("SubRooms", out _));
|
||||
|
||||
// A moderator may not change accessibility; a co-owner may.
|
||||
using var modRooms = Client("rooms", moderator);
|
||||
using (var denied = await modRooms.PutAsync($"/roomserver/rooms/{cloneId}/accessibility",
|
||||
new FormUrlEncodedContent(new Dictionary<string, string> { ["accessibility"] = "0" })))
|
||||
Assert.False(denied.IsSuccessStatusCode);
|
||||
using var coRooms = Client("rooms", coOwner);
|
||||
var unpublished = await PutAccessibilityAsync(coRooms, cloneId, "0");
|
||||
Assert.Equal(0, unpublished.GetProperty("Accessibility").GetInt32());
|
||||
}
|
||||
|
||||
private static async Task<JsonElement> PutAccessibilityAsync(HttpClient client, long roomId, string value)
|
||||
{
|
||||
using var response = await client.PutAsync($"/roomserver/rooms/{roomId}/accessibility",
|
||||
new FormUrlEncodedContent(new Dictionary<string, string> { ["accessibility"] = value }));
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
Assert.True(response.IsSuccessStatusCode, $"{(int)response.StatusCode}: {body}");
|
||||
return JsonDocument.Parse(body).RootElement.Clone();
|
||||
}
|
||||
|
||||
private static async Task<JsonElement> GetJsonAsync(HttpClient client, string path)
|
||||
{
|
||||
using var response = await client.GetAsync(path);
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
Assert.True(response.IsSuccessStatusCode, $"GET {path} -> {(int)response.StatusCode}: {body}");
|
||||
return JsonDocument.Parse(body).RootElement.Clone();
|
||||
}
|
||||
|
||||
private HttpClient Client(string subdomain, GameClientSession? session = null)
|
||||
{
|
||||
var client = _factory.CreateClient(new() { AllowAutoRedirect = false });
|
||||
client.BaseAddress = new Uri($"http://{subdomain}.{_factory.ApexDomain}");
|
||||
client.Timeout = TimeSpan.FromSeconds(30);
|
||||
client.DefaultRequestHeaders.Accept.ParseAdd("application/json");
|
||||
client.DefaultRequestHeaders.UserAgent.ParseAdd("RecRoom/2023.03.21");
|
||||
if (session is not null)
|
||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", session.AccessToken);
|
||||
return client;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,454 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using DorkNet.Server.Data;
|
||||
using DorkNet.Server.Data.Entities;
|
||||
using DorkNet.Server.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace DorkNet.Server.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// 2023 room roles, the co-owner invite flow and the sub-room role-permission
|
||||
/// matrix, against the client contract:
|
||||
/// <list type="bullet">
|
||||
/// <item>Role enum OMMBGJMJJPN: None 0, Host 10, Moderator 20, CoOwner 30,
|
||||
/// TemporaryCoOwner 31, Creator 255. The creator must be listed as 255 —
|
||||
/// the client takes the local role from the Roles list and only lets a
|
||||
/// player edit roles strictly below their own, Creator excepted
|
||||
/// (OMJKHJLFOCO.DIIMLMKEJLC).</item>
|
||||
/// <item>Invite: owner <c>PUT rooms/{id}/roles/{target}/invite</c> role=30;
|
||||
/// invitee gets message Type 62 with RoomId; accepts with
|
||||
/// <c>PUT rooms/{id}/roles/{me}</c> role=30, rejects with
|
||||
/// <c>PUT rooms/{id}/roles/{me}/invite</c> role=0.</item>
|
||||
/// <item>Permissions: <c>PUT rooms/{id}/subrooms/{sub}/permissions</c> raw
|
||||
/// JSON array of {Permission, Role, Override, Type, Value}; read back from
|
||||
/// <c>GET photon_access_token</c>.</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public sealed class RoomRoles2023Tests : IClassFixture<DorkNetServerFactory>
|
||||
{
|
||||
private readonly DorkNetServerFactory _factory;
|
||||
|
||||
public RoomRoles2023Tests(DorkNetServerFactory factory) => _factory = factory;
|
||||
|
||||
// ── pure rules ──────────────────────────────────────────────────────
|
||||
|
||||
private static RoomEntity Room(long creator) => new() { Id = 42, CreatorPlayerId = creator };
|
||||
|
||||
private static RoomRoleEntity Row(long player, int stored, bool accepted = true) =>
|
||||
new() { RoomId = 42, PlayerId = player, Role = stored, Accepted = accepted };
|
||||
|
||||
[Fact]
|
||||
public void Creator_is_255_and_listed_once()
|
||||
{
|
||||
var room = Room(creator: 7);
|
||||
var wires = RoomRoleRules.BuildWires(room, new[]
|
||||
{
|
||||
Row(7, RoomRoleRules.StoredCoOwner), // a stray row for the creator
|
||||
Row(8, RoomRoleRules.StoredModerator),
|
||||
Row(9, RoomRoleRules.StoredCoOwner, accepted: false),
|
||||
});
|
||||
|
||||
Assert.Single(wires, w => w.AccountId == 7);
|
||||
Assert.Equal(255, wires.Single(w => w.AccountId == 7).Role);
|
||||
Assert.Equal(20, wires.Single(w => w.AccountId == 8).Role);
|
||||
var invited = wires.Single(w => w.AccountId == 9);
|
||||
Assert.Equal(0, invited.Role);
|
||||
Assert.Equal(30, invited.InvitedRole);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Creator_may_edit_every_role_including_co_owner_and_creator_permissions()
|
||||
{
|
||||
var room = Room(creator: 7);
|
||||
var rows = Array.Empty<RoomRoleEntity>();
|
||||
foreach (var role in new[] { 0, 10, 20, 30, 31, 255 })
|
||||
Assert.True(RoomRoleRules.CanEditRolePermissions(room, rows, 7, role), $"creator role {role}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Co_owner_may_edit_only_roles_below_their_own()
|
||||
{
|
||||
var room = Room(creator: 7);
|
||||
var rows = new[] { Row(8, RoomRoleRules.StoredCoOwner), Row(9, RoomRoleRules.StoredModerator) };
|
||||
|
||||
Assert.True(RoomRoleRules.CanEditRolePermissions(room, rows, 8, 20));
|
||||
Assert.True(RoomRoleRules.CanEditRolePermissions(room, rows, 8, 0));
|
||||
Assert.False(RoomRoleRules.CanEditRolePermissions(room, rows, 8, 30));
|
||||
Assert.False(RoomRoleRules.CanEditRolePermissions(room, rows, 8, 31));
|
||||
Assert.False(RoomRoleRules.CanEditRolePermissions(room, rows, 8, 255));
|
||||
// A moderator edits nothing.
|
||||
Assert.False(RoomRoleRules.CanEditRolePermissions(room, rows, 9, 0));
|
||||
// Nor does a stranger.
|
||||
Assert.False(RoomRoleRules.CanEditRolePermissions(room, rows, 99, 0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Role_changes_follow_the_ladder()
|
||||
{
|
||||
var room = Room(creator: 7);
|
||||
var rows = new List<RoomRoleEntity>
|
||||
{
|
||||
Row(8, RoomRoleRules.StoredCoOwner),
|
||||
Row(9, RoomRoleRules.StoredModerator),
|
||||
Row(10, RoomRoleRules.StoredCoOwner, accepted: false),
|
||||
};
|
||||
|
||||
// Creator: anything on anyone but themselves.
|
||||
Assert.Null(RoomRoleRules.CheckChange(room, rows, 7, 8, 0, false, out var k));
|
||||
Assert.Equal(RoomRoleRules.ChangeKind.Clear, k);
|
||||
Assert.Null(RoomRoleRules.CheckChange(room, rows, 7, 11, 30, true, out k));
|
||||
Assert.Equal(RoomRoleRules.ChangeKind.Invite, k);
|
||||
Assert.Equal("cannot_change_creator_role", RoomRoleRules.CheckChange(room, rows, 7, 7, 0, false, out _));
|
||||
Assert.Equal("cannot_change_creator_role", RoomRoleRules.CheckChange(room, rows, 8, 7, 0, false, out _));
|
||||
|
||||
// Co-owner: only below 30, on targets below 30.
|
||||
Assert.Null(RoomRoleRules.CheckChange(room, rows, 8, 11, 20, false, out _));
|
||||
Assert.Null(RoomRoleRules.CheckChange(room, rows, 8, 9, 10, false, out _));
|
||||
Assert.Equal("forbidden", RoomRoleRules.CheckChange(room, rows, 8, 11, 30, true, out _));
|
||||
Assert.Equal("forbidden", RoomRoleRules.CheckChange(room, rows, 8, 10, 0, true, out _)); // invited co-owner
|
||||
// …and cannot remove a peer co-owner.
|
||||
Assert.Equal("forbidden", RoomRoleRules.CheckChange(
|
||||
room, new List<RoomRoleEntity> { Row(8, 0), Row(12, 0) }, 8, 12, 0, false, out _));
|
||||
|
||||
// Moderator and strangers manage nobody.
|
||||
Assert.Equal("forbidden", RoomRoleRules.CheckChange(room, rows, 9, 11, 10, false, out _));
|
||||
Assert.Equal("forbidden", RoomRoleRules.CheckChange(room, rows, 99, 11, 10, false, out _));
|
||||
|
||||
// Self-service: accept exactly the invited role, decline, leave.
|
||||
Assert.Null(RoomRoleRules.CheckChange(room, rows, 10, 10, 30, false, out k));
|
||||
Assert.Equal(RoomRoleRules.ChangeKind.AcceptInvite, k);
|
||||
Assert.Null(RoomRoleRules.CheckChange(room, rows, 10, 10, 0, true, out k));
|
||||
Assert.Equal(RoomRoleRules.ChangeKind.Clear, k);
|
||||
Assert.Null(RoomRoleRules.CheckChange(room, rows, 9, 9, 0, false, out _));
|
||||
Assert.Equal("forbidden", RoomRoleRules.CheckChange(room, rows, 9, 9, 30, false, out _)); // no invite
|
||||
Assert.Equal("forbidden", RoomRoleRules.CheckChange(room, rows, 10, 10, 20, false, out _)); // not the invited role
|
||||
|
||||
Assert.Equal("invalid_role", RoomRoleRules.CheckChange(room, rows, 7, 11, 255, false, out _));
|
||||
Assert.Equal("invalid_role", RoomRoleRules.CheckChange(room, rows, 7, 11, 31, false, out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Permission_matrix_merges_stored_cells_over_defaults()
|
||||
{
|
||||
var merged = RoomPermissionMatrix.Merge(new[]
|
||||
{
|
||||
new RoomRolePermissionEntity { Id = 1, Role = 30, Permission = "13", Type = 0, Override = true, Value = "false" },
|
||||
new RoomRolePermissionEntity { Id = 2, Role = 20, Permission = "13", Type = 0, Override = true, Value = "true" },
|
||||
});
|
||||
|
||||
Assert.Equal("false", merged.Single(c => c.Role == 30 && c.Permission == "13").Value);
|
||||
Assert.Equal("true", merged.Single(c => c.Role == 20 && c.Permission == "13").Value);
|
||||
Assert.Equal("true", merged.Single(c => c.Role == 255 && c.Permission == "13").Value);
|
||||
Assert.Equal(RoomPermissionMatrix.Defaults.Count + 1, merged.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Permission_body_accepts_numbers_strings_and_enum_names()
|
||||
{
|
||||
var cells = RoomPermissionMatrix.Parse(
|
||||
"""[{"Permission":"13","Role":30,"Override":true,"Type":0,"Value":"false"},""" +
|
||||
"""{"permission":"CAN_USE_MAKER_PEN","role":"Moderator","override":"true","type":"Bool","value":true},""" +
|
||||
"""{"Permission":"20","Role":"255","Override":false,"Type":1,"Value":5}]""");
|
||||
|
||||
Assert.NotNull(cells);
|
||||
Assert.Equal(3, cells!.Count);
|
||||
Assert.Equal(new RoomPermissionMatrix.Cell("13", 30, true, 0, "false"), cells[0]);
|
||||
Assert.Equal(new RoomPermissionMatrix.Cell("CAN_USE_MAKER_PEN", 20, true, 0, "true"), cells[1]);
|
||||
Assert.Equal(new RoomPermissionMatrix.Cell("20", 255, false, 1, "5"), cells[2]);
|
||||
Assert.Null(RoomPermissionMatrix.Parse("role=30"));
|
||||
}
|
||||
|
||||
// ── over HTTP ───────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task Roles_list_reports_the_creator_as_Creator_255()
|
||||
{
|
||||
var (owner, _, _) = await PlayersAsync();
|
||||
var roomId = await SeedRoomAsync(owner.PlayerId);
|
||||
|
||||
using var rooms = Client("rooms", owner);
|
||||
var list = await GetJsonAsync(rooms, $"/roomserver/rooms/{roomId}/roles");
|
||||
var me = list.EnumerateArray().Single(e => e.GetProperty("AccountId").GetInt64() == owner.PlayerId);
|
||||
Assert.Equal(255, me.GetProperty("Role").GetInt32());
|
||||
|
||||
var details = await GetJsonAsync(rooms, $"/roomserver/rooms/{roomId}");
|
||||
var detailsRole = details.GetProperty("Roles").EnumerateArray()
|
||||
.Single(e => e.GetProperty("AccountId").GetInt64() == owner.PlayerId);
|
||||
Assert.Equal(255, detailsRole.GetProperty("Role").GetInt32());
|
||||
|
||||
var single = await GetJsonAsync(rooms, $"/roomserver/rooms/{roomId}/roles/{owner.PlayerId}");
|
||||
Assert.Equal(255, single.GetProperty("Role").GetInt32());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Co_owner_invite_is_notified_then_accepted_by_the_invitee()
|
||||
{
|
||||
var (owner, invitee, _) = await PlayersAsync();
|
||||
var roomId = await SeedRoomAsync(owner.PlayerId);
|
||||
|
||||
using var ownerRooms = Client("rooms", owner);
|
||||
var invited = await PutFormAsync(ownerRooms, $"/roomserver/rooms/{roomId}/roles/{invitee.PlayerId}/invite", ("role", "30"));
|
||||
var inviteeRow = RoleOf(invited, invitee.PlayerId);
|
||||
Assert.Equal(0, inviteeRow.GetProperty("Role").GetInt32());
|
||||
Assert.Equal(30, inviteeRow.GetProperty("InvitedRole").GetInt32());
|
||||
|
||||
// The invitee's inbox carries message Type 62 (RoomCoOwnerInvited) with RoomId.
|
||||
long messageId;
|
||||
await using (var scope = _factory.Services.CreateAsyncScope())
|
||||
{
|
||||
var db = scope.ServiceProvider.GetRequiredService<DorkNetDbContext>();
|
||||
var msg = await db.Messages.SingleAsync(m =>
|
||||
m.RecipientPlayerId == invitee.PlayerId && m.RoomId == roomId
|
||||
&& m.Type == SystemNotificationService.MessageType.RoomCoOwnerInvited);
|
||||
Assert.Equal(owner.PlayerId, msg.SenderPlayerId);
|
||||
Assert.Null(msg.ReadAt);
|
||||
messageId = msg.Id;
|
||||
}
|
||||
|
||||
// Accept: the invitee PUTs their own role to 30.
|
||||
using var inviteeRooms = Client("rooms", invitee);
|
||||
var accepted = await PutFormAsync(inviteeRooms, $"/roomserver/rooms/{roomId}/roles/{invitee.PlayerId}", ("role", "30"));
|
||||
var acceptedRow = RoleOf(accepted, invitee.PlayerId);
|
||||
Assert.Equal(30, acceptedRow.GetProperty("Role").GetInt32());
|
||||
Assert.Equal(0, acceptedRow.GetProperty("InvitedRole").GetInt32());
|
||||
|
||||
await using (var scope = _factory.Services.CreateAsyncScope())
|
||||
{
|
||||
var db = scope.ServiceProvider.GetRequiredService<DorkNetDbContext>();
|
||||
Assert.NotNull((await db.Messages.SingleAsync(m => m.Id == messageId)).ReadAt);
|
||||
Assert.True(await db.RoomRoles.AnyAsync(r =>
|
||||
r.RoomId == roomId && r.PlayerId == invitee.PlayerId && r.Accepted && r.Role == RoomRoleRules.StoredCoOwner));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Invitee_rejects_through_the_invite_route_and_cannot_self_promote()
|
||||
{
|
||||
var (owner, invitee, stranger) = await PlayersAsync();
|
||||
var roomId = await SeedRoomAsync(owner.PlayerId);
|
||||
|
||||
using var ownerRooms = Client("rooms", owner);
|
||||
await PutFormAsync(ownerRooms, $"/roomserver/rooms/{roomId}/roles/{invitee.PlayerId}/invite", ("role", "30"));
|
||||
|
||||
using var inviteeRooms = Client("rooms", invitee);
|
||||
var rejected = await PutFormAsync(inviteeRooms, $"/roomserver/rooms/{roomId}/roles/{invitee.PlayerId}/invite", ("role", "0"));
|
||||
Assert.DoesNotContain(rejected.GetProperty("Roles").EnumerateArray(),
|
||||
e => e.GetProperty("AccountId").GetInt64() == invitee.PlayerId);
|
||||
|
||||
// With the invite gone, the same accept is refused.
|
||||
using var again = await PutFormRawAsync(inviteeRooms, $"/roomserver/rooms/{roomId}/roles/{invitee.PlayerId}", ("role", "30"));
|
||||
Assert.Equal(HttpStatusCode.Forbidden, again.StatusCode);
|
||||
|
||||
// A stranger can neither promote themselves nor invite anyone.
|
||||
using var strangerRooms = Client("rooms", stranger);
|
||||
using var self = await PutFormRawAsync(strangerRooms, $"/roomserver/rooms/{roomId}/roles/{stranger.PlayerId}", ("role", "20"));
|
||||
Assert.Equal(HttpStatusCode.Forbidden, self.StatusCode);
|
||||
using var other = await PutFormRawAsync(strangerRooms, $"/roomserver/rooms/{roomId}/roles/{invitee.PlayerId}/invite", ("role", "30"));
|
||||
Assert.Equal(HttpStatusCode.Forbidden, other.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Co_owner_manages_roles_below_but_not_co_owners()
|
||||
{
|
||||
var (owner, coOwner, target) = await PlayersAsync();
|
||||
var roomId = await SeedRoomAsync(owner.PlayerId, (coOwner.PlayerId, RoomRoleRules.StoredCoOwner));
|
||||
|
||||
using var coRooms = Client("rooms", coOwner);
|
||||
var granted = await PutFormAsync(coRooms, $"/roomserver/rooms/{roomId}/roles/{target.PlayerId}", ("role", "20"));
|
||||
Assert.Equal(20, RoleOf(granted, target.PlayerId).GetProperty("Role").GetInt32());
|
||||
|
||||
using var inviteCo = await PutFormRawAsync(coRooms, $"/roomserver/rooms/{roomId}/roles/{target.PlayerId}/invite", ("role", "30"));
|
||||
Assert.Equal(HttpStatusCode.Forbidden, inviteCo.StatusCode);
|
||||
|
||||
// The creator can directly add and remove a co-owner.
|
||||
using var ownerRooms = Client("rooms", owner);
|
||||
var promoted = await PutFormAsync(ownerRooms, $"/roomserver/rooms/{roomId}/roles/{target.PlayerId}", ("role", "30"));
|
||||
Assert.Equal(30, RoleOf(promoted, target.PlayerId).GetProperty("Role").GetInt32());
|
||||
|
||||
// …and now the co-owner can't touch their peer.
|
||||
using var demote = await PutFormRawAsync(coRooms, $"/roomserver/rooms/{roomId}/roles/{target.PlayerId}", ("role", "0"));
|
||||
Assert.Equal(HttpStatusCode.Forbidden, demote.StatusCode);
|
||||
|
||||
var removed = await PutFormAsync(ownerRooms, $"/roomserver/rooms/{roomId}/roles/{target.PlayerId}", ("role", "0"));
|
||||
Assert.DoesNotContain(removed.GetProperty("Roles").EnumerateArray(),
|
||||
e => e.GetProperty("AccountId").GetInt64() == target.PlayerId);
|
||||
|
||||
await using var scope = _factory.Services.CreateAsyncScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<DorkNetDbContext>();
|
||||
Assert.True(await db.Messages.AnyAsync(m => m.RecipientPlayerId == target.PlayerId && m.RoomId == roomId
|
||||
&& m.Type == SystemNotificationService.MessageType.RoomCoOwnerAdded));
|
||||
Assert.True(await db.Messages.AnyAsync(m => m.RecipientPlayerId == target.PlayerId && m.RoomId == roomId
|
||||
&& m.Type == SystemNotificationService.MessageType.RoomCoOwnerRemoved));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Creator_saves_co_owner_and_creator_permissions_and_reads_them_back()
|
||||
{
|
||||
var (owner, coOwner, _) = await PlayersAsync();
|
||||
var roomId = await SeedRoomAsync(owner.PlayerId, (coOwner.PlayerId, RoomRoleRules.StoredCoOwner));
|
||||
|
||||
using var ownerRooms = Client("rooms", owner);
|
||||
var body = """[{"Permission":"13","Role":30,"Override":true,"Type":0,"Value":"false"},{"Permission":"20","Role":255,"Override":true,"Type":0,"Value":"false"}]""";
|
||||
using (var put = await PutJsonRawAsync(ownerRooms, $"/roomserver/rooms/{roomId}/subrooms/1/permissions", body))
|
||||
{
|
||||
var text = await put.Content.ReadAsStringAsync();
|
||||
Assert.True(put.IsSuccessStatusCode, $"{(int)put.StatusCode}: {text}");
|
||||
using var json = JsonDocument.Parse(text);
|
||||
Assert.Equal(roomId, json.RootElement.GetProperty("RoomId").GetInt64()); // full room details
|
||||
}
|
||||
|
||||
// The owner enters the room, then the matrix comes back with the token.
|
||||
using var match = Client("match", owner);
|
||||
using (var join = await match.PostAsync($"/matchmake/room/{roomId}", new FormUrlEncodedContent(new Dictionary<string, string>())))
|
||||
Assert.True(join.IsSuccessStatusCode, await join.Content.ReadAsStringAsync());
|
||||
var token = await GetJsonAsync(ownerRooms, "/roomserver/photon_access_token");
|
||||
var cells = token.GetProperty("Permissions").EnumerateArray().ToList();
|
||||
Assert.Equal("false", cells.Single(c => c.GetProperty("Role").GetInt32() == 30
|
||||
&& c.GetProperty("Permission").GetString() == "13").GetProperty("Value").GetString());
|
||||
Assert.Equal("false", cells.Single(c => c.GetProperty("Role").GetInt32() == 255
|
||||
&& c.GetProperty("Permission").GetString() == "20").GetProperty("Value").GetString());
|
||||
Assert.Equal("true", cells.Single(c => c.GetProperty("Role").GetInt32() == 255
|
||||
&& c.GetProperty("Permission").GetString() == "13").GetProperty("Value").GetString());
|
||||
|
||||
// A co-owner may not edit the Co-Owner row, but may edit Moderator.
|
||||
using var coRooms = Client("rooms", coOwner);
|
||||
using (var denied = await PutJsonRawAsync(coRooms, $"/roomserver/rooms/{roomId}/subrooms/1/permissions",
|
||||
"""[{"Permission":"13","Role":30,"Override":true,"Type":0,"Value":"true"}]"""))
|
||||
Assert.Equal(HttpStatusCode.Forbidden, denied.StatusCode);
|
||||
using (var allowed = await PutJsonRawAsync(coRooms, $"/roomserver/rooms/{roomId}/subrooms/1/permissions",
|
||||
"""[{"Permission":"13","Role":20,"Override":true,"Type":0,"Value":"true"}]"""))
|
||||
Assert.True(allowed.IsSuccessStatusCode, await allowed.Content.ReadAsStringAsync());
|
||||
|
||||
await using var scope = _factory.Services.CreateAsyncScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<DorkNetDbContext>();
|
||||
var stored = await db.RoomRolePermissions.Where(p => p.RoomId == roomId).ToListAsync();
|
||||
Assert.Equal(3, stored.Count);
|
||||
Assert.All(stored, p => Assert.Equal(0, p.SubRoomId)); // wire 1 → OrderIndex 0
|
||||
Assert.Equal("false", stored.Single(p => p.Role == 30).Value); // the denied write changed nothing
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Invite_pushes_the_message_and_room_and_permission_saves_push_the_access_token()
|
||||
{
|
||||
var hub = new CapturingHubContext();
|
||||
var factory = _factory.WithWebHostBuilder(builder =>
|
||||
builder.ConfigureServices(services =>
|
||||
{
|
||||
Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions
|
||||
.RemoveAll<Microsoft.AspNetCore.SignalR.IHubContext<DorkNet.Server.Hubs.NotifyHub>>(services);
|
||||
services.AddSingleton<Microsoft.AspNetCore.SignalR.IHubContext<DorkNet.Server.Hubs.NotifyHub>>(hub);
|
||||
}));
|
||||
|
||||
HttpClient FactoryClient(string sub, GameClientSession? session = null)
|
||||
{
|
||||
var c = factory.CreateClient(new() { AllowAutoRedirect = false });
|
||||
c.BaseAddress = new Uri($"http://{sub}.{_factory.ApexDomain}");
|
||||
c.DefaultRequestHeaders.Accept.ParseAdd("application/json");
|
||||
c.DefaultRequestHeaders.UserAgent.ParseAdd("RecRoom/2023.03.21");
|
||||
if (session is not null)
|
||||
c.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", session.AccessToken);
|
||||
return c;
|
||||
}
|
||||
|
||||
using var setup = FactoryClient("api");
|
||||
var owner = await GameClientSessionFactory.CreateAsync(setup, _factory.ApexDomain);
|
||||
var invitee = await GameClientSessionFactory.CreateAsync(setup, _factory.ApexDomain);
|
||||
var roomId = await SeedRoomAsync(owner.PlayerId);
|
||||
|
||||
using var ownerRooms = FactoryClient("rooms", owner);
|
||||
await PutFormAsync(ownerRooms, $"/roomserver/rooms/{roomId}/roles/{invitee.PlayerId}/invite", ("role", "30"));
|
||||
|
||||
var message = Assert.Single(hub.To(CapturingHubContext.PlayerGroup(invitee.PlayerId), "2"));
|
||||
Assert.Equal(62, message.Msg.GetProperty("Type").GetInt32());
|
||||
Assert.Equal(roomId, message.Msg.GetProperty("RoomId").GetInt64());
|
||||
Assert.Equal((int)owner.PlayerId, message.Msg.GetProperty("FromPlayerId").GetInt32());
|
||||
var roomPush = Assert.Single(hub.To(CapturingHubContext.PlayerGroup(invitee.PlayerId), "RoomUpdate"));
|
||||
Assert.Equal(30, RoleOf(roomPush.Msg, invitee.PlayerId).GetProperty("InvitedRole").GetInt32());
|
||||
|
||||
using var match = FactoryClient("match", owner);
|
||||
using (var join = await match.PostAsync($"/matchmake/room/{roomId}", new FormUrlEncodedContent(new Dictionary<string, string>())))
|
||||
Assert.True(join.IsSuccessStatusCode, await join.Content.ReadAsStringAsync());
|
||||
using (var put = await PutJsonRawAsync(ownerRooms, $"/roomserver/rooms/{roomId}/subrooms/1/permissions",
|
||||
"""[{"Permission":"21","Role":20,"Override":true,"Type":0,"Value":"true"}]"""))
|
||||
Assert.True(put.IsSuccessStatusCode, await put.Content.ReadAsStringAsync());
|
||||
|
||||
var token = Assert.Single(hub.To(CapturingHubContext.PlayerGroup(owner.PlayerId), "PhotonAccessToken"));
|
||||
Assert.True(token.Msg.GetProperty("RoomInstanceId").GetInt64() > 0);
|
||||
Assert.Contains(token.Msg.GetProperty("Permissions").EnumerateArray(),
|
||||
c => c.GetProperty("Role").GetInt32() == 20 && c.GetProperty("Permission").GetString() == "21");
|
||||
}
|
||||
|
||||
// ── helpers ─────────────────────────────────────────────────────────
|
||||
|
||||
private async Task<(GameClientSession A, GameClientSession B, GameClientSession C)> PlayersAsync()
|
||||
{
|
||||
using var setup = Client("api");
|
||||
return (
|
||||
await GameClientSessionFactory.CreateAsync(setup, _factory.ApexDomain),
|
||||
await GameClientSessionFactory.CreateAsync(setup, _factory.ApexDomain),
|
||||
await GameClientSessionFactory.CreateAsync(setup, _factory.ApexDomain));
|
||||
}
|
||||
|
||||
private async Task<long> SeedRoomAsync(long creatorId, params (long PlayerId, int StoredRole)[] roles)
|
||||
{
|
||||
var roomId = 9_300_000 + Random.Shared.Next(1, 99_999);
|
||||
await using var scope = _factory.Services.CreateAsyncScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<DorkNetDbContext>();
|
||||
db.Rooms.Add(new RoomEntity
|
||||
{
|
||||
Id = roomId,
|
||||
Name = $"Roles{Guid.NewGuid():N}"[..18],
|
||||
CreatorPlayerId = creatorId,
|
||||
ImageName = RoomService.DefaultRoomImageName,
|
||||
Accessibility = 0,
|
||||
LocationReplicationId = "a75f7547-79eb-47c6-8986-6767abcb4f92",
|
||||
});
|
||||
db.RoomScenes.Add(new RoomSceneEntity { RoomId = roomId, OrderIndex = 0, Name = "Home" });
|
||||
foreach (var (pid, role) in roles)
|
||||
db.RoomRoles.Add(new RoomRoleEntity { RoomId = roomId, PlayerId = pid, Role = role, Accepted = true });
|
||||
await db.SaveChangesAsync();
|
||||
return roomId;
|
||||
}
|
||||
|
||||
private static JsonElement RoleOf(JsonElement details, long playerId) =>
|
||||
details.GetProperty("Roles").EnumerateArray()
|
||||
.Single(e => e.GetProperty("AccountId").GetInt64() == playerId);
|
||||
|
||||
private static Task<HttpResponseMessage> PutFormRawAsync(HttpClient client, string path, params (string Key, string Value)[] fields)
|
||||
=> client.PutAsync(path, new FormUrlEncodedContent(fields.Select(f => new KeyValuePair<string, string>(f.Key, f.Value))));
|
||||
|
||||
private static async Task<JsonElement> PutFormAsync(HttpClient client, string path, params (string Key, string Value)[] fields)
|
||||
{
|
||||
using var response = await PutFormRawAsync(client, path, fields);
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
Assert.True(response.IsSuccessStatusCode, $"PUT {path} -> {(int)response.StatusCode}: {body}");
|
||||
return JsonDocument.Parse(body).RootElement.Clone();
|
||||
}
|
||||
|
||||
private static Task<HttpResponseMessage> PutJsonRawAsync(HttpClient client, string path, string json)
|
||||
=> client.PutAsync(path, new StringContent(json, Encoding.UTF8, "application/json"));
|
||||
|
||||
private static async Task<JsonElement> GetJsonAsync(HttpClient client, string path)
|
||||
{
|
||||
using var response = await client.GetAsync(path);
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
Assert.True(response.IsSuccessStatusCode, $"GET {path} -> {(int)response.StatusCode}: {body}");
|
||||
return JsonDocument.Parse(body).RootElement.Clone();
|
||||
}
|
||||
|
||||
private HttpClient Client(string subdomain, GameClientSession? session = null)
|
||||
{
|
||||
var client = _factory.CreateClient(new() { AllowAutoRedirect = false });
|
||||
client.BaseAddress = new Uri($"http://{subdomain}.{_factory.ApexDomain}");
|
||||
client.Timeout = TimeSpan.FromSeconds(30);
|
||||
client.DefaultRequestHeaders.Accept.ParseAdd("application/json");
|
||||
client.DefaultRequestHeaders.UserAgent.ParseAdd("RecRoom/2023.03.21");
|
||||
if (session is not null)
|
||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", session.AccessToken);
|
||||
return client;
|
||||
}
|
||||
}
|
||||
@@ -416,9 +416,10 @@ public sealed class RroRoomBlobTests : IClassFixture<DorkNetServerFactory>
|
||||
}
|
||||
}
|
||||
|
||||
// The entry sub-room (OrderIndex 0) is wire SubRoomId 1 (RoomSceneIds).
|
||||
private static JsonElement SubRoomZero(JsonElement details) =>
|
||||
details.GetProperty("SubRooms").EnumerateArray()
|
||||
.First(e => e.GetProperty("SubRoomId").GetInt64() == 0);
|
||||
.First(e => e.GetProperty("SubRoomId").GetInt64() == 1);
|
||||
|
||||
private static async Task<JsonElement> GetJsonAsync(HttpClient client, string path)
|
||||
{
|
||||
|
||||
@@ -58,7 +58,9 @@ public sealed class SubRoomMaxPlayersTests : IClassFixture<DorkNetServerFactory>
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
// ── in-game: the owner's slider on sub-room 1 only ──────────────────
|
||||
// ── in-game: the owner's slider on the second sub-room only ─────────
|
||||
// The client addresses it by its wire SubRoomId, OrderIndex + 1 = 2
|
||||
// (RoomSceneIds — the client will not load a sub-room whose id is 0).
|
||||
// Sent exactly as the client sends it. NLDBPDCNNCF.MNBPCGJNLNP(Int64,
|
||||
// Int64, Int32) builds a BLNOGFGHIIF whose single JSON key is
|
||||
// "maxPlayers" and issues it with verb 3 = PUT — NOT the form encoding
|
||||
@@ -66,7 +68,7 @@ public sealed class SubRoomMaxPlayersTests : IClassFixture<DorkNetServerFactory>
|
||||
// real client's body went unread.
|
||||
using var ownerClient = Client("rooms", owner);
|
||||
using var slider = await ownerClient.PutAsJsonAsync(
|
||||
$"/rooms/{roomId}/subrooms/1/maxplayers", new { maxPlayers = 24 });
|
||||
$"/rooms/{roomId}/subrooms/2/maxplayers", new { maxPlayers = 24 });
|
||||
var sliderBody = await slider.Content.ReadAsStringAsync();
|
||||
Assert.True(slider.IsSuccessStatusCode,
|
||||
$"owner PUT maxplayers -> {(int)slider.StatusCode}: {sliderBody}");
|
||||
@@ -75,10 +77,11 @@ public sealed class SubRoomMaxPlayersTests : IClassFixture<DorkNetServerFactory>
|
||||
// shape — so the slider's own response has to carry the new value.
|
||||
var echoed = JsonDocument.Parse(sliderBody).RootElement
|
||||
.GetProperty("SubRooms").EnumerateArray()
|
||||
.First(e => e.GetProperty("SubRoomId").GetInt64() == 1);
|
||||
.First(e => e.GetProperty("SubRoomId").GetInt64() == 2);
|
||||
Assert.Equal(24, echoed.GetProperty("MaxPlayers").GetInt32());
|
||||
|
||||
// ── admin: reads what the owner just set, then edits sub-room 0 ─────
|
||||
// (the admin API addresses sub-rooms by OrderIndex)
|
||||
using var adminClient = Client("admin", admin);
|
||||
var listed = await GetJsonAsync(adminClient, $"/api/admin/v1/rooms/{roomId}/subrooms");
|
||||
var subRooms = listed.EnumerateArray()
|
||||
@@ -99,8 +102,9 @@ public sealed class SubRoomMaxPlayersTests : IClassFixture<DorkNetServerFactory>
|
||||
.ToDictionary(e => e.GetProperty("SubRoomId").GetInt64(),
|
||||
e => e.GetProperty("MaxPlayers").GetInt32());
|
||||
|
||||
Assert.Equal(40, wire[0]);
|
||||
Assert.Equal(24, wire[1]);
|
||||
// Wire ids are OrderIndex + 1.
|
||||
Assert.Equal(40, wire[1]);
|
||||
Assert.Equal(24, wire[2]);
|
||||
|
||||
// The room-level figure is the room's advertised capacity, NOT a copy
|
||||
// of sub-room 0 — otherwise the admin edit above would have moved it.
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text.Json;
|
||||
using DorkNet.Server.Data;
|
||||
using DorkNet.Server.Data.Entities;
|
||||
using DorkNet.Server.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace DorkNet.Server.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Sub-room ids and the room instance browser for the 2023 client.
|
||||
/// <list type="bullet">
|
||||
/// <item>The client will not load a sub-room whose id is not positive
|
||||
/// (SubroomModel.DOKCPLJOKIE: <c>RoomId > 0 && SubroomId > 0</c>),
|
||||
/// so the wire SubRoomId is OrderIndex + 1 everywhere the client sees it:
|
||||
/// room details, sub-room routes, matchmaking and the instance browser.</item>
|
||||
/// <item>A new room always owns a real, named entry sub-room, and a startup
|
||||
/// pass repairs rooms that lack one.</item>
|
||||
/// <item><c>GET room/{id}/instances</c> lists live instances with their real
|
||||
/// players, including the private instance of a private room for the people
|
||||
/// allowed into it.</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public sealed class SubRoomsAndInstances2023Tests : IClassFixture<DorkNetServerFactory>
|
||||
{
|
||||
private readonly DorkNetServerFactory _factory;
|
||||
|
||||
public SubRoomsAndInstances2023Tests(DorkNetServerFactory factory) => _factory = factory;
|
||||
|
||||
[Fact]
|
||||
public void Wire_sub_room_ids_are_positive_and_round_trip()
|
||||
{
|
||||
Assert.Equal(1, RoomSceneIds.ToWire(0));
|
||||
Assert.Equal(3, RoomSceneIds.ToWire(2));
|
||||
Assert.Equal(0, RoomSceneIds.ToOrderIndex(1));
|
||||
Assert.Equal(2, RoomSceneIds.ToOrderIndex(3));
|
||||
// 0 (cached client state, bare matchmake) is the entry sub-room.
|
||||
Assert.Equal(0, RoomSceneIds.ToOrderIndex(0));
|
||||
Assert.Equal(0, RoomSceneIds.ToOrderIndex(-1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Details_matchmaking_and_sub_room_routes_share_the_wire_id()
|
||||
{
|
||||
var owner = await PlayerAsync();
|
||||
var roomId = await SeedRoomAsync(owner.PlayerId, accessibility: 1, scenes: new[] { "Home", "Arena" });
|
||||
|
||||
using var rooms = Client("rooms", owner);
|
||||
var details = await GetJsonAsync(rooms, $"/roomserver/rooms/{roomId}");
|
||||
var subRooms = details.GetProperty("SubRooms").EnumerateArray().ToList();
|
||||
Assert.Equal(new long[] { 1, 2 }, subRooms.Select(s => s.GetProperty("SubRoomId").GetInt64()));
|
||||
Assert.Equal("Home", subRooms[0].GetProperty("Name").GetString());
|
||||
|
||||
// matchmake/room/{id}/{sub} takes the wire id and echoes it.
|
||||
using var match = Client("match", owner);
|
||||
var arena = await PostFormAsync(match, $"/matchmake/room/{roomId}/2");
|
||||
Assert.Equal(2, arena.GetProperty("roomInstance").GetProperty("subRoomId").GetInt64());
|
||||
var lobby = await PostFormAsync(match, $"/matchmake/room/{roomId}");
|
||||
Assert.Equal(1, lobby.GetProperty("roomInstance").GetProperty("subRoomId").GetInt64());
|
||||
|
||||
// A sub-room route addressed by wire id 2 edits OrderIndex 1.
|
||||
using (var rename = await rooms.PutAsync($"/roomserver/rooms/{roomId}/subrooms/2/name",
|
||||
new FormUrlEncodedContent(new Dictionary<string, string> { ["name"] = "Basement" })))
|
||||
Assert.True(rename.IsSuccessStatusCode, await rename.Content.ReadAsStringAsync());
|
||||
|
||||
await using var scope = _factory.Services.CreateAsyncScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<DorkNetDbContext>();
|
||||
Assert.Equal("Basement", (await db.RoomScenes.SingleAsync(s => s.RoomId == roomId && s.OrderIndex == 1)).Name);
|
||||
Assert.Equal("Home", (await db.RoomScenes.SingleAsync(s => s.RoomId == roomId && s.OrderIndex == 0)).Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Cloning_a_template_without_scene_rows_creates_a_named_home_sub_room()
|
||||
{
|
||||
var owner = await PlayerAsync();
|
||||
var templateId = await SeedRoomAsync(1, accessibility: 1, scenes: Array.Empty<string>(), isAg: true);
|
||||
|
||||
using var rooms = Client("rooms", owner);
|
||||
using var resp = await rooms.PostAsync($"/roomserver/rooms/{templateId}/clone",
|
||||
new FormUrlEncodedContent(new Dictionary<string, string> { ["name"] = $"Fresh{Guid.NewGuid():N}"[..20] }));
|
||||
var text = await resp.Content.ReadAsStringAsync();
|
||||
Assert.True(resp.IsSuccessStatusCode, text);
|
||||
using var json = JsonDocument.Parse(text);
|
||||
var value = json.RootElement.GetProperty("value");
|
||||
var cloneId = value.GetProperty("RoomId").GetInt64();
|
||||
var sub = value.GetProperty("SubRooms").EnumerateArray().Single();
|
||||
Assert.Equal(1, sub.GetProperty("SubRoomId").GetInt64());
|
||||
Assert.Equal("Home", sub.GetProperty("Name").GetString());
|
||||
|
||||
await using var scope = _factory.Services.CreateAsyncScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<DorkNetDbContext>();
|
||||
var scene = await db.RoomScenes.SingleAsync(s => s.RoomId == cloneId);
|
||||
Assert.Equal(0, scene.OrderIndex);
|
||||
Assert.Equal("Home", scene.Name);
|
||||
Assert.Equal("a75f7547-79eb-47c6-8986-6767abcb4f92", scene.RoomSceneLocationId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Startup_repair_gives_scene_less_rooms_a_home_and_names_blank_rows()
|
||||
{
|
||||
var bare = await SeedRoomAsync(1, accessibility: 0, scenes: Array.Empty<string>());
|
||||
var blank = await SeedRoomAsync(1, accessibility: 0, scenes: new[] { "", " " });
|
||||
|
||||
await using var scope = _factory.Services.CreateAsyncScope();
|
||||
var service = scope.ServiceProvider.GetRequiredService<RoomService>();
|
||||
var (added, renamed) = await service.RepairRoomScenesAsync();
|
||||
Assert.True(added >= 1);
|
||||
Assert.True(renamed >= 2);
|
||||
|
||||
var db = scope.ServiceProvider.GetRequiredService<DorkNetDbContext>();
|
||||
var home = await db.RoomScenes.SingleAsync(s => s.RoomId == bare);
|
||||
Assert.Equal((0, "Home"), (home.OrderIndex, home.Name));
|
||||
var names = await db.RoomScenes.Where(s => s.RoomId == blank).OrderBy(s => s.OrderIndex).Select(s => s.Name).ToListAsync();
|
||||
Assert.Equal(new[] { "Home", "Subroom 2" }, names);
|
||||
|
||||
// Idempotent.
|
||||
Assert.Equal((0, 0), await service.RepairRoomScenesAsync());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Instance_browser_lists_the_private_instance_with_its_players_for_allowed_callers()
|
||||
{
|
||||
var owner = await PlayerAsync();
|
||||
var coOwner = await PlayerAsync();
|
||||
var stranger = await PlayerAsync();
|
||||
var roomId = await SeedRoomAsync(owner.PlayerId, accessibility: 0, scenes: new[] { "Home" });
|
||||
await using (var scope = _factory.Services.CreateAsyncScope())
|
||||
{
|
||||
var db = scope.ServiceProvider.GetRequiredService<DorkNetDbContext>();
|
||||
db.RoomRoles.Add(new RoomRoleEntity
|
||||
{
|
||||
RoomId = roomId, PlayerId = coOwner.PlayerId, Role = RoomRoleRules.StoredCoOwner, Accepted = true,
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
using var ownerMatch = Client("match", owner);
|
||||
var joined = await PostFormAsync(ownerMatch, $"/matchmake/room/{roomId}");
|
||||
var instanceId = joined.GetProperty("roomInstance").GetProperty("roomInstanceId").GetInt64();
|
||||
|
||||
foreach (var viewer in new[] { owner, coOwner })
|
||||
{
|
||||
using var match = Client("match", viewer);
|
||||
var list = await GetJsonAsync(match, $"/room/{roomId}/instances");
|
||||
var row = list.EnumerateArray().Single();
|
||||
Assert.Equal(instanceId, row.GetProperty("roomInstanceId").GetInt64());
|
||||
Assert.Equal(roomId, row.GetProperty("roomId").GetInt64());
|
||||
Assert.Equal(1, row.GetProperty("subRoomId").GetInt64());
|
||||
Assert.False(row.GetProperty("isFull").GetBoolean());
|
||||
Assert.Equal(new[] { (int)owner.PlayerId },
|
||||
row.GetProperty("playerIds").EnumerateArray().Select(p => p.GetInt32()));
|
||||
}
|
||||
|
||||
// Someone with no way into the private room is not shown it.
|
||||
using var strangerMatch = Client("match", stranger);
|
||||
var hidden = await GetJsonAsync(strangerMatch, $"/room/{roomId}/instances");
|
||||
Assert.Empty(hidden.EnumerateArray());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Private_instance_visibility_rule()
|
||||
{
|
||||
var row = new PrivateInstanceEntity { Id = 5, OwnerPlayerId = 3 };
|
||||
Assert.True(Controllers.Match.MatchController.CanSeePrivateInstance(9, true, Array.Empty<long>(), row, false));
|
||||
Assert.True(Controllers.Match.MatchController.CanSeePrivateInstance(9, false, new long[] { 9 }, row, false));
|
||||
Assert.True(Controllers.Match.MatchController.CanSeePrivateInstance(3, false, Array.Empty<long>(), row, false));
|
||||
Assert.True(Controllers.Match.MatchController.CanSeePrivateInstance(9, false, Array.Empty<long>(), row, true));
|
||||
Assert.False(Controllers.Match.MatchController.CanSeePrivateInstance(9, false, Array.Empty<long>(), row, false));
|
||||
Assert.False(Controllers.Match.MatchController.CanSeePrivateInstance(0, true, Array.Empty<long>(), row, true));
|
||||
}
|
||||
|
||||
// ── helpers ─────────────────────────────────────────────────────────
|
||||
|
||||
private async Task<GameClientSession> PlayerAsync()
|
||||
{
|
||||
using var setup = Client("api");
|
||||
return await GameClientSessionFactory.CreateAsync(setup, _factory.ApexDomain);
|
||||
}
|
||||
|
||||
private async Task<long> SeedRoomAsync(long creatorId, int accessibility, string[] scenes, bool isAg = false)
|
||||
{
|
||||
var roomId = 9_200_000 + Random.Shared.Next(1, 99_999);
|
||||
await using var scope = _factory.Services.CreateAsyncScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<DorkNetDbContext>();
|
||||
db.Rooms.Add(new RoomEntity
|
||||
{
|
||||
Id = roomId,
|
||||
Name = $"Subs{Guid.NewGuid():N}"[..18],
|
||||
CreatorPlayerId = creatorId,
|
||||
ImageName = RoomService.DefaultRoomImageName,
|
||||
Accessibility = accessibility,
|
||||
CloningAllowed = true,
|
||||
IsAGRoom = isAg,
|
||||
LocationReplicationId = "a75f7547-79eb-47c6-8986-6767abcb4f92",
|
||||
});
|
||||
for (var i = 0; i < scenes.Length; i++)
|
||||
db.RoomScenes.Add(new RoomSceneEntity { RoomId = roomId, OrderIndex = i, Name = scenes[i] });
|
||||
await db.SaveChangesAsync();
|
||||
return roomId;
|
||||
}
|
||||
|
||||
private static async Task<JsonElement> PostFormAsync(HttpClient client, string path)
|
||||
{
|
||||
using var response = await client.PostAsync(path, new FormUrlEncodedContent(new Dictionary<string, string>()));
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
Assert.True(response.IsSuccessStatusCode, $"POST {path} -> {(int)response.StatusCode}: {body}");
|
||||
return JsonDocument.Parse(body).RootElement.Clone();
|
||||
}
|
||||
|
||||
private static async Task<JsonElement> GetJsonAsync(HttpClient client, string path)
|
||||
{
|
||||
using var response = await client.GetAsync(path);
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
Assert.True(response.IsSuccessStatusCode, $"GET {path} -> {(int)response.StatusCode}: {body}");
|
||||
return JsonDocument.Parse(body).RootElement.Clone();
|
||||
}
|
||||
|
||||
private HttpClient Client(string subdomain, GameClientSession? session = null)
|
||||
{
|
||||
var client = _factory.CreateClient(new() { AllowAutoRedirect = false });
|
||||
client.BaseAddress = new Uri($"http://{subdomain}.{_factory.ApexDomain}");
|
||||
client.Timeout = TimeSpan.FromSeconds(30);
|
||||
client.DefaultRequestHeaders.Accept.ParseAdd("application/json");
|
||||
client.DefaultRequestHeaders.UserAgent.ParseAdd("RecRoom/2023.03.21");
|
||||
if (session is not null)
|
||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", session.AccessToken);
|
||||
return client;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user