feat(project): implement CRUD operations for projects
This commit is contained in:
parent
d3362571f2
commit
82074add45
@ -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);
|
||||
}
|
||||
}
|
||||
@ -11,7 +11,7 @@ import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/workspace")
|
||||
@RequestMapping("/workspaces")
|
||||
@RequiredArgsConstructor
|
||||
public class WorkspaceController {
|
||||
private final WorkspaceService workspaceService;
|
||||
@ -30,6 +30,16 @@ public class WorkspaceController {
|
||||
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(
|
||||
summary = "Create a workspace",
|
||||
description = "Create a workspace",
|
||||
|
||||
@ -0,0 +1,5 @@
|
||||
package ai.decompile.workspace.model.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
public record ProjectRequest(@NotBlank String name, String description) {}
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
@ -7,12 +7,18 @@ import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
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) {
|
||||
var projects = workspace.getProjects();
|
||||
return new WorkspaceResponse(
|
||||
workspace.getId(),
|
||||
workspace.getName(),
|
||||
workspace.getDescription(),
|
||||
workspace.getUpdatedAt());
|
||||
workspace.getUpdatedAt(),
|
||||
projects != null ? projects.size() : 0);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -2,6 +2,7 @@ package ai.decompile.workspace.model.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import lombok.*;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
@ -32,4 +33,7 @@ public class Workspace {
|
||||
@UpdateTimestamp
|
||||
@Column(name = "updated_at", nullable = false)
|
||||
private Instant updatedAt;
|
||||
|
||||
@OneToMany(mappedBy = "workspace", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
private List<Project> projects;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -1,12 +1,12 @@
|
||||
package ai.decompile.workspace.model.specification;
|
||||
|
||||
import ai.decompile.workspace.model.entity.Workspace;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.springframework.data.jpa.domain.Specification;
|
||||
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class WorkspaceSpecification {
|
||||
|
||||
private WorkspaceSpecification() {}
|
||||
|
||||
public static Specification<Workspace> nameContains(String name) {
|
||||
return (root, _, cb) -> cb.like(cb.lower(root.get("name")), "%" + name.toLowerCase() + "%");
|
||||
}
|
||||
|
||||
@ -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"));
|
||||
}
|
||||
}
|
||||
@ -19,13 +19,18 @@ public class WorkspaceService {
|
||||
private final WorkspaceRepository workspaceRepository;
|
||||
|
||||
public List<WorkspaceResponse> getWorkspaces(String name) {
|
||||
List<Workspace> workspaces;
|
||||
if (StringUtils.isNotBlank(name)) {
|
||||
return workspaceRepository.findAll(WorkspaceSpecification.nameContains(name)).stream()
|
||||
.map(WorkspaceResponse::from)
|
||||
.toList();
|
||||
workspaces = workspaceRepository.findAll(WorkspaceSpecification.nameContains(name));
|
||||
} else {
|
||||
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) {
|
||||
@ -55,7 +60,7 @@ public class WorkspaceService {
|
||||
workspaceRepository.delete(workspace);
|
||||
}
|
||||
|
||||
private Workspace getWorkspace(UUID id) {
|
||||
protected Workspace getWorkspace(UUID id) {
|
||||
return workspaceRepository
|
||||
.findById(id)
|
||||
.orElseThrow(() -> new NotFoundException("Workspace with id '" + id + "' not found"));
|
||||
|
||||
@ -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
|
||||
);
|
||||
@ -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"));
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -35,11 +35,11 @@ class WorkspaceControllerTest {
|
||||
@Test
|
||||
void getWorkspacesShouldReturn200WithList() throws Exception {
|
||||
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));
|
||||
|
||||
mockMvc
|
||||
.perform(get("/workspace"))
|
||||
.perform(get("/workspaces"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$[0].id").value(id.toString()))
|
||||
.andExpect(jsonPath("$[0].name").value("Test"))
|
||||
@ -49,26 +49,53 @@ class WorkspaceControllerTest {
|
||||
@Test
|
||||
void getWorkspacesShouldFilterByName() throws Exception {
|
||||
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));
|
||||
|
||||
mockMvc
|
||||
.perform(get("/workspace").param("name", "fil"))
|
||||
.perform(get("/workspaces").param("name", "fil"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$[0].id").value(id.toString()))
|
||||
.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
|
||||
void createWorkspaceShouldReturn200() throws Exception {
|
||||
var id = UUID.randomUUID();
|
||||
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);
|
||||
|
||||
mockMvc
|
||||
.perform(
|
||||
post("/workspace")
|
||||
post("/workspaces")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isOk())
|
||||
@ -84,7 +111,7 @@ class WorkspaceControllerTest {
|
||||
|
||||
mockMvc
|
||||
.perform(
|
||||
post("/workspace")
|
||||
post("/workspaces")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isConflict())
|
||||
@ -96,12 +123,12 @@ class WorkspaceControllerTest {
|
||||
void updateWorkspaceShouldReturn200() throws Exception {
|
||||
var id = UUID.randomUUID();
|
||||
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);
|
||||
|
||||
mockMvc
|
||||
.perform(
|
||||
put("/workspace/{id}", id)
|
||||
put("/workspaces/{id}", id)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isOk())
|
||||
@ -117,7 +144,7 @@ class WorkspaceControllerTest {
|
||||
|
||||
mockMvc
|
||||
.perform(
|
||||
put("/workspace/{id}", id)
|
||||
put("/workspaces/{id}", id)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isNotFound())
|
||||
@ -130,7 +157,7 @@ class WorkspaceControllerTest {
|
||||
var id = UUID.randomUUID();
|
||||
doNothing().when(workspaceService).deleteWorkspace(id);
|
||||
|
||||
mockMvc.perform(delete("/workspace/{id}", id)).andExpect(status().isOk());
|
||||
mockMvc.perform(delete("/workspaces/{id}", id)).andExpect(status().isOk());
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -141,7 +168,7 @@ class WorkspaceControllerTest {
|
||||
.deleteWorkspace(id);
|
||||
|
||||
mockMvc
|
||||
.perform(delete("/workspace/{id}", id))
|
||||
.perform(delete("/workspaces/{id}", id))
|
||||
.andExpect(status().isNotFound())
|
||||
.andExpect(jsonPath("$.status").value(404))
|
||||
.andExpect(jsonPath("$.error").value("Not Found"));
|
||||
|
||||
@ -28,12 +28,37 @@ class WorkspaceIntegrationTest {
|
||||
@Test
|
||||
void getWorkspacesShouldReturnEmptyList() throws Exception {
|
||||
mockMvc
|
||||
.perform(get("/workspace"))
|
||||
.perform(get("/workspaces"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$").isArray())
|
||||
.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
|
||||
void getWorkspacesShouldFilterByName() throws Exception {
|
||||
var requestA = new WorkspaceRequest("Alpha Project", "desc");
|
||||
@ -41,20 +66,20 @@ class WorkspaceIntegrationTest {
|
||||
|
||||
mockMvc
|
||||
.perform(
|
||||
post("/workspace")
|
||||
post("/workspaces")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(requestA)))
|
||||
.andExpect(status().isOk());
|
||||
|
||||
mockMvc
|
||||
.perform(
|
||||
post("/workspace")
|
||||
post("/workspaces")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(requestB)))
|
||||
.andExpect(status().isOk());
|
||||
|
||||
mockMvc
|
||||
.perform(get("/workspace").param("name", "alpha"))
|
||||
.perform(get("/workspaces").param("name", "alpha"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$").isArray())
|
||||
.andExpect(jsonPath("$.length()").value(1))
|
||||
@ -68,7 +93,7 @@ class WorkspaceIntegrationTest {
|
||||
var createdJson =
|
||||
mockMvc
|
||||
.perform(
|
||||
post("/workspace")
|
||||
post("/workspaces")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isOk())
|
||||
@ -82,15 +107,15 @@ class WorkspaceIntegrationTest {
|
||||
var updateRequest = new WorkspaceRequest("CRUD Updated", "Updated Description");
|
||||
mockMvc
|
||||
.perform(
|
||||
put("/workspace/{id}", created.id())
|
||||
put("/workspaces/{id}", created.id())
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(updateRequest)))
|
||||
.andExpect(status().isOk())
|
||||
.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
|
||||
@ -99,14 +124,14 @@ class WorkspaceIntegrationTest {
|
||||
|
||||
mockMvc
|
||||
.perform(
|
||||
post("/workspace")
|
||||
post("/workspaces")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isOk());
|
||||
|
||||
mockMvc
|
||||
.perform(
|
||||
post("/workspace")
|
||||
post("/workspaces")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isConflict())
|
||||
@ -119,16 +144,21 @@ class WorkspaceIntegrationTest {
|
||||
var nonExistentId = "00000000-0000-0000-0000-000000000000";
|
||||
var request = new WorkspaceRequest("Update", "desc");
|
||||
|
||||
mockMvc
|
||||
.perform(get("/workspaces/{id}", nonExistentId))
|
||||
.andExpect(status().isNotFound())
|
||||
.andExpect(jsonPath("$.status").value(404));
|
||||
|
||||
mockMvc
|
||||
.perform(
|
||||
put("/workspace/{id}", nonExistentId)
|
||||
put("/workspaces/{id}", nonExistentId)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isNotFound())
|
||||
.andExpect(jsonPath("$.status").value(404));
|
||||
|
||||
mockMvc
|
||||
.perform(delete("/workspace/{id}", nonExistentId))
|
||||
.perform(delete("/workspaces/{id}", nonExistentId))
|
||||
.andExpect(status().isNotFound())
|
||||
.andExpect(jsonPath("$.status").value(404));
|
||||
}
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
@ -43,6 +43,7 @@ class WorkspaceServiceTest {
|
||||
.description("Test Description")
|
||||
.createdAt(Instant.now())
|
||||
.updatedAt(Instant.now())
|
||||
.projects(List.of())
|
||||
.build();
|
||||
request = new WorkspaceRequest("Test Workspace", "Test Description");
|
||||
}
|
||||
@ -66,6 +67,7 @@ class WorkspaceServiceTest {
|
||||
assertEquals(1, result.size());
|
||||
assertEquals(id, result.getFirst().id());
|
||||
assertEquals("Test Workspace", result.getFirst().name());
|
||||
assertEquals(0, result.getFirst().projectCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -76,9 +78,32 @@ class WorkspaceServiceTest {
|
||||
|
||||
assertEquals(1, result.size());
|
||||
assertEquals("Test Workspace", result.getFirst().name());
|
||||
assertEquals(0, result.getFirst().projectCount());
|
||||
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
|
||||
void createWorkspaceShouldSucceed() {
|
||||
when(workspaceRepository.existsByNameIgnoreCase(request.name())).thenReturn(false);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user