using System.Security.Claims; using System.Text.Json; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using RadicadeServer.API.Compatibility; using RadicadeServer.API.Config; using RadicadeServer.API.Storefront; using RadicadeServer.DB; using RadicadeServer.RecNet.Accounts; using RadicadeServer.RecNet.Avatar; using RadicadeServer.RecNet.Enums; using RadicadeServer.RecNet.Enums.Store; using RadicadeServer.RecNet.Store; using Xunit; namespace RadicadeServer.Tests; /// /// The free-gift button and the "Star Boxes" in the Rec Center storefront. /// /// Three separate causes sat behind the gift complaints: the free-gift button config answered /// false (the client's "this feature has been temporarily disabled" screen), sendmultiple /// answered an empty array where the client reads a two-field result (so a 200 still surfaced as /// "Failed to SendFreeGift"), and a star box - a drop that names a rarity instead of an item - was /// granted empty, so buying the 2/3/4-star box did nothing at all. /// public class GiftClusterTests { private const long BuyerId = 55; private static DbContextOptions MemoryOptions() => TestDbOptions.Create() .UseInMemoryDatabase(Guid.NewGuid().ToString()) .Options; /// /// The purchase path opens its own transaction, which the in-memory provider refuses, so this /// suite uses SQLite - the same reason the registration tests do. /// /// /// The star boxes are resolved from the avatar catalogue the server ships, so the test loads the /// same files the server does. Without it the catalogue is the one-item starter fallback and no /// box could resolve anything, which would make the assertion below pass for the wrong reason. /// private static void LoadShippedCatalog() => DBConfig.LoadFiles(); private static async Task<(SqliteConnection Connection, RecDBContext Db)> OpenDbAsync() { var connection = new SqliteConnection("DataSource=:memory:"); await connection.OpenAsync(); var options = TestDbOptions.Create().UseSqlite(connection).Options; var db = new RecDBContext(options); await db.Database.EnsureCreatedAsync(); return (connection, db); } [Fact] public void TheFreeGiftButtonIsReportedAsEnabled() { using var db = new RecDBContext(MemoryOptions()); var result = Assert.IsType(new ConfigAPI(db).GetFreeGiftButtonConfig()); Assert.Equal(true, result.Value); } [Fact] public async Task SendMultipleAnswersTheClientsTwoFieldResult() { // The response DTO (RecNet.OMJLLFDFEJL) is a success flag and an error string, so even a // refusal has to come back in that shape - the empty array this route once answered could // not be read into it and surfaced as "Failed to SendFreeGift" against a 200. The send // itself (boxes, items, the daily cooldown) is covered by FriendotronTests. using var db = new RecDBContext(MemoryOptions()); var api = new CompatibilityAPI(db); var identity = new ClaimsIdentity("TestAuth"); identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, BuyerId.ToString())); api.ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal(identity) } }; var result = Assert.IsType(await api.SendFreeGifts()); using var document = JsonDocument.Parse(JsonSerializer.Serialize(result.Value)); Assert.False(document.RootElement.GetProperty("ALOCDGMHFKA").GetBoolean()); Assert.Equal("No recipients were given.", document.RootElement.GetProperty("FBILGKFKFJE").GetString()); } [Fact] public async Task SendMultipleRefusesACallWithNoResolvedAccount() { using var db = new RecDBContext(MemoryOptions()); var api = new CompatibilityAPI(db) { ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal(new ClaimsIdentity()) } } }; Assert.IsType(await api.SendFreeGifts()); } [Theory] [InlineData(541, 10)] [InlineData(538, 20)] [InlineData(539, 30)] public async Task AStarBoxGrantsARandomCatalogueItemOfThatRarity(int purchasableItemId, int expectedRarity) { LoadShippedCatalog(); var (connection, db) = await OpenDbAsync(); await using var disposeConnection = connection; await using var disposeDb = db; db.Currency.Add(new Currency { AccountId = BuyerId, RecTokens = 100_000 }); await db.SaveChangesAsync(); var outcome = await StorefrontShared.ExecutePurchaseAsync( db, BuyerId, StorefrontTypes.RecCenter, purchasableItemId, CurrencyType.RecCenterTokens, null, null); Assert.True(outcome.Success, outcome.ErrorDetail); var update = Assert.Single(outcome.Response!.BalanceUpdates); Assert.Equal(UpdateResponseTypes.OK, update.UpdateResponse); var gifted = Assert.Single(update.Data); Assert.False(gifted.IsQuery); Assert.False(string.IsNullOrWhiteSpace(gifted.FriendlyName)); Assert.Equal(expectedRarity, (int)gifted.Rarity); Assert.False(string.IsNullOrWhiteSpace(gifted.AvatarItemDesc)); // The box must hand over a real catalogued item of that rarity - not an empty drop. var catalogueItem = AvatarUnlockCatalog.FindItem(gifted.AvatarItemDesc); Assert.NotNull(catalogueItem); Assert.Equal(expectedRarity, catalogueItem!.Rarity); var row = await db.Gifts.AsNoTracking().SingleAsync(x => x.AccountId == BuyerId); Assert.Contains(StorefrontShared.ParseGiftList(row.Data), pending => pending.AvatarItemDesc == gifted.AvatarItemDesc); } }