mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-25 15:09:26 -07:00
707 lines
31 KiB
C#
707 lines
31 KiB
C#
using System.Security.Claims;
|
|
using System.Text;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.EntityFrameworkCore.Diagnostics;
|
|
using RadicadeServer.API.Avatars.V2;
|
|
using RadicadeServer.API.GameRewards.V1;
|
|
using RadicadeServer.API.Storefront;
|
|
using RadicadeServer.DB;
|
|
using RadicadeServer.Extensions;
|
|
using RadicadeServer.RecNet.Avatar;
|
|
using RadicadeServer.RecNet.Enums.Store;
|
|
using RadicadeServer.RecNet.Moderation;
|
|
using RadicadeServer.RecNet.Progression;
|
|
using RadicadeServer.RecNet.Rooms;
|
|
using RadicadeServer.RecNet.Storefront;
|
|
using RadicadeServer.Utils;
|
|
using Xunit;
|
|
|
|
namespace RadicadeServer.Tests;
|
|
|
|
/// <summary>
|
|
/// What a completion is filed as, and what the player is handed when they choose.
|
|
///
|
|
/// Three things were wrong together: the first activity of the day was not treated as a completion
|
|
/// at all, so it paid tokens; a game whose own pool could not pay was paid tokens and nothing else;
|
|
/// and completing a selection credited the item without telling the client, so the selection could
|
|
/// not be replayed and the reward only showed after a relog.
|
|
/// </summary>
|
|
public class GameRewardFlowTests
|
|
{
|
|
private const long PlayerId = 77;
|
|
|
|
private static RecDBContext NewDb() =>
|
|
new(TestDbOptions.Create()
|
|
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
|
.Options);
|
|
|
|
/// <summary>
|
|
/// The same store, with the one console warning the consume route would otherwise raise turned
|
|
/// off: it opens a transaction, which the in-memory provider ignores - and reports as an error -
|
|
/// rather than supporting. The route's own behaviour is what is under test, not that warning.
|
|
/// </summary>
|
|
private static RecDBContext NewTransactionalDb() =>
|
|
new(TestDbOptions.Create()
|
|
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
|
.ConfigureWarnings(warnings => warnings.Ignore(InMemoryEventId.TransactionIgnoredWarning))
|
|
.Options);
|
|
|
|
private static GameRewardsAPI AsPlayer(RecDBContext db, long playerId = PlayerId) => new(db)
|
|
{
|
|
ControllerContext = new ControllerContext
|
|
{
|
|
HttpContext = new DefaultHttpContext
|
|
{
|
|
User = new ClaimsPrincipal(new ClaimsIdentity(
|
|
[new Claim(ClaimTypes.NameIdentifier, playerId.ToString())], "test"))
|
|
}
|
|
}
|
|
};
|
|
|
|
/// <summary>
|
|
/// Points the player's last visited room at a game room. The activity a completion is filed under
|
|
/// is named by the room the player was in, and that is the last room the server recorded a visit
|
|
/// to - there is no other way for a test to say which game was played.
|
|
/// </summary>
|
|
private static async Task SeedLastVisitedRoomAsync(RecDBContext db, string roomName)
|
|
{
|
|
var room = new RoomData { Name = roomName };
|
|
db.Rooms.Add(room);
|
|
await db.SaveChangesAsync();
|
|
db.RoomInteractions.Add(new PlayerInteractionDB { PlayerId = PlayerId, RoomId = room.RoomId, Visited = true });
|
|
await db.SaveChangesAsync();
|
|
}
|
|
|
|
private static async Task<GameReward> RequestRewardAsync(GameRewardsAPI api, RewardType type, GiftContext? giftContext = null)
|
|
{
|
|
await RequestOnlyAsync(api, type, giftContext);
|
|
return await LatestPendingAsync(api);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Files a reward request and asserts only that it was accepted.
|
|
///
|
|
/// A quest's reward does not reach /pending: it is handed over as the run's own box with no
|
|
/// selection screen, so the tests that check what a quest pays read the player's gifts instead.
|
|
/// </summary>
|
|
private static async Task RequestOnlyAsync(GameRewardsAPI api, RewardType type, GiftContext? giftContext = null)
|
|
{
|
|
var body = Encoding.UTF8.GetBytes(giftContext.HasValue
|
|
? $"{{\"rewardType\":{(int)type},\"giftContext\":{(int)giftContext.Value}}}"
|
|
: $"{{\"rewardType\":{(int)type}}}");
|
|
api.ControllerContext.HttpContext.Request.Body = new MemoryStream(body);
|
|
api.ControllerContext.HttpContext.Request.ContentType = "application/json";
|
|
|
|
var result = await api.RequestReward();
|
|
Assert.True(result.Result is OkObjectResult, $"{(result.Result as ObjectResult)?.StatusCode.ToString() ?? "no result"}");
|
|
|
|
// The route answers the shared success object, not the reward: the client's own wrapper for it
|
|
// parses one of those, and the reward reaches the watch through the box the route hands over.
|
|
var ok = Assert.IsType<OkResponse>(((OkObjectResult)result.Result!).Value);
|
|
Assert.True(ok.Success);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Requests a quest completion and returns the box it paid.
|
|
///
|
|
/// A quest asks the player to choose nothing, so the reward never reaches /pending as a selection:
|
|
/// it is filed as the run's own box for the watch to open. These tests read that box, and the empty
|
|
/// /pending is asserted here because a quest that left a selection behind would be a quest offering
|
|
/// a choice it does not have.
|
|
/// </summary>
|
|
private static async Task<Gift> RequestAndTakeQuestBoxAsync(
|
|
GameRewardsAPI api, RecDBContext db, GiftContext claimed, long playerId = PlayerId)
|
|
{
|
|
await RequestOnlyAsync(api, RewardType.PostGameActivity, claimed);
|
|
Assert.Empty(await api.GetPendingGameRewards());
|
|
|
|
var boxes = await StorefrontShared.GetPendingGiftsAsync(db, playerId);
|
|
return Assert.Single(boxes);
|
|
}
|
|
|
|
private static async Task<GameReward> LatestPendingAsync(GameRewardsAPI api)
|
|
{
|
|
var pending = await api.GetPendingGameRewards();
|
|
Assert.NotEmpty(pending);
|
|
return pending[^1];
|
|
}
|
|
|
|
private static GameRewardsAPI AsSelecting(RecDBContext db, long selectionId, long giftDropId)
|
|
{
|
|
var api = AsPlayer(db);
|
|
var body = Encoding.UTF8.GetBytes(
|
|
$"{{\"rewardSelectionId\":{selectionId},\"giftDropId\":{giftDropId}}}");
|
|
api.ControllerContext.HttpContext.Request.Body = new MemoryStream(body);
|
|
api.ControllerContext.HttpContext.Request.ContentType = "application/json";
|
|
return api;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------
|
|
// Which type a completion is filed under
|
|
// ---------------------------------------------------------------------------------
|
|
|
|
private static readonly DateTime Monday = new(2026, 10, 5, 12, 0, 0, DateTimeKind.Utc);
|
|
|
|
[Fact]
|
|
public void OnlyTheFirstCompletionOfTheDayIsTheFirstActivityOfTheDay()
|
|
{
|
|
var ledger = RewardDay.NewDay(Monday);
|
|
|
|
// The client asks for FirstActivityOfDay on every completion. Honouring that claim every time
|
|
// is what printed "First Game of the Day" on every reward all day.
|
|
Assert.Equal(RewardType.FirstActivityOfDay, RewardDay.CompletionType(ledger, RewardType.FirstActivityOfDay));
|
|
Assert.True(RewardDay.TryTakeFirstActivity(ledger));
|
|
Assert.Equal(RewardType.PostGameActivity, RewardDay.CompletionType(ledger, RewardType.FirstActivityOfDay));
|
|
Assert.False(RewardDay.TryTakeFirstActivity(ledger));
|
|
|
|
// The first box of the day is its own: it does not eat into the twelve the games pay.
|
|
Assert.Equal(0, ledger.ActivityBoxes);
|
|
}
|
|
|
|
[Fact]
|
|
public void AFirstCompletionIsTheFirstActivityOfTheDayEvenWhenTheClientCallsItAPostGameReward()
|
|
{
|
|
var ledger = RewardDay.NewDay(Monday);
|
|
|
|
Assert.Equal(RewardType.FirstActivityOfDay, RewardDay.CompletionType(ledger, RewardType.PostGameActivity));
|
|
RewardDay.TryTakeFirstActivity(ledger);
|
|
Assert.Equal(RewardType.PostGameActivity, RewardDay.CompletionType(ledger, RewardType.PostGameActivity));
|
|
}
|
|
|
|
[Fact]
|
|
public void AQuestKeepsItsOwnTypeAndStillTakesTheDay()
|
|
{
|
|
var ledger = RewardDay.NewDay(Monday);
|
|
|
|
Assert.Equal(RewardType.PostQuestActivity, RewardDay.CompletionType(ledger, RewardType.PostQuestActivity));
|
|
RewardDay.TryTakeFirstActivity(ledger);
|
|
// The quest was the day's activity, so the game played after it is an ordinary game reward.
|
|
Assert.Equal(RewardType.PostGameActivity, RewardDay.CompletionType(ledger, RewardType.FirstActivityOfDay));
|
|
}
|
|
|
|
/// <summary>
|
|
/// The budget: one first-of-the-day box, twelve game boxes, and a daily-challenge box of its own.
|
|
/// </summary>
|
|
[Fact]
|
|
public void TheDayPaysFourteenBoxesAndNoMore()
|
|
{
|
|
var ledger = RewardDay.NewDay(Monday);
|
|
|
|
Assert.True(RewardDay.TryTakeFirstActivity(ledger));
|
|
Assert.False(RewardDay.TryTakeFirstActivity(ledger));
|
|
|
|
for (var i = 0; i < RewardDay.ActivityBoxesPerDay; i++)
|
|
Assert.True(RewardDay.TryTakeActivityBox(ledger), $"activity box {i + 1} was refused");
|
|
Assert.False(RewardDay.TryTakeActivityBox(ledger));
|
|
|
|
Assert.True(RewardDay.TryTakeDailyChallengeBox(ledger));
|
|
Assert.False(RewardDay.TryTakeDailyChallengeBox(ledger));
|
|
|
|
Assert.Equal(14, RewardDay.BoxesPerDay);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The ledger is the player's for the day, so a day rolls over - and a second day starts its own
|
|
/// count rather than inheriting yesterday's.
|
|
/// </summary>
|
|
[Fact]
|
|
public void ANewDayStartsANewLedgerAndTheSameDayDoesNot()
|
|
{
|
|
var json = new ProgressionJson();
|
|
|
|
var monday = RewardDay.For(json, Monday);
|
|
Assert.True(RewardDay.TryTakeFirstActivity(monday));
|
|
RewardDay.TryTakeActivityBox(monday);
|
|
|
|
// The same day is the same ledger, whatever moment it is asked at.
|
|
Assert.Same(monday, RewardDay.For(json, Monday.AddHours(6)));
|
|
Assert.True(monday.FirstActivityPaid);
|
|
Assert.Equal(1, monday.ActivityBoxes);
|
|
|
|
// A new day is a new count.
|
|
var tuesday = RewardDay.For(json, Monday.AddDays(1));
|
|
Assert.NotSame(monday, tuesday);
|
|
Assert.False(tuesday.FirstActivityPaid);
|
|
Assert.Equal(0, tuesday.ActivityBoxes);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------
|
|
// What a completion is filed as
|
|
// ---------------------------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public async Task TheFirstGameOfTheDayIsFiledAsOneAndTheSecondIsNot()
|
|
{
|
|
await using var db = NewDb();
|
|
await SeedLastVisitedRoomAsync(db, "RecRoyaleSolos");
|
|
var api = AsPlayer(db);
|
|
|
|
var first = await RequestRewardAsync(api, RewardType.FirstActivityOfDay);
|
|
var second = await RequestRewardAsync(api, RewardType.FirstActivityOfDay);
|
|
|
|
Assert.Equal(RewardType.FirstActivityOfDay, first.RewardType);
|
|
Assert.Equal(RewardType.PostGameActivity, second.RewardType);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ARewardThatIsNotACompletionStillPaysTokens()
|
|
{
|
|
await using var db = NewDb();
|
|
var api = AsPlayer(db);
|
|
|
|
var reward = await RequestRewardAsync(api, RewardType.Levelup);
|
|
|
|
Assert.Equal(RewardType.Levelup, reward.RewardType);
|
|
Assert.All(new[] { reward.GiftDrop1, reward.GiftDrop2, reward.GiftDrop3 },
|
|
box => Assert.Equal(CurrencyType.RecCenterTokens, box.CurrencyType));
|
|
// The boxes are titled with the amount they hold rather than all being "Rec Center Tokens",
|
|
// which made a 100 and a 300 read as the same reward twice.
|
|
Assert.Equal("100 Tokens", reward.GiftDrop1.FriendlyName);
|
|
Assert.Equal("200 Tokens", reward.GiftDrop2.FriendlyName);
|
|
Assert.Equal("300 Tokens", reward.GiftDrop3.FriendlyName);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------
|
|
// The day's boxes: one first of the day, twelve games, one daily challenge
|
|
// ---------------------------------------------------------------------------------
|
|
|
|
/// <summary>
|
|
/// A completion that pays no box, because the day's are spent. The route still answers success:
|
|
/// the client asked for a game reward and the request was accepted, the player is simply not
|
|
/// owed another box today.
|
|
/// </summary>
|
|
private static async Task RequestRewardWithNoBoxAsync(GameRewardsAPI api, RewardType type)
|
|
{
|
|
var before = (await api.GetPendingGameRewards()).Count;
|
|
var body = Encoding.UTF8.GetBytes($"{{\"rewardType\":{(int)type}}}");
|
|
api.ControllerContext.HttpContext.Request.Body = new MemoryStream(body);
|
|
api.ControllerContext.HttpContext.Request.ContentType = "application/json";
|
|
|
|
var result = await api.RequestReward();
|
|
Assert.True(Assert.IsType<OkResponse>(((OkObjectResult)result.Result!).Value).Success);
|
|
Assert.Equal(before, (await api.GetPendingGameRewards()).Count);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TheDayStopsPayingAfterItsFirstBoxAndTwelveGames()
|
|
{
|
|
await using var db = NewDb();
|
|
await SeedLastVisitedRoomAsync(db, "RecRoyaleSolos");
|
|
var api = AsPlayer(db);
|
|
|
|
var first = await RequestRewardAsync(api, RewardType.FirstActivityOfDay);
|
|
Assert.Equal(RewardType.FirstActivityOfDay, first.RewardType);
|
|
|
|
for (var game = 0; game < RewardDay.ActivityBoxesPerDay; game++)
|
|
{
|
|
var box = await RequestRewardAsync(api, RewardType.FirstActivityOfDay);
|
|
Assert.Equal(RewardType.PostGameActivity, box.RewardType);
|
|
}
|
|
|
|
// Thirteen boxes: the first of the day and the twelve games.
|
|
Assert.Equal(RewardDay.BoxesPerDay - 1, (await api.GetPendingGameRewards()).Count);
|
|
|
|
// The fourteenth completion of the day is answered, and pays nothing.
|
|
await RequestRewardWithNoBoxAsync(api, RewardType.FirstActivityOfDay);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TheFirstGameOfTheDayIsNotPaidAgainAfterARestart()
|
|
{
|
|
await using var db = NewDb();
|
|
await SeedLastVisitedRoomAsync(db, "RecRoyaleSolos");
|
|
|
|
// A second API over the same progression is what a server restart looks like from here. The
|
|
// day used to be counted in memory, so the first completion after a restart was the first of
|
|
// the day again and the player was handed that box a second time.
|
|
var beforeRestart = await RequestRewardAsync(AsPlayer(db), RewardType.FirstActivityOfDay);
|
|
Assert.Equal(RewardType.FirstActivityOfDay, beforeRestart.RewardType);
|
|
Assert.Equal("First Game of the Day", beforeRestart.Message);
|
|
|
|
var afterRestart = await RequestRewardAsync(AsPlayer(db), RewardType.FirstActivityOfDay);
|
|
Assert.Equal(RewardType.PostGameActivity, afterRestart.RewardType);
|
|
|
|
// And the title is the server's. The client sends "First Game of the Day" as the message on
|
|
// every completion, which is why the screen still read that on the day's fifth box.
|
|
Assert.Equal("Game Reward", afterRestart.Message);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TheDailyChallengePaysOneBoxADay()
|
|
{
|
|
await using var db = NewDb();
|
|
var api = AsPlayer(db);
|
|
|
|
var reward = await RequestRewardAsync(api, RewardType.DailyChallengesComplete);
|
|
|
|
Assert.Equal(RewardType.DailyChallengesComplete, reward.RewardType);
|
|
// The daily challenge is a box of its own, filed under the context the client's own enum
|
|
// gives it: it is not a game and it names no room.
|
|
Assert.Equal(GiftContext.Daily_Challenge_Complete, reward.GiftContext);
|
|
|
|
// And one a day - the same completion reported again pays nothing.
|
|
await RequestRewardWithNoBoxAsync(api, RewardType.DailyChallengesComplete);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------
|
|
// Which activity the reward is answered from
|
|
// ---------------------------------------------------------------------------------
|
|
|
|
/// <summary>
|
|
/// The reward request arrives after the player has left the game: a finished Stunt Runner run's
|
|
/// request came in with the player already back in the Rec Center, so the last visited room was
|
|
/// the Rec Center, which pays nothing, and every completion was paid the token package. The
|
|
/// context the client claims is what survives the player moving on, and it is what decides the
|
|
/// activity when the room they last visited does not pay it.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task TheClaimedContextDecidesTheActivityWhenTheLastRoomIsNotOne()
|
|
{
|
|
await using var db = NewDb();
|
|
await SeedLastVisitedRoomAsync(db, "RecCenter");
|
|
var api = AsPlayer(db);
|
|
|
|
var box = await RequestAndTakeQuestBoxAsync(api, db, GiftContext.StuntRunner_TheMainEvent_C);
|
|
|
|
// Before the claim was consulted the Rec Center named the activity, which is none, and the
|
|
// reward was the token package filed under GameRewards_Tokens. A quest asks the player to
|
|
// choose nothing, so what the completion pays is the run's own box - not a selection.
|
|
Assert.Equal(GiftContext.StuntRunner_TheMainEvent_C, box.Context);
|
|
Assert.NotEqual(GiftContext.GameRewards_Tokens, box.Context);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ALasertagRewardIsPaidTicketsEvenWhenTheLastRoomIsNotOne()
|
|
{
|
|
await using var db = NewDb();
|
|
await SeedLastVisitedRoomAsync(db, "RecCenter");
|
|
var api = AsPlayer(db);
|
|
|
|
// Tickets are the one reward that needs no catalogue to tell apart from the token package,
|
|
// so this is what shows the claim took over the room without depending on the shipped items.
|
|
var reward = await RequestRewardAsync(api, RewardType.PostGameActivity, GiftContext.Lasertag);
|
|
|
|
Assert.All(new[] { reward.GiftDrop1, reward.GiftDrop2, reward.GiftDrop3 },
|
|
box => Assert.Equal(CurrencyType.LaserTagTickets, box.CurrencyType));
|
|
Assert.All(new[] { reward.GiftDrop1, reward.GiftDrop2, reward.GiftDrop3 },
|
|
box => Assert.Equal("Laser Tag Tickets", box.FriendlyName));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AClaimTheServerDoesNotKnowStillLeavesTheRoomInCharge()
|
|
{
|
|
await using var db = NewDb();
|
|
await SeedLastVisitedRoomAsync(db, "RecCenter");
|
|
var api = AsPlayer(db);
|
|
|
|
var reward = await RequestRewardAsync(api, RewardType.PostGameActivity, GiftContext.Default);
|
|
|
|
// A claim that names no activity leaves the room in charge, and the room pays nothing, so the
|
|
// reward is the token package - filed under the claim it was asked for, which names no game.
|
|
Assert.Null(RroRewardPools.RoomForContext(reward.GiftContext));
|
|
Assert.All(new[] { reward.GiftDrop1, reward.GiftDrop2, reward.GiftDrop3 },
|
|
box => Assert.Equal(CurrencyType.RecCenterTokens, box.CurrencyType));
|
|
// Each box is titled with its own amount, not all with "Rec Center Tokens".
|
|
Assert.Equal("50 Tokens", reward.GiftDrop1.FriendlyName);
|
|
Assert.Equal("100 Tokens", reward.GiftDrop2.FriendlyName);
|
|
Assert.Equal("150 Tokens", reward.GiftDrop3.FriendlyName);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The quest's item is what the whole collection flow leaves in the player's inventory.
|
|
///
|
|
/// This is the half that was broken. The selection was right and so was the box it held, and then
|
|
/// the item became a box of Rec Center tokens at the moment it was collected, because the collect
|
|
/// - a generate naming the reward's context, exactly as the client sends it - asked the server to
|
|
/// invent a gift instead of handing over the one the reward was holding. The last assertion is the
|
|
/// one that matters: the quest's own descriptor and name, in the player's own unlocks.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task AQuestsItemIsWhatTheGenerateAndConsumeFlowLeavesInTheInventory()
|
|
{
|
|
await using var db = NewTransactionalDb();
|
|
|
|
var item = new Gift
|
|
{
|
|
GiftDropId = 6104,
|
|
FriendlyName = "Runner Harness (Green)",
|
|
Tooltip = "Stunt Runner: The Main Event",
|
|
AvatarItemDesc = "d2d3264b-fbf6-40b5-9f10-c85ec737d112,1ba65dd5-9df9-4ff3-be96-f83a7abf67b3",
|
|
CurrencyType = CurrencyType.Invalid,
|
|
Currency = 0,
|
|
Rarity = Rarity.Uncommon,
|
|
Context = GiftContext.StuntRunner_TheMainEvent_C
|
|
};
|
|
var reward = new GameReward
|
|
{
|
|
RewardSelectionId = 8877,
|
|
Message = "Stunt Runner: The Main Event",
|
|
GiftContext = GiftContext.StuntRunner_TheMainEvent_C,
|
|
RewardType = RewardType.PostQuestActivity,
|
|
GiftDrop1 = item,
|
|
GiftDrop2 = item,
|
|
GiftDrop3 = item,
|
|
CreatedAt = DateTime.UtcNow
|
|
};
|
|
var json = new ProgressionJson { PendingGameRewards = new List<GameReward> { reward } };
|
|
db.Progression.Add(new ProgressionData { PlayerId = PlayerId, Progress = json.ToJson() });
|
|
await db.SaveChangesAsync();
|
|
|
|
// The collect the client sends: a generate naming the reward's context, then a consume of the
|
|
// box that answered it. Neither names an item; both have to end up naming this one.
|
|
var generated = Assert.IsType<Gift>(Assert.IsType<OkObjectResult>(
|
|
await AsAvatarV2(db, $"MFPELEJLJLD={(int)GiftContext.StuntRunner_TheMainEvent_C}&DPAJDIEBNKM=Stunt+Runner")
|
|
.GenerateGift()).Value);
|
|
|
|
Assert.Equal("Runner Harness (Green)", generated.FriendlyName);
|
|
Assert.Equal(item.AvatarItemDesc, generated.AvatarItemDesc);
|
|
Assert.Equal(CurrencyType.Invalid, generated.CurrencyType);
|
|
Assert.Equal(0, generated.Currency);
|
|
|
|
var consume = AsAvatarV2(db, $"{{\"giftDropId\":{generated.GiftDropId}}}");
|
|
Assert.IsType<OkObjectResult>(await consume.ConsumeGiftBody());
|
|
|
|
var unlocks = await db.Unlocks.AsNoTracking().FirstOrDefaultAsync(x => x.AccountId == PlayerId);
|
|
var unlocked = Assert.Single(unlocks!.GetAvatarItems(), x => x.AvatarItemDesc == item.AvatarItemDesc);
|
|
Assert.Equal("Runner Harness (Green)", unlocked.FriendlyName);
|
|
}
|
|
|
|
private static AvatarV2 AsAvatarV2(RecDBContext db, string body)
|
|
{
|
|
var api = new AvatarV2(db)
|
|
{
|
|
ControllerContext = new ControllerContext
|
|
{
|
|
HttpContext = new DefaultHttpContext
|
|
{
|
|
User = new ClaimsPrincipal(new ClaimsIdentity(
|
|
[new Claim(ClaimTypes.NameIdentifier, PlayerId.ToString())], "test"))
|
|
}
|
|
}
|
|
};
|
|
api.ControllerContext.HttpContext.Request.ContentType = "application/json";
|
|
api.ControllerContext.HttpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(body));
|
|
api.ControllerContext.HttpContext.Request.ContentLength = api.ControllerContext.HttpContext.Request.Body.Length;
|
|
return api;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Records a room visit at a point in time, which is what says which game was played and when.
|
|
/// </summary>
|
|
private static async Task SeedVisitAsync(RecDBContext db, string roomName, DateTime visitedAt)
|
|
{
|
|
var room = await db.Rooms.FirstOrDefaultAsync(x => x.Name == roomName);
|
|
if (room == null)
|
|
{
|
|
room = new RoomData { Name = roomName };
|
|
db.Rooms.Add(room);
|
|
await db.SaveChangesAsync();
|
|
}
|
|
|
|
db.RoomInteractions.Add(new PlayerInteractionDB
|
|
{
|
|
PlayerId = PlayerId,
|
|
RoomId = room.RoomId,
|
|
Visited = true,
|
|
LastVisted = visitedAt,
|
|
});
|
|
await db.SaveChangesAsync();
|
|
}
|
|
|
|
/// <summary>
|
|
/// What the log showed: the reward request for a finished game arrives with the player already
|
|
/// back in the Rec Center and a body that names no activity at all. The room they walked into is
|
|
/// the newest visit and pays nothing, so the activity has to come from the newest visit that
|
|
/// does - the game they were actually placed into.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task ACompletionThatNamesNoActivityIsAnsweredFromTheNewestActivityThePlayerWasPlacedInto()
|
|
{
|
|
await using var db = NewDb();
|
|
var now = DateTime.Now;
|
|
await SeedVisitAsync(db, "Dodgeball", now.AddMinutes(-5));
|
|
await SeedVisitAsync(db, "RecCenter", now.AddSeconds(-5));
|
|
var api = AsPlayer(db);
|
|
|
|
var reward = await RequestRewardAsync(api, RewardType.PostGameActivity);
|
|
|
|
// Before the room history was consulted the Rec Center named the activity, which is none,
|
|
// and the reward was the token package filed under GameRewards_Tokens.
|
|
Assert.Equal(GiftContext.Dodgeball, reward.GiftContext);
|
|
Assert.NotEqual(GiftContext.GameRewards_Tokens, reward.GiftContext);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The claim outranks the room history: a client that names the activity is believed, so the game
|
|
/// played most recently does not answer for the one the completion belongs to.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task TheClaimedActivityOutranksTheNewestOneThePlayerWasPlacedInto()
|
|
{
|
|
await using var db = NewDb();
|
|
var now = DateTime.Now;
|
|
await SeedVisitAsync(db, "Dodgeball", now.AddMinutes(-30));
|
|
await SeedVisitAsync(db, "RecCenter", now.AddSeconds(-5));
|
|
var api = AsPlayer(db);
|
|
|
|
var box = await RequestAndTakeQuestBoxAsync(api, db, GiftContext.StuntRunner_TheMainEvent_C);
|
|
|
|
// The quest the claim names, not the game played most recently: a Stunt Runner run's box is
|
|
// filed under Stunt Runner even though a Dodgeball game is what the room history remembers.
|
|
Assert.Equal(GiftContext.StuntRunner_TheMainEvent_C, box.Context);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Only recent visits count: a game played hours ago must not answer for a game the server cannot
|
|
/// place, or yesterday's activity pays for today's reward.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task AStaleActivityDoesNotAnswerForACompletionThatNamesNothing()
|
|
{
|
|
await using var db = NewDb();
|
|
var now = DateTime.Now;
|
|
await SeedVisitAsync(db, "Dodgeball", now.AddHours(-8));
|
|
await SeedVisitAsync(db, "RecCenter", now.AddSeconds(-5));
|
|
var api = AsPlayer(db);
|
|
|
|
var reward = await RequestRewardAsync(api, RewardType.PostGameActivity);
|
|
|
|
Assert.Equal(GiftContext.GameRewards_Tokens, reward.GiftContext);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------
|
|
// What is not a completion
|
|
// ---------------------------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public async Task ALevelUpRequestIsNotPaidTheGameCompletedXp()
|
|
{
|
|
await using var db = NewDb();
|
|
// A player of its own, so this account starts with no XP.
|
|
var api = AsPlayer(db, 9201);
|
|
|
|
var reward = await RequestRewardAsync(api, RewardType.Levelup);
|
|
|
|
Assert.Equal(RewardType.Levelup, reward.RewardType);
|
|
var progression = await db.Progression.FirstOrDefaultAsync(x => x.PlayerId == 9201);
|
|
Assert.Equal(0, progression?.XP ?? 0);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ACompletionIsPaidTheGameCompletedXp()
|
|
{
|
|
await using var db = NewDb();
|
|
var api = AsPlayer(db, 9202);
|
|
|
|
// No room visited, so the reward is the token package - and it is still a completion, so it
|
|
// is still paid the game-completed XP.
|
|
await RequestRewardAsync(api, RewardType.PostGameActivity);
|
|
|
|
var progression = await db.Progression.FirstOrDefaultAsync(x => x.PlayerId == 9202);
|
|
Assert.Equal(ProgressionAwardService.XpPerGameCompleted, progression?.XP ?? 0);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ALevelUpPackageIsFiledUnderItsOwnContext()
|
|
{
|
|
await using var db = NewDb();
|
|
var api = AsPlayer(db, 9203);
|
|
|
|
// A bonus is not a game: the game context the client sent with it does not become the context
|
|
// of the boxes, which is what the client paints and groups a gift by.
|
|
var reward = await RequestRewardAsync(api, RewardType.Levelup, GiftContext.StuntRunner_TheMainEvent_C);
|
|
|
|
Assert.Equal(GiftContext.LevelUp, reward.GiftContext);
|
|
Assert.All(new[] { reward.GiftDrop1, reward.GiftDrop2, reward.GiftDrop3 },
|
|
box => Assert.Equal(GiftContext.LevelUp, box.Context));
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------
|
|
// Choosing, and choosing again
|
|
// ---------------------------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public async Task ChoosingHandsOverAGiftBoxAndRetryingIsNotAFailure()
|
|
{
|
|
await using var db = NewDb();
|
|
const long selectionId = 280843441;
|
|
const long itemDropId = 2045372488;
|
|
var item = new Gift
|
|
{
|
|
GiftDropId = itemDropId,
|
|
FriendlyName = "Archer Hat",
|
|
AvatarItemDesc = "Archer Hat (Tan)",
|
|
Context = GiftContext.Quest_Goblin_C,
|
|
CurrencyType = CurrencyType.Invalid
|
|
};
|
|
db.Progression.Add(new ProgressionData
|
|
{
|
|
PlayerId = PlayerId,
|
|
Progress = new ProgressionJson
|
|
{
|
|
PendingGameRewards =
|
|
[
|
|
new GameReward
|
|
{
|
|
RewardSelectionId = selectionId,
|
|
RewardType = RewardType.PostGameActivity,
|
|
GiftContext = GiftContext.Quest_Goblin_C,
|
|
GiftDrop1 = item,
|
|
GiftDrop2 = item,
|
|
GiftDrop3 = item
|
|
}
|
|
]
|
|
}.ToJson()
|
|
});
|
|
await db.SaveChangesAsync();
|
|
|
|
var api = AsSelecting(db, selectionId, itemDropId);
|
|
var first = await api.SelectReward();
|
|
Assert.True(Assert.IsType<OkResponse>(((OkObjectResult)first.Result!).Value).Success);
|
|
|
|
// Handed over, not credited here: what the player opens is the box, and the drop keeps the id
|
|
// the client already knows because that is the id its own consume call names.
|
|
var boxes = await StorefrontShared.GetPendingGiftsAsync(db, PlayerId);
|
|
var box = Assert.Single(boxes);
|
|
Assert.Equal(itemDropId, box.GiftDropId);
|
|
Assert.Equal("Archer Hat (Tan)", box.AvatarItemDesc);
|
|
Assert.Empty(await api.GetPendingGameRewards());
|
|
|
|
// The retry the client makes after a slow answer: the same success, and no second box.
|
|
var retryApi = AsSelecting(db, selectionId, itemDropId);
|
|
var retry = await retryApi.SelectReward();
|
|
Assert.True(Assert.IsType<OkResponse>(((OkObjectResult)retry.Result!).Value).Success);
|
|
Assert.Single(await StorefrontShared.GetPendingGiftsAsync(db, PlayerId));
|
|
|
|
var persisted = (await db.Progression.SingleAsync()).GetProgressionJson()!;
|
|
var completed = Assert.Single(persisted.PendingGameRewards);
|
|
Assert.True(completed.Consumed);
|
|
Assert.Equal(itemDropId, completed.ConsumedGiftDropId);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AnUnrecognisedBoxIsStillTheRewardAndSaysSo()
|
|
{
|
|
await using var db = NewDb();
|
|
const long selectionId = 99;
|
|
var item = new Gift { GiftDropId = 5, FriendlyName = "Knight Cape", AvatarItemDesc = "Knight Cape (Blue)" };
|
|
db.Progression.Add(new ProgressionData
|
|
{
|
|
PlayerId = PlayerId,
|
|
Progress = new ProgressionJson
|
|
{
|
|
PendingGameRewards = [new GameReward { RewardSelectionId = selectionId, GiftDrop1 = item }]
|
|
}.ToJson()
|
|
});
|
|
await db.SaveChangesAsync();
|
|
|
|
var api = AsSelecting(db, selectionId, giftDropId: 123456);
|
|
var result = await api.SelectReward();
|
|
|
|
Assert.True(Assert.IsType<OkResponse>(((OkObjectResult)result.Result!).Value).Success);
|
|
Assert.Equal(5, Assert.Single(await StorefrontShared.GetPendingGiftsAsync(db, PlayerId)).GiftDropId);
|
|
}
|
|
}
|