books/backend/Books.Api.Tests/Domain/ApiKeyIntegrationTests.cs
Nicolaj Hartmann 1f75c5d791 Add all backend domain, commands, repositories, and tests
This commit includes all previously untracked backend files:

Domain:
- Accounts, Attachments, BankConnections, Customers
- FiscalYears, Invoices, JournalEntryDrafts
- Orders, Products, UserAccess

Commands & Handlers:
- Full CQRS command structure for all domains

Repositories:
- PostgreSQL repositories for all read models
- Bank transaction and ledger repositories

GraphQL:
- Input types, scalars, and types for all entities
- Mutations and queries

Infrastructure:
- Banking integration (Enable Banking client)
- File storage, Invoicing, Reporting, SAF-T export
- Database migrations (003-029)

Tests:
- Integration tests for GraphQL endpoints
- Domain tests
- Invoicing and reporting tests

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 22:19:42 +01:00

282 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();
}
}