Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Swashbuckle annotations #203

Merged
merged 22 commits into from
Jul 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
6027a8f
Add Swashbuckle.AspNetCore.Annotations depend.
TheTedder Nov 7, 2023
eea6577
Enable annotations.
TheTedder Nov 7, 2023
c3b961f
Convert AccountController::Register's XML comments to annotations.
TheTedder Nov 7, 2023
0620703
Use constructor instead of Summary property.
TheTedder Nov 7, 2023
627051b
Convert AccounController::Login XML comments to annotations.
TheTedder Nov 7, 2023
7b22b05
Replace AccountController::ResendConfirmation XML comments with annot…
TheTedder Nov 7, 2023
febef61
Remove unneeded param XML comment.
TheTedder Nov 7, 2023
7fcba44
Convert AccountController::RecoverAccount XML comments into annotations.
TheTedder Nov 7, 2023
e3c5998
Convert AccountController::ConfirmAccount XML comments to annotations.
TheTedder Nov 7, 2023
893f141
Add missing response types.
TheTedder Nov 7, 2023
3ce2377
Convert TestRecovery XML comments to annotations.
TheTedder Nov 7, 2023
233ec9b
Convert ResetPassword XML comments to annotations.
TheTedder Nov 7, 2023
d7c0f72
Convert UserController XML comments to annotations.
TheTedder Nov 7, 2023
a427660
Update openapi.json.
TheTedder Nov 7, 2023
85e14bb
Add missing 500 response.
TheTedder Jun 24, 2024
47237ed
Update openapi.json.
TheTedder Jun 24, 2024
1ce7edb
Remove XML from swagger descriptions.
TheTedder Jun 24, 2024
203370d
Add 400 and 500 errors to every controller.
TheTedder Jul 1, 2024
d22484a
Fix up endpoint annotations.
TheTedder Jul 1, 2024
9b6dd34
Update openapi.json.
TheTedder Jul 1, 2024
36f5a02
Delete conventions.
TheTedder Jul 1, 2024
a61566f
Delete bogus test.
TheTedder Jul 1, 2024
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
13 changes: 1 addition & 12 deletions LeaderboardBackend.Test/Categories.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,6 @@ public void OneTimeTearDown()
_factory.Dispose();
}

[Test]
public static void GetCategory_Unauthorized()
{
RequestFailureException e = Assert.ThrowsAsync<RequestFailureException>(
async () => await _apiClient.Get<CategoryViewModel>($"/api/categories/1", new())
)!;

Assert.AreEqual(HttpStatusCode.Unauthorized, e.Response.StatusCode);
}

[Test]
public static void GetCategory_NotFound()
{
Expand Down Expand Up @@ -87,8 +77,7 @@ public static async Task CreateCategory_GetCategory_OK()
);

CategoryViewModel retrievedCategory = await _apiClient.Get<CategoryViewModel>(
$"/api/categories/{createdCategory?.Id}",
new() { Jwt = _jwt }
$"/api/categories/{createdCategory?.Id}", new() { }
);

Assert.AreEqual(createdCategory, retrievedCategory);
Expand Down
252 changes: 105 additions & 147 deletions LeaderboardBackend/Controllers/AccountController.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using LeaderboardBackend.Controllers.Annotations;
using LeaderboardBackend.Models.Entities;
using LeaderboardBackend.Models.Requests;
using LeaderboardBackend.Models.ViewModels;
Expand All @@ -8,6 +7,7 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.FeatureManagement.Mvc;
using OneOf;
using Swashbuckle.AspNetCore.Annotations;

namespace LeaderboardBackend.Controllers;

Expand All @@ -21,44 +21,44 @@ public AccountController(IUserService userService)
_userService = userService;
}

/// <summary>
/// Registers a new User.
/// </summary>
/// <param name="request">
/// The `RegisterRequest` instance from which register the `User`.
/// </param>
/// <param name="confirmationService">The IConfirmationService dependency.</param>
/// <response code="201">The `User` was registered and returned successfully.</response>
/// <response code="400">
/// The request was malformed.
/// </response>
/// <response code="409">
/// A `User` with the specified username or email already exists.<br/><br/>
/// Validation error codes by property:
/// - **Username**:
/// - **UsernameTaken**: the username is already in use
/// - **Email**:
/// - **EmailAlreadyUsed**: the email is already in use
/// </response>
/// <response code="422">
/// The request contains errors.<br/><br/>
/// Validation error codes by property:
/// - **Username**:
/// - **UsernameFormat**: Invalid username format
/// - **Password**:
/// - **PasswordFormat**: Invalid password format
/// - **Email**:
/// - **EmailValidator**: Invalid email format
/// </response>
[AllowAnonymous]
[HttpPost("register")]
[ApiConventionMethod(typeof(Conventions), nameof(Conventions.PostAnon))]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status409Conflict, Type = typeof(ValidationProblemDetails))]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
[FeatureGate(Features.ACCOUNT_REGISTRATION)]
[HttpPost("register")]
[SwaggerOperation("Registers a new User.")]
[SwaggerResponse(201, "The `User` was registered and returned successfully.")]
[SwaggerResponse(
409,
"""
A `User` with the specified username or email already exists.
Validation error codes by property:
- **Username**:
- **UsernameTaken**: the username is already in use
- **Email**:
- **EmailAlreadyUsed**: the email is already in use
""",
typeof(ValidationProblemDetails)
)]
[SwaggerResponse(
422,
"""
The request contains errors.
Validation error codes by property:
- **Username**:
- **UsernameFormat**: Invalid username format
- **Password**:
- **PasswordFormat**: Invalid password format
- **Email**:
- **EmailValidator**: Invalid email format
""",
typeof(ValidationProblemDetails)
)]
public async Task<ActionResult<UserViewModel>> Register(
[FromBody] RegisterRequest request,

[FromBody, SwaggerRequestBody(
"The `RegisterRequest` instance from which to register the `User`.",
Required = true
)] RegisterRequest request
,
[FromServices] IAccountConfirmationService confirmationService
)
{
Expand Down Expand Up @@ -93,38 +93,38 @@ [FromServices] IAccountConfirmationService confirmationService
return Conflict(new ValidationProblemDetails(ModelState));
}

