Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 119 additions & 0 deletions ArenaService.BackOffice.Tests/ApiKeyAuthenticationHandlerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
using System.Text.Encodings.Web;
using ArenaService.BackOffice.Authentication;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Moq;
using Xunit;

namespace ArenaService.BackOffice.Tests;

public class ApiKeyAuthenticationHandlerTests
{
private const string ConfiguredKey = "configured-secret";

private static async Task<AuthenticateResult> AuthenticateAsync(
string? configuredKey,
string? providedKey,
bool sendHeader = true
)
{
var settings = new Dictionary<string, string?>();
if (configuredKey is not null)
{
settings[ApiKeyAuthenticationDefaults.ConfigurationKey] = configuredKey;
}

var configuration = new ConfigurationBuilder().AddInMemoryCollection(settings).Build();

var optionsMonitor = new Mock<IOptionsMonitor<AuthenticationSchemeOptions>>();
optionsMonitor
.Setup(o => o.Get(It.IsAny<string>()))
.Returns(new AuthenticationSchemeOptions());

var handler = new ApiKeyAuthenticationHandler(
optionsMonitor.Object,
NullLoggerFactory.Instance,
UrlEncoder.Default,
configuration
);

var context = new DefaultHttpContext();
if (sendHeader)
{
context.Request.Headers[ApiKeyAuthenticationDefaults.HeaderName] = providedKey;
}

await handler.InitializeAsync(
new AuthenticationScheme(
ApiKeyAuthenticationDefaults.AuthenticationScheme,
null,
typeof(ApiKeyAuthenticationHandler)
),
context
);

return await handler.AuthenticateAsync();
}

[Fact]
public async Task WhenApiKeyMatches_ShouldSucceed()
{
var result = await AuthenticateAsync(ConfiguredKey, ConfiguredKey);

Assert.True(result.Succeeded);
Assert.Equal(
ApiKeyAuthenticationDefaults.AuthenticationScheme,
result.Ticket!.AuthenticationScheme
);
Assert.Equal("ApiUser", result.Principal!.Identity!.Name);
}

[Fact]
public async Task WhenHeaderIsMissing_ShouldReturnNoResult()
{
var result = await AuthenticateAsync(ConfiguredKey, null, sendHeader: false);

Assert.False(result.Succeeded);
Assert.True(result.None);
}

[Theory]
[InlineData("")]
[InlineData(" ")]
public async Task WhenHeaderIsBlank_ShouldFail(string providedKey)
{
var result = await AuthenticateAsync(ConfiguredKey, providedKey);

Assert.False(result.Succeeded);
Assert.False(result.None);
}

[Fact]
public async Task WhenApiKeyDoesNotMatch_ShouldFail()
{
var result = await AuthenticateAsync(ConfiguredKey, "wrong-secret");

Assert.False(result.Succeeded);
Assert.Equal("Invalid API Key.", result.Failure!.Message);
}

[Fact]
public async Task WhenApiKeyDiffersOnlyByCase_ShouldFail()
{
var result = await AuthenticateAsync(ConfiguredKey, ConfiguredKey.ToUpperInvariant());

Assert.False(result.Succeeded);
}

[Fact]
public async Task WhenApiKeyIsNotConfigured_ShouldFail()
{
var result = await AuthenticateAsync(null, ConfiguredKey);

Assert.False(result.Succeeded);
Assert.Equal("API Key authentication is not configured.", result.Failure!.Message);
}
}
26 changes: 26 additions & 0 deletions ArenaService.BackOffice.Tests/ArenaService.BackOffice.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.9.3" />
</ItemGroup>

<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\ArenaService.BackOffice\ArenaService.BackOffice.csproj" />
</ItemGroup>

</Project>
134 changes: 134 additions & 0 deletions ArenaService.BackOffice.Tests/ConfirmTxControllerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
using ArenaService.BackOffice.Controllers;
using ArenaService.BackOffice.Models;
using ArenaService.Shared.Models;
using ArenaService.Shared.Models.BattleTicket;
using ArenaService.Shared.Models.RefreshTicket;
using ArenaService.Shared.Repositories;
using Libplanet.Crypto;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Xunit;

namespace ArenaService.BackOffice.Tests;

