From aaa9a8d0ed7d6b1dfea152109ae84f441fdadc7e Mon Sep 17 00:00:00 2001 From: zysim <9867871+zysim@users.noreply.github.com> Date: Mon, 14 Oct 2024 23:21:14 +0800 Subject: [PATCH 01/11] Create functionality and test --- LeaderboardBackend.Test/Leaderboards.cs | 67 + .../TestApi/TestApiClient.cs | 5 + .../Controllers/LeaderboardsController.cs | 23 + LeaderboardBackend/Results.cs | 2 + .../Services/ILeaderboardService.cs | 4 + .../Services/Impl/LeaderboardService.cs | 23 + LeaderboardBackend/openapi.json | 2466 +++++++++++++++-- 7 files changed, 2400 insertions(+), 190 deletions(-) diff --git a/LeaderboardBackend.Test/Leaderboards.cs b/LeaderboardBackend.Test/Leaderboards.cs index 1bb4642a..1dc1407f 100644 --- a/LeaderboardBackend.Test/Leaderboards.cs +++ b/LeaderboardBackend.Test/Leaderboards.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Net; +using System.Net.Http; using System.Threading; using System.Threading.Tasks; using LeaderboardBackend.Models.Entities; @@ -14,6 +15,7 @@ using Microsoft.AspNetCore.TestHost; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; +using Microsoft.VisualBasic; using NodaTime; using NodaTime.Testing; using NUnit.Framework; @@ -348,4 +350,69 @@ public async Task GetLeaderboards() LeaderboardViewModel[] returned = await _apiClient.Get("/api/leaderboards", new()); returned.Should().BeEquivalentTo(boards.Take(2), config => config.Excluding(lb => lb.Categories)); } + + [Test] + public async Task RestoreLeaderboard_OK() + { + ApplicationContext context = _factory.Services.CreateScope().ServiceProvider.GetRequiredService(); + + Leaderboard deletedBoard = new() + { + Name = "Super Mario World", + Slug = "super-mario-world-deleted", + DeletedAt = _clock.GetCurrentInstant() + }; + + context.Leaderboards.Add(deletedBoard); + await context.SaveChangesAsync(); + deletedBoard.Id.Should().NotBe(default); + + HttpResponseMessage res = await _apiClient.Put($"/leaderboard/{deletedBoard.Id}/restore", new() + { + Jwt = _jwt + }); + + res.StatusCode.Should().Be(HttpStatusCode.NoContent); + + Leaderboard? board = await context.Leaderboards.FindAsync(deletedBoard.Id); + board.Should().NotBeNull(); + // TODO: `DeletedAt` is still not null here. Don't know how to fix it. + board!.DeletedAt.Should().BeNull(); + } + + [Test] + public async Task RestoreLeaderboard_NotFound() + { + Func> act = async () => await _apiClient.Put($"/leaderboard/100/restore", new() + { + Jwt = _jwt + }); + + await act.Should().ThrowAsync().Where(e => e.Response.StatusCode == HttpStatusCode.NotFound); + } + + [Test] + public async Task RestoreLeaderboard_NotFound_WasNeverDeleted() + { + ApplicationContext context = _factory.Services.CreateScope().ServiceProvider.GetRequiredService(); + + Leaderboard board = new() + { + Name = "Super Mario World", + Slug = "super-mario-world-deleted", + }; + + context.Leaderboards.Add(board); + await context.SaveChangesAsync(); + board.Id.Should().NotBe(default); + + Func> act = async () => await _apiClient.Put($"/leaderboard/{board.Id}/restore", new() + { + Jwt = _jwt + }); + + await act.Should().ThrowAsync() + .Where(e => e.Response.StatusCode == HttpStatusCode.NotFound); + // TODO: Don't know how to test for the response message. + } } diff --git a/LeaderboardBackend.Test/TestApi/TestApiClient.cs b/LeaderboardBackend.Test/TestApi/TestApiClient.cs index 55aa880e..1e5aab1a 100644 --- a/LeaderboardBackend.Test/TestApi/TestApiClient.cs +++ b/LeaderboardBackend.Test/TestApi/TestApiClient.cs @@ -46,6 +46,11 @@ public async Task Post(string endpoint, HttpRequestInit in return await SendAndRead(endpoint, init with { Method = HttpMethod.Post }); } + public async Task Put(string endpoint, HttpRequestInit init) + { + return await Send(endpoint, init with { Method = HttpMethod.Put }); + } + public async Task Delete(string endpoint, HttpRequestInit init) { return await Send(endpoint, init with { Method = HttpMethod.Delete }); diff --git a/LeaderboardBackend/Controllers/LeaderboardsController.cs b/LeaderboardBackend/Controllers/LeaderboardsController.cs index cabf8bbb..af50e070 100644 --- a/LeaderboardBackend/Controllers/LeaderboardsController.cs +++ b/LeaderboardBackend/Controllers/LeaderboardsController.cs @@ -86,4 +86,27 @@ public async Task> CreateLeaderboard( } ); } + + [Authorize(Policy = UserTypes.ADMINISTRATOR)] + [HttpPut("leaderboard/{id:long}/restore")] + [SwaggerResponse(201)] + [SwaggerResponse(401)] + [SwaggerResponse(403, "The requesting `User` is unauthorized to restore `Leaderboard`s.")] + [SwaggerResponse(404, "The `Leaderboard` was not found, or it wasn't deleted in the first place.")] + public async Task> RestoreLeaderboard( + long id + ) + { + RestoreLeaderboardResult r = await leaderboardService.RestoreLeaderboard(id); + + return r.Match>( + _ => NoContent(), + notFound => NotFound(), + neverDeleted => + { + ModelState.AddModelError("Leaderboard", "LeaderboardWasNeverPreviouslyDeleted"); + return NotFound(new ValidationProblemDetails(ModelState)); + } + ); + } } diff --git a/LeaderboardBackend/Results.cs b/LeaderboardBackend/Results.cs index 9bd33669..abeadeff 100644 --- a/LeaderboardBackend/Results.cs +++ b/LeaderboardBackend/Results.cs @@ -8,5 +8,7 @@ namespace LeaderboardBackend.Result; public readonly record struct EmailFailed; public readonly record struct Expired; public readonly record struct CreateLeaderboardConflict; +public readonly record struct LeaderboardNotFound; +public readonly record struct LeaderboardNeverDeleted; public readonly record struct UserNotFound; public readonly record struct UserBanned; diff --git a/LeaderboardBackend/Services/ILeaderboardService.cs b/LeaderboardBackend/Services/ILeaderboardService.cs index ae3aa72a..f2c07624 100644 --- a/LeaderboardBackend/Services/ILeaderboardService.cs +++ b/LeaderboardBackend/Services/ILeaderboardService.cs @@ -11,7 +11,11 @@ public interface ILeaderboardService Task GetLeaderboardBySlug(string slug); Task> ListLeaderboards(); Task CreateLeaderboard(CreateLeaderboardRequest request); + Task RestoreLeaderboard(long id); } [GenerateOneOf] public partial class CreateLeaderboardResult : OneOfBase; + +[GenerateOneOf] +public partial class RestoreLeaderboardResult : OneOfBase; diff --git a/LeaderboardBackend/Services/Impl/LeaderboardService.cs b/LeaderboardBackend/Services/Impl/LeaderboardService.cs index 5efbf0b1..9aa622ab 100644 --- a/LeaderboardBackend/Services/Impl/LeaderboardService.cs +++ b/LeaderboardBackend/Services/Impl/LeaderboardService.cs @@ -42,4 +42,27 @@ public async Task CreateLeaderboard(CreateLeaderboardRe return lb; } + + public async Task RestoreLeaderboard(long id) + { + Leaderboard? lb = await applicationContext.Leaderboards.FindAsync([id]); + + if (lb == null) + { + return new LeaderboardNotFound(); + } + + if (lb.DeletedAt == null) + { + return new LeaderboardNeverDeleted(); + } + + applicationContext.Leaderboards.Update(lb); + + lb.DeletedAt = null; + + await applicationContext.SaveChangesAsync(); + + return Task.CompletedTask; + } } diff --git a/LeaderboardBackend/openapi.json b/LeaderboardBackend/openapi.json index 665b934d..6185c76b 100644 --- a/LeaderboardBackend/openapi.json +++ b/LeaderboardBackend/openapi.json @@ -664,6 +664,58 @@ } } }, + "/leaderboard/{id}/restore": { + "put": { + "tags": [ + "Leaderboards" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "500": { + "description": "Internal Server Error" + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RestoreLeaderboardResult" + } + } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "The requesting `User` is unauthorized to restore `Leaderboard`s." + }, + "404": { + "description": "The `Leaderboard` was not found, or it wasn't deleted in the first place." + } + } + } + }, "/api/run/{id}": { "get": { "tags": [ @@ -915,6 +967,154 @@ }, "components": { "schemas": { + "AggregateException": { + "type": "object", + "properties": { + "targetSite": { + "$ref": "#/components/schemas/MethodBase" + }, + "data": { + "type": "object", + "additionalProperties": { }, + "readOnly": true + }, + "innerException": { + "$ref": "#/components/schemas/Exception" + }, + "helpLink": { + "type": "string", + "nullable": true + }, + "source": { + "type": "string", + "nullable": true + }, + "hResult": { + "type": "integer", + "format": "int32" + }, + "stackTrace": { + "type": "string", + "nullable": true, + "readOnly": true + }, + "innerExceptions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Exception" + }, + "readOnly": true + }, + "message": { + "type": "string", + "readOnly": true + } + }, + "additionalProperties": false + }, + "Assembly": { + "type": "object", + "properties": { + "definedTypes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TypeInfo" + }, + "readOnly": true + }, + "exportedTypes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Type" + }, + "readOnly": true + }, + "codeBase": { + "type": "string", + "nullable": true, + "readOnly": true, + "deprecated": true + }, + "entryPoint": { + "$ref": "#/components/schemas/MethodInfo" + }, + "fullName": { + "type": "string", + "nullable": true, + "readOnly": true + }, + "imageRuntimeVersion": { + "type": "string", + "readOnly": true + }, + "isDynamic": { + "type": "boolean", + "readOnly": true + }, + "location": { + "type": "string", + "readOnly": true + }, + "reflectionOnly": { + "type": "boolean", + "readOnly": true + }, + "isCollectible": { + "type": "boolean", + "readOnly": true + }, + "isFullyTrusted": { + "type": "boolean", + "readOnly": true + }, + "customAttributes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CustomAttributeData" + }, + "readOnly": true + }, + "escapedCodeBase": { + "type": "string", + "readOnly": true, + "deprecated": true + }, + "manifestModule": { + "$ref": "#/components/schemas/Module" + }, + "modules": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Module" + }, + "readOnly": true + }, + "globalAssemblyCache": { + "type": "boolean", + "readOnly": true, + "deprecated": true + }, + "hostContext": { + "type": "integer", + "format": "int64", + "readOnly": true + }, + "securityRuleSet": { + "$ref": "#/components/schemas/SecurityRuleSet" + } + }, + "additionalProperties": false + }, + "CallingConventions": { + "enum": [ + "Standard", + "VarArgs", + "Any", + "HasThis", + "ExplicitThis" + ], + "type": "string" + }, "CategoryViewModel": { "required": [ "createdAt", @@ -990,6 +1190,136 @@ }, "additionalProperties": false }, + "ConstructorInfo": { + "type": "object", + "properties": { + "name": { + "type": "string", + "readOnly": true + }, + "declaringType": { + "$ref": "#/components/schemas/Type" + }, + "reflectedType": { + "$ref": "#/components/schemas/Type" + }, + "module": { + "$ref": "#/components/schemas/Module" + }, + "customAttributes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CustomAttributeData" + }, + "readOnly": true + }, + "isCollectible": { + "type": "boolean", + "readOnly": true + }, + "metadataToken": { + "type": "integer", + "format": "int32", + "readOnly": true + }, + "attributes": { + "$ref": "#/components/schemas/MethodAttributes" + }, + "methodImplementationFlags": { + "$ref": "#/components/schemas/MethodImplAttributes" + }, + "callingConvention": { + "$ref": "#/components/schemas/CallingConventions" + }, + "isAbstract": { + "type": "boolean", + "readOnly": true + }, + "isConstructor": { + "type": "boolean", + "readOnly": true + }, + "isFinal": { + "type": "boolean", + "readOnly": true + }, + "isHideBySig": { + "type": "boolean", + "readOnly": true + }, + "isSpecialName": { + "type": "boolean", + "readOnly": true + }, + "isStatic": { + "type": "boolean", + "readOnly": true + }, + "isVirtual": { + "type": "boolean", + "readOnly": true + }, + "isAssembly": { + "type": "boolean", + "readOnly": true + }, + "isFamily": { + "type": "boolean", + "readOnly": true + }, + "isFamilyAndAssembly": { + "type": "boolean", + "readOnly": true + }, + "isFamilyOrAssembly": { + "type": "boolean", + "readOnly": true + }, + "isPrivate": { + "type": "boolean", + "readOnly": true + }, + "isPublic": { + "type": "boolean", + "readOnly": true + }, + "isConstructedGenericMethod": { + "type": "boolean", + "readOnly": true + }, + "isGenericMethod": { + "type": "boolean", + "readOnly": true + }, + "isGenericMethodDefinition": { + "type": "boolean", + "readOnly": true + }, + "containsGenericParameters": { + "type": "boolean", + "readOnly": true + }, + "methodHandle": { + "$ref": "#/components/schemas/RuntimeMethodHandle" + }, + "isSecurityCritical": { + "type": "boolean", + "readOnly": true + }, + "isSecuritySafeCritical": { + "type": "boolean", + "readOnly": true + }, + "isSecurityTransparent": { + "type": "boolean", + "readOnly": true + }, + "memberType": { + "$ref": "#/components/schemas/MemberTypes" + } + }, + "additionalProperties": false + }, "CreateCategoryRequest": { "required": [ "info", @@ -1091,293 +1421,1359 @@ "additionalProperties": false, "description": "This request object is sent when creating a `Run`." }, - "LeaderboardViewModel": { - "required": [ - "categories", - "createdAt", - "deletedAt", - "id", - "info", - "name", - "slug", - "updatedAt" - ], + "CustomAttributeData": { "type": "object", "properties": { - "id": { - "type": "integer", - "description": "The unique identifier of the `Leaderboard`.\n\r\nGenerated on creation.", - "format": "int64" - }, - "name": { - "type": "string", - "description": "The display name of the `Leaderboard` to create.", - "example": "Foo Bar" - }, - "slug": { - "type": "string", - "description": "The URL-scoped unique identifier of the `Leaderboard`.\n\r\nMust be [2, 80] in length and consist only of alphanumeric characters and hyphens.", - "example": "foo-bar" - }, - "info": { - "type": "string", - "description": "The general information for the Leaderboard.", - "nullable": true, - "example": "Timer starts on selecting New Game and ends when the final boss is beaten." - }, - "createdAt": { - "type": "string", - "description": "The time the Leaderboard was created.", - "format": "date-time", - "example": "1984-01-01T00:00:00Z" + "attributeType": { + "$ref": "#/components/schemas/Type" }, - "updatedAt": { - "type": "string", - "description": "The last time the Leaderboard was updated or null.", - "format": "date-time", - "nullable": true, - "example": "1984-01-01T00:00:00Z" + "constructor": { + "$ref": "#/components/schemas/ConstructorInfo" }, - "deletedAt": { - "type": "string", - "description": "The time at which the Leaderboard was deleted, or null if the Leaderboard has not been deleted.", - "format": "date-time", - "nullable": true, - "example": "1984-01-01T00:00:00Z" + "constructorArguments": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CustomAttributeTypedArgument" + }, + "readOnly": true }, - "categories": { + "namedArguments": { "type": "array", "items": { - "$ref": "#/components/schemas/CategoryViewModel" + "$ref": "#/components/schemas/CustomAttributeNamedArgument" }, - "description": "A collection of `Category` entities for the `Leaderboard`." + "readOnly": true } }, - "additionalProperties": false, - "description": "Represents a collection of `Leaderboard` entities." + "additionalProperties": false }, - "LoginRequest": { - "required": [ - "email", - "password" - ], + "CustomAttributeNamedArgument": { "type": "object", "properties": { - "email": { - "minLength": 1, - "type": "string", - "description": "The `User`'s email address.", - "format": "email", - "example": "john.doe@example.com" + "memberInfo": { + "$ref": "#/components/schemas/MemberInfo" }, - "password": { - "minLength": 1, + "typedValue": { + "$ref": "#/components/schemas/CustomAttributeTypedArgument" + }, + "memberName": { "type": "string", - "description": "The `User`'s password. It:\r\n
  • supports Unicode;
  • must be [8, 80] in length;
  • must have at least:
    • one uppercase letter;
    • one lowercase letter; and
    • one number.
", - "example": "P4ssword" + "readOnly": true + }, + "isField": { + "type": "boolean", + "readOnly": true } }, - "additionalProperties": false, - "description": "This request object is sent when a `User` is attempting to log in." + "additionalProperties": false }, - "LoginResponse": { - "required": [ - "token" - ], + "CustomAttributeTypedArgument": { "type": "object", "properties": { - "token": { - "minLength": 1, - "type": "string", - "description": "A JSON Web Token to authenticate and authorize queries with." + "argumentType": { + "$ref": "#/components/schemas/Type" + }, + "value": { + "nullable": true } }, - "additionalProperties": false, - "description": "This response object is received upon a successful log-in request." + "additionalProperties": false }, - "ProblemDetails": { + "EventAttributes": { + "enum": [ + "None", + "SpecialName", + "RTSpecialName" + ], + "type": "string" + }, + "EventInfo": { "type": "object", "properties": { - "type": { + "name": { "type": "string", - "nullable": true + "readOnly": true }, - "title": { - "type": "string", - "nullable": true + "declaringType": { + "$ref": "#/components/schemas/Type" }, - "status": { + "reflectedType": { + "$ref": "#/components/schemas/Type" + }, + "module": { + "$ref": "#/components/schemas/Module" + }, + "customAttributes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CustomAttributeData" + }, + "readOnly": true + }, + "isCollectible": { + "type": "boolean", + "readOnly": true + }, + "metadataToken": { "type": "integer", "format": "int32", - "nullable": true + "readOnly": true }, - "detail": { - "type": "string", - "nullable": true + "memberType": { + "$ref": "#/components/schemas/MemberTypes" }, - "instance": { - "type": "string", - "nullable": true + "attributes": { + "$ref": "#/components/schemas/EventAttributes" + }, + "isSpecialName": { + "type": "boolean", + "readOnly": true + }, + "addMethod": { + "$ref": "#/components/schemas/MethodInfo" + }, + "removeMethod": { + "$ref": "#/components/schemas/MethodInfo" + }, + "raiseMethod": { + "$ref": "#/components/schemas/MethodInfo" + }, + "isMulticast": { + "type": "boolean", + "readOnly": true + }, + "eventHandlerType": { + "$ref": "#/components/schemas/Type" } }, - "additionalProperties": { } + "additionalProperties": false }, - "RecoverAccountRequest": { - "required": [ - "email", - "username" - ], + "Exception": { "type": "object", "properties": { - "username": { - "minLength": 1, + "targetSite": { + "$ref": "#/components/schemas/MethodBase" + }, + "message": { "type": "string", - "description": "The user's name." + "readOnly": true }, - "email": { - "minLength": 1, + "data": { + "type": "object", + "additionalProperties": { }, + "readOnly": true + }, + "innerException": { + "$ref": "#/components/schemas/Exception" + }, + "helpLink": { "type": "string", - "description": "The user's email address.", - "format": "email" + "nullable": true + }, + "source": { + "type": "string", + "nullable": true + }, + "hResult": { + "type": "integer", + "format": "int32" + }, + "stackTrace": { + "type": "string", + "nullable": true, + "readOnly": true } }, "additionalProperties": false }, - "RegisterRequest": { - "required": [ - "email", - "password", - "username" + "FieldAttributes": { + "enum": [ + "PrivateScope", + "Private", + "FamANDAssem", + "Assembly", + "Family", + "FamORAssem", + "Public", + "FieldAccessMask", + "Static", + "InitOnly", + "Literal", + "NotSerialized", + "HasFieldRVA", + "SpecialName", + "RTSpecialName", + "HasFieldMarshal", + "PinvokeImpl", + "HasDefault", + "ReservedMask" ], + "type": "string" + }, + "FieldInfo": { "type": "object", "properties": { - "username": { - "minLength": 1, + "name": { "type": "string", - "description": "The username of the `User`. It:\r\n
  • must be [2, 25] in length;
  • must be made up of letters sandwiching zero or one of:
    • hyphen;
    • underscore; or
    • apostrophe
\r\nUsernames are saved case-sensitively, but matched against case-insensitively.\r\nA `User` may not register with the name 'Cool' when another `User` with the name 'cool'\r\nexists.", - "example": "J'on-Doe" + "readOnly": true }, - "email": { - "minLength": 1, - "type": "string", - "description": "The `User`'s email address.", - "example": "john.doe@example.com" + "declaringType": { + "$ref": "#/components/schemas/Type" }, - "password": { - "minLength": 1, - "type": "string", - "description": "The `User`'s password. It:\r\n
  • supports Unicode;
  • must be [8, 80] in length;
  • must have at least:
    • one uppercase letter;
    • one lowercase letter; and
    • one number.
", - "example": "P4ssword" + "reflectedType": { + "$ref": "#/components/schemas/Type" + }, + "module": { + "$ref": "#/components/schemas/Module" + }, + "customAttributes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CustomAttributeData" + }, + "readOnly": true + }, + "isCollectible": { + "type": "boolean", + "readOnly": true + }, + "metadataToken": { + "type": "integer", + "format": "int32", + "readOnly": true + }, + "memberType": { + "$ref": "#/components/schemas/MemberTypes" + }, + "attributes": { + "$ref": "#/components/schemas/FieldAttributes" + }, + "fieldType": { + "$ref": "#/components/schemas/Type" + }, + "isInitOnly": { + "type": "boolean", + "readOnly": true + }, + "isLiteral": { + "type": "boolean", + "readOnly": true + }, + "isNotSerialized": { + "type": "boolean", + "readOnly": true, + "deprecated": true + }, + "isPinvokeImpl": { + "type": "boolean", + "readOnly": true + }, + "isSpecialName": { + "type": "boolean", + "readOnly": true + }, + "isStatic": { + "type": "boolean", + "readOnly": true + }, + "isAssembly": { + "type": "boolean", + "readOnly": true + }, + "isFamily": { + "type": "boolean", + "readOnly": true + }, + "isFamilyAndAssembly": { + "type": "boolean", + "readOnly": true + }, + "isFamilyOrAssembly": { + "type": "boolean", + "readOnly": true + }, + "isPrivate": { + "type": "boolean", + "readOnly": true + }, + "isPublic": { + "type": "boolean", + "readOnly": true + }, + "isSecurityCritical": { + "type": "boolean", + "readOnly": true + }, + "isSecuritySafeCritical": { + "type": "boolean", + "readOnly": true + }, + "isSecurityTransparent": { + "type": "boolean", + "readOnly": true + }, + "fieldHandle": { + "$ref": "#/components/schemas/RuntimeFieldHandle" } }, - "additionalProperties": false, - "description": "This request object is sent when a `User` is attempting to register." + "additionalProperties": false }, - "RunType": { + "GenericParameterAttributes": { "enum": [ - "Time", - "Score" + "None", + "Covariant", + "Contravariant", + "VarianceMask", + "ReferenceTypeConstraint", + "NotNullableValueTypeConstraint", + "DefaultConstructorConstraint", + "SpecialConstraintMask" ], "type": "string" }, - "RunViewModel": { + "ICustomAttributeProvider": { + "type": "object", + "additionalProperties": false + }, + "IntPtr": { + "type": "object", + "additionalProperties": false + }, + "LayoutKind": { + "enum": [ + "Sequential", + "Explicit", + "Auto" + ], + "type": "string" + }, + "LeaderboardNeverDeleted": { + "type": "object", + "additionalProperties": false + }, + "LeaderboardNotFound": { + "type": "object", + "additionalProperties": false + }, + "LeaderboardViewModel": { "required": [ - "$type", - "categoryId", + "categories", "createdAt", "deletedAt", "id", "info", - "updatedAt", - "userId" + "name", + "slug", + "updatedAt" ], "type": "object", "properties": { - "$type": { - "type": "string" - }, "id": { - "pattern": "^[a-zA-Z0-9-_]{22}$", + "type": "integer", + "description": "The unique identifier of the `Leaderboard`.\n\r\nGenerated on creation.", + "format": "int64" + }, + "name": { "type": "string", - "description": "The unique identifier of the `Run`.\n\r\nGenerated on creation." + "description": "The display name of the `Leaderboard` to create.", + "example": "Foo Bar" + }, + "slug": { + "type": "string", + "description": "The URL-scoped unique identifier of the `Leaderboard`.\n\r\nMust be [2, 80] in length and consist only of alphanumeric characters and hyphens.", + "example": "foo-bar" }, "info": { "type": "string", - "description": "User-provided details about the run.", - "nullable": true + "description": "The general information for the Leaderboard.", + "nullable": true, + "example": "Timer starts on selecting New Game and ends when the final boss is beaten." }, "createdAt": { "type": "string", - "description": "The time the run was created.", + "description": "The time the Leaderboard was created.", "format": "date-time", "example": "1984-01-01T00:00:00Z" }, "updatedAt": { "type": "string", - "description": "The last time the run was updated or null.", + "description": "The last time the Leaderboard was updated or null.", "format": "date-time", "nullable": true, "example": "1984-01-01T00:00:00Z" }, "deletedAt": { "type": "string", - "description": "The time at which the run was deleted, or null if the run has not been deleted.", + "description": "The time at which the Leaderboard was deleted, or null if the Leaderboard has not been deleted.", "format": "date-time", "nullable": true, "example": "1984-01-01T00:00:00Z" }, - "categoryId": { - "type": "integer", - "description": "The ID of the `Category` for `Run`.", - "format": "int64" - }, - "userId": { - "pattern": "^[a-zA-Z0-9-_]{22}$", - "type": "string", - "description": "The ID of the LeaderboardBackend.Models.Entities.User who submitted this run." + "categories": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CategoryViewModel" + }, + "description": "A collection of `Category` entities for the `Leaderboard`." } }, "additionalProperties": false, - "discriminator": { - "propertyName": "$type", - "mapping": { - "Time": "#/components/schemas/TimedRunViewModel", - "Score": "#/components/schemas/ScoredRunViewModel" - } - } + "description": "Represents a collection of `Leaderboard` entities." }, - "ScoredRunViewModel": { - "allOf": [ - { - "$ref": "#/components/schemas/RunViewModel" + "LoginRequest": { + "required": [ + "email", + "password" + ], + "type": "object", + "properties": { + "email": { + "minLength": 1, + "type": "string", + "description": "The `User`'s email address.", + "format": "email", + "example": "john.doe@example.com" }, - { - "required": [ - "score" - ], - "type": "object", - "properties": { - "score": { - "type": "integer", - "description": "The score achieved during the run.", - "format": "int64" - } - }, - "additionalProperties": false + "password": { + "minLength": 1, + "type": "string", + "description": "The `User`'s password. It:\r\n
  • supports Unicode;
  • must be [8, 80] in length;
  • must have at least:
    • one uppercase letter;
    • one lowercase letter; and
    • one number.
", + "example": "P4ssword" } - ] + }, + "additionalProperties": false, + "description": "This request object is sent when a `User` is attempting to log in." }, - "SortDirection": { - "enum": [ - "Ascending", - "Descending" + "LoginResponse": { + "required": [ + "token" ], - "type": "string" + "type": "object", + "properties": { + "token": { + "minLength": 1, + "type": "string", + "description": "A JSON Web Token to authenticate and authorize queries with." + } + }, + "additionalProperties": false, + "description": "This response object is received upon a successful log-in request." }, - "TimedRunViewModel": { - "allOf": [ - { - "$ref": "#/components/schemas/RunViewModel" + "MemberInfo": { + "type": "object", + "properties": { + "memberType": { + "$ref": "#/components/schemas/MemberTypes" + }, + "name": { + "type": "string", + "readOnly": true + }, + "declaringType": { + "$ref": "#/components/schemas/Type" + }, + "reflectedType": { + "$ref": "#/components/schemas/Type" + }, + "module": { + "$ref": "#/components/schemas/Module" + }, + "customAttributes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CustomAttributeData" + }, + "readOnly": true + }, + "isCollectible": { + "type": "boolean", + "readOnly": true + }, + "metadataToken": { + "type": "integer", + "format": "int32", + "readOnly": true + } + }, + "additionalProperties": false + }, + "MemberTypes": { + "enum": [ + "Constructor", + "Event", + "Field", + "Method", + "Property", + "TypeInfo", + "Custom", + "NestedType", + "All" + ], + "type": "string" + }, + "MethodAttributes": { + "enum": [ + "PrivateScope", + "Private", + "FamANDAssem", + "Assembly", + "Family", + "FamORAssem", + "Public", + "MemberAccessMask", + "UnmanagedExport", + "Static", + "Final", + "Virtual", + "HideBySig", + "NewSlot", + "CheckAccessOnOverride", + "Abstract", + "SpecialName", + "RTSpecialName", + "PinvokeImpl", + "HasSecurity", + "RequireSecObject", + "ReservedMask" + ], + "type": "string" + }, + "MethodBase": { + "type": "object", + "properties": { + "memberType": { + "$ref": "#/components/schemas/MemberTypes" + }, + "name": { + "type": "string", + "readOnly": true + }, + "declaringType": { + "$ref": "#/components/schemas/Type" + }, + "reflectedType": { + "$ref": "#/components/schemas/Type" + }, + "module": { + "$ref": "#/components/schemas/Module" + }, + "customAttributes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CustomAttributeData" + }, + "readOnly": true + }, + "isCollectible": { + "type": "boolean", + "readOnly": true + }, + "metadataToken": { + "type": "integer", + "format": "int32", + "readOnly": true + }, + "attributes": { + "$ref": "#/components/schemas/MethodAttributes" + }, + "methodImplementationFlags": { + "$ref": "#/components/schemas/MethodImplAttributes" + }, + "callingConvention": { + "$ref": "#/components/schemas/CallingConventions" + }, + "isAbstract": { + "type": "boolean", + "readOnly": true + }, + "isConstructor": { + "type": "boolean", + "readOnly": true + }, + "isFinal": { + "type": "boolean", + "readOnly": true + }, + "isHideBySig": { + "type": "boolean", + "readOnly": true + }, + "isSpecialName": { + "type": "boolean", + "readOnly": true + }, + "isStatic": { + "type": "boolean", + "readOnly": true + }, + "isVirtual": { + "type": "boolean", + "readOnly": true + }, + "isAssembly": { + "type": "boolean", + "readOnly": true + }, + "isFamily": { + "type": "boolean", + "readOnly": true + }, + "isFamilyAndAssembly": { + "type": "boolean", + "readOnly": true + }, + "isFamilyOrAssembly": { + "type": "boolean", + "readOnly": true + }, + "isPrivate": { + "type": "boolean", + "readOnly": true + }, + "isPublic": { + "type": "boolean", + "readOnly": true + }, + "isConstructedGenericMethod": { + "type": "boolean", + "readOnly": true + }, + "isGenericMethod": { + "type": "boolean", + "readOnly": true + }, + "isGenericMethodDefinition": { + "type": "boolean", + "readOnly": true + }, + "containsGenericParameters": { + "type": "boolean", + "readOnly": true + }, + "methodHandle": { + "$ref": "#/components/schemas/RuntimeMethodHandle" + }, + "isSecurityCritical": { + "type": "boolean", + "readOnly": true + }, + "isSecuritySafeCritical": { + "type": "boolean", + "readOnly": true + }, + "isSecurityTransparent": { + "type": "boolean", + "readOnly": true + } + }, + "additionalProperties": false + }, + "MethodImplAttributes": { + "enum": [ + "IL", + "Native", + "OPTIL", + "CodeTypeMask", + "ManagedMask", + "NoInlining", + "ForwardRef", + "Synchronized", + "NoOptimization", + "PreserveSig", + "AggressiveInlining", + "AggressiveOptimization", + "InternalCall", + "MaxMethodImplVal" + ], + "type": "string" + }, + "MethodInfo": { + "type": "object", + "properties": { + "name": { + "type": "string", + "readOnly": true + }, + "declaringType": { + "$ref": "#/components/schemas/Type" + }, + "reflectedType": { + "$ref": "#/components/schemas/Type" + }, + "module": { + "$ref": "#/components/schemas/Module" + }, + "customAttributes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CustomAttributeData" + }, + "readOnly": true + }, + "isCollectible": { + "type": "boolean", + "readOnly": true + }, + "metadataToken": { + "type": "integer", + "format": "int32", + "readOnly": true + }, + "attributes": { + "$ref": "#/components/schemas/MethodAttributes" + }, + "methodImplementationFlags": { + "$ref": "#/components/schemas/MethodImplAttributes" + }, + "callingConvention": { + "$ref": "#/components/schemas/CallingConventions" + }, + "isAbstract": { + "type": "boolean", + "readOnly": true + }, + "isConstructor": { + "type": "boolean", + "readOnly": true + }, + "isFinal": { + "type": "boolean", + "readOnly": true + }, + "isHideBySig": { + "type": "boolean", + "readOnly": true + }, + "isSpecialName": { + "type": "boolean", + "readOnly": true + }, + "isStatic": { + "type": "boolean", + "readOnly": true + }, + "isVirtual": { + "type": "boolean", + "readOnly": true + }, + "isAssembly": { + "type": "boolean", + "readOnly": true + }, + "isFamily": { + "type": "boolean", + "readOnly": true + }, + "isFamilyAndAssembly": { + "type": "boolean", + "readOnly": true + }, + "isFamilyOrAssembly": { + "type": "boolean", + "readOnly": true + }, + "isPrivate": { + "type": "boolean", + "readOnly": true + }, + "isPublic": { + "type": "boolean", + "readOnly": true + }, + "isConstructedGenericMethod": { + "type": "boolean", + "readOnly": true + }, + "isGenericMethod": { + "type": "boolean", + "readOnly": true + }, + "isGenericMethodDefinition": { + "type": "boolean", + "readOnly": true + }, + "containsGenericParameters": { + "type": "boolean", + "readOnly": true + }, + "methodHandle": { + "$ref": "#/components/schemas/RuntimeMethodHandle" + }, + "isSecurityCritical": { + "type": "boolean", + "readOnly": true + }, + "isSecuritySafeCritical": { + "type": "boolean", + "readOnly": true + }, + "isSecurityTransparent": { + "type": "boolean", + "readOnly": true + }, + "memberType": { + "$ref": "#/components/schemas/MemberTypes" + }, + "returnParameter": { + "$ref": "#/components/schemas/ParameterInfo" + }, + "returnType": { + "$ref": "#/components/schemas/Type" + }, + "returnTypeCustomAttributes": { + "$ref": "#/components/schemas/ICustomAttributeProvider" + } + }, + "additionalProperties": false + }, + "Module": { + "type": "object", + "properties": { + "assembly": { + "$ref": "#/components/schemas/Assembly" + }, + "fullyQualifiedName": { + "type": "string", + "readOnly": true + }, + "name": { + "type": "string", + "readOnly": true + }, + "mdStreamVersion": { + "type": "integer", + "format": "int32", + "readOnly": true + }, + "moduleVersionId": { + "pattern": "^[a-zA-Z0-9-_]{22}$", + "type": "string", + "readOnly": true + }, + "scopeName": { + "type": "string", + "readOnly": true + }, + "moduleHandle": { + "$ref": "#/components/schemas/ModuleHandle" + }, + "customAttributes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CustomAttributeData" + }, + "readOnly": true + }, + "metadataToken": { + "type": "integer", + "format": "int32", + "readOnly": true + } + }, + "additionalProperties": false + }, + "ModuleHandle": { + "type": "object", + "properties": { + "mdStreamVersion": { + "type": "integer", + "format": "int32", + "readOnly": true + } + }, + "additionalProperties": false + }, + "ParameterAttributes": { + "enum": [ + "None", + "In", + "Out", + "Lcid", + "Retval", + "Optional", + "HasDefault", + "HasFieldMarshal", + "Reserved3", + "Reserved4", + "ReservedMask" + ], + "type": "string" + }, + "ParameterInfo": { + "type": "object", + "properties": { + "attributes": { + "$ref": "#/components/schemas/ParameterAttributes" + }, + "member": { + "$ref": "#/components/schemas/MemberInfo" + }, + "name": { + "type": "string", + "nullable": true, + "readOnly": true + }, + "parameterType": { + "$ref": "#/components/schemas/Type" + }, + "position": { + "type": "integer", + "format": "int32", + "readOnly": true + }, + "isIn": { + "type": "boolean", + "readOnly": true + }, + "isLcid": { + "type": "boolean", + "readOnly": true + }, + "isOptional": { + "type": "boolean", + "readOnly": true + }, + "isOut": { + "type": "boolean", + "readOnly": true + }, + "isRetval": { + "type": "boolean", + "readOnly": true + }, + "defaultValue": { + "nullable": true, + "readOnly": true + }, + "rawDefaultValue": { + "nullable": true, + "readOnly": true + }, + "hasDefaultValue": { + "type": "boolean", + "readOnly": true + }, + "customAttributes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CustomAttributeData" + }, + "readOnly": true + }, + "metadataToken": { + "type": "integer", + "format": "int32", + "readOnly": true + } + }, + "additionalProperties": false + }, + "ProblemDetails": { + "type": "object", + "properties": { + "type": { + "type": "string", + "nullable": true + }, + "title": { + "type": "string", + "nullable": true + }, + "status": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "detail": { + "type": "string", + "nullable": true + }, + "instance": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": { } + }, + "PropertyAttributes": { + "enum": [ + "None", + "SpecialName", + "RTSpecialName", + "HasDefault", + "Reserved2", + "Reserved3", + "Reserved4", + "ReservedMask" + ], + "type": "string" + }, + "PropertyInfo": { + "type": "object", + "properties": { + "name": { + "type": "string", + "readOnly": true + }, + "declaringType": { + "$ref": "#/components/schemas/Type" + }, + "reflectedType": { + "$ref": "#/components/schemas/Type" + }, + "module": { + "$ref": "#/components/schemas/Module" + }, + "customAttributes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CustomAttributeData" + }, + "readOnly": true + }, + "isCollectible": { + "type": "boolean", + "readOnly": true + }, + "metadataToken": { + "type": "integer", + "format": "int32", + "readOnly": true + }, + "memberType": { + "$ref": "#/components/schemas/MemberTypes" + }, + "propertyType": { + "$ref": "#/components/schemas/Type" + }, + "attributes": { + "$ref": "#/components/schemas/PropertyAttributes" + }, + "isSpecialName": { + "type": "boolean", + "readOnly": true + }, + "canRead": { + "type": "boolean", + "readOnly": true + }, + "canWrite": { + "type": "boolean", + "readOnly": true + }, + "getMethod": { + "$ref": "#/components/schemas/MethodInfo" + }, + "setMethod": { + "$ref": "#/components/schemas/MethodInfo" + } + }, + "additionalProperties": false + }, + "RecoverAccountRequest": { + "required": [ + "email", + "username" + ], + "type": "object", + "properties": { + "username": { + "minLength": 1, + "type": "string", + "description": "The user's name." + }, + "email": { + "minLength": 1, + "type": "string", + "description": "The user's email address.", + "format": "email" + } + }, + "additionalProperties": false + }, + "RegisterRequest": { + "required": [ + "email", + "password", + "username" + ], + "type": "object", + "properties": { + "username": { + "minLength": 1, + "type": "string", + "description": "The username of the `User`. It:\r\n
  • must be [2, 25] in length;
  • must be made up of letters sandwiching zero or one of:
    • hyphen;
    • underscore; or
    • apostrophe
\r\nUsernames are saved case-sensitively, but matched against case-insensitively.\r\nA `User` may not register with the name 'Cool' when another `User` with the name 'cool'\r\nexists.", + "example": "J'on-Doe" + }, + "email": { + "minLength": 1, + "type": "string", + "description": "The `User`'s email address.", + "example": "john.doe@example.com" + }, + "password": { + "minLength": 1, + "type": "string", + "description": "The `User`'s password. It:\r\n
  • supports Unicode;
  • must be [8, 80] in length;
  • must have at least:
    • one uppercase letter;
    • one lowercase letter; and
    • one number.
", + "example": "P4ssword" + } + }, + "additionalProperties": false, + "description": "This request object is sent when a `User` is attempting to register." + }, + "RestoreLeaderboardResult": { + "type": "object", + "properties": { + "value": { + "nullable": true, + "readOnly": true + }, + "index": { + "type": "integer", + "format": "int32", + "readOnly": true + }, + "isT0": { + "type": "boolean", + "readOnly": true + }, + "isT1": { + "type": "boolean", + "readOnly": true + }, + "isT2": { + "type": "boolean", + "readOnly": true + }, + "asT0": { + "$ref": "#/components/schemas/Task" + }, + "asT1": { + "$ref": "#/components/schemas/LeaderboardNotFound" + }, + "asT2": { + "$ref": "#/components/schemas/LeaderboardNeverDeleted" + } + }, + "additionalProperties": false + }, + "RunType": { + "enum": [ + "Time", + "Score" + ], + "type": "string" + }, + "RunViewModel": { + "required": [ + "$type", + "categoryId", + "createdAt", + "deletedAt", + "id", + "info", + "updatedAt", + "userId" + ], + "type": "object", + "properties": { + "$type": { + "type": "string" + }, + "id": { + "pattern": "^[a-zA-Z0-9-_]{22}$", + "type": "string", + "description": "The unique identifier of the `Run`.\n\r\nGenerated on creation." + }, + "info": { + "type": "string", + "description": "User-provided details about the run.", + "nullable": true + }, + "createdAt": { + "type": "string", + "description": "The time the run was created.", + "format": "date-time", + "example": "1984-01-01T00:00:00Z" + }, + "updatedAt": { + "type": "string", + "description": "The last time the run was updated or null.", + "format": "date-time", + "nullable": true, + "example": "1984-01-01T00:00:00Z" + }, + "deletedAt": { + "type": "string", + "description": "The time at which the run was deleted, or null if the run has not been deleted.", + "format": "date-time", + "nullable": true, + "example": "1984-01-01T00:00:00Z" + }, + "categoryId": { + "type": "integer", + "description": "The ID of the `Category` for `Run`.", + "format": "int64" + }, + "userId": { + "pattern": "^[a-zA-Z0-9-_]{22}$", + "type": "string", + "description": "The ID of the LeaderboardBackend.Models.Entities.User who submitted this run." + } + }, + "additionalProperties": false, + "discriminator": { + "propertyName": "$type", + "mapping": { + "Time": "#/components/schemas/TimedRunViewModel", + "Score": "#/components/schemas/ScoredRunViewModel" + } + } + }, + "RuntimeFieldHandle": { + "type": "object", + "properties": { + "value": { + "$ref": "#/components/schemas/IntPtr" + } + }, + "additionalProperties": false + }, + "RuntimeMethodHandle": { + "type": "object", + "properties": { + "value": { + "$ref": "#/components/schemas/IntPtr" + } + }, + "additionalProperties": false + }, + "RuntimeTypeHandle": { + "type": "object", + "properties": { + "value": { + "$ref": "#/components/schemas/IntPtr" + } + }, + "additionalProperties": false + }, + "ScoredRunViewModel": { + "allOf": [ + { + "$ref": "#/components/schemas/RunViewModel" + }, + { + "required": [ + "score" + ], + "type": "object", + "properties": { + "score": { + "type": "integer", + "description": "The score achieved during the run.", + "format": "int64" + } + }, + "additionalProperties": false + } + ] + }, + "SecurityRuleSet": { + "enum": [ + "None", + "Level1", + "Level2" + ], + "type": "string" + }, + "SortDirection": { + "enum": [ + "Ascending", + "Descending" + ], + "type": "string" + }, + "StructLayoutAttribute": { + "type": "object", + "properties": { + "typeId": { + "readOnly": true + }, + "value": { + "$ref": "#/components/schemas/LayoutKind" + } + }, + "additionalProperties": false + }, + "Task": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32", + "readOnly": true + }, + "exception": { + "$ref": "#/components/schemas/AggregateException" + }, + "status": { + "$ref": "#/components/schemas/TaskStatus" + }, + "isCanceled": { + "type": "boolean", + "readOnly": true + }, + "isCompleted": { + "type": "boolean", + "readOnly": true + }, + "isCompletedSuccessfully": { + "type": "boolean", + "readOnly": true + }, + "creationOptions": { + "$ref": "#/components/schemas/TaskCreationOptions" + }, + "asyncState": { + "nullable": true, + "readOnly": true + }, + "isFaulted": { + "type": "boolean", + "readOnly": true + } + }, + "additionalProperties": false + }, + "TaskCreationOptions": { + "enum": [ + "None", + "PreferFairness", + "LongRunning", + "AttachedToParent", + "DenyChildAttach", + "HideScheduler", + "RunContinuationsAsynchronously" + ], + "type": "string" + }, + "TaskStatus": { + "enum": [ + "Created", + "WaitingForActivation", + "WaitingToRun", + "Running", + "WaitingForChildrenToComplete", + "RanToCompletion", + "Canceled", + "Faulted" + ], + "type": "string" + }, + "TimedRunViewModel": { + "allOf": [ + { + "$ref": "#/components/schemas/RunViewModel" }, { "required": [ @@ -1395,6 +2791,696 @@ } ] }, + "Type": { + "type": "object", + "properties": { + "name": { + "type": "string", + "readOnly": true + }, + "customAttributes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CustomAttributeData" + }, + "readOnly": true + }, + "isCollectible": { + "type": "boolean", + "readOnly": true + }, + "metadataToken": { + "type": "integer", + "format": "int32", + "readOnly": true + }, + "isInterface": { + "type": "boolean", + "readOnly": true + }, + "memberType": { + "$ref": "#/components/schemas/MemberTypes" + }, + "namespace": { + "type": "string", + "nullable": true, + "readOnly": true + }, + "assemblyQualifiedName": { + "type": "string", + "nullable": true, + "readOnly": true + }, + "fullName": { + "type": "string", + "nullable": true, + "readOnly": true + }, + "assembly": { + "$ref": "#/components/schemas/Assembly" + }, + "module": { + "$ref": "#/components/schemas/Module" + }, + "isNested": { + "type": "boolean", + "readOnly": true + }, + "declaringType": { + "$ref": "#/components/schemas/Type" + }, + "declaringMethod": { + "$ref": "#/components/schemas/MethodBase" + }, + "reflectedType": { + "$ref": "#/components/schemas/Type" + }, + "underlyingSystemType": { + "$ref": "#/components/schemas/Type" + }, + "isTypeDefinition": { + "type": "boolean", + "readOnly": true + }, + "isArray": { + "type": "boolean", + "readOnly": true + }, + "isByRef": { + "type": "boolean", + "readOnly": true + }, + "isPointer": { + "type": "boolean", + "readOnly": true + }, + "isConstructedGenericType": { + "type": "boolean", + "readOnly": true + }, + "isGenericParameter": { + "type": "boolean", + "readOnly": true + }, + "isGenericTypeParameter": { + "type": "boolean", + "readOnly": true + }, + "isGenericMethodParameter": { + "type": "boolean", + "readOnly": true + }, + "isGenericType": { + "type": "boolean", + "readOnly": true + }, + "isGenericTypeDefinition": { + "type": "boolean", + "readOnly": true + }, + "isSZArray": { + "type": "boolean", + "readOnly": true + }, + "isVariableBoundArray": { + "type": "boolean", + "readOnly": true + }, + "isByRefLike": { + "type": "boolean", + "readOnly": true + }, + "isFunctionPointer": { + "type": "boolean", + "readOnly": true + }, + "isUnmanagedFunctionPointer": { + "type": "boolean", + "readOnly": true + }, + "hasElementType": { + "type": "boolean", + "readOnly": true + }, + "genericTypeArguments": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Type" + }, + "readOnly": true + }, + "genericParameterPosition": { + "type": "integer", + "format": "int32", + "readOnly": true + }, + "genericParameterAttributes": { + "$ref": "#/components/schemas/GenericParameterAttributes" + }, + "attributes": { + "$ref": "#/components/schemas/TypeAttributes" + }, + "isAbstract": { + "type": "boolean", + "readOnly": true + }, + "isImport": { + "type": "boolean", + "readOnly": true + }, + "isSealed": { + "type": "boolean", + "readOnly": true + }, + "isSpecialName": { + "type": "boolean", + "readOnly": true + }, + "isClass": { + "type": "boolean", + "readOnly": true + }, + "isNestedAssembly": { + "type": "boolean", + "readOnly": true + }, + "isNestedFamANDAssem": { + "type": "boolean", + "readOnly": true + }, + "isNestedFamily": { + "type": "boolean", + "readOnly": true + }, + "isNestedFamORAssem": { + "type": "boolean", + "readOnly": true + }, + "isNestedPrivate": { + "type": "boolean", + "readOnly": true + }, + "isNestedPublic": { + "type": "boolean", + "readOnly": true + }, + "isNotPublic": { + "type": "boolean", + "readOnly": true + }, + "isPublic": { + "type": "boolean", + "readOnly": true + }, + "isAutoLayout": { + "type": "boolean", + "readOnly": true + }, + "isExplicitLayout": { + "type": "boolean", + "readOnly": true + }, + "isLayoutSequential": { + "type": "boolean", + "readOnly": true + }, + "isAnsiClass": { + "type": "boolean", + "readOnly": true + }, + "isAutoClass": { + "type": "boolean", + "readOnly": true + }, + "isUnicodeClass": { + "type": "boolean", + "readOnly": true + }, + "isCOMObject": { + "type": "boolean", + "readOnly": true + }, + "isContextful": { + "type": "boolean", + "readOnly": true + }, + "isEnum": { + "type": "boolean", + "readOnly": true + }, + "isMarshalByRef": { + "type": "boolean", + "readOnly": true + }, + "isPrimitive": { + "type": "boolean", + "readOnly": true + }, + "isValueType": { + "type": "boolean", + "readOnly": true + }, + "isSignatureType": { + "type": "boolean", + "readOnly": true + }, + "isSecurityCritical": { + "type": "boolean", + "readOnly": true + }, + "isSecuritySafeCritical": { + "type": "boolean", + "readOnly": true + }, + "isSecurityTransparent": { + "type": "boolean", + "readOnly": true + }, + "structLayoutAttribute": { + "$ref": "#/components/schemas/StructLayoutAttribute" + }, + "typeInitializer": { + "$ref": "#/components/schemas/ConstructorInfo" + }, + "typeHandle": { + "$ref": "#/components/schemas/RuntimeTypeHandle" + }, + "guid": { + "pattern": "^[a-zA-Z0-9-_]{22}$", + "type": "string", + "readOnly": true + }, + "baseType": { + "$ref": "#/components/schemas/Type" + }, + "isSerializable": { + "type": "boolean", + "readOnly": true, + "deprecated": true + }, + "containsGenericParameters": { + "type": "boolean", + "readOnly": true + }, + "isVisible": { + "type": "boolean", + "readOnly": true + } + }, + "additionalProperties": false + }, + "TypeAttributes": { + "enum": [ + "NotPublic", + "Public", + "NestedPublic", + "NestedPrivate", + "NestedFamily", + "NestedAssembly", + "NestedFamANDAssem", + "VisibilityMask", + "SequentialLayout", + "ExplicitLayout", + "LayoutMask", + "Interface", + "Abstract", + "Sealed", + "SpecialName", + "RTSpecialName", + "Import", + "Serializable", + "WindowsRuntime", + "UnicodeClass", + "AutoClass", + "StringFormatMask", + "HasSecurity", + "ReservedMask", + "BeforeFieldInit", + "CustomFormatMask" + ], + "type": "string" + }, + "TypeInfo": { + "type": "object", + "properties": { + "name": { + "type": "string", + "readOnly": true + }, + "customAttributes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CustomAttributeData" + }, + "readOnly": true + }, + "isCollectible": { + "type": "boolean", + "readOnly": true + }, + "metadataToken": { + "type": "integer", + "format": "int32", + "readOnly": true + }, + "isInterface": { + "type": "boolean", + "readOnly": true + }, + "memberType": { + "$ref": "#/components/schemas/MemberTypes" + }, + "namespace": { + "type": "string", + "nullable": true, + "readOnly": true + }, + "assemblyQualifiedName": { + "type": "string", + "nullable": true, + "readOnly": true + }, + "fullName": { + "type": "string", + "nullable": true, + "readOnly": true + }, + "assembly": { + "$ref": "#/components/schemas/Assembly" + }, + "module": { + "$ref": "#/components/schemas/Module" + }, + "isNested": { + "type": "boolean", + "readOnly": true + }, + "declaringType": { + "$ref": "#/components/schemas/Type" + }, + "declaringMethod": { + "$ref": "#/components/schemas/MethodBase" + }, + "reflectedType": { + "$ref": "#/components/schemas/Type" + }, + "underlyingSystemType": { + "$ref": "#/components/schemas/Type" + }, + "isTypeDefinition": { + "type": "boolean", + "readOnly": true + }, + "isArray": { + "type": "boolean", + "readOnly": true + }, + "isByRef": { + "type": "boolean", + "readOnly": true + }, + "isPointer": { + "type": "boolean", + "readOnly": true + }, + "isConstructedGenericType": { + "type": "boolean", + "readOnly": true + }, + "isGenericParameter": { + "type": "boolean", + "readOnly": true + }, + "isGenericTypeParameter": { + "type": "boolean", + "readOnly": true + }, + "isGenericMethodParameter": { + "type": "boolean", + "readOnly": true + }, + "isGenericType": { + "type": "boolean", + "readOnly": true + }, + "isGenericTypeDefinition": { + "type": "boolean", + "readOnly": true + }, + "isSZArray": { + "type": "boolean", + "readOnly": true + }, + "isVariableBoundArray": { + "type": "boolean", + "readOnly": true + }, + "isByRefLike": { + "type": "boolean", + "readOnly": true + }, + "isFunctionPointer": { + "type": "boolean", + "readOnly": true + }, + "isUnmanagedFunctionPointer": { + "type": "boolean", + "readOnly": true + }, + "hasElementType": { + "type": "boolean", + "readOnly": true + }, + "genericTypeArguments": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Type" + }, + "readOnly": true + }, + "genericParameterPosition": { + "type": "integer", + "format": "int32", + "readOnly": true + }, + "genericParameterAttributes": { + "$ref": "#/components/schemas/GenericParameterAttributes" + }, + "attributes": { + "$ref": "#/components/schemas/TypeAttributes" + }, + "isAbstract": { + "type": "boolean", + "readOnly": true + }, + "isImport": { + "type": "boolean", + "readOnly": true + }, + "isSealed": { + "type": "boolean", + "readOnly": true + }, + "isSpecialName": { + "type": "boolean", + "readOnly": true + }, + "isClass": { + "type": "boolean", + "readOnly": true + }, + "isNestedAssembly": { + "type": "boolean", + "readOnly": true + }, + "isNestedFamANDAssem": { + "type": "boolean", + "readOnly": true + }, + "isNestedFamily": { + "type": "boolean", + "readOnly": true + }, + "isNestedFamORAssem": { + "type": "boolean", + "readOnly": true + }, + "isNestedPrivate": { + "type": "boolean", + "readOnly": true + }, + "isNestedPublic": { + "type": "boolean", + "readOnly": true + }, + "isNotPublic": { + "type": "boolean", + "readOnly": true + }, + "isPublic": { + "type": "boolean", + "readOnly": true + }, + "isAutoLayout": { + "type": "boolean", + "readOnly": true + }, + "isExplicitLayout": { + "type": "boolean", + "readOnly": true + }, + "isLayoutSequential": { + "type": "boolean", + "readOnly": true + }, + "isAnsiClass": { + "type": "boolean", + "readOnly": true + }, + "isAutoClass": { + "type": "boolean", + "readOnly": true + }, + "isUnicodeClass": { + "type": "boolean", + "readOnly": true + }, + "isCOMObject": { + "type": "boolean", + "readOnly": true + }, + "isContextful": { + "type": "boolean", + "readOnly": true + }, + "isEnum": { + "type": "boolean", + "readOnly": true + }, + "isMarshalByRef": { + "type": "boolean", + "readOnly": true + }, + "isPrimitive": { + "type": "boolean", + "readOnly": true + }, + "isValueType": { + "type": "boolean", + "readOnly": true + }, + "isSignatureType": { + "type": "boolean", + "readOnly": true + }, + "isSecurityCritical": { + "type": "boolean", + "readOnly": true + }, + "isSecuritySafeCritical": { + "type": "boolean", + "readOnly": true + }, + "isSecurityTransparent": { + "type": "boolean", + "readOnly": true + }, + "structLayoutAttribute": { + "$ref": "#/components/schemas/StructLayoutAttribute" + }, + "typeInitializer": { + "$ref": "#/components/schemas/ConstructorInfo" + }, + "typeHandle": { + "$ref": "#/components/schemas/RuntimeTypeHandle" + }, + "guid": { + "pattern": "^[a-zA-Z0-9-_]{22}$", + "type": "string", + "readOnly": true + }, + "baseType": { + "$ref": "#/components/schemas/Type" + }, + "isSerializable": { + "type": "boolean", + "readOnly": true, + "deprecated": true + }, + "containsGenericParameters": { + "type": "boolean", + "readOnly": true + }, + "isVisible": { + "type": "boolean", + "readOnly": true + }, + "genericTypeParameters": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Type" + }, + "readOnly": true + }, + "declaredConstructors": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ConstructorInfo" + }, + "readOnly": true + }, + "declaredEvents": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EventInfo" + }, + "readOnly": true + }, + "declaredFields": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FieldInfo" + }, + "readOnly": true + }, + "declaredMembers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MemberInfo" + }, + "readOnly": true + }, + "declaredMethods": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MethodInfo" + }, + "readOnly": true + }, + "declaredNestedTypes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TypeInfo" + }, + "readOnly": true + }, + "declaredProperties": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PropertyInfo" + }, + "readOnly": true + }, + "implementedInterfaces": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Type" + }, + "readOnly": true + } + }, + "additionalProperties": false + }, "UserRole": { "enum": [ "Registered", From 1ccf75c88586409c17542a615ebd755428298642 Mon Sep 17 00:00:00 2001 From: zysim <9867871+zysim@users.noreply.github.com> Date: Mon, 14 Oct 2024 23:29:06 +0800 Subject: [PATCH 02/11] Followed linter suggestions --- .../TestApi/TestApiClient.cs | 32 +++++++------------ 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/LeaderboardBackend.Test/TestApi/TestApiClient.cs b/LeaderboardBackend.Test/TestApi/TestApiClient.cs index 1e5aab1a..5c448a0c 100644 --- a/LeaderboardBackend.Test/TestApi/TestApiClient.cs +++ b/LeaderboardBackend.Test/TestApi/TestApiClient.cs @@ -36,25 +36,17 @@ public TestApiClient(HttpClient client) _client = client; } - public async Task Get(string endpoint, HttpRequestInit init) - { - return await SendAndRead(endpoint, init with { Method = HttpMethod.Get }); - } + public async Task Get(string endpoint, HttpRequestInit init) => + await SendAndRead(endpoint, init with { Method = HttpMethod.Get }); - public async Task Post(string endpoint, HttpRequestInit init) - { - return await SendAndRead(endpoint, init with { Method = HttpMethod.Post }); - } + public async Task Post(string endpoint, HttpRequestInit init) => + await SendAndRead(endpoint, init with { Method = HttpMethod.Post }); - public async Task Put(string endpoint, HttpRequestInit init) - { - return await Send(endpoint, init with { Method = HttpMethod.Put }); - } + public async Task Put(string endpoint, HttpRequestInit init) => + await Send(endpoint, init with { Method = HttpMethod.Put }); - public async Task Delete(string endpoint, HttpRequestInit init) - { - return await Send(endpoint, init with { Method = HttpMethod.Delete }); - } + public async Task Delete(string endpoint, HttpRequestInit init) => + await Send(endpoint, init with { Method = HttpMethod.Delete }); private async Task SendAndRead(string endpoint, HttpRequestInit init) { @@ -82,7 +74,7 @@ private static async Task ReadFromResponseBody(HttpResponseMessage respons string rawJson = await response.Content.ReadAsStringAsync(); T? obj = JsonSerializer.Deserialize(rawJson, TestInitCommonFields.JsonSerializerOptions); - Assert.NotNull(obj); + obj.Should().NotBeNull(); return obj!; } @@ -91,9 +83,8 @@ private static HttpRequestMessage CreateRequestMessage( string endpoint, HttpRequestInit init, JsonSerializerOptions options - ) - { - return new(init.Method, endpoint) + ) => + new(init.Method, endpoint) { Headers = { Authorization = new(JwtBearerDefaults.AuthenticationScheme, init.Jwt) }, Content = init.Body switch @@ -106,5 +97,4 @@ not null _ => default } }; - } } From 6a6e74959bc4330d06ed9d1320f69c50e978a70f Mon Sep 17 00:00:00 2001 From: zysim <9867871+zysim@users.noreply.github.com> Date: Wed, 16 Oct 2024 20:49:53 +0800 Subject: [PATCH 03/11] Fix tests --- LeaderboardBackend.Test/Leaderboards.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/LeaderboardBackend.Test/Leaderboards.cs b/LeaderboardBackend.Test/Leaderboards.cs index 1dc1407f..545e787a 100644 --- a/LeaderboardBackend.Test/Leaderboards.cs +++ b/LeaderboardBackend.Test/Leaderboards.cs @@ -373,7 +373,7 @@ public async Task RestoreLeaderboard_OK() }); res.StatusCode.Should().Be(HttpStatusCode.NoContent); - + context.ChangeTracker.Clear(); Leaderboard? board = await context.Leaderboards.FindAsync(deletedBoard.Id); board.Should().NotBeNull(); // TODO: `DeletedAt` is still not null here. Don't know how to fix it. @@ -399,7 +399,7 @@ public async Task RestoreLeaderboard_NotFound_WasNeverDeleted() Leaderboard board = new() { Name = "Super Mario World", - Slug = "super-mario-world-deleted", + Slug = "super-mario-world-non-deleted", }; context.Leaderboards.Add(board); From 227e632a292095dbedb554be72e413da99b6cca2 Mon Sep 17 00:00:00 2001 From: zysim <9867871+zysim@users.noreply.github.com> Date: Wed, 16 Oct 2024 21:26:41 +0800 Subject: [PATCH 04/11] Return board on restore success --- LeaderboardBackend.Test/Leaderboards.cs | 21 +- .../TestApi/TestApiClient.cs | 4 +- .../Controllers/LeaderboardsController.cs | 11 +- .../Services/ILeaderboardService.cs | 2 +- .../Services/Impl/LeaderboardService.cs | 6 +- LeaderboardBackend/openapi.json | 2434 ++--------------- 6 files changed, 226 insertions(+), 2252 deletions(-) diff --git a/LeaderboardBackend.Test/Leaderboards.cs b/LeaderboardBackend.Test/Leaderboards.cs index 545e787a..8a8c040a 100644 --- a/LeaderboardBackend.Test/Leaderboards.cs +++ b/LeaderboardBackend.Test/Leaderboards.cs @@ -363,27 +363,30 @@ public async Task RestoreLeaderboard_OK() DeletedAt = _clock.GetCurrentInstant() }; + Instant now = Instant.FromUnixTimeSeconds(1); + _clock.Reset(now); + context.Leaderboards.Add(deletedBoard); await context.SaveChangesAsync(); deletedBoard.Id.Should().NotBe(default); - HttpResponseMessage res = await _apiClient.Put($"/leaderboard/{deletedBoard.Id}/restore", new() + _clock.AdvanceMinutes(1); + + LeaderboardViewModel res = await _apiClient.Put($"/leaderboard/{deletedBoard.Id}/restore", new() { Jwt = _jwt }); - res.StatusCode.Should().Be(HttpStatusCode.NoContent); - context.ChangeTracker.Clear(); - Leaderboard? board = await context.Leaderboards.FindAsync(deletedBoard.Id); - board.Should().NotBeNull(); - // TODO: `DeletedAt` is still not null here. Don't know how to fix it. - board!.DeletedAt.Should().BeNull(); + res.Id.Should().Be(deletedBoard.Id); + res.Slug.Should().Be(deletedBoard.Slug); + res.UpdatedAt.Should().Be(res.CreatedAt + Duration.FromMinutes(1)); + res.DeletedAt.Should().BeNull(); } [Test] public async Task RestoreLeaderboard_NotFound() { - Func> act = async () => await _apiClient.Put($"/leaderboard/100/restore", new() + Func> act = async () => await _apiClient.Put($"/leaderboard/100/restore", new() { Jwt = _jwt }); @@ -406,7 +409,7 @@ public async Task RestoreLeaderboard_NotFound_WasNeverDeleted() await context.SaveChangesAsync(); board.Id.Should().NotBe(default); - Func> act = async () => await _apiClient.Put($"/leaderboard/{board.Id}/restore", new() + Func> act = async () => await _apiClient.Put($"/leaderboard/{board.Id}/restore", new() { Jwt = _jwt }); diff --git a/LeaderboardBackend.Test/TestApi/TestApiClient.cs b/LeaderboardBackend.Test/TestApi/TestApiClient.cs index 5c448a0c..d9403dd5 100644 --- a/LeaderboardBackend.Test/TestApi/TestApiClient.cs +++ b/LeaderboardBackend.Test/TestApi/TestApiClient.cs @@ -42,8 +42,8 @@ public async Task Get(string endpoint, HttpRequestInit ini public async Task Post(string endpoint, HttpRequestInit init) => await SendAndRead(endpoint, init with { Method = HttpMethod.Post }); - public async Task Put(string endpoint, HttpRequestInit init) => - await Send(endpoint, init with { Method = HttpMethod.Put }); + public async Task Put(string endpoint, HttpRequestInit init) => + await SendAndRead(endpoint, init with { Method = HttpMethod.Put }); public async Task Delete(string endpoint, HttpRequestInit init) => await Send(endpoint, init with { Method = HttpMethod.Delete }); diff --git a/LeaderboardBackend/Controllers/LeaderboardsController.cs b/LeaderboardBackend/Controllers/LeaderboardsController.cs index af50e070..0b66c095 100644 --- a/LeaderboardBackend/Controllers/LeaderboardsController.cs +++ b/LeaderboardBackend/Controllers/LeaderboardsController.cs @@ -31,7 +31,7 @@ public async Task> GetLeaderboard(long id) [AllowAnonymous] [HttpGet("api/leaderboard")] - [SwaggerOperation("Gets a Leaderboard by its slug.", OperationId = "getLeaderboardBySlug")] + [SwaggerOperation("Gets a leaderboard by its slug.", OperationId = "getLeaderboardBySlug")] [SwaggerResponse(200)] [SwaggerResponse(404)] public async Task> GetLeaderboardBySlug([FromQuery, SwaggerParameter(Required = true)] string slug) @@ -89,18 +89,19 @@ public async Task> CreateLeaderboard( [Authorize(Policy = UserTypes.ADMINISTRATOR)] [HttpPut("leaderboard/{id:long}/restore")] - [SwaggerResponse(201)] + [SwaggerOperation("Restores a deleted leaderboard.", OperationId = "restoreLeaderboard")] + [SwaggerResponse(200)] [SwaggerResponse(401)] [SwaggerResponse(403, "The requesting `User` is unauthorized to restore `Leaderboard`s.")] [SwaggerResponse(404, "The `Leaderboard` was not found, or it wasn't deleted in the first place.")] - public async Task> RestoreLeaderboard( + public async Task> RestoreLeaderboard( long id ) { RestoreLeaderboardResult r = await leaderboardService.RestoreLeaderboard(id); - return r.Match>( - _ => NoContent(), + return r.Match>( + board => Ok(LeaderboardViewModel.MapFrom(board)), notFound => NotFound(), neverDeleted => { diff --git a/LeaderboardBackend/Services/ILeaderboardService.cs b/LeaderboardBackend/Services/ILeaderboardService.cs index f2c07624..a0979e4b 100644 --- a/LeaderboardBackend/Services/ILeaderboardService.cs +++ b/LeaderboardBackend/Services/ILeaderboardService.cs @@ -18,4 +18,4 @@ public interface ILeaderboardService public partial class CreateLeaderboardResult : OneOfBase; [GenerateOneOf] -public partial class RestoreLeaderboardResult : OneOfBase; +public partial class RestoreLeaderboardResult : OneOfBase; diff --git a/LeaderboardBackend/Services/Impl/LeaderboardService.cs b/LeaderboardBackend/Services/Impl/LeaderboardService.cs index 9aa622ab..5b5130d6 100644 --- a/LeaderboardBackend/Services/Impl/LeaderboardService.cs +++ b/LeaderboardBackend/Services/Impl/LeaderboardService.cs @@ -2,11 +2,12 @@ using LeaderboardBackend.Models.Requests; using LeaderboardBackend.Result; using Microsoft.EntityFrameworkCore; +using NodaTime; using Npgsql; namespace LeaderboardBackend.Services; -public class LeaderboardService(ApplicationContext applicationContext) : ILeaderboardService +public class LeaderboardService(ApplicationContext applicationContext, IClock clock) : ILeaderboardService { public async Task GetLeaderboard(long id) => await applicationContext.Leaderboards.FindAsync(id); @@ -59,10 +60,11 @@ public async Task RestoreLeaderboard(long id) applicationContext.Leaderboards.Update(lb); + lb.UpdatedAt = clock.GetCurrentInstant(); lb.DeletedAt = null; await applicationContext.SaveChangesAsync(); - return Task.CompletedTask; + return lb; } } diff --git a/LeaderboardBackend/openapi.json b/LeaderboardBackend/openapi.json index 6185c76b..a49938e0 100644 --- a/LeaderboardBackend/openapi.json +++ b/LeaderboardBackend/openapi.json @@ -515,7 +515,7 @@ "tags": [ "Leaderboards" ], - "summary": "Gets a Leaderboard by its slug.", + "summary": "Gets a leaderboard by its slug.", "operationId": "getLeaderboardBySlug", "parameters": [ { @@ -669,6 +669,8 @@ "tags": [ "Leaderboards" ], + "summary": "Restores a deleted leaderboard.", + "operationId": "restoreLeaderboard", "parameters": [ { "name": "id", @@ -694,12 +696,12 @@ "500": { "description": "Internal Server Error" }, - "201": { - "description": "Created", + "200": { + "description": "OK", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RestoreLeaderboardResult" + "$ref": "#/components/schemas/LeaderboardViewModel" } } } @@ -967,154 +969,6 @@ }, "components": { "schemas": { - "AggregateException": { - "type": "object", - "properties": { - "targetSite": { - "$ref": "#/components/schemas/MethodBase" - }, - "data": { - "type": "object", - "additionalProperties": { }, - "readOnly": true - }, - "innerException": { - "$ref": "#/components/schemas/Exception" - }, - "helpLink": { - "type": "string", - "nullable": true - }, - "source": { - "type": "string", - "nullable": true - }, - "hResult": { - "type": "integer", - "format": "int32" - }, - "stackTrace": { - "type": "string", - "nullable": true, - "readOnly": true - }, - "innerExceptions": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Exception" - }, - "readOnly": true - }, - "message": { - "type": "string", - "readOnly": true - } - }, - "additionalProperties": false - }, - "Assembly": { - "type": "object", - "properties": { - "definedTypes": { - "type": "array", - "items": { - "$ref": "#/components/schemas/TypeInfo" - }, - "readOnly": true - }, - "exportedTypes": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Type" - }, - "readOnly": true - }, - "codeBase": { - "type": "string", - "nullable": true, - "readOnly": true, - "deprecated": true - }, - "entryPoint": { - "$ref": "#/components/schemas/MethodInfo" - }, - "fullName": { - "type": "string", - "nullable": true, - "readOnly": true - }, - "imageRuntimeVersion": { - "type": "string", - "readOnly": true - }, - "isDynamic": { - "type": "boolean", - "readOnly": true - }, - "location": { - "type": "string", - "readOnly": true - }, - "reflectionOnly": { - "type": "boolean", - "readOnly": true - }, - "isCollectible": { - "type": "boolean", - "readOnly": true - }, - "isFullyTrusted": { - "type": "boolean", - "readOnly": true - }, - "customAttributes": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CustomAttributeData" - }, - "readOnly": true - }, - "escapedCodeBase": { - "type": "string", - "readOnly": true, - "deprecated": true - }, - "manifestModule": { - "$ref": "#/components/schemas/Module" - }, - "modules": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Module" - }, - "readOnly": true - }, - "globalAssemblyCache": { - "type": "boolean", - "readOnly": true, - "deprecated": true - }, - "hostContext": { - "type": "integer", - "format": "int64", - "readOnly": true - }, - "securityRuleSet": { - "$ref": "#/components/schemas/SecurityRuleSet" - } - }, - "additionalProperties": false - }, - "CallingConventions": { - "enum": [ - "Standard", - "VarArgs", - "Any", - "HasThis", - "ExplicitThis" - ], - "type": "string" - }, "CategoryViewModel": { "required": [ "createdAt", @@ -1190,136 +1044,6 @@ }, "additionalProperties": false }, - "ConstructorInfo": { - "type": "object", - "properties": { - "name": { - "type": "string", - "readOnly": true - }, - "declaringType": { - "$ref": "#/components/schemas/Type" - }, - "reflectedType": { - "$ref": "#/components/schemas/Type" - }, - "module": { - "$ref": "#/components/schemas/Module" - }, - "customAttributes": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CustomAttributeData" - }, - "readOnly": true - }, - "isCollectible": { - "type": "boolean", - "readOnly": true - }, - "metadataToken": { - "type": "integer", - "format": "int32", - "readOnly": true - }, - "attributes": { - "$ref": "#/components/schemas/MethodAttributes" - }, - "methodImplementationFlags": { - "$ref": "#/components/schemas/MethodImplAttributes" - }, - "callingConvention": { - "$ref": "#/components/schemas/CallingConventions" - }, - "isAbstract": { - "type": "boolean", - "readOnly": true - }, - "isConstructor": { - "type": "boolean", - "readOnly": true - }, - "isFinal": { - "type": "boolean", - "readOnly": true - }, - "isHideBySig": { - "type": "boolean", - "readOnly": true - }, - "isSpecialName": { - "type": "boolean", - "readOnly": true - }, - "isStatic": { - "type": "boolean", - "readOnly": true - }, - "isVirtual": { - "type": "boolean", - "readOnly": true - }, - "isAssembly": { - "type": "boolean", - "readOnly": true - }, - "isFamily": { - "type": "boolean", - "readOnly": true - }, - "isFamilyAndAssembly": { - "type": "boolean", - "readOnly": true - }, - "isFamilyOrAssembly": { - "type": "boolean", - "readOnly": true - }, - "isPrivate": { - "type": "boolean", - "readOnly": true - }, - "isPublic": { - "type": "boolean", - "readOnly": true - }, - "isConstructedGenericMethod": { - "type": "boolean", - "readOnly": true - }, - "isGenericMethod": { - "type": "boolean", - "readOnly": true - }, - "isGenericMethodDefinition": { - "type": "boolean", - "readOnly": true - }, - "containsGenericParameters": { - "type": "boolean", - "readOnly": true - }, - "methodHandle": { - "$ref": "#/components/schemas/RuntimeMethodHandle" - }, - "isSecurityCritical": { - "type": "boolean", - "readOnly": true - }, - "isSecuritySafeCritical": { - "type": "boolean", - "readOnly": true - }, - "isSecurityTransparent": { - "type": "boolean", - "readOnly": true - }, - "memberType": { - "$ref": "#/components/schemas/MemberTypes" - } - }, - "additionalProperties": false - }, "CreateCategoryRequest": { "required": [ "info", @@ -1421,2066 +1145,310 @@ "additionalProperties": false, "description": "This request object is sent when creating a `Run`." }, - "CustomAttributeData": { + "LeaderboardViewModel": { + "required": [ + "categories", + "createdAt", + "deletedAt", + "id", + "info", + "name", + "slug", + "updatedAt" + ], "type": "object", "properties": { - "attributeType": { - "$ref": "#/components/schemas/Type" + "id": { + "type": "integer", + "description": "The unique identifier of the `Leaderboard`.\n\r\nGenerated on creation.", + "format": "int64" }, - "constructor": { - "$ref": "#/components/schemas/ConstructorInfo" + "name": { + "type": "string", + "description": "The display name of the `Leaderboard` to create.", + "example": "Foo Bar" }, - "constructorArguments": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CustomAttributeTypedArgument" - }, - "readOnly": true + "slug": { + "type": "string", + "description": "The URL-scoped unique identifier of the `Leaderboard`.\n\r\nMust be [2, 80] in length and consist only of alphanumeric characters and hyphens.", + "example": "foo-bar" + }, + "info": { + "type": "string", + "description": "The general information for the Leaderboard.", + "nullable": true, + "example": "Timer starts on selecting New Game and ends when the final boss is beaten." + }, + "createdAt": { + "type": "string", + "description": "The time the Leaderboard was created.", + "format": "date-time", + "example": "1984-01-01T00:00:00Z" }, - "namedArguments": { + "updatedAt": { + "type": "string", + "description": "The last time the Leaderboard was updated or null.", + "format": "date-time", + "nullable": true, + "example": "1984-01-01T00:00:00Z" + }, + "deletedAt": { + "type": "string", + "description": "The time at which the Leaderboard was deleted, or null if the Leaderboard has not been deleted.", + "format": "date-time", + "nullable": true, + "example": "1984-01-01T00:00:00Z" + }, + "categories": { "type": "array", "items": { - "$ref": "#/components/schemas/CustomAttributeNamedArgument" + "$ref": "#/components/schemas/CategoryViewModel" }, - "readOnly": true + "description": "A collection of `Category` entities for the `Leaderboard`." } }, - "additionalProperties": false + "additionalProperties": false, + "description": "Represents a collection of `Leaderboard` entities." }, - "CustomAttributeNamedArgument": { + "LoginRequest": { + "required": [ + "email", + "password" + ], "type": "object", "properties": { - "memberInfo": { - "$ref": "#/components/schemas/MemberInfo" - }, - "typedValue": { - "$ref": "#/components/schemas/CustomAttributeTypedArgument" - }, - "memberName": { + "email": { + "minLength": 1, "type": "string", - "readOnly": true + "description": "The `User`'s email address.", + "format": "email", + "example": "john.doe@example.com" }, - "isField": { - "type": "boolean", - "readOnly": true + "password": { + "minLength": 1, + "type": "string", + "description": "The `User`'s password. It:\r\n
  • supports Unicode;
  • must be [8, 80] in length;
  • must have at least:
    • one uppercase letter;
    • one lowercase letter; and
    • one number.
", + "example": "P4ssword" } }, - "additionalProperties": false + "additionalProperties": false, + "description": "This request object is sent when a `User` is attempting to log in." }, - "CustomAttributeTypedArgument": { + "LoginResponse": { + "required": [ + "token" + ], "type": "object", "properties": { - "argumentType": { - "$ref": "#/components/schemas/Type" - }, - "value": { - "nullable": true + "token": { + "minLength": 1, + "type": "string", + "description": "A JSON Web Token to authenticate and authorize queries with." } }, - "additionalProperties": false - }, - "EventAttributes": { - "enum": [ - "None", - "SpecialName", - "RTSpecialName" - ], - "type": "string" + "additionalProperties": false, + "description": "This response object is received upon a successful log-in request." }, - "EventInfo": { + "ProblemDetails": { "type": "object", "properties": { - "name": { + "type": { "type": "string", - "readOnly": true - }, - "declaringType": { - "$ref": "#/components/schemas/Type" - }, - "reflectedType": { - "$ref": "#/components/schemas/Type" - }, - "module": { - "$ref": "#/components/schemas/Module" - }, - "customAttributes": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CustomAttributeData" - }, - "readOnly": true + "nullable": true }, - "isCollectible": { - "type": "boolean", - "readOnly": true + "title": { + "type": "string", + "nullable": true }, - "metadataToken": { + "status": { "type": "integer", "format": "int32", - "readOnly": true - }, - "memberType": { - "$ref": "#/components/schemas/MemberTypes" - }, - "attributes": { - "$ref": "#/components/schemas/EventAttributes" - }, - "isSpecialName": { - "type": "boolean", - "readOnly": true - }, - "addMethod": { - "$ref": "#/components/schemas/MethodInfo" - }, - "removeMethod": { - "$ref": "#/components/schemas/MethodInfo" - }, - "raiseMethod": { - "$ref": "#/components/schemas/MethodInfo" + "nullable": true }, - "isMulticast": { - "type": "boolean", - "readOnly": true + "detail": { + "type": "string", + "nullable": true }, - "eventHandlerType": { - "$ref": "#/components/schemas/Type" + "instance": { + "type": "string", + "nullable": true } }, - "additionalProperties": false + "additionalProperties": { } }, - "Exception": { + "RecoverAccountRequest": { + "required": [ + "email", + "username" + ], "type": "object", "properties": { - "targetSite": { - "$ref": "#/components/schemas/MethodBase" - }, - "message": { + "username": { + "minLength": 1, "type": "string", - "readOnly": true + "description": "The user's name." }, - "data": { - "type": "object", - "additionalProperties": { }, - "readOnly": true - }, - "innerException": { - "$ref": "#/components/schemas/Exception" - }, - "helpLink": { - "type": "string", - "nullable": true - }, - "source": { - "type": "string", - "nullable": true - }, - "hResult": { - "type": "integer", - "format": "int32" - }, - "stackTrace": { + "email": { + "minLength": 1, "type": "string", - "nullable": true, - "readOnly": true + "description": "The user's email address.", + "format": "email" } }, "additionalProperties": false }, - "FieldAttributes": { - "enum": [ - "PrivateScope", - "Private", - "FamANDAssem", - "Assembly", - "Family", - "FamORAssem", - "Public", - "FieldAccessMask", - "Static", - "InitOnly", - "Literal", - "NotSerialized", - "HasFieldRVA", - "SpecialName", - "RTSpecialName", - "HasFieldMarshal", - "PinvokeImpl", - "HasDefault", - "ReservedMask" + "RegisterRequest": { + "required": [ + "email", + "password", + "username" ], - "type": "string" - }, - "FieldInfo": { "type": "object", "properties": { - "name": { + "username": { + "minLength": 1, "type": "string", - "readOnly": true - }, - "declaringType": { - "$ref": "#/components/schemas/Type" - }, - "reflectedType": { - "$ref": "#/components/schemas/Type" - }, - "module": { - "$ref": "#/components/schemas/Module" - }, - "customAttributes": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CustomAttributeData" - }, - "readOnly": true - }, - "isCollectible": { - "type": "boolean", - "readOnly": true - }, - "metadataToken": { - "type": "integer", - "format": "int32", - "readOnly": true - }, - "memberType": { - "$ref": "#/components/schemas/MemberTypes" - }, - "attributes": { - "$ref": "#/components/schemas/FieldAttributes" - }, - "fieldType": { - "$ref": "#/components/schemas/Type" - }, - "isInitOnly": { - "type": "boolean", - "readOnly": true - }, - "isLiteral": { - "type": "boolean", - "readOnly": true - }, - "isNotSerialized": { - "type": "boolean", - "readOnly": true, - "deprecated": true - }, - "isPinvokeImpl": { - "type": "boolean", - "readOnly": true - }, - "isSpecialName": { - "type": "boolean", - "readOnly": true - }, - "isStatic": { - "type": "boolean", - "readOnly": true - }, - "isAssembly": { - "type": "boolean", - "readOnly": true - }, - "isFamily": { - "type": "boolean", - "readOnly": true - }, - "isFamilyAndAssembly": { - "type": "boolean", - "readOnly": true - }, - "isFamilyOrAssembly": { - "type": "boolean", - "readOnly": true - }, - "isPrivate": { - "type": "boolean", - "readOnly": true - }, - "isPublic": { - "type": "boolean", - "readOnly": true - }, - "isSecurityCritical": { - "type": "boolean", - "readOnly": true - }, - "isSecuritySafeCritical": { - "type": "boolean", - "readOnly": true + "description": "The username of the `User`. It:\r\n
  • must be [2, 25] in length;
  • must be made up of letters sandwiching zero or one of:
    • hyphen;
    • underscore; or
    • apostrophe
\r\nUsernames are saved case-sensitively, but matched against case-insensitively.\r\nA `User` may not register with the name 'Cool' when another `User` with the name 'cool'\r\nexists.", + "example": "J'on-Doe" }, - "isSecurityTransparent": { - "type": "boolean", - "readOnly": true + "email": { + "minLength": 1, + "type": "string", + "description": "The `User`'s email address.", + "example": "john.doe@example.com" }, - "fieldHandle": { - "$ref": "#/components/schemas/RuntimeFieldHandle" + "password": { + "minLength": 1, + "type": "string", + "description": "The `User`'s password. It:\r\n
  • supports Unicode;
  • must be [8, 80] in length;
  • must have at least:
    • one uppercase letter;
    • one lowercase letter; and
    • one number.
", + "example": "P4ssword" } }, - "additionalProperties": false - }, - "GenericParameterAttributes": { - "enum": [ - "None", - "Covariant", - "Contravariant", - "VarianceMask", - "ReferenceTypeConstraint", - "NotNullableValueTypeConstraint", - "DefaultConstructorConstraint", - "SpecialConstraintMask" - ], - "type": "string" - }, - "ICustomAttributeProvider": { - "type": "object", - "additionalProperties": false - }, - "IntPtr": { - "type": "object", - "additionalProperties": false + "additionalProperties": false, + "description": "This request object is sent when a `User` is attempting to register." }, - "LayoutKind": { + "RunType": { "enum": [ - "Sequential", - "Explicit", - "Auto" + "Time", + "Score" ], "type": "string" }, - "LeaderboardNeverDeleted": { - "type": "object", - "additionalProperties": false - }, - "LeaderboardNotFound": { - "type": "object", - "additionalProperties": false - }, - "LeaderboardViewModel": { + "RunViewModel": { "required": [ - "categories", + "$type", + "categoryId", "createdAt", "deletedAt", "id", "info", - "name", - "slug", - "updatedAt" + "updatedAt", + "userId" ], "type": "object", "properties": { - "id": { - "type": "integer", - "description": "The unique identifier of the `Leaderboard`.\n\r\nGenerated on creation.", - "format": "int64" - }, - "name": { - "type": "string", - "description": "The display name of the `Leaderboard` to create.", - "example": "Foo Bar" + "$type": { + "type": "string" }, - "slug": { + "id": { + "pattern": "^[a-zA-Z0-9-_]{22}$", "type": "string", - "description": "The URL-scoped unique identifier of the `Leaderboard`.\n\r\nMust be [2, 80] in length and consist only of alphanumeric characters and hyphens.", - "example": "foo-bar" + "description": "The unique identifier of the `Run`.\n\r\nGenerated on creation." }, "info": { "type": "string", - "description": "The general information for the Leaderboard.", - "nullable": true, - "example": "Timer starts on selecting New Game and ends when the final boss is beaten." + "description": "User-provided details about the run.", + "nullable": true }, "createdAt": { "type": "string", - "description": "The time the Leaderboard was created.", + "description": "The time the run was created.", "format": "date-time", "example": "1984-01-01T00:00:00Z" }, "updatedAt": { "type": "string", - "description": "The last time the Leaderboard was updated or null.", + "description": "The last time the run was updated or null.", "format": "date-time", "nullable": true, "example": "1984-01-01T00:00:00Z" }, "deletedAt": { "type": "string", - "description": "The time at which the Leaderboard was deleted, or null if the Leaderboard has not been deleted.", + "description": "The time at which the run was deleted, or null if the run has not been deleted.", "format": "date-time", "nullable": true, "example": "1984-01-01T00:00:00Z" }, - "categories": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CategoryViewModel" - }, - "description": "A collection of `Category` entities for the `Leaderboard`." - } - }, - "additionalProperties": false, - "description": "Represents a collection of `Leaderboard` entities." - }, - "LoginRequest": { - "required": [ - "email", - "password" - ], - "type": "object", - "properties": { - "email": { - "minLength": 1, - "type": "string", - "description": "The `User`'s email address.", - "format": "email", - "example": "john.doe@example.com" + "categoryId": { + "type": "integer", + "description": "The ID of the `Category` for `Run`.", + "format": "int64" }, - "password": { - "minLength": 1, + "userId": { + "pattern": "^[a-zA-Z0-9-_]{22}$", "type": "string", - "description": "The `User`'s password. It:\r\n
  • supports Unicode;
  • must be [8, 80] in length;
  • must have at least:
    • one uppercase letter;
    • one lowercase letter; and
    • one number.
", - "example": "P4ssword" + "description": "The ID of the LeaderboardBackend.Models.Entities.User who submitted this run." } }, "additionalProperties": false, - "description": "This request object is sent when a `User` is attempting to log in." - }, - "LoginResponse": { - "required": [ - "token" - ], - "type": "object", - "properties": { - "token": { - "minLength": 1, - "type": "string", - "description": "A JSON Web Token to authenticate and authorize queries with." + "discriminator": { + "propertyName": "$type", + "mapping": { + "Time": "#/components/schemas/TimedRunViewModel", + "Score": "#/components/schemas/ScoredRunViewModel" } - }, - "additionalProperties": false, - "description": "This response object is received upon a successful log-in request." + } }, - "MemberInfo": { - "type": "object", - "properties": { - "memberType": { - "$ref": "#/components/schemas/MemberTypes" - }, - "name": { - "type": "string", - "readOnly": true - }, - "declaringType": { - "$ref": "#/components/schemas/Type" - }, - "reflectedType": { - "$ref": "#/components/schemas/Type" - }, - "module": { - "$ref": "#/components/schemas/Module" + "ScoredRunViewModel": { + "allOf": [ + { + "$ref": "#/components/schemas/RunViewModel" }, - "customAttributes": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CustomAttributeData" + { + "required": [ + "score" + ], + "type": "object", + "properties": { + "score": { + "type": "integer", + "description": "The score achieved during the run.", + "format": "int64" + } }, - "readOnly": true - }, - "isCollectible": { - "type": "boolean", - "readOnly": true - }, - "metadataToken": { - "type": "integer", - "format": "int32", - "readOnly": true + "additionalProperties": false } - }, - "additionalProperties": false - }, - "MemberTypes": { - "enum": [ - "Constructor", - "Event", - "Field", - "Method", - "Property", - "TypeInfo", - "Custom", - "NestedType", - "All" - ], - "type": "string" + ] }, - "MethodAttributes": { + "SortDirection": { "enum": [ - "PrivateScope", - "Private", - "FamANDAssem", - "Assembly", - "Family", - "FamORAssem", - "Public", - "MemberAccessMask", - "UnmanagedExport", - "Static", - "Final", - "Virtual", - "HideBySig", - "NewSlot", - "CheckAccessOnOverride", - "Abstract", - "SpecialName", - "RTSpecialName", - "PinvokeImpl", - "HasSecurity", - "RequireSecObject", - "ReservedMask" + "Ascending", + "Descending" ], "type": "string" }, - "MethodBase": { - "type": "object", - "properties": { - "memberType": { - "$ref": "#/components/schemas/MemberTypes" - }, - "name": { - "type": "string", - "readOnly": true - }, - "declaringType": { - "$ref": "#/components/schemas/Type" - }, - "reflectedType": { - "$ref": "#/components/schemas/Type" - }, - "module": { - "$ref": "#/components/schemas/Module" + "TimedRunViewModel": { + "allOf": [ + { + "$ref": "#/components/schemas/RunViewModel" }, - "customAttributes": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CustomAttributeData" - }, - "readOnly": true - }, - "isCollectible": { - "type": "boolean", - "readOnly": true - }, - "metadataToken": { - "type": "integer", - "format": "int32", - "readOnly": true - }, - "attributes": { - "$ref": "#/components/schemas/MethodAttributes" - }, - "methodImplementationFlags": { - "$ref": "#/components/schemas/MethodImplAttributes" - }, - "callingConvention": { - "$ref": "#/components/schemas/CallingConventions" - }, - "isAbstract": { - "type": "boolean", - "readOnly": true - }, - "isConstructor": { - "type": "boolean", - "readOnly": true - }, - "isFinal": { - "type": "boolean", - "readOnly": true - }, - "isHideBySig": { - "type": "boolean", - "readOnly": true - }, - "isSpecialName": { - "type": "boolean", - "readOnly": true - }, - "isStatic": { - "type": "boolean", - "readOnly": true - }, - "isVirtual": { - "type": "boolean", - "readOnly": true - }, - "isAssembly": { - "type": "boolean", - "readOnly": true - }, - "isFamily": { - "type": "boolean", - "readOnly": true - }, - "isFamilyAndAssembly": { - "type": "boolean", - "readOnly": true - }, - "isFamilyOrAssembly": { - "type": "boolean", - "readOnly": true - }, - "isPrivate": { - "type": "boolean", - "readOnly": true - }, - "isPublic": { - "type": "boolean", - "readOnly": true - }, - "isConstructedGenericMethod": { - "type": "boolean", - "readOnly": true - }, - "isGenericMethod": { - "type": "boolean", - "readOnly": true - }, - "isGenericMethodDefinition": { - "type": "boolean", - "readOnly": true - }, - "containsGenericParameters": { - "type": "boolean", - "readOnly": true - }, - "methodHandle": { - "$ref": "#/components/schemas/RuntimeMethodHandle" - }, - "isSecurityCritical": { - "type": "boolean", - "readOnly": true - }, - "isSecuritySafeCritical": { - "type": "boolean", - "readOnly": true - }, - "isSecurityTransparent": { - "type": "boolean", - "readOnly": true - } - }, - "additionalProperties": false - }, - "MethodImplAttributes": { - "enum": [ - "IL", - "Native", - "OPTIL", - "CodeTypeMask", - "ManagedMask", - "NoInlining", - "ForwardRef", - "Synchronized", - "NoOptimization", - "PreserveSig", - "AggressiveInlining", - "AggressiveOptimization", - "InternalCall", - "MaxMethodImplVal" - ], - "type": "string" - }, - "MethodInfo": { - "type": "object", - "properties": { - "name": { - "type": "string", - "readOnly": true - }, - "declaringType": { - "$ref": "#/components/schemas/Type" - }, - "reflectedType": { - "$ref": "#/components/schemas/Type" - }, - "module": { - "$ref": "#/components/schemas/Module" - }, - "customAttributes": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CustomAttributeData" - }, - "readOnly": true - }, - "isCollectible": { - "type": "boolean", - "readOnly": true - }, - "metadataToken": { - "type": "integer", - "format": "int32", - "readOnly": true - }, - "attributes": { - "$ref": "#/components/schemas/MethodAttributes" - }, - "methodImplementationFlags": { - "$ref": "#/components/schemas/MethodImplAttributes" - }, - "callingConvention": { - "$ref": "#/components/schemas/CallingConventions" - }, - "isAbstract": { - "type": "boolean", - "readOnly": true - }, - "isConstructor": { - "type": "boolean", - "readOnly": true - }, - "isFinal": { - "type": "boolean", - "readOnly": true - }, - "isHideBySig": { - "type": "boolean", - "readOnly": true - }, - "isSpecialName": { - "type": "boolean", - "readOnly": true - }, - "isStatic": { - "type": "boolean", - "readOnly": true - }, - "isVirtual": { - "type": "boolean", - "readOnly": true - }, - "isAssembly": { - "type": "boolean", - "readOnly": true - }, - "isFamily": { - "type": "boolean", - "readOnly": true - }, - "isFamilyAndAssembly": { - "type": "boolean", - "readOnly": true - }, - "isFamilyOrAssembly": { - "type": "boolean", - "readOnly": true - }, - "isPrivate": { - "type": "boolean", - "readOnly": true - }, - "isPublic": { - "type": "boolean", - "readOnly": true - }, - "isConstructedGenericMethod": { - "type": "boolean", - "readOnly": true - }, - "isGenericMethod": { - "type": "boolean", - "readOnly": true - }, - "isGenericMethodDefinition": { - "type": "boolean", - "readOnly": true - }, - "containsGenericParameters": { - "type": "boolean", - "readOnly": true - }, - "methodHandle": { - "$ref": "#/components/schemas/RuntimeMethodHandle" - }, - "isSecurityCritical": { - "type": "boolean", - "readOnly": true - }, - "isSecuritySafeCritical": { - "type": "boolean", - "readOnly": true - }, - "isSecurityTransparent": { - "type": "boolean", - "readOnly": true - }, - "memberType": { - "$ref": "#/components/schemas/MemberTypes" - }, - "returnParameter": { - "$ref": "#/components/schemas/ParameterInfo" - }, - "returnType": { - "$ref": "#/components/schemas/Type" - }, - "returnTypeCustomAttributes": { - "$ref": "#/components/schemas/ICustomAttributeProvider" - } - }, - "additionalProperties": false - }, - "Module": { - "type": "object", - "properties": { - "assembly": { - "$ref": "#/components/schemas/Assembly" - }, - "fullyQualifiedName": { - "type": "string", - "readOnly": true - }, - "name": { - "type": "string", - "readOnly": true - }, - "mdStreamVersion": { - "type": "integer", - "format": "int32", - "readOnly": true - }, - "moduleVersionId": { - "pattern": "^[a-zA-Z0-9-_]{22}$", - "type": "string", - "readOnly": true - }, - "scopeName": { - "type": "string", - "readOnly": true - }, - "moduleHandle": { - "$ref": "#/components/schemas/ModuleHandle" - }, - "customAttributes": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CustomAttributeData" - }, - "readOnly": true - }, - "metadataToken": { - "type": "integer", - "format": "int32", - "readOnly": true - } - }, - "additionalProperties": false - }, - "ModuleHandle": { - "type": "object", - "properties": { - "mdStreamVersion": { - "type": "integer", - "format": "int32", - "readOnly": true - } - }, - "additionalProperties": false - }, - "ParameterAttributes": { - "enum": [ - "None", - "In", - "Out", - "Lcid", - "Retval", - "Optional", - "HasDefault", - "HasFieldMarshal", - "Reserved3", - "Reserved4", - "ReservedMask" - ], - "type": "string" - }, - "ParameterInfo": { - "type": "object", - "properties": { - "attributes": { - "$ref": "#/components/schemas/ParameterAttributes" - }, - "member": { - "$ref": "#/components/schemas/MemberInfo" - }, - "name": { - "type": "string", - "nullable": true, - "readOnly": true - }, - "parameterType": { - "$ref": "#/components/schemas/Type" - }, - "position": { - "type": "integer", - "format": "int32", - "readOnly": true - }, - "isIn": { - "type": "boolean", - "readOnly": true - }, - "isLcid": { - "type": "boolean", - "readOnly": true - }, - "isOptional": { - "type": "boolean", - "readOnly": true - }, - "isOut": { - "type": "boolean", - "readOnly": true - }, - "isRetval": { - "type": "boolean", - "readOnly": true - }, - "defaultValue": { - "nullable": true, - "readOnly": true - }, - "rawDefaultValue": { - "nullable": true, - "readOnly": true - }, - "hasDefaultValue": { - "type": "boolean", - "readOnly": true - }, - "customAttributes": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CustomAttributeData" - }, - "readOnly": true - }, - "metadataToken": { - "type": "integer", - "format": "int32", - "readOnly": true - } - }, - "additionalProperties": false - }, - "ProblemDetails": { - "type": "object", - "properties": { - "type": { - "type": "string", - "nullable": true - }, - "title": { - "type": "string", - "nullable": true - }, - "status": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "detail": { - "type": "string", - "nullable": true - }, - "instance": { - "type": "string", - "nullable": true - } - }, - "additionalProperties": { } - }, - "PropertyAttributes": { - "enum": [ - "None", - "SpecialName", - "RTSpecialName", - "HasDefault", - "Reserved2", - "Reserved3", - "Reserved4", - "ReservedMask" - ], - "type": "string" - }, - "PropertyInfo": { - "type": "object", - "properties": { - "name": { - "type": "string", - "readOnly": true - }, - "declaringType": { - "$ref": "#/components/schemas/Type" - }, - "reflectedType": { - "$ref": "#/components/schemas/Type" - }, - "module": { - "$ref": "#/components/schemas/Module" - }, - "customAttributes": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CustomAttributeData" - }, - "readOnly": true - }, - "isCollectible": { - "type": "boolean", - "readOnly": true - }, - "metadataToken": { - "type": "integer", - "format": "int32", - "readOnly": true - }, - "memberType": { - "$ref": "#/components/schemas/MemberTypes" - }, - "propertyType": { - "$ref": "#/components/schemas/Type" - }, - "attributes": { - "$ref": "#/components/schemas/PropertyAttributes" - }, - "isSpecialName": { - "type": "boolean", - "readOnly": true - }, - "canRead": { - "type": "boolean", - "readOnly": true - }, - "canWrite": { - "type": "boolean", - "readOnly": true - }, - "getMethod": { - "$ref": "#/components/schemas/MethodInfo" - }, - "setMethod": { - "$ref": "#/components/schemas/MethodInfo" - } - }, - "additionalProperties": false - }, - "RecoverAccountRequest": { - "required": [ - "email", - "username" - ], - "type": "object", - "properties": { - "username": { - "minLength": 1, - "type": "string", - "description": "The user's name." - }, - "email": { - "minLength": 1, - "type": "string", - "description": "The user's email address.", - "format": "email" - } - }, - "additionalProperties": false - }, - "RegisterRequest": { - "required": [ - "email", - "password", - "username" - ], - "type": "object", - "properties": { - "username": { - "minLength": 1, - "type": "string", - "description": "The username of the `User`. It:\r\n
  • must be [2, 25] in length;
  • must be made up of letters sandwiching zero or one of:
    • hyphen;
    • underscore; or
    • apostrophe
\r\nUsernames are saved case-sensitively, but matched against case-insensitively.\r\nA `User` may not register with the name 'Cool' when another `User` with the name 'cool'\r\nexists.", - "example": "J'on-Doe" - }, - "email": { - "minLength": 1, - "type": "string", - "description": "The `User`'s email address.", - "example": "john.doe@example.com" - }, - "password": { - "minLength": 1, - "type": "string", - "description": "The `User`'s password. It:\r\n
  • supports Unicode;
  • must be [8, 80] in length;
  • must have at least:
    • one uppercase letter;
    • one lowercase letter; and
    • one number.
", - "example": "P4ssword" - } - }, - "additionalProperties": false, - "description": "This request object is sent when a `User` is attempting to register." - }, - "RestoreLeaderboardResult": { - "type": "object", - "properties": { - "value": { - "nullable": true, - "readOnly": true - }, - "index": { - "type": "integer", - "format": "int32", - "readOnly": true - }, - "isT0": { - "type": "boolean", - "readOnly": true - }, - "isT1": { - "type": "boolean", - "readOnly": true - }, - "isT2": { - "type": "boolean", - "readOnly": true - }, - "asT0": { - "$ref": "#/components/schemas/Task" - }, - "asT1": { - "$ref": "#/components/schemas/LeaderboardNotFound" - }, - "asT2": { - "$ref": "#/components/schemas/LeaderboardNeverDeleted" - } - }, - "additionalProperties": false - }, - "RunType": { - "enum": [ - "Time", - "Score" - ], - "type": "string" - }, - "RunViewModel": { - "required": [ - "$type", - "categoryId", - "createdAt", - "deletedAt", - "id", - "info", - "updatedAt", - "userId" - ], - "type": "object", - "properties": { - "$type": { - "type": "string" - }, - "id": { - "pattern": "^[a-zA-Z0-9-_]{22}$", - "type": "string", - "description": "The unique identifier of the `Run`.\n\r\nGenerated on creation." - }, - "info": { - "type": "string", - "description": "User-provided details about the run.", - "nullable": true - }, - "createdAt": { - "type": "string", - "description": "The time the run was created.", - "format": "date-time", - "example": "1984-01-01T00:00:00Z" - }, - "updatedAt": { - "type": "string", - "description": "The last time the run was updated or null.", - "format": "date-time", - "nullable": true, - "example": "1984-01-01T00:00:00Z" - }, - "deletedAt": { - "type": "string", - "description": "The time at which the run was deleted, or null if the run has not been deleted.", - "format": "date-time", - "nullable": true, - "example": "1984-01-01T00:00:00Z" - }, - "categoryId": { - "type": "integer", - "description": "The ID of the `Category` for `Run`.", - "format": "int64" - }, - "userId": { - "pattern": "^[a-zA-Z0-9-_]{22}$", - "type": "string", - "description": "The ID of the LeaderboardBackend.Models.Entities.User who submitted this run." - } - }, - "additionalProperties": false, - "discriminator": { - "propertyName": "$type", - "mapping": { - "Time": "#/components/schemas/TimedRunViewModel", - "Score": "#/components/schemas/ScoredRunViewModel" - } - } - }, - "RuntimeFieldHandle": { - "type": "object", - "properties": { - "value": { - "$ref": "#/components/schemas/IntPtr" - } - }, - "additionalProperties": false - }, - "RuntimeMethodHandle": { - "type": "object", - "properties": { - "value": { - "$ref": "#/components/schemas/IntPtr" - } - }, - "additionalProperties": false - }, - "RuntimeTypeHandle": { - "type": "object", - "properties": { - "value": { - "$ref": "#/components/schemas/IntPtr" - } - }, - "additionalProperties": false - }, - "ScoredRunViewModel": { - "allOf": [ - { - "$ref": "#/components/schemas/RunViewModel" - }, - { - "required": [ - "score" - ], - "type": "object", - "properties": { - "score": { - "type": "integer", - "description": "The score achieved during the run.", - "format": "int64" - } - }, - "additionalProperties": false - } - ] - }, - "SecurityRuleSet": { - "enum": [ - "None", - "Level1", - "Level2" - ], - "type": "string" - }, - "SortDirection": { - "enum": [ - "Ascending", - "Descending" - ], - "type": "string" - }, - "StructLayoutAttribute": { - "type": "object", - "properties": { - "typeId": { - "readOnly": true - }, - "value": { - "$ref": "#/components/schemas/LayoutKind" - } - }, - "additionalProperties": false - }, - "Task": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int32", - "readOnly": true - }, - "exception": { - "$ref": "#/components/schemas/AggregateException" - }, - "status": { - "$ref": "#/components/schemas/TaskStatus" - }, - "isCanceled": { - "type": "boolean", - "readOnly": true - }, - "isCompleted": { - "type": "boolean", - "readOnly": true - }, - "isCompletedSuccessfully": { - "type": "boolean", - "readOnly": true - }, - "creationOptions": { - "$ref": "#/components/schemas/TaskCreationOptions" - }, - "asyncState": { - "nullable": true, - "readOnly": true - }, - "isFaulted": { - "type": "boolean", - "readOnly": true - } - }, - "additionalProperties": false - }, - "TaskCreationOptions": { - "enum": [ - "None", - "PreferFairness", - "LongRunning", - "AttachedToParent", - "DenyChildAttach", - "HideScheduler", - "RunContinuationsAsynchronously" - ], - "type": "string" - }, - "TaskStatus": { - "enum": [ - "Created", - "WaitingForActivation", - "WaitingToRun", - "Running", - "WaitingForChildrenToComplete", - "RanToCompletion", - "Canceled", - "Faulted" - ], - "type": "string" - }, - "TimedRunViewModel": { - "allOf": [ - { - "$ref": "#/components/schemas/RunViewModel" - }, - { - "required": [ - "time" - ], - "type": "object", - "properties": { - "time": { - "type": "string", - "description": "The duration of the run.", - "example": "25:01:01.001" - } + { + "required": [ + "time" + ], + "type": "object", + "properties": { + "time": { + "type": "string", + "description": "The duration of the run.", + "example": "25:01:01.001" + } }, "additionalProperties": false } ] }, - "Type": { - "type": "object", - "properties": { - "name": { - "type": "string", - "readOnly": true - }, - "customAttributes": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CustomAttributeData" - }, - "readOnly": true - }, - "isCollectible": { - "type": "boolean", - "readOnly": true - }, - "metadataToken": { - "type": "integer", - "format": "int32", - "readOnly": true - }, - "isInterface": { - "type": "boolean", - "readOnly": true - }, - "memberType": { - "$ref": "#/components/schemas/MemberTypes" - }, - "namespace": { - "type": "string", - "nullable": true, - "readOnly": true - }, - "assemblyQualifiedName": { - "type": "string", - "nullable": true, - "readOnly": true - }, - "fullName": { - "type": "string", - "nullable": true, - "readOnly": true - }, - "assembly": { - "$ref": "#/components/schemas/Assembly" - }, - "module": { - "$ref": "#/components/schemas/Module" - }, - "isNested": { - "type": "boolean", - "readOnly": true - }, - "declaringType": { - "$ref": "#/components/schemas/Type" - }, - "declaringMethod": { - "$ref": "#/components/schemas/MethodBase" - }, - "reflectedType": { - "$ref": "#/components/schemas/Type" - }, - "underlyingSystemType": { - "$ref": "#/components/schemas/Type" - }, - "isTypeDefinition": { - "type": "boolean", - "readOnly": true - }, - "isArray": { - "type": "boolean", - "readOnly": true - }, - "isByRef": { - "type": "boolean", - "readOnly": true - }, - "isPointer": { - "type": "boolean", - "readOnly": true - }, - "isConstructedGenericType": { - "type": "boolean", - "readOnly": true - }, - "isGenericParameter": { - "type": "boolean", - "readOnly": true - }, - "isGenericTypeParameter": { - "type": "boolean", - "readOnly": true - }, - "isGenericMethodParameter": { - "type": "boolean", - "readOnly": true - }, - "isGenericType": { - "type": "boolean", - "readOnly": true - }, - "isGenericTypeDefinition": { - "type": "boolean", - "readOnly": true - }, - "isSZArray": { - "type": "boolean", - "readOnly": true - }, - "isVariableBoundArray": { - "type": "boolean", - "readOnly": true - }, - "isByRefLike": { - "type": "boolean", - "readOnly": true - }, - "isFunctionPointer": { - "type": "boolean", - "readOnly": true - }, - "isUnmanagedFunctionPointer": { - "type": "boolean", - "readOnly": true - }, - "hasElementType": { - "type": "boolean", - "readOnly": true - }, - "genericTypeArguments": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Type" - }, - "readOnly": true - }, - "genericParameterPosition": { - "type": "integer", - "format": "int32", - "readOnly": true - }, - "genericParameterAttributes": { - "$ref": "#/components/schemas/GenericParameterAttributes" - }, - "attributes": { - "$ref": "#/components/schemas/TypeAttributes" - }, - "isAbstract": { - "type": "boolean", - "readOnly": true - }, - "isImport": { - "type": "boolean", - "readOnly": true - }, - "isSealed": { - "type": "boolean", - "readOnly": true - }, - "isSpecialName": { - "type": "boolean", - "readOnly": true - }, - "isClass": { - "type": "boolean", - "readOnly": true - }, - "isNestedAssembly": { - "type": "boolean", - "readOnly": true - }, - "isNestedFamANDAssem": { - "type": "boolean", - "readOnly": true - }, - "isNestedFamily": { - "type": "boolean", - "readOnly": true - }, - "isNestedFamORAssem": { - "type": "boolean", - "readOnly": true - }, - "isNestedPrivate": { - "type": "boolean", - "readOnly": true - }, - "isNestedPublic": { - "type": "boolean", - "readOnly": true - }, - "isNotPublic": { - "type": "boolean", - "readOnly": true - }, - "isPublic": { - "type": "boolean", - "readOnly": true - }, - "isAutoLayout": { - "type": "boolean", - "readOnly": true - }, - "isExplicitLayout": { - "type": "boolean", - "readOnly": true - }, - "isLayoutSequential": { - "type": "boolean", - "readOnly": true - }, - "isAnsiClass": { - "type": "boolean", - "readOnly": true - }, - "isAutoClass": { - "type": "boolean", - "readOnly": true - }, - "isUnicodeClass": { - "type": "boolean", - "readOnly": true - }, - "isCOMObject": { - "type": "boolean", - "readOnly": true - }, - "isContextful": { - "type": "boolean", - "readOnly": true - }, - "isEnum": { - "type": "boolean", - "readOnly": true - }, - "isMarshalByRef": { - "type": "boolean", - "readOnly": true - }, - "isPrimitive": { - "type": "boolean", - "readOnly": true - }, - "isValueType": { - "type": "boolean", - "readOnly": true - }, - "isSignatureType": { - "type": "boolean", - "readOnly": true - }, - "isSecurityCritical": { - "type": "boolean", - "readOnly": true - }, - "isSecuritySafeCritical": { - "type": "boolean", - "readOnly": true - }, - "isSecurityTransparent": { - "type": "boolean", - "readOnly": true - }, - "structLayoutAttribute": { - "$ref": "#/components/schemas/StructLayoutAttribute" - }, - "typeInitializer": { - "$ref": "#/components/schemas/ConstructorInfo" - }, - "typeHandle": { - "$ref": "#/components/schemas/RuntimeTypeHandle" - }, - "guid": { - "pattern": "^[a-zA-Z0-9-_]{22}$", - "type": "string", - "readOnly": true - }, - "baseType": { - "$ref": "#/components/schemas/Type" - }, - "isSerializable": { - "type": "boolean", - "readOnly": true, - "deprecated": true - }, - "containsGenericParameters": { - "type": "boolean", - "readOnly": true - }, - "isVisible": { - "type": "boolean", - "readOnly": true - } - }, - "additionalProperties": false - }, - "TypeAttributes": { - "enum": [ - "NotPublic", - "Public", - "NestedPublic", - "NestedPrivate", - "NestedFamily", - "NestedAssembly", - "NestedFamANDAssem", - "VisibilityMask", - "SequentialLayout", - "ExplicitLayout", - "LayoutMask", - "Interface", - "Abstract", - "Sealed", - "SpecialName", - "RTSpecialName", - "Import", - "Serializable", - "WindowsRuntime", - "UnicodeClass", - "AutoClass", - "StringFormatMask", - "HasSecurity", - "ReservedMask", - "BeforeFieldInit", - "CustomFormatMask" - ], - "type": "string" - }, - "TypeInfo": { - "type": "object", - "properties": { - "name": { - "type": "string", - "readOnly": true - }, - "customAttributes": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CustomAttributeData" - }, - "readOnly": true - }, - "isCollectible": { - "type": "boolean", - "readOnly": true - }, - "metadataToken": { - "type": "integer", - "format": "int32", - "readOnly": true - }, - "isInterface": { - "type": "boolean", - "readOnly": true - }, - "memberType": { - "$ref": "#/components/schemas/MemberTypes" - }, - "namespace": { - "type": "string", - "nullable": true, - "readOnly": true - }, - "assemblyQualifiedName": { - "type": "string", - "nullable": true, - "readOnly": true - }, - "fullName": { - "type": "string", - "nullable": true, - "readOnly": true - }, - "assembly": { - "$ref": "#/components/schemas/Assembly" - }, - "module": { - "$ref": "#/components/schemas/Module" - }, - "isNested": { - "type": "boolean", - "readOnly": true - }, - "declaringType": { - "$ref": "#/components/schemas/Type" - }, - "declaringMethod": { - "$ref": "#/components/schemas/MethodBase" - }, - "reflectedType": { - "$ref": "#/components/schemas/Type" - }, - "underlyingSystemType": { - "$ref": "#/components/schemas/Type" - }, - "isTypeDefinition": { - "type": "boolean", - "readOnly": true - }, - "isArray": { - "type": "boolean", - "readOnly": true - }, - "isByRef": { - "type": "boolean", - "readOnly": true - }, - "isPointer": { - "type": "boolean", - "readOnly": true - }, - "isConstructedGenericType": { - "type": "boolean", - "readOnly": true - }, - "isGenericParameter": { - "type": "boolean", - "readOnly": true - }, - "isGenericTypeParameter": { - "type": "boolean", - "readOnly": true - }, - "isGenericMethodParameter": { - "type": "boolean", - "readOnly": true - }, - "isGenericType": { - "type": "boolean", - "readOnly": true - }, - "isGenericTypeDefinition": { - "type": "boolean", - "readOnly": true - }, - "isSZArray": { - "type": "boolean", - "readOnly": true - }, - "isVariableBoundArray": { - "type": "boolean", - "readOnly": true - }, - "isByRefLike": { - "type": "boolean", - "readOnly": true - }, - "isFunctionPointer": { - "type": "boolean", - "readOnly": true - }, - "isUnmanagedFunctionPointer": { - "type": "boolean", - "readOnly": true - }, - "hasElementType": { - "type": "boolean", - "readOnly": true - }, - "genericTypeArguments": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Type" - }, - "readOnly": true - }, - "genericParameterPosition": { - "type": "integer", - "format": "int32", - "readOnly": true - }, - "genericParameterAttributes": { - "$ref": "#/components/schemas/GenericParameterAttributes" - }, - "attributes": { - "$ref": "#/components/schemas/TypeAttributes" - }, - "isAbstract": { - "type": "boolean", - "readOnly": true - }, - "isImport": { - "type": "boolean", - "readOnly": true - }, - "isSealed": { - "type": "boolean", - "readOnly": true - }, - "isSpecialName": { - "type": "boolean", - "readOnly": true - }, - "isClass": { - "type": "boolean", - "readOnly": true - }, - "isNestedAssembly": { - "type": "boolean", - "readOnly": true - }, - "isNestedFamANDAssem": { - "type": "boolean", - "readOnly": true - }, - "isNestedFamily": { - "type": "boolean", - "readOnly": true - }, - "isNestedFamORAssem": { - "type": "boolean", - "readOnly": true - }, - "isNestedPrivate": { - "type": "boolean", - "readOnly": true - }, - "isNestedPublic": { - "type": "boolean", - "readOnly": true - }, - "isNotPublic": { - "type": "boolean", - "readOnly": true - }, - "isPublic": { - "type": "boolean", - "readOnly": true - }, - "isAutoLayout": { - "type": "boolean", - "readOnly": true - }, - "isExplicitLayout": { - "type": "boolean", - "readOnly": true - }, - "isLayoutSequential": { - "type": "boolean", - "readOnly": true - }, - "isAnsiClass": { - "type": "boolean", - "readOnly": true - }, - "isAutoClass": { - "type": "boolean", - "readOnly": true - }, - "isUnicodeClass": { - "type": "boolean", - "readOnly": true - }, - "isCOMObject": { - "type": "boolean", - "readOnly": true - }, - "isContextful": { - "type": "boolean", - "readOnly": true - }, - "isEnum": { - "type": "boolean", - "readOnly": true - }, - "isMarshalByRef": { - "type": "boolean", - "readOnly": true - }, - "isPrimitive": { - "type": "boolean", - "readOnly": true - }, - "isValueType": { - "type": "boolean", - "readOnly": true - }, - "isSignatureType": { - "type": "boolean", - "readOnly": true - }, - "isSecurityCritical": { - "type": "boolean", - "readOnly": true - }, - "isSecuritySafeCritical": { - "type": "boolean", - "readOnly": true - }, - "isSecurityTransparent": { - "type": "boolean", - "readOnly": true - }, - "structLayoutAttribute": { - "$ref": "#/components/schemas/StructLayoutAttribute" - }, - "typeInitializer": { - "$ref": "#/components/schemas/ConstructorInfo" - }, - "typeHandle": { - "$ref": "#/components/schemas/RuntimeTypeHandle" - }, - "guid": { - "pattern": "^[a-zA-Z0-9-_]{22}$", - "type": "string", - "readOnly": true - }, - "baseType": { - "$ref": "#/components/schemas/Type" - }, - "isSerializable": { - "type": "boolean", - "readOnly": true, - "deprecated": true - }, - "containsGenericParameters": { - "type": "boolean", - "readOnly": true - }, - "isVisible": { - "type": "boolean", - "readOnly": true - }, - "genericTypeParameters": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Type" - }, - "readOnly": true - }, - "declaredConstructors": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ConstructorInfo" - }, - "readOnly": true - }, - "declaredEvents": { - "type": "array", - "items": { - "$ref": "#/components/schemas/EventInfo" - }, - "readOnly": true - }, - "declaredFields": { - "type": "array", - "items": { - "$ref": "#/components/schemas/FieldInfo" - }, - "readOnly": true - }, - "declaredMembers": { - "type": "array", - "items": { - "$ref": "#/components/schemas/MemberInfo" - }, - "readOnly": true - }, - "declaredMethods": { - "type": "array", - "items": { - "$ref": "#/components/schemas/MethodInfo" - }, - "readOnly": true - }, - "declaredNestedTypes": { - "type": "array", - "items": { - "$ref": "#/components/schemas/TypeInfo" - }, - "readOnly": true - }, - "declaredProperties": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PropertyInfo" - }, - "readOnly": true - }, - "implementedInterfaces": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Type" - }, - "readOnly": true - } - }, - "additionalProperties": false - }, "UserRole": { "enum": [ "Registered", From f17d8d068a1fa6acbd7c978ef61000ede1a4f450 Mon Sep 17 00:00:00 2001 From: zysim <9867871+zysim@users.noreply.github.com> Date: Wed, 16 Oct 2024 21:45:51 +0800 Subject: [PATCH 05/11] Add authN/authZ tests --- LeaderboardBackend.Test/Leaderboards.cs | 35 +++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/LeaderboardBackend.Test/Leaderboards.cs b/LeaderboardBackend.Test/Leaderboards.cs index 8a8c040a..ab5bf3cf 100644 --- a/LeaderboardBackend.Test/Leaderboards.cs +++ b/LeaderboardBackend.Test/Leaderboards.cs @@ -383,6 +383,41 @@ public async Task RestoreLeaderboard_OK() res.DeletedAt.Should().BeNull(); } + [Test] + public async Task RestoreLeaderboard_Unauthenticated() + { + Func> act = async () => await _apiClient.Put($"/leaderboard/100/restore", new() + { + Jwt = "" + }); + + await act.Should().ThrowAsync().Where(e => e.Response.StatusCode == HttpStatusCode.Unauthorized); + } + + [Test] + public async Task RestoreLeaderboard_Unauthorized() + { + IUserService userService = _factory.Services.CreateScope().ServiceProvider.GetRequiredService(); + + RegisterRequest registerRequest = new() + { + Email = "user@example.com", + Password = "Passw0rd", + Username = "unauthorized" + }; + + await userService.CreateUser(registerRequest); + + string jwt = (await _apiClient.LoginUser(registerRequest.Email, registerRequest.Password)).Token; + + Func> act = async () => await _apiClient.Put($"/leaderboard/100/restore", new() + { + Jwt = jwt, + }); + + await act.Should().ThrowAsync().Where(e => e.Response.StatusCode == HttpStatusCode.Forbidden); + } + [Test] public async Task RestoreLeaderboard_NotFound() { From 835091f82803badd737d1fdf1660b5e3d4551e21 Mon Sep 17 00:00:00 2001 From: zysim <9867871+zysim@users.noreply.github.com> Date: Sat, 19 Oct 2024 23:52:35 +0800 Subject: [PATCH 06/11] Add conflict case --- LeaderboardBackend.Test/Leaderboards.cs | 39 ++++++++----------- .../Controllers/LeaderboardsController.cs | 10 +++-- LeaderboardBackend/Results.cs | 3 ++ .../Services/ILeaderboardService.cs | 2 +- .../Services/Impl/LeaderboardService.cs | 10 ++++- LeaderboardBackend/openapi.json | 21 +++++++++- 6 files changed, 53 insertions(+), 32 deletions(-) diff --git a/LeaderboardBackend.Test/Leaderboards.cs b/LeaderboardBackend.Test/Leaderboards.cs index ab5bf3cf..ec4b4ba9 100644 --- a/LeaderboardBackend.Test/Leaderboards.cs +++ b/LeaderboardBackend.Test/Leaderboards.cs @@ -1,9 +1,6 @@ using System; -using System.Collections.Generic; using System.Linq; using System.Net; -using System.Net.Http; -using System.Threading; using System.Threading.Tasks; using LeaderboardBackend.Models.Entities; using LeaderboardBackend.Models.Requests; @@ -15,7 +12,6 @@ using Microsoft.AspNetCore.TestHost; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; -using Microsoft.VisualBasic; using NodaTime; using NodaTime.Testing; using NUnit.Framework; @@ -359,13 +355,10 @@ public async Task RestoreLeaderboard_OK() Leaderboard deletedBoard = new() { Name = "Super Mario World", - Slug = "super-mario-world-deleted", + Slug = "super-mario-world-to-restore", DeletedAt = _clock.GetCurrentInstant() }; - Instant now = Instant.FromUnixTimeSeconds(1); - _clock.Reset(now); - context.Leaderboards.Add(deletedBoard); await context.SaveChangesAsync(); deletedBoard.Id.Should().NotBe(default); @@ -394,21 +387,22 @@ public async Task RestoreLeaderboard_Unauthenticated() await act.Should().ThrowAsync().Where(e => e.Response.StatusCode == HttpStatusCode.Unauthorized); } - [Test] - public async Task RestoreLeaderboard_Unauthorized() + [TestCase("restore-leaderboard-unauth1@example.com", "RestoreBoard1", UserRole.Confirmed)] + [TestCase("restore-leaderboard-unauth2@example.com", "RestoreBoard2", UserRole.Registered)] + public async Task RestoreLeaderboard_Unauthorized(string email, string username, UserRole role) { - IUserService userService = _factory.Services.CreateScope().ServiceProvider.GetRequiredService(); + UserViewModel userModel = await _apiClient.RegisterUser(username, email, "P4ssword"); - RegisterRequest registerRequest = new() - { - Email = "user@example.com", - Password = "Passw0rd", - Username = "unauthorized" - }; + ApplicationContext context = _factory.Services.CreateScope().ServiceProvider.GetRequiredService(); - await userService.CreateUser(registerRequest); + User? user = await context.Users.FindAsync([userModel.Id]); + + context.Users.Update(user!); + user!.Role = role; - string jwt = (await _apiClient.LoginUser(registerRequest.Email, registerRequest.Password)).Token; + await context.SaveChangesAsync(); + + string jwt = (await _apiClient.LoginUser(email, "P4ssword")).Token; Func> act = async () => await _apiClient.Put($"/leaderboard/100/restore", new() { @@ -421,7 +415,7 @@ public async Task RestoreLeaderboard_Unauthorized() [Test] public async Task RestoreLeaderboard_NotFound() { - Func> act = async () => await _apiClient.Put($"/leaderboard/100/restore", new() + Func> act = async () => await _apiClient.Put($"/leaderboard/{1e10}/restore", new() { Jwt = _jwt }); @@ -436,8 +430,8 @@ public async Task RestoreLeaderboard_NotFound_WasNeverDeleted() Leaderboard board = new() { - Name = "Super Mario World", - Slug = "super-mario-world-non-deleted", + Name = "Hyper Mario World", + Slug = "hyper-mario-world-non-deleted", }; context.Leaderboards.Add(board); @@ -451,6 +445,5 @@ public async Task RestoreLeaderboard_NotFound_WasNeverDeleted() await act.Should().ThrowAsync() .Where(e => e.Response.StatusCode == HttpStatusCode.NotFound); - // TODO: Don't know how to test for the response message. } } diff --git a/LeaderboardBackend/Controllers/LeaderboardsController.cs b/LeaderboardBackend/Controllers/LeaderboardsController.cs index 0b66c095..092ba2a0 100644 --- a/LeaderboardBackend/Controllers/LeaderboardsController.cs +++ b/LeaderboardBackend/Controllers/LeaderboardsController.cs @@ -90,10 +90,11 @@ public async Task> CreateLeaderboard( [Authorize(Policy = UserTypes.ADMINISTRATOR)] [HttpPut("leaderboard/{id:long}/restore")] [SwaggerOperation("Restores a deleted leaderboard.", OperationId = "restoreLeaderboard")] - [SwaggerResponse(200)] + [SwaggerResponse(200, "The restored `Leaderboard`s view model.", typeof(LeaderboardViewModel))] [SwaggerResponse(401)] [SwaggerResponse(403, "The requesting `User` is unauthorized to restore `Leaderboard`s.")] - [SwaggerResponse(404, "The `Leaderboard` was not found, or it wasn't deleted in the first place.")] + [SwaggerResponse(404, "The `Leaderboard` was not found, or it wasn't deleted in the first place.", typeof(string))] + [SwaggerResponse(409, "Another `Leaderboard` with the same slug has been created since, and therefore can't be restored.", typeof(LeaderboardViewModel))] public async Task> RestoreLeaderboard( long id ) @@ -106,8 +107,9 @@ long id neverDeleted => { ModelState.AddModelError("Leaderboard", "LeaderboardWasNeverPreviouslyDeleted"); - return NotFound(new ValidationProblemDetails(ModelState)); - } + return NotFound("Was never deleted"); + }, + conflict => Conflict(conflict.Board) ); } } diff --git a/LeaderboardBackend/Results.cs b/LeaderboardBackend/Results.cs index abeadeff..cba5ddb7 100644 --- a/LeaderboardBackend/Results.cs +++ b/LeaderboardBackend/Results.cs @@ -1,3 +1,5 @@ +using LeaderboardBackend.Models.Entities; + namespace LeaderboardBackend.Result; public readonly record struct AccountConfirmed; @@ -10,5 +12,6 @@ namespace LeaderboardBackend.Result; public readonly record struct CreateLeaderboardConflict; public readonly record struct LeaderboardNotFound; public readonly record struct LeaderboardNeverDeleted; +public readonly record struct RestoreLeaderboardConflict(Leaderboard Board); public readonly record struct UserNotFound; public readonly record struct UserBanned; diff --git a/LeaderboardBackend/Services/ILeaderboardService.cs b/LeaderboardBackend/Services/ILeaderboardService.cs index a0979e4b..c6432365 100644 --- a/LeaderboardBackend/Services/ILeaderboardService.cs +++ b/LeaderboardBackend/Services/ILeaderboardService.cs @@ -18,4 +18,4 @@ public interface ILeaderboardService public partial class CreateLeaderboardResult : OneOfBase; [GenerateOneOf] -public partial class RestoreLeaderboardResult : OneOfBase; +public partial class RestoreLeaderboardResult : OneOfBase; diff --git a/LeaderboardBackend/Services/Impl/LeaderboardService.cs b/LeaderboardBackend/Services/Impl/LeaderboardService.cs index 5b5130d6..c7b09b85 100644 --- a/LeaderboardBackend/Services/Impl/LeaderboardService.cs +++ b/LeaderboardBackend/Services/Impl/LeaderboardService.cs @@ -7,7 +7,7 @@ namespace LeaderboardBackend.Services; -public class LeaderboardService(ApplicationContext applicationContext, IClock clock) : ILeaderboardService +public class LeaderboardService(ApplicationContext applicationContext) : ILeaderboardService { public async Task GetLeaderboard(long id) => await applicationContext.Leaderboards.FindAsync(id); @@ -58,9 +58,15 @@ public async Task RestoreLeaderboard(long id) return new LeaderboardNeverDeleted(); } + Leaderboard? maybe = await applicationContext.Leaderboards.SingleOrDefaultAsync(board => board.Slug == lb.Slug && board.DeletedAt == null); + + if (maybe != null) + { + return new RestoreLeaderboardConflict(maybe); + } + applicationContext.Leaderboards.Update(lb); - lb.UpdatedAt = clock.GetCurrentInstant(); lb.DeletedAt = null; await applicationContext.SaveChangesAsync(); diff --git a/LeaderboardBackend/openapi.json b/LeaderboardBackend/openapi.json index a49938e0..01166b48 100644 --- a/LeaderboardBackend/openapi.json +++ b/LeaderboardBackend/openapi.json @@ -697,7 +697,7 @@ "description": "Internal Server Error" }, "200": { - "description": "OK", + "description": "The restored `Leaderboard`s view model.", "content": { "application/json": { "schema": { @@ -713,7 +713,24 @@ "description": "The requesting `User` is unauthorized to restore `Leaderboard`s." }, "404": { - "description": "The `Leaderboard` was not found, or it wasn't deleted in the first place." + "description": "The `Leaderboard` was not found, or it wasn't deleted in the first place.", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + }, + "409": { + "description": "Another `Leaderboard` with the same slug has been created since, and therefore can't be restored.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LeaderboardViewModel" + } + } + } } } } From eb1b0534bc420863cfec4cbac6d78c05f23c0907 Mon Sep 17 00:00:00 2001 From: zysim <9867871+zysim@users.noreply.github.com> Date: Sun, 20 Oct 2024 21:10:51 +0800 Subject: [PATCH 07/11] Fix conflict response object type and add more test cases --- LeaderboardBackend.Test/Leaderboards.cs | 111 ++++++++++++++++-- .../Controllers/LeaderboardsController.cs | 7 +- .../Services/Impl/LeaderboardService.cs | 2 - 3 files changed, 102 insertions(+), 18 deletions(-) diff --git a/LeaderboardBackend.Test/Leaderboards.cs b/LeaderboardBackend.Test/Leaderboards.cs index ec4b4ba9..d41905e4 100644 --- a/LeaderboardBackend.Test/Leaderboards.cs +++ b/LeaderboardBackend.Test/Leaderboards.cs @@ -1,13 +1,16 @@ using System; using System.Linq; using System.Net; +using System.Net.Http.Json; using System.Threading.Tasks; using LeaderboardBackend.Models.Entities; using LeaderboardBackend.Models.Requests; using LeaderboardBackend.Models.ViewModels; using LeaderboardBackend.Services; +using LeaderboardBackend.Test.Lib; using LeaderboardBackend.Test.TestApi; using LeaderboardBackend.Test.TestApi.Extensions; +using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.AspNetCore.TestHost; using Microsoft.EntityFrameworkCore; @@ -372,19 +375,55 @@ public async Task RestoreLeaderboard_OK() res.Id.Should().Be(deletedBoard.Id); res.Slug.Should().Be(deletedBoard.Slug); - res.UpdatedAt.Should().Be(res.CreatedAt + Duration.FromMinutes(1)); + res.UpdatedAt.Should().Be(_clock.GetCurrentInstant()); res.DeletedAt.Should().BeNull(); } [Test] public async Task RestoreLeaderboard_Unauthenticated() { - Func> act = async () => await _apiClient.Put($"/leaderboard/100/restore", new() + Func> act = async () => await _apiClient.Put($"/leaderboard/100/restore", new()); + + await act.Should().ThrowAsync().Where(e => e.Response.StatusCode == HttpStatusCode.Unauthorized); + } + + [Test] + public async Task RestoreLeaderboard_Banned_Unauthorized() + { + string email = "restore-leaderboard-banned@example.com"; + string password = "P4ssword"; + + UserViewModel userModel = await _apiClient.RegisterUser( + "RestoreBoardBanned", + email, + password + ); + + string jwt = (await _apiClient.LoginUser(email, password)).Token; + + ApplicationContext context = _factory.Services.CreateScope().ServiceProvider.GetRequiredService(); + + context.Users.Update(new() { - Jwt = "" + Id = userModel.Id, + Role = UserRole.Banned, + Username = userModel.Username, + Email = email, + Password = password }); - await act.Should().ThrowAsync().Where(e => e.Response.StatusCode == HttpStatusCode.Unauthorized); + await context.SaveChangesAsync(); + + await FluentActions.Awaiting( + async () => await _apiClient.Put( + $"/leaderboard/1/restore", + new() + { + Jwt = jwt, + } + ) + ).Should().ThrowAsync() + .Where(e => e.Response.StatusCode == HttpStatusCode.Forbidden); } [TestCase("restore-leaderboard-unauth1@example.com", "RestoreBoard1", UserRole.Confirmed)] @@ -395,7 +434,7 @@ public async Task RestoreLeaderboard_Unauthorized(string email, string username, ApplicationContext context = _factory.Services.CreateScope().ServiceProvider.GetRequiredService(); - User? user = await context.Users.FindAsync([userModel.Id]); + User? user = await context.Users.FindAsync(userModel.Id); context.Users.Update(user!); user!.Role = role; @@ -430,7 +469,7 @@ public async Task RestoreLeaderboard_NotFound_WasNeverDeleted() Leaderboard board = new() { - Name = "Hyper Mario World", + Name = "Hyper Mario World Not Deleted", Slug = "hyper-mario-world-non-deleted", }; @@ -438,12 +477,62 @@ public async Task RestoreLeaderboard_NotFound_WasNeverDeleted() await context.SaveChangesAsync(); board.Id.Should().NotBe(default); - Func> act = async () => await _apiClient.Put($"/leaderboard/{board.Id}/restore", new() + try { - Jwt = _jwt - }); + await _apiClient.Put( + $"/leaderboard/{board.Id}/restore", + new() + { + Jwt = _jwt, + } + ); + } + catch (RequestFailureException e) + { + e.Response.StatusCode.Should().Be(HttpStatusCode.NotFound); + string? model = await e.Response.Content.ReadAsStringAsync(); + model!.Should().MatchRegex("Not Deleted"); + } + } + + [Test] + public async Task RestoreLeaderboard_Conflict() + { + ApplicationContext context = _factory.Services.CreateScope().ServiceProvider.GetRequiredService(); + + Leaderboard deleted = new() + { + Name = "Conflicted Mario World", + Slug = "conflicted-mario-world", + DeletedAt = _clock.GetCurrentInstant() + }; - await act.Should().ThrowAsync() - .Where(e => e.Response.StatusCode == HttpStatusCode.NotFound); + Leaderboard reclaimed = new() + { + Name = "Reclaimed Mario World", + Slug = "conflicted-mario-world", + }; + + context.Leaderboards.Add(deleted); + context.Leaderboards.Add(reclaimed); + await context.SaveChangesAsync(); + + try + { + await _apiClient.Put( + $"/leaderboard/{deleted.Id}/restore", + new() + { + Jwt = _jwt, + } + ); + } + catch (RequestFailureException e) + { + e.Response.StatusCode.Should().Be(HttpStatusCode.Conflict); + LeaderboardViewModel? model = await e.Response.Content.ReadFromJsonAsync(TestInitCommonFields.JsonSerializerOptions); + model.Should().NotBeNull(); + model!.Slug.Should().Be("conflicted-mario-world"); + } } } diff --git a/LeaderboardBackend/Controllers/LeaderboardsController.cs b/LeaderboardBackend/Controllers/LeaderboardsController.cs index 092ba2a0..b279ed5b 100644 --- a/LeaderboardBackend/Controllers/LeaderboardsController.cs +++ b/LeaderboardBackend/Controllers/LeaderboardsController.cs @@ -105,11 +105,8 @@ long id board => Ok(LeaderboardViewModel.MapFrom(board)), notFound => NotFound(), neverDeleted => - { - ModelState.AddModelError("Leaderboard", "LeaderboardWasNeverPreviouslyDeleted"); - return NotFound("Was never deleted"); - }, - conflict => Conflict(conflict.Board) + NotFound(ProblemDetailsFactory.CreateProblemDetails(HttpContext, 404, "Not Deleted")), + conflict => Conflict(LeaderboardViewModel.MapFrom(conflict.Board)) ); } } diff --git a/LeaderboardBackend/Services/Impl/LeaderboardService.cs b/LeaderboardBackend/Services/Impl/LeaderboardService.cs index c7b09b85..d5f26cc1 100644 --- a/LeaderboardBackend/Services/Impl/LeaderboardService.cs +++ b/LeaderboardBackend/Services/Impl/LeaderboardService.cs @@ -65,8 +65,6 @@ public async Task RestoreLeaderboard(long id) return new RestoreLeaderboardConflict(maybe); } - applicationContext.Leaderboards.Update(lb); - lb.DeletedAt = null; await applicationContext.SaveChangesAsync(); From 68e35b6d90398c9fd37ecc9aa1edee9cb70a0767 Mon Sep 17 00:00:00 2001 From: zysim <9867871+zysim@users.noreply.github.com> Date: Tue, 22 Oct 2024 00:40:36 +0800 Subject: [PATCH 08/11] Address more comments --- LeaderboardBackend.Test/Leaderboards.cs | 51 +++++++------------ .../Controllers/LeaderboardsController.cs | 4 +- .../Services/Impl/LeaderboardService.cs | 2 +- LeaderboardBackend/openapi.json | 6 +-- 4 files changed, 25 insertions(+), 38 deletions(-) diff --git a/LeaderboardBackend.Test/Leaderboards.cs b/LeaderboardBackend.Test/Leaderboards.cs index d41905e4..44b8f80d 100644 --- a/LeaderboardBackend.Test/Leaderboards.cs +++ b/LeaderboardBackend.Test/Leaderboards.cs @@ -3,6 +3,7 @@ using System.Net; using System.Net.Http.Json; using System.Threading.Tasks; +using FluentAssertions.Specialized; using LeaderboardBackend.Models.Entities; using LeaderboardBackend.Models.Requests; using LeaderboardBackend.Models.ViewModels; @@ -403,14 +404,8 @@ public async Task RestoreLeaderboard_Banned_Unauthorized() ApplicationContext context = _factory.Services.CreateScope().ServiceProvider.GetRequiredService(); - context.Users.Update(new() - { - Id = userModel.Id, - Role = UserRole.Banned, - Username = userModel.Username, - Email = email, - Password = password - }); + User update = await context.Users.FirstAsync(user => user.Id == userModel.Id); + update.Role = UserRole.Banned; await context.SaveChangesAsync(); @@ -436,7 +431,6 @@ public async Task RestoreLeaderboard_Unauthorized(string email, string username, User? user = await context.Users.FindAsync(userModel.Id); - context.Users.Update(user!); user!.Role = role; await context.SaveChangesAsync(); @@ -477,22 +471,19 @@ public async Task RestoreLeaderboard_NotFound_WasNeverDeleted() await context.SaveChangesAsync(); board.Id.Should().NotBe(default); - try - { - await _apiClient.Put( + ExceptionAssertions exAssert = await FluentActions.Awaiting(() => + _apiClient.Put( $"/leaderboard/{board.Id}/restore", new() { Jwt = _jwt, } - ); - } - catch (RequestFailureException e) - { - e.Response.StatusCode.Should().Be(HttpStatusCode.NotFound); - string? model = await e.Response.Content.ReadAsStringAsync(); - model!.Should().MatchRegex("Not Deleted"); - } + ) + ).Should().ThrowAsync().Where(ex => ex.Response.StatusCode == HttpStatusCode.NotFound); + + ProblemDetails? problemDetails = await exAssert.Which.Response.Content.ReadFromJsonAsync(TestInitCommonFields.JsonSerializerOptions); + problemDetails.Should().NotBeNull(); + problemDetails!.Title.Should().Be("Not Deleted"); } [Test] @@ -517,22 +508,18 @@ public async Task RestoreLeaderboard_Conflict() context.Leaderboards.Add(reclaimed); await context.SaveChangesAsync(); - try - { - await _apiClient.Put( + ExceptionAssertions exAssert = await FluentActions.Awaiting(() => + _apiClient.Put( $"/leaderboard/{deleted.Id}/restore", new() { Jwt = _jwt, } - ); - } - catch (RequestFailureException e) - { - e.Response.StatusCode.Should().Be(HttpStatusCode.Conflict); - LeaderboardViewModel? model = await e.Response.Content.ReadFromJsonAsync(TestInitCommonFields.JsonSerializerOptions); - model.Should().NotBeNull(); - model!.Slug.Should().Be("conflicted-mario-world"); - } + ) + ).Should().ThrowAsync().Where(ex => ex.Response.StatusCode == HttpStatusCode.Conflict); + + LeaderboardViewModel? model = await exAssert.Which.Response.Content.ReadFromJsonAsync(TestInitCommonFields.JsonSerializerOptions); + model.Should().NotBeNull(); + model!.Slug.Should().Be("conflicted-mario-world"); } } diff --git a/LeaderboardBackend/Controllers/LeaderboardsController.cs b/LeaderboardBackend/Controllers/LeaderboardsController.cs index b279ed5b..039871c5 100644 --- a/LeaderboardBackend/Controllers/LeaderboardsController.cs +++ b/LeaderboardBackend/Controllers/LeaderboardsController.cs @@ -93,8 +93,8 @@ public async Task> CreateLeaderboard( [SwaggerResponse(200, "The restored `Leaderboard`s view model.", typeof(LeaderboardViewModel))] [SwaggerResponse(401)] [SwaggerResponse(403, "The requesting `User` is unauthorized to restore `Leaderboard`s.")] - [SwaggerResponse(404, "The `Leaderboard` was not found, or it wasn't deleted in the first place.", typeof(string))] - [SwaggerResponse(409, "Another `Leaderboard` with the same slug has been created since, and therefore can't be restored.", typeof(LeaderboardViewModel))] + [SwaggerResponse(404, "The `Leaderboard` was not found, or it wasn't deleted in the first place. Includes a field, `title`, which will be \"Not Found\" in the former case, and \"Not Deleted\" in the latter.", typeof(ProblemDetails))] + [SwaggerResponse(409, "Another `Leaderboard` with the same slug has been created since, and therefore can't be restored. Will include the conflicting board in the response.", typeof(LeaderboardViewModel))] public async Task> RestoreLeaderboard( long id ) diff --git a/LeaderboardBackend/Services/Impl/LeaderboardService.cs b/LeaderboardBackend/Services/Impl/LeaderboardService.cs index d5f26cc1..1754ddac 100644 --- a/LeaderboardBackend/Services/Impl/LeaderboardService.cs +++ b/LeaderboardBackend/Services/Impl/LeaderboardService.cs @@ -46,7 +46,7 @@ public async Task CreateLeaderboard(CreateLeaderboardRe public async Task RestoreLeaderboard(long id) { - Leaderboard? lb = await applicationContext.Leaderboards.FindAsync([id]); + Leaderboard? lb = await applicationContext.Leaderboards.FindAsync(id); if (lb == null) { diff --git a/LeaderboardBackend/openapi.json b/LeaderboardBackend/openapi.json index 01166b48..4e8fabb1 100644 --- a/LeaderboardBackend/openapi.json +++ b/LeaderboardBackend/openapi.json @@ -713,17 +713,17 @@ "description": "The requesting `User` is unauthorized to restore `Leaderboard`s." }, "404": { - "description": "The `Leaderboard` was not found, or it wasn't deleted in the first place.", + "description": "The `Leaderboard` was not found, or it wasn't deleted in the first place. Includes a field, `title`, which will be \"Not Found\" in the former case, and \"Not Deleted\" in the latter.", "content": { "application/json": { "schema": { - "type": "string" + "$ref": "#/components/schemas/ProblemDetails" } } } }, "409": { - "description": "Another `Leaderboard` with the same slug has been created since, and therefore can't be restored.", + "description": "Another `Leaderboard` with the same slug has been created since, and therefore can't be restored. Will include the conflicting board in the response.", "content": { "application/json": { "schema": { From d86c90a116e231094da85c1e12740e9cef6c18f9 Mon Sep 17 00:00:00 2001 From: zysim <9867871+zysim@users.noreply.github.com> Date: Wed, 23 Oct 2024 09:52:09 +0800 Subject: [PATCH 09/11] Remove unnecessary DB calls in service method --- .../Services/Impl/LeaderboardService.cs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/LeaderboardBackend/Services/Impl/LeaderboardService.cs b/LeaderboardBackend/Services/Impl/LeaderboardService.cs index 1754ddac..ae1feba6 100644 --- a/LeaderboardBackend/Services/Impl/LeaderboardService.cs +++ b/LeaderboardBackend/Services/Impl/LeaderboardService.cs @@ -47,7 +47,7 @@ public async Task CreateLeaderboard(CreateLeaderboardRe public async Task RestoreLeaderboard(long id) { Leaderboard? lb = await applicationContext.Leaderboards.FindAsync(id); - + if (lb == null) { return new LeaderboardNotFound(); @@ -58,16 +58,18 @@ public async Task RestoreLeaderboard(long id) return new LeaderboardNeverDeleted(); } - Leaderboard? maybe = await applicationContext.Leaderboards.SingleOrDefaultAsync(board => board.Slug == lb.Slug && board.DeletedAt == null); + lb.DeletedAt = null; - if (maybe != null) + try { - return new RestoreLeaderboardConflict(maybe); + await applicationContext.SaveChangesAsync(); + } + catch (DbUpdateException e) + when(e.InnerException is PostgresException { SqlState: PostgresErrorCodes.UniqueViolation } pgEx) + { + Leaderboard conflict = await applicationContext.Leaderboards.SingleAsync(c => c.Slug == lb.Slug && c.DeletedAt == null); + return new RestoreLeaderboardConflict(conflict); } - - lb.DeletedAt = null; - - await applicationContext.SaveChangesAsync(); return lb; } From a5ee3c1d0aaeaa4f812adf33e21138e7d172c12b Mon Sep 17 00:00:00 2001 From: zysim <9867871+zysim@users.noreply.github.com> Date: Wed, 23 Oct 2024 22:18:05 +0800 Subject: [PATCH 10/11] Formatting --- LeaderboardBackend/Services/Impl/LeaderboardService.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/LeaderboardBackend/Services/Impl/LeaderboardService.cs b/LeaderboardBackend/Services/Impl/LeaderboardService.cs index ae1feba6..00da12fd 100644 --- a/LeaderboardBackend/Services/Impl/LeaderboardService.cs +++ b/LeaderboardBackend/Services/Impl/LeaderboardService.cs @@ -47,7 +47,7 @@ public async Task CreateLeaderboard(CreateLeaderboardRe public async Task RestoreLeaderboard(long id) { Leaderboard? lb = await applicationContext.Leaderboards.FindAsync(id); - + if (lb == null) { return new LeaderboardNotFound(); @@ -65,7 +65,7 @@ public async Task RestoreLeaderboard(long id) await applicationContext.SaveChangesAsync(); } catch (DbUpdateException e) - when(e.InnerException is PostgresException { SqlState: PostgresErrorCodes.UniqueViolation } pgEx) + when (e.InnerException is PostgresException { SqlState: PostgresErrorCodes.UniqueViolation } pgEx) { Leaderboard conflict = await applicationContext.Leaderboards.SingleAsync(c => c.Slug == lb.Slug && c.DeletedAt == null); return new RestoreLeaderboardConflict(conflict); From 8f5227db24ced05c3ad5b6f1dab8ae37342a8a45 Mon Sep 17 00:00:00 2001 From: zysim <9867871+zysim@users.noreply.github.com> Date: Thu, 24 Oct 2024 01:32:47 +0800 Subject: [PATCH 11/11] Change assertion in test --- LeaderboardBackend.Test/Leaderboards.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LeaderboardBackend.Test/Leaderboards.cs b/LeaderboardBackend.Test/Leaderboards.cs index 44b8f80d..17c7830b 100644 --- a/LeaderboardBackend.Test/Leaderboards.cs +++ b/LeaderboardBackend.Test/Leaderboards.cs @@ -520,6 +520,6 @@ public async Task RestoreLeaderboard_Conflict() LeaderboardViewModel? model = await exAssert.Which.Response.Content.ReadFromJsonAsync(TestInitCommonFields.JsonSerializerOptions); model.Should().NotBeNull(); - model!.Slug.Should().Be("conflicted-mario-world"); + model!.Id.Should().Be(reclaimed.Id); } }