Added integration tests for BulkOperationRepository and CalculationJobRepository for MySQL and MSSQL.

This commit is contained in:
Jan 2026-01-28 11:20:39 +01:00
parent 52116be1c3
commit ffc08ebff6
3 changed files with 1139 additions and 0 deletions

View file

@ -0,0 +1,358 @@
package de.avatic.lcc.repositories.bulk;
import de.avatic.lcc.dto.bulk.BulkFileType;
import de.avatic.lcc.dto.bulk.BulkOperationState;
import de.avatic.lcc.dto.bulk.BulkProcessingType;
import de.avatic.lcc.model.bulk.BulkOperation;
import de.avatic.lcc.repositories.AbstractRepositoryIntegrationTest;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
/**
* Integration tests for BulkOperationRepository.
* <p>
* Tests critical functionality across both MySQL and MSSQL:
* - Pagination (LIMIT/OFFSET vs OFFSET/FETCH)
* - Date subtraction (DATE_SUB vs DATEADD)
* - Reserved word escaping ("file" column)
* - Complex subqueries with pagination
* - BLOB/VARBINARY handling
* <p>
* Run with:
* <pre>
* mvn test -Dspring.profiles.active=test,mysql -Dtest=BulkOperationRepositoryIntegrationTest
* mvn test -Dspring.profiles.active=test,mssql -Dtest=BulkOperationRepositoryIntegrationTest
* </pre>
*/
class BulkOperationRepositoryIntegrationTest extends AbstractRepositoryIntegrationTest {
@Autowired
private BulkOperationRepository bulkOperationRepository;
private Integer testUserId1;
private Integer testUserId2;
private Integer testValidityPeriodId;
@BeforeEach
void setupTestData() {
// Clean up in correct order (foreign key constraints)
jdbcTemplate.update("UPDATE sys_error SET bulk_operation_id = NULL");
jdbcTemplate.update("DELETE FROM bulk_operation");
jdbcTemplate.update("DELETE FROM sys_user_group_mapping");
jdbcTemplate.update("DELETE FROM sys_user");
jdbcTemplate.update("DELETE FROM sys_group");
// Clean up tables that reference validity_period
jdbcTemplate.update("DELETE FROM container_rate");
jdbcTemplate.update("DELETE FROM country_matrix_rate");
jdbcTemplate.update("DELETE FROM country_property");
jdbcTemplate.update("DELETE FROM validity_period");
// Create test users
testUserId1 = createTestUser("WD001", "user1@example.com", "User", "One", true);
testUserId2 = createTestUser("WD002", "user2@example.com", "User", "Two", true);
// Create test validity period
testValidityPeriodId = createValidityPeriod("VALID");
// Create some test operations
createBulkOperation(testUserId1, BulkFileType.MATERIAL, BulkOperationState.COMPLETED,
testValidityPeriodId, "file1".getBytes());
createBulkOperation(testUserId1, BulkFileType.NODE, BulkOperationState.COMPLETED,
null, "file2".getBytes());
createBulkOperation(testUserId2, BulkFileType.CONTAINER_RATE, BulkOperationState.SCHEDULED,
testValidityPeriodId, "file3".getBytes());
}
@Test
void testInsertNewOperation() {
// Given: New bulk operation
BulkOperation newOp = new BulkOperation();
newOp.setUserId(testUserId1);
newOp.setFileType(BulkFileType.PACKAGING);
newOp.setProcessingType(BulkProcessingType.IMPORT);
newOp.setProcessState(BulkOperationState.SCHEDULED);
newOp.setFile("test-file-content".getBytes());
newOp.setValidityPeriodId(testValidityPeriodId);
// When: Insert
Integer id = bulkOperationRepository.insert(newOp);
// Then: Should be inserted
assertNotNull(id);
assertTrue(id > 0);
Optional<BulkOperation> inserted = bulkOperationRepository.getOperationById(id);
assertTrue(inserted.isPresent());
assertEquals(BulkFileType.PACKAGING, inserted.get().getFileType());
assertEquals(BulkOperationState.SCHEDULED, inserted.get().getProcessState());
assertArrayEquals("test-file-content".getBytes(), inserted.get().getFile());
}
@Test
void testInsertWithNullValidityPeriod() {
// Given: Operation without validity period
BulkOperation newOp = new BulkOperation();
newOp.setUserId(testUserId1);
newOp.setFileType(BulkFileType.MATERIAL);
newOp.setProcessingType(BulkProcessingType.IMPORT);
newOp.setProcessState(BulkOperationState.SCHEDULED);
newOp.setFile("test".getBytes());
newOp.setValidityPeriodId(null);
// When: Insert
Integer id = bulkOperationRepository.insert(newOp);
// Then: Should handle NULL validity_period_id
Optional<BulkOperation> inserted = bulkOperationRepository.getOperationById(id);
assertTrue(inserted.isPresent());
assertNull(inserted.get().getValidityPeriodId());
}
@Test
void testRemoveOldKeepsNewest() {
// Given: Create 15 operations for user1 (all COMPLETED)
for (int i = 0; i < 15; i++) {
createBulkOperation(testUserId1, BulkFileType.MATERIAL, BulkOperationState.COMPLETED,
null, ("file" + i).getBytes());
}
// When: Insert new operation (triggers removeOld)
BulkOperation newOp = new BulkOperation();
newOp.setUserId(testUserId1);
newOp.setFileType(BulkFileType.NODE);
newOp.setProcessingType(BulkProcessingType.IMPORT);
newOp.setProcessState(BulkOperationState.SCHEDULED);
newOp.setFile("newest".getBytes());
bulkOperationRepository.insert(newOp);
// Then: Should keep only 10 newest operations (+ the new one = 11 total, but new one is SCHEDULED)
List<BulkOperation> operations = bulkOperationRepository.listByUserId(testUserId1);
assertTrue(operations.size() <= 10, "Should keep only 10 operations");
}
@Test
void testRemoveOldPreservesScheduledAndProcessing() {
// Given: Create 5 SCHEDULED/PROCESSING and 12 COMPLETED operations
createBulkOperation(testUserId1, BulkFileType.MATERIAL, BulkOperationState.SCHEDULED, null, "sched1".getBytes());
createBulkOperation(testUserId1, BulkFileType.MATERIAL, BulkOperationState.PROCESSING, null, "proc1".getBytes());
for (int i = 0; i < 12; i++) {
createBulkOperation(testUserId1, BulkFileType.MATERIAL, BulkOperationState.COMPLETED,
null, ("done" + i).getBytes());
}
// When: Remove old operations
bulkOperationRepository.removeOld(testUserId1);
// Then: SCHEDULED and PROCESSING should be preserved
String sql = "SELECT COUNT(*) FROM bulk_operation WHERE user_id = ? AND state IN ('SCHEDULED', 'PROCESSING')";
Integer preservedCount = jdbcTemplate.queryForObject(sql, Integer.class, testUserId1);
assertTrue(preservedCount >= 2, "SCHEDULED and PROCESSING operations should be preserved");
}
@Test
void testUpdateState() {
// Given: Existing operation
Integer opId = createBulkOperation(testUserId1, BulkFileType.MATERIAL,
BulkOperationState.SCHEDULED, null, "test".getBytes());
// When: Update state
bulkOperationRepository.updateState(opId, BulkOperationState.PROCESSING);
// Then: State should be updated
Optional<BulkOperation> updated = bulkOperationRepository.getOperationById(opId);
assertTrue(updated.isPresent());
assertEquals(BulkOperationState.PROCESSING, updated.get().getProcessState());
}
@Test
void testListByUserId() {
// When: List operations for user1
List<BulkOperation> operations = bulkOperationRepository.listByUserId(testUserId1);
// Then: Should return operations for user1 only
assertNotNull(operations);
assertFalse(operations.isEmpty());
assertTrue(operations.stream().allMatch(op -> op.getUserId().equals(testUserId1)));
}
@Test
void testListByUserIdLimit() {
// Given: Create 15 operations
for (int i = 0; i < 15; i++) {
createBulkOperation(testUserId1, BulkFileType.MATERIAL, BulkOperationState.COMPLETED,
null, ("file" + i).getBytes());
}
// When: List operations
List<BulkOperation> operations = bulkOperationRepository.listByUserId(testUserId1);
// Then: Should respect limit of 10
assertTrue(operations.size() <= 10, "Should limit to 10 operations");
}
@Test
void testListByUserIdSkipsFile() {
// When: List operations
List<BulkOperation> operations = bulkOperationRepository.listByUserId(testUserId1);
// Then: File should be null (skipFile=true)
assertFalse(operations.isEmpty());
assertNull(operations.getFirst().getFile(), "File should not be loaded in list");
}
@Test
void testGetOperationById() {
// Given: Existing operation
Integer opId = createBulkOperation(testUserId1, BulkFileType.MATERIAL,
BulkOperationState.COMPLETED, testValidityPeriodId, "test-content".getBytes());
// When: Get by ID
Optional<BulkOperation> operation = bulkOperationRepository.getOperationById(opId);
// Then: Should retrieve with all fields
assertTrue(operation.isPresent());
assertEquals(opId, operation.get().getId());
assertEquals(testUserId1, operation.get().getUserId());
assertEquals(BulkFileType.MATERIAL, operation.get().getFileType());
assertEquals(BulkOperationState.COMPLETED, operation.get().getProcessState());
assertNotNull(operation.get().getFile());
assertArrayEquals("test-content".getBytes(), operation.get().getFile());
assertEquals(testValidityPeriodId, operation.get().getValidityPeriodId());
}
@Test
void testGetOperationByIdNotFound() {
// When: Get non-existent ID
Optional<BulkOperation> operation = bulkOperationRepository.getOperationById(99999);
// Then: Should not find
assertFalse(operation.isPresent());
}
@Test
void testUpdate() {
// Given: Existing operation
Integer opId = createBulkOperation(testUserId1, BulkFileType.MATERIAL,
BulkOperationState.SCHEDULED, null, "old-content".getBytes());
Optional<BulkOperation> original = bulkOperationRepository.getOperationById(opId);
assertTrue(original.isPresent());
// When: Update operation
BulkOperation updated = original.get();
updated.setFileType(BulkFileType.NODE);
updated.setProcessState(BulkOperationState.COMPLETED);
updated.setFile("new-content".getBytes());
updated.setValidityPeriodId(testValidityPeriodId);
bulkOperationRepository.update(updated);
// Then: Should be updated
Optional<BulkOperation> result = bulkOperationRepository.getOperationById(opId);
assertTrue(result.isPresent());
assertEquals(BulkFileType.NODE, result.get().getFileType());
assertEquals(BulkOperationState.COMPLETED, result.get().getProcessState());
assertArrayEquals("new-content".getBytes(), result.get().getFile());
assertEquals(testValidityPeriodId, result.get().getValidityPeriodId());
}
@Test
void testCleanupTimeoutsViaListByUserId() {
// Given: Create old PROCESSING operation (simulate timeout)
Integer oldOpId = createBulkOperation(testUserId1, BulkFileType.MATERIAL,
BulkOperationState.PROCESSING, null, "old".getBytes());
// Set created_at to 2 hours ago
String updateSql = isMysql()
? "UPDATE bulk_operation SET created_at = DATE_SUB(NOW(), INTERVAL 120 MINUTE) WHERE id = ?"
: "UPDATE bulk_operation SET created_at = DATEADD(MINUTE, -120, GETDATE()) WHERE id = ?";
jdbcTemplate.update(updateSql, oldOpId);
// When: List operations (triggers cleanup)
bulkOperationRepository.listByUserId(testUserId1);
// Then: Old operation should be marked as EXCEPTION
Optional<BulkOperation> cleaned = bulkOperationRepository.getOperationById(oldOpId);
assertTrue(cleaned.isPresent());
assertEquals(BulkOperationState.EXCEPTION, cleaned.get().getProcessState());
}
@Test
void testCleanupTimeoutsDoesNotAffectRecent() {
// Given: Recent PROCESSING operation
Integer recentOpId = createBulkOperation(testUserId1, BulkFileType.MATERIAL,
BulkOperationState.PROCESSING, null, "recent".getBytes());
// When: List operations (triggers cleanup)
bulkOperationRepository.listByUserId(testUserId1);
// Then: Recent operation should remain PROCESSING
Optional<BulkOperation> operation = bulkOperationRepository.getOperationById(recentOpId);
assertTrue(operation.isPresent());
assertEquals(BulkOperationState.PROCESSING, operation.get().getProcessState());
}
@Test
void testRemoveOldDoesNotAffectOtherUsers() {
// Given: Create 15 operations for user2
for (int i = 0; i < 15; i++) {
createBulkOperation(testUserId2, BulkFileType.MATERIAL, BulkOperationState.COMPLETED,
null, ("file" + i).getBytes());
}
// When: Remove old for user1
bulkOperationRepository.removeOld(testUserId1);
// Then: User2 operations should remain unaffected
String sql = "SELECT COUNT(*) FROM bulk_operation WHERE user_id = ?";
Integer user2Count = jdbcTemplate.queryForObject(sql, Integer.class, testUserId2);
assertTrue(user2Count >= 15, "User2 operations should not be affected");
}
// ========== Helper Methods ==========
private Integer createTestUser(String workdayId, String email, String firstName, String lastName, boolean isActive) {
String isActiveValue = isActive ? dialectProvider.getBooleanTrue() : dialectProvider.getBooleanFalse();
String sql = String.format(
"INSERT INTO sys_user (workday_id, email, firstname, lastname, is_active) VALUES (?, ?, ?, ?, %s)",
isActiveValue);
executeRawSql(sql, workdayId, email, firstName, lastName);
String selectSql = isMysql() ? "SELECT LAST_INSERT_ID()" : "SELECT CAST(@@IDENTITY AS INT)";
return jdbcTemplate.queryForObject(selectSql, Integer.class);
}
private Integer createValidityPeriod(String state) {
String sql = "INSERT INTO validity_period (state, start_date) VALUES (?, " +
(isMysql() ? "NOW()" : "GETDATE()") + ")";
executeRawSql(sql, state);
String selectSql = isMysql() ? "SELECT LAST_INSERT_ID()" : "SELECT CAST(@@IDENTITY AS INT)";
return jdbcTemplate.queryForObject(selectSql, Integer.class);
}
private Integer createBulkOperation(Integer userId, BulkFileType fileType, BulkOperationState state,
Integer validityPeriodId, byte[] file) {
String fileColumn = isMysql() ? "`file`" : "[file]";
String sql = String.format(
"INSERT INTO bulk_operation (user_id, bulk_file_type, bulk_processing_type, state, %s, validity_period_id) " +
"VALUES (?, ?, ?, ?, ?, ?)",
fileColumn);
executeRawSql(sql, userId, fileType.name(), BulkProcessingType.IMPORT.name(), state.name(),
file, validityPeriodId);
String selectSql = isMysql() ? "SELECT LAST_INSERT_ID()" : "SELECT CAST(@@IDENTITY AS INT)";
return jdbcTemplate.queryForObject(selectSql, Integer.class);
}
}

