Skip to content

Commit e2816c8

Browse files
committed
list flows by parent id
1 parent 6616635 commit e2816c8

File tree

9 files changed

+168
-23
lines changed

9 files changed

+168
-23
lines changed

src/main/java/com/flowci/common/GlobalExceptionHandler.java

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,17 @@
44
import com.flowci.common.exception.BusinessException;
55
import com.flowci.common.model.ErrorResponse;
66
import lombok.extern.slf4j.Slf4j;
7-
import org.springframework.http.HttpStatus;
7+
import org.springframework.context.MessageSourceResolvable;
88
import org.springframework.http.ResponseEntity;
99
import org.springframework.web.bind.MethodArgumentNotValidException;
1010
import org.springframework.web.bind.MissingServletRequestParameterException;
1111
import org.springframework.web.bind.annotation.ControllerAdvice;
1212
import org.springframework.web.bind.annotation.ExceptionHandler;
1313
import org.springframework.web.bind.annotation.ResponseBody;
14-
import org.springframework.web.bind.annotation.ResponseStatus;
14+
import org.springframework.web.method.annotation.HandlerMethodValidationException;
1515

1616
import static com.flowci.common.exception.ExceptionUtils.findRootCause;
17+
import static java.util.stream.Collectors.joining;
1718
import static org.springframework.http.HttpStatus.BAD_REQUEST;
1819
import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR;
1920

