Release/v2.1.0 #44
@@ -26,6 +26,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Security.Tests", "
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Api.Tests", "tests\HopFrame.Api.Tests\HopFrame.Api.Tests.csproj", "{25DE1510-47E5-46FF-89A4-B9F99542218E}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Web.Tests", "tests\HopFrame.Web.Tests\HopFrame.Web.Tests.csproj", "{566C13B9-4ECA-48C4-8D02-FEB6CDF523E6}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -72,6 +74,10 @@ Global
|
||||
{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}
|
||||
@@ -84,5 +90,6 @@ Global
|
||||
{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
|
||||
|
||||
@@ -35,4 +35,10 @@
|
||||
<None Include="README.md" Pack="true" PackagePath="\"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
|
||||
<_Parameter1>HopFrame.Web.Tests</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -91,15 +91,12 @@ internal class AuthService(
|
||||
}
|
||||
|
||||
public async Task<bool> IsLoggedIn() {
|
||||
var accessToken = httpAccessor.HttpContext?.Request.Cookies[ITokenContext.AccessTokenType];
|
||||
if (string.IsNullOrEmpty(accessToken)) return false;
|
||||
var accessToken = context.AccessToken;
|
||||
|
||||
var tokenEntry = await tokens.GetToken(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;
|
||||
}
|
||||
|
||||
94
tests/HopFrame.Web.Tests/AuthMiddlewareTests.cs
Normal file
94
tests/HopFrame.Web.Tests/AuthMiddlewareTests.cs
Normal file
@@ -0,0 +1,94 @@
|
||||
using System.Security.Claims;
|
||||
using HopFrame.Database.Models;
|
||||
using HopFrame.Database.Repositories;
|
||||
using HopFrame.Security.Claims;
|
||||
using HopFrame.Web.Services;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Moq;
|
||||
|
||||
namespace HopFrame.Web.Tests;
|
||||
|
||||
public class AuthMiddlewareTests {
|
||||
private readonly RequestDelegate _delegate = _ => Task.CompletedTask;
|
||||
|
||||
public AuthMiddleware SetupEnvironment(bool isLoggedIn = true, Token newToken = null) {
|
||||
var auth = new Mock<IAuthService>();
|
||||
auth
|
||||
.Setup(a => a.IsLoggedIn())
|
||||
.ReturnsAsync(isLoggedIn);
|
||||
auth
|
||||
.Setup(a => a.RefreshLogin())
|
||||
.ReturnsAsync(newToken);
|
||||
|
||||
var perms = new Mock<IPermissionRepository>();
|
||||
perms
|
||||
.Setup(p => p.GetFullPermissions(It.Is<User>(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<Permission> {
|
||||
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));
|
||||
}
|
||||
}
|
||||
334
tests/HopFrame.Web.Tests/AuthServiceTests.cs
Normal file
334
tests/HopFrame.Web.Tests/AuthServiceTests.cs
Normal file
@@ -0,0 +1,334 @@
|
||||
using HopFrame.Api.Tests.Extensions;
|
||||
using HopFrame.Database.Models;
|
||||
using HopFrame.Database.Repositories;
|
||||
using HopFrame.Security.Claims;
|
||||
using HopFrame.Security.Models;
|
||||
using HopFrame.Web.Services;
|
||||
using HopFrame.Web.Services.Implementation;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Moq;
|
||||
|
||||
namespace HopFrame.Web.Tests;
|
||||
|
||||
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<IRequestCookieCollection>();
|
||||
cookies
|
||||
.SetupGet(c => c[ITokenContext.RefreshTokenType])
|
||||
.Returns(providedTokenCookie);
|
||||
accessor.HttpContext.Request.Cookies = cookies.Object;
|
||||
}
|
||||
|
||||
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());
|
||||
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);
|
||||
}
|
||||
}
|
||||
29
tests/HopFrame.Web.Tests/Extensions/HttpContextExtensions.cs
Normal file
29
tests/HopFrame.Web.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.Web.Tests/HopFrame.Web.Tests.csproj
Normal file
28
tests/HopFrame.Web.Tests/HopFrame.Web.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.Web\HopFrame.Web.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user