Skip to content

Commit

Permalink
Create JudgementsController (leaderboardsgg#82)
Browse files Browse the repository at this point in the history
* Create Files

* Change Model Fields

* Add CreateJudgement

* Return 500 as Intended, and Remove <remarks> Tags
<remarks> tags don't work for non-controller actions

* Add Second Condition

* Get Rid of Null Mod Check
We already check if the user exists in authZ, cuz we need to know if
they're a mod.

* Add JudgementViewModel for Controller Actions

* Uncomment Admin User Check for Authz

* Add RunService to Program

* Simplify creating JudgementViewModels

* Add/change docs to be agreeable w/ Swashbuckle
Can't have fields explicitly defined in record structs; Swagger won't
generate.

* Fixed IsMod auth decorator and CreateJudgement

* Wrote test for CreateJudgement route 200 path
* Fixed IsMod to actually load modships so it doesn't always fail
* CreatedAtAction was being called in a way that caused it to fail

* Using Utc in example timestamp

`Now` doesn't work in NPGSql, it has to be Utc

ok epic

* Allow admins to do mod things

I forgot we said I was gonna include this too

* Reorder ResponseType Attribute

* Convert ViewModel to be A Public Record

* Update CONTRIBUTING to link to the separate style guide

* If This Actually Needs to be Done

* Fix tests, move ViewModels folder

Co-authored-by: Sim <[email protected]>
Co-authored-by: RageCage64 <[email protected]>
  • Loading branch information
3 people authored May 21, 2022
1 parent 5e00513 commit ac93960
Show file tree
Hide file tree
Showing 18 changed files with 1,801 additions and 461 deletions.
3 changes: 1 addition & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,7 @@ Bug: {short description}

### C#

- Add tests for any new feature or bug fix, to ensure things continue to work.
- Early returns are great, they help reduce nesting!
- Please refer to the [style guide](https://github.com/leaderboardsgg/leaderboard-backend/wiki/Style-Guide).

### Git

Expand Down
104 changes: 104 additions & 0 deletions LeaderboardBackend.Test/Judgements.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
using LeaderboardBackend.Models.Entities;
using LeaderboardBackend.Models.Requests;
using LeaderboardBackend.Test.Lib;
using LeaderboardBackend.Test.TestApi;
using LeaderboardBackend.Test.TestApi.Extensions;
using LeaderboardBackend.Models.ViewModels;
using NUnit.Framework;
using System;
using System.Threading.Tasks;

namespace LeaderboardBackend.Test;

[TestFixture]
internal class Judgements
{
private static TestApiFactory Factory = null!;
private static TestApiClient ApiClient = null!;
private static Leaderboard DefaultLeaderboard = null!;
private static string? Jwt;

private static readonly string ValidUsername = "Test";
private static readonly string ValidPassword = "c00l_pAssword";
private static readonly string ValidEmail = "[email protected]";

[SetUp]
public static async Task SetUp()
{
Factory = new TestApiFactory();
ApiClient = Factory.CreateTestApiClient();

// Set up a default Leaderboard and a mod user for that leaderboard to use as the Jwt for tests
string adminJwt = (await ApiClient.LoginAdminUser()).Token;
User mod = await ApiClient.RegisterUser(
ValidUsername,
ValidEmail,
ValidPassword
);
DefaultLeaderboard = await ApiClient.Post<Leaderboard>(
"/api/leaderboards",
new()
{
Body = new CreateLeaderboardRequest
{
Name = Generators.GenerateRandomString(),
Slug = Generators.GenerateRandomString(),
},
Jwt = adminJwt,
}
);
Modship modship = await ApiClient.Post<Modship>(
"/api/modships",
new()
{
Body = new CreateModshipRequest
{
LeaderboardId = DefaultLeaderboard.Id,
UserId = mod.Id,
},
Jwt = adminJwt,
}
);

Jwt = (await ApiClient.LoginUser(ValidEmail, ValidPassword)).Token;
}

[Test]
public async Task CreateJudgement_OK()
{
Run run = await CreateRun();

JudgementViewModel? createdJudgement = await ApiClient.Post<JudgementViewModel>(
"/api/judgements",
new()
{
Body = new CreateJudgementRequest
{
RunId = run.Id,
Note = "It is a cool run",
Approved = true,
},
Jwt = Jwt,
}
);

Assert.NotNull(createdJudgement);
}

private async Task<Run> CreateRun()
{
return await ApiClient.Post<Run>(
"/api/runs",
new()
{
Body = new CreateRunRequest
{
Played = DateTime.UtcNow,
Submitted = DateTime.UtcNow,
Status = RunStatus.SUBMITTED,
},
Jwt = Jwt,
}
);
}
}
2 changes: 1 addition & 1 deletion LeaderboardBackend.Test/TestApi/TestApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ internal sealed class RequestFailureException : Exception
{
public HttpResponseMessage Response { get; private set; }

public RequestFailureException(HttpResponseMessage response) : base("The attempted request failed")
public RequestFailureException(HttpResponseMessage response) : base($"The attempted request failed with status code {response.StatusCode}")
{
Response = response;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public class UserTypeAuthorizationHandler : AuthorizationHandler<UserTypeRequire
private readonly JwtSecurityTokenHandler _jwtHandler;
private readonly TokenValidationParameters _jwtValidationParams;
private readonly IUserService _userService;
private readonly IModshipService _modshipService;

public UserTypeAuthorizationHandler(
IConfiguration config,
Expand All @@ -23,6 +24,7 @@ IUserService userService
_jwtHandler = JwtSecurityTokenHandlerSingleton.Instance;
_jwtValidationParams = TokenValidationParametersSingleton.Instance(config);
_userService = userService;
_modshipService = modshipService;
}

protected override Task HandleRequirementAsync(
Expand Down Expand Up @@ -56,15 +58,12 @@ UserTypeRequirement requirement
) => requirement.Type switch
{
UserTypes.Admin => user.Admin,
UserTypes.Mod => IsMod(user),
UserTypes.Mod => user.Admin || IsMod(user),
UserTypes.User => true,
_ => false,
};

//private bool IsAdmin(User user) => user.Admin;

// FIXME: Users don't get automagically populated with Modships when on creation of the latter.
private bool IsMod(User user) => user.Modships?.Count() > 0;
private bool IsMod(User user) => _modshipService.LoadUserModships(user.Id).Result.Count() > 0;

private bool TryGetJwtFromHttpContext(AuthorizationHandlerContext context, out string token)
{
Expand Down
95 changes: 95 additions & 0 deletions LeaderboardBackend/Controllers/JudgementsController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
using LeaderboardBackend.Authorization;
using LeaderboardBackend.Controllers.Annotations;
using LeaderboardBackend.Models.Entities;
using LeaderboardBackend.Models.Requests;
using LeaderboardBackend.Services;
using LeaderboardBackend.Models.ViewModels;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace LeaderboardBackend.Controllers;

[Route("api/[controller]")]
[ApiController]
[Produces("application/json")]
public class JudgementsController : ControllerBase
{
private readonly ILogger _logger;
private readonly IJudgementService _judgementService;
private readonly IRunService _runService;
private readonly IUserService _userService;

public JudgementsController(
ILogger<JudgementsController> logger,
IJudgementService judgementService,
IRunService runService,
IUserService userService
)
{
_logger = logger;
_judgementService = judgementService;
_runService = runService;
_userService = userService;
}

/// <summary>Gets a Judgement from its ID.</summary>
/// <response code="200">The Judgement with the provided ID.</response>
/// <response code="404">If no Judgement can be found.</response>
[ApiConventionMethod(typeof(Conventions),
nameof(Conventions.Get))]
[AllowAnonymous]
[HttpGet("{id}")]
public async Task<ActionResult<JudgementViewModel>> GetJudgement(long id)
{
Judgement? judgement = await _judgementService.GetJudgement(id);
if (judgement is null)
{
return NotFound();
}

return Ok(new JudgementViewModel(judgement));
}

/// <summary>Creates a judgement for a run.</summary>
/// <response code="201">The created judgement.</response>
/// <response code="400">The request body is malformed.</response>
/// <response code="404">For an invalid judgement.</response>
[ApiConventionMethod(typeof(Conventions),
nameof(Conventions.Post))]
[Authorize(Policy = UserTypes.Mod)]
[HttpPost]
public async Task<ActionResult<JudgementViewModel>> CreateJudgement([FromBody] CreateJudgementRequest body)
{
User? mod = await _userService.GetUserFromClaims(HttpContext.User);
Run? run = await _runService.GetRun(body.RunId);

if (run is null)
{
_logger.LogError($"CreateJudgement: run is null. ID = {body.RunId}");
return NotFound($"Run not found for ID = {body.RunId}");
}

if (run.Status == RunStatus.CREATED)
{
_logger.LogError($"CreateJudgement: run has pending participations (i.e. run status == CREATED). ID = {body.RunId}");
return BadRequest($"Run has pending Participations. ID = {body.RunId}");
}

// TODO: Update run status on body.Approved's value
Judgement judgement = new()
{
Approved = body.Approved,
Mod = mod!,
ModId = mod!.Id,
Note = body.Note,
Run = run,
RunId = run.Id,
};

await _judgementService.CreateJudgement(judgement);

JudgementViewModel judgementView = new(judgement);

return CreatedAtAction(nameof(GetJudgement), new { id = judgementView.Id }, judgementView);
}
}
Loading

0 comments on commit ac93960

Please sign in to comment.