M4 AP-006 Rollback-Semantik und Bootstrap-Scope bereinigen
This commit is contained in:
@@ -39,19 +39,20 @@ public class SqliteUnitOfWorkAdapter implements UnitOfWorkPort {
|
||||
@Override
|
||||
public void executeInTransaction(Consumer<TransactionOperations> operations) {
|
||||
Objects.requireNonNull(operations, "operations must not be null");
|
||||
|
||||
|
||||
Connection connection = null;
|
||||
try {
|
||||
connection = DriverManager.getConnection(jdbcUrl);
|
||||
connection.setAutoCommit(false);
|
||||
|
||||
|
||||
TransactionOperationsImpl txOps = new TransactionOperationsImpl(connection);
|
||||
operations.accept(txOps);
|
||||
|
||||
|
||||
connection.commit();
|
||||
logger.debug("Transaction committed successfully");
|
||||
|
||||
} catch (SQLException e) {
|
||||
|
||||
} catch (Exception e) {
|
||||
// Rollback for ANY exception, not just SQLException
|
||||
if (connection != null) {
|
||||
try {
|
||||
connection.rollback();
|
||||
@@ -60,6 +61,10 @@ public class SqliteUnitOfWorkAdapter implements UnitOfWorkPort {
|
||||
logger.error("Failed to rollback transaction: {}", rollbackEx.getMessage(), rollbackEx);
|
||||
}
|
||||
}
|
||||
// Re-throw as DocumentPersistenceException if not already
|
||||
if (e instanceof DocumentPersistenceException) {
|
||||
throw (DocumentPersistenceException) e;
|
||||
}
|
||||
throw new DocumentPersistenceException("Transaction failed: " + e.getMessage(), e);
|
||||
} finally {
|
||||
if (connection != null) {
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
package de.gecheckt.pdf.umbenenner.adapter.out.sqlite;
|
||||
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.DocumentPersistenceException;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.DocumentRecord;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.FailureCounters;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.ProcessingAttempt;
|
||||
import de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint;
|
||||
import de.gecheckt.pdf.umbenenner.domain.model.ProcessingStatus;
|
||||
import de.gecheckt.pdf.umbenenner.domain.model.RunId;
|
||||
import de.gecheckt.pdf.umbenenner.domain.model.SourceDocumentLocator;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link SqliteUnitOfWorkAdapter}.
|
||||
* <p>
|
||||
* Tests verify transactional semantics: successful commits, rollback on first-write failure,
|
||||
* rollback on second-write failure, and proper handling of DocumentPersistenceException.
|
||||
*
|
||||
* @since M4-AP-006
|
||||
*/
|
||||
class SqliteUnitOfWorkAdapterTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
private String jdbcUrl;
|
||||
private SqliteUnitOfWorkAdapter unitOfWorkAdapter;
|
||||
private SqliteSchemaInitializationAdapter schemaAdapter;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
Path dbFile = tempDir.resolve("test.db");
|
||||
jdbcUrl = "jdbc:sqlite:" + dbFile.toAbsolutePath().toString().replace('\\', '/');
|
||||
|
||||
// Initialize schema
|
||||
schemaAdapter = new SqliteSchemaInitializationAdapter(jdbcUrl);
|
||||
schemaAdapter.initializeSchema();
|
||||
|
||||
// Create the unit of work adapter
|
||||
unitOfWorkAdapter = new SqliteUnitOfWorkAdapter(jdbcUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies DocumentPersistenceException is properly re-thrown
|
||||
* without double-wrapping.
|
||||
*/
|
||||
@Test
|
||||
void executeInTransaction_reThrowsDocumentPersistenceExceptionAsIs() {
|
||||
DocumentPersistenceException originalException =
|
||||
new DocumentPersistenceException("Original error");
|
||||
|
||||
DocumentPersistenceException thrownException = assertThrows(
|
||||
DocumentPersistenceException.class,
|
||||
() -> {
|
||||
unitOfWorkAdapter.executeInTransaction(txOps -> {
|
||||
throw originalException;
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
assertSame(originalException, thrownException,
|
||||
"DocumentPersistenceException should be re-thrown as-is, not wrapped");
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies rollback occurs when the first write fails.
|
||||
* The transaction should not commit any data.
|
||||
*/
|
||||
@Test
|
||||
void executeInTransaction_rollsBackWhenFirstWriteFails() {
|
||||
DocumentFingerprint fingerprint = new DocumentFingerprint("2222222222222222222222222222222222222222222222222222222222222222");
|
||||
|
||||
// Create repositories for verification
|
||||
SqliteDocumentRecordRepositoryAdapter docRepository =
|
||||
new SqliteDocumentRecordRepositoryAdapter(jdbcUrl);
|
||||
|
||||
assertThrows(DocumentPersistenceException.class, () -> {
|
||||
unitOfWorkAdapter.executeInTransaction(txOps -> {
|
||||
// First write: throw exception directly (simulates write failure)
|
||||
throw new DocumentPersistenceException("Simulated first write failure");
|
||||
});
|
||||
});
|
||||
|
||||
// Verify no records were persisted (rollback occurred)
|
||||
var lookupResult = docRepository.findByFingerprint(fingerprint);
|
||||
assertTrue(lookupResult instanceof de.gecheckt.pdf.umbenenner.application.port.out.DocumentUnknown,
|
||||
"No DocumentRecord should be persisted after rollback");
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies rollback occurs when the second write fails after the first write succeeds.
|
||||
* The transaction should not commit the first write without the second.
|
||||
*/
|
||||
@Test
|
||||
void executeInTransaction_rollsBackWhenSecondWriteFails() {
|
||||
DocumentFingerprint fingerprint = new DocumentFingerprint("3333333333333333333333333333333333333333333333333333333333333333");
|
||||
Instant now = Instant.now().truncatedTo(ChronoUnit.MICROS);
|
||||
DocumentRecord record = new DocumentRecord(
|
||||
fingerprint,
|
||||
new SourceDocumentLocator("/source/test.pdf"),
|
||||
"test.pdf",
|
||||
ProcessingStatus.PROCESSING,
|
||||
FailureCounters.zero(),
|
||||
null,
|
||||
null,
|
||||
now,
|
||||
now
|
||||
);
|
||||
|
||||
// Create repositories for verification
|
||||
SqliteDocumentRecordRepositoryAdapter docRepository =
|
||||
new SqliteDocumentRecordRepositoryAdapter(jdbcUrl);
|
||||
|
||||
assertThrows(DocumentPersistenceException.class, () -> {
|
||||
unitOfWorkAdapter.executeInTransaction(txOps -> {
|
||||
// First write: succeeds
|
||||
txOps.createDocumentRecord(record);
|
||||
|
||||
// Second write: fails by throwing DocumentPersistenceException
|
||||
throw new DocumentPersistenceException("Simulated write failure");
|
||||
});
|
||||
});
|
||||
|
||||
// Verify no records were persisted due to rollback
|
||||
var lookupResult = docRepository.findByFingerprint(fingerprint);
|
||||
assertTrue(lookupResult instanceof de.gecheckt.pdf.umbenenner.application.port.out.DocumentUnknown,
|
||||
"DocumentRecord should be rolled back when second write fails");
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies rollback occurs when an arbitrary RuntimeException is thrown.
|
||||
*/
|
||||
@Test
|
||||
void executeInTransaction_rollsBackOnArbitraryRuntimeException() {
|
||||
DocumentFingerprint fingerprint = new DocumentFingerprint("4444444444444444444444444444444444444444444444444444444444444444");
|
||||
Instant now = Instant.now().truncatedTo(ChronoUnit.MICROS);
|
||||
DocumentRecord record = new DocumentRecord(
|
||||
fingerprint,
|
||||
new SourceDocumentLocator("/source/test.pdf"),
|
||||
"test.pdf",
|
||||
ProcessingStatus.PROCESSING,
|
||||
FailureCounters.zero(),
|
||||
null,
|
||||
null,
|
||||
now,
|
||||
now
|
||||
);
|
||||
|
||||
RuntimeException customException = new RuntimeException("Custom runtime error");
|
||||
|
||||
// Create repositories for verification
|
||||
SqliteDocumentRecordRepositoryAdapter docRepository =
|
||||
new SqliteDocumentRecordRepositoryAdapter(jdbcUrl);
|
||||
|
||||
DocumentPersistenceException thrownException = assertThrows(
|
||||
DocumentPersistenceException.class,
|
||||
() -> {
|
||||
unitOfWorkAdapter.executeInTransaction(txOps -> {
|
||||
txOps.createDocumentRecord(record);
|
||||
throw customException;
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
// Verify the exception was wrapped in DocumentPersistenceException
|
||||
assertSame(customException, thrownException.getCause(),
|
||||
"RuntimeException should be wrapped in DocumentPersistenceException");
|
||||
|
||||
// Verify rollback occurred
|
||||
var lookupResult = docRepository.findByFingerprint(fingerprint);
|
||||
assertTrue(lookupResult instanceof de.gecheckt.pdf.umbenenner.application.port.out.DocumentUnknown,
|
||||
"DocumentRecord should be rolled back on runtime exception");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user