Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions src/Orbit.Api/Authorization/AdminAuthorizationHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using Orbit.Domain.Entities;
using Orbit.Domain.Interfaces;

namespace Orbit.Api.Authorization;

/// <summary>
/// Marker requirement for the admin authorization boundary; satisfied by <see cref="AdminAuthorizationHandler"/>.
/// </summary>
public sealed class AdminRequirement : IAuthorizationRequirement;

/// <summary>
/// Authorizes admin-only endpoints by reading <c>User.IsAdmin</c> live from the database on every request,
/// so revoking admin in the DB takes effect immediately rather than lingering until the caller's token expires.
/// The JWT carries identity only; it never asserts admin.
/// </summary>
public sealed class AdminAuthorizationHandler(IGenericRepository<User> userRepository)
: AuthorizationHandler<AdminRequirement>
{
protected override async Task HandleRequirementAsync(
AuthorizationHandlerContext context, AdminRequirement requirement)
{
if (!Guid.TryParse(context.User.FindFirstValue(ClaimTypes.NameIdentifier), out var userId))
return;

if (await userRepository.AnyAsync(user => user.Id == userId && user.IsAdmin))
context.Succeed(requirement);
}
}
5 changes: 4 additions & 1 deletion src/Orbit.Api/Extensions/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
using FluentValidation;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using Sentry;
using Sentry.AspNetCore;
using Orbit.Api.Authentication;
using Orbit.Api.Authorization;
using Orbit.Api.OAuth;
using Orbit.Application.Behaviors;
using Orbit.Application.Common;
Expand Down Expand Up @@ -157,8 +159,9 @@ public static WebApplicationBuilder AddOrbitAuthentication(this WebApplicationBu
builder.Services.AddAuthorization(options =>
{
options.AddPolicy(AdminPolicy.Name, policy =>
policy.RequireClaim(AdminPolicy.ClaimType, AdminPolicy.ClaimValue));
policy.Requirements.Add(new AdminRequirement()));
});
builder.Services.AddScoped<IAuthorizationHandler, AdminAuthorizationHandler>();

return builder;
}
Expand Down
2 changes: 1 addition & 1 deletion src/Orbit.Application/Auth/Commands/GoogleAuthCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
if (wasReactivated || request.GoogleAccessToken is not null)
await unitOfWork.SaveChangesAsync(cancellationToken);

var sessionResult = await authSessionService.CreateSessionAsync(user.Id, user.Email, user.IsAdmin, cancellationToken);
var sessionResult = await authSessionService.CreateSessionAsync(user.Id, user.Email, cancellationToken);
if (sessionResult.IsFailure)
return sessionResult.PropagateError<LoginResponse>();

Expand Down Expand Up @@ -152,7 +152,7 @@
}

private bool HandlePostLogin(
User user, GoogleAuthCommand request, bool isNewUser, CancellationToken cancellationToken)

Check warning on line 155 in src/Orbit.Application/Auth/Commands/GoogleAuthCommand.cs

View workflow job for this annotation

GitHub Actions / SonarCloud Analysis

