Merged PR 78: Added roles and groups in authorization result
Added roles and groups in authorization resultmaster
parent
ebb0f4de62
commit
fdb08acd21
|
@ -1,7 +1,7 @@
|
|||
<Project>
|
||||
<Import Project="dependencies.props" />
|
||||
<PropertyGroup>
|
||||
<Version>2.4.1</Version>
|
||||
<Version>2.4.2</Version>
|
||||
<Authors>Tudor Stanciu</Authors>
|
||||
<Company>STA</Company>
|
||||
<PackageTags>Tuitio</PackageTags>
|
||||
|
|
|
@ -103,4 +103,15 @@
|
|||
◾ The authentication handler has been updated to skip the token validation if the method from controller is marked with [AllowAnonymous] attribute.
|
||||
</Content>
|
||||
</Note>
|
||||
<Note>
|
||||
<Version>2.4.2</Version>
|
||||
<Date>2023-04-08 01:48</Date>
|
||||
<Content>
|
||||
Added user roles and groups in authorization result
|
||||
◾ The authorization result will contain the user role and group codes. They are very useful for an application because after the token is authorized, the application can directly validate its internal permissions based on roles or groups, without calling another method to obtain this information.
|
||||
◾ In addition to these changes, some refactorings were also made.
|
||||
◾ The token "expires in" information measuring unit was changed from milliseconds to seconds.
|
||||
◾ New versions of nuget packages have been released.
|
||||
</Content>
|
||||
</Note>
|
||||
</ReleaseNotes>
|
|
@ -8,7 +8,7 @@ namespace Tuitio.Application.Extensions
|
|||
{
|
||||
internal static class EntityExtensions
|
||||
{
|
||||
public static UserRole[] GetUserRoles(this AppUser user)
|
||||
public static IEnumerable<UserRole> GetUserRoles(this AppUser user)
|
||||
{
|
||||
var roles = new List<UserRole>();
|
||||
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
// Copyright (c) 2020 Tudor Stanciu
|
||||
|
||||
using AutoMapper;
|
||||
using Tuitio.Application.Extensions;
|
||||
using Tuitio.Domain.Entities;
|
||||
using dto = Tuitio.PublishedLanguage.Dto;
|
||||
using models = Tuitio.Domain.Models;
|
||||
|
||||
|
@ -13,9 +11,6 @@ namespace Tuitio.Application.Mappings
|
|||
public MappingProfile()
|
||||
{
|
||||
CreateMap<models.Token, dto.AuthorizationResult>();
|
||||
CreateMap<AppUser, models.Token>()
|
||||
.ForMember(z => z.Claims, src => src.MapFrom(z => z.Claims.ToDictionary()));
|
||||
|
||||
CreateMap<models.Account.LogoutResult, dto.AccountLogoutResult>();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,5 @@ namespace Tuitio.Application.Services.Abstractions
|
|||
internal interface ITokenService
|
||||
{
|
||||
Token GenerateToken(AppUser user);
|
||||
string GenerateTokenRaw(Token token);
|
||||
Token ExtractToken(string tokenRaw);
|
||||
}
|
||||
}
|
|
@ -7,6 +7,7 @@ using System.Threading.Tasks;
|
|||
using Tuitio.Application.Services.Abstractions;
|
||||
using Tuitio.Application.Stores;
|
||||
using Tuitio.Domain.Entities;
|
||||
using Tuitio.Domain.Models;
|
||||
using Tuitio.Domain.Repositories;
|
||||
|
||||
namespace Tuitio.Application.Services
|
||||
|
@ -15,14 +16,12 @@ namespace Tuitio.Application.Services
|
|||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly ILogger<BehaviorService> _logger;
|
||||
private readonly ITokenService _tokenService;
|
||||
private readonly ITokenStore _securityStore;
|
||||
|
||||
public BehaviorService(IServiceProvider serviceProvider, ILogger<BehaviorService> logger, ITokenService tokenService, ITokenStore securityStore)
|
||||
public BehaviorService(IServiceProvider serviceProvider, ILogger<BehaviorService> logger, ITokenStore securityStore)
|
||||
{
|
||||
_serviceProvider=serviceProvider;
|
||||
_logger=logger;
|
||||
_tokenService=tokenService;
|
||||
_securityStore=securityStore;
|
||||
}
|
||||
|
||||
|
@ -44,7 +43,7 @@ namespace Tuitio.Application.Services
|
|||
_logger.LogInformation($"BehaviorService: {activeTokens.Length} active tokens were found in database.");
|
||||
foreach (var token in activeTokens)
|
||||
{
|
||||
var storeToken = _tokenService.ExtractToken(token.Token);
|
||||
var storeToken = Token.Import(token.Token);
|
||||
_securityStore.Set(token.Token, storeToken);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
// Copyright (c) 2020 Tudor Stanciu
|
||||
|
||||
using AutoMapper;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Linq;
|
||||
using Tuitio.Application.Extensions;
|
||||
using Tuitio.Application.Services.Abstractions;
|
||||
using Tuitio.Domain.Abstractions;
|
||||
using Tuitio.Domain.Entities;
|
||||
|
@ -14,63 +11,21 @@ namespace Tuitio.Application.Services
|
|||
{
|
||||
internal class TokenService : ITokenService
|
||||
{
|
||||
private readonly IMapper _mapper;
|
||||
private readonly IConfigProvider _configProvider;
|
||||
|
||||
public TokenService(IMapper mapper, IConfigProvider configProvider)
|
||||
public TokenService(IConfigProvider configProvider)
|
||||
{
|
||||
_mapper=mapper;
|
||||
_configProvider=configProvider;
|
||||
}
|
||||
|
||||
public Token GenerateToken(AppUser user)
|
||||
{
|
||||
var currentDate = DateTime.UtcNow;
|
||||
var validUntil = currentDate.AddMinutes(_configProvider.Token.ValidityInMinutes);
|
||||
|
||||
var token = _mapper.Map<Token>(user);
|
||||
token.TokenId = Guid.NewGuid();
|
||||
token.LockStamp = Regex.Replace(Convert.ToBase64String(Guid.NewGuid().ToByteArray()), "[/+=]", "");
|
||||
token.CreatedAt = currentDate;
|
||||
token.ExpiresIn = (validUntil - currentDate).TotalMilliseconds;
|
||||
var token = new Token(_configProvider.Token.ValidityInMinutes);
|
||||
var claims = user.Claims?.ToDictionary();
|
||||
var userRoles = user.GetUserRoles().Select(z => z.UserRoleCode);
|
||||
var userGroups = user.UserGroups?.Select(z => z.UserGroup.UserGroupCode);
|
||||
token.SetUserData(user.UserId, user.UserName, user.FirstName, user.LastName, user.Email, user.SecurityStamp, claims, userRoles, userGroups);
|
||||
return token;
|
||||
}
|
||||
|
||||
public string GenerateTokenRaw(Token token)
|
||||
{
|
||||
var tokenString = JsonConvert.SerializeObject(token);
|
||||
var tokenBytes = Encoding.UTF8.GetBytes(tokenString);
|
||||
var tokenRaw = Convert.ToBase64String(tokenBytes);
|
||||
return tokenRaw;
|
||||
}
|
||||
|
||||
public Token ExtractToken(string tokenRaw)
|
||||
{
|
||||
var valid = ValidateTokenRaw(tokenRaw);
|
||||
if (!valid)
|
||||
return null;
|
||||
|
||||
var tokenBytes = Convert.FromBase64String(tokenRaw);
|
||||
var tokenString = Encoding.UTF8.GetString(tokenBytes);
|
||||
var token = JsonConvert.DeserializeObject<Token>(tokenString);
|
||||
return token;
|
||||
}
|
||||
|
||||
private bool ValidateTokenRaw(string tokenRaw)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(tokenRaw))
|
||||
return false;
|
||||
|
||||
if (!StringIsBase64(tokenRaw))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool StringIsBase64(string str)
|
||||
{
|
||||
str = str.Trim();
|
||||
return (str.Length % 4 == 0) && Regex.IsMatch(str, @"^[a-zA-Z0-9+/]*={0,3}$", RegexOptions.None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,7 +43,7 @@ namespace Tuitio.Application.Services
|
|||
return null;
|
||||
|
||||
var token = _tokenService.GenerateToken(user);
|
||||
var raw = _tokenService.GenerateTokenRaw(token);
|
||||
var raw = token.Export();
|
||||
_securityStore.Set(raw, token);
|
||||
|
||||
await _userRepository.UpdateUserAfterLogin(user, token, raw);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
|
@ -11,7 +11,6 @@
|
|||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="$(MicrosoftExtensionsPackageVersion)" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="$(MicrosoftExtensionsPackageVersion)" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="$(MicrosoftExtensionsPackageVersion)" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="$(NewtonsoftJsonPackageVersion)" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
@ -25,6 +25,8 @@ namespace Tuitio.Domain.Data.Repositories
|
|||
return _dbContext.Users
|
||||
.Include(z => z.Status)
|
||||
.Include(z => z.Claims)
|
||||
.Include(z => z.UserRoles).ThenInclude(z => z.UserRole)
|
||||
.Include(z => z.UserGroups).ThenInclude(z => z.UserGroup).ThenInclude(z => z.GroupRoles).ThenInclude(z => z.UserRole)
|
||||
.FirstOrDefaultAsync(z => z.UserName == userName && z.Password == password);
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
// Copyright (c) 2020 Tudor Stanciu
|
||||
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Tuitio.Domain.Helpers
|
||||
{
|
||||
internal static class DataValidationHelper
|
||||
{
|
||||
public static bool StringIsBase64(string str)
|
||||
{
|
||||
str = str.Trim();
|
||||
return (str.Length % 4 == 0) && Regex.IsMatch(str, @"^[a-zA-Z0-9+/]*={0,3}$", RegexOptions.None);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +1,11 @@
|
|||
// Copyright (c) 2020 Tudor Stanciu
|
||||
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using Tuitio.Domain.Helpers;
|
||||
|
||||
namespace Tuitio.Domain.Models
|
||||
{
|
||||
|
@ -16,7 +20,68 @@ namespace Tuitio.Domain.Models
|
|||
public string SecurityStamp { get; set; }
|
||||
public string LockStamp { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public double ExpiresIn { get; set; }
|
||||
public long ExpiresIn { get; set; }
|
||||
public Dictionary<string, string> Claims { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public IEnumerable<string> UserRoles { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public IEnumerable<string> UserGroups { get; set; }
|
||||
|
||||
[Obsolete("This constructor is only used for deserialization and should not be used for any other purpose.")]
|
||||
public Token() { }
|
||||
|
||||
public Token(int validityInMinutes)
|
||||
{
|
||||
TokenId = Guid.NewGuid();
|
||||
CreatedAt = DateTime.UtcNow;
|
||||
LockStamp = Regex.Replace(Convert.ToBase64String(Guid.NewGuid().ToByteArray()), "[/+=]", "");
|
||||
ExpiresIn = validityInMinutes * 60; // seconds
|
||||
}
|
||||
|
||||
public void SetUserData(int userId, string userName, string firstName, string lastName, string email, string securityStamp, Dictionary<string, string> claims, IEnumerable<string> userRoles, IEnumerable<string> userGroups)
|
||||
{
|
||||
UserId = userId;
|
||||
UserName = userName;
|
||||
FirstName = firstName;
|
||||
LastName = lastName;
|
||||
Email = email;
|
||||
SecurityStamp = securityStamp;
|
||||
Claims = claims;
|
||||
UserRoles = userRoles;
|
||||
UserGroups = userGroups;
|
||||
}
|
||||
|
||||
public string Export()
|
||||
{
|
||||
var tokenString = JsonConvert.SerializeObject(this);
|
||||
var tokenBytes = Encoding.UTF8.GetBytes(tokenString);
|
||||
var tokenRaw = Convert.ToBase64String(tokenBytes);
|
||||
return tokenRaw;
|
||||
}
|
||||
|
||||
public static Token Import(string tokenRaw)
|
||||
{
|
||||
var valid = ValidateTokenRaw(tokenRaw);
|
||||
if (!valid)
|
||||
return null;
|
||||
|
||||
var tokenBytes = Convert.FromBase64String(tokenRaw);
|
||||
var tokenString = Encoding.UTF8.GetString(tokenBytes);
|
||||
var token = JsonConvert.DeserializeObject<Token>(tokenString);
|
||||
return token;
|
||||
}
|
||||
|
||||
private static bool ValidateTokenRaw(string tokenRaw)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(tokenRaw))
|
||||
return false;
|
||||
|
||||
if (!DataValidationHelper.StringIsBase64(tokenRaw))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="$(NewtonsoftJsonPackageVersion)" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
|
@ -18,7 +18,9 @@ namespace Tuitio.PublishedLanguage.Dto
|
|||
public string SecurityStamp { get; init; }
|
||||
public string LockStamp { get; init; }
|
||||
public DateTime CreatedAt { get; init; }
|
||||
public double ExpiresIn { get; init; }
|
||||
public long ExpiresIn { get; init; }
|
||||
public Dictionary<string, string> Claims { get; init; }
|
||||
public string[] UserRoles { get; init; }
|
||||
public string[] UserGroups { get; init; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
2.2.0 release [2023-03-27 19:20]
|
||||
2.2.1 release [2023-04-08 01:48]
|
||||
◾ Added user roles and groups in authorization result
|
||||
|
||||
2.2.0 release [2023-03-27 19:20]
|
||||
◾ Added "user-info" method in API
|
||||
|
||||
2.1.0 release [2023-03-07 22:17]
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<RepositoryUrl>https://lab.code-rove.com/gitea/tudor.stanciu/tuitio</RepositoryUrl>
|
||||
<RepositoryType>Git</RepositoryType>
|
||||
<PackageReleaseNotes>$([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/ReleaseNotes.txt"))</PackageReleaseNotes>
|
||||
<Version>2.2.0</Version>
|
||||
<Version>2.2.1</Version>
|
||||
<PackageIcon>logo.png</PackageIcon>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<Company>Toodle HomeLab</Company>
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
2.2.0 release [2023-03-27 19:20]
|
||||
2.2.1 release [2023-04-08 01:48]
|
||||
◾ Added user roles and groups in authorization result
|
||||
|
||||
2.2.0 release [2023-03-27 19:20]
|
||||
◾ Added "user-info" method in API
|
||||
|
||||
2.1.0 release [2023-03-07 22:17]
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<RepositoryUrl>https://lab.code-rove.com/gitea/tudor.stanciu/tuitio</RepositoryUrl>
|
||||
<RepositoryType>Git</RepositoryType>
|
||||
<PackageReleaseNotes>$([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/ReleaseNotes.txt"))</PackageReleaseNotes>
|
||||
<Version>2.2.0</Version>
|
||||
<Version>2.2.1</Version>
|
||||
<PackageIcon>logo.png</PackageIcon>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<Company>Toodle HomeLab</Company>
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
using MediatR;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Tuitio.Application.CommandHandlers;
|
||||
using Tuitio.Application.Tests.Fixtures;
|
||||
|
@ -132,6 +133,7 @@ namespace Tuitio.Application.Tests
|
|||
Assert.NotNull(authorizationResult.Result.LockStamp);
|
||||
Assert.True(authorizationResult.Result.TokenId != Guid.Empty, "Token id cannot be an empty guid.");
|
||||
Assert.True(authorizationResult.Result.ExpiresIn > 0, "Token expiration must be a positive number.");
|
||||
Assert.True(authorizationResult.Result.UserRoles.Any(), "User must have at least one role.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
@ -14,6 +14,7 @@ using System.IO;
|
|||
using System.Threading.Tasks;
|
||||
using Tuitio.Domain.Data;
|
||||
using Tuitio.Domain.Data.DbContexts;
|
||||
using Tuitio.Domain.Entities;
|
||||
using Xunit;
|
||||
|
||||
namespace Tuitio.Application.Tests.Fixtures
|
||||
|
@ -136,7 +137,21 @@ namespace Tuitio.Application.Tests.Fixtures
|
|||
SecurityStamp = "A93650FF-1FC4-4999-BAB6-3EEB174F6892",
|
||||
StatusId = 1,
|
||||
CreationDate = DateTime.Now,
|
||||
FailedLoginAttempts = 0
|
||||
FailedLoginAttempts = 0,
|
||||
UserRoles = new UserXUserRole[]
|
||||
{
|
||||
new UserXUserRole()
|
||||
{
|
||||
UserId = 1,
|
||||
UserRoleId = 1,
|
||||
UserRole = new UserRole()
|
||||
{
|
||||
UserRoleId = 1,
|
||||
UserRoleCode = "MOCK_ROLE",
|
||||
UserRoleName = "Mock role"
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
#endregion
|
||||
|
||||
|
|
|
@ -2,9 +2,11 @@
|
|||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System;
|
||||
using System.Text;
|
||||
using Tuitio.Application.Services.Abstractions;
|
||||
using Tuitio.Application.Tests.Fixtures;
|
||||
using Tuitio.Domain.Entities;
|
||||
using Tuitio.Domain.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace Tuitio.Application.Tests
|
||||
|
@ -24,7 +26,7 @@ namespace Tuitio.Application.Tests
|
|||
{
|
||||
var user = new AppUser()
|
||||
{
|
||||
UserId = 0,
|
||||
UserId = 1,
|
||||
UserName = "tuitio.test",
|
||||
Password = "9B8769A4A742959A2D0298C36FB70623F2DFACDA8436237DF08D8DFD5B37374C", //pass123
|
||||
Email = "tuitio.test@test.test",
|
||||
|
@ -33,7 +35,21 @@ namespace Tuitio.Application.Tests
|
|||
StatusId = 1,
|
||||
FailedLoginAttempts = 0,
|
||||
SecurityStamp = "A93650FF-1FC4-4999-BAB6-3EEB174F6892",
|
||||
CreationDate = DateTime.Now
|
||||
CreationDate = DateTime.Now,
|
||||
UserRoles = new UserXUserRole[]
|
||||
{
|
||||
new UserXUserRole()
|
||||
{
|
||||
UserId = 1,
|
||||
UserRoleId = 1,
|
||||
UserRole = new UserRole()
|
||||
{
|
||||
UserRoleId = 1,
|
||||
UserRoleCode = "MOCK_ROLE",
|
||||
UserRoleName = "Mock role"
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return user;
|
||||
|
@ -52,16 +68,22 @@ namespace Tuitio.Application.Tests
|
|||
|
||||
// Act
|
||||
var token = _tokenService.GenerateToken(_userMock);
|
||||
var raw = _tokenService.GenerateTokenRaw(token);
|
||||
var extracted = _tokenService.ExtractToken(raw);
|
||||
var raw = token.Export();
|
||||
var extracted = Token.Import(raw);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(token);
|
||||
Assert.NotNull(raw);
|
||||
Assert.NotNull(extracted);
|
||||
|
||||
Assert.True(_userMock.UserName == extracted.UserName);
|
||||
Assert.True(_userMock.SecurityStamp == extracted.SecurityStamp);
|
||||
Assert.Equal(_userMock.UserName, extracted.UserName);
|
||||
Assert.Equal(_userMock.FirstName, extracted.FirstName);
|
||||
Assert.Equal(_userMock.LastName, extracted.LastName);
|
||||
Assert.Equal(_userMock.Email, extracted.Email);
|
||||
Assert.Equal(_userMock.SecurityStamp, extracted.SecurityStamp);
|
||||
|
||||
var decodedTokenString = Encoding.UTF8.GetString(Convert.FromBase64String(raw));
|
||||
Assert.DoesNotContain("UserRoles", decodedTokenString);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,12 +9,20 @@ namespace Tuitio.Application.Tests
|
|||
{
|
||||
public class TokenStoreTests
|
||||
{
|
||||
private Token GetMockedToken()
|
||||
{
|
||||
var token = new Token(1);
|
||||
token.SetUserData(0, "test.tuitio", "tuitio", "user", "user.tuitio@lab.com", Guid.NewGuid().ToString(), null, null, null);
|
||||
return token;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Set_ShouldSetTokenInStore()
|
||||
{
|
||||
// Arrange
|
||||
var key = "user001";
|
||||
var expected = new Token() { TokenId = Guid.NewGuid(), UserId = 0, UserName = "test.tuitio", CreatedAt = DateTime.Now };
|
||||
var expected = GetMockedToken();
|
||||
|
||||
var store = new TokenStore();
|
||||
|
||||
// Act
|
||||
|
@ -30,7 +38,7 @@ namespace Tuitio.Application.Tests
|
|||
{
|
||||
// Arrange
|
||||
var key = "user001";
|
||||
var mock = new Token() { TokenId = Guid.NewGuid(), UserId = 0, UserName = "test.tuitio", CreatedAt = DateTime.Now };
|
||||
var mock = GetMockedToken();
|
||||
var store = new TokenStore();
|
||||
|
||||
// Act
|
||||
|
|
Loading…
Reference in New Issue