Ajay Yadav commited on
Commit
2b88acc
·
1 Parent(s): 04ef45d

Initial deployment of da-autodelete-dev

Browse files
Files changed (45) hide show
  1. Dockerfile +27 -0
  2. README.md +33 -5
  3. build.gradle.kts +20 -0
  4. src/main/docker/Dockerfile +23 -0
  5. src/main/docker/Dockerfile.alpine-jlink +43 -0
  6. src/main/docker/Dockerfile.layered +34 -0
  7. src/main/docker/Dockerfile.native +20 -0
  8. src/main/java/com/dalab/autodelete/DaAutodeleteApplication.java +35 -0
  9. src/main/java/com/dalab/autodelete/controller/DeletionConfigController.java +49 -0
  10. src/main/java/com/dalab/autodelete/controller/DeletionTaskController.java +110 -0
  11. src/main/java/com/dalab/autodelete/dto/DeletionConfigDTO.java +30 -0
  12. src/main/java/com/dalab/autodelete/dto/DeletionTaskListResponse.java +40 -0
  13. src/main/java/com/dalab/autodelete/dto/DeletionTaskRequest.java +58 -0
  14. src/main/java/com/dalab/autodelete/dto/DeletionTaskResponse.java +23 -0
  15. src/main/java/com/dalab/autodelete/dto/DeletionTaskStatusDTO.java +42 -0
  16. src/main/java/com/dalab/autodelete/dto/TaskApprovalRequest.java +20 -0
  17. src/main/java/com/dalab/autodelete/exception/DeletionException.java +15 -0
  18. src/main/java/com/dalab/autodelete/mapper/DeletionConfigMapper.java +21 -0
  19. src/main/java/com/dalab/autodelete/mapper/DeletionTaskMapper.java +50 -0
  20. src/main/java/com/dalab/autodelete/model/DeletionConfigEntity.java +40 -0
  21. src/main/java/com/dalab/autodelete/model/DeletionResult.java +28 -0
  22. src/main/java/com/dalab/autodelete/model/DeletionStatus.java +14 -0
  23. src/main/java/com/dalab/autodelete/model/DeletionTask.java +78 -0
  24. src/main/java/com/dalab/autodelete/model/DeletionTaskEntity.java +83 -0
  25. src/main/java/com/dalab/autodelete/provider/GcpDeletionProvider.java +45 -0
  26. src/main/java/com/dalab/autodelete/provider/IGcpDeletionProvider.java +43 -0
  27. src/main/java/com/dalab/autodelete/repository/DeletionConfigRepository.java +11 -0
  28. src/main/java/com/dalab/autodelete/repository/DeletionTaskRepository.java +12 -0
  29. src/main/java/com/dalab/autodelete/service/IDeletionConfigService.java +18 -0
  30. src/main/java/com/dalab/autodelete/service/IDeletionTaskService.java +48 -0
  31. src/main/java/com/dalab/autodelete/service/exception/DeletionTaskNotFoundException.java +15 -0
  32. src/main/java/com/dalab/autodelete/service/impl/DeletionConfigServiceImpl.java +84 -0
  33. src/main/java/com/dalab/autodelete/service/impl/DeletionTaskServiceImpl.java +156 -0
  34. src/main/java/com/dalab/autodelete/service/storage/BulkDeletionResult.java +30 -0
  35. src/main/java/com/dalab/autodelete/service/storage/DeletionResult.java +34 -0
  36. src/main/java/com/dalab/autodelete/service/storage/DeletionStatus.java +14 -0
  37. src/main/java/com/dalab/autodelete/service/storage/DeletionType.java +26 -0
  38. src/main/java/com/dalab/autodelete/service/storage/DeletionVerificationResult.java +29 -0
  39. src/main/java/com/dalab/autodelete/service/storage/ICloudDeletionService.java +74 -0
  40. src/main/java/com/dalab/autodelete/service/storage/StorageReclaimEstimate.java +25 -0
  41. src/main/resources/application.properties +72 -0
  42. src/test/java/com/dalab/autodelete/controller/DeletionConfigControllerTest.java +111 -0
  43. src/test/java/com/dalab/autodelete/controller/DeletionTaskControllerTest.java +186 -0
  44. src/test/java/com/dalab/autodelete/service/impl/DeletionConfigServiceImplTest.java +119 -0
  45. src/test/java/com/dalab/autodelete/service/impl/DeletionTaskServiceImplTest.java +240 -0
