diff --git a/backend/audit-writer-service/src/test/java/lt/satsyuk/distributed/audit/auditwriter/blockchain/AuditLedgerContractGanacheTest.java b/backend/audit-writer-service/src/test/java/lt/satsyuk/distributed/audit/auditwriter/blockchain/AuditLedgerContractGanacheTest.java index dc5c9bc7..7790560a 100644 --- a/backend/audit-writer-service/src/test/java/lt/satsyuk/distributed/audit/auditwriter/blockchain/AuditLedgerContractGanacheTest.java +++ b/backend/audit-writer-service/src/test/java/lt/satsyuk/distributed/audit/auditwriter/blockchain/AuditLedgerContractGanacheTest.java @@ -56,6 +56,7 @@ */ @Testcontainers(disabledWithoutDocker = true) class AuditLedgerContractGanacheTest { + private static final Instant FIXED_EVENT_TIME = Instant.parse("2026-05-15T10:15:30Z"); /** * Deterministic account 0 private key for Ganache mnemonic @@ -111,7 +112,7 @@ static void shutdown() { @Test void appendAuditRecord_anchorsHashAndIsHashExistsConfirmsIt() { byte[] hash = randomHash(); - BigInteger timestamp = BigInteger.valueOf(Instant.now().getEpochSecond()); + BigInteger timestamp = BigInteger.valueOf(FIXED_EVENT_TIME.getEpochSecond()); assertThat(contract.isHashExists(hash)) .as("hash must not exist before anchoring") diff --git a/backend/audit-writer-service/src/test/java/lt/satsyuk/distributed/audit/auditwriter/service/BlockchainWriterServiceTest.java b/backend/audit-writer-service/src/test/java/lt/satsyuk/distributed/audit/auditwriter/service/BlockchainWriterServiceTest.java index a676b3d3..57e24787 100644 --- a/backend/audit-writer-service/src/test/java/lt/satsyuk/distributed/audit/auditwriter/service/BlockchainWriterServiceTest.java +++ b/backend/audit-writer-service/src/test/java/lt/satsyuk/distributed/audit/auditwriter/service/BlockchainWriterServiceTest.java @@ -45,6 +45,7 @@ */ @ExtendWith(MockitoExtension.class) class BlockchainWriterServiceTest { + private static final Instant FIXED_NOW = Instant.parse("2026-05-15T10:15:30Z"); @Mock private Web3j web3j; @Mock private Credentials credentials; @@ -300,7 +301,7 @@ void anchorEvent_failsFastOnMissingEventId() { // eventId is null; occurredAt and eventType are valid → should fail on eventId check UserLoggedInEvent event = UserLoggedInEvent.builder().build(); event.setEventType(EventType.USER_LOGGED_IN); - event.setOccurredAt(Instant.now()); + event.setOccurredAt(FIXED_NOW); assertThatThrownBy(() -> service.anchorEvent(event)) .isInstanceOf(BlockchainWriterService.NonRecoverableEventException.class) @@ -312,7 +313,7 @@ void anchorEvent_failsFastOnMissingEventType() { // eventId and occurredAt are valid; eventType is null → should fail on eventType check UserLoggedInEvent event = UserLoggedInEvent.builder().build(); event.setEventId("00000000-0000-0000-0000-000000000001"); - event.setOccurredAt(Instant.now()); + event.setOccurredAt(FIXED_NOW); // eventType intentionally left null assertThatThrownBy(() -> service.anchorEvent(event)) @@ -370,7 +371,7 @@ void anchorEvent_allowsNullUserIdToStayAlignedWithEventStoreFallbacks() { void anchorEvent_allowsFutureTimestampBeyondDefaultToleranceToStayAlignedWithEventStore() { // Even when beyond tolerance, audit-writer should not DLT the record only due to clock skew. UserLoggedInEvent event = UserLoggedInEvent.of("u1", null, null); - event.setOccurredAt(Instant.now().plusSeconds(360)); + event.setOccurredAt(FIXED_NOW.plusSeconds(360)); TransactionReceipt receipt = new TransactionReceipt(); receipt.setStatus("0x1"); @@ -425,7 +426,7 @@ void anchorEvent_rejectsEventIdWithWrongLength() { void anchorEvent_acceptsFutureTimestampWithinDefaultTolerance() { // Default tolerance is 300 seconds; use 299 seconds in future UserLoggedInEvent event = UserLoggedInEvent.of("u1", null, null); - event.setOccurredAt(Instant.now().plusSeconds(299)); + event.setOccurredAt(FIXED_NOW.plusSeconds(299)); TransactionReceipt receipt = new TransactionReceipt(); receipt.setStatus("0x1"); @@ -448,7 +449,7 @@ void anchorEvent_acceptsFutureTimestampWithinDefaultTolerance() { void anchorEvent_allowsFutureTimestampBeyondCustomToleranceToStayAlignedWithEventStore() { props.setFutureTimestampToleranceSeconds(60); UserLoggedInEvent event = UserLoggedInEvent.of("u1", null, null); - event.setOccurredAt(Instant.now().plusSeconds(120)); + event.setOccurredAt(FIXED_NOW.plusSeconds(120)); TransactionReceipt receipt = new TransactionReceipt(); receipt.setStatus("0x1"); diff --git a/backend/command-service/src/main/java/lt/satsyuk/distributed/audit/command/config/KafkaProducerConfig.java b/backend/command-service/src/main/java/lt/satsyuk/distributed/audit/command/config/KafkaProducerConfig.java index f909bdac..d5a9ae3f 100644 --- a/backend/command-service/src/main/java/lt/satsyuk/distributed/audit/command/config/KafkaProducerConfig.java +++ b/backend/command-service/src/main/java/lt/satsyuk/distributed/audit/command/config/KafkaProducerConfig.java @@ -1,10 +1,10 @@ package lt.satsyuk.distributed.audit.command.config; import lt.satsyuk.distributed.audit.event.AuditEvent; -import org.springframework.boot.context.properties.bind.Bindable; -import org.springframework.boot.context.properties.bind.Binder; import org.apache.kafka.clients.producer.ProducerConfig; import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.EnumerablePropertySource; +import org.springframework.core.env.PropertySource; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -13,7 +13,7 @@ import org.springframework.kafka.core.KafkaTemplate; import org.springframework.kafka.core.ProducerFactory; import org.apache.kafka.common.serialization.StringSerializer; -import org.springframework.kafka.support.serializer.JsonSerializer; +import org.springframework.kafka.support.serializer.JacksonJsonSerializer; import java.util.Map; import java.util.HashMap; @@ -39,7 +39,7 @@ public ProducerFactory auditEventProducerFactory( mergeKafkaOverrides(props, environment); props.putIfAbsent(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); props.putIfAbsent(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); - props.putIfAbsent(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class); + props.putIfAbsent(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JacksonJsonSerializer.class); props.putIfAbsent(JSON_ADD_TYPE_HEADERS_CONFIG, false); return new DefaultKafkaProducerFactory<>(props); } @@ -54,19 +54,34 @@ public KafkaTemplate auditEventKafkaTemplate( private static void mergeKafkaOverrides(Map props, ConfigurableEnvironment environment) { - Binder binder = Binder.get(environment); + for (PropertySource propertySource : environment.getPropertySources()) { + if (propertySource instanceof EnumerablePropertySource enumerablePropertySource) { + mergeKafkaOverridesFromPropertySource(props, enumerablePropertySource, environment); + } + } + } - binder.bind("spring.kafka.properties", Bindable.mapOf(String.class, String.class)) - .ifBound(props::putAll); + private static void mergeKafkaOverridesFromPropertySource(Map props, + EnumerablePropertySource propertySource, + ConfigurableEnvironment environment) { + for (String propertyName : propertySource.getPropertyNames()) { + if (propertyName.startsWith("spring.kafka.properties.")) { + String kafkaKey = propertyName.substring("spring.kafka.properties.".length()); + putIfPresent(props, kafkaKey, environment.getProperty(propertyName)); + } else if (propertyName.startsWith("spring.kafka.producer.")) { + String producerKey = propertyName.substring("spring.kafka.producer.".length()); + String kafkaKey = producerKey.startsWith("properties.") + ? producerKey.substring("properties.".length()) + : producerKey.replace('-', '.'); + putIfPresent(props, kafkaKey, environment.getProperty(propertyName)); + } + } + } - binder.bind("spring.kafka.producer", Bindable.mapOf(String.class, String.class)) - .ifBound(m -> m.forEach((key, value) -> { - if (key.startsWith("properties.")) { - props.put(key.substring("properties.".length()), value); - } else { - props.put(key.replace('-', '.'), value); - } - })); + private static void putIfPresent(Map props, String key, String value) { + if (value != null) { + props.put(key, value); + } } } diff --git a/backend/command-service/src/test/java/lt/satsyuk/distributed/audit/command/config/KafkaProducerConfigIntegrationTest.java b/backend/command-service/src/test/java/lt/satsyuk/distributed/audit/command/config/KafkaProducerConfigIntegrationTest.java index 53aad286..860eca63 100644 --- a/backend/command-service/src/test/java/lt/satsyuk/distributed/audit/command/config/KafkaProducerConfigIntegrationTest.java +++ b/backend/command-service/src/test/java/lt/satsyuk/distributed/audit/command/config/KafkaProducerConfigIntegrationTest.java @@ -32,7 +32,7 @@ class KafkaProducerConfigIntegrationTest { private static final String JSON_SERIALIZER_FQCN = - "org.springframework.kafka.support.serializer.JsonSerializer"; + "org.springframework.kafka.support.serializer.JacksonJsonSerializer"; private final KafkaTemplate auditEventKafkaTemplate; private final ProducerFactory auditEventProducerFactory; diff --git a/backend/common/shared-contracts/src/test/java/lt/satsyuk/distributed/audit/contracts/auth/security/JwtSecurityComponentsTest.java b/backend/common/shared-contracts/src/test/java/lt/satsyuk/distributed/audit/contracts/auth/security/JwtSecurityComponentsTest.java index bec3a429..d56700ce 100644 --- a/backend/common/shared-contracts/src/test/java/lt/satsyuk/distributed/audit/contracts/auth/security/JwtSecurityComponentsTest.java +++ b/backend/common/shared-contracts/src/test/java/lt/satsyuk/distributed/audit/contracts/auth/security/JwtSecurityComponentsTest.java @@ -18,6 +18,7 @@ import static org.assertj.core.api.Assertions.assertThat; class JwtSecurityComponentsTest { + private static final Instant FIXED_ISSUED_AT = Instant.parse("2099-05-15T10:15:30Z"); @Test void bearerTokenConverterExtractsTokenFromAuthorizationHeader() { @@ -52,7 +53,7 @@ void bearerTokenConverterAcceptsLowercaseBearerScheme() { @Test void jwtAuthenticationManagerMapsRolesToGrantedAuthorities() { JwtService jwtService = new JwtService("shared-security-test-secret-123456", "dal-test", Duration.ofMinutes(30)); - String token = jwtService.generateToken("auditor", Set.of(UserRole.AUDITOR, UserRole.ADMIN), Instant.now()); + String token = jwtService.generateToken("auditor", Set.of(UserRole.AUDITOR, UserRole.ADMIN), FIXED_ISSUED_AT); JwtTokenReactiveAuthenticationManager authenticationManager = new JwtTokenReactiveAuthenticationManager(jwtService); diff --git a/backend/event-store-service/src/test/java/lt/satsyuk/distributed/audit/eventstore/service/EventPersistenceServiceTest.java b/backend/event-store-service/src/test/java/lt/satsyuk/distributed/audit/eventstore/service/EventPersistenceServiceTest.java index e941be73..9b7191d3 100644 --- a/backend/event-store-service/src/test/java/lt/satsyuk/distributed/audit/eventstore/service/EventPersistenceServiceTest.java +++ b/backend/event-store-service/src/test/java/lt/satsyuk/distributed/audit/eventstore/service/EventPersistenceServiceTest.java @@ -19,6 +19,7 @@ import java.time.Instant; import java.time.LocalDateTime; +import java.time.Month; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -72,7 +73,7 @@ void persistMapsUserLoginEventToEntityAndStoresHash() { assertEquals(64, saved.getEventHash().length()); assertEquals("661fa908d63fb896bf2f5b8aca9b0db207a73e99d04af2216005bdb98237607b", saved.getEventHash()); assertNotNull(saved.getCreatedAt()); - assertEquals(LocalDateTime.of(2026, 5, 15, 10, 15, 30), saved.getCreatedAt()); + assertEquals(LocalDateTime.of(2026, Month.MAY, 15, 10, 15, 30), saved.getCreatedAt()); ArgumentCaptor captor = ArgumentCaptor.forClass(StoredAuditEvent.class); verify(repository).save(captor.capture()); diff --git a/backend/query-service/src/test/java/lt/satsyuk/distributed/audit/query/integration/IntegrityCheckIntegrationTest.java b/backend/query-service/src/test/java/lt/satsyuk/distributed/audit/query/integration/IntegrityCheckIntegrationTest.java index 1fc8518b..530c887c 100644 --- a/backend/query-service/src/test/java/lt/satsyuk/distributed/audit/query/integration/IntegrityCheckIntegrationTest.java +++ b/backend/query-service/src/test/java/lt/satsyuk/distributed/audit/query/integration/IntegrityCheckIntegrationTest.java @@ -44,6 +44,7 @@ class IntegrityCheckIntegrationTest { /** Valid 64-char hex hash used across multiple test cases. */ private static final String HASH_64 = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; + private static final Instant FIXED_ISSUED_AT = Instant.parse("2099-05-15T10:15:30Z"); private final JwtService jwtService; @SuppressWarnings("resource") @@ -321,7 +322,7 @@ private String bearerToken(UserRole... roles) { return "Bearer " + jwtService.generateToken( "integration-user", Set.of(roles), - Instant.now() + FIXED_ISSUED_AT ); } } diff --git a/backend/query-service/src/test/java/lt/satsyuk/distributed/audit/query/mapper/AuditEventDtoMapperTest.java b/backend/query-service/src/test/java/lt/satsyuk/distributed/audit/query/mapper/AuditEventDtoMapperTest.java index 1404cad1..a0d276a8 100644 --- a/backend/query-service/src/test/java/lt/satsyuk/distributed/audit/query/mapper/AuditEventDtoMapperTest.java +++ b/backend/query-service/src/test/java/lt/satsyuk/distributed/audit/query/mapper/AuditEventDtoMapperTest.java @@ -7,6 +7,7 @@ import org.mapstruct.factory.Mappers; import java.time.LocalDateTime; +import java.time.Month; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -23,7 +24,7 @@ void toDtoMapsCoreFields() { eventRecord.setUserId("user-15"); eventRecord.setPayload("{\"userId\":\"user-15\"}"); eventRecord.setEventHash("abc123xyz"); - eventRecord.setCreatedAt(LocalDateTime.of(2026, 5, 15, 10, 30, 0)); + eventRecord.setCreatedAt(LocalDateTime.of(2026, Month.MAY, 15, 10, 30, 0)); AuditEventDto dto = mapper.toDto(eventRecord); diff --git a/backend/query-service/src/test/java/lt/satsyuk/distributed/audit/query/service/AuditLogQueryServiceTest.java b/backend/query-service/src/test/java/lt/satsyuk/distributed/audit/query/service/AuditLogQueryServiceTest.java index d1ec9855..51b34630 100644 --- a/backend/query-service/src/test/java/lt/satsyuk/distributed/audit/query/service/AuditLogQueryServiceTest.java +++ b/backend/query-service/src/test/java/lt/satsyuk/distributed/audit/query/service/AuditLogQueryServiceTest.java @@ -16,6 +16,7 @@ import java.time.Instant; import java.time.LocalDateTime; +import java.time.Month; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -126,7 +127,7 @@ private AuditEventRecord sampleRecord() { eventRecord.setEventId("event-101"); eventRecord.setEventType("USER_LOGGED_IN"); eventRecord.setUserId("user-1"); - eventRecord.setCreatedAt(LocalDateTime.of(2026, 5, 16, 10, 0)); + eventRecord.setCreatedAt(LocalDateTime.of(2026, Month.MAY, 16, 10, 0)); eventRecord.setPayload("{\"userId\":\"user-1\"}"); return eventRecord; } diff --git a/frontend/audit-ui/src/app/features/audit-dashboard/audit-dashboard.component.html b/frontend/audit-ui/src/app/features/audit-dashboard/audit-dashboard.component.html index bd6f66f1..00757c52 100644 --- a/frontend/audit-ui/src/app/features/audit-dashboard/audit-dashboard.component.html +++ b/frontend/audit-ui/src/app/features/audit-dashboard/audit-dashboard.component.html @@ -98,8 +98,10 @@

Payload

Payload search @@ -107,19 +109,33 @@

Payload

Event type - + User ID - + Date range - - + + diff --git a/frontend/audit-ui/src/app/features/auth-login/auth-login.component.html b/frontend/audit-ui/src/app/features/auth-login/auth-login.component.html index 88acff38..5d804058 100644 --- a/frontend/audit-ui/src/app/features/auth-login/auth-login.component.html +++ b/frontend/audit-ui/src/app/features/auth-login/auth-login.component.html @@ -5,12 +5,12 @@

Sign In