using System.Reflection; using System.Security.Claims; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using RadicadeServer.API.Messages; using RadicadeServer.API.Notifications; using RadicadeServer.DB; using RadicadeServer.RecNet.Enums.Messages; using RadicadeServer.RecNet.Messages; using RadicadeServer.RecNet.Responses; using Xunit; namespace RadicadeServer.Tests; /// /// The message fan-out the September client posts as `POST /api/messages/v1/sendMultiple` when /// one message goes to several people. The 2026-10-03 client run showed the other account's /// client reading a 404 here while an invite was being sorted out - which is what a missing /// route looks like from the client side - so the route is pinned by shape and behaviour now. /// public class MessagesFanoutTests { private const long SenderId = 2; private const long FriendId = 5; private sealed class TestDbContextFactory : IDbContextFactory { private readonly DbContextOptions options = TestDbOptions.Create() .UseInMemoryDatabase(Guid.NewGuid().ToString()) .Options; public RecDBContext CreateDbContext() => new(options); } private static void SetUser(Controller controller, long userId) { var identity = new ClaimsIdentity("TestAuth"); identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, userId.ToString())); controller.ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal(identity) } }; } [Fact] public void SendMultipleIsServedUnderItsClientSpelling() { var routes = typeof(MessagesAPI).GetMethod(nameof(MessagesAPI.SendMultipleMessages))! .GetCustomAttributes() .Select(attribute => attribute.Template!) .ToArray(); Assert.Contains("v1/sendMultiple", routes); } /// /// One stored row per unique recipient - the same MessageReceived push and v2/get inbox row /// the single send produces - with the sender from the token, the type passed through /// unmapped and the data carried as sent. /// [Fact] public async Task SendMultipleStoresOneRowPerUniqueRecipient() { var factory = new TestDbContextFactory(); var previous = Notify.DbFactory; Notify.DbFactory = factory; try { using var db = factory.CreateDbContext(); var api = new MessagesAPI(db); SetUser(api, SenderId); var result = await api.SendMultipleMessages(new SendMultipleMessagesRequestDTO { ToPlayerIds = new List { FriendId, FriendId, 0, -3 }, Type = MessageType.FriendStatusOnline, Data = "hello" }); Assert.True(result.success); var stored = Assert.Single(await db.Messages.ToListAsync()); Assert.Equal(FriendId, stored.ToPlayerId); Assert.Equal(SenderId, stored.FromPlayerId); Assert.Equal(MessageType.FriendStatusOnline, stored.Type); Assert.Equal("hello", stored.Data); } finally { Notify.DbFactory = previous; } } /// /// A body with no usable id is refused by the name the reference uses for that case, and /// nothing is stored or pushed. /// [Fact] public async Task SendMultipleWithoutAUsableRecipientStoresNothing() { var factory = new TestDbContextFactory(); var previous = Notify.DbFactory; Notify.DbFactory = factory; try { using var db = factory.CreateDbContext(); var api = new MessagesAPI(db); SetUser(api, SenderId); var result = await api.SendMultipleMessages(new SendMultipleMessagesRequestDTO { ToPlayerIds = new List { 0, -1 } }); Assert.False(result.success); Assert.Empty(await db.Messages.ToListAsync()); } finally { Notify.DbFactory = previous; } } }