Erweiterungen
This commit is contained in:
343
docs/JOB_JSON.md
Normal file
343
docs/JOB_JSON.md
Normal 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
12
pom.xml
@@ -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.
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user