feat(project): implement CRUD operations for projects

This commit is contained in:
Rodrigo Verdiani 2026-06-06 20:34:39 -03:00
parent d3362571f2
commit 82074add45
19 changed files with 1075 additions and 35 deletions

View File

@ -0,0 +1,66 @@
package ai.decompile.workspace.controller;
import ai.decompile.workspace.model.dto.ProjectRequest;
import ai.decompile.workspace.model.dto.ProjectResponse;
import ai.decompile.workspace.service.ProjectService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import java.util.List;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/workspaces/{workspaceId}/projects")
@RequiredArgsConstructor
public class ProjectController {
private final ProjectService projectService;
@Operation(
summary = "List projects of a workspace",
description = "Retrieve all projects belonging to a workspace, optionally filtered by name.",
tags = {"Project"},
operationId = "getProjects")
@GetMapping
public List<ProjectResponse> getProjects(
@PathVariable UUID workspaceId,
@Parameter(description = "Filter projects whose name contains this value (case-insensitive)")
@RequestParam(required = false)
String name) {
return projectService.getProjects(workspaceId, name);
}
@Operation(
summary = "Create a project",
description = "Create a project in a workspace.",
tags = {"Project"},
operationId = "createProject")
@PostMapping
public ProjectResponse createProject(
@PathVariable UUID workspaceId, @RequestBody ProjectRequest request) {
return projectService.createProject(workspaceId, request);
}
@Operation(
summary = "Update a project",
description = "Update a project's name and description.",
tags = {"Project"},
operationId = "updateProject")
@PutMapping("/{projectId}")
public ProjectResponse updateProject(
@PathVariable UUID workspaceId,
@PathVariable UUID projectId,
@RequestBody ProjectRequest request) {
return projectService.updateProject(projectId, request);
}
@Operation(
summary = "Delete a project",
description = "Delete a project.",
tags = {"Project"},
operationId = "deleteProject")
@DeleteMapping("/{projectId}")
public void deleteProject(@PathVariable UUID workspaceId, @PathVariable UUID projectId) {
projectService.deleteProject(projectId);
}
}

View File

