diff --git a/LeaderboardBackend.Test/Controllers/CategoriesControllerTest.cs b/LeaderboardBackend.Test/Controllers/CategoriesControllerTest.cs index 942188e..c99ac24 100644 --- a/LeaderboardBackend.Test/Controllers/CategoriesControllerTest.cs +++ b/LeaderboardBackend.Test/Controllers/CategoriesControllerTest.cs @@ -6,6 +6,7 @@ using LeaderboardBackend.Models.Entities; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; +using LeaderboardBackend.Test.Helpers; namespace LeaderboardBackend.Test.Controllers; @@ -32,11 +33,9 @@ public async Task GetCategory_NotFound_CategoryDoesNotExist() .Setup(x => x.GetCategory(It.IsAny())) .Returns(Task.FromResult(null)); - ActionResult response = await _controller.GetCategory((long)1); + ActionResult response = await _controller.GetCategory(1); - NotFoundResult? actual = response.Result as NotFoundResult; - Assert.NotNull(actual); - Assert.AreEqual(404, actual!.StatusCode); + ObjectResultHelpers.AssertResponseNotFound(response); } [Test] @@ -47,8 +46,8 @@ public async Task GetCategory_Ok_CategoryExists() .Returns(Task.FromResult(new Category { Id = 1 })); ActionResult response = await _controller.GetCategory(1); - Category? category = Helpers.GetValueFromObjectResult(response.Result); + Category? category = ObjectResultHelpers.GetValueFromObjectResult(response); Assert.NotNull(category); Assert.AreEqual(1, category!.Id); } diff --git a/LeaderboardBackend.Test/Controllers/LeaderboardsControllerTest.cs b/LeaderboardBackend.Test/Controllers/LeaderboardsControllerTest.cs index fb0d7ac..9408280 100644 --- a/LeaderboardBackend.Test/Controllers/LeaderboardsControllerTest.cs +++ b/LeaderboardBackend.Test/Controllers/LeaderboardsControllerTest.cs @@ -1,6 +1,7 @@ using LeaderboardBackend.Controllers; using LeaderboardBackend.Models.Entities; using LeaderboardBackend.Services; +using LeaderboardBackend.Test.Helpers; using Microsoft.AspNetCore.Mvc; using Moq; using NUnit.Framework; @@ -36,10 +37,7 @@ public async Task GetLeaderboard_NotFound_LeaderboardDoesNotExist() .Returns(Task.FromResult(null)); ActionResult response = await _controller.GetLeaderboard(1); - var actual = response.Result as NotFoundResult; - - Assert.NotNull(actual); - Assert.AreEqual(404, actual!.StatusCode); + ObjectResultHelpers.AssertResponseNotFound(response); } [Test] @@ -50,10 +48,10 @@ public async Task GetLeaderboard_Ok_LeaderboardExists() .Returns(Task.FromResult(_defaultLeaderboard)); ActionResult response = await _controller.GetLeaderboard(1); - Leaderboard? leaderboard = Helpers.GetValueFromObjectResult(response); + Leaderboard? leaderboard = ObjectResultHelpers.GetValueFromObjectResult(response); Assert.NotNull(leaderboard); - Assert.AreEqual(1, leaderboard!.Id); + Assert.AreEqual(_defaultLeaderboard, leaderboard); } [Test] @@ -70,8 +68,8 @@ public async Task GetLeaderboards_Ok_ListExists() .Returns(Task.FromResult(mockList)); ActionResult> response = await _controller.GetLeaderboards(new long[] { 1, 2 }); - List? leaderboards = Helpers.GetValueFromObjectResult>(response); + List? leaderboards = ObjectResultHelpers.GetValueFromObjectResult, OkObjectResult>(response); Assert.NotNull(leaderboards); Assert.AreEqual(new ulong[] { 1, 2 }, leaderboards!.Select(l => l.Id)); } diff --git a/LeaderboardBackend.Test/Controllers/RunsControllerTest.cs b/LeaderboardBackend.Test/Controllers/RunsControllerTest.cs new file mode 100644 index 0000000..c530f19 --- /dev/null +++ b/LeaderboardBackend.Test/Controllers/RunsControllerTest.cs @@ -0,0 +1,30 @@ +using LeaderboardBackend.Controllers; +using LeaderboardBackend.Models.Entities; +using LeaderboardBackend.Services; +using LeaderboardBackend.Services.Impl; +using LeaderboardBackend.Test.Helpers; +using Microsoft.AspNetCore.Mvc; +using NUnit.Framework; +using System; +using System.Threading.Tasks; + +namespace LeaderboardBackend.Test.Controllers; + +internal class RunsControllerTest +{ + private RunsController _controller = null!; + + [SetUp] + public void Setup() + { + RunService service = new RunService(ApplicationContextFactory.CreateNewContext()); + _controller = new RunsController(service); + } + + [Test] + public async Task GetRun_NotFound_WhenNotExist() + { + ActionResult response = await _controller.GetRun(new Guid()); + ObjectResultHelpers.AssertResponseNotFound(response); + } +} diff --git a/LeaderboardBackend.Test/Controllers/UsersControllerTests.cs b/LeaderboardBackend.Test/Controllers/UsersControllerTest.cs similarity index 87% rename from LeaderboardBackend.Test/Controllers/UsersControllerTests.cs rename to LeaderboardBackend.Test/Controllers/UsersControllerTest.cs index 4a08277..67091b2 100644 --- a/LeaderboardBackend.Test/Controllers/UsersControllerTests.cs +++ b/LeaderboardBackend.Test/Controllers/UsersControllerTest.cs @@ -10,10 +10,11 @@ using System.Security.Claims; using System.Threading.Tasks; using BCryptNet = BCrypt.Net.BCrypt; +using LeaderboardBackend.Test.Helpers; namespace LeaderboardBackend.Test.Controllers; -public class UsersControllerTests +public class UsersControllerTest { private UsersController _controller = null!; private Mock _userServiceMock = null!; @@ -47,8 +48,8 @@ public async Task GetUserById_NotFound_UserDoesNotExist() .Returns(Task.FromResult(null)); ActionResult response = await _controller.GetUserById(defaultUserId); - - Helpers.AssertResponseNotFound(response); + + ObjectResultHelpers.AssertResponseNotFound(response); } [Test] @@ -60,8 +61,7 @@ public async Task GetUserById_Ok_UserExists() ActionResult response = await _controller.GetUserById(defaultUserId); - User? user = Helpers.GetValueFromObjectResult(response); - + User? user = ObjectResultHelpers.GetValueFromObjectResult(response); Assert.NotNull(user); Assert.AreEqual(defaultUser, user); } @@ -78,7 +78,8 @@ public async Task Register_BadRequest_PasswordsMismatch() }; ActionResult response = await _controller.Register(body); - Helpers.AssertResponseBadRequest(response); + + ObjectResultHelpers.AssertResponseBadRequest(response); } [Test] @@ -93,12 +94,11 @@ public async Task Register_OK_PasswordsMatchCreateSuccess() }; ActionResult response = await _controller.Register(body); - User? user = Helpers.GetValueFromObjectResult(response); + User? user = ObjectResultHelpers.GetValueFromObjectResult(response); Assert.NotNull(user); Assert.AreEqual(defaultUser.Username, user!.Username); Assert.AreEqual(defaultUser.Email, user!.Email); - // This route creates a new user, and thus does a new password hash. // Since hashing the password again won't produce the same hash as // defaultUser, we do a cryptographic verify instead. @@ -115,7 +115,7 @@ public async Task Me_Forbid_NoUserInClaims() ActionResult response = await _controller.Me(); - Helpers.AssertResponseForbid(response); + ObjectResultHelpers.AssertResponseForbid(response); } [Test] @@ -127,8 +127,8 @@ public async Task Me_Ok_UserFoundFromClaims() .Returns(Task.FromResult(defaultUser)); ActionResult response = await _controller.Me(); - User? user = Helpers.GetValueFromObjectResult(response); + User? user = ObjectResultHelpers.GetValueFromObjectResult(response); Assert.NotNull(user); Assert.AreEqual(defaultUser, user); } diff --git a/LeaderboardBackend.Test/Controllers/Helpers.cs b/LeaderboardBackend.Test/Helpers/ObjectResultHelpers.cs similarity index 79% rename from LeaderboardBackend.Test/Controllers/Helpers.cs rename to LeaderboardBackend.Test/Helpers/ObjectResultHelpers.cs index 161c893..f107eeb 100644 --- a/LeaderboardBackend.Test/Controllers/Helpers.cs +++ b/LeaderboardBackend.Test/Helpers/ObjectResultHelpers.cs @@ -1,10 +1,10 @@ +using System; using Microsoft.AspNetCore.Mvc; using NUnit.Framework; -using System; -namespace LeaderboardBackend.Test.Controllers; +namespace LeaderboardBackend.Test.Helpers; -internal static class Helpers +internal static class ObjectResultHelpers { public static void AssertResponseNotFound(ActionResult response) { @@ -32,7 +32,7 @@ public static void AssertResponseForbid(ActionResult response) // a Forbidden 403 result. } - public static TResult? GetValueFromObjectResult(ActionResult result) where TObjectResult : ObjectResult + public static TValue? GetValueFromObjectResult(ActionResult result) where TObjectResult : ObjectResult { TObjectResult? objectResult = null; try @@ -44,6 +44,7 @@ public static void AssertResponseForbid(ActionResult response) Assert.Fail(ex.Message); } - return (TResult?)(objectResult?.Value); + return (TValue?)(objectResult?.Value); } + } diff --git a/LeaderboardBackend/Controllers/RunsController.cs b/LeaderboardBackend/Controllers/RunsController.cs new file mode 100644 index 0000000..d56d6cc --- /dev/null +++ b/LeaderboardBackend/Controllers/RunsController.cs @@ -0,0 +1,61 @@ +using Microsoft.AspNetCore.Mvc; +using LeaderboardBackend.Models.Entities; +using LeaderboardBackend.Services; +using Microsoft.AspNetCore.Http; +using LeaderboardBackend.Controllers.Annotations; +using LeaderboardBackend.Models.Requests.Run; + +namespace LeaderboardBackend.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class RunsController : ControllerBase + { + private readonly IRunService _runService; + + public RunsController(IRunService runService) + { + _runService = runService; + } + + /// Gets a Run. + /// The Run ID. Must parse to a long for this route to be hit. + /// The Run with the provided ID. + /// If no Run is found with the provided ID. + [ApiConventionMethod(typeof(Conventions), + nameof(Conventions.Get))] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [HttpGet("{id}")] + public async Task> GetRun(Guid id) + { + Run? run = await _runService.GetRun(id); + if (run == null) + { + return NotFound(); + } + + return Ok(run); + } + + /// Creates a Run. + [ApiConventionMethod(typeof(Conventions), + nameof(Conventions.Post))] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesDefaultResponseType] + [HttpPost] + public async Task CreateRun([FromBody] CreateRunRequest request) + { + Run run = new() + { + Played = request.Played, + Submitted = request.Submitted, + Status = request.Status, + }; + + await _runService.CreateRun(run); + return CreatedAtAction(nameof(GetRun), new { id = run.Id }, run); + } + } +} diff --git a/LeaderboardBackend/Models/Entities/Run.cs b/LeaderboardBackend/Models/Entities/Run.cs index ea8015a..d6ee576 100644 --- a/LeaderboardBackend/Models/Entities/Run.cs +++ b/LeaderboardBackend/Models/Entities/Run.cs @@ -4,11 +4,11 @@ namespace LeaderboardBackend.Models.Entities; public enum RunStatus { - Created, - Submitted, - Pending, - Approved, - Rejected + CREATED, + SUBMITTED, + PENDING, + APPROVED, + REJECTED } public class Run diff --git a/LeaderboardBackend/Models/Requests/Runs/Create.cs b/LeaderboardBackend/Models/Requests/Runs/Create.cs new file mode 100644 index 0000000..a282a3b --- /dev/null +++ b/LeaderboardBackend/Models/Requests/Runs/Create.cs @@ -0,0 +1,16 @@ +using LeaderboardBackend.Models.Entities; +using System.ComponentModel.DataAnnotations; + +namespace LeaderboardBackend.Models.Requests.Run; + +public class CreateRunRequest +{ + [Required] + public DateTime Played { get; set; } + + [Required] + public DateTime Submitted { get; set; } + + [Required] + public RunStatus Status { get; set; } +} diff --git a/LeaderboardBackend/Services/IRunService.cs b/LeaderboardBackend/Services/IRunService.cs new file mode 100644 index 0000000..c717609 --- /dev/null +++ b/LeaderboardBackend/Services/IRunService.cs @@ -0,0 +1,10 @@ +using LeaderboardBackend.Models.Entities; + +namespace LeaderboardBackend.Services; + +public interface IRunService +{ + Task GetRun(Guid id); + + Task CreateRun(Run run); +} diff --git a/LeaderboardBackend/Services/AuthService.cs b/LeaderboardBackend/Services/Impl/AuthService.cs similarity index 100% rename from LeaderboardBackend/Services/AuthService.cs rename to LeaderboardBackend/Services/Impl/AuthService.cs diff --git a/LeaderboardBackend/Services/CategoryService.cs b/LeaderboardBackend/Services/Impl/CategoryService.cs similarity index 90% rename from LeaderboardBackend/Services/CategoryService.cs rename to LeaderboardBackend/Services/Impl/CategoryService.cs index 4a03bc9..9f3dd5a 100644 --- a/LeaderboardBackend/Services/CategoryService.cs +++ b/LeaderboardBackend/Services/Impl/CategoryService.cs @@ -4,7 +4,8 @@ namespace LeaderboardBackend.Services { public class CategoryService : ICategoryService { - private ApplicationContext _applicationContext; + private readonly ApplicationContext _applicationContext; + public CategoryService(ApplicationContext applicationContext) { _applicationContext = applicationContext; diff --git a/LeaderboardBackend/Services/LeaderboardService.cs b/LeaderboardBackend/Services/Impl/LeaderboardService.cs similarity index 100% rename from LeaderboardBackend/Services/LeaderboardService.cs rename to LeaderboardBackend/Services/Impl/LeaderboardService.cs diff --git a/LeaderboardBackend/Services/Impl/RunService.cs b/LeaderboardBackend/Services/Impl/RunService.cs new file mode 100644 index 0000000..ebc3c2b --- /dev/null +++ b/LeaderboardBackend/Services/Impl/RunService.cs @@ -0,0 +1,24 @@ +using LeaderboardBackend.Models.Entities; + +namespace LeaderboardBackend.Services.Impl; + +public class RunService : IRunService +{ + private readonly ApplicationContext _applicationContext; + + public RunService(ApplicationContext applicationContext) + { + _applicationContext = applicationContext; + } + + public async Task GetRun(Guid id) + { + return await _applicationContext.Runs.FindAsync(id); + } + + public async Task CreateRun(Run run) + { + _applicationContext.Runs.Add(run); + await _applicationContext.SaveChangesAsync(); + } +} diff --git a/LeaderboardBackend/Services/UserService.cs b/LeaderboardBackend/Services/Impl/UserService.cs similarity index 90% rename from LeaderboardBackend/Services/UserService.cs rename to LeaderboardBackend/Services/Impl/UserService.cs index ca0c262..867e470 100644 --- a/LeaderboardBackend/Services/UserService.cs +++ b/LeaderboardBackend/Services/Impl/UserService.cs @@ -1,4 +1,4 @@ -using LeaderboardBackend.Models.Entities; +using LeaderboardBackend.Models.Entities; using Microsoft.EntityFrameworkCore; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; @@ -28,7 +28,7 @@ public UserService(ApplicationContext applicationContext, IConfiguration config) { // We save a username with casing, but match without. // Effectively you can't have two separate users named e.g. "cool" and "cOoL". - return await _applicationContext.Users.FirstOrDefaultAsync(u => u.Username != null && u.Username.ToLower() == name.ToLower()); + return await _applicationContext.Users.SingleOrDefaultAsync(u => u.Username != null && u.Username.ToLower() == name.ToLower()); } public async Task GetUserFromClaims(ClaimsPrincipal claims)