Skip to content

Commit

Permalink
list flows by parent id
Browse files Browse the repository at this point in the history
  • Loading branch information
gy2006 committed Dec 19, 2024
1 parent 6616635 commit e2816c8
Show file tree
Hide file tree
Showing 9 changed files with 168 additions and 23 deletions.
24 changes: 20 additions & 4 deletions src/main/java/com/flowci/common/GlobalExceptionHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,17 @@
import com.flowci.common.exception.BusinessException;
import com.flowci.common.model.ErrorResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.context.MessageSourceResolvable;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.method.annotation.HandlerMethodValidationException;

import static com.flowci.common.exception.ExceptionUtils.findRootCause;
import static java.util.stream.Collectors.joining;
import static org.springframework.http.HttpStatus.BAD_REQUEST;
import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR;

Expand All @@ -22,7 +23,7 @@
public final class GlobalExceptionHandler {

@ResponseBody
@ExceptionHandler(MethodArgumentNotValidException.class)
@ExceptionHandler({MethodArgumentNotValidException.class})
public ResponseEntity<ErrorResponse> inputArgumentException(MethodArgumentNotValidException e) {
var msg = e.getMessage();

Expand All @@ -35,6 +36,22 @@ public ResponseEntity<ErrorResponse> inputArgumentException(MethodArgumentNotVal
.body(new ErrorResponse(BAD_REQUEST.value(), msg));
}

@ResponseBody
@ExceptionHandler(HandlerMethodValidationException.class)
public ResponseEntity<ErrorResponse> inputArgumentException(HandlerMethodValidationException e) {
var message = e.getAllValidationResults()
.stream()
.map(r -> r.getResolvableErrors()
.stream()
.map(MessageSourceResolvable::getDefaultMessage)
.collect(joining("; ")))
.collect(joining("; "));


return ResponseEntity.status(BAD_REQUEST)
.body(new ErrorResponse(BAD_REQUEST.value(), message));
}

@ResponseBody
@ExceptionHandler({
BusinessException.class,
Expand All @@ -46,7 +63,6 @@ public ResponseEntity<ErrorResponse> onBusinessException(Throwable e) {
}

@ResponseBody
@ResponseStatus(HttpStatus.OK)
@ExceptionHandler(Throwable.class)
public ResponseEntity<ErrorResponse> fatalException(Throwable e) {
log.error("Fatal exception", e);
Expand Down
38 changes: 38 additions & 0 deletions src/main/java/com/flowci/common/validator/ValidId.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.flowci.common.validator;

import jakarta.validation.Constraint;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import jakarta.validation.Payload;

import java.lang.annotation.*;

@Constraint(validatedBy = ValidId.IdValidator.class)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ValidId {

String message() default "invalid id";

Class<?>[] groups() default {};

Class<? extends Payload>[] payload() default {};

class IdValidator implements ConstraintValidator<ValidId, String> {

@Override
public void initialize(ValidId constraintAnnotation) {
ConstraintValidator.super.initialize(constraintAnnotation);
}

@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
try {
return Long.parseLong(value) > 0L;
} catch (NumberFormatException e) {
return false;
}
}
}
}
2 changes: 1 addition & 1 deletion src/main/java/com/flowci/common/validator/ValidName.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import java.lang.annotation.*;

@Constraint(validatedBy = ValidName.NameValidator.class)
@Target({ElementType.METHOD, ElementType.FIELD})
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ValidName {
Expand Down
29 changes: 22 additions & 7 deletions src/main/java/com/flowci/flow/FlowController.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,49 @@

import com.flowci.common.exception.DuplicateException;
import com.flowci.common.exception.ExceptionUtils;
import com.flowci.common.validator.ValidId;
import com.flowci.flow.business.CreateFlow;
import com.flowci.flow.business.FetchFlow;
import com.flowci.flow.business.FetchTemplates;
import com.flowci.flow.business.ListFlows;
import com.flowci.flow.model.CreateFlowParam;
import com.flowci.flow.model.Flow;
import com.flowci.flow.model.YamlTemplate;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Min;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.PageRequest;
import org.springframework.web.bind.annotation.*;

import java.util.List;

import static java.lang.Long.parseLong;

@Slf4j
@RestController
@RequestMapping("/v2/flows")
@AllArgsConstructor
public class FlowController {

private final FetchTemplates fetchTemplates;

private final CreateFlow createFlow;

public FlowController(FetchTemplates fetchTemplates, CreateFlow createFlow) {
this.fetchTemplates = fetchTemplates;
this.createFlow = createFlow;
}
private final ListFlows listFlows;

private final FetchFlow fetchFlow;

@GetMapping("/{id}")
public Flow getFlow(@PathVariable("id") Long id) {
throw new UnsupportedOperationException("Not supported yet.");
public Flow getFlow(@PathVariable("id") @Valid @ValidId String id) {
return fetchFlow.invoke(parseLong(id));
}

@GetMapping
public List<Flow> getFlows(@RequestParam(required = false, defaultValue = "10000") @Valid @ValidId String parentId,
@RequestParam(required = false, defaultValue = "0") @Valid @Min(0) Integer page,
@RequestParam(required = false, defaultValue = "20") @Valid @Min(20) Integer size) {
return listFlows.invoke(parseLong(parentId), PageRequest.of(page, size));
}

@GetMapping("/templates")
Expand All @@ -38,7 +53,7 @@ public List<YamlTemplate> getTemplates() {
}

@PostMapping
public Flow createFlow(@Valid @RequestBody CreateFlowParam param) {
public Flow createFlow(@RequestBody @Valid CreateFlowParam param) {
try {
return createFlow.invoke(param);
} catch (Throwable e) {
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/com/flowci/flow/business/ListFlows.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@
import java.util.List;

public interface ListFlows {
List<Flow> invoke(@Nullable Long rootId, PageRequest pageRequest);
List<Flow> invoke(@Nullable Long parentId, PageRequest pageRequest);
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@ public class ListFlowsImpl implements ListFlows {
private final RequestContextHolder requestContextHolder;

@Override
public List<Flow> invoke(@Nullable Long rootId, PageRequest pageRequest) {
if (rootId == null) {
rootId = Flow.ROOT_ID;
public List<Flow> invoke(@Nullable Long parentId, PageRequest pageRequest) {
if (parentId == null) {
parentId = Flow.ROOT_ID;
}

return flowRepo.findAllByParentIdAndUserIdOrderByCreatedAt(
rootId,
parentId,
requestContextHolder.getUserId(),
pageRequest
);
Expand Down
2 changes: 2 additions & 0 deletions src/test/java/com/flowci/SpringTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.test.mock.mockito.MockBean;
Expand All @@ -22,6 +23,7 @@
import java.nio.charset.Charset;

@SpringBootTest
@AutoConfigureMockMvc
@EnableAutoConfiguration(exclude = {
DataSourceAutoConfiguration.class,
DataSourceTransactionManagerAutoConfiguration.class,
Expand Down
36 changes: 30 additions & 6 deletions src/test/java/com/flowci/flow/FlowControllerTest.java
Original file line number Diff line number Diff line change
@@ -1,30 +1,31 @@
package com.flowci.flow;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.flowci.SpringTest;
import com.flowci.common.model.ErrorResponse;
import com.flowci.flow.business.CreateFlow;
import com.flowci.flow.business.FetchTemplates;
import com.flowci.flow.business.FetchFlow;
import com.flowci.flow.model.CreateFlowParam;
import com.flowci.flow.model.Flow;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;

import java.sql.SQLException;

import static com.flowci.TestUtils.newDummyInstance;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@WebMvcTest(controllers = FlowController.class)
class FlowControllerTest {
class FlowControllerTest extends SpringTest {

@Autowired
private MockMvc mvc;
Expand All @@ -35,9 +36,8 @@ class FlowControllerTest {
@MockBean
private CreateFlow createFlow;

// keep it !!
@MockBean
private FetchTemplates fetchTemplates;
private FetchFlow fetchFlow;

@Test
void givenCreateFlowParameter_whenCreateFlow_thenReturnFlowId() throws Exception {
Expand Down Expand Up @@ -111,4 +111,28 @@ void givenCreateFlowParameter_whenCreateFlowWithUnexpectedError_thenReturnError(
assertEquals("something went wrong", error.message());
assertEquals(500, error.code());
}

@Test
void givenFlowId_whenFetching_thenReturnFlow() throws Exception {
var mockFlow = newDummyInstance(Flow.class).create();
when(fetchFlow.invoke(any())).thenReturn(mockFlow);

var r = mvc.perform(get("/v2/flows/" + mockFlow.getId()))
.andExpect(status().is2xxSuccessful())
.andReturn();

var fetched = objectMapper.readValue(r.getResponse().getContentAsString(), Flow.class);
assertEquals(mockFlow.getId(), fetched.getId());
}

@Test
void givenInvalidFlowId_whenFetching_thenReturnError() throws Exception {
var r = mvc.perform(get("/v2/flows/-1"))
.andExpect(status().is4xxClientError())
.andReturn();

var error = objectMapper.readValue(r.getResponse().getContentAsString(), ErrorResponse.class);
assertEquals(400, error.code());
assertEquals("invalid id", error.message());
}
}
50 changes: 50 additions & 0 deletions src/test/java/com/flowci/flow/business/ListFlowsTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.flowci.flow.business;

import com.flowci.SpringTest;
import com.flowci.common.RequestContextHolder;
import com.flowci.flow.model.Flow;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;

import java.util.Collections;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;

class ListFlowsTest extends SpringTest {

@Autowired
private MockRepositoriesConfig mockRepositoriesConfig;

@Autowired
private ListFlows listFlows;

@MockBean
private RequestContextHolder requestContextHolder;

@Test
void givenParentId_whenFetching_thenReturnAllFlowsUnderTheParent() {
var flowRepoMock = mockRepositoriesConfig.getFlowRepo();
var parentIdCaptor = ArgumentCaptor.forClass(Long.class);
var userIdCaptor = ArgumentCaptor.forClass(Long.class);
when(flowRepoMock.findAllByParentIdAndUserIdOrderByCreatedAt(
parentIdCaptor.capture(),
userIdCaptor.capture(),
any(Pageable.class))
).thenReturn(Collections.emptyList());

var userIdMock = 1L;
when(requestContextHolder.getUserId()).thenReturn(userIdMock);

listFlows.invoke(null, PageRequest.of(0, 1));

assertEquals(Flow.ROOT_ID, parentIdCaptor.getValue());
verify(flowRepoMock, times(1)).findAllByParentIdAndUserIdOrderByCreatedAt(
parentIdCaptor.capture(), userIdCaptor.capture(), any(Pageable.class));
}
}

0 comments on commit e2816c8

Please sign in to comment.