Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,11 @@
* against expected definitions. Users should not interact with this class directly;
* they should use {@link AuditEntryDefinition} instead.</p>
*/
class AuditEntryExpectation {
public class AuditEntryExpectation {

private final AuditEntryDefinition definition;

AuditEntryExpectation(AuditEntryDefinition definition) {
public AuditEntryExpectation(AuditEntryDefinition definition) {
this.definition = definition;
}

Expand All @@ -48,7 +48,7 @@ class AuditEntryExpectation {
* @param actual the actual audit entry to compare against
* @return list of field mismatch errors (empty if all match)
*/
List<FieldMismatchError> compareWith(AuditEntry actual) {
public List<FieldMismatchError> compareWith(AuditEntry actual) {
List<FieldMismatchError> errors = new ArrayList<>();

// Required fields - always verified
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,33 +15,99 @@
*/
package io.flamingock.support.validation.impl;

import io.flamingock.internal.common.core.audit.AuditEntry;
import io.flamingock.internal.core.store.AuditStore;
import io.flamingock.support.domain.AuditEntryDefinition;
import io.flamingock.support.validation.SimpleValidator;
import io.flamingock.support.validation.error.CountMismatchError;
import io.flamingock.support.validation.error.ValidationError;
import io.flamingock.support.validation.error.ValidationResult;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

/**
* Validator that performs strict sequence validation of audit entries.
*
* <p>This validator verifies that the actual audit entries match the expected
* sequence exactly, both in count and in field values. Checking:</p>
* <ul>
* <li>Exact count match between expected and actual entries</li>
* <li>Strict field-by-field validation for each entry at each index</li>
* <li>Order preservation (expected[0] must match actual[0], etc.)</li>
* </ul>
*/
public class AuditSequenceStrictValidator implements SimpleValidator {

private static final String VALIDATOR_NAME = "Audit Sequence (Strict)";

private final AuditStore<?> auditStore;
private final List<AuditEntryExpectation> expectations;

private final List<AuditEntryExpectation> expectedExpectations;
private final List<AuditEntry> actualEntries;

/**
* Creates a strict sequence validator from an AuditStore and expected definitions.
*
* @param auditStore the audit store to read actual entries from
* @param definitions the expected audit entry definitions
*/
public AuditSequenceStrictValidator(AuditStore<?> auditStore, AuditEntryDefinition... definitions) {
this.auditStore = auditStore;
this.expectations = Arrays.stream(definitions)
this(Arrays.asList(definitions), auditStore.getPersistence().getAuditHistory());
}

/**
* Internal constructor for direct list initialization (used by tests).
*/
AuditSequenceStrictValidator(List<AuditEntryDefinition> expectedDefinitions, List<AuditEntry> actualEntries) {
this.expectedExpectations = expectedDefinitions.stream()
.map(AuditEntryExpectation::new)
.collect(Collectors.toList());
this.actualEntries = actualEntries != null ? actualEntries : new ArrayList<>();
}

@Override
public ValidationResult validate() {
// TODO: Implement actual validation logic
return ValidationResult.success(VALIDATOR_NAME);
// Check count
if (expectedExpectations.size() != actualEntries.size()) {
return ValidationResult.failure(VALIDATOR_NAME,
Comment thread
davidfrigolet marked this conversation as resolved.
Outdated
new CountMismatchError(getExpectedChangeIds(), getActualChangeIds()));
}

// Validate each entry in sequence
List<ValidationError> allErrors = getValidationErrors(expectedExpectations, actualEntries);

if (allErrors.isEmpty()) {
return ValidationResult.success(VALIDATOR_NAME);
}

return ValidationResult.failure(VALIDATOR_NAME, allErrors.toArray(
new io.flamingock.support.validation.error.ValidationError[0]));
}

private static List<ValidationError> getValidationErrors(List<AuditEntryExpectation> expectedExpectations, List<AuditEntry> actualEntries) {
Comment thread
davidfrigolet marked this conversation as resolved.
Outdated
List<ValidationError> allErrors = new ArrayList<>();
for (int i = 0; i < expectedExpectations.size(); i++) {
AuditEntryExpectation expected = expectedExpectations.get(i);
AuditEntry actual = actualEntries.get(i);
Comment thread
davidfrigolet marked this conversation as resolved.
Outdated

List<io.flamingock.support.validation.error.FieldMismatchError> entryErrors = expected.compareWith(actual);
Comment thread
davidfrigolet marked this conversation as resolved.
Outdated
if (!entryErrors.isEmpty()) {
allErrors.addAll(entryErrors);
}
Comment thread
davidfrigolet marked this conversation as resolved.
Outdated
}
return allErrors;
}

private List<String> getExpectedChangeIds() {
return expectedExpectations.stream()
.map(exp -> exp.getDefinition().getChangeId())
.collect(Collectors.toList());
}

private List<String> getActualChangeIds() {
return actualEntries.stream()
.map(AuditEntry::getTaskId)
.collect(Collectors.toList());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
/*
* Copyright 2025 Flamingock (https://www.flamingock.io)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.flamingock.support.validation.impl;

import io.flamingock.internal.common.core.audit.AuditEntry;
import io.flamingock.support.domain.AuditEntryDefinition;
import io.flamingock.support.validation.error.CountMismatchError;
import io.flamingock.support.validation.error.FieldMismatchError;
import io.flamingock.support.validation.error.ValidationResult;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import static io.flamingock.internal.common.core.audit.AuditEntry.ExecutionType.EXECUTION;
import static io.flamingock.internal.common.core.audit.AuditEntry.Status.APPLIED;
import static io.flamingock.internal.common.core.audit.AuditEntry.Status.FAILED;
import static io.flamingock.support.domain.AuditEntryDefinition.APPLIED;
import static org.junit.jupiter.api.Assertions.*;

class AuditSequenceStrictValidatorTest {

private List<AuditEntry> actualEntries;

@BeforeEach
Comment thread
davidfrigolet marked this conversation as resolved.
void setUp() {
actualEntries = Arrays.asList(
createAuditEntry("change-1", APPLIED),
createAuditEntry("change-2", APPLIED),
createAuditEntry("change-3", FAILED)
);
}

@Test
void shouldPassValidation_whenEntriesMatchExactly() {
List<AuditEntryDefinition> expectedDefinitions = Arrays.asList(
APPLIED("change-1"),
APPLIED("change-2"),
AuditEntryDefinition.FAILED("change-3")
Comment thread
davidfrigolet marked this conversation as resolved.
Outdated
);

AuditSequenceStrictValidator validator = new AuditSequenceStrictValidator(expectedDefinitions, actualEntries);
ValidationResult result = validator.validate();

assertTrue(result.isSuccess());
}

@Test
void shouldFailValidation_whenCountMismatch() {
List<AuditEntryDefinition> expectedDefinitions = Arrays.asList(
APPLIED("change-1"),
APPLIED("change-2")
);

AuditSequenceStrictValidator validator = new AuditSequenceStrictValidator(expectedDefinitions, actualEntries);
ValidationResult result = validator.validate();

assertFalse(result.isSuccess());
assertEquals(1, result.getErrors().size());
assertInstanceOf(CountMismatchError.class, result.getErrors().get(0));
}

@Test
void shouldFailValidation_whenStatusMismatch() {
List<AuditEntryDefinition> expectedDefinitions = Arrays.asList(
APPLIED("change-1"),
APPLIED("change-2"),
APPLIED("change-3") // Expected APPLIED but actual is FAILED
);

AuditSequenceStrictValidator validator = new AuditSequenceStrictValidator(expectedDefinitions, actualEntries);
ValidationResult result = validator.validate();

assertFalse(result.isSuccess());
assertEquals(1, result.getErrors().size());
assertInstanceOf(FieldMismatchError.class, result.getErrors().get(0));

FieldMismatchError error = (FieldMismatchError) result.getErrors().get(0);
assertEquals("status", error.getFieldName());
}

@Test
void shouldFailValidation_whenChangeIdMismatch() {
List<AuditEntryDefinition> expectedDefinitions = Arrays.asList(
APPLIED("change-1"),
APPLIED("wrong-id"), // Mismatch
AuditEntryDefinition.FAILED("change-3")
);

AuditSequenceStrictValidator validator = new AuditSequenceStrictValidator(expectedDefinitions, actualEntries);
ValidationResult result = validator.validate();

assertFalse(result.isSuccess());
assertEquals(1, result.getErrors().size());
assertInstanceOf(FieldMismatchError.class, result.getErrors().get(0));

FieldMismatchError error = (FieldMismatchError) result.getErrors().get(0);
assertEquals("changeId", error.getFieldName());
}

@Test
void shouldFailValidation_whenMissingEntry() {
List<AuditEntry> actualEntriesSubset = Arrays.asList(
createAuditEntry("change-1", APPLIED),
createAuditEntry("change-2", APPLIED)
);

List<AuditEntryDefinition> expectedDefinitions = Arrays.asList(
APPLIED("change-1"),
APPLIED("change-2"),
AuditEntryDefinition.FAILED("change-3")
);

AuditSequenceStrictValidator validator = new AuditSequenceStrictValidator(expectedDefinitions, actualEntriesSubset);
ValidationResult result = validator.validate();

assertFalse(result.isSuccess());
assertTrue(result.getErrors().stream().anyMatch(e -> e instanceof CountMismatchError));
}
Comment thread
davidfrigolet marked this conversation as resolved.

@Test
void shouldFailValidation_whenUnexpectedEntry() {
List<AuditEntry> actualEntriesExtra = Arrays.asList(
createAuditEntry("change-1", APPLIED),
createAuditEntry("change-2", APPLIED),
createAuditEntry("change-3", FAILED),
createAuditEntry("change-4", APPLIED)
);

List<AuditEntryDefinition> expectedDefinitions = Arrays.asList(
APPLIED("change-1"),
APPLIED("change-2"),
AuditEntryDefinition.FAILED("change-3")
);

AuditSequenceStrictValidator validator = new AuditSequenceStrictValidator(expectedDefinitions, actualEntriesExtra);
ValidationResult result = validator.validate();

assertFalse(result.isSuccess());
assertTrue(result.getErrors().stream().anyMatch(e -> e instanceof CountMismatchError));
}
Comment thread
davidfrigolet marked this conversation as resolved.

@Test
void shouldPassValidation_whenOptionalFieldsMatch() {
AuditEntry actualWithOptionalFields = new AuditEntry(
"exec-1",
"stage-1",
"change-1",
"author",
LocalDateTime.now(),
APPLIED,
EXECUTION,
"com.example.Change",
"apply",
100L,
"host",
null,
false,
null,
null,
"target-1",
"1",
null,
true
);

AuditEntryDefinition expectedWithOptionalFields = APPLIED("change-1")
.withAuthor("author")
.withClassName("com.example.Change")
.withMethodName("apply")
.withTargetSystemId("target-1")
.withOrder("1")
.withTransactional(true);

AuditSequenceStrictValidator validator = new AuditSequenceStrictValidator(
Collections.singletonList(expectedWithOptionalFields),
Collections.singletonList(actualWithOptionalFields)
);

ValidationResult result = validator.validate();

assertTrue(result.isSuccess());
}

@Test
void shouldFailValidation_whenOptionalFieldMismatch() {
AuditEntry actualEntry = new AuditEntry(
"exec-1",
"stage-1",
"change-1",
"author",
LocalDateTime.now(),
APPLIED,
EXECUTION,
"com.example.Change",
"apply",
100L,
"host",
null,
false,
null,
null,
"target-1",
null,
null,
null
);

AuditEntryDefinition expectedWithDifferentOptional = APPLIED("change-1")
.withTargetSystemId("different-target");

AuditSequenceStrictValidator validator = new AuditSequenceStrictValidator(
Collections.singletonList(expectedWithDifferentOptional),
Collections.singletonList(actualEntry)
);

ValidationResult result = validator.validate();

assertFalse(result.isSuccess());
assertEquals(1, result.getErrors().size());
assertInstanceOf(FieldMismatchError.class, result.getErrors().get(0));

FieldMismatchError error = (FieldMismatchError) result.getErrors().get(0);
assertEquals("targetSystemId", error.getFieldName());
}

private AuditEntry createAuditEntry(String changeId, AuditEntry.Status status) {
return new AuditEntry(
"exec-id",
"stage-id",
changeId,
"test-author",
LocalDateTime.now(),
status,
EXECUTION,
"com.example.TestChange",
"apply",
100L,
"localhost",
null,
false,
null,
null,
null,
null,
null,
null
);
}
}
Loading