diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 6287fe9..d8ae244 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -21,7 +21,7 @@ build:
test:
stage: test
script:
- - dotnet test --no-restore --verbosity normal
+ - dotnet test --verbosity normal
publish:
stage: publish
diff --git a/HopFrame.sln b/HopFrame.sln
index 2e65007..06d8283 100644
--- a/HopFrame.sln
+++ b/HopFrame.sln
@@ -2,7 +2,7 @@
Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Database", "src\HopFrame.Database\HopFrame.Database.csproj", "{003120AE-F38B-4632-8497-BE4505189627}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RestApiTest", "test\RestApiTest\RestApiTest.csproj", "{921159CE-AF75-44C3-A3F9-6B9B1A4E85CF}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Testing.Api", "testing\HopFrame.Testing.Api\HopFrame.Testing.Api.csproj", "{921159CE-AF75-44C3-A3F9-6B9B1A4E85CF}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Security", "src\HopFrame.Security\HopFrame.Security.csproj", "{7F82E1C6-4A42-4337-9E03-2EE6429D004F}"
EndProject
@@ -10,10 +10,24 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Api", "src\HopFram
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Web", "src\HopFrame.Web\HopFrame.Web.csproj", "{3BE585BC-13A5-4BE4-A806-E9EC2D825956}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FrontendTest", "test\FrontendTest\FrontendTest.csproj", "{8F983A37-63CF-48D5-988D-58B78EF8AECD}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Testing.Web", "testing\HopFrame.Testing.Web\HopFrame.Testing.Web.csproj", "{8F983A37-63CF-48D5-988D-58B78EF8AECD}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Web.Admin", "src\HopFrame.Web.Admin\HopFrame.Web.Admin.csproj", "{02D9F10A-664A-4EF7-BF19-310C26FF4DEB}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Core", "Core", "{64EDCBED-A84F-4936-8697-78DC43CB2427}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Testing", "Testing", "{EEA20D27-D471-44AF-A287-C0E068D93182}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{1D98E5DE-CB8B-4C1C-A319-D49AC137441A}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Tests.Database", "tests\HopFrame.Tests.Database\HopFrame.Tests.Database.csproj", "{1CAAC943-B8FE-48DD-9712-92699647DE18}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Tests.Security", "tests\HopFrame.Tests.Security\HopFrame.Tests.Security.csproj", "{6747753A-6059-48F1-B779-D73765A373A6}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Tests.Api", "tests\HopFrame.Tests.Api\HopFrame.Tests.Api.csproj", "{25DE1510-47E5-46FF-89A4-B9F99542218E}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Tests.Web", "tests\HopFrame.Tests.Web\HopFrame.Tests.Web.csproj", "{566C13B9-4ECA-48C4-8D02-FEB6CDF523E6}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -48,7 +62,34 @@ Global
{02D9F10A-664A-4EF7-BF19-310C26FF4DEB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{02D9F10A-664A-4EF7-BF19-310C26FF4DEB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{02D9F10A-664A-4EF7-BF19-310C26FF4DEB}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1CAAC943-B8FE-48DD-9712-92699647DE18}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1CAAC943-B8FE-48DD-9712-92699647DE18}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1CAAC943-B8FE-48DD-9712-92699647DE18}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1CAAC943-B8FE-48DD-9712-92699647DE18}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6747753A-6059-48F1-B779-D73765A373A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6747753A-6059-48F1-B779-D73765A373A6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6747753A-6059-48F1-B779-D73765A373A6}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6747753A-6059-48F1-B779-D73765A373A6}.Release|Any CPU.Build.0 = Release|Any CPU
+ {25DE1510-47E5-46FF-89A4-B9F99542218E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {25DE1510-47E5-46FF-89A4-B9F99542218E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {25DE1510-47E5-46FF-89A4-B9F99542218E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {25DE1510-47E5-46FF-89A4-B9F99542218E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {566C13B9-4ECA-48C4-8D02-FEB6CDF523E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {566C13B9-4ECA-48C4-8D02-FEB6CDF523E6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {566C13B9-4ECA-48C4-8D02-FEB6CDF523E6}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {566C13B9-4ECA-48C4-8D02-FEB6CDF523E6}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
+ {1E821490-AEDC-4F55-B758-52F4FADAB53A} = {64EDCBED-A84F-4936-8697-78DC43CB2427}
+ {003120AE-F38B-4632-8497-BE4505189627} = {64EDCBED-A84F-4936-8697-78DC43CB2427}
+ {7F82E1C6-4A42-4337-9E03-2EE6429D004F} = {64EDCBED-A84F-4936-8697-78DC43CB2427}
+ {3BE585BC-13A5-4BE4-A806-E9EC2D825956} = {64EDCBED-A84F-4936-8697-78DC43CB2427}
+ {02D9F10A-664A-4EF7-BF19-310C26FF4DEB} = {64EDCBED-A84F-4936-8697-78DC43CB2427}
+ {8F983A37-63CF-48D5-988D-58B78EF8AECD} = {EEA20D27-D471-44AF-A287-C0E068D93182}
+ {921159CE-AF75-44C3-A3F9-6B9B1A4E85CF} = {EEA20D27-D471-44AF-A287-C0E068D93182}
+ {1CAAC943-B8FE-48DD-9712-92699647DE18} = {1D98E5DE-CB8B-4C1C-A319-D49AC137441A}
+ {6747753A-6059-48F1-B779-D73765A373A6} = {1D98E5DE-CB8B-4C1C-A319-D49AC137441A}
+ {25DE1510-47E5-46FF-89A4-B9F99542218E} = {1D98E5DE-CB8B-4C1C-A319-D49AC137441A}
+ {566C13B9-4ECA-48C4-8D02-FEB6CDF523E6} = {1D98E5DE-CB8B-4C1C-A319-D49AC137441A}
EndGlobalSection
EndGlobal
diff --git a/HopFrame.sln.DotSettings.user b/HopFrame.sln.DotSettings.user
index a38eed3..1e30ef8 100644
--- a/HopFrame.sln.DotSettings.user
+++ b/HopFrame.sln.DotSettings.user
@@ -1,8 +1,74 @@
+ ForceIncluded
ForceIncluded
+ ForceIncluded
+ ForceIncluded
ForceIncluded
+ ForceIncluded
ForceIncluded
<AssemblyExplorer>
<Assembly Path="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\7.0.16\ref\net7.0\System.ComponentModel.Annotations.dll" />
<Assembly Path="C:\Users\Remote\.nuget\packages\blazorstrap\5.2.100.61524\lib\net7.0\BlazorStrap.dll" />
-</AssemblyExplorer>
\ No newline at end of file
+</AssemblyExplorer>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/HopFrame.Api/HopFrame.Api.csproj b/src/HopFrame.Api/HopFrame.Api.csproj
index 744a466..091e5a1 100644
--- a/src/HopFrame.Api/HopFrame.Api.csproj
+++ b/src/HopFrame.Api/HopFrame.Api.csproj
@@ -22,4 +22,10 @@
+
+
+ <_Parameter1>HopFrame.Tests.Api
+
+
+
diff --git a/src/HopFrame.Api/Logic/Implementation/AuthLogic.cs b/src/HopFrame.Api/Logic/Implementation/AuthLogic.cs
index e792add..c1f3c90 100644
--- a/src/HopFrame.Api/Logic/Implementation/AuthLogic.cs
+++ b/src/HopFrame.Api/Logic/Implementation/AuthLogic.cs
@@ -8,7 +8,7 @@ using Microsoft.AspNetCore.Http;
namespace HopFrame.Api.Logic.Implementation;
-public class AuthLogic(IUserRepository users, ITokenRepository tokens, ITokenContext tokenContext, IHttpContextAccessor accessor) : IAuthLogic {
+internal class AuthLogic(IUserRepository users, ITokenRepository tokens, ITokenContext tokenContext, IHttpContextAccessor accessor) : IAuthLogic {
public async Task>> Login(UserLogin login) {
var user = await users.GetUserByEmail(login.Email);
@@ -38,7 +38,7 @@ public class AuthLogic(IUserRepository users, ITokenRepository tokens, ITokenCon
public async Task>> Register(UserRegister register) {
if (register.Password.Length < 8)
- return LogicResult>.Conflict("Password needs to be at least 8 characters long");
+ return LogicResult>.BadRequest("Password needs to be at least 8 characters long");
var allUsers = await users.GetUsers();
if (allUsers.Any(user => user.Username == register.Username || user.Email == register.Email))
@@ -71,18 +71,18 @@ public class AuthLogic(IUserRepository users, ITokenRepository tokens, ITokenCon
var refreshToken = accessor.HttpContext?.Request.Cookies[ITokenContext.RefreshTokenType];
if (string.IsNullOrEmpty(refreshToken))
- return LogicResult>.Conflict("Refresh token not provided");
+ return LogicResult>.BadRequest("Refresh token not provided");
var token = await tokens.GetToken(refreshToken);
- if (token.Type != Token.RefreshTokenType)
- return LogicResult>.BadRequest("The provided token is not a refresh token");
-
if (token is null)
return LogicResult>.NotFound("Refresh token not valid");
+ if (token.Type != Token.RefreshTokenType)
+ return LogicResult>.Conflict("The provided token is not a refresh token");
+
if (token.CreatedAt + HopFrameAuthentication.RefreshTokenTime < DateTime.Now)
- return LogicResult>.Conflict("Refresh token is expired");
+ return LogicResult>.Forbidden("Refresh token is expired");
var accessToken = await tokens.CreateToken(Token.AccessTokenType, token.Owner);
diff --git a/src/HopFrame.Database/HopFrame.Database.csproj b/src/HopFrame.Database/HopFrame.Database.csproj
index de55cd5..cd670af 100644
--- a/src/HopFrame.Database/HopFrame.Database.csproj
+++ b/src/HopFrame.Database/HopFrame.Database.csproj
@@ -22,4 +22,10 @@
+
+
+ <_Parameter1>HopFrame.Tests.Database
+
+
+
diff --git a/src/HopFrame.Security/Authentication/HopFrameAuthentication.cs b/src/HopFrame.Security/Authentication/HopFrameAuthentication.cs
index 9c65b14..8709f91 100644
--- a/src/HopFrame.Security/Authentication/HopFrameAuthentication.cs
+++ b/src/HopFrame.Security/Authentication/HopFrameAuthentication.cs
@@ -17,7 +17,6 @@ public class HopFrameAuthentication(
UrlEncoder encoder,
ISystemClock clock,
ITokenRepository tokens,
- IUserRepository users,
IPermissionRepository perms)
: AuthenticationHandler(options, logger, encoder, clock) {
diff --git a/src/HopFrame.Web/HopFrame.Web.csproj b/src/HopFrame.Web/HopFrame.Web.csproj
index a512b95..1eb0490 100644
--- a/src/HopFrame.Web/HopFrame.Web.csproj
+++ b/src/HopFrame.Web/HopFrame.Web.csproj
@@ -35,4 +35,10 @@
+
+
+ <_Parameter1>HopFrame.Tests.Web
+
+
+
diff --git a/src/HopFrame.Web/Pages/Administration/AdminLogin.razor b/src/HopFrame.Web/Pages/Administration/AdminLogin.razor
index 1d9a61e..8e0f1e1 100644
--- a/src/HopFrame.Web/Pages/Administration/AdminLogin.razor
+++ b/src/HopFrame.Web/Pages/Administration/AdminLogin.razor
@@ -43,7 +43,7 @@
private UserLogin UserLogin { get; set; }
[SupplyParameterFromQuery(Name = "redirect")]
- private string RedirectAfter { get; set; }
+ public string RedirectAfter { get; set; }
private const string DefaultRedirect = "/administration";
diff --git a/src/HopFrame.Web/Pages/Administration/AdminPageList.razor b/src/HopFrame.Web/Pages/Administration/AdminPageList.razor
index 163f66f..bf4c17d 100644
--- a/src/HopFrame.Web/Pages/Administration/AdminPageList.razor
+++ b/src/HopFrame.Web/Pages/Administration/AdminPageList.razor
@@ -33,7 +33,7 @@
-
+
Add Entry
diff --git a/src/HopFrame.Web/Services/Implementation/AuthService.cs b/src/HopFrame.Web/Services/Implementation/AuthService.cs
index 6a96a97..e5f1ec0 100644
--- a/src/HopFrame.Web/Services/Implementation/AuthService.cs
+++ b/src/HopFrame.Web/Services/Implementation/AuthService.cs
@@ -91,15 +91,12 @@ internal class AuthService(
}
public async Task IsLoggedIn() {
- var accessToken = httpAccessor.HttpContext?.Request.Cookies[ITokenContext.AccessTokenType];
- if (string.IsNullOrEmpty(accessToken)) return false;
-
- var tokenEntry = await tokens.GetToken(accessToken);
+ var accessToken = context.AccessToken;
- if (tokenEntry is null) return false;
- if (tokenEntry.Type != Token.AccessTokenType) return false;
- if (tokenEntry.CreatedAt + HopFrameAuthentication.AccessTokenTime < DateTime.Now) return false;
- if (tokenEntry.Owner is null) return false;
+ if (accessToken is null) return false;
+ if (accessToken.Type != Token.AccessTokenType) return false;
+ if (accessToken.CreatedAt + HopFrameAuthentication.AccessTokenTime < DateTime.Now) return false;
+ if (accessToken.Owner is null) return false;
return true;
}
diff --git a/test/FrontendTest/.gitignore b/testing/HopFrame.Testing.Api/.gitignore
similarity index 100%
rename from test/FrontendTest/.gitignore
rename to testing/HopFrame.Testing.Api/.gitignore
diff --git a/test/RestApiTest/Controllers/TestController.cs b/testing/HopFrame.Testing.Api/Controllers/TestController.cs
similarity index 95%
rename from test/RestApiTest/Controllers/TestController.cs
rename to testing/HopFrame.Testing.Api/Controllers/TestController.cs
index 092784f..fb39666 100644
--- a/test/RestApiTest/Controllers/TestController.cs
+++ b/testing/HopFrame.Testing.Api/Controllers/TestController.cs
@@ -2,11 +2,11 @@ using HopFrame.Api.Logic;
using HopFrame.Database.Models;
using HopFrame.Security.Authorization;
using HopFrame.Security.Claims;
+using HopFrame.Testing.Api.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
-using RestApiTest.Models;
-namespace RestApiTest.Controllers;
+namespace HopFrame.Testing.Api.Controllers;
[ApiController]
[Route("test")]
diff --git a/test/RestApiTest/DatabaseContext.cs b/testing/HopFrame.Testing.Api/DatabaseContext.cs
similarity index 80%
rename from test/RestApiTest/DatabaseContext.cs
rename to testing/HopFrame.Testing.Api/DatabaseContext.cs
index ef370c7..4c707f4 100644
--- a/test/RestApiTest/DatabaseContext.cs
+++ b/testing/HopFrame.Testing.Api/DatabaseContext.cs
@@ -1,8 +1,8 @@
using HopFrame.Database;
+using HopFrame.Testing.Api.Models;
using Microsoft.EntityFrameworkCore;
-using RestApiTest.Models;
-namespace RestApiTest;
+namespace HopFrame.Testing.Api;
public class DatabaseContext : HopDbContextBase {
@@ -12,7 +12,7 @@ public class DatabaseContext : HopDbContextBase {
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) {
base.OnConfiguring(optionsBuilder);
- optionsBuilder.UseSqlite(@"Data Source=C:\Users\leon\Documents\Projekte\HopFrame\test\RestApiTest\bin\Debug\net8.0\test.db;Mode=ReadWrite;");
+ optionsBuilder.UseSqlite(@"Data Source=C:\Users\leon\Documents\Projekte\HopFrame\testing\HopFrame.Testing.Api\bin\Debug\net8.0\test.db;Mode=ReadWrite;");
}
protected override void OnModelCreating(ModelBuilder modelBuilder) {
diff --git a/test/RestApiTest/RestApiTest.csproj b/testing/HopFrame.Testing.Api/HopFrame.Testing.Api.csproj
similarity index 100%
rename from test/RestApiTest/RestApiTest.csproj
rename to testing/HopFrame.Testing.Api/HopFrame.Testing.Api.csproj
diff --git a/test/FrontendTest/Models/Address.cs b/testing/HopFrame.Testing.Api/Models/Address.cs
similarity index 92%
rename from test/FrontendTest/Models/Address.cs
rename to testing/HopFrame.Testing.Api/Models/Address.cs
index 386114d..5688ad1 100644
--- a/test/FrontendTest/Models/Address.cs
+++ b/testing/HopFrame.Testing.Api/Models/Address.cs
@@ -2,7 +2,7 @@ using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Mvc.ModelBinding;
-namespace RestApiTest.Models;
+namespace HopFrame.Testing.Api.Models;
public class Address {
[ForeignKey("Employee")]
diff --git a/test/RestApiTest/Models/Employee.cs b/testing/HopFrame.Testing.Api/Models/Employee.cs
similarity index 79%
rename from test/RestApiTest/Models/Employee.cs
rename to testing/HopFrame.Testing.Api/Models/Employee.cs
index 6f70edc..f7e3e27 100644
--- a/test/RestApiTest/Models/Employee.cs
+++ b/testing/HopFrame.Testing.Api/Models/Employee.cs
@@ -1,4 +1,4 @@
-namespace RestApiTest.Models;
+namespace HopFrame.Testing.Api.Models;
public class Employee {
public int EmployeeId { get; set; }
diff --git a/test/RestApiTest/Program.cs b/testing/HopFrame.Testing.Api/Program.cs
similarity index 98%
rename from test/RestApiTest/Program.cs
rename to testing/HopFrame.Testing.Api/Program.cs
index bfcbc38..6651ecd 100644
--- a/test/RestApiTest/Program.cs
+++ b/testing/HopFrame.Testing.Api/Program.cs
@@ -1,4 +1,4 @@
-using RestApiTest;
+using HopFrame.Testing.Api;
using HopFrame.Api.Extensions;
using Microsoft.OpenApi.Models;
diff --git a/test/RestApiTest/Properties/launchSettings.json b/testing/HopFrame.Testing.Api/Properties/launchSettings.json
similarity index 100%
rename from test/RestApiTest/Properties/launchSettings.json
rename to testing/HopFrame.Testing.Api/Properties/launchSettings.json
diff --git a/test/FrontendTest/appsettings.json b/testing/HopFrame.Testing.Api/appsettings.json
similarity index 100%
rename from test/FrontendTest/appsettings.json
rename to testing/HopFrame.Testing.Api/appsettings.json
diff --git a/test/RestApiTest/.gitignore b/testing/HopFrame.Testing.Web/.gitignore
similarity index 100%
rename from test/RestApiTest/.gitignore
rename to testing/HopFrame.Testing.Web/.gitignore
diff --git a/test/FrontendTest/AdminContext.cs b/testing/HopFrame.Testing.Web/AdminContext.cs
similarity index 90%
rename from test/FrontendTest/AdminContext.cs
rename to testing/HopFrame.Testing.Web/AdminContext.cs
index 2eaff8d..9d33628 100644
--- a/test/FrontendTest/AdminContext.cs
+++ b/testing/HopFrame.Testing.Web/AdminContext.cs
@@ -1,10 +1,10 @@
-using FrontendTest.Providers;
using HopFrame.Web.Admin;
using HopFrame.Web.Admin.Generators;
using HopFrame.Web.Admin.Models;
-using RestApiTest.Models;
+using HopFrame.Testing.Api.Models;
+using HopFrame.Testing.Web.Providers;
-namespace FrontendTest;
+namespace HopFrame.Testing.Web;
public class AdminContext : AdminPagesContext {
diff --git a/test/FrontendTest/Components/App.razor b/testing/HopFrame.Testing.Web/Components/App.razor
similarity index 86%
rename from test/FrontendTest/Components/App.razor
rename to testing/HopFrame.Testing.Web/Components/App.razor
index 35c8065..b3faba0 100644
--- a/test/FrontendTest/Components/App.razor
+++ b/testing/HopFrame.Testing.Web/Components/App.razor
@@ -7,7 +7,7 @@
-
+
diff --git a/test/FrontendTest/Components/Layout/MainLayout.razor b/testing/HopFrame.Testing.Web/Components/Layout/MainLayout.razor
similarity index 100%
rename from test/FrontendTest/Components/Layout/MainLayout.razor
rename to testing/HopFrame.Testing.Web/Components/Layout/MainLayout.razor
diff --git a/test/FrontendTest/Components/Layout/MainLayout.razor.css b/testing/HopFrame.Testing.Web/Components/Layout/MainLayout.razor.css
similarity index 100%
rename from test/FrontendTest/Components/Layout/MainLayout.razor.css
rename to testing/HopFrame.Testing.Web/Components/Layout/MainLayout.razor.css
diff --git a/test/FrontendTest/Components/Layout/NavMenu.razor b/testing/HopFrame.Testing.Web/Components/Layout/NavMenu.razor
similarity index 100%
rename from test/FrontendTest/Components/Layout/NavMenu.razor
rename to testing/HopFrame.Testing.Web/Components/Layout/NavMenu.razor
diff --git a/test/FrontendTest/Components/Layout/NavMenu.razor.css b/testing/HopFrame.Testing.Web/Components/Layout/NavMenu.razor.css
similarity index 100%
rename from test/FrontendTest/Components/Layout/NavMenu.razor.css
rename to testing/HopFrame.Testing.Web/Components/Layout/NavMenu.razor.css
diff --git a/test/FrontendTest/Components/Pages/Counter.razor b/testing/HopFrame.Testing.Web/Components/Pages/Counter.razor
similarity index 100%
rename from test/FrontendTest/Components/Pages/Counter.razor
rename to testing/HopFrame.Testing.Web/Components/Pages/Counter.razor
diff --git a/test/FrontendTest/Components/Pages/Error.razor b/testing/HopFrame.Testing.Web/Components/Pages/Error.razor
similarity index 100%
rename from test/FrontendTest/Components/Pages/Error.razor
rename to testing/HopFrame.Testing.Web/Components/Pages/Error.razor
diff --git a/test/FrontendTest/Components/Pages/Home.razor b/testing/HopFrame.Testing.Web/Components/Pages/Home.razor
similarity index 100%
rename from test/FrontendTest/Components/Pages/Home.razor
rename to testing/HopFrame.Testing.Web/Components/Pages/Home.razor
diff --git a/test/FrontendTest/Components/Pages/Weather.razor b/testing/HopFrame.Testing.Web/Components/Pages/Weather.razor
similarity index 100%
rename from test/FrontendTest/Components/Pages/Weather.razor
rename to testing/HopFrame.Testing.Web/Components/Pages/Weather.razor
diff --git a/test/FrontendTest/Components/Routes.razor b/testing/HopFrame.Testing.Web/Components/Routes.razor
similarity index 100%
rename from test/FrontendTest/Components/Routes.razor
rename to testing/HopFrame.Testing.Web/Components/Routes.razor
diff --git a/test/FrontendTest/Components/_Imports.razor b/testing/HopFrame.Testing.Web/Components/_Imports.razor
similarity index 83%
rename from test/FrontendTest/Components/_Imports.razor
rename to testing/HopFrame.Testing.Web/Components/_Imports.razor
index b17e0c0..f7abb33 100644
--- a/test/FrontendTest/Components/_Imports.razor
+++ b/testing/HopFrame.Testing.Web/Components/_Imports.razor
@@ -6,5 +6,5 @@
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
-@using FrontendTest
-@using FrontendTest.Components
\ No newline at end of file
+@using HopFrame.Testing.Web
+@using HopFrame.Testing.Web.Components
\ No newline at end of file
diff --git a/test/FrontendTest/DatabaseContext.cs b/testing/HopFrame.Testing.Web/DatabaseContext.cs
similarity index 80%
rename from test/FrontendTest/DatabaseContext.cs
rename to testing/HopFrame.Testing.Web/DatabaseContext.cs
index 0dede8a..bd12346 100644
--- a/test/FrontendTest/DatabaseContext.cs
+++ b/testing/HopFrame.Testing.Web/DatabaseContext.cs
@@ -1,8 +1,8 @@
using HopFrame.Database;
using Microsoft.EntityFrameworkCore;
-using RestApiTest.Models;
+using HopFrame.Testing.Api.Models;
-namespace FrontendTest;
+namespace HopFrame.Testing.Web;
public class DatabaseContext : HopDbContextBase {
public DbSet Employees { get; set; }
@@ -11,7 +11,7 @@ public class DatabaseContext : HopDbContextBase {
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) {
base.OnConfiguring(optionsBuilder);
- optionsBuilder.UseSqlite(@"Data Source=C:\Users\leon\Documents\Projekte\HopFrame\test\RestApiTest\bin\Debug\net8.0\test.db;Mode=ReadWrite;");
+ optionsBuilder.UseSqlite(@"Data Source=C:\Users\leon\Documents\Projekte\HopFrame\testing\HopFrame.Testing.Api\bin\Debug\net8.0\test.db;Mode=ReadWrite;");
}
protected override void OnModelCreating(ModelBuilder modelBuilder) {
diff --git a/test/FrontendTest/FrontendTest.csproj b/testing/HopFrame.Testing.Web/HopFrame.Testing.Web.csproj
similarity index 100%
rename from test/FrontendTest/FrontendTest.csproj
rename to testing/HopFrame.Testing.Web/HopFrame.Testing.Web.csproj
diff --git a/test/RestApiTest/Models/Address.cs b/testing/HopFrame.Testing.Web/Models/Address.cs
similarity index 92%
rename from test/RestApiTest/Models/Address.cs
rename to testing/HopFrame.Testing.Web/Models/Address.cs
index 386114d..5688ad1 100644
--- a/test/RestApiTest/Models/Address.cs
+++ b/testing/HopFrame.Testing.Web/Models/Address.cs
@@ -2,7 +2,7 @@ using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Mvc.ModelBinding;
-namespace RestApiTest.Models;
+namespace HopFrame.Testing.Api.Models;
public class Address {
[ForeignKey("Employee")]
diff --git a/test/FrontendTest/Models/Employee.cs b/testing/HopFrame.Testing.Web/Models/Employee.cs
similarity index 79%
rename from test/FrontendTest/Models/Employee.cs
rename to testing/HopFrame.Testing.Web/Models/Employee.cs
index 6f70edc..f7e3e27 100644
--- a/test/FrontendTest/Models/Employee.cs
+++ b/testing/HopFrame.Testing.Web/Models/Employee.cs
@@ -1,4 +1,4 @@
-namespace RestApiTest.Models;
+namespace HopFrame.Testing.Api.Models;
public class Employee {
public int EmployeeId { get; set; }
diff --git a/test/FrontendTest/Program.cs b/testing/HopFrame.Testing.Web/Program.cs
similarity index 93%
rename from test/FrontendTest/Program.cs
rename to testing/HopFrame.Testing.Web/Program.cs
index 7547722..7957fff 100644
--- a/test/FrontendTest/Program.cs
+++ b/testing/HopFrame.Testing.Web/Program.cs
@@ -1,5 +1,5 @@
-using FrontendTest;
-using FrontendTest.Components;
+using HopFrame.Testing.Web;
+using HopFrame.Testing.Web.Components;
using HopFrame.Web;
using HopFrame.Web.Admin;
diff --git a/test/FrontendTest/Properties/launchSettings.json b/testing/HopFrame.Testing.Web/Properties/launchSettings.json
similarity index 100%
rename from test/FrontendTest/Properties/launchSettings.json
rename to testing/HopFrame.Testing.Web/Properties/launchSettings.json
diff --git a/test/FrontendTest/Providers/AddressProvider.cs b/testing/HopFrame.Testing.Web/Providers/AddressProvider.cs
similarity index 91%
rename from test/FrontendTest/Providers/AddressProvider.cs
rename to testing/HopFrame.Testing.Web/Providers/AddressProvider.cs
index de5f13f..6ed1d6f 100644
--- a/test/FrontendTest/Providers/AddressProvider.cs
+++ b/testing/HopFrame.Testing.Web/Providers/AddressProvider.cs
@@ -1,8 +1,8 @@
using HopFrame.Web.Admin;
using Microsoft.EntityFrameworkCore;
-using RestApiTest.Models;
+using HopFrame.Testing.Api.Models;
-namespace FrontendTest.Providers;
+namespace HopFrame.Testing.Web.Providers;
public class AddressProvider(DatabaseContext context) : ModelProvider {
diff --git a/test/FrontendTest/Providers/EmployeeProvider.cs b/testing/HopFrame.Testing.Web/Providers/EmployeeProvider.cs
similarity index 91%
rename from test/FrontendTest/Providers/EmployeeProvider.cs
rename to testing/HopFrame.Testing.Web/Providers/EmployeeProvider.cs
index 89f7b84..9078eea 100644
--- a/test/FrontendTest/Providers/EmployeeProvider.cs
+++ b/testing/HopFrame.Testing.Web/Providers/EmployeeProvider.cs
@@ -1,8 +1,8 @@
using HopFrame.Web.Admin;
using Microsoft.EntityFrameworkCore;
-using RestApiTest.Models;
+using HopFrame.Testing.Api.Models;
-namespace FrontendTest.Providers;
+namespace HopFrame.Testing.Web.Providers;
public class EmployeeProvider(DatabaseContext context) : ModelProvider {
diff --git a/test/RestApiTest/appsettings.json b/testing/HopFrame.Testing.Web/appsettings.json
similarity index 100%
rename from test/RestApiTest/appsettings.json
rename to testing/HopFrame.Testing.Web/appsettings.json
diff --git a/test/FrontendTest/wwwroot/app.css b/testing/HopFrame.Testing.Web/wwwroot/app.css
similarity index 100%
rename from test/FrontendTest/wwwroot/app.css
rename to testing/HopFrame.Testing.Web/wwwroot/app.css
diff --git a/test/FrontendTest/wwwroot/favicon.png b/testing/HopFrame.Testing.Web/wwwroot/favicon.png
similarity index 100%
rename from test/FrontendTest/wwwroot/favicon.png
rename to testing/HopFrame.Testing.Web/wwwroot/favicon.png
diff --git a/tests/HopFrame.Tests.Api/AuthLogicTests.cs b/tests/HopFrame.Tests.Api/AuthLogicTests.cs
new file mode 100644
index 0000000..ca86b5b
--- /dev/null
+++ b/tests/HopFrame.Tests.Api/AuthLogicTests.cs
@@ -0,0 +1,403 @@
+using System.Net;
+using System.Security.Claims;
+using HopFrame.Api.Logic;
+using HopFrame.Api.Logic.Implementation;
+using HopFrame.Api.Models;
+using HopFrame.Tests.Api.Extensions;
+using HopFrame.Database.Models;
+using HopFrame.Database.Repositories;
+using HopFrame.Security.Authentication;
+using HopFrame.Security.Claims;
+using HopFrame.Security.Models;
+using Microsoft.AspNetCore.Http;
+using Moq;
+
+namespace HopFrame.Tests.Api;
+
+public class AuthLogicTests {
+
+ private readonly Guid _refreshToken = Guid.NewGuid();
+ private readonly Guid _accessToken = Guid.NewGuid();
+
+ private (IAuthLogic, HttpContext) SetupEnvironment(bool passwordIsCorrect = true, Token providedRefreshToken = null, string providedTokenCookie = null, bool provideAccessToken = true) {
+ var accessor = new HttpContextAccessor {
+ HttpContext = new DefaultHttpContext()
+ };
+
+ if (providedTokenCookie != null) {
+ var cookies = new Mock();
+ cookies
+ .SetupGet(c => c[ITokenContext.RefreshTokenType])
+ .Returns(providedTokenCookie);
+ accessor.HttpContext.Request.Cookies = cookies.Object;
+ }
+
+ if (provideAccessToken) {
+ var claims = new List {
+ new(HopFrameClaimTypes.AccessTokenId, _accessToken.ToString())
+ };
+ accessor.HttpContext.User.AddIdentity(new ClaimsIdentity(claims, HopFrameAuthentication.SchemeName));
+ }
+
+ var users = new Mock();
+ users
+ .Setup(u => u.GetUserByEmail(It.Is(email => CreateDummyUser().Email == email)))
+ .ReturnsAsync(CreateDummyUser());
+ users
+ .Setup(u => u.CheckUserPassword(It.Is(u => u.Email == CreateDummyUser().Email), It.IsAny()))
+ .ReturnsAsync(passwordIsCorrect);
+ users
+ .Setup(u => u.AddUser(It.IsAny()))
+ .ReturnsAsync(CreateDummyUser());
+ users
+ .Setup(u => u.GetUsers())
+ .ReturnsAsync(new List { CreateDummyUser() });
+
+ var tokens = new Mock();
+ tokens
+ .Setup(t => t.CreateToken(It.Is(t => t == Token.RefreshTokenType), It.IsAny()))
+ .ReturnsAsync(new Token {
+ Content = _refreshToken,
+ Type = Token.RefreshTokenType
+ });
+ tokens
+ .Setup(t => t.CreateToken(It.Is(t => t == Token.AccessTokenType), It.IsAny()))
+ .ReturnsAsync(new Token {
+ Content = _accessToken,
+ Type = Token.AccessTokenType
+ });
+ tokens
+ .Setup(t => t.GetToken(It.Is(token => token == _refreshToken.ToString())))
+ .ReturnsAsync(providedRefreshToken);
+
+ var context = new Mock();
+ context
+ .Setup(c => c.User)
+ .Returns(CreateDummyUser());
+
+ return (new AuthLogic(users.Object, tokens.Object, context.Object, accessor), accessor.HttpContext);
+ }
+
+ private User CreateDummyUser() => new() {
+ Id = Guid.NewGuid(),
+ CreatedAt = DateTime.Now,
+ Email = "test@example.com",
+ Username = "ExampleUser",
+ Password = "1234567890"
+ };
+
+ [Fact]
+ public async Task Login_Should_Succeed() {
+ // Arrange
+ var (auth, context) = SetupEnvironment();
+ var login = new UserLogin {
+ Email = CreateDummyUser().Email,
+ Password = CreateDummyUser().Password
+ };
+
+ // Act
+ var result = await auth.Login(login);
+
+ // Assert
+ Assert.True(result.IsSuccessful);
+ Assert.Equal(_accessToken.ToString(), result.Data.Value);
+ Assert.Equal(_accessToken.ToString(), context.Response.Headers.FindCookie(ITokenContext.AccessTokenType));
+ Assert.Equal(_refreshToken.ToString(), context.Response.Headers.FindCookie(ITokenContext.RefreshTokenType));
+ }
+
+ [Fact]
+ public async Task Login_With_WrongEmail_Should_Fail() {
+ // Arrange
+ var (auth, context) = SetupEnvironment();
+ var login = new UserLogin {
+ Email = "wrong@example.com",
+ Password = CreateDummyUser().Password
+ };
+
+ // Act
+ var result = await auth.Login(login);
+
+ // Assert
+ Assert.False(result.IsSuccessful);
+ Assert.Equal(HttpStatusCode.NotFound, result.State);
+ Assert.Null(context.Response.Headers.FindCookie(ITokenContext.RefreshTokenType));
+ Assert.Null(context.Response.Headers.FindCookie(ITokenContext.AccessTokenType));
+ }
+
+ [Fact]
+ public async Task Login_With_WrongPassword_Should_Fail() {
+ // Arrange
+ var (auth, context) = SetupEnvironment(false);
+ var login = new UserLogin {
+ Email = CreateDummyUser().Email,
+ Password = CreateDummyUser().Password
+ };
+
+ // Act
+ var result = await auth.Login(login);
+
+ // Assert
+ Assert.False(result.IsSuccessful);
+ Assert.Equal(HttpStatusCode.Forbidden, result.State);
+ Assert.Null(context.Response.Headers.FindCookie(ITokenContext.RefreshTokenType));
+ Assert.Null(context.Response.Headers.FindCookie(ITokenContext.AccessTokenType));
+ }
+
+ [Fact]
+ public async Task Register_Should_Succeed() {
+ // Arrange
+ var (auth, context) = SetupEnvironment();
+ var register = new UserRegister {
+ Email = "new@example.com",
+ Password = "1234567890",
+ Username = "NewUser"
+ };
+
+ // Act
+ var result = await auth.Register(register);
+
+ // Assert
+ Assert.True(result.IsSuccessful);
+ Assert.Equal(_accessToken.ToString(), result.Data.Value);
+ Assert.Equal(_accessToken.ToString(), context.Response.Headers.FindCookie(ITokenContext.AccessTokenType));
+ Assert.Equal(_refreshToken.ToString(), context.Response.Headers.FindCookie(ITokenContext.RefreshTokenType));
+ }
+
+ [Fact]
+ public async Task Register_With_ShortPassword_Should_Fail() {
+ // Arrange
+ var (auth, context) = SetupEnvironment();
+ var register = new UserRegister {
+ Email = "new@example.com",
+ Password = "12345",
+ Username = "NewUser"
+ };
+
+ // Act
+ var result = await auth.Register(register);
+
+ // Assert
+ Assert.False(result.IsSuccessful);
+ Assert.Equal(HttpStatusCode.BadRequest, result.State);
+ Assert.Null(context.Response.Headers.FindCookie(ITokenContext.RefreshTokenType));
+ Assert.Null(context.Response.Headers.FindCookie(ITokenContext.AccessTokenType));
+ }
+
+ [Fact]
+ public async Task Register_With_ExistingUsername_Should_Fail() {
+ // Arrange
+ var (auth, context) = SetupEnvironment();
+ var register = new UserRegister {
+ Email = "new@example.com",
+ Password = "1234567890",
+ Username = CreateDummyUser().Username
+ };
+
+ // Act
+ var result = await auth.Register(register);
+
+ // Assert
+ Assert.False(result.IsSuccessful);
+ Assert.Equal(HttpStatusCode.Conflict, result.State);
+ Assert.Null(context.Response.Headers.FindCookie(ITokenContext.RefreshTokenType));
+ Assert.Null(context.Response.Headers.FindCookie(ITokenContext.AccessTokenType));
+ }
+
+ [Fact]
+ public async Task Register_With_ExistingEmail_Should_Fail() {
+ // Arrange
+ var (auth, context) = SetupEnvironment();
+ var register = new UserRegister {
+ Email = CreateDummyUser().Email,
+ Password = "1234567890",
+ Username = "NewUser"
+ };
+
+ // Act
+ var result = await auth.Register(register);
+
+ // Assert
+ Assert.False(result.IsSuccessful);
+ Assert.Equal(HttpStatusCode.Conflict, result.State);
+ Assert.Null(context.Response.Headers.FindCookie(ITokenContext.RefreshTokenType));
+ Assert.Null(context.Response.Headers.FindCookie(ITokenContext.AccessTokenType));
+ }
+
+ [Fact]
+ public async Task Authenticate_Should_Succeed() {
+ // Arrange
+ var token = new Token {
+ Type = Token.RefreshTokenType,
+ Content = _refreshToken,
+ CreatedAt = DateTime.Now,
+ Owner = CreateDummyUser()
+ };
+ var (auth, context) = SetupEnvironment(true, token, token.Content.ToString());
+
+ // Act
+ var result = await auth.Authenticate();
+
+ // Assert
+ Assert.True(result.IsSuccessful);
+ Assert.Equal(_accessToken.ToString(), result.Data.Value);
+ Assert.Equal(_accessToken.ToString(), context.Response.Headers.FindCookie(ITokenContext.AccessTokenType));
+ }
+
+ [Fact]
+ public async Task Authenticate_With_NoProvidedToken_Should_Fail() {
+ // Arrange
+ var (auth, context) = SetupEnvironment();
+
+ // Act
+ var result = await auth.Authenticate();
+
+ // Assert
+ Assert.False(result.IsSuccessful);
+ Assert.Equal(HttpStatusCode.BadRequest, result.State);
+ Assert.Null(context.Response.Headers.FindCookie(ITokenContext.AccessTokenType));
+ }
+
+ [Fact]
+ public async Task Authenticate_With_WrongToken_Should_Fail() {
+ // Arrange
+ var (auth, context) = SetupEnvironment(true, null, _refreshToken.ToString());
+
+ // Act
+ var result = await auth.Authenticate();
+
+ // Assert
+ Assert.False(result.IsSuccessful);
+ Assert.Equal(HttpStatusCode.NotFound, result.State);
+ Assert.Null(context.Response.Headers.FindCookie(ITokenContext.AccessTokenType));
+ }
+
+ [Fact]
+ public async Task Authenticate_With_WrongTokenType_Should_Fail() {
+ // Arrange
+ var token = new Token {
+ Type = Token.AccessTokenType,
+ Content = _refreshToken,
+ CreatedAt = DateTime.Now,
+ Owner = CreateDummyUser()
+ };
+ var (auth, context) = SetupEnvironment(true, token, token.Content.ToString());
+
+ // Act
+ var result = await auth.Authenticate();
+
+ // Assert
+ Assert.False(result.IsSuccessful);
+ Assert.Equal(HttpStatusCode.Conflict, result.State);
+ Assert.Null(context.Response.Headers.FindCookie(ITokenContext.AccessTokenType));
+ }
+
+ [Fact]
+ public async Task Authenticate_With_ExpiredToken_Should_Fail() {
+ // Arrange
+ var token = new Token {
+ Type = Token.RefreshTokenType,
+ Content = _refreshToken,
+ CreatedAt = DateTime.MinValue,
+ Owner = CreateDummyUser()
+ };
+ var (auth, context) = SetupEnvironment(true, token, token.Content.ToString());
+
+ // Act
+ var result = await auth.Authenticate();
+
+ // Assert
+ Assert.False(result.IsSuccessful);
+ Assert.Equal(HttpStatusCode.Forbidden, result.State);
+ Assert.Null(context.Response.Headers.FindCookie(ITokenContext.AccessTokenType));
+ }
+
+ [Fact]
+ public async Task Logout_Should_Succeed() {
+ // Arrange
+ var (auth, context) = SetupEnvironment(providedTokenCookie: _refreshToken.ToString());
+ context.Response.Cookies.Append(ITokenContext.AccessTokenType, _accessToken.ToString());
+ context.Response.Cookies.Append(ITokenContext.RefreshTokenType, _refreshToken.ToString());
+
+ // Act
+ var result = await auth.Logout();
+
+ // Assert
+ Assert.True(result.IsSuccessful);
+ Assert.Null(context.Response.Headers.FindCookie(ITokenContext.AccessTokenType));
+ Assert.Null(context.Response.Headers.FindCookie(ITokenContext.RefreshTokenType));
+ }
+
+ [Fact]
+ public async Task Logout_With_NoAccessToken_Should_Fail() {
+ // Arrange
+ var (auth, context) = SetupEnvironment(provideAccessToken: false);
+ context.Response.Cookies.Append(ITokenContext.AccessTokenType, _accessToken.ToString());
+ context.Response.Cookies.Append(ITokenContext.RefreshTokenType, _refreshToken.ToString());
+
+ // Act
+ var result = await auth.Logout();
+
+ // Assert
+ Assert.False(result.IsSuccessful);
+ Assert.Equal(HttpStatusCode.Conflict, result.State);
+ Assert.Equal(_accessToken.ToString(), context.Response.Headers.FindCookie(ITokenContext.AccessTokenType));
+ Assert.Equal(_refreshToken.ToString(), context.Response.Headers.FindCookie(ITokenContext.RefreshTokenType));
+ }
+
+ [Fact]
+ public async Task Logout_With_NoRefreshToken_Should_Fail() {
+ // Arrange
+ var (auth, context) = SetupEnvironment();
+ context.Response.Cookies.Append(ITokenContext.AccessTokenType, _accessToken.ToString());
+ context.Response.Cookies.Append(ITokenContext.RefreshTokenType, _refreshToken.ToString());
+
+ // Act
+ var result = await auth.Logout();
+
+ // Assert
+ Assert.False(result.IsSuccessful);
+ Assert.Equal(HttpStatusCode.Conflict, result.State);
+ Assert.Equal(_accessToken.ToString(), context.Response.Headers.FindCookie(ITokenContext.AccessTokenType));
+ Assert.Equal(_refreshToken.ToString(), context.Response.Headers.FindCookie(ITokenContext.RefreshTokenType));
+ }
+
+ [Fact]
+ public async Task Delete_Should_Succeed() {
+ // Arrange
+ var (auth, context) = SetupEnvironment();
+ context.Response.Cookies.Append(ITokenContext.AccessTokenType, _accessToken.ToString());
+ context.Response.Cookies.Append(ITokenContext.RefreshTokenType, _refreshToken.ToString());
+ var validation = new UserPasswordValidation {
+ Password = CreateDummyUser().Password
+ };
+
+ // Act
+ var result = await auth.Delete(validation);
+
+ // Assert
+ Assert.True(result.IsSuccessful);
+ Assert.Null(context.Response.Headers.FindCookie(ITokenContext.AccessTokenType));
+ Assert.Null(context.Response.Headers.FindCookie(ITokenContext.RefreshTokenType));
+ }
+
+ [Fact]
+ public async Task Delete_With_WrongPassword_Should_Fail() {
+ // Arrange
+ var (auth, context) = SetupEnvironment(false);
+ context.Response.Cookies.Append(ITokenContext.AccessTokenType, _accessToken.ToString());
+ context.Response.Cookies.Append(ITokenContext.RefreshTokenType, _refreshToken.ToString());
+ var validation = new UserPasswordValidation {
+ Password = CreateDummyUser().Password
+ };
+
+ // Act
+ var result = await auth.Delete(validation);
+
+ // Assert
+ Assert.False(result.IsSuccessful);
+ Assert.Equal(HttpStatusCode.Forbidden, result.State);
+ Assert.Equal(_accessToken.ToString(), context.Response.Headers.FindCookie(ITokenContext.AccessTokenType));
+ Assert.Equal(_refreshToken.ToString(), context.Response.Headers.FindCookie(ITokenContext.RefreshTokenType));
+ }
+
+}
\ No newline at end of file
diff --git a/tests/HopFrame.Tests.Api/Extensions/HttpContextExtensions.cs b/tests/HopFrame.Tests.Api/Extensions/HttpContextExtensions.cs
new file mode 100644
index 0000000..8f4de69
--- /dev/null
+++ b/tests/HopFrame.Tests.Api/Extensions/HttpContextExtensions.cs
@@ -0,0 +1,29 @@
+using System.Web;
+using Microsoft.AspNetCore.Http;
+
+namespace HopFrame.Tests.Api.Extensions;
+
+internal static class HttpContextExtensions {
+ /// Extracts the partial cookie value from the header section.
+ ///
+ /// The key for identifying the cookie.
+ /// The value of the cookie.
+ public static string FindCookie(this IHeaderDictionary headers, string key)
+ {
+ string headerKey = $"{key}=";
+ var cookies = headers.Values
+ .SelectMany(h => h)
+ .Where(header => header.StartsWith(headerKey))
+ .Select(header => header.Substring(headerKey.Length).Split(';').First())
+ .ToArray();
+
+ //Note: cookie values in a header are encoded like a uri parameter value.
+ var value = cookies.LastOrDefault();//and the last set value, is the relevant one.
+ if (string.IsNullOrEmpty(value))
+ return null;
+
+ //That's why we should decode that last value, before we return it.
+ var decoded = HttpUtility.UrlDecode(value);
+ return decoded;
+ }
+}
\ No newline at end of file
diff --git a/tests/HopFrame.Tests.Api/HopFrame.Tests.Api.csproj b/tests/HopFrame.Tests.Api/HopFrame.Tests.Api.csproj
new file mode 100644
index 0000000..6c7f59f
--- /dev/null
+++ b/tests/HopFrame.Tests.Api/HopFrame.Tests.Api.csproj
@@ -0,0 +1,28 @@
+
+
+
+ net8.0
+ enable
+ disable
+
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/HopFrame.Tests.Database/Data/DatabaseContext.cs b/tests/HopFrame.Tests.Database/Data/DatabaseContext.cs
new file mode 100644
index 0000000..271628f
--- /dev/null
+++ b/tests/HopFrame.Tests.Database/Data/DatabaseContext.cs
@@ -0,0 +1,12 @@
+using HopFrame.Database;
+using Microsoft.EntityFrameworkCore;
+
+namespace HopFrame.Tests.Database.Data;
+
+public class DatabaseContext : HopDbContextBase {
+ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) {
+ base.OnConfiguring(optionsBuilder);
+
+ optionsBuilder.UseInMemoryDatabase(Guid.NewGuid().ToString());
+ }
+}
\ No newline at end of file
diff --git a/tests/HopFrame.Tests.Database/HopFrame.Tests.Database.csproj b/tests/HopFrame.Tests.Database/HopFrame.Tests.Database.csproj
new file mode 100644
index 0000000..ce93d67
--- /dev/null
+++ b/tests/HopFrame.Tests.Database/HopFrame.Tests.Database.csproj
@@ -0,0 +1,28 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/HopFrame.Tests.Database/PermissionValidatorTests.cs b/tests/HopFrame.Tests.Database/PermissionValidatorTests.cs
new file mode 100644
index 0000000..693f093
--- /dev/null
+++ b/tests/HopFrame.Tests.Database/PermissionValidatorTests.cs
@@ -0,0 +1,59 @@
+using HopFrame.Database;
+
+namespace HopFrame.Tests.Database;
+
+public class PermissionValidatorTests {
+
+ [Fact]
+ public void IncludesPermission_Returns_True_For_ExactPermission() {
+ // Arrange
+ var permissions = new [] { "test.permission", "test.permission.exact" };
+ var permission = "test.permission.exact";
+
+ // Act
+ var hasPermission = PermissionValidator.IncludesPermission(permission, permissions);
+
+ // Assert
+ Assert.True(hasPermission);
+ }
+
+ [Fact]
+ public void IncludesPermission_Returns_True_For_GroupPermission() {
+ // Arrange
+ var permissions = new [] { "test.permission.*", "test.permission.exact" };
+ var permission = "test.permission.exact.other";
+
+ // Act
+ var hasPermission = PermissionValidator.IncludesPermission(permission, permissions);
+
+ // Assert
+ Assert.True(hasPermission);
+ }
+
+ [Fact]
+ public void IncludesPermission_Returns_True_For_StarPermission() {
+ // Arrange
+ var permissions = new [] { "test.permission", "test.permission.exact", "*" };
+ var permission = "test.permission.exact.other";
+
+ // Act
+ var hasPermission = PermissionValidator.IncludesPermission(permission, permissions);
+
+ // Assert
+ Assert.True(hasPermission);
+ }
+
+ [Fact]
+ public void IncludesPermission_Returns_False() {
+ // Arrange
+ var permissions = new [] { "test.permission", "test.permission.exact" };
+ var permission = "test.permission.exact.other";
+
+ // Act
+ var hasPermission = PermissionValidator.IncludesPermission(permission, permissions);
+
+ // Assert
+ Assert.False(hasPermission);
+ }
+
+}
\ No newline at end of file
diff --git a/tests/HopFrame.Tests.Database/Repositories/GroupRepositoryTests.cs b/tests/HopFrame.Tests.Database/Repositories/GroupRepositoryTests.cs
new file mode 100644
index 0000000..e3fd4ff
--- /dev/null
+++ b/tests/HopFrame.Tests.Database/Repositories/GroupRepositoryTests.cs
@@ -0,0 +1,152 @@
+using HopFrame.Database.Models;
+using HopFrame.Database.Repositories;
+using HopFrame.Database.Repositories.Implementation;
+using HopFrame.Tests.Database.Data;
+
+namespace HopFrame.Tests.Database.Repositories;
+
+public class GroupRepositoryTests {
+
+ private async Task<(DatabaseContext, IGroupRepository)> SetupEnvironment(int count = 5) {
+ var context = new DatabaseContext();
+ var repo = new GroupRepository(context);
+
+ for (int i = 0; i < count; i++) {
+ await context.Groups.AddAsync(new() {
+ Name = Guid.NewGuid().ToString()
+ });
+ }
+ await context.SaveChangesAsync();
+
+ return (context, repo);
+ }
+
+ [Fact]
+ public async Task GetPermissionGroups_Returns_AllPermissionGroups() {
+ // Arrange
+ var count = 5;
+ var (context, repo) = await SetupEnvironment(count);
+
+ // Act
+ var groups = await repo.GetPermissionGroups();
+
+ // Assert
+ Assert.Equal(count, groups.Count);
+ }
+
+ [Fact]
+ public async Task GetDefaultGroups_Returns_OnlyDefaultGroups() {
+ // Arrange
+ var (context, repo) = await SetupEnvironment();
+ await context.Groups.AddAsync(new () {
+ Name = Guid.NewGuid().ToString(),
+ IsDefaultGroup = true
+ });
+ await context.SaveChangesAsync();
+
+ // Act
+ var groups = await repo.GetDefaultGroups();
+
+ // Assert
+ Assert.Single(groups);
+ Assert.True(groups[0].IsDefaultGroup);
+ }
+
+ [Fact]
+ public async Task GetUserGroups_Returns_OnlyUserGroups() {
+ // Arrange
+ var (context, repo) = await SetupEnvironment();
+ await context.Groups.AddAsync(new () {
+ Name = "group.user_should_have_these",
+ });
+ var user = new User {
+ Id = Guid.NewGuid(),
+ Email = "",
+ Username = "",
+ Password = ""
+ };
+ user.Permissions = new() {
+ new() {
+ User = user,
+ PermissionName = "group.user_should_have_these"
+ }
+ };
+ await context.Users.AddAsync(user);
+ await context.SaveChangesAsync();
+
+ // Act
+ var groups = await repo.GetUserGroups(user);
+
+ // Assert
+ Assert.Single(groups);
+ Assert.Equal("group.user_should_have_these", groups[0].Name);
+ }
+
+ [Fact]
+ public async Task GetPermissionGroup_Returns_OnlyOnePermissionGroup() {
+ // Arrange
+ var (context, repo) = await SetupEnvironment();
+ await context.Groups.AddAsync(new () {
+ Name = "group.return"
+ });
+ await context.SaveChangesAsync();
+
+ // Act
+ var group = await repo.GetPermissionGroup("group.return");
+
+ // Assert
+ Assert.NotNull(group);
+ Assert.Equal("group.return", group.Name);
+ }
+
+ [Fact]
+ public async Task EditPermissionGroup_Should_EditOnePermissionGroup() {
+ // Arrange
+ var (context, repo) = await SetupEnvironment();
+ var groupToEdit = new PermissionGroup {
+ Name = "group.edit"
+ };
+ await context.Groups.AddAsync(groupToEdit);
+ await context.SaveChangesAsync();
+
+ // Act
+ groupToEdit.Description = "This description was edited";
+ await repo.EditPermissionGroup(groupToEdit);
+
+ // Assert
+ var group = context.Groups.SingleOrDefault(g => g.Name == "group.edit");
+ Assert.NotNull(group);
+ Assert.Equal("This description was edited", group.Description);
+ }
+
+ [Fact]
+ public async Task CreatePermissionGroup_Should_AddOnePermissionGroup() {
+ // Arrange
+ var (context, repo) = await SetupEnvironment(0);
+ var group = new PermissionGroup {
+ Name = "group",
+ Description = "Group to add"
+ };
+
+ // Act
+ var result = await repo.CreatePermissionGroup(group);
+
+ // Assert
+ Assert.Equal(group, result);
+ Assert.Single(context.Groups);
+ }
+
+ [Fact]
+ public async Task DeletePermissionGroup_Should_DeleteOnePermissionGroup() {
+ // Arrange
+ var count = 5;
+ var (context, repo) = await SetupEnvironment(count);
+
+ // Act
+ await repo.DeletePermissionGroup(context.Groups.First());
+
+ // Assert
+ Assert.Equal(count - 1, context.Groups.Count());
+ }
+
+}
\ No newline at end of file
diff --git a/tests/HopFrame.Tests.Database/Repositories/PermissionRepositoryTests.cs b/tests/HopFrame.Tests.Database/Repositories/PermissionRepositoryTests.cs
new file mode 100644
index 0000000..79fff4c
--- /dev/null
+++ b/tests/HopFrame.Tests.Database/Repositories/PermissionRepositoryTests.cs
@@ -0,0 +1,118 @@
+using HopFrame.Database.Models;
+using HopFrame.Database.Repositories;
+using HopFrame.Database.Repositories.Implementation;
+using HopFrame.Tests.Database.Data;
+using Microsoft.EntityFrameworkCore;
+
+namespace HopFrame.Tests.Database.Repositories;
+
+public class PermissionRepositoryTests {
+
+ private async Task<(DatabaseContext, IPermissionRepository)> SetupEnvironment(int count = 5) {
+ var context = new DatabaseContext();
+ var repo = new PermissionRepository(context, new GroupRepository(context));
+
+ for (int i = 0; i < count; i++) {
+ await context.Permissions.AddAsync(new () {
+ PermissionName = Guid.NewGuid().ToString(),
+ User = CreateTestUser()
+ });
+ }
+ await context.SaveChangesAsync();
+
+ return (context, repo);
+ }
+
+ private User CreateTestUser() => new () {
+ Username = "",
+ Email = "",
+ Password = ""
+ };
+
+ [Fact]
+ public async Task HasPermission_Returns_True() {
+ // Arrange
+ var (context, repo) = await SetupEnvironment();
+ var user = CreateTestUser();
+ await context.Permissions.AddAsync(new() {
+ PermissionName = "*",
+ User = user
+ });
+ await context.SaveChangesAsync();
+
+ // Act
+ var hasPermission = await repo.HasPermission(user, "*");
+
+ // Assert
+ Assert.True(hasPermission);
+ }
+
+ [Fact]
+ public async Task AddPermission_Should_AddOnePermission() {
+ // Arrange
+ var (context, repo) = await SetupEnvironment();
+ var user = CreateTestUser();
+
+ // Act
+ var result = await repo.AddPermission(user, "test.permission");
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Single(context.Permissions
+ .Include(p => p.User)
+ .Where(p => p.User.Id == user.Id && p.PermissionName == "test.permission"));
+ }
+
+ [Fact]
+ public async Task RemovePermission_Should_RemoveOnePermission() {
+ // Arrange
+ var (context, repo) = await SetupEnvironment();
+ var user = CreateTestUser();
+ await context.Permissions.AddAsync(new() {
+ PermissionName = "test.permission",
+ User = user
+ });
+ await context.SaveChangesAsync();
+
+ // Act
+ await repo.RemovePermission(user, "test.permission");
+
+ // Assert
+ Assert.Empty(context.Permissions
+ .Include(p => p.User)
+ .Where(p => p.User.Id == user.Id && p.PermissionName == "test.permission"));
+ }
+
+ [Fact]
+ public async Task GetFullPermissions_Return_AllPermissions_Including_Inherited() {
+ // Arrange
+ var (context, repo) = await SetupEnvironment();
+ var user = CreateTestUser();
+ var group = new PermissionGroup {
+ Name = "group.admin"
+ };
+ await context.Permissions.AddRangeAsync(new List {
+ new() {
+ PermissionName = "test.permission.inherited",
+ Group = group
+ },
+ new() {
+ PermissionName = "test.permission",
+ User = user
+ },
+ new() {
+ PermissionName = "group.admin",
+ User = user
+ }
+ });
+ await context.SaveChangesAsync();
+
+ // Act
+ var perms = await repo.GetFullPermissions(user);
+
+ // Assert
+ Assert.NotNull(perms);
+ Assert.Equal(3, perms.Count);
+ }
+
+}
\ No newline at end of file
diff --git a/tests/HopFrame.Tests.Database/Repositories/TokenRepositoryTests.cs b/tests/HopFrame.Tests.Database/Repositories/TokenRepositoryTests.cs
new file mode 100644
index 0000000..83dc770
--- /dev/null
+++ b/tests/HopFrame.Tests.Database/Repositories/TokenRepositoryTests.cs
@@ -0,0 +1,89 @@
+using HopFrame.Database.Models;
+using HopFrame.Database.Repositories;
+using HopFrame.Database.Repositories.Implementation;
+using HopFrame.Tests.Database.Data;
+using Microsoft.EntityFrameworkCore;
+
+namespace HopFrame.Tests.Database.Repositories;
+
+public class TokenRepositoryTests {
+
+ private async Task<(DatabaseContext, ITokenRepository)> SetupEnvironment(int count = 5) {
+ var context = new DatabaseContext();
+ var repo = new TokenRepository(context);
+
+ for (int i = 0; i < count; i++) {
+ await context.Tokens.AddAsync(new() {
+ Content = Guid.NewGuid(),
+ Owner = CreateTestUser(),
+ Type = Token.AccessTokenType
+ });
+ }
+ await context.SaveChangesAsync();
+
+ return (context, repo);
+ }
+
+ private User CreateTestUser() => new () {
+ Username = "",
+ Email = "",
+ Password = ""
+ };
+
+ [Fact]
+ public async Task GetToken_Return_CorrectToken() {
+ // Arrange
+ var (context, repo) = await SetupEnvironment();
+ var token = context.Tokens.First();
+
+ // Act
+ var result = await repo.GetToken(token.Content.ToString());
+
+ // Assert
+ Assert.Equal(token, result);
+ }
+
+ [Fact]
+ public async Task CreateToken_Should_CreateOneToken() {
+ // Arrange
+ var (context, repo) = await SetupEnvironment(0);
+
+ // Act
+ var result = await repo.CreateToken(Token.AccessTokenType, CreateTestUser());
+
+ // Assert
+ Assert.NotEmpty(context.Tokens);
+ Assert.Equal(result, context.Tokens.First());
+ }
+
+ [Fact]
+ public async Task DeleteUserTokens_Should_DeleteOnlyUserTokens() {
+ // Arrange
+ var dummyCount = 5;
+ var (context, repo) = await SetupEnvironment(dummyCount);
+ var user = CreateTestUser();
+ await context.Tokens.AddRangeAsync(new List {
+ new() {
+ Content = Guid.NewGuid(),
+ Owner = user,
+ Type = Token.AccessTokenType
+ },
+ new() {
+ Content = Guid.NewGuid(),
+ Owner = user,
+ Type = Token.RefreshTokenType
+ }
+ });
+ await context.SaveChangesAsync();
+
+ // Act
+ await repo.DeleteUserTokens(user);
+
+ // Assert
+ Assert.Equal(dummyCount, context.Tokens.Count());
+ Assert.Empty(context.Tokens
+ .Include(t => t.Owner)
+ .Where(t => t.Owner == user));
+ }
+
+}
\ No newline at end of file
diff --git a/tests/HopFrame.Tests.Database/Repositories/UserRepositoryTests.cs b/tests/HopFrame.Tests.Database/Repositories/UserRepositoryTests.cs
new file mode 100644
index 0000000..5730064
--- /dev/null
+++ b/tests/HopFrame.Tests.Database/Repositories/UserRepositoryTests.cs
@@ -0,0 +1,184 @@
+using HopFrame.Database.Models;
+using HopFrame.Database.Repositories;
+using HopFrame.Database.Repositories.Implementation;
+using HopFrame.Tests.Database.Data;
+
+namespace HopFrame.Tests.Database.Repositories;
+
+public class UserRepositoryTests {
+
+ private async Task<(DatabaseContext, IUserRepository)> SetupEnvironment(int count = 5) {
+ var context = new DatabaseContext();
+ var repo = new UserRepository(context, new GroupRepository(context));
+
+ for (int i = 0; i < count; i++) {
+ await context.Users.AddAsync(CreateTestUser());
+ }
+ await context.SaveChangesAsync();
+
+ return (context, repo);
+ }
+
+ private User CreateTestUser() => new () {
+ Username = "",
+ Email = "",
+ Password = ""
+ };
+
+ [Fact]
+ public async Task GetUsers_Returns_AllUsers() {
+ // Arrange
+ var count = 5;
+ var (context, repo) = await SetupEnvironment(count);
+
+ // Act
+ var users = await repo.GetUsers();
+
+ // Assert
+ Assert.NotNull(users);
+ Assert.Equal(count, users.Count);
+ }
+
+ [Fact]
+ public async Task GetUser_Returns_SingleUser() {
+ // Arrange
+ var (context, repo) = await SetupEnvironment();
+ var guid = context.Users.First().Id;
+
+ // Act
+ var user = await repo.GetUser(guid);
+
+ // Assert
+ Assert.NotNull(user);
+ Assert.Equal(guid, user.Id);
+ }
+
+ [Fact]
+ public async Task GetUserByMail_Returns_SingleUser() {
+ // Arrange
+ var (context, repo) = await SetupEnvironment();
+ var user = CreateTestUser();
+ user.Email = "test@example.com";
+ await context.Users.AddAsync(user);
+ await context.SaveChangesAsync();
+
+ // Act
+ var result = await repo.GetUserByEmail("test@example.com");
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal(user, result);
+ }
+
+ [Fact]
+ public async Task GetUserByUsername_Returns_SingleUser() {
+ // Arrange
+ var (context, repo) = await SetupEnvironment();
+ var user = CreateTestUser();
+ user.Username = "test.user";
+ await context.Users.AddAsync(user);
+ await context.SaveChangesAsync();
+
+ // Act
+ var result = await repo.GetUserByUsername("test.user");
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal(user, result);
+ }
+
+ [Fact]
+ public async Task AddUser_Returns_NewUser() {
+ // Arrange
+ var count = 5;
+ var (context, repo) = await SetupEnvironment(count);
+
+ // Act
+ var user = await repo.AddUser(new User {
+ Username = "test.user",
+ Email = "test@example.com",
+ Password = "changeme"
+ });
+
+ // Assert
+ Assert.NotNull(user);
+ Assert.Equal(count + 1, context.Users.Count());
+ }
+
+ [Fact]
+ public async Task UpdateUser_Should_UpdateAUser() {
+ // Arrange
+ var (context, repo) = await SetupEnvironment();
+ var user = context.Users.First();
+
+ // Act
+ user.Username = "test.user";
+ await repo.UpdateUser(user);
+
+ // Assert
+ var result = context.Users.SingleOrDefault(u => u.Username == "test.user");
+ Assert.NotNull(result);
+ }
+
+ [Fact]
+ public async Task DeleteUser_Should_DeleteSingleUser() {
+ // Arrange
+ var count = 5;
+ var (context, repo) = await SetupEnvironment(count);
+ var user = context.Users.First();
+
+ // Act
+ await repo.DeleteUser(user);
+
+ // Assert
+ Assert.Equal(count - 1, context.Users.Count());
+ Assert.DoesNotContain(user, context.Users);
+ }
+
+ [Fact]
+ public async Task CheckUserPassword_Returns_True() {
+ // Arrange
+ var (context, repo) = await SetupEnvironment(0);
+ var user = CreateTestUser();
+ user.Password = "changeme";
+ user = await repo.AddUser(user);
+
+ // Act
+ var result = await repo.CheckUserPassword(user, "changeme");
+
+ // Assert
+ Assert.True(result);
+ }
+
+ [Fact]
+ public async Task CheckUserPassword_Returns_False() {
+ // Arrange
+ var (context, repo) = await SetupEnvironment(0);
+ var user = CreateTestUser();
+ user.Password = "changeme";
+ user = await repo.AddUser(user);
+
+ // Act
+ var result = await repo.CheckUserPassword(user, "dontchangeme");
+
+ // Assert
+ Assert.False(result);
+ }
+
+ [Fact]
+ public async Task ChangePassword_Should_ChangeUserPassword() {
+ // Arrange
+ var (context, repo) = await SetupEnvironment(0);
+ var user = CreateTestUser();
+ user.Password = "changeme";
+ user = await repo.AddUser(user);
+
+ // Act
+ await repo.ChangePassword(user, "changedme");
+
+ // Assert
+ var result = await repo.CheckUserPassword(user, "changedme");
+ Assert.True(result);
+ }
+
+}
\ No newline at end of file
diff --git a/tests/HopFrame.Tests.Security/AuthenticationTests.cs b/tests/HopFrame.Tests.Security/AuthenticationTests.cs
new file mode 100644
index 0000000..7c80e7d
--- /dev/null
+++ b/tests/HopFrame.Tests.Security/AuthenticationTests.cs
@@ -0,0 +1,141 @@
+using System.Text.Encodings.Web;
+using HopFrame.Database.Models;
+using HopFrame.Database.Repositories;
+using HopFrame.Security.Authentication;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Moq;
+
+namespace HopFrame.Tests.Security;
+
+public class AuthenticationTests {
+
+ private async Task SetupEnvironment(Token correctToken = null, string providedToken = null) {
+ var options = new Mock>();
+ options
+ .Setup(x => x.Get(It.IsAny()))
+ .Returns(new AuthenticationSchemeOptions());
+
+ var logger = new Mock();
+ logger
+ .Setup(x => x.CreateLogger(It.IsAny()))
+ .Returns(new Mock>().Object);
+
+ var encoder = new Mock();
+ var clock = new Mock();
+ var tokens = new Mock();
+ var perms = new Mock();
+
+ var provideCorrectToken = correctToken is null;
+ correctToken ??= new Token {
+ Content = Guid.NewGuid(),
+ CreatedAt = DateTime.Now,
+ Type = Token.AccessTokenType,
+ Owner = new User {
+ Id = Guid.NewGuid()
+ }
+ };
+
+ tokens
+ .Setup(x => x.GetToken(It.Is(t => t == correctToken.Content.ToString())))
+ .ReturnsAsync(correctToken);
+
+ perms
+ .Setup(x => x.GetFullPermissions(It.IsAny()))
+ .ReturnsAsync(new List());
+
+ var auth = new HopFrameAuthentication(options.Object, logger.Object, encoder.Object, clock.Object, tokens.Object, perms.Object);
+ var context = new DefaultHttpContext();
+ if (provideCorrectToken)
+ context.HttpContext.Request.Headers.Append(HopFrameAuthentication.SchemeName, correctToken.Content.ToString());
+ if (providedToken is not null)
+ context.HttpContext.Request.Headers.Append(HopFrameAuthentication.SchemeName, providedToken);
+
+ await auth.InitializeAsync(new AuthenticationScheme(HopFrameAuthentication.SchemeName, null, typeof(HopFrameAuthentication)), context);
+ return auth;
+ }
+
+ [Fact]
+ public async Task Authentication_Should_Succeed() {
+ // Arrange
+ var auth = await SetupEnvironment();
+
+ // Act
+ var result = await auth.AuthenticateAsync();
+
+ // Assert
+ Assert.True(result.Succeeded);
+ }
+
+ [Fact]
+ public async Task Authentication_With_NoToken_Should_Fail() {
+ // Arrange
+ var auth = await SetupEnvironment(new Token());
+
+ // Act
+ var result = await auth.AuthenticateAsync();
+
+ // Assert
+ Assert.False(result.Succeeded);
+ Assert.NotNull(result.Failure);
+ Assert.Equal("No Access Token provided", result.Failure.Message);
+ }
+
+ [Fact]
+ public async Task Authentication_With_InvalidToken_Should_Fail() {
+ // Arrange
+ var auth = await SetupEnvironment(null, Guid.NewGuid().ToString());
+
+ // Act
+ var result = await auth.AuthenticateAsync();
+
+ // Assert
+ Assert.False(result.Succeeded);
+ Assert.NotNull(result.Failure);
+ Assert.Equal("The provided Access Token does not exist", result.Failure.Message);
+ }
+
+ [Fact]
+ public async Task Authentication_With_ExpiredToken_Should_Fail() {
+ // Arrange
+ var token = new Token {
+ Content = Guid.NewGuid(),
+ CreatedAt = DateTime.MinValue,
+ Type = Token.AccessTokenType,
+ Owner = new User()
+ };
+ var auth = await SetupEnvironment(token, token.Content.ToString());
+
+ // Act
+ var result = await auth.AuthenticateAsync();
+
+ // Assert
+ Assert.False(result.Succeeded);
+ Assert.NotNull(result.Failure);
+ Assert.Equal("The provided Access Token is expired", result.Failure.Message);
+ }
+
+ [Fact]
+ public async Task Authentication_With_UnownedToken_Should_Fail() {
+ // Arrange
+ var token = new Token {
+ Content = Guid.NewGuid(),
+ CreatedAt = DateTime.Now,
+ Type = Token.AccessTokenType,
+ Owner = null
+ };
+ var auth = await SetupEnvironment(token, token.Content.ToString());
+
+ // Act
+ var result = await auth.AuthenticateAsync();
+
+ // Assert
+ Assert.False(result.Succeeded);
+ Assert.NotNull(result.Failure);
+ Assert.Equal("The provided Access Token does not match any user", result.Failure.Message);
+ }
+
+
+}
\ No newline at end of file
diff --git a/tests/HopFrame.Tests.Security/AuthorizationTests.cs b/tests/HopFrame.Tests.Security/AuthorizationTests.cs
new file mode 100644
index 0000000..98032fb
--- /dev/null
+++ b/tests/HopFrame.Tests.Security/AuthorizationTests.cs
@@ -0,0 +1,92 @@
+using System.Security.Claims;
+using HopFrame.Security.Authentication;
+using HopFrame.Security.Authorization;
+using HopFrame.Security.Claims;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Abstractions;
+using Microsoft.AspNetCore.Mvc.Filters;
+using Microsoft.AspNetCore.Routing;
+using Moq;
+
+namespace HopFrame.Tests.Security;
+
+public class AuthorizationTests {
+
+ private (AuthorizedFilter, AuthorizationFilterContext) SetupEnvironment(string[] userPermissions, string[] requiredPermissions, bool accessTokenProvided = true) {
+ var filter = new AuthorizedFilter(requiredPermissions);
+
+ var httpContext = new DefaultHttpContext();
+ var actionContext = new ActionContext { HttpContext = httpContext, RouteData = new RouteData(), ActionDescriptor = new ActionDescriptor() };
+ var context = new Mock(MockBehavior.Default, actionContext, new List());
+
+ context
+ .Setup(x => x.Filters)
+ .Returns(new List());
+
+ context.SetupProperty(c => c.Result);
+
+ var claims = new List {
+ new(HopFrameClaimTypes.UserId, Guid.NewGuid().ToString())
+ };
+ if (accessTokenProvided)
+ claims.Add(new (HopFrameClaimTypes.AccessTokenId, Guid.NewGuid().ToString()));
+ claims.AddRange(userPermissions.Select(perm => new Claim(HopFrameClaimTypes.Permission, perm)));
+
+ context.Object.HttpContext.User.AddIdentity(new ClaimsIdentity(claims, HopFrameAuthentication.SchemeName));
+
+ return (filter, context.Object);
+ }
+
+ [Fact]
+ public void OnAuthorization_Should_Succeed() {
+ // Arrange
+ var (filter, context) = SetupEnvironment(["test.permission"], ["test.permission"]);
+
+ // Act
+ filter.OnAuthorization(context);
+
+ // Assert
+ Assert.Null(context.Result);
+ }
+
+ [Fact]
+ public void OnAuthorization_With_NoToken_Should_Fail() {
+ // Arrange
+ var (filter, context) = SetupEnvironment([], [], false);
+
+ // Act
+ filter.OnAuthorization(context);
+
+ // Assert
+ Assert.NotNull(context.Result);
+ Assert.IsType(context.Result);
+ }
+
+ [Fact]
+ public void OnAuthorization_With_NoPermissions_Should_Fail() {
+ // Arrange
+ var (filter, context) = SetupEnvironment([], ["test.permission"]);
+
+ // Act
+ filter.OnAuthorization(context);
+
+ // Assert
+ Assert.NotNull(context.Result);
+ Assert.IsType(context.Result);
+ }
+
+ [Fact]
+ public void OnAuthorization_With_InsufficientPermissions_Should_Fail() {
+ // Arrange
+ var (filter, context) = SetupEnvironment(["permission.other"], ["test.permission"]);
+
+ // Act
+ filter.OnAuthorization(context);
+
+ // Assert
+ Assert.NotNull(context.Result);
+ Assert.IsType(context.Result);
+ }
+
+}
\ No newline at end of file
diff --git a/tests/HopFrame.Tests.Security/HopFrame.Tests.Security.csproj b/tests/HopFrame.Tests.Security/HopFrame.Tests.Security.csproj
new file mode 100644
index 0000000..c1feef2
--- /dev/null
+++ b/tests/HopFrame.Tests.Security/HopFrame.Tests.Security.csproj
@@ -0,0 +1,34 @@
+
+
+
+ net8.0
+ enable
+ disable
+
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ..\..\..\..\..\..\..\Program Files\dotnet\shared\Microsoft.AspNetCore.App\8.0.11\Microsoft.AspNetCore.Authentication.dll
+
+
+
+
+
+
+
+
diff --git a/tests/HopFrame.Tests.Web/AuthMiddlewareTests.cs b/tests/HopFrame.Tests.Web/AuthMiddlewareTests.cs
new file mode 100644
index 0000000..d9e136f
--- /dev/null
+++ b/tests/HopFrame.Tests.Web/AuthMiddlewareTests.cs
@@ -0,0 +1,95 @@
+using System.Security.Claims;
+using HopFrame.Database.Models;
+using HopFrame.Database.Repositories;
+using HopFrame.Security.Claims;
+using HopFrame.Web;
+using HopFrame.Web.Services;
+using Microsoft.AspNetCore.Http;
+using Moq;
+
+namespace HopFrame.Tests.Web;
+
+public class AuthMiddlewareTests {
+ private readonly RequestDelegate _delegate = _ => Task.CompletedTask;
+
+ public AuthMiddleware SetupEnvironment(bool isLoggedIn = true, Token newToken = null) {
+ var auth = new Mock();
+ auth
+ .Setup(a => a.IsLoggedIn())
+ .ReturnsAsync(isLoggedIn);
+ auth
+ .Setup(a => a.RefreshLogin())
+ .ReturnsAsync(newToken);
+
+ var perms = new Mock();
+ perms
+ .Setup(p => p.GetFullPermissions(It.Is(u => newToken.Owner.Id == u.Id)))
+ .ReturnsAsync(CreateDummyUser().Permissions.Select(p => p.PermissionName).ToList);
+
+ return new AuthMiddleware(auth.Object, perms.Object);
+ }
+
+ private User CreateDummyUser() => new() {
+ Id = Guid.NewGuid(),
+ CreatedAt = DateTime.Now,
+ Email = "test@example.com",
+ Username = "ExampleUser",
+ Password = "1234567890",
+ Permissions = new List {
+ new () {
+ PermissionName = "test.permission"
+ }
+ }
+ };
+
+ [Fact]
+ public async Task InvokeAsync_With_ValidLogin_Should_Succeed() {
+ // Arrange
+ var auth = SetupEnvironment();
+ var context = new DefaultHttpContext();
+
+ // Act
+ await auth.InvokeAsync(context, _delegate);
+
+ // Assert
+ Assert.Null(context.User.FindFirst(HopFrameClaimTypes.UserId));
+ Assert.Null(context.User.FindFirst(HopFrameClaimTypes.AccessTokenId));
+ Assert.Null(context.User.FindFirst(HopFrameClaimTypes.Permission));
+ }
+
+ [Fact]
+ public async Task InvokeAsync_With_InvalidLoginValidToken_Should_Succeed() {
+ // Arrange
+ var token = new Token {
+ Content = Guid.NewGuid(),
+ CreatedAt = DateTime.Now,
+ Type = Token.AccessTokenType,
+ Owner = CreateDummyUser()
+ };
+ var auth = SetupEnvironment(false, token);
+ var context = new DefaultHttpContext();
+
+ // Act
+ await auth.InvokeAsync(context, _delegate);
+
+ // Assert
+ Assert.Equal(token.Owner.Id.ToString(), context.User.FindFirstValue(HopFrameClaimTypes.UserId));
+ Assert.Equal(token.Content.ToString(), context.User.FindFirstValue(HopFrameClaimTypes.AccessTokenId));
+ Assert.Equal(token.Owner.Permissions.First().PermissionName, context.User.FindFirstValue(HopFrameClaimTypes.Permission));
+ }
+
+ [Fact]
+ public async Task InvokeAsync_With_InvalidLoginInvalidToken_Should_Succeed() {
+ // Arrange
+ var auth = SetupEnvironment(false);
+ var context = new DefaultHttpContext();
+
+ // Act
+ await auth.InvokeAsync(context, _delegate);
+
+ // Assert
+ Assert.Null(context.User.FindFirst(HopFrameClaimTypes.UserId));
+ Assert.Null(context.User.FindFirst(HopFrameClaimTypes.AccessTokenId));
+ Assert.Null(context.User.FindFirst(HopFrameClaimTypes.Permission));
+ }
+}
\ No newline at end of file
diff --git a/tests/HopFrame.Tests.Web/AuthServiceTests.cs b/tests/HopFrame.Tests.Web/AuthServiceTests.cs
new file mode 100644
index 0000000..a5df287
--- /dev/null
+++ b/tests/HopFrame.Tests.Web/AuthServiceTests.cs
@@ -0,0 +1,334 @@
+using HopFrame.Database.Models;
+using HopFrame.Database.Repositories;
+using HopFrame.Security.Claims;
+using HopFrame.Security.Models;
+using HopFrame.Tests.Web.Extensions;
+using HopFrame.Web.Services;
+using HopFrame.Web.Services.Implementation;
+using Microsoft.AspNetCore.Http;
+using Moq;
+
+namespace HopFrame.Tests.Web;
+
+public class AuthServiceTests {
+ private readonly Guid _refreshToken = Guid.NewGuid();
+ private readonly Guid _accessToken = Guid.NewGuid();
+
+ private (IAuthService, HttpContext) SetupEnvironment(bool passwordIsCorrect = true, Token providedRefreshToken = null, string providedTokenCookie = null, Token providedAccessToken = null) {
+ var accessor = new HttpContextAccessor {
+ HttpContext = new DefaultHttpContext()
+ };
+
+ if (providedTokenCookie != null) {
+ var cookies = new Mock();
+ cookies
+ .SetupGet(c => c[ITokenContext.RefreshTokenType])
+ .Returns(providedTokenCookie);
+ accessor.HttpContext.Request.Cookies = cookies.Object;
+ }
+
+ var users = new Mock();
+ users
+ .Setup(u => u.GetUserByEmail(It.Is(email => CreateDummyUser().Email == email)))
+ .ReturnsAsync(CreateDummyUser());
+ users
+ .Setup(u => u.CheckUserPassword(It.Is(u => u.Email == CreateDummyUser().Email), It.IsAny()))
+ .ReturnsAsync(passwordIsCorrect);
+ users
+ .Setup(u => u.AddUser(It.IsAny()))
+ .ReturnsAsync(CreateDummyUser());
+ users
+ .Setup(u => u.GetUsers())
+ .ReturnsAsync(new List { CreateDummyUser() });
+
+ var tokens = new Mock();
+ tokens
+ .Setup(t => t.CreateToken(It.Is(t => t == Token.RefreshTokenType), It.IsAny()))
+ .ReturnsAsync(new Token {
+ Content = _refreshToken,
+ Type = Token.RefreshTokenType
+ });
+ tokens
+ .Setup(t => t.CreateToken(It.Is(t => t == Token.AccessTokenType), It.IsAny()))
+ .ReturnsAsync(new Token {
+ Content = _accessToken,
+ Type = Token.AccessTokenType
+ });
+ tokens
+ .Setup(t => t.GetToken(It.Is(token => token == _refreshToken.ToString())))
+ .ReturnsAsync(providedRefreshToken);
+
+ var context = new Mock();
+ context
+ .Setup(c => c.User)
+ .Returns(CreateDummyUser());
+ context
+ .Setup(c => c.AccessToken)
+ .Returns(providedAccessToken);
+
+ return (new AuthService(users.Object, accessor, tokens.Object, context.Object), accessor.HttpContext);
+ }
+
+ private User CreateDummyUser() => new() {
+ Id = Guid.NewGuid(),
+ CreatedAt = DateTime.Now,
+ Email = "test@example.com",
+ Username = "ExampleUser",
+ Password = "1234567890"
+ };
+
+ [Fact]
+ public async Task Register_Should_Succeed() {
+ // Arrange
+ var (service, context) = SetupEnvironment();
+ var register = new UserRegister {
+ Email = CreateDummyUser().Email,
+ Username = CreateDummyUser().Username,
+ Password = CreateDummyUser().Password
+ };
+
+ // Act
+ await service.Register(register);
+
+ // Assert
+ Assert.Equal(_accessToken.ToString(), context.Response.Headers.FindCookie(ITokenContext.AccessTokenType));
+ Assert.Equal(_refreshToken.ToString(), context.Response.Headers.FindCookie(ITokenContext.RefreshTokenType));
+ }
+
+ [Fact]
+ public async Task Login_Should_Succeed() {
+ // Arrange
+ var (service, context) = SetupEnvironment();
+ var login = new UserLogin {
+ Email = CreateDummyUser().Email,
+ Password = CreateDummyUser().Password
+ };
+
+ // Act
+ var result = await service.Login(login);
+
+ // Assert
+ Assert.True(result);
+ Assert.Equal(_accessToken.ToString(), context.Response.Headers.FindCookie(ITokenContext.AccessTokenType));
+ Assert.Equal(_refreshToken.ToString(), context.Response.Headers.FindCookie(ITokenContext.RefreshTokenType));
+ }
+
+ [Fact]
+ public async Task Login_With_WrongPassword_Should_Fail() {
+ // Arrange
+ var (service, context) = SetupEnvironment(false);
+ var login = new UserLogin {
+ Email = CreateDummyUser().Email,
+ Password = CreateDummyUser().Password
+ };
+
+ // Act
+ var result = await service.Login(login);
+
+ // Assert
+ Assert.False(result);
+ Assert.Null(context.Response.Headers.FindCookie(ITokenContext.AccessTokenType));
+ Assert.Null(context.Response.Headers.FindCookie(ITokenContext.RefreshTokenType));
+ }
+
+ [Fact]
+ public async Task Login_With_WrongEmail_Should_Fail() {
+ // Arrange
+ var (service, context) = SetupEnvironment();
+ var login = new UserLogin {
+ Email = "wrong@example.com",
+ Password = CreateDummyUser().Password
+ };
+
+ // Act
+ var result = await service.Login(login);
+
+ // Assert
+ Assert.False(result);
+ Assert.Null(context.Response.Headers.FindCookie(ITokenContext.RefreshTokenType));
+ Assert.Null(context.Response.Headers.FindCookie(ITokenContext.AccessTokenType));
+ }
+
+ [Fact]
+ public async Task Logout_Should_Succeed() {
+ // Arrange
+ var (service, context) = SetupEnvironment(providedTokenCookie: _refreshToken.ToString());
+ context.Response.Cookies.Append(ITokenContext.AccessTokenType, _accessToken.ToString());
+ context.Response.Cookies.Append(ITokenContext.RefreshTokenType, _refreshToken.ToString());
+
+ // Act
+ await service.Logout();
+
+ // Assert
+ Assert.Null(context.Response.Headers.FindCookie(ITokenContext.AccessTokenType));
+ Assert.Null(context.Response.Headers.FindCookie(ITokenContext.RefreshTokenType));
+ }
+
+ [Fact]
+ public async Task RefreshLogin_Should_Succeed() {
+ // Arrange
+ var token = new Token {
+ Type = Token.RefreshTokenType,
+ Content = _refreshToken,
+ CreatedAt = DateTime.Now,
+ Owner = CreateDummyUser()
+ };
+ var (service, context) = SetupEnvironment(true, token, token.Content.ToString());
+
+ // Act
+ var result = await service.RefreshLogin();
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal(_accessToken, result.Content);
+ Assert.Equal(_accessToken.ToString(), context.Response.Headers.FindCookie(ITokenContext.AccessTokenType));
+ }
+
+ [Fact]
+ public async Task RefreshLogin_With_NoProvidedToken_Should_Fail() {
+ // Arrange
+ var (service, context) = SetupEnvironment();
+
+ // Act
+ var result = await service.RefreshLogin();
+
+ // Assert
+ Assert.Null(result);
+ Assert.Null(context.Response.Headers.FindCookie(ITokenContext.AccessTokenType));
+ }
+
+ [Fact]
+ public async Task RefreshLogin_With_WrongToken_Should_Fail() {
+ // Arrange
+ var (service, context) = SetupEnvironment(true, null, _refreshToken.ToString());
+
+ // Act
+ var result = await service.RefreshLogin();
+
+ // Assert
+ Assert.Null(result);
+ Assert.Null(context.Response.Headers.FindCookie(ITokenContext.AccessTokenType));
+ }
+
+ [Fact]
+ public async Task RefreshLogin_With_WrongTokenType_Should_Fail() {
+ // Arrange
+ var token = new Token {
+ Type = Token.AccessTokenType,
+ Content = _refreshToken,
+ CreatedAt = DateTime.Now,
+ Owner = CreateDummyUser()
+ };
+ var (service, context) = SetupEnvironment(true, token, token.Content.ToString());
+
+ // Act
+ var result = await service.RefreshLogin();
+
+ // Assert
+ Assert.Null(result);
+ Assert.Null(context.Response.Headers.FindCookie(ITokenContext.AccessTokenType));
+ }
+
+ [Fact]
+ public async Task RefreshLogin_With_ExpiredToken_Should_Fail() {
+ // Arrange
+ var token = new Token {
+ Type = Token.RefreshTokenType,
+ Content = _refreshToken,
+ CreatedAt = DateTime.MinValue,
+ Owner = CreateDummyUser()
+ };
+ var (service, context) = SetupEnvironment(true, token, token.Content.ToString());
+
+ // Act
+ var result = await service.RefreshLogin();
+
+ // Assert
+ Assert.Null(result);
+ Assert.Null(context.Response.Headers.FindCookie(ITokenContext.AccessTokenType));
+ }
+
+ [Fact]
+ public async Task IsLoggedIn_Should_Succeed() {
+ // Arrange
+ var token = new Token {
+ Type = Token.AccessTokenType,
+ Content = _accessToken,
+ CreatedAt = DateTime.Now,
+ Owner = CreateDummyUser()
+ };
+ var (service, context) = SetupEnvironment(providedAccessToken: token);
+
+ // Act
+ var result = await service.IsLoggedIn();
+
+ // Assert
+ Assert.True(result);
+ }
+
+ [Fact]
+ public async Task IsLoggedIn_With_NoProvidedToken_Should_Fail() {
+ // Arrange
+ var (service, context) = SetupEnvironment();
+
+ // Act
+ var result = await service.IsLoggedIn();
+
+ // Assert
+ Assert.False(result);
+ }
+
+ [Fact]
+ public async Task IsLoggedIn_With_WrongTokenType_Should_Fail() {
+ // Arrange
+ var token = new Token {
+ Type = Token.RefreshTokenType,
+ Content = _accessToken,
+ CreatedAt = DateTime.Now,
+ Owner = CreateDummyUser()
+ };
+ var (service, context) = SetupEnvironment(providedAccessToken: token);
+
+ // Act
+ var result = await service.IsLoggedIn();
+
+ // Assert
+ Assert.False(result);
+ }
+
+ [Fact]
+ public async Task IsLoggedIn_With_ExpiredToken_Should_Fail() {
+ // Arrange
+ var token = new Token {
+ Type = Token.AccessTokenType,
+ Content = _accessToken,
+ CreatedAt = DateTime.MinValue,
+ Owner = CreateDummyUser()
+ };
+ var (service, context) = SetupEnvironment(providedAccessToken: token);
+
+ // Act
+ var result = await service.IsLoggedIn();
+
+ // Assert
+ Assert.False(result);
+ }
+
+ [Fact]
+ public async Task IsLoggedIn_With_NoOwner_Should_Fail() {
+ // Arrange
+ var token = new Token {
+ Type = Token.AccessTokenType,
+ Content = _accessToken,
+ CreatedAt = DateTime.Now,
+ Owner = null
+ };
+ var (service, context) = SetupEnvironment(providedAccessToken: token);
+
+ // Act
+ var result = await service.IsLoggedIn();
+
+ // Assert
+ Assert.False(result);
+ }
+}
\ No newline at end of file
diff --git a/tests/HopFrame.Tests.Web/Extensions/HttpContextExtensions.cs b/tests/HopFrame.Tests.Web/Extensions/HttpContextExtensions.cs
new file mode 100644
index 0000000..a26537a
--- /dev/null
+++ b/tests/HopFrame.Tests.Web/Extensions/HttpContextExtensions.cs
@@ -0,0 +1,29 @@
+using System.Web;
+using Microsoft.AspNetCore.Http;
+
+namespace HopFrame.Tests.Web.Extensions;
+
+internal static class HttpContextExtensions {
+ /// Extracts the partial cookie value from the header section.
+ ///
+ /// The key for identifying the cookie.
+ /// The value of the cookie.
+ public static string FindCookie(this IHeaderDictionary headers, string key)
+ {
+ string headerKey = $"{key}=";
+ var cookies = headers.Values
+ .SelectMany(h => h)
+ .Where(header => header.StartsWith(headerKey))
+ .Select(header => header.Substring(headerKey.Length).Split(';').First())
+ .ToArray();
+
+ //Note: cookie values in a header are encoded like a uri parameter value.
+ var value = cookies.LastOrDefault();//and the last set value, is the relevant one.
+ if (string.IsNullOrEmpty(value))
+ return null;
+
+ //That's why we should decode that last value, before we return it.
+ var decoded = HttpUtility.UrlDecode(value);
+ return decoded;
+ }
+}
\ No newline at end of file
diff --git a/tests/HopFrame.Tests.Web/HopFrame.Tests.Web.csproj b/tests/HopFrame.Tests.Web/HopFrame.Tests.Web.csproj
new file mode 100644
index 0000000..b6e4aa4
--- /dev/null
+++ b/tests/HopFrame.Tests.Web/HopFrame.Tests.Web.csproj
@@ -0,0 +1,29 @@
+
+
+
+ net8.0
+ enable
+ disable
+
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/HopFrame.Tests.Web/Pages/AdminLoginTests.cs b/tests/HopFrame.Tests.Web/Pages/AdminLoginTests.cs
new file mode 100644
index 0000000..0e2909a
--- /dev/null
+++ b/tests/HopFrame.Tests.Web/Pages/AdminLoginTests.cs
@@ -0,0 +1,123 @@
+using BlazorStrap;
+using Bunit;
+using Bunit.TestDoubles;
+using CurrieTechnologies.Razor.SweetAlert2;
+using HopFrame.Security.Models;
+using HopFrame.Web.Pages.Administration;
+using HopFrame.Web.Services;
+using Microsoft.AspNetCore.Components;
+using Microsoft.Extensions.DependencyInjection;
+using Moq;
+
+namespace HopFrame.Tests.Web.Pages;
+
+public class AdminLoginTests : TestContext {
+
+ private (IRenderedComponent, NavigationManager) SetupEnvironment(bool correctCredentials = true) {
+ var auth = new Mock();
+ auth
+ .Setup(a => a.Login(It.IsAny()))
+ .ReturnsAsync(correctCredentials);
+
+ Services.AddSweetAlert2();
+ Services.AddBlazorStrap();
+ Services.AddSingleton(auth.Object);
+ var navigator = Services.GetRequiredService();
+
+ var component = RenderComponent();
+ return (component, navigator);
+ }
+
+ [Fact]
+ public void Login_Has_RequiredFields() {
+ // Arrange
+ var (component, _) = SetupEnvironment();
+
+ // Act
+ var inputs = component.FindAll("input");
+ var buttons = component.FindAll("button");
+ var form = component.FindAll("form");
+
+ // Assert
+ Assert.Equal(2, inputs.Count);
+ Assert.Single(buttons);
+ Assert.Single(form);
+ Assert.Equal("submit", buttons[0].Attributes.GetNamedItem("type")?.Value);
+
+ foreach (var input in inputs) {
+ var attribute = input.Attributes.GetNamedItem("required");
+ Assert.NotNull(attribute);
+ Assert.NotEqual("false", attribute?.Value);
+ }
+ }
+
+ [Fact]
+ public void Login_With_CorrectCredentials_Should_Redirect() {
+ // Arrange
+ var (component, nav) = SetupEnvironment();
+ var email = component.Find("""input[type="email"]""");
+ var password = component.Find("""input[type="password"]""");
+ var submit = component.Find("button");
+
+ // Act
+ email.Change("test@example.com");
+ password.Change("1234567890");
+ submit.Click();
+
+ // Assert
+ Assert.EndsWith("/administration", nav.Uri);
+ }
+
+ [Fact]
+ public void Login_With_CorrectCredentials_And_CustomRedirect_Should_Redirect() {
+ // Arrange
+ var (component, nav) = SetupEnvironment();
+ var email = component.Find("""input[type="email"]""");
+ var password = component.Find("""input[type="password"]""");
+ var submit = component.Find("button");
+
+ component.Instance.RedirectAfter = "testRedirect";
+
+ // Act
+ email.Change("test@example.com");
+ password.Change("1234567890");
+ submit.Click();
+
+ // Assert
+ Assert.EndsWith("/administration/testRedirect", nav.Uri);
+ }
+
+ [Fact]
+ public void Login_With_IncorrectCredentials_Should_Fail() {
+ // Arrange
+ var (component, nav) = SetupEnvironment(false);
+ var email = component.Find("""input[type="email"]""");
+ var password = component.Find("""input[type="password"]""");
+ var submit = component.Find("button");
+
+ // Act
+ email.Change("test@example.com");
+ password.Change("1234567890");
+ submit.Click();
+
+ // Assert
+ Assert.False(nav.Uri.EndsWith("/administration"));
+ }
+
+ [Fact]
+ public void Login_With_IncorrectCredentials_DisplaysError() {
+ // Arrange
+ var (component, _) = SetupEnvironment(false);
+ var email = component.Find("""input[type="email"]""");
+ var password = component.Find("""input[type="password"]""");
+ var submit = component.Find("button");
+
+ // Act
+ email.Change("test@example.com");
+ password.Change("1234567890");
+ submit.Click();
+
+ // Assert
+ Assert.Contains("Email or password does not match any account!", component.Markup);
+ }
+}
\ No newline at end of file
diff --git a/tests/HopFrame.Tests.Web/Pages/AuthorizedViewTests.cs b/tests/HopFrame.Tests.Web/Pages/AuthorizedViewTests.cs
new file mode 100644
index 0000000..92b9611
--- /dev/null
+++ b/tests/HopFrame.Tests.Web/Pages/AuthorizedViewTests.cs
@@ -0,0 +1,133 @@
+using System.Security.Claims;
+using Bunit;
+using Bunit.TestDoubles;
+using HopFrame.Security.Authentication;
+using HopFrame.Security.Claims;
+using HopFrame.Web.Components;
+using Microsoft.AspNetCore.Components;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.DependencyInjection;
+using Moq;
+
+namespace HopFrame.Tests.Web.Pages;
+
+public class AuthorizedViewTests : TestContext {
+ private readonly string _testRedirect = "testRedirect";
+ private readonly string _testPermission = "test.permission";
+ private readonly string _innerHtml = "Inner Render
";
+
+ public NavigationManager SetupEnvironment(bool authenticated = true, params string[] userPermissions) {
+ var auth = new Mock();
+ auth
+ .Setup(a => a.IsAuthenticated)
+ .Returns(authenticated);
+
+ var context = new DefaultHttpContext();
+ var claims = userPermissions?.Select(perm => new Claim(HopFrameClaimTypes.Permission, perm)).ToList();
+ context.User.AddIdentity(new ClaimsIdentity(claims, HopFrameAuthentication.SchemeName));
+ var accessor = new Mock();
+ accessor
+ .Setup(a => a.HttpContext)
+ .Returns(context);
+
+ Services.AddSingleton(auth.Object);
+ Services.AddSingleton(accessor.Object);
+ return Services.GetRequiredService();
+ }
+
+ [Fact]
+ public void AuthorizedView_With_NoValidLogin_And_Redirection_Should_Redirect() {
+ // Arrange
+ var navigator = SetupEnvironment(false);
+
+ // Act
+ RenderComponent(parameters => parameters
+ .Add(a => a.RedirectIfUnauthorized, _testRedirect));
+
+ // Assert
+ Assert.EndsWith(_testRedirect, navigator.Uri);
+ }
+
+ [Fact]
+ public void AuthorizedView_With_NoPermissions_And_Redirection_Should_Redirect() {
+ // Arrange
+ var navigator = SetupEnvironment();
+
+ // Act
+ RenderComponent(parameters => parameters
+ .Add(a => a.RedirectIfUnauthorized, _testRedirect)
+ .Add(a => a.Permission, _testPermission));
+
+ // Assert
+ Assert.EndsWith(_testRedirect, navigator.Uri);
+ }
+
+ [Fact]
+ public void AuthorizedView_With_FewPermissions_And_Redirection_Should_Redirect() {
+ // Arrange
+ var navigator = SetupEnvironment(true, "other.permission");
+
+ // Act
+ RenderComponent(parameters => parameters
+ .Add(a => a.RedirectIfUnauthorized, _testRedirect)
+ .Add(a => a.Permissions, [_testPermission, "other.permission"]));
+
+ // Assert
+ Assert.EndsWith(_testRedirect, navigator.Uri);
+ }
+
+ [Fact]
+ public void AuthorizedView_With_Permissions_And_Redirection_Should_NotRedirect() {
+ // Arrange
+ var navigator = SetupEnvironment(true, _testPermission);
+
+ // Act
+ RenderComponent(parameters => parameters
+ .Add(a => a.RedirectIfUnauthorized, _testRedirect)
+ .Add(a => a.Permission, _testPermission));
+
+ // Assert
+ Assert.False(navigator.Uri.EndsWith(_testRedirect));
+ }
+
+ [Fact]
+ public void AuthorizedView_With_AllPermissions_And_Redirection_Should_NotRedirect() {
+ // Arrange
+ var navigator = SetupEnvironment(true, _testPermission, "other.permission");
+
+ // Act
+ RenderComponent(parameters => parameters
+ .Add(a => a.RedirectIfUnauthorized, _testRedirect)
+ .Add(a => a.Permissions, [_testPermission, "other.permission"]));
+
+ // Assert
+ Assert.False(navigator.Uri.EndsWith(_testRedirect));
+ }
+
+ [Fact]
+ public void AuthorizedView_With_ChildComponent_And_ValidLogin_Should_DisplayChildren() {
+ // Arrange
+ SetupEnvironment();
+
+ // Act
+ var component = RenderComponent(parameters => parameters
+ .AddChildContent(_innerHtml));
+
+ // Assert
+ Assert.Contains(_innerHtml, component.Markup);
+ }
+
+ [Fact]
+ public void AuthorizedView_With_ChildComponent_And_InvalidLogin_Should_NotDisplayChildren() {
+ // Arrange
+ SetupEnvironment(false);
+
+ // Act
+ var component = RenderComponent(parameters => parameters
+ .AddChildContent(_innerHtml));
+
+ // Assert
+ Assert.DoesNotContain(_innerHtml, component.Markup);
+ }
+
+}
\ No newline at end of file