Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
17700e7
Move types into ".Species"
Weffuls Oct 14, 2026
8398555
Move and rename types into ".PokemonTypes"
Weffuls Oct 14, 2026
b0c25ed
Rename ServerResponse to ServerJSONResponse
Weffuls Oct 23, 2026
eb31339
Add result-pattern
Weffuls Oct 25, 2026
ecab321
Use Result-Pattern in RequestHandlers (breaking)
Weffuls Oct 26, 2026
edcaea4
Use Result-Pattern in Auth RequestHandlers.
Weffuls Oct 26, 2026
bf592a5
IEnumerable in ResponseResultPermissionError
Weffuls Oct 26, 2026
e8be031
Add logic in TryAuthenticateUser
Weffuls Oct 26, 2026
79e07d2
Use Result-Pattern in Permissions RequestHandlers
Weffuls Oct 26, 2026
26ebee9
Use Result-Pattern in PokemonTypes RequestHandlers
Weffuls Oct 26, 2026
dc98f64
Add CommonResponseHandlerChecks
Weffuls Oct 26, 2026
46793aa
Use Result-Pattern in Roles RequestHandlers
Weffuls Oct 26, 2026
f54c91c
Use Result-Pattern in Species RequestHandlers
Weffuls Oct 27, 2026
358c222
Use Result-Pattern in Users RequesetHandlers (fixing)
Weffuls Oct 27, 2026
6d9bead
Make JsonRequestHandler use Result-Pattern
Weffuls Oct 28, 2026
6f44722
Rename TResult to TSuccess and document
Weffuls Oct 29, 2026
b6de6e3
Remove ResponseJsonResult for ResponseResult
Weffuls Oct 29, 2026
bff2ee8
Document ResultError
Weffuls Oct 29, 2026
2d414e1
Refactor ResponseResultDataMissingError to ResponseResultIndexMissing…
Weffuls Oct 29, 2026
024ca4b
Further documented ResponseResultErrors
Weffuls Oct 30, 2026
d66dc35
Improve ResponseResultHierarchyError message.
Weffuls Oct 30, 2026
cb4c2ce
Document CommonResponseHandlerChecks
Weffuls Oct 30, 2026
6bb39f8
Add PMDDesktop.Server.Tests
Weffuls Nov 2, 2026
8b995ec
Fixed oversight in IRequestHandlerTests.AllRequestsHaveHandlers
Weffuls Nov 2, 2026
3e4b14c
Change ServerImageResponse to ServerStreamResponse
Weffuls Nov 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions PMDDesktop.Server.Tests/PMDDesktop.Server.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="../PMDDesktop.Server/PMDDesktop.Server.csproj" />
<ProjectReference Include="../PMDDesktop/PMDDesktop.csproj" />
</ItemGroup>

<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>

</Project>
42 changes: 42 additions & 0 deletions PMDDesktop.Server.Tests/RequestHandlers/IRequestHandlerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using PMDDesktop.Requests;
using PMDDesktop.Server.RequestHandlers;
using PMDDesktop.Utils;

namespace PMDDesktop.Server.Tests.RequestHandlers;

public class IRequestHandlerTests
{

/// <summary>
/// This test makes sure that every <see cref="ServerRequest{}"/> has exactly one corresponding <see cref="IRequestHandler"/>.
/// </summary>
[Fact]
public void AllRequestsHaveHandlers()
{

IEnumerable<Type> handlerTypes = TypeUtils.GetInstanceableClassesAssignableTo(typeof(IRequestHandler));

List<Type> handledRequestTypes = [];

foreach (Type type in handlerTypes)
{

IRequestHandler handler =
((IRequestHandler?)Activator.CreateInstance(type))
?? throw new InvalidCastException($"Unable to cast instance of {type} to {typeof(IRequestHandler).FullName}");

Type requestType = handler.GetRequestType();

handledRequestTypes.Add(requestType);

}

Assert.Distinct(handledRequestTypes);

List<Type> allRequests = [.. TypeUtils.GetInstanceableClassesAssignableTo(typeof(ServerRequest<>))];

Assert.All(allRequests, (req) => Assert.Contains(req, handledRequestTypes));

}

}
7 changes: 7 additions & 0 deletions PMDDesktop.Server.Users/IUserHierarchyComparable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,11 @@ public interface IUserHierarchyComparable
/// <returns>Number indicating the highest place in the hierarchy this user has.</returns>
int GetHierarchyPosition();

/// <summary>
/// <para>Returns a name that is reasonable to show to clients.</para>
/// <para>This is used in error messages relating to hierarchy comparisons.</para>
/// </summary>
/// <returns>A name to be displayed to clients.</returns>
string GetClientFacingName();

}
5 changes: 5 additions & 0 deletions PMDDesktop.Server.Users/Roles/UserRole.cs
Original file line number Diff line number Diff line change
Expand Up @@ -274,4 +274,9 @@ public int GetCurrentOrderIndex()

