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; /// /// Integration tests for ApiKey domain operations. /// Tests the ApiKeyAggregate via CommandBus since ApiKeys are not exposed via GraphQL. /// [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(); 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(); 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() .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(); 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(); 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(); 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(); 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() .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(); 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(); 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(); 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(); 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(); 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(); var validationDto = await repo.GetByIdForValidationAsync(nonExistentId.Value); // Assert validationDto.Should().BeNull(); } }