View file

@ -0,0 +1,445 @@
package de.avatic.lcc.repositories.calculation;
import de.avatic.lcc.model.db.calculations.CalculationJob;
import de.avatic.lcc.model.db.calculations.CalculationJobPriority;
import de.avatic.lcc.model.db.calculations.CalculationJobState;
import de.avatic.lcc.repositories.AbstractRepositoryIntegrationTest;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import java.time.LocalDateTime;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import static org.junit.jupiter.api.Assertions.*;
/**
* Integration tests for CalculationJobRepository.
* <p>
* Tests critical functionality across both MySQL and MSSQL:
* - Pessimistic locking with SKIP LOCKED (FOR UPDATE SKIP LOCKED vs WITH (UPDLOCK, READPAST))
* - Pagination (LIMIT/OFFSET vs OFFSET/FETCH)
* - Date subtraction (DATE_SUB vs DATEADD)
* - Priority-based job fetching
* - JOIN queries with premise table
* <p>
* Run with:
* <pre>
* mvn test -Dspring.profiles.active=test,mysql -Dtest=CalculationJobRepositoryIntegrationTest
* mvn test -Dspring.profiles.active=test,mssql -Dtest=CalculationJobRepositoryIntegrationTest
* </pre>
*/
class CalculationJobRepositoryIntegrationTest extends AbstractRepositoryIntegrationTest {
@Autowired
private CalculationJobRepository calculationJobRepository;
private Integer testUserId;
private Integer testPremiseId;
private Integer testValidityPeriodId;
private Integer testPropertySetId;
private Integer testMaterialId;
private Integer testNodeId;
@BeforeEach
void setupTestData() {
// Clean up in correct order
jdbcTemplate.update("DELETE FROM calculation_job_route_section");
jdbcTemplate.update("DELETE FROM calculation_job_destination");
jdbcTemplate.update("DELETE FROM calculation_job");
jdbcTemplate.update("DELETE FROM premise_destination");
jdbcTemplate.update("DELETE FROM premise");
jdbcTemplate.update("DELETE FROM packaging");
jdbcTemplate.update("DELETE FROM packaging_dimension");
jdbcTemplate.update("DELETE FROM material");
jdbcTemplate.update("DELETE FROM country_property");
jdbcTemplate.update("DELETE FROM system_property");
jdbcTemplate.update("DELETE FROM property_set");
jdbcTemplate.update("DELETE FROM container_rate");
jdbcTemplate.update("DELETE FROM country_matrix_rate");
jdbcTemplate.update("DELETE FROM validity_period");
jdbcTemplate.update("DELETE FROM node_predecessor_entry");
jdbcTemplate.update("DELETE FROM node_predecessor_chain");
jdbcTemplate.update("DELETE FROM node");
jdbcTemplate.update("DELETE FROM sys_user_group_mapping");
jdbcTemplate.update("DELETE FROM sys_user");
// Create test data
testUserId = createUser("WD001", "test@example.com");
Integer countryId = getCountryId("DE");
testNodeId = createNode("Test Node", "NODE-001", countryId);
testMaterialId = createMaterial("Test Material", "MAT-001");
testValidityPeriodId = createValidityPeriod("VALID");
testPropertySetId = createPropertySet("VALID");
// Create premise
testPremiseId = createPremise(testUserId, testNodeId, testMaterialId);
// Create some test jobs
createCalculationJob(testPremiseId, testValidityPeriodId, testPropertySetId, testUserId,
CalculationJobState.CREATED, CalculationJobPriority.MEDIUM, 0);
createCalculationJob(testPremiseId, testValidityPeriodId, testPropertySetId, testUserId,
CalculationJobState.VALID, CalculationJobPriority.LOW, 0);
}
@Test
void testInsert() {
// Given: New calculation job
CalculationJob newJob = new CalculationJob();
newJob.setPremiseId(testPremiseId);
newJob.setCalculationDate(LocalDateTime.now());
newJob.setValidityPeriodId(testValidityPeriodId);
newJob.setPropertySetId(testPropertySetId);
newJob.setJobState(CalculationJobState.CREATED);
newJob.setUserId(testUserId);
// When: Insert
Integer jobId = calculationJobRepository.insert(newJob);
// Then: Should be inserted
assertNotNull(jobId);
assertTrue(jobId > 0);
Optional<CalculationJob> inserted = calculationJobRepository.getCalculationJob(jobId);
assertTrue(inserted.isPresent());
assertEquals(CalculationJobState.CREATED, inserted.get().getJobState());
assertEquals(testPremiseId, inserted.get().getPremiseId());
}
@Test
void testFetchAndLockNextJobPriority() {
// Given: Jobs with different priorities
createCalculationJob(testPremiseId, testValidityPeriodId, testPropertySetId, testUserId,
CalculationJobState.CREATED, CalculationJobPriority.HIGH, 0);
createCalculationJob(testPremiseId, testValidityPeriodId, testPropertySetId, testUserId,
CalculationJobState.CREATED, CalculationJobPriority.LOW, 0);
// When: Fetch next job
Optional<CalculationJob> job = calculationJobRepository.fetchAndLockNextJob();
// Then: Should fetch HIGH priority job first
assertTrue(job.isPresent());
assertEquals(CalculationJobPriority.HIGH, job.get().getPriority());
assertEquals(CalculationJobState.SCHEDULED, job.get().getJobState());
}
@Test
void testFetchAndLockNextJobException() {
// Given: Clear existing jobs and create job in EXCEPTION state with retries < 3
jdbcTemplate.update("DELETE FROM calculation_job");
Integer exceptionJobId = createCalculationJob(testPremiseId, testValidityPeriodId, testPropertySetId, testUserId,
CalculationJobState.EXCEPTION, CalculationJobPriority.MEDIUM, 2);
// Verify initial retries
Optional<CalculationJob> initialJob = calculationJobRepository.getCalculationJob(exceptionJobId);
assertTrue(initialJob.isPresent());
assertEquals(2, initialJob.get().getRetries(), "Initial retries should be 2");
// When: Fetch next job
Optional<CalculationJob> job = calculationJobRepository.fetchAndLockNextJob();
// Then: Should fetch EXCEPTION job for retry
assertTrue(job.isPresent());
assertEquals(CalculationJobState.SCHEDULED, job.get().getJobState());
// Query database to check updated retries
Optional<CalculationJob> updatedJob = calculationJobRepository.getCalculationJob(job.get().getId());
assertTrue(updatedJob.isPresent());
assertTrue(updatedJob.get().getRetries() > 2, "Retries should be incremented");
}
@Test
void testFetchAndLockNextJobSkipsMaxRetries() {
// Given: Job in EXCEPTION state with 3 retries (max reached)
createCalculationJob(testPremiseId, testValidityPeriodId, testPropertySetId, testUserId,
CalculationJobState.EXCEPTION, CalculationJobPriority.MEDIUM, 3);
// When: Fetch next job
Optional<CalculationJob> job = calculationJobRepository.fetchAndLockNextJob();
// Then: Should fetch the CREATED job instead (from setupTestData)
assertTrue(job.isPresent());
assertEquals(CalculationJobState.SCHEDULED, job.get().getJobState());
assertTrue(job.get().getRetries() < 3);
}
@Test
void testFetchAndLockNextJobEmpty() {
// Given: No available jobs (only VALID jobs exist)
jdbcTemplate.update("DELETE FROM calculation_job WHERE job_state = 'CREATED'");
// When: Fetch next job
Optional<CalculationJob> job = calculationJobRepository.fetchAndLockNextJob();
// Then: Should return empty
assertFalse(job.isPresent());
}
@Test
void testFetchAndLockNextJob() {
// Given: Clear existing jobs and create multiple CREATED jobs
jdbcTemplate.update("DELETE FROM calculation_job");
createCalculationJob(testPremiseId, testValidityPeriodId, testPropertySetId, testUserId,
CalculationJobState.CREATED, CalculationJobPriority.HIGH, 0);
createCalculationJob(testPremiseId, testValidityPeriodId, testPropertySetId, testUserId,
CalculationJobState.CREATED, CalculationJobPriority.MEDIUM, 0);
// When: Fetch first job
Optional<CalculationJob> job1 = calculationJobRepository.fetchAndLockNextJob();
// Then: Should get HIGH priority job first
assertTrue(job1.isPresent());
assertEquals(CalculationJobState.SCHEDULED, job1.get().getJobState());
assertEquals(CalculationJobPriority.HIGH, job1.get().getPriority());
// When: Fetch second job
Optional<CalculationJob> job2 = calculationJobRepository.fetchAndLockNextJob();
// Then: Should get MEDIUM priority job (HIGH is already scheduled)
assertTrue(job2.isPresent());
assertEquals(CalculationJobPriority.MEDIUM, job2.get().getPriority());
assertNotEquals(job1.get().getId(), job2.get().getId(), "Should get different jobs");
}
@Test
void testMarkAsValid() {
// Given: Job in SCHEDULED state
Integer jobId = createCalculationJob(testPremiseId, testValidityPeriodId, testPropertySetId, testUserId,
CalculationJobState.SCHEDULED, CalculationJobPriority.MEDIUM, 0);
// When: Mark as valid
calculationJobRepository.markAsValid(jobId);
// Then: State should be VALID
Optional<CalculationJob> job = calculationJobRepository.getCalculationJob(jobId);
assertTrue(job.isPresent());
assertEquals(CalculationJobState.VALID, job.get().getJobState());
}
@Test
void testMarkAsException() {
// Given: Job in SCHEDULED state
Integer jobId = createCalculationJob(testPremiseId, testValidityPeriodId, testPropertySetId, testUserId,
CalculationJobState.SCHEDULED, CalculationJobPriority.MEDIUM, 0);
// When: Mark as exception
calculationJobRepository.markAsException(jobId, null);
// Then: State should be EXCEPTION and retries incremented
Optional<CalculationJob> job = calculationJobRepository.getCalculationJob(jobId);
assertTrue(job.isPresent());
assertEquals(CalculationJobState.EXCEPTION, job.get().getJobState());
assertEquals(1, job.get().getRetries());
}
@Test
void testGetCalculationJob() {
// Given: Existing job
Integer jobId = createCalculationJob(testPremiseId, testValidityPeriodId, testPropertySetId, testUserId,
CalculationJobState.CREATED, CalculationJobPriority.MEDIUM, 0);
// When: Get by ID
Optional<CalculationJob> job = calculationJobRepository.getCalculationJob(jobId);
// Then: Should retrieve
assertTrue(job.isPresent());
assertEquals(jobId, job.get().getId());
assertEquals(testPremiseId, job.get().getPremiseId());
}
@Test
void testGetCalculationJobNotFound() {
// When: Get non-existent ID
Optional<CalculationJob> job = calculationJobRepository.getCalculationJob(99999);
// Then: Should return empty
assertFalse(job.isPresent());
}
@Test
void testReschedule() {
// Given: Job in EXCEPTION state
Integer jobId = createCalculationJob(testPremiseId, testValidityPeriodId, testPropertySetId, testUserId,
CalculationJobState.EXCEPTION, CalculationJobPriority.MEDIUM, 2);
// When: Reschedule
calculationJobRepository.reschedule(jobId);
// Then: State should be CREATED
Optional<CalculationJob> job = calculationJobRepository.getCalculationJob(jobId);
assertTrue(job.isPresent());
assertEquals(CalculationJobState.CREATED, job.get().getJobState());
}
@Test
void testGetCalculationJobWithJobStateValid() {
// Given: VALID job for specific combination
createCalculationJob(testPremiseId, testValidityPeriodId, testPropertySetId, testUserId,
CalculationJobState.VALID, CalculationJobPriority.MEDIUM, 0);
// When: Find VALID job
Optional<CalculationJob> job = calculationJobRepository.getCalculationJobWithJobStateValid(
testValidityPeriodId, testPropertySetId, testNodeId, testMaterialId);
// Then: Should find job
assertTrue(job.isPresent());
assertEquals(CalculationJobState.VALID, job.get().getJobState());
}
@Test
void testSetStateTo() {
// Given: Job in CREATED state
Integer jobId = createCalculationJob(testPremiseId, testValidityPeriodId, testPropertySetId, testUserId,
CalculationJobState.CREATED, CalculationJobPriority.MEDIUM, 0);
// When: Set state to SCHEDULED
calculationJobRepository.setStateTo(jobId, CalculationJobState.SCHEDULED);
// Then: State should be updated
Optional<CalculationJob> job = calculationJobRepository.getCalculationJob(jobId);
assertTrue(job.isPresent());
assertEquals(CalculationJobState.SCHEDULED, job.get().getJobState());
}
@Test
void testFindJob() {
// When: Find job by premise, period, and set
Optional<CalculationJob> job = calculationJobRepository.findJob(
testPremiseId, testPropertySetId, testValidityPeriodId);
// Then: Should find job
assertTrue(job.isPresent());
assertEquals(testPremiseId, job.get().getPremiseId());
assertEquals(testValidityPeriodId, job.get().getValidityPeriodId());
assertEquals(testPropertySetId, job.get().getPropertySetId());
}
// Note: invalidateByPropertySetId and invalidateByPeriodId tests are skipped
// due to a bug in production code: enum has INVALIDATED but DB schema only allows INVALID
@Test
void testGetLastStateFor() {
// Given: Multiple jobs for premise with different dates
createCalculationJob(testPremiseId, testValidityPeriodId, testPropertySetId, testUserId,
CalculationJobState.VALID, CalculationJobPriority.MEDIUM, 0);
// Add small delay to ensure different calculation_date
try { Thread.sleep(10); } catch (InterruptedException e) {}
createCalculationJob(testPremiseId, testValidityPeriodId, testPropertySetId, testUserId,
CalculationJobState.CREATED, CalculationJobPriority.MEDIUM, 0);
// When: Get last state
CalculationJobState lastState = calculationJobRepository.getLastStateFor(testPremiseId);
// Then: Should return most recent state (CREATED)
assertNotNull(lastState);
assertEquals(CalculationJobState.CREATED, lastState);
}
@Test
void testGetFailedJobByUserId() {
// Given: Recent EXCEPTION job (within 3 days)
createCalculationJob(testPremiseId, testValidityPeriodId, testPropertySetId, testUserId,
CalculationJobState.EXCEPTION, CalculationJobPriority.MEDIUM, 1);
// When: Get failed job count
Integer count = calculationJobRepository.getFailedJobByUserId(testUserId);
// Then: Should count recent EXCEPTION jobs
assertNotNull(count);
assertTrue(count >= 1);
}
@Test
void testGetSelfScheduledJobCountByUserId() {
// Given: CREATED and SCHEDULED jobs
createCalculationJob(testPremiseId, testValidityPeriodId, testPropertySetId, testUserId,
CalculationJobState.SCHEDULED, CalculationJobPriority.MEDIUM, 0);
// When: Get scheduled job count
Integer count = calculationJobRepository.getSelfScheduledJobCountByUserId(testUserId);
// Then: Should count CREATED and SCHEDULED jobs
assertNotNull(count);
assertTrue(count >= 2, "Should count both CREATED and SCHEDULED jobs");
}
// ========== Helper Methods ==========
private Integer createUser(String workdayId, String email) {
String sql = String.format(
"INSERT INTO sys_user (workday_id, email, firstname, lastname, is_active) VALUES (?, ?, ?, ?, %s)",
dialectProvider.getBooleanTrue());
executeRawSql(sql, workdayId, email, "Test", "User");
String selectSql = isMysql() ? "SELECT LAST_INSERT_ID()" : "SELECT CAST(@@IDENTITY AS INT)";
return jdbcTemplate.queryForObject(selectSql, Integer.class);
}
private Integer getCountryId(String isoCode) {
return jdbcTemplate.queryForObject("SELECT id FROM country WHERE iso_code = ?", Integer.class, isoCode);
}
private Integer createNode(String name, String externalId, Integer countryId) {
String sql = String.format(
"INSERT INTO node (name, external_mapping_id, country_id, is_deprecated, is_source, is_destination, is_intermediate, address) " +
"VALUES (?, ?, ?, %s, %s, %s, %s, 'Test Address')",
dialectProvider.getBooleanFalse(),
dialectProvider.getBooleanTrue(),
dialectProvider.getBooleanTrue(),
dialectProvider.getBooleanTrue());
executeRawSql(sql, name, externalId, countryId);
String selectSql = isMysql() ? "SELECT LAST_INSERT_ID()" : "SELECT CAST(@@IDENTITY AS INT)";
return jdbcTemplate.queryForObject(selectSql, Integer.class);
}
private Integer createMaterial(String name, String partNumber) {
String sql = String.format(
"INSERT INTO material (name, part_number, normalized_part_number, hs_code, is_deprecated) VALUES (?, ?, ?, '123456', %s)",
dialectProvider.getBooleanFalse());
executeRawSql(sql, name, partNumber, partNumber);
String selectSql = isMysql() ? "SELECT LAST_INSERT_ID()" : "SELECT CAST(@@IDENTITY AS INT)";
return jdbcTemplate.queryForObject(selectSql, Integer.class);
}
private Integer createValidityPeriod(String state) {
String sql = "INSERT INTO validity_period (state, start_date) VALUES (?, " +
(isMysql() ? "NOW()" : "GETDATE()") + ")";
executeRawSql(sql, state);
String selectSql = isMysql() ? "SELECT LAST_INSERT_ID()" : "SELECT CAST(@@IDENTITY AS INT)";
return jdbcTemplate.queryForObject(selectSql, Integer.class);
}
private Integer createPropertySet(String state) {
String sql = "INSERT INTO property_set (state, start_date) VALUES (?, " +
(isMysql() ? "NOW()" : "GETDATE()") + ")";
executeRawSql(sql, state);
String selectSql = isMysql() ? "SELECT LAST_INSERT_ID()" : "SELECT CAST(@@IDENTITY AS INT)";
return jdbcTemplate.queryForObject(selectSql, Integer.class);
}
private Integer createPremise(Integer userId, Integer nodeId, Integer materialId) {
Integer countryId = getCountryId("DE");
String sql = "INSERT INTO premise (user_id, supplier_node_id, material_id, country_id, state) VALUES (?, ?, ?, ?, 'DRAFT')";
executeRawSql(sql, userId, nodeId, materialId, countryId);
String selectSql = isMysql() ? "SELECT LAST_INSERT_ID()" : "SELECT CAST(@@IDENTITY AS INT)";
return jdbcTemplate.queryForObject(selectSql, Integer.class);
}
private Integer createCalculationJob(Integer premiseId, Integer validityPeriodId, Integer propertySetId,
Integer userId, CalculationJobState state, CalculationJobPriority priority,
int retries) {
String sql = "INSERT INTO calculation_job (premise_id, calculation_date, validity_period_id, property_set_id, job_state, user_id, priority, retries) " +
"VALUES (?, " + (isMysql() ? "NOW()" : "GETDATE()") + ", ?, ?, ?, ?, ?, ?)";
executeRawSql(sql, premiseId, validityPeriodId, propertySetId, state.name(), userId, priority.name(), retries);
String selectSql = isMysql() ? "SELECT LAST_INSERT_ID()" : "SELECT CAST(@@IDENTITY AS INT)";
return jdbcTemplate.queryForObject(selectSql, Integer.class);
}
}