/// <summary>
/// Logs a User in.
/// </summary>
/// <param name="request">
/// The `LoginRequest` instance from which to perform the login.
/// </param>
/// <response code="200">
/// The `User` was logged in successfully. A `LoginResponse` is returned, containing a token.
/// </response>
/// <response code="400">The request was malformed.</response>
/// <response code="401">The password given was incorrect.</response>
/// <response code="403">The associated `User` is banned.</response>
/// <response code="404">No `User` with the requested details could be found.</response>
/// <response code="422">
/// The request contains errors.<br/><br/>
/// Validation error codes by property:
/// - **Password**:
/// - **NotEmptyValidator**: No password was passed
/// - **PasswordFormat**: Invalid password format
/// - **Email**:
/// - **NotNullValidator**: No email was passed
/// - **EmailValidator**: Invalid email format
/// </response>
[AllowAnonymous]
[HttpPost("/login")]
[ApiConventionMethod(typeof(Conventions), nameof(Conventions.PostAnon))]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[FeatureGate(Features.LOGIN)]
public async Task<ActionResult<LoginResponse>> Login([FromBody] LoginRequest request)
[HttpPost("/login")]
[SwaggerOperation("Logs a User in.")]
[SwaggerResponse(
200,
"The `User` was logged in successfully. A `LoginResponse` is returned, containing a token.",
typeof(LoginResponse)
)]
[SwaggerResponse(401, "The password given was incorrect.")]
[SwaggerResponse(403, "The associated `User` is banned.")]
[SwaggerResponse(404, "No `User` with the requested details could be found.")]
[SwaggerResponse(
422,
"""
The request contains errors.
Validation error codes by property:
- **Password**:
- **NotEmptyValidator**: No password was passed
- **PasswordFormat**: Invalid password format
- **Email**:
- **NotNullValidator**: No email was passed
- **EmailValidator**: Invalid email format
""",
typeof(ValidationProblemDetails)
)]
public async Task<ActionResult<LoginResponse>> Login(
[FromBody, SwaggerRequestBody(
"The `LoginRequest` instance with which to perform the login.",
Required = true
)] LoginRequest request
)
{
LoginResult result = await _userService.LoginByEmailAndPassword(request.Email, request.Password);

Expand All @@ -136,30 +136,12 @@ public async Task<ActionResult<LoginResponse>> Login([FromBody] LoginRequest req
);
}

/// <summary>
/// Resends the account confirmation link.
/// </summary>
/// <param name="confirmationService">IAccountConfirmationService dependency.</param>
/// <response code="200">A new confirmation link was generated.</response>
/// <response code="400">
/// The request was malformed.
/// </response>
/// <response code="401">
/// The request doesn't contain a valid session token.
/// </response>
/// <response code="409">
/// The `User`'s account has already been confirmed.
/// </response>
/// <response code="500">
/// The account recovery email failed to be created.
/// </response>
[HttpPost("confirm")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
[ProducesResponseType(StatusCodes.Status429TooManyRequests)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
[SwaggerOperation("Resends the account confirmation link.")]
[SwaggerResponse(200, "A new confirmation link was generated.")]
[SwaggerResponse(401)]
[SwaggerResponse(409, "The `User`'s account has already been confirmed.")]
[SwaggerResponse(500, "The account recovery email failed to be created.")]
public async Task<ActionResult> ResendConfirmation(
[FromServices] IAccountConfirmationService confirmationService
)
Expand All @@ -186,23 +168,15 @@ [FromServices] IAccountConfirmationService confirmationService
);
}

