Created tests for HopFrame.Api
This commit is contained in:
@@ -24,6 +24,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Database.Tests", "
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Security.Tests", "tests\HopFrame.Security.Tests\HopFrame.Security.Tests.csproj", "{6747753A-6059-48F1-B779-D73765A373A6}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Security.Tests", "tests\HopFrame.Security.Tests\HopFrame.Security.Tests.csproj", "{6747753A-6059-48F1-B779-D73765A373A6}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Api.Tests", "tests\HopFrame.Api.Tests\HopFrame.Api.Tests.csproj", "{25DE1510-47E5-46FF-89A4-B9F99542218E}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@@ -66,6 +68,10 @@ Global
|
|||||||
{6747753A-6059-48F1-B779-D73765A373A6}.Debug|Any CPU.Build.0 = 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.ActiveCfg = Release|Any CPU
|
||||||
{6747753A-6059-48F1-B779-D73765A373A6}.Release|Any CPU.Build.0 = 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
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(NestedProjects) = preSolution
|
GlobalSection(NestedProjects) = preSolution
|
||||||
{1E821490-AEDC-4F55-B758-52F4FADAB53A} = {64EDCBED-A84F-4936-8697-78DC43CB2427}
|
{1E821490-AEDC-4F55-B758-52F4FADAB53A} = {64EDCBED-A84F-4936-8697-78DC43CB2427}
|
||||||
@@ -77,5 +83,6 @@ Global
|
|||||||
{921159CE-AF75-44C3-A3F9-6B9B1A4E85CF} = {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}
|
{1CAAC943-B8FE-48DD-9712-92699647DE18} = {1D98E5DE-CB8B-4C1C-A319-D49AC137441A}
|
||||||
{6747753A-6059-48F1-B779-D73765A373A6} = {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}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAuthenticationSchemeOptions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003F8525b7a9e58c77f532f1a88d4f2897e3c2baf316b9eb2c391b242a3885fcce6_003FAuthenticationSchemeOptions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAuthenticationSchemeOptions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003F8525b7a9e58c77f532f1a88d4f2897e3c2baf316b9eb2c391b242a3885fcce6_003FAuthenticationSchemeOptions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEditContextDataAnnotationsExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fbc307cd57fb42fc4c7fb9795381958122734d3750f41b6c1735c7d132ecda70_003FEditContextDataAnnotationsExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEditContextDataAnnotationsExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fbc307cd57fb42fc4c7fb9795381958122734d3750f41b6c1735c7d132ecda70_003FEditContextDataAnnotationsExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AExceptionDispatchInfo_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fbd1d5c50194fea68ff3559c160230b0ab50f5acf4ce3061bffd6d62958e2182_003FExceptionDispatchInfo_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AExceptionDispatchInfo_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fbd1d5c50194fea68ff3559c160230b0ab50f5acf4ce3061bffd6d62958e2182_003FExceptionDispatchInfo_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIHttpContextAccessor_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Ffe1b239a13ce466b829e79538c654ff54e938_003Fa5_003F4093f165_003FIHttpContextAccessor_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AList_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fb7208b3f72528d22781d25fde9a55271bdf2b5aade4f03b1324579a25493cd8_003FList_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AList_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fb7208b3f72528d22781d25fde9a55271bdf2b5aade4f03b1324579a25493cd8_003FList_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AThrowHelper_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003F2c8e7ca976f350cba9836d5565dac56b11e0b56656fa786460eb1395857a6fa_003FThrowHelper_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AThrowHelper_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003F2c8e7ca976f350cba9836d5565dac56b11e0b56656fa786460eb1395857a6fa_003FThrowHelper_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AValidationMessageStore_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003Ffc81648e473bb3cc818f71427c286ecddc3604d2f4c69c565205bb89e8b4ef4_003FValidationMessageStore_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AValidationMessageStore_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003Ffc81648e473bb3cc818f71427c286ecddc3604d2f4c69c565205bb89e8b4ef4_003FValidationMessageStore_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
@@ -16,11 +17,18 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=de547b37_002D85df_002D4c95_002Da13a_002D03b9d37ad50b/@EntryIndexedValue"><SessionState ContinuousTestingMode="0" Name="Authentication_With_NoToken_Should_Fail" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session">
|
|
||||||
<TestAncestor>
|
|
||||||
<TestId>xUnit::6747753A-6059-48F1-B779-D73765A373A6::net8.0::HopFrame.Security.Tests.AuthenticationTests.Authentication_With_NoToken_Should_Fail</TestId>
|
|
||||||
</TestAncestor>
|
|
||||||
</SessionState></s:String>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -22,4 +22,10 @@
|
|||||||
<None Include="README.md" Pack="true" PackagePath="\"/>
|
<None Include="README.md" Pack="true" PackagePath="\"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
|
||||||
|
<_Parameter1>HopFrame.Api.Tests</_Parameter1>
|
||||||
|
</AssemblyAttribute>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ using Microsoft.AspNetCore.Http;
|
|||||||
|
|
||||||
namespace HopFrame.Api.Logic.Implementation;
|
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<LogicResult<SingleValueResult<string>>> Login(UserLogin login) {
|
public async Task<LogicResult<SingleValueResult<string>>> Login(UserLogin login) {
|
||||||
var user = await users.GetUserByEmail(login.Email);
|
var user = await users.GetUserByEmail(login.Email);
|
||||||
@@ -38,7 +38,7 @@ public class AuthLogic(IUserRepository users, ITokenRepository tokens, ITokenCon
|
|||||||
|
|
||||||
public async Task<LogicResult<SingleValueResult<string>>> Register(UserRegister register) {
|
public async Task<LogicResult<SingleValueResult<string>>> Register(UserRegister register) {
|
||||||
if (register.Password.Length < 8)
|
if (register.Password.Length < 8)
|
||||||
return LogicResult<SingleValueResult<string>>.Conflict("Password needs to be at least 8 characters long");
|
return LogicResult<SingleValueResult<string>>.BadRequest("Password needs to be at least 8 characters long");
|
||||||
|
|
||||||
var allUsers = await users.GetUsers();
|
var allUsers = await users.GetUsers();
|
||||||
if (allUsers.Any(user => user.Username == register.Username || user.Email == register.Email))
|
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];
|
var refreshToken = accessor.HttpContext?.Request.Cookies[ITokenContext.RefreshTokenType];
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(refreshToken))
|
if (string.IsNullOrEmpty(refreshToken))
|
||||||
return LogicResult<SingleValueResult<string>>.Conflict("Refresh token not provided");
|
return LogicResult<SingleValueResult<string>>.BadRequest("Refresh token not provided");
|
||||||
|
|
||||||
var token = await tokens.GetToken(refreshToken);
|
var token = await tokens.GetToken(refreshToken);
|
||||||
|
|
||||||
if (token.Type != Token.RefreshTokenType)
|
|
||||||
return LogicResult<SingleValueResult<string>>.BadRequest("The provided token is not a refresh token");
|
|
||||||
|
|
||||||
if (token is null)
|
if (token is null)
|
||||||
return LogicResult<SingleValueResult<string>>.NotFound("Refresh token not valid");
|
return LogicResult<SingleValueResult<string>>.NotFound("Refresh token not valid");
|
||||||
|
|
||||||
|
if (token.Type != Token.RefreshTokenType)
|
||||||
|
return LogicResult<SingleValueResult<string>>.Conflict("The provided token is not a refresh token");
|
||||||
|
|
||||||
if (token.CreatedAt + HopFrameAuthentication.RefreshTokenTime < DateTime.Now)
|
if (token.CreatedAt + HopFrameAuthentication.RefreshTokenTime < DateTime.Now)
|
||||||
return LogicResult<SingleValueResult<string>>.Conflict("Refresh token is expired");
|
return LogicResult<SingleValueResult<string>>.Forbidden("Refresh token is expired");
|
||||||
|
|
||||||
var accessToken = await tokens.CreateToken(Token.AccessTokenType, token.Owner);
|
var accessToken = await tokens.CreateToken(Token.AccessTokenType, token.Owner);
|
||||||
|
|
||||||
|
|||||||
403
tests/HopFrame.Api.Tests/AuthLogicTests.cs
Normal file
403
tests/HopFrame.Api.Tests/AuthLogicTests.cs
Normal file
@@ -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.Api.Tests.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.Api.Tests;
|
||||||
|
|
||||||
|
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<IRequestCookieCollection>();
|
||||||
|
cookies
|
||||||
|
.SetupGet(c => c[ITokenContext.RefreshTokenType])
|
||||||
|
.Returns(providedTokenCookie);
|
||||||
|
accessor.HttpContext.Request.Cookies = cookies.Object;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (provideAccessToken) {
|
||||||
|
var claims = new List<Claim> {
|
||||||
|
new(HopFrameClaimTypes.AccessTokenId, _accessToken.ToString())
|
||||||
|
};
|
||||||
|
accessor.HttpContext.User.AddIdentity(new ClaimsIdentity(claims, HopFrameAuthentication.SchemeName));
|
||||||
|
}
|
||||||
|
|
||||||
|
var users = new Mock<IUserRepository>();
|
||||||
|
users
|
||||||
|
.Setup(u => u.GetUserByEmail(It.Is<string>(email => CreateDummyUser().Email == email)))
|
||||||
|
.ReturnsAsync(CreateDummyUser());
|
||||||
|
users
|
||||||
|
.Setup(u => u.CheckUserPassword(It.Is<User>(u => u.Email == CreateDummyUser().Email), It.IsAny<string>()))
|
||||||
|
.ReturnsAsync(passwordIsCorrect);
|
||||||
|
users
|
||||||
|
.Setup(u => u.AddUser(It.IsAny<User>()))
|
||||||
|
.ReturnsAsync(CreateDummyUser());
|
||||||
|
users
|
||||||
|
.Setup(u => u.GetUsers())
|
||||||
|
.ReturnsAsync(new List<User> { CreateDummyUser() });
|
||||||
|
|
||||||
|
var tokens = new Mock<ITokenRepository>();
|
||||||
|
tokens
|
||||||
|
.Setup(t => t.CreateToken(It.Is<int>(t => t == Token.RefreshTokenType), It.IsAny<User>()))
|
||||||
|
.ReturnsAsync(new Token {
|
||||||
|
Content = _refreshToken,
|
||||||
|
Type = Token.RefreshTokenType
|
||||||
|
});
|
||||||
|
tokens
|
||||||
|
.Setup(t => t.CreateToken(It.Is<int>(t => t == Token.AccessTokenType), It.IsAny<User>()))
|
||||||
|
.ReturnsAsync(new Token {
|
||||||
|
Content = _accessToken,
|
||||||
|
Type = Token.AccessTokenType
|
||||||
|
});
|
||||||
|
tokens
|
||||||
|
.Setup(t => t.GetToken(It.Is<string>(token => token == _refreshToken.ToString())))
|
||||||
|
.ReturnsAsync(providedRefreshToken);
|
||||||
|
|
||||||
|
var context = new Mock<ITokenContext>();
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
29
tests/HopFrame.Api.Tests/Extensions/HttpContextExtensions.cs
Normal file
29
tests/HopFrame.Api.Tests/Extensions/HttpContextExtensions.cs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
using System.Web;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
|
namespace HopFrame.Api.Tests.Extensions;
|
||||||
|
|
||||||
|
internal static class HttpContextExtensions {
|
||||||
|
/// <summary>Extracts the partial cookie value from the header section.</summary>
|
||||||
|
/// <param name="headers"><inheritdoc cref="IHeaderDictionary" path="/summary"/></param>
|
||||||
|
/// <param name="key">The key for identifying the cookie.</param>
|
||||||
|
/// <returns>The value of the cookie.</returns>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
28
tests/HopFrame.Api.Tests/HopFrame.Api.Tests.csproj
Normal file
28
tests/HopFrame.Api.Tests/HopFrame.Api.Tests.csproj
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>disable</Nullable>
|
||||||
|
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
<IsTestProject>true</IsTestProject>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="coverlet.collector" Version="6.0.0"/>
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0"/>
|
||||||
|
<PackageReference Include="Moq" Version="4.20.72" />
|
||||||
|
<PackageReference Include="xunit" Version="2.5.3"/>
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3"/>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Using Include="Xunit"/>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\src\HopFrame.Api\HopFrame.Api.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
Reference in New Issue
Block a user