1
0
Fork 0
mirror of https://github.com/s-frick/effigenix.git synced 2026-03-28 12:09:35 +01:00

feat(test): Jazzer Fuzz-Tests für Production-Aggregate einführen

Coverage-guided Fuzz-Tests für ProductionOrder, Recipe und Batch.
Jeder Test fuzzt create() + zufällige Sequenzen aller Mutations-Methoden,
um unbehandelte Exceptions bei beliebigen Inputs aufzudecken.

- jazzer-junit 0.24.0 als Test-Dependency
- Maven-Profile: -Pfuzz (echtes Fuzzing), -Pfuzz-regression (Crash-Replay)
- Surefire: FuzzTests im Default-Lauf ausgeschlossen, reuseForks=false
- Makefile: make fuzz, make fuzz/regression, make fuzz/single
- .gitignore: .cifuzz-corpus/ ausgeschlossen
This commit is contained in:
Sebastian Frick 2026-02-26 08:50:02 +01:00
parent 74dc9a6981
commit 8a84bf5f25
6 changed files with 312 additions and 1 deletions

View file

@ -0,0 +1,79 @@
package de.effigenix.domain.production;
import com.code_intelligence.jazzer.api.FuzzedDataProvider;
import com.code_intelligence.jazzer.junit.FuzzTest;
import de.effigenix.shared.common.Result;
import java.time.LocalDate;
/**
* Fuzz test for the Batch aggregate.
*
* Exercises plan() with fuzzed drafts, then applies a random sequence of
* state-mutating operations (startProduction, recordConsumption, complete, cancel).
* Verifies that no input combination causes an unhandled exception
* all invalid inputs must be caught and returned as Result.Failure.
*
* Run: make fuzz | make fuzz/single TEST=BatchFuzzTest
*/
class BatchFuzzTest {
@FuzzTest(maxDuration = "5m")
void fuzzBatch(FuzzedDataProvider data) {
var draft = new BatchDraft(
data.consumeString(50),
data.consumeString(30),
data.consumeString(20),
consumeLocalDate(data),
consumeLocalDate(data)
);
BatchNumber batchNumber;
try {
batchNumber = BatchNumber.generate(LocalDate.now(), 1);
} catch (Exception e) {
return;
}
switch (Batch.plan(draft, batchNumber)) {
case Result.Failure(var err) -> { }
case Result.Success(var batch) -> {
int ops = data.consumeInt(1, 10);
for (int i = 0; i < ops; i++) {
int op = data.consumeInt(0, 3);
switch (op) {
case 0 -> batch.startProduction();
case 1 -> {
batch.recordConsumption(new ConsumptionDraft(
data.consumeString(50),
data.consumeString(50),
data.consumeString(30),
data.consumeString(20)
));
}
case 2 -> {
batch.complete(new CompleteBatchDraft(
data.consumeString(30),
data.consumeString(20),
data.consumeString(30),
data.consumeString(20),
data.consumeString(100)
));
}
case 3 -> batch.cancel(new CancelBatchDraft(data.consumeString(600)));
}
}
}
}
}
private static LocalDate consumeLocalDate(FuzzedDataProvider data) {
if (data.consumeBoolean()) {
return null;
}
int year = data.consumeInt(1900, 2100);
int month = data.consumeInt(1, 12);
int day = data.consumeInt(1, 28);
return LocalDate.of(year, month, day);
}
}

View file

