Erweiterungen

This commit is contained in:
2026-01-26 09:39:41 +01:00
parent 5111d6b4f5
commit 608417331b
9 changed files with 2392 additions and 3 deletions

343
docs/JOB_JSON.md Normal file
View File

@@ -0,0 +1,343 @@
# Job JSON Struktur
Diese Dokumentation beschreibt die JSON-Struktur eines Jobs mit allen zugehörigen Tasks.
## Job Objekt
```json
{
"id": "507f1f77bcf86cd799439011",
"jobNumber": "JOB-2024-001",
"status": "CREATED",
"createdAt": "2024-01-15T10:30:00",
"updatedAt": "2024-01-15T14:45:00",
"createdBy": "admin@example.com",
"isDraft": false,
"customerSelection": "Kunde01",
"pickupCompany": "Absender GmbH",
"pickupSalutation": "Herr",
"pickupFirstName": "Max",
"pickupLastName": "Mustermann",
"pickupPhone": "+49 123 456789",
"pickupStreet": "Musterstraße",
"pickupHouseNumber": "42",
"pickupAddressAddition": "2. OG",
"pickupZip": "12345",
"pickupCity": "Musterstadt",
"deliveryCompany": "Empfänger AG",
"deliverySalutation": "Frau",
"deliveryFirstName": "Erika",
"deliveryLastName": "Musterfrau",
"deliveryPhone": "+49 987 654321",
"deliveryStreet": "Beispielweg",
"deliveryHouseNumber": "7",
"deliveryAddressAddition": null,
"deliveryZip": "54321",
"deliveryCity": "Beispielstadt",
"digitalProcessing": true,
"appUser": "fahrer01",
"pickupDate": "2024-01-20",
"deliveryDate": "2024-01-21",
"remark": "Bitte zwischen 9-12 Uhr liefern",
"price": 150.00
}
```
## Job Status Werte
| Status | Display Name | Beschreibung |
|--------|-------------|--------------|
| `CREATED` | Erstellt | Job wurde angelegt |
| `IN_PROGRESS` | In Bearbeitung | Job wird bearbeitet |
| `PICKUP_SCHEDULED` | Abholung geplant | Abholtermin wurde festgelegt |
| `PICKED_UP` | Abgeholt | Ware wurde abgeholt |
| `IN_TRANSIT` | Unterwegs | Ware ist auf dem Transportweg |
| `DELIVERED` | Zugestellt | Ware wurde zugestellt |
| `COMPLETED` | Abgeschlossen | Job vollständig abgeschlossen |
| `CANCELLED` | Storniert | Job wurde storniert |
## Task Struktur
Tasks sind polymorph und haben eine gemeinsame Basisstruktur plus typspezifische Daten.
### Basis Task Felder
```json
{
"id": "507f1f77bcf86cd799439012",
"jobId": "507f1f77bcf86cd799439011",
"taskType": "PHOTO",
"taskOrder": 1,
"description": "Fotos der Ware bei Abholung",
"completed": false,
"completedAt": null,
"completedBy": null,
"taskSpecificData": { ... }
}
```
### Task Typen
| TaskType | Display Name | Beschreibung |
|----------|-------------|--------------|
| `CONFIRMATION` | Bestätigung | Einfache Bestätigung per Button |
| `SIGNATURE` | Unterschrift | Unterschrift erfassen |
| `TODOLIST` | To-Do Liste | Checkliste mit Punkten |
| `PHOTO` | Foto | Fotos aufnehmen |
| `BARCODE` | Barcode | Barcodes scannen |
| `COMMENT` | Kommentar | Textkommentar eingeben |
---
## Task Beispiele nach Typ
### CONFIRMATION Task
```json
{
"id": "507f1f77bcf86cd799439012",
"jobId": "507f1f77bcf86cd799439011",
"taskType": "CONFIRMATION",
"taskOrder": 1,
"description": "Bitte bestätigen Sie die Übernahme der Ware",
"completed": false,
"completedAt": null,
"completedBy": null,
"taskSpecificData": {
"taskType": "CONFIRMATION",
"buttonText": "Ware übernommen"
}
}
```
### SIGNATURE Task
```json
{
"id": "507f1f77bcf86cd799439013",
"jobId": "507f1f77bcf86cd799439011",
"taskType": "SIGNATURE",
"taskOrder": 2,
"description": "Unterschrift des Empfängers",
"completed": false,
"completedAt": null,
"completedBy": null,
"taskSpecificData": {
"taskType": "SIGNATURE"
}
}
```
### PHOTO Task
```json
{
"id": "507f1f77bcf86cd799439014",
"jobId": "507f1f77bcf86cd799439011",
"taskType": "PHOTO",
"taskOrder": 3,
"description": "Fotos der Ware bei Abholung",
"completed": false,
"completedAt": null,
"completedBy": null,
"taskSpecificData": {
"taskType": "PHOTO",
"minPhotoCount": 1,
"maxPhotoCount": 5
}
}
```
### BARCODE Task
```json
{
"id": "507f1f77bcf86cd799439015",
"jobId": "507f1f77bcf86cd799439011",
"taskType": "BARCODE",
"taskOrder": 4,
"description": "Scannen Sie alle Pakete",
"completed": false,
"completedAt": null,
"completedBy": null,
"taskSpecificData": {
"taskType": "BARCODE",
"minBarcodeCount": 1,
"maxBarcodeCount": 10
}
}
```
### TODOLIST Task
```json
{
"id": "507f1f77bcf86cd799439016",
"jobId": "507f1f77bcf86cd799439011",
"taskType": "TODOLIST",
"taskOrder": 5,
"description": "Checkliste vor Auslieferung",
"completed": false,
"completedAt": null,
"completedBy": null,
"taskSpecificData": {
"taskType": "TODOLIST",
"todoItems": [
"Verpackung auf Beschädigungen prüfen",
"Anzahl der Pakete kontrollieren",
"Lieferschein beiliegen"
]
}
}
```
### COMMENT Task
```json
{
"id": "507f1f77bcf86cd799439017",
"jobId": "507f1f77bcf86cd799439011",
"taskType": "COMMENT",
"taskOrder": 6,
"description": "Anmerkungen zur Lieferung",
"completed": false,
"completedAt": null,
"completedBy": null,
"taskSpecificData": {
"taskType": "COMMENT",
"commentText": null,
"required": false
}
}
```
---
## Vollständiges Beispiel: Job mit Tasks
```json
{
"job": {
"id": "507f1f77bcf86cd799439011",
"jobNumber": "JOB-2024-001",
"status": "IN_PROGRESS",
"createdAt": "2024-01-15T10:30:00",
"updatedAt": "2024-01-15T14:45:00",
"createdBy": "admin@example.com",
"isDraft": false,
"customerSelection": "Kunde01",
"pickupCompany": "Absender GmbH",
"pickupSalutation": "Herr",
"pickupFirstName": "Max",
"pickupLastName": "Mustermann",
"pickupPhone": "+49 123 456789",
"pickupStreet": "Musterstraße",
"pickupHouseNumber": "42",
"pickupAddressAddition": "2. OG",
"pickupZip": "12345",
"pickupCity": "Musterstadt",
"deliveryCompany": "Empfänger AG",
"deliverySalutation": "Frau",
"deliveryFirstName": "Erika",
"deliveryLastName": "Musterfrau",
"deliveryPhone": "+49 987 654321",
"deliveryStreet": "Beispielweg",
"deliveryHouseNumber": "7",
"deliveryAddressAddition": null,
"deliveryZip": "54321",
"deliveryCity": "Beispielstadt",
"digitalProcessing": true,
"appUser": "fahrer01",
"pickupDate": "2024-01-20",
"deliveryDate": "2024-01-21",
"remark": "Bitte zwischen 9-12 Uhr liefern",
"price": 150.00
},
"tasks": [
{
"id": "507f1f77bcf86cd799439012",
"jobId": "507f1f77bcf86cd799439011",
"taskType": "CONFIRMATION",
"taskOrder": 1,
"description": "Ware übernommen bestätigen",
"completed": true,
"completedAt": "2024-01-20T09:15:00",
"completedBy": "fahrer01",
"taskSpecificData": {
"taskType": "CONFIRMATION",
"buttonText": "Ware übernommen"
}
},
{
"id": "507f1f77bcf86cd799439013",
"jobId": "507f1f77bcf86cd799439011",
"taskType": "PHOTO",
"taskOrder": 2,
"description": "Fotos bei Abholung",
"completed": true,
"completedAt": "2024-01-20T09:20:00",
"completedBy": "fahrer01",
"taskSpecificData": {
"taskType": "PHOTO",
"minPhotoCount": 2,
"maxPhotoCount": 5
}
},
{
"id": "507f1f77bcf86cd799439014",
"jobId": "507f1f77bcf86cd799439011",
"taskType": "BARCODE",
"taskOrder": 3,
"description": "Pakete scannen",
"completed": false,
"completedAt": null,
"completedBy": null,
"taskSpecificData": {
"taskType": "BARCODE",
"minBarcodeCount": 1,
"maxBarcodeCount": 3
}
},
{
"id": "507f1f77bcf86cd799439015",
"jobId": "507f1f77bcf86cd799439011",
"taskType": "SIGNATURE",
"taskOrder": 4,
"description": "Unterschrift Empfänger",
"completed": false,
"completedAt": null,
"completedBy": null,
"taskSpecificData": {
"taskType": "SIGNATURE"
}
}
]
}
```
---
## Feldtypen Referenz
| Feld | Typ | Nullable | Beschreibung |
|------|-----|----------|--------------|
| `id` | String (ObjectId) | Nein | MongoDB ObjectId als String |
| `jobNumber` | String | Nein | Eindeutige Auftragsnummer |
| `status` | String (Enum) | Nein | Siehe Job Status Werte |
| `createdAt` | ISO DateTime | Nein | Erstellungszeitpunkt |
| `updatedAt` | ISO DateTime | Ja | Letzter Änderungszeitpunkt |
| `createdBy` | String | Nein | Benutzername des Erstellers |
| `isDraft` | Boolean | Nein | Entwurf-Kennzeichen |
| `pickupDate` | ISO Date | Ja | Abholdatum |
| `deliveryDate` | ISO Date | Ja | Lieferdatum |
| `price` | Decimal | Ja | Preis in EUR (netto) |
| `taskOrder` | Integer | Nein | Reihenfolge der Tasks (0-basiert) |
| `completed` | Boolean | Nein | Task abgeschlossen |
| `completedAt` | ISO DateTime | Ja | Abschlusszeitpunkt |
| `completedBy` | String | Ja | App-User der den Task abgeschlossen hat |

