using System.Text.Json; using Microsoft.Data.SqlClient; using Microsoft.EntityFrameworkCore; using RadicadeServer.API.Notifications; using RadicadeServer.DB; using RadicadeServer.RecNet.Enums.Messages; using RadicadeServer.RecNet.Messages; using Xunit; namespace RadicadeServer.Tests; public sealed class SqlServerInviteFactAttribute : FactAttribute { public SqlServerInviteFactAttribute() { if (string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("RADICADE_SQL_TEST_CONNECTION"))) Skip = "Set RADICADE_SQL_TEST_CONNECTION to a SQL Server with permission to create a scratch database."; } } [Collection(DbFactoryCollection.Name)] public class SqlServerInvitePersistenceTests { private sealed class Factory(string connection) : IDbContextFactory { public RecDBContext CreateDbContext() => new(TestDbOptions.Create() .UseSqlServer(connection, sql => sql.EnableRetryOnFailure(3)).Options); } [SqlServerInviteFact] public async Task InvitePayloadCommitsWithSqlServerRetriesEnabled() { // Never write test invitations into the user's database or notify real players. var database = "RadicadeInviteTest_" + Guid.NewGuid().ToString("N"); var connection = new SqlConnectionStringBuilder( Environment.GetEnvironmentVariable("RADICADE_SQL_TEST_CONNECTION")); connection.InitialCatalog = "master"; await using var admin = new SqlConnection(connection.ConnectionString); await admin.OpenAsync(); await using (var create = new SqlCommand($"CREATE DATABASE [{database}]", admin)) await create.ExecuteNonQueryAsync(); var previous = Notify.DbFactory; try { connection.InitialCatalog = database; var factory = new Factory(connection.ConnectionString); await using var db = factory.CreateDbContext(); await db.Database.ExecuteSqlRawAsync(""" CREATE TABLE [Messages] ( [Id] bigint IDENTITY PRIMARY KEY, [FromPlayerId] bigint NOT NULL, [ToPlayerId] bigint NOT NULL, [SentTime] datetime2 NOT NULL, [Type] int NOT NULL, [Data] nvarchar(max) NOT NULL, [Details] nvarchar(max) NOT NULL, [RoomId] bigint NULL, [PlayerEventId] bigint NULL, [IsDismissed] bit NOT NULL); """); Notify.DbFactory = factory; var message = new Message { FromPlayerId = long.MaxValue - 1, RoomId = 45, Type = MessageType.GameInviteV2 }; await Notify.SendMessage(long.MaxValue, message, prepareWithId: stored => stored.Data = JsonSerializer.Serialize(new RoomInviteDetails { InviteId = stored.Id, Name = "SQL invite test", RoomInstanceId = 123 })); var saved = await db.Messages.AsNoTracking().SingleAsync(); Assert.True(saved.Id > 0); Assert.Equal(saved.Id, JsonSerializer.Deserialize(saved.Data)!.InviteId); Assert.Equal(123L, RoomInviteDetails.GetInstanceId(saved)); Assert.Equal(long.MaxValue, saved.ToPlayerId); } finally { Notify.DbFactory = previous; SqlConnection.ClearAllPools(); await using var drop = new SqlCommand( $"ALTER DATABASE [{database}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE; DROP DATABASE [{database}]", admin); await drop.ExecuteNonQueryAsync(); } } }