Skip to content

Commit

Permalink
Refactoring after review - v1
Browse files Browse the repository at this point in the history
  • Loading branch information
BogdanYarotsky committed Aug 21, 2023
1 parent cc7a0af commit 04e2944
Show file tree
Hide file tree
Showing 18 changed files with 211 additions and 127 deletions.
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
namespace Gambling.API.Tests;

[TestClass]
public class GamblingTests
public class BetHttpHandlerTests
{
[TestInitialize]
public void Init()
{

}


[TestMethod]
public void WebApplicationIsCreatedWithoutExceptions()
{
Expand All @@ -15,14 +22,14 @@ public void GamblingServiceResolvedFromServiceProvider()
{
var serviceProvider = new ServiceCollection().AddGamblingServices().BuildServiceProvider(true);
using var scope = serviceProvider.CreateAsyncScope();
var service = scope.ServiceProvider.GetService<GamblingService>();
var service = scope.ServiceProvider.GetService<BetHttpHandler>();
Assert.IsNotNull(service);
}

[TestMethod]
public async Task BetLessThanZeroNotAllowed()
{
var request = new BetRequest(-5, 0);
var request = new BetHttpRequest(-5, 0);
var response = await MakeMockBetAsync(request);
Assert.IsNotNull(response);
var problemResponse = (ProblemHttpResult) response;
Expand All @@ -32,7 +39,7 @@ public async Task BetLessThanZeroNotAllowed()
[TestMethod]
public async Task BettingOnNumberLessThanZeroNotAllowed()
{
var request = new BetRequest(0, -5);
var request = new BetHttpRequest(0, -5);
var response = await MakeMockBetAsync(request);
var problemResponse = (ProblemHttpResult)response;
Assert.AreEqual(StatusCodes.Status400BadRequest, problemResponse.StatusCode);
Expand All @@ -41,7 +48,7 @@ public async Task BettingOnNumberLessThanZeroNotAllowed()
[TestMethod]
public async Task BettingOnNumberBiggerThanNineNotAllowed()
{
var request = new BetRequest(0, 10);
var request = new BetHttpRequest(0, 10);
var response = await MakeMockBetAsync(request);
var problemResponse = (ProblemHttpResult)response;
Assert.AreEqual(StatusCodes.Status400BadRequest, problemResponse.StatusCode);
Expand All @@ -50,24 +57,24 @@ public async Task BettingOnNumberBiggerThanNineNotAllowed()
[TestMethod]
public async Task EmptyBetReturnsOkResponse()
{
var request = new BetRequest(0, 0);
var request = new BetHttpRequest(0, 0);
var response = await MakeMockBetAsync(request);
var okResponse = (Ok<BetResponse>)response;
var okResponse = (Ok<BetHttpResponse>)response;
Assert.AreEqual(StatusCodes.Status200OK, okResponse.StatusCode);
}

[TestMethod]
public async Task UserHas10KPointsToStartWith()
{
var request = new BetRequest(0, 0);
var request = new BetHttpRequest(0, 0);
var response = await MakeMockBetAsyncUnwrap(request);
Assert.AreEqual(10_000, response.Account);
}

[TestMethod]
public async Task ManyUsersStartWith10K()
{
var bet = new BetRequest(0, 0);
var bet = new BetHttpRequest(0, 0);
var aliceResponse = await MakeMockBetAsyncUnwrap(bet, "Alice");
var bobResponse = await MakeMockBetAsyncUnwrap(bet, "Bob");
var charlieResponse = await MakeMockBetAsyncUnwrap(bet, "Charlie");
Expand All @@ -79,7 +86,7 @@ public async Task ManyUsersStartWith10K()
[TestMethod]
public async Task WinGives9TimesTheBetAsReward()
{
var bet = new BetRequest(200, 3);
var bet = new BetHttpRequest(200, 3);
var response = await MakeWinningBetAsync(bet, "Alice");
var expectedWin = bet.Points * 9;
Assert.AreEqual(10_000 + expectedWin, response.Account);
Expand All @@ -90,7 +97,7 @@ public async Task WinGives9TimesTheBetAsReward()
[TestMethod]
public async Task LosingDeductsPointsFromAccount()
{
var bet = new BetRequest(200, 3);
var bet = new BetHttpRequest(200, 3);
var response = await MakeLosingBetAsync(bet, "Henry");
Assert.AreEqual(10_000 - bet.Points, response.Account);
Assert.AreEqual($"-{bet.Points}", response.Points);
Expand All @@ -100,25 +107,25 @@ public async Task LosingDeductsPointsFromAccount()
[TestMethod]
public async Task MultiUserScenarioIsHandled()
{
var archieBet = new BetRequest(200, 3);
var archieBet = new BetHttpRequest(200, 3);
var archieResponse = await MakeLosingBetAsync(archieBet, "Archie");
Assert.AreEqual(10_000 - archieBet.Points, archieResponse.Account);

var bobBet = new BetRequest(700, 5);
var bobBet = new BetHttpRequest(700, 5);
var bobResponse = await MakeWinningBetAsync(bobBet, "Bob");
Assert.AreEqual(10_000 + bobBet.Points * 9, bobResponse.Account);

var charlieBet = new BetRequest(555, 2);
var charlieBet = new BetHttpRequest(555, 2);
var charlieResponse = await MakeLosingBetAsync(charlieBet, "Charlie");
Assert.AreEqual(10_000 - charlieBet.Points, charlieResponse.Account);
}

[TestMethod]
public async Task BalanceIsPreservedBetweenBets()
{
var firstBet = new BetRequest(300, 5);
var secondBet = new BetRequest(50, 6);
var thirdBet = new BetRequest(800, 7);
var firstBet = new BetHttpRequest(300, 5);
var secondBet = new BetHttpRequest(50, 6);
var thirdBet = new BetHttpRequest(800, 7);

var firstResponse = await MakeLosingBetAsync(firstBet, "Mortimer");
var secondResponse = await MakeWinningBetAsync(secondBet, "Mortimer");
Expand All @@ -134,35 +141,37 @@ public async Task BalanceIsPreservedBetweenBets()
Assert.AreEqual(expectedBalanceAfterThirdBet, thirdResponse.Account);
}

private static async Task<IResult> MakeMockBetAsync(BetRequest request, string? userId = null, int? winningNumber = null)
private static async Task<IResult> MakeMockBetAsync(BetHttpRequest httpRequest, string? userId = null, int? winningNumber = null)
{
var authService = new MockAuthService(userId ?? "");
var rngService = new MockRngService(winningNumber ?? 0);
var gamblingService = new GamblingService(authService, rngService);
return await gamblingService.BetAsync(request);
var betService = new BetService((_, _) => winningNumber ?? 0, new InMemoryBetRepository());
var httpHandler = new BetHttpHandler(authService, betService);
return await httpHandler.HandleAsync(httpRequest);
}

private static async Task<BetResponse> MakeMockBetAsyncUnwrap(BetRequest request, string? userId = null, int? winningNumber = null)
private static async Task<BetHttpResponse> MakeMockBetAsyncUnwrap(BetHttpRequest httpRequest, string? userId = null, int? winningNumber = null)
{
var response = await MakeMockBetAsync(request, userId, winningNumber);
var okResponse = (Ok<BetResponse>)response;
var response = await MakeMockBetAsync(httpRequest, userId, winningNumber);
Assert.IsInstanceOfType(response, typeof(Ok<BetHttpResponse>));
var okResponse = (Ok<BetHttpResponse>)response;
return okResponse.Value ?? throw new NullReferenceException("response was null for some reason");
}
private static async Task<BetResponse> MakeWinningBetAsync(BetRequest request, string userId)

private static async Task<BetHttpResponse> MakeWinningBetAsync(BetHttpRequest httpRequest, string userId)
{
return await MakeMockBetAsyncUnwrap(request, userId, request.Number);
return await MakeMockBetAsyncUnwrap(httpRequest, userId, httpRequest.Number);
}

private static async Task<BetResponse> MakeLosingBetAsync(BetRequest request, string userId)
private static async Task<BetHttpResponse> MakeLosingBetAsync(BetHttpRequest httpRequest, string userId)
{
return await MakeMockBetAsyncUnwrap(request, userId, request.Number == 0 ? 1 : 0);
return await MakeMockBetAsyncUnwrap(httpRequest, userId, httpRequest.Number == 0 ? 1 : 0);
}

private class MockRngService : IRandomService
{
private readonly int _number;
public MockRngService(int number) => _number = number;
public int GetNumber(int min, int max) => _number;
public int GetNumber(int min, int toInclusive) => _number;
}

private class MockAuthService : IAuthService
Expand Down
2 changes: 1 addition & 1 deletion Gambling.API/Interfaces/IRandomService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@

public interface IRandomService
{
int GetNumber(int min, int max);
int GetNumber(int min, int toInclusive);
}
23 changes: 23 additions & 0 deletions Gambling.API/Models/BetHttpRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
namespace Gambling.API.Models;

public record BetHttpRequest(int Points, int Number)
{
private const int MinAllowedNumber = 0;
private const int MaxAllowedNumber = 9;
public bool IsNotValid(out Dictionary<string, string[]> problems)
{
problems = new Dictionary<string, string[]>();

if (Points < 0)
{
problems.Add(nameof(Points), new[] { "Bet can't be negative" });
}

if (Number is < MinAllowedNumber or > MaxAllowedNumber)
{
problems.Add(nameof(Number), new[] { $"Winning number must be between {MinAllowedNumber} and {MaxAllowedNumber}" });
}

return problems.Any();
}
}
3 changes: 3 additions & 0 deletions Gambling.API/Models/BetHttpResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace Gambling.API.Models;

public record BetHttpResponse(int Account, string Status, string Points);
3 changes: 0 additions & 3 deletions Gambling.API/Models/BetRequest.cs

This file was deleted.

3 changes: 0 additions & 3 deletions Gambling.API/Models/BetResponse.cs

This file was deleted.

12 changes: 9 additions & 3 deletions Gambling.API/Program.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
using Microsoft.AspNetCore.Diagnostics;
using System.Security.Cryptography;

namespace Gambling.API;


public delegate int RngFunc(int fromInclusive, int toExclusive);

public static class Program
{
private static void Main(string[] args)
Expand All @@ -17,16 +21,18 @@ public static WebApplication CreateWebApplication(string[] args)
var app = builder.Build();
app.WrapUnhandledExceptionsInProblemDetails();
app.UseAuthentication();
app.MapPost("/", async (BetRequest request, GamblingService service) => await service.BetAsync(request));
app.MapPost("/api/v1/bet", async (BetHttpRequest request, BetHttpHandler handler) => await handler.HandleAsync(request));
return app;
}

public static IServiceCollection AddGamblingServices(this IServiceCollection services)
{
services.AddSingleton<IRandomService, CryptoRngService>();
services.AddSingleton<RngFunc>(_ => RandomNumberGenerator.GetInt32);
services.AddScoped<IBetRepository, InMemoryBetRepository>();
services.AddScoped<IAuthService, CookiesAuthService>();
services.AddScoped<GamblingService>();
services.AddScoped<BetHttpHandler>();
services.AddHttpContextAccessor();
services.AddScoped<BetService>();
return services;
}

Expand Down
3 changes: 3 additions & 0 deletions Gambling.API/Services/BalanceUpdateCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace Gambling.API.Services;

public record BalanceUpdateCommand(string UserId, int BetAmount, int TheoreticalReward, int StartingBalance);
3 changes: 3 additions & 0 deletions Gambling.API/Services/BalanceUpdateResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace Gambling.API.Services;

public record BalanceUpdateResult(bool HadEnoughCredit, int CurrentBalance);
3 changes: 3 additions & 0 deletions Gambling.API/Services/BetCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace Gambling.API.Services;

public record BetRequest(string UserId, int Points, int Number);
40 changes: 40 additions & 0 deletions Gambling.API/Services/BetHttpHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@

namespace Gambling.API.Services;

public class BetHttpHandler
{
private readonly IAuthService _auth;
private readonly BetService _service;

public BetHttpHandler(IAuthService auth, BetService service)
{
_auth = auth;
_service = service;
}

public async Task<IResult> HandleAsync(BetHttpRequest httpRequest)
{
if (httpRequest.IsNotValid(out var problemDetails))
{
return Results.ValidationProblem(problemDetails);
}

var userId = await _auth.GetCurrentUserIdAsync();
var request = new BetRequest(userId, httpRequest.Points, httpRequest.Number);
var betResult = await _service.BetAsync(request);
if (!betResult.UserHadEnoughCredit)
{
return Results.BadRequest("Not enough points on the account!");
};

var response = MapToResponse(betResult);
return Results.Ok(response);
}

private static BetHttpResponse MapToResponse(BetResult betResult)
{
var statusString = betResult.HasWon ? "won" : "lost";
var rewardString = betResult.Reward >= 0 ? $"+{betResult.Reward}" : betResult.Reward.ToString();
return new BetHttpResponse(betResult.CurrentBalance, statusString, rewardString);
}
}
23 changes: 23 additions & 0 deletions Gambling.API/Services/BetHttpRequestValidator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
namespace Gambling.API.Services;

public class BetHttpRequestValidator
{
private const int MinAllowedNumber = 0;
private const int MaxAllowedNumber = 9;
public bool IsNotValid(BetHttpRequest httpRequest, out Dictionary<string, string[]> problems)
{
problems = new Dictionary<string, string[]>();

if (httpRequest.Points < 0)
{
problems.Add(nameof(httpRequest.Points), new[] { "Bet can't be negative" });
}

if (httpRequest.Number is < MinAllowedNumber or > MaxAllowedNumber)
{
problems.Add(nameof(httpRequest.Number), new[] { $"Winning number must be between {MinAllowedNumber} and {MaxAllowedNumber}" });
}

return problems.Any();
}
}
3 changes: 3 additions & 0 deletions Gambling.API/Services/BetResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace Gambling.API.Services;

public record BetResult(bool UserHadEnoughCredit, int CurrentBalance, int Reward, bool HasWon);
27 changes: 27 additions & 0 deletions Gambling.API/Services/BetService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
namespace Gambling.API.Services;

public class BetService
{
private readonly RngFunc _rngFunc;
private readonly IBetRepository _repository;
private const int StartingBalance = 10_000;
private const int WinPointsMultiplier = 9;
private const int MinWinningNumber = 0;
private const int MaxWinningNumber = 9;

public BetService(RngFunc rngFunc, IBetRepository repository)
{
_rngFunc = rngFunc;
_repository = repository;
}

public async Task<BetResult> BetAsync(BetRequest request)
{
var winningNumber = _rngFunc(MinWinningNumber, MaxWinningNumber + 1);
var hasWon = request.Number == winningNumber;
var reward = hasWon ? request.Points * WinPointsMultiplier : -request.Points;
var updateCommand = new BalanceUpdateCommand(request.UserId, request.Points, reward, StartingBalance);
var updateResult = await _repository.UpdateBalance(updateCommand);
return new BetResult(updateResult.HadEnoughCredit, updateResult.CurrentBalance, reward, hasWon);
}
}
2 changes: 1 addition & 1 deletion Gambling.API/Services/CryptoRngService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ namespace Gambling.API.Services;

public class CryptoRngService : IRandomService
{
public int GetNumber(int min, int max) => RandomNumberGenerator.GetInt32(min, max);
public int GetNumber(int min, int toInclusive) => RandomNumberGenerator.GetInt32(min, toInclusive + 1);
}
Loading

0 comments on commit 04e2944

Please sign in to comment.