Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -42,7 +42,7 @@
* FAILED(FailedMigrationChange.class)
* )
* .whenRun()
* .thenExpectAuditSequenceStrict(
* .thenExpectAuditFinalStateSequence(
* APPLIED(SchemaV2Change.class)
* .withAuthor("dev-team"),
* APPLIED(DataMigrationChange.class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@
* APPLIED(MigrationV1.class)
* )
* .whenRun()
* .thenExpectAuditSequenceStrict(
* .thenExpectAuditFinalStateSequence(
* APPLIED(NewChange.class)
* )
* .verify();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,10 @@
* <h2>Example with Multiple Assertions</h2>
* <pre>{@code
* .whenRun()
* .thenExpectAuditSequenceStrict(
* .thenExpectAuditFinalStateSequence(
* AuditEntryDefinition.APPLIED("change-1")
* )
* .andExpectAuditSequenceStrict(
* .andExpectAuditFinalStateSequence(
* AuditEntryDefinition.APPLIED("change-2")
* )
* .verify(); // Execution and verification happen here
Expand All @@ -52,24 +52,23 @@
public interface ThenStage {

/**
* Adds an additional strict expectation for the audit entry sequence.
* Adds an additional expectation that the final state sequence of audit entries matches
* the given definitions exactly.
*
* <p>This method has the same semantics as
* {@link WhenStage#thenExpectAuditSequenceStrict(AuditEntryDefinition...)}
* but allows chaining multiple sequence expectations.</p>
* <p>This validates only <strong>final states</strong> (APPLIED, FAILED, ROLLED_BACK, ROLLBACK_FAILED),
* filtering out intermediate states like STARTED.</p>
*
* <p><strong>Exact matching:</strong> The number of definitions must match the number of
* final-state entries in the audit log. This is not a subset check.</p>
*
* <p><b>Strict validation</b> means:</p>
* <ul>
* <li>The number of actual audit entries must exactly match the number of definitions</li>
* <li>The order of audit entries must exactly match the order of definitions</li>
* <li>Each audit entry is validated against its corresponding definition</li>
* </ul>
* <p><strong>Field validation:</strong> Only fields set in each definition are validated.
* See {@link AuditEntryDefinition} for details on class-based vs string-based factories.</p>
*
* @param definitions the expected audit entries in exact order
* @param definitions the expected audit entry definitions, in exact order
* @return this stage for method chaining
* @see AuditEntryDefinition
*/
ThenStage andExpectAuditSequenceStrict(AuditEntryDefinition... definitions);
ThenStage andExpectAuditFinalStateSequence(AuditEntryDefinition... definitions);

/**
* Adds an expectation that the execution should throw a specific exception.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
import io.flamingock.support.precondition.PreconditionInserter;
import io.flamingock.support.validation.ValidationHandler;
import io.flamingock.support.validation.ValidatorArgs;
import io.flamingock.support.validation.impl.AuditSequenceStrictValidator;
import io.flamingock.support.validation.impl.AuditFinalStateSequenceValidator;
import io.flamingock.support.validation.impl.DefaultExceptionValidator;

import java.util.ArrayList;
Expand All @@ -39,9 +39,9 @@ final class ThenStageImpl implements ThenStage {
}

@Override
public ThenStage andExpectAuditSequenceStrict(AuditEntryDefinition... definitions) {
public ThenStage andExpectAuditFinalStateSequence(AuditEntryDefinition... definitions) {
List<AuditEntryDefinition> definitionsList = definitions != null ? Arrays.asList(definitions) : Collections.<AuditEntryDefinition>emptyList();
validators.add(new AuditSequenceStrictValidator.Args(definitionsList));
validators.add(new AuditFinalStateSequenceValidator.Args(definitionsList));
return this;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
* <h2>Example</h2>
* <pre>{@code
* .whenRun()
* .thenExpectAuditSequenceStrict(
* .thenExpectAuditFinalStateSequence(
* AuditEntryDefinition.APPLIED("change-1"),
* AuditEntryDefinition.APPLIED("change-2")
* )
Expand All @@ -52,23 +52,24 @@
public interface WhenStage {

/**
* Defines a strict expectation for the audit entry sequence.
* Asserts that the final state sequence of audit entries matches the given definitions exactly.
*
* <p><b>Strict validation</b> means:</p>
* <ul>
* <li>The number of actual audit entries must exactly match the number of definitions</li>
* <li>The order of audit entries must exactly match the order of definitions</li>
* <li>Each audit entry is validated against its corresponding definition</li>
* </ul>
* <p>This validates only <strong>final states</strong> (APPLIED, FAILED, ROLLED_BACK, ROLLBACK_FAILED),
* filtering out intermediate states like STARTED. The actual audit log may contain multiple entries
* per change (e.g., STARTED then APPLIED), but only the final outcome is validated.</p>
*
* <p>For each {@link AuditEntryDefinition}, only the fields explicitly set via
* {@code withXxx()} methods are verified. The change ID and status are always verified.</p>
* <p><strong>Exact matching:</strong> The number of definitions must match the number of
* final-state entries in the audit log. This is not a subset check.</p>
*
* @param definitions the expected audit entries in exact order
* @return the {@link ThenStage} for chaining additional assertions or calling {@code verify()}
* <p><strong>Field validation:</strong> Only fields set in each definition are validated.
* Use class-based factories (e.g., {@code APPLIED(MyChange.class)}) to auto-extract fields
* from annotations, or string-based factories with {@code withXxx()} methods for fine-grained control.</p>
*
* @param definitions the expected audit entry definitions, in exact order
* @return a ThenStage for adding additional expectations or calling verify()
* @see AuditEntryDefinition
*/
ThenStage thenExpectAuditSequenceStrict(AuditEntryDefinition... definitions);
ThenStage thenExpectAuditFinalStateSequence(AuditEntryDefinition... definitions);

/**
* Defines an expectation that the execution should throw a specific exception.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ public class WhenStageImpl implements WhenStage {
}

@Override
public ThenStage thenExpectAuditSequenceStrict(AuditEntryDefinition... definitions) {
return new ThenStageImpl(testContext).andExpectAuditSequenceStrict(definitions);
public ThenStage thenExpectAuditFinalStateSequence(AuditEntryDefinition... definitions) {
return new ThenStageImpl(testContext).andExpectAuditFinalStateSequence(definitions);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
* <h2>Implementation</h2>
* <p>Implementations are typically static inner classes within their corresponding validators:</p>
* <pre>{@code
* public class AuditSequenceStrictValidator implements SimpleValidator {
* public class AuditFinalStateSequenceValidator implements SimpleValidator {
* // ... validator implementation
*
* public static class Args implements ValidatorArgs {
Expand All @@ -55,7 +55,7 @@
* <h2>Usage in Framework</h2>
* <p>The framework uses these argument carriers as follows:</p>
* <ol>
* <li>User calls expectation method (e.g., {@code thenExpectAuditSequenceStrict(...)})</li>
* <li>User calls expectation method (e.g., {@code thenExpectAuditFinalStateSequence(...)})</li>
* <li>Stage creates an {@code Args} instance and stores it in the validators list</li>
* <li>When {@code verify()} is called, the {@link ValidationHandler} constructs actual
* validators using {@link ValidatorFactory#getValidator(ValidatorArgs)}</li>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

import io.flamingock.internal.common.core.audit.AuditReader;
import io.flamingock.support.domain.AuditEntryDefinition;
import io.flamingock.support.validation.impl.AuditSequenceStrictValidator;
import io.flamingock.support.validation.impl.AuditFinalStateSequenceValidator;
import io.flamingock.support.validation.impl.DefaultExceptionValidator;

import java.util.List;
Expand All @@ -31,18 +31,18 @@ public ValidatorFactory(AuditReader auditReader) {
this.auditReader = auditReader;
}

public Validator getAuditSeqStrictValidator(List<AuditEntryDefinition> definitions) {
return new AuditSequenceStrictValidator(auditReader, definitions);
public Validator getAuditFinalStateSequenceValidator(List<AuditEntryDefinition> definitions) {
return new AuditFinalStateSequenceValidator(auditReader, definitions);
}

public Validator getExceptionValidator(Class<? extends Throwable> exceptionClass, Consumer<Throwable> exceptionConsumer) {
return new DefaultExceptionValidator(exceptionClass, exceptionConsumer);
}

public Validator getValidator(ValidatorArgs args) {
if (args instanceof AuditSequenceStrictValidator.Args) {
AuditSequenceStrictValidator.Args a = (AuditSequenceStrictValidator.Args) args;
return new AuditSequenceStrictValidator(auditReader, a.getExpectations());
if (args instanceof AuditFinalStateSequenceValidator.Args) {
AuditFinalStateSequenceValidator.Args a = (AuditFinalStateSequenceValidator.Args) args;
return new AuditFinalStateSequenceValidator(auditReader, a.getExpectations());
}

if (args instanceof DefaultExceptionValidator.Args) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
* against expected definitions. Users should not interact with this class directly;
* they should use {@link AuditEntryDefinition} instead.</p>
*/

public class AuditEntryExpectation {

private final AuditEntryDefinition definition;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import io.flamingock.internal.common.core.audit.AuditEntry;
import io.flamingock.internal.common.core.audit.AuditReader;
import io.flamingock.support.domain.AuditEntryDefinition;
import io.flamingock.support.stages.ThenStage;
import io.flamingock.support.validation.SimpleValidator;
import io.flamingock.support.validation.ValidatorArgs;
import io.flamingock.support.validation.error.*;
Expand All @@ -28,36 +29,66 @@
import java.util.stream.Collectors;

/**
* Validator that performs strict sequence validation of audit entries.
* Validates that the final state sequence of audit entries matches the expected definitions exactly.
*
* <p>This validator verifies that the actual audit entries match the expected
* sequence exactly, both in count and in field values. Checking:</p>
* <p>This validator focuses on the <strong>final state</strong> of each change, filtering out
* intermediate states like {@code STARTED}. It validates only the states that represent
* actual outcomes: {@code APPLIED}, {@code FAILED}, {@code ROLLED_BACK}, {@code ROLLBACK_FAILED}.</p>
*
* <p><strong>Exact sequence validation:</strong></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>
* <li>The number of actual entries must exactly match the number of expected definitions</li>
* <li>Order is preserved: expected[0] must match actual[0], expected[1] must match actual[1], etc.</li>
* </ul>
*
* <p>This is not a "contains" validator - if the audit log has 3 changes, you must
* provide exactly 3 expected definitions.</p>
*
* <p><strong>Field validation:</strong></p>
* <p>Only fields that are set in the {@link AuditEntryDefinition} are validated. The {@code changeId}
* and {@code state} are always compared (they are required). Additional fields depend on how the
* definition is constructed:</p>
* <ul>
* <li>Class-based factory methods (e.g., {@code APPLIED(MyChange.class)}) auto-extract fields
* from annotations: author, className, methodName, targetSystemId, recoveryStrategy, order, transactional</li>
* <li>String-based factory methods (e.g., {@code APPLIED("change-id")}) only set changeId and state</li>
* <li>Use {@code withXxx()} methods to add or override specific fields to validate</li>
* <li>Fields that are null in the definition are not compared</li>
* </ul>
*
* <p><strong>Example:</strong></p>
* <pre>{@code
* testSupport.givenBuilderFromContext()
* .whenRun()
* .thenExpectAuditFinalStateSequence(
* APPLIED(CreateUsersChange.class), // validates fields from annotations
* APPLIED("template-change-id"), // validates only changeId and state
* FAILED(BrokenChange.class).withErrorTrace("Expected error") // adds error trace validation
* )
* .verify();
* }</pre>
*
* @see AuditEntryDefinition
* @see ThenStage#andExpectAuditFinalStateSequence(AuditEntryDefinition...)
*/
public class AuditSequenceStrictValidator implements SimpleValidator {
public class AuditFinalStateSequenceValidator implements SimpleValidator {

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

private final AuditReader auditReader;
private final List<AuditEntryExpectation> expectations;
private final List<AuditEntry> actualEntries;
private static final List<AuditEntry.Status> EXCLUDED_STATES = Collections.singletonList(
AuditEntry.Status.STARTED
);

public AuditSequenceStrictValidator(AuditReader auditReader, List<AuditEntryDefinition> definitions) {
this.auditReader = auditReader;
public AuditFinalStateSequenceValidator(AuditReader auditReader, List<AuditEntryDefinition> definitions) {
this.expectations = definitions != null
? definitions.stream()
.map(AuditEntryExpectation::new)
.collect(Collectors.toList())
: new ArrayList<>();

this.actualEntries = auditReader.getAuditHistory().stream()
this.actualEntries = auditReader.getAuditHistory().stream()
.filter(entry -> !EXCLUDED_STATES.contains(entry.getState()))
.sorted()
.collect(Collectors.toList());
Expand All @@ -66,13 +97,12 @@ public AuditSequenceStrictValidator(AuditReader auditReader, List<AuditEntryDefi
/**
* Internal constructor for direct list initialization (used by tests).
*/
AuditSequenceStrictValidator(List<AuditEntryDefinition> expectedDefinitions, List<AuditEntry> actualEntries, AuditReader auditReader) {
AuditFinalStateSequenceValidator(List<AuditEntryDefinition> expectedDefinitions, List<AuditEntry> actualEntries) {
this.expectations = expectedDefinitions != null
? expectedDefinitions.stream()
.map(AuditEntryExpectation::new)
.collect(Collectors.toList())
: new ArrayList<>();
this.auditReader = auditReader;
this.actualEntries = actualEntries != null ? actualEntries : new ArrayList<>();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ void shouldExecuteNonTransactionalChange() {
FlamingockTestSupport
.givenBuilder(testKit.createBuilder().addTargetSystem(targetSystem))
.whenRun()
.thenExpectAuditSequenceStrict(
.thenExpectAuditFinalStateSequence(
APPLIED(_001__SimpleNonTransactionalChange.class)
)
.verify();
Expand Down Expand Up @@ -80,7 +80,7 @@ void shouldVerifyMultipleChangesInSequence() {
.addTargetSystem(new NonTransactionalTargetSystem("elasticsearch"))
.addTargetSystem(new NonTransactionalTargetSystem("s3")))
.whenRun()
.thenExpectAuditSequenceStrict(
.thenExpectAuditFinalStateSequence(
APPLIED(_003__MultiTest1NonTransactionalChange.class),
APPLIED(_004__MultiTest2TransactionalChange.class)
)
Expand Down Expand Up @@ -108,7 +108,7 @@ void shouldVerifyFailingTransactionalChangeTriggersRollback() {
.addTargetSystem(new NonTransactionalTargetSystem("s3")))
.whenRun()
.thenExpectException(PipelineExecutionException.class, null)
.andExpectAuditSequenceStrict(
.andExpectAuditFinalStateSequence(
FAILED(_006__FailingTransactionalChange.class),
ROLLED_BACK(_006__FailingTransactionalChange.class)
)
Expand Down Expand Up @@ -138,7 +138,7 @@ void shouldVerifyAlreadyAppliedChangesAreSkipped() {
APPLIED(_005__SecondRunNonTransactionalChange.class)
)
.whenRun()
.thenExpectAuditSequenceStrict(
.thenExpectAuditFinalStateSequence(
APPLIED(_005__SecondRunNonTransactionalChange.class)
)
.verify();
Expand Down Expand Up @@ -171,7 +171,7 @@ void shouldVerifyDependencyInjectionInRollbackForNonTransactionalChanges() {
assertTrue(counter.isExecuted(), "Counter should be executed");
assertTrue(counter.isRollbacked(), "Counter should be rolled back");
})
.andExpectAuditSequenceStrict(
.andExpectAuditFinalStateSequence(
FAILED(_007__SimpleNonTransactionalChangeWithError.class),
ROLLED_BACK(_007__SimpleNonTransactionalChangeWithError.class)
)
Expand All @@ -197,7 +197,7 @@ void shouldVerifyTransactionalChangeExecutesSuccessfully() {
.addTargetSystem(new NonTransactionalTargetSystem("elasticsearch"))
.addTargetSystem(new NonTransactionalTargetSystem("s3")))
.whenRun()
.thenExpectAuditSequenceStrict(
.thenExpectAuditFinalStateSequence(
APPLIED(_002__SimpleTransactionalChange.class)
)
.verify();
Expand Down
Loading
Loading