Implementierung für M2 vorläufig abgeschlossen
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
package de.gecheckt.pdf.umbenenner.domain.model;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Technical context representing a single batch processing run.
|
||||
* <p>
|
||||
* Each batch run is assigned a unique {@link RunId} and has associated timestamp information.
|
||||
* The context flows through the entire execution from Bootstrap through Use Case,
|
||||
* enabling correlation of all activities and results within a single run.
|
||||
* <p>
|
||||
* Responsibilities:
|
||||
* <ul>
|
||||
* <li>Track the unique identity of the batch run</li>
|
||||
* <li>Record when the run started (and eventually when it ends)</li>
|
||||
* <li>Provide run context to persistence, logging, and result tracking (future milestones)</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* This context is independent of individual document processing and contains
|
||||
* no business logic. It is purely a technical container for run identity and timing.
|
||||
*
|
||||
* @since M2-AP-003
|
||||
*/
|
||||
public final class BatchRunContext {
|
||||
|
||||
private final RunId runId;
|
||||
private final Instant startInstant;
|
||||
private Instant endInstant;
|
||||
|
||||
/**
|
||||
* Creates a new BatchRunContext with the given run ID and start time.
|
||||
* <p>
|
||||
* The end instant is initially null and may be set later via {@link #setEndInstant(Instant)}.
|
||||
*
|
||||
* @param runId the unique identifier for this run, must not be null
|
||||
* @param startInstant the moment when the run started, must not be null
|
||||
* @throws NullPointerException if runId or startInstant is null
|
||||
*/
|
||||
public BatchRunContext(RunId runId, Instant startInstant) {
|
||||
this.runId = Objects.requireNonNull(runId, "RunId must not be null");
|
||||
this.startInstant = Objects.requireNonNull(startInstant, "Start instant must not be null");
|
||||
this.endInstant = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the unique identifier of this run.
|
||||
*
|
||||
* @return the run ID, never null
|
||||
*/
|
||||
public RunId runId() {
|
||||
return runId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the instant when this run started.
|
||||
*
|
||||
* @return the start instant, never null
|
||||
*/
|
||||
public Instant startInstant() {
|
||||
return startInstant;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the instant when this run ended, or null if the run has not yet completed.
|
||||
* <p>
|
||||
* The end instant is set by {@link #setEndInstant(Instant)} during run completion.
|
||||
*
|
||||
* @return the end instant, or null if the run is still in progress
|
||||
*/
|
||||
public Instant endInstant() {
|
||||
return endInstant;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the instant when this run ended.
|
||||
* <p>
|
||||
* This should be called once at the end of the batch run to mark completion.
|
||||
* Typically called by the orchestration layer (Bootstrap) after the use case completes.
|
||||
*
|
||||
* @param endInstant the moment when the run completed, must not be null
|
||||
* @throws NullPointerException if endInstant is null
|
||||
* @throws IllegalStateException if end instant has already been set
|
||||
*/
|
||||
public void setEndInstant(Instant endInstant) {
|
||||
Objects.requireNonNull(endInstant, "End instant must not be null");
|
||||
if (this.endInstant != null) {
|
||||
throw new IllegalStateException("End instant has already been set");
|
||||
}
|
||||
this.endInstant = endInstant;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this run has completed (end instant is set).
|
||||
*
|
||||
* @return true if end instant is not null, false otherwise
|
||||
*/
|
||||
public boolean isCompleted() {
|
||||
return endInstant != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "BatchRunContext{" +
|
||||
"runId=" + runId +
|
||||
", startInstant=" + startInstant +
|
||||
", endInstant=" + endInstant +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package de.gecheckt.pdf.umbenenner.domain.model;
|
||||
|
||||
/**
|
||||
* Enumeration of all valid processing status values for a document within a batch run.
|
||||
* <p>
|
||||
* Each status reflects the outcome or current state of a document processing attempt.
|
||||
* Status transitions follow the rules defined in the architecture specification and persist
|
||||
* across multiple batch runs via the repository layer.
|
||||
* <p>
|
||||
* Status Categories:
|
||||
* <ul>
|
||||
* <li><strong>Final Success:</strong> {@link #SUCCESS}</li>
|
||||
* <li><strong>Retryable Failure:</strong> {@link #FAILED_RETRYABLE}</li>
|
||||
* <li><strong>Final Failure:</strong> {@link #FAILED_FINAL}</li>
|
||||
* <li><strong>Skip (Already Processed):</strong> {@link #SKIPPED_ALREADY_PROCESSED}</li>
|
||||
* <li><strong>Skip (Final Failure):</strong> {@link #SKIPPED_FINAL_FAILURE}</li>
|
||||
* <li><strong>Processing (Transient):</strong> {@link #PROCESSING}</li>
|
||||
* </ul>
|
||||
*
|
||||
* @since M2-AP-001
|
||||
*/
|
||||
public enum ProcessingStatus {
|
||||
|
||||
/**
|
||||
* Document was successfully processed and written to the target location.
|
||||
* <p>
|
||||
* A document with this status will be skipped in all future batch runs.
|
||||
* Status is final and irreversible.
|
||||
*/
|
||||
SUCCESS,
|
||||
|
||||
/**
|
||||
* Processing failed with a transient error (temporary infrastructure problem).
|
||||
* <p>
|
||||
* Examples: API timeout, temporary file lock, momentary network issue.
|
||||
* <p>
|
||||
* A document with this status may be retried in a later batch run, up to the
|
||||
* configured maximum retry count. Retry count is tracked separately.
|
||||
*/
|
||||
FAILED_RETRYABLE,
|
||||
|
||||
/**
|
||||
* Processing failed with a deterministic content error (non-recoverable problem).
|
||||
* <p>
|
||||
* Examples: PDF has no extractable text, page limit exceeded, document is ambiguous.
|
||||
* <p>
|
||||
* A document with this status receives exactly one retry in a later batch run.
|
||||
* After that retry, if it still fails, status becomes {@link #FAILED_FINAL}.
|
||||
* No further retries are attempted.
|
||||
*/
|
||||
FAILED_FINAL,
|
||||
|
||||
/**
|
||||
* Document was skipped because it has already been successfully processed.
|
||||
* <p>
|
||||
* This is a final skip state. The document will remain skipped in all future runs.
|
||||
* Document fingerprint matching ensures that identical content is never reprocessed.
|
||||
*/
|
||||
SKIPPED_ALREADY_PROCESSED,
|
||||
|
||||
/**
|
||||
* Document was skipped because it has already failed finally.
|
||||
* <p>
|
||||
* This is a final skip state. The document will remain skipped in all future runs.
|
||||
* No further processing attempts will be made.
|
||||
*/
|
||||
SKIPPED_FINAL_FAILURE,
|
||||
|
||||
/**
|
||||
* Technical transient status: Document is currently being processed.
|
||||
* <p>
|
||||
* This status is used internally during a batch run to mark documents
|
||||
* that are actively being worked on. If a batch run is interrupted,
|
||||
* documents in this state will be re-attempted in the next run.
|
||||
* <p>
|
||||
* This status should not persist across batch runs in normal operation.
|
||||
*/
|
||||
PROCESSING
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package de.gecheckt.pdf.umbenenner.domain.model;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Unique identifier for a batch run.
|
||||
* <p>
|
||||
* Each invocation of the PDF Umbenenner application receives a unique RunId
|
||||
* that persists for the entire batch processing cycle. The RunId is used to:
|
||||
* <ul>
|
||||
* <li>Correlate all documents processed in a single run</li>
|
||||
* <li>Track attempt history and retry decisions across runs</li>
|
||||
* <li>Identify logs and audit trails for a specific execution</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* RunId is intentionally simple: a non-null, immutable string value.
|
||||
* Implementations may choose UUID format, timestamp-based IDs, or sequential IDs.
|
||||
* The internal structure is opaque to consumers.
|
||||
*
|
||||
* @since M2-AP-003
|
||||
*/
|
||||
public final class RunId implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final String value;
|
||||
|
||||
/**
|
||||
* Creates a new RunId with the given string value.
|
||||
* <p>
|
||||
* The value must be non-null and non-empty. No format validation is enforced;
|
||||
* implementations are free to choose their own ID scheme.
|
||||
*
|
||||
* @param value the unique identifier string, must not be null or empty
|
||||
* @throws NullPointerException if value is null
|
||||
* @throws IllegalArgumentException if value is empty
|
||||
*/
|
||||
public RunId(String value) {
|
||||
Objects.requireNonNull(value, "RunId value must not be null");
|
||||
if (value.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("RunId value must not be empty");
|
||||
}
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the string representation of this RunId.
|
||||
*
|
||||
* @return the unique identifier value
|
||||
*/
|
||||
public String value() {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
RunId runId = (RunId) o;
|
||||
return Objects.equals(value, runId.value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Domain model package containing core value objects and enumerations.
|
||||
* <p>
|
||||
* This package contains the fundamental domain entities and status models required for document processing:
|
||||
* <ul>
|
||||
* <li>{@link de.gecheckt.pdf.umbenenner.domain.model.ProcessingStatus} — enumeration of all valid document processing states</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* All classes in this package are:
|
||||
* <ul>
|
||||
* <li>Infrastructure-agnostic (no database, filesystem, network, or framework dependencies)</li>
|
||||
* <li>Immutable value objects or enumerations</li>
|
||||
* <li>Reusable across all layers via the Application and Adapter contracts</li>
|
||||
* </ul>
|
||||
*
|
||||
* @since M2-AP-001
|
||||
*/
|
||||
package de.gecheckt.pdf.umbenenner.domain.model;
|
||||
@@ -1,7 +1,18 @@
|
||||
/**
|
||||
* Domain layer containing business entities and value objects.
|
||||
* <p>
|
||||
* This package is infrastructure-agnostic and contains no dependencies on external systems.
|
||||
* AP-003: Currently empty as no domain logic has been implemented yet.
|
||||
* This package is infrastructure-agnostic and contains no dependencies on external systems,
|
||||
* databases, filesystems, HTTP clients, or any other technical infrastructure.
|
||||
* <p>
|
||||
* M2-AP-001 Implementation:
|
||||
* <ul>
|
||||
* <li>{@link de.gecheckt.pdf.umbenenner.domain.model} — Core domain model including
|
||||
* {@link de.gecheckt.pdf.umbenenner.domain.model.ProcessingStatus} enumeration</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* Subpackages:
|
||||
* <ul>
|
||||
* <li><strong>model:</strong> Domain entities and value objects (e.g., status enumerations)</li>
|
||||
* </ul>
|
||||
*/
|
||||
package de.gecheckt.pdf.umbenenner.domain;
|
||||
@@ -0,0 +1,175 @@
|
||||
package de.gecheckt.pdf.umbenenner.domain.model;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link BatchRunContext}.
|
||||
* <p>
|
||||
* Verifies correct modeling of batch run lifecycle including initialization,
|
||||
* timestamp tracking, completion marking, and state validation.
|
||||
*/
|
||||
class BatchRunContextTest {
|
||||
|
||||
@Test
|
||||
void constructor_createsContextWithRunIdAndStartTime() {
|
||||
RunId runId = new RunId("test-run");
|
||||
Instant startTime = Instant.now();
|
||||
|
||||
BatchRunContext context = new BatchRunContext(runId, startTime);
|
||||
|
||||
assertNotNull(context);
|
||||
assertEquals(runId, context.runId());
|
||||
assertEquals(startTime, context.startInstant());
|
||||
}
|
||||
|
||||
@Test
|
||||
void constructor_throwsNullPointerExceptionWhenRunIdIsNull() {
|
||||
Instant startTime = Instant.now();
|
||||
assertThrows(NullPointerException.class, () -> new BatchRunContext(null, startTime),
|
||||
"Constructor should throw NullPointerException when RunId is null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void constructor_throwsNullPointerExceptionWhenStartInstantIsNull() {
|
||||
RunId runId = new RunId("test-run");
|
||||
assertThrows(NullPointerException.class, () -> new BatchRunContext(runId, null),
|
||||
"Constructor should throw NullPointerException when start instant is null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void runId_returnsTheConstructorRunId() {
|
||||
RunId expectedRunId = new RunId("run-123");
|
||||
Instant startTime = Instant.now();
|
||||
BatchRunContext context = new BatchRunContext(expectedRunId, startTime);
|
||||
|
||||
assertEquals(expectedRunId, context.runId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void startInstant_returnsTheConstructorStartTime() {
|
||||
RunId runId = new RunId("test-run");
|
||||
Instant expectedStartTime = Instant.parse("2026-03-31T20:00:00Z");
|
||||
BatchRunContext context = new BatchRunContext(runId, expectedStartTime);
|
||||
|
||||
assertEquals(expectedStartTime, context.startInstant());
|
||||
}
|
||||
|
||||
@Test
|
||||
void endInstant_isNullImmediatelyAfterConstruction() {
|
||||
RunId runId = new RunId("test-run");
|
||||
Instant startTime = Instant.now();
|
||||
BatchRunContext context = new BatchRunContext(runId, startTime);
|
||||
|
||||
assertNull(context.endInstant(), "End instant should be null immediately after construction");
|
||||
}
|
||||
|
||||
@Test
|
||||
void isCompleted_returnsFalseWhenEndInstantNotSet() {
|
||||
RunId runId = new RunId("test-run");
|
||||
Instant startTime = Instant.now();
|
||||
BatchRunContext context = new BatchRunContext(runId, startTime);
|
||||
|
||||
assertFalse(context.isCompleted(), "isCompleted should return false when end instant is not set");
|
||||
}
|
||||
|
||||
@Test
|
||||
void setEndInstant_setsTheEndTime() {
|
||||
RunId runId = new RunId("test-run");
|
||||
Instant startTime = Instant.parse("2026-03-31T20:00:00Z");
|
||||
Instant endTime = Instant.parse("2026-03-31T20:01:00Z");
|
||||
|
||||
BatchRunContext context = new BatchRunContext(runId, startTime);
|
||||
context.setEndInstant(endTime);
|
||||
|
||||
assertEquals(endTime, context.endInstant());
|
||||
}
|
||||
|
||||
@Test
|
||||
void setEndInstant_throwsNullPointerExceptionWhenEndInstantIsNull() {
|
||||
RunId runId = new RunId("test-run");
|
||||
Instant startTime = Instant.now();
|
||||
BatchRunContext context = new BatchRunContext(runId, startTime);
|
||||
|
||||
assertThrows(NullPointerException.class, () -> context.setEndInstant(null),
|
||||
"setEndInstant should throw NullPointerException when end instant is null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void setEndInstant_throwsIllegalStateExceptionWhenAlreadySet() {
|
||||
RunId runId = new RunId("test-run");
|
||||
Instant startTime = Instant.now();
|
||||
Instant firstEndTime = Instant.now().plusSeconds(60);
|
||||
Instant secondEndTime = Instant.now().plusSeconds(120);
|
||||
|
||||
BatchRunContext context = new BatchRunContext(runId, startTime);
|
||||
context.setEndInstant(firstEndTime);
|
||||
|
||||
assertThrows(IllegalStateException.class, () -> context.setEndInstant(secondEndTime),
|
||||
"setEndInstant should throw IllegalStateException when called a second time");
|
||||
}
|
||||
|
||||
@Test
|
||||
void isCompleted_returnsTrueAfterSetEndInstant() {
|
||||
RunId runId = new RunId("test-run");
|
||||
Instant startTime = Instant.now();
|
||||
Instant endTime = Instant.now().plusSeconds(60);
|
||||
|
||||
BatchRunContext context = new BatchRunContext(runId, startTime);
|
||||
context.setEndInstant(endTime);
|
||||
|
||||
assertTrue(context.isCompleted(), "isCompleted should return true after setting end instant");
|
||||
}
|
||||
|
||||
@Test
|
||||
void toString_containsRunIdAndTimestamps() {
|
||||
RunId runId = new RunId("test-run-001");
|
||||
Instant startTime = Instant.parse("2026-03-31T20:00:00Z");
|
||||
|
||||
BatchRunContext context = new BatchRunContext(runId, startTime);
|
||||
String representation = context.toString();
|
||||
|
||||
assertTrue(representation.contains("test-run-001"), "toString should contain RunId");
|
||||
assertTrue(representation.contains("2026-03-31"), "toString should contain timestamp information");
|
||||
}
|
||||
|
||||
@Test
|
||||
void contextLifecycle_startToCompletion() {
|
||||
RunId runId = new RunId("test-run-lifecycle");
|
||||
Instant startTime = Instant.parse("2026-03-31T20:00:00Z");
|
||||
Instant endTime = Instant.parse("2026-03-31T20:05:00Z");
|
||||
|
||||
// Create context
|
||||
BatchRunContext context = new BatchRunContext(runId, startTime);
|
||||
assertFalse(context.isCompleted());
|
||||
|
||||
// Complete context
|
||||
context.setEndInstant(endTime);
|
||||
assertTrue(context.isCompleted());
|
||||
|
||||
// Verify all data
|
||||
assertEquals(runId, context.runId());
|
||||
assertEquals(startTime, context.startInstant());
|
||||
assertEquals(endTime, context.endInstant());
|
||||
}
|
||||
|
||||
@Test
|
||||
void multipleContextsAreIndependent() {
|
||||
RunId runId1 = new RunId("run-1");
|
||||
RunId runId2 = new RunId("run-2");
|
||||
Instant startTime1 = Instant.parse("2026-03-31T20:00:00Z");
|
||||
Instant startTime2 = Instant.parse("2026-03-31T21:00:00Z");
|
||||
|
||||
BatchRunContext context1 = new BatchRunContext(runId1, startTime1);
|
||||
BatchRunContext context2 = new BatchRunContext(runId2, startTime2);
|
||||
|
||||
Instant endTime1 = Instant.parse("2026-03-31T20:05:00Z");
|
||||
context1.setEndInstant(endTime1);
|
||||
|
||||
assertTrue(context1.isCompleted());
|
||||
assertFalse(context2.isCompleted(), "Context2 should not be affected by Context1's completion");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package de.gecheckt.pdf.umbenenner.domain.model;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link ProcessingStatus} enumeration.
|
||||
* <p>
|
||||
* Verifies that all required status values are present and correctly defined
|
||||
* for M2 and future milestones.
|
||||
*/
|
||||
class ProcessingStatusTest {
|
||||
|
||||
@Test
|
||||
void allRequiredStatusValuesExist() {
|
||||
// Verify all status values required by the architecture are present
|
||||
assertNotNull(ProcessingStatus.SUCCESS);
|
||||
assertNotNull(ProcessingStatus.FAILED_RETRYABLE);
|
||||
assertNotNull(ProcessingStatus.FAILED_FINAL);
|
||||
assertNotNull(ProcessingStatus.SKIPPED_ALREADY_PROCESSED);
|
||||
assertNotNull(ProcessingStatus.SKIPPED_FINAL_FAILURE);
|
||||
assertNotNull(ProcessingStatus.PROCESSING);
|
||||
}
|
||||
|
||||
@Test
|
||||
void successStatus_isDefinedAndAccessible() {
|
||||
ProcessingStatus status = ProcessingStatus.SUCCESS;
|
||||
assertEquals(ProcessingStatus.SUCCESS, status);
|
||||
}
|
||||
|
||||
@Test
|
||||
void failedRetryableStatus_isDefinedAndAccessible() {
|
||||
ProcessingStatus status = ProcessingStatus.FAILED_RETRYABLE;
|
||||
assertEquals(ProcessingStatus.FAILED_RETRYABLE, status);
|
||||
}
|
||||
|
||||
@Test
|
||||
void failedFinalStatus_isDefinedAndAccessible() {
|
||||
ProcessingStatus status = ProcessingStatus.FAILED_FINAL;
|
||||
assertEquals(ProcessingStatus.FAILED_FINAL, status);
|
||||
}
|
||||
|
||||
@Test
|
||||
void skippedAlreadyProcessedStatus_isDefinedAndAccessible() {
|
||||
ProcessingStatus status = ProcessingStatus.SKIPPED_ALREADY_PROCESSED;
|
||||
assertEquals(ProcessingStatus.SKIPPED_ALREADY_PROCESSED, status);
|
||||
}
|
||||
|
||||
@Test
|
||||
void skippedFinalFailureStatus_isDefinedAndAccessible() {
|
||||
ProcessingStatus status = ProcessingStatus.SKIPPED_FINAL_FAILURE;
|
||||
assertEquals(ProcessingStatus.SKIPPED_FINAL_FAILURE, status);
|
||||
}
|
||||
|
||||
@Test
|
||||
void processingStatus_isDefinedAndAccessible() {
|
||||
ProcessingStatus status = ProcessingStatus.PROCESSING;
|
||||
assertEquals(ProcessingStatus.PROCESSING, status);
|
||||
}
|
||||
|
||||
@Test
|
||||
void statusEquality_worksByReference() {
|
||||
// Enums have identity-based equality
|
||||
assertTrue(ProcessingStatus.SUCCESS == ProcessingStatus.SUCCESS);
|
||||
assertFalse(ProcessingStatus.SUCCESS == ProcessingStatus.FAILED_FINAL);
|
||||
}
|
||||
|
||||
@Test
|
||||
void statusCanBeUsedInSwitch() {
|
||||
ProcessingStatus status = ProcessingStatus.FAILED_RETRYABLE;
|
||||
String result = "";
|
||||
|
||||
switch (status) {
|
||||
case SUCCESS -> result = "success";
|
||||
case FAILED_RETRYABLE -> result = "retryable";
|
||||
case FAILED_FINAL -> result = "final";
|
||||
case SKIPPED_ALREADY_PROCESSED -> result = "skip-processed";
|
||||
case SKIPPED_FINAL_FAILURE -> result = "skip-failed";
|
||||
case PROCESSING -> result = "processing";
|
||||
}
|
||||
|
||||
assertEquals("retryable", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void statusValues_areSixInTotal() {
|
||||
ProcessingStatus[] values = ProcessingStatus.values();
|
||||
assertEquals(6, values.length, "ProcessingStatus should have exactly 6 values");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package de.gecheckt.pdf.umbenenner.domain.model;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link RunId} value object.
|
||||
* <p>
|
||||
* Verifies immutability, value semantics, validation, and correct behavior
|
||||
* as a unique identifier for batch runs.
|
||||
*/
|
||||
class RunIdTest {
|
||||
|
||||
@Test
|
||||
void constructor_createsRunIdWithValidValue() {
|
||||
RunId runId = new RunId("test-run-123");
|
||||
assertNotNull(runId);
|
||||
assertEquals("test-run-123", runId.value());
|
||||
}
|
||||
|
||||
@Test
|
||||
void constructor_throwsNullPointerExceptionWhenValueIsNull() {
|
||||
assertThrows(NullPointerException.class, () -> new RunId(null),
|
||||
"Constructor should throw NullPointerException for null value");
|
||||
}
|
||||
|
||||
@Test
|
||||
void constructor_throwsIllegalArgumentExceptionWhenValueIsEmpty() {
|
||||
assertThrows(IllegalArgumentException.class, () -> new RunId(""),
|
||||
"Constructor should throw IllegalArgumentException for empty value");
|
||||
}
|
||||
|
||||
@Test
|
||||
void constructor_throwsIllegalArgumentExceptionWhenValueIsOnlyWhitespace() {
|
||||
assertThrows(IllegalArgumentException.class, () -> new RunId(" "),
|
||||
"Constructor should throw IllegalArgumentException for whitespace-only value");
|
||||
}
|
||||
|
||||
@Test
|
||||
void value_returnsTheConstructorValue() {
|
||||
String expectedValue = "run-id-456";
|
||||
RunId runId = new RunId(expectedValue);
|
||||
assertEquals(expectedValue, runId.value());
|
||||
}
|
||||
|
||||
@Test
|
||||
void equals_returnsTrueForIdenticalValues() {
|
||||
RunId runId1 = new RunId("same-id");
|
||||
RunId runId2 = new RunId("same-id");
|
||||
assertEquals(runId1, runId2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void equals_returnsFalseForDifferentValues() {
|
||||
RunId runId1 = new RunId("id-1");
|
||||
RunId runId2 = new RunId("id-2");
|
||||
assertNotEquals(runId1, runId2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void equals_returnsFalseWhenComparedWithNull() {
|
||||
RunId runId = new RunId("test-id");
|
||||
assertNotEquals(runId, null);
|
||||
assertFalse(runId.equals(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void equals_returnsFalseWhenComparedWithDifferentType() {
|
||||
RunId runId = new RunId("test-id");
|
||||
assertNotEquals(runId, "test-id");
|
||||
assertFalse(runId.equals("test-id"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void hashCode_isSameForIdenticalValues() {
|
||||
RunId runId1 = new RunId("same-id");
|
||||
RunId runId2 = new RunId("same-id");
|
||||
assertEquals(runId1.hashCode(), runId2.hashCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void hashCode_isDifferentForDifferentValues() {
|
||||
RunId runId1 = new RunId("id-1");
|
||||
RunId runId2 = new RunId("id-2");
|
||||
// Note: this is not guaranteed for different values, but likely
|
||||
assertNotEquals(runId1.hashCode(), runId2.hashCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void toString_returnsTheValue() {
|
||||
String value = "run-id-789";
|
||||
RunId runId = new RunId(value);
|
||||
assertEquals(value, runId.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void runIdCanBeUsedInCollections() {
|
||||
RunId runId1 = new RunId("id-1");
|
||||
RunId runId2 = new RunId("id-1");
|
||||
RunId runId3 = new RunId("id-2");
|
||||
|
||||
var set = new java.util.HashSet<RunId>();
|
||||
set.add(runId1);
|
||||
set.add(runId2); // Should not add duplicate
|
||||
set.add(runId3);
|
||||
|
||||
// runId1 and runId2 are equal, so set should have 2 elements
|
||||
assertEquals(2, set.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void runIdCanBeMapKey() {
|
||||
RunId runId1 = new RunId("id-1");
|
||||
RunId runId2 = new RunId("id-1");
|
||||
|
||||
var map = new java.util.HashMap<RunId, String>();
|
||||
map.put(runId1, "first");
|
||||
map.put(runId2, "second");
|
||||
|
||||
// runId1 and runId2 are equal, so second put should overwrite
|
||||
assertEquals(1, map.size());
|
||||
assertEquals("second", map.get(runId1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void constructor_acceptsUuidFormatValue() {
|
||||
String uuidValue = "550e8400-e29b-41d4-a716-446655440000";
|
||||
RunId runId = new RunId(uuidValue);
|
||||
assertEquals(uuidValue, runId.value());
|
||||
}
|
||||
|
||||
@Test
|
||||
void constructor_acceptsTimestampFormatValue() {
|
||||
String timestampValue = "2026-03-31T21:41:48.000Z";
|
||||
RunId runId = new RunId(timestampValue);
|
||||
assertEquals(timestampValue, runId.value());
|
||||
}
|
||||
|
||||
@Test
|
||||
void constructor_acceptsSequentialNumberValue() {
|
||||
String sequentialValue = "00001";
|
||||
RunId runId = new RunId(sequentialValue);
|
||||
assertEquals(sequentialValue, runId.value());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user