Refactor nunit tests off InMemory/WebApplicationFactory, remove tuple returns (#4)
## Summary - Extract `IMicCheckDbContext` and mock DB access with Moq instead of the EF InMemory provider, per CLAUDE.md testing guidelines. - Rewrite HTTP-pipeline tests to exercise controllers/auth handlers directly instead of `WebApplicationFactory`. - Remove tuple return types across the API in favor of named records (`PagedResult<T>`, `ProfileUpdateResult`, `ApiKeyCreationResult`). ## Test plan - [x] `dotnet build` (full solution) - [x] `dotnet test tests/api/MicCheck.Api.Tests.Unit` — 187/187 pass - [x] `dotnet test tests/api/MicCheck.Api.Tests.Integration` — 8/8 pass (live API + Postgres via Aspire) - [x] Verified Aspire AppHost/dashboard starts and API responds Reviewed-on: #4
This commit was merged in pull request #4.
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