feat(binary): implement CRUD operations for binaries
This commit is contained in:
parent
82074add45
commit
559f137a27
3
.gitignore
vendored
3
.gitignore
vendored
@ -3,6 +3,9 @@
|
||||
|
||||
target/
|
||||
|
||||
# Uploaded binaries
|
||||
uploads/
|
||||
|
||||
### Intellij ###
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
@ -0,0 +1,79 @@
|
||||
package ai.decompile.workspace.controller;
|
||||
|
||||
import ai.decompile.workspace.model.dto.BinaryResponse;
|
||||
import ai.decompile.workspace.service.BinaryService;
|
||||
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.core.io.InputStreamResource;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
public class BinaryController {
|
||||
private final BinaryService binaryService;
|
||||
|
||||
@Operation(
|
||||
summary = "List binaries of a project",
|
||||
description =
|
||||
"Retrieve all binaries belonging to a project, optionally filtered by filename.",
|
||||
tags = {"Binary"},
|
||||
operationId = "getBinaries")
|
||||
@GetMapping("/projects/{projectId}/binaries")
|
||||
public List<BinaryResponse> getBinaries(
|
||||
@PathVariable UUID projectId,
|
||||
@Parameter(
|
||||
description = "Filter binaries whose filename contains this value (case-insensitive)")
|
||||
@RequestParam(required = false)
|
||||
String name) {
|
||||
return binaryService.getBinaries(projectId, name);
|
||||
}
|
||||
|
||||
@Operation(
|
||||
summary = "Upload a binary",
|
||||
description = "Upload a binary file to a project.",
|
||||
tags = {"Binary"},
|
||||
operationId = "uploadBinary")
|
||||
@PostMapping(
|
||||
value = "/projects/{projectId}/binaries",
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@ResponseStatus(HttpStatus.CREATED)
|
||||
public BinaryResponse uploadBinary(
|
||||
@PathVariable UUID projectId, @RequestPart MultipartFile file) {
|
||||
return binaryService.uploadBinary(projectId, file);
|
||||
}
|
||||
|
||||
@Operation(
|
||||
summary = "Delete a binary",
|
||||
description = "Delete a binary and its file.",
|
||||
tags = {"Binary"},
|
||||
operationId = "deleteBinary")
|
||||
@DeleteMapping("/binaries/{binaryId}")
|
||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||
public void deleteBinary(@PathVariable UUID binaryId) {
|
||||
binaryService.deleteBinary(binaryId);
|
||||
}
|
||||
|
||||
@Operation(
|
||||
summary = "Download a binary",
|
||||
description = "Download the binary file.",
|
||||
tags = {"Binary"},
|
||||
operationId = "downloadBinary")
|
||||
@GetMapping("/binaries/{binaryId}/download")
|
||||
public ResponseEntity<InputStreamResource> downloadBinary(@PathVariable UUID binaryId) {
|
||||
var resource = binaryService.downloadBinary(binaryId);
|
||||
return ResponseEntity.ok()
|
||||
.header(
|
||||
HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + resource.filename() + "\"")
|
||||
.contentLength(resource.contentLength())
|
||||
.contentType(MediaType.APPLICATION_OCTET_STREAM)
|
||||
.body(new InputStreamResource(resource.inputStream()));
|
||||
}
|
||||
}
|
||||
@ -11,7 +11,6 @@ import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/workspaces/{workspaceId}/projects")
|
||||
@RequiredArgsConstructor
|
||||
public class ProjectController {
|
||||
private final ProjectService projectService;
|
||||
@ -21,7 +20,7 @@ public class ProjectController {
|
||||
description = "Retrieve all projects belonging to a workspace, optionally filtered by name.",
|
||||
tags = {"Project"},
|
||||
operationId = "getProjects")
|
||||
@GetMapping
|
||||
@GetMapping("/workspaces/{workspaceId}/projects")
|
||||
public List<ProjectResponse> getProjects(
|
||||
@PathVariable UUID workspaceId,
|
||||
@Parameter(description = "Filter projects whose name contains this value (case-insensitive)")
|
||||
@ -30,12 +29,22 @@ public class ProjectController {
|
||||
return projectService.getProjects(workspaceId, name);
|
||||
}
|
||||
|
||||
@Operation(
|
||||
summary = "Get a project",
|
||||
description = "Retrieve a single project by ID.",
|
||||
tags = {"Project"},
|
||||
operationId = "getProject")
|
||||
@GetMapping("/projects/{projectId}")
|
||||
public ProjectResponse getProject(@PathVariable UUID projectId) {
|
||||
return projectService.getProjectById(projectId);
|
||||
}
|
||||
|
||||
@Operation(
|
||||
summary = "Create a project",
|
||||
description = "Create a project in a workspace.",
|
||||
tags = {"Project"},
|
||||
operationId = "createProject")
|
||||
@PostMapping
|
||||
@PostMapping("/workspaces/{workspaceId}/projects")
|
||||
public ProjectResponse createProject(
|
||||
@PathVariable UUID workspaceId, @RequestBody ProjectRequest request) {
|
||||
return projectService.createProject(workspaceId, request);
|
||||
@ -46,11 +55,9 @@ public class ProjectController {
|
||||
description = "Update a project's name and description.",
|
||||
tags = {"Project"},
|
||||
operationId = "updateProject")
|
||||
@PutMapping("/{projectId}")
|
||||
@PutMapping("/projects/{projectId}")
|
||||
public ProjectResponse updateProject(
|
||||
@PathVariable UUID workspaceId,
|
||||
@PathVariable UUID projectId,
|
||||
@RequestBody ProjectRequest request) {
|
||||
@PathVariable UUID projectId, @RequestBody ProjectRequest request) {
|
||||
return projectService.updateProject(projectId, request);
|
||||
}
|
||||
|
||||
@ -59,8 +66,8 @@ public class ProjectController {
|
||||
description = "Delete a project.",
|
||||
tags = {"Project"},
|
||||
operationId = "deleteProject")
|
||||
@DeleteMapping("/{projectId}")
|
||||
public void deleteProject(@PathVariable UUID workspaceId, @PathVariable UUID projectId) {
|
||||
@DeleteMapping("/projects/{projectId}")
|
||||
public void deleteProject(@PathVariable UUID projectId) {
|
||||
projectService.deleteProject(projectId);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,29 @@
|
||||
package ai.decompile.workspace.model.dto;
|
||||
|
||||
import ai.decompile.workspace.model.entity.Binary;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
public record BinaryResponse(
|
||||
@NotNull UUID id,
|
||||
@NotNull UUID projectId,
|
||||
@NotBlank String filename,
|
||||
long fileSize,
|
||||
String format,
|
||||
String architecture,
|
||||
@NotBlank String filePath,
|
||||
@NotNull Instant updatedAt) {
|
||||
public static BinaryResponse from(Binary binary) {
|
||||
return new BinaryResponse(
|
||||
binary.getId(),
|
||||
binary.getProject().getId(),
|
||||
binary.getFilename(),
|
||||
binary.getFileSize(),
|
||||
binary.getFormat(),
|
||||
binary.getArchitecture(),
|
||||
binary.getFilePath(),
|
||||
binary.getUpdatedAt());
|
||||
}
|
||||
}
|
||||
@ -11,13 +11,15 @@ public record ProjectResponse(
|
||||
@NotNull UUID workspaceId,
|
||||
@NotBlank String name,
|
||||
String description,
|
||||
@NotNull Instant updatedAt) {
|
||||
@NotNull Instant updatedAt,
|
||||
int binaryCount) {
|
||||
public static ProjectResponse from(Project project) {
|
||||
return new ProjectResponse(
|
||||
project.getId(),
|
||||
project.getWorkspace().getId(),
|
||||
project.getName(),
|
||||
project.getDescription(),
|
||||
project.getUpdatedAt());
|
||||
project.getUpdatedAt(),
|
||||
project.getBinaries().size());
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,12 +13,11 @@ public record WorkspaceResponse(
|
||||
@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(),
|
||||
projects != null ? projects.size() : 0);
|
||||
workspace.getProjects().size());
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,48 @@
|
||||
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 = "binaries")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class Binary {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.UUID)
|
||||
private UUID id;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String filename;
|
||||
|
||||
@Column(name = "file_size", nullable = false)
|
||||
private long fileSize;
|
||||
|
||||
@Column(length = 20)
|
||||
private String format;
|
||||
|
||||
@Column(length = 20)
|
||||
private String architecture;
|
||||
|
||||
@Column(name = "file_path", nullable = false, length = 500)
|
||||
private String filePath;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "project_id", nullable = false)
|
||||
private Project project;
|
||||
|
||||
@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,8 @@ package ai.decompile.workspace.model.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import lombok.*;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
@ -29,6 +31,10 @@ public class Project {
|
||||
@JoinColumn(name = "workspace_id", nullable = false)
|
||||
private Workspace workspace;
|
||||
|
||||
@Builder.Default
|
||||
@OneToMany(mappedBy = "project", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
private List<Binary> binaries = new ArrayList<>();
|
||||
|
||||
@CreationTimestamp
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private Instant createdAt;
|
||||
|
||||
@ -2,6 +2,7 @@ package ai.decompile.workspace.model.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import lombok.*;
|
||||
@ -34,6 +35,7 @@ public class Workspace {
|
||||
@Column(name = "updated_at", nullable = false)
|
||||
private Instant updatedAt;
|
||||
|
||||
@Builder.Default
|
||||
@OneToMany(mappedBy = "workspace", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
private List<Project> projects;
|
||||
private List<Project> projects = new ArrayList<>();
|
||||
}
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
package ai.decompile.workspace.model.repository;
|
||||
|
||||
import ai.decompile.workspace.model.entity.Binary;
|
||||
import java.util.UUID;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
|
||||
public interface BinaryRepository
|
||||
extends JpaRepository<Binary, UUID>, JpaSpecificationExecutor<Binary> {}
|
||||
@ -0,0 +1,19 @@
|
||||
package ai.decompile.workspace.model.specification;
|
||||
|
||||
import ai.decompile.workspace.model.entity.Binary;
|
||||
import java.util.UUID;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.springframework.data.jpa.domain.Specification;
|
||||
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class BinarySpecification {
|
||||
public static Specification<Binary> filenameContains(String filename) {
|
||||
return (root, _, cb) ->
|
||||
cb.like(cb.lower(root.get("filename")), "%" + filename.toLowerCase() + "%");
|
||||
}
|
||||
|
||||
public static Specification<Binary> hasProjectId(UUID projectId) {
|
||||
return (root, _, cb) -> cb.equal(root.get("project").get("id"), projectId);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,89 @@
|
||||
package ai.decompile.workspace.service;
|
||||
|
||||
import ai.decompile.exception.NotFoundException;
|
||||
import ai.decompile.workspace.model.dto.BinaryResponse;
|
||||
import ai.decompile.workspace.model.entity.Binary;
|
||||
import ai.decompile.workspace.model.repository.BinaryRepository;
|
||||
import ai.decompile.workspace.model.specification.BinarySpecification;
|
||||
import ai.decompile.workspace.service.storage.FileResource;
|
||||
import ai.decompile.workspace.service.storage.FileStorage;
|
||||
import java.io.IOException;
|
||||
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;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class BinaryService {
|
||||
private final ProjectService projectService;
|
||||
private final BinaryRepository binaryRepository;
|
||||
private final FileStorage fileStorage;
|
||||
|
||||
public List<BinaryResponse> getBinaries(UUID projectId, String filename) {
|
||||
projectService.getProject(projectId);
|
||||
|
||||
Specification<Binary> spec = BinarySpecification.hasProjectId(projectId);
|
||||
if (StringUtils.isNotBlank(filename)) {
|
||||
spec = spec.and(BinarySpecification.filenameContains(filename));
|
||||
}
|
||||
|
||||
return binaryRepository.findAll(spec).stream().map(BinaryResponse::from).toList();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public BinaryResponse uploadBinary(UUID projectId, MultipartFile file) {
|
||||
var project = projectService.getProject(projectId);
|
||||
|
||||
String storedPath;
|
||||
try {
|
||||
storedPath = fileStorage.store(file.getInputStream(), file.getOriginalFilename());
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to store file", e);
|
||||
}
|
||||
|
||||
var binary =
|
||||
binaryRepository.save(
|
||||
Binary.builder()
|
||||
.filename(file.getOriginalFilename())
|
||||
.fileSize(file.getSize())
|
||||
.filePath(storedPath)
|
||||
.project(project)
|
||||
.build());
|
||||
|
||||
return BinaryResponse.from(binary);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void deleteBinary(UUID binaryId) {
|
||||
var binary = getBinary(binaryId);
|
||||
|
||||
try {
|
||||
fileStorage.delete(binary.getFilePath());
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to delete file", e);
|
||||
}
|
||||
|
||||
binaryRepository.delete(binary);
|
||||
}
|
||||
|
||||
public FileResource downloadBinary(UUID binaryId) {
|
||||
var binary = getBinary(binaryId);
|
||||
try {
|
||||
return new FileResource(
|
||||
fileStorage.read(binary.getFilePath()), binary.getFilename(), binary.getFileSize());
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to read file", e);
|
||||
}
|
||||
}
|
||||
|
||||
private Binary getBinary(UUID binaryId) {
|
||||
return binaryRepository
|
||||
.findById(binaryId)
|
||||
.orElseThrow(() -> new NotFoundException("Binary with id '" + binaryId + "' not found"));
|
||||
}
|
||||
}
|
||||
@ -29,6 +29,10 @@ public class ProjectService {
|
||||
return projectRepository.findAll(spec).stream().map(ProjectResponse::from).toList();
|
||||
}
|
||||
|
||||
public ProjectResponse getProjectById(UUID projectId) {
|
||||
return ProjectResponse.from(getProject(projectId));
|
||||
}
|
||||
|
||||
public ProjectResponse createProject(UUID workspaceId, ProjectRequest request) {
|
||||
var workspace = workspaceService.getWorkspace(workspaceId);
|
||||
|
||||
@ -69,7 +73,7 @@ public class ProjectService {
|
||||
projectRepository.delete(project);
|
||||
}
|
||||
|
||||
private Project getProject(UUID projectId) {
|
||||
Project getProject(UUID projectId) {
|
||||
return projectRepository
|
||||
.findById(projectId)
|
||||
.orElseThrow(() -> new NotFoundException("Project with id '" + projectId + "' not found"));
|
||||
|
||||
@ -0,0 +1,5 @@
|
||||
package ai.decompile.workspace.service.storage;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
public record FileResource(InputStream inputStream, String filename, long contentLength) {}
|
||||
@ -0,0 +1,12 @@
|
||||
package ai.decompile.workspace.service.storage;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
public interface FileStorage {
|
||||
String store(InputStream inputStream, String filename) throws IOException;
|
||||
|
||||
void delete(String path) throws IOException;
|
||||
|
||||
InputStream read(String path) throws IOException;
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
package ai.decompile.workspace.service.storage;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.UUID;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@Primary
|
||||
public class LocalFileStorage implements FileStorage {
|
||||
private final Path baseDir;
|
||||
|
||||
public LocalFileStorage(@Value("${app.storage.upload-dir}") String uploadDir) {
|
||||
this.baseDir = Path.of(uploadDir).toAbsolutePath().normalize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String store(InputStream inputStream, String filename) throws IOException {
|
||||
Files.createDirectories(baseDir);
|
||||
|
||||
var storedName = UUID.randomUUID() + "_" + filename;
|
||||
var targetPath = baseDir.resolve(storedName);
|
||||
Files.copy(inputStream, targetPath, StandardCopyOption.REPLACE_EXISTING);
|
||||
|
||||
return storedName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String path) throws IOException {
|
||||
var filePath = baseDir.resolve(path);
|
||||
Files.deleteIfExists(filePath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream read(String path) throws IOException {
|
||||
var filePath = baseDir.resolve(path);
|
||||
return Files.newInputStream(filePath);
|
||||
}
|
||||
}
|
||||
@ -10,6 +10,14 @@ spring:
|
||||
ddl-auto: validate
|
||||
flyway:
|
||||
enabled: true
|
||||
servlet:
|
||||
multipart:
|
||||
max-file-size: 500MB
|
||||
max-request-size: 500MB
|
||||
|
||||
app:
|
||||
storage:
|
||||
upload-dir: ./uploads/binaries
|
||||
|
||||
logging:
|
||||
level:
|
||||
|
||||
@ -0,0 +1,12 @@
|
||||
CREATE TABLE binaries
|
||||
(
|
||||
id UUID PRIMARY KEY,
|
||||
project_id UUID NOT NULL REFERENCES projects (id) ON DELETE CASCADE,
|
||||
filename VARCHAR(255) NOT NULL,
|
||||
file_size BIGINT NOT NULL,
|
||||
format VARCHAR(20),
|
||||
architecture VARCHAR(20),
|
||||
file_path VARCHAR(500) NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL
|
||||
);
|
||||
@ -38,7 +38,7 @@ class ProjectControllerTest {
|
||||
var workspaceId = UUID.randomUUID();
|
||||
var projectId = UUID.randomUUID();
|
||||
var response =
|
||||
new ProjectResponse(projectId, workspaceId, "Test Project", "Desc", Instant.now());
|
||||
new ProjectResponse(projectId, workspaceId, "Test Project", "Desc", Instant.now(), 0);
|
||||
when(projectService.getProjects(eq(workspaceId), isNull())).thenReturn(List.of(response));
|
||||
|
||||
mockMvc
|
||||
@ -66,7 +66,7 @@ class ProjectControllerTest {
|
||||
var workspaceId = UUID.randomUUID();
|
||||
var projectId = UUID.randomUUID();
|
||||
var response =
|
||||
new ProjectResponse(projectId, workspaceId, "Filtered Project", "Desc", Instant.now());
|
||||
new ProjectResponse(projectId, workspaceId, "Filtered Project", "Desc", Instant.now(), 0);
|
||||
when(projectService.getProjects(eq(workspaceId), eq("filter"))).thenReturn(List.of(response));
|
||||
|
||||
mockMvc
|
||||
@ -82,7 +82,8 @@ class ProjectControllerTest {
|
||||
var projectId = UUID.randomUUID();
|
||||
var request = new ProjectRequest("New Project", "A description");
|
||||
var response =
|
||||
new ProjectResponse(projectId, workspaceId, "New Project", "A description", Instant.now());
|
||||
new ProjectResponse(
|
||||
projectId, workspaceId, "New Project", "A description", Instant.now(), 0);
|
||||
when(projectService.createProject(eq(workspaceId), any())).thenReturn(response);
|
||||
|
||||
mockMvc
|
||||
@ -138,12 +139,12 @@ class ProjectControllerTest {
|
||||
var projectId = UUID.randomUUID();
|
||||
var request = new ProjectRequest("Updated", "Updated desc");
|
||||
var response =
|
||||
new ProjectResponse(projectId, workspaceId, "Updated", "Updated desc", Instant.now());
|
||||
new ProjectResponse(projectId, workspaceId, "Updated", "Updated desc", Instant.now(), 0);
|
||||
when(projectService.updateProject(eq(projectId), any())).thenReturn(response);
|
||||
|
||||
mockMvc
|
||||
.perform(
|
||||
put("/workspaces/{workspaceId}/projects/{projectId}", workspaceId, projectId)
|
||||
put("/projects/{projectId}", projectId)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isOk())
|
||||
@ -160,7 +161,7 @@ class ProjectControllerTest {
|
||||
|
||||
mockMvc
|
||||
.perform(
|
||||
put("/workspaces/{workspaceId}/projects/{projectId}", workspaceId, projectId)
|
||||
put("/projects/{projectId}", projectId)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isNotFound())
|
||||
@ -174,9 +175,7 @@ class ProjectControllerTest {
|
||||
var projectId = UUID.randomUUID();
|
||||
doNothing().when(projectService).deleteProject(projectId);
|
||||
|
||||
mockMvc
|
||||
.perform(delete("/workspaces/{workspaceId}/projects/{projectId}", workspaceId, projectId))
|
||||
.andExpect(status().isOk());
|
||||
mockMvc.perform(delete("/projects/{projectId}", projectId)).andExpect(status().isOk());
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -188,7 +187,7 @@ class ProjectControllerTest {
|
||||
.deleteProject(projectId);
|
||||
|
||||
mockMvc
|
||||
.perform(delete("/workspaces/{workspaceId}/projects/{projectId}", workspaceId, projectId))
|
||||
.perform(delete("/projects/{projectId}", projectId))
|
||||
.andExpect(status().isNotFound())
|
||||
.andExpect(jsonPath("$.status").value(404))
|
||||
.andExpect(jsonPath("$.error").value("Not Found"));
|
||||
|
||||
@ -96,21 +96,15 @@ class ProjectIntegrationTest {
|
||||
var updateRequest = new ProjectRequest("CRUD Updated", "Updated Description");
|
||||
mockMvc
|
||||
.perform(
|
||||
put("/workspaces/{workspaceId}/projects/{projectId}", workspaceId, created.id())
|
||||
put("/projects/{projectId}", 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("/projects/{projectId}", created.id())).andExpect(status().isOk());
|
||||
|
||||
mockMvc
|
||||
.perform(
|
||||
delete("/workspaces/{workspaceId}/projects/{projectId}", workspaceId, created.id()))
|
||||
.andExpect(status().isNotFound());
|
||||
mockMvc.perform(delete("/projects/{projectId}", created.id())).andExpect(status().isNotFound());
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -180,15 +174,14 @@ class ProjectIntegrationTest {
|
||||
|
||||
mockMvc
|
||||
.perform(
|
||||
put("/workspaces/{workspaceId}/projects/{projectId}", workspaceId, nonExistentId)
|
||||
put("/projects/{projectId}", 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))
|
||||
.perform(delete("/projects/{projectId}", nonExistentId))
|
||||
.andExpect(status().isNotFound())
|
||||
.andExpect(jsonPath("$.status").value(404));
|
||||
}
|
||||
|
||||
@ -0,0 +1,167 @@
|
||||
package ai.decompile.workspace.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.NotFoundException;
|
||||
import ai.decompile.workspace.model.dto.BinaryResponse;
|
||||
import ai.decompile.workspace.service.BinaryService;
|
||||
import ai.decompile.workspace.service.storage.FileResource;
|
||||
import java.io.ByteArrayInputStream;
|
||||
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.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
@WebMvcTest(BinaryController.class)
|
||||
class BinaryControllerTest {
|
||||
|
||||
@Autowired private MockMvc mockMvc;
|
||||
|
||||
@MockitoBean private BinaryService binaryService;
|
||||
|
||||
@Test
|
||||
void getBinariesShouldReturn200WithList() throws Exception {
|
||||
var projectId = UUID.randomUUID();
|
||||
var binaryId = UUID.randomUUID();
|
||||
var response =
|
||||
new BinaryResponse(
|
||||
binaryId, projectId, "test.exe", 1024L, null, null, "abc_test.exe", Instant.now());
|
||||
when(binaryService.getBinaries(eq(projectId), isNull())).thenReturn(List.of(response));
|
||||
|
||||
mockMvc
|
||||
.perform(get("/projects/{projectId}/binaries", projectId))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$[0].id").value(binaryId.toString()))
|
||||
.andExpect(jsonPath("$[0].filename").value("test.exe"))
|
||||
.andExpect(jsonPath("$[0].fileSize").value(1024));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getBinariesShouldReturn200WithEmptyList() throws Exception {
|
||||
var projectId = UUID.randomUUID();
|
||||
when(binaryService.getBinaries(eq(projectId), isNull())).thenReturn(List.of());
|
||||
|
||||
mockMvc
|
||||
.perform(get("/projects/{projectId}/binaries", projectId))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$").isArray())
|
||||
.andExpect(jsonPath("$").isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getBinariesShouldFilterByName() throws Exception {
|
||||
var projectId = UUID.randomUUID();
|
||||
var binaryId = UUID.randomUUID();
|
||||
var response =
|
||||
new BinaryResponse(
|
||||
binaryId,
|
||||
projectId,
|
||||
"filtered.exe",
|
||||
512L,
|
||||
null,
|
||||
null,
|
||||
"abc_filtered.exe",
|
||||
Instant.now());
|
||||
when(binaryService.getBinaries(eq(projectId), eq("filter"))).thenReturn(List.of(response));
|
||||
|
||||
mockMvc
|
||||
.perform(get("/projects/{projectId}/binaries", projectId).param("name", "filter"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$[0].filename").value("filtered.exe"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getBinariesShouldReturn404WhenProjectNotFound() throws Exception {
|
||||
var projectId = UUID.randomUUID();
|
||||
when(binaryService.getBinaries(eq(projectId), isNull()))
|
||||
.thenThrow(new NotFoundException("Project with id '" + projectId + "' not found"));
|
||||
|
||||
mockMvc
|
||||
.perform(get("/projects/{projectId}/binaries", projectId))
|
||||
.andExpect(status().isNotFound())
|
||||
.andExpect(jsonPath("$.status").value(404));
|
||||
}
|
||||
|
||||
@Test
|
||||
void uploadBinaryShouldReturn201() throws Exception {
|
||||
var projectId = UUID.randomUUID();
|
||||
var binaryId = UUID.randomUUID();
|
||||
var file =
|
||||
new MockMultipartFile("file", "test.exe", "application/octet-stream", new byte[] {1, 2, 3});
|
||||
var response =
|
||||
new BinaryResponse(
|
||||
binaryId, projectId, "test.exe", 3L, null, null, "abc_test.exe", Instant.now());
|
||||
when(binaryService.uploadBinary(eq(projectId), any())).thenReturn(response);
|
||||
|
||||
mockMvc
|
||||
.perform(multipart("/projects/{projectId}/binaries", projectId).file(file))
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.id").value(binaryId.toString()))
|
||||
.andExpect(jsonPath("$.filename").value("test.exe"))
|
||||
.andExpect(jsonPath("$.fileSize").value(3));
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteBinaryShouldReturn204() throws Exception {
|
||||
var projectId = UUID.randomUUID();
|
||||
var binaryId = UUID.randomUUID();
|
||||
doNothing().when(binaryService).deleteBinary(binaryId);
|
||||
|
||||
mockMvc.perform(delete("/binaries/{binaryId}", binaryId)).andExpect(status().isNoContent());
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteBinaryShouldReturn404OnNotFound() throws Exception {
|
||||
var projectId = UUID.randomUUID();
|
||||
var binaryId = UUID.randomUUID();
|
||||
doThrow(new NotFoundException("Binary with id '" + binaryId + "' not found"))
|
||||
.when(binaryService)
|
||||
.deleteBinary(binaryId);
|
||||
|
||||
mockMvc
|
||||
.perform(delete("/binaries/{binaryId}", binaryId))
|
||||
.andExpect(status().isNotFound())
|
||||
.andExpect(jsonPath("$.status").value(404));
|
||||
}
|
||||
|
||||
@Test
|
||||
void downloadBinaryShouldReturn200() throws Exception {
|
||||
var binaryId = UUID.randomUUID();
|
||||
var content = new byte[] {1, 2, 3, 4};
|
||||
var resource = new FileResource(new ByteArrayInputStream(content), "test.exe", 4L);
|
||||
when(binaryService.downloadBinary(binaryId)).thenReturn(resource);
|
||||
|
||||
mockMvc
|
||||
.perform(get("/binaries/{binaryId}/download", binaryId))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(
|
||||
header().string(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"test.exe\""))
|
||||
.andExpect(header().longValue(HttpHeaders.CONTENT_LENGTH, 4))
|
||||
.andExpect(content().contentType(MediaType.APPLICATION_OCTET_STREAM))
|
||||
.andExpect(content().bytes(content));
|
||||
}
|
||||
|
||||
@Test
|
||||
void downloadBinaryShouldReturn404OnNotFound() throws Exception {
|
||||
var binaryId = UUID.randomUUID();
|
||||
when(binaryService.downloadBinary(binaryId))
|
||||
.thenThrow(new NotFoundException("Binary with id '" + binaryId + "' not found"));
|
||||
|
||||
mockMvc
|
||||
.perform(get("/binaries/{binaryId}/download", binaryId))
|
||||
.andExpect(status().isNotFound())
|
||||
.andExpect(jsonPath("$.status").value(404));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,214 @@
|
||||
package ai.decompile.workspace.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.BinaryResponse;
|
||||
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.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
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 BinaryIntegrationTest {
|
||||
|
||||
@Autowired private MockMvc mockMvc;
|
||||
|
||||
@Autowired private EntityManager entityManager;
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
@Test
|
||||
void fullCrudFlow() throws Exception {
|
||||
var workspaceId = createWorkspace("Binary WS");
|
||||
var projectId = createProject(workspaceId, "Binary Project");
|
||||
|
||||
mockMvc
|
||||
.perform(get("/projects/{pid}/binaries", projectId))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$").isArray())
|
||||
.andExpect(jsonPath("$").isEmpty());
|
||||
|
||||
var file =
|
||||
new MockMultipartFile(
|
||||
"file",
|
||||
"binary.exe",
|
||||
"application/octet-stream",
|
||||
new byte[] {0x01, 0x02, 0x03, 0x04, 0x05});
|
||||
|
||||
var createdJson =
|
||||
mockMvc
|
||||
.perform(multipart("/projects/{pid}/binaries", projectId).file(file))
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.filename").value("binary.exe"))
|
||||
.andExpect(jsonPath("$.fileSize").value(5))
|
||||
.andExpect(jsonPath("$.format").isEmpty())
|
||||
.andExpect(jsonPath("$.architecture").isEmpty())
|
||||
.andReturn()
|
||||
.getResponse()
|
||||
.getContentAsString();
|
||||
|
||||
var created = objectMapper.readValue(createdJson, BinaryResponse.class);
|
||||
|
||||
mockMvc
|
||||
.perform(get("/projects/{pid}/binaries", projectId))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.length()").value(1))
|
||||
.andExpect(jsonPath("$[0].filename").value("binary.exe"))
|
||||
.andExpect(jsonPath("$[0].fileSize").value(5));
|
||||
|
||||
mockMvc
|
||||
.perform(get("/projects/{pid}/binaries", projectId).param("name", "binary"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.length()").value(1));
|
||||
|
||||
mockMvc
|
||||
.perform(get("/projects/{pid}/binaries", projectId).param("name", "nonexistent"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.length()").value(0));
|
||||
|
||||
mockMvc.perform(delete("/binaries/{bid}", created.id())).andExpect(status().isNoContent());
|
||||
|
||||
mockMvc
|
||||
.perform(get("/projects/{pid}/binaries", projectId))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$").isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void uploadBinaryToNonexistentProjectShouldReturn404() throws Exception {
|
||||
var fakeProjectId = UUID.randomUUID();
|
||||
var file =
|
||||
new MockMultipartFile("file", "test.exe", "application/octet-stream", new byte[] {1, 2});
|
||||
|
||||
mockMvc
|
||||
.perform(multipart("/projects/{pid}/binaries", fakeProjectId).file(file))
|
||||
.andExpect(status().isNotFound())
|
||||
.andExpect(jsonPath("$.status").value(404));
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteNonexistentBinaryShouldReturn404() throws Exception {
|
||||
var workspaceId = createWorkspace("Binary WS3");
|
||||
var projectId = createProject(workspaceId, "Binary Project 3");
|
||||
var fakeBinaryId = UUID.randomUUID();
|
||||
|
||||
mockMvc
|
||||
.perform(delete("/binaries/{bid}", fakeBinaryId))
|
||||
.andExpect(status().isNotFound())
|
||||
.andExpect(jsonPath("$.status").value(404));
|
||||
}
|
||||
|
||||
@Test
|
||||
void projectResponseShouldIncludeBinaryCount() throws Exception {
|
||||
var workspaceId = createWorkspace("Count WS");
|
||||
var projectId = createProject(workspaceId, "Count Project");
|
||||
|
||||
mockMvc
|
||||
.perform(get("/projects/{pid}", projectId))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.binaryCount").value(0));
|
||||
|
||||
var file1 = new MockMultipartFile("file", "a.exe", "application/octet-stream", new byte[] {1});
|
||||
mockMvc
|
||||
.perform(multipart("/projects/{pid}/binaries", projectId).file(file1))
|
||||
.andExpect(status().isCreated());
|
||||
|
||||
var file2 = new MockMultipartFile("file", "b.exe", "application/octet-stream", new byte[] {2});
|
||||
mockMvc
|
||||
.perform(multipart("/projects/{pid}/binaries", projectId).file(file2))
|
||||
.andExpect(status().isCreated());
|
||||
|
||||
entityManager.flush();
|
||||
entityManager.clear();
|
||||
|
||||
mockMvc
|
||||
.perform(get("/projects/{pid}", projectId))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.binaryCount").value(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
void downloadBinaryShouldReturnFile() throws Exception {
|
||||
var workspaceId = createWorkspace("Download WS");
|
||||
var projectId = createProject(workspaceId, "Download Project");
|
||||
|
||||
var content = new byte[] {0x01, 0x02, 0x03};
|
||||
var file = new MockMultipartFile("file", "download.bin", "application/octet-stream", content);
|
||||
|
||||
var createdJson =
|
||||
mockMvc
|
||||
.perform(multipart("/projects/{pid}/binaries", projectId).file(file))
|
||||
.andExpect(status().isCreated())
|
||||
.andReturn()
|
||||
.getResponse()
|
||||
.getContentAsString();
|
||||
var created = objectMapper.readValue(createdJson, BinaryResponse.class);
|
||||
|
||||
mockMvc
|
||||
.perform(get("/binaries/{bid}/download", created.id()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(
|
||||
header()
|
||||
.string(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"download.bin\""))
|
||||
.andExpect(header().longValue(HttpHeaders.CONTENT_LENGTH, 3))
|
||||
.andExpect(content().contentType(MediaType.APPLICATION_OCTET_STREAM))
|
||||
.andExpect(content().bytes(content));
|
||||
}
|
||||
|
||||
@Test
|
||||
void downloadBinaryShouldReturn404OnNotFound() throws Exception {
|
||||
var fakeBinaryId = UUID.randomUUID();
|
||||
|
||||
mockMvc
|
||||
.perform(get("/binaries/{bid}/download", fakeBinaryId))
|
||||
.andExpect(status().isNotFound())
|
||||
.andExpect(jsonPath("$.status").value(404));
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
private UUID createProject(UUID workspaceId, String name) throws Exception {
|
||||
var request = new ProjectRequest(name, "desc");
|
||||
var json =
|
||||
mockMvc
|
||||
.perform(
|
||||
post("/workspaces/{workspaceId}/projects", workspaceId)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isOk())
|
||||
.andReturn()
|
||||
.getResponse()
|
||||
.getContentAsString();
|
||||
return objectMapper.readValue(json, ProjectResponse.class).id();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,169 @@
|
||||
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.NotFoundException;
|
||||
import ai.decompile.workspace.model.dto.BinaryResponse;
|
||||
import ai.decompile.workspace.model.entity.Binary;
|
||||
import ai.decompile.workspace.model.entity.Project;
|
||||
import ai.decompile.workspace.model.entity.Workspace;
|
||||
import ai.decompile.workspace.model.repository.BinaryRepository;
|
||||
import ai.decompile.workspace.service.storage.FileStorage;
|
||||
import java.io.IOException;
|
||||
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;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class BinaryServiceTest {
|
||||
|
||||
@Mock private ProjectService projectService;
|
||||
|
||||
@Mock private BinaryRepository binaryRepository;
|
||||
|
||||
@Mock private FileStorage fileStorage;
|
||||
|
||||
@InjectMocks private BinaryService binaryService;
|
||||
|
||||
private UUID projectId;
|
||||
private UUID binaryId;
|
||||
private Project project;
|
||||
private Binary binary;
|
||||
private byte[] fileContent;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
projectId = UUID.randomUUID();
|
||||
binaryId = UUID.randomUUID();
|
||||
var workspace =
|
||||
Workspace.builder()
|
||||
.id(UUID.randomUUID())
|
||||
.name("WS")
|
||||
.createdAt(Instant.now())
|
||||
.updatedAt(Instant.now())
|
||||
.build();
|
||||
project =
|
||||
Project.builder()
|
||||
.id(projectId)
|
||||
.name("Test Project")
|
||||
.workspace(workspace)
|
||||
.createdAt(Instant.now())
|
||||
.updatedAt(Instant.now())
|
||||
.build();
|
||||
binary =
|
||||
Binary.builder()
|
||||
.id(binaryId)
|
||||
.filename("test.bin")
|
||||
.fileSize(100L)
|
||||
.filePath("abc_test.bin")
|
||||
.project(project)
|
||||
.createdAt(Instant.now())
|
||||
.updatedAt(Instant.now())
|
||||
.build();
|
||||
fileContent = new byte[] {1, 2, 3, 4};
|
||||
project.setBinaries(List.of(binary));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getBinariesShouldReturnEmptyList() {
|
||||
when(projectService.getProject(projectId)).thenReturn(project);
|
||||
when(binaryRepository.findAll(any(Specification.class))).thenReturn(List.of());
|
||||
|
||||
List<BinaryResponse> result = binaryService.getBinaries(projectId, null);
|
||||
|
||||
assertTrue(result.isEmpty());
|
||||
verify(projectService).getProject(projectId);
|
||||
verify(binaryRepository).findAll(any(Specification.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getBinariesShouldReturnList() {
|
||||
when(projectService.getProject(projectId)).thenReturn(project);
|
||||
when(binaryRepository.findAll(any(Specification.class))).thenReturn(List.of(binary));
|
||||
|
||||
List<BinaryResponse> result = binaryService.getBinaries(projectId, null);
|
||||
|
||||
assertEquals(1, result.size());
|
||||
assertEquals(binaryId, result.getFirst().id());
|
||||
assertEquals("test.bin", result.getFirst().filename());
|
||||
assertEquals(projectId, result.getFirst().projectId());
|
||||
assertEquals(100L, result.getFirst().fileSize());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getBinariesShouldFilterByFilename() {
|
||||
when(projectService.getProject(projectId)).thenReturn(project);
|
||||
when(binaryRepository.findAll(any(Specification.class))).thenReturn(List.of(binary));
|
||||
|
||||
List<BinaryResponse> result = binaryService.getBinaries(projectId, "test");
|
||||
|
||||
assertEquals(1, result.size());
|
||||
assertEquals("test.bin", result.getFirst().filename());
|
||||
verify(binaryRepository).findAll(any(Specification.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getBinariesShouldThrowNotFoundWhenProjectDoesNotExist() {
|
||||
when(projectService.getProject(projectId))
|
||||
.thenThrow(new NotFoundException("Project with id '" + projectId + "' not found"));
|
||||
|
||||
assertThrows(NotFoundException.class, () -> binaryService.getBinaries(projectId, null));
|
||||
verify(binaryRepository, never()).findAll(any(Specification.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void uploadBinaryShouldSucceed() throws IOException {
|
||||
var file = new MockMultipartFile("file", "test.exe", "application/octet-stream", fileContent);
|
||||
when(projectService.getProject(projectId)).thenReturn(project);
|
||||
when(fileStorage.store(any(), eq("test.exe"))).thenReturn("stored_test.exe");
|
||||
when(binaryRepository.save(any(Binary.class))).thenReturn(binary);
|
||||
|
||||
BinaryResponse result = binaryService.uploadBinary(projectId, file);
|
||||
|
||||
assertEquals(binaryId, result.id());
|
||||
assertEquals("test.bin", result.filename());
|
||||
verify(projectService).getProject(projectId);
|
||||
verify(fileStorage).store(any(), eq("test.exe"));
|
||||
verify(binaryRepository).save(any(Binary.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void uploadBinaryShouldThrowRuntimeExceptionOnStorageFailure() throws IOException {
|
||||
var file = new MockMultipartFile("file", "test.exe", "application/octet-stream", fileContent);
|
||||
when(projectService.getProject(projectId)).thenReturn(project);
|
||||
when(fileStorage.store(any(), eq("test.exe"))).thenThrow(new IOException("disk full"));
|
||||
|
||||
assertThrows(RuntimeException.class, () -> binaryService.uploadBinary(projectId, file));
|
||||
verify(binaryRepository, never()).save(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteBinaryShouldSucceed() throws IOException {
|
||||
when(binaryRepository.findById(binaryId)).thenReturn(Optional.of(binary));
|
||||
|
||||
binaryService.deleteBinary(binaryId);
|
||||
|
||||
verify(fileStorage).delete(binary.getFilePath());
|
||||
verify(binaryRepository).delete(binary);
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteBinaryShouldThrowNotFound() throws IOException {
|
||||
when(binaryRepository.findById(binaryId)).thenReturn(Optional.empty());
|
||||
|
||||
assertThrows(NotFoundException.class, () -> binaryService.deleteBinary(binaryId));
|
||||
verify(fileStorage, never()).delete(any());
|
||||
verify(binaryRepository, never()).delete((Binary) any());
|
||||
}
|
||||
}
|
||||
@ -9,3 +9,11 @@ spring:
|
||||
ddl-auto: create-drop
|
||||
flyway:
|
||||
enabled: false
|
||||
servlet:
|
||||
multipart:
|
||||
max-file-size: 500MB
|
||||
max-request-size: 500MB
|
||||
|
||||
app:
|
||||
storage:
|
||||
upload-dir: /tmp/decompile-ai-test/binaries
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user