public class ConfirmTxControllerTests
{
private readonly Mock<IBattleRepository> _battleRepo = new();
private readonly Mock<ITicketRepository> _ticketRepo = new();
private readonly ConfirmTxController _controller;

public ConfirmTxControllerTests()
{
_controller = new ConfirmTxController(
_battleRepo.Object,
_ticketRepo.Object,
NullLogger<ConfirmTxController>.Instance
);
}

[Fact]
public async Task GetUnreviewedBattles_ShouldLowerCaseAvatarAddress()
{
var address = new PrivateKey().Address;
_battleRepo
.Setup(r => r.GetUnReviewedBattlesAsync())
.ReturnsAsync(new List<Battle> { new() { Id = 7, AvatarAddress = address, SeasonId = 1, RoundId = 2, Token = "token" } });

var ok = Assert.IsType<OkObjectResult>((await _controller.GetUnreviewedBattles()).Result);
var payload = Assert.IsType<ApiResponse<List<BattleDto>>>(ok.Value);

Assert.Equal(address.ToString().ToLower(), payload.Data![0].AvatarAddress);
}

[Fact]
public async Task GetUnreviewedTicketPurchases_ShouldMergeBothTicketTypes()
{
_ticketRepo
.Setup(r => r.GetUnReviewedBattleTicketPurchasesAsync())
.ReturnsAsync(new List<BattleTicketPurchaseLog>
{
new() { Id = 1, AvatarAddress = new PrivateKey().Address }
});
_ticketRepo
.Setup(r => r.GetUnReviewedRefreshTicketPurchasesAsync())
.ReturnsAsync(new List<RefreshTicketPurchaseLog>
{
new() { Id = 2, AvatarAddress = new PrivateKey().Address }
});

var ok = Assert.IsType<OkObjectResult>((await _controller.GetUnreviewedTicketPurchases()).Result);
var payload = Assert.IsType<ApiResponse<List<TicketPurchaseLogDto>>>(ok.Value);

Assert.Equal(2, payload.Data!.Count);
Assert.Contains(payload.Data!, d => d.TicketType == "battle");
Assert.Contains(payload.Data!, d => d.TicketType == "refresh");
}

[Fact]
public async Task MarkBattleAsReviewed_ShouldUpdateBattle()
{
_battleRepo
.Setup(r => r.UpdateBattle(3, It.IsAny<Action<Battle>>()))
.ReturnsAsync(new Battle { Id = 3, Token = "token" });

Assert.IsType<OkObjectResult>((await _controller.MarkBattleAsReviewed(3)).Result);
_battleRepo.Verify(r => r.UpdateBattle(3, It.IsAny<Action<Battle>>()), Times.Once);
}

[Theory]
[InlineData("battle")]
[InlineData("BATTLE")]
public async Task MarkTicketPurchaseAsReviewed_WithBattleType_ShouldUpdateBattleLog(string type)
{
_ticketRepo
.Setup(r => r.UpdateBattleTicketPurchaseLog(5, It.IsAny<Action<BattleTicketPurchaseLog>>()))
.ReturnsAsync(new BattleTicketPurchaseLog { Id = 5 });

Assert.IsType<OkObjectResult>((await _controller.MarkTicketPurchaseAsReviewed(5, type)).Result);
_ticketRepo.Verify(
r => r.UpdateBattleTicketPurchaseLog(5, It.IsAny<Action<BattleTicketPurchaseLog>>()),
Times.Once
);
_ticketRepo.Verify(
r => r.UpdateRefreshTicketPurchaseLog(It.IsAny<int>(), It.IsAny<Action<RefreshTicketPurchaseLog>>()),
Times.Never
);
}

[Theory]
[InlineData("refresh")]
[InlineData("Refresh")]
public async Task MarkTicketPurchaseAsReviewed_WithRefreshType_ShouldUpdateRefreshLog(string type)
{
_ticketRepo
.Setup(r => r.UpdateRefreshTicketPurchaseLog(6, It.IsAny<Action<RefreshTicketPurchaseLog>>()))
.ReturnsAsync(new RefreshTicketPurchaseLog { Id = 6 });

Assert.IsType<OkObjectResult>((await _controller.MarkTicketPurchaseAsReviewed(6, type)).Result);
_ticketRepo.Verify(
r => r.UpdateRefreshTicketPurchaseLog(6, It.IsAny<Action<RefreshTicketPurchaseLog>>()),
Times.Once
);
_ticketRepo.Verify(
r => r.UpdateBattleTicketPurchaseLog(It.IsAny<int>(), It.IsAny<Action<BattleTicketPurchaseLog>>()),
Times.Never
);
}

[Fact]
public async Task MarkTicketPurchaseAsReviewed_WithUnknownType_ShouldReturnBadRequest()
{
var result = await _controller.MarkTicketPurchaseAsReviewed(7, "foo");

Assert.IsType<BadRequestObjectResult>(result.Result);
_ticketRepo.Verify(
r => r.UpdateBattleTicketPurchaseLog(It.IsAny<int>(), It.IsAny<Action<BattleTicketPurchaseLog>>()),
Times.Never
);
_ticketRepo.Verify(
r => r.UpdateRefreshTicketPurchaseLog(It.IsAny<int>(), It.IsAny<Action<RefreshTicketPurchaseLog>>()),
Times.Never
);
}
}
109 changes: 109 additions & 0 deletions ArenaService.BackOffice.Tests/LeaderboardControllerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
using ArenaService.BackOffice.Controllers;
using ArenaService.BackOffice.Models;
using ArenaService.Client;
using ArenaService.Options;
using ArenaService.Shared.Models;
using ArenaService.Shared.Repositories;
using Libplanet.Crypto;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Moq;
using Xunit;

