mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-25 15:09:26 -07:00
782 lines
32 KiB
C#
782 lines
32 KiB
C#
using System.IO;
|
|
using System.Security.Claims;
|
|
using System.Text;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using RadicadeServer.API.Events;
|
|
using RadicadeServer.DB;
|
|
using RadicadeServer.RecNet.Enums;
|
|
using RadicadeServer.RecNet.Events;
|
|
using RadicadeServer.RecNet.Rooms;
|
|
using Xunit;
|
|
|
|
namespace RadicadeServer.Tests;
|
|
|
|
/// <summary>
|
|
/// End-to-end tests for the game's event endpoints, exercising the real
|
|
/// EventsAPI controller against an in-memory database.
|
|
/// </summary>
|
|
public class EventsApiTests
|
|
{
|
|
private const long CreatorId = 42;
|
|
|
|
private static RecDBContext CreateDb()
|
|
{
|
|
var options = TestDbOptions.Create()
|
|
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
|
.Options;
|
|
return new RecDBContext(options);
|
|
}
|
|
|
|
private static EventsAPI CreateController(RecDBContext db, long? userId = CreatorId)
|
|
{
|
|
var identity = new ClaimsIdentity("TestAuth");
|
|
if (userId.HasValue)
|
|
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, userId.Value.ToString()));
|
|
|
|
return new EventsAPI(db)
|
|
{
|
|
ControllerContext = new ControllerContext
|
|
{
|
|
HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal(identity) }
|
|
}
|
|
};
|
|
}
|
|
|
|
private static EventsAPI CreateController(RecDBContext db, long? userId, string jsonBody)
|
|
{
|
|
var controller = CreateController(db, userId);
|
|
SetJsonBody(controller, jsonBody);
|
|
return controller;
|
|
}
|
|
|
|
private static void SetJsonBody(EventsAPI controller, string jsonBody)
|
|
{
|
|
var bytes = Encoding.UTF8.GetBytes(jsonBody);
|
|
controller.HttpContext.Request.Body = new MemoryStream(bytes);
|
|
controller.HttpContext.Request.ContentType = "application/json";
|
|
controller.HttpContext.Request.ContentLength = bytes.Length;
|
|
}
|
|
|
|
private static async Task<long> CreateV2EventAsync(RecDBContext db, long roomId)
|
|
{
|
|
var createController = CreateController(
|
|
db, CreatorId,
|
|
$$"""{ "Name": "Club meetup", "RoomId": {{roomId}} }""");
|
|
var create = Assert.IsType<OkObjectResult>(await createController.CreateEventV2());
|
|
using var document = System.Text.Json.JsonDocument.Parse(
|
|
System.Text.Json.JsonSerializer.Serialize(create.Value));
|
|
return document.RootElement.GetProperty("PlayerEvent").GetProperty("PlayerEventId").GetInt64();
|
|
}
|
|
|
|
private static async Task<System.Text.Json.JsonDocument> UpdateEventV2Async(
|
|
RecDBContext db, long eventId, string route, string body)
|
|
{
|
|
var controller = CreateController(db);
|
|
SetJsonBody(controller, body);
|
|
controller.HttpContext.Request.Path = $"/api/playerevents/v2/{eventId}/{route}";
|
|
var result = Assert.IsType<OkObjectResult>(await controller.UpdateEventV2(eventId));
|
|
return System.Text.Json.JsonDocument.Parse(System.Text.Json.JsonSerializer.Serialize(result.Value));
|
|
}
|
|
|
|
private static async Task<long> SeedRoomAsync(RecDBContext db, string name = "Test Room")
|
|
{
|
|
var room = new RoomData { Name = name };
|
|
db.Rooms.Add(room);
|
|
await db.SaveChangesAsync();
|
|
return room.RoomId;
|
|
}
|
|
|
|
private static async Task<PlayerEvent> CreateEventAsync(
|
|
EventsAPI controller,
|
|
long roomId,
|
|
string name = "Launch Party")
|
|
{
|
|
var result = await controller.CreateEvent(
|
|
name, "A test event", roomId,
|
|
DateTime.UtcNow.AddHours(1), null, null, null);
|
|
|
|
var ok = Assert.IsType<OkObjectResult>(result.Result);
|
|
return Assert.IsType<PlayerEvent>(ok.Value);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateEvent_ThenAppearsInSearchAndRoomEvents()
|
|
{
|
|
var db = CreateDb();
|
|
var controller = CreateController(db);
|
|
var roomId = await SeedRoomAsync(db);
|
|
|
|
var created = await CreateEventAsync(controller, roomId, "Launch Party"); var search = await controller.SearchEvents("launch", null);
|
|
Assert.Single(search);
|
|
Assert.Contains(search, e => e.PlayerEventId == created.PlayerEventId);
|
|
|
|
var roomEvents = await controller.GetEventsInRoom(roomId);
|
|
Assert.Contains(roomEvents, e => e.PlayerEventId == created.PlayerEventId);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SearchEvents_OnlyReturnsPublicUpcomingEvents()
|
|
{
|
|
var db = CreateDb();
|
|
var controller = CreateController(db);
|
|
var roomId = await SeedRoomAsync(db);
|
|
|
|
await CreateEventAsync(controller, roomId, "Public Upcoming");
|
|
db.PlayerEvents.Add(new PlayerEventDB
|
|
{
|
|
Name = "Ended",
|
|
Description = "",
|
|
ImageName = "",
|
|
StartTime = DateTime.UtcNow.AddHours(-4),
|
|
EndTime = DateTime.UtcNow.AddHours(-2),
|
|
CreatorPlayerId = CreatorId,
|
|
AttendeeCount = 1,
|
|
RoomId = roomId,
|
|
Accessibility = Accessibility.Public
|
|
});
|
|
db.PlayerEvents.Add(new PlayerEventDB
|
|
{
|
|
Name = "Private",
|
|
Description = "",
|
|
ImageName = "",
|
|
StartTime = DateTime.UtcNow.AddHours(1),
|
|
EndTime = DateTime.UtcNow.AddHours(3),
|
|
CreatorPlayerId = CreatorId,
|
|
AttendeeCount = 1,
|
|
RoomId = roomId,
|
|
Accessibility = Accessibility.Private
|
|
});
|
|
await db.SaveChangesAsync(); var search = await controller.SearchEvents(null, null);
|
|
Assert.Single(search);
|
|
Assert.Equal("Public Upcoming", search[0].Name);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateEvent_BlankName_ReturnsBadRequest()
|
|
{
|
|
var db = CreateDb();
|
|
var controller = CreateController(db);
|
|
var roomId = await SeedRoomAsync(db);
|
|
|
|
var result = await controller.CreateEvent(" ", null, roomId, null, null, null, null);
|
|
|
|
Assert.IsType<BadRequestObjectResult>(result.Result);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateEvent_NonPositiveRoomId_ReturnsBadRequest()
|
|
{
|
|
var db = CreateDb();
|
|
var controller = CreateController(db);
|
|
|
|
var result = await controller.CreateEvent("Launch", null, 0, null, null, null, null);
|
|
|
|
Assert.IsType<BadRequestObjectResult>(result.Result);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateEvent_UnknownRoom_ReturnsBadRequest()
|
|
{
|
|
var db = CreateDb();
|
|
var controller = CreateController(db);
|
|
|
|
var result = await controller.CreateEvent("Launch", null, 9999, null, null, null, null);
|
|
|
|
Assert.IsType<BadRequestObjectResult>(result.Result);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateEvent_NoUser_ReturnsUnauthorized()
|
|
{
|
|
var db = CreateDb();
|
|
var controller = CreateController(db, userId: null);
|
|
var roomId = await SeedRoomAsync(db);
|
|
|
|
var result = await controller.CreateEvent("Launch", null, roomId, null, null, null, null);
|
|
|
|
Assert.IsType<UnauthorizedResult>(result.Result);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateEvent_ImplicitlyRSVPsCreatorYes()
|
|
{
|
|
var db = CreateDb();
|
|
var controller = CreateController(db);
|
|
var roomId = await SeedRoomAsync(db);
|
|
|
|
var created = await CreateEventAsync(controller, roomId);
|
|
|
|
var response = await db.PlayerEventResponses
|
|
.SingleAsync(r => r.PlayerEventId == created.PlayerEventId);
|
|
Assert.Equal(CreatorId, response.PlayerId);
|
|
Assert.Equal(EventResponses.Yes, response.Type);
|
|
Assert.Equal(1, created.AttendeeCount);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetCreatedEvents_ReturnsOnlyTheUsersEvents()
|
|
{
|
|
var db = CreateDb();
|
|
var controller = CreateController(db, userId: 42);
|
|
var otherController = CreateController(db, userId: 43);
|
|
var roomId = await SeedRoomAsync(db);
|
|
|
|
await CreateEventAsync(controller, roomId, "Mine");
|
|
await CreateEventAsync(otherController, roomId, "Theirs");
|
|
|
|
var mine = await controller.GetCreatedEvents();
|
|
|
|
Assert.Single(mine);
|
|
Assert.Equal("Mine", mine[0].Name);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetAllEvents_ReturnsCreatedAndNoWireResponses()
|
|
{
|
|
var db = CreateDb();
|
|
var controller = CreateController(db);
|
|
var roomId = await SeedRoomAsync(db);
|
|
await CreateEventAsync(controller, roomId, "Mine");
|
|
|
|
var all = await controller.GetAllEvents();
|
|
|
|
Assert.Single(all.Created);
|
|
// The 2023 client parses Responses entries with its typed MessagePack
|
|
// deserializer and a shape it does not fully understand deserializes to
|
|
// null, failing the boot-time events refresh. The RecFlare reference
|
|
// server always serves an empty Responses list, so Radicade does too.
|
|
Assert.Empty(all.Responses);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetAllEvents_NeverServesAnEmptyImageName()
|
|
{
|
|
var db = CreateDb();
|
|
var controller = CreateController(db);
|
|
var roomId = await SeedRoomAsync(db);
|
|
db.PlayerEvents.Add(new PlayerEventDB
|
|
{
|
|
Name = "No image",
|
|
Description = "",
|
|
ImageName = "",
|
|
StartTime = DateTime.UtcNow.AddHours(1),
|
|
EndTime = DateTime.UtcNow.AddHours(2),
|
|
CreatorPlayerId = CreatorId,
|
|
AttendeeCount = 0,
|
|
RoomId = roomId,
|
|
Accessibility = Accessibility.Public
|
|
});
|
|
await db.SaveChangesAsync();
|
|
|
|
var all = await controller.GetAllEvents();
|
|
var created = Assert.Single(all.Created);
|
|
|
|
// The 2023 client's RRUI events cache resolves an image for every event it
|
|
// lists and throws on an empty name before making any request, which fails
|
|
// the whole refresh and makes the client retry it in a tight loop.
|
|
Assert.Equal("default", created.ImageName);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ClubEventFeedsReturnTheExpectedDifferentShapes()
|
|
{
|
|
var db = CreateDb();
|
|
var controller = CreateController(db);
|
|
var roomId = await SeedRoomAsync(db);
|
|
var now = DateTime.UtcNow;
|
|
db.PlayerEvents.AddRange(
|
|
new PlayerEventDB
|
|
{
|
|
Name = "Club 12 event", Description = "", ImageName = "", RoomId = roomId,
|
|
CreatorPlayerId = CreatorId, StartTime = now.AddHours(1), EndTime = now.AddHours(2),
|
|
ClubId = 12, Accessibility = Accessibility.Public
|
|
},
|
|
new PlayerEventDB
|
|
{
|
|
Name = "Club 13 event", Description = "", ImageName = "", RoomId = roomId,
|
|
CreatorPlayerId = CreatorId, StartTime = now.AddHours(2), EndTime = now.AddHours(3),
|
|
ClubId = 13, Accessibility = Accessibility.Public
|
|
},
|
|
new PlayerEventDB
|
|
{
|
|
Name = "Unattached event", Description = "", ImageName = "", RoomId = roomId,
|
|
CreatorPlayerId = CreatorId, StartTime = now.AddHours(3), EndTime = now.AddHours(4),
|
|
Accessibility = Accessibility.Public
|
|
});
|
|
await db.SaveChangesAsync();
|
|
|
|
var clubs = await controller.GetEventsByClubs([12, 13]);
|
|
Assert.Equal(new[] { "Club 12 event", "Club 13 event" }, clubs.Select(e => e.Name));
|
|
Assert.Equal(12, clubs[0].ClubId);
|
|
|
|
var single = Assert.IsType<OkObjectResult>(await controller.GetEventsByClub(12));
|
|
using var json = System.Text.Json.JsonDocument.Parse(System.Text.Json.JsonSerializer.Serialize(single.Value));
|
|
Assert.Equal(string.Empty, json.RootElement.GetProperty("ContinuationToken").GetString());
|
|
Assert.Equal("Club 12 event", json.RootElement.GetProperty("Events")[0].GetProperty("Name").GetString());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetEvent_ReturnsEventOrNotFound()
|
|
{
|
|
var db = CreateDb();
|
|
var controller = CreateController(db);
|
|
var roomId = await SeedRoomAsync(db);
|
|
var created = await CreateEventAsync(controller, roomId);
|
|
|
|
var found = await controller.GetEvent(created.PlayerEventId);
|
|
var ok = Assert.IsType<OkObjectResult>(found.Result);
|
|
Assert.Equal(created.PlayerEventId, Assert.IsType<PlayerEvent>(ok.Value).PlayerEventId);
|
|
|
|
var missing = await controller.GetEvent(9999);
|
|
Assert.IsType<NotFoundResult>(missing.Result);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetEvent_SerializesAllClientRequiredFields()
|
|
{
|
|
// The client deserializes every key of its PlayerEvent model and logs
|
|
// "Failed to find key 'Tags'" (then "Malformed Response") when one is
|
|
// missing. Guard the full wire shape here.
|
|
var db = CreateDb();
|
|
var controller = CreateController(db);
|
|
var roomId = await SeedRoomAsync(db);
|
|
var created = await CreateEventAsync(controller, roomId);
|
|
|
|
var json = System.Text.Json.JsonSerializer.Serialize(created);
|
|
|
|
Assert.Contains("\"Tags\"", json);
|
|
Assert.Contains("\"ClubId\"", json);
|
|
Assert.Contains("\"State\"", json);
|
|
Assert.Contains("\"CreatedAt\"", json);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetEventTagFilters_ReturnsUsableTagCatalog()
|
|
{
|
|
// The client refuses to save an event while every tag group is empty, so the
|
|
// endpoint serves the reference server's lowercase catalog instead of three
|
|
// empty sets. TrendingFilters is null exactly as the reference serves it -
|
|
// the client renders no trending row for null.
|
|
var db = CreateDb();
|
|
var controller = CreateController(db);
|
|
|
|
var filters = controller.GetEventTagFilters();
|
|
|
|
Assert.NotNull(filters.PinnedFilters);
|
|
Assert.NotNull(filters.PopularFilters);
|
|
Assert.Null(filters.TrendingFilters);
|
|
Assert.NotEmpty(filters.PinnedFilters);
|
|
Assert.NotEmpty(filters.PopularFilters);
|
|
Assert.All(filters.PinnedFilters, t => Assert.Equal(t, t.ToLowerInvariant()));
|
|
Assert.All(filters.PopularFilters, t => Assert.Equal(t, t.ToLowerInvariant()));
|
|
Assert.Contains("game", filters.PopularFilters);
|
|
Assert.Contains("meetup", filters.PinnedFilters);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SetResponse_AddsRsvpAndUpdatesAttendeeCount()
|
|
{
|
|
var db = CreateDb();
|
|
var creator = CreateController(db, userId: 42);
|
|
var guest = CreateController(db, userId: 43);
|
|
var roomId = await SeedRoomAsync(db);
|
|
var created = await CreateEventAsync(creator, roomId);
|
|
|
|
var yes = await guest.SetResponse(created.PlayerEventId, EventResponses.Yes);
|
|
Assert.Equal(
|
|
EventResponses.Yes,
|
|
Assert.IsType<EventResponse>(Assert.IsType<OkObjectResult>(yes.Result).Value).Type);
|
|
|
|
var ev = await db.PlayerEvents.SingleAsync(e => e.PlayerEventId == created.PlayerEventId);
|
|
Assert.Equal(2, ev.AttendeeCount);
|
|
|
|
await guest.SetResponse(created.PlayerEventId, EventResponses.No);
|
|
ev = await db.PlayerEvents.SingleAsync(e => e.PlayerEventId == created.PlayerEventId);
|
|
Assert.Equal(1, ev.AttendeeCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetLegacyPlaceholderResponses_ReturnsAnEmptyList()
|
|
{
|
|
var controller = CreateController(CreateDb());
|
|
var route = typeof(EventsAPI).GetMethod(nameof(EventsAPI.GetLegacyPlaceholderResponses))!
|
|
.GetCustomAttributes(typeof(Microsoft.AspNetCore.Mvc.HttpGetAttribute), false)
|
|
.Cast<Microsoft.AspNetCore.Mvc.HttpGetAttribute>()
|
|
.Single();
|
|
Assert.Equal("v1/-1/responses", route.Template);
|
|
|
|
var responses = Assert.IsType<OkObjectResult>(controller.GetLegacyPlaceholderResponses());
|
|
var list = Assert.IsAssignableFrom<IEnumerable<RadicadeServer.RecNet.Events.EventResponse>>(responses.Value);
|
|
Assert.Empty(list);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetResponsesAndAttendees_ReflectRsvps()
|
|
{
|
|
var db = CreateDb();
|
|
var creator = CreateController(db, userId: 42);
|
|
var guest = CreateController(db, userId: 43);
|
|
var roomId = await SeedRoomAsync(db);
|
|
var created = await CreateEventAsync(creator, roomId);
|
|
await guest.SetResponse(created.PlayerEventId, EventResponses.Yes);
|
|
|
|
var responses = await creator.GetResponses(created.PlayerEventId);
|
|
var list = Assert.IsType<List<EventResponse>>(
|
|
Assert.IsType<OkObjectResult>(responses.Result).Value);
|
|
Assert.Equal(2, list.Count);
|
|
|
|
var attendees = await creator.GetAttendees(created.PlayerEventId);
|
|
var ids = Assert.IsType<List<long>>(
|
|
Assert.IsType<OkObjectResult>(attendees.Result).Value);
|
|
Assert.Contains(CreatorId, ids);
|
|
Assert.Contains(43, ids);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DeleteEvent_CreatorCanDelete_NonCreatorCannot()
|
|
{
|
|
var db = CreateDb();
|
|
var creator = CreateController(db, userId: 42);
|
|
var stranger = CreateController(db, userId: 99);
|
|
var roomId = await SeedRoomAsync(db);
|
|
var created = await CreateEventAsync(creator, roomId);
|
|
|
|
var forbidden = await stranger.DeleteEvent(created.PlayerEventId);
|
|
Assert.IsType<ForbidResult>(forbidden);
|
|
Assert.NotNull(await db.PlayerEvents.FindAsync(created.PlayerEventId));
|
|
|
|
var ok = await creator.DeleteEvent(created.PlayerEventId);
|
|
Assert.IsType<OkResult>(ok);
|
|
Assert.Null(await db.PlayerEvents.FindAsync(created.PlayerEventId));
|
|
Assert.Empty(await db.PlayerEventResponses
|
|
.Where(r => r.PlayerEventId == created.PlayerEventId).ToListAsync());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DeleteEvent_MissingEvent_ReturnsNotFound()
|
|
{
|
|
var db = CreateDb();
|
|
var controller = CreateController(db);
|
|
|
|
var result = await controller.DeleteEvent(9999);
|
|
|
|
Assert.IsType<NotFoundResult>(result);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateEventV2_CreatesEventAndReturnsWrapper()
|
|
{
|
|
var db = CreateDb();
|
|
var roomId = await SeedRoomAsync(db);
|
|
var body =
|
|
$$"""
|
|
{
|
|
"Name": "Launch Party",
|
|
"Description": "Big launch",
|
|
"RoomId": {{roomId}},
|
|
"StartTime": "{{DateTime.UtcNow.AddHours(1):o}}",
|
|
"EndTime": "{{DateTime.UtcNow.AddHours(3):o}}",
|
|
"Accessibility": "public",
|
|
"SubRoomId": null,
|
|
"ImageName": ""
|
|
}
|
|
""";
|
|
var controller = CreateController(db, CreatorId, body);
|
|
|
|
var result = await controller.CreateEventV2();
|
|
|
|
var ok = Assert.IsType<OkObjectResult>(result);
|
|
var json = System.Text.Json.JsonSerializer.Serialize(ok.Value);
|
|
using var doc = System.Text.Json.JsonDocument.Parse(json);
|
|
|
|
// The write envelope: { PlayerEvent, Result, TagModifyResult } (the shape the
|
|
// reference server answers and the client's typed deserializer requires).
|
|
Assert.Equal(0, doc.RootElement.GetProperty("Result").GetInt32());
|
|
var tagModify = doc.RootElement.GetProperty("TagModifyResult");
|
|
Assert.Equal(0, tagModify.GetProperty("Result").GetInt32());
|
|
Assert.Equal(System.Text.Json.JsonValueKind.Array, tagModify.GetProperty("Tags").ValueKind);
|
|
|
|
var created = doc.RootElement.GetProperty("PlayerEvent");
|
|
Assert.Equal("Launch Party", created.GetProperty("Name").GetString());
|
|
Assert.Equal(roomId, created.GetProperty("RoomId").GetInt64());
|
|
Assert.Equal(Accessibility.Public, (Accessibility)created.GetProperty("Accessibility").GetInt32());
|
|
|
|
var eventId = created.GetProperty("PlayerEventId").GetInt64();
|
|
var ev = await db.PlayerEvents.SingleAsync(e => e.PlayerEventId == eventId);
|
|
Assert.Equal(1, ev.AttendeeCount);
|
|
var rsvp = await db.PlayerEventResponses.SingleAsync(r => r.PlayerEventId == eventId);
|
|
Assert.Equal(CreatorId, rsvp.PlayerId);
|
|
Assert.Equal(EventResponses.Yes, rsvp.Type);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateEventV2_UnknownRoom_ReturnsBadRequest()
|
|
{
|
|
var db = CreateDb();
|
|
var body = """{ "Name": "Launch", "RoomId": 9999 }""";
|
|
var controller = CreateController(db, CreatorId, body);
|
|
|
|
var result = await controller.CreateEventV2();
|
|
|
|
Assert.IsType<BadRequestObjectResult>(result);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UpdateEventV2StoresClubAndEventSettings()
|
|
{
|
|
var db = CreateDb();
|
|
var roomId = await SeedRoomAsync(db);
|
|
var eventId = await CreateV2EventAsync(db, roomId);
|
|
var payload = """{ "ClubId": 27 }""";
|
|
|
|
using var updatedJson = await UpdateEventV2Async(db, eventId, "club", payload);
|
|
var playerEvent = updatedJson.RootElement.GetProperty("PlayerEvent");
|
|
Assert.Equal(27, playerEvent.GetProperty("ClubId").GetInt64());
|
|
Assert.False(playerEvent.GetProperty("IsMultiInstance").GetBoolean());
|
|
|
|
var routeAttributes = typeof(EventsAPI).GetMethod(nameof(EventsAPI.UpdateEventV2))!
|
|
.GetCustomAttributes(typeof(Microsoft.AspNetCore.Mvc.Routing.HttpMethodAttribute), false)
|
|
.Cast<Microsoft.AspNetCore.Mvc.Routing.HttpMethodAttribute>();
|
|
Assert.Contains(routeAttributes, attribute => attribute.Template == "v2/{eventId:long}/club");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UpdateEventV2ClubRouteSupportsNullableClubId()
|
|
{
|
|
var db = CreateDb();
|
|
var roomId = await SeedRoomAsync(db);
|
|
var eventId = await CreateV2EventAsync(db, roomId);
|
|
|
|
using var assigned = await UpdateEventV2Async(db, eventId, "club", """{ "ClubId": 27 }""");
|
|
using var cleared = await UpdateEventV2Async(db, eventId, "club", """{ "ClubId": null }""");
|
|
|
|
Assert.Equal(27, assigned.RootElement.GetProperty("PlayerEvent").GetProperty("ClubId").GetInt64());
|
|
Assert.Equal(System.Text.Json.JsonValueKind.Null,
|
|
cleared.RootElement.GetProperty("PlayerEvent").GetProperty("ClubId").ValueKind);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UpdateEventV2MultiInstanceRouteReadsItsFocusedPayload()
|
|
{
|
|
var db = CreateDb();
|
|
var roomId = await SeedRoomAsync(db);
|
|
var eventId = await CreateV2EventAsync(db, roomId);
|
|
var body = """{ "IsMultiInstance": true, "SupportMultiInstanceRoomChat": true, "DefaultBroadcastPermissions": 3, "CanRequestBroadcastPermissions": 7 }""";
|
|
|
|
using var updated = await UpdateEventV2Async(db, eventId, "multiinstance", body);
|
|
var playerEvent = updated.RootElement.GetProperty("PlayerEvent");
|
|
Assert.True(playerEvent.GetProperty("IsMultiInstance").GetBoolean());
|
|
Assert.True(playerEvent.GetProperty("SupportMultiInstanceRoomChat").GetBoolean());
|
|
Assert.Equal(3, playerEvent.GetProperty("DefaultBroadcastPermissions").GetInt32());
|
|
Assert.Equal(7, playerEvent.GetProperty("CanRequestBroadcastPermissions").GetInt32());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UpdateEventV2StoresStructuredTagsInTheClientShape()
|
|
{
|
|
var db = CreateDb();
|
|
var roomId = await SeedRoomAsync(db);
|
|
var eventId = await CreateV2EventAsync(db, roomId);
|
|
var body = """{ "Tags": [{ "Tag": "meetup", "Type": 1 }] }""";
|
|
|
|
using var updated = await UpdateEventV2Async(db, eventId, "tags", body);
|
|
var tag = updated.RootElement.GetProperty("PlayerEvent").GetProperty("Tags")[0];
|
|
Assert.Equal("meetup", tag.GetProperty("Tag").GetString());
|
|
Assert.Equal(1, tag.GetProperty("Type").GetInt32());
|
|
Assert.Equal("[{\"Tag\":\"meetup\",\"Type\":1}]",
|
|
(await db.PlayerEvents.SingleAsync(x => x.PlayerEventId == eventId)).TagsJson);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DeleteEventV2_RemovesTheEvent()
|
|
{
|
|
var db = CreateDb();
|
|
var roomId = await SeedRoomAsync(db);
|
|
var controller = CreateController(
|
|
db, CreatorId, $$"""{ "Name": "To Delete", "RoomId": {{roomId}} }""");
|
|
|
|
var create = await controller.CreateEventV2();
|
|
long createdId;
|
|
using (var doc = System.Text.Json.JsonDocument.Parse(
|
|
System.Text.Json.JsonSerializer.Serialize(Assert.IsType<OkObjectResult>(create).Value)))
|
|
createdId = doc.RootElement.GetProperty("PlayerEvent").GetProperty("PlayerEventId").GetInt64();
|
|
|
|
var delete = await controller.DeleteEventV2(createdId);
|
|
|
|
Assert.IsType<OkObjectResult>(delete);
|
|
Assert.Null(await db.PlayerEvents.FindAsync(createdId));
|
|
Assert.Empty(await db.PlayerEventResponses
|
|
.Where(r => r.PlayerEventId == createdId).ToListAsync());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DeleteEventV2_AlreadyGoneEvent_AnswersSuccessInsteadOfNotFound()
|
|
{
|
|
// The client clears a card only when the 82 frame says so, so a list still showing an
|
|
// event whose row is gone is otherwise stuck: every retry is a 404 it can only render as
|
|
// an error and no frame is ever pushed again. A repeat delete answers success and puts
|
|
// the frame back on the wire, which is what lets the retry clear the card.
|
|
var db = CreateDb();
|
|
var controller = CreateController(db);
|
|
|
|
var result = await controller.DeleteEventV2(9999);
|
|
|
|
var ok = Assert.IsType<OkObjectResult>(result);
|
|
using var document = System.Text.Json.JsonDocument.Parse(
|
|
System.Text.Json.JsonSerializer.Serialize(ok.Value));
|
|
Assert.Equal(0, document.RootElement.GetProperty("Result").GetInt32());
|
|
Assert.Equal(9999,
|
|
document.RootElement.GetProperty("PlayerEvent").GetProperty("PlayerEventId").GetInt64());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DeleteEventV2_OtherPlayersEvent_IsStillForbidden()
|
|
{
|
|
// Answering "already gone" must not widen who may delete a row that is still there.
|
|
var db = CreateDb();
|
|
var roomId = await SeedRoomAsync(db);
|
|
var createdId = await CreateV2EventAsync(db, roomId);
|
|
|
|
var stranger = CreateController(db, userId: 99);
|
|
Assert.IsType<ForbidResult>(await stranger.DeleteEventV2(createdId));
|
|
Assert.NotNull(await db.PlayerEvents.FindAsync(createdId));
|
|
}
|
|
|
|
private static async Task<long> MakeMultiInstanceAsync(RecDBContext db, long eventId)
|
|
{
|
|
using var updated = await UpdateEventV2Async(
|
|
db, eventId, "multiinstance", """{ "IsMultiInstance": true }""");
|
|
return eventId;
|
|
}
|
|
|
|
[Fact]
|
|
public async Task BroadcastRoomInstance_SetsAndClearsTheBroadcastingInstance()
|
|
{
|
|
// The client posts `BroadcastRoomInstanceRequest { PlayerEventId, RoomInstanceId }`
|
|
// to api/playerevents/v1/broadcast when a broadcaster goes live, and again with a
|
|
// null instance when they stop. The server never had the route, so the call 404ed.
|
|
var db = CreateDb();
|
|
var roomId = await SeedRoomAsync(db);
|
|
var eventId = await MakeMultiInstanceAsync(db, await CreateV2EventAsync(db, roomId));
|
|
var instance = new RoomInstance { roomId = roomId, subRoomId = 1, eventId = eventId };
|
|
db.RoomInstances.Add(instance);
|
|
await db.SaveChangesAsync();
|
|
|
|
var start = CreateController(db, CreatorId,
|
|
$$"""{ "PlayerEventId": {{eventId}}, "RoomInstanceId": {{instance.roomInstanceId}} }""");
|
|
var started = Assert.IsType<OkObjectResult>(await start.BroadcastRoomInstance());
|
|
using (var json = System.Text.Json.JsonDocument.Parse(
|
|
System.Text.Json.JsonSerializer.Serialize(started.Value)))
|
|
{
|
|
Assert.Equal(0, json.RootElement.GetProperty("Result").GetInt32());
|
|
Assert.Equal(instance.roomInstanceId,
|
|
json.RootElement.GetProperty("PlayerEvent").GetProperty("BroadcastingRoomInstanceId").GetInt64());
|
|
}
|
|
Assert.Equal(instance.roomInstanceId,
|
|
(await db.PlayerEvents.SingleAsync(x => x.PlayerEventId == eventId)).BroadcastingRoomInstanceId);
|
|
|
|
var stop = CreateController(db, CreatorId,
|
|
$$"""{ "PlayerEventId": {{eventId}}, "RoomInstanceId": null }""");
|
|
var stopped = Assert.IsType<OkObjectResult>(await stop.BroadcastRoomInstance());
|
|
using (var json = System.Text.Json.JsonDocument.Parse(
|
|
System.Text.Json.JsonSerializer.Serialize(stopped.Value)))
|
|
{
|
|
Assert.Equal(System.Text.Json.JsonValueKind.Null,
|
|
json.RootElement.GetProperty("PlayerEvent").GetProperty("BroadcastingRoomInstanceId").ValueKind);
|
|
}
|
|
Assert.Null((await db.PlayerEvents.SingleAsync(x => x.PlayerEventId == eventId)).BroadcastingRoomInstanceId);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task BroadcastRoomInstance_NonMultiInstanceEvent_AnswersNotMultiInstance()
|
|
{
|
|
var db = CreateDb();
|
|
var roomId = await SeedRoomAsync(db);
|
|
var eventId = await CreateV2EventAsync(db, roomId);
|
|
var controller = CreateController(db, CreatorId,
|
|
$$"""{ "PlayerEventId": {{eventId}}, "RoomInstanceId": 7 }""");
|
|
|
|
var result = Assert.IsType<OkObjectResult>(await controller.BroadcastRoomInstance());
|
|
|
|
using var json = System.Text.Json.JsonDocument.Parse(
|
|
System.Text.Json.JsonSerializer.Serialize(result.Value));
|
|
Assert.Equal(27, json.RootElement.GetProperty("Result").GetInt32());
|
|
Assert.Equal(System.Text.Json.JsonValueKind.Null,
|
|
json.RootElement.GetProperty("PlayerEvent").GetProperty("BroadcastingRoomInstanceId").ValueKind);
|
|
Assert.Null((await db.PlayerEvents.SingleAsync(x => x.PlayerEventId == eventId)).BroadcastingRoomInstanceId);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task BroadcastRoomInstance_NonCreator_IsForbidden()
|
|
{
|
|
var db = CreateDb();
|
|
var roomId = await SeedRoomAsync(db);
|
|
var eventId = await MakeMultiInstanceAsync(db, await CreateV2EventAsync(db, roomId));
|
|
var stranger = CreateController(db, userId: 99,
|
|
$$"""{ "PlayerEventId": {{eventId}}, "RoomInstanceId": 7 }""");
|
|
|
|
Assert.IsType<ForbidResult>(await stranger.BroadcastRoomInstance());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task BroadcastRoomInstance_RoundTripsThroughTheReadShapes()
|
|
{
|
|
// Viewers find the broadcast from the event feed, so the persisted instance has to
|
|
// survive into every read, not just the write envelope.
|
|
var db = CreateDb();
|
|
var roomId = await SeedRoomAsync(db);
|
|
var eventId = await MakeMultiInstanceAsync(db, await CreateV2EventAsync(db, roomId));
|
|
db.PlayerEvents.Single(x => x.PlayerEventId == eventId).BroadcastingRoomInstanceId = 1234;
|
|
await db.SaveChangesAsync();
|
|
|
|
var feed = await CreateController(db).GetBrowseFeed();
|
|
var feedEvent = Assert.Single(feed, e => e.PlayerEventId == eventId);
|
|
Assert.Equal(1234, feedEvent.BroadcastingRoomInstanceId);
|
|
|
|
var byId = await CreateController(db).GetEvent(eventId);
|
|
var ok = Assert.IsType<OkObjectResult>(byId.Result);
|
|
Assert.Equal(1234, Assert.IsType<PlayerEvent>(ok.Value).BroadcastingRoomInstanceId);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DeleteEventResponse_RemovesTheCallersRsvpAndRecomputesAttendees()
|
|
{
|
|
var db = CreateDb();
|
|
var roomId = await SeedRoomAsync(db);
|
|
var creator = CreateController(db, CreatorId);
|
|
var created = await CreateEventAsync(creator, roomId);
|
|
var guest = CreateController(db, userId: 43);
|
|
await guest.SetResponse(created.PlayerEventId, EventResponses.Yes);
|
|
Assert.Equal(2, (await db.PlayerEvents.SingleAsync(x => x.PlayerEventId == created.PlayerEventId)).AttendeeCount);
|
|
|
|
var delete = CreateController(db, userId: 43,
|
|
$$"""{ "PlayerEventId": {{created.PlayerEventId}} }""");
|
|
var result = Assert.IsType<OkObjectResult>(await delete.DeleteEventResponse());
|
|
|
|
using var json = System.Text.Json.JsonDocument.Parse(
|
|
System.Text.Json.JsonSerializer.Serialize(result.Value));
|
|
Assert.Equal(0, json.RootElement.GetProperty("Result").GetInt32());
|
|
Assert.False(await db.PlayerEventResponses
|
|
.AnyAsync(r => r.PlayerEventId == created.PlayerEventId && r.PlayerId == 43));
|
|
Assert.Equal(1, (await db.PlayerEvents.SingleAsync(x => x.PlayerEventId == created.PlayerEventId)).AttendeeCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void BroadcastAndDeleteResponseRoutes_MatchTheClientRouteTable()
|
|
{
|
|
// The 2023 client's route table (global-metadata literals #39422 and #39427) posts to
|
|
// api/playerevents/v1/broadcast and api/playerevents/v1/deleteResponse. Both routes
|
|
// used to 404, which is why event broadcasting never started.
|
|
static IEnumerable<(string Method, string Template)> Routes(string methodName)
|
|
=> typeof(EventsAPI).GetMethod(methodName)!
|
|
.GetCustomAttributes(typeof(Microsoft.AspNetCore.Mvc.Routing.HttpMethodAttribute), false)
|
|
.Cast<Microsoft.AspNetCore.Mvc.Routing.HttpMethodAttribute>()
|
|
.SelectMany(a => a.HttpMethods.Select(m => (m, a.Template)));
|
|
|
|
Assert.Contains(("POST", "v1/broadcast"), Routes(nameof(EventsAPI.BroadcastRoomInstance)));
|
|
Assert.Contains(("POST", "v1/deleteResponse"), Routes(nameof(EventsAPI.DeleteEventResponse)));
|
|
}
|
|
}
|