@ -11,7 +11,7 @@ import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@RestController @RestController
@RequestMapping("/workspace") @RequestMapping("/workspaces")
@RequiredArgsConstructor @RequiredArgsConstructor
public class WorkspaceController { public class WorkspaceController {
private final WorkspaceService workspaceService; private final WorkspaceService workspaceService;
@ -30,6 +30,16 @@ public class WorkspaceController {
return workspaceService.getWorkspaces(name); return workspaceService.getWorkspaces(name);
} }
@Operation(
summary = "Get a workspace by ID",
description = "Retrieve a single workspace by its unique identifier.",
tags = {"Workspace"},
operationId = "getWorkspace")
@GetMapping("/{id}")
public WorkspaceResponse getWorkspace(@PathVariable UUID id) {
return workspaceService.getWorkspaceById(id);
}
@Operation( @Operation(
summary = "Create a workspace", summary = "Create a workspace",
description = "Create a workspace", description = "Create a workspace",

View File

@ -0,0 +1,5 @@
package ai.decompile.workspace.model.dto;
import jakarta.validation.constraints.NotBlank;
public record ProjectRequest(@NotBlank String name, String description) {}

View File

@ -0,0 +1,23 @@
package ai.decompile.workspace.model.dto;
import ai.decompile.workspace.model.entity.Project;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.time.Instant;
import java.util.UUID;
public record ProjectResponse(
@NotNull UUID id,
@NotNull UUID workspaceId,
@NotBlank String name,
String description,
@NotNull Instant updatedAt) {
public static ProjectResponse from(Project project) {
return new ProjectResponse(
project.getId(),
project.getWorkspace().getId(),
project.getName(),
project.getDescription(),
project.getUpdatedAt());
}
}

View File

@ -7,12 +7,18 @@ import java.time.Instant;
import java.util.UUID; import java.util.UUID;
public record WorkspaceResponse( public record WorkspaceResponse(
@NotNull UUID id, @NotBlank String name, String description, @NotNull Instant updatedAt) { @NotNull UUID id,
@NotBlank String name,
String description,
@NotNull Instant updatedAt,
int projectCount) {
public static WorkspaceResponse from(Workspace workspace) { public static WorkspaceResponse from(Workspace workspace) {
var projects = workspace.getProjects();
return new WorkspaceResponse( return new WorkspaceResponse(
workspace.getId(), workspace.getId(),
workspace.getName(), workspace.getName(),
workspace.getDescription(), workspace.getDescription(),
workspace.getUpdatedAt()); workspace.getUpdatedAt(),
projects != null ? projects.size() : 0);
} }
} }

View File

@ -0,0 +1,39 @@
package ai.decompile.workspace.model.entity;
import jakarta.persistence.*;
import java.time.Instant;
import java.util.UUID;
import lombok.*;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
@Entity
@Table(name = "projects")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Project {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
@Column(nullable = false, length = 150)
private String name;
@Column(length = 500)
private String description;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "workspace_id", nullable = false)
private Workspace workspace;
@CreationTimestamp
@Column(name = "created_at", nullable = false, updatable = false)
private Instant createdAt;
@UpdateTimestamp
@Column(name = "updated_at", nullable = false)
private Instant updatedAt;
}

View File

@ -2,6 +2,7 @@ package ai.decompile.workspace.model.entity;
import jakarta.persistence.*; import jakarta.persistence.*;
import java.time.Instant; import java.time.Instant;
import java.util.List;
import java.util.UUID; import java.util.UUID;
import lombok.*; import lombok.*;
import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.CreationTimestamp;
@ -32,4 +33,7 @@ public class Workspace {
@UpdateTimestamp @UpdateTimestamp
@Column(name = "updated_at", nullable = false) @Column(name = "updated_at", nullable = false)
private Instant updatedAt; private Instant updatedAt;
@OneToMany(mappedBy = "workspace", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Project> projects;
} }

View File

@ -0,0 +1,12 @@
package ai.decompile.workspace.model.repository;
import ai.decompile.workspace.model.entity.Project;
import jakarta.validation.constraints.NotBlank;
import java.util.UUID;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
public interface ProjectRepository
extends JpaRepository<Project, UUID>, JpaSpecificationExecutor<Project> {
boolean existsByNameIgnoreCaseAndWorkspace_Id(@NotBlank String name, UUID workspaceId);
}

View File

@ -0,0 +1,18 @@
package ai.decompile.workspace.model.specification;
import ai.decompile.workspace.model.entity.Project;
import java.util.UUID;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.springframework.data.jpa.domain.Specification;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class ProjectSpecification {
public static Specification<Project> nameContains(String name) {
return (root, _, cb) -> cb.like(cb.lower(root.get("name")), "%" + name.toLowerCase() + "%");
}
public static Specification<Project> hasWorkspaceId(UUID workspaceId) {
return (root, _, cb) -> cb.equal(root.get("workspace").get("id"), workspaceId);
}
}

View File

@ -1,12 +1,12 @@
package ai.decompile.workspace.model.specification; package ai.decompile.workspace.model.specification;
import ai.decompile.workspace.model.entity.Workspace; import ai.decompile.workspace.model.entity.Workspace;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.springframework.data.jpa.domain.Specification; import org.springframework.data.jpa.domain.Specification;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class WorkspaceSpecification { public class WorkspaceSpecification {
private WorkspaceSpecification() {}
public static Specification<Workspace> nameContains(String name) { public static Specification<Workspace> nameContains(String name) {
return (root, _, cb) -> cb.like(cb.lower(root.get("name")), "%" + name.toLowerCase() + "%"); return (root, _, cb) -> cb.like(cb.lower(root.get("name")), "%" + name.toLowerCase() + "%");
} }

View File

@ -0,0 +1,77 @@
package ai.decompile.workspace.service;
import ai.decompile.exception.ConflictException;
import ai.decompile.exception.NotFoundException;
import ai.decompile.workspace.model.dto.ProjectRequest;
import ai.decompile.workspace.model.dto.ProjectResponse;
import ai.decompile.workspace.model.entity.Project;
import ai.decompile.workspace.model.repository.ProjectRepository;
import ai.decompile.workspace.model.specification.ProjectSpecification;
import java.util.List;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class ProjectService {
private final WorkspaceService workspaceService;
private final ProjectRepository projectRepository;
public List<ProjectResponse> getProjects(UUID workspaceId, String name) {
Specification<Project> spec = ProjectSpecification.hasWorkspaceId(workspaceId);
if (StringUtils.isNotBlank(name)) {
spec = spec.and(ProjectSpecification.nameContains(name));
}
return projectRepository.findAll(spec).stream().map(ProjectResponse::from).toList();
}
public ProjectResponse createProject(UUID workspaceId, ProjectRequest request) {
var workspace = workspaceService.getWorkspace(workspaceId);
if (projectRepository.existsByNameIgnoreCaseAndWorkspace_Id(request.name(), workspaceId)) {
throw new ConflictException(
"A project with the name '" + request.name() + "' already exists in this workspace.");
}
var project =
projectRepository.save(
Project.builder()
.name(request.name())
.description(request.description())
.workspace(workspace)
.build());
return ProjectResponse.from(project);
}
public ProjectResponse updateProject(UUID projectId, ProjectRequest request) {
var project = getProject(projectId);
if (!project.getName().equalsIgnoreCase(request.name())
&& projectRepository.existsByNameIgnoreCaseAndWorkspace_Id(
request.name(), project.getWorkspace().getId())) {
throw new ConflictException(
"A project with the name '" + request.name() + "' already exists in this workspace.");
}
project.setName(request.name());
project.setDescription(request.description());
return ProjectResponse.from(projectRepository.save(project));
}
public void deleteProject(UUID projectId) {
var project = getProject(projectId);
projectRepository.delete(project);
}
private Project getProject(UUID projectId) {
return projectRepository
.findById(projectId)
.orElseThrow(() -> new NotFoundException("Project with id '" + projectId + "' not found"));
}
}

View File

@ -19,13 +19,18 @@ public class WorkspaceService {
private final WorkspaceRepository workspaceRepository; private final WorkspaceRepository workspaceRepository;
public List<WorkspaceResponse> getWorkspaces(String name) { public List<WorkspaceResponse> getWorkspaces(String name) {
List<Workspace> workspaces;
if (StringUtils.isNotBlank(name)) { if (StringUtils.isNotBlank(name)) {
return workspaceRepository.findAll(WorkspaceSpecification.nameContains(name)).stream() workspaces = workspaceRepository.findAll(WorkspaceSpecification.nameContains(name));
.map(WorkspaceResponse::from) } else {
.toList(); workspaces = workspaceRepository.findAll();
} }
return workspaceRepository.findAll().stream().map(WorkspaceResponse::from).toList(); return workspaces.stream().map(WorkspaceResponse::from).toList();
}
public WorkspaceResponse getWorkspaceById(UUID id) {
return WorkspaceResponse.from(getWorkspace(id));
} }
public WorkspaceResponse createWorkspace(WorkspaceRequest request) { public WorkspaceResponse createWorkspace(WorkspaceRequest request) {
@ -55,7 +60,7 @@ public class WorkspaceService {
workspaceRepository.delete(workspace); workspaceRepository.delete(workspace);
} }
private Workspace getWorkspace(UUID id) { protected Workspace getWorkspace(UUID id) {
return workspaceRepository return workspaceRepository
.findById(id) .findById(id)
.orElseThrow(() -> new NotFoundException("Workspace with id '" + id + "' not found")); .orElseThrow(() -> new NotFoundException("Workspace with id '" + id + "' not found"));

View File

@ -0,0 +1,9 @@
CREATE TABLE projects
(
id UUID PRIMARY KEY,
workspace_id UUID NOT NULL REFERENCES workspaces (id) ON DELETE CASCADE,
name VARCHAR(150) NOT NULL,
description VARCHAR(500),
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL
);

View File

@ -0,0 +1,196 @@
package ai.decompile.project.controller;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import ai.decompile.exception.ConflictException;
import ai.decompile.exception.NotFoundException;
import ai.decompile.workspace.controller.ProjectController;
import ai.decompile.workspace.model.dto.ProjectRequest;
import ai.decompile.workspace.model.dto.ProjectResponse;
import ai.decompile.workspace.service.ProjectService;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.time.Instant;
import java.util.List;
import java.util.UUID;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;
@WebMvcTest(ProjectController.class)
class ProjectControllerTest {
@Autowired private MockMvc mockMvc;
@MockitoBean private ProjectService projectService;
private final ObjectMapper objectMapper = new ObjectMapper();
@Test
void getProjectsShouldReturn200WithList() throws Exception {
var workspaceId = UUID.randomUUID();
var projectId = UUID.randomUUID();
var response =
new ProjectResponse(projectId, workspaceId, "Test Project", "Desc", Instant.now());
when(projectService.getProjects(eq(workspaceId), isNull())).thenReturn(List.of(response));
mockMvc
.perform(get("/workspaces/{workspaceId}/projects", workspaceId))
.andExpect(status().isOk())
.andExpect(jsonPath("$[0].id").value(projectId.toString()))
.andExpect(jsonPath("$[0].name").value("Test Project"))
.andExpect(jsonPath("$[0].workspaceId").value(workspaceId.toString()));
}
@Test
void getProjectsShouldReturn200WithEmptyList() throws Exception {
var workspaceId = UUID.randomUUID();
when(projectService.getProjects(eq(workspaceId), isNull())).thenReturn(List.of());
mockMvc
.perform(get("/workspaces/{workspaceId}/projects", workspaceId))
.andExpect(status().isOk())
.andExpect(jsonPath("$").isArray())
.andExpect(jsonPath("$").isEmpty());
}
@Test
void getProjectsShouldFilterByName() throws Exception {
var workspaceId = UUID.randomUUID();
var projectId = UUID.randomUUID();
var response =
new ProjectResponse(projectId, workspaceId, "Filtered Project", "Desc", Instant.now());
when(projectService.getProjects(eq(workspaceId), eq("filter"))).thenReturn(List.of(response));
mockMvc
.perform(get("/workspaces/{workspaceId}/projects", workspaceId).param("name", "filter"))
.andExpect(status().isOk())
.andExpect(jsonPath("$[0].id").value(projectId.toString()))
.andExpect(jsonPath("$[0].name").value("Filtered Project"));
}
@Test
void createProjectShouldReturn200() throws Exception {
var workspaceId = UUID.randomUUID();
var projectId = UUID.randomUUID();
var request = new ProjectRequest("New Project", "A description");
var response =
new ProjectResponse(projectId, workspaceId, "New Project", "A description", Instant.now());
when(projectService.createProject(eq(workspaceId), any())).thenReturn(response);
mockMvc
.perform(
post("/workspaces/{workspaceId}/projects", workspaceId)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(projectId.toString()))
.andExpect(jsonPath("$.name").value("New Project"))
.andExpect(jsonPath("$.workspaceId").value(workspaceId.toString()));
}
@Test
void createProjectShouldReturn404WhenWorkspaceNotFound() throws Exception {
var workspaceId = UUID.randomUUID();
var request = new ProjectRequest("New Project", "desc");
when(projectService.createProject(eq(workspaceId), any()))
.thenThrow(new NotFoundException("Workspace with id '" + workspaceId + "' not found"));
mockMvc
.perform(
post("/workspaces/{workspaceId}/projects", workspaceId)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.status").value(404))
.andExpect(jsonPath("$.error").value("Not Found"));
}
@Test
void createProjectShouldReturn409OnConflict() throws Exception {
var workspaceId = UUID.randomUUID();
var request = new ProjectRequest("Duplicate", "desc");
when(projectService.createProject(eq(workspaceId), any()))
.thenThrow(
new ConflictException(
"A project with the name 'Duplicate' already exists in this workspace."));
mockMvc
.perform(
post("/workspaces/{workspaceId}/projects", workspaceId)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isConflict())
.andExpect(jsonPath("$.status").value(409))
.andExpect(jsonPath("$.error").value("Conflict"));
}
@Test
void updateProjectShouldReturn200() throws Exception {
var workspaceId = UUID.randomUUID();
var projectId = UUID.randomUUID();
var request = new ProjectRequest("Updated", "Updated desc");
var response =
new ProjectResponse(projectId, workspaceId, "Updated", "Updated desc", Instant.now());
when(projectService.updateProject(eq(projectId), any())).thenReturn(response);
mockMvc
.perform(
put("/workspaces/{workspaceId}/projects/{projectId}", workspaceId, projectId)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("Updated"));
}
@Test
void updateProjectShouldReturn404OnNotFound() throws Exception {
var workspaceId = UUID.randomUUID();
var projectId = UUID.randomUUID();
var request = new ProjectRequest("Updated", "desc");
when(projectService.updateProject(eq(projectId), any()))
.thenThrow(new NotFoundException("Project with id '" + projectId + "' not found"));
mockMvc
.perform(
put("/workspaces/{workspaceId}/projects/{projectId}", workspaceId, projectId)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.status").value(404))
.andExpect(jsonPath("$.error").value("Not Found"));
}
@Test
void deleteProjectShouldReturn200() throws Exception {
var workspaceId = UUID.randomUUID();
var projectId = UUID.randomUUID();
doNothing().when(projectService).deleteProject(projectId);
mockMvc
.perform(delete("/workspaces/{workspaceId}/projects/{projectId}", workspaceId, projectId))
.andExpect(status().isOk());
}
@Test
void deleteProjectShouldReturn404OnNotFound() throws Exception {
var workspaceId = UUID.randomUUID();
var projectId = UUID.randomUUID();
doThrow(new NotFoundException("Project with id '" + projectId + "' not found"))
.when(projectService)
.deleteProject(projectId);
mockMvc
.perform(delete("/workspaces/{workspaceId}/projects/{projectId}", workspaceId, projectId))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.status").value(404))
.andExpect(jsonPath("$.error").value("Not Found"));
}
}

View File

@ -0,0 +1,274 @@
package ai.decompile.project.controller;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import ai.decompile.workspace.model.dto.ProjectRequest;
import ai.decompile.workspace.model.dto.ProjectResponse;
import ai.decompile.workspace.model.dto.WorkspaceRequest;
import ai.decompile.workspace.model.dto.WorkspaceResponse;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.persistence.EntityManager;
import java.util.UUID;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
import org.springframework.http.MediaType;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.transaction.annotation.Transactional;
@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("test")
@Transactional
class ProjectIntegrationTest {
@Autowired private MockMvc mockMvc;
@Autowired private EntityManager entityManager;
private final ObjectMapper objectMapper = new ObjectMapper();
@Test
void getProjectsShouldReturnEmptyList() throws Exception {
var workspaceId = createWorkspace("Empty Workspace");
mockMvc
.perform(get("/workspaces/{workspaceId}/projects", workspaceId))
.andExpect(status().isOk())
.andExpect(jsonPath("$").isArray())
.andExpect(jsonPath("$").isEmpty());
}
@Test
void getProjectsShouldFilterByName() throws Exception {
var workspaceId = createWorkspace("Filter WS");
mockMvc
.perform(
post("/workspaces/{workspaceId}/projects", workspaceId)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(new ProjectRequest("Alpha", "desc"))))
.andExpect(status().isOk());
mockMvc
.perform(
post("/workspaces/{workspaceId}/projects", workspaceId)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(new ProjectRequest("Beta", "desc"))))
.andExpect(status().isOk());
mockMvc
.perform(get("/workspaces/{workspaceId}/projects", workspaceId).param("name", "alpha"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.length()").value(1))
.andExpect(jsonPath("$[0].name").value("Alpha"));
mockMvc
.perform(get("/workspaces/{workspaceId}/projects", workspaceId))
.andExpect(status().isOk())
.andExpect(jsonPath("$.length()").value(2));
}
@Test
void fullCrudFlow() throws Exception {
var workspaceId = createWorkspace("CRUD Workspace");
var request = new ProjectRequest("CRUD Project", "CRUD Description");
var createdJson =
mockMvc
.perform(
post("/workspaces/{workspaceId}/projects", workspaceId)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("CRUD Project"))
.andExpect(jsonPath("$.workspaceId").value(workspaceId.toString()))
.andReturn()
.getResponse()
.getContentAsString();
var created = objectMapper.readValue(createdJson, ProjectResponse.class);
var updateRequest = new ProjectRequest("CRUD Updated", "Updated Description");
mockMvc
.perform(
put("/workspaces/{workspaceId}/projects/{projectId}", workspaceId, created.id())
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(updateRequest)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("CRUD Updated"));
mockMvc
.perform(
delete("/workspaces/{workspaceId}/projects/{projectId}", workspaceId, created.id()))
.andExpect(status().isOk());
mockMvc
.perform(
delete("/workspaces/{workspaceId}/projects/{projectId}", workspaceId, created.id()))
.andExpect(status().isNotFound());
}
@Test
void createProjectShouldReturn409OnDuplicateNameInSameWorkspace() throws Exception {
var workspaceId = createWorkspace("Dup WS");
var request = new ProjectRequest("Unique Name", "desc");
mockMvc
.perform(
post("/workspaces/{workspaceId}/projects", workspaceId)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isOk());
mockMvc
.perform(
post("/workspaces/{workspaceId}/projects", workspaceId)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isConflict())
.andExpect(jsonPath("$.status").value(409))
.andExpect(jsonPath("$.error").value("Conflict"));
}
@Test
void sameNameInDifferentWorkspacesShouldSucceed() throws Exception {
var ws1 = createWorkspace("Workspace One");
var ws2 = createWorkspace("Workspace Two");
var request = new ProjectRequest("Same Name", "desc");
mockMvc
.perform(
post("/workspaces/{workspaceId}/projects", ws1)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isOk());
mockMvc
.perform(
post("/workspaces/{workspaceId}/projects", ws2)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isOk());
}
@Test
void createProjectShouldReturn404WhenWorkspaceDoesNotExist() throws Exception {
var nonExistentId = UUID.randomUUID();
var request = new ProjectRequest("Project", "desc");
mockMvc
.perform(
post("/workspaces/{workspaceId}/projects", nonExistentId)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.status").value(404));
}
@Test
void nonexistentProjectShouldReturn404() throws Exception {
var workspaceId = createWorkspace("WS");
var nonExistentId = UUID.randomUUID();
var request = new ProjectRequest("Update", "desc");
mockMvc
.perform(
put("/workspaces/{workspaceId}/projects/{projectId}", workspaceId, nonExistentId)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.status").value(404));
mockMvc
.perform(
delete("/workspaces/{workspaceId}/projects/{projectId}", workspaceId, nonExistentId))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.status").value(404));
}
@Test
void workspaceListShouldReturnProjectCount() throws Exception {
var workspaceId = createWorkspace("Count WS");
mockMvc
.perform(get("/workspaces"))
.andExpect(status().isOk())
.andExpect(jsonPath("$[0].projectCount").value(0));
mockMvc
.perform(
post("/workspaces/{workspaceId}/projects", workspaceId)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(new ProjectRequest("P1", "desc"))))
.andExpect(status().isOk());
mockMvc
.perform(
post("/workspaces/{workspaceId}/projects", workspaceId)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(new ProjectRequest("P2", "desc"))))
.andExpect(status().isOk());
entityManager.flush();
entityManager.clear();
mockMvc
.perform(get("/workspaces"))
.andExpect(status().isOk())
.andExpect(jsonPath("$[0].projectCount").value(2));
}
@Test
void listProjectsForSpecificWorkspace() throws Exception {
var ws1 = createWorkspace("WS1");
var ws2 = createWorkspace("WS2");
mockMvc
.perform(
post("/workspaces/{workspaceId}/projects", ws1)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(new ProjectRequest("Project A", "desc"))))
.andExpect(status().isOk());
mockMvc
.perform(
post("/workspaces/{workspaceId}/projects", ws2)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(new ProjectRequest("Project B", "desc"))))
.andExpect(status().isOk());
mockMvc
.perform(get("/workspaces/{workspaceId}/projects", ws1))
.andExpect(status().isOk())
.andExpect(jsonPath("$.length()").value(1))
.andExpect(jsonPath("$[0].name").value("Project A"));
mockMvc
.perform(get("/workspaces/{workspaceId}/projects", ws2))
.andExpect(status().isOk())
.andExpect(jsonPath("$.length()").value(1))
.andExpect(jsonPath("$[0].name").value("Project B"));
}
private UUID createWorkspace(String name) throws Exception {
var request = new WorkspaceRequest(name, "description");
var json =
mockMvc
.perform(
post("/workspaces")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isOk())
.andReturn()
.getResponse()
.getContentAsString();
return objectMapper.readValue(json, WorkspaceResponse.class).id();
}
}

View File

@ -35,11 +35,11 @@ class WorkspaceControllerTest {
@Test @Test
void getWorkspacesShouldReturn200WithList() throws Exception { void getWorkspacesShouldReturn200WithList() throws Exception {
var id = UUID.randomUUID(); var id = UUID.randomUUID();
var response = new WorkspaceResponse(id, "Test", "Desc", Instant.now()); var response = new WorkspaceResponse(id, "Test", "Desc", Instant.now(), 0);
when(workspaceService.getWorkspaces(isNull())).thenReturn(List.of(response)); when(workspaceService.getWorkspaces(isNull())).thenReturn(List.of(response));
mockMvc mockMvc
.perform(get("/workspace")) .perform(get("/workspaces"))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$[0].id").value(id.toString())) .andExpect(jsonPath("$[0].id").value(id.toString()))
.andExpect(jsonPath("$[0].name").value("Test")) .andExpect(jsonPath("$[0].name").value("Test"))
@ -49,26 +49,53 @@ class WorkspaceControllerTest {
@Test @Test
void getWorkspacesShouldFilterByName() throws Exception { void getWorkspacesShouldFilterByName() throws Exception {
var id = UUID.randomUUID(); var id = UUID.randomUUID();
var response = new WorkspaceResponse(id, "Filtered", "Desc", Instant.now()); var response = new WorkspaceResponse(id, "Filtered", "Desc", Instant.now(), 0);
when(workspaceService.getWorkspaces(eq("fil"))).thenReturn(List.of(response)); when(workspaceService.getWorkspaces(eq("fil"))).thenReturn(List.of(response));
mockMvc mockMvc
.perform(get("/workspace").param("name", "fil")) .perform(get("/workspaces").param("name", "fil"))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$[0].id").value(id.toString())) .andExpect(jsonPath("$[0].id").value(id.toString()))
.andExpect(jsonPath("$[0].name").value("Filtered")); .andExpect(jsonPath("$[0].name").value("Filtered"));
} }
@Test
void getWorkspaceShouldReturn200() throws Exception {
var id = UUID.randomUUID();
var response = new WorkspaceResponse(id, "Single", "Desc", Instant.now(), 0);
when(workspaceService.getWorkspaceById(id)).thenReturn(response);
mockMvc
.perform(get("/workspaces/{id}", id))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(id.toString()))
.andExpect(jsonPath("$.name").value("Single"))
.andExpect(jsonPath("$.description").value("Desc"));
}
@Test
void getWorkspaceShouldReturn404OnNotFound() throws Exception {
var id = UUID.randomUUID();
when(workspaceService.getWorkspaceById(id))
.thenThrow(new NotFoundException("Workspace with id '" + id + "' not found"));
mockMvc
.perform(get("/workspaces/{id}", id))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.status").value(404))
.andExpect(jsonPath("$.error").value("Not Found"));
}
@Test @Test
void createWorkspaceShouldReturn200() throws Exception { void createWorkspaceShouldReturn200() throws Exception {
var id = UUID.randomUUID(); var id = UUID.randomUUID();
var request = new WorkspaceRequest("New Workspace", "A description"); var request = new WorkspaceRequest("New Workspace", "A description");
var response = new WorkspaceResponse(id, "New Workspace", "A description", Instant.now()); var response = new WorkspaceResponse(id, "New Workspace", "A description", Instant.now(), 0);
when(workspaceService.createWorkspace(any())).thenReturn(response); when(workspaceService.createWorkspace(any())).thenReturn(response);
mockMvc mockMvc
.perform( .perform(
post("/workspace") post("/workspaces")
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request))) .content(objectMapper.writeValueAsString(request)))
.andExpect(status().isOk()) .andExpect(status().isOk())
@ -84,7 +111,7 @@ class WorkspaceControllerTest {
mockMvc mockMvc
.perform( .perform(
post("/workspace") post("/workspaces")
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request))) .content(objectMapper.writeValueAsString(request)))
.andExpect(status().isConflict()) .andExpect(status().isConflict())
@ -96,12 +123,12 @@ class WorkspaceControllerTest {
void updateWorkspaceShouldReturn200() throws Exception { void updateWorkspaceShouldReturn200() throws Exception {
var id = UUID.randomUUID(); var id = UUID.randomUUID();
var request = new WorkspaceRequest("Updated", "Updated desc"); var request = new WorkspaceRequest("Updated", "Updated desc");
var response = new WorkspaceResponse(id, "Updated", "Updated desc", Instant.now()); var response = new WorkspaceResponse(id, "Updated", "Updated desc", Instant.now(), 0);
when(workspaceService.updateWorkspace(eq(id), any())).thenReturn(response); when(workspaceService.updateWorkspace(eq(id), any())).thenReturn(response);
mockMvc mockMvc
.perform( .perform(
put("/workspace/{id}", id) put("/workspaces/{id}", id)
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request))) .content(objectMapper.writeValueAsString(request)))
.andExpect(status().isOk()) .andExpect(status().isOk())
@ -117,7 +144,7 @@ class WorkspaceControllerTest {
mockMvc mockMvc
.perform( .perform(
put("/workspace/{id}", id) put("/workspaces/{id}", id)
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request))) .content(objectMapper.writeValueAsString(request)))
.andExpect(status().isNotFound()) .andExpect(status().isNotFound())
@ -130,7 +157,7 @@ class WorkspaceControllerTest {
var id = UUID.randomUUID(); var id = UUID.randomUUID();
doNothing().when(workspaceService).deleteWorkspace(id); doNothing().when(workspaceService).deleteWorkspace(id);
mockMvc.perform(delete("/workspace/{id}", id)).andExpect(status().isOk()); mockMvc.perform(delete("/workspaces/{id}", id)).andExpect(status().isOk());
} }
@Test @Test
@ -141,7 +168,7 @@ class WorkspaceControllerTest {
.deleteWorkspace(id); .deleteWorkspace(id);
mockMvc mockMvc
.perform(delete("/workspace/{id}", id)) .perform(delete("/workspaces/{id}", id))
.andExpect(status().isNotFound()) .andExpect(status().isNotFound())
.andExpect(jsonPath("$.status").value(404)) .andExpect(jsonPath("$.status").value(404))
.andExpect(jsonPath("$.error").value("Not Found")); .andExpect(jsonPath("$.error").value("Not Found"));

View File

@ -28,12 +28,37 @@ class WorkspaceIntegrationTest {
@Test @Test
void getWorkspacesShouldReturnEmptyList() throws Exception { void getWorkspacesShouldReturnEmptyList() throws Exception {
mockMvc mockMvc
.perform(get("/workspace")) .perform(get("/workspaces"))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$").isArray()) .andExpect(jsonPath("$").isArray())
.andExpect(jsonPath("$").isEmpty()); .andExpect(jsonPath("$").isEmpty());
} }
@Test
void getWorkspaceByIdShouldReturnWorkspace() throws Exception {
var request = new WorkspaceRequest("Get By Id", "desc");
var createdJson =
mockMvc
.perform(
post("/workspaces")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isOk())
.andReturn()
.getResponse()
.getContentAsString();
var created = objectMapper.readValue(createdJson, WorkspaceResponse.class);
mockMvc
.perform(get("/workspaces/{id}", created.id()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(created.id().toString()))
.andExpect(jsonPath("$.name").value("Get By Id"))
.andExpect(jsonPath("$.description").value("desc"))
.andExpect(jsonPath("$.projectCount").value(0));
}
@Test @Test
void getWorkspacesShouldFilterByName() throws Exception { void getWorkspacesShouldFilterByName() throws Exception {
var requestA = new WorkspaceRequest("Alpha Project", "desc"); var requestA = new WorkspaceRequest("Alpha Project", "desc");
@ -41,20 +66,20 @@ class WorkspaceIntegrationTest {
mockMvc mockMvc
.perform( .perform(
post("/workspace") post("/workspaces")
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(requestA))) .content(objectMapper.writeValueAsString(requestA)))
.andExpect(status().isOk()); .andExpect(status().isOk());
mockMvc mockMvc
.perform( .perform(
post("/workspace") post("/workspaces")
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(requestB))) .content(objectMapper.writeValueAsString(requestB)))
.andExpect(status().isOk()); .andExpect(status().isOk());
mockMvc mockMvc
.perform(get("/workspace").param("name", "alpha")) .perform(get("/workspaces").param("name", "alpha"))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$").isArray()) .andExpect(jsonPath("$").isArray())
.andExpect(jsonPath("$.length()").value(1)) .andExpect(jsonPath("$.length()").value(1))
@ -68,7 +93,7 @@ class WorkspaceIntegrationTest {
var createdJson = var createdJson =
mockMvc mockMvc
.perform( .perform(
post("/workspace") post("/workspaces")
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request))) .content(objectMapper.writeValueAsString(request)))
.andExpect(status().isOk()) .andExpect(status().isOk())
@ -82,15 +107,15 @@ class WorkspaceIntegrationTest {
var updateRequest = new WorkspaceRequest("CRUD Updated", "Updated Description"); var updateRequest = new WorkspaceRequest("CRUD Updated", "Updated Description");
mockMvc mockMvc
.perform( .perform(
put("/workspace/{id}", created.id()) put("/workspaces/{id}", created.id())
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(updateRequest))) .content(objectMapper.writeValueAsString(updateRequest)))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("CRUD Updated")); .andExpect(jsonPath("$.name").value("CRUD Updated"));
mockMvc.perform(delete("/workspace/{id}", created.id())).andExpect(status().isOk()); mockMvc.perform(delete("/workspaces/{id}", created.id())).andExpect(status().isOk());
mockMvc.perform(delete("/workspace/{id}", created.id())).andExpect(status().isNotFound()); mockMvc.perform(delete("/workspaces/{id}", created.id())).andExpect(status().isNotFound());
} }
@Test @Test
@ -99,14 +124,14 @@ class WorkspaceIntegrationTest {
mockMvc mockMvc
.perform( .perform(
post("/workspace") post("/workspaces")
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request))) .content(objectMapper.writeValueAsString(request)))
.andExpect(status().isOk()); .andExpect(status().isOk());
mockMvc mockMvc
.perform( .perform(
post("/workspace") post("/workspaces")
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request))) .content(objectMapper.writeValueAsString(request)))
.andExpect(status().isConflict()) .andExpect(status().isConflict())
@ -119,16 +144,21 @@ class WorkspaceIntegrationTest {
var nonExistentId = "00000000-0000-0000-0000-000000000000"; var nonExistentId = "00000000-0000-0000-0000-000000000000";
var request = new WorkspaceRequest("Update", "desc"); var request = new WorkspaceRequest("Update", "desc");
mockMvc
.perform(get("/workspaces/{id}", nonExistentId))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.status").value(404));
mockMvc mockMvc
.perform( .perform(
put("/workspace/{id}", nonExistentId) put("/workspaces/{id}", nonExistentId)
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request))) .content(objectMapper.writeValueAsString(request)))
.andExpect(status().isNotFound()) .andExpect(status().isNotFound())
.andExpect(jsonPath("$.status").value(404)); .andExpect(jsonPath("$.status").value(404));
mockMvc mockMvc
.perform(delete("/workspace/{id}", nonExistentId)) .perform(delete("/workspaces/{id}", nonExistentId))
.andExpect(status().isNotFound()) .andExpect(status().isNotFound())
.andExpect(jsonPath("$.status").value(404)); .andExpect(jsonPath("$.status").value(404));
} }

View File

@ -0,0 +1,214 @@
package ai.decompile.workspace.service;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
import ai.decompile.exception.ConflictException;
import ai.decompile.exception.NotFoundException;
import ai.decompile.workspace.model.dto.ProjectRequest;
import ai.decompile.workspace.model.dto.ProjectResponse;
import ai.decompile.workspace.model.entity.Project;
import ai.decompile.workspace.model.entity.Workspace;
import ai.decompile.workspace.model.repository.ProjectRepository;
import java.time.Instant;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import org.junit.jupiter.api.BeforeEach;
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.jpa.domain.Specification;
@ExtendWith(MockitoExtension.class)
class ProjectServiceTest {
@Mock private ProjectRepository projectRepository;
@Mock private WorkspaceService workspaceService;
@InjectMocks private ProjectService projectService;
private UUID workspaceId;
private UUID projectId;
private Workspace workspace;
private Project project;
private ProjectRequest request;
@BeforeEach
void setUp() {
workspaceId = UUID.randomUUID();
projectId = UUID.randomUUID();
workspace =
Workspace.builder()
.id(workspaceId)
.name("Test Workspace")
.description("WS Desc")
.createdAt(Instant.now())
.updatedAt(Instant.now())
.build();
project =
Project.builder()
.id(projectId)
.name("Test Project")
.description("Project Desc")
.workspace(workspace)
.createdAt(Instant.now())
.updatedAt(Instant.now())
.build();
request = new ProjectRequest("Test Project", "Project Desc");
}
@Test
void getProjectsShouldReturnEmptyList() {
when(projectRepository.findAll(any(Specification.class))).thenReturn(List.of());
List<ProjectResponse> result = projectService.getProjects(workspaceId, null);
assertTrue(result.isEmpty());
verify(projectRepository).findAll(any(Specification.class));
}
@Test
void getProjectsShouldReturnList() {
when(projectRepository.findAll(any(Specification.class))).thenReturn(List.of(project));
List<ProjectResponse> result = projectService.getProjects(workspaceId, null);
assertEquals(1, result.size());
assertEquals(projectId, result.getFirst().id());
assertEquals("Test Project", result.getFirst().name());
assertEquals(workspaceId, result.getFirst().workspaceId());
}
@Test
void getProjectsShouldFilterByName() {
when(projectRepository.findAll(any(Specification.class))).thenReturn(List.of(project));
List<ProjectResponse> result = projectService.getProjects(workspaceId, "test");
assertEquals(1, result.size());
assertEquals("Test Project", result.getFirst().name());
verify(projectRepository).findAll(any(Specification.class));
}
@Test
void createProjectShouldSucceed() {
when(workspaceService.getWorkspace(workspaceId)).thenReturn(workspace);
when(projectRepository.existsByNameIgnoreCaseAndWorkspace_Id(request.name(), workspaceId))
.thenReturn(false);
when(projectRepository.save(any(Project.class))).thenReturn(project);
ProjectResponse result = projectService.createProject(workspaceId, request);
assertEquals(projectId, result.id());
assertEquals("Test Project", result.name());
assertEquals(workspaceId, result.workspaceId());
verify(workspaceService).getWorkspace(workspaceId);
verify(projectRepository).existsByNameIgnoreCaseAndWorkspace_Id(request.name(), workspaceId);
verify(projectRepository).save(any(Project.class));
}
@Test
void createProjectShouldThrowNotFoundWhenWorkspaceDoesNotExist() {
when(workspaceService.getWorkspace(workspaceId))
.thenThrow(new NotFoundException("Workspace with id '" + workspaceId + "' not found"));
NotFoundException ex =
assertThrows(
NotFoundException.class, () -> projectService.createProject(workspaceId, request));
assertTrue(ex.getMessage().contains(workspaceId.toString()));
verify(projectRepository, never()).save(any());
}
@Test
void createProjectShouldThrowConflictWhenNameExistsInWorkspace() {
when(workspaceService.getWorkspace(workspaceId)).thenReturn(workspace);
when(projectRepository.existsByNameIgnoreCaseAndWorkspace_Id(request.name(), workspaceId))
.thenReturn(true);
ConflictException ex =
assertThrows(
ConflictException.class, () -> projectService.createProject(workspaceId, request));
assertTrue(ex.getMessage().contains("Test Project"));
verify(projectRepository, never()).save(any());
}
@Test
void updateProjectShouldSucceed() {
when(projectRepository.findById(projectId)).thenReturn(Optional.of(project));
when(projectRepository.save(any(Project.class))).thenReturn(project);
var newRequest = new ProjectRequest("Updated Name", "Updated Desc");
ProjectResponse result = projectService.updateProject(projectId, newRequest);
assertEquals(projectId, result.id());
assertEquals("Updated Name", result.name());
verify(projectRepository).findById(projectId);
verify(projectRepository).save(any(Project.class));
}
@Test
void updateProjectShouldSucceedWhenNameUnchanged() {
when(projectRepository.findById(projectId)).thenReturn(Optional.of(project));
when(projectRepository.save(any(Project.class))).thenReturn(project);
ProjectResponse result = projectService.updateProject(projectId, request);
assertEquals(projectId, result.id());
verify(projectRepository).save(any(Project.class));
}
@Test
void updateProjectShouldThrowNotFound() {
when(projectRepository.findById(projectId)).thenReturn(Optional.empty());
NotFoundException ex =
assertThrows(
NotFoundException.class, () -> projectService.updateProject(projectId, request));
assertTrue(ex.getMessage().contains(projectId.toString()));
verify(projectRepository, never()).save(any());
}
@Test
void updateProjectShouldThrowConflictWhenNameExistsInWorkspace() {
var newRequest = new ProjectRequest("New Name", "desc");
when(projectRepository.findById(projectId)).thenReturn(Optional.of(project));
when(projectRepository.existsByNameIgnoreCaseAndWorkspace_Id("New Name", workspaceId))
.thenReturn(true);
ConflictException ex =
assertThrows(
ConflictException.class, () -> projectService.updateProject(projectId, newRequest));
assertTrue(ex.getMessage().contains("New Name"));
verify(projectRepository, never()).save(any());
}
@Test
void deleteProjectShouldSucceed() {
when(projectRepository.findById(projectId)).thenReturn(Optional.of(project));
projectService.deleteProject(projectId);
verify(projectRepository).findById(projectId);
verify(projectRepository).delete(project);
}
@Test
void deleteProjectShouldThrowNotFound() {
when(projectRepository.findById(projectId)).thenReturn(Optional.empty());
NotFoundException ex =
assertThrows(NotFoundException.class, () -> projectService.deleteProject(projectId));
assertTrue(ex.getMessage().contains(projectId.toString()));
verify(projectRepository, never()).delete((Project) any());
}
}

View File

@ -43,6 +43,7 @@ class WorkspaceServiceTest {
.description("Test Description") .description("Test Description")
.createdAt(Instant.now()) .createdAt(Instant.now())
.updatedAt(Instant.now()) .updatedAt(Instant.now())
.projects(List.of())
.build(); .build();
request = new WorkspaceRequest("Test Workspace", "Test Description"); request = new WorkspaceRequest("Test Workspace", "Test Description");
} }
@ -66,6 +67,7 @@ class WorkspaceServiceTest {
assertEquals(1, result.size()); assertEquals(1, result.size());
assertEquals(id, result.getFirst().id()); assertEquals(id, result.getFirst().id());
assertEquals("Test Workspace", result.getFirst().name()); assertEquals("Test Workspace", result.getFirst().name());
assertEquals(0, result.getFirst().projectCount());
} }
@Test @Test
@ -76,9 +78,32 @@ class WorkspaceServiceTest {
assertEquals(1, result.size()); assertEquals(1, result.size());
assertEquals("Test Workspace", result.getFirst().name()); assertEquals("Test Workspace", result.getFirst().name());
assertEquals(0, result.getFirst().projectCount());
verify(workspaceRepository).findAll(any(Specification.class)); verify(workspaceRepository).findAll(any(Specification.class));
} }
@Test
void getWorkspaceByIdShouldReturnWorkspace() {
when(workspaceRepository.findById(id)).thenReturn(Optional.of(workspace));
WorkspaceResponse result = workspaceService.getWorkspaceById(id);
assertEquals(id, result.id());
assertEquals("Test Workspace", result.name());
assertEquals(0, result.projectCount());
verify(workspaceRepository).findById(id);
}
@Test
void getWorkspaceByIdShouldThrowNotFound() {
when(workspaceRepository.findById(id)).thenReturn(Optional.empty());
NotFoundException ex =
assertThrows(NotFoundException.class, () -> workspaceService.getWorkspaceById(id));
assertTrue(ex.getMessage().contains(id.toString()));
}
@Test @Test
void createWorkspaceShouldSucceed() { void createWorkspaceShouldSucceed() {
when(workspaceRepository.existsByNameIgnoreCase(request.name())).thenReturn(false); when(workspaceRepository.existsByNameIgnoreCase(request.name())).thenReturn(false);