namespace ArenaService.BackOffice.Tests;

public class LeaderboardControllerTests
{
private readonly Mock<ILeaderboardRepository> _leaderboardRepo = new();
private readonly Mock<ISeasonRepository> _seasonRepo = new();
private readonly Mock<IHeadlessClient> _headlessClient = new();
private readonly LeaderboardController _controller;

public LeaderboardControllerTests()
{
_controller = new LeaderboardController(
_leaderboardRepo.Object,
_seasonRepo.Object,
_headlessClient.Object,
Microsoft.Extensions.Options.Options.Create(
new HeadlessOptions { Planet = "odin", HeadlessEndpoint = new Uri("http://localhost") }
),
NullLogger<LeaderboardController>.Instance
);
}

private static (Participant, int, int) CreateEntry(Address avatarAddress, Address agentAddress, int score, int rank) =>
(
new Participant
{
AvatarAddress = avatarAddress,
SeasonId = 1,
Score = score,
TotalWin = 3,
TotalLose = 2,
User = new User
{
AvatarAddress = avatarAddress,
AgentAddress = agentAddress,
Level = 200,
Cp = 12345,
PortraitId = 10,
NameWithHash = "tester#1234"
}
},
score,
rank
);

[Fact]
public async Task GetLeaderboard_ShouldLowerCaseAddresses()
{
var avatarAddress = new PrivateKey().Address;
var agentAddress = new PrivateKey().Address;
_leaderboardRepo
.Setup(r => r.GetLeaderboardAsync(1))
.ReturnsAsync(new List<(Participant, int, int)> { CreateEntry(avatarAddress, agentAddress, 1500, 1) });

var ok = Assert.IsType<OkObjectResult>((await _controller.GetLeaderboard(1)).Result);
var payload = Assert.IsType<ApiResponse<List<LeaderboardEntryDto>>>(ok.Value);

Assert.Equal(avatarAddress.ToString().ToLower(), payload.Data![0].AvatarAddress);
Assert.Equal(agentAddress.ToString().ToLower(), payload.Data![0].AgentAddress);
}

[Fact]
public async Task GetLeaderboard_ShouldProjectRankAndRecord()
{
_leaderboardRepo
.Setup(r => r.GetLeaderboardAsync(1))
.ReturnsAsync(new List<(Participant, int, int)>
{
CreateEntry(new PrivateKey().Address, new PrivateKey().Address, 1500, 1),
CreateEntry(new PrivateKey().Address, new PrivateKey().Address, 1400, 2)
});

var ok = Assert.IsType<OkObjectResult>((await _controller.GetLeaderboard(1)).Result);
var payload = Assert.IsType<ApiResponse<List<LeaderboardEntryDto>>>(ok.Value);

Assert.Equal(new[] { 1, 2 }, payload.Data!.Select(e => e.Rank));
Assert.Equal(new[] { 1500, 1400 }, payload.Data!.Select(e => e.Score));
Assert.Equal(3, payload.Data![0].TotalWin);
Assert.Equal(2, payload.Data![0].TotalLose);
}

[Fact]
public async Task GetLeaderboardCsv_ShouldNameFileWithPlanetAndBlockRange()
{
_seasonRepo
.Setup(r => r.GetSeasonAsync(1, It.IsAny<Func<IQueryable<Season>, IQueryable<Season>>>()))
.ReturnsAsync(new Season { Id = 1, SeasonGroupId = 7, StartBlock = 100, EndBlock = 200 });
_leaderboardRepo.Setup(r => r.GenerateLeaderboardCsvAsync(1)).ReturnsAsync(new byte[] { 1, 2, 3 });

var file = Assert.IsType<FileContentResult>(await _controller.GetLeaderboardCsv(1));

Assert.Equal("text/csv", file.ContentType);
Assert.Equal("odin_leaderboard_group_7_100_200.csv", file.FileDownloadName);
Assert.Equal(3, file.FileContents.Length);
}
}
Loading
Loading