/// <summary>
/// Sends an account recovery email.
/// </summary>
/// <param name="recoveryService">IAccountRecoveryService dependency.</param>
/// <param name="logger"></param>
/// <param name="request">The account recovery request.</param>
/// <response code="200">This endpoint returns 200 OK regardless of whether the email was sent successfully or not.</response>
/// <response code="400">The request object was malformed.</response>
[AllowAnonymous]
[HttpPost("recover")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[SwaggerOperation("Sends an account recovery email.")]
[SwaggerResponse(200, "This endpoint returns 200 OK regardless of whether the email was sent successfully or not.")]
[FeatureGate(Features.ACCOUNT_RECOVERY)]
public async Task<ActionResult> RecoverAccount(
[FromServices] IAccountRecoveryService recoveryService,
[FromServices] ILogger<AccountController> logger,
[FromBody] RecoverAccountRequest request
[FromBody, SwaggerRequestBody("The account recovery request.")] RecoverAccountRequest request
)
{
User? user = await _userService.GetUserByNameAndEmail(request.Username, request.Email);
Expand All @@ -220,21 +194,16 @@ [FromBody] RecoverAccountRequest request
return Ok();
}

/// <summary>
/// Confirms a user account.
/// </summary>
/// <param name="id">The confirmation token.</param>
/// <param name="confirmationService">IAccountConfirmationService dependency.</param>
/// <response code="200">The account was confirmed successfully.</response>
/// <response code="404">The token provided was invalid or expired.</response>
/// <response code="409">The user's account was either already confirmed or banned.</response>
[AllowAnonymous]
[HttpPut("confirm/{id}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
public async Task<ActionResult> ConfirmAccount(Guid id, [FromServices] IAccountConfirmationService confirmationService)
[SwaggerOperation("Confirms a user account.")]
[SwaggerResponse(200, "The account was confirmed successfully.")]
[SwaggerResponse(404, "The token provided was invalid or expired.")]
[SwaggerResponse(409, "the user's account was either already confirmed or banned.")]
public async Task<ActionResult> ConfirmAccount(
[SwaggerParameter("The confirmation token.")] Guid id,
[FromServices] IAccountConfirmationService confirmationService
)
{
ConfirmAccountResult result = await confirmationService.ConfirmAccount(id);

Expand All @@ -247,20 +216,16 @@ public async Task<ActionResult> ConfirmAccount(Guid id, [FromServices] IAccountC
);
}

/// <summary>
/// Tests an account recovery token for validity.
/// </summary>
/// <param name="id">The recovery token.</param>
/// <param name="recoveryService">IAccountRecoveryService dependency.</param>
/// <response code="200">The token provided is valid.</response>
/// <response code="404">The token provided is invalid or expired, or the user is banned.</response>
[AllowAnonymous]
[HttpGet("recover/{id}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[SwaggerOperation("Tests an account recovery token for validity.")]
[SwaggerResponse(200, "The token provided is valid.")]
[SwaggerResponse(404, "The token provided is invalid or expired, or the user is banned.")]
[FeatureGate(Features.ACCOUNT_RECOVERY)]
public async Task<ActionResult> TestRecovery(Guid id, [FromServices] IAccountRecoveryService recoveryService)
public async Task<ActionResult> TestRecovery(
[SwaggerParameter("The recovery token.")] Guid id,
[FromServices] IAccountRecoveryService recoveryService
)
{
TestRecoveryResult result = await recoveryService.TestRecovery(id);

Expand All @@ -273,32 +238,25 @@ public async Task<ActionResult> TestRecovery(Guid id, [FromServices] IAccountRec
);
}

/// <summary>
/// Recover the user's account by resetting their password to a new value.
/// </summary>
/// <param name="id">The recovery token.</param>
/// <param name="request">The password recovery request object.</param>
/// <param name="recoveryService">IAccountRecoveryService dependency</param>
/// <response code="200">The user's password was reset successfully.</response>
/// <response code="403">The user is banned.</response>
/// <response code="404">The token provided is invalid or expired.</response>
/// <response code="409">The new password is the same as the user's existing password.</response>
/// <response code="422">
/// The request body contains errors.<br/>
/// A **PasswordFormat** Validation error on the Password field indicates that the password format is invalid.
/// </response>
[AllowAnonymous]
[HttpPost("recover/{id}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
[ProducesResponseType(StatusCodes.Status422UnprocessableEntity, Type = typeof(ValidationProblemDetails))]
[FeatureGate(Features.ACCOUNT_RECOVERY)]
[HttpPost("recover/{id}")]
[SwaggerOperation("Recover the user's account by resetting their password to a new value.")]
[SwaggerResponse(200, "The user's password was reset successfully.")]
[SwaggerResponse(403, "The user is banned.")]
[SwaggerResponse(404, "The token provided is invalid or expired.")]
[SwaggerResponse(409, "The new password is the same as the user's existing password.")]
[SwaggerResponse(
422,
"""
The request body contains errors.
A **PasswordFormat** Validation error on the Password field indicates that the password format is invalid.
""",
typeof(ValidationProblemDetails)
)]
public async Task<ActionResult> ResetPassword(
Guid id,
[FromBody] ChangePasswordRequest request,
[SwaggerParameter("The recovery token.")] Guid id,
[FromBody, SwaggerRequestBody("The password recovery request object.", Required = true)] ChangePasswordRequest request,
[FromServices] IAccountRecoveryService recoveryService
)
{
Expand Down
Loading
Loading