public int GetHierarchyPosition() => GetCurrentOrderIndex();

public string GetClientFacingName()
{
return Name;
}

}
11 changes: 8 additions & 3 deletions PMDDesktop.Server.Users/User.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
using Microsoft.AspNetCore.Identity;
using PMDDesktop.Server.Users.Roles;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Identity;
using PMDDesktop.Server.Users.Roles;

namespace PMDDesktop.Server.Users;

Expand Down Expand Up @@ -682,6 +682,11 @@ public int GetHierarchyPosition()

}

public string GetClientFacingName()
{
return Name;
}

#endregion

}
4 changes: 4 additions & 0 deletions PMDDesktop.Server/PMDDesktop.Server.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
<PackageReference Include="System.CommandLine" Version="2.0.10" />
</ItemGroup>

<ItemGroup>
<InternalsVisibleTo Include="PMDDesktop.Server.Tests" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="../PMDDesktop.Server.Users/PMDDesktop.Server.Users.csproj" />
<ProjectReference Include="..\PMDDesktop.Server.Assets\PMDDesktop.Server.Assets.csproj" />
Expand Down
10 changes: 5 additions & 5 deletions PMDDesktop.Server/RequestHandlers/Auth/LoginRequestHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,19 @@ namespace PMDDesktop.Server.RequestHandlers.Auth;
public class LoginRequestHandler : JsonRequestHandler<LoginRequest, LoginResponse>
{

protected override async Task<LoginResponse> CreateResponse(LoginRequest request, GameServer game, User? requestingUser)
protected override async Task<ResponseResult<LoginResponse, ResponseResultError>> CreateResponse(LoginRequest request, GameServer game, User? requestingUser)
{

if (!game.State.Users.TryGetUserByLoginHandle(request.Handle, out User? user))
throw new UserRequestException($"Invalid username or password.");
return new(new ResponseResultLoginError());

if (!await user.VerifyPassword(request.Password))
throw new UserRequestException($"Invalid username or password."); // Throw the same thing to be more "secure" I guess.
return new(new ResponseResultLoginError());

return new()
return new(new LoginResponse()
{
AuthorizationToken = (await user.CreateAccessToken()).TokenString
};
});

}

Expand Down
11 changes: 11 additions & 0 deletions PMDDesktop.Server/RequestHandlers/Auth/ResponseResultLoginError.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using PMDDesktop.Server.RequestHandlers;

namespace PMDDesktop.Server.RequestHandlers.Auth;

public sealed class ResponseResultLoginError() : ResponseResultError
{

public override string GetErrorReason() => $"Invalid handle or password.";
public override int GetStatusCode() => 400;

}
10 changes: 5 additions & 5 deletions PMDDesktop.Server/RequestHandlers/Auth/WhoAmIRequestHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,16 @@ namespace PMDDesktop.Server.RequestHandlers.Auth;
public class WhoAmIRequestHandler : JsonRequestHandler<WhoAmIRequest, WhoAmIResponse>
{

protected override async Task<WhoAmIResponse> CreateResponse(WhoAmIRequest request, GameServer game, User? requestingUser)
protected override async Task<ResponseResult<WhoAmIResponse, ResponseResultError>> CreateResponse(WhoAmIRequest request, GameServer game, User? requestingUser)
{

if (requestingUser is null)
throw new UserRequestException($"Please send your valid token in the Authorization header to access this endpoint.");
if (!CommonResponseHandlerChecks.TryAuthorizeUser(requestingUser, [], out ResponseResultError? error))
return new(error);

return new()
return new(new WhoAmIResponse()
{
GUID = requestingUser.GUID
};
});

}

Expand Down
177 changes: 177 additions & 0 deletions PMDDesktop.Server/RequestHandlers/CommonResponseHandlerChecks.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
using System.Diagnostics.CodeAnalysis;
using PMDDesktop.Server.Assets;
using PMDDesktop.Server.Saving;
using PMDDesktop.Server.Users;
using PMDDesktop.Server.Users.Roles;

namespace PMDDesktop.Server.RequestHandlers;