Remove this unused method parameter 'cancellationToken'.
{
if (isNewUser && !string.IsNullOrWhiteSpace(request.ReferralCode))
ProcessReferralInBackground(user.Id, request.ReferralCode);
Expand Down
2 changes: 1 addition & 1 deletion src/Orbit.Application/Auth/Commands/VerifyCodeCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public async Task<Result<LoginResponse>> Handle(VerifyCodeCommand request, Cance

var wasReactivated = await HandlePostLoginAsync(user, isNewUser, request, cancellationToken);

var sessionResult = await authSessionService.CreateSessionAsync(user.Id, user.Email, user.IsAdmin, cancellationToken);
var sessionResult = await authSessionService.CreateSessionAsync(user.Id, user.Email, cancellationToken);
if (sessionResult.IsFailure)
return sessionResult.PropagateError<LoginResponse>();

Expand Down
10 changes: 4 additions & 6 deletions src/Orbit.Application/Common/AdminPolicy.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
namespace Orbit.Application.Common;

/// <summary>
/// Names the single administrative authorization boundary. A user's IsAdmin flag mints the
/// admin claim into the JWT; the "Admin" policy (RequireClaim on that claim) gates admin-only
/// endpoints. The first admin is granted by a direct DB update (Users.IsAdmin = true); there is
/// no email-based gate.
/// Names the single administrative authorization boundary. The "Admin" policy reads
/// <c>User.IsAdmin</c> live from the database on every request, so revoking admin takes effect
/// immediately; the JWT carries identity only and never asserts admin. The first admin is granted
/// by a direct DB update (Users.IsAdmin = true).
/// </summary>
public static class AdminPolicy
{
public const string Name = "Admin";
public const string ClaimType = "admin";
public const string ClaimValue = "true";
}
2 changes: 1 addition & 1 deletion src/Orbit.Domain/Interfaces/IAuthSessionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace Orbit.Domain.Interfaces;

public interface IAuthSessionService
{
Task<Result<SessionTokens>> CreateSessionAsync(Guid userId, string email, bool isAdmin, CancellationToken cancellationToken = default);
Task<Result<SessionTokens>> CreateSessionAsync(Guid userId, string email, CancellationToken cancellationToken = default);
Task<Result<SessionTokens>> RefreshSessionAsync(string refreshToken, CancellationToken cancellationToken = default);
Task<Result> RevokeSessionAsync(string refreshToken, CancellationToken cancellationToken = default);
}
2 changes: 1 addition & 1 deletion src/Orbit.Domain/Interfaces/ITokenService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ namespace Orbit.Domain.Interfaces;

public interface ITokenService
{
string GenerateToken(Guid userId, string email, bool isAdmin);
string GenerateToken(Guid userId, string email);
}
6 changes: 3 additions & 3 deletions src/Orbit.Infrastructure/Services/AuthSessionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public class AuthSessionService(
? nowUtc.AddDays(_jwtSettings.RefreshExpiryDays.Value)
: null;

public async Task<Result<SessionTokens>> CreateSessionAsync(Guid userId, string email, bool isAdmin, CancellationToken cancellationToken = default)
public async Task<Result<SessionTokens>> CreateSessionAsync(Guid userId, string email, CancellationToken cancellationToken = default)
{
var refreshToken = GenerateRefreshToken();
var nowUtc = DateTime.UtcNow;
Expand All @@ -40,7 +40,7 @@ public async Task<Result<SessionTokens>> CreateSessionAsync(Guid userId, string
await unitOfWork.SaveChangesAsync(cancellationToken);

return Result.Success(new SessionTokens(
tokenService.GenerateToken(userId, email, isAdmin),
tokenService.GenerateToken(userId, email),
refreshToken));
}

Expand Down Expand Up @@ -69,7 +69,7 @@ public async Task<Result<SessionTokens>> RefreshSessionAsync(string refreshToken
await unitOfWork.SaveChangesAsync(cancellationToken);

return Result.Success(new SessionTokens(
tokenService.GenerateToken(user.Id, user.Email, user.IsAdmin),
tokenService.GenerateToken(user.Id, user.Email),
newRefreshToken));
}

Expand Down
5 changes: 1 addition & 4 deletions src/Orbit.Infrastructure/Services/JwtTokenService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public class JwtTokenService(IOptions<JwtSettings> options) : ITokenService
{
private readonly JwtSettings _settings = options.Value;

public string GenerateToken(Guid userId, string email, bool isAdmin)
public string GenerateToken(Guid userId, string email)
{
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_settings.SecretKey));

Expand All @@ -24,9 +24,6 @@ public string GenerateToken(Guid userId, string email, bool isAdmin)
new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
};

if (isAdmin)
claims.Add(new Claim(AdminPolicy.ClaimType, AdminPolicy.ClaimValue));

var descriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims),
Expand Down
7 changes: 5 additions & 2 deletions src/Orbit.Infrastructure/Services/ResendEmailService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,13 @@

var tokens = new Dictionary<string, string>
{
["heading"] = copy.Heading,

Check warning on line 47 in src/Orbit.Infrastructure/Services/ResendEmailService.cs

View workflow job for this annotation

GitHub Actions / SonarCloud Analysis

Define a constant instead of using this literal 'heading' 4 times.
["intro"] = copy.Intro,

Check warning on line 48 in src/Orbit.Infrastructure/Services/ResendEmailService.cs

View workflow job for this annotation

GitHub Actions / SonarCloud Analysis

Define a constant instead of using this literal 'intro' 4 times.
["code"] = code,
["cta"] = copy.Cta,
["signInUrl"] = signInUrl,
["warning"] = copy.Warning,
["footer"] = copy.Footer,

Check warning on line 53 in src/Orbit.Infrastructure/Services/ResendEmailService.cs

View workflow job for this annotation

GitHub Actions / SonarCloud Analysis

Define a constant instead of using this literal 'footer' 4 times.
};

var layout = new EmailLayout(LangCode(isPtBr), copy.Preheader, copy.Footer, LogoUrl, GradientHeader: false);
Expand Down Expand Up @@ -110,7 +110,10 @@
var isPtBr = LocaleHelper.IsPortuguese(language);
var footer = MarketingFooterHtml(isPtBr, unsubscribeUrl);
var layout = new EmailLayout(LangCode(isPtBr), Preheader: "", footer, LogoUrl, GradientHeader: true);
var html = EmailTemplateRenderer.RenderLayout(layout, bodyHtml);
var readableBody =
"<div style=\"font-family: Rubik, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; " +
$"font-size: 16px; line-height: 1.6; color: #E2E8F0;\">{bodyHtml}</div>";
var html = EmailTemplateRenderer.RenderLayout(layout, readableBody);

var payload = new
{
Expand All @@ -131,7 +134,7 @@
private static string MarketingFooterHtml(bool isPtBr, string unsubscribeUrl)
{
const string legalIdentity =
"TL SOFTWARE ENGINEERING LTDA · CNPJ 58.429.979/0001-06 · Av. Nova Independência, 651Brooklin Paulista, São PauloSP · CEP 04570-001";
"TL SOFTWARE ENGINEERING LTDA · CNPJ 58.429.979/0001-06 · Av. Nova Independência, 651, Brooklin Paulista, São Paulo/SP · CEP 04570-001";

var (reason, unsubscribeLabel) = isPtBr
? ("Você está recebendo este e-mail porque optou por receber novidades do Orbit.", "Cancelar inscrição")
Expand All @@ -142,7 +145,7 @@
$"<a href=\"{encodedUrl}\" style=\"color: #90A1B9; text-decoration: underline;\">{unsubscribeLabel}</a>";
}

private async Task SendMarketingWithBackoffAsync(string to, string subject, string serializedPayload, CancellationToken cancellationToken)

Check warning on line 148 in src/Orbit.Infrastructure/Services/ResendEmailService.cs

View workflow job for this annotation

GitHub Actions / SonarCloud Analysis

Refactor this method to reduce its Cognitive Complexity from 22 to the 15 allowed.
{
if (IsTestAccount(to))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ public async Task VerifyCode_Valid_ReturnsLoginResponse()
var user = User.Create("Test", TestEmail).Value;
SetupCacheWithCode("123456");
SetupExistingUser(user);
_authSessionService.CreateSessionAsync(Arg.Any<Guid>(), Arg.Any<string>(), Arg.Any<bool>(), Arg.Any<CancellationToken>())
_authSessionService.CreateSessionAsync(Arg.Any<Guid>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
.Returns(Result.Success(new SessionTokens("jwt-token", "refresh-token")));

var handler = new VerifyCodeCommandHandler(_cache, _userRepo, _unitOfWork, _authSessionService, _emailService, Substitute.For<MediatR.IMediator>(), Substitute.For<ILogger<VerifyCodeCommandHandler>>());
Expand Down Expand Up @@ -111,7 +111,7 @@ public async Task VerifyCode_MaxAttempts_ReturnsFailure()
public async Task VerifyCode_NewUser_CreatesAccount()
{
SetupCacheWithCode("123456");
_authSessionService.CreateSessionAsync(Arg.Any<Guid>(), Arg.Any<string>(), Arg.Any<bool>(), Arg.Any<CancellationToken>())
_authSessionService.CreateSessionAsync(Arg.Any<Guid>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
.Returns(Result.Success(new SessionTokens("jwt-token", "refresh-token")));

var handler = new VerifyCodeCommandHandler(_cache, _userRepo, _unitOfWork, _authSessionService, _emailService, Substitute.For<MediatR.IMediator>(), Substitute.For<ILogger<VerifyCodeCommandHandler>>());
Expand All @@ -130,7 +130,7 @@ public async Task VerifyCode_ExistingUser_ReturnsToken()
var user = User.Create("Existing", TestEmail).Value;
SetupCacheWithCode("123456");
SetupExistingUser(user);
_authSessionService.CreateSessionAsync(Arg.Any<Guid>(), Arg.Any<string>(), Arg.Any<bool>(), Arg.Any<CancellationToken>())
_authSessionService.CreateSessionAsync(Arg.Any<Guid>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
.Returns(Result.Success(new SessionTokens("jwt-token", "refresh-token")));

var handler = new VerifyCodeCommandHandler(_cache, _userRepo, _unitOfWork, _authSessionService, _emailService, Substitute.For<MediatR.IMediator>(), Substitute.For<ILogger<VerifyCodeCommandHandler>>());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public GoogleAuthCommandHandlerTests()
Substitute.For<Microsoft.Extensions.DependencyInjection.IServiceScopeFactory>(),
Substitute.For<ILogger<GoogleAuthCommandHandler>>());

_authSessionService.CreateSessionAsync(Arg.Any<Guid>(), Arg.Any<string>(), Arg.Any<bool>(), Arg.Any<CancellationToken>())
_authSessionService.CreateSessionAsync(Arg.Any<Guid>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
.Returns(Result.Success(new SessionTokens("jwt-token", "refresh-token")));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ private VerifyCodeCommandHandler BuildVerifyHandler()
var authSessionService = Substitute.For<IAuthSessionService>();
var mediator = Substitute.For<IMediator>();

authSessionService.CreateSessionAsync(Arg.Any<Guid>(), Arg.Any<string>(), Arg.Any<bool>(), Arg.Any<CancellationToken>())
authSessionService.CreateSessionAsync(Arg.Any<Guid>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
.Returns(Result.Success(new SessionTokens("jwt-token", "refresh-token")));

return new VerifyCodeCommandHandler(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public VerifyCodeCommandHandlerTests()
_cache, _userRepo, _unitOfWork, _authSessionService, _emailService, _mediator,
Substitute.For<ILogger<VerifyCodeCommandHandler>>());

_authSessionService.CreateSessionAsync(Arg.Any<Guid>(), Arg.Any<string>(), Arg.Any<bool>(), Arg.Any<CancellationToken>())
_authSessionService.CreateSessionAsync(Arg.Any<Guid>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
.Returns(Result.Success(new SessionTokens("jwt-token", "refresh-token")));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,14 +66,14 @@ public AuthSessionServiceTests()
.Returns(_user);

_tokenService
.GenerateToken(Arg.Any<Guid>(), Arg.Any<string>(), Arg.Any<bool>())
.GenerateToken(Arg.Any<Guid>(), Arg.Any<string>())
.Returns("access-token-1", "access-token-2", "access-token-3");
}

[Fact]
public async Task CreateSessionAsync_WithNullRefreshExpiry_CreatesNonExpiringSession()
{
var result = await _service.CreateSessionAsync(_user.Id, _user.Email, false, CancellationToken.None);
var result = await _service.CreateSessionAsync(_user.Id, _user.Email, CancellationToken.None);

result.IsSuccess.Should().BeTrue();
result.Value.AccessToken.Should().Be("access-token-1");
Expand All @@ -86,7 +86,7 @@ public async Task CreateSessionAsync_WithNullRefreshExpiry_CreatesNonExpiringSes
[Fact]
public async Task RefreshSessionAsync_WithNonExpiringSession_RotatesRefreshTokenWithoutAddingExpiry()
{
var createResult = await _service.CreateSessionAsync(_user.Id, _user.Email, false, CancellationToken.None);
var createResult = await _service.CreateSessionAsync(_user.Id, _user.Email, CancellationToken.None);
var originalTokenHash = _storedSession!.TokenHash;
var originalLastUsedAtUtc = _storedSession.LastUsedAtUtc;

Expand All @@ -106,7 +106,7 @@ public async Task RefreshSessionAsync_WithNonExpiringSession_RotatesRefreshToken
[Fact]
public async Task RevokeSessionAsync_RevokesStoredSession()
{
var createResult = await _service.CreateSessionAsync(_user.Id, _user.Email, false, CancellationToken.None);
var createResult = await _service.CreateSessionAsync(_user.Id, _user.Email, CancellationToken.None);

var revokeResult = await _service.RevokeSessionAsync(createResult.Value.RefreshToken, CancellationToken.None);

Expand All @@ -117,7 +117,7 @@ public async Task RevokeSessionAsync_RevokesStoredSession()
[Fact]
public async Task RefreshSessionAsync_WithRevokedSession_ReturnsFailure()
{
var createResult = await _service.CreateSessionAsync(_user.Id, _user.Email, false, CancellationToken.None);
var createResult = await _service.CreateSessionAsync(_user.Id, _user.Email, CancellationToken.None);
_storedSession!.Revoke(DateTime.UtcNow);

var refreshResult = await _service.RefreshSessionAsync(createResult.Value.RefreshToken, CancellationToken.None);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,46 +1,75 @@
using System.Linq.Expressions;
using System.Security.Claims;
using FluentAssertions;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.DependencyInjection;
using Orbit.Application.Common;
using NSubstitute;
using Orbit.Api.Authorization;
using Orbit.Domain.Entities;
using Orbit.Domain.Interfaces;

namespace Orbit.Infrastructure.Tests.Authorization;

public class AdminAuthorizationPolicyTests
{
private static IAuthorizationService BuildAuthorizationService()
private readonly IGenericRepository<User> _userRepository = Substitute.For<IGenericRepository<User>>();
private readonly AdminAuthorizationHandler _handler;

public AdminAuthorizationPolicyTests()
{
_handler = new AdminAuthorizationHandler(_userRepository);
}

[Fact]
public async Task AdminUser_Succeeds()
{
var services = new ServiceCollection();
services.AddLogging();
services.AddAuthorization(options =>
options.AddPolicy(AdminPolicy.Name, policy =>
policy.RequireClaim(AdminPolicy.ClaimType, AdminPolicy.ClaimValue)));
_userRepository.AnyAsync(Arg.Any<Expression<Func<User, bool>>>(), Arg.Any<CancellationToken>())
.Returns(true);
var context = ContextFor(Principal(new Claim(ClaimTypes.NameIdentifier, Guid.NewGuid().ToString())));

return services.BuildServiceProvider().GetRequiredService<IAuthorizationService>();
await _handler.HandleAsync(context);

context.HasSucceeded.Should().BeTrue();
}

private static ClaimsPrincipal Principal(params Claim[] claims) =>
new(new ClaimsIdentity(claims, authenticationType: "Test"));
[Fact]
public async Task AuthenticatedNonAdmin_DoesNotSucceed()
{
_userRepository.AnyAsync(Arg.Any<Expression<Func<User, bool>>>(), Arg.Any<CancellationToken>())
.Returns(false);
var context = ContextFor(Principal(new Claim(ClaimTypes.NameIdentifier, Guid.NewGuid().ToString())));

await _handler.HandleAsync(context);

context.HasSucceeded.Should().BeFalse();
}

[Fact]
public async Task AdminClaim_IsAllowed()
public async Task MissingNameIdentifier_DoesNotSucceed_AndSkipsRepository()
{
var principal = Principal(
new Claim(ClaimTypes.NameIdentifier, Guid.NewGuid().ToString()),
new Claim(AdminPolicy.ClaimType, AdminPolicy.ClaimValue));
var context = ContextFor(Principal());

var result = await BuildAuthorizationService().AuthorizeAsync(principal, resource: null, AdminPolicy.Name);
await _handler.HandleAsync(context);

result.Succeeded.Should().BeTrue();
context.HasSucceeded.Should().BeFalse();
await _userRepository.DidNotReceive()
.AnyAsync(Arg.Any<Expression<Func<User, bool>>>(), Arg.Any<CancellationToken>());
}

[Fact]
public async Task AuthenticatedNonAdmin_IsDenied()
public async Task UnparseableNameIdentifier_DoesNotSucceed_AndSkipsRepository()
{
var principal = Principal(new Claim(ClaimTypes.NameIdentifier, Guid.NewGuid().ToString()));
var context = ContextFor(Principal(new Claim(ClaimTypes.NameIdentifier, "not-a-guid")));

var result = await BuildAuthorizationService().AuthorizeAsync(principal, resource: null, AdminPolicy.Name);
await _handler.HandleAsync(context);

result.Succeeded.Should().BeFalse();
context.HasSucceeded.Should().BeFalse();
await _userRepository.DidNotReceive()
.AnyAsync(Arg.Any<Expression<Func<User, bool>>>(), Arg.Any<CancellationToken>());
}

private static ClaimsPrincipal Principal(params Claim[] claims) =>
new(new ClaimsIdentity(claims, authenticationType: "Test"));

private static AuthorizationHandlerContext ContextFor(ClaimsPrincipal principal) =>
new([new AdminRequirement()], principal, resource: null);
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public class AuthSessionServiceTests

public AuthSessionServiceTests()
{
_tokenService.GenerateToken(Arg.Any<Guid>(), Arg.Any<string>(), Arg.Any<bool>()).Returns("access-token");
_tokenService.GenerateToken(Arg.Any<Guid>(), Arg.Any<string>()).Returns("access-token");

_sut = new AuthSessionService(
_userSessionRepository,
Expand All @@ -42,7 +42,7 @@ public async Task CreateSessionAsync_AddsPersistedSessionAndReturnsTokens()
{
var userId = Guid.NewGuid();

var result = await _sut.CreateSessionAsync(userId, "thomas@test.com", false, CancellationToken.None);
var result = await _sut.CreateSessionAsync(userId, "thomas@test.com", CancellationToken.None);

result.IsSuccess.Should().BeTrue();
result.Value.AccessToken.Should().Be("access-token");
Expand All @@ -59,7 +59,7 @@ await _userSessionRepository.AddAsync(
Arg.Do<UserSession>(session => captured = session),
Arg.Any<CancellationToken>());

await _sut.CreateSessionAsync(Guid.NewGuid(), "thomas@test.com", false, CancellationToken.None);
await _sut.CreateSessionAsync(Guid.NewGuid(), "thomas@test.com", CancellationToken.None);

captured.Should().NotBeNull();
captured!.ExpiresAtUtc.Should().NotBeNull();
Expand Down
Loading
Loading