From f5e8060156dfdfcec062b55ce0afb9ae7cd99dd5 Mon Sep 17 00:00:00 2001 From: Matheus Cunha Date: Sun, 12 Jul 2026 03:49:37 -0300 Subject: [PATCH 1/3] Introduce Problem Management module: includes CRUD operations, REST API, persistence layer, and related configurations --- .../adapter/in/web/problem/ProblemApi.java | 4 + .../in/web/problem/ProblemController.java | 99 ++++++++++++++ .../in/web/problem/ProblemWebMapper.java | 30 +++++ .../web/problem/dto/CreateProblemRequest.java | 20 +++ .../problem/dto/ProblemSummaryResponse.java | 18 +++ .../web/problem/dto/ProblemViewResponse.java | 20 +++ .../web/problem/dto/UpdateProblemRequest.java | 15 +++ .../dto/UpdateProblemStatusRequest.java | 10 ++ .../persistence/problem/ProblemEntity.java | 121 ++++++++++++++++++ .../persistence/problem/ProblemMapper.java | 15 +++ .../problem/ProblemPersistenceAdapter.java | 53 ++++++++ .../problem/SpringDataProblemRepository.java | 11 ++ .../impl/problem/CreateProblemImpl.java | 42 ++++++ .../impl/problem/DeleteProblemImpl.java | 35 +++++ .../problem/GetAllProblemsByProjectImpl.java | 31 +++++ .../impl/problem/GetProblemByIdImpl.java | 35 +++++ .../impl/problem/UpdateProblemImpl.java | 42 ++++++ .../impl/problem/UpdateProblemStatusImpl.java | 39 ++++++ .../port/in/problem/CreateProblemCommand.java | 16 +++ .../port/in/problem/CreateProblemUseCase.java | 7 + .../port/in/problem/DeleteProblemUseCase.java | 7 + .../GetAllProblemsByProjectUseCase.java | 10 ++ .../in/problem/GetProblemByIdUseCase.java | 9 ++ .../port/in/problem/UpdateProblemCommand.java | 16 +++ .../problem/UpdateProblemStatusCommand.java | 12 ++ .../problem/UpdateProblemStatusUseCase.java | 7 + .../port/in/problem/UpdateProblemUseCase.java | 7 + .../persistence/ProblemRepositoryPort.java | 17 +++ .../backend/domain/model/Problem.java | 101 +++++++++++++++ .../domain/model/enums/ProblemSeverity.java | 8 ++ .../domain/model/enums/ProblemStatus.java | 8 ++ .../configuration/ProblemBeanConfig.java | 78 +++++++++++ .../changesets/004-create-problems-table.yaml | 70 ++++++++++ .../db/changelog/db.changelog-master.yaml | 5 +- 34 files changed, 1017 insertions(+), 1 deletion(-) create mode 100644 backend/src/main/java/com/devaulty/backend/adapter/in/web/problem/ProblemApi.java create mode 100644 backend/src/main/java/com/devaulty/backend/adapter/in/web/problem/ProblemController.java create mode 100644 backend/src/main/java/com/devaulty/backend/adapter/in/web/problem/ProblemWebMapper.java create mode 100644 backend/src/main/java/com/devaulty/backend/adapter/in/web/problem/dto/CreateProblemRequest.java create mode 100644 backend/src/main/java/com/devaulty/backend/adapter/in/web/problem/dto/ProblemSummaryResponse.java create mode 100644 backend/src/main/java/com/devaulty/backend/adapter/in/web/problem/dto/ProblemViewResponse.java create mode 100644 backend/src/main/java/com/devaulty/backend/adapter/in/web/problem/dto/UpdateProblemRequest.java create mode 100644 backend/src/main/java/com/devaulty/backend/adapter/in/web/problem/dto/UpdateProblemStatusRequest.java create mode 100644 backend/src/main/java/com/devaulty/backend/adapter/out/persistence/problem/ProblemEntity.java create mode 100644 backend/src/main/java/com/devaulty/backend/adapter/out/persistence/problem/ProblemMapper.java create mode 100644 backend/src/main/java/com/devaulty/backend/adapter/out/persistence/problem/ProblemPersistenceAdapter.java create mode 100644 backend/src/main/java/com/devaulty/backend/adapter/out/persistence/problem/SpringDataProblemRepository.java create mode 100644 backend/src/main/java/com/devaulty/backend/application/impl/problem/CreateProblemImpl.java create mode 100644 backend/src/main/java/com/devaulty/backend/application/impl/problem/DeleteProblemImpl.java create mode 100644 backend/src/main/java/com/devaulty/backend/application/impl/problem/GetAllProblemsByProjectImpl.java create mode 100644 backend/src/main/java/com/devaulty/backend/application/impl/problem/GetProblemByIdImpl.java create mode 100644 backend/src/main/java/com/devaulty/backend/application/impl/problem/UpdateProblemImpl.java create mode 100644 backend/src/main/java/com/devaulty/backend/application/impl/problem/UpdateProblemStatusImpl.java create mode 100644 backend/src/main/java/com/devaulty/backend/application/port/in/problem/CreateProblemCommand.java create mode 100644 backend/src/main/java/com/devaulty/backend/application/port/in/problem/CreateProblemUseCase.java create mode 100644 backend/src/main/java/com/devaulty/backend/application/port/in/problem/DeleteProblemUseCase.java create mode 100644 backend/src/main/java/com/devaulty/backend/application/port/in/problem/GetAllProblemsByProjectUseCase.java create mode 100644 backend/src/main/java/com/devaulty/backend/application/port/in/problem/GetProblemByIdUseCase.java create mode 100644 backend/src/main/java/com/devaulty/backend/application/port/in/problem/UpdateProblemCommand.java create mode 100644 backend/src/main/java/com/devaulty/backend/application/port/in/problem/UpdateProblemStatusCommand.java create mode 100644 backend/src/main/java/com/devaulty/backend/application/port/in/problem/UpdateProblemStatusUseCase.java create mode 100644 backend/src/main/java/com/devaulty/backend/application/port/in/problem/UpdateProblemUseCase.java create mode 100644 backend/src/main/java/com/devaulty/backend/application/port/out/persistence/ProblemRepositoryPort.java create mode 100644 backend/src/main/java/com/devaulty/backend/domain/model/Problem.java create mode 100644 backend/src/main/java/com/devaulty/backend/domain/model/enums/ProblemSeverity.java create mode 100644 backend/src/main/java/com/devaulty/backend/domain/model/enums/ProblemStatus.java create mode 100644 backend/src/main/java/com/devaulty/backend/infrastructure/configuration/ProblemBeanConfig.java create mode 100644 backend/src/main/resources/db/changelog/changesets/004-create-problems-table.yaml diff --git a/backend/src/main/java/com/devaulty/backend/adapter/in/web/problem/ProblemApi.java b/backend/src/main/java/com/devaulty/backend/adapter/in/web/problem/ProblemApi.java new file mode 100644 index 0000000..71fe483 --- /dev/null +++ b/backend/src/main/java/com/devaulty/backend/adapter/in/web/problem/ProblemApi.java @@ -0,0 +1,4 @@ +package com.devaulty.backend.adapter.in.web.problem; + +public interface ProblemApi { +} diff --git a/backend/src/main/java/com/devaulty/backend/adapter/in/web/problem/ProblemController.java b/backend/src/main/java/com/devaulty/backend/adapter/in/web/problem/ProblemController.java new file mode 100644 index 0000000..6989387 --- /dev/null +++ b/backend/src/main/java/com/devaulty/backend/adapter/in/web/problem/ProblemController.java @@ -0,0 +1,99 @@ +package com.devaulty.backend.adapter.in.web.problem; + +import com.devaulty.backend.adapter.in.web.problem.dto.*; +import com.devaulty.backend.adapter.in.web.util.UriLocationBuilderHelper; +import com.devaulty.backend.application.port.in.problem.*; +import com.devaulty.backend.domain.model.Problem; +import jakarta.validation.Valid; +import org.springframework.data.domain.Page; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.net.URI; +import java.util.UUID; + +@RestController +@RequestMapping("/api/v1/projects/{projectId}/problems") +public class ProblemController implements ProblemApi{ + + private final CreateProblemUseCase createProblemUseCase; + private final GetAllProblemsByProjectUseCase getAllProblemsByProjectUseCase; + private final GetProblemByIdUseCase getProblemByIdUseCase; + private final UpdateProblemUseCase updateProblemUseCase; + private final UpdateProblemStatusUseCase updateProblemStatusUseCase; + private final DeleteProblemUseCase deleteProblemUseCase; + private final ProblemWebMapper mapper; + private final UriLocationBuilderHelper uriLocationBuilderHelper; + + public ProblemController(CreateProblemUseCase createProblemUseCase, GetAllProblemsByProjectUseCase getAllProblemsByProjectUseCase, GetProblemByIdUseCase getProblemByIdUseCase, UpdateProblemUseCase updateProblemUseCase, UpdateProblemStatusUseCase updateProblemStatusUseCase, DeleteProblemUseCase deleteProblemUseCase, ProblemWebMapper mapper, UriLocationBuilderHelper uriLocationBuilderHelper) { + this.createProblemUseCase = createProblemUseCase; + this.getAllProblemsByProjectUseCase = getAllProblemsByProjectUseCase; + this.getProblemByIdUseCase = getProblemByIdUseCase; + this.updateProblemUseCase = updateProblemUseCase; + this.updateProblemStatusUseCase = updateProblemStatusUseCase; + this.deleteProblemUseCase = deleteProblemUseCase; + this.mapper = mapper; + this.uriLocationBuilderHelper = uriLocationBuilderHelper; + } + + @PostMapping + public ResponseEntity create( + @PathVariable UUID projectId, + @RequestBody @Valid CreateProblemRequest request) + { + CreateProblemCommand command = mapper.toCreateProblemCommand(request, projectId); + Problem problem = createProblemUseCase.execute(command); + URI location = uriLocationBuilderHelper.buildLocationUri(problem.getId()); + return ResponseEntity.created(location).body(mapper.toViewResponse(problem)); + } + + @GetMapping + public ResponseEntity> getAllByProject( + @PathVariable UUID projectId, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "10") int size + ){ + Page problems = getAllProblemsByProjectUseCase.execute(projectId, page, size); + return ResponseEntity.ok(problems.map(mapper::toSummaryResponse)); + } + + @GetMapping("/{problemId}") + public ResponseEntity getById( + @PathVariable UUID projectId, + @PathVariable UUID problemId + ){ + Problem problem = getProblemByIdUseCase.execute(projectId, problemId); + return ResponseEntity.ok(mapper.toViewResponse(problem)); + } + + @PatchMapping("/{problemId}") + public ResponseEntity update( + @PathVariable UUID projectId, + @PathVariable UUID problemId, + @RequestBody @Valid UpdateProblemRequest request + ){ + UpdateProblemCommand command = mapper.toUpdateProblemCommand(request, projectId, problemId); + Problem problem = updateProblemUseCase.execute(command); + return ResponseEntity.ok(mapper.toViewResponse(problem)); + } + + @PatchMapping("/{problemId}/status") + public ResponseEntity updateStatus( + @PathVariable UUID projectId, + @PathVariable UUID problemId, + @RequestBody @Valid UpdateProblemStatusRequest request + ){ + UpdateProblemStatusCommand command = mapper.toUpdateProblemStatusCommand(request, projectId, problemId); + Problem problem = updateProblemStatusUseCase.execute(command); + return ResponseEntity.ok(mapper.toViewResponse(problem)); + } + + @DeleteMapping("/{problemId}") + public ResponseEntity delete( + @PathVariable UUID projectId, + @PathVariable UUID problemId + ){ + deleteProblemUseCase.execute(projectId, problemId); + return ResponseEntity.noContent().build(); + } +} diff --git a/backend/src/main/java/com/devaulty/backend/adapter/in/web/problem/ProblemWebMapper.java b/backend/src/main/java/com/devaulty/backend/adapter/in/web/problem/ProblemWebMapper.java new file mode 100644 index 0000000..c9fb41d --- /dev/null +++ b/backend/src/main/java/com/devaulty/backend/adapter/in/web/problem/ProblemWebMapper.java @@ -0,0 +1,30 @@ +package com.devaulty.backend.adapter.in.web.problem; + +import com.devaulty.backend.adapter.in.web.problem.dto.*; +import com.devaulty.backend.application.port.in.problem.CreateProblemCommand; +import com.devaulty.backend.application.port.in.problem.UpdateProblemCommand; +import com.devaulty.backend.application.port.in.problem.UpdateProblemStatusCommand; +import com.devaulty.backend.domain.model.Problem; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +import java.util.UUID; + +@Mapper(componentModel = "spring") +public interface ProblemWebMapper { + + ProblemViewResponse toViewResponse(Problem problem); + + ProblemSummaryResponse toSummaryResponse(Problem problem); + + @Mapping(target = "projectId", source = "projectId") + CreateProblemCommand toCreateProblemCommand(CreateProblemRequest request, UUID projectId); + + @Mapping(target = "projectId", source = "projectId") + @Mapping(target = "id", source = "problemId") + UpdateProblemCommand toUpdateProblemCommand(UpdateProblemRequest request, UUID projectId, UUID problemId); + + @Mapping(target = "projectId", source = "projectId") + @Mapping(target = "id", source = "problemId") + UpdateProblemStatusCommand toUpdateProblemStatusCommand(UpdateProblemStatusRequest request, UUID projectId, UUID problemId); +} diff --git a/backend/src/main/java/com/devaulty/backend/adapter/in/web/problem/dto/CreateProblemRequest.java b/backend/src/main/java/com/devaulty/backend/adapter/in/web/problem/dto/CreateProblemRequest.java new file mode 100644 index 0000000..e7cbec8 --- /dev/null +++ b/backend/src/main/java/com/devaulty/backend/adapter/in/web/problem/dto/CreateProblemRequest.java @@ -0,0 +1,20 @@ +package com.devaulty.backend.adapter.in.web.problem.dto; + +import com.devaulty.backend.domain.model.enums.ProblemSeverity; +import com.devaulty.backend.domain.model.enums.ProblemStatus; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +public record CreateProblemRequest( + @NotBlank(message = "Title must not be blank") + @Size(min = 2, max = 255, message = "Title must be between 2 and 255 characters") + String title, + String errorDescription, + String solution, + @NotNull(message = "Status must not be null") + ProblemStatus status, + @NotNull(message = "Severity must not be null") + ProblemSeverity severity +) { +} diff --git a/backend/src/main/java/com/devaulty/backend/adapter/in/web/problem/dto/ProblemSummaryResponse.java b/backend/src/main/java/com/devaulty/backend/adapter/in/web/problem/dto/ProblemSummaryResponse.java new file mode 100644 index 0000000..6b81ca6 --- /dev/null +++ b/backend/src/main/java/com/devaulty/backend/adapter/in/web/problem/dto/ProblemSummaryResponse.java @@ -0,0 +1,18 @@ +package com.devaulty.backend.adapter.in.web.problem.dto; + +import com.devaulty.backend.domain.model.enums.ProblemSeverity; +import com.devaulty.backend.domain.model.enums.ProblemStatus; + +import java.time.LocalDateTime; +import java.util.UUID; + +public record ProblemSummaryResponse( + UUID id, + UUID projectId, + String title, + ProblemStatus status, + ProblemSeverity severity, + LocalDateTime createdAt, + LocalDateTime updatedAt +) { +} diff --git a/backend/src/main/java/com/devaulty/backend/adapter/in/web/problem/dto/ProblemViewResponse.java b/backend/src/main/java/com/devaulty/backend/adapter/in/web/problem/dto/ProblemViewResponse.java new file mode 100644 index 0000000..61323b4 --- /dev/null +++ b/backend/src/main/java/com/devaulty/backend/adapter/in/web/problem/dto/ProblemViewResponse.java @@ -0,0 +1,20 @@ +package com.devaulty.backend.adapter.in.web.problem.dto; + +import com.devaulty.backend.domain.model.enums.ProblemSeverity; +import com.devaulty.backend.domain.model.enums.ProblemStatus; + +import java.time.LocalDateTime; +import java.util.UUID; + +public record ProblemViewResponse( + UUID id, + UUID projectId, + String title, + String errorDescription, + String solution, + ProblemStatus status, + ProblemSeverity severity, + LocalDateTime createdAt, + LocalDateTime updatedAt +) { +} diff --git a/backend/src/main/java/com/devaulty/backend/adapter/in/web/problem/dto/UpdateProblemRequest.java b/backend/src/main/java/com/devaulty/backend/adapter/in/web/problem/dto/UpdateProblemRequest.java new file mode 100644 index 0000000..30c290f --- /dev/null +++ b/backend/src/main/java/com/devaulty/backend/adapter/in/web/problem/dto/UpdateProblemRequest.java @@ -0,0 +1,15 @@ +package com.devaulty.backend.adapter.in.web.problem.dto; + +import com.devaulty.backend.domain.model.enums.ProblemSeverity; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; + +public record UpdateProblemRequest( + @Pattern(regexp = "(?s).*\\S.*", message = "Title must not be blank") + @Size(min = 2, max = 255, message = "Title must be between 2 and 255 characters") + String title, + String errorDescription, + String solution, + ProblemSeverity severity +) { +} diff --git a/backend/src/main/java/com/devaulty/backend/adapter/in/web/problem/dto/UpdateProblemStatusRequest.java b/backend/src/main/java/com/devaulty/backend/adapter/in/web/problem/dto/UpdateProblemStatusRequest.java new file mode 100644 index 0000000..73ab4ad --- /dev/null +++ b/backend/src/main/java/com/devaulty/backend/adapter/in/web/problem/dto/UpdateProblemStatusRequest.java @@ -0,0 +1,10 @@ +package com.devaulty.backend.adapter.in.web.problem.dto; + +import com.devaulty.backend.domain.model.enums.ProblemStatus; +import jakarta.validation.constraints.NotNull; + +public record UpdateProblemStatusRequest( + @NotNull(message = "Status must not be null") + ProblemStatus status +) { +} diff --git a/backend/src/main/java/com/devaulty/backend/adapter/out/persistence/problem/ProblemEntity.java b/backend/src/main/java/com/devaulty/backend/adapter/out/persistence/problem/ProblemEntity.java new file mode 100644 index 0000000..9823f9b --- /dev/null +++ b/backend/src/main/java/com/devaulty/backend/adapter/out/persistence/problem/ProblemEntity.java @@ -0,0 +1,121 @@ +package com.devaulty.backend.adapter.out.persistence.problem; + +import com.devaulty.backend.adapter.out.persistence.base.BaseJpaEntity; +import com.devaulty.backend.adapter.out.persistence.project.ProjectEntity; +import com.devaulty.backend.domain.model.enums.ProblemSeverity; +import com.devaulty.backend.domain.model.enums.ProblemStatus; +import jakarta.persistence.*; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; + +import java.util.Objects; +import java.util.UUID; + +@Table(name = "problems") +@Entity +public class ProblemEntity extends BaseJpaEntity { + + @Id + @JdbcTypeCode(SqlTypes.VARCHAR) + private UUID id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "project_id", nullable = false) + private ProjectEntity project; + + @Column(nullable = false) + private String title; + + @Column(name = "error_description") + private String errorDescription; + + private String solution; + + @Enumerated(EnumType.STRING) + private ProblemStatus status = ProblemStatus.OPEN; + + @Enumerated(EnumType.STRING) + private ProblemSeverity severity = ProblemSeverity.MEDIUM; + + public ProblemEntity() { + // Empty constructor + } + + public ProblemEntity(UUID id, ProjectEntity project, String title, String errorDescription, String solution, ProblemStatus status, ProblemSeverity severity) { + this.id = id; + this.project = project; + this.title = title; + this.errorDescription = errorDescription; + this.solution = solution; + this.status = status; + this.severity = severity; + } + + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public ProjectEntity getProject() { + return project; + } + + public void setProject(ProjectEntity project) { + this.project = project; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getErrorDescription() { + return errorDescription; + } + + public void setErrorDescription(String errorDescription) { + this.errorDescription = errorDescription; + } + + public String getSolution() { + return solution; + } + + public void setSolution(String solution) { + this.solution = solution; + } + + public ProblemStatus getStatus() { + return status; + } + + public void setStatus(ProblemStatus status) { + this.status = status; + } + + public ProblemSeverity getSeverity() { + return severity; + } + + public void setSeverity(ProblemSeverity severity) { + this.severity = severity; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + ProblemEntity that = (ProblemEntity) o; + return Objects.equals(id, that.id); + } + + @Override + public int hashCode() { + return Objects.hashCode(id); + } +} diff --git a/backend/src/main/java/com/devaulty/backend/adapter/out/persistence/problem/ProblemMapper.java b/backend/src/main/java/com/devaulty/backend/adapter/out/persistence/problem/ProblemMapper.java new file mode 100644 index 0000000..4a54049 --- /dev/null +++ b/backend/src/main/java/com/devaulty/backend/adapter/out/persistence/problem/ProblemMapper.java @@ -0,0 +1,15 @@ +package com.devaulty.backend.adapter.out.persistence.problem; + +import com.devaulty.backend.domain.model.Problem; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +@Mapper(componentModel = "spring") +public interface ProblemMapper { + + @Mapping(target = "project", ignore = true) + ProblemEntity toEntity(Problem problem); + + @Mapping(target = "projectId", source = "project.id") + Problem toDomain(ProblemEntity problemEntity); +} diff --git a/backend/src/main/java/com/devaulty/backend/adapter/out/persistence/problem/ProblemPersistenceAdapter.java b/backend/src/main/java/com/devaulty/backend/adapter/out/persistence/problem/ProblemPersistenceAdapter.java new file mode 100644 index 0000000..3dd1a55 --- /dev/null +++ b/backend/src/main/java/com/devaulty/backend/adapter/out/persistence/problem/ProblemPersistenceAdapter.java @@ -0,0 +1,53 @@ +package com.devaulty.backend.adapter.out.persistence.problem; + +import com.devaulty.backend.adapter.out.persistence.project.SpringDataProjectRepository; +import com.devaulty.backend.application.port.out.persistence.ProblemRepositoryPort; +import com.devaulty.backend.domain.model.Problem; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Component; + +import java.util.Optional; +import java.util.UUID; + +@Component +public class ProblemPersistenceAdapter implements ProblemRepositoryPort { + + private final SpringDataProblemRepository problemRepository; + private final SpringDataProjectRepository projectRepository; + private final ProblemMapper problemMapper; + + public ProblemPersistenceAdapter(SpringDataProblemRepository problemRepository, SpringDataProjectRepository projectRepository, ProblemMapper problemMapper) { + this.problemRepository = problemRepository; + this.projectRepository = projectRepository; + this.problemMapper = problemMapper; + } + + @Override + public Problem save(Problem problem) { + ProblemEntity entity = problemMapper.toEntity(problem); + entity.setProject(projectRepository.getReferenceById(problem.getProjectId())); + ProblemEntity savedEntity = problemRepository.save(entity); + return problemMapper.toDomain(savedEntity); + } + + @Override + public Optional findById(UUID id) { + Optional entity = problemRepository.findById(id); + return entity.map(problemMapper::toDomain); + } + + @Override + public Page findAllByProject(UUID projectId, int page, int size) { + Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "createdAt")); + Page entities = problemRepository.findAllByProject_Id(projectId, pageable); + return entities.map(problemMapper::toDomain); + } + + @Override + public void deleteById(UUID id) { + problemRepository.deleteById(id); + } +} diff --git a/backend/src/main/java/com/devaulty/backend/adapter/out/persistence/problem/SpringDataProblemRepository.java b/backend/src/main/java/com/devaulty/backend/adapter/out/persistence/problem/SpringDataProblemRepository.java new file mode 100644 index 0000000..5b30d48 --- /dev/null +++ b/backend/src/main/java/com/devaulty/backend/adapter/out/persistence/problem/SpringDataProblemRepository.java @@ -0,0 +1,11 @@ +package com.devaulty.backend.adapter.out.persistence.problem; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.UUID; + +public interface SpringDataProblemRepository extends JpaRepository { + Page findAllByProject_Id(UUID projectId, Pageable pageable); +} diff --git a/backend/src/main/java/com/devaulty/backend/application/impl/problem/CreateProblemImpl.java b/backend/src/main/java/com/devaulty/backend/application/impl/problem/CreateProblemImpl.java new file mode 100644 index 0000000..624475a --- /dev/null +++ b/backend/src/main/java/com/devaulty/backend/application/impl/problem/CreateProblemImpl.java @@ -0,0 +1,42 @@ +package com.devaulty.backend.application.impl.problem; + +import com.devaulty.backend.application.exception.ResourceNotFoundException; +import com.devaulty.backend.application.port.in.problem.CreateProblemCommand; +import com.devaulty.backend.application.port.in.problem.CreateProblemUseCase; +import com.devaulty.backend.application.port.out.persistence.ProblemRepositoryPort; +import com.devaulty.backend.application.port.out.persistence.ProjectRepositoryPort; +import com.devaulty.backend.domain.model.Problem; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.UUID; + +public class CreateProblemImpl implements CreateProblemUseCase { + + private final ProblemRepositoryPort problemRepositoryPort; + private final ProjectRepositoryPort projectRepositoryPort; + + public CreateProblemImpl(ProblemRepositoryPort problemRepositoryPort, ProjectRepositoryPort projectRepositoryPort) { + this.problemRepositoryPort = problemRepositoryPort; + this.projectRepositoryPort = projectRepositoryPort; + } + + @Override + @Transactional + public Problem execute(CreateProblemCommand command) { + + if (!projectRepositoryPort.existsById(command.projectId())) throw new ResourceNotFoundException("Project", command.projectId()); + + Problem problem = new Problem(); + problem.setId(UUID.randomUUID()); + problem.setProjectId(command.projectId()); + problem.setTitle(command.title()); + problem.setErrorDescription(command.errorDescription()); + problem.setSolution(command.solution()); + problem.setStatus(command.status()); + problem.setSeverity(command.severity()); + problem.setCreatedAt(LocalDateTime.now()); + + return problemRepositoryPort.save(problem); + } +} diff --git a/backend/src/main/java/com/devaulty/backend/application/impl/problem/DeleteProblemImpl.java b/backend/src/main/java/com/devaulty/backend/application/impl/problem/DeleteProblemImpl.java new file mode 100644 index 0000000..66957d3 --- /dev/null +++ b/backend/src/main/java/com/devaulty/backend/application/impl/problem/DeleteProblemImpl.java @@ -0,0 +1,35 @@ +package com.devaulty.backend.application.impl.problem; + +import com.devaulty.backend.application.exception.ResourceNotFoundException; +import com.devaulty.backend.application.port.in.problem.DeleteProblemUseCase; +import com.devaulty.backend.application.port.out.persistence.ProblemRepositoryPort; +import com.devaulty.backend.application.port.out.persistence.ProjectRepositoryPort; +import org.springframework.transaction.annotation.Transactional; + +import java.util.UUID; + +public class DeleteProblemImpl implements DeleteProblemUseCase { + + private final ProblemRepositoryPort problemRepositoryPort; + private final ProjectRepositoryPort projectRepositoryPort; + + public DeleteProblemImpl(ProblemRepositoryPort problemRepositoryPort, ProjectRepositoryPort projectRepositoryPort) { + this.problemRepositoryPort = problemRepositoryPort; + this.projectRepositoryPort = projectRepositoryPort; + } + + @Override + @Transactional + public void execute(UUID projectId, UUID id) { + if(!projectRepositoryPort.existsById(projectId)) throw new ResourceNotFoundException("Project", projectId); + + if (problemRepositoryPort.findById(id) + .filter(problem -> projectId.equals(problem.getProjectId())) + .isEmpty()) { + throw new ResourceNotFoundException("Problem", id); + } + + problemRepositoryPort.deleteById(id); + + } +} diff --git a/backend/src/main/java/com/devaulty/backend/application/impl/problem/GetAllProblemsByProjectImpl.java b/backend/src/main/java/com/devaulty/backend/application/impl/problem/GetAllProblemsByProjectImpl.java new file mode 100644 index 0000000..b397f99 --- /dev/null +++ b/backend/src/main/java/com/devaulty/backend/application/impl/problem/GetAllProblemsByProjectImpl.java @@ -0,0 +1,31 @@ +package com.devaulty.backend.application.impl.problem; + +import com.devaulty.backend.application.exception.ResourceNotFoundException; +import com.devaulty.backend.application.port.in.problem.GetAllProblemsByProjectUseCase; +import com.devaulty.backend.application.port.out.persistence.ProblemRepositoryPort; +import com.devaulty.backend.application.port.out.persistence.ProjectRepositoryPort; +import com.devaulty.backend.domain.model.Problem; +import org.springframework.data.domain.Page; +import org.springframework.transaction.annotation.Transactional; + +import java.util.UUID; + +public class GetAllProblemsByProjectImpl implements GetAllProblemsByProjectUseCase { + + private final ProblemRepositoryPort problemRepositoryPort; + private final ProjectRepositoryPort projectRepositoryPort; + + public GetAllProblemsByProjectImpl(ProblemRepositoryPort problemRepositoryPort, ProjectRepositoryPort projectRepositoryPort) { + this.problemRepositoryPort = problemRepositoryPort; + this.projectRepositoryPort = projectRepositoryPort; + } + + @Override + @Transactional(readOnly = true) + public Page execute(UUID projectId, int page, int size) { + + if(!projectRepositoryPort.existsById(projectId)) throw new ResourceNotFoundException("Project", projectId); + + return problemRepositoryPort.findAllByProject(projectId, page, size); + } +} diff --git a/backend/src/main/java/com/devaulty/backend/application/impl/problem/GetProblemByIdImpl.java b/backend/src/main/java/com/devaulty/backend/application/impl/problem/GetProblemByIdImpl.java new file mode 100644 index 0000000..95b5624 --- /dev/null +++ b/backend/src/main/java/com/devaulty/backend/application/impl/problem/GetProblemByIdImpl.java @@ -0,0 +1,35 @@ +package com.devaulty.backend.application.impl.problem; + +import com.devaulty.backend.application.exception.ResourceNotFoundException; +import com.devaulty.backend.application.port.in.problem.GetProblemByIdUseCase; +import com.devaulty.backend.application.port.out.persistence.ProblemRepositoryPort; +import com.devaulty.backend.application.port.out.persistence.ProjectRepositoryPort; +import com.devaulty.backend.domain.model.Problem; +import org.springframework.transaction.annotation.Transactional; + +import java.util.UUID; + +public class GetProblemByIdImpl implements GetProblemByIdUseCase { + + private final ProblemRepositoryPort problemRepositoryPort; + private final ProjectRepositoryPort projectRepositoryPort; + + public GetProblemByIdImpl(ProblemRepositoryPort problemRepositoryPort, ProjectRepositoryPort projectRepositoryPort) { + this.problemRepositoryPort = problemRepositoryPort; + this.projectRepositoryPort = projectRepositoryPort; + } + + @Override + @Transactional(readOnly = true) + public Problem execute(UUID projectId, UUID id) { + + if (!projectRepositoryPort.existsById(projectId)) throw new ResourceNotFoundException("Project", id); + + Problem problem = problemRepositoryPort.findById(id) + .orElseThrow(() -> new ResourceNotFoundException("Problem", id)); + + if(!problem.getProjectId().equals(projectId)) throw new ResourceNotFoundException("Problem", id); + + return problem; + } +} diff --git a/backend/src/main/java/com/devaulty/backend/application/impl/problem/UpdateProblemImpl.java b/backend/src/main/java/com/devaulty/backend/application/impl/problem/UpdateProblemImpl.java new file mode 100644 index 0000000..3f51316 --- /dev/null +++ b/backend/src/main/java/com/devaulty/backend/application/impl/problem/UpdateProblemImpl.java @@ -0,0 +1,42 @@ +package com.devaulty.backend.application.impl.problem; + +import com.devaulty.backend.application.exception.ResourceNotFoundException; +import com.devaulty.backend.application.port.in.problem.UpdateProblemCommand; +import com.devaulty.backend.application.port.in.problem.UpdateProblemUseCase; +import com.devaulty.backend.application.port.out.persistence.ProblemRepositoryPort; +import com.devaulty.backend.application.port.out.persistence.ProjectRepositoryPort; +import com.devaulty.backend.domain.model.Problem; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; + +public class UpdateProblemImpl implements UpdateProblemUseCase { + + private final ProblemRepositoryPort problemRepositoryPort; + private final ProjectRepositoryPort projectRepositoryPort; + + public UpdateProblemImpl(ProblemRepositoryPort problemRepositoryPort, ProjectRepositoryPort projectRepositoryPort) { + this.problemRepositoryPort = problemRepositoryPort; + this.projectRepositoryPort = projectRepositoryPort; + } + + @Override + @Transactional + public Problem execute(UpdateProblemCommand command) { + + if(!projectRepositoryPort.existsById(command.projectId())) throw new ResourceNotFoundException("Project", command.projectId()); + + Problem problem = problemRepositoryPort.findById(command.id()) + .orElseThrow(() -> new ResourceNotFoundException("Problem", command.id())); + + if(!problem.getProjectId().equals(command.projectId())) throw new ResourceNotFoundException("Problem", command.id()); + + if (command.title() != null ) problem.setTitle(command.title()); + if (command.errorDescription() != null ) problem.setErrorDescription(command.errorDescription()); + if (command.solution() != null ) problem.setSolution(command.solution()); + if (command.severity() != null ) problem.setSeverity(command.severity()); + problem.setUpdatedAt(LocalDateTime.now()); + + return problemRepositoryPort.save(problem); + } +} diff --git a/backend/src/main/java/com/devaulty/backend/application/impl/problem/UpdateProblemStatusImpl.java b/backend/src/main/java/com/devaulty/backend/application/impl/problem/UpdateProblemStatusImpl.java new file mode 100644 index 0000000..460f5b5 --- /dev/null +++ b/backend/src/main/java/com/devaulty/backend/application/impl/problem/UpdateProblemStatusImpl.java @@ -0,0 +1,39 @@ +package com.devaulty.backend.application.impl.problem; + +import com.devaulty.backend.application.exception.ResourceNotFoundException; +import com.devaulty.backend.application.port.in.problem.UpdateProblemStatusCommand; +import com.devaulty.backend.application.port.in.problem.UpdateProblemStatusUseCase; +import com.devaulty.backend.application.port.out.persistence.ProblemRepositoryPort; +import com.devaulty.backend.application.port.out.persistence.ProjectRepositoryPort; +import com.devaulty.backend.domain.model.Problem; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; + +public class UpdateProblemStatusImpl implements UpdateProblemStatusUseCase { + + private final ProblemRepositoryPort problemRepositoryPort; + private final ProjectRepositoryPort projectRepositoryPort; + + public UpdateProblemStatusImpl(ProblemRepositoryPort problemRepositoryPort, ProjectRepositoryPort projectRepositoryPort) { + this.problemRepositoryPort = problemRepositoryPort; + this.projectRepositoryPort = projectRepositoryPort; + } + + @Override + @Transactional + public Problem execute(UpdateProblemStatusCommand command) { + + if(!projectRepositoryPort.existsById(command.projectId())) throw new ResourceNotFoundException("Project", command.projectId()); + + Problem problem = problemRepositoryPort.findById(command.id()) + .orElseThrow(() -> new ResourceNotFoundException("Problem", command.id())); + + if(!problem.getProjectId().equals(command.projectId())) throw new ResourceNotFoundException("Problem", command.id()); + + problem.setStatus(command.status()); + problem.setUpdatedAt(LocalDateTime.now()); + + return problemRepositoryPort.save(problem); + } +} diff --git a/backend/src/main/java/com/devaulty/backend/application/port/in/problem/CreateProblemCommand.java b/backend/src/main/java/com/devaulty/backend/application/port/in/problem/CreateProblemCommand.java new file mode 100644 index 0000000..f2739b2 --- /dev/null +++ b/backend/src/main/java/com/devaulty/backend/application/port/in/problem/CreateProblemCommand.java @@ -0,0 +1,16 @@ +package com.devaulty.backend.application.port.in.problem; + +import com.devaulty.backend.domain.model.enums.ProblemSeverity; +import com.devaulty.backend.domain.model.enums.ProblemStatus; + +import java.util.UUID; + +public record CreateProblemCommand( + UUID projectId, + String title, + String errorDescription, + String solution, + ProblemStatus status, + ProblemSeverity severity +) { +} diff --git a/backend/src/main/java/com/devaulty/backend/application/port/in/problem/CreateProblemUseCase.java b/backend/src/main/java/com/devaulty/backend/application/port/in/problem/CreateProblemUseCase.java new file mode 100644 index 0000000..e4291ff --- /dev/null +++ b/backend/src/main/java/com/devaulty/backend/application/port/in/problem/CreateProblemUseCase.java @@ -0,0 +1,7 @@ +package com.devaulty.backend.application.port.in.problem; + +import com.devaulty.backend.domain.model.Problem; + +public interface CreateProblemUseCase { + Problem execute(CreateProblemCommand command); +} diff --git a/backend/src/main/java/com/devaulty/backend/application/port/in/problem/DeleteProblemUseCase.java b/backend/src/main/java/com/devaulty/backend/application/port/in/problem/DeleteProblemUseCase.java new file mode 100644 index 0000000..c096fe8 --- /dev/null +++ b/backend/src/main/java/com/devaulty/backend/application/port/in/problem/DeleteProblemUseCase.java @@ -0,0 +1,7 @@ +package com.devaulty.backend.application.port.in.problem; + +import java.util.UUID; + +public interface DeleteProblemUseCase { + void execute(UUID projectId, UUID id); +} diff --git a/backend/src/main/java/com/devaulty/backend/application/port/in/problem/GetAllProblemsByProjectUseCase.java b/backend/src/main/java/com/devaulty/backend/application/port/in/problem/GetAllProblemsByProjectUseCase.java new file mode 100644 index 0000000..4763b28 --- /dev/null +++ b/backend/src/main/java/com/devaulty/backend/application/port/in/problem/GetAllProblemsByProjectUseCase.java @@ -0,0 +1,10 @@ +package com.devaulty.backend.application.port.in.problem; + +import com.devaulty.backend.domain.model.Problem; +import org.springframework.data.domain.Page; + +import java.util.UUID; + +public interface GetAllProblemsByProjectUseCase { + Page execute(UUID projectId, int page, int size); +} diff --git a/backend/src/main/java/com/devaulty/backend/application/port/in/problem/GetProblemByIdUseCase.java b/backend/src/main/java/com/devaulty/backend/application/port/in/problem/GetProblemByIdUseCase.java new file mode 100644 index 0000000..ad0301e --- /dev/null +++ b/backend/src/main/java/com/devaulty/backend/application/port/in/problem/GetProblemByIdUseCase.java @@ -0,0 +1,9 @@ +package com.devaulty.backend.application.port.in.problem; + +import com.devaulty.backend.domain.model.Problem; + +import java.util.UUID; + +public interface GetProblemByIdUseCase { + Problem execute(UUID projectId,UUID id); +} diff --git a/backend/src/main/java/com/devaulty/backend/application/port/in/problem/UpdateProblemCommand.java b/backend/src/main/java/com/devaulty/backend/application/port/in/problem/UpdateProblemCommand.java new file mode 100644 index 0000000..d84dbe4 --- /dev/null +++ b/backend/src/main/java/com/devaulty/backend/application/port/in/problem/UpdateProblemCommand.java @@ -0,0 +1,16 @@ +package com.devaulty.backend.application.port.in.problem; + +import com.devaulty.backend.domain.model.enums.ProblemSeverity; + +import java.util.UUID; + +public record UpdateProblemCommand( + UUID id, + UUID projectId, + String title, + String errorDescription, + String solution, + // Problem Status has a dedicated update logic + ProblemSeverity severity +) { +} diff --git a/backend/src/main/java/com/devaulty/backend/application/port/in/problem/UpdateProblemStatusCommand.java b/backend/src/main/java/com/devaulty/backend/application/port/in/problem/UpdateProblemStatusCommand.java new file mode 100644 index 0000000..c0ded68 --- /dev/null +++ b/backend/src/main/java/com/devaulty/backend/application/port/in/problem/UpdateProblemStatusCommand.java @@ -0,0 +1,12 @@ +package com.devaulty.backend.application.port.in.problem; + +import com.devaulty.backend.domain.model.enums.ProblemStatus; + +import java.util.UUID; + +public record UpdateProblemStatusCommand( + UUID id, + UUID projectId, + ProblemStatus status +) { +} diff --git a/backend/src/main/java/com/devaulty/backend/application/port/in/problem/UpdateProblemStatusUseCase.java b/backend/src/main/java/com/devaulty/backend/application/port/in/problem/UpdateProblemStatusUseCase.java new file mode 100644 index 0000000..468dfac --- /dev/null +++ b/backend/src/main/java/com/devaulty/backend/application/port/in/problem/UpdateProblemStatusUseCase.java @@ -0,0 +1,7 @@ +package com.devaulty.backend.application.port.in.problem; + +import com.devaulty.backend.domain.model.Problem; + +public interface UpdateProblemStatusUseCase { + Problem execute(UpdateProblemStatusCommand command); +} diff --git a/backend/src/main/java/com/devaulty/backend/application/port/in/problem/UpdateProblemUseCase.java b/backend/src/main/java/com/devaulty/backend/application/port/in/problem/UpdateProblemUseCase.java new file mode 100644 index 0000000..ce416e6 --- /dev/null +++ b/backend/src/main/java/com/devaulty/backend/application/port/in/problem/UpdateProblemUseCase.java @@ -0,0 +1,7 @@ +package com.devaulty.backend.application.port.in.problem; + +import com.devaulty.backend.domain.model.Problem; + +public interface UpdateProblemUseCase { + Problem execute(UpdateProblemCommand command); +} diff --git a/backend/src/main/java/com/devaulty/backend/application/port/out/persistence/ProblemRepositoryPort.java b/backend/src/main/java/com/devaulty/backend/application/port/out/persistence/ProblemRepositoryPort.java new file mode 100644 index 0000000..0000a53 --- /dev/null +++ b/backend/src/main/java/com/devaulty/backend/application/port/out/persistence/ProblemRepositoryPort.java @@ -0,0 +1,17 @@ +package com.devaulty.backend.application.port.out.persistence; + +import com.devaulty.backend.domain.model.Problem; +import org.springframework.data.domain.Page; + +import java.util.Optional; +import java.util.UUID; + +public interface ProblemRepositoryPort { + Problem save(Problem problem); + + Optional findById(UUID id); + + Page findAllByProject(UUID projectId, int page, int size); + + void deleteById(UUID id); +} diff --git a/backend/src/main/java/com/devaulty/backend/domain/model/Problem.java b/backend/src/main/java/com/devaulty/backend/domain/model/Problem.java new file mode 100644 index 0000000..1aa8e8c --- /dev/null +++ b/backend/src/main/java/com/devaulty/backend/domain/model/Problem.java @@ -0,0 +1,101 @@ +package com.devaulty.backend.domain.model; + +import com.devaulty.backend.domain.model.base.BaseEntity; +import com.devaulty.backend.domain.model.enums.ProblemSeverity; +import com.devaulty.backend.domain.model.enums.ProblemStatus; + +import java.util.Objects; +import java.util.UUID; + +public class Problem extends BaseEntity { + + private UUID id; + private UUID projectId; + private String title; + private String errorDescription; + private String solution; + private ProblemStatus status = ProblemStatus.OPEN; + private ProblemSeverity severity = ProblemSeverity.MEDIUM; // Medium by default + + public Problem() { + // Empty constructor + } + + public Problem(UUID id, UUID projectId, String title, String errorDescription, String solution, ProblemStatus status, ProblemSeverity severity) { + this.id = id; + this.projectId = projectId; + this.title = title; + this.errorDescription = errorDescription; + this.solution = solution; + this.status = status; + this.severity = severity; + } + + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public UUID getProjectId() { + return projectId; + } + + public void setProjectId(UUID projectId) { + this.projectId = projectId; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getErrorDescription() { + return errorDescription; + } + + public void setErrorDescription(String errorDescription) { + this.errorDescription = errorDescription; + } + + public String getSolution() { + return solution; + } + + public void setSolution(String solution) { + this.solution = solution; + } + + public ProblemStatus getStatus() { + return status; + } + + public void setStatus(ProblemStatus status) { + this.status = status; + } + + public ProblemSeverity getSeverity() { + return severity; + } + + public void setSeverity(ProblemSeverity severity) { + this.severity = severity; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + Problem problem = (Problem) o; + return Objects.equals(id, problem.id); + } + + @Override + public int hashCode() { + return Objects.hashCode(id); + } +} diff --git a/backend/src/main/java/com/devaulty/backend/domain/model/enums/ProblemSeverity.java b/backend/src/main/java/com/devaulty/backend/domain/model/enums/ProblemSeverity.java new file mode 100644 index 0000000..2d8cfbe --- /dev/null +++ b/backend/src/main/java/com/devaulty/backend/domain/model/enums/ProblemSeverity.java @@ -0,0 +1,8 @@ +package com.devaulty.backend.domain.model.enums; + +public enum ProblemSeverity { + LOW, + MEDIUM, + HIGH, + CRITICAL +} diff --git a/backend/src/main/java/com/devaulty/backend/domain/model/enums/ProblemStatus.java b/backend/src/main/java/com/devaulty/backend/domain/model/enums/ProblemStatus.java new file mode 100644 index 0000000..7f5d0e3 --- /dev/null +++ b/backend/src/main/java/com/devaulty/backend/domain/model/enums/ProblemStatus.java @@ -0,0 +1,8 @@ +package com.devaulty.backend.domain.model.enums; + +public enum ProblemStatus { + OPEN, + WORKING_ON, + RESOLVED, + WONT_FIX +} diff --git a/backend/src/main/java/com/devaulty/backend/infrastructure/configuration/ProblemBeanConfig.java b/backend/src/main/java/com/devaulty/backend/infrastructure/configuration/ProblemBeanConfig.java new file mode 100644 index 0000000..ec913f4 --- /dev/null +++ b/backend/src/main/java/com/devaulty/backend/infrastructure/configuration/ProblemBeanConfig.java @@ -0,0 +1,78 @@ +package com.devaulty.backend.infrastructure.configuration; + +import com.devaulty.backend.application.impl.problem.*; +import com.devaulty.backend.application.port.in.problem.*; +import com.devaulty.backend.application.port.out.persistence.ProblemRepositoryPort; +import com.devaulty.backend.application.port.out.persistence.ProjectRepositoryPort; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class ProblemBeanConfig { + + @Bean + public CreateProblemUseCase createProblemUseCase( + ProblemRepositoryPort problemRepositoryPort, + ProjectRepositoryPort projectRepositoryPort + ){ + return new CreateProblemImpl( + problemRepositoryPort, + projectRepositoryPort + ); + } + + @Bean + public GetAllProblemsByProjectUseCase getAllProblemsByProjectUseCase( + ProblemRepositoryPort problemRepositoryPort, + ProjectRepositoryPort projectRepositoryPort + ){ + return new GetAllProblemsByProjectImpl( + problemRepositoryPort, + projectRepositoryPort + ); + } + + @Bean + public GetProblemByIdUseCase getProblemByIdUseCase( + ProblemRepositoryPort problemRepositoryPort, + ProjectRepositoryPort projectRepositoryPort + ){ + return new GetProblemByIdImpl( + problemRepositoryPort, + projectRepositoryPort + ); + } + + @Bean + public DeleteProblemUseCase deleteProblemUseCase( + ProblemRepositoryPort problemRepositoryPort, + ProjectRepositoryPort projectRepositoryPort + ){ + return new DeleteProblemImpl( + problemRepositoryPort, + projectRepositoryPort + ); + } + + @Bean + public UpdateProblemUseCase updateProblemUseCase( + ProblemRepositoryPort problemRepositoryPort, + ProjectRepositoryPort projectRepositoryPort + ){ + return new UpdateProblemImpl( + problemRepositoryPort, + projectRepositoryPort + ); + } + + @Bean + public UpdateProblemStatusUseCase updateProblemStatusUseCase( + ProblemRepositoryPort problemRepositoryPort, + ProjectRepositoryPort projectRepositoryPort + ){ + return new UpdateProblemStatusImpl( + problemRepositoryPort, + projectRepositoryPort + ); + } +} diff --git a/backend/src/main/resources/db/changelog/changesets/004-create-problems-table.yaml b/backend/src/main/resources/db/changelog/changesets/004-create-problems-table.yaml new file mode 100644 index 0000000..68b6249 --- /dev/null +++ b/backend/src/main/resources/db/changelog/changesets/004-create-problems-table.yaml @@ -0,0 +1,70 @@ +databaseChangeLog: + - changeSet: + id: 004-create-problems-table + author: Matheus Cunha + changes: + - createTable: + tableName: problems + columns: + - column: + name: id + type: TEXT + constraints: + primaryKey: true + nullable: false + + - column: + name: project_id + type: TEXT + constraints: + nullable: false + foreignKeyName: fk_problems_project + references: projects(id) + deleteCascade: true + + - column: + name: title + type: TEXT + constraints: + nullable: false + + - column: + name: error_description + type: TEXT + + - column: + name: solution + type: TEXT + + - column: + name: status + type: TEXT + constraints: + nullable: false + + - column: + name: severity + type: TEXT + constraints: + nullable: false + + - column: + name: created_at + type: TEXT + constraints: + nullable: false + + - column: + name: updated_at + type: TEXT + + - createIndex: + indexName: idx_problems_project + tableName: problems + columns: + - column: + name: project_id + + rollback: + - dropTable: + tableName: problems \ No newline at end of file diff --git a/backend/src/main/resources/db/changelog/db.changelog-master.yaml b/backend/src/main/resources/db/changelog/db.changelog-master.yaml index 4a1e450..e10b322 100644 --- a/backend/src/main/resources/db/changelog/db.changelog-master.yaml +++ b/backend/src/main/resources/db/changelog/db.changelog-master.yaml @@ -6,4 +6,7 @@ databaseChangeLog: file: db/changelog/changesets/002-create-snippets-table.yaml - include: - file: db/changelog/changesets/003-create-links-table.yaml \ No newline at end of file + file: db/changelog/changesets/003-create-links-table.yaml + + - include: + file: db/changelog/changesets/004-create-problems-table.yaml \ No newline at end of file From 88085a07844318241cc64cc92f59fd54156470c6 Mon Sep 17 00:00:00 2001 From: Matheus Cunha Date: Sun, 12 Jul 2026 03:58:44 -0300 Subject: [PATCH 2/3] Add unit tests for Problem Management module's use cases and enhance REST API with interface documentation and annotations --- .../adapter/in/web/problem/ProblemApi.java | 251 +++++++++++++++ .../in/web/problem/ProblemController.java | 6 + .../in/web/problem/ProblemControllerIT.java | 302 ++++++++++++++++++ .../impl/problem/CreateProblemImplTest.java | 91 ++++++ .../impl/problem/DeleteProblemImplTest.java | 111 +++++++ .../GetAllProblemsByProjectImplTest.java | 81 +++++ .../impl/problem/GetProblemByIdImplTest.java | 115 +++++++ .../impl/problem/UpdateProblemImplTest.java | 181 +++++++++++ .../problem/UpdateProblemStatusImplTest.java | 126 ++++++++ 9 files changed, 1264 insertions(+) create mode 100644 backend/src/test/java/com/devaulty/backend/adapter/in/web/problem/ProblemControllerIT.java create mode 100644 backend/src/test/java/com/devaulty/backend/application/impl/problem/CreateProblemImplTest.java create mode 100644 backend/src/test/java/com/devaulty/backend/application/impl/problem/DeleteProblemImplTest.java create mode 100644 backend/src/test/java/com/devaulty/backend/application/impl/problem/GetAllProblemsByProjectImplTest.java create mode 100644 backend/src/test/java/com/devaulty/backend/application/impl/problem/GetProblemByIdImplTest.java create mode 100644 backend/src/test/java/com/devaulty/backend/application/impl/problem/UpdateProblemImplTest.java create mode 100644 backend/src/test/java/com/devaulty/backend/application/impl/problem/UpdateProblemStatusImplTest.java diff --git a/backend/src/main/java/com/devaulty/backend/adapter/in/web/problem/ProblemApi.java b/backend/src/main/java/com/devaulty/backend/adapter/in/web/problem/ProblemApi.java index 71fe483..3062b50 100644 --- a/backend/src/main/java/com/devaulty/backend/adapter/in/web/problem/ProblemApi.java +++ b/backend/src/main/java/com/devaulty/backend/adapter/in/web/problem/ProblemApi.java @@ -1,4 +1,255 @@ package com.devaulty.backend.adapter.in.web.problem; +import com.devaulty.backend.adapter.in.web.exception.ApiErrorResponse; +import com.devaulty.backend.adapter.in.web.problem.dto.*; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import org.springframework.data.domain.Page; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.UUID; + +@Tag(name = "Problems", description = "Endpoints for managing problems/errors encountered within a project") public interface ProblemApi { + + @Operation( + summary = "Create a new problem log", + description = "Logs a new problem associated with the specified project ID." + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "201", + description = "Problem logged successfully", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ProblemViewResponse.class) + ) + ), + @ApiResponse( + responseCode = "400", + description = "Invalid request body or validation constraints failed", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ApiErrorResponse.class) + ) + ), + @ApiResponse( + responseCode = "404", + description = "Project not found", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ApiErrorResponse.class) + ) + ), + @ApiResponse( + responseCode = "500", + description = "Internal server error occurred", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ApiErrorResponse.class) + ) + ) + }) + @PostMapping + ResponseEntity create( + @PathVariable UUID projectId, + @RequestBody @Valid CreateProblemRequest request + ); + + @Operation( + summary = "Get all problems by project", + description = "Retrieves a paginated list of all problem summaries belonging to the specified project ID." + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "Successfully retrieved list of problem summaries" + ), + @ApiResponse( + responseCode = "404", + description = "Project not found", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ApiErrorResponse.class) + ) + ), + @ApiResponse( + responseCode = "500", + description = "Internal server error occurred", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ApiErrorResponse.class) + ) + ) + }) + @GetMapping + ResponseEntity> getAllByProject( + @PathVariable UUID projectId, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "10") int size + ); + + @Operation( + summary = "Get problem details by ID", + description = "Retrieves details of a single problem by its unique identifier and its project ID." + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "Successfully retrieved the problem details", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ProblemViewResponse.class) + ) + ), + @ApiResponse( + responseCode = "404", + description = "Project or Problem not found", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ApiErrorResponse.class) + ) + ), + @ApiResponse( + responseCode = "500", + description = "Internal server error occurred", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ApiErrorResponse.class) + ) + ) + }) + @GetMapping("/{problemId}") + ResponseEntity getById( + @PathVariable UUID projectId, + @PathVariable UUID problemId + ); + + @Operation( + summary = "Update a problem", + description = "Updates details of an existing problem by its unique identifier and its project ID." + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "Problem updated successfully", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ProblemViewResponse.class) + ) + ), + @ApiResponse( + responseCode = "400", + description = "Invalid request body or validation constraints failed", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ApiErrorResponse.class) + ) + ), + @ApiResponse( + responseCode = "404", + description = "Project or Problem not found", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ApiErrorResponse.class) + ) + ), + @ApiResponse( + responseCode = "500", + description = "Internal server error occurred", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ApiErrorResponse.class) + ) + ) + }) + @PatchMapping("/{problemId}") + ResponseEntity update( + @PathVariable UUID projectId, + @PathVariable UUID problemId, + @RequestBody @Valid UpdateProblemRequest request + ); + + @Operation( + summary = "Update problem status", + description = "Updates status of an existing problem by its unique identifier and its project ID." + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "Problem status updated successfully", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ProblemViewResponse.class) + ) + ), + @ApiResponse( + responseCode = "400", + description = "Invalid request body or validation constraints failed", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ApiErrorResponse.class) + ) + ), + @ApiResponse( + responseCode = "404", + description = "Project or Problem not found", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ApiErrorResponse.class) + ) + ), + @ApiResponse( + responseCode = "500", + description = "Internal server error occurred", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ApiErrorResponse.class) + ) + ) + }) + @PatchMapping("/{problemId}/status") + ResponseEntity updateStatus( + @PathVariable UUID projectId, + @PathVariable UUID problemId, + @RequestBody @Valid UpdateProblemStatusRequest request + ); + + @Operation( + summary = "Delete a problem log", + description = "Deletes an existing problem log by its unique identifier and its project ID." + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "204", + description = "Problem deleted successfully" + ), + @ApiResponse( + responseCode = "404", + description = "Project or Problem not found", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ApiErrorResponse.class) + ) + ), + @ApiResponse( + responseCode = "500", + description = "Internal server error occurred", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ApiErrorResponse.class) + ) + ) + }) + @DeleteMapping("/{problemId}") + ResponseEntity delete( + @PathVariable UUID projectId, + @PathVariable UUID problemId + ); } diff --git a/backend/src/main/java/com/devaulty/backend/adapter/in/web/problem/ProblemController.java b/backend/src/main/java/com/devaulty/backend/adapter/in/web/problem/ProblemController.java index 6989387..04b4f63 100644 --- a/backend/src/main/java/com/devaulty/backend/adapter/in/web/problem/ProblemController.java +++ b/backend/src/main/java/com/devaulty/backend/adapter/in/web/problem/ProblemController.java @@ -36,6 +36,7 @@ public ProblemController(CreateProblemUseCase createProblemUseCase, GetAllProble this.uriLocationBuilderHelper = uriLocationBuilderHelper; } + @Override @PostMapping public ResponseEntity create( @PathVariable UUID projectId, @@ -47,6 +48,7 @@ public ResponseEntity create( return ResponseEntity.created(location).body(mapper.toViewResponse(problem)); } + @Override @GetMapping public ResponseEntity> getAllByProject( @PathVariable UUID projectId, @@ -57,6 +59,7 @@ public ResponseEntity> getAllByProject( return ResponseEntity.ok(problems.map(mapper::toSummaryResponse)); } + @Override @GetMapping("/{problemId}") public ResponseEntity getById( @PathVariable UUID projectId, @@ -66,6 +69,7 @@ public ResponseEntity getById( return ResponseEntity.ok(mapper.toViewResponse(problem)); } + @Override @PatchMapping("/{problemId}") public ResponseEntity update( @PathVariable UUID projectId, @@ -77,6 +81,7 @@ public ResponseEntity update( return ResponseEntity.ok(mapper.toViewResponse(problem)); } + @Override @PatchMapping("/{problemId}/status") public ResponseEntity updateStatus( @PathVariable UUID projectId, @@ -88,6 +93,7 @@ public ResponseEntity updateStatus( return ResponseEntity.ok(mapper.toViewResponse(problem)); } + @Override @DeleteMapping("/{problemId}") public ResponseEntity delete( @PathVariable UUID projectId, diff --git a/backend/src/test/java/com/devaulty/backend/adapter/in/web/problem/ProblemControllerIT.java b/backend/src/test/java/com/devaulty/backend/adapter/in/web/problem/ProblemControllerIT.java new file mode 100644 index 0000000..717263d --- /dev/null +++ b/backend/src/test/java/com/devaulty/backend/adapter/in/web/problem/ProblemControllerIT.java @@ -0,0 +1,302 @@ +package com.devaulty.backend.adapter.in.web.problem; + +import com.devaulty.backend.infrastructure.BaseIntegrationTest; +import com.devaulty.backend.adapter.in.web.problem.dto.*; +import com.devaulty.backend.adapter.out.persistence.project.ProjectEntity; +import com.devaulty.backend.adapter.out.persistence.project.SpringDataProjectRepository; +import com.devaulty.backend.adapter.out.persistence.problem.ProblemEntity; +import com.devaulty.backend.adapter.out.persistence.problem.SpringDataProblemRepository; +import com.devaulty.backend.domain.model.enums.ProblemSeverity; +import com.devaulty.backend.domain.model.enums.ProblemStatus; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; + +import java.time.LocalDateTime; +import java.util.UUID; + +import static org.hamcrest.Matchers.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +class ProblemControllerIT extends BaseIntegrationTest { + + @Autowired + private SpringDataProblemRepository problemRepository; + + @Autowired + private SpringDataProjectRepository projectRepository; + + private ProjectEntity savedProject; + + @BeforeEach + void setUpData() { + problemRepository.deleteAll(); + projectRepository.deleteAll(); + + ProjectEntity project = new ProjectEntity( + UUID.randomUUID(), + "Integration Project", + "Description", + "folder", + "#FFF", + false + ); + project.setCreatedAt(LocalDateTime.now()); + savedProject = projectRepository.save(project); + } + + @Test + void createProblem_shouldReturnCreated() throws Exception { + CreateProblemRequest request = new CreateProblemRequest( + "My Problem", + "Exception stacktrace", + "Fix configuration", + ProblemStatus.OPEN, + ProblemSeverity.HIGH + ); + + mockMvc.perform(post("/api/v1/projects/{projectId}/problems", savedProject.getId()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isCreated()) + .andExpect(header().exists("Location")) + .andExpect(jsonPath("$.id").isNotEmpty()) + .andExpect(jsonPath("$.title").value("My Problem")) + .andExpect(jsonPath("$.errorDescription").value("Exception stacktrace")) + .andExpect(jsonPath("$.solution").value("Fix configuration")) + .andExpect(jsonPath("$.status").value("OPEN")) + .andExpect(jsonPath("$.severity").value("HIGH")) + .andExpect(jsonPath("$.createdAt").isNotEmpty()); + + assertEquals(1, problemRepository.count()); + } + + @Test + void createProblem_shouldReturnBadRequest_whenValidationFails() throws Exception { + // Title blank (min 2), Status null, Severity null + CreateProblemRequest request = new CreateProblemRequest( + " ", + "Error description", + "Solution", + null, + null + ); + + mockMvc.perform(post("/api/v1/projects/{projectId}/problems", savedProject.getId()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value("Validation error")) + .andExpect(jsonPath("$.errors", hasSize(3))); + } + + @Test + void createProblem_shouldReturnNotFound_whenProjectDoesNotExist() throws Exception { + UUID nonExistentProjectId = UUID.randomUUID(); + CreateProblemRequest request = new CreateProblemRequest( + "Problem Title", + "description", + "solution", + ProblemStatus.OPEN, + ProblemSeverity.MEDIUM + ); + + mockMvc.perform(post("/api/v1/projects/{projectId}/problems", nonExistentProjectId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.status").value(404)) + .andExpect(jsonPath("$.message").value("Project not found with identifier " + nonExistentProjectId)); + } + + @Test + void getAllProblems_shouldReturnPagedProblems() throws Exception { + ProblemEntity p1 = new ProblemEntity(UUID.randomUUID(), savedProject, "Problem 1", "Error 1", "Sol 1", ProblemStatus.OPEN, ProblemSeverity.MEDIUM); + p1.setCreatedAt(LocalDateTime.now().minusHours(1)); + ProblemEntity p2 = new ProblemEntity(UUID.randomUUID(), savedProject, "Problem 2", "Error 2", "Sol 2", ProblemStatus.RESOLVED, ProblemSeverity.HIGH); + p2.setCreatedAt(LocalDateTime.now()); + problemRepository.save(p1); + problemRepository.save(p2); + + mockMvc.perform(get("/api/v1/projects/{projectId}/problems", savedProject.getId()) + .param("page", "0") + .param("size", "5")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.content", hasSize(2))) + .andExpect(jsonPath("$.content[0].title").value("Problem 2")) // Sorted DESC by createdAt + .andExpect(jsonPath("$.content[1].title").value("Problem 1")) + .andExpect(jsonPath("$.page.totalElements").value(2)); + } + + @Test + void getAllProblems_shouldReturnNotFound_whenProjectDoesNotExist() throws Exception { + UUID nonExistentProjectId = UUID.randomUUID(); + + mockMvc.perform(get("/api/v1/projects/{projectId}/problems", nonExistentProjectId)) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.status").value(404)); + } + + @Test + void getProblemById_shouldReturnProblem() throws Exception { + UUID id = UUID.randomUUID(); + ProblemEntity problem = new ProblemEntity(id, savedProject, "Get Problem", "Error", "Sol", ProblemStatus.OPEN, ProblemSeverity.LOW); + problem.setCreatedAt(LocalDateTime.now()); + problemRepository.save(problem); + + mockMvc.perform(get("/api/v1/projects/{projectId}/problems/{problemId}", savedProject.getId(), id)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(id.toString())) + .andExpect(jsonPath("$.title").value("Get Problem")); + } + + @Test + void getProblemById_shouldReturnNotFound_whenProjectDoesNotExist() throws Exception { + UUID nonExistentProjectId = UUID.randomUUID(); + UUID problemId = UUID.randomUUID(); + + mockMvc.perform(get("/api/v1/projects/{projectId}/problems/{problemId}", nonExistentProjectId, problemId)) + .andExpect(status().isNotFound()); + } + + @Test + void getProblemById_shouldReturnNotFound_whenProblemDoesNotExist() throws Exception { + UUID nonExistentProblemId = UUID.randomUUID(); + + mockMvc.perform(get("/api/v1/projects/{projectId}/problems/{problemId}", savedProject.getId(), nonExistentProblemId)) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.message").value("Problem not found with identifier " + nonExistentProblemId)); + } + + @Test + void getProblemById_shouldReturnNotFound_whenProblemDoesNotBelongToProject() throws Exception { + // Create another project + ProjectEntity otherProject = new ProjectEntity(UUID.randomUUID(), "Other", "Desc", "folder", "#000", false); + otherProject.setCreatedAt(LocalDateTime.now()); + projectRepository.save(otherProject); + + // Create problem for other project + UUID problemId = UUID.randomUUID(); + ProblemEntity problem = new ProblemEntity(problemId, otherProject, "Other Problem", "Error", "Sol", ProblemStatus.OPEN, ProblemSeverity.MEDIUM); + problem.setCreatedAt(LocalDateTime.now()); + problemRepository.save(problem); + + // Request other project's problem under savedProject's path + mockMvc.perform(get("/api/v1/projects/{projectId}/problems/{problemId}", savedProject.getId(), problemId)) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.message").value("Problem not found with identifier " + problemId)); + } + + @Test + void updateProblem_shouldUpdateAndReturnProblem() throws Exception { + UUID id = UUID.randomUUID(); + ProblemEntity problem = new ProblemEntity(id, savedProject, "Old Title", "Old Error", "Old Sol", ProblemStatus.OPEN, ProblemSeverity.LOW); + problem.setCreatedAt(LocalDateTime.now()); + problemRepository.save(problem); + + UpdateProblemRequest request = new UpdateProblemRequest( + "New Title", + "New Error", + "New Sol", + ProblemSeverity.HIGH + ); + + mockMvc.perform(patch("/api/v1/projects/{projectId}/problems/{problemId}", savedProject.getId(), id) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.title").value("New Title")) + .andExpect(jsonPath("$.errorDescription").value("New Error")) + .andExpect(jsonPath("$.solution").value("New Sol")) + .andExpect(jsonPath("$.severity").value("HIGH")) + .andExpect(jsonPath("$.updatedAt").isNotEmpty()); + + ProblemEntity updated = problemRepository.findById(id).orElseThrow(); + assertEquals("New Title", updated.getTitle()); + } + + @Test + void updateProblem_shouldReturnBadRequest_whenValidationFails() throws Exception { + UUID id = UUID.randomUUID(); + ProblemEntity problem = new ProblemEntity(id, savedProject, "Old Title", "Old Error", "Old Sol", ProblemStatus.OPEN, ProblemSeverity.LOW); + problem.setCreatedAt(LocalDateTime.now()); + problemRepository.save(problem); + + // Blank title + UpdateProblemRequest request = new UpdateProblemRequest(" ", null, null, null); + + mockMvc.perform(patch("/api/v1/projects/{projectId}/problems/{problemId}", savedProject.getId(), id) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value("Validation error")); + } + + @Test + void updateProblemStatus_shouldUpdateAndReturnProblem() throws Exception { + UUID id = UUID.randomUUID(); + ProblemEntity problem = new ProblemEntity(id, savedProject, "Title", "Error", "Sol", ProblemStatus.OPEN, ProblemSeverity.LOW); + problem.setCreatedAt(LocalDateTime.now()); + problemRepository.save(problem); + + UpdateProblemStatusRequest request = new UpdateProblemStatusRequest(ProblemStatus.RESOLVED); + + mockMvc.perform(patch("/api/v1/projects/{projectId}/problems/{problemId}/status", savedProject.getId(), id) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status").value("RESOLVED")) + .andExpect(jsonPath("$.updatedAt").isNotEmpty()); + + ProblemEntity updated = problemRepository.findById(id).orElseThrow(); + assertEquals(ProblemStatus.RESOLVED, updated.getStatus()); + } + + @Test + void updateProblemStatus_shouldReturnBadRequest_whenStatusIsNull() throws Exception { + UUID id = UUID.randomUUID(); + ProblemEntity problem = new ProblemEntity(id, savedProject, "Title", "Error", "Sol", ProblemStatus.OPEN, ProblemSeverity.LOW); + problem.setCreatedAt(LocalDateTime.now()); + problemRepository.save(problem); + + UpdateProblemStatusRequest request = new UpdateProblemStatusRequest(null); + + mockMvc.perform(patch("/api/v1/projects/{projectId}/problems/{problemId}/status", savedProject.getId(), id) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()); + } + + @Test + void deleteProblem_shouldDeleteAndReturnNoContent() throws Exception { + UUID id = UUID.randomUUID(); + ProblemEntity problem = new ProblemEntity(id, savedProject, "To Delete", "Error", "Sol", ProblemStatus.OPEN, ProblemSeverity.LOW); + problem.setCreatedAt(LocalDateTime.now()); + problemRepository.save(problem); + + mockMvc.perform(delete("/api/v1/projects/{projectId}/problems/{problemId}", savedProject.getId(), id)) + .andExpect(status().isNoContent()); + + assertFalse(problemRepository.existsById(id)); + } + + @Test + void deleteProblem_shouldReturnNotFound_whenProjectDoesNotExist() throws Exception { + UUID nonExistentProjectId = UUID.randomUUID(); + UUID problemId = UUID.randomUUID(); + + mockMvc.perform(delete("/api/v1/projects/{projectId}/problems/{problemId}", nonExistentProjectId, problemId)) + .andExpect(status().isNotFound()); + } + + @Test + void deleteProblem_shouldReturnNotFound_whenProblemDoesNotExist() throws Exception { + UUID nonExistentProblemId = UUID.randomUUID(); + + mockMvc.perform(delete("/api/v1/projects/{projectId}/problems/{problemId}", savedProject.getId(), nonExistentProblemId)) + .andExpect(status().isNotFound()); + } +} diff --git a/backend/src/test/java/com/devaulty/backend/application/impl/problem/CreateProblemImplTest.java b/backend/src/test/java/com/devaulty/backend/application/impl/problem/CreateProblemImplTest.java new file mode 100644 index 0000000..9a171f0 --- /dev/null +++ b/backend/src/test/java/com/devaulty/backend/application/impl/problem/CreateProblemImplTest.java @@ -0,0 +1,91 @@ +package com.devaulty.backend.application.impl.problem; + +import com.devaulty.backend.application.exception.ResourceNotFoundException; +import com.devaulty.backend.application.port.in.problem.CreateProblemCommand; +import com.devaulty.backend.application.port.out.persistence.ProblemRepositoryPort; +import com.devaulty.backend.application.port.out.persistence.ProjectRepositoryPort; +import com.devaulty.backend.domain.model.Problem; +import com.devaulty.backend.domain.model.enums.ProblemSeverity; +import com.devaulty.backend.domain.model.enums.ProblemStatus; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class CreateProblemImplTest { + + @Mock + private ProblemRepositoryPort problemRepository; + + @Mock + private ProjectRepositoryPort projectRepository; + + @InjectMocks + private CreateProblemImpl createProblemUseCase; + + @Test + void shouldCreateProblemSuccessfully() { + // Arrange + UUID projectId = UUID.randomUUID(); + CreateProblemCommand command = new CreateProblemCommand( + projectId, + "Test Title", + "Error Description", + "Solution", + ProblemStatus.OPEN, + ProblemSeverity.HIGH + ); + + when(projectRepository.existsById(projectId)).thenReturn(true); + when(problemRepository.save(any(Problem.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + // Act + Problem result = createProblemUseCase.execute(command); + + // Assert + assertNotNull(result); + assertNotNull(result.getId()); + assertEquals(projectId, result.getProjectId()); + assertEquals("Test Title", result.getTitle()); + assertEquals("Error Description", result.getErrorDescription()); + assertEquals("Solution", result.getSolution()); + assertEquals(ProblemStatus.OPEN, result.getStatus()); + assertEquals(ProblemSeverity.HIGH, result.getSeverity()); + assertNotNull(result.getCreatedAt()); + + verify(projectRepository, times(1)).existsById(projectId); + verify(problemRepository, times(1)).save(any(Problem.class)); + } + + @Test + void shouldThrowResourceNotFoundExceptionWhenProjectDoesNotExist() { + // Arrange + UUID projectId = UUID.randomUUID(); + CreateProblemCommand command = new CreateProblemCommand( + projectId, + "Test Title", + "Error Description", + "Solution", + ProblemStatus.OPEN, + ProblemSeverity.HIGH + ); + + when(projectRepository.existsById(projectId)).thenReturn(false); + + // Act & Assert + assertThrows(ResourceNotFoundException.class, () -> { + createProblemUseCase.execute(command); + }); + + verify(projectRepository, times(1)).existsById(projectId); + verify(problemRepository, never()).save(any(Problem.class)); + } +} diff --git a/backend/src/test/java/com/devaulty/backend/application/impl/problem/DeleteProblemImplTest.java b/backend/src/test/java/com/devaulty/backend/application/impl/problem/DeleteProblemImplTest.java new file mode 100644 index 0000000..b8b7c0b --- /dev/null +++ b/backend/src/test/java/com/devaulty/backend/application/impl/problem/DeleteProblemImplTest.java @@ -0,0 +1,111 @@ +package com.devaulty.backend.application.impl.problem; + +import com.devaulty.backend.application.exception.ResourceNotFoundException; +import com.devaulty.backend.application.port.out.persistence.ProblemRepositoryPort; +import com.devaulty.backend.application.port.out.persistence.ProjectRepositoryPort; +import com.devaulty.backend.domain.model.Problem; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Optional; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class DeleteProblemImplTest { + + @Mock + private ProblemRepositoryPort problemRepository; + + @Mock + private ProjectRepositoryPort projectRepository; + + @InjectMocks + private DeleteProblemImpl deleteProblemUseCase; + + @Test + void shouldDeleteProblemSuccessfully() { + // Arrange + UUID projectId = UUID.randomUUID(); + UUID problemId = UUID.randomUUID(); + Problem problem = new Problem(); + problem.setId(problemId); + problem.setProjectId(projectId); + + when(projectRepository.existsById(projectId)).thenReturn(true); + when(problemRepository.findById(problemId)).thenReturn(Optional.of(problem)); + + // Act + deleteProblemUseCase.execute(projectId, problemId); + + // Assert + verify(projectRepository, times(1)).existsById(projectId); + verify(problemRepository, times(1)).findById(problemId); + verify(problemRepository, times(1)).deleteById(problemId); + } + + @Test + void shouldThrowResourceNotFoundExceptionWhenProjectDoesNotExist() { + // Arrange + UUID projectId = UUID.randomUUID(); + UUID problemId = UUID.randomUUID(); + + when(projectRepository.existsById(projectId)).thenReturn(false); + + // Act & Assert + assertThrows(ResourceNotFoundException.class, () -> { + deleteProblemUseCase.execute(projectId, problemId); + }); + + verify(projectRepository, times(1)).existsById(projectId); + verify(problemRepository, never()).findById(any(UUID.class)); + verify(problemRepository, never()).deleteById(any(UUID.class)); + } + + @Test + void shouldThrowResourceNotFoundExceptionWhenProblemDoesNotExist() { + // Arrange + UUID projectId = UUID.randomUUID(); + UUID problemId = UUID.randomUUID(); + + when(projectRepository.existsById(projectId)).thenReturn(true); + when(problemRepository.findById(problemId)).thenReturn(Optional.empty()); + + // Act & Assert + assertThrows(ResourceNotFoundException.class, () -> { + deleteProblemUseCase.execute(projectId, problemId); + }); + + verify(projectRepository, times(1)).existsById(projectId); + verify(problemRepository, times(1)).findById(problemId); + verify(problemRepository, never()).deleteById(any(UUID.class)); + } + + @Test + void shouldThrowResourceNotFoundExceptionWhenProblemDoesNotBelongToProject() { + // Arrange + UUID projectId = UUID.randomUUID(); + UUID otherProjectId = UUID.randomUUID(); + UUID problemId = UUID.randomUUID(); + Problem problem = new Problem(); + problem.setId(problemId); + problem.setProjectId(otherProjectId); + + when(projectRepository.existsById(projectId)).thenReturn(true); + when(problemRepository.findById(problemId)).thenReturn(Optional.of(problem)); + + // Act & Assert + assertThrows(ResourceNotFoundException.class, () -> { + deleteProblemUseCase.execute(projectId, problemId); + }); + + verify(projectRepository, times(1)).existsById(projectId); + verify(problemRepository, times(1)).findById(problemId); + verify(problemRepository, never()).deleteById(any(UUID.class)); + } +} diff --git a/backend/src/test/java/com/devaulty/backend/application/impl/problem/GetAllProblemsByProjectImplTest.java b/backend/src/test/java/com/devaulty/backend/application/impl/problem/GetAllProblemsByProjectImplTest.java new file mode 100644 index 0000000..7b51912 --- /dev/null +++ b/backend/src/test/java/com/devaulty/backend/application/impl/problem/GetAllProblemsByProjectImplTest.java @@ -0,0 +1,81 @@ +package com.devaulty.backend.application.impl.problem; + +import com.devaulty.backend.application.exception.ResourceNotFoundException; +import com.devaulty.backend.application.port.out.persistence.ProblemRepositoryPort; +import com.devaulty.backend.application.port.out.persistence.ProjectRepositoryPort; +import com.devaulty.backend.domain.model.Problem; +import com.devaulty.backend.domain.model.enums.ProblemSeverity; +import com.devaulty.backend.domain.model.enums.ProblemStatus; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; + +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class GetAllProblemsByProjectImplTest { + + @Mock + private ProblemRepositoryPort problemRepository; + + @Mock + private ProjectRepositoryPort projectRepository; + + @InjectMocks + private GetAllProblemsByProjectImpl getAllProblemsUseCase; + + @Test + void shouldReturnPageOfProblems() { + // Arrange + UUID projectId = UUID.randomUUID(); + int page = 0; + int size = 10; + Problem problem = new Problem(); + problem.setId(UUID.randomUUID()); + problem.setProjectId(projectId); + problem.setTitle("Title"); + problem.setStatus(ProblemStatus.OPEN); + problem.setSeverity(ProblemSeverity.MEDIUM); + List list = Collections.singletonList(problem); + Page expectedPage = new PageImpl<>(list); + + when(projectRepository.existsById(projectId)).thenReturn(true); + when(problemRepository.findAllByProject(projectId, page, size)).thenReturn(expectedPage); + + // Act + Page result = getAllProblemsUseCase.execute(projectId, page, size); + + // Assert + assertNotNull(result); + assertEquals(expectedPage, result); + assertEquals(1, result.getTotalElements()); + assertEquals(problem, result.getContent().getFirst()); + + verify(projectRepository, times(1)).existsById(projectId); + verify(problemRepository, times(1)).findAllByProject(projectId, page, size); + } + + @Test + void shouldThrowResourceNotFoundExceptionWhenProjectDoesNotExist() { + // Arrange + UUID projectId = UUID.randomUUID(); + when(projectRepository.existsById(projectId)).thenReturn(false); + + // Act & Assert + assertThrows(ResourceNotFoundException.class, () -> { + getAllProblemsUseCase.execute(projectId, 0, 10); + }); + + verify(projectRepository, times(1)).existsById(projectId); + verify(problemRepository, never()).findAllByProject(any(UUID.class), anyInt(), anyInt()); + } +} diff --git a/backend/src/test/java/com/devaulty/backend/application/impl/problem/GetProblemByIdImplTest.java b/backend/src/test/java/com/devaulty/backend/application/impl/problem/GetProblemByIdImplTest.java new file mode 100644 index 0000000..7db6464 --- /dev/null +++ b/backend/src/test/java/com/devaulty/backend/application/impl/problem/GetProblemByIdImplTest.java @@ -0,0 +1,115 @@ +package com.devaulty.backend.application.impl.problem; + +import com.devaulty.backend.application.exception.ResourceNotFoundException; +import com.devaulty.backend.application.port.out.persistence.ProblemRepositoryPort; +import com.devaulty.backend.application.port.out.persistence.ProjectRepositoryPort; +import com.devaulty.backend.domain.model.Problem; +import com.devaulty.backend.domain.model.enums.ProblemSeverity; +import com.devaulty.backend.domain.model.enums.ProblemStatus; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Optional; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class GetProblemByIdImplTest { + + @Mock + private ProblemRepositoryPort problemRepository; + + @Mock + private ProjectRepositoryPort projectRepository; + + @InjectMocks + private GetProblemByIdImpl getProblemByIdUseCase; + + @Test + void shouldReturnProblemWhenFoundAndBelongsToProject() { + // Arrange + UUID projectId = UUID.randomUUID(); + UUID problemId = UUID.randomUUID(); + Problem expectedProblem = new Problem(); + expectedProblem.setId(problemId); + expectedProblem.setProjectId(projectId); + expectedProblem.setTitle("Title"); + expectedProblem.setStatus(ProblemStatus.OPEN); + expectedProblem.setSeverity(ProblemSeverity.MEDIUM); + + when(projectRepository.existsById(projectId)).thenReturn(true); + when(problemRepository.findById(problemId)).thenReturn(Optional.of(expectedProblem)); + + // Act + Problem result = getProblemByIdUseCase.execute(projectId, problemId); + + // Assert + assertNotNull(result); + assertEquals(expectedProblem, result); + + verify(projectRepository, times(1)).existsById(projectId); + verify(problemRepository, times(1)).findById(problemId); + } + + @Test + void shouldThrowResourceNotFoundExceptionWhenProjectDoesNotExist() { + // Arrange + UUID projectId = UUID.randomUUID(); + UUID problemId = UUID.randomUUID(); + + when(projectRepository.existsById(projectId)).thenReturn(false); + + // Act & Assert + assertThrows(ResourceNotFoundException.class, () -> { + getProblemByIdUseCase.execute(projectId, problemId); + }); + + verify(projectRepository, times(1)).existsById(projectId); + verify(problemRepository, never()).findById(any(UUID.class)); + } + + @Test + void shouldThrowResourceNotFoundExceptionWhenProblemDoesNotExist() { + // Arrange + UUID projectId = UUID.randomUUID(); + UUID problemId = UUID.randomUUID(); + + when(projectRepository.existsById(projectId)).thenReturn(true); + when(problemRepository.findById(problemId)).thenReturn(Optional.empty()); + + // Act & Assert + assertThrows(ResourceNotFoundException.class, () -> { + getProblemByIdUseCase.execute(projectId, problemId); + }); + + verify(projectRepository, times(1)).existsById(projectId); + verify(problemRepository, times(1)).findById(problemId); + } + + @Test + void shouldThrowResourceNotFoundExceptionWhenProblemDoesNotBelongToProject() { + // Arrange + UUID projectId = UUID.randomUUID(); + UUID otherProjectId = UUID.randomUUID(); + UUID problemId = UUID.randomUUID(); + Problem problemOfOtherProject = new Problem(); + problemOfOtherProject.setId(problemId); + problemOfOtherProject.setProjectId(otherProjectId); + + when(projectRepository.existsById(projectId)).thenReturn(true); + when(problemRepository.findById(problemId)).thenReturn(Optional.of(problemOfOtherProject)); + + // Act & Assert + assertThrows(ResourceNotFoundException.class, () -> { + getProblemByIdUseCase.execute(projectId, problemId); + }); + + verify(projectRepository, times(1)).existsById(projectId); + verify(problemRepository, times(1)).findById(problemId); + } +} diff --git a/backend/src/test/java/com/devaulty/backend/application/impl/problem/UpdateProblemImplTest.java b/backend/src/test/java/com/devaulty/backend/application/impl/problem/UpdateProblemImplTest.java new file mode 100644 index 0000000..ac227cb --- /dev/null +++ b/backend/src/test/java/com/devaulty/backend/application/impl/problem/UpdateProblemImplTest.java @@ -0,0 +1,181 @@ +package com.devaulty.backend.application.impl.problem; + +import com.devaulty.backend.application.exception.ResourceNotFoundException; +import com.devaulty.backend.application.port.in.problem.UpdateProblemCommand; +import com.devaulty.backend.application.port.out.persistence.ProblemRepositoryPort; +import com.devaulty.backend.application.port.out.persistence.ProjectRepositoryPort; +import com.devaulty.backend.domain.model.Problem; +import com.devaulty.backend.domain.model.enums.ProblemSeverity; +import com.devaulty.backend.domain.model.enums.ProblemStatus; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Optional; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class UpdateProblemImplTest { + + @Mock + private ProblemRepositoryPort problemRepository; + + @Mock + private ProjectRepositoryPort projectRepository; + + @InjectMocks + private UpdateProblemImpl updateProblemUseCase; + + @Test + void shouldUpdateFieldsWhenValidCommand() { + // Arrange + UUID projectId = UUID.randomUUID(); + UUID problemId = UUID.randomUUID(); + Problem existingProblem = new Problem(); + existingProblem.setId(problemId); + existingProblem.setProjectId(projectId); + existingProblem.setTitle("Old Title"); + existingProblem.setErrorDescription("Old Error"); + existingProblem.setSolution("Old Sol"); + existingProblem.setSeverity(ProblemSeverity.LOW); + existingProblem.setStatus(ProblemStatus.OPEN); + + UpdateProblemCommand command = new UpdateProblemCommand( + problemId, + projectId, + "New Title", + "New Error", + "New Sol", + ProblemSeverity.HIGH + ); + + when(projectRepository.existsById(projectId)).thenReturn(true); + when(problemRepository.findById(problemId)).thenReturn(Optional.of(existingProblem)); + when(problemRepository.save(any(Problem.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + // Act + Problem result = updateProblemUseCase.execute(command); + + // Assert + assertNotNull(result); + assertEquals("New Title", result.getTitle()); + assertEquals("New Error", result.getErrorDescription()); + assertEquals("New Sol", result.getSolution()); + assertEquals(ProblemSeverity.HIGH, result.getSeverity()); + assertNotNull(result.getUpdatedAt()); + + verify(projectRepository, times(1)).existsById(projectId); + verify(problemRepository, times(1)).findById(problemId); + verify(problemRepository, times(1)).save(existingProblem); + } + + @Test + void shouldNotUpdateFieldsWhenNull() { + // Arrange + UUID projectId = UUID.randomUUID(); + UUID problemId = UUID.randomUUID(); + Problem existingProblem = new Problem(); + existingProblem.setId(problemId); + existingProblem.setProjectId(projectId); + existingProblem.setTitle("Old Title"); + existingProblem.setErrorDescription("Old Error"); + existingProblem.setSolution("Old Sol"); + existingProblem.setSeverity(ProblemSeverity.LOW); + existingProblem.setStatus(ProblemStatus.OPEN); + + UpdateProblemCommand command = new UpdateProblemCommand( + problemId, + projectId, + null, + null, + null, + null + ); + + when(projectRepository.existsById(projectId)).thenReturn(true); + when(problemRepository.findById(problemId)).thenReturn(Optional.of(existingProblem)); + when(problemRepository.save(any(Problem.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + // Act + Problem result = updateProblemUseCase.execute(command); + + // Assert + assertNotNull(result); + assertEquals("Old Title", result.getTitle()); + assertEquals("Old Error", result.getErrorDescription()); + assertEquals("Old Sol", result.getSolution()); + assertEquals(ProblemSeverity.LOW, result.getSeverity()); + assertNotNull(result.getUpdatedAt()); + + verify(projectRepository, times(1)).existsById(projectId); + verify(problemRepository, times(1)).findById(problemId); + verify(problemRepository, times(1)).save(existingProblem); + } + + @Test + void shouldThrowResourceNotFoundExceptionWhenProjectDoesNotExist() { + // Arrange + UUID projectId = UUID.randomUUID(); + UUID problemId = UUID.randomUUID(); + UpdateProblemCommand command = new UpdateProblemCommand(problemId, projectId, "Title", null, null, null); + + when(projectRepository.existsById(projectId)).thenReturn(false); + + // Act & Assert + assertThrows(ResourceNotFoundException.class, () -> { + updateProblemUseCase.execute(command); + }); + + verify(projectRepository, times(1)).existsById(projectId); + verify(problemRepository, never()).findById(any(UUID.class)); + } + + @Test + void shouldThrowResourceNotFoundExceptionWhenProblemDoesNotExist() { + // Arrange + UUID projectId = UUID.randomUUID(); + UUID problemId = UUID.randomUUID(); + UpdateProblemCommand command = new UpdateProblemCommand(problemId, projectId, "Title", null, null, null); + + when(projectRepository.existsById(projectId)).thenReturn(true); + when(problemRepository.findById(problemId)).thenReturn(Optional.empty()); + + // Act & Assert + assertThrows(ResourceNotFoundException.class, () -> { + updateProblemUseCase.execute(command); + }); + + verify(projectRepository, times(1)).existsById(projectId); + verify(problemRepository, times(1)).findById(problemId); + } + + @Test + void shouldThrowResourceNotFoundExceptionWhenProblemDoesNotBelongToProject() { + // Arrange + UUID projectId = UUID.randomUUID(); + UUID otherProjectId = UUID.randomUUID(); + UUID problemId = UUID.randomUUID(); + Problem problemOfOtherProject = new Problem(); + problemOfOtherProject.setId(problemId); + problemOfOtherProject.setProjectId(otherProjectId); + UpdateProblemCommand command = new UpdateProblemCommand(problemId, projectId, "Title", null, null, null); + + when(projectRepository.existsById(projectId)).thenReturn(true); + when(problemRepository.findById(problemId)).thenReturn(Optional.of(problemOfOtherProject)); + + // Act & Assert + assertThrows(ResourceNotFoundException.class, () -> { + updateProblemUseCase.execute(command); + }); + + verify(projectRepository, times(1)).existsById(projectId); + verify(problemRepository, times(1)).findById(problemId); + verify(problemRepository, never()).save(any(Problem.class)); + } +} diff --git a/backend/src/test/java/com/devaulty/backend/application/impl/problem/UpdateProblemStatusImplTest.java b/backend/src/test/java/com/devaulty/backend/application/impl/problem/UpdateProblemStatusImplTest.java new file mode 100644 index 0000000..2d0c5c0 --- /dev/null +++ b/backend/src/test/java/com/devaulty/backend/application/impl/problem/UpdateProblemStatusImplTest.java @@ -0,0 +1,126 @@ +package com.devaulty.backend.application.impl.problem; + +import com.devaulty.backend.application.exception.ResourceNotFoundException; +import com.devaulty.backend.application.port.in.problem.UpdateProblemStatusCommand; +import com.devaulty.backend.application.port.out.persistence.ProblemRepositoryPort; +import com.devaulty.backend.application.port.out.persistence.ProjectRepositoryPort; +import com.devaulty.backend.domain.model.Problem; +import com.devaulty.backend.domain.model.enums.ProblemSeverity; +import com.devaulty.backend.domain.model.enums.ProblemStatus; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Optional; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class UpdateProblemStatusImplTest { + + @Mock + private ProblemRepositoryPort problemRepository; + + @Mock + private ProjectRepositoryPort projectRepository; + + @InjectMocks + private UpdateProblemStatusImpl updateProblemStatusUseCase; + + @Test + void shouldUpdateStatusSuccessfully() { + // Arrange + UUID projectId = UUID.randomUUID(); + UUID problemId = UUID.randomUUID(); + Problem existingProblem = new Problem(); + existingProblem.setId(problemId); + existingProblem.setProjectId(projectId); + existingProblem.setTitle("Title"); + existingProblem.setStatus(ProblemStatus.OPEN); + existingProblem.setSeverity(ProblemSeverity.MEDIUM); + + UpdateProblemStatusCommand command = new UpdateProblemStatusCommand(problemId, projectId, ProblemStatus.RESOLVED); + + when(projectRepository.existsById(projectId)).thenReturn(true); + when(problemRepository.findById(problemId)).thenReturn(Optional.of(existingProblem)); + when(problemRepository.save(any(Problem.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + // Act + Problem result = updateProblemStatusUseCase.execute(command); + + // Assert + assertNotNull(result); + assertEquals(ProblemStatus.RESOLVED, result.getStatus()); + assertNotNull(result.getUpdatedAt()); + + verify(projectRepository, times(1)).existsById(projectId); + verify(problemRepository, times(1)).findById(problemId); + verify(problemRepository, times(1)).save(existingProblem); + } + + @Test + void shouldThrowResourceNotFoundExceptionWhenProjectDoesNotExist() { + // Arrange + UUID projectId = UUID.randomUUID(); + UUID problemId = UUID.randomUUID(); + UpdateProblemStatusCommand command = new UpdateProblemStatusCommand(problemId, projectId, ProblemStatus.RESOLVED); + + when(projectRepository.existsById(projectId)).thenReturn(false); + + // Act & Assert + assertThrows(ResourceNotFoundException.class, () -> { + updateProblemStatusUseCase.execute(command); + }); + + verify(projectRepository, times(1)).existsById(projectId); + verify(problemRepository, never()).findById(any(UUID.class)); + } + + @Test + void shouldThrowResourceNotFoundExceptionWhenProblemDoesNotExist() { + // Arrange + UUID projectId = UUID.randomUUID(); + UUID problemId = UUID.randomUUID(); + UpdateProblemStatusCommand command = new UpdateProblemStatusCommand(problemId, projectId, ProblemStatus.RESOLVED); + + when(projectRepository.existsById(projectId)).thenReturn(true); + when(problemRepository.findById(problemId)).thenReturn(Optional.empty()); + + // Act & Assert + assertThrows(ResourceNotFoundException.class, () -> { + updateProblemStatusUseCase.execute(command); + }); + + verify(projectRepository, times(1)).existsById(projectId); + verify(problemRepository, times(1)).findById(problemId); + } + + @Test + void shouldThrowResourceNotFoundExceptionWhenProblemDoesNotBelongToProject() { + // Arrange + UUID projectId = UUID.randomUUID(); + UUID otherProjectId = UUID.randomUUID(); + UUID problemId = UUID.randomUUID(); + Problem problemOfOtherProject = new Problem(); + problemOfOtherProject.setId(problemId); + problemOfOtherProject.setProjectId(otherProjectId); + UpdateProblemStatusCommand command = new UpdateProblemStatusCommand(problemId, projectId, ProblemStatus.RESOLVED); + + when(projectRepository.existsById(projectId)).thenReturn(true); + when(problemRepository.findById(problemId)).thenReturn(Optional.of(problemOfOtherProject)); + + // Act & Assert + assertThrows(ResourceNotFoundException.class, () -> { + updateProblemStatusUseCase.execute(command); + }); + + verify(projectRepository, times(1)).existsById(projectId); + verify(problemRepository, times(1)).findById(problemId); + verify(problemRepository, never()).save(any(Problem.class)); + } +} From e44e227675dc941a01e39fcd7858ac2677d244d1 Mon Sep 17 00:00:00 2001 From: Matheus Cunha Date: Sun, 12 Jul 2026 04:26:07 -0300 Subject: [PATCH 3/3] Fix incorrect parameter in ResourceNotFoundException for project ID in GetProblemByIdImpl --- .../backend/application/impl/problem/GetProblemByIdImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/java/com/devaulty/backend/application/impl/problem/GetProblemByIdImpl.java b/backend/src/main/java/com/devaulty/backend/application/impl/problem/GetProblemByIdImpl.java index 95b5624..23ed6b0 100644 --- a/backend/src/main/java/com/devaulty/backend/application/impl/problem/GetProblemByIdImpl.java +++ b/backend/src/main/java/com/devaulty/backend/application/impl/problem/GetProblemByIdImpl.java @@ -23,7 +23,7 @@ public GetProblemByIdImpl(ProblemRepositoryPort problemRepositoryPort, ProjectRe @Transactional(readOnly = true) public Problem execute(UUID projectId, UUID id) { - if (!projectRepositoryPort.existsById(projectId)) throw new ResourceNotFoundException("Project", id); + if (!projectRepositoryPort.existsById(projectId)) throw new ResourceNotFoundException("Project", projectId); Problem problem = problemRepositoryPort.findById(id) .orElseThrow(() -> new ResourceNotFoundException("Problem", id));