internal static class CommonResponseHandlerChecks
{

/// <summary>
/// <para>Try to authenticate a <paramref name="user"/>, and authorize them against certain permissions.</para>
/// <para>To pass, the <paramref name="user"/> needs to both be not null, and have all the <see cref="UserPermission"/>s in <paramref name="permissionsNeeded"/>.</para>
/// </summary>
/// <param name="user">The <see cref="User"/> to check the authentication and authorization of.</param>
/// <param name="permissionsNeeded">A list of all permissions needed to be fully authorized.</param>
/// <param name="error">A <see cref="ResponseResultAuthenticationError"/> or <see cref="ResponseResultPermissionError"/> describing why authentication/authorization failed, to be returned.</param>
/// <returns>True if authenticated and authorized; if false, <paramref name="error"/> contains the reason the authentication/authorization failed.</returns>
internal static bool TryAuthorizeUser([NotNullWhen(true)] User? user, IEnumerable<UserPermission> permissionsNeeded, [NotNullWhen(false)] out ResponseResultError? error)
{

if (user is null)
{
error = new ResponseResultAuthenticationError(null);
return false;
}

IEnumerable<UserPermission> missingPermissions = permissionsNeeded.Where(permission => !user.HasPermission(permission));

if (missingPermissions.Any())
{
error = new ResponseResultPermissionError(missingPermissions);
return false;
}

error = null;
return true;

}

/// <summary>
/// <para>Helper function to call <see cref="IUserIndexable.TryGetUser(Guid, out User?)"/>, but additionally output a <see cref="ResponseResultError"/> if no <see cref="User"/> is found.</para>
/// <para>Intended to be used in senarios where a response cannot continue if the <see cref="User"/> cannot be found.</para>
/// </summary>
/// <param name="indexable">A <see cref="IUserIndexable"/> to get the <see cref="User"/> from.</param>
/// <param name="guid">The <see cref="Guid"/> of the <see cref="User"/> you're trying to find.</param>
/// <param name="user">The <see cref="User"/>, if one was found. </param>
/// <param name="error">An <see cref="ResponseResultError"/>, if no <see cref="User"/> was found.</param>
/// <returns>True if <paramref name="user"/> is not null; False if <paramref name="error"/> is not null.</returns>
internal static bool TryGetUser(IUserIndexable indexable, Guid guid, [NotNullWhen(true)] out User? user, [NotNullWhen(false)] out ResponseResultError? error)
{

if (indexable.TryGetUser(guid, out user))
{
error = null;
return true;
}

error = new ResponseResultIndexMissingError<User>(guid.ToString());
return false;

}

/// <summary>
/// <para>Helper function to call <see cref="IRoleIndexable.TryGetRole(Guid, out UserRole?)"/>, but additionally output a <see cref="ResponseResultError"/> if no <see cref="UserRole"/> is found.</para>
/// <para>Intended to be used in senarios where a response cannot continue if the <see cref="UserRole"/> cannot be found.</para>
/// </summary>
/// <param name="indexable">A <see cref="IUserIndexable"/> to get the <see cref="UserRole"/> from.</param>
/// <param name="guid">The <see cref="Guid"/> of the <see cref="UserRole"/> you're trying to find.</param>
/// <param name="role">The <see cref="UserRole"/>, if one was found. </param>
/// <param name="error">An <see cref="ResponseResultError"/>, if no <see cref="UserRole"/> was found.</param>
/// <returns>True if <paramref name="role"/> is not null; False if <paramref name="error"/> is not null.</returns>
internal static bool TryGetRole(IRoleIndexable indexable, Guid guid, [NotNullWhen(true)] out UserRole? role, [NotNullWhen(false)] out ResponseResultError? error)
{

if (indexable.TryGetRole(guid, out role))
{
error = null;
return true;
}

error = new ResponseResultIndexMissingError<UserRole>(guid.ToString());
return false;

}

/// <summary>
/// <para>Helper function to find a <see cref="UserPermission"/> by its <see cref="UserPermission.DataName"/>, but additionally output a <see cref="ResponseResultError"/> if no <see cref="UserPermission"/> is found.</para>
/// <para>Intended to be used in senarios where a response cannot continue if the <see cref="UserPermission"/> cannot be found.</para>
/// </summary>
/// <param name="dataName">The <see cref="UserPermission.DataName"/> corresponding to the <see cref="UserPermission"/> you're looking for </param>
/// <param name="permission">The <see cref="UserPermission"/>, if one was found. </param>
/// <param name="error">An <see cref="ResponseResultError"/>, if no <see cref="UserPermission"/> was found.</param>
/// <returns>True if <paramref name="permission"/> is not null; False if <paramref name="error"/> is not null.</returns>
internal static bool TryGetPermission(string dataName, [NotNullWhen(true)] out UserPermission? permission, [NotNullWhen(false)] out ResponseResultError? error)
{

if (UserPermission.TryGetPermissionByDataName(dataName, out permission))
{
error = null;
return true;
}

error = new ResponseResultIndexMissingError<UserPermission>(dataName);
return false;

}

/// <summary>
/// <para>Helper function to call <see cref="ISaveDataIndexable.TryGetSave{T}(Guid, out T?)"/>, but additionally output a <see cref="ResponseResultError"/> if no <see cref="SaveData"/> is found.</para>
/// <para>Intended to be used in senarios where a response cannot continue if the <see cref="SaveData"/> cannot be found.</para>
/// </summary>
/// <param name="indexable">A <see cref="ISaveDataIndexable"/> to get the <typeparamref name="TData"/> from.</param>
/// <param name="guid">The <see cref="Guid"/> of the <typeparamref name="TData"/> you're trying to find.</param>
/// <param name="data">The <typeparamref name="TData"/> if one was found. </param>
/// <param name="error">An <see cref="ResponseResultError"/>, if no <typeparamref name="TData"/> was found.</param>
/// <returns>True if <paramref name="data"/> is not null; False if <paramref name="error"/> is not null.</returns>
internal static bool TryGetSaveData<TData>(ISaveDataIndexable indexable, Guid guid, [NotNullWhen(true)] out TData? data, [NotNullWhen(false)] out ResponseResultError? error) where TData : SaveData
{

if (indexable.TryGetSave(guid, out data))
{
error = null;
return true;
}

error = new ResponseResultIndexMissingError<TData>(guid.ToString());
return false;

}

/// <summary>
/// <para>Helper function to call <see cref="IAssetIndexable.TryGetAsset{T}(AssetLocation, out T?)"/>, but additionally output a <see cref="ResponseResultError"/> if no <see cref="Asset"/> is found.</para>
/// <para>Intended to be used in senarios where a response cannot continue if the <see cref="Asset"/> cannot be found.</para>
/// </summary>
/// <param name="indexable">A <see cref="IAssetIndexable"/> to get the <typeparamref name="TAsset"/> from.</param>
/// <param name="location">The <see cref="AssetLocation"/> of the <typeparamref name="TAsset"/> you're trying to find.</param>
/// <param name="asset">The <typeparamref name="TAsset"/> if one was found. </param>
/// <param name="error">An <see cref="ResponseResultError"/>, if no <typeparamref name="TAsset"/> was found.</param>
/// <returns>True if <paramref name="data"/> is not null; False if <paramref name="error"/> is not null.</returns>
internal static bool TryGetAsset<TAsset>(IAssetIndexable indexable, AssetLocation location, [NotNullWhen(true)] out TAsset? asset, [NotNullWhen(false)] out ResponseResultError? error) where TAsset : Asset
{

if (indexable.TryGetAsset(location, out asset))
{
error = null;
return true;
}

error = new ResponseResultIndexMissingError<TAsset>(location);
return false;

}

/// <summary>
/// Compares the <paramref name="requestingUser"/> to the provided <see cref="IUserHierarchyComparable"/>, returning true if <paramref name="requestingUser"/> is higher, and false + a <see cref="ResponseResultError"/> otherwise.
/// </summary>
/// <param name="requestingUser">The <see cref="User"/> who needs to be higher than <paramref name="expectedLower"/>.</param>
/// <param name="expectedLower">The <see cref="IUserHierarchyComparable"/> that needs to be lower than <paramref name="requestingUser"/>.</param>
/// <param name="error">An <see cref="ResponseResultError"/>, if <paramref name="requestingUser"/> was equal or lower than <paramref name="expectedLower"/>.</param>
/// <returns>True if user is higher; False if user is equal or lower + <paramref name="error"/> will not be null. </returns>
internal static bool TryCompareHierarchy(User requestingUser, IUserHierarchyComparable expectedLower, [NotNullWhen(false)] out ResponseResultError? error)
{

if (!requestingUser.HigherThan(expectedLower))
{
error = new ResponseResultHierarchyError(requestingUser, expectedLower);
return false;
}

error = null;
return true;

}

}
4 changes: 2 additions & 2 deletions PMDDesktop.Server/RequestHandlers/IRequestHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ static void AddApiPoints(GameServer server)

Type requestType = handler.GetRequestType();

if (!ServerRequest<ServerResponse>.ATTRIBUTES.TryGetValue(requestType, out ServerRequestAttribute? attribute))
if (!ServerRequest<ServerJsonResponse>.ATTRIBUTES.TryGetValue(requestType, out ServerRequestAttribute? attribute))
throw new MissingAttributeException(requestType, typeof(ServerRequestAttribute));

EndpointMapper mapper = ServerRequest<ServerResponse>.IsAllQueryProperties(requestType)
EndpointMapper mapper = ServerRequest<ServerJsonResponse>.IsAllQueryProperties(requestType)
? server.WebApp.MapGet
: server.WebApp.MapPost;

Expand Down
Loading
Loading