Files
Radicade/ServerTests/MessagesFanoutTests.cs
2026-10-16 23:33:13 -07:00

124 lines
4.2 KiB
C#

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;
/// <summary>
/// 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.
/// </summary>
public class MessagesFanoutTests
{
private const long SenderId = 2;
private const long FriendId = 5;
private sealed class TestDbContextFactory : IDbContextFactory<RecDBContext>
{
private readonly DbContextOptions<RecDBContext> 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<HttpPostAttribute>()
.Select(attribute => attribute.Template!)
.ToArray();
Assert.Contains("v1/sendMultiple", routes);
}
/// <summary>
/// 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.
/// </summary>
[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<long> { 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;
}
}
/// <summary>
/// A body with no usable id is refused by the name the reference uses for that case, and
/// nothing is stored or pushed.
/// </summary>
[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<long> { 0, -1 }
});
Assert.False(result.success);
Assert.Empty(await db.Messages.ToListAsync());
}
finally
{
Notify.DbFactory = previous;
}
}
}