Skip to content

Commit c8a46a7

Browse files
committed
feat: flamingock test support AuditSequenceStrictValidator
1 parent 6e5db25 commit c8a46a7

File tree

3 files changed

+342
-10
lines changed

3 files changed

+342
-10
lines changed

core/flamingock-test-support/src/main/java/io/flamingock/support/validation/impl/AuditEntryExpectation.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,11 @@
3030
* against expected definitions. Users should not interact with this class directly;
3131
* they should use {@link AuditEntryDefinition} instead.</p>
3232
*/
33-
class AuditEntryExpectation {
33+
public class AuditEntryExpectation {
3434

3535
private final AuditEntryDefinition definition;
3636

37-
AuditEntryExpectation(AuditEntryDefinition definition) {
37+
public AuditEntryExpectation(AuditEntryDefinition definition) {
3838
this.definition = definition;
3939
}
4040

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

5454
// Required fields - always verified

core/flamingock-test-support/src/main/java/io/flamingock/support/validation/impl/AuditSequenceStrictValidator.java

Lines changed: 73 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,33 +15,99 @@
1515
*/
1616
package io.flamingock.support.validation.impl;
1717

18+
import io.flamingock.internal.common.core.audit.AuditEntry;
1819
import io.flamingock.internal.core.store.AuditStore;
1920
import io.flamingock.support.domain.AuditEntryDefinition;
2021
import io.flamingock.support.validation.SimpleValidator;
22+
import io.flamingock.support.validation.error.CountMismatchError;
23+
import io.flamingock.support.validation.error.ValidationError;
2124
import io.flamingock.support.validation.error.ValidationResult;
2225

26+
import java.util.ArrayList;
2327
import java.util.Arrays;
2428
import java.util.List;
2529
import java.util.stream.Collectors;
2630

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

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

31-
private final AuditStore<?> auditStore;
32-
private final List<AuditEntryExpectation> expectations;
33-
46+
private final List<AuditEntryExpectation> expectedExpectations;
47+
private final List<AuditEntry> actualEntries;
3448

49+
/**
50+
* Creates a strict sequence validator from an AuditStore and expected definitions.
51+
*
52+
* @param auditStore the audit store to read actual entries from
53+
* @param definitions the expected audit entry definitions
54+
*/
3555
public AuditSequenceStrictValidator(AuditStore<?> auditStore, AuditEntryDefinition... definitions) {
36-
this.auditStore = auditStore;
37-
this.expectations = Arrays.stream(definitions)
56+
this(Arrays.asList(definitions), auditStore.getPersistence().getAuditHistory());
57+
}
58+
59+
/**
60+
* Internal constructor for direct list initialization (used by tests).
61+
*/
62+
AuditSequenceStrictValidator(List<AuditEntryDefinition> expectedDefinitions, List<AuditEntry> actualEntries) {
63+
this.expectedExpectations = expectedDefinitions.stream()
3864
.map(AuditEntryExpectation::new)
3965
.collect(Collectors.toList());
66+
this.actualEntries = actualEntries != null ? actualEntries : new ArrayList<>();
4067
}
4168

4269
@Override
4370
public ValidationResult validate() {
44-
// TODO: Implement actual validation logic
45-
return ValidationResult.success(VALIDATOR_NAME);
71+
// Check count
72+
if (expectedExpectations.size() != actualEntries.size()) {
73+
return ValidationResult.failure(VALIDATOR_NAME,
74+
new CountMismatchError(getExpectedChangeIds(), getActualChangeIds()));
75+
}
76+
77+
// Validate each entry in sequence
78+
List<ValidationError> allErrors = getValidationErrors(expectedExpectations, actualEntries);
79+
80+
if (allErrors.isEmpty()) {
81+
return ValidationResult.success(VALIDATOR_NAME);
82+
}
83+
84+
return ValidationResult.failure(VALIDATOR_NAME, allErrors.toArray(
85+
new io.flamingock.support.validation.error.ValidationError[0]));
86+
}
87+
88+
private static List<ValidationError> getValidationErrors(List<AuditEntryExpectation> expectedExpectations, List<AuditEntry> actualEntries) {
89+
List<ValidationError> allErrors = new ArrayList<>();
90+
for (int i = 0; i < expectedExpectations.size(); i++) {
91+
AuditEntryExpectation expected = expectedExpectations.get(i);
92+
AuditEntry actual = actualEntries.get(i);
93+
94+
List<io.flamingock.support.validation.error.FieldMismatchError> entryErrors = expected.compareWith(actual);
95+
if (!entryErrors.isEmpty()) {
96+
allErrors.addAll(entryErrors);
97+
}
98+
}
99+
return allErrors;
100+
}
101+
102+
private List<String> getExpectedChangeIds() {
103+
return expectedExpectations.stream()
104+
.map(exp -> exp.getDefinition().getChangeId())
105+
.collect(Collectors.toList());
106+
}
107+
108+
private List<String> getActualChangeIds() {
109+
return actualEntries.stream()
110+
.map(AuditEntry::getTaskId)
111+
.collect(Collectors.toList());
46112
}
47113
}
Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
/*
2+
* Copyright 2025 Flamingock (https://www.flamingock.io)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.flamingock.support.validation.impl;
17+
18+
import io.flamingock.internal.common.core.audit.AuditEntry;
19+
import io.flamingock.support.domain.AuditEntryDefinition;
20+
import io.flamingock.support.validation.error.CountMismatchError;
21+
import io.flamingock.support.validation.error.FieldMismatchError;
22+
import io.flamingock.support.validation.error.ValidationResult;
23+
import org.junit.jupiter.api.BeforeEach;
24+
import org.junit.jupiter.api.Test;
25+
26+
import java.time.LocalDateTime;
27+
import java.util.Arrays;
28+
import java.util.Collections;
29+
import java.util.List;
30+
31+
import static io.flamingock.internal.common.core.audit.AuditEntry.ExecutionType.EXECUTION;
32+
import static io.flamingock.internal.common.core.audit.AuditEntry.Status.APPLIED;
33+
import static io.flamingock.internal.common.core.audit.AuditEntry.Status.FAILED;
34+
import static io.flamingock.support.domain.AuditEntryDefinition.APPLIED;
35+
import static org.junit.jupiter.api.Assertions.*;
36+
37+
class AuditSequenceStrictValidatorTest {
38+
39+
private List<AuditEntry> actualEntries;
40+
41+
@BeforeEach
42+
void setUp() {
43+
actualEntries = Arrays.asList(
44+
createAuditEntry("change-1", APPLIED),
45+
createAuditEntry("change-2", APPLIED),
46+
createAuditEntry("change-3", FAILED)
47+
);
48+
}
49+
50+
@Test
51+
void shouldPassValidation_whenEntriesMatchExactly() {
52+
List<AuditEntryDefinition> expectedDefinitions = Arrays.asList(
53+
APPLIED("change-1"),
54+
APPLIED("change-2"),
55+
AuditEntryDefinition.FAILED("change-3")
56+
);
57+
58+
AuditSequenceStrictValidator validator = new AuditSequenceStrictValidator(expectedDefinitions, actualEntries);
59+
ValidationResult result = validator.validate();
60+
61+
assertTrue(result.isSuccess());
62+
}
63+
64+
@Test
65+
void shouldFailValidation_whenCountMismatch() {
66+
List<AuditEntryDefinition> expectedDefinitions = Arrays.asList(
67+
APPLIED("change-1"),
68+
APPLIED("change-2")
69+
);
70+
71+
AuditSequenceStrictValidator validator = new AuditSequenceStrictValidator(expectedDefinitions, actualEntries);
72+
ValidationResult result = validator.validate();
73+
74+
assertFalse(result.isSuccess());
75+
assertEquals(1, result.getErrors().size());
76+
assertInstanceOf(CountMismatchError.class, result.getErrors().get(0));
77+
}
78+
79+
@Test
80+
void shouldFailValidation_whenStatusMismatch() {
81+
List<AuditEntryDefinition> expectedDefinitions = Arrays.asList(
82+
APPLIED("change-1"),
83+
APPLIED("change-2"),
84+
APPLIED("change-3") // Expected APPLIED but actual is FAILED
85+
);
86+
87+
AuditSequenceStrictValidator validator = new AuditSequenceStrictValidator(expectedDefinitions, actualEntries);
88+
ValidationResult result = validator.validate();
89+
90+
assertFalse(result.isSuccess());
91+
assertEquals(1, result.getErrors().size());
92+
assertInstanceOf(FieldMismatchError.class, result.getErrors().get(0));
93+
94+
FieldMismatchError error = (FieldMismatchError) result.getErrors().get(0);
95+
assertEquals("status", error.getFieldName());
96+
}
97+
98+
@Test
99+
void shouldFailValidation_whenChangeIdMismatch() {
100+
List<AuditEntryDefinition> expectedDefinitions = Arrays.asList(
101+
APPLIED("change-1"),
102+
APPLIED("wrong-id"), // Mismatch
103+
AuditEntryDefinition.FAILED("change-3")
104+
);
105+
106+
AuditSequenceStrictValidator validator = new AuditSequenceStrictValidator(expectedDefinitions, actualEntries);
107+
ValidationResult result = validator.validate();
108+
109+
assertFalse(result.isSuccess());
110+
assertEquals(1, result.getErrors().size());
111+
assertInstanceOf(FieldMismatchError.class, result.getErrors().get(0));
112+
113+
FieldMismatchError error = (FieldMismatchError) result.getErrors().get(0);
114+
assertEquals("changeId", error.getFieldName());
115+
}
116+
117+
@Test
118+
void shouldFailValidation_whenMissingEntry() {
119+
List<AuditEntry> actualEntriesSubset = Arrays.asList(
120+
createAuditEntry("change-1", APPLIED),
121+
createAuditEntry("change-2", APPLIED)
122+
);
123+
124+
List<AuditEntryDefinition> expectedDefinitions = Arrays.asList(
125+
APPLIED("change-1"),
126+
APPLIED("change-2"),
127+
AuditEntryDefinition.FAILED("change-3")
128+
);
129+
130+
AuditSequenceStrictValidator validator = new AuditSequenceStrictValidator(expectedDefinitions, actualEntriesSubset);
131+
ValidationResult result = validator.validate();
132+
133+
assertFalse(result.isSuccess());
134+
assertTrue(result.getErrors().stream().anyMatch(e -> e instanceof CountMismatchError));
135+
}
136+
137+
@Test
138+
void shouldFailValidation_whenUnexpectedEntry() {
139+
List<AuditEntry> actualEntriesExtra = Arrays.asList(
140+
createAuditEntry("change-1", APPLIED),
141+
createAuditEntry("change-2", APPLIED),
142+
createAuditEntry("change-3", FAILED),
143+
createAuditEntry("change-4", APPLIED)
144+
);
145+
146+
List<AuditEntryDefinition> expectedDefinitions = Arrays.asList(
147+
APPLIED("change-1"),
148+
APPLIED("change-2"),
149+
AuditEntryDefinition.FAILED("change-3")
150+
);
151+
152+
AuditSequenceStrictValidator validator = new AuditSequenceStrictValidator(expectedDefinitions, actualEntriesExtra);
153+
ValidationResult result = validator.validate();
154+
155+
assertFalse(result.isSuccess());
156+
assertTrue(result.getErrors().stream().anyMatch(e -> e instanceof CountMismatchError));
157+
}
158+
159+
@Test
160+
void shouldPassValidation_whenOptionalFieldsMatch() {
161+
AuditEntry actualWithOptionalFields = new AuditEntry(
162+
"exec-1",
163+
"stage-1",
164+
"change-1",
165+
"author",
166+
LocalDateTime.now(),
167+
APPLIED,
168+
EXECUTION,
169+
"com.example.Change",
170+
"apply",
171+
100L,
172+
"host",
173+
null,
174+
false,
175+
null,
176+
null,
177+
"target-1",
178+
"1",
179+
null,
180+
true
181+
);
182+
183+
AuditEntryDefinition expectedWithOptionalFields = APPLIED("change-1")
184+
.withAuthor("author")
185+
.withClassName("com.example.Change")
186+
.withMethodName("apply")
187+
.withTargetSystemId("target-1")
188+
.withOrder("1")
189+
.withTransactional(true);
190+
191+
AuditSequenceStrictValidator validator = new AuditSequenceStrictValidator(
192+
Collections.singletonList(expectedWithOptionalFields),
193+
Collections.singletonList(actualWithOptionalFields)
194+
);
195+
196+
ValidationResult result = validator.validate();
197+
198+
assertTrue(result.isSuccess());
199+
}
200+
201+
@Test
202+
void shouldFailValidation_whenOptionalFieldMismatch() {
203+
AuditEntry actualEntry = new AuditEntry(
204+
"exec-1",
205+
"stage-1",
206+
"change-1",
207+
"author",
208+
LocalDateTime.now(),
209+
APPLIED,
210+
EXECUTION,
211+
"com.example.Change",
212+
"apply",
213+
100L,
214+
"host",
215+
null,
216+
false,
217+
null,
218+
null,
219+
"target-1",
220+
null,
221+
null,
222+
null
223+
);
224+
225+
AuditEntryDefinition expectedWithDifferentOptional = APPLIED("change-1")
226+
.withTargetSystemId("different-target");
227+
228+
AuditSequenceStrictValidator validator = new AuditSequenceStrictValidator(
229+
Collections.singletonList(expectedWithDifferentOptional),
230+
Collections.singletonList(actualEntry)
231+
);
232+
233+
ValidationResult result = validator.validate();
234+
235+
assertFalse(result.isSuccess());
236+
assertEquals(1, result.getErrors().size());
237+
assertInstanceOf(FieldMismatchError.class, result.getErrors().get(0));
238+
239+
FieldMismatchError error = (FieldMismatchError) result.getErrors().get(0);
240+
assertEquals("targetSystemId", error.getFieldName());
241+
}
242+
243+
private AuditEntry createAuditEntry(String changeId, AuditEntry.Status status) {
244+
return new AuditEntry(
245+
"exec-id",
246+
"stage-id",
247+
changeId,
248+
"test-author",
249+
LocalDateTime.now(),
250+
status,
251+
EXECUTION,
252+
"com.example.TestChange",
253+
"apply",
254+
100L,
255+
"localhost",
256+
null,
257+
false,
258+
null,
259+
null,
260+
null,
261+
null,
262+
null,
263+
null
264+
);
265+
}
266+
}

0 commit comments

Comments
 (0)