12
pom.xml
View File

@@ -147,6 +147,18 @@
<version>5.0.5</version>
</dependency>
<!-- Test Dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>

Binary file not shown.

View File

@@ -46,6 +46,7 @@ import de.assecutor.votianlt.pages.service.AppUserService;
import de.assecutor.votianlt.service.MessageService;
import jakarta.annotation.security.RolesAllowed;
import org.bson.types.ObjectId;
import org.springframework.beans.factory.annotation.Value;
import java.util.ArrayList;
import java.util.List;
@@ -66,6 +67,9 @@ public class JobSummaryView extends Main implements HasUrlParameter<String> {
private final CommentRepository commentRepository;
private final AppUserService appUserService;
@Value("${app.google.maps.api-key}")
private String googleMapsApiKey;
private final VerticalLayout content;
private final List<Div> taskCards = new ArrayList<>();
@@ -938,8 +942,7 @@ public class JobSummaryView extends Main implements HasUrlParameter<String> {
}
private String getGoogleMapsApiKey() {
// TODO: Move API key to configuration properties
return "AIzaSyDnbitL06iLp3elmj-WtPudCykX9xvXcVE";
return googleMapsApiKey;
}
private void resetAllTaskCardHoverStates() {

View File

@@ -101,3 +101,6 @@ app.client.ping.timeout-seconds=5
# Application Version - automatically set from pom.xml during build
app.version=@project.version@
# Google Maps API Key
app.google.maps.api-key=AIzaSyDnbitL06iLp3elmj-WtPudCykX9xvXcVE

View File

@@ -0,0 +1,601 @@
package de.assecutor.votianlt.model;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import de.assecutor.votianlt.model.task.*;
import org.bson.types.ObjectId;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Unit tests for Job and Task JSON serialization according to JOB_JSON.md specification.
* See docs/JOB_JSON.md for the JSON structure documentation.
*/
@DisplayName("Job JSON Serialization Tests")
class JobJsonSerializationTest {
private ObjectMapper objectMapper;
@BeforeEach
void setUp() {
objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
}
@Nested
@DisplayName("Job Serialization Tests")
class JobSerializationTests {
@Test
@DisplayName("should serialize job with all fields according to spec")
void shouldSerializeJobWithAllFields() throws JsonProcessingException {
// Given
Job job = createFullJob();
// When
String json = objectMapper.writeValueAsString(job);
JsonNode node = objectMapper.readTree(json);
// Then - verify all required fields according to JOB_JSON.md
assertThat(node.has("id")).isTrue();
assertThat(node.get("id").asText()).isNotEmpty();
assertThat(node.get("jobNumber").asText()).isEqualTo("JOB-2024-001");
assertThat(node.get("status").asText()).isEqualTo("IN_PROGRESS");
assertThat(node.has("createdAt")).isTrue();
assertThat(node.get("createdBy").asText()).isEqualTo("admin@example.com");
assertThat(node.get("draft").asBoolean()).isFalse();
// Pickup address
assertThat(node.get("pickupCompany").asText()).isEqualTo("Absender GmbH");
assertThat(node.get("pickupSalutation").asText()).isEqualTo("Herr");
assertThat(node.get("pickupFirstName").asText()).isEqualTo("Max");
assertThat(node.get("pickupLastName").asText()).isEqualTo("Mustermann");
assertThat(node.get("pickupPhone").asText()).isEqualTo("+49 123 456789");
assertThat(node.get("pickupStreet").asText()).isEqualTo("Musterstraße");
assertThat(node.get("pickupHouseNumber").asText()).isEqualTo("42");
assertThat(node.get("pickupAddressAddition").asText()).isEqualTo("2. OG");
assertThat(node.get("pickupZip").asText()).isEqualTo("12345");
assertThat(node.get("pickupCity").asText()).isEqualTo("Musterstadt");
// Delivery address
assertThat(node.get("deliveryCompany").asText()).isEqualTo("Empfänger AG");
assertThat(node.get("deliverySalutation").asText()).isEqualTo("Frau");
assertThat(node.get("deliveryFirstName").asText()).isEqualTo("Erika");
assertThat(node.get("deliveryLastName").asText()).isEqualTo("Musterfrau");
assertThat(node.get("deliveryPhone").asText()).isEqualTo("+49 987 654321");
assertThat(node.get("deliveryStreet").asText()).isEqualTo("Beispielweg");
assertThat(node.get("deliveryHouseNumber").asText()).isEqualTo("7");
assertThat(node.get("deliveryZip").asText()).isEqualTo("54321");
assertThat(node.get("deliveryCity").asText()).isEqualTo("Beispielstadt");
// Digital processing
assertThat(node.get("digitalProcessing").asBoolean()).isTrue();
assertThat(node.get("appUser").asText()).isEqualTo("fahrer01");
// Dates
assertThat(node.has("pickupDate")).isTrue();
assertThat(node.has("deliveryDate")).isTrue();
// Other fields
assertThat(node.get("remark").asText()).isEqualTo("Bitte zwischen 9-12 Uhr liefern");
assertThat(node.get("price").decimalValue()).isEqualByComparingTo(new BigDecimal("150.00"));
}
@Test
@DisplayName("should serialize job id as string")
void shouldSerializeJobIdAsString() throws JsonProcessingException {
// Given
Job job = new Job();
ObjectId objectId = new ObjectId("507f1f77bcf86cd799439011");
job.setId(objectId);
job.setJobNumber("TEST-001");
job.setStatus(JobStatus.CREATED);
// When
String json = objectMapper.writeValueAsString(job);
JsonNode node = objectMapper.readTree(json);
// Then
assertThat(node.get("id").asText()).isEqualTo("507f1f77bcf86cd799439011");
assertThat(node.get("id").isTextual()).isTrue();
}
@Test
@DisplayName("should serialize all job status values correctly")
void shouldSerializeAllJobStatusValues() throws JsonProcessingException {
// According to JOB_JSON.md: CREATED, IN_PROGRESS, PICKUP_SCHEDULED, PICKED_UP, IN_TRANSIT, DELIVERED, COMPLETED, CANCELLED
JobStatus[] expectedStatuses = {
JobStatus.CREATED,
JobStatus.IN_PROGRESS,
JobStatus.PICKUP_SCHEDULED,
JobStatus.PICKED_UP,
JobStatus.IN_TRANSIT,
JobStatus.DELIVERED,
JobStatus.COMPLETED,
JobStatus.CANCELLED
};
for (JobStatus status : expectedStatuses) {
Job job = new Job();
job.setId(new ObjectId());
job.setStatus(status);
String json = objectMapper.writeValueAsString(job);
JsonNode node = objectMapper.readTree(json);
assertThat(node.get("status").asText()).isEqualTo(status.name());
}
}
@Test
@DisplayName("should handle null optional fields gracefully")
void shouldHandleNullOptionalFieldsGracefully() throws JsonProcessingException {
// Given
Job job = new Job();
job.setId(new ObjectId());
job.setJobNumber("JOB-001");
job.setStatus(JobStatus.CREATED);
// All other fields are null
// When
String json = objectMapper.writeValueAsString(job);
JsonNode node = objectMapper.readTree(json);
// Then - should serialize without error
assertThat(node.get("jobNumber").asText()).isEqualTo("JOB-001");
assertThat(node.get("deliveryAddressAddition").isNull()).isTrue();
assertThat(node.get("remark").isNull()).isTrue();
assertThat(node.get("price").isNull()).isTrue();
}
}
@Nested
@DisplayName("Task Serialization Tests")
class TaskSerializationTests {
@Test
@DisplayName("should serialize ConfirmationTask according to spec")
void shouldSerializeConfirmationTask() throws JsonProcessingException {
// Given
ConfirmationTask task = new ConfirmationTask("Ware übernommen");
task.setId(new ObjectId("507f1f77bcf86cd799439012"));
task.setJobId(new ObjectId("507f1f77bcf86cd799439011"));
task.setTaskOrder(1);
task.setDescription("Bitte bestätigen Sie die Übernahme der Ware");
task.setCompleted(false);
// When
String json = objectMapper.writeValueAsString(task);
JsonNode node = objectMapper.readTree(json);
// Then
assertThat(node.get("id").asText()).isEqualTo("507f1f77bcf86cd799439012");
assertThat(node.get("jobId").asText()).isEqualTo("507f1f77bcf86cd799439011");
assertThat(node.get("taskType").asText()).isEqualTo("CONFIRMATION");
assertThat(node.get("taskOrder").asInt()).isEqualTo(1);
assertThat(node.get("description").asText()).isEqualTo("Bitte bestätigen Sie die Übernahme der Ware");
assertThat(node.get("completed").asBoolean()).isFalse();
// taskSpecificData
JsonNode specificData = node.get("taskSpecificData");
assertThat(specificData).isNotNull();
assertThat(specificData.get("taskType").asText()).isEqualTo("CONFIRMATION");
assertThat(specificData.get("buttonText").asText()).isEqualTo("Ware übernommen");
}
@Test
@DisplayName("should serialize SignatureTask according to spec")
void shouldSerializeSignatureTask() throws JsonProcessingException {
// Given
SignatureTask task = new SignatureTask();
task.setId(new ObjectId("507f1f77bcf86cd799439013"));
task.setJobId(new ObjectId("507f1f77bcf86cd799439011"));
task.setTaskOrder(2);
task.setDescription("Unterschrift des Empfängers");
task.setCompleted(false);
// When
String json = objectMapper.writeValueAsString(task);
JsonNode node = objectMapper.readTree(json);
// Then
assertThat(node.get("taskType").asText()).isEqualTo("SIGNATURE");
JsonNode specificData = node.get("taskSpecificData");
assertThat(specificData).isNotNull();
assertThat(specificData.get("taskType").asText()).isEqualTo("SIGNATURE");
}
@Test
@DisplayName("should serialize PhotoTask according to spec")
void shouldSerializePhotoTask() throws JsonProcessingException {
// Given
PhotoTask task = new PhotoTask(1, 5);
task.setId(new ObjectId("507f1f77bcf86cd799439014"));
task.setJobId(new ObjectId("507f1f77bcf86cd799439011"));
task.setTaskOrder(3);
task.setDescription("Fotos der Ware bei Abholung");
task.setCompleted(false);
// When
String json = objectMapper.writeValueAsString(task);
JsonNode node = objectMapper.readTree(json);
// Then
assertThat(node.get("taskType").asText()).isEqualTo("PHOTO");
JsonNode specificData = node.get("taskSpecificData");
assertThat(specificData).isNotNull();
assertThat(specificData.get("taskType").asText()).isEqualTo("PHOTO");
assertThat(specificData.get("minPhotoCount").asInt()).isEqualTo(1);
assertThat(specificData.get("maxPhotoCount").asInt()).isEqualTo(5);
}
@Test
@DisplayName("should serialize BarcodeTask according to spec")
void shouldSerializeBarcodeTask() throws JsonProcessingException {
// Given
BarcodeTask task = new BarcodeTask(1, 10);
task.setId(new ObjectId("507f1f77bcf86cd799439015"));
task.setJobId(new ObjectId("507f1f77bcf86cd799439011"));
task.setTaskOrder(4);
task.setDescription("Scannen Sie alle Pakete");
task.setCompleted(false);
// When
String json = objectMapper.writeValueAsString(task);
JsonNode node = objectMapper.readTree(json);
// Then
assertThat(node.get("taskType").asText()).isEqualTo("BARCODE");
JsonNode specificData = node.get("taskSpecificData");
assertThat(specificData).isNotNull();
assertThat(specificData.get("taskType").asText()).isEqualTo("BARCODE");
assertThat(specificData.get("minBarcodeCount").asInt()).isEqualTo(1);
assertThat(specificData.get("maxBarcodeCount").asInt()).isEqualTo(10);
}
@Test
@DisplayName("should serialize TodoListTask according to spec")
void shouldSerializeTodoListTask() throws JsonProcessingException {
// Given
List<String> todoItems = Arrays.asList(
"Verpackung auf Beschädigungen prüfen",
"Anzahl der Pakete kontrollieren",
"Lieferschein beiliegen"
);
TodoListTask task = new TodoListTask(todoItems);
task.setId(new ObjectId("507f1f77bcf86cd799439016"));
task.setJobId(new ObjectId("507f1f77bcf86cd799439011"));
task.setTaskOrder(5);
task.setDescription("Checkliste vor Auslieferung");
task.setCompleted(false);
// When
String json = objectMapper.writeValueAsString(task);
JsonNode node = objectMapper.readTree(json);
// Then
assertThat(node.get("taskType").asText()).isEqualTo("TODOLIST");
JsonNode specificData = node.get("taskSpecificData");
assertThat(specificData).isNotNull();
assertThat(specificData.get("taskType").asText()).isEqualTo("TODOLIST");
assertThat(specificData.get("todoItems").isArray()).isTrue();
assertThat(specificData.get("todoItems").size()).isEqualTo(3);
assertThat(specificData.get("todoItems").get(0).asText()).isEqualTo("Verpackung auf Beschädigungen prüfen");
}
@Test
@DisplayName("should serialize CommentTask according to spec")
void shouldSerializeCommentTask() throws JsonProcessingException {
// Given
CommentTask task = new CommentTask(null, false);
task.setId(new ObjectId("507f1f77bcf86cd799439017"));
task.setJobId(new ObjectId("507f1f77bcf86cd799439011"));
task.setTaskOrder(6);
task.setDescription("Anmerkungen zur Lieferung");
task.setCompleted(false);
// When
String json = objectMapper.writeValueAsString(task);
JsonNode node = objectMapper.readTree(json);
// Then
assertThat(node.get("taskType").asText()).isEqualTo("COMMENT");
JsonNode specificData = node.get("taskSpecificData");
assertThat(specificData).isNotNull();
assertThat(specificData.get("taskType").asText()).isEqualTo("COMMENT");
assertThat(specificData.get("commentText").isNull()).isTrue();
assertThat(specificData.get("required").asBoolean()).isFalse();
}
@Test
@DisplayName("should serialize completed task with completion data")
void shouldSerializeCompletedTaskWithCompletionData() throws JsonProcessingException {
// Given
ConfirmationTask task = new ConfirmationTask("Ware übernommen");
task.setId(new ObjectId("507f1f77bcf86cd799439012"));
task.setJobId(new ObjectId("507f1f77bcf86cd799439011"));
task.setTaskOrder(1);
task.setDescription("Ware übernommen bestätigen");
task.setCompleted(true);
task.setCompletedAt(LocalDateTime.of(2024, 1, 20, 9, 15, 0));
task.setCompletedBy("fahrer01");
// When
String json = objectMapper.writeValueAsString(task);
JsonNode node = objectMapper.readTree(json);
// Then
assertThat(node.get("completed").asBoolean()).isTrue();
assertThat(node.has("completedAt")).isTrue();
assertThat(node.get("completedBy").asText()).isEqualTo("fahrer01");
}
@Test
@DisplayName("should serialize all task types correctly")
void shouldSerializeAllTaskTypes() throws JsonProcessingException {
// According to JOB_JSON.md: CONFIRMATION, SIGNATURE, TODOLIST, PHOTO, BARCODE, COMMENT
BaseTask[] tasks = {
new ConfirmationTask("Test"),
new SignatureTask(),
new TodoListTask(List.of("Item")),
new PhotoTask(1, 3),
new BarcodeTask(1, 5),
new CommentTask(null, false)
};
String[] expectedTypes = {"CONFIRMATION", "SIGNATURE", "TODOLIST", "PHOTO", "BARCODE", "COMMENT"};
for (int i = 0; i < tasks.length; i++) {
tasks[i].setId(new ObjectId());
tasks[i].setJobId(new ObjectId());
String json = objectMapper.writeValueAsString(tasks[i]);
JsonNode node = objectMapper.readTree(json);
assertThat(node.get("taskType").asText())
.as("Task type for %s", tasks[i].getClass().getSimpleName())
.isEqualTo(expectedTypes[i]);
}
}
}
@Nested
@DisplayName("Job with Tasks Serialization Tests")
class JobWithTasksSerializationTests {
@Test
@DisplayName("should serialize complete job with tasks structure")
void shouldSerializeCompleteJobWithTasksStructure() throws JsonProcessingException {
// Given
Job job = createFullJob();
List<BaseTask> tasks = createAllTaskTypes(job.getId());
// Create wrapper object similar to the spec
record JobWithTasks(Job job, List<BaseTask> tasks) {}
JobWithTasks jobWithTasks = new JobWithTasks(job, tasks);
// When
String json = objectMapper.writeValueAsString(jobWithTasks);
JsonNode node = objectMapper.readTree(json);
// Then
assertThat(node.has("job")).isTrue();
assertThat(node.has("tasks")).isTrue();
assertThat(node.get("tasks").isArray()).isTrue();
assertThat(node.get("tasks").size()).isEqualTo(4);
// Verify task types are serialized correctly
JsonNode tasksNode = node.get("tasks");
assertThat(tasksNode.get(0).get("taskType").asText()).isEqualTo("CONFIRMATION");
assertThat(tasksNode.get(1).get("taskType").asText()).isEqualTo("PHOTO");
assertThat(tasksNode.get(2).get("taskType").asText()).isEqualTo("BARCODE");
assertThat(tasksNode.get(3).get("taskType").asText()).isEqualTo("SIGNATURE");
}
}
@Nested
@DisplayName("Field Type Tests")
class FieldTypeTests {
@Test
@DisplayName("should serialize ObjectId as String (not ObjectId type)")
void shouldSerializeObjectIdAsString() throws JsonProcessingException {
// According to spec: id is String (ObjectId) - should be serialized as string
Job job = new Job();
job.setId(new ObjectId("507f1f77bcf86cd799439011"));
job.setJobNumber("TEST");
job.setStatus(JobStatus.CREATED);
String json = objectMapper.writeValueAsString(job);
JsonNode node = objectMapper.readTree(json);
assertThat(node.get("id").isTextual()).isTrue();
assertThat(node.get("id").asText()).hasSize(24); // ObjectId hex length
}
@Test
@DisplayName("should serialize dates in ISO format")
void shouldSerializeDatesInIsoFormat() throws JsonProcessingException {
// According to spec: createdAt is ISO DateTime
Job job = new Job();
job.setId(new ObjectId());
job.setJobNumber("TEST");
job.setStatus(JobStatus.CREATED);
job.setCreatedAt(LocalDateTime.of(2024, 1, 15, 10, 30, 0));
job.setPickupDate(LocalDate.of(2024, 1, 20));
String json = objectMapper.writeValueAsString(job);
JsonNode node = objectMapper.readTree(json);
assertThat(node.get("createdAt").asText()).isEqualTo("2024-01-15T10:30:00");
assertThat(node.get("pickupDate").asText()).isEqualTo("2024-01-20");
}
@Test
@DisplayName("should serialize price as decimal")
void shouldSerializePriceAsDecimal() throws JsonProcessingException {
// According to spec: price is Decimal
Job job = new Job();
job.setId(new ObjectId());
job.setJobNumber("TEST");
job.setStatus(JobStatus.CREATED);
job.setPrice(new BigDecimal("150.50"));
String json = objectMapper.writeValueAsString(job);
JsonNode node = objectMapper.readTree(json);
assertThat(node.get("price").isNumber()).isTrue();
assertThat(node.get("price").decimalValue()).isEqualByComparingTo(new BigDecimal("150.50"));
}
@Test
@DisplayName("should serialize taskOrder as Integer")
void shouldSerializeTaskOrderAsInteger() throws JsonProcessingException {
// According to spec: taskOrder is Integer
ConfirmationTask task = new ConfirmationTask("Test");
task.setId(new ObjectId());
task.setJobId(new ObjectId());
task.setTaskOrder(5);
String json = objectMapper.writeValueAsString(task);
JsonNode node = objectMapper.readTree(json);
assertThat(node.get("taskOrder").isInt()).isTrue();
assertThat(node.get("taskOrder").asInt()).isEqualTo(5);
}
@Test
@DisplayName("should serialize boolean fields correctly")
void shouldSerializeBooleanFieldsCorrectly() throws JsonProcessingException {
// According to spec: isDraft, completed are Boolean
Job job = new Job();
job.setId(new ObjectId());
job.setJobNumber("TEST");
job.setStatus(JobStatus.CREATED);
job.setDraft(true);
ConfirmationTask task = new ConfirmationTask("Test");
task.setId(new ObjectId());
task.setJobId(new ObjectId());
task.setCompleted(true);
String jobJson = objectMapper.writeValueAsString(job);
String taskJson = objectMapper.writeValueAsString(task);
JsonNode jobNode = objectMapper.readTree(jobJson);
JsonNode taskNode = objectMapper.readTree(taskJson);
assertThat(jobNode.get("draft").isBoolean()).isTrue();
assertThat(jobNode.get("draft").asBoolean()).isTrue();
assertThat(taskNode.get("completed").isBoolean()).isTrue();
assertThat(taskNode.get("completed").asBoolean()).isTrue();
}
}
// Helper methods
private Job createFullJob() {
Job job = new Job();
job.setId(new ObjectId("507f1f77bcf86cd799439011"));
job.setJobNumber("JOB-2024-001");
job.setStatus(JobStatus.IN_PROGRESS);
job.setCreatedAt(LocalDateTime.of(2024, 1, 15, 10, 30, 0));
job.setUpdatedAt(LocalDateTime.of(2024, 1, 15, 14, 45, 0));
job.setCreatedBy("admin@example.com");
job.setDraft(false);
job.setCustomerSelection("Kunde01");
// Pickup address
job.setPickupCompany("Absender GmbH");
job.setPickupSalutation("Herr");
job.setPickupFirstName("Max");
job.setPickupLastName("Mustermann");
job.setPickupPhone("+49 123 456789");
job.setPickupStreet("Musterstraße");
job.setPickupHouseNumber("42");
job.setPickupAddressAddition("2. OG");
job.setPickupZip("12345");
job.setPickupCity("Musterstadt");
// Delivery address
job.setDeliveryCompany("Empfänger AG");
job.setDeliverySalutation("Frau");
job.setDeliveryFirstName("Erika");
job.setDeliveryLastName("Musterfrau");
job.setDeliveryPhone("+49 987 654321");
job.setDeliveryStreet("Beispielweg");
job.setDeliveryHouseNumber("7");
job.setDeliveryZip("54321");
job.setDeliveryCity("Beispielstadt");
// Digital processing
job.setDigitalProcessing(true);
job.setAppUser("fahrer01");
// Dates
job.setPickupDate(LocalDate.of(2024, 1, 20));
job.setDeliveryDate(LocalDate.of(2024, 1, 21));
// Other
job.setRemark("Bitte zwischen 9-12 Uhr liefern");
job.setPrice(new BigDecimal("150.00"));
return job;
}
private List<BaseTask> createAllTaskTypes(ObjectId jobId) {
ConfirmationTask confirmationTask = new ConfirmationTask("Ware übernommen");
confirmationTask.setId(new ObjectId("507f1f77bcf86cd799439012"));
confirmationTask.setJobId(jobId);
confirmationTask.setTaskOrder(1);
confirmationTask.setDescription("Ware übernommen bestätigen");
confirmationTask.setCompleted(true);
confirmationTask.setCompletedAt(LocalDateTime.of(2024, 1, 20, 9, 15, 0));
confirmationTask.setCompletedBy("fahrer01");
PhotoTask photoTask = new PhotoTask(2, 5);
photoTask.setId(new ObjectId("507f1f77bcf86cd799439013"));
photoTask.setJobId(jobId);
photoTask.setTaskOrder(2);
photoTask.setDescription("Fotos bei Abholung");
photoTask.setCompleted(true);
photoTask.setCompletedAt(LocalDateTime.of(2024, 1, 20, 9, 20, 0));
photoTask.setCompletedBy("fahrer01");
BarcodeTask barcodeTask = new BarcodeTask(1, 3);
barcodeTask.setId(new ObjectId("507f1f77bcf86cd799439014"));
barcodeTask.setJobId(jobId);
barcodeTask.setTaskOrder(3);
barcodeTask.setDescription("Pakete scannen");
barcodeTask.setCompleted(false);
SignatureTask signatureTask = new SignatureTask();
signatureTask.setId(new ObjectId("507f1f77bcf86cd799439015"));
signatureTask.setJobId(jobId);
signatureTask.setTaskOrder(4);
signatureTask.setDescription("Unterschrift Empfänger");
signatureTask.setCompleted(false);
return Arrays.asList(confirmationTask, photoTask, barcodeTask, signatureTask);
}
}

View File

@@ -0,0 +1,489 @@
package de.assecutor.votianlt.service;
import com.fasterxml.jackson.databind.ObjectMapper;
import de.assecutor.votianlt.messaging.delivery.MessageDeliveryService;
import de.assecutor.votianlt.messaging.plugin.PluginException;
import de.assecutor.votianlt.messaging.plugin.PluginManager;
import de.assecutor.votianlt.messaging.plugin.SendOptions;
import de.assecutor.votianlt.service.ClientConnectionService.ClientState;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import java.time.Instant;
import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
@DisplayName("ClientConnectionService Tests")
class ClientConnectionServiceTest {
@Mock
private PluginManager pluginManager;
@Mock
private MessageDeliveryService messageDeliveryService;
private ObjectMapper objectMapper;
private ClientConnectionService service;
@BeforeEach
void setUp() {
objectMapper = new ObjectMapper();
service = new ClientConnectionService(pluginManager, objectMapper, messageDeliveryService);
ReflectionTestUtils.setField(service, "pingIntervalSeconds", 15);
ReflectionTestUtils.setField(service, "pingTimeoutSeconds", 5);
}
@Nested
@DisplayName("registerClient Tests")
class RegisterClientTests {
@Test
@DisplayName("should register new client successfully")
void shouldRegisterNewClient() {
// When
service.registerClient("client-123", "user-456");
// Then
assertThat(service.isClientConnected("client-123")).isTrue();
assertThat(service.getConnectedClientCount()).isEqualTo(1);
}
@Test
@DisplayName("should not register client with null clientId")
void shouldNotRegisterNullClientId() {
// When
service.registerClient(null, "user-456");
// Then
assertThat(service.getConnectedClientCount()).isZero();
}
@Test
@DisplayName("should not register client with blank clientId")
void shouldNotRegisterBlankClientId() {
// When
service.registerClient(" ", "user-456");
// Then
assertThat(service.getConnectedClientCount()).isZero();
}
@Test
@DisplayName("should trigger retry for previously disconnected client")
void shouldTriggerRetryForReconnectedClient() {
// Given - register and mark as disconnected
service.registerClient("client-123", "user-456");
// Simulate disconnect by manipulating internal state
ClientState state = service.getClientState("client-123");
ClientState disconnectedState = state.withConnected(false);
ReflectionTestUtils.invokeMethod(service, "registerClient", "client-123", "user-456");
// First registration and re-registration
service.registerClient("client-123", "user-456");
// Then - verify the client is connected
assertThat(service.isClientConnected("client-123")).isTrue();
}
@Test
@DisplayName("should store client state correctly")
void shouldStoreClientStateCorrectly() {
// When
service.registerClient("client-123", "user-456");
// Then
ClientState state = service.getClientState("client-123");
assertThat(state).isNotNull();
assertThat(state.clientId()).isEqualTo("client-123");
assertThat(state.userId()).isEqualTo("user-456");
assertThat(state.connected()).isTrue();
assertThat(state.connectedAt()).isNotNull();
}
}
@Nested
@DisplayName("unregisterClient Tests")
class UnregisterClientTests {
@Test
@DisplayName("should unregister existing client")
void shouldUnregisterExistingClient() {
// Given
service.registerClient("client-123", "user-456");
// When
service.unregisterClient("client-123");
// Then
assertThat(service.isClientConnected("client-123")).isFalse();
assertThat(service.getClientState("client-123")).isNull();
assertThat(service.getConnectedClientCount()).isZero();
}
@Test
@DisplayName("should handle unregistering non-existent client gracefully")
void shouldHandleUnregisteringNonExistentClient() {
// When/Then - should not throw
service.unregisterClient("non-existent");
assertThat(service.getConnectedClientCount()).isZero();
}
}
@Nested
@DisplayName("handlePong Tests")
class HandlePongTests {
@Test
@DisplayName("should update client state on pong by clientId")
void shouldUpdateClientStateOnPongByClientId() {
// Given
service.registerClient("client-123", "user-456");
Instant beforePong = Instant.now();
// When
service.handlePong("client-123");
// Then
ClientState state = service.getClientState("client-123");
assertThat(state.lastPongReceived()).isAfterOrEqualTo(beforePong);
assertThat(state.connected()).isTrue();
}
@Test
@DisplayName("should update client state on pong by userId")
void shouldUpdateClientStateOnPongByUserId() {
// Given
service.registerClient("client-123", "user-456");
Instant beforePong = Instant.now();
// When
service.handlePong("user-456");
// Then
ClientState state = service.getClientState("client-123");
assertThat(state.lastPongReceived()).isAfterOrEqualTo(beforePong);
}
@Test
@DisplayName("should handle pong from unknown client gracefully")
void shouldHandlePongFromUnknownClient() {
// When/Then - should not throw
service.handlePong("unknown-client");
}
@Test
@DisplayName("should handle null pong id gracefully")
void shouldHandleNullPongId() {
// When/Then - should not throw
service.handlePong(null);
}
@Test
@DisplayName("should handle blank pong id gracefully")
void shouldHandleBlankPongId() {
// When/Then - should not throw
service.handlePong(" ");
}
@Test
@DisplayName("should trigger retry when disconnected client sends pong")
void shouldTriggerRetryWhenDisconnectedClientSendsPong() {
// Given - create a disconnected client state
service.registerClient("client-123", "user-456");
// Simulate disconnect
ClientState state = service.getClientState("client-123");
ClientState disconnectedState = state.withConnected(false);
// Use reflection to put the disconnected state
java.util.Map<String, ClientState> connectedClients =
(java.util.Map<String, ClientState>) ReflectionTestUtils.getField(service, "connectedClients");
connectedClients.put("client-123", disconnectedState);
// When
service.handlePong("client-123");
// Then
verify(messageDeliveryService).retryPendingDeliveriesForClient("client-123");
}
}
@Nested
@DisplayName("isClientConnected Tests")
class IsClientConnectedTests {
@Test
@DisplayName("should return true for connected client by clientId")
void shouldReturnTrueForConnectedClientByClientId() {
// Given
service.registerClient("client-123", "user-456");
// When/Then
assertThat(service.isClientConnected("client-123")).isTrue();
}
@Test
@DisplayName("should return true for connected client by userId")
void shouldReturnTrueForConnectedClientByUserId() {
// Given
service.registerClient("client-123", "user-456");
// When/Then
assertThat(service.isClientConnected("user-456")).isTrue();
}
@Test
@DisplayName("should return false for non-existent client")
void shouldReturnFalseForNonExistentClient() {
// When/Then
assertThat(service.isClientConnected("non-existent")).isFalse();
}
@Test
@DisplayName("should return false for null id")
void shouldReturnFalseForNullId() {
// When/Then
assertThat(service.isClientConnected(null)).isFalse();
}
@Test
@DisplayName("should return false for blank id")
void shouldReturnFalseForBlankId() {
// When/Then
assertThat(service.isClientConnected(" ")).isFalse();
}
}
@Nested
@DisplayName("getConnectedClientIds Tests")
class GetConnectedClientIdsTests {
@Test
@DisplayName("should return empty set when no clients connected")
void shouldReturnEmptySetWhenNoClients() {
// When
Set<String> ids = service.getConnectedClientIds();
// Then
assertThat(ids).isEmpty();
}
@Test
@DisplayName("should return all connected client ids")
void shouldReturnAllConnectedClientIds() {
// Given
service.registerClient("client-1", "user-1");
service.registerClient("client-2", "user-2");
service.registerClient("client-3", "user-3");
// When
Set<String> ids = service.getConnectedClientIds();
// Then
assertThat(ids).containsExactlyInAnyOrder("client-1", "client-2", "client-3");
}
@Test
@DisplayName("should not include disconnected clients")
void shouldNotIncludeDisconnectedClients() {
// Given
service.registerClient("client-1", "user-1");
service.registerClient("client-2", "user-2");
// Simulate disconnect for client-2
ClientState state = service.getClientState("client-2");
ClientState disconnectedState = state.withConnected(false);
java.util.Map<String, ClientState> connectedClients =
(java.util.Map<String, ClientState>) ReflectionTestUtils.getField(service, "connectedClients");
connectedClients.put("client-2", disconnectedState);
// When
Set<String> ids = service.getConnectedClientIds();
// Then
assertThat(ids).containsExactly("client-1");
}
}
@Nested
@DisplayName("sendPingsToAllClients Tests")
class SendPingsToAllClientsTests {
@Test
@DisplayName("should skip ping when plugin not connected")
void shouldSkipPingWhenPluginNotConnected() throws PluginException {
// Given
service.registerClient("client-123", "user-456");
when(pluginManager.isConnected()).thenReturn(false);
// When
service.sendPingsToAllClients();
// Then
verify(pluginManager, never()).sendToClient(anyString(), anyString(), any(), any());
}
@Test
@DisplayName("should skip ping when no clients connected")
void shouldSkipPingWhenNoClientsConnected() throws PluginException {
// Given
when(pluginManager.isConnected()).thenReturn(true);
// When
service.sendPingsToAllClients();
// Then
verify(pluginManager, never()).sendToClient(anyString(), anyString(), any(), any());
}
@Test
@DisplayName("should send ping to connected clients")
void shouldSendPingToConnectedClients() throws PluginException {
// Given
service.registerClient("client-123", "user-456");
when(pluginManager.isConnected()).thenReturn(true);
// When
service.sendPingsToAllClients();
// Then
verify(pluginManager).sendToClient(eq("user-456"), eq("ping"), any(byte[].class), any(SendOptions.class));
}
@Test
@DisplayName("should mark client as disconnected when ping times out")
void shouldMarkClientAsDisconnectedWhenPingTimesOut() {
// Given
service.registerClient("client-123", "user-456");
when(pluginManager.isConnected()).thenReturn(true);
// First ping
service.sendPingsToAllClients();
// Simulate time passing - set lastPingSent to past
ClientState state = service.getClientState("client-123");
Instant pastTime = Instant.now().minusSeconds(10);
ClientState stateWithOldPing = new ClientState(
state.clientId(),
state.userId(),
true,
pastTime,
null,
state.connectedAt()
);
java.util.Map<String, ClientState> connectedClients =
(java.util.Map<String, ClientState>) ReflectionTestUtils.getField(service, "connectedClients");
connectedClients.put("client-123", stateWithOldPing);
// When - second ping cycle
service.sendPingsToAllClients();
// Then
ClientState updatedState = service.getClientState("client-123");
assertThat(updatedState.connected()).isFalse();
}
}
@Nested
@DisplayName("ClientState Record Tests")
class ClientStateRecordTests {
@Test
@DisplayName("should create new state with ping sent")
void shouldCreateNewStateWithPingSent() {
// Given
Instant now = Instant.now();
ClientState state = new ClientState("client-1", "user-1", true, null, null, now);
// When
Instant pingSent = Instant.now();
ClientState newState = state.withPingSent(pingSent);
// Then
assertThat(newState.lastPingSent()).isEqualTo(pingSent);
assertThat(newState.clientId()).isEqualTo("client-1");
assertThat(newState.userId()).isEqualTo("user-1");
assertThat(newState.connected()).isTrue();
}
@Test
@DisplayName("should create new state with pong received")
void shouldCreateNewStateWithPongReceived() {
// Given
Instant now = Instant.now();
ClientState state = new ClientState("client-1", "user-1", false, null, null, now);
// When
Instant pongReceived = Instant.now();
ClientState newState = state.withPongReceived(pongReceived);
// Then
assertThat(newState.lastPongReceived()).isEqualTo(pongReceived);
assertThat(newState.connected()).isTrue(); // withPongReceived sets connected to true
}
@Test
@DisplayName("should create new state with connected flag")
void shouldCreateNewStateWithConnectedFlag() {
// Given
Instant now = Instant.now();
ClientState state = new ClientState("client-1", "user-1", true, null, null, now);
// When
ClientState newState = state.withConnected(false);
// Then
assertThat(newState.connected()).isFalse();
assertThat(newState.clientId()).isEqualTo("client-1");
}
}
@Nested
@DisplayName("Count Methods Tests")
class CountMethodsTests {
@Test
@DisplayName("should return correct connected client count")
void shouldReturnCorrectConnectedClientCount() {
// Given
service.registerClient("client-1", "user-1");
service.registerClient("client-2", "user-2");
// When/Then
assertThat(service.getConnectedClientCount()).isEqualTo(2);
}
@Test
@DisplayName("should return correct total client count")
void shouldReturnCorrectTotalClientCount() {
// Given
service.registerClient("client-1", "user-1");
service.registerClient("client-2", "user-2");
// Disconnect one client
ClientState state = service.getClientState("client-2");
ClientState disconnectedState = state.withConnected(false);
java.util.Map<String, ClientState> connectedClients =
(java.util.Map<String, ClientState>) ReflectionTestUtils.getField(service, "connectedClients");
connectedClients.put("client-2", disconnectedState);
// When/Then
assertThat(service.getConnectedClientCount()).isEqualTo(1);
assertThat(service.getTotalClientCount()).isEqualTo(2);
}
}
}

View File

@@ -0,0 +1,402 @@
package de.assecutor.votianlt.service;
import de.assecutor.votianlt.model.Job;
import de.assecutor.votianlt.model.JobStatus;
import de.assecutor.votianlt.model.User;
import de.assecutor.votianlt.repository.JobRepository;
import de.assecutor.votianlt.repository.TaskRepository;
import de.assecutor.votianlt.repository.UserRepository;
import org.bson.types.ObjectId;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.test.util.ReflectionTestUtils;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
@DisplayName("EmailService Tests")
class EmailServiceTest {
@Mock
private UserRepository userRepository;
@Mock
private JobRepository jobRepository;
@Mock
private TaskRepository taskRepository;
@Mock
private JavaMailSender mailSender;
@InjectMocks
private EmailService emailService;
@Captor
private ArgumentCaptor<SimpleMailMessage> messageCaptor;
private static final String SMTP_USERNAME = "test@votianlt.de";
@BeforeEach
void setUp() {
ReflectionTestUtils.setField(emailService, "smtpUsername", SMTP_USERNAME);
}
private User createTestUser() {
User user = new User();
user.setId(new ObjectId());
user.setTitle("Dr.");
user.setFirstname("Max");
user.setName("Mustermann");
user.setEmail("max.mustermann@example.com");
return user;
}
private Job createTestJob() {
Job job = new Job();
job.setId(new ObjectId());
job.setJobNumber("JOB-2024-001");
job.setDeliveryCompany("Test GmbH");
job.setPickupCity("Berlin");
job.setDeliveryCity("Hamburg");
job.setStatus(JobStatus.IN_PROGRESS);
job.setCreatedAt(LocalDateTime.now());
job.setCreatedBy(new ObjectId().toHexString());
return job;
}
@Nested
@DisplayName("sendTaskCompletionNotification Tests")
class SendTaskCompletionNotificationTests {
@Test
@DisplayName("should send email when job and user exist with valid email")
void shouldSendEmailWhenJobAndUserExist() {
// Given
Job job = createTestJob();
User user = createTestUser();
job.setCreatedBy(user.getId().toHexString());
when(jobRepository.findById(job.getId())).thenReturn(Optional.of(job));
when(userRepository.findById(user.getId())).thenReturn(Optional.of(user));
// When
emailService.sendTaskCompletionNotification(job.getId(), "PHOTO", "task-123", "app-user");
// Then
verify(mailSender).send(messageCaptor.capture());
SimpleMailMessage sentMessage = messageCaptor.getValue();
assertThat(sentMessage.getFrom()).isEqualTo(SMTP_USERNAME);
assertThat(sentMessage.getTo()).containsExactly(user.getEmail());
assertThat(sentMessage.getSubject()).contains("Aufgabe abgeschlossen");
assertThat(sentMessage.getSubject()).contains(job.getJobNumber());
assertThat(sentMessage.getText()).contains("Dr. Max Mustermann");
assertThat(sentMessage.getText()).contains("Foto-Aufgabe");
}
@Test
@DisplayName("should not send email when job not found")
void shouldNotSendEmailWhenJobNotFound() {
// Given
ObjectId jobId = new ObjectId();
when(jobRepository.findById(jobId)).thenReturn(Optional.empty());
// When
emailService.sendTaskCompletionNotification(jobId, "PHOTO", "task-123", "app-user");
// Then
verify(mailSender, never()).send(any(SimpleMailMessage.class));
}
@Test
@DisplayName("should not send email when user not found")
void shouldNotSendEmailWhenUserNotFound() {
// Given
Job job = createTestJob();
when(jobRepository.findById(job.getId())).thenReturn(Optional.of(job));
when(userRepository.findById(any(ObjectId.class))).thenReturn(Optional.empty());
// When
emailService.sendTaskCompletionNotification(job.getId(), "PHOTO", "task-123", "app-user");
// Then
verify(mailSender, never()).send(any(SimpleMailMessage.class));
}
@Test
@DisplayName("should not send email when user has no email address")
void shouldNotSendEmailWhenUserHasNoEmail() {
// Given
Job job = createTestJob();
User user = createTestUser();
user.setEmail(null);
job.setCreatedBy(user.getId().toHexString());
when(jobRepository.findById(job.getId())).thenReturn(Optional.of(job));
when(userRepository.findById(user.getId())).thenReturn(Optional.of(user));
// When
emailService.sendTaskCompletionNotification(job.getId(), "PHOTO", "task-123", "app-user");
// Then
verify(mailSender, never()).send(any(SimpleMailMessage.class));
}
@Test
@DisplayName("should not send email when user has blank email address")
void shouldNotSendEmailWhenUserHasBlankEmail() {
// Given
Job job = createTestJob();
User user = createTestUser();
user.setEmail(" ");
job.setCreatedBy(user.getId().toHexString());
when(jobRepository.findById(job.getId())).thenReturn(Optional.of(job));
when(userRepository.findById(user.getId())).thenReturn(Optional.of(user));
// When
emailService.sendTaskCompletionNotification(job.getId(), "PHOTO", "task-123", "app-user");
// Then
verify(mailSender, never()).send(any(SimpleMailMessage.class));
}
@Test
@DisplayName("should handle different task types correctly")
void shouldHandleDifferentTaskTypes() {
// Given
Job job = createTestJob();
User user = createTestUser();
job.setCreatedBy(user.getId().toHexString());
when(jobRepository.findById(job.getId())).thenReturn(Optional.of(job));
when(userRepository.findById(user.getId())).thenReturn(Optional.of(user));
// Test each task type
String[][] taskTypes = {
{"PHOTO", "Foto-Aufgabe"},
{"SIGNATURE", "Unterschrift"},
{"BARCODE", "Barcode scannen"},
{"CONFIRMATION", "Bestätigung"},
{"TODO_LIST", "Checkliste"},
{"COMMENT", "Kommentar"},
{"UNKNOWN_TYPE", "UNKNOWN_TYPE"}
};
for (String[] taskType : taskTypes) {
reset(mailSender);
// When
emailService.sendTaskCompletionNotification(job.getId(), taskType[0], "task-123", "app-user");
// Then
verify(mailSender).send(messageCaptor.capture());
assertThat(messageCaptor.getValue().getText()).contains(taskType[1]);
}
}
@Test
@DisplayName("should include route in email when cities are set")
void shouldIncludeRouteWhenCitiesAreSet() {
// Given
Job job = createTestJob();
User user = createTestUser();
job.setCreatedBy(user.getId().toHexString());
when(jobRepository.findById(job.getId())).thenReturn(Optional.of(job));
when(userRepository.findById(user.getId())).thenReturn(Optional.of(user));
// When
emailService.sendTaskCompletionNotification(job.getId(), "PHOTO", "task-123", "app-user");
// Then
verify(mailSender).send(messageCaptor.capture());
String text = messageCaptor.getValue().getText();
assertThat(text).contains("Route: Berlin → Hamburg");
}
}
@Nested
@DisplayName("sendJobCompletionNotification Tests")
class SendJobCompletionNotificationTests {
@Test
@DisplayName("should send email with task count when job is completed")
void shouldSendEmailWithTaskCount() {
// Given
Job job = createTestJob();
User user = createTestUser();
job.setCreatedBy(user.getId().toHexString());
when(jobRepository.findById(job.getId())).thenReturn(Optional.of(job));
when(userRepository.findById(user.getId())).thenReturn(Optional.of(user));
when(taskRepository.findByJobIdOrderByTaskOrderAsc(job.getId())).thenReturn(Collections.emptyList());
// When
emailService.sendJobCompletionNotification(job.getId(), "app-user");
// Then
verify(mailSender).send(messageCaptor.capture());
SimpleMailMessage sentMessage = messageCaptor.getValue();
assertThat(sentMessage.getSubject()).contains("Job abgeschlossen");
assertThat(sentMessage.getText()).contains("alle Aufgaben für den folgenden Job wurden erfolgreich abgeschlossen");
assertThat(sentMessage.getText()).contains("Anzahl erledigter Aufgaben: 0");
}
@Test
@DisplayName("should not send email when job not found")
void shouldNotSendEmailWhenJobNotFound() {
// Given
ObjectId jobId = new ObjectId();
when(jobRepository.findById(jobId)).thenReturn(Optional.empty());
// When
emailService.sendJobCompletionNotification(jobId, "app-user");
// Then
verify(mailSender, never()).send(any(SimpleMailMessage.class));
}
}
@Nested
@DisplayName("sendJobCreationNotification Tests")
class SendJobCreationNotificationTests {
@Test
@DisplayName("should send email when job is created")
void shouldSendEmailWhenJobIsCreated() {
// Given
User user = createTestUser();
Job job = createTestJob();
job.setCreatedBy(user.getId().toHexString());
job.setRemark("Wichtige Lieferung");
when(jobRepository.findById(job.getId())).thenReturn(Optional.of(job));
when(userRepository.findById(user.getId())).thenReturn(Optional.of(user));
when(taskRepository.findByJobIdOrderByTaskOrderAsc(job.getId())).thenReturn(Collections.emptyList());
// When
emailService.sendJobCreationNotification(job.getId(), user.getId().toHexString());
// Then
verify(mailSender).send(messageCaptor.capture());
SimpleMailMessage sentMessage = messageCaptor.getValue();
assertThat(sentMessage.getSubject()).contains("Neuer Job erstellt");
assertThat(sentMessage.getText()).contains("ein neuer Job wurde erfolgreich erstellt");
assertThat(sentMessage.getText()).contains("Bemerkung: Wichtige Lieferung");
}
@Test
@DisplayName("should handle invalid createdBy format")
void shouldHandleInvalidCreatedByFormat() {
// Given
Job job = createTestJob();
when(jobRepository.findById(job.getId())).thenReturn(Optional.of(job));
// When
emailService.sendJobCreationNotification(job.getId(), "invalid-object-id");
// Then
verify(mailSender, never()).send(any(SimpleMailMessage.class));
}
}
@Nested
@DisplayName("sendSimpleEmail Tests")
class SendSimpleEmailTests {
@Test
@DisplayName("should send simple email successfully")
void shouldSendSimpleEmail() {
// When
emailService.sendSimpleEmail("recipient@example.com", "Test Subject", "Test Body");
// Then
verify(mailSender).send(messageCaptor.capture());
SimpleMailMessage sentMessage = messageCaptor.getValue();
assertThat(sentMessage.getFrom()).isEqualTo(SMTP_USERNAME);
assertThat(sentMessage.getTo()).containsExactly("recipient@example.com");
assertThat(sentMessage.getSubject()).isEqualTo("Test Subject");
assertThat(sentMessage.getText()).isEqualTo("Test Body");
}
@Test
@DisplayName("should throw exception when mail sending fails")
void shouldThrowExceptionWhenMailSendingFails() {
// Given
doThrow(new RuntimeException("SMTP error")).when(mailSender).send(any(SimpleMailMessage.class));
// When/Then
assertThatThrownBy(() ->
emailService.sendSimpleEmail("recipient@example.com", "Subject", "Body"))
.isInstanceOf(RuntimeException.class)
.hasMessageContaining("Failed to send email");
}
}
@Nested
@DisplayName("buildFullName Tests")
class BuildFullNameTests {
@Test
@DisplayName("should build full name with title, firstname and lastname")
void shouldBuildFullNameWithAllParts() {
// Given
Job job = createTestJob();
User user = createTestUser();
job.setCreatedBy(user.getId().toHexString());
when(jobRepository.findById(job.getId())).thenReturn(Optional.of(job));
when(userRepository.findById(user.getId())).thenReturn(Optional.of(user));
// When
emailService.sendTaskCompletionNotification(job.getId(), "PHOTO", "task-123", "app-user");
// Then
verify(mailSender).send(messageCaptor.capture());
assertThat(messageCaptor.getValue().getText()).contains("Dr. Max Mustermann");
}
@Test
@DisplayName("should use default name when user has no name parts")
void shouldUseDefaultNameWhenNoNameParts() {
// Given
Job job = createTestJob();
User user = new User();
user.setId(new ObjectId());
user.setEmail("test@example.com");
job.setCreatedBy(user.getId().toHexString());
when(jobRepository.findById(job.getId())).thenReturn(Optional.of(job));
when(userRepository.findById(user.getId())).thenReturn(Optional.of(user));
// When
emailService.sendTaskCompletionNotification(job.getId(), "PHOTO", "task-123", "app-user");
// Then
verify(mailSender).send(messageCaptor.capture());
assertThat(messageCaptor.getValue().getText()).contains("Hallo Benutzer");
}
}
}

View File

@@ -0,0 +1,536 @@
package de.assecutor.votianlt.service;
import com.fasterxml.jackson.databind.ObjectMapper;
import de.assecutor.votianlt.model.Job;
import de.assecutor.votianlt.model.JobHistory;
import de.assecutor.votianlt.model.JobHistoryType;
import de.assecutor.votianlt.model.JobStatus;
import de.assecutor.votianlt.repository.JobHistoryRepository;
import org.bson.types.ObjectId;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
@DisplayName("JobHistoryService Tests")
class JobHistoryServiceTest {
@Mock
private JobHistoryRepository jobHistoryRepository;
private ObjectMapper objectMapper;
private JobHistoryService service;
@Captor
private ArgumentCaptor<JobHistory> historyCaptor;
@BeforeEach
void setUp() {
objectMapper = new ObjectMapper();
objectMapper.findAndRegisterModules();
service = new JobHistoryService(jobHistoryRepository, objectMapper);
}
private Job createTestJob() {
Job job = new Job();
job.setId(new ObjectId());
job.setJobNumber("JOB-2024-001");
job.setDeliveryCompany("Test GmbH");
job.setPickupCity("Berlin");
job.setDeliveryCity("Hamburg");
job.setStatus(JobStatus.CREATED);
job.setCreatedAt(LocalDateTime.now());
job.setRemark("Test Bemerkung");
return job;
}
@Nested
@DisplayName("logJobCreation Tests")
class LogJobCreationTests {
@Test
@DisplayName("should log job creation with job number")
void shouldLogJobCreationWithJobNumber() {
// Given
Job job = createTestJob();
String createdBy = "user-123";
// When
service.logJobCreation(job, createdBy);
// Then
verify(jobHistoryRepository).save(historyCaptor.capture());
JobHistory history = historyCaptor.getValue();
assertThat(history.getJobId()).isEqualTo(job.getId());
assertThat(history.getReason()).isEqualTo("Job erstellt");
assertThat(history.getDescription()).contains("JOB-2024-001");
assertThat(history.getChangedBy()).isEqualTo(createdBy);
assertThat(history.getChangeType()).isEqualTo(JobHistoryType.CREATE);
assertThat(history.getNewValue()).isEqualTo("Job erstellt");
}
@Test
@DisplayName("should log job creation without job number")
void shouldLogJobCreationWithoutJobNumber() {
// Given
Job job = createTestJob();
job.setJobNumber(null);
String createdBy = "user-123";
// When
service.logJobCreation(job, createdBy);
// Then
verify(jobHistoryRepository).save(historyCaptor.capture());
JobHistory history = historyCaptor.getValue();
assertThat(history.getDescription()).contains("Ohne Nummer");
}
@Test
@DisplayName("should include delivery company in details")
void shouldIncludeDeliveryCompanyInDetails() {
// Given
Job job = createTestJob();
String createdBy = "user-123";
// When
service.logJobCreation(job, createdBy);
// Then
verify(jobHistoryRepository).save(historyCaptor.capture());
JobHistory history = historyCaptor.getValue();
assertThat(history.getDetails()).contains("Test GmbH");
}
@Test
@DisplayName("should handle exception gracefully")
void shouldHandleExceptionGracefully() {
// Given
Job job = createTestJob();
when(jobHistoryRepository.save(any())).thenThrow(new RuntimeException("DB error"));
// When/Then - should not throw
service.logJobCreation(job, "user-123");
verify(jobHistoryRepository).save(any());
}
}
@Nested
@DisplayName("logStatusChange Tests")
class LogStatusChangeTests {
@Test
@DisplayName("should log status change correctly")
void shouldLogStatusChangeCorrectly() {
// Given
Job job = createTestJob();
JobStatus oldStatus = JobStatus.CREATED;
JobStatus newStatus = JobStatus.IN_PROGRESS;
// When
service.logStatusChange(job, oldStatus, newStatus, "user-123");
// Then
verify(jobHistoryRepository).save(historyCaptor.capture());
JobHistory history = historyCaptor.getValue();
assertThat(history.getJobId()).isEqualTo(job.getId());
assertThat(history.getReason()).isEqualTo("Status-Änderung");
assertThat(history.getDescription()).contains("Erstellt");
assertThat(history.getDescription()).contains("In Bearbeitung");
assertThat(history.getChangeType()).isEqualTo(JobHistoryType.STATUS_CHANGE);
assertThat(history.getOldValue()).isEqualTo("Erstellt");
assertThat(history.getNewValue()).isEqualTo("In Bearbeitung");
}
@Test
@DisplayName("should handle null old status")
void shouldHandleNullOldStatus() {
// Given
Job job = createTestJob();
JobStatus newStatus = JobStatus.CREATED;
// When
service.logStatusChange(job, null, newStatus, "user-123");
// Then
verify(jobHistoryRepository).save(historyCaptor.capture());
JobHistory history = historyCaptor.getValue();
assertThat(history.getDescription()).contains("Unbekannt");
assertThat(history.getOldValue()).isNull();
}
@Test
@DisplayName("should handle null new status")
void shouldHandleNullNewStatus() {
// Given
Job job = createTestJob();
JobStatus oldStatus = JobStatus.IN_PROGRESS;
// When
service.logStatusChange(job, oldStatus, null, "user-123");
// Then
verify(jobHistoryRepository).save(historyCaptor.capture());
JobHistory history = historyCaptor.getValue();
assertThat(history.getDescription()).contains("Unbekannt");
assertThat(history.getNewValue()).isNull();
}
@Test
@DisplayName("should format all status types correctly")
void shouldFormatAllStatusTypesCorrectly() {
// Given
Job job = createTestJob();
JobStatus[] statuses = JobStatus.values();
for (JobStatus status : statuses) {
reset(jobHistoryRepository);
// When
service.logStatusChange(job, JobStatus.CREATED, status, "user-123");
// Then
verify(jobHistoryRepository).save(historyCaptor.capture());
assertThat(historyCaptor.getValue().getNewValue()).isNotNull();
}
}
}
@Nested
@DisplayName("logJobUpdate Tests")
class LogJobUpdateTests {
@Test
@DisplayName("should log job update with reason")
void shouldLogJobUpdateWithReason() {
// Given
Job oldJob = createTestJob();
Job newJob = createTestJob();
newJob.setId(oldJob.getId());
newJob.setDeliveryCompany("Neue GmbH");
// When
service.logJobUpdate(oldJob, newJob, "user-123", "Kundendaten geändert");
// Then
verify(jobHistoryRepository).save(historyCaptor.capture());
JobHistory history = historyCaptor.getValue();
assertThat(history.getReason()).isEqualTo("Kundendaten geändert");
assertThat(history.getChangeType()).isEqualTo(JobHistoryType.UPDATE);
assertThat(history.getDescription()).contains("Kunde");
}
@Test
@DisplayName("should use default reason when null")
void shouldUseDefaultReasonWhenNull() {
// Given
Job oldJob = createTestJob();
Job newJob = createTestJob();
newJob.setId(oldJob.getId());
// When
service.logJobUpdate(oldJob, newJob, "user-123", null);
// Then
verify(jobHistoryRepository).save(historyCaptor.capture());
JobHistory history = historyCaptor.getValue();
assertThat(history.getReason()).isEqualTo("Job aktualisiert");
}
@Test
@DisplayName("should detect city changes")
void shouldDetectCityChanges() {
// Given
Job oldJob = createTestJob();
Job newJob = createTestJob();
newJob.setId(oldJob.getId());
newJob.setPickupCity("München");
// When
service.logJobUpdate(oldJob, newJob, "user-123", null);
// Then
verify(jobHistoryRepository).save(historyCaptor.capture());
JobHistory history = historyCaptor.getValue();
assertThat(history.getDescription()).contains("Orte");
}
@Test
@DisplayName("should detect remark changes")
void shouldDetectRemarkChanges() {
// Given
Job oldJob = createTestJob();
Job newJob = createTestJob();
newJob.setId(oldJob.getId());
newJob.setRemark("Neue Bemerkung");
// When
service.logJobUpdate(oldJob, newJob, "user-123", null);
// Then
verify(jobHistoryRepository).save(historyCaptor.capture());
JobHistory history = historyCaptor.getValue();
assertThat(history.getDescription()).contains("Bemerkung");
}
@Test
@DisplayName("should handle null old job")
void shouldHandleNullOldJob() {
// Given
Job newJob = createTestJob();
// When
service.logJobUpdate(null, newJob, "user-123", null);
// Then
verify(jobHistoryRepository).save(historyCaptor.capture());
JobHistory history = historyCaptor.getValue();
assertThat(history.getDescription()).isEqualTo("Job-Daten aktualisiert");
}
}
@Nested
@DisplayName("logTaskCompletion Tests")
class LogTaskCompletionTests {
@Test
@DisplayName("should log task completion with basic info")
void shouldLogTaskCompletionWithBasicInfo() {
// Given
ObjectId jobId = new ObjectId();
// When
service.logTaskCompletion(jobId, "PHOTO", "task-123", "app-user");
// Then
verify(jobHistoryRepository).save(historyCaptor.capture());
JobHistory history = historyCaptor.getValue();
assertThat(history.getJobId()).isEqualTo(jobId);
assertThat(history.getReason()).isEqualTo("Aufgabe abgeschlossen");
assertThat(history.getDescription()).contains("PHOTO");
assertThat(history.getChangedBy()).isEqualTo("app-user");
assertThat(history.getChangeType()).isEqualTo(JobHistoryType.TASK_COMPLETED);
}
@Test
@DisplayName("should log task completion with display name and extra data")
void shouldLogTaskCompletionWithDisplayNameAndExtraData() {
// Given
ObjectId jobId = new ObjectId();
// When
service.logTaskCompletion(jobId, "PHOTO", "task-123", "app-user",
"Ablieferungsfoto", "3 Fotos hochgeladen");
// Then
verify(jobHistoryRepository).save(historyCaptor.capture());
JobHistory history = historyCaptor.getValue();
assertThat(history.getDescription()).contains("Ablieferungsfoto");
assertThat(history.getDescription()).contains("3 Fotos hochgeladen");
assertThat(history.getDetails()).contains("Task-ID: task-123");
assertThat(history.getDetails()).contains("Task-Typ: PHOTO");
assertThat(history.getDetails()).contains("Name: Ablieferungsfoto");
assertThat(history.getDetails()).contains("Zusatzdaten: 3 Fotos hochgeladen");
}
@Test
@DisplayName("should handle null display name")
void shouldHandleNullDisplayName() {
// Given
ObjectId jobId = new ObjectId();
// When
service.logTaskCompletion(jobId, "BARCODE", "task-123", "app-user", null, null);
// Then
verify(jobHistoryRepository).save(historyCaptor.capture());
JobHistory history = historyCaptor.getValue();
assertThat(history.getDescription()).contains("BARCODE");
}
}
@Nested
@DisplayName("logJobAssignment Tests")
class LogJobAssignmentTests {
@Test
@DisplayName("should log new assignment")
void shouldLogNewAssignment() {
// Given
Job job = createTestJob();
// When
service.logJobAssignment(job, null, "Max Mustermann", "user-123");
// Then
verify(jobHistoryRepository).save(historyCaptor.capture());
JobHistory history = historyCaptor.getValue();
assertThat(history.getReason()).isEqualTo("Zuweisung geändert");
assertThat(history.getDescription()).isEqualTo("Job zugewiesen an: Max Mustermann");
assertThat(history.getChangeType()).isEqualTo(JobHistoryType.ASSIGNMENT);
assertThat(history.getOldValue()).isNull();
assertThat(history.getNewValue()).isEqualTo("Max Mustermann");
}
@Test
@DisplayName("should log assignment removal")
void shouldLogAssignmentRemoval() {
// Given
Job job = createTestJob();
// When
service.logJobAssignment(job, "Max Mustermann", null, "user-123");
// Then
verify(jobHistoryRepository).save(historyCaptor.capture());
JobHistory history = historyCaptor.getValue();
assertThat(history.getDescription()).isEqualTo("Job-Zuweisung entfernt von: Max Mustermann");
}
@Test
@DisplayName("should log assignment change")
void shouldLogAssignmentChange() {
// Given
Job job = createTestJob();
// When
service.logJobAssignment(job, "Max Mustermann", "Erika Musterfrau", "user-123");
// Then
verify(jobHistoryRepository).save(historyCaptor.capture());
JobHistory history = historyCaptor.getValue();
assertThat(history.getDescription())
.isEqualTo("Job-Zuweisung geändert von Max Mustermann zu Erika Musterfrau");
}
}
@Nested
@DisplayName("logCustomEvent Tests")
class LogCustomEventTests {
@Test
@DisplayName("should log custom event correctly")
void shouldLogCustomEventCorrectly() {
// Given
ObjectId jobId = new ObjectId();
// When
service.logCustomEvent(jobId, "Export", "Job als PDF exportiert",
"user-123", JobHistoryType.EXPORT);
// Then
verify(jobHistoryRepository).save(historyCaptor.capture());
JobHistory history = historyCaptor.getValue();
assertThat(history.getJobId()).isEqualTo(jobId);
assertThat(history.getReason()).isEqualTo("Export");
assertThat(history.getDescription()).isEqualTo("Job als PDF exportiert");
assertThat(history.getChangedBy()).isEqualTo("user-123");
assertThat(history.getChangeType()).isEqualTo(JobHistoryType.EXPORT);
}
}
@Nested
@DisplayName("getJobHistory Tests")
class GetJobHistoryTests {
@Test
@DisplayName("should return job history from repository")
void shouldReturnJobHistoryFromRepository() {
// Given
ObjectId jobId = new ObjectId();
JobHistory history1 = new JobHistory(jobId, "Event 1", "Description 1", "user-1");
JobHistory history2 = new JobHistory(jobId, "Event 2", "Description 2", "user-2");
List<JobHistory> expectedHistory = Arrays.asList(history1, history2);
when(jobHistoryRepository.findByJobIdOrderByTimestampDesc(jobId)).thenReturn(expectedHistory);
// When
List<JobHistory> result = service.getJobHistory(jobId);
// Then
assertThat(result).hasSize(2);
assertThat(result).containsExactlyElementsOf(expectedHistory);
}
@Test
@DisplayName("should return empty list when no history exists")
void shouldReturnEmptyListWhenNoHistoryExists() {
// Given
ObjectId jobId = new ObjectId();
when(jobHistoryRepository.findByJobIdOrderByTimestampDesc(jobId)).thenReturn(Collections.emptyList());
// When
List<JobHistory> result = service.getJobHistory(jobId);
// Then
assertThat(result).isEmpty();
}
}
@Nested
@DisplayName("getJobHistoryCount Tests")
class GetJobHistoryCountTests {
@Test
@DisplayName("should return correct count from repository")
void shouldReturnCorrectCountFromRepository() {
// Given
ObjectId jobId = new ObjectId();
when(jobHistoryRepository.countByJobId(jobId)).thenReturn(5L);
// When
long count = service.getJobHistoryCount(jobId);
// Then
assertThat(count).isEqualTo(5L);
}
@Test
@DisplayName("should return zero when no history exists")
void shouldReturnZeroWhenNoHistoryExists() {
// Given
ObjectId jobId = new ObjectId();
when(jobHistoryRepository.countByJobId(jobId)).thenReturn(0L);
// When
long count = service.getJobHistoryCount(jobId);
// Then
assertThat(count).isZero();
}
}
}