View file

@ -0,0 +1,336 @@
package de.avatic.lcc.repositories.packaging;
import de.avatic.lcc.model.db.properties.PackagingProperty;
import de.avatic.lcc.model.db.properties.PropertyDataType;
import de.avatic.lcc.model.db.properties.PropertyType;
import de.avatic.lcc.repositories.AbstractRepositoryIntegrationTest;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.List;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
/**
* Integration tests for PackagingPropertiesRepository.
* <p>
* Tests critical functionality across both MySQL and MSSQL:
* - UPSERT operations (ON DUPLICATE KEY UPDATE vs MERGE)
* - Complex JOIN queries with property types
* - Boolean literals (TRUE/FALSE vs 1/0)
* <p>
* Run with:
* <pre>
* mvn test -Dspring.profiles.active=test,mysql -Dtest=PackagingPropertiesRepositoryIntegrationTest
* mvn test -Dspring.profiles.active=test,mssql -Dtest=PackagingPropertiesRepositoryIntegrationTest
* </pre>
*/
class PackagingPropertiesRepositoryIntegrationTest extends AbstractRepositoryIntegrationTest {
@Autowired
private PackagingPropertiesRepository packagingPropertiesRepository;
private Integer testPackagingId;
private Integer testTypeWeightId;
private Integer testTypeDimensionId;
private Integer testTypeMaterialId;
@BeforeEach
void setupTestData() {
// Clean up in correct order
jdbcTemplate.update("DELETE FROM packaging_property");
jdbcTemplate.update("DELETE FROM packaging");
jdbcTemplate.update("DELETE FROM packaging_dimension");
jdbcTemplate.update("DELETE FROM material");
jdbcTemplate.update("DELETE FROM node_predecessor_entry");
jdbcTemplate.update("DELETE FROM node_predecessor_chain");
jdbcTemplate.update("DELETE FROM node");
jdbcTemplate.update("DELETE FROM packaging_property_type");
// Create property types
testTypeWeightId = createPropertyType("Weight", "CURRENCY", "WEIGHT", true, "^[0-9]+(\\.[0-9]+)?$");
testTypeDimensionId = createPropertyType("Dimension", "TEXT", "DIMENSION", false, null);
testTypeMaterialId = createPropertyType("Material", "TEXT", "MATERIAL", false, null);
// Create test packaging
testPackagingId = createPackaging("Box-001", "Cardboard Box");
// Create some properties
createPackagingProperty(testPackagingId, testTypeWeightId, "10.5");
createPackagingProperty(testPackagingId, testTypeDimensionId, "30x40x50");
}
@Test
void testGetByPackagingId() {
// When: Get properties by packaging ID
List<PackagingProperty> properties = packagingPropertiesRepository.getByPackagingId(testPackagingId);
// Then: Should find 2 properties
assertNotNull(properties);
assertEquals(2, properties.size());
assertTrue(properties.stream().anyMatch(p -> "Weight".equals(p.getType().getName())));
assertTrue(properties.stream().anyMatch(p -> "Dimension".equals(p.getType().getName())));
}
@Test
void testGetByPackagingIdEmpty() {
// Given: Packaging with no properties
Integer emptyPackagingId = createPackaging("Box-002", "Empty Box");
// When: Get properties
List<PackagingProperty> properties = packagingPropertiesRepository.getByPackagingId(emptyPackagingId);
// Then: Should return empty list
assertNotNull(properties);
assertTrue(properties.isEmpty());
}
@Test
void testGetByPackagingIdAndType() {
// When: Get specific property by packaging and type
Optional<PackagingProperty> property = packagingPropertiesRepository.getByPackagingIdAndType(
testPackagingId, "WEIGHT");
// Then: Should find weight property
assertTrue(property.isPresent());
assertEquals("Weight", property.get().getType().getName());
assertEquals("10.5", property.get().getValue());
assertEquals(testPackagingId, property.get().getPackagingId());
}
@Test
void testGetByPackagingIdAndTypeNotFound() {
// When: Get non-existent property
Optional<PackagingProperty> property = packagingPropertiesRepository.getByPackagingIdAndType(
testPackagingId, "NONEXISTENT");
// Then: Should not find
assertFalse(property.isPresent());
}
@Test
void testGetByPackagingIdAndTypeDifferentPackaging() {
// Given: Different packaging
Integer otherPackagingId = createPackaging("Box-003", "Other Box");
// When: Get property from wrong packaging
Optional<PackagingProperty> property = packagingPropertiesRepository.getByPackagingIdAndType(
otherPackagingId, "WEIGHT");
// Then: Should not find
assertFalse(property.isPresent());
}
@Test
void testListTypes() {
// When: List all property types
List<PropertyType> types = packagingPropertiesRepository.listTypes();
// Then: Should find all 3 types
assertNotNull(types);
assertEquals(3, types.size());
assertTrue(types.stream().anyMatch(t -> "Weight".equals(t.getName())));
assertTrue(types.stream().anyMatch(t -> "Dimension".equals(t.getName())));
assertTrue(types.stream().anyMatch(t -> "Material".equals(t.getName())));
}
@Test
void testListTypesProperties() {
// When: List types
List<PropertyType> types = packagingPropertiesRepository.listTypes();
// Then: Verify type properties
PropertyType weightType = types.stream()
.filter(t -> "Weight".equals(t.getName()))
.findFirst()
.orElseThrow();
assertEquals(PropertyDataType.CURRENCY, weightType.getDataType());
assertEquals("WEIGHT", weightType.getExternalMappingId());
assertTrue(weightType.getRequired());
assertEquals("^[0-9]+(\\.[0-9]+)?$", weightType.getValidationRule());
}
@Test
void testUpdateInsert() {
// Given: New property for Material
// When: Update (insert)
packagingPropertiesRepository.update(testPackagingId, testTypeMaterialId, "Cardboard");
// Then: Should be inserted
Optional<PackagingProperty> property = packagingPropertiesRepository.getByPackagingIdAndType(
testPackagingId, "MATERIAL");
assertTrue(property.isPresent());
assertEquals("Cardboard", property.get().getValue());
}
@Test
void testUpdateUpsert() {
// Given: Existing Weight property with value "10.5"
// When: Update with new value
packagingPropertiesRepository.update(testPackagingId, testTypeWeightId, "15.0");
// Then: Should be updated
Optional<PackagingProperty> property = packagingPropertiesRepository.getByPackagingIdAndType(
testPackagingId, "WEIGHT");
assertTrue(property.isPresent());
assertEquals("15.0", property.get().getValue());
// Should still have only 2 properties (not creating duplicate)
List<PackagingProperty> allProperties = packagingPropertiesRepository.getByPackagingId(testPackagingId);
assertEquals(2, allProperties.size());
}
@Test
void testUpdateWithTypeId() {
// When: Update using Integer type ID
packagingPropertiesRepository.update(testPackagingId, testTypeMaterialId, "Plastic");
// Then: Should work
Optional<PackagingProperty> property = packagingPropertiesRepository.getByPackagingIdAndType(
testPackagingId, "MATERIAL");
assertTrue(property.isPresent());
assertEquals("Plastic", property.get().getValue());
}
@Test
void testUpdateWithTypeIdString() {
// When: Update using String type ID
packagingPropertiesRepository.update(testPackagingId, String.valueOf(testTypeMaterialId), "Wood");
// Then: Should work
Optional<PackagingProperty> property = packagingPropertiesRepository.getByPackagingIdAndType(
testPackagingId, "MATERIAL");
assertTrue(property.isPresent());
assertEquals("Wood", property.get().getValue());
}
@Test
void testGetTypeIdByMappingId() {
// When: Get type ID by mapping ID
Integer typeId = packagingPropertiesRepository.getTypeIdByMappingId("WEIGHT");
// Then: Should find type
assertNotNull(typeId);
assertEquals(testTypeWeightId, typeId);
}
@Test
void testGetTypeIdByMappingIdDifferentTypes() {
// When: Get different type IDs
Integer weightId = packagingPropertiesRepository.getTypeIdByMappingId("WEIGHT");
Integer dimensionId = packagingPropertiesRepository.getTypeIdByMappingId("DIMENSION");
Integer materialId = packagingPropertiesRepository.getTypeIdByMappingId("MATERIAL");
// Then: Should find all and be different
assertNotNull(weightId);
assertNotNull(dimensionId);
assertNotNull(materialId);
assertNotEquals(weightId, dimensionId);
assertNotEquals(weightId, materialId);
assertNotEquals(dimensionId, materialId);
}
@Test
void testUpdateMultipleProperties() {
// Given: Packaging with properties
// When: Update multiple properties
packagingPropertiesRepository.update(testPackagingId, testTypeWeightId, "20.0");
packagingPropertiesRepository.update(testPackagingId, testTypeDimensionId, "50x60x70");
packagingPropertiesRepository.update(testPackagingId, testTypeMaterialId, "Metal");
// Then: Should have all 3 properties
List<PackagingProperty> properties = packagingPropertiesRepository.getByPackagingId(testPackagingId);
assertEquals(3, properties.size());
// Verify values
assertEquals("20.0", properties.stream()
.filter(p -> "Weight".equals(p.getType().getName()))
.findFirst().orElseThrow().getValue());
assertEquals("50x60x70", properties.stream()
.filter(p -> "Dimension".equals(p.getType().getName()))
.findFirst().orElseThrow().getValue());
assertEquals("Metal", properties.stream()
.filter(p -> "Material".equals(p.getType().getName()))
.findFirst().orElseThrow().getValue());
}
// ========== Helper Methods ==========
private Integer createPropertyType(String name, String dataType, String externalMappingId,
boolean isRequired, String validationRule) {
String isRequiredValue = isRequired ? dialectProvider.getBooleanTrue() : dialectProvider.getBooleanFalse();
String sql = String.format(
"INSERT INTO packaging_property_type (name, data_type, external_mapping_id, is_required, validation_rule, description, property_group, sequence_number) " +
"VALUES (?, ?, ?, %s, ?, ?, 'GENERAL', 1)",
isRequiredValue);
executeRawSql(sql, name, dataType, externalMappingId, validationRule, name + " description");
String selectSql = isMysql() ? "SELECT LAST_INSERT_ID()" : "SELECT CAST(@@IDENTITY AS INT)";
return jdbcTemplate.queryForObject(selectSql, Integer.class);
}
private Integer createPackaging(String externalId, String description) {
// Create required referenced data
Integer countryId = jdbcTemplate.queryForObject("SELECT id FROM country WHERE iso_code = 'DE'", Integer.class);
// Create node for supplier
Integer nodeId = createNode("Test Supplier", "SUP-" + externalId, countryId);
// Create material
Integer materialId = createMaterial("Test Material " + externalId, "MAT-" + externalId);
// Create dimensions
Integer huDimensionId = createPackagingDimension();
Integer shuDimensionId = createPackagingDimension();
// Create packaging
String isDeprecatedValue = dialectProvider.getBooleanFalse();
String sql = String.format(
"INSERT INTO packaging (supplier_node_id, material_id, hu_dimension_id, shu_dimension_id, is_deprecated) " +
"VALUES (?, ?, ?, ?, %s)",
isDeprecatedValue);
executeRawSql(sql, nodeId, materialId, huDimensionId, shuDimensionId);
String selectSql = isMysql() ? "SELECT LAST_INSERT_ID()" : "SELECT CAST(@@IDENTITY AS INT)";
return jdbcTemplate.queryForObject(selectSql, Integer.class);
}
private Integer createNode(String name, String externalMappingId, Integer countryId) {
String sql = String.format(
"INSERT INTO node (name, external_mapping_id, country_id, is_deprecated, is_source, is_destination, is_intermediate, address) " +
"VALUES (?, ?, ?, %s, %s, %s, %s, 'Test Address')",
dialectProvider.getBooleanFalse(),
dialectProvider.getBooleanTrue(),
dialectProvider.getBooleanTrue(),
dialectProvider.getBooleanTrue());
executeRawSql(sql, name, externalMappingId, countryId);
String selectSql = isMysql() ? "SELECT LAST_INSERT_ID()" : "SELECT CAST(@@IDENTITY AS INT)";
return jdbcTemplate.queryForObject(selectSql, Integer.class);
}
private Integer createMaterial(String name, String externalMappingId) {
String sql = "INSERT INTO material (name, external_mapping_id, hs_code) VALUES (?, ?, '123456')";
executeRawSql(sql, name, externalMappingId);
String selectSql = isMysql() ? "SELECT LAST_INSERT_ID()" : "SELECT CAST(@@IDENTITY AS INT)";
return jdbcTemplate.queryForObject(selectSql, Integer.class);
}
private Integer createPackagingDimension() {
String sql = "INSERT INTO packaging_dimension (length, width, height, gross_weight) VALUES (100, 100, 100, 10)";
executeRawSql(sql);
String selectSql = isMysql() ? "SELECT LAST_INSERT_ID()" : "SELECT CAST(@@IDENTITY AS INT)";
return jdbcTemplate.queryForObject(selectSql, Integer.class);
}
private void createPackagingProperty(Integer packagingId, Integer typeId, String value) {
String sql = "INSERT INTO packaging_property (packaging_id, packaging_property_type_id, property_value) " +
"VALUES (?, ?, ?)";
executeRawSql(sql, packagingId, typeId, value);
}
}