@@ -22,7 +23,7 @@
2223
public final class GlobalExceptionHandler {
2324

2425
@ResponseBody
25-
@ExceptionHandler(MethodArgumentNotValidException.class)
26+
@ExceptionHandler({MethodArgumentNotValidException.class})
2627
public ResponseEntity<ErrorResponse> inputArgumentException(MethodArgumentNotValidException e) {
2728
var msg = e.getMessage();
2829

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

39+
@ResponseBody
40+
@ExceptionHandler(HandlerMethodValidationException.class)
41+
public ResponseEntity<ErrorResponse> inputArgumentException(HandlerMethodValidationException e) {
42+
var message = e.getAllValidationResults()
43+
.stream()
44+
.map(r -> r.getResolvableErrors()
45+
.stream()
46+
.map(MessageSourceResolvable::getDefaultMessage)
47+
.collect(joining("; ")))
48+
.collect(joining("; "));
49+
50+
51+
return ResponseEntity.status(BAD_REQUEST)
52+
.body(new ErrorResponse(BAD_REQUEST.value(), message));
53+
}
54+
3855
@ResponseBody
3956
@ExceptionHandler({
4057
BusinessException.class,
@@ -46,7 +63,6 @@ public ResponseEntity<ErrorResponse> onBusinessException(Throwable e) {
4663
}
4764

4865
@ResponseBody
49-
@ResponseStatus(HttpStatus.OK)
5066
@ExceptionHandler(Throwable.class)
5167
public ResponseEntity<ErrorResponse> fatalException(Throwable e) {
5268
log.error("Fatal exception", e);
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package com.flowci.common.validator;
2+
3+
import jakarta.validation.Constraint;
4+
import jakarta.validation.ConstraintValidator;
5+
import jakarta.validation.ConstraintValidatorContext;
6+
import jakarta.validation.Payload;
7+
8+
import java.lang.annotation.*;
9+
10+
@Constraint(validatedBy = ValidId.IdValidator.class)
11+
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER})
12+
@Retention(RetentionPolicy.RUNTIME)
13+
@Documented
14+
public @interface ValidId {
15+
16+
String message() default "invalid id";
17+
18+
Class<?>[] groups() default {};
19+
20+
Class<? extends Payload>[] payload() default {};
21+
22+
class IdValidator implements ConstraintValidator<ValidId, String> {
23+
24+
@Override
25+
public void initialize(ValidId constraintAnnotation) {
26+
ConstraintValidator.super.initialize(constraintAnnotation);
27+
}
28+
29+
@Override
30+
public boolean isValid(String value, ConstraintValidatorContext context) {
31+
try {
32+
return Long.parseLong(value) > 0L;
33+
} catch (NumberFormatException e) {
34+
return false;
35+
}
36+
}
37+
}
38+
}

src/main/java/com/flowci/common/validator/ValidName.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import java.lang.annotation.*;
1010

1111
@Constraint(validatedBy = ValidName.NameValidator.class)
12-
@Target({ElementType.METHOD, ElementType.FIELD})
12+
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER})
1313
@Retention(RetentionPolicy.RUNTIME)
1414
@Documented
1515
public @interface ValidName {

src/main/java/com/flowci/flow/FlowController.java

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,34 +2,49 @@
22

33
import com.flowci.common.exception.DuplicateException;
44
import com.flowci.common.exception.ExceptionUtils;
5+
import com.flowci.common.validator.ValidId;
56
import com.flowci.flow.business.CreateFlow;
7+
import com.flowci.flow.business.FetchFlow;
68
import com.flowci.flow.business.FetchTemplates;
9+
import com.flowci.flow.business.ListFlows;
710
import com.flowci.flow.model.CreateFlowParam;
811
import com.flowci.flow.model.Flow;
912
import com.flowci.flow.model.YamlTemplate;
1013
import jakarta.validation.Valid;
14+
import jakarta.validation.constraints.Min;
15+
import lombok.AllArgsConstructor;
1116
import lombok.extern.slf4j.Slf4j;
17+
import org.springframework.data.domain.PageRequest;
1218
import org.springframework.web.bind.annotation.*;
1319

1420
import java.util.List;
1521

22+
import static java.lang.Long.parseLong;
23+
1624
@Slf4j
1725
@RestController
1826
@RequestMapping("/v2/flows")
27+
@AllArgsConstructor
1928
public class FlowController {
2029

2130
private final FetchTemplates fetchTemplates;
2231

2332
private final CreateFlow createFlow;
2433

25-
public FlowController(FetchTemplates fetchTemplates, CreateFlow createFlow) {
26-
this.fetchTemplates = fetchTemplates;
27-
this.createFlow = createFlow;
28-
}
34+
private final ListFlows listFlows;
35+
36+
private final FetchFlow fetchFlow;
2937

3038
@GetMapping("/{id}")
31-
public Flow getFlow(@PathVariable("id") Long id) {
32-
throw new UnsupportedOperationException("Not supported yet.");
39+
public Flow getFlow(@PathVariable("id") @Valid @ValidId String id) {
40+
return fetchFlow.invoke(parseLong(id));
41+
}
42+
43+
@GetMapping
44+
public List<Flow> getFlows(@RequestParam(required = false, defaultValue = "10000") @Valid @ValidId String parentId,
45+
@RequestParam(required = false, defaultValue = "0") @Valid @Min(0) Integer page,
46+
@RequestParam(required = false, defaultValue = "20") @Valid @Min(20) Integer size) {
47+
return listFlows.invoke(parseLong(parentId), PageRequest.of(page, size));
3348
}
3449

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

4055
@PostMapping
41-
public Flow createFlow(@Valid @RequestBody CreateFlowParam param) {
56+
public Flow createFlow(@RequestBody @Valid CreateFlowParam param) {
4257
try {
4358
return createFlow.invoke(param);
4459
} catch (Throwable e) {

src/main/java/com/flowci/flow/business/ListFlows.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@
77
import java.util.List;
88

99
public interface ListFlows {
10-
List<Flow> invoke(@Nullable Long rootId, PageRequest pageRequest);
10+
List<Flow> invoke(@Nullable Long parentId, PageRequest pageRequest);
1111
}

src/main/java/com/flowci/flow/business/impl/ListFlowsImpl.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,13 @@ public class ListFlowsImpl implements ListFlows {
2222
private final RequestContextHolder requestContextHolder;
2323

2424
@Override
25-
public List<Flow> invoke(@Nullable Long rootId, PageRequest pageRequest) {
26-
if (rootId == null) {
27-
rootId = Flow.ROOT_ID;
25+
public List<Flow> invoke(@Nullable Long parentId, PageRequest pageRequest) {
26+
if (parentId == null) {
27+
parentId = Flow.ROOT_ID;
2828
}
2929

3030
return flowRepo.findAllByParentIdAndUserIdOrderByCreatedAt(
31-
rootId,
31+
parentId,
3232
requestContextHolder.getUserId(),
3333
pageRequest
3434
);

src/test/java/com/flowci/SpringTest.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
1010
import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration;
1111
import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration;
12+
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
1213
import org.springframework.boot.test.context.SpringBootTest;
1314
import org.springframework.boot.test.context.TestConfiguration;
1415
import org.springframework.boot.test.mock.mockito.MockBean;
@@ -22,6 +23,7 @@
2223
import java.nio.charset.Charset;
2324

2425
@SpringBootTest
26+
@AutoConfigureMockMvc
2527
@EnableAutoConfiguration(exclude = {
2628
DataSourceAutoConfiguration.class,
2729
DataSourceTransactionManagerAutoConfiguration.class,

src/test/java/com/flowci/flow/FlowControllerTest.java

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,31 @@
11
package com.flowci.flow;
22

33
import com.fasterxml.jackson.databind.ObjectMapper;
4+
import com.flowci.SpringTest;
45
import com.flowci.common.model.ErrorResponse;
56
import com.flowci.flow.business.CreateFlow;
6-
import com.flowci.flow.business.FetchTemplates;
7+
import com.flowci.flow.business.FetchFlow;
78
import com.flowci.flow.model.CreateFlowParam;
89
import com.flowci.flow.model.Flow;
910
import org.junit.jupiter.api.Test;
1011
import org.springframework.beans.factory.annotation.Autowired;
11-
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
1212
import org.springframework.boot.test.mock.mockito.MockBean;
1313
import org.springframework.dao.DataIntegrityViolationException;
1414
import org.springframework.http.MediaType;
1515
import org.springframework.test.web.servlet.MockMvc;
1616

1717
import java.sql.SQLException;
1818

19+
import static com.flowci.TestUtils.newDummyInstance;
1920
import static org.junit.jupiter.api.Assertions.assertEquals;
2021
import static org.mockito.ArgumentMatchers.any;
2122
import static org.mockito.Mockito.mock;
2223
import static org.mockito.Mockito.when;
24+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
2325
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
2426
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
2527

26-
@WebMvcTest(controllers = FlowController.class)
27-
class FlowControllerTest {
28+
class FlowControllerTest extends SpringTest {
2829

2930
@Autowired
3031
private MockMvc mvc;
@@ -35,9 +36,8 @@ class FlowControllerTest {
3536
@MockBean
3637
private CreateFlow createFlow;
3738

38-
// keep it !!
3939
@MockBean
40-
private FetchTemplates fetchTemplates;
40+
private FetchFlow fetchFlow;
4141

4242
@Test
4343
void givenCreateFlowParameter_whenCreateFlow_thenReturnFlowId() throws Exception {
@@ -111,4 +111,28 @@ void givenCreateFlowParameter_whenCreateFlowWithUnexpectedError_thenReturnError(
111111
assertEquals("something went wrong", error.message());
112112
assertEquals(500, error.code());
113113
}
114+
115+
@Test
116+
void givenFlowId_whenFetching_thenReturnFlow() throws Exception {
117+
var mockFlow = newDummyInstance(Flow.class).create();
118+
when(fetchFlow.invoke(any())).thenReturn(mockFlow);
119+
120+
var r = mvc.perform(get("/v2/flows/" + mockFlow.getId()))
121+
.andExpect(status().is2xxSuccessful())
122+
.andReturn();
123+
124+
var fetched = objectMapper.readValue(r.getResponse().getContentAsString(), Flow.class);
125+
assertEquals(mockFlow.getId(), fetched.getId());
126+
}
127+
128+
@Test
129+
void givenInvalidFlowId_whenFetching_thenReturnError() throws Exception {
130+
var r = mvc.perform(get("/v2/flows/-1"))
131+
.andExpect(status().is4xxClientError())
132+
.andReturn();
133+
134+
var error = objectMapper.readValue(r.getResponse().getContentAsString(), ErrorResponse.class);
135+
assertEquals(400, error.code());
136+
assertEquals("invalid id", error.message());
137+
}
114138
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package com.flowci.flow.business;
2+
3+
import com.flowci.SpringTest;
4+
import com.flowci.common.RequestContextHolder;
5+
import com.flowci.flow.model.Flow;
6+
import org.junit.jupiter.api.Test;
7+
import org.mockito.ArgumentCaptor;
8+
import org.springframework.beans.factory.annotation.Autowired;
9+
import org.springframework.boot.test.mock.mockito.MockBean;
10+
import org.springframework.data.domain.PageRequest;
11+
import org.springframework.data.domain.Pageable;
12+
13+
import java.util.Collections;
14+
15+
import static org.junit.jupiter.api.Assertions.assertEquals;
16+
import static org.mockito.ArgumentMatchers.any;
17+
import static org.mockito.Mockito.*;
18+
19+
class ListFlowsTest extends SpringTest {
20+
21+
@Autowired
22+
private MockRepositoriesConfig mockRepositoriesConfig;
23+
24+
@Autowired
25+
private ListFlows listFlows;
26+
27+
@MockBean
28+
private RequestContextHolder requestContextHolder;
29+
30+
@Test
31+
void givenParentId_whenFetching_thenReturnAllFlowsUnderTheParent() {
32+
var flowRepoMock = mockRepositoriesConfig.getFlowRepo();
33+
var parentIdCaptor = ArgumentCaptor.forClass(Long.class);
34+
var userIdCaptor = ArgumentCaptor.forClass(Long.class);
35+
when(flowRepoMock.findAllByParentIdAndUserIdOrderByCreatedAt(
36+
parentIdCaptor.capture(),
37+
userIdCaptor.capture(),
38+
any(Pageable.class))
39+
).thenReturn(Collections.emptyList());
40+
41+
var userIdMock = 1L;
42+
when(requestContextHolder.getUserId()).thenReturn(userIdMock);
43+
44+
listFlows.invoke(null, PageRequest.of(0, 1));
45+
46+
assertEquals(Flow.ROOT_ID, parentIdCaptor.getValue());
47+
verify(flowRepoMock, times(1)).findAllByParentIdAndUserIdOrderByCreatedAt(
48+
parentIdCaptor.capture(), userIdCaptor.capture(), any(Pageable.class));
49+
}
50+
}

0 commit comments

Comments
 (0)