Skip to content

Commit

Permalink
Get Categories for Leaderboard Endpoint (#282)
Browse files Browse the repository at this point in the history
* Add get categories for leaderboard endpoint

* Add test

* Reduce DB calls in test scaffolding

* Switch query method call when fetching leaderboards
  • Loading branch information
zysim authored Feb 2, 2025
1 parent b0f5b69 commit 94ed5a0
Show file tree
Hide file tree
Showing 5 changed files with 171 additions and 0 deletions.
65 changes: 65 additions & 0 deletions LeaderboardBackend.Test/Categories.cs
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,71 @@ await _apiClient.Awaiting(
.Where(e => e.Response.StatusCode == HttpStatusCode.NotFound);
}

[Test]
public async Task GetCategoriesForLeaderboard_OK()
{
IServiceScope scope = _factory.Services.CreateScope();
ApplicationContext context = scope.ServiceProvider.GetRequiredService<ApplicationContext>();

Leaderboard board = new()
{
Name = "get cats ok",
Slug = "getcategories-ok",
Categories = [
new()
{
Name = "get cats ok",
Slug = "getcategories-ok",
SortDirection = SortDirection.Ascending,
Type = RunType.Score,
},
new()
{
Name = "get cats ok deleted",
Slug = "getcategories-ok-deleted",
SortDirection = SortDirection.Ascending,
Type = RunType.Score,
DeletedAt = _clock.GetCurrentInstant(),
},
],
};
context.Add(board);
await context.SaveChangesAsync();
board.Id.Should().NotBe(default);

CategoryViewModel[]? resultSansDeleted = await _apiClient.Get<CategoryViewModel[]>(
$"api/leaderboard/{board.Id}/categories",
new() { }
);
resultSansDeleted!.Single().Should().BeEquivalentTo(board.Categories[0], opts => opts.ExcludingMissingMembers());

CategoryViewModel[]? resultWithDeleted = await _apiClient.Get<CategoryViewModel[]>(
$"api/leaderboard/{board.Id}/categories?includeDeleted=true",
new() { }
);
resultWithDeleted!.Should().BeEquivalentTo(board.Categories, options => options.ExcludingMissingMembers());

board.Categories[0].DeletedAt = _clock.GetCurrentInstant();
await context.SaveChangesAsync();

CategoryViewModel[]? resultEmpty = await _apiClient.Get<CategoryViewModel[]>(
$"api/leaderboard/{board.Id}/categories",
new() { }
);
resultEmpty.Should().BeEmpty();
}

[Test]
public async Task GetCategoriesForLeaderboard_NotFound() =>
await _apiClient.Awaiting(
a => a.Get<CategoryViewModel>(
$"api/leaderboard/{short.MaxValue}/categories",
new() { }
)
).Should()
.ThrowAsync<RequestFailureException>()
.Where(e => e.Response.StatusCode == HttpStatusCode.NotFound);

[Test]
public async Task CreateCategory_GetCategory_OK()
{
Expand Down
18 changes: 18 additions & 0 deletions LeaderboardBackend/Controllers/CategoriesController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,24 @@ public async Task<ActionResult<CategoryViewModel>> GetCategoryBySlug(
return Ok(CategoryViewModel.MapFrom(category));
}

[AllowAnonymous]
[HttpGet("api/leaderboard/{id:long}/categories")]
[SwaggerOperation("Gets all Categories of Leaderboard `id`.", OperationId = "getCategoriesForLeaderboard")]
[SwaggerResponse(200)]
[SwaggerResponse(404, "The Leaderboard with ID `id` could not be found.", typeof(ProblemDetails))]
public async Task<ActionResult<CategoryViewModel[]>> GetCategoriesForLeaderboard(
[FromRoute] long id,
[FromQuery, SwaggerParameter(Description = "Whether to include deleted Categories. Defaults to `false`.")] bool includeDeleted = false
)
{
GetCategoriesForLeaderboardResult r = await categoryService.GetCategoriesForLeaderboard(id, includeDeleted);

return r.Match<ActionResult<CategoryViewModel[]>>(
categories => Ok(from cat in categories select CategoryViewModel.MapFrom(cat)),
notFound => NotFound()
);
}

[Authorize(Policy = UserTypes.ADMINISTRATOR)]
[HttpPost("leaderboard/{id:long}/categories/create")]
[SwaggerOperation("Creates a new Category for a Leaderboard with ID `id`. This request is restricted to Administrators.", OperationId = "createCategory")]
Expand Down
4 changes: 4 additions & 0 deletions LeaderboardBackend/Services/ICategoryService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public interface ICategoryService
{
Task<Category?> GetCategory(long id);
Task<Category?> GetCategoryBySlug(long leaderboardId, string slug);
Task<GetCategoriesForLeaderboardResult> GetCategoriesForLeaderboard(long leaderboardId, bool includeDeleted);
Task<CreateCategoryResult> CreateCategory(long leaderboardId, CreateCategoryRequest request);
Task<Category?> GetCategoryForRun(Run run);
Task<UpdateResult<Category>> UpdateCategory(long id, UpdateCategoryRequest request);
Expand All @@ -19,3 +20,6 @@ public interface ICategoryService

[GenerateOneOf]
public partial class CreateCategoryResult : OneOfBase<Category, Conflict<Category>, NotFound>;

[GenerateOneOf]
public partial class GetCategoriesForLeaderboardResult : OneOfBase<Category[], NotFound>;
17 changes: 17 additions & 0 deletions LeaderboardBackend/Services/Impl/CategoryService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,23 @@ public class CategoryService(ApplicationContext applicationContext, IClock clock
await applicationContext.Categories
.FirstOrDefaultAsync(c => c.Slug == slug && c.LeaderboardId == leaderboardId && c.DeletedAt == null);

public async Task<GetCategoriesForLeaderboardResult> GetCategoriesForLeaderboard(long leaderboardId, bool includeDeleted)
{
Leaderboard? board = await applicationContext.Leaderboards
.Where(board => board.Id == leaderboardId)
.Include(board => board.Categories)
.SingleOrDefaultAsync();

if (board is null)
{
return new NotFound();
}

return board.Categories!
.Where(cat => includeDeleted || cat.DeletedAt == null)
.ToArray();
}

public async Task<CreateCategoryResult> CreateCategory(long leaderboardId, CreateCategoryRequest request)
{
Category category =
Expand Down
67 changes: 67 additions & 0 deletions LeaderboardBackend/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,73 @@
}
}
},
"/api/leaderboard/{id}/categories": {
"get": {
"tags": [
"Categories"
],
"summary": "Gets all Categories of Leaderboard `id`.",
"operationId": "getCategoriesForLeaderboard",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "integer",
"format": "int64"
}
},
{
"name": "includeDeleted",
"in": "query",
"description": "Whether to include deleted Categories. Defaults to `false`.",
"schema": {
"type": "boolean",
"default": false
}
}
],
"responses": {
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
}
}
},
"500": {
"description": "Internal Server Error"
},
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/CategoryViewModel"
}
}
}
}
},
"404": {
"description": "The Leaderboard with ID `id` could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
}
}
}
}
}
},
"/leaderboard/{id}/categories/create": {
"post": {
"tags": [
Expand Down

0 comments on commit 94ed5a0

Please sign in to comment.