@ -0,0 +1,65 @@
package de.effigenix.domain.production;
import com.code_intelligence.jazzer.api.FuzzedDataProvider;
import com.code_intelligence.jazzer.junit.FuzzTest;
import de.effigenix.shared.common.Result;
import java.time.LocalDate;
/**
* Fuzz test for the ProductionOrder aggregate.
*
* Exercises create() with fuzzed drafts, then applies a random sequence of
* state-mutating operations (release, startProduction, complete, cancel, reschedule).
* Verifies that no input combination causes an unhandled exception
* all invalid inputs must be caught and returned as Result.Failure.
*
* Run: make fuzz | make fuzz/single TEST=ProductionOrderFuzzTest
*/
class ProductionOrderFuzzTest {
@FuzzTest(maxDuration = "5m")
void fuzzProductionOrder(FuzzedDataProvider data) {
var draft = new ProductionOrderDraft(
data.consumeString(50),
data.consumeString(30),
data.consumeString(20),
consumeLocalDate(data),
data.consumeString(10),
data.consumeString(100)
);
switch (ProductionOrder.create(draft)) {
case Result.Failure(var err) -> {
// Invalid draft expected
}
case Result.Success(var order) -> {
int ops = data.consumeInt(1, 10);
for (int i = 0; i < ops; i++) {
int op = data.consumeInt(0, 4);
switch (op) {
case 0 -> order.release();
case 1 -> {
try {
order.startProduction(BatchId.of(data.consumeString(50)));
} catch (Exception ignored) { }
}
case 2 -> order.complete();
case 3 -> order.cancel(data.consumeString(50));
case 4 -> order.reschedule(consumeLocalDate(data));
}
}
}
}
}
private static LocalDate consumeLocalDate(FuzzedDataProvider data) {
if (data.consumeBoolean()) {
return null;
}
int year = data.consumeInt(1900, 2100);
int month = data.consumeInt(1, 12);
int day = data.consumeInt(1, 28);
return LocalDate.of(year, month, day);
}
}

View file

@ -0,0 +1,86 @@
package de.effigenix.domain.production;
import com.code_intelligence.jazzer.api.FuzzedDataProvider;
import com.code_intelligence.jazzer.junit.FuzzTest;
import de.effigenix.shared.common.Result;
/**
* Fuzz test for the Recipe aggregate.
*
* Exercises create() with fuzzed drafts, then applies a random sequence of
* state-mutating operations (addIngredient, addProductionStep, removeIngredient,
* removeProductionStep, activate, archive).
* Verifies that no input combination causes an unhandled exception
* all invalid inputs must be caught and returned as Result.Failure.
*
* Run: make fuzz | make fuzz/single TEST=RecipeFuzzTest
*/
class RecipeFuzzTest {
private static final RecipeType[] RECIPE_TYPES = RecipeType.values();
@FuzzTest(maxDuration = "5m")
void fuzzRecipe(FuzzedDataProvider data) {
var draft = new RecipeDraft(
data.consumeString(200),
data.consumeInt(0, 100),
data.consumeBoolean() ? data.pickValue(RECIPE_TYPES) : null,
data.consumeString(200),
data.consumeInt(0, 300),
data.consumeBoolean() ? data.consumeInt(-10, 400) : null,
data.consumeString(30),
data.consumeString(20),
data.consumeString(40)
);
switch (Recipe.create(draft)) {
case Result.Failure(var err) -> { }
case Result.Success(var recipe) -> {
int ops = data.consumeInt(1, 12);
for (int i = 0; i < ops; i++) {
int op = data.consumeInt(0, 5);
switch (op) {
case 0 -> {
var ingredientDraft = consumeIngredientDraft(data);
if (ingredientDraft != null) recipe.addIngredient(ingredientDraft);
}
case 1 -> {
recipe.addProductionStep(new ProductionStepDraft(
data.consumeInt(), data.consumeString(100),
data.consumeBoolean() ? data.consumeInt() : null,
data.consumeBoolean() ? data.consumeInt() : null
));
}
case 2 -> recipe.activate();
case 3 -> recipe.archive();
case 4 -> {
if (!recipe.ingredients().isEmpty()) {
recipe.removeIngredient(recipe.ingredients().getFirst().id());
}
}
case 5 -> {
if (!recipe.productionSteps().isEmpty()) {
recipe.removeProductionStep(recipe.productionSteps().getFirst().stepNumber());
}
}
}
}
}
}
}
private static IngredientDraft consumeIngredientDraft(FuzzedDataProvider data) {
try {
return new IngredientDraft(
data.consumeInt(),
data.consumeString(50),
data.consumeString(30),
data.consumeString(20),
data.consumeBoolean() ? data.consumeString(40) : null,
data.consumeBoolean()
);
} catch (IllegalArgumentException e) {
return null;
}
}
}