Refactor nunit tests off EF InMemory provider and web server
Extract IMicCheckDbContext so DB access can be mocked with Moq instead of the InMemory provider, per project testing guidelines. Add shared async DbSet test doubles (TestSupport/) to back the mocks. Rewrite HTTP-pipeline tests (WebApplicationFactory) to exercise controllers and auth handlers directly instead of running the web server. Drop DbContext/seeder tests that only made sense against a real provider, and remove stale duplicate test files left over from a namespace move.
This commit is contained in:
@@ -1,142 +1,120 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using MicCheck.Api.Common.Security.ApiKeys;
|
||||
using MicCheck.Api.Data;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace MicCheck.Api.Tests.Unit.Common.Security.Authentication;
|
||||
|
||||
[TestFixture]
|
||||
public class ApiKeyAuthenticationHandlerTests
|
||||
{
|
||||
private MicCheckWebApplicationFactory _factory = null!;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
_factory = new MicCheckWebApplicationFactory();
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void TearDown() => _factory.Dispose();
|
||||
|
||||
private async Task<string> SeedActiveApiKeyAsync(int organizationId = 1)
|
||||
{
|
||||
using var scope = _factory.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<MicCheckDbContext>();
|
||||
|
||||
var rawKey = ApiKeyHasher.GenerateKey();
|
||||
db.ApiKeys.Add(new ApiKey
|
||||
{
|
||||
Key = ApiKeyHasher.Hash(rawKey),
|
||||
Prefix = rawKey[..8],
|
||||
Name = "Test Key",
|
||||
OrganizationId = organizationId,
|
||||
IsActive = true,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return rawKey;
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenAuthorizationHeaderIsAbsent_ThenUnauthorizedIsReturned()
|
||||
{
|
||||
var client = _factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync("/api/v1/organisation/1/api-keys/");
|
||||
|
||||
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenAuthorizationHeaderIsNotApiKeyScheme_ThenUnauthorizedIsReturned()
|
||||
{
|
||||
var client = _factory.CreateClient();
|
||||
client.DefaultRequestHeaders.Authorization =
|
||||
new AuthenticationHeaderValue("Bearer", "not-a-jwt");
|
||||
|
||||
var response = await client.GetAsync("/api/v1/organisation/1/api-keys/");
|
||||
|
||||
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenApiKeyIsInvalid_ThenUnauthorizedIsReturned()
|
||||
{
|
||||
var client = _factory.CreateClient();
|
||||
client.DefaultRequestHeaders.Add("Authorization", "Api-Key not-a-real-key");
|
||||
|
||||
var response = await client.GetAsync("/api/v1/organisation/1/api-keys/");
|
||||
|
||||
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenApiKeyIsInactive_ThenUnauthorizedIsReturned()
|
||||
{
|
||||
using var scope = _factory.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<MicCheckDbContext>();
|
||||
|
||||
var rawKey = ApiKeyHasher.GenerateKey();
|
||||
db.ApiKeys.Add(new ApiKey
|
||||
{
|
||||
Key = ApiKeyHasher.Hash(rawKey),
|
||||
Prefix = rawKey[..8],
|
||||
Name = "Inactive Key",
|
||||
OrganizationId = 1,
|
||||
IsActive = false,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var client = _factory.CreateClient();
|
||||
client.DefaultRequestHeaders.Add("Authorization", $"Api-Key {rawKey}");
|
||||
|
||||
var response = await client.GetAsync("/api/v1/organisation/1/api-keys/");
|
||||
|
||||
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenApiKeyIsExpired_ThenUnauthorizedIsReturned()
|
||||
{
|
||||
using var scope = _factory.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<MicCheckDbContext>();
|
||||
|
||||
var rawKey = ApiKeyHasher.GenerateKey();
|
||||
db.ApiKeys.Add(new ApiKey
|
||||
{
|
||||
Key = ApiKeyHasher.Hash(rawKey),
|
||||
Prefix = rawKey[..8],
|
||||
Name = "Expired Key",
|
||||
OrganizationId = 1,
|
||||
IsActive = true,
|
||||
ExpiresAt = DateTimeOffset.UtcNow.AddDays(-1),
|
||||
CreatedAt = DateTimeOffset.UtcNow.AddDays(-10)
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var client = _factory.CreateClient();
|
||||
client.DefaultRequestHeaders.Add("Authorization", $"Api-Key {rawKey}");
|
||||
|
||||
var response = await client.GetAsync("/api/v1/organisation/1/api-keys/");
|
||||
|
||||
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenApiKeyIsValid_ThenOkIsReturned()
|
||||
{
|
||||
var rawKey = await SeedActiveApiKeyAsync(organizationId: 1);
|
||||
|
||||
var client = _factory.CreateClient();
|
||||
client.DefaultRequestHeaders.Add("Authorization", $"Api-Key {rawKey}");
|
||||
|
||||
var response = await client.GetAsync("/api/v1/organisation/1/api-keys/");
|
||||
|
||||
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
|
||||
}
|
||||
}
|
||||
using System.Text.Encodings.Web;
|
||||
using MicCheck.Api.Common.Security.ApiKeys;
|
||||
using MicCheck.Api.Common.Security.Authentication;
|
||||
using MicCheck.Api.Data;
|
||||
using MicCheck.Api.Tests.Unit.TestSupport;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace MicCheck.Api.Tests.Unit.Common.Security.Authentication;
|
||||
|
||||
[TestFixture]
|
||||
public class ApiKeyAuthenticationHandlerTests
|
||||
{
|
||||
private List<ApiKey> _apiKeys = null!;
|
||||
private Mock<IMicCheckDbContext> _db = null!;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
_db = new Mock<IMicCheckDbContext>();
|
||||
_apiKeys = [];
|
||||
_db.SetupDbSet(c => c.ApiKeys, _apiKeys);
|
||||
}
|
||||
|
||||
private async Task<AuthenticateResult> AuthenticateAsync(string? authorizationHeader)
|
||||
{
|
||||
var optionsMonitor = new Mock<IOptionsMonitor<AuthenticationSchemeOptions>>();
|
||||
optionsMonitor.Setup(o => o.Get(It.IsAny<string>())).Returns(new AuthenticationSchemeOptions());
|
||||
|
||||
var handler = new ApiKeyAuthenticationHandler(optionsMonitor.Object, NullLoggerFactory.Instance, UrlEncoder.Default, _db.Object);
|
||||
|
||||
var httpContext = new DefaultHttpContext();
|
||||
if (authorizationHeader is not null)
|
||||
httpContext.Request.Headers.Authorization = authorizationHeader;
|
||||
|
||||
var scheme = new AuthenticationScheme(ApiKeyAuthenticationHandler.SchemeName, null, typeof(ApiKeyAuthenticationHandler));
|
||||
await handler.InitializeAsync(scheme, httpContext);
|
||||
|
||||
return await handler.AuthenticateAsync();
|
||||
}
|
||||
|
||||
private ApiKey AddApiKey(string rawKey, int organizationId = 1, bool isActive = true, DateTimeOffset? expiresAt = null)
|
||||
{
|
||||
var apiKey = new ApiKey
|
||||
{
|
||||
Key = ApiKeyHasher.Hash(rawKey),
|
||||
Prefix = rawKey[..8],
|
||||
Name = "Test Key",
|
||||
OrganizationId = organizationId,
|
||||
IsActive = isActive,
|
||||
ExpiresAt = expiresAt,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
_apiKeys.Add(apiKey);
|
||||
return apiKey;
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenAuthorizationHeaderIsAbsent_ThenNoResultIsReturned()
|
||||
{
|
||||
var result = await AuthenticateAsync(null);
|
||||
|
||||
Assert.That(result.None, Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenAuthorizationHeaderIsNotApiKeyScheme_ThenNoResultIsReturned()
|
||||
{
|
||||
var result = await AuthenticateAsync("Bearer not-a-jwt");
|
||||
|
||||
Assert.That(result.None, Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenApiKeyIsInvalid_ThenAuthenticationFails()
|
||||
{
|
||||
var result = await AuthenticateAsync("Api-Key not-a-real-key");
|
||||
|
||||
Assert.That(result.Succeeded, Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenApiKeyIsInactive_ThenAuthenticationFails()
|
||||
{
|
||||
var rawKey = ApiKeyHasher.GenerateKey();
|
||||
AddApiKey(rawKey, isActive: false);
|
||||
|
||||
var result = await AuthenticateAsync($"Api-Key {rawKey}");
|
||||
|
||||
Assert.That(result.Succeeded, Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenApiKeyIsExpired_ThenAuthenticationFails()
|
||||
{
|
||||
var rawKey = ApiKeyHasher.GenerateKey();
|
||||
AddApiKey(rawKey, expiresAt: DateTimeOffset.UtcNow.AddDays(-1));
|
||||
|
||||
var result = await AuthenticateAsync($"Api-Key {rawKey}");
|
||||
|
||||
Assert.That(result.Succeeded, Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenApiKeyIsValid_ThenAuthenticationSucceedsWithOrganizationClaims()
|
||||
{
|
||||
var rawKey = ApiKeyHasher.GenerateKey();
|
||||
AddApiKey(rawKey, organizationId: 42);
|
||||
|
||||
var result = await AuthenticateAsync($"Api-Key {rawKey}");
|
||||
|
||||
Assert.That(result.Succeeded, Is.True);
|
||||
Assert.That(result.Principal!.FindFirst("OrganizationId")!.Value, Is.EqualTo("42"));
|
||||
Assert.That(result.Principal!.FindFirst("OrganizationRole")!.Value, Is.EqualTo("Admin"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
using System.Text.Encodings.Web;
|
||||
using MicCheck.Api.Common.Security.Authentication;
|
||||
using MicCheck.Api.Data;
|
||||
using MicCheck.Api.Tests.Unit.TestSupport;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using AppEnvironment = MicCheck.Api.Environments.Environment;
|
||||
|
||||
namespace MicCheck.Api.Tests.Unit.Common.Security.Authentication;
|
||||
|
||||
[TestFixture]
|
||||
public class EnvironmentKeyAuthenticationHandlerTests
|
||||
{
|
||||
private List<AppEnvironment> _environments = null!;
|
||||
private Mock<IMicCheckDbContext> _db = null!;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
_db = new Mock<IMicCheckDbContext>();
|
||||
_environments = [];
|
||||
_db.SetupDbSet(c => c.Environments, _environments);
|
||||
}
|
||||
|
||||
private async Task<AuthenticateResult> AuthenticateAsync(string? environmentKeyHeader)
|
||||
{
|
||||
var optionsMonitor = new Mock<IOptionsMonitor<AuthenticationSchemeOptions>>();
|
||||
optionsMonitor.Setup(o => o.Get(It.IsAny<string>())).Returns(new AuthenticationSchemeOptions());
|
||||
|
||||
var handler = new EnvironmentKeyAuthenticationHandler(optionsMonitor.Object, NullLoggerFactory.Instance, UrlEncoder.Default, _db.Object);
|
||||
|
||||
var httpContext = new DefaultHttpContext();
|
||||
if (environmentKeyHeader is not null)
|
||||
httpContext.Request.Headers["X-Environment-Key"] = environmentKeyHeader;
|
||||
|
||||
var scheme = new AuthenticationScheme(EnvironmentKeyAuthenticationHandler.SchemeName, null, typeof(EnvironmentKeyAuthenticationHandler));
|
||||
await handler.InitializeAsync(scheme, httpContext);
|
||||
|
||||
return await handler.AuthenticateAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenEnvironmentKeyHeaderIsAbsent_ThenNoResultIsReturned()
|
||||
{
|
||||
var result = await AuthenticateAsync(null);
|
||||
|
||||
Assert.That(result.None, Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenEnvironmentKeyIsInvalid_ThenAuthenticationFails()
|
||||
{
|
||||
var result = await AuthenticateAsync("not-a-real-key");
|
||||
|
||||
Assert.That(result.Succeeded, Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenEnvironmentKeyIsValid_ThenAuthenticationSucceedsWithEnvironmentClaims()
|
||||
{
|
||||
_environments.Add(new AppEnvironment { Id = 7, Name = "Prod", ApiKey = "valid-env-key", ProjectId = 3, CreatedAt = DateTimeOffset.UtcNow });
|
||||
|
||||
var result = await AuthenticateAsync("valid-env-key");
|
||||
|
||||
Assert.That(result.Succeeded, Is.True);
|
||||
Assert.That(result.Principal!.FindFirst("EnvironmentId")!.Value, Is.EqualTo("7"));
|
||||
Assert.That(result.Principal!.FindFirst("ProjectId")!.Value, Is.EqualTo("3"));
|
||||
}
|
||||
}
|
||||
@@ -1,185 +0,0 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using MicCheck.Api.Common.Security.Authorization;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace MicCheck.Api.Tests.Unit.Common.Security.Authorization;
|
||||
|
||||
[TestFixture]
|
||||
public class AuthEndpointsTests
|
||||
{
|
||||
private MicCheckWebApplicationFactory _factory = null!;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
_factory = new MicCheckWebApplicationFactory();
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void TearDown() => _factory.Dispose();
|
||||
|
||||
[Test]
|
||||
public async Task WhenRegisteringWithValidDetails_ThenOkIsReturnedWithTokens()
|
||||
{
|
||||
var client = _factory.CreateClient();
|
||||
|
||||
var response = await client.PostAsJsonAsync("/api/v1/auth/register", new RegisterRequest(
|
||||
"alice@example.com", "SecurePass1!", "Alice", "Smith", "Acme Corp"));
|
||||
|
||||
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
|
||||
|
||||
var body = await response.Content.ReadFromJsonAsync<LoginResponse>();
|
||||
Assert.That(body?.AccessToken, Is.Not.Null.And.Not.Empty);
|
||||
Assert.That(body?.RefreshToken, Is.Not.Null.And.Not.Empty);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenRegisteringWithDuplicateEmail_ThenConflictIsReturned()
|
||||
{
|
||||
var client = _factory.CreateClient();
|
||||
|
||||
await client.PostAsJsonAsync("/api/v1/auth/register", new RegisterRequest(
|
||||
"alice@example.com", "SecurePass1!", "Alice", "Smith", "Acme Corp"));
|
||||
|
||||
var response = await client.PostAsJsonAsync("/api/v1/auth/register", new RegisterRequest(
|
||||
"alice@example.com", "AnotherPass1!", "Alice", "Jones", "Other Corp"));
|
||||
|
||||
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Conflict));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenLoginWithValidCredentials_ThenOkIsReturnedWithTokens()
|
||||
{
|
||||
var client = _factory.CreateClient();
|
||||
|
||||
await client.PostAsJsonAsync("/api/v1/auth/register", new RegisterRequest(
|
||||
"bob@example.com", "SecurePass1!", "Bob", "Jones", "BobCo"));
|
||||
|
||||
var response = await client.PostAsJsonAsync("/api/v1/auth/login",
|
||||
new LoginRequest("bob@example.com", "SecurePass1!"));
|
||||
|
||||
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
|
||||
|
||||
var body = await response.Content.ReadFromJsonAsync<LoginResponse>();
|
||||
Assert.That(body?.AccessToken, Is.Not.Null.And.Not.Empty);
|
||||
Assert.That(body?.RefreshToken, Is.Not.Null.And.Not.Empty);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenLoginWithWrongPassword_ThenUnauthorizedIsReturned()
|
||||
{
|
||||
var client = _factory.CreateClient();
|
||||
|
||||
await client.PostAsJsonAsync("/api/v1/auth/register", new RegisterRequest(
|
||||
"carol@example.com", "CorrectPass1!", "Carol", "White", "CarolCo"));
|
||||
|
||||
var response = await client.PostAsJsonAsync("/api/v1/auth/login",
|
||||
new LoginRequest("carol@example.com", "WrongPassword"));
|
||||
|
||||
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenLoginWithUnknownEmail_ThenUnauthorizedIsReturned()
|
||||
{
|
||||
var client = _factory.CreateClient();
|
||||
|
||||
var response = await client.PostAsJsonAsync("/api/v1/auth/login",
|
||||
new LoginRequest("nobody@example.com", "SomePass1!"));
|
||||
|
||||
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenLoginWithEmptyEmail_ThenBadRequestIsReturned()
|
||||
{
|
||||
var client = _factory.CreateClient();
|
||||
|
||||
var response = await client.PostAsJsonAsync("/api/v1/auth/login",
|
||||
new LoginRequest("", "SomePass1!"));
|
||||
|
||||
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenRefreshingWithValidToken_ThenOkIsReturnedWithNewTokens()
|
||||
{
|
||||
var client = _factory.CreateClient();
|
||||
|
||||
var registerResponse = await client.PostAsJsonAsync("/api/v1/auth/register", new RegisterRequest(
|
||||
"dave@example.com", "SecurePass1!", "Dave", "Brown", "DaveCo"));
|
||||
var registerBody = await registerResponse.Content.ReadFromJsonAsync<LoginResponse>();
|
||||
|
||||
var response = await client.PostAsJsonAsync("/api/v1/auth/refresh",
|
||||
new RefreshRequest(registerBody!.RefreshToken));
|
||||
|
||||
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
|
||||
|
||||
var body = await response.Content.ReadFromJsonAsync<LoginResponse>();
|
||||
Assert.That(body?.AccessToken, Is.Not.Null.And.Not.Empty);
|
||||
Assert.That(body?.RefreshToken, Is.Not.EqualTo(registerBody.RefreshToken));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenRefreshingWithInvalidToken_ThenUnauthorizedIsReturned()
|
||||
{
|
||||
var client = _factory.CreateClient();
|
||||
|
||||
var response = await client.PostAsJsonAsync("/api/v1/auth/refresh",
|
||||
new RefreshRequest("not-a-valid-refresh-token"));
|
||||
|
||||
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenRefreshingWithRevokedToken_ThenUnauthorizedIsReturned()
|
||||
{
|
||||
var client = _factory.CreateClient();
|
||||
|
||||
var registerResponse = await client.PostAsJsonAsync("/api/v1/auth/register", new RegisterRequest(
|
||||
"eve@example.com", "SecurePass1!", "Eve", "Davis", "EveCo"));
|
||||
var registerBody = await registerResponse.Content.ReadFromJsonAsync<LoginResponse>();
|
||||
|
||||
await client.PostAsJsonAsync("/api/v1/auth/refresh",
|
||||
new RefreshRequest(registerBody!.RefreshToken));
|
||||
|
||||
var response = await client.PostAsJsonAsync("/api/v1/auth/refresh",
|
||||
new RefreshRequest(registerBody.RefreshToken));
|
||||
|
||||
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenLoggingOut_ThenNoContentIsReturned()
|
||||
{
|
||||
var client = _factory.CreateClient();
|
||||
|
||||
var registerResponse = await client.PostAsJsonAsync("/api/v1/auth/register", new RegisterRequest(
|
||||
"frank@example.com", "SecurePass1!", "Frank", "Green", "FrankCo"));
|
||||
var registerBody = await registerResponse.Content.ReadFromJsonAsync<LoginResponse>();
|
||||
|
||||
var response = await client.PostAsJsonAsync("/api/v1/auth/logout",
|
||||
new LogoutRequest(registerBody!.RefreshToken));
|
||||
|
||||
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.NoContent));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenLoggingOutAndRefreshing_ThenUnauthorizedIsReturned()
|
||||
{
|
||||
var client = _factory.CreateClient();
|
||||
|
||||
var registerResponse = await client.PostAsJsonAsync("/api/v1/auth/register", new RegisterRequest(
|
||||
"grace@example.com", "SecurePass1!", "Grace", "Black", "GraceCo"));
|
||||
var registerBody = await registerResponse.Content.ReadFromJsonAsync<LoginResponse>();
|
||||
|
||||
await client.PostAsJsonAsync("/api/v1/auth/logout",
|
||||
new LogoutRequest(registerBody!.RefreshToken));
|
||||
|
||||
var response = await client.PostAsJsonAsync("/api/v1/auth/refresh",
|
||||
new RefreshRequest(registerBody.RefreshToken));
|
||||
|
||||
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
using MicCheck.Api.Common.Security.Authorization;
|
||||
using MicCheck.Api.Data;
|
||||
using MicCheck.Api.Organizations;
|
||||
using MicCheck.Api.Tests.Unit.TestSupport;
|
||||
using MicCheck.Api.Users;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace MicCheck.Api.Tests.Unit.Common.Security.Authorization;
|
||||
|
||||
[TestFixture]
|
||||
public class AuthServiceTests
|
||||
{
|
||||
private Mock<IMicCheckDbContext> _db = null!;
|
||||
private List<User> _users = null!;
|
||||
private List<Organization> _organizations = null!;
|
||||
private List<OrganizationUser> _organizationUsers = null!;
|
||||
private List<RefreshToken> _refreshTokens = null!;
|
||||
private PasswordHasher<User> _passwordHasher = null!;
|
||||
private AuthService _service = null!;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
_db = new Mock<IMicCheckDbContext>();
|
||||
_users = [];
|
||||
_db.SetupDbSetWithGeneratedIds(c => c.Users, _users);
|
||||
_organizations = [];
|
||||
_db.SetupDbSetWithGeneratedIds(c => c.Organizations, _organizations);
|
||||
_organizationUsers = [];
|
||||
_db.SetupDbSet(c => c.OrganizationUsers, _organizationUsers);
|
||||
_refreshTokens = [];
|
||||
_db.SetupDbSetWithGeneratedIds(c => c.RefreshTokens, _refreshTokens);
|
||||
|
||||
_passwordHasher = new PasswordHasher<User>();
|
||||
|
||||
var tokenService = new Mock<ITokenService>();
|
||||
tokenService.Setup(t => t.GenerateToken(It.IsAny<User>()))
|
||||
.Returns(new TokenResponse("access-token", DateTime.UtcNow.AddHours(1)));
|
||||
|
||||
_service = new AuthService(_db.Object, tokenService.Object, _passwordHasher);
|
||||
}
|
||||
|
||||
private User AddUser(string email, string password, bool isActive = true)
|
||||
{
|
||||
var user = new User
|
||||
{
|
||||
Email = email,
|
||||
FirstName = "First",
|
||||
LastName = "Last",
|
||||
IsActive = isActive,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
PasswordHash = string.Empty
|
||||
};
|
||||
user.PasswordHash = _passwordHasher.HashPassword(user, password);
|
||||
_db.Object.Users.Add(user);
|
||||
return user;
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenRegisteringWithValidDetails_ThenTokensAreReturned()
|
||||
{
|
||||
var response = await _service.RegisterAsync(
|
||||
"alice@example.com", "SecurePass1!", "Alice", "Smith", "Acme Corp");
|
||||
|
||||
Assert.That(response, Is.Not.Null);
|
||||
Assert.That(response!.AccessToken, Is.Not.Null.And.Not.Empty);
|
||||
Assert.That(response.RefreshToken, Is.Not.Null.And.Not.Empty);
|
||||
Assert.That(_users, Has.Count.EqualTo(1));
|
||||
Assert.That(_organizations, Has.Count.EqualTo(1));
|
||||
Assert.That(_organizationUsers.Single().Role, Is.EqualTo(OrganizationRole.Admin));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenRegisteringWithDuplicateEmail_ThenNullIsReturned()
|
||||
{
|
||||
AddUser("alice@example.com", "SecurePass1!");
|
||||
|
||||
var response = await _service.RegisterAsync(
|
||||
"alice@example.com", "AnotherPass1!", "Alice", "Jones", "Other Corp");
|
||||
|
||||
Assert.That(response, Is.Null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenLoginWithValidCredentials_ThenTokensAreReturned()
|
||||
{
|
||||
AddUser("bob@example.com", "SecurePass1!");
|
||||
|
||||
var response = await _service.LoginAsync("bob@example.com", "SecurePass1!");
|
||||
|
||||
Assert.That(response, Is.Not.Null);
|
||||
Assert.That(response!.AccessToken, Is.Not.Null.And.Not.Empty);
|
||||
Assert.That(response.RefreshToken, Is.Not.Null.And.Not.Empty);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenLoginWithWrongPassword_ThenNullIsReturned()
|
||||
{
|
||||
AddUser("carol@example.com", "CorrectPass1!");
|
||||
|
||||
var response = await _service.LoginAsync("carol@example.com", "WrongPassword");
|
||||
|
||||
Assert.That(response, Is.Null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenLoginWithUnknownEmail_ThenNullIsReturned()
|
||||
{
|
||||
var response = await _service.LoginAsync("nobody@example.com", "SomePass1!");
|
||||
|
||||
Assert.That(response, Is.Null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenRefreshingWithValidToken_ThenNewTokensAreReturnedAndOldTokenIsRevoked()
|
||||
{
|
||||
var user = AddUser("dave@example.com", "SecurePass1!");
|
||||
var refreshToken = new RefreshToken
|
||||
{
|
||||
Token = "valid-refresh-token",
|
||||
UserId = user.Id,
|
||||
User = user,
|
||||
ExpiresAt = DateTimeOffset.UtcNow.AddDays(30),
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
_db.Object.RefreshTokens.Add(refreshToken);
|
||||
|
||||
var response = await _service.RefreshAsync("valid-refresh-token");
|
||||
|
||||
Assert.That(response, Is.Not.Null);
|
||||
Assert.That(response!.RefreshToken, Is.Not.EqualTo("valid-refresh-token"));
|
||||
Assert.That(refreshToken.IsRevoked, Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenRefreshingWithInvalidToken_ThenNullIsReturned()
|
||||
{
|
||||
var response = await _service.RefreshAsync("not-a-valid-refresh-token");
|
||||
|
||||
Assert.That(response, Is.Null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenRefreshingWithRevokedToken_ThenNullIsReturned()
|
||||
{
|
||||
var user = AddUser("eve@example.com", "SecurePass1!");
|
||||
_db.Object.RefreshTokens.Add(new RefreshToken
|
||||
{
|
||||
Token = "revoked-token",
|
||||
UserId = user.Id,
|
||||
User = user,
|
||||
ExpiresAt = DateTimeOffset.UtcNow.AddDays(30),
|
||||
IsRevoked = true,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
});
|
||||
|
||||
var response = await _service.RefreshAsync("revoked-token");
|
||||
|
||||
Assert.That(response, Is.Null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenRefreshingWithExpiredToken_ThenNullIsReturned()
|
||||
{
|
||||
var user = AddUser("frank@example.com", "SecurePass1!");
|
||||
_db.Object.RefreshTokens.Add(new RefreshToken
|
||||
{
|
||||
Token = "expired-token",
|
||||
UserId = user.Id,
|
||||
User = user,
|
||||
ExpiresAt = DateTimeOffset.UtcNow.AddDays(-1),
|
||||
CreatedAt = DateTimeOffset.UtcNow.AddDays(-31)
|
||||
});
|
||||
|
||||
var response = await _service.RefreshAsync("expired-token");
|
||||
|
||||
Assert.That(response, Is.Null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenLoggingOut_ThenTokenIsRevoked()
|
||||
{
|
||||
var user = AddUser("grace@example.com", "SecurePass1!");
|
||||
var refreshToken = new RefreshToken
|
||||
{
|
||||
Token = "logout-token",
|
||||
UserId = user.Id,
|
||||
User = user,
|
||||
ExpiresAt = DateTimeOffset.UtcNow.AddDays(30),
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
_db.Object.RefreshTokens.Add(refreshToken);
|
||||
|
||||
await _service.LogoutAsync("logout-token");
|
||||
|
||||
Assert.That(refreshToken.IsRevoked, Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenLoggingOutAndRefreshing_ThenNullIsReturned()
|
||||
{
|
||||
var user = AddUser("henry@example.com", "SecurePass1!");
|
||||
_db.Object.RefreshTokens.Add(new RefreshToken
|
||||
{
|
||||
Token = "logout-then-refresh-token",
|
||||
UserId = user.Id,
|
||||
User = user,
|
||||
ExpiresAt = DateTimeOffset.UtcNow.AddDays(30),
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
});
|
||||
|
||||
await _service.LogoutAsync("logout-then-refresh-token");
|
||||
var response = await _service.RefreshAsync("logout-then-refresh-token");
|
||||
|
||||
Assert.That(response, Is.Null);
|
||||
}
|
||||
}
|
||||
@@ -1,180 +1,173 @@
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using MicCheck.Api.Common.Security.Authorization;
|
||||
using MicCheck.Api.Data;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace MicCheck.Api.Tests.Unit.Common.Security.Authorization;
|
||||
|
||||
[TestFixture]
|
||||
public class ProjectPermissionRequirementHandlerTests
|
||||
{
|
||||
private MicCheckDbContext _db = null!;
|
||||
private Mock<IHttpContextAccessor> _httpContextAccessor = null!;
|
||||
private ProjectPermissionRequirementHandler _handler = null!;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<MicCheckDbContext>()
|
||||
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
||||
.Options;
|
||||
_db = new MicCheckDbContext(options);
|
||||
|
||||
_httpContextAccessor = new Mock<IHttpContextAccessor>();
|
||||
_handler = new ProjectPermissionRequirementHandler(_db, _httpContextAccessor.Object);
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void TearDown() => _db.Dispose();
|
||||
|
||||
private static AuthorizationHandlerContext CreateContext(
|
||||
ClaimsPrincipal user,
|
||||
ProjectPermission permission = ProjectPermission.ViewProject)
|
||||
{
|
||||
var requirement = new ProjectPermissionRequirement(permission);
|
||||
return new AuthorizationHandlerContext([requirement], user, null);
|
||||
}
|
||||
|
||||
private static ClaimsPrincipal CreateUserWithClaims(params Claim[] claims)
|
||||
{
|
||||
return new ClaimsPrincipal(new ClaimsIdentity(claims, "Bearer"));
|
||||
}
|
||||
|
||||
private void SetupRouteProjectId(int projectId)
|
||||
{
|
||||
var httpContext = new DefaultHttpContext();
|
||||
httpContext.Request.RouteValues["projectId"] = projectId.ToString();
|
||||
_httpContextAccessor.Setup(a => a.HttpContext).Returns(httpContext);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenUserIsOrganizationAdmin_ThenRequirementSucceeds()
|
||||
{
|
||||
var user = CreateUserWithClaims(
|
||||
new Claim(JwtRegisteredClaimNames.Sub, "1"),
|
||||
new Claim("OrganizationRole", "Admin"));
|
||||
|
||||
var context = CreateContext(user);
|
||||
|
||||
await _handler.HandleAsync(context);
|
||||
|
||||
Assert.That(context.HasSucceeded, Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenUserHasRequiredPermission_ThenRequirementSucceeds()
|
||||
{
|
||||
_db.UserProjectPermissions.Add(new UserProjectPermission
|
||||
{
|
||||
UserId = 1,
|
||||
ProjectId = 10,
|
||||
Permissions = [ProjectPermission.ViewProject]
|
||||
});
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
SetupRouteProjectId(10);
|
||||
|
||||
var user = CreateUserWithClaims(
|
||||
new Claim(JwtRegisteredClaimNames.Sub, "1"));
|
||||
|
||||
var context = CreateContext(user, ProjectPermission.ViewProject);
|
||||
|
||||
await _handler.HandleAsync(context);
|
||||
|
||||
Assert.That(context.HasSucceeded, Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenUserIsProjectAdmin_ThenAllPermissionsAreGranted()
|
||||
{
|
||||
_db.UserProjectPermissions.Add(new UserProjectPermission
|
||||
{
|
||||
UserId = 2,
|
||||
ProjectId = 20,
|
||||
IsAdmin = true,
|
||||
Permissions = []
|
||||
});
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
SetupRouteProjectId(20);
|
||||
|
||||
var user = CreateUserWithClaims(
|
||||
new Claim(JwtRegisteredClaimNames.Sub, "2"));
|
||||
|
||||
var context = CreateContext(user, ProjectPermission.DeleteFeature);
|
||||
|
||||
await _handler.HandleAsync(context);
|
||||
|
||||
Assert.That(context.HasSucceeded, Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenUserDoesNotHaveRequiredPermission_ThenRequirementFails()
|
||||
{
|
||||
_db.UserProjectPermissions.Add(new UserProjectPermission
|
||||
{
|
||||
UserId = 3,
|
||||
ProjectId = 30,
|
||||
Permissions = [ProjectPermission.ViewProject]
|
||||
});
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
SetupRouteProjectId(30);
|
||||
|
||||
var user = CreateUserWithClaims(
|
||||
new Claim(JwtRegisteredClaimNames.Sub, "3"));
|
||||
|
||||
var context = CreateContext(user, ProjectPermission.DeleteFeature);
|
||||
|
||||
await _handler.HandleAsync(context);
|
||||
|
||||
Assert.That(context.HasSucceeded, Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenUserHasNoProjectPermissionRecord_ThenRequirementFails()
|
||||
{
|
||||
SetupRouteProjectId(99);
|
||||
|
||||
var user = CreateUserWithClaims(
|
||||
new Claim(JwtRegisteredClaimNames.Sub, "5"));
|
||||
|
||||
var context = CreateContext(user);
|
||||
|
||||
await _handler.HandleAsync(context);
|
||||
|
||||
Assert.That(context.HasSucceeded, Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenUserIdClaimIsMissing_ThenRequirementFails()
|
||||
{
|
||||
var user = CreateUserWithClaims();
|
||||
|
||||
var context = CreateContext(user);
|
||||
|
||||
await _handler.HandleAsync(context);
|
||||
|
||||
Assert.That(context.HasSucceeded, Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenProjectIdIsNotInRoute_ThenRequirementFails()
|
||||
{
|
||||
_httpContextAccessor.Setup(a => a.HttpContext).Returns(new DefaultHttpContext());
|
||||
|
||||
var user = CreateUserWithClaims(
|
||||
new Claim(JwtRegisteredClaimNames.Sub, "1"));
|
||||
|
||||
var context = CreateContext(user);
|
||||
|
||||
await _handler.HandleAsync(context);
|
||||
|
||||
Assert.That(context.HasSucceeded, Is.False);
|
||||
}
|
||||
}
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using MicCheck.Api.Common.Security.Authorization;
|
||||
using MicCheck.Api.Data;
|
||||
using MicCheck.Api.Tests.Unit.TestSupport;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace MicCheck.Api.Tests.Unit.Common.Security.Authorization;
|
||||
|
||||
[TestFixture]
|
||||
public class ProjectPermissionRequirementHandlerTests
|
||||
{
|
||||
private List<UserProjectPermission> _userProjectPermissions = null!;
|
||||
private Mock<IHttpContextAccessor> _httpContextAccessor = null!;
|
||||
private ProjectPermissionRequirementHandler _handler = null!;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
var db = new Mock<IMicCheckDbContext>();
|
||||
_userProjectPermissions = [];
|
||||
db.SetupDbSet(c => c.UserProjectPermissions, _userProjectPermissions);
|
||||
|
||||
_httpContextAccessor = new Mock<IHttpContextAccessor>();
|
||||
_handler = new ProjectPermissionRequirementHandler(db.Object, _httpContextAccessor.Object);
|
||||
}
|
||||
|
||||
private static AuthorizationHandlerContext CreateContext(
|
||||
ClaimsPrincipal user,
|
||||
ProjectPermission permission = ProjectPermission.ViewProject)
|
||||
{
|
||||
var requirement = new ProjectPermissionRequirement(permission);
|
||||
return new AuthorizationHandlerContext([requirement], user, null);
|
||||
}
|
||||
|
||||
private static ClaimsPrincipal CreateUserWithClaims(params Claim[] claims)
|
||||
{
|
||||
return new ClaimsPrincipal(new ClaimsIdentity(claims, "Bearer"));
|
||||
}
|
||||
|
||||
private void SetupRouteProjectId(int projectId)
|
||||
{
|
||||
var httpContext = new DefaultHttpContext();
|
||||
httpContext.Request.RouteValues["projectId"] = projectId.ToString();
|
||||
_httpContextAccessor.Setup(a => a.HttpContext).Returns(httpContext);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenUserIsOrganizationAdmin_ThenRequirementSucceeds()
|
||||
{
|
||||
var user = CreateUserWithClaims(
|
||||
new Claim(JwtRegisteredClaimNames.Sub, "1"),
|
||||
new Claim("OrganizationRole", "Admin"));
|
||||
|
||||
var context = CreateContext(user);
|
||||
|
||||
await _handler.HandleAsync(context);
|
||||
|
||||
Assert.That(context.HasSucceeded, Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenUserHasRequiredPermission_ThenRequirementSucceeds()
|
||||
{
|
||||
_userProjectPermissions.Add(new UserProjectPermission
|
||||
{
|
||||
UserId = 1,
|
||||
ProjectId = 10,
|
||||
Permissions = [ProjectPermission.ViewProject]
|
||||
});
|
||||
|
||||
SetupRouteProjectId(10);
|
||||
|
||||
var user = CreateUserWithClaims(
|
||||
new Claim(JwtRegisteredClaimNames.Sub, "1"));
|
||||
|
||||
var context = CreateContext(user, ProjectPermission.ViewProject);
|
||||
|
||||
await _handler.HandleAsync(context);
|
||||
|
||||
Assert.That(context.HasSucceeded, Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenUserIsProjectAdmin_ThenAllPermissionsAreGranted()
|
||||
{
|
||||
_userProjectPermissions.Add(new UserProjectPermission
|
||||
{
|
||||
UserId = 2,
|
||||
ProjectId = 20,
|
||||
IsAdmin = true,
|
||||
Permissions = []
|
||||
});
|
||||
|
||||
SetupRouteProjectId(20);
|
||||
|
||||
var user = CreateUserWithClaims(
|
||||
new Claim(JwtRegisteredClaimNames.Sub, "2"));
|
||||
|
||||
var context = CreateContext(user, ProjectPermission.DeleteFeature);
|
||||
|
||||
await _handler.HandleAsync(context);
|
||||
|
||||
Assert.That(context.HasSucceeded, Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenUserDoesNotHaveRequiredPermission_ThenRequirementFails()
|
||||
{
|
||||
_userProjectPermissions.Add(new UserProjectPermission
|
||||
{
|
||||
UserId = 3,
|
||||
ProjectId = 30,
|
||||
Permissions = [ProjectPermission.ViewProject]
|
||||
});
|
||||
|
||||
SetupRouteProjectId(30);
|
||||
|
||||
var user = CreateUserWithClaims(
|
||||
new Claim(JwtRegisteredClaimNames.Sub, "3"));
|
||||
|
||||
var context = CreateContext(user, ProjectPermission.DeleteFeature);
|
||||
|
||||
await _handler.HandleAsync(context);
|
||||
|
||||
Assert.That(context.HasSucceeded, Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenUserHasNoProjectPermissionRecord_ThenRequirementFails()
|
||||
{
|
||||
SetupRouteProjectId(99);
|
||||
|
||||
var user = CreateUserWithClaims(
|
||||
new Claim(JwtRegisteredClaimNames.Sub, "5"));
|
||||
|
||||
var context = CreateContext(user);
|
||||
|
||||
await _handler.HandleAsync(context);
|
||||
|
||||
Assert.That(context.HasSucceeded, Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenUserIdClaimIsMissing_ThenRequirementFails()
|
||||
{
|
||||
var user = CreateUserWithClaims();
|
||||
|
||||
var context = CreateContext(user);
|
||||
|
||||
await _handler.HandleAsync(context);
|
||||
|
||||
Assert.That(context.HasSucceeded, Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenProjectIdIsNotInRoute_ThenRequirementFails()
|
||||
{
|
||||
_httpContextAccessor.Setup(a => a.HttpContext).Returns(new DefaultHttpContext());
|
||||
|
||||
var user = CreateUserWithClaims(
|
||||
new Claim(JwtRegisteredClaimNames.Sub, "1"));
|
||||
|
||||
var context = CreateContext(user);
|
||||
|
||||
await _handler.HandleAsync(context);
|
||||
|
||||
Assert.That(context.HasSucceeded, Is.False);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user