Skip to content

Commit

Permalink
Get Category by Slug Endpoint (#281)
Browse files Browse the repository at this point in the history
* Add get category by slug endpoint

* Address comments:
- Specify the 404 returns a ProblemDetails
- Organise imports in test
  • Loading branch information
zysim authored Feb 2, 2025
1 parent 57de0d8 commit b0f5b69
Show file tree
Hide file tree
Showing 6 changed files with 204 additions and 4 deletions.
116 changes: 114 additions & 2 deletions LeaderboardBackend.Test/Categories.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ public async Task OneTimeSetUp()
public void OneTimeTearDown() => _factory.Dispose();

[Test]
public async Task GetCategory_OK()
public async Task GetCategoryByID_OK()
{
IServiceScope scope = _factory.Services.CreateScope();
ApplicationContext context = scope.ServiceProvider.GetRequiredService<ApplicationContext>();
Expand Down Expand Up @@ -106,7 +106,7 @@ await _apiClient.Awaiting(

[TestCase("NotANumber")]
[TestCase("69")]
public async Task GetCategory_NotFound(object id) =>
public async Task GetCategoryByID_NotFound(object id) =>
await _apiClient.Awaiting(
a => a.Get<CategoryViewModel>(
$"/api/category/{id}",
Expand All @@ -116,6 +116,118 @@ await _apiClient.Awaiting(
.ThrowAsync<RequestFailureException>()
.Where(e => e.Response.StatusCode == HttpStatusCode.NotFound);

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

Category created = new()
{
Name = "get slug ok",
Slug = "getcategory-slug-ok",
LeaderboardId = _createdLeaderboard.Id,
SortDirection = SortDirection.Ascending,
Type = RunType.Score,
};

context.Add(created);
await context.SaveChangesAsync();
created.Id.Should().NotBe(default);

await _apiClient.Awaiting(
a => a.Get<CategoryViewModel>(
$"api/leaderboard/{_createdLeaderboard.Id}/category?slug=getcategory-slug-ok",
new() { }
)
).Should()
.NotThrowAsync()
.WithResult(new()
{
Id = created.Id,
Name = "get slug ok",
Slug = "getcategory-slug-ok",
Info = "",
Type = RunType.Score,
SortDirection = SortDirection.Ascending,
LeaderboardId = _createdLeaderboard.Id,
CreatedAt = _clock.GetCurrentInstant(),
UpdatedAt = null,
DeletedAt = null,
});
}

[Test]
public async Task GetCategoryBySlug_NotFound_WrongSlug() =>
await _apiClient.Awaiting(
a => a.Get<CategoryViewModel>(
$"api/leaderboard/{_createdLeaderboard.Id}/category?slug=wrong-slug",
new() { }
)
).Should()
.ThrowAsync<RequestFailureException>()
.Where(e => e.Response.StatusCode == HttpStatusCode.NotFound);

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

Category created = new()
{
Name = "get slug not found",
Slug = "getcategory-slug-wrong-board-id",
LeaderboardId = _createdLeaderboard.Id,
SortDirection = SortDirection.Ascending,
Type = RunType.Score,
DeletedAt = _clock.GetCurrentInstant(),
};

context.Add(created);
await context.SaveChangesAsync();
created.Id.Should().NotBe(default);

await _apiClient.Awaiting(
a => a.Get<CategoryViewModel>(
$"api/leaderboard/{short.MaxValue}/category?slug={created.Slug}",
new() { }
)
).Should()
.ThrowAsync<RequestFailureException>()
.Where(e => e.Response.StatusCode == HttpStatusCode.NotFound);
}

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

Category created = new()
{
Name = "get slug not found",
Slug = "getcategory-slug-deleted",
LeaderboardId = _createdLeaderboard.Id,
SortDirection = SortDirection.Ascending,
Type = RunType.Score,
DeletedAt = _clock.GetCurrentInstant(),
};

context.Add(created);
await context.SaveChangesAsync();
created.Id.Should().NotBe(default);

await _apiClient.Awaiting(
a => a.Get<CategoryViewModel>(
$"api/leaderboard/{_createdLeaderboard.Id}/category?slug={created.Slug}",
new() { }
)
).Should()
.ThrowAsync<RequestFailureException>()
.Where(e => e.Response.StatusCode == HttpStatusCode.NotFound);
}

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

[AllowAnonymous]
[HttpGet("api/leaderboard/{id:long}/category")]
[SwaggerOperation("Gets a Category of Leaderboard `id` by its slug. Will not return deleted Categories.", OperationId = "getCategoryBySlug")]
[SwaggerResponse(200)]
[SwaggerResponse(404, "The Category either doesn't exist for the Leaderboard, or it has been deleted.", typeof(ProblemDetails))]
public async Task<ActionResult<CategoryViewModel>> GetCategoryBySlug(
[FromRoute] long id,
[FromQuery, SwaggerParameter(Required = true)] string slug
)
{
Category? category = await categoryService.GetCategoryBySlug(id, slug);

if (category == null)
{
return NotFound();
}

return Ok(CategoryViewModel.MapFrom(category));
}

[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
2 changes: 1 addition & 1 deletion LeaderboardBackend/Controllers/LeaderboardsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public async Task<ActionResult<LeaderboardViewModel>> GetLeaderboard([FromRoute]

[AllowAnonymous]
[HttpGet("api/leaderboard")]
[SwaggerOperation("Gets a leaderboard by its slug.", OperationId = "getLeaderboardBySlug")]
[SwaggerOperation("Gets a leaderboard by its slug. Will not return deleted boards.", OperationId = "getLeaderboardBySlug")]
[SwaggerResponse(200)]
[SwaggerResponse(404)]
public async Task<ActionResult<LeaderboardViewModel>> GetLeaderboardBySlug([FromQuery, SwaggerParameter(Required = true)] string slug)
Expand Down
1 change: 1 addition & 0 deletions LeaderboardBackend/Services/ICategoryService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ namespace LeaderboardBackend.Services;
public interface ICategoryService
{
Task<Category?> GetCategory(long id);
Task<Category?> GetCategoryBySlug(long leaderboardId, string slug);
Task<CreateCategoryResult> CreateCategory(long leaderboardId, CreateCategoryRequest request);
Task<Category?> GetCategoryForRun(Run run);
Task<UpdateResult<Category>> UpdateCategory(long id, UpdateCategoryRequest request);
Expand Down
4 changes: 4 additions & 0 deletions LeaderboardBackend/Services/Impl/CategoryService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ public class CategoryService(ApplicationContext applicationContext, IClock clock
public async Task<Category?> GetCategory(long id) =>
await applicationContext.Categories.FindAsync(id);

public async Task<Category?> GetCategoryBySlug(long leaderboardId, string slug) =>
await applicationContext.Categories
.FirstOrDefaultAsync(c => c.Slug == slug && c.LeaderboardId == leaderboardId && c.DeletedAt == null);

public async Task<CreateCategoryResult> CreateCategory(long leaderboardId, CreateCategoryRequest request)
{
Category category =
Expand Down
65 changes: 64 additions & 1 deletion LeaderboardBackend/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,69 @@
}
}
},
"/api/leaderboard/{id}/category": {
"get": {
"tags": [
"Categories"
],
"summary": "Gets a Category of Leaderboard `id` by its slug. Will not return deleted Categories.",
"operationId": "getCategoryBySlug",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "integer",
"format": "int64"
}
},
{
"name": "slug",
"in": "query",
"required": true,
"schema": {
"type": "string"
}
}
],
"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": {
"$ref": "#/components/schemas/CategoryViewModel"
}
}
}
},
"404": {
"description": "The Category either doesn't exist for the Leaderboard, or it has been deleted.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
}
}
}
}
}
},
"/leaderboard/{id}/categories/create": {
"post": {
"tags": [
Expand Down Expand Up @@ -757,7 +820,7 @@
"tags": [
"Leaderboards"
],
"summary": "Gets a leaderboard by its slug.",
"summary": "Gets a leaderboard by its slug. Will not return deleted boards.",
"operationId": "getLeaderboardBySlug",
"parameters": [
{
Expand Down

0 comments on commit b0f5b69

Please sign in to comment.