From a4d1d3227be2ceae7d48c37e91f55680582c85d8 Mon Sep 17 00:00:00 2001 From: Leon Hoppe Date: Mon, 9 Dec 2024 18:41:51 +0100 Subject: [PATCH] Created tests for HopFrame.Api --- HopFrame.sln | 7 + HopFrame.sln.DotSettings.user | 18 +- src/HopFrame.Api/HopFrame.Api.csproj | 6 + .../Logic/Implementation/AuthLogic.cs | 14 +- tests/HopFrame.Api.Tests/AuthLogicTests.cs | 403 ++++++++++++++++++ .../Extensions/HttpContextExtensions.cs | 29 ++ .../HopFrame.Api.Tests.csproj | 28 ++ 7 files changed, 493 insertions(+), 12 deletions(-) create mode 100644 tests/HopFrame.Api.Tests/AuthLogicTests.cs create mode 100644 tests/HopFrame.Api.Tests/Extensions/HttpContextExtensions.cs create mode 100644 tests/HopFrame.Api.Tests/HopFrame.Api.Tests.csproj diff --git a/HopFrame.sln b/HopFrame.sln index 94da369..b5779b1 100644 --- a/HopFrame.sln +++ b/HopFrame.sln @@ -24,6 +24,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Database.Tests", " EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Security.Tests", "tests\HopFrame.Security.Tests\HopFrame.Security.Tests.csproj", "{6747753A-6059-48F1-B779-D73765A373A6}" 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 GlobalSection(SolutionConfigurationPlatforms) = preSolution 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}.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 EndGlobalSection GlobalSection(NestedProjects) = preSolution {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} {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} EndGlobalSection EndGlobal diff --git a/HopFrame.sln.DotSettings.user b/HopFrame.sln.DotSettings.user index 4f1a50c..ea6ada4 100644 --- a/HopFrame.sln.DotSettings.user +++ b/HopFrame.sln.DotSettings.user @@ -2,6 +2,7 @@ ForceIncluded ForceIncluded ForceIncluded + ForceIncluded ForceIncluded ForceIncluded ForceIncluded @@ -16,11 +17,18 @@ - <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> + + + + + + + + + + + + diff --git a/src/HopFrame.Api/HopFrame.Api.csproj b/src/HopFrame.Api/HopFrame.Api.csproj index 744a466..c8b21c4 100644 --- a/src/HopFrame.Api/HopFrame.Api.csproj +++ b/src/HopFrame.Api/HopFrame.Api.csproj @@ -22,4 +22,10 @@ + + + <_Parameter1>HopFrame.Api.Tests + + + 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/tests/HopFrame.Api.Tests/AuthLogicTests.cs b/tests/HopFrame.Api.Tests/AuthLogicTests.cs new file mode 100644 index 0000000..ae82b65 --- /dev/null +++ b/tests/HopFrame.Api.Tests/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.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(); + 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.Api.Tests/Extensions/HttpContextExtensions.cs b/tests/HopFrame.Api.Tests/Extensions/HttpContextExtensions.cs new file mode 100644 index 0000000..4ccbb38 --- /dev/null +++ b/tests/HopFrame.Api.Tests/Extensions/HttpContextExtensions.cs @@ -0,0 +1,29 @@ +using System.Web; +using Microsoft.AspNetCore.Http; + +namespace HopFrame.Api.Tests.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.Api.Tests/HopFrame.Api.Tests.csproj b/tests/HopFrame.Api.Tests/HopFrame.Api.Tests.csproj new file mode 100644 index 0000000..6c7f59f --- /dev/null +++ b/tests/HopFrame.Api.Tests/HopFrame.Api.Tests.csproj @@ -0,0 +1,28 @@ + + + + net8.0 + enable + disable + + false + true + + + + + + + + + + + + + + + + + + +