Dockerfile ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM openjdk:21-jdk-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Install required packages
6
+ RUN apt-get update && apt-get install -y \
7
+ curl \
8
+ wget \
9
+ && rm -rf /var/lib/apt/lists/*
10
+
11
+ # Copy application files
12
+ COPY . .
13
+
14
+ # Build application (if build.gradle.kts exists)
15
+ RUN if [ -f "build.gradle.kts" ]; then \
16
+ ./gradlew build -x test; \
17
+ fi
18
+
19
+ # Expose port
20
+ EXPOSE 8080
21
+
22
+ # Health check
23
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
24
+ CMD curl -f http://localhost:8080/actuator/health || exit 1
25
+
26
+ # Run application
27
+ CMD ["java", "-jar", "build/libs/da-autodelete.jar"]
README.md CHANGED
@@ -1,10 +1,38 @@
1
  ---
2
- title: Da Autodelete Dev
3
- emoji: 🏃
4
  colorFrom: blue
5
- colorTo: gray
6
  sdk: docker
7
- pinned: false
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: da-autodelete (dev)
3
+ emoji: 🔧
4
  colorFrom: blue
5
+ colorTo: green
6
  sdk: docker
7
+ app_port: 8080
8
  ---
9
 
10
+ # da-autodelete - dev Environment
11
+
12
+ This is the da-autodelete microservice deployed in the dev environment.
13
+
14
+ ## Features
15
+
16
+ - RESTful API endpoints
17
+ - Health monitoring via Actuator
18
+ - JWT authentication integration
19
+ - PostgreSQL database connectivity
20
+
21
+ ## API Documentation
22
+
23
+ Once deployed, API documentation will be available at:
24
+ - Swagger UI: https://huggingface.co/spaces/dalabsai/da-autodelete-dev/swagger-ui.html
25
+ - Health Check: https://huggingface.co/spaces/dalabsai/da-autodelete-dev/actuator/health
26
+
27
+ ## Environment
28
+
29
+ - **Environment**: dev
30
+ - **Port**: 8080
31
+ - **Java Version**: 21
32
+ - **Framework**: Spring Boot
33
+
34
+ ## Deployment
35
+
36
+ This service is automatically deployed via the DALab CI/CD pipeline.
37
+
38
+ Last updated: 2025-06-16 23:40:24
build.gradle.kts ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // da-autodelete inherits common configuration from parent build.gradle.kts
2
+ // This build file adds autodelete-specific dependencies
3
+
4
+ dependencies {
5
+ // da-protos common entities and utilities
6
+ implementation(project(":da-protos"))
7
+
8
+ // Cloud Storage SDKs for deletion operations
9
+ implementation("software.amazon.awssdk:s3:2.20.162")
10
+ implementation("com.azure:azure-storage-blob:12.23.0")
11
+ implementation("com.google.cloud:google-cloud-storage:2.28.0")
12
+
13
+ // Additional dependencies specific to da-autodelete
14
+ implementation("org.springframework.cloud:spring-cloud-starter-openfeign:4.1.1")
15
+ }
16
+
17
+ // Configure main application class
18
+ configure<org.springframework.boot.gradle.dsl.SpringBootExtension> {
19
+ mainClass.set("com.dalab.autodelete.DaAutodeleteApplication")
20
+ }
src/main/docker/Dockerfile ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Ultra-lean container using Google Distroless
2
+ # Expected final size: ~120-180MB (minimal base + JRE + JAR only)
3
+
4
+ FROM gcr.io/distroless/java21-debian12:nonroot
5
+
6
+ # Set working directory
7
+ WORKDIR /app
8
+
9
+ # Copy JAR file
10
+ COPY build/libs/da-autodelete.jar app.jar
11
+
12
+ # Expose standard Spring Boot port
13
+ EXPOSE 8080
14
+
15
+ # Run application (distroless has no shell, so use exec form)
16
+ ENTRYPOINT ["java", \
17
+ "-XX:+UseContainerSupport", \
18
+ "-XX:MaxRAMPercentage=75.0", \
19
+ "-XX:+UseG1GC", \
20
+ "-XX:+UseStringDeduplication", \
21
+ "-Djava.security.egd=file:/dev/./urandom", \
22
+ "-Dspring.backgroundpreinitializer.ignore=true", \
23
+ "-jar", "app.jar"]
src/main/docker/Dockerfile.alpine-jlink ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Ultra-minimal Alpine + Custom JRE
2
+ # Expected size: ~120-160MB
3
+
4
+ # Stage 1: Create custom JRE with only needed modules
5
+ FROM eclipse-temurin:21-jdk-alpine as jre-builder
6
+ WORKDIR /app
7
+
8
+ # Analyze JAR to find required modules
9
+ COPY build/libs/*.jar app.jar
10
+ RUN jdeps --ignore-missing-deps --print-module-deps app.jar > modules.txt
11
+
12
+ # Create minimal JRE with only required modules
13
+ RUN jlink \
14
+ --add-modules $(cat modules.txt),java.logging,java.xml,java.sql,java.naming,java.desktop,java.management,java.security.jgss,java.instrument \
15
+ --strip-debug \
16
+ --no-man-pages \
17
+ --no-header-files \
18
+ --compress=2 \
19
+ --output /custom-jre
20
+
21
+ # Stage 2: Production image
22
+ FROM alpine:3.19
23
+ RUN apk add --no-cache tzdata && \
24
+ addgroup -g 1001 -S appgroup && \
25
+ adduser -u 1001 -S appuser -G appgroup
26
+
27
+ # Copy custom JRE
28
+ COPY --from=jre-builder /custom-jre /opt/java
29
+ ENV JAVA_HOME=/opt/java
30
+ ENV PATH="$JAVA_HOME/bin:$PATH"
31
+
32
+ WORKDIR /app
33
+ COPY build/libs/*.jar app.jar
34
+ RUN chown appuser:appgroup app.jar
35
+
36
+ USER appuser
37
+ EXPOSE 8080
38
+
39
+ ENTRYPOINT ["java", \
40
+ "-XX:+UseContainerSupport", \
41
+ "-XX:MaxRAMPercentage=70.0", \
42
+ "-XX:+UseG1GC", \
43
+ "-jar", "app.jar"]
src/main/docker/Dockerfile.layered ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Ultra-optimized layered build using Distroless
2
+ # Expected size: ~180-220MB with better caching
3
+
4
+ FROM gcr.io/distroless/java21-debian12:nonroot as base
5
+
6
+ # Stage 1: Extract JAR layers for optimal caching
7
+ FROM eclipse-temurin:21-jdk-alpine as extractor
8
+ WORKDIR /app
9
+ COPY build/libs/*.jar app.jar
10
+ RUN java -Djarmode=layertools -jar app.jar extract
11
+
12
+ # Stage 2: Production image with extracted layers
13
+ FROM base
14
+ WORKDIR /app
15
+
16
+ # Copy layers in dependency order (best caching)
17
+ COPY --from=extractor /app/dependencies/ ./
18
+ COPY --from=extractor /app/spring-boot-loader/ ./
19
+ COPY --from=extractor /app/snapshot-dependencies/ ./
20
+ COPY --from=extractor /app/application/ ./
21
+
22
+ EXPOSE 8080
23
+
24
+ # Optimized JVM settings for micro-containers
25
+ ENTRYPOINT ["java", \
26
+ "-XX:+UseContainerSupport", \
27
+ "-XX:MaxRAMPercentage=70.0", \
28
+ "-XX:+UseG1GC", \
29
+ "-XX:+UseStringDeduplication", \
30
+ "-XX:+CompactStrings", \
31
+ "-Xshare:on", \
32
+ "-Djava.security.egd=file:/dev/./urandom", \
33
+ "-Dspring.backgroundpreinitializer.ignore=true", \
34
+ "org.springframework.boot.loader.JarLauncher"]
src/main/docker/Dockerfile.native ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # GraalVM Native Image - Ultra-fast startup, tiny size
2
+ # Expected size: ~50-80MB, startup <100ms
3
+ # Note: Requires native compilation support in Spring Boot
4
+
5
+ # Stage 1: Native compilation
6
+ FROM ghcr.io/graalvm/graalvm-ce:ol9-java21 as native-builder
7
+ WORKDIR /app
8
+
9
+ # Install native-image
10
+ RUN gu install native-image
11
+
12
+ # Copy source and build native executable
13
+ COPY . .
14
+ RUN ./gradlew nativeCompile
15
+
16
+ # Stage 2: Minimal runtime
17
+ FROM scratch
18
+ COPY --from=native-builder /app/build/native/nativeCompile/app /app
19
+ EXPOSE 8080
20
+ ENTRYPOINT ["/app"]
src/main/java/com/dalab/autodelete/DaAutodeleteApplication.java ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autodelete;
2
+
3
+ import io.swagger.v3.oas.models.OpenAPI;
4
+ import io.swagger.v3.oas.models.info.Info;
5
+ import io.swagger.v3.oas.models.info.License;
6
+ import org.springframework.beans.factory.annotation.Value;
7
+ import org.springframework.boot.SpringApplication;
8
+ import org.springframework.boot.autoconfigure.SpringBootApplication;
9
+ import org.springframework.context.annotation.Bean;
10
+ import org.springframework.scheduling.annotation.EnableAsync;
11
+ import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
12
+
13
+ @SpringBootApplication
14
+ @EnableMethodSecurity // For @PreAuthorize annotations
15
+ @EnableAsync // If async operations are needed
16
+ // @EnableFeignClients // If this service will call other services via Feign
17
+ public class DaAutodeleteApplication {
18
+
19
+ public static void main(String[] args) {
20
+ SpringApplication.run(DaAutodeleteApplication.class, args);
21
+ }
22
+
23
+ @Bean
24
+ public OpenAPI customOpenAPI(@Value("${spring.application.name:DALab AutoDelete Service}") String appName,
25
+ @Value("${spring.application.description:API for AutoDelete Service}") String appDescription,
26
+ @Value("${spring.application.version:0.0.1-SNAPSHOT}") String appVersion) {
27
+ return new OpenAPI()
28
+ .info(new Info()
29
+ .title(appName)
30
+ .version(appVersion)
31
+ .description(appDescription)
32
+ .termsOfService("http://swagger.io/terms/") // Placeholder
33
+ .license(new License().name("Apache 2.0").url("http://springdoc.org"))); // Placeholder
34
+ }
35
+ }
src/main/java/com/dalab/autodelete/controller/DeletionConfigController.java ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autodelete.controller;
2
+
3
+ import com.dalab.autodelete.dto.DeletionConfigDTO;
4
+ import com.dalab.autodelete.service.IDeletionConfigService;
5
+ import io.swagger.v3.oas.annotations.Operation;
6
+ import io.swagger.v3.oas.annotations.responses.ApiResponse;
7
+ import io.swagger.v3.oas.annotations.tags.Tag;
8
+ import lombok.RequiredArgsConstructor;
9
+ import org.springframework.http.ResponseEntity;
10
+ import org.springframework.security.access.prepost.PreAuthorize;
11
+ import org.springframework.web.bind.annotation.*;
12
+
13
+ @RestController
14
+ @RequestMapping("/api/v1/deletion/config")
15
+ @Tag(name = "Deletion Configuration API", description = "APIs for managing auto-deletion configurations")
16
+ @RequiredArgsConstructor
17
+ public class DeletionConfigController {
18
+
19
+ private final IDeletionConfigService deletionConfigService;
20
+
21
+ @Operation(summary = "Get current deletion configuration",
22
+ responses = {
23
+ @ApiResponse(responseCode = "200", description = "Successfully retrieved configuration"),
24
+ @ApiResponse(responseCode = "403", description = "Forbidden if user does not have required role")
25
+ })
26
+ @GetMapping
27
+ @PreAuthorize("hasAnyRole('ADMIN', 'DATA_STEWARD')")
28
+ public ResponseEntity<DeletionConfigDTO> getDeletionConfiguration() {
29
+ return ResponseEntity.ok(deletionConfigService.getDeletionConfig());
30
+ }
31
+
32
+ @Operation(summary = "Update deletion configuration",
33
+ description = "Updates the global auto-deletion configuration. Only ADMINs can perform this.",
34
+ responses = {
35
+ @ApiResponse(responseCode = "200", description = "Configuration updated successfully"),
36
+ @ApiResponse(responseCode = "400", description = "Invalid configuration provided"),
37
+ @ApiResponse(responseCode = "403", description = "Forbidden if user is not an ADMIN")
38
+ })
39
+ @PutMapping
40
+ @PreAuthorize("hasRole('ADMIN')")
41
+ public ResponseEntity<Void> updateDeletionConfiguration(@RequestBody DeletionConfigDTO deletionConfigDTO) {
42
+ try {
43
+ deletionConfigService.updateDeletionConfig(deletionConfigDTO);
44
+ return ResponseEntity.ok().build();
45
+ } catch (IllegalArgumentException e) {
46
+ return ResponseEntity.badRequest().build();
47
+ }
48
+ }
49
+ }
src/main/java/com/dalab/autodelete/controller/DeletionTaskController.java ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autodelete.controller;
2
+
3
+ import com.dalab.autodelete.dto.*;
4
+ import com.dalab.autodelete.service.IDeletionTaskService;
5
+ import io.swagger.v3.oas.annotations.Operation;
6
+ import io.swagger.v3.oas.annotations.Parameter;
7
+ import io.swagger.v3.oas.annotations.responses.ApiResponse;
8
+ import io.swagger.v3.oas.annotations.tags.Tag;
9
+ import jakarta.validation.Valid;
10
+ import lombok.RequiredArgsConstructor;
11
+ import org.springframework.data.domain.Pageable;
12
+ import org.springframework.data.web.PageableDefault;
13
+ import org.springframework.http.HttpStatus;
14
+ import org.springframework.http.ResponseEntity;
15
+ import org.springframework.security.access.prepost.PreAuthorize;
16
+ import org.springframework.web.bind.annotation.*;
17
+
18
+ @RestController
19
+ @RequestMapping("/api/v1/deletion/tasks")
20
+ @Tag(name = "Deletion Task API", description = "APIs for managing auto-deletion tasks")
21
+ @RequiredArgsConstructor
22
+ public class DeletionTaskController {
23
+
24
+ private final IDeletionTaskService deletionTaskService;
25
+
26
+ @Operation(summary = "Submit a new deletion task",
27
+ responses = {
28
+ @ApiResponse(responseCode = "202", description = "Task accepted for processing or approval"),
29
+ @ApiResponse(responseCode = "400", description = "Invalid request payload or validation error")
30
+ })
31
+ @PostMapping
32
+ @PreAuthorize("hasAnyRole('ADMIN', 'DATA_STEWARD', 'AUTOMATION_SERVICE')") // AUTOMATION_SERVICE for system-triggered tasks
33
+ public ResponseEntity<DeletionTaskResponse> submitDeletionTask(@Valid @RequestBody DeletionTaskRequest request) {
34
+ DeletionTaskResponse response = deletionTaskService.submitDeletionTask(request);
35
+ if ("FAILED_VALIDATION".equals(response.getStatus())) {
36
+ return ResponseEntity.badRequest().body(response);
37
+ }
38
+ return ResponseEntity.status(HttpStatus.ACCEPTED).body(response);
39
+ }
40
+
41
+ @Operation(summary = "Get deletion task status by ID",
42
+ responses = {
43
+ @ApiResponse(responseCode = "200", description = "Successfully retrieved task status"),
44
+ @ApiResponse(responseCode = "404", description = "Task not found")
45
+ })
46
+ @GetMapping("/{taskId}")
47
+ @PreAuthorize("hasAnyRole('ADMIN', 'DATA_STEWARD', 'USER', 'AUTOMATION_SERVICE')") // USER can see their own tasks, need filtering logic in service
48
+ public ResponseEntity<DeletionTaskStatusDTO> getDeletionTaskStatus(@Parameter(description = "ID of the deletion task") @PathVariable String taskId) {
49
+ DeletionTaskStatusDTO status = deletionTaskService.getTaskStatus(taskId);
50
+ if (status == null) {
51
+ return ResponseEntity.notFound().build();
52
+ }
53
+ // TODO: Add logic to check if the authenticated user is allowed to see this task (e.g., if they triggered it)
54
+ return ResponseEntity.ok(status);
55
+ }
56
+
57
+ @Operation(summary = "List all deletion tasks",
58
+ description = "Lists deletion tasks with pagination. ADMINs/DATA_STEWARDS see all, others might see a filtered list.")
59
+ @GetMapping
60
+ @PreAuthorize("hasAnyRole('ADMIN', 'DATA_STEWARD', 'USER', 'AUTOMATION_SERVICE')")
61
+ public ResponseEntity<DeletionTaskListResponse> listDeletionTasks(@PageableDefault(size = 20) Pageable pageable) {
62
+ // TODO: Service layer should handle filtering based on user role (e.g., non-admins see only their tasks)
63
+ DeletionTaskListResponse response = deletionTaskService.listTasks(pageable);
64
+ return ResponseEntity.ok(response);
65
+ }
66
+
67
+ @Operation(summary = "Approve a pending deletion task",
68
+ description = "Only users with ADMIN or DATA_STEWARD role can approve tasks.",
69
+ responses = {
70
+ @ApiResponse(responseCode = "200", description = "Task approved successfully"),
71
+ @ApiResponse(responseCode = "404", description = "Task not found"),
72
+ @ApiResponse(responseCode = "409", description = "Task is not in a state that can be approved")
73
+ })
74
+ @PostMapping("/{taskId}/approve")
75
+ @PreAuthorize("hasAnyRole('ADMIN', 'DATA_STEWARD')")
76
+ public ResponseEntity<DeletionTaskStatusDTO> approveDeletionTask(
77
+ @Parameter(description = "ID of the task to approve") @PathVariable String taskId,
78
+ @RequestBody(required = false) TaskApprovalRequest approvalRequest) {
79
+ DeletionTaskStatusDTO status = deletionTaskService.approveTask(taskId, approvalRequest);
80
+ if (status == null) { // Should be handled by an exception in a real service normally
81
+ return ResponseEntity.notFound().build();
82
+ }
83
+ if (status.getErrorMessages() != null && !status.getErrorMessages().isEmpty()) {
84
+ return ResponseEntity.status(HttpStatus.CONFLICT).body(status);
85
+ }
86
+ return ResponseEntity.ok(status);
87
+ }
88
+
89
+ @Operation(summary = "Reject a pending deletion task",
90
+ description = "Only users with ADMIN or DATA_STEWARD role can reject tasks.",
91
+ responses = {
92
+ @ApiResponse(responseCode = "200", description = "Task rejected successfully"),
93
+ @ApiResponse(responseCode = "404", description = "Task not found"),
94
+ @ApiResponse(responseCode = "409", description = "Task is not in a state that can be rejected")
95
+ })
96
+ @PostMapping("/{taskId}/reject")
97
+ @PreAuthorize("hasAnyRole('ADMIN', 'DATA_STEWARD')")
98
+ public ResponseEntity<DeletionTaskStatusDTO> rejectDeletionTask(
99
+ @Parameter(description = "ID of the task to reject") @PathVariable String taskId,
100
+ @RequestBody(required = false) TaskApprovalRequest rejectionRequest) {
101
+ DeletionTaskStatusDTO status = deletionTaskService.rejectTask(taskId, rejectionRequest);
102
+ if (status == null) { // Should be handled by an exception
103
+ return ResponseEntity.notFound().build();
104
+ }
105
+ if (status.getErrorMessages() != null && !status.getErrorMessages().isEmpty()) {
106
+ return ResponseEntity.status(HttpStatus.CONFLICT).body(status);
107
+ }
108
+ return ResponseEntity.ok(status);
109
+ }
110
+ }
src/main/java/com/dalab/autodelete/dto/DeletionConfigDTO.java ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autodelete.dto;
2
+
3
+ import lombok.Builder;
4
+ import lombok.Data;
5
+ import lombok.NoArgsConstructor;
6
+ import lombok.AllArgsConstructor;
7
+
8
+ import java.util.List;
9
+ import java.util.Map;
10
+
11
+ /**
12
+ * DTO for representing the auto-deletion configuration.
13
+ */
14
+ @Data
15
+ @Builder
16
+ @NoArgsConstructor
17
+ @AllArgsConstructor
18
+ public class DeletionConfigDTO {
19
+
20
+ private Boolean enabled; // Global enable/disable for auto-deletion
21
+ private boolean softDeleteByDefault; // If true, performs a soft delete (marks as deleted)
22
+ private Long defaultSoftDeleteRetentionDays; // If soft-deleted, how long before permanent deletion (if applicable)
23
+ private boolean requireApprovalForHardDelete; // If manual approval is needed for permanent (hard) deletion
24
+ private boolean requireApprovalForSoftDelete; // If manual approval is needed for soft deletion
25
+
26
+ // Could define specific deletion strategies or grace periods per data source type or classification
27
+ // For example: Map<String, DeletionStrategy> deletionStrategies;
28
+ // DeletionStrategy could include things like: immediate, after X days, notify and wait, etc.
29
+ private List<String> notificationEmailsOnError; // Who to notify if deletion task fails critically
30
+ }
src/main/java/com/dalab/autodelete/dto/DeletionTaskListResponse.java ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autodelete.dto;
2
+
3
+ import lombok.Builder;
4
+ import lombok.Data;
5
+ import lombok.NoArgsConstructor;
6
+ import lombok.AllArgsConstructor;
7
+ import org.springframework.data.domain.Page;
8
+
9
+ import java.util.List;
10
+
11
+ /**
12
+ * DTO for a paginated list of deletion task statuses.
13
+ */
14
+ @Data
15
+ @Builder
16
+ @NoArgsConstructor
17
+ @AllArgsConstructor
18
+ public class DeletionTaskListResponse {
19
+ private List<DeletionTaskStatusDTO> tasks;
20
+ private int pageNumber;
21
+ private int pageSize;
22
+ private long totalElements;
23
+ private int totalPages;
24
+ private boolean first;
25
+ private boolean last;
26
+ private int numberOfElements;
27
+
28
+ public static DeletionTaskListResponse fromPage(Page<DeletionTaskStatusDTO> page) {
29
+ return DeletionTaskListResponse.builder()
30
+ .tasks(page.getContent())
31
+ .pageNumber(page.getNumber())
32
+ .pageSize(page.getSize())
33
+ .totalElements(page.getTotalElements())
34
+ .totalPages(page.getTotalPages())
35
+ .first(page.isFirst())
36
+ .last(page.isLast())
37
+ .numberOfElements(page.getNumberOfElements())
38
+ .build();
39
+ }
40
+ }
src/main/java/com/dalab/autodelete/dto/DeletionTaskRequest.java ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autodelete.dto;
2
+
3
+ import lombok.Builder;
4
+ import lombok.Data;
5
+ import lombok.NoArgsConstructor;
6
+ import lombok.AllArgsConstructor;
7
+
8
+ import jakarta.validation.constraints.NotEmpty;
9
+ import jakarta.validation.constraints.NotNull;
10
+ import jakarta.validation.constraints.Size;
11
+ import java.util.List;
12
+
13
+ /**
14
+ * DTO for requesting a new deletion task.
15
+ */
16
+ @Data
17
+ @Builder
18
+ @NoArgsConstructor
19
+ @AllArgsConstructor
20
+ public class DeletionTaskRequest {
21
+
22
+ @Size(max = 255, message = "Task name cannot exceed 255 characters.")
23
+ private String taskName; // Optional friendly name for the task
24
+
25
+ @NotNull(message = "Deletion scope cannot be null.")
26
+ private DeletionScope scope;
27
+
28
+ private String triggeredBy; // User or system that triggered the task
29
+ private String justification; // Reason for deletion
30
+
31
+ private DeletionConfigOverride overrideConfig; // Optional config overrides for this specific task
32
+
33
+ /**
34
+ * Defines the scope of assets to be deleted.
35
+ */
36
+ @Data
37
+ @Builder
38
+ @NoArgsConstructor
39
+ @AllArgsConstructor
40
+ public static class DeletionScope {
41
+ @NotEmpty(message = "At least one asset ID must be provided for deletion.")
42
+ private List<String> assetIds; // List of asset IDs to delete
43
+ // Could also include criteria like tags, creation date ranges, etc.
44
+ }
45
+
46
+ /**
47
+ * Allows overriding global deletion settings for this specific task.
48
+ */
49
+ @Data
50
+ @Builder
51
+ @NoArgsConstructor
52
+ @AllArgsConstructor
53
+ public static class DeletionConfigOverride {
54
+ private Boolean softDelete; // Override global soft-delete setting
55
+ private Long softDeleteRetentionDays; // Override retention if soft-deleting
56
+ private Boolean requireApproval; // Override approval requirement for this task
57
+ }
58
+ }
src/main/java/com/dalab/autodelete/dto/DeletionTaskResponse.java ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autodelete.dto;
2
+
3
+ import lombok.Builder;
4
+ import lombok.Data;
5
+ import lombok.NoArgsConstructor;
6
+ import lombok.AllArgsConstructor;
7
+
8
+ import java.time.LocalDateTime;
9
+
10
+ /**
11
+ * DTO representing the response after submitting a deletion task.
12
+ */
13
+ @Data
14
+ @Builder
15
+ @NoArgsConstructor
16
+ @AllArgsConstructor
17
+ public class DeletionTaskResponse {
18
+ private String taskId;
19
+ private String taskName;
20
+ private String status; // e.g., PENDING_APPROVAL, SUBMITTED, FAILED_VALIDATION
21
+ private LocalDateTime submittedAt;
22
+ private String message; // Any relevant message, e.g., reason for validation failure
23
+ }
src/main/java/com/dalab/autodelete/dto/DeletionTaskStatusDTO.java ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autodelete.dto;
2
+
3
+ import lombok.Builder;
4
+ import lombok.Data;
5
+ import lombok.NoArgsConstructor;
6
+ import lombok.AllArgsConstructor;
7
+
8
+ import java.time.LocalDateTime;
9
+ import java.util.List;
10
+
11
+ /**
12
+ * DTO for representing the detailed status of a deletion task.
13
+ */
14
+ @Data
15
+ @Builder
16
+ @NoArgsConstructor
17
+ @AllArgsConstructor
18
+ public class DeletionTaskStatusDTO {
19
+ private String taskId;
20
+ private String taskName;
21
+ private String status; // PENDING_APPROVAL, APPROVED, REJECTED, SUBMITTED, IN_PROGRESS, COMPLETED, FAILED, PARTIALLY_COMPLETED
22
+ private LocalDateTime submittedAt;
23
+ private LocalDateTime startedAt;
24
+ private LocalDateTime completedAt;
25
+ private String triggeredBy;
26
+ private String justification;
27
+
28
+ private DeletionTaskRequest.DeletionScope scope; // Original scope
29
+ private DeletionTaskRequest.DeletionConfigOverride overrideConfig; // Applied overrides
30
+
31
+ private int totalAssetsInScope;
32
+ private int assetsProcessedSuccessfully;
33
+ private int assetsFailedToProcess;
34
+ private int assetsPendingProcessing;
35
+
36
+ // Could add a list of individual asset statuses if needed for detailed view
37
+ // private List<AssetDeletionStatus> assetStatuses;
38
+
39
+ private String approvalComments;
40
+ private String rejectionComments;
41
+ private List<String> errorMessages;
42
+ }
src/main/java/com/dalab/autodelete/dto/TaskApprovalRequest.java ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autodelete.dto;
2
+
3
+ import lombok.Builder;
4
+ import lombok.Data;
5
+ import lombok.NoArgsConstructor;
6
+ import lombok.AllArgsConstructor;
7
+
8
+ import jakarta.validation.constraints.Size;
9
+
10
+ /**
11
+ * DTO for providing comments during task approval or rejection.
12
+ */
13
+ @Data
14
+ @Builder
15
+ @NoArgsConstructor
16
+ @AllArgsConstructor
17
+ public class TaskApprovalRequest {
18
+ @Size(max = 1000, message = "Comments cannot exceed 1000 characters.")
19
+ private String comments;
20
+ }
src/main/java/com/dalab/autodelete/exception/DeletionException.java ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autodelete.exception;
2
+
3
+ /**
4
+ * Custom exception for errors occurring during deletion operations.
5
+ */
6
+ public class DeletionException extends Exception {
7
+
8
+ public DeletionException(String message) {
9
+ super(message);
10
+ }
11
+
12
+ public DeletionException(String message, Throwable cause) {
13
+ super(message, cause);
14
+ }
15
+ }
src/main/java/com/dalab/autodelete/mapper/DeletionConfigMapper.java ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autodelete.mapper;
2
+
3
+ import com.dalab.autodelete.dto.DeletionConfigDTO;
4
+ import com.dalab.autodelete.model.DeletionConfigEntity;
5
+ import org.mapstruct.Mapper;
6
+ import org.mapstruct.MappingTarget;
7
+ import org.mapstruct.NullValuePropertyMappingStrategy;
8
+ import org.mapstruct.factory.Mappers;
9
+
10
+ @Mapper(componentModel = "spring",
11
+ nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
12
+ public interface DeletionConfigMapper {
13
+
14
+ DeletionConfigMapper INSTANCE = Mappers.getMapper(DeletionConfigMapper.class);
15
+
16
+ DeletionConfigDTO toDto(DeletionConfigEntity entity);
17
+
18
+ DeletionConfigEntity toEntity(DeletionConfigDTO dto);
19
+
20
+ void updateEntityFromDto(DeletionConfigDTO dto, @MappingTarget DeletionConfigEntity entity);
21
+ }
src/main/java/com/dalab/autodelete/mapper/DeletionTaskMapper.java ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autodelete.mapper;
2
+
3
+ import com.dalab.autodelete.dto.DeletionTaskRequest;
4
+ import com.dalab.autodelete.dto.DeletionTaskResponse;
5
+ import com.dalab.autodelete.dto.DeletionTaskStatusDTO;
6
+ import com.dalab.autodelete.model.DeletionTaskEntity;
7
+ import org.mapstruct.Mapper;
8
+ import org.mapstruct.Mapping;
9
+ import org.mapstruct.NullValuePropertyMappingStrategy;
10
+ import org.mapstruct.factory.Mappers;
11
+
12
+ @Mapper(componentModel = "spring",
13
+ nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
14
+ public interface DeletionTaskMapper {
15
+
16
+ DeletionTaskMapper INSTANCE = Mappers.getMapper(DeletionTaskMapper.class);
17
+
18
+ // From Request DTO to Entity
19
+ @Mapping(target = "taskId", ignore = true) // taskId is generated in service
20
+ @Mapping(target = "status", ignore = true) // status is set in service
21
+ @Mapping(target = "submittedAt", ignore = true)
22
+ @Mapping(target = "startedAt", ignore = true)
23
+ @Mapping(target = "completedAt", ignore = true)
24
+ @Mapping(target = "totalAssetsInScope", ignore = true)
25
+ @Mapping(target = "assetsProcessedSuccessfully", ignore = true)
26
+ @Mapping(target = "assetsFailedToProcess", ignore = true)
27
+ @Mapping(target = "assetsPendingProcessing", ignore = true)
28
+ @Mapping(target = "assetStatuses", ignore = true)
29
+ @Mapping(target = "errorMessages", ignore = true)
30
+ @Mapping(target = "approvalComments", ignore = true)
31
+ @Mapping(target = "rejectionComments", ignore = true)
32
+ DeletionTaskEntity requestToEntity(DeletionTaskRequest requestDto);
33
+
34
+ // From Entity to Response DTO (simple initial response)
35
+ DeletionTaskResponse entityToTaskResponse(DeletionTaskEntity entity);
36
+
37
+ // From Entity to Status DTO (detailed status)
38
+ DeletionTaskStatusDTO entityToStatusDTO(DeletionTaskEntity entity);
39
+
40
+ // For mapping nested DTOs to nested Entity data classes (if not automatically handled by MapStruct)
41
+ DeletionTaskEntity.DeletionScopeData scopeDtoToData(DeletionTaskRequest.DeletionScope scopeDto);
42
+ DeletionTaskRequest.DeletionScope scopeDataToDto(DeletionTaskEntity.DeletionScopeData scopeData);
43
+
44
+ DeletionTaskEntity.DeletionConfigOverrideData overrideDtoToData(DeletionTaskRequest.DeletionConfigOverride overrideDto);
45
+ DeletionTaskRequest.DeletionConfigOverride overrideDataToDto(DeletionTaskEntity.DeletionConfigOverrideData overrideData);
46
+
47
+ // If AssetDeletionStatusData needs explicit mapping (often MapStruct handles lists of identical beans)
48
+ // List<DeletionTaskEntity.AssetDeletionStatusData> assetStatusDtoListToDataList(List<AssetDeletionStatusDTO> dtoList);
49
+ // List<AssetDeletionStatusDTO> assetStatusDataListToDtoList(List<DeletionTaskEntity.AssetDeletionStatusData> dataList);
50
+ }
src/main/java/com/dalab/autodelete/model/DeletionConfigEntity.java ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autodelete.model;
2
+
3
+ import jakarta.persistence.*;
4
+ import lombok.Data;
5
+ import lombok.NoArgsConstructor;
6
+ import lombok.AllArgsConstructor;
7
+ // import org.hibernate.annotations.JdbcTypeCode; // Not needed if not using complex JSON types directly here
8
+ // import org.hibernate.type.SqlTypes;
9
+
10
+ import java.util.List;
11
+
12
+ @Entity
13
+ @Table(name = "dalab_deletion_config")
14
+ @Data
15
+ @NoArgsConstructor
16
+ @AllArgsConstructor
17
+ public class DeletionConfigEntity {
18
+
19
+ @Id
20
+ private Long id; // Use a fixed ID, e.g., 1L, as it's a global config
21
+
22
+ private boolean enabled;
23
+ private boolean softDeleteByDefault;
24
+ private Long defaultSoftDeleteRetentionDays;
25
+ private boolean requireApprovalForHardDelete;
26
+ private boolean requireApprovalForSoftDelete;
27
+
28
+ @ElementCollection(fetch = FetchType.EAGER) // Store as a collection of strings
29
+ @CollectionTable(name = "dalab_deletion_config_notification_emails", joinColumns = @JoinColumn(name = "config_id"))
30
+ @Column(name = "email")
31
+ private List<String> notificationEmailsOnError;
32
+
33
+ // If you add more complex configurations like per-provider or per-tag rules that are better stored as JSON,
34
+ // you can use @JdbcTypeCode(SqlTypes.JSON) similar to da-autoarchival's ProviderConfigData.
35
+ // For example:
36
+ // @JdbcTypeCode(SqlTypes.JSON)
37
+ // @Column(columnDefinition = "jsonb")
38
+ // private Map<String, DeletionStrategyData> deletionStrategies;
39
+ // public static class DeletionStrategyData { /* fields */ }
40
+ }
src/main/java/com/dalab/autodelete/model/DeletionResult.java ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autodelete.model;
2
+
3
+ import java.time.OffsetDateTime;
4
+
5
+ public class DeletionResult {
6
+
7
+ private boolean success;
8
+ private String message; // e.g., "Object deleted successfully" or error details
9
+ private OffsetDateTime completionTimestamp;
10
+ private long itemsDeleted; // Useful for folder deletions
11
+
12
+ public DeletionResult(boolean success, String message, long itemsDeleted) {
13
+ this.success = success;
14
+ this.message = message;
15
+ this.itemsDeleted = itemsDeleted;
16
+ this.completionTimestamp = OffsetDateTime.now();
17
+ }
18
+
19
+ // Getters and Setters
20
+ public boolean isSuccess() { return success; }
21
+ public void setSuccess(boolean success) { this.success = success; }
22
+ public String getMessage() { return message; }
23
+ public void setMessage(String message) { this.message = message; }
24
+ public OffsetDateTime getCompletionTimestamp() { return completionTimestamp; }
25
+ public void setCompletionTimestamp(OffsetDateTime completionTimestamp) { this.completionTimestamp = completionTimestamp; }
26
+ public long getItemsDeleted() { return itemsDeleted; }
27
+ public void setItemsDeleted(long itemsDeleted) { this.itemsDeleted = itemsDeleted; }
28
+ }
src/main/java/com/dalab/autodelete/model/DeletionStatus.java ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autodelete.model;
2
+
3
+ public enum DeletionStatus {
4
+ PENDING_APPROVAL, // Task is awaiting approval before execution
5
+ APPROVED, // Task has been approved and is ready for processing
6
+ REJECTED, // Task has been rejected and will not be processed
7
+ SUBMITTED, // Task submitted directly (no approval needed) or post-approval, ready for queue
8
+ QUEUED, // Task is in the processing queue
9
+ IN_PROGRESS, // Task execution is actively in progress
10
+ COMPLETED, // Task completed successfully (all assets processed)
11
+ PARTIALLY_COMPLETED, // Task completed, but some assets failed to delete
12
+ FAILED, // Task failed entirely or due to a critical error
13
+ CANCELLED // Task was cancelled before completion
14
+ }
src/main/java/com/dalab/autodelete/model/DeletionTask.java ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autodelete.model;
2
+
3
+ import java.time.OffsetDateTime;
4
+ import java.util.Map;
5
+
6
+ public class DeletionTask {
7
+
8
+ private String taskId;
9
+ private String assetId; // ID of the asset in the catalog that this deletion task refers to
10
+ private String gcpProjectId; // GCP Project ID where the resource resides
11
+ private GcpResourceType resourceType; // GCS_OBJECT, GCS_FOLDER, BIGQUERY_TABLE
12
+
13
+ // For GCS
14
+ private String gcsBucketName;
15
+ private String gcsObjectPath; // Full path for object, or prefix for folder
16
+
17
+ // For BigQuery
18
+ private String bqDatasetId;
19
+ private String bqTableId;
20
+
21
+ private DeletionStatus status;
22
+ private OffsetDateTime lastAttemptedAt;
23
+ private OffsetDateTime completedAt;
24
+ private String failureReason;
25
+
26
+ // Configuration specific to this deletion operation, if any (e.g., force delete)
27
+ private Map<String, String> operationConfig;
28
+
29
+
30
+ public enum GcpResourceType {
31
+ GCS_BUCKET,
32
+ GCS_OBJECT,
33
+ GCS_FOLDER,
34
+ BIGQUERY_DATASET,
35
+ BIGQUERY_TABLE
36
+ }
37
+
38
+ public enum DeletionStatus {
39
+ PENDING,
40
+ IN_PROGRESS,
41
+ COMPLETED,
42
+ FAILED,
43
+ REQUIRES_APPROVAL
44
+ }
45
+
46
+ // Constructors, Getters, Setters
47
+ public DeletionTask(String taskId, String assetId) {
48
+ this.taskId = taskId;
49
+ this.assetId = assetId;
50
+ }
51
+
52
+ public String getTaskId() { return taskId; }
53
+ public void setTaskId(String taskId) { this.taskId = taskId; }
54
+ public String getAssetId() { return assetId; }
55
+ public void setAssetId(String assetId) { this.assetId = assetId; }
56
+ public String getGcpProjectId() { return gcpProjectId; }
57
+ public void setGcpProjectId(String gcpProjectId) { this.gcpProjectId = gcpProjectId; }
58
+ public GcpResourceType getResourceType() { return resourceType; }
59
+ public void setResourceType(GcpResourceType resourceType) { this.resourceType = resourceType; }
60
+ public String getGcsBucketName() { return gcsBucketName; }
61
+ public void setGcsBucketName(String gcsBucketName) { this.gcsBucketName = gcsBucketName; }
62
+ public String getGcsObjectPath() { return gcsObjectPath; }
63
+ public void setGcsObjectPath(String gcsObjectPath) { this.gcsObjectPath = gcsObjectPath; }
64
+ public String getBqDatasetId() { return bqDatasetId; }
65
+ public void setBqDatasetId(String bqDatasetId) { this.bqDatasetId = bqDatasetId; }
66
+ public String getBqTableId() { return bqTableId; }
67
+ public void setBqTableId(String bqTableId) { this.bqTableId = bqTableId; }
68
+ public DeletionStatus getStatus() { return status; }
69
+ public void setStatus(DeletionStatus status) { this.status = status; }
70
+ public Map<String, String> getOperationConfig() { return operationConfig; }
71
+ public void setOperationConfig(Map<String, String> operationConfig) { this.operationConfig = operationConfig; }
72
+ public OffsetDateTime getLastAttemptedAt() { return lastAttemptedAt; }
73
+ public void setLastAttemptedAt(OffsetDateTime lastAttemptedAt) { this.lastAttemptedAt = lastAttemptedAt; }
74
+ public OffsetDateTime getCompletedAt() { return completedAt; }
75
+ public void setCompletedAt(OffsetDateTime completedAt) { this.completedAt = completedAt; }
76
+ public String getFailureReason() { return failureReason; }
77
+ public void setFailureReason(String failureReason) { this.failureReason = failureReason; }
78
+ }
src/main/java/com/dalab/autodelete/model/DeletionTaskEntity.java ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autodelete.model;
2
+
3
+ import jakarta.persistence.*;
4
+ import lombok.Data;
5
+ import lombok.NoArgsConstructor;
6
+ import lombok.AllArgsConstructor;
7
+ import org.hibernate.annotations.JdbcTypeCode;
8
+ import org.hibernate.type.SqlTypes;
9
+
10
+ import java.time.LocalDateTime;
11
+ import java.util.List;
12
+ // import java.util.Map; // If using Map for complex JSON fields
13
+
14
+ @Entity
15
+ @Table(name = "dalab_deletion_tasks")
16
+ @Data
17
+ @NoArgsConstructor
18
+ @AllArgsConstructor
19
+ public class DeletionTaskEntity {
20
+
21
+ @Id
22
+ private String taskId;
23
+
24
+ private String taskName;
25
+
26
+ @Enumerated(EnumType.STRING)
27
+ private DeletionStatus status;
28
+
29
+ private LocalDateTime submittedAt;
30
+ private LocalDateTime startedAt;
31
+ private LocalDateTime completedAt;
32
+
33
+ private String triggeredBy;
34
+ private String justification;
35
+
36
+ @JdbcTypeCode(SqlTypes.JSON)
37
+ @Column(columnDefinition = "jsonb")
38
+ private DeletionScopeData scope; // Store as JSON
39
+
40
+ @JdbcTypeCode(SqlTypes.JSON)
41
+ @Column(columnDefinition = "jsonb")
42
+ private DeletionConfigOverrideData overrideConfig; // Store as JSON
43
+
44
+ private int totalAssetsInScope;
45
+ private int assetsProcessedSuccessfully;
46
+ private int assetsFailedToProcess;
47
+ private int assetsPendingProcessing;
48
+
49
+ @JdbcTypeCode(SqlTypes.JSON)
50
+ @Column(columnDefinition = "jsonb")
51
+ private List<AssetDeletionStatusData> assetStatuses; // Store detailed asset status as JSON
52
+
53
+ @JdbcTypeCode(SqlTypes.JSON)
54
+ @Column(columnDefinition = "jsonb")
55
+ private List<String> errorMessages; // Store task-level error messages as JSON
56
+
57
+ private String approvalComments;
58
+ private String rejectionComments;
59
+
60
+ // Static inner classes for JSONB data structures
61
+ @Data @NoArgsConstructor @AllArgsConstructor
62
+ public static class DeletionScopeData {
63
+ private List<String> assetIds;
64
+ // Potentially add other criteria here like tags, patterns etc.
65
+ }
66
+
67
+ @Data @NoArgsConstructor @AllArgsConstructor
68
+ public static class DeletionConfigOverrideData {
69
+ private Boolean softDelete;
70
+ private Long softDeleteRetentionDays;
71
+ private Boolean requireApproval;
72
+ }
73
+
74
+ @Data @NoArgsConstructor @AllArgsConstructor
75
+ public static class AssetDeletionStatusData {
76
+ private String assetId;
77
+ private String assetName; // Optional, for easier reporting
78
+ private String status; // e.g., PENDING, SOFT_DELETED, HARD_DELETED, FAILED
79
+ private String deletionType; // SOFT, HARD
80
+ private String errorMessage;
81
+ private LocalDateTime processedAt;
82
+ }
83
+ }
src/main/java/com/dalab/autodelete/provider/GcpDeletionProvider.java ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autodelete.provider;
2
+
3
+ import java.util.Map;
4
+
5
+ import org.springframework.stereotype.Component;
6
+
7
+ import com.dalab.autodelete.exception.DeletionException;
8
+ import com.dalab.autodelete.model.DeletionResult;
9
+ import com.dalab.autodelete.model.DeletionTask;
10
+
11
+ @Component // Or configure as a bean
12
+ public class GcpDeletionProvider implements IGcpDeletionProvider {
13
+
14
+ private static final String NOT_IMPLEMENTED_MESSAGE = "GCP deletion feature is not yet implemented for this resource type.";
15
+
16
+ @Override
17
+ public DeletionResult deleteGcsObject(DeletionTask task, Map<String, String> providerConfig) throws DeletionException {
18
+ // TODO: Implement GCS object deletion
19
+ // 1. Get GCS client (com.google.cloud.storage.Storage)
20
+ // 2. Use task.getGcsBucketName(), task.getGcsObjectPath()
21
+ // 3. Call storage.delete(bucketName, objectPath)
22
+ // 4. Return DeletionResult
23
+ throw new DeletionException(String.format("GCS object deletion for '%s/%s' not yet implemented.", task.getGcsBucketName(), task.getGcsObjectPath()));
24
+ }
25
+
26
+ @Override
27
+ public DeletionResult deleteGcsFolder(DeletionTask task, Map<String, String> providerConfig) throws DeletionException {
28
+ // TODO: Implement GCS folder deletion (iterating and deleting objects with prefix)
29
+ // 1. Get GCS client
30
+ // 2. List objects with prefix task.getGcsObjectPath() in bucket task.getGcsBucketName()
31
+ // 3. Delete each object found
32
+ // 4. Return DeletionResult with count of items deleted
33
+ throw new DeletionException(String.format("GCS folder deletion for prefix '%s' in bucket '%s' not yet implemented.", task.getGcsObjectPath(), task.getGcsBucketName()));
34
+ }
35
+
36
+ @Override
37
+ public DeletionResult deleteBigQueryTable(DeletionTask task, Map<String, String> providerConfig) throws DeletionException {
38
+ // TODO: Implement BigQuery table deletion
39
+ // 1. Get BigQuery client (com.google.cloud.bigquery.BigQuery)
40
+ // 2. Use task.getGcpProjectId(), task.getBqDatasetId(), task.getBqTableId()
41
+ // 3. Call bigquery.delete(TableId.of(projectId, datasetId, tableId))
42
+ // 4. Return DeletionResult
43
+ throw new DeletionException(String.format("BigQuery table deletion for '%s:%s.%s' not yet implemented.", task.getGcpProjectId(), task.getBqDatasetId(), task.getBqTableId()));
44
+ }
45
+ }
src/main/java/com/dalab/autodelete/provider/IGcpDeletionProvider.java ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autodelete.provider;
2
+
3
+ import java.util.Map;
4
+
5
+ import com.dalab.autodelete.exception.DeletionException;
6
+ import com.dalab.autodelete.model.DeletionResult;
7
+ import com.dalab.autodelete.model.DeletionTask;
8
+
9
+ /**
10
+ * Interface for GCP-specific deletion operations.
11
+ */
12
+ public interface IGcpDeletionProvider {
13
+
14
+ String PROVIDER_TYPE = "GCP"; // Constant for provider type
15
+
16
+ /**
17
+ * Deletes a GCS object.
18
+ * @param task Details of the deletion task, including bucket and object path.
19
+ * @param providerConfig Provider-specific configurations (e.g., credentials, project if not in task).
20
+ * @return Result of the deletion operation.
21
+ * @throws DeletionException if an error occurs.
22
+ */
23
+ DeletionResult deleteGcsObject(DeletionTask task, Map<String, String> providerConfig) throws DeletionException;
24
+
25
+ /**
26
+ * Deletes a GCS "folder" (objects with a common prefix).
27
+ * @param task Details of the deletion task, including bucket and folder prefix.
28
+ * @param providerConfig Provider-specific configurations.
29
+ * @return Result of the deletion operation, potentially including count of deleted objects.
30
+ * @throws DeletionException if an error occurs.
31
+ */
32
+ DeletionResult deleteGcsFolder(DeletionTask task, Map<String, String> providerConfig) throws DeletionException;
33
+
34
+ /**
35
+ * Deletes a BigQuery table.
36
+ * @param task Details of the deletion task, including dataset and table ID.
37
+ * @param providerConfig Provider-specific configurations.
38
+ * @return Result of the deletion operation.
39
+ * @throws DeletionException if an error occurs.
40
+ */
41
+ DeletionResult deleteBigQueryTable(DeletionTask task, Map<String, String> providerConfig) throws DeletionException;
42
+
43
+ }
src/main/java/com/dalab/autodelete/repository/DeletionConfigRepository.java ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autodelete.repository;
2
+
3
+ import com.dalab.autodelete.model.DeletionConfigEntity;
4
+ import org.springframework.data.jpa.repository.JpaRepository;
5
+ import org.springframework.stereotype.Repository;
6
+
7
+ @Repository
8
+ public interface DeletionConfigRepository extends JpaRepository<DeletionConfigEntity, Long> {
9
+ // Global config with fixed ID (1L), specific find methods likely not needed.
10
+ // findById(1L) will be used.
11
+ }
src/main/java/com/dalab/autodelete/repository/DeletionTaskRepository.java ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autodelete.repository;
2
+
3
+ import com.dalab.autodelete.model.DeletionTaskEntity;
4
+ import org.springframework.data.jpa.repository.JpaRepository;
5
+ import org.springframework.stereotype.Repository;
6
+
7
+ @Repository
8
+ public interface DeletionTaskRepository extends JpaRepository<DeletionTaskEntity, String> {
9
+ // Custom query methods can be added here if needed, e.g.:
10
+ // List<DeletionTaskEntity> findByStatus(com.dalab.autodelete.model.DeletionStatus status);
11
+ // Page<DeletionTaskEntity> findByTriggeredBy(String triggeredBy, org.springframework.data.domain.Pageable pageable);
12
+ }
src/main/java/com/dalab/autodelete/service/IDeletionConfigService.java ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autodelete.service;
2
+
3
+ import com.dalab.autodelete.dto.DeletionConfigDTO;
4
+
5
+ public interface IDeletionConfigService {
6
+
7
+ /**
8
+ * Retrieves the current deletion configuration.
9
+ * @return The current DeletionConfigDTO.
10
+ */
11
+ DeletionConfigDTO getDeletionConfig();
12
+
13
+ /**
14
+ * Updates the deletion configuration.
15
+ * @param deletionConfigDTO The new configuration to apply.
16
+ */
17
+ void updateDeletionConfig(DeletionConfigDTO deletionConfigDTO);
18
+ }
src/main/java/com/dalab/autodelete/service/IDeletionTaskService.java ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autodelete.service;
2
+
3
+ import com.dalab.autodelete.dto.DeletionTaskRequest;
4
+ import com.dalab.autodelete.dto.DeletionTaskResponse;
5
+ import com.dalab.autodelete.dto.DeletionTaskStatusDTO;
6
+ import com.dalab.autodelete.dto.DeletionTaskListResponse;
7
+ import com.dalab.autodelete.dto.TaskApprovalRequest;
8
+ import org.springframework.data.domain.Pageable;
9
+
10
+ public interface IDeletionTaskService {
11
+
12
+ /**
13
+ * Submits a new task for asset deletion.
14
+ * @param request The deletion task request details.
15
+ * @return A response containing the task ID and initial status.
16
+ */
17
+ DeletionTaskResponse submitDeletionTask(DeletionTaskRequest request);
18
+
19
+ /**
20
+ * Retrieves the status of a specific deletion task.
21
+ * @param taskId The ID of the task.
22
+ * @return The status DTO of the task, or null if not found.
23
+ */
24
+ DeletionTaskStatusDTO getTaskStatus(String taskId);
25
+
26
+ /**
27
+ * Lists all deletion tasks with pagination.
28
+ * @param pageable Pagination information.
29
+ * @return A paginated list of task statuses.
30
+ */
31
+ DeletionTaskListResponse listTasks(Pageable pageable);
32
+
33
+ /**
34
+ * Approves a pending deletion task.
35
+ * @param taskId The ID of the task to approve.
36
+ * @param approvalRequest Details for the approval (e.g., comments).
37
+ * @return The updated status DTO of the task.
38
+ */
39
+ DeletionTaskStatusDTO approveTask(String taskId, TaskApprovalRequest approvalRequest);
40
+
41
+ /**
42
+ * Rejects a pending deletion task.
43
+ * @param taskId The ID of the task to reject.
44
+ * @param rejectionRequest Details for the rejection (e.g., comments).
45
+ * @return The updated status DTO of the task.
46
+ */
47
+ DeletionTaskStatusDTO rejectTask(String taskId, TaskApprovalRequest rejectionRequest);
48
+ }
src/main/java/com/dalab/autodelete/service/exception/DeletionTaskNotFoundException.java ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autodelete.service.exception;
2
+
3
+ import org.springframework.http.HttpStatus;
4
+ import org.springframework.web.bind.annotation.ResponseStatus;
5
+
6
+ @ResponseStatus(HttpStatus.NOT_FOUND)
7
+ public class DeletionTaskNotFoundException extends RuntimeException {
8
+ public DeletionTaskNotFoundException(String taskId, String operation) {
9
+ super(String.format("Deletion task with ID '%s' not found for operation: %s", taskId, operation));
10
+ }
11
+
12
+ public DeletionTaskNotFoundException(String taskId) {
13
+ super(String.format("Deletion task with ID '%s' not found.", taskId));
14
+ }
15
+ }
src/main/java/com/dalab/autodelete/service/impl/DeletionConfigServiceImpl.java ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autodelete.service.impl;
2
+
3
+ import com.dalab.autodelete.dto.DeletionConfigDTO;
4
+ import com.dalab.autodelete.mapper.DeletionConfigMapper;
5
+ import com.dalab.autodelete.model.DeletionConfigEntity;
6
+ import com.dalab.autodelete.repository.DeletionConfigRepository;
7
+ import com.dalab.autodelete.service.IDeletionConfigService;
8
+ import jakarta.annotation.PostConstruct;
9
+ import lombok.RequiredArgsConstructor;
10
+ import lombok.extern.slf4j.Slf4j;
11
+ import org.springframework.stereotype.Service;
12
+ import org.springframework.transaction.annotation.Transactional;
13
+
14
+ import java.util.ArrayList;
15
+ import java.util.Optional;
16
+
17
+ @Service("deletionConfigService") // Explicit bean name to distinguish from potential in-memory version
18
+ @RequiredArgsConstructor
19
+ @Slf4j
20
+ public class DeletionConfigServiceImpl implements IDeletionConfigService {
21
+
22
+ private final DeletionConfigRepository configRepository;
23
+ private final DeletionConfigMapper configMapper;
24
+ private static final Long GLOBAL_CONFIG_ID = 1L;
25
+
26
+ @PostConstruct
27
+ @Transactional
28
+ public void init() {
29
+ // Ensure a default config exists if the DB is empty for this service
30
+ if (!configRepository.existsById(GLOBAL_CONFIG_ID)) {
31
+ log.info("No global deletion configuration found. Initializing with default values.");
32
+ DeletionConfigEntity defaultConfig = new DeletionConfigEntity(
33
+ GLOBAL_CONFIG_ID,
34
+ false, // enabled
35
+ true, // softDeleteByDefault
36
+ 30L, // defaultSoftDeleteRetentionDays
37
+ true, // requireApprovalForHardDelete
38
+ false, // requireApprovalForSoftDelete
39
+ new ArrayList<>() // notificationEmailsOnError
40
+ );
41
+ configRepository.save(defaultConfig);
42
+ log.info("Default global deletion configuration saved with ID: {}", GLOBAL_CONFIG_ID);
43
+ } else {
44
+ log.info("Global deletion configuration already exists with ID: {}", GLOBAL_CONFIG_ID);
45
+ }
46
+ }
47
+
48
+ @Override
49
+ @Transactional(readOnly = true)
50
+ public DeletionConfigDTO getDeletionConfig() {
51
+ log.debug("Fetching global deletion configuration.");
52
+ Optional<DeletionConfigEntity> entityOptional = configRepository.findById(GLOBAL_CONFIG_ID);
53
+
54
+ return entityOptional.map(configMapper::toDto)
55
+ .orElseGet(() -> {
56
+ log.warn("Global deletion configuration (ID: {}) not found, returning empty DTO. This might indicate an issue if init() failed or was bypassed.", GLOBAL_CONFIG_ID);
57
+ // Consider if throwing an exception or returning a default non-persistent DTO is better here.
58
+ // For now, returning an empty DTO as per previous in-memory behavior for safety, but this is unusual for JPA.
59
+ return new DeletionConfigDTO();
60
+ });
61
+ }
62
+
63
+ @Override
64
+ @Transactional
65
+ public void updateDeletionConfig(DeletionConfigDTO deletionConfigDTO) {
66
+ if (deletionConfigDTO == null) {
67
+ log.warn("Attempted to update deletion configuration with null DTO.");
68
+ throw new IllegalArgumentException("Deletion configuration DTO cannot be null.");
69
+ }
70
+ log.info("Updating global deletion configuration.");
71
+ DeletionConfigEntity entity = configRepository.findById(GLOBAL_CONFIG_ID)
72
+ .orElseThrow(() -> {
73
+ log.error("Global deletion config (ID: {}) not found for update. This is unexpected post-initialization.", GLOBAL_CONFIG_ID);
74
+ // This state (config not found during update) should ideally not happen if PostConstruct works.
75
+ return new IllegalStateException("Global deletion configuration not found for update. Initialize first.");
76
+ });
77
+
78
+ configMapper.updateEntityFromDto(deletionConfigDTO, entity);
79
+ // Ensure ID is not accidentally changed by the mapper if DTO were to carry it
80
+ entity.setId(GLOBAL_CONFIG_ID);
81
+ configRepository.save(entity);
82
+ log.info("Global deletion configuration updated successfully for ID: {}.", GLOBAL_CONFIG_ID);
83
+ }
84
+ }
src/main/java/com/dalab/autodelete/service/impl/DeletionTaskServiceImpl.java ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autodelete.service.impl;
2
+
3
+ import com.dalab.autodelete.dto.*;
4
+ import com.dalab.autodelete.mapper.DeletionTaskMapper;
5
+ import com.dalab.autodelete.model.DeletionStatus;
6
+ import com.dalab.autodelete.model.DeletionTaskEntity;
7
+ import com.dalab.autodelete.repository.DeletionTaskRepository;
8
+ import com.dalab.autodelete.service.IDeletionConfigService;
9
+ import com.dalab.autodelete.service.IDeletionTaskService;
10
+ import com.dalab.autodelete.service.exception.DeletionTaskNotFoundException;
11
+ import lombok.RequiredArgsConstructor;
12
+ import lombok.extern.slf4j.Slf4j;
13
+ import org.springframework.data.domain.Page;
14
+ import org.springframework.data.domain.PageImpl;
15
+ import org.springframework.data.domain.Pageable;
16
+ import org.springframework.stereotype.Service;
17
+ import org.springframework.transaction.annotation.Transactional;
18
+
19
+ import java.time.LocalDateTime;
20
+ import java.util.Collections;
21
+ import java.util.List;
22
+ import java.util.UUID;
23
+ import java.util.stream.Collectors;
24
+
25
+ @Service("deletionTaskService") // Explicit bean name
26
+ @RequiredArgsConstructor
27
+ @Slf4j
28
+ public class DeletionTaskServiceImpl implements IDeletionTaskService {
29
+
30
+ private final DeletionTaskRepository taskRepository;
31
+ private final DeletionTaskMapper taskMapper;
32
+ private final IDeletionConfigService deletionConfigService; // JPA version
33
+
34
+ @Override
35
+ @Transactional
36
+ public DeletionTaskResponse submitDeletionTask(DeletionTaskRequest request) {
37
+ log.info("Submitting new deletion task: Name='{}', TriggeredBy='{}'", request.getTaskName(), request.getTriggeredBy());
38
+
39
+ if (request.getScope() == null || request.getScope().getAssetIds() == null || request.getScope().getAssetIds().isEmpty()) {
40
+ log.warn("Deletion task submission failed for task '{}': Asset IDs are missing.", request.getTaskName());
41
+ return DeletionTaskResponse.builder()
42
+ .taskId(null)
43
+ .taskName(request.getTaskName())
44
+ .status(DeletionStatus.FAILED.name()) // Using enum name for status consistency
45
+ .submittedAt(LocalDateTime.now())
46
+ .message("Deletion scope must contain at least one asset ID.")
47
+ .build();
48
+ }
49
+
50
+ DeletionTaskEntity entity = taskMapper.requestToEntity(request);
51
+ entity.setTaskId(UUID.randomUUID().toString());
52
+ entity.setSubmittedAt(LocalDateTime.now());
53
+
54
+ DeletionConfigDTO globalConfig = deletionConfigService.getDeletionConfig();
55
+ boolean isSoftDelete = determineIfSoftDelete(request, globalConfig);
56
+ boolean requiresApproval = determineApprovalRequirement(request, globalConfig, isSoftDelete);
57
+
58
+ entity.setStatus(requiresApproval ? DeletionStatus.PENDING_APPROVAL : DeletionStatus.SUBMITTED);
59
+
60
+ // Initialize asset statuses if scope is present
61
+ if (request.getScope() != null && request.getScope().getAssetIds() != null) {
62
+ List<DeletionTaskEntity.AssetDeletionStatusData> initialAssetStatuses = request.getScope().getAssetIds().stream()
63
+ .map(assetId -> new DeletionTaskEntity.AssetDeletionStatusData(assetId, null, "PENDING", isSoftDelete ? "SOFT" : "HARD", null, null))
64
+ .collect(Collectors.toList());
65
+ entity.setAssetStatuses(initialAssetStatuses);
66
+ entity.setTotalAssetsInScope(initialAssetStatuses.size());
67
+ entity.setAssetsPendingProcessing(initialAssetStatuses.size());
68
+ entity.setAssetsFailedToProcess(0);
69
+ entity.setAssetsProcessedSuccessfully(0);
70
+ }
71
+
72
+ DeletionTaskEntity savedEntity = taskRepository.save(entity);
73
+ log.info("Deletion task '{}' submitted with ID {} and status {}.", savedEntity.getTaskName(), savedEntity.getTaskId(), savedEntity.getStatus());
74
+ return taskMapper.entityToTaskResponse(savedEntity);
75
+ }
76
+
77
+ private boolean determineIfSoftDelete(DeletionTaskRequest request, DeletionConfigDTO globalConfig) {
78
+ if (request.getOverrideConfig() != null && request.getOverrideConfig().getSoftDelete() != null) {
79
+ return request.getOverrideConfig().getSoftDelete();
80
+ }
81
+ return globalConfig.isSoftDeleteByDefault();
82
+ }
83
+
84
+ private boolean determineApprovalRequirement(DeletionTaskRequest request, DeletionConfigDTO globalConfig, boolean isSoftDelete) {
85
+ if (request.getOverrideConfig() != null && request.getOverrideConfig().getRequireApproval() != null) {
86
+ return request.getOverrideConfig().getRequireApproval();
87
+ }
88
+ return isSoftDelete ? globalConfig.isRequireApprovalForSoftDelete() : globalConfig.isRequireApprovalForHardDelete();
89
+ }
90
+
91
+ @Override
92
+ @Transactional(readOnly = true)
93
+ public DeletionTaskStatusDTO getTaskStatus(String taskId) {
94
+ log.debug("Fetching status for deletion task ID: {}", taskId);
95
+ return taskRepository.findById(taskId)
96
+ .map(taskMapper::entityToStatusDTO)
97
+ .orElse(null); // Controller handles null by returning 404
98
+ }
99
+
100
+ @Override
101
+ @Transactional(readOnly = true)
102
+ public DeletionTaskListResponse listTasks(Pageable pageable) {
103
+ log.debug("Listing deletion tasks with pagination: {}", pageable);
104
+ Page<DeletionTaskEntity> taskPage = taskRepository.findAll(pageable);
105
+ List<DeletionTaskStatusDTO> dtoList = taskPage.getContent().stream()
106
+ .map(taskMapper::entityToStatusDTO)
107
+ .collect(Collectors.toList());
108
+ PageImpl<DeletionTaskStatusDTO> dtoPage = new PageImpl<>(dtoList, pageable, taskPage.getTotalElements());
109
+ return DeletionTaskListResponse.fromPage(dtoPage);
110
+ }
111
+
112
+ @Override
113
+ @Transactional
114
+ public DeletionTaskStatusDTO approveTask(String taskId, TaskApprovalRequest approvalRequest) {
115
+ log.info("Approving deletion task ID: {}", taskId);
116
+ DeletionTaskEntity task = taskRepository.findById(taskId)
117
+ .orElseThrow(() -> new DeletionTaskNotFoundException(taskId, "approve"));
118
+
119
+ if (task.getStatus() != DeletionStatus.PENDING_APPROVAL) {
120
+ log.warn("Cannot approve task {}: not in PENDING_APPROVAL state (current: {}).", taskId, task.getStatus());
121
+ DeletionTaskStatusDTO currentStatusDto = taskMapper.entityToStatusDTO(task);
122
+ currentStatusDto.setErrorMessages(Collections.singletonList("Task is not in a state that can be approved."));
123
+ return currentStatusDto;
124
+ }
125
+ task.setStatus(DeletionStatus.APPROVED); // Or SUBMITTED if it bypasses a queue and goes to processing
126
+ if (approvalRequest != null) {
127
+ task.setApprovalComments(approvalRequest.getComments());
128
+ }
129
+ // TODO: Trigger async processing of the task if approval is the gate
130
+ DeletionTaskEntity savedTask = taskRepository.save(task);
131
+ log.info("Deletion task {} approved, new status: {}.", taskId, savedTask.getStatus());
132
+ return taskMapper.entityToStatusDTO(savedTask);
133
+ }
134
+
135
+ @Override
136
+ @Transactional
137
+ public DeletionTaskStatusDTO rejectTask(String taskId, TaskApprovalRequest rejectionRequest) {
138
+ log.info("Rejecting deletion task ID: {}", taskId);
139
+ DeletionTaskEntity task = taskRepository.findById(taskId)
140
+ .orElseThrow(() -> new DeletionTaskNotFoundException(taskId, "reject"));
141
+
142
+ if (task.getStatus() != DeletionStatus.PENDING_APPROVAL) {
143
+ log.warn("Cannot reject task {}: not in PENDING_APPROVAL state (current: {}).", taskId, task.getStatus());
144
+ DeletionTaskStatusDTO currentStatusDto = taskMapper.entityToStatusDTO(task);
145
+ currentStatusDto.setErrorMessages(Collections.singletonList("Task is not in a state that can be rejected."));
146
+ return currentStatusDto;
147
+ }
148
+ task.setStatus(DeletionStatus.REJECTED);
149
+ if (rejectionRequest != null) {
150
+ task.setRejectionComments(rejectionRequest.getComments());
151
+ }
152
+ DeletionTaskEntity savedTask = taskRepository.save(task);
153
+ log.info("Deletion task {} rejected, new status: {}.", taskId, savedTask.getStatus());
154
+ return taskMapper.entityToStatusDTO(savedTask);
155
+ }
156
+ }
src/main/java/com/dalab/autodelete/service/storage/BulkDeletionResult.java ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autodelete.service.storage;
2
+
3
+ import java.time.LocalDateTime;
4
+ import java.util.List;
5
+
6
+ import lombok.AllArgsConstructor;
7
+ import lombok.Builder;
8
+ import lombok.Data;
9
+ import lombok.NoArgsConstructor;
10
+
11
+ /**
12
+ * Result of a bulk deletion operation.
13
+ */
14
+ @Data
15
+ @Builder
16
+ @NoArgsConstructor
17
+ @AllArgsConstructor
18
+ public class BulkDeletionResult {
19
+
20
+ private String bulkOperationId;
21
+ private List<DeletionResult> individualResults;
22
+ private int totalItems;
23
+ private int successfulDeletions;
24
+ private int failedDeletions;
25
+ private DeletionStatus overallStatus;
26
+ private LocalDateTime initiatedAt;
27
+ private LocalDateTime completedAt;
28
+ private Double totalStorageReclaimed; // In GB
29
+ private Double totalCostSavings; // Monthly savings in USD
30
+ }
src/main/java/com/dalab/autodelete/service/storage/DeletionResult.java ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autodelete.service.storage;
2
+
3
+ import java.time.LocalDateTime;
4
+ import java.util.Map;
5
+ import java.util.UUID;
6
+
7
+ import lombok.AllArgsConstructor;
8
+ import lombok.Builder;
9
+ import lombok.Data;
10
+ import lombok.NoArgsConstructor;
11
+
12
+ /**
13
+ * Result of a deletion operation.
14
+ */
15
+ @Data
16
+ @Builder
17
+ @NoArgsConstructor
18
+ @AllArgsConstructor
19
+ public class DeletionResult {
20
+
21
+ private UUID assetId;
22
+ private String operationId;
23
+ private String dataLocation;
24
+ private DeletionType deletionType;
25
+ private DeletionStatus status;
26
+ private LocalDateTime initiatedAt;
27
+ private LocalDateTime completedAt;
28
+ private Long dataSizeBytes;
29
+ private Map<String, String> metadata;
30
+ private String errorMessage;
31
+ private Double storageSpaceReclaimed; // In GB
32
+ private Double costSavings; // Monthly savings in USD
33
+ private String complianceLevel; // e.g., "GDPR", "HIPAA", "SOX"
34
+ }
src/main/java/com/dalab/autodelete/service/storage/DeletionStatus.java ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autodelete.service.storage;
2
+
3
+ /**
4
+ * Status of deletion operations.
5
+ */
6
+ public enum DeletionStatus {
7
+ INITIATED,
8
+ IN_PROGRESS,
9
+ COMPLETED,
10
+ FAILED,
11
+ CANCELLED,
12
+ PENDING_APPROVAL,
13
+ VERIFIED
14
+ }
src/main/java/com/dalab/autodelete/service/storage/DeletionType.java ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autodelete.service.storage;
2
+
3
+ /**
4
+ * Types of deletion operations available.
5
+ */
6
+ public enum DeletionType {
7
+ /**
8
+ * Soft delete - mark as deleted but keep data recoverable for a period.
9
+ */
10
+ SOFT,
11
+
12
+ /**
13
+ * Hard delete - permanently remove data from storage.
14
+ */
15
+ HARD,
16
+
17
+ /**
18
+ * Secure overwrite - overwrite data multiple times before deletion for compliance.
19
+ */
20
+ SECURE_OVERWRITE,
21
+
22
+ /**
23
+ * Immediate delete - delete data immediately without any recovery period.
24
+ */
25
+ IMMEDIATE
26
+ }
src/main/java/com/dalab/autodelete/service/storage/DeletionVerificationResult.java ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autodelete.service.storage;
2
+
3
+ import java.time.LocalDateTime;
4
+ import java.util.List;
5
+
6
+ import lombok.AllArgsConstructor;
7
+ import lombok.Builder;
8
+ import lombok.Data;
9
+ import lombok.NoArgsConstructor;
10
+
11
+ /**
12
+ * Result of deletion verification for compliance purposes.
13
+ */
14
+ @Data
15
+ @Builder
16
+ @NoArgsConstructor
17
+ @AllArgsConstructor
18
+ public class DeletionVerificationResult {
19
+
20
+ private String operationId;
21
+ private boolean isCompletelyDeleted;
22
+ private boolean isCompliant;
23
+ private List<String> complianceStandards; // e.g., ["GDPR", "HIPAA"]
24
+ private LocalDateTime verificationTimestamp;
25
+ private String verificationMethod;
26
+ private List<String> remainingTraces; // Any remaining data traces found
27
+ private String certificateUrl; // URL to compliance certificate if available
28
+ private String auditTrail; // Audit trail information
29
+ }
src/main/java/com/dalab/autodelete/service/storage/ICloudDeletionService.java ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autodelete.service.storage;
2
+
3
+ import java.util.List;
4
+ import java.util.Map;
5
+ import java.util.UUID;
6
+
7
+ /**
8
+ * Interface for cloud storage deletion operations.
9
+ * Abstracts different cloud providers (AWS S3, Azure Blob, GCP Cloud Storage).
10
+ */
11
+ public interface ICloudDeletionService {
12
+
13
+ /**
14
+ * Perform secure deletion of data from cloud storage.
15
+ *
16
+ * @param assetId The unique identifier of the asset being deleted
17
+ * @param dataLocation The location of the data to delete (e.g., S3 key, blob name)
18
+ * @param deletionType The type of deletion (SOFT, HARD, SECURE_OVERWRITE)
19
+ * @param metadata Additional metadata for the deletion operation
20
+ * @return DeletionResult containing the operation details
21
+ */
22
+ DeletionResult deleteData(UUID assetId, String dataLocation, DeletionType deletionType, Map<String, String> metadata);
23
+
24
+ /**
25
+ * Perform bulk deletion of multiple data objects.
26
+ *
27
+ * @param assetIds List of asset IDs to delete
28
+ * @param dataLocations List of data locations corresponding to the assets
29
+ * @param deletionType The type of deletion to perform
30
+ * @param metadata Additional metadata for the bulk operation
31
+ * @return BulkDeletionResult containing results for all operations
32
+ */
33
+ BulkDeletionResult bulkDeleteData(List<UUID> assetIds, List<String> dataLocations, DeletionType deletionType, Map<String, String> metadata);
34
+
35
+ /**
36
+ * Get the status of a deletion operation.
37
+ *
38
+ * @param operationId The ID of the deletion operation
39
+ * @return DeletionStatus indicating the current state
40
+ */
41
+ DeletionStatus getDeletionStatus(String operationId);
42
+
43
+ /**
44
+ * Calculate storage space that would be reclaimed by deletion.
45
+ *
46
+ * @param dataLocations List of data locations to analyze
47
+ * @return StorageReclaimEstimate with space and cost information
48
+ */
49
+ StorageReclaimEstimate calculateStorageReclaim(List<String> dataLocations);
50
+
51
+ /**
52
+ * Verify that data has been completely deleted (compliance check).
53
+ *
54
+ * @param operationId The deletion operation ID
55
+ * @return DeletionVerificationResult with compliance details
56
+ */
57
+ DeletionVerificationResult verifyDeletion(String operationId);
58
+
59
+ /**
60
+ * Check if the deletion service supports the specified cloud provider.
61
+ *
62
+ * @param cloudProvider The cloud provider (AWS, AZURE, GCP)
63
+ * @return true if supported, false otherwise
64
+ */
65
+ boolean supportsCloudProvider(String cloudProvider);
66
+
67
+ /**
68
+ * Get available deletion types for the cloud provider.
69
+ *
70
+ * @param cloudProvider The cloud provider
71
+ * @return Array of available deletion type names
72
+ */
73
+ DeletionType[] getAvailableDeletionTypes(String cloudProvider);
74
+ }
src/main/java/com/dalab/autodelete/service/storage/StorageReclaimEstimate.java ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autodelete.service.storage;
2
+
3
+ import lombok.AllArgsConstructor;
4
+ import lombok.Builder;
5
+ import lombok.Data;
6
+ import lombok.NoArgsConstructor;
7
+
8
+ /**
9
+ * Estimate of storage space and cost that would be reclaimed by deletion.
10
+ */
11
+ @Data
12
+ @Builder
13
+ @NoArgsConstructor
14
+ @AllArgsConstructor
15
+ public class StorageReclaimEstimate {
16
+
17
+ private int totalObjects;
18
+ private Long totalSizeBytes;
19
+ private Double totalSizeGB;
20
+ private Double currentMonthlyCost;
21
+ private Double reclaimedMonthlySavings;
22
+ private String currency;
23
+ private String region;
24
+ private String storageClass;
25
+ }
src/main/resources/application.properties ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # DALab AutoDelete Service Configuration
2
+ spring.application.name=da-autodelete
3
+ server.port=8080
4
+
5
+ # Database Configuration - da_autodelete database
6
+ spring.datasource.url=jdbc:postgresql://localhost:5432/da_autodelete
7
+ spring.datasource.username=da_autodelete_user
8
+ spring.datasource.password=da_autodelete_pass
9
+ spring.datasource.driver-class-name=org.postgresql.Driver
10
+
11
+ # JPA Configuration
12
+ spring.jpa.hibernate.ddl-auto=update
13
+ spring.jpa.show-sql=false
14
+ spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
15
+ spring.jpa.properties.hibernate.format_sql=true
16
+
17
+ # Common entities database configuration (for da-protos entities)
18
+ dalab.common.datasource.url=jdbc:postgresql://localhost:5432/dalab_common
19
+ dalab.common.datasource.username=dalab_common_user
20
+ dalab.common.datasource.password=dalab_common_pass
21
+
22
+ # Kafka Configuration
23
+ spring.kafka.bootstrap-servers=localhost:9092
24
+ spring.kafka.consumer.group-id=da-autodelete-group
25
+ spring.kafka.consumer.auto-offset-reset=earliest
26
+ spring.kafka.consumer.key-deserializer=org.apache.kafka.common.serialization.StringDeserializer
27
+ spring.kafka.consumer.value-deserializer=org.springframework.kafka.support.serializer.JsonDeserializer
28
+ spring.kafka.consumer.properties.spring.json.trusted.packages=*
29
+ spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer
30
+ spring.kafka.producer.value-serializer=org.springframework.kafka.support.serializer.JsonSerializer
31
+
32
+ # Kafka Topics
33
+ dalab.kafka.topics.policy-actions=dalab.policies.actions
34
+ dalab.kafka.topics.deletion-events=dalab.deletion.events
35
+
36
+ # Security Configuration (Keycloak JWT)
37
+ spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8180/realms/dalab
38
+ spring.security.oauth2.resourceserver.jwt.jwk-set-uri=http://localhost:8180/realms/dalab/protocol/openid-connect/certs
39
+
40
+ # Cloud Storage Configuration
41
+ # AWS S3 Configuration
42
+ aws.s3.region=us-east-1
43
+ aws.s3.bucket.primary=dalab-primary-bucket
44
+
45
+ # Azure Blob Storage Configuration
46
+ azure.storage.account.name=dalabprimary
47
+ azure.storage.container.primary=primary-container
48
+
49
+ # Google Cloud Storage Configuration
50
+ gcp.storage.project.id=dalab-project
51
+ gcp.storage.bucket.primary=dalab-gcp-primary
52
+
53
+ # Deletion Configuration
54
+ deletion.safety.confirmation.required=true
55
+ deletion.backup.enabled=true
56
+ deletion.backup.retention.days=30
57
+ deletion.secure.overwrite.enabled=false
58
+ deletion.batch.size=50
59
+
60
+ # Actuator Configuration
61
+ management.endpoints.web.exposure.include=health,info,metrics,prometheus
62
+ management.endpoint.health.show-details=when-authorized
63
+ management.metrics.export.prometheus.enabled=true
64
+
65
+ # OpenAPI Documentation
66
+ springdoc.api-docs.path=/v3/api-docs
67
+ springdoc.swagger-ui.path=/swagger-ui.html
68
+
69
+ # Logging Configuration
70
+ logging.level.com.dalab.autodelete=INFO
71
+ logging.level.org.springframework.kafka=WARN
72
+ logging.level.org.springframework.security=WARN
src/test/java/com/dalab/autodelete/controller/DeletionConfigControllerTest.java ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autodelete.controller;
2
+
3
+ import com.dalab.autodelete.dto.DeletionConfigDTO;
4
+ import com.dalab.autodelete.service.IDeletionConfigService;
5
+ import com.fasterxml.jackson.databind.ObjectMapper;
6
+ import org.junit.jupiter.api.BeforeEach;
7
+ import org.junit.jupiter.api.Test;
8
+ import org.springframework.beans.factory.annotation.Autowired;
9
+ import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
10
+ import org.springframework.boot.test.mock.mockito.MockBean;
11
+ import org.springframework.http.MediaType;
12
+ import org.springframework.security.test.context.support.WithMockUser;
13
+ import org.springframework.test.web.servlet.MockMvc;
14
+
15
+ import java.util.ArrayList;
16
+
17
+ import static org.mockito.ArgumentMatchers.any;
18
+ import static org.mockito.Mockito.*;
19
+ import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
20
+ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
21
+ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
22
+
23
+ @WebMvcTest(DeletionConfigController.class)
24
+ class DeletionConfigControllerTest {
25
+
26
+ @Autowired
27
+ private MockMvc mockMvc;
28
+
29
+ @MockBean
30
+ private IDeletionConfigService deletionConfigService;
31
+
32
+ @Autowired
33
+ private ObjectMapper objectMapper;
34
+
35
+ private DeletionConfigDTO sampleConfigDTO;
36
+
37
+ @BeforeEach
38
+ void setUp() {
39
+ sampleConfigDTO = DeletionConfigDTO.builder()
40
+ .enabled(true)
41
+ .softDeleteByDefault(true)
42
+ .defaultSoftDeleteRetentionDays(30L)
43
+ .requireApprovalForHardDelete(true)
44
+ .requireApprovalForSoftDelete(false)
45
+ .notificationEmailsOnError(new ArrayList<>())
46
+ .build();
47
+ }
48
+
49
+ @Test
50
+ @WithMockUser(authorities = "ROLE_ADMIN")
51
+ void getDeletionConfiguration_AsAdmin_ShouldReturnConfig() throws Exception {
52
+ when(deletionConfigService.getDeletionConfig()).thenReturn(sampleConfigDTO);
53
+
54
+ mockMvc.perform(get("/api/v1/deletion/config"))
55
+ .andExpect(status().isOk())
56
+ .andExpect(jsonPath("$.enabled").value(true))
57
+ .andExpect(jsonPath("$.softDeleteByDefault").value(true));
58
+ }
59
+
60
+ @Test
61
+ @WithMockUser(authorities = "ROLE_DATA_STEWARD")
62
+ void getDeletionConfiguration_AsDataSteward_ShouldReturnConfig() throws Exception {
63
+ when(deletionConfigService.getDeletionConfig()).thenReturn(sampleConfigDTO);
64
+
65
+ mockMvc.perform(get("/api/v1/deletion/config"))
66
+ .andExpect(status().isOk());
67
+ }
68
+
69
+ @Test
70
+ @WithMockUser(authorities = "ROLE_USER") // A regular user should not access this
71
+ void getDeletionConfiguration_AsUser_ShouldBeForbidden() throws Exception {
72
+ mockMvc.perform(get("/api/v1/deletion/config"))
73
+ .andExpect(status().isForbidden());
74
+ }
75
+
76
+ @Test
77
+ @WithMockUser(authorities = "ROLE_ADMIN")
78
+ void updateDeletionConfiguration_AsAdmin_ShouldReturnOk() throws Exception {
79
+ doNothing().when(deletionConfigService).updateDeletionConfig(any(DeletionConfigDTO.class));
80
+
81
+ mockMvc.perform(put("/api/v1/deletion/config")
82
+ .with(csrf())
83
+ .contentType(MediaType.APPLICATION_JSON)
84
+ .content(objectMapper.writeValueAsString(sampleConfigDTO)))
85
+ .andExpect(status().isOk());
86
+
87
+ verify(deletionConfigService, times(1)).updateDeletionConfig(any(DeletionConfigDTO.class));
88
+ }
89
+
90
+ @Test
91
+ @WithMockUser(authorities = "ROLE_DATA_STEWARD")
92
+ void updateDeletionConfiguration_AsDataSteward_ShouldBeForbidden() throws Exception {
93
+ mockMvc.perform(put("/api/v1/deletion/config")
94
+ .with(csrf())
95
+ .contentType(MediaType.APPLICATION_JSON)
96
+ .content(objectMapper.writeValueAsString(sampleConfigDTO)))
97
+ .andExpect(status().isForbidden());
98
+ }
99
+ @Test
100
+ @WithMockUser(authorities = "ROLE_ADMIN")
101
+ void updateDeletionConfiguration_WithNullBody_AsAdmin_ShouldReturnBadRequest() throws Exception {
102
+ // Simulate service throwing IllegalArgumentException for null DTO
103
+ doThrow(new IllegalArgumentException("DTO cannot be null")).when(deletionConfigService).updateDeletionConfig(null);
104
+
105
+ mockMvc.perform(put("/api/v1/deletion/config")
106
+ .with(csrf())
107
+ .contentType(MediaType.APPLICATION_JSON)
108
+ /* .content(objectMapper.writeValueAsString(null)) */) // Sending empty content or invalid content
109
+ .andExpect(status().isBadRequest()); // Spring typically handles invalid JSON or missing body as 400
110
+ }
111
+ }
src/test/java/com/dalab/autodelete/controller/DeletionTaskControllerTest.java ADDED
@@ -0,0 +1,186 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autodelete.controller;
2
+
3
+ import com.dalab.autodelete.dto.*;
4
+ import com.dalab.autodelete.service.IDeletionTaskService;
5
+ import com.dalab.autodelete.service.exception.DeletionTaskNotFoundException;
6
+ import com.fasterxml.jackson.databind.ObjectMapper;
7
+ import org.junit.jupiter.api.BeforeEach;
8
+ import org.junit.jupiter.api.Test;
9
+ import org.springframework.beans.factory.annotation.Autowired;
10
+ import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
11
+ import org.springframework.boot.test.mock.mockito.MockBean;
12
+ import org.springframework.data.domain.PageImpl;
13
+ import org.springframework.data.domain.PageRequest;
14
+ import org.springframework.data.domain.Pageable;
15
+ import org.springframework.http.MediaType;
16
+ import org.springframework.security.test.context.support.WithMockUser;
17
+ import org.springframework.test.web.servlet.MockMvc;
18
+
19
+ import java.time.LocalDateTime;
20
+ import java.util.Collections;
21
+ import java.util.List;
22
+ import java.util.UUID;
23
+
24
+ import static org.mockito.ArgumentMatchers.any;
25
+ import static org.mockito.ArgumentMatchers.eq;
26
+ import static org.mockito.Mockito.*;
27
+ import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
28
+ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
29
+ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
30
+
31
+ @WebMvcTest(DeletionTaskController.class)
32
+ class DeletionTaskControllerTest {
33
+
34
+ @Autowired
35
+ private MockMvc mockMvc;
36
+
37
+ @MockBean
38
+ private IDeletionTaskService taskService;
39
+
40
+ @Autowired
41
+ private ObjectMapper objectMapper;
42
+
43
+ private DeletionTaskRequest sampleTaskRequest;
44
+ private DeletionTaskResponse sampleTaskResponse;
45
+ private DeletionTaskStatusDTO sampleTaskStatusDTO;
46
+ private String sampleTaskId;
47
+
48
+ @BeforeEach
49
+ void setUp() {
50
+ sampleTaskId = UUID.randomUUID().toString();
51
+
52
+ DeletionTaskRequest.DeletionScope scope = DeletionTaskRequest.DeletionScope.builder()
53
+ .assetIds(List.of("asset1", "asset2"))
54
+ .build();
55
+ sampleTaskRequest = DeletionTaskRequest.builder()
56
+ .taskName("Test Deletion Task")
57
+ .scope(scope)
58
+ .build();
59
+
60
+ sampleTaskResponse = DeletionTaskResponse.builder()
61
+ .taskId(sampleTaskId)
62
+ .taskName("Test Deletion Task")
63
+ .status("SUBMITTED")
64
+ .submittedAt(LocalDateTime.now())
65
+ .build();
66
+
67
+ sampleTaskStatusDTO = DeletionTaskStatusDTO.builder()
68
+ .taskId(sampleTaskId)
69
+ .taskName("Test Deletion Task")
70
+ .status("SUBMITTED")
71
+ .submittedAt(LocalDateTime.now())
72
+ .scope(scope) // Assuming DeletionTaskRequest.DeletionScope is compatible or mapped
73
+ .totalAssetsInScope(2)
74
+ .build();
75
+ }
76
+
77
+ @Test
78
+ @WithMockUser(authorities = "ROLE_ADMIN")
79
+ void submitDeletionTask_AsAdmin_ValidRequest_ShouldReturnAccepted() throws Exception {
80
+ when(taskService.submitDeletionTask(any(DeletionTaskRequest.class))).thenReturn(sampleTaskResponse);
81
+
82
+ mockMvc.perform(post("/api/v1/deletion/tasks")
83
+ .with(csrf())
84
+ .contentType(MediaType.APPLICATION_JSON)
85
+ .content(objectMapper.writeValueAsString(sampleTaskRequest)))
86
+ .andExpect(status().isAccepted())
87
+ .andExpect(jsonPath("$.taskId").value(sampleTaskId))
88
+ .andExpect(jsonPath("$.status").value("SUBMITTED"));
89
+ }
90
+
91
+ @Test
92
+ @WithMockUser(authorities = "ROLE_ADMIN")
93
+ void submitDeletionTask_InvalidScope_ShouldReturnBadRequest() throws Exception {
94
+ DeletionTaskRequest invalidRequest = DeletionTaskRequest.builder().taskName("Invalid Task").scope(null).build();
95
+ // Service returns a specific response for validation failure
96
+ DeletionTaskResponse validationFailedResponse = DeletionTaskResponse.builder()
97
+ .taskId(null)
98
+ .taskName(invalidRequest.getTaskName())
99
+ .status("FAILED_VALIDATION")
100
+ .message("Scope is null or empty")
101
+ .submittedAt(LocalDateTime.now())
102
+ .build();
103
+ when(taskService.submitDeletionTask(any(DeletionTaskRequest.class))).thenReturn(validationFailedResponse);
104
+
105
+ mockMvc.perform(post("/api/v1/deletion/tasks")
106
+ .with(csrf())
107
+ .contentType(MediaType.APPLICATION_JSON)
108
+ .content(objectMapper.writeValueAsString(invalidRequest)))
109
+ .andExpect(status().isBadRequest()) // Controller logic changes status for FAILED_VALIDATION
110
+ .andExpect(jsonPath("$.status").value("FAILED_VALIDATION"));
111
+ }
112
+
113
+ @Test
114
+ @WithMockUser(authorities = "ROLE_USER") // Assuming USER can get status of their own tasks (logic in service)
115
+ void getDeletionTaskStatus_WhenTaskExists_ShouldReturnStatus() throws Exception {
116
+ when(taskService.getTaskStatus(sampleTaskId)).thenReturn(sampleTaskStatusDTO);
117
+
118
+ mockMvc.perform(get("/api/v1/deletion/tasks/{taskId}", sampleTaskId))
119
+ .andExpect(status().isOk())
120
+ .andExpect(jsonPath("$.taskId").value(sampleTaskId))
121
+ .andExpect(jsonPath("$.status").value("SUBMITTED"));
122
+ }
123
+
124
+ @Test
125
+ @WithMockUser(authorities = "ROLE_USER")
126
+ void getDeletionTaskStatus_WhenTaskNotExists_ShouldReturnNotFound() throws Exception {
127
+ when(taskService.getTaskStatus(sampleTaskId)).thenReturn(null); // Service returns null for not found
128
+
129
+ mockMvc.perform(get("/api/v1/deletion/tasks/{taskId}", sampleTaskId))
130
+ .andExpect(status().isNotFound());
131
+ }
132
+
133
+ @Test
134
+ @WithMockUser(authorities = "ROLE_DATA_STEWARD")
135
+ void listDeletionTasks_ShouldReturnPageOfTasks() throws Exception {
136
+ List<DeletionTaskStatusDTO> taskList = Collections.singletonList(sampleTaskStatusDTO);
137
+ PageImpl<DeletionTaskStatusDTO> page = new PageImpl<>(taskList, PageRequest.of(0, 20), 1);
138
+ DeletionTaskListResponse listResponse = DeletionTaskListResponse.fromPage(page);
139
+
140
+ when(taskService.listTasks(any(Pageable.class))).thenReturn(listResponse);
141
+
142
+ mockMvc.perform(get("/api/v1/deletion/tasks").param("page", "0").param("size", "20"))
143
+ .andExpect(status().isOk())
144
+ .andExpect(jsonPath("$.tasks[0].taskId").value(sampleTaskId));
145
+ }
146
+
147
+ @Test
148
+ @WithMockUser(authorities = "ROLE_ADMIN")
149
+ void approveDeletionTask_AsAdmin_WhenTaskApprovable_ShouldReturnOk() throws Exception {
150
+ DeletionTaskStatusDTO approvedStatus = DeletionTaskStatusDTO.builder().taskId(sampleTaskId).status("APPROVED").build();
151
+ when(taskService.approveTask(eq(sampleTaskId), any())).thenReturn(approvedStatus);
152
+
153
+ mockMvc.perform(post("/api/v1/deletion/tasks/{taskId}/approve", sampleTaskId)
154
+ .with(csrf()))
155
+ .andExpect(status().isOk())
156
+ .andExpect(jsonPath("$.status").value("APPROVED"));
157
+ }
158
+
159
+ @Test
160
+ @WithMockUser(authorities = "ROLE_ADMIN")
161
+ void approveDeletionTask_TaskNotApprovable_ShouldReturnConflict() throws Exception {
162
+ DeletionTaskStatusDTO conflictStatus = DeletionTaskStatusDTO.builder()
163
+ .taskId(sampleTaskId)
164
+ .status("COMPLETED") // Example of a non-approvable state
165
+ .errorMessages(List.of("Task not in approvable state"))
166
+ .build();
167
+ when(taskService.approveTask(eq(sampleTaskId), any())).thenReturn(conflictStatus);
168
+
169
+ mockMvc.perform(post("/api/v1/deletion/tasks/{taskId}/approve", sampleTaskId)
170
+ .with(csrf()))
171
+ .andExpect(status().isConflict()) // Controller maps this from DTO having errorMessages
172
+ .andExpect(jsonPath("$.status").value("COMPLETED"));
173
+ }
174
+
175
+ @Test
176
+ @WithMockUser(authorities = "ROLE_DATA_STEWARD")
177
+ void rejectDeletionTask_AsDataSteward_WhenTaskRejectable_ShouldReturnOk() throws Exception {
178
+ DeletionTaskStatusDTO rejectedStatus = DeletionTaskStatusDTO.builder().taskId(sampleTaskId).status("REJECTED").build();
179
+ when(taskService.rejectTask(eq(sampleTaskId), any())).thenReturn(rejectedStatus);
180
+
181
+ mockMvc.perform(post("/api/v1/deletion/tasks/{taskId}/reject", sampleTaskId)
182
+ .with(csrf()))
183
+ .andExpect(status().isOk())
184
+ .andExpect(jsonPath("$.status").value("REJECTED"));
185
+ }
186
+ }
src/test/java/com/dalab/autodelete/service/impl/DeletionConfigServiceImplTest.java ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autodelete.service.impl;
2
+
3
+ import com.dalab.autodelete.dto.DeletionConfigDTO;
4
+ import com.dalab.autodelete.mapper.DeletionConfigMapper;
5
+ import com.dalab.autodelete.model.DeletionConfigEntity;
6
+ import com.dalab.autodelete.repository.DeletionConfigRepository;
7
+ import org.junit.jupiter.api.BeforeEach;
8
+ import org.junit.jupiter.api.Test;
9
+ import org.junit.jupiter.api.extension.ExtendWith;
10
+ import org.mockito.InjectMocks;
11
+ import org.mockito.Mock;
12
+ import org.mockito.junit.jupiter.MockitoExtension;
13
+
14
+ import java.util.ArrayList;
15
+ import java.util.Optional;
16
+
17
+ import static org.junit.jupiter.api.Assertions.*;
18
+ import static org.mockito.Mockito.*;
19
+
20
+ @ExtendWith(MockitoExtension.class)
21
+ class DeletionConfigServiceImplTest {
22
+
23
+ @Mock
24
+ private DeletionConfigRepository configRepository;
25
+
26
+ @Mock
27
+ private DeletionConfigMapper configMapper;
28
+
29
+ @InjectMocks
30
+ private DeletionConfigServiceImpl configService;
31
+
32
+ private DeletionConfigEntity sampleEntity;
33
+ private DeletionConfigDTO sampleDTO;
34
+ private static final Long GLOBAL_CONFIG_ID = 1L;
35
+
36
+ @BeforeEach
37
+ void setUp() {
38
+ sampleEntity = new DeletionConfigEntity(
39
+ GLOBAL_CONFIG_ID, true, true, 30L, true, false, new ArrayList<>()
40
+ );
41
+ sampleDTO = DeletionConfigDTO.builder()
42
+ .enabled(true).softDeleteByDefault(true).defaultSoftDeleteRetentionDays(30L)
43
+ .requireApprovalForHardDelete(true).requireApprovalForSoftDelete(false)
44
+ .notificationEmailsOnError(new ArrayList<>()).build();
45
+ }
46
+
47
+ @Test
48
+ void init_WhenConfigExists_ShouldNotSave() {
49
+ when(configRepository.existsById(GLOBAL_CONFIG_ID)).thenReturn(true);
50
+ configService.init();
51
+ verify(configRepository, never()).save(any(DeletionConfigEntity.class));
52
+ }
53
+
54
+ @Test
55
+ void init_WhenConfigNotExists_ShouldSaveDefault() {
56
+ when(configRepository.existsById(GLOBAL_CONFIG_ID)).thenReturn(false);
57
+ when(configRepository.save(any(DeletionConfigEntity.class))).thenReturn(sampleEntity); // Mock save
58
+ configService.init();
59
+ verify(configRepository, times(1)).save(any(DeletionConfigEntity.class));
60
+ }
61
+
62
+ @Test
63
+ void getDeletionConfig_WhenEntityExists_ShouldReturnMappedDTO() {
64
+ when(configRepository.findById(GLOBAL_CONFIG_ID)).thenReturn(Optional.of(sampleEntity));
65
+ when(configMapper.toDto(sampleEntity)).thenReturn(sampleDTO);
66
+
67
+ DeletionConfigDTO result = configService.getDeletionConfig();
68
+
69
+ assertNotNull(result);
70
+ assertEquals(sampleDTO.isSoftDeleteByDefault(), result.isSoftDeleteByDefault());
71
+ verify(configRepository, times(1)).findById(GLOBAL_CONFIG_ID);
72
+ verify(configMapper, times(1)).toDto(sampleEntity);
73
+ }
74
+
75
+ @Test
76
+ void getDeletionConfig_WhenEntityNotExists_ShouldReturnEmptyDTO() {
77
+ // This scenario tests the defensive elseGet in the service, though it should ideally not happen after init.
78
+ when(configRepository.findById(GLOBAL_CONFIG_ID)).thenReturn(Optional.empty());
79
+
80
+ DeletionConfigDTO result = configService.getDeletionConfig();
81
+
82
+ assertNotNull(result); // Service returns new DTO(), not null
83
+ // Assert default fields of new DeletionConfigDTO() if necessary, or that it's just not null
84
+ assertNull(result.getEnabled()); // Example: Builder default for Boolean is null
85
+
86
+ verify(configRepository, times(1)).findById(GLOBAL_CONFIG_ID);
87
+ verify(configMapper, never()).toDto(any());
88
+ }
89
+
90
+
91
+ @Test
92
+ void updateDeletionConfig_WhenEntityExists_ShouldUpdateAndSave() {
93
+ when(configRepository.findById(GLOBAL_CONFIG_ID)).thenReturn(Optional.of(sampleEntity));
94
+ doNothing().when(configMapper).updateEntityFromDto(eq(sampleDTO), eq(sampleEntity));
95
+ when(configRepository.save(any(DeletionConfigEntity.class))).thenReturn(sampleEntity);
96
+
97
+ configService.updateDeletionConfig(sampleDTO);
98
+
99
+ verify(configRepository, times(1)).findById(GLOBAL_CONFIG_ID);
100
+ verify(configMapper, times(1)).updateEntityFromDto(sampleDTO, sampleEntity);
101
+ verify(configRepository, times(1)).save(sampleEntity);
102
+ assertEquals(GLOBAL_CONFIG_ID, sampleEntity.getId()); // Ensure ID is preserved
103
+ }
104
+
105
+ @Test
106
+ void updateDeletionConfig_WhenDtoIsNull_ShouldThrowIllegalArgumentException() {
107
+ assertThrows(IllegalArgumentException.class, () -> {
108
+ configService.updateDeletionConfig(null);
109
+ });
110
+ }
111
+
112
+ @Test
113
+ void updateDeletionConfig_WhenEntityNotFound_ShouldThrowIllegalStateException() {
114
+ when(configRepository.findById(GLOBAL_CONFIG_ID)).thenReturn(Optional.empty());
115
+ assertThrows(IllegalStateException.class, () -> {
116
+ configService.updateDeletionConfig(sampleDTO);
117
+ });
118
+ }
119
+ }
src/test/java/com/dalab/autodelete/service/impl/DeletionTaskServiceImplTest.java ADDED
@@ -0,0 +1,240 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autodelete.service.impl;
2
+
3
+ import com.dalab.autodelete.dto.*;
4
+ import com.dalab.autodelete.mapper.DeletionTaskMapper;
5
+ import com.dalab.autodelete.model.DeletionStatus;
6
+ import com.dalab.autodelete.model.DeletionTaskEntity;
7
+ import com.dalab.autodelete.repository.DeletionTaskRepository;
8
+ import com.dalab.autodelete.service.IDeletionConfigService;
9
+ import com.dalab.autodelete.service.exception.DeletionTaskNotFoundException;
10
+ import org.junit.jupiter.api.BeforeEach;
11
+ import org.junit.jupiter.api.Test;
12
+ import org.junit.jupiter.api.extension.ExtendWith;
13
+ import org.mockito.ArgumentCaptor;
14
+ import org.mockito.InjectMocks;
15
+ import org.mockito.Mock;
16
+ import org.mockito.junit.jupiter.MockitoExtension;
17
+ import org.springframework.data.domain.Page;
18
+ import org.springframework.data.domain.PageImpl;
19
+ import org.springframework.data.domain.PageRequest;
20
+ import org.springframework.data.domain.Pageable;
21
+
22
+ import java.time.LocalDateTime;
23
+ import java.util.ArrayList;
24
+ import java.util.Collections;
25
+ import java.util.List;
26
+ import java.util.Optional;
27
+ import java.util.UUID;
28
+
29
+ import static org.junit.jupiter.api.Assertions.*;
30
+ import static org.mockito.ArgumentMatchers.any;
31
+ import static org.mockito.Mockito.*;
32
+
33
+ @ExtendWith(MockitoExtension.class)
34
+ class DeletionTaskServiceImplTest {
35
+
36
+ @Mock
37
+ private DeletionTaskRepository taskRepository;
38
+
39
+ @Mock
40
+ private DeletionTaskMapper taskMapper;
41
+
42
+ @Mock
43
+ private IDeletionConfigService deletionConfigService;
44
+
45
+ @InjectMocks
46
+ private DeletionTaskServiceImpl taskService;
47
+
48
+ private DeletionTaskRequest sampleTaskRequest;
49
+ private DeletionTaskEntity sampleTaskEntity;
50
+ private DeletionTaskStatusDTO sampleTaskStatusDTO;
51
+ private DeletionTaskResponse sampleTaskResponse;
52
+ private DeletionConfigDTO globalConfigDefault;
53
+ private DeletionConfigDTO globalConfigSoftDeleteApproval;
54
+ private String sampleTaskId;
55
+
56
+ @BeforeEach
57
+ void setUp() {
58
+ sampleTaskId = UUID.randomUUID().toString();
59
+ DeletionTaskRequest.DeletionScope scope = DeletionTaskRequest.DeletionScope.builder()
60
+ .assetIds(List.of("asset1")).build();
61
+
62
+ sampleTaskRequest = DeletionTaskRequest.builder()
63
+ .taskName("Test Task")
64
+ .scope(scope)
65
+ .triggeredBy("user@test.com")
66
+ .build();
67
+
68
+ sampleTaskEntity = new DeletionTaskEntity();
69
+ sampleTaskEntity.setTaskId(sampleTaskId);
70
+ sampleTaskEntity.setTaskName("Test Task");
71
+ sampleTaskEntity.setStatus(DeletionStatus.SUBMITTED);
72
+ sampleTaskEntity.setSubmittedAt(LocalDateTime.now());
73
+ sampleTaskEntity.setScope(new DeletionTaskEntity.DeletionScopeData(List.of("asset1")));
74
+ sampleTaskEntity.setAssetStatuses(new ArrayList<>());
75
+
76
+ sampleTaskStatusDTO = DeletionTaskStatusDTO.builder().taskId(sampleTaskId).status(DeletionStatus.SUBMITTED.name()).build();
77
+ sampleTaskResponse = DeletionTaskResponse.builder().taskId(sampleTaskId).status(DeletionStatus.SUBMITTED.name()).build();
78
+
79
+ globalConfigDefault = DeletionConfigDTO.builder()
80
+ .enabled(true)
81
+ .softDeleteByDefault(false) // Hard delete by default
82
+ .requireApprovalForHardDelete(false) // No approval for hard delete
83
+ .requireApprovalForSoftDelete(true) // Approval for soft delete
84
+ .build();
85
+
86
+ globalConfigSoftDeleteApproval = DeletionConfigDTO.builder()
87
+ .enabled(true)
88
+ .softDeleteByDefault(true) // Soft delete by default
89
+ .requireApprovalForHardDelete(true) // Approval for hard delete
90
+ .requireApprovalForSoftDelete(true) // Approval for soft delete
91
+ .build();
92
+ }
93
+
94
+ @Test
95
+ void submitDeletionTask_ValidRequest_NoApprovalNeeded_ShouldSaveAndReturnResponse() {
96
+ when(deletionConfigService.getDeletionConfig()).thenReturn(globalConfigDefault);
97
+ when(taskMapper.requestToEntity(sampleTaskRequest)).thenReturn(sampleTaskEntity); // sampleTaskEntity is already set up
98
+ when(taskRepository.save(any(DeletionTaskEntity.class))).thenReturn(sampleTaskEntity);
99
+ when(taskMapper.entityToTaskResponse(sampleTaskEntity)).thenReturn(sampleTaskResponse);
100
+
101
+ DeletionTaskResponse response = taskService.submitDeletionTask(sampleTaskRequest);
102
+
103
+ assertNotNull(response);
104
+ assertEquals(DeletionStatus.SUBMITTED.name(), response.getStatus());
105
+ ArgumentCaptor<DeletionTaskEntity> entityCaptor = ArgumentCaptor.forClass(DeletionTaskEntity.class);
106
+ verify(taskRepository).save(entityCaptor.capture());
107
+ assertEquals(DeletionStatus.SUBMITTED, entityCaptor.getValue().getStatus());
108
+ assertNotNull(entityCaptor.getValue().getAssetStatuses());
109
+ assertEquals(1, entityCaptor.getValue().getAssetsPendingProcessing());
110
+ assertEquals("HARD", entityCaptor.getValue().getAssetStatuses().get(0).getDeletionType());
111
+ }
112
+
113
+ @Test
114
+ void submitDeletionTask_RequiresHardDeleteApproval_ShouldSetPendingApproval() {
115
+ DeletionConfigDTO config = DeletionConfigDTO.builder().softDeleteByDefault(false).requireApprovalForHardDelete(true).build();
116
+ when(deletionConfigService.getDeletionConfig()).thenReturn(config);
117
+ when(taskMapper.requestToEntity(sampleTaskRequest)).thenReturn(sampleTaskEntity);
118
+ when(taskRepository.save(any(DeletionTaskEntity.class))).thenReturn(sampleTaskEntity);
119
+
120
+ taskService.submitDeletionTask(sampleTaskRequest);
121
+
122
+ ArgumentCaptor<DeletionTaskEntity> entityCaptor = ArgumentCaptor.forClass(DeletionTaskEntity.class);
123
+ verify(taskRepository).save(entityCaptor.capture());
124
+ assertEquals(DeletionStatus.PENDING_APPROVAL, entityCaptor.getValue().getStatus());
125
+ }
126
+
127
+ @Test
128
+ void submitDeletionTask_SoftDeleteOverride_RequiresSoftApproval_ShouldSetPendingApproval() {
129
+ sampleTaskRequest.setOverrideConfig(DeletionTaskRequest.DeletionConfigOverride.builder().softDelete(true).build());
130
+ when(deletionConfigService.getDeletionConfig()).thenReturn(globalConfigSoftDeleteApproval);
131
+ when(taskMapper.requestToEntity(sampleTaskRequest)).thenReturn(sampleTaskEntity);
132
+ when(taskRepository.save(any(DeletionTaskEntity.class))).thenReturn(sampleTaskEntity);
133
+
134
+ taskService.submitDeletionTask(sampleTaskRequest);
135
+ ArgumentCaptor<DeletionTaskEntity> entityCaptor = ArgumentCaptor.forClass(DeletionTaskEntity.class);
136
+ verify(taskRepository).save(entityCaptor.capture());
137
+ assertEquals(DeletionStatus.PENDING_APPROVAL, entityCaptor.getValue().getStatus());
138
+ assertEquals("SOFT", entityCaptor.getValue().getAssetStatuses().get(0).getDeletionType());
139
+ }
140
+
141
+ @Test
142
+ void submitDeletionTask_InvalidScope_ShouldReturnFailedStatus() {
143
+ DeletionTaskRequest invalidRequest = DeletionTaskRequest.builder().taskName("Invalid").scope(null).build();
144
+ DeletionTaskResponse response = taskService.submitDeletionTask(invalidRequest);
145
+ assertEquals(DeletionStatus.FAILED.name(), response.getStatus());
146
+ verify(taskRepository, never()).save(any());
147
+ }
148
+
149
+ @Test
150
+ void getTaskStatus_TaskExists_ShouldReturnDTO() {
151
+ when(taskRepository.findById(sampleTaskId)).thenReturn(Optional.of(sampleTaskEntity));
152
+ when(taskMapper.entityToStatusDTO(sampleTaskEntity)).thenReturn(sampleTaskStatusDTO);
153
+
154
+ DeletionTaskStatusDTO result = taskService.getTaskStatus(sampleTaskId);
155
+
156
+ assertNotNull(result);
157
+ assertEquals(sampleTaskId, result.getTaskId());
158
+ }
159
+
160
+ @Test
161
+ void getTaskStatus_TaskNotExists_ShouldReturnNull() {
162
+ when(taskRepository.findById(sampleTaskId)).thenReturn(Optional.empty());
163
+ DeletionTaskStatusDTO result = taskService.getTaskStatus(sampleTaskId);
164
+ assertNull(result);
165
+ }
166
+
167
+ @Test
168
+ void listTasks_ShouldReturnPaginatedDTOs() {
169
+ Pageable pageable = PageRequest.of(0, 10);
170
+ List<DeletionTaskEntity> taskList = Collections.singletonList(sampleTaskEntity);
171
+ Page<DeletionTaskEntity> page = new PageImpl<>(taskList, pageable, 1);
172
+ when(taskRepository.findAll(pageable)).thenReturn(page);
173
+ when(taskMapper.entityToStatusDTO(sampleTaskEntity)).thenReturn(sampleTaskStatusDTO);
174
+
175
+ DeletionTaskListResponse response = taskService.listTasks(pageable);
176
+
177
+ assertNotNull(response);
178
+ assertEquals(1, response.getTasks().size());
179
+ assertEquals(sampleTaskId, response.getTasks().get(0).getTaskId());
180
+ }
181
+
182
+ @Test
183
+ void approveTask_TaskExistsAndPending_ShouldApprove() {
184
+ sampleTaskEntity.setStatus(DeletionStatus.PENDING_APPROVAL);
185
+ when(taskRepository.findById(sampleTaskId)).thenReturn(Optional.of(sampleTaskEntity));
186
+ when(taskRepository.save(any(DeletionTaskEntity.class))).thenReturn(sampleTaskEntity);
187
+ when(taskMapper.entityToStatusDTO(any(DeletionTaskEntity.class))).thenAnswer(invocation -> {
188
+ DeletionTaskEntity entity = invocation.getArgument(0);
189
+ return DeletionTaskStatusDTO.builder().taskId(entity.getTaskId()).status(entity.getStatus().name()).build();
190
+ });
191
+
192
+ TaskApprovalRequest approvalReq = TaskApprovalRequest.builder().comments("Approved by test").build();
193
+ DeletionTaskStatusDTO result = taskService.approveTask(sampleTaskId, approvalReq);
194
+
195
+ assertEquals(DeletionStatus.APPROVED.name(), result.getStatus());
196
+ assertEquals("Approved by test", sampleTaskEntity.getApprovalComments());
197
+ verify(taskRepository).save(sampleTaskEntity);
198
+ }
199
+
200
+ @Test
201
+ void approveTask_TaskNotPending_ShouldReturnCurrentStatusWithErrors() {
202
+ sampleTaskEntity.setStatus(DeletionStatus.COMPLETED);
203
+ when(taskRepository.findById(sampleTaskId)).thenReturn(Optional.of(sampleTaskEntity));
204
+ // Mapper will be called to return the current DTO
205
+ when(taskMapper.entityToStatusDTO(sampleTaskEntity)).thenReturn(DeletionTaskStatusDTO.builder().taskId(sampleTaskId).status(DeletionStatus.COMPLETED.name()).build());
206
+
207
+ DeletionTaskStatusDTO result = taskService.approveTask(sampleTaskId, new TaskApprovalRequest());
208
+
209
+ assertEquals(DeletionStatus.COMPLETED.name(), result.getStatus());
210
+ assertNotNull(result.getErrorMessages());
211
+ assertFalse(result.getErrorMessages().isEmpty());
212
+ verify(taskRepository, never()).save(any());
213
+ }
214
+
215
+ @Test
216
+ void approveTask_TaskNotFound_ShouldThrowException() {
217
+ when(taskRepository.findById(sampleTaskId)).thenReturn(Optional.empty());
218
+ assertThrows(DeletionTaskNotFoundException.class, () -> {
219
+ taskService.approveTask(sampleTaskId, new TaskApprovalRequest());
220
+ });
221
+ }
222
+
223
+ @Test
224
+ void rejectTask_TaskExistsAndPending_ShouldReject() {
225
+ sampleTaskEntity.setStatus(DeletionStatus.PENDING_APPROVAL);
226
+ when(taskRepository.findById(sampleTaskId)).thenReturn(Optional.of(sampleTaskEntity));
227
+ when(taskRepository.save(any(DeletionTaskEntity.class))).thenReturn(sampleTaskEntity);
228
+ when(taskMapper.entityToStatusDTO(any(DeletionTaskEntity.class))).thenAnswer(invocation -> {
229
+ DeletionTaskEntity entity = invocation.getArgument(0);
230
+ return DeletionTaskStatusDTO.builder().taskId(entity.getTaskId()).status(entity.getStatus().name()).build();
231
+ });
232
+
233
+ TaskApprovalRequest rejectionReq = TaskApprovalRequest.builder().comments("Rejected by test").build();
234
+ DeletionTaskStatusDTO result = taskService.rejectTask(sampleTaskId, rejectionReq);
235
+
236
+ assertEquals(DeletionStatus.REJECTED.name(), result.getStatus());
237
+ assertEquals("Rejected by test", sampleTaskEntity.getRejectionComments());
238
+ verify(taskRepository).save(sampleTaskEntity);
239
+ }
240
+ }