283 lines
10 KiB
C#
283 lines
10 KiB
C#
|
|
using Books.Api.Commands.ApiKeys;
|
||
|
|
using Books.Api.Domain.ApiKeys;
|
||
|
|
using Books.Api.EventFlow.Repositories;
|
||
|
|
using Books.Api.Tests.Helpers;
|
||
|
|
using Books.Api.Tests.Infrastructure;
|
||
|
|
using AwesomeAssertions;
|
||
|
|
using Microsoft.Extensions.DependencyInjection;
|
||
|
|
|
||
|
|
namespace Books.Api.Tests.Domain;
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Integration tests for ApiKey domain operations.
|
||
|
|
/// Tests the ApiKeyAggregate via CommandBus since ApiKeys are not exposed via GraphQL.
|
||
|
|
/// </summary>
|
||
|
|
[Trait("Category", "Integration")]
|
||
|
|
public class ApiKeyIntegrationTests(TestWebApplicationFactory factory)
|
||
|
|
: IntegrationTestBase(factory)
|
||
|
|
{
|
||
|
|
[Fact]
|
||
|
|
public async Task CreateApiKey_CreatesKeySuccessfully()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
var apiKeyId = ApiKeyId.New;
|
||
|
|
var keyHash = Convert.ToBase64String(System.Security.Cryptography.SHA256.HashData(
|
||
|
|
System.Text.Encoding.UTF8.GetBytes($"test-key-{Guid.NewGuid()}")));
|
||
|
|
var companyId = $"company-{Guid.NewGuid():N}";
|
||
|
|
|
||
|
|
var command = new CreateApiKeyCommand(
|
||
|
|
apiKeyId,
|
||
|
|
"Test API Key",
|
||
|
|
keyHash,
|
||
|
|
companyId,
|
||
|
|
"test-user");
|
||
|
|
|
||
|
|
// Act
|
||
|
|
await CommandBus.PublishAsync(command, CancellationToken.None);
|
||
|
|
|
||
|
|
// Assert
|
||
|
|
var apiKeys = await Eventually.GetListAsync(async () =>
|
||
|
|
{
|
||
|
|
var repo = GetService<IApiKeyRepository>();
|
||
|
|
return await repo.GetByCompanyIdAsync(companyId);
|
||
|
|
}, 1);
|
||
|
|
|
||
|
|
apiKeys.Should().ContainSingle();
|
||
|
|
apiKeys.First().Name.Should().Be("Test API Key");
|
||
|
|
apiKeys.First().CompanyId.Should().Be(companyId);
|
||
|
|
apiKeys.First().IsActive.Should().BeTrue();
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task CreateApiKey_FailsForDuplicate()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
var apiKeyId = ApiKeyId.New;
|
||
|
|
var keyHash = Convert.ToBase64String(System.Security.Cryptography.SHA256.HashData(
|
||
|
|
System.Text.Encoding.UTF8.GetBytes($"test-key-{Guid.NewGuid()}")));
|
||
|
|
var companyId = $"company-{Guid.NewGuid():N}";
|
||
|
|
|
||
|
|
var command = new CreateApiKeyCommand(
|
||
|
|
apiKeyId,
|
||
|
|
"First API Key",
|
||
|
|
keyHash,
|
||
|
|
companyId,
|
||
|
|
"test-user");
|
||
|
|
|
||
|
|
await CommandBus.PublishAsync(command, CancellationToken.None);
|
||
|
|
|
||
|
|
// Wait for first key to be created
|
||
|
|
await Eventually.GetListAsync(async () =>
|
||
|
|
{
|
||
|
|
var repo = GetService<IApiKeyRepository>();
|
||
|
|
return await repo.GetByCompanyIdAsync(companyId);
|
||
|
|
}, 1);
|
||
|
|
|
||
|
|
// Act - Try to create with same ID
|
||
|
|
var duplicateCommand = new CreateApiKeyCommand(
|
||
|
|
apiKeyId, // Same ID
|
||
|
|
"Duplicate API Key",
|
||
|
|
keyHash,
|
||
|
|
companyId,
|
||
|
|
"test-user");
|
||
|
|
|
||
|
|
var act = async () => await CommandBus.PublishAsync(duplicateCommand, CancellationToken.None);
|
||
|
|
|
||
|
|
// Assert
|
||
|
|
await act.Should().ThrowAsync<Exception>()
|
||
|
|
.Where(e => e.Message.Contains("APIKEY_EXISTS") ||
|
||
|
|
(e.InnerException != null && e.InnerException.Message.Contains("APIKEY_EXISTS")) ||
|
||
|
|
e.Message.Contains("already exists"));
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task RevokeApiKey_RevokesSuccessfully()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
var apiKeyId = ApiKeyId.New;
|
||
|
|
var keyHash = Convert.ToBase64String(System.Security.Cryptography.SHA256.HashData(
|
||
|
|
System.Text.Encoding.UTF8.GetBytes($"test-key-{Guid.NewGuid()}")));
|
||
|
|
var companyId = $"company-{Guid.NewGuid():N}";
|
||
|
|
|
||
|
|
await CommandBus.PublishAsync(new CreateApiKeyCommand(
|
||
|
|
apiKeyId, "Key To Revoke", keyHash, companyId, "test-user"), CancellationToken.None);
|
||
|
|
|
||
|
|
await Eventually.GetListAsync(async () =>
|
||
|
|
{
|
||
|
|
var repo = GetService<IApiKeyRepository>();
|
||
|
|
return await repo.GetByCompanyIdAsync(companyId);
|
||
|
|
}, 1);
|
||
|
|
|
||
|
|
// Act
|
||
|
|
var revokeCommand = new RevokeApiKeyCommand(apiKeyId, "admin-user");
|
||
|
|
await CommandBus.PublishAsync(revokeCommand, CancellationToken.None);
|
||
|
|
|
||
|
|
// Assert
|
||
|
|
var revokedKey = await Eventually.GetAsync(async () =>
|
||
|
|
{
|
||
|
|
var repo = GetService<IApiKeyRepository>();
|
||
|
|
var keys = await repo.GetByCompanyIdAsync(companyId);
|
||
|
|
var key = keys.FirstOrDefault(k => k.Id == apiKeyId.Value);
|
||
|
|
return key?.IsActive == false ? key : null;
|
||
|
|
});
|
||
|
|
|
||
|
|
revokedKey.Should().NotBeNull();
|
||
|
|
revokedKey!.IsActive.Should().BeFalse();
|
||
|
|
revokedKey.RevokedBy.Should().Be("admin-user");
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task RevokeApiKey_FailsForAlreadyRevoked()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
var apiKeyId = ApiKeyId.New;
|
||
|
|
var keyHash = Convert.ToBase64String(System.Security.Cryptography.SHA256.HashData(
|
||
|
|
System.Text.Encoding.UTF8.GetBytes($"test-key-{Guid.NewGuid()}")));
|
||
|
|
var companyId = $"company-{Guid.NewGuid():N}";
|
||
|
|
|
||
|
|
await CommandBus.PublishAsync(new CreateApiKeyCommand(
|
||
|
|
apiKeyId, "Key To Double Revoke", keyHash, companyId, "test-user"), CancellationToken.None);
|
||
|
|
|
||
|
|
await Eventually.GetListAsync(async () =>
|
||
|
|
{
|
||
|
|
var repo = GetService<IApiKeyRepository>();
|
||
|
|
return await repo.GetByCompanyIdAsync(companyId);
|
||
|
|
}, 1);
|
||
|
|
|
||
|
|
// Revoke first time
|
||
|
|
await CommandBus.PublishAsync(new RevokeApiKeyCommand(apiKeyId, "admin-user"), CancellationToken.None);
|
||
|
|
|
||
|
|
await Eventually.GetAsync(async () =>
|
||
|
|
{
|
||
|
|
var repo = GetService<IApiKeyRepository>();
|
||
|
|
var keys = await repo.GetByCompanyIdAsync(companyId);
|
||
|
|
var key = keys.FirstOrDefault(k => k.Id == apiKeyId.Value);
|
||
|
|
return key?.IsActive == false ? key : null;
|
||
|
|
});
|
||
|
|
|
||
|
|
// Act - Try to revoke again
|
||
|
|
var act = async () => await CommandBus.PublishAsync(
|
||
|
|
new RevokeApiKeyCommand(apiKeyId, "admin-user"), CancellationToken.None);
|
||
|
|
|
||
|
|
// Assert
|
||
|
|
await act.Should().ThrowAsync<Exception>()
|
||
|
|
.Where(e => e.Message.Contains("APIKEY_REVOKED") ||
|
||
|
|
(e.InnerException != null && e.InnerException.Message.Contains("APIKEY_REVOKED")) ||
|
||
|
|
e.Message.Contains("already revoked"));
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task GetByCompanyId_ReturnsKeysForCompany()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
var companyId = $"company-{Guid.NewGuid():N}";
|
||
|
|
var otherCompanyId = $"company-{Guid.NewGuid():N}";
|
||
|
|
|
||
|
|
// Create keys for our company
|
||
|
|
for (var i = 0; i < 3; i++)
|
||
|
|
{
|
||
|
|
var keyHash = Convert.ToBase64String(System.Security.Cryptography.SHA256.HashData(
|
||
|
|
System.Text.Encoding.UTF8.GetBytes($"key-{i}-{Guid.NewGuid()}")));
|
||
|
|
await CommandBus.PublishAsync(new CreateApiKeyCommand(
|
||
|
|
ApiKeyId.New, $"Key {i}", keyHash, companyId, "test-user"), CancellationToken.None);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Create key for other company
|
||
|
|
var otherKeyHash = Convert.ToBase64String(System.Security.Cryptography.SHA256.HashData(
|
||
|
|
System.Text.Encoding.UTF8.GetBytes($"other-key-{Guid.NewGuid()}")));
|
||
|
|
await CommandBus.PublishAsync(new CreateApiKeyCommand(
|
||
|
|
ApiKeyId.New, "Other Key", otherKeyHash, otherCompanyId, "test-user"), CancellationToken.None);
|
||
|
|
|
||
|
|
// Act
|
||
|
|
var keys = await Eventually.GetListAsync(async () =>
|
||
|
|
{
|
||
|
|
var repo = GetService<IApiKeyRepository>();
|
||
|
|
return await repo.GetByCompanyIdAsync(companyId);
|
||
|
|
}, 3);
|
||
|
|
|
||
|
|
// Assert
|
||
|
|
keys.Should().HaveCount(3);
|
||
|
|
keys.Should().AllSatisfy(k => k.CompanyId.Should().Be(companyId));
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task GetByIdForValidation_ReturnsActiveKey()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
var apiKeyId = ApiKeyId.New;
|
||
|
|
var keyHash = Convert.ToBase64String(System.Security.Cryptography.SHA256.HashData(
|
||
|
|
System.Text.Encoding.UTF8.GetBytes($"validation-key-{Guid.NewGuid()}")));
|
||
|
|
var companyId = $"company-{Guid.NewGuid():N}";
|
||
|
|
|
||
|
|
await CommandBus.PublishAsync(new CreateApiKeyCommand(
|
||
|
|
apiKeyId, "Validation Key", keyHash, companyId, "test-user"), CancellationToken.None);
|
||
|
|
|
||
|
|
// Act
|
||
|
|
var validationDto = await Eventually.GetAsync(async () =>
|
||
|
|
{
|
||
|
|
var repo = GetService<IApiKeyRepository>();
|
||
|
|
return await repo.GetByIdForValidationAsync(apiKeyId.Value);
|
||
|
|
});
|
||
|
|
|
||
|
|
// Assert
|
||
|
|
validationDto.Should().NotBeNull();
|
||
|
|
validationDto!.ApiKeyId.Should().Be(apiKeyId.Value);
|
||
|
|
validationDto.Name.Should().Be("Validation Key");
|
||
|
|
validationDto.KeyHash.Should().Be(keyHash);
|
||
|
|
validationDto.CompanyId.Should().Be(companyId);
|
||
|
|
validationDto.IsActive.Should().BeTrue();
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task GetByIdForValidation_ReturnsNull_ForRevokedKey()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
var apiKeyId = ApiKeyId.New;
|
||
|
|
var keyHash = Convert.ToBase64String(System.Security.Cryptography.SHA256.HashData(
|
||
|
|
System.Text.Encoding.UTF8.GetBytes($"revoked-validation-key-{Guid.NewGuid()}")));
|
||
|
|
var companyId = $"company-{Guid.NewGuid():N}";
|
||
|
|
|
||
|
|
await CommandBus.PublishAsync(new CreateApiKeyCommand(
|
||
|
|
apiKeyId, "Revoked Validation Key", keyHash, companyId, "test-user"), CancellationToken.None);
|
||
|
|
|
||
|
|
await Eventually.GetAsync(async () =>
|
||
|
|
{
|
||
|
|
var repo = GetService<IApiKeyRepository>();
|
||
|
|
return await repo.GetByIdForValidationAsync(apiKeyId.Value);
|
||
|
|
});
|
||
|
|
|
||
|
|
// Revoke the key
|
||
|
|
await CommandBus.PublishAsync(new RevokeApiKeyCommand(apiKeyId, "admin-user"), CancellationToken.None);
|
||
|
|
|
||
|
|
// Wait for revocation
|
||
|
|
await Eventually.GetAsync(async () =>
|
||
|
|
{
|
||
|
|
var repo = GetService<IApiKeyRepository>();
|
||
|
|
var keys = await repo.GetByCompanyIdAsync(companyId);
|
||
|
|
var key = keys.FirstOrDefault(k => k.Id == apiKeyId.Value);
|
||
|
|
return key?.IsActive == false ? key : null;
|
||
|
|
});
|
||
|
|
|
||
|
|
// Act
|
||
|
|
var repo = GetService<IApiKeyRepository>();
|
||
|
|
var validationDto = await repo.GetByIdForValidationAsync(apiKeyId.Value);
|
||
|
|
|
||
|
|
// Assert - Revoked keys should not be returned for validation
|
||
|
|
validationDto.Should().BeNull();
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task GetByIdForValidation_ReturnsNull_ForNonExistentKey()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
var nonExistentId = ApiKeyId.New;
|
||
|
|
|
||
|
|
// Act
|
||
|
|
var repo = GetService<IApiKeyRepository>();
|
||
|
|
var validationDto = await repo.GetByIdForValidationAsync(nonExistentId.Value);
|
||
|
|
|
||
|
|
// Assert
|
||
|
|
validationDto.Should().BeNull();
|
||
|
|
}
|
||
|
|
}
|