diff --git a/backend/src/main/java/heartbeat/client/GitHubFeignClient.java b/backend/src/main/java/heartbeat/client/GitHubFeignClient.java index c07d2163d6..75ad40fc57 100644 --- a/backend/src/main/java/heartbeat/client/GitHubFeignClient.java +++ b/backend/src/main/java/heartbeat/client/GitHubFeignClient.java @@ -17,16 +17,26 @@ @FeignClient(name = "githubFeignClient", url = "${github.url}", configuration = GitHubFeignClientDecoder.class) public interface GitHubFeignClient { + @GetMapping(path = "/octocat") + void verifyToken(@RequestHeader("Authorization") String token); + + @GetMapping(path = "/repos/{repository}/branches/{branchName}") + void verifyCanReadTargetBranch(@PathVariable String repository, @PathVariable String branchName, + @RequestHeader("Authorization") String token); + @GetMapping(path = "/user/orgs") @ResponseStatus(HttpStatus.OK) + @Deprecated List getGithubOrganizationsInfo(@RequestHeader("Authorization") String token); @GetMapping(path = "/user/repos") @ResponseStatus(HttpStatus.OK) + @Deprecated List getAllRepos(@RequestHeader("Authorization") String token); @GetMapping(path = "/orgs/{organizationName}/repos") @ResponseStatus(HttpStatus.OK) + @Deprecated List getReposByOrganizationName(@PathVariable String organizationName, @RequestHeader("Authorization") String token); diff --git a/backend/src/main/java/heartbeat/client/JiraFeignClient.java b/backend/src/main/java/heartbeat/client/JiraFeignClient.java index b0304b8b57..1e62c0c44f 100644 --- a/backend/src/main/java/heartbeat/client/JiraFeignClient.java +++ b/backend/src/main/java/heartbeat/client/JiraFeignClient.java @@ -3,6 +3,8 @@ import heartbeat.client.dto.board.jira.CardHistoryResponseDTO; import heartbeat.client.dto.board.jira.FieldResponseDTO; import heartbeat.client.dto.board.jira.JiraBoardConfigDTO; +import heartbeat.client.dto.board.jira.JiraBoardProject; +import heartbeat.client.dto.board.jira.JiraBoardVerifyDTO; import heartbeat.client.dto.board.jira.StatusSelfDTO; import heartbeat.decoder.JiraFeignClientDecoder; import org.springframework.cache.annotation.Cacheable; @@ -30,13 +32,20 @@ StatusSelfDTO getColumnStatusCategory(URI baseUrl, @PathVariable String statusNu String getJiraCards(URI baseUrl, @PathVariable String boardId, @PathVariable int queryCount, @PathVariable int startAt, @PathVariable String jql, @RequestHeader String authorization); - @Cacheable(cacheNames = "jiraActivityFeed", key = "#jiraCardKey") - @GetMapping(path = "/rest/internal/2/issue/{jiraCardKey}/activityfeed") - CardHistoryResponseDTO getJiraCardHistory(URI baseUrl, @PathVariable String jiraCardKey, - @RequestHeader String authorization); + @GetMapping(path = "/rest/internal/2/issue/{jiraCardKey}/activityfeed?startAt={startAt}&maxResults={queryCount}") + CardHistoryResponseDTO getJiraCardHistoryByCount(URI baseUrl, @PathVariable String jiraCardKey, + @PathVariable int startAt, @PathVariable int queryCount, @RequestHeader String authorization); @Cacheable(cacheNames = "targetField", key = "#projectKey") @GetMapping(path = "/rest/api/2/issue/createmeta?projectKeys={projectKey}&expand=projects.issuetypes.fields") FieldResponseDTO getTargetField(URI baseUrl, @PathVariable String projectKey, @RequestHeader String authorization); + @Cacheable(cacheNames = "boardVerification", key = "#boardId") + @GetMapping(path = "/rest/agile/1.0/board/{boardId}") + JiraBoardVerifyDTO getBoard(URI baseUrl, @PathVariable String boardId, @RequestHeader String authorization); + + @Cacheable(cacheNames = "boardProject", key = "#projectIdOrKey") + @GetMapping(path = "rest/api/2/project/{projectIdOrKey}") + JiraBoardProject getProject(URI baseUrl, @PathVariable String projectIdOrKey, @RequestHeader String authorization); + } diff --git a/backend/src/main/java/heartbeat/client/dto/board/jira/CardHistoryResponseDTO.java b/backend/src/main/java/heartbeat/client/dto/board/jira/CardHistoryResponseDTO.java index bdbf8f295a..57bfae92ff 100644 --- a/backend/src/main/java/heartbeat/client/dto/board/jira/CardHistoryResponseDTO.java +++ b/backend/src/main/java/heartbeat/client/dto/board/jira/CardHistoryResponseDTO.java @@ -14,6 +14,8 @@ @AllArgsConstructor public class CardHistoryResponseDTO implements Serializable { + private Boolean isLast; + private List items; } diff --git a/backend/src/main/java/heartbeat/client/dto/board/jira/JiraBoardProject.java b/backend/src/main/java/heartbeat/client/dto/board/jira/JiraBoardProject.java new file mode 100644 index 0000000000..fae99c457e --- /dev/null +++ b/backend/src/main/java/heartbeat/client/dto/board/jira/JiraBoardProject.java @@ -0,0 +1,18 @@ +package heartbeat.client.dto.board.jira; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@AllArgsConstructor +@Data +@Builder +@NoArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) +public class JiraBoardProject { + + private String style; + +} diff --git a/backend/src/main/java/heartbeat/client/dto/board/jira/JiraBoardVerifyDTO.java b/backend/src/main/java/heartbeat/client/dto/board/jira/JiraBoardVerifyDTO.java new file mode 100644 index 0000000000..21308a06d0 --- /dev/null +++ b/backend/src/main/java/heartbeat/client/dto/board/jira/JiraBoardVerifyDTO.java @@ -0,0 +1,26 @@ +package heartbeat.client.dto.board.jira; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) +public class JiraBoardVerifyDTO implements Serializable { + + private String id; + + private String name; + + private String type; + + private Location location; + +} diff --git a/backend/src/main/java/heartbeat/client/dto/board/jira/Location.java b/backend/src/main/java/heartbeat/client/dto/board/jira/Location.java new file mode 100644 index 0000000000..0d847c7274 --- /dev/null +++ b/backend/src/main/java/heartbeat/client/dto/board/jira/Location.java @@ -0,0 +1,20 @@ +package heartbeat.client.dto.board.jira; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) +public class Location implements Serializable { + + private String projectKey; + +} diff --git a/backend/src/main/java/heartbeat/config/CacheConfig.java b/backend/src/main/java/heartbeat/config/CacheConfig.java index aef139a6ed..783c9e46a2 100644 --- a/backend/src/main/java/heartbeat/config/CacheConfig.java +++ b/backend/src/main/java/heartbeat/config/CacheConfig.java @@ -3,6 +3,8 @@ import heartbeat.client.dto.board.jira.CardHistoryResponseDTO; import heartbeat.client.dto.board.jira.FieldResponseDTO; import heartbeat.client.dto.board.jira.JiraBoardConfigDTO; +import heartbeat.client.dto.board.jira.JiraBoardProject; +import heartbeat.client.dto.board.jira.JiraBoardVerifyDTO; import heartbeat.client.dto.board.jira.StatusSelfDTO; import java.time.Duration; import javax.cache.CacheManager; @@ -31,6 +33,8 @@ public CacheManager ehCacheManager() { cacheManager.createCache("jiraStatusCategory", getCacheConfiguration(StatusSelfDTO.class)); cacheManager.createCache("jiraActivityFeed", getCacheConfiguration(CardHistoryResponseDTO.class)); cacheManager.createCache("targetField", getCacheConfiguration(FieldResponseDTO.class)); + cacheManager.createCache("boardVerification", getCacheConfiguration(JiraBoardVerifyDTO.class)); + cacheManager.createCache("boardProject", getCacheConfiguration(JiraBoardProject.class)); return cacheManager; } diff --git a/backend/src/main/java/heartbeat/controller/board/JiraController.java b/backend/src/main/java/heartbeat/controller/board/JiraController.java index 4c705af5e7..fbcd51f97a 100644 --- a/backend/src/main/java/heartbeat/controller/board/JiraController.java +++ b/backend/src/main/java/heartbeat/controller/board/JiraController.java @@ -2,7 +2,9 @@ import heartbeat.controller.board.dto.request.BoardRequestParam; import heartbeat.controller.board.dto.request.BoardType; +import heartbeat.controller.board.dto.request.BoardVerifyRequestParam; import heartbeat.controller.board.dto.response.BoardConfigDTO; +import heartbeat.controller.board.dto.response.JiraVerifyResponse; import heartbeat.service.board.jira.JiraService; import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; @@ -26,4 +28,17 @@ public BoardConfigDTO getBoard(@PathVariable @NotBlank BoardType boardType, return jiraService.getJiraConfiguration(boardType, boardRequestParam); } + @PostMapping("/{boardType}/verify") + public JiraVerifyResponse verify(@PathVariable @NotBlank BoardType boardType, + @Valid @RequestBody BoardVerifyRequestParam boardRequestParam) { + String projectKey = jiraService.verify(boardType, boardRequestParam); + return JiraVerifyResponse.builder().projectKey(projectKey).build(); + } + + @PostMapping("/{boardType}/info") + public BoardConfigDTO getInfo(@PathVariable @NotBlank BoardType boardType, + @Valid @RequestBody BoardRequestParam boardRequestParam) { + return jiraService.getInfo(boardType, boardRequestParam); + } + } diff --git a/backend/src/main/java/heartbeat/controller/board/dto/request/BoardVerifyRequestParam.java b/backend/src/main/java/heartbeat/controller/board/dto/request/BoardVerifyRequestParam.java new file mode 100644 index 0000000000..e0f09dd6f9 --- /dev/null +++ b/backend/src/main/java/heartbeat/controller/board/dto/request/BoardVerifyRequestParam.java @@ -0,0 +1,27 @@ +package heartbeat.controller.board.dto.request; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class BoardVerifyRequestParam { + + @NotBlank(message = "Board Id cannot be empty.") + private String boardId; + + @NotBlank(message = "Site cannot be empty.") + private String site; + + @Valid + @NotBlank(message = "Token cannot be empty.") + private String token; + +} diff --git a/backend/src/main/java/heartbeat/controller/board/dto/response/JiraVerifyResponse.java b/backend/src/main/java/heartbeat/controller/board/dto/response/JiraVerifyResponse.java new file mode 100644 index 0000000000..d9b3f1db1f --- /dev/null +++ b/backend/src/main/java/heartbeat/controller/board/dto/response/JiraVerifyResponse.java @@ -0,0 +1,16 @@ +package heartbeat.controller.board.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class JiraVerifyResponse { + + private String projectKey; + +} diff --git a/backend/src/main/java/heartbeat/controller/source/GithubController.java b/backend/src/main/java/heartbeat/controller/source/GithubController.java index 18e280e0b6..3f9b16676d 100644 --- a/backend/src/main/java/heartbeat/controller/source/GithubController.java +++ b/backend/src/main/java/heartbeat/controller/source/GithubController.java @@ -2,13 +2,18 @@ import heartbeat.controller.source.dto.GitHubResponse; import heartbeat.controller.source.dto.SourceControlDTO; +import heartbeat.controller.source.dto.VerifyBranchRequest; +import heartbeat.exception.BadRequestException; import heartbeat.service.source.github.GitHubService; import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -23,10 +28,13 @@ @Log4j2 public class GithubController { + public static final String TOKEN_PATTER = "^(ghp|gho|ghu|ghs|ghr)_([a-zA-Z0-9]{36})$"; + private final GitHubService gitHubService; @PostMapping @CrossOrigin + @Deprecated(since = "frontend completed") @ResponseStatus(HttpStatus.OK) public GitHubResponse getRepos(@RequestBody @Valid SourceControlDTO sourceControlDTO) { log.info("Start to get repos by token"); @@ -36,4 +44,38 @@ public GitHubResponse getRepos(@RequestBody @Valid SourceControlDTO sourceContro } + @PostMapping("/{sourceType}/verify") + public ResponseEntity verifyToken(@PathVariable @NotBlank String sourceType, + @RequestBody @Valid SourceControlDTO sourceControlDTO) { + log.info("Start to verify source type: {} token.", sourceType); + switch (SourceType.matchSourceType(sourceType)) { + case GITHUB -> { + gitHubService.verifyTokenV2(sourceControlDTO.getToken()); + log.info("Successfully verify source type: {} token.", sourceType); + } + default -> { + log.error("Failed to verify source type: {} token.", sourceType); + throw new BadRequestException("Source type is incorrect."); + } + } + return ResponseEntity.status(HttpStatus.NO_CONTENT).build(); + } + + @PostMapping("/{sourceType}/repos/branches/{branch}/verify") + public ResponseEntity verifyBranch(@PathVariable @NotBlank String sourceType, + @PathVariable @NotBlank String branch, @RequestBody @Valid VerifyBranchRequest request) { + log.info("Start to verify source type: {} branch: {}.", sourceType, branch); + switch (SourceType.matchSourceType(sourceType)) { + case GITHUB -> { + gitHubService.verifyCanReadTargetBranch(request.getRepository(), branch, request.getToken()); + log.info("Successfully verify source type: {} branch: {}.", sourceType, branch); + } + default -> { + log.error("Failed to verify source type: {} branch: {}.", sourceType, branch); + throw new BadRequestException("Source type is incorrect."); + } + } + return ResponseEntity.status(HttpStatus.NO_CONTENT).build(); + } + } diff --git a/backend/src/main/java/heartbeat/controller/source/SourceType.java b/backend/src/main/java/heartbeat/controller/source/SourceType.java new file mode 100644 index 0000000000..daa583dcef --- /dev/null +++ b/backend/src/main/java/heartbeat/controller/source/SourceType.java @@ -0,0 +1,25 @@ +package heartbeat.controller.source; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public enum SourceType { + + GITHUB("github"), + + ANOTHERSOURCETYPE("anotherSourceType"); + + private String value; + + public static SourceType matchSourceType(String value) { + return switch (value) { + case "github" -> GITHUB; + default -> ANOTHERSOURCETYPE; + }; + } + +} diff --git a/backend/src/main/java/heartbeat/controller/source/dto/GitHubResponse.java b/backend/src/main/java/heartbeat/controller/source/dto/GitHubResponse.java index 5efd15e7ac..73d011f60f 100644 --- a/backend/src/main/java/heartbeat/controller/source/dto/GitHubResponse.java +++ b/backend/src/main/java/heartbeat/controller/source/dto/GitHubResponse.java @@ -7,6 +7,7 @@ @Data @Builder +@Deprecated public class GitHubResponse { private LinkedHashSet githubRepos; diff --git a/backend/src/main/java/heartbeat/controller/source/dto/SourceControlDTO.java b/backend/src/main/java/heartbeat/controller/source/dto/SourceControlDTO.java index bf005f3394..93c0672e17 100644 --- a/backend/src/main/java/heartbeat/controller/source/dto/SourceControlDTO.java +++ b/backend/src/main/java/heartbeat/controller/source/dto/SourceControlDTO.java @@ -1,7 +1,6 @@ package heartbeat.controller.source.dto; -import jakarta.validation.Valid; -import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Pattern; import lombok.AllArgsConstructor; import lombok.Builder; @@ -9,6 +8,8 @@ import lombok.NoArgsConstructor; import lombok.Setter; +import static heartbeat.controller.source.GithubController.TOKEN_PATTER; + @Builder @NoArgsConstructor @AllArgsConstructor @@ -16,11 +17,8 @@ @Setter public class SourceControlDTO { - private String type; - - @Valid - @NotBlank(message = "Token cannot be empty.") - @Pattern(regexp = "^(ghp|gho|ghu|ghs|ghr)_([a-zA-Z0-9]{36})$", message = "token's pattern is incorrect") + @NotNull(message = "Token cannot be empty.") + @Pattern(regexp = TOKEN_PATTER, message = "token's pattern is incorrect") private String token; } diff --git a/backend/src/main/java/heartbeat/controller/source/dto/VerifyBranchRequest.java b/backend/src/main/java/heartbeat/controller/source/dto/VerifyBranchRequest.java new file mode 100644 index 0000000000..dc25ef6f80 --- /dev/null +++ b/backend/src/main/java/heartbeat/controller/source/dto/VerifyBranchRequest.java @@ -0,0 +1,28 @@ +package heartbeat.controller.source.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import static heartbeat.controller.source.GithubController.TOKEN_PATTER; + +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +public class VerifyBranchRequest { + + @NotNull(message = "Token cannot be empty.") + @Pattern(regexp = TOKEN_PATTER, message = "token's pattern is incorrect") + private String token; + + @NotBlank(message = "Repository is required.") + private String repository; + +} diff --git a/backend/src/main/java/heartbeat/enums/AssigneeFilterMethod.java b/backend/src/main/java/heartbeat/enums/AssigneeFilterMethod.java index 8b92a3b354..9903ff32d5 100644 --- a/backend/src/main/java/heartbeat/enums/AssigneeFilterMethod.java +++ b/backend/src/main/java/heartbeat/enums/AssigneeFilterMethod.java @@ -7,6 +7,8 @@ @AllArgsConstructor public enum AssigneeFilterMethod { + ASSIGNEE_FIELD_ID("assignee"), + LAST_ASSIGNEE("lastAssignee"), HISTORICAL_ASSIGNEE("historicalAssignee"); diff --git a/backend/src/main/java/heartbeat/service/board/jira/JiraService.java b/backend/src/main/java/heartbeat/service/board/jira/JiraService.java index 3e216568c5..b7773dc746 100644 --- a/backend/src/main/java/heartbeat/service/board/jira/JiraService.java +++ b/backend/src/main/java/heartbeat/service/board/jira/JiraService.java @@ -16,6 +16,7 @@ import heartbeat.client.dto.board.jira.IssueField; import heartbeat.client.dto.board.jira.Issuetype; import heartbeat.client.dto.board.jira.JiraBoardConfigDTO; +import heartbeat.client.dto.board.jira.JiraBoardVerifyDTO; import heartbeat.client.dto.board.jira.JiraCard; import heartbeat.client.dto.board.jira.JiraCardWithFields; import heartbeat.client.dto.board.jira.JiraColumn; @@ -23,6 +24,7 @@ import heartbeat.client.dto.board.jira.StatusSelfDTO; import heartbeat.controller.board.dto.request.BoardRequestParam; import heartbeat.controller.board.dto.request.BoardType; +import heartbeat.controller.board.dto.request.BoardVerifyRequestParam; import heartbeat.controller.board.dto.request.CardStepsEnum; import heartbeat.controller.board.dto.request.RequestJiraBoardColumnSetting; import heartbeat.controller.board.dto.request.StoryPointsAndCycleTimeRequest; @@ -57,6 +59,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -104,6 +107,68 @@ public void shutdownExecutor() { customTaskExecutor.shutdown(); } + public String verify(BoardType boardType, BoardVerifyRequestParam boardVerifyRequestParam) { + URI baseUrl = urlGenerator.getUri(boardVerifyRequestParam.getSite()); + try { + if (!BoardType.JIRA.equals(boardType)) { + throw new BadRequestException("boardType param is not correct"); + } + JiraBoardVerifyDTO jiraBoardVerifyDTO = jiraFeignClient.getBoard(baseUrl, + boardVerifyRequestParam.getBoardId(), boardVerifyRequestParam.getToken()); + return jiraBoardVerifyDTO.getLocation().getProjectKey(); + } + catch (RuntimeException e) { + Throwable cause = Optional.ofNullable(e.getCause()).orElse(e); + log.error("Failed when call Jira to verify board, board id: {}, e: {}", + boardVerifyRequestParam.getBoardId(), cause.getMessage()); + if (cause instanceof BaseException baseException) { + throw baseException; + } + throw new InternalServerErrorException( + String.format("Failed when call Jira to verify board, cause is %s", cause.getMessage())); + } + } + + public BoardConfigDTO getInfo(BoardType boardType, BoardRequestParam boardRequestParam) { + URI baseUrl = urlGenerator.getUri(boardRequestParam.getSite()); + try { + if (!BoardType.JIRA.equals(boardType)) { + throw new BadRequestException("boardType param is not correct"); + } + String jiraBoardStyle = jiraFeignClient + .getProject(baseUrl, boardRequestParam.getProjectKey(), boardRequestParam.getToken()) + .getStyle(); + BoardType jiraBoardType = "classic".equals(jiraBoardStyle) ? BoardType.CLASSIC_JIRA : BoardType.JIRA; + + Map> partitions = getTargetFieldAsync(baseUrl, boardRequestParam).join() + .stream() + .collect(Collectors.partitioningBy(this::isIgnoredTargetField)); + List ignoredTargetFields = partitions.get(true); + List neededTargetFields = partitions.get(false); + + return getJiraColumnsAsync(boardRequestParam, baseUrl, + getJiraBoardConfig(baseUrl, boardRequestParam.getBoardId(), boardRequestParam.getToken())) + .thenCombine(getUserAsync(jiraBoardType, baseUrl, boardRequestParam), + (jiraColumnResult, users) -> BoardConfigDTO.builder() + .targetFields(neededTargetFields) + .jiraColumnResponse(jiraColumnResult.getJiraColumnResponse()) + .ignoredTargetFields(ignoredTargetFields) + .users(users) + .build()) + .join(); + } + catch (RuntimeException e) { + Throwable cause = Optional.ofNullable(e.getCause()).orElse(e); + log.error("Failed when call Jira to get board config, project key: {}, board id: {}, e: {}", + boardRequestParam.getBoardId(), boardRequestParam.getProjectKey(), cause.getMessage()); + if (cause instanceof BaseException baseException) { + throw baseException; + } + throw new InternalServerErrorException( + String.format("Failed when call Jira to get board config, cause is %s", cause.getMessage())); + } + } + public BoardConfigDTO getJiraConfiguration(BoardType boardType, BoardRequestParam boardRequestParam) { URI baseUrl = urlGenerator.getUri(boardRequestParam.getSite()); try { @@ -397,8 +462,7 @@ private String parseJiraJql(BoardType boardType, List doneColumns, Board private List getAssigneeSet(URI baseUrl, JiraCard jiraCard, String jiraToken) { log.info("Start to get jira card history, _cardKey: {}", jiraCard.getKey()); - CardHistoryResponseDTO cardHistoryResponseDTO = jiraFeignClient.getJiraCardHistory(baseUrl, jiraCard.getKey(), - jiraToken); + CardHistoryResponseDTO cardHistoryResponseDTO = getJiraCardHistory(baseUrl, jiraCard.getKey(), 0, jiraToken); log.info("Successfully get jira card history, _cardKey: {}, _cardHistoryItemsSize: {}", jiraCard.getKey(), cardHistoryResponseDTO.getItems().size()); @@ -460,16 +524,18 @@ private List getRealDoneCards(StoryPointsAndCycleTimeRequest reques List jiraCards = new ArrayList<>(); for (JiraCard allDoneCard : allDoneCards) { - CardHistoryResponseDTO jiraCardHistory = jiraFeignClient.getJiraCardHistory(baseUrl, allDoneCard.getKey(), + CardHistoryResponseDTO jiraCardHistory = getJiraCardHistory(baseUrl, allDoneCard.getKey(), 0, request.getToken()); if (isRealDoneCardByHistory(jiraCardHistory, request)) { jiraCards.add(allDoneCard); } } jiraCards.forEach(doneCard -> { - CycleTimeInfoDTO cycleTimeInfoDTO = getCycleTime(baseUrl, doneCard.getKey(), request.getToken(), - request.isTreatFlagCardAsBlock(), keyFlagged, request.getStatus()); - List assigneeSet = getAssigneeSet(request, baseUrl, filterMethod, doneCard); + CardHistoryResponseDTO cardHistoryResponseDTO = getJiraCardHistory(baseUrl, doneCard.getKey(), 0, + request.getToken()); + CycleTimeInfoDTO cycleTimeInfoDTO = getCycleTime(cardHistoryResponseDTO, request.isTreatFlagCardAsBlock(), + keyFlagged, request.getStatus()); + List assigneeSet = getAssigneeSet(cardHistoryResponseDTO, filterMethod, doneCard); if (users.stream().anyMatch(assigneeSet::contains)) { JiraCardDTO jiraCardDTO = JiraCardDTO.builder() .baseInfo(doneCard) @@ -484,31 +550,57 @@ private List getRealDoneCards(StoryPointsAndCycleTimeRequest reques return realDoneCards; } - private List getAssigneeSet(StoryPointsAndCycleTimeRequest request, URI baseUrl, String filterMethod, + private List getAssigneeSet(CardHistoryResponseDTO jiraCardHistory, String assigneeFilter, JiraCard doneCard) { List assigneeSet = new ArrayList<>(); Assignee assignee = doneCard.getFields().getAssignee(); - if (assignee != null && useLastAssignee(filterMethod)) { - assigneeSet.add(assignee.getDisplayName()); + if (useLastAssignee(assigneeFilter)) { + if (assignee != null) { + assigneeSet.add(assignee.getDisplayName()); + } + else { + List historicalAssignees = getHistoricalAssignees(jiraCardHistory); + assigneeSet.add(getLastHistoricalAssignee(historicalAssignees)); + } } - - if (assignee != null && filterMethod.equals(AssigneeFilterMethod.HISTORICAL_ASSIGNEE.getDescription())) { - List historyDisplayName = getHistoricalAssignees(baseUrl, doneCard.getKey(), request.getToken()); - assigneeSet.addAll(historyDisplayName); + else { + List historicalAssignees = getHistoricalAssignees(jiraCardHistory); + assigneeSet.addAll(historicalAssignees); } return assigneeSet; } - private List getHistoricalAssignees(URI baseUrl, String cardKey, String token) { - CardHistoryResponseDTO jiraCardHistory = jiraFeignClient.getJiraCardHistory(baseUrl, cardKey, token); - return jiraCardHistory.getItems().stream().map(item -> item.getActor().getDisplayName()).distinct().toList(); + private String getLastHistoricalAssignee(List historicalAssignees) { + return historicalAssignees.stream().filter(Objects::nonNull).findFirst().orElse(null); + } + + private List getHistoricalAssignees(CardHistoryResponseDTO jiraCardHistory) { + return jiraCardHistory.getItems() + .stream() + .filter(item -> AssigneeFilterMethod.ASSIGNEE_FIELD_ID.getDescription().equalsIgnoreCase(item.getFieldId())) + .sorted(Comparator.comparing(HistoryDetail::getTimestamp).reversed()) + .map(item -> item.getTo().getDisplayValue()) + .distinct() + .toList(); } - private boolean useLastAssignee(String filterMethod) { - return filterMethod.equals(AssigneeFilterMethod.LAST_ASSIGNEE.getDescription()) - || StringUtils.isEmpty(filterMethod); + private CardHistoryResponseDTO getJiraCardHistory(URI baseUrl, String cardKey, int startAt, String token) { + int queryCount = 100; + CardHistoryResponseDTO jiraCardHistory = jiraFeignClient.getJiraCardHistoryByCount(baseUrl, cardKey, startAt, + queryCount, token); + if (Boolean.FALSE.equals(jiraCardHistory.getIsLast())) { + CardHistoryResponseDTO cardAllHistory = getJiraCardHistory(baseUrl, cardKey, startAt + queryCount, token); + jiraCardHistory.getItems().addAll(cardAllHistory.getItems()); + jiraCardHistory.setIsLast(jiraCardHistory.getIsLast()); + } + return jiraCardHistory; + } + + private boolean useLastAssignee(String assigneeFilter) { + return assigneeFilter.equals(AssigneeFilterMethod.LAST_ASSIGNEE.getDescription()) + || StringUtils.isEmpty(assigneeFilter); } private boolean isRealDoneCardByHistory(CardHistoryResponseDTO jiraCardHistory, @@ -528,9 +620,8 @@ private boolean isRealDoneCardByHistory(CardHistoryResponseDTO jiraCardHistory, .isPresent(); } - private CycleTimeInfoDTO getCycleTime(URI baseUrl, String doneCardKey, String token, Boolean treatFlagCardAsBlock, + private CycleTimeInfoDTO getCycleTime(CardHistoryResponseDTO cardHistoryResponseDTO, Boolean treatFlagCardAsBlock, String keyFlagged, List realDoneStatus) { - CardHistoryResponseDTO cardHistoryResponseDTO = jiraFeignClient.getJiraCardHistory(baseUrl, doneCardKey, token); List statusChangedArray = putStatusChangeEventsIntoAnArray(cardHistoryResponseDTO, keyFlagged); List cycleTimeInfos = boardUtil.getCycleTimeInfos(statusChangedArray, realDoneStatus, @@ -696,8 +787,10 @@ private List getMatchedNonDoneCards(StoryPointsAndCycleTimeRequest String keyFlagged = cardCustomFieldKey.getFlagged(); allNonDoneCards.forEach(card -> { - CycleTimeInfoDTO cycleTimeInfoDTO = getCycleTime(baseUrl, card.getKey(), request.getToken(), - request.isTreatFlagCardAsBlock(), keyFlagged, request.getStatus()); + CardHistoryResponseDTO cardHistoryResponseDTO = getJiraCardHistory(baseUrl, card.getKey(), 0, + request.getToken()); + CycleTimeInfoDTO cycleTimeInfoDTO = getCycleTime(cardHistoryResponseDTO, request.isTreatFlagCardAsBlock(), + keyFlagged, request.getStatus()); List assigneeSet = getAssigneeSetWithDisplayName(baseUrl, card, request.getToken()); if (users.stream().anyMatch(assigneeSet::contains)) { diff --git a/backend/src/main/java/heartbeat/service/source/github/GitHubService.java b/backend/src/main/java/heartbeat/service/source/github/GitHubService.java index 1f7a7df34f..5838fa3a12 100644 --- a/backend/src/main/java/heartbeat/service/source/github/GitHubService.java +++ b/backend/src/main/java/heartbeat/service/source/github/GitHubService.java @@ -14,6 +14,7 @@ import heartbeat.exception.GithubRepoEmptyException; import heartbeat.exception.InternalServerErrorException; import heartbeat.exception.NotFoundException; +import heartbeat.exception.PermissionDenyException; import heartbeat.service.source.github.model.PipelineInfoOfRepository; import heartbeat.util.GithubUtil; import jakarta.annotation.PreDestroy; @@ -40,6 +41,10 @@ @Log4j2 public class GitHubService { + public static final String TOKEN_TITLE = "token "; + + public static final String BEARER_TITLE = "Bearer "; + private final ThreadPoolTaskExecutor customTaskExecutor; private final GitHubFeignClient gitHubFeignClient; @@ -49,9 +54,10 @@ public void shutdownExecutor() { customTaskExecutor.shutdown(); } + @Deprecated public GitHubResponse verifyToken(String githubToken) { try { - String token = "token " + githubToken; + String token = TOKEN_TITLE + githubToken; log.info("Start to query repository url by token"); CompletableFuture> githubReposByUserFuture = CompletableFuture .supplyAsync(() -> gitHubFeignClient.getAllRepos(token), customTaskExecutor); @@ -87,6 +93,47 @@ public GitHubResponse verifyToken(String githubToken) { } } + public void verifyTokenV2(String githubToken) { + try { + String token = TOKEN_TITLE + githubToken; + log.info("Start to request github with token"); + gitHubFeignClient.verifyToken(token); + log.info("Successfully verify token from github"); + } + catch (RuntimeException e) { + Throwable cause = Optional.ofNullable(e.getCause()).orElse(e); + log.error("Failed to call GitHub with token_error: {} ", cause.getMessage()); + if (cause instanceof BaseException baseException) { + throw baseException; + } + throw new InternalServerErrorException( + String.format("Failed to call GitHub with token_error: %s", cause.getMessage())); + } + } + + public void verifyCanReadTargetBranch(String repository, String branch, String githubToken) { + try { + String token = TOKEN_TITLE + githubToken; + log.info("Start to request github branch: {}", branch); + gitHubFeignClient.verifyCanReadTargetBranch(GithubUtil.getGithubUrlFullName(repository), branch, token); + log.info("Successfully verify target branch for github, branch: {}", branch); + } + catch (NotFoundException e) { + log.error("Failed to call GitHub with branch: {}, error: {} ", branch, e.getMessage()); + throw new PermissionDenyException(String.format("Unable to read target branch: %s", branch)); + } + catch (RuntimeException e) { + Throwable cause = Optional.ofNullable(e.getCause()).orElse(e); + log.error("Failed to call GitHub branch:{} with error: {} ", branch, cause.getMessage()); + if (cause instanceof BaseException baseException) { + throw baseException; + } + throw new InternalServerErrorException( + String.format("Failed to call GitHub branch: %s with error: %s", branch, cause.getMessage())); + } + } + + @Deprecated private CompletableFuture> getAllGitHubReposAsync(String token, List gitHubOrganizations) { List>> repoFutures = gitHubOrganizations.stream() @@ -117,7 +164,7 @@ private CompletableFuture> getAllGitHubReposAsync(String token, public List fetchPipelinesLeadTime(List deployTimes, Map repositories, String token) { try { - String realToken = "Bearer " + token; + String realToken = BEARER_TITLE + token; List pipelineInfoOfRepositories = getInfoOfRepositories(deployTimes, repositories); @@ -144,7 +191,7 @@ public List fetchPipelinesLeadTime(List deployTim }) .toList(); - return pipelineLeadTimeFutures.stream().map(CompletableFuture::join).collect(Collectors.toList()); + return pipelineLeadTimeFutures.stream().map(CompletableFuture::join).toList(); } catch (RuntimeException e) { Throwable cause = Optional.ofNullable(e.getCause()).orElse(e); @@ -287,7 +334,7 @@ public LeadTime mapLeadTimeWithInfo(PullRequestInfo pullRequestInfo, DeployInfo public CommitInfo fetchCommitInfo(String commitId, String repositoryId, String token) { try { - String realToken = "Bearer " + token; + String realToken = BEARER_TITLE + token; log.info("Start to get commit info, repoId: {},commitId: {}", repositoryId, commitId); CommitInfo commitInfo = gitHubFeignClient.getCommitInfo(repositoryId, commitId, realToken); log.info("Successfully get commit info, repoId: {},commitId: {}, author: {}", repositoryId, commitId, diff --git a/backend/src/test/java/heartbeat/TestFixtures.java b/backend/src/test/java/heartbeat/TestFixtures.java index 92ca9e1a2c..0b237754cd 100644 --- a/backend/src/test/java/heartbeat/TestFixtures.java +++ b/backend/src/test/java/heartbeat/TestFixtures.java @@ -6,4 +6,6 @@ public class TestFixtures { public static final String BUILDKITE_TOKEN = "bkua_6xxxafcc3bxxxxxxb8xxx8d8dxxxf7897cc8b2f1"; + public static final String GITHUB_REPOSITORY = "git@github.com:fake/repo.git"; + } diff --git a/backend/src/test/java/heartbeat/controller/board/BoardRequestFixture.java b/backend/src/test/java/heartbeat/controller/board/BoardRequestFixture.java index 497c3b4b00..d6250cff15 100644 --- a/backend/src/test/java/heartbeat/controller/board/BoardRequestFixture.java +++ b/backend/src/test/java/heartbeat/controller/board/BoardRequestFixture.java @@ -6,6 +6,8 @@ public class BoardRequestFixture { + public static final String BOARD_ID = "unknown"; + public static BoardRequestParam.BoardRequestParamBuilder BOARD_REQUEST_BUILDER() { return BoardRequestParam.builder() .boardId(BOARD_ID) diff --git a/backend/src/test/java/heartbeat/controller/board/JiraControllerTest.java b/backend/src/test/java/heartbeat/controller/board/JiraControllerTest.java index c65d2481ca..bdc58f7ec1 100644 --- a/backend/src/test/java/heartbeat/controller/board/JiraControllerTest.java +++ b/backend/src/test/java/heartbeat/controller/board/JiraControllerTest.java @@ -2,6 +2,8 @@ import static heartbeat.controller.board.BoardConfigResponseFixture.BOARD_CONFIG_RESPONSE_BUILDER; import static heartbeat.controller.board.BoardRequestFixture.BOARD_REQUEST_BUILDER; +import static heartbeat.controller.board.dto.request.BoardVerifyRequestFixture.BOARD_VERIFY_REQUEST_BUILDER; +import static heartbeat.controller.board.dto.request.BoardVerifyRequestFixture.PROJECT_KEY; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -11,6 +13,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; import heartbeat.controller.board.dto.request.BoardRequestParam; +import heartbeat.controller.board.dto.request.BoardType; +import heartbeat.controller.board.dto.request.BoardVerifyRequestParam; import heartbeat.controller.board.dto.response.BoardConfigDTO; import heartbeat.exception.RequestFailedException; import heartbeat.service.board.jira.JiraService; @@ -51,6 +55,35 @@ void shouldReturnCorrectBoardConfigResponseWhenGivenTheCorrectBoardRequest() thr .andExpect(jsonPath("$.targetFields[0].name").value("Priority")); } + @Test + void shouldReturnCorrectBoardInfoResponseWhenGivenTheCorrectBoardRequest() throws Exception { + BoardConfigDTO boardConfigDTO = BOARD_CONFIG_RESPONSE_BUILDER().build(); + BoardRequestParam boardRequestParam = BOARD_REQUEST_BUILDER().build(); + + when(jiraService.getInfo(any(), any())).thenReturn(boardConfigDTO); + + mockMvc + .perform(post("/boards/{boardType}/info", "jira").contentType(MediaType.APPLICATION_JSON) + .content(new ObjectMapper().writeValueAsString(boardRequestParam))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.jiraColumns[0].value.name").value("TODO")) + .andExpect(jsonPath("$.users[0]").value("Zhang San")) + .andExpect(jsonPath("$.targetFields[0].name").value("Priority")); + } + + @Test + void shouldReturnCorrectBoardVerificationResponseWhenGivenTheCorrectBoardRequest() throws Exception { + BoardVerifyRequestParam boardVerifyRequestParam = BOARD_VERIFY_REQUEST_BUILDER().build(); + + when(jiraService.verify(any(), any())).thenReturn(PROJECT_KEY); + + mockMvc + .perform(post("/boards/{boardType}/verify", BoardType.JIRA).contentType(MediaType.APPLICATION_JSON) + .content(new ObjectMapper().writeValueAsString(boardVerifyRequestParam))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.projectKey").value(PROJECT_KEY)); + } + @Test void shouldHandleServiceExceptionAndReturnWithStatusAndMessage() throws Exception { RequestFailedException mockException = mock(RequestFailedException.class); @@ -68,6 +101,40 @@ void shouldHandleServiceExceptionAndReturnWithStatusAndMessage() throws Exceptio .andExpect(jsonPath("$.message").value(message)); } + @Test + void shouldHandleServiceExceptionAndReturnWithStatusAndMessageWhenGetBoardInfo() throws Exception { + RequestFailedException mockException = mock(RequestFailedException.class); + String message = "message"; + when(jiraService.getInfo(any(), any())).thenThrow(mockException); + when(mockException.getMessage()).thenReturn(message); + when(mockException.getStatus()).thenReturn(400); + + BoardRequestParam boardRequestParam = BOARD_REQUEST_BUILDER().build(); + + mockMvc + .perform(post("/boards/{boardType}/info", "jira").contentType(MediaType.APPLICATION_JSON) + .content(new ObjectMapper().writeValueAsString(boardRequestParam))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value(message)); + } + + @Test + void shouldHandleVerifyServiceExceptionAndReturnWithStatusAndMessage() throws Exception { + RequestFailedException mockException = mock(RequestFailedException.class); + String message = "message"; + when(jiraService.verify(any(), any())).thenThrow(mockException); + when(mockException.getMessage()).thenReturn(message); + when(mockException.getStatus()).thenReturn(400); + + BoardVerifyRequestParam boardVerifyRequestParam = BOARD_VERIFY_REQUEST_BUILDER().build(); + + mockMvc + .perform(post("/boards/{boardType}/verify", BoardType.JIRA).contentType(MediaType.APPLICATION_JSON) + .content(new ObjectMapper().writeValueAsString(boardVerifyRequestParam))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value(message)); + } + @Test void shouldVerifyRequestTokenNotBlank() throws Exception { BoardRequestParam boardRequestParam = BOARD_REQUEST_BUILDER().token(null).build(); @@ -78,6 +145,26 @@ void shouldVerifyRequestTokenNotBlank() throws Exception { .andExpect(status().isBadRequest()); } + @Test + void shouldRequestTokenNotBlankWhenGetBoardInfo() throws Exception { + BoardRequestParam boardRequestParam = BOARD_REQUEST_BUILDER().token(null).build(); + + mockMvc + .perform(post("/boards/{boardType}/info", "jira").contentType(MediaType.APPLICATION_JSON) + .content(new ObjectMapper().writeValueAsString(boardRequestParam))) + .andExpect(status().isBadRequest()); + } + + @Test + void shouldBoardVerifyRequestTokenNotBlank() throws Exception { + BoardVerifyRequestParam boardVerifyRequestParam = BOARD_VERIFY_REQUEST_BUILDER().token(null).build(); + + mockMvc + .perform(post("/boards/{boardType}/verify", "jira").contentType(MediaType.APPLICATION_JSON) + .content(new ObjectMapper().writeValueAsString(boardVerifyRequestParam))) + .andExpect(status().isBadRequest()); + } + @Test void shouldThrowExceptionWhenBoardTypeNotSupported() throws Exception { BoardRequestParam boardRequestParam = BOARD_REQUEST_BUILDER().token("").build(); @@ -89,4 +176,15 @@ void shouldThrowExceptionWhenBoardTypeNotSupported() throws Exception { .andExpect(jsonPath("$.message").isNotEmpty()); } + @Test + void shouldThrowExceptionWhenGetBoardInfoAndBoardTypeNotSupported() throws Exception { + BoardRequestParam boardRequestParam = BOARD_REQUEST_BUILDER().token("").build(); + + mockMvc + .perform(post("/boards/{boardType}/info", "invalid").contentType(MediaType.APPLICATION_JSON) + .content(new ObjectMapper().writeValueAsString(boardRequestParam))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").isNotEmpty()); + } + } diff --git a/backend/src/test/java/heartbeat/controller/board/dto/request/BoardVerifyRequestFixture.java b/backend/src/test/java/heartbeat/controller/board/dto/request/BoardVerifyRequestFixture.java new file mode 100644 index 0000000000..bd4f0799c5 --- /dev/null +++ b/backend/src/test/java/heartbeat/controller/board/dto/request/BoardVerifyRequestFixture.java @@ -0,0 +1,11 @@ +package heartbeat.controller.board.dto.request; + +public class BoardVerifyRequestFixture { + + public static final String PROJECT_KEY = "ADM"; + + public static BoardVerifyRequestParam.BoardVerifyRequestParamBuilder BOARD_VERIFY_REQUEST_BUILDER() { + return BoardVerifyRequestParam.builder().boardId("unknown").site("site").token("token"); + } + +} diff --git a/backend/src/test/java/heartbeat/controller/source/GithubControllerTest.java b/backend/src/test/java/heartbeat/controller/source/GithubControllerTest.java index 7f9f52610e..3a99f3fa1c 100644 --- a/backend/src/test/java/heartbeat/controller/source/GithubControllerTest.java +++ b/backend/src/test/java/heartbeat/controller/source/GithubControllerTest.java @@ -4,9 +4,12 @@ import com.jayway.jsonpath.JsonPath; import heartbeat.controller.source.dto.GitHubResponse; import heartbeat.controller.source.dto.SourceControlDTO; +import heartbeat.controller.source.dto.VerifyBranchRequest; import heartbeat.service.source.github.GitHubService; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.json.AutoConfigureJsonTesters; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; @@ -18,11 +21,12 @@ import java.util.LinkedHashSet; import java.util.List; +import static heartbeat.TestFixtures.GITHUB_REPOSITORY; import static heartbeat.TestFixtures.GITHUB_TOKEN; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; 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.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -39,6 +43,7 @@ class GithubControllerTest { private MockMvc mockMvc; @Test + @Deprecated void shouldReturnOkStatusAndCorrectResponseWithRepos() throws Exception { LinkedHashSet repos = new LinkedHashSet<>( List.of("https://github.com/xxxx1/repo1", "https://github.com/xxxx2/repo2")); @@ -57,18 +62,175 @@ void shouldReturnOkStatusAndCorrectResponseWithRepos() throws Exception { } @Test + void shouldReturnNoContentStatusWhenVerifyToken() throws Exception { + doNothing().when(gitHubVerifyService).verifyTokenV2(GITHUB_TOKEN); + SourceControlDTO sourceControlDTO = SourceControlDTO.builder().token(GITHUB_TOKEN).build(); + + mockMvc + .perform(post("/source-control/github/verify") + .content(new ObjectMapper().writeValueAsString(sourceControlDTO)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()); + } + + @Test + void shouldReturnNoContentStatusWhenVerifyTargetBranch() throws Exception { + VerifyBranchRequest verifyBranchRequest = VerifyBranchRequest.builder() + .repository(GITHUB_REPOSITORY) + .token(GITHUB_TOKEN) + .build(); + doNothing().when(gitHubVerifyService).verifyCanReadTargetBranch("fake/repo", "main", GITHUB_TOKEN); + + mockMvc + .perform(post("/source-control/github/repos/branches/main/verify") + .content(new ObjectMapper().writeValueAsString(verifyBranchRequest)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()); + } + + @Test + @Deprecated void shouldReturnBadRequestWhenRequestBodyIsBlank() throws Exception { - final var response = mockMvc.perform(post("/source-control")) - .andExpect(status().isInternalServerError()) + SourceControlDTO sourceControlDTO = SourceControlDTO.builder().token(null).build(); + + final var response = mockMvc + .perform(post("/source-control").content(new ObjectMapper().writeValueAsString(sourceControlDTO)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()) + .andReturn() + .getResponse(); + + final var content = response.getContentAsString(); + final var result = JsonPath.parse(content).read("$.token").toString(); + assertThat(result).contains("Token cannot be empty."); + } + + @Test + void shouldReturnBadRequestGivenRequestBodyIsNullWhenVerifyToken() throws Exception { + SourceControlDTO sourceControlDTO = SourceControlDTO.builder().build(); + + final var response = mockMvc + .perform(post("/source-control/GitHub/verify") + .content(new ObjectMapper().writeValueAsString(sourceControlDTO)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()) + .andReturn() + .getResponse(); + + final var content = response.getContentAsString(); + final var result = JsonPath.parse(content).read("$.token").toString(); + assertThat(result).contains("Token cannot be empty."); + } + + @Test + void shouldReturnBadRequestGivenRequestBodyIsNullWhenVerifyBranch() throws Exception { + VerifyBranchRequest verifyBranchRequest = VerifyBranchRequest.builder().build(); + + final var response = mockMvc + .perform(post("/source-control/GitHub/repos/branches/main/verify") + .content(new ObjectMapper().writeValueAsString(verifyBranchRequest)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()) + .andReturn() + .getResponse(); + + final var content = response.getContentAsString(); + final var result = JsonPath.parse(content).read("$.token").toString(); + assertThat(result).contains("Token cannot be empty."); + } + + @Test + void shouldReturnBadRequestGivenRepositoryIsNullWhenVerifyBranch() throws Exception { + VerifyBranchRequest verifyBranchRequest = VerifyBranchRequest.builder().token(GITHUB_TOKEN).build(); + + final var response = mockMvc + .perform(post("/source-control/GitHub/repos/branches/main/verify") + .content(new ObjectMapper().writeValueAsString(verifyBranchRequest)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()) + .andReturn() + .getResponse(); + + final var content = response.getContentAsString(); + final var result = JsonPath.parse(content).read("$.repository").toString(); + assertThat(result).contains("Repository is required."); + } + + @Test + void shouldReturnBadRequestGivenSourceTypeIsWrongWhenVerifyToken() throws Exception { + SourceControlDTO sourceControlDTO = SourceControlDTO.builder().token(GITHUB_TOKEN).build(); + + final var response = mockMvc + .perform(post("/source-control/GitHub/verify") + .content(new ObjectMapper().writeValueAsString(sourceControlDTO)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()) + .andReturn() + .getResponse(); + + final var content = response.getContentAsString(); + final var result = JsonPath.parse(content).read("$.message").toString(); + assertThat(result).contains("Source type is incorrect."); + } + + @Test + void shouldReturnBadRequestGivenSourceTypeIsWrongWhenVerifyBranch() throws Exception { + VerifyBranchRequest request = VerifyBranchRequest.builder() + .repository(GITHUB_REPOSITORY) + .token(GITHUB_TOKEN) + .build(); + + final var response = mockMvc + .perform(post("/source-control/GitHub/repos/branches/main/verify") + .content(new ObjectMapper().writeValueAsString(request)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()) + .andReturn() + .getResponse(); + + final var content = response.getContentAsString(); + final var result = JsonPath.parse(content).read("$.message").toString(); + assertThat(result).contains("Source type is incorrect."); + } + + @Test + void shouldReturnBadRequestGivenSourceTypeIsBlankWhenVerifyToken() throws Exception { + SourceControlDTO sourceControlDTO = SourceControlDTO.builder().token(GITHUB_TOKEN).build(); + + final var response = mockMvc + .perform(post("/source-control/ /verify").content(new ObjectMapper().writeValueAsString(sourceControlDTO)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()) .andReturn() .getResponse(); final var content = response.getContentAsString(); final var result = JsonPath.parse(content).read("$.message").toString(); - assertThat(result).contains("Required request body is missing"); + assertThat(result).contains("verifyToken.sourceType: must not be blank"); } @Test + void shouldReturnBadRequestGivenSourceTypeIsBlankWhenVerifyBranch() throws Exception { + VerifyBranchRequest request = VerifyBranchRequest.builder() + .token(GITHUB_TOKEN) + .repository(GITHUB_REPOSITORY) + .build(); + + final var response = mockMvc + .perform(post("/source-control/GitHub/repos/branches/ /verify") + .content(new ObjectMapper().writeValueAsString(request)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()) + .andReturn() + .getResponse(); + + final var content = response.getContentAsString(); + final var result = JsonPath.parse(content).read("$.message").toString(); + assertThat(result).contains("verifyBranch.branch: must not be blank"); + } + + @Test + @Deprecated void shouldReturnBadRequestWhenRequestParamPatternIsIncorrect() throws Exception { SourceControlDTO sourceControlDTO = SourceControlDTO.builder().token("12345").build(); @@ -84,4 +246,40 @@ void shouldReturnBadRequestWhenRequestParamPatternIsIncorrect() throws Exception assertThat(result).isEqualTo("token's pattern is incorrect"); } + @ParameterizedTest + @ValueSource(strings = { "12345", " ", "" }) + void shouldReturnBadRequestGivenRequestParamPatternIsIncorrectWhenVerifyToken(String token) throws Exception { + SourceControlDTO sourceControlDTO = SourceControlDTO.builder().token(token).build(); + + final var response = mockMvc + .perform(post("/source-control/github/verify") + .content(new ObjectMapper().writeValueAsString(sourceControlDTO)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()) + .andReturn() + .getResponse(); + + final var content = response.getContentAsString(); + final var result = JsonPath.parse(content).read("$.token").toString(); + assertThat(result).isEqualTo("token's pattern is incorrect"); + } + + @ParameterizedTest + @ValueSource(strings = { "12345", " ", "" }) + void shouldReturnBadRequestGivenRequestParamPatternIsIncorrectWhenVerifyBranch(String token) throws Exception { + VerifyBranchRequest request = VerifyBranchRequest.builder().token(token).build(); + + final var response = mockMvc + .perform(post("/source-control/GitHub/repos/branches/main/verify") + .content(new ObjectMapper().writeValueAsString(request)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()) + .andReturn() + .getResponse(); + + final var content = response.getContentAsString(); + final var result = JsonPath.parse(content).read("$.token").toString(); + assertThat(result).isEqualTo("token's pattern is incorrect"); + } + } diff --git a/backend/src/test/java/heartbeat/service/jira/JiraBoardConfigDTOFixture.java b/backend/src/test/java/heartbeat/service/jira/JiraBoardConfigDTOFixture.java index ba111a016e..72882d3d11 100644 --- a/backend/src/test/java/heartbeat/service/jira/JiraBoardConfigDTOFixture.java +++ b/backend/src/test/java/heartbeat/service/jira/JiraBoardConfigDTOFixture.java @@ -64,6 +64,20 @@ public class JiraBoardConfigDTOFixture { public static final String ASSIGNEE_NAME = "Zhang San"; + public static final String DISPLAY_NAME_ONE = "Da Pei"; + + public static final String DISPLAY_NAME_TWO = "Xiao Pei"; + + public static final String START_TIME = "1672556350000"; + + public static final String END_TIME = "1676908799000"; + + public static final long TIMESTAMP_1 = 1673556350000L; + + public static final long TIMESTAMP_2 = 1674556350000L; + + public static final long TIMESTAMP_3 = 1673556350001L; + public static JiraBoardConfigDTO.JiraBoardConfigDTOBuilder JIRA_BOARD_CONFIG_RESPONSE_BUILDER() { return JiraBoardConfigDTO.builder() @@ -173,6 +187,7 @@ public static AllDoneCardsResponseDTO.AllDoneCardsResponseDTOBuilder ONE_PAGE_NO public static CardHistoryResponseDTO.CardHistoryResponseDTOBuilder CARD_HISTORY_RESPONSE_BUILDER() { return CardHistoryResponseDTO.builder() + .isLast(true) .items(List.of(new HistoryDetail(2, "status", new Status("In Dev"), new Status("To do"), null), new HistoryDetail(3, "status", new Status(REVIEW), new Status("In Dev"), null), new HistoryDetail(4, "status", new Status(WAITING_FOR_TESTING), new Status(REVIEW), null), @@ -182,6 +197,7 @@ public static CardHistoryResponseDTO.CardHistoryResponseDTOBuilder CARD_HISTORY_ public static CardHistoryResponseDTO.CardHistoryResponseDTOBuilder CARD_HISTORY_RESPONSE_BUILDER_TO_DONE() { return CardHistoryResponseDTO.builder() + .isLast(true) .items(List.of(new HistoryDetail(2, "status", new Status("In Dev"), new Status("To do"), null), new HistoryDetail(3, "status", new Status(REVIEW), new Status("In Dev"), null), new HistoryDetail(4, "status", new Status(WAITING_FOR_TESTING), new Status(REVIEW), null), @@ -190,6 +206,7 @@ public static CardHistoryResponseDTO.CardHistoryResponseDTOBuilder CARD_HISTORY_ public static CardHistoryResponseDTO.CardHistoryResponseDTOBuilder CARD_HISTORY_MULTI_RESPONSE_BUILDER() { return CardHistoryResponseDTO.builder() + .isLast(true) .items(List.of(new HistoryDetail(1, "status", new Status("To do"), new Status(BLOCK), null), new HistoryDetail(2, "assignee", new Status("In Dev"), new Status("To do"), null), new HistoryDetail(3, "status", new Status(REVIEW), new Status("In Dev"), null), @@ -377,8 +394,8 @@ public static StoryPointsAndCycleTimeRequest.StoryPointsAndCycleTimeRequestBuild .project(jiraBoardSetting.getProjectKey()) .boardId(jiraBoardSetting.getBoardId()) .status(jiraBoardSetting.getDoneColumn()) - .startTime("1672556350000") - .endTime("1676908799000") + .startTime(START_TIME) + .endTime(END_TIME) .targetFields(jiraBoardSetting.getTargetFields()) .treatFlagCardAsBlock(jiraBoardSetting.getTreatFlagCardAsBlock()); } @@ -392,8 +409,8 @@ public static StoryPointsAndCycleTimeRequest.StoryPointsAndCycleTimeRequestBuild .project(jiraBoardSetting.getProjectKey()) .boardId(jiraBoardSetting.getBoardId()) .status(List.of("Done", "Testing")) - .startTime("1672556350000") - .endTime("1676908799000") + .startTime(START_TIME) + .endTime(END_TIME) .targetFields(jiraBoardSetting.getTargetFields()) .treatFlagCardAsBlock(jiraBoardSetting.getTreatFlagCardAsBlock()); } @@ -407,8 +424,8 @@ public static StoryPointsAndCycleTimeRequest.StoryPointsAndCycleTimeRequestBuild .project(jiraBoardSetting.getProjectKey()) .boardId(jiraBoardSetting.getBoardId()) .status(jiraBoardSetting.getDoneColumn()) - .startTime("1672556350000") - .endTime("1676908799000") + .startTime(START_TIME) + .endTime(END_TIME) .targetFields(jiraBoardSetting.getTargetFields()) .treatFlagCardAsBlock(jiraBoardSetting.getTreatFlagCardAsBlock()); } @@ -422,8 +439,8 @@ public static StoryPointsAndCycleTimeRequest.StoryPointsAndCycleTimeRequestBuild .project(jiraBoardSetting.getProjectKey()) .boardId(jiraBoardSetting.getBoardId()) .status(Collections.emptyList()) - .startTime("1672556350000") - .endTime("1676908799000") + .startTime(START_TIME) + .endTime(END_TIME) .targetFields(jiraBoardSetting.getTargetFields()) .treatFlagCardAsBlock(jiraBoardSetting.getTreatFlagCardAsBlock()); } @@ -458,7 +475,7 @@ public static JiraBoardSetting.JiraBoardSettingBuilder JIRA_BOARD_SETTING_WITH_H .type("jira") .projectKey("PLL") .assigneeFilter("historicalAssignee") - .users(List.of("da pei")) + .users(List.of(DISPLAY_NAME_ONE)) .targetFields(List.of(TargetField.builder().key("testKey1").name("Story Points").flag(true).build(), TargetField.builder().key("testKey2").name("Sprint").flag(true).build(), TargetField.builder().key("testKey3").name("Flagged").flag(true).build())); @@ -473,8 +490,8 @@ public static StoryPointsAndCycleTimeRequest.StoryPointsAndCycleTimeRequestBuild .project(jiraBoardSetting.getProjectKey()) .boardId(jiraBoardSetting.getBoardId()) .status(jiraBoardSetting.getDoneColumn()) - .startTime("1672556350000") - .endTime("1676908799000") + .startTime(START_TIME) + .endTime(END_TIME) .targetFields(jiraBoardSetting.getTargetFields()) .treatFlagCardAsBlock(jiraBoardSetting.getTreatFlagCardAsBlock()); } @@ -488,64 +505,106 @@ public static StoryPointsAndCycleTimeRequest.StoryPointsAndCycleTimeRequestBuild .project(jiraBoardSetting.getProjectKey()) .boardId(jiraBoardSetting.getBoardId()) .status(List.of(DONE, IN_DEV)) - .startTime("1672556350000") - .endTime("1676908799000") + .startTime(START_TIME) + .endTime(END_TIME) .targetFields(jiraBoardSetting.getTargetFields()) .treatFlagCardAsBlock(jiraBoardSetting.getTreatFlagCardAsBlock()); } - public static AllDoneCardsResponseDTO.AllDoneCardsResponseDTOBuilder ALL_DONE_CARDS_RESPONSE_FOR_ASSIGNEE_FILTER_METHOD_TEST() { + public static AllDoneCardsResponseDTO.AllDoneCardsResponseDTOBuilder ALL_DONE_CARDS_RESPONSE_FOR_ASSIGNEE_FILTER_TEST() { return AllDoneCardsResponseDTO.builder() - .total("2") + .total("3") .issues(List.of( new JiraCard("ADM-475", JiraCardField.builder() - .assignee(new Assignee("da pei")) + .assignee(new Assignee(DISPLAY_NAME_ONE)) + .status(new Status(CardStepsEnum.DONE.getValue())) + .build()), + new JiraCard("ADM-520", + JiraCardField.builder() + .assignee(null) .status(new Status(CardStepsEnum.DONE.getValue())) .build()), new JiraCard("ADM-524", JiraCardField.builder() - .assignee(new Assignee("xiao pei")) + .assignee(new Assignee(DISPLAY_NAME_TWO)) .status(new Status(CardStepsEnum.DONE.getValue())) .build()))); } - public static CardHistoryResponseDTO.CardHistoryResponseDTOBuilder CARD1_HISTORY_FOR_HISTORICAL_ASSIGNEE_FILTER_METHOD() { + public static CardHistoryResponseDTO.CardHistoryResponseDTOBuilder CARD1_HISTORY_FOR_HISTORICAL_ASSIGNEE_FILTER() { + return CardHistoryResponseDTO.builder() + .items(List.of( + new HistoryDetail(TIMESTAMP_1, "status", new Status(TESTING), new Status(REVIEW), + new HistoryDetail.Actor(DISPLAY_NAME_ONE)), + new HistoryDetail(TIMESTAMP_2, "status", new Status(DONE), new Status(TESTING), + new HistoryDetail.Actor(DISPLAY_NAME_ONE)), + new HistoryDetail(TIMESTAMP_1, "assignee", new Status("yun"), new Status(null), + new HistoryDetail.Actor(DISPLAY_NAME_ONE)), + new HistoryDetail(TIMESTAMP_3, "assignee", new Status("song"), new Status("yun"), + new HistoryDetail.Actor(DISPLAY_NAME_ONE)))); + } + + public static CardHistoryResponseDTO.CardHistoryResponseDTOBuilder CARD2_HISTORY_FOR_HISTORICAL_ASSIGNEE_FILTER() { return CardHistoryResponseDTO.builder() .items(List.of( - new HistoryDetail(1673556350000L, "status", new Status(TESTING), new Status(REVIEW), - new HistoryDetail.Actor("da pei")), - new HistoryDetail(1674556350000L, "status", new Status(DONE), new Status(TESTING), - new HistoryDetail.Actor("da pei")))); + new HistoryDetail(TIMESTAMP_1, "status", new Status(TESTING), new Status(REVIEW), + new HistoryDetail.Actor(DISPLAY_NAME_ONE)), + new HistoryDetail(TIMESTAMP_2, "status", new Status(DONE), new Status(TESTING), + new HistoryDetail.Actor(DISPLAY_NAME_ONE)), + new HistoryDetail(TIMESTAMP_1, "assignee", new Status("yun"), new Status(null), + new HistoryDetail.Actor(DISPLAY_NAME_ONE)), + new HistoryDetail(TIMESTAMP_3, "assignee", new Status("kun"), new Status("yun"), + new HistoryDetail.Actor(DISPLAY_NAME_ONE)))); } - public static CardHistoryResponseDTO.CardHistoryResponseDTOBuilder CARD2_HISTORY_FOR_HISTORICAL_ASSIGNEE_FILTER_METHOD() { + public static CardHistoryResponseDTO.CardHistoryResponseDTOBuilder CARD3_HISTORY_FOR_HISTORICAL_ASSIGNEE_FILTER() { return CardHistoryResponseDTO.builder() .items(List.of( - new HistoryDetail(1673556350000L, "status", new Status(TESTING), new Status(REVIEW), - new HistoryDetail.Actor("da pei")), - new HistoryDetail(1674556350000L, "status", new Status(DONE), new Status(TESTING), - new HistoryDetail.Actor("xiao pei")))); + new HistoryDetail(TIMESTAMP_1, "status", new Status(TESTING), new Status(REVIEW), + new HistoryDetail.Actor(DISPLAY_NAME_ONE)), + new HistoryDetail(TIMESTAMP_2, "status", new Status(DONE), new Status(TESTING), + new HistoryDetail.Actor(DISPLAY_NAME_ONE)), + new HistoryDetail(TIMESTAMP_1, "assignee", new Status("yun"), new Status(null), + new HistoryDetail.Actor(DISPLAY_NAME_ONE)), + new HistoryDetail(TIMESTAMP_3, "assignee", new Status(null), new Status("yun"), + new HistoryDetail.Actor(DISPLAY_NAME_ONE)))); + } + + public static AllDoneCardsResponseDTO.AllDoneCardsResponseDTOBuilder ALL_DONE_CARDS_RESPONSE_FOR_MULTIPLE_STATUS() { + return AllDoneCardsResponseDTO.builder() + .total("2") + .issues(List.of( + new JiraCard("ADM-475", + JiraCardField.builder() + .assignee(new Assignee(DISPLAY_NAME_ONE)) + .status(new Status(CardStepsEnum.DONE.getValue())) + .build()), + new JiraCard("ADM-524", + JiraCardField.builder() + .assignee(new Assignee(DISPLAY_NAME_TWO)) + .status(new Status(CardStepsEnum.DONE.getValue())) + .build()))); } public static CardHistoryResponseDTO.CardHistoryResponseDTOBuilder CARD1_HISTORY_FOR_MULTIPLE_STATUSES() { return CardHistoryResponseDTO.builder() - .items(List.of(new HistoryDetail(1673556350000L, "status", new Status(IN_DEV), new Status(ANALYSE), - new HistoryDetail.Actor("da pei")))); + .items(List.of(new HistoryDetail(TIMESTAMP_1, "status", new Status(IN_DEV), new Status(ANALYSE), + new HistoryDetail.Actor(DISPLAY_NAME_ONE)))); } public static CardHistoryResponseDTO.CardHistoryResponseDTOBuilder CARD2_HISTORY_FOR_MULTIPLE_STATUSES() { return CardHistoryResponseDTO.builder() - .items(List.of(new HistoryDetail(1673556350000L, "status", new Status(TESTING), new Status(ANALYSE), - new HistoryDetail.Actor("da pei")))); + .items(List.of(new HistoryDetail(TIMESTAMP_1, "status", new Status(TESTING), new Status(ANALYSE), + new HistoryDetail.Actor(DISPLAY_NAME_ONE)))); } public static CardHistoryResponseDTO.CardHistoryResponseDTOBuilder CARD_HISTORY_WITH_NO_STATUS_FIELD() { return CardHistoryResponseDTO.builder() .items(List.of(new HistoryDetail(2, "assignee", new Status("In Dev"), new Status("To do"), null), - new HistoryDetail(1682642750001L, "customfield_10021", new Status("Impediment"), new Status(FLAG), + new HistoryDetail(TIMESTAMP_1, "customfield_10021", new Status("Impediment"), new Status(FLAG), null), - new HistoryDetail(1682642750002L, "flagged", new Status("Impediment"), new Status("removeFlag"), + new HistoryDetail(TIMESTAMP_2, "flagged", new Status("Impediment"), new Status("removeFlag"), null))); } diff --git a/backend/src/test/java/heartbeat/service/jira/JiraBoardVerifyDTOFixture.java b/backend/src/test/java/heartbeat/service/jira/JiraBoardVerifyDTOFixture.java new file mode 100644 index 0000000000..83a52d85ea --- /dev/null +++ b/backend/src/test/java/heartbeat/service/jira/JiraBoardVerifyDTOFixture.java @@ -0,0 +1,29 @@ +package heartbeat.service.jira; + +import heartbeat.client.dto.board.jira.JiraBoardVerifyDTO; +import heartbeat.client.dto.board.jira.Location; + +public class JiraBoardVerifyDTOFixture { + + public static final String BOARD_ID = "1"; + + public static final String BOARD_NAME_JIRA = "jira"; + + public static final String TYPE = "adm board"; + + public static final String PROJECT_KEY = "ADM"; + + public static JiraBoardVerifyDTO.JiraBoardVerifyDTOBuilder JIRA_BOARD_VERIFY_RESPONSE_BUILDER() { + + return JiraBoardVerifyDTO.builder() + .id(BOARD_ID) + .name(BOARD_NAME_JIRA) + .type(TYPE) + .location(Location.builder().projectKey(PROJECT_KEY).build()); + } + + public static JiraBoardVerifyDTO.JiraBoardVerifyDTOBuilder JIRA_BOARD_VERIFY_FAILED_RESPONSE_BUILDER() { + return JiraBoardVerifyDTO.builder(); + } + +} diff --git a/backend/src/test/java/heartbeat/service/jira/JiraServiceTest.java b/backend/src/test/java/heartbeat/service/jira/JiraServiceTest.java index af243580f8..983c74bb7d 100644 --- a/backend/src/test/java/heartbeat/service/jira/JiraServiceTest.java +++ b/backend/src/test/java/heartbeat/service/jira/JiraServiceTest.java @@ -6,15 +6,21 @@ import heartbeat.client.component.JiraUriGenerator; import heartbeat.client.dto.board.jira.CardHistoryResponseDTO; import heartbeat.client.dto.board.jira.FieldResponseDTO; +import heartbeat.client.dto.board.jira.HistoryDetail; import heartbeat.client.dto.board.jira.JiraBoardConfigDTO; +import heartbeat.client.dto.board.jira.JiraBoardProject; +import heartbeat.client.dto.board.jira.JiraBoardVerifyDTO; +import heartbeat.client.dto.board.jira.Status; import heartbeat.client.dto.board.jira.StatusSelfDTO; import heartbeat.controller.board.dto.request.BoardRequestParam; import heartbeat.controller.board.dto.request.BoardType; +import heartbeat.controller.board.dto.request.BoardVerifyRequestParam; import heartbeat.controller.board.dto.request.StoryPointsAndCycleTimeRequest; import heartbeat.controller.board.dto.response.BoardConfigDTO; import heartbeat.controller.board.dto.response.CardCollection; import heartbeat.controller.board.dto.response.TargetField; import heartbeat.controller.report.dto.request.JiraBoardSetting; +import heartbeat.enums.AssigneeFilterMethod; import heartbeat.exception.BadRequestException; import heartbeat.exception.CustomFeignClientException; import heartbeat.exception.InternalServerErrorException; @@ -34,24 +40,29 @@ import java.io.IOException; import java.net.URI; +import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.concurrent.CompletionException; import static heartbeat.controller.board.BoardRequestFixture.BOARD_REQUEST_BUILDER; +import static heartbeat.controller.board.dto.request.BoardVerifyRequestFixture.BOARD_VERIFY_REQUEST_BUILDER; import static heartbeat.service.board.jira.JiraService.QUERY_COUNT; -import static heartbeat.service.jira.JiraBoardConfigDTOFixture.ALL_DONE_CARDS_RESPONSE_FOR_ASSIGNEE_FILTER_METHOD_TEST; +import static heartbeat.service.jira.JiraBoardConfigDTOFixture.ALL_DONE_CARDS_RESPONSE_FOR_ASSIGNEE_FILTER_TEST; +import static heartbeat.service.jira.JiraBoardConfigDTOFixture.ALL_DONE_CARDS_RESPONSE_FOR_MULTIPLE_STATUS; import static heartbeat.service.jira.JiraBoardConfigDTOFixture.ALL_DONE_CARDS_RESPONSE_FOR_STORY_POINT_BUILDER; import static heartbeat.service.jira.JiraBoardConfigDTOFixture.ALL_DONE_TWO_PAGES_CARDS_RESPONSE_BUILDER; import static heartbeat.service.jira.JiraBoardConfigDTOFixture.ALL_FIELD_RESPONSE_BUILDER; import static heartbeat.service.jira.JiraBoardConfigDTOFixture.ALL_NON_DONE_CARDS_RESPONSE_FOR_STORY_POINT_BUILDER; import static heartbeat.service.jira.JiraBoardConfigDTOFixture.BOARD_ID; -import static heartbeat.service.jira.JiraBoardConfigDTOFixture.CARD1_HISTORY_FOR_HISTORICAL_ASSIGNEE_FILTER_METHOD; +import static heartbeat.service.jira.JiraBoardConfigDTOFixture.CARD1_HISTORY_FOR_HISTORICAL_ASSIGNEE_FILTER; import static heartbeat.service.jira.JiraBoardConfigDTOFixture.CARD1_HISTORY_FOR_MULTIPLE_STATUSES; -import static heartbeat.service.jira.JiraBoardConfigDTOFixture.CARD2_HISTORY_FOR_HISTORICAL_ASSIGNEE_FILTER_METHOD; +import static heartbeat.service.jira.JiraBoardConfigDTOFixture.CARD2_HISTORY_FOR_HISTORICAL_ASSIGNEE_FILTER; import static heartbeat.service.jira.JiraBoardConfigDTOFixture.CARD2_HISTORY_FOR_MULTIPLE_STATUSES; +import static heartbeat.service.jira.JiraBoardConfigDTOFixture.CARD3_HISTORY_FOR_HISTORICAL_ASSIGNEE_FILTER; import static heartbeat.service.jira.JiraBoardConfigDTOFixture.CARD_HISTORY_DONE_TIME_GREATER_THAN_END_TIME_BUILDER; import static heartbeat.service.jira.JiraBoardConfigDTOFixture.CARD_HISTORY_MULTI_RESPONSE_BUILDER; import static heartbeat.service.jira.JiraBoardConfigDTOFixture.CARD_HISTORY_RESPONSE_BUILDER; @@ -82,13 +93,18 @@ import static heartbeat.service.jira.JiraBoardConfigDTOFixture.STORY_POINTS_FORM_ALL_DONE_CARD_WITH_EMPTY_STATUS; import static heartbeat.service.jira.JiraBoardConfigDTOFixture.STORY_POINTS_REQUEST_WITH_ASSIGNEE_FILTER_METHOD; import static heartbeat.service.jira.JiraBoardConfigDTOFixture.STORY_POINTS_REQUEST_WITH_MULTIPLE_REAL_DONE_STATUSES; +import static heartbeat.service.jira.JiraBoardVerifyDTOFixture.JIRA_BOARD_VERIFY_FAILED_RESPONSE_BUILDER; +import static heartbeat.service.jira.JiraBoardVerifyDTOFixture.JIRA_BOARD_VERIFY_RESPONSE_BUILDER; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.catchThrowable; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) @@ -167,7 +183,7 @@ void shouldCallJiraFeignClientAndReturnBoardConfigResponseWhenGetJiraBoardConfig when(jiraFeignClient.getColumnStatusCategory(baseUrl, COLUM_SELF_ID_1, token)).thenReturn(doneStatusSelf); when(jiraFeignClient.getColumnStatusCategory(baseUrl, COLUM_SELF_ID_2, token)).thenReturn(doingStatusSelf); when(jiraFeignClient.getJiraCards(baseUrl, BOARD_ID, QUERY_COUNT, 0, jql, token)).thenReturn(allDoneCards); - when(jiraFeignClient.getJiraCardHistory(any(), any(), any())) + when(jiraFeignClient.getJiraCardHistoryByCount(any(), any(), anyInt(), anyInt(), any())) .thenReturn(CARD_HISTORY_RESPONSE_BUILDER().build()); when(jiraFeignClient.getTargetField(baseUrl, "project key", token)) .thenReturn(FIELD_RESPONSE_BUILDER().build()); @@ -183,6 +199,66 @@ void shouldCallJiraFeignClientAndReturnBoardConfigResponseWhenGetJiraBoardConfig assertThat(boardConfigDTO.getTargetFields()).isEqualTo(expectTargetField); } + @Test + void shouldCallJiraFeignClientAndReturnBoardInfoResponseWhenGetJiraBoardInfo() throws JsonProcessingException { + JiraBoardConfigDTO jiraBoardConfigDTO = JIRA_BOARD_CONFIG_RESPONSE_BUILDER().build(); + StatusSelfDTO doneStatusSelf = DONE_STATUS_SELF_RESPONSE_BUILDER().build(); + StatusSelfDTO doingStatusSelf = DOING_STATUS_SELF_RESPONSE_BUILDER().build(); + URI baseUrl = URI.create(SITE_ATLASSIAN_NET); + String token = "token"; + BoardRequestParam boardRequestParam = BOARD_REQUEST_BUILDER().build(); + String jql = String.format(ALL_CARDS_JQL, boardRequestParam.getStartTime(), boardRequestParam.getEndTime()); + List expectTargetField = List.of( + new TargetField("customfield_10016", "Story point estimate", false), + new TargetField("customfield_10020", "Sprint", false), + new TargetField("customfield_10021", "Flagged", false)); + + String allDoneCards = objectMapper.writeValueAsString(ALL_DONE_CARDS_RESPONSE_FOR_STORY_POINT_BUILDER().build()) + .replaceAll("sprint", "customfield_10020") + .replaceAll("partner", "customfield_10037") + .replaceAll("flagged", "customfield_10021") + .replaceAll("development", "customfield_10000"); + + doReturn(jiraBoardConfigDTO).when(jiraFeignClient).getJiraBoardConfiguration(baseUrl, BOARD_ID, token); + when(urlGenerator.getUri(any())).thenReturn(URI.create(SITE_ATLASSIAN_NET)); + when(jiraFeignClient.getProject(baseUrl, "project key", token)) + .thenReturn(JiraBoardProject.builder().style("next-gen").build()); + when(jiraFeignClient.getColumnStatusCategory(baseUrl, COLUM_SELF_ID_1, token)).thenReturn(doneStatusSelf); + when(jiraFeignClient.getColumnStatusCategory(baseUrl, COLUM_SELF_ID_2, token)).thenReturn(doingStatusSelf); + when(jiraFeignClient.getJiraCards(baseUrl, BOARD_ID, QUERY_COUNT, 0, jql, token)).thenReturn(allDoneCards); + when(jiraFeignClient.getJiraCardHistoryByCount(any(), any(), anyInt(), anyInt(), any())) + .thenReturn(CARD_HISTORY_RESPONSE_BUILDER().build()); + when(jiraFeignClient.getTargetField(baseUrl, "project key", token)) + .thenReturn(FIELD_RESPONSE_BUILDER().build()); + BoardConfigDTO boardConfigDTO = jiraService.getInfo(boardTypeJira, boardRequestParam); + jiraService.shutdownExecutor(); + + assertThat(boardConfigDTO.getJiraColumnResponse()).hasSize(1); + assertThat(boardConfigDTO.getJiraColumnResponse().get(0).getValue().getName()).isEqualTo("TODO"); + assertThat(boardConfigDTO.getJiraColumnResponse().get(0).getValue().getStatuses().get(0)).isEqualTo("DONE"); + assertThat(boardConfigDTO.getJiraColumnResponse().get(0).getValue().getStatuses().get(1)).isEqualTo("DOING"); + assertThat(boardConfigDTO.getJiraColumnResponse().get(0).getKey()).isEqualTo("done"); + assertThat(boardConfigDTO.getUsers()).hasSize(1); + assertThat(boardConfigDTO.getTargetFields()).isEqualTo(expectTargetField); + } + + @Test + void shouldCallJiraFeignClientAndReturnBoardVerifyResponseWhenVerifyJiraBoard() { + JiraBoardVerifyDTO jiraBoardVerifyDTO = JIRA_BOARD_VERIFY_RESPONSE_BUILDER().build(); + + BoardVerifyRequestParam boardVerifyRequestParam = BOARD_VERIFY_REQUEST_BUILDER().build(); + URI baseUrl = URI.create(SITE_ATLASSIAN_NET); + + doReturn(jiraBoardVerifyDTO).when(jiraFeignClient) + .getBoard(baseUrl, boardVerifyRequestParam.getBoardId(), boardVerifyRequestParam.getToken()); + when(urlGenerator.getUri(any())).thenReturn(URI.create(SITE_ATLASSIAN_NET)); + + String projectKey = jiraService.verify(BoardType.JIRA, boardVerifyRequestParam); + jiraService.shutdownExecutor(); + + assertThat(Objects.requireNonNull(projectKey)).isEqualTo("ADM"); + } + @Test void shouldCallJiraFeignClientAndReturnBoardConfigResponseWhenGetJiraBoardConfigHasTwoPage() throws IOException { JiraBoardConfigDTO jiraBoardConfigDTO = JIRA_BOARD_CONFIG_RESPONSE_BUILDER().build(); @@ -205,7 +281,7 @@ void shouldCallJiraFeignClientAndReturnBoardConfigResponseWhenGetJiraBoardConfig when(jiraFeignClient.getColumnStatusCategory(baseUrl, COLUM_SELF_ID_2, token)).thenReturn(doingStatusSelf); when(jiraFeignClient.getJiraCards(baseUrl, BOARD_ID, QUERY_COUNT, 0, jql, token)).thenReturn(allDoneCards); when(jiraFeignClient.getJiraCards(baseUrl, BOARD_ID, QUERY_COUNT, 100, jql, token)).thenReturn(allDoneCards); - when(jiraFeignClient.getJiraCardHistory(baseUrl, "1", token)) + when(jiraFeignClient.getJiraCardHistoryByCount(baseUrl, "1", 0, 100, token)) .thenReturn(CARD_HISTORY_RESPONSE_BUILDER().build()); when(jiraFeignClient.getTargetField(baseUrl, "project key", token)) .thenReturn(FIELD_RESPONSE_BUILDER().build()); @@ -222,6 +298,47 @@ void shouldCallJiraFeignClientAndReturnBoardConfigResponseWhenGetJiraBoardConfig assertThat(boardConfigDTO.getTargetFields()).isEqualTo(expectTargetField); } + @Test + void shouldCallJiraFeignClientAndReturnBoardInfoResponseWhenGetJiraBoardInfoHasTwoPage() throws IOException { + JiraBoardConfigDTO jiraBoardConfigDTO = JIRA_BOARD_CONFIG_RESPONSE_BUILDER().build(); + StatusSelfDTO doneStatusSelf = DONE_STATUS_SELF_RESPONSE_BUILDER().build(); + StatusSelfDTO doingStatusSelf = DOING_STATUS_SELF_RESPONSE_BUILDER().build(); + URI baseUrl = URI.create(SITE_ATLASSIAN_NET); + String token = "token"; + BoardRequestParam boardRequestParam = BOARD_REQUEST_BUILDER().build(); + String jql = String.format(ALL_CARDS_JQL, boardRequestParam.getStartTime(), boardRequestParam.getEndTime()); + List expectTargetField = List.of( + new TargetField("customfield_10016", "Story point estimate", false), + new TargetField("customfield_10020", "Sprint", false), + new TargetField("customfield_10021", "Flagged", false)); + String allDoneCards = objectMapper.writeValueAsString(ALL_DONE_TWO_PAGES_CARDS_RESPONSE_BUILDER().build()) + .replaceAll("storyPoints", "customfield_10016"); + + doReturn(jiraBoardConfigDTO).when(jiraFeignClient).getJiraBoardConfiguration(baseUrl, BOARD_ID, token); + when(urlGenerator.getUri(any())).thenReturn(URI.create(SITE_ATLASSIAN_NET)); + when(jiraFeignClient.getProject(baseUrl, "project key", token)) + .thenReturn(JiraBoardProject.builder().style("next-gen").build()); + when(jiraFeignClient.getColumnStatusCategory(baseUrl, COLUM_SELF_ID_1, token)).thenReturn(doneStatusSelf); + when(jiraFeignClient.getColumnStatusCategory(baseUrl, COLUM_SELF_ID_2, token)).thenReturn(doingStatusSelf); + when(jiraFeignClient.getJiraCards(baseUrl, BOARD_ID, QUERY_COUNT, 0, jql, token)).thenReturn(allDoneCards); + when(jiraFeignClient.getJiraCards(baseUrl, BOARD_ID, QUERY_COUNT, 100, jql, token)).thenReturn(allDoneCards); + when(jiraFeignClient.getJiraCardHistoryByCount(baseUrl, "1", 0, 100, token)) + .thenReturn(CARD_HISTORY_RESPONSE_BUILDER().build()); + when(jiraFeignClient.getTargetField(baseUrl, "project key", token)) + .thenReturn(FIELD_RESPONSE_BUILDER().build()); + + BoardConfigDTO boardConfigDTO = jiraService.getInfo(boardTypeJira, boardRequestParam); + + assertThat(boardConfigDTO.getJiraColumnResponse()).hasSize(1); + assertThat(boardConfigDTO.getJiraColumnResponse().get(0).getValue().getName()).isEqualTo("TODO"); + assertThat(boardConfigDTO.getJiraColumnResponse().get(0).getValue().getStatuses().get(0)).isEqualTo("DONE"); + assertThat(boardConfigDTO.getJiraColumnResponse().get(0).getValue().getStatuses().get(1)).isEqualTo("DOING"); + assertThat(boardConfigDTO.getJiraColumnResponse().get(0).getKey()).isEqualTo("done"); + assertThat(boardConfigDTO.getUsers()).hasSize(1); + assertThat(boardConfigDTO.getTargetFields()).hasSize(3); + assertThat(boardConfigDTO.getTargetFields()).isEqualTo(expectTargetField); + } + @Test void shouldCallJiraFeignClientAndReturnBoardConfigResponseWhenGetClassicJiraBoardConfig() throws JsonProcessingException { @@ -246,7 +363,7 @@ void shouldCallJiraFeignClientAndReturnBoardConfigResponseWhenGetClassicJiraBoar when(jiraFeignClient.getColumnStatusCategory(baseUrl, COLUM_SELF_ID_3, token)).thenReturn(completeStatusSelf); when(jiraFeignClient.getColumnStatusCategory(baseUrl, COLUM_SELF_ID_2, token)).thenReturn(doingStatusSelf); when(jiraFeignClient.getJiraCards(baseUrl, BOARD_ID, QUERY_COUNT, 0, jql, token)).thenReturn(allDoneCards); - when(jiraFeignClient.getJiraCardHistory(any(), any(), any())) + when(jiraFeignClient.getJiraCardHistoryByCount(any(), any(), anyInt(), anyInt(), any())) .thenReturn(CARD_HISTORY_RESPONSE_BUILDER().build()); when(jiraFeignClient.getTargetField(baseUrl, "project key", token)) .thenReturn(FIELD_RESPONSE_BUILDER().build()); @@ -263,6 +380,49 @@ void shouldCallJiraFeignClientAndReturnBoardConfigResponseWhenGetClassicJiraBoar assertThat(boardConfigDTO.getTargetFields()).isEqualTo(expectTargetField); } + @Test + void shouldCallJiraFeignClientAndReturnBoardInfoResponseWhenGetClassicJiraBoardInfo() + throws JsonProcessingException { + JiraBoardConfigDTO jiraBoardConfigDTO = CLASSIC_JIRA_BOARD_CONFIG_RESPONSE_BUILDER().build(); + StatusSelfDTO doneStatusSelf = DONE_STATUS_SELF_RESPONSE_BUILDER().build(); + StatusSelfDTO completeStatusSelf = COMPLETE_STATUS_SELF_RESPONSE_BUILDER().build(); + StatusSelfDTO doingStatusSelf = DOING_STATUS_SELF_RESPONSE_BUILDER().build(); + URI baseUrl = URI.create(SITE_ATLASSIAN_NET); + String token = "token"; + BoardRequestParam boardRequestParam = BOARD_REQUEST_BUILDER().build(); + String jql = String.format(ALL_CARDS_JQL, boardRequestParam.getStartTime(), boardRequestParam.getEndTime()); + List expectTargetField = List.of( + new TargetField("customfield_10016", "Story point estimate", false), + new TargetField("customfield_10020", "Sprint", false), + new TargetField("customfield_10021", "Flagged", false)); + String allDoneCards = objectMapper.writeValueAsString(ALL_DONE_CARDS_RESPONSE_FOR_STORY_POINT_BUILDER().build()) + .replaceAll("storyPoints", "customfield_10016"); + + doReturn(jiraBoardConfigDTO).when(jiraFeignClient).getJiraBoardConfiguration(baseUrl, BOARD_ID, token); + when(urlGenerator.getUri(any())).thenReturn(URI.create(SITE_ATLASSIAN_NET)); + when(jiraFeignClient.getProject(baseUrl, "project key", token)) + .thenReturn(JiraBoardProject.builder().style("classic").build()); + when(jiraFeignClient.getColumnStatusCategory(baseUrl, COLUM_SELF_ID_1, token)).thenReturn(doneStatusSelf); + when(jiraFeignClient.getColumnStatusCategory(baseUrl, COLUM_SELF_ID_3, token)).thenReturn(completeStatusSelf); + when(jiraFeignClient.getColumnStatusCategory(baseUrl, COLUM_SELF_ID_2, token)).thenReturn(doingStatusSelf); + when(jiraFeignClient.getJiraCards(baseUrl, BOARD_ID, QUERY_COUNT, 0, jql, token)).thenReturn(allDoneCards); + when(jiraFeignClient.getJiraCardHistoryByCount(any(), any(), anyInt(), anyInt(), any())) + .thenReturn(CARD_HISTORY_RESPONSE_BUILDER().build()); + when(jiraFeignClient.getTargetField(baseUrl, "project key", token)) + .thenReturn(FIELD_RESPONSE_BUILDER().build()); + + BoardConfigDTO boardConfigDTO = jiraService.getInfo(boardTypeJira, boardRequestParam); + + assertThat(boardConfigDTO.getJiraColumnResponse()).hasSize(1); + assertThat(boardConfigDTO.getJiraColumnResponse().get(0).getValue().getName()).isEqualTo("TODO"); + assertThat(boardConfigDTO.getJiraColumnResponse().get(0).getValue().getStatuses().get(0)).isEqualTo("DONE"); + assertThat(boardConfigDTO.getJiraColumnResponse().get(0).getValue().getStatuses().get(1)).isEqualTo("DOING"); + assertThat(boardConfigDTO.getJiraColumnResponse().get(0).getKey()).isEqualTo("done"); + assertThat(boardConfigDTO.getUsers()).hasSize(1); + assertThat(boardConfigDTO.getTargetFields()).hasSize(3); + assertThat(boardConfigDTO.getTargetFields()).isEqualTo(expectTargetField); + } + @Test void shouldCallJiraFeignClientAndThrowParamExceptionWhenGetJiraBoardConfig() { JiraBoardConfigDTO jiraBoardConfigDTO = JIRA_BOARD_CONFIG_RESPONSE_BUILDER().build(); @@ -283,6 +443,38 @@ void shouldCallJiraFeignClientAndThrowParamExceptionWhenGetJiraBoardConfig() { .hasMessageContaining("boardType param is not correct"); } + @Test + void shouldCallJiraFeignClientAndThrowParamExceptionWhenGetJiraBoardInfo() { + BoardRequestParam boardRequestParam = BOARD_REQUEST_BUILDER().build(); + + assertThatThrownBy(() -> jiraService.getInfo(null, boardRequestParam)).isInstanceOf(BadRequestException.class) + .hasMessageContaining("boardType param is not correct"); + } + + @Test + void shouldThrowExceptionWhenVerifyJiraBoardTypeNotCorrect() { + BoardVerifyRequestParam boardVerifyRequestParam = BOARD_VERIFY_REQUEST_BUILDER().build(); + + assertThatThrownBy(() -> jiraService.verify(BoardType.CLASSIC_JIRA, boardVerifyRequestParam)) + .isInstanceOf(BadRequestException.class) + .hasMessageContaining("boardType param is not correct"); + } + + @Test + void shouldCallJiraFeignClientAndThrowNonColumnWhenVerifyJiraBoard() { + JiraBoardVerifyDTO jiraBoardVerifyDTO = JIRA_BOARD_VERIFY_FAILED_RESPONSE_BUILDER().build(); + URI baseUrl = URI.create(SITE_ATLASSIAN_NET); + String token = "token"; + BoardVerifyRequestParam boardVerifyRequestParam = BOARD_VERIFY_REQUEST_BUILDER().build(); + + when(urlGenerator.getUri(any())).thenReturn(URI.create(SITE_ATLASSIAN_NET)); + doReturn(jiraBoardVerifyDTO).when(jiraFeignClient).getBoard(baseUrl, BOARD_ID, token); + + Throwable thrown = catchThrowable(() -> jiraService.verify(boardTypeJira, boardVerifyRequestParam)); + assertThat(thrown).isInstanceOf(InternalServerErrorException.class) + .hasMessageContaining("Failed when call Jira to verify board, cause is"); + } + @Test void shouldCallJiraFeignClientAndThrowNotFoundExceptionWhenGetJiraBoardConfig() throws JsonProcessingException { JiraBoardConfigDTO jiraBoardConfigDTO = JIRA_BOARD_CONFIG_RESPONSE_BUILDER().build(); @@ -301,8 +493,8 @@ void shouldCallJiraFeignClientAndThrowNotFoundExceptionWhenGetJiraBoardConfig() when(jiraFeignClient.getColumnStatusCategory(baseUrl, COLUM_SELF_ID_2, token)) .thenThrow(new NotFoundException("message")); when(jiraFeignClient.getJiraCards(baseUrl, BOARD_ID, QUERY_COUNT, 0, jql, token)).thenReturn(allDoneCards); - when(jiraFeignClient.getJiraCardHistory(any(), any(), any())) - .thenReturn(new CardHistoryResponseDTO(Collections.emptyList())); + when(jiraFeignClient.getJiraCardHistoryByCount(any(), any(), anyInt(), anyInt(), any())) + .thenReturn(new CardHistoryResponseDTO(true, Collections.emptyList())); when(jiraFeignClient.getTargetField(baseUrl, "project key", token)) .thenReturn(FIELD_RESPONSE_BUILDER().build()); @@ -310,6 +502,35 @@ void shouldCallJiraFeignClientAndThrowNotFoundExceptionWhenGetJiraBoardConfig() .doesNotThrowAnyException(); } + @Test + void shouldCallJiraFeignClientTwiceGivenTwoPageHistoryDataWhenGetJiraBoardConfig() throws JsonProcessingException { + // given + URI baseUrl = URI.create(SITE_ATLASSIAN_NET); + String token = "token"; + + JiraBoardSetting jiraBoardSetting = CLASSIC_JIRA_BOARD_SETTING_BUILD().build(); + StoryPointsAndCycleTimeRequest storyPointsAndCycleTimeRequest = CLASSIC_JIRA_STORY_POINTS_FORM_ALL_DONE_CARD() + .build(); + String allDoneCards = objectMapper.writeValueAsString(NEED_FILTERED_ALL_DONE_CARDS_BUILDER().build()); + + when(urlGenerator.getUri(any())).thenReturn(baseUrl); + when(jiraFeignClient.getJiraCards(any(), any(), anyInt(), anyInt(), any(), any())).thenReturn(allDoneCards); + when(jiraFeignClient.getJiraCardHistoryByCount(baseUrl, "1", 0, 100, token)) + .thenReturn(CARD_HISTORY_MULTI_RESPONSE_BUILDER().build()); + when(jiraFeignClient.getJiraCardHistoryByCount(baseUrl, "2", 0, 100, token)) + .thenReturn(new CardHistoryResponseDTO(false, new ArrayList<>( + List.of(new HistoryDetail(1, "status", new Status("To do"), new Status("Block"), null))))); + when(jiraFeignClient.getJiraCardHistoryByCount(baseUrl, "2", 100, 100, token)) + .thenReturn(new CardHistoryResponseDTO(true, new ArrayList<>( + List.of(new HistoryDetail(2, "assignee", new Status("In Dev"), new Status("To do"), null))))); + when(jiraFeignClient.getTargetField(baseUrl, "PLL", token)).thenReturn(ALL_FIELD_RESPONSE_BUILDER().build()); + // when + jiraService.getStoryPointsAndCycleTimeForDoneCards(storyPointsAndCycleTimeRequest, + jiraBoardSetting.getBoardColumns(), List.of("Zhang San"), ""); + // then + verify(jiraFeignClient, times(2)).getJiraCardHistoryByCount(any(), eq("2"), anyInt(), anyInt(), any()); + } + @Test void shouldCallJiraFeignClientAndThrowNonContentCodeWhenGetJiraBoardConfig() throws JsonProcessingException { JiraBoardConfigDTO jiraBoardConfigDTO = JIRA_BOARD_CONFIG_RESPONSE_BUILDER().build(); @@ -334,6 +555,32 @@ void shouldCallJiraFeignClientAndThrowNonContentCodeWhenGetJiraBoardConfig() thr .hasMessageContaining("There is no cards."); } + @Test + void shouldCallJiraFeignClientAndThrowNonContentCodeWhenGetJiraBoardInfo() throws JsonProcessingException { + JiraBoardConfigDTO jiraBoardConfigDTO = JIRA_BOARD_CONFIG_RESPONSE_BUILDER().build(); + StatusSelfDTO doneStatusSelf = DONE_STATUS_SELF_RESPONSE_BUILDER().build(); + StatusSelfDTO doingStatusSelf = DOING_STATUS_SELF_RESPONSE_BUILDER().build(); + URI baseUrl = URI.create(SITE_ATLASSIAN_NET); + String token = "token"; + BoardRequestParam boardRequestParam = BOARD_REQUEST_BUILDER().build(); + String jql = String.format(ALL_CARDS_JQL, boardRequestParam.getStartTime(), boardRequestParam.getEndTime()); + + when(urlGenerator.getUri(any())).thenReturn(URI.create(SITE_ATLASSIAN_NET)); + doReturn(jiraBoardConfigDTO).when(jiraFeignClient).getJiraBoardConfiguration(baseUrl, BOARD_ID, token); + when(jiraFeignClient.getProject(baseUrl, "project key", token)) + .thenReturn(JiraBoardProject.builder().style("next-gen").build()); + when(jiraFeignClient.getColumnStatusCategory(baseUrl, COLUM_SELF_ID_1, token)).thenReturn(doneStatusSelf); + when(jiraFeignClient.getColumnStatusCategory(baseUrl, COLUM_SELF_ID_2, token)).thenReturn(doingStatusSelf); + when(jiraFeignClient.getJiraCards(baseUrl, BOARD_ID, QUERY_COUNT, 0, jql, token)) + .thenReturn(objectMapper.writeValueAsString(ONE_PAGE_NO_DONE_CARDS_RESPONSE_BUILDER().build())); + when(jiraFeignClient.getTargetField(baseUrl, "project key", token)) + .thenReturn(FIELD_RESPONSE_BUILDER().build()); + + assertThatThrownBy(() -> jiraService.getInfo(boardTypeJira, boardRequestParam)) + .isInstanceOf(NoContentException.class) + .hasMessageContaining("There is no cards."); + } + @Test void shouldCallJiraFeignClientAndThrowNonColumnWhenGetJiraBoardConfig() { JiraBoardConfigDTO jiraBoardConfigDTO = JIRA_BOARD_CONFIG_RESPONSE_BUILDER().build(); @@ -356,6 +603,30 @@ void shouldCallJiraFeignClientAndThrowNonColumnWhenGetJiraBoardConfig() { .hasMessageContaining("Failed when call Jira to get board config, cause is"); } + @Test + void shouldCallJiraFeignClientAndThrowNonColumnWhenGetJiraBoardInfo() { + JiraBoardConfigDTO jiraBoardConfigDTO = JIRA_BOARD_CONFIG_RESPONSE_BUILDER().build(); + StatusSelfDTO noneStatusSelf = NONE_STATUS_SELF_RESPONSE_BUILDER().build(); + StatusSelfDTO doingStatusSelf = DOING_STATUS_SELF_RESPONSE_BUILDER().build(); + URI baseUrl = URI.create(SITE_ATLASSIAN_NET); + String token = "token"; + BoardRequestParam boardRequestParam = BOARD_REQUEST_BUILDER().build(); + when(urlGenerator.getUri(any())).thenReturn(URI.create(SITE_ATLASSIAN_NET)); + doReturn(jiraBoardConfigDTO).when(jiraFeignClient).getJiraBoardConfiguration(baseUrl, BOARD_ID, token); + when(jiraFeignClient.getProject(baseUrl, "project key", token)) + .thenReturn(JiraBoardProject.builder().style("next-gen").build()); + when(jiraFeignClient.getColumnStatusCategory(baseUrl, COLUM_SELF_ID_1, token)).thenReturn(noneStatusSelf); + when(jiraFeignClient.getColumnStatusCategory(baseUrl, COLUM_SELF_ID_2, token)).thenReturn(doingStatusSelf); + when(jiraFeignClient.getTargetField(baseUrl, "project key", token)) + .thenReturn(FIELD_RESPONSE_BUILDER().build()); + + Throwable thrown = catchThrowable(() -> { + jiraService.getInfo(boardTypeJira, boardRequestParam); + }); + assertThat(thrown).isInstanceOf(InternalServerErrorException.class) + .hasMessageContaining("Failed when call Jira to get board config, cause is"); + } + @Test void shouldGetCardsWhenCallGetStoryPointsAndCycleTimeGiveStoryPointKey() throws JsonProcessingException { URI baseUrl = URI.create(SITE_ATLASSIAN_NET); @@ -373,9 +644,9 @@ void shouldGetCardsWhenCallGetStoryPointsAndCycleTimeGiveStoryPointKey() throws when(urlGenerator.getUri(any())).thenReturn(baseUrl); when(jiraFeignClient.getJiraCards(baseUrl, BOARD_ID, QUERY_COUNT, 0, jql, boardRequestParam.getToken())) .thenReturn(allDoneCards); - when(jiraFeignClient.getJiraCardHistory(baseUrl, "1", token)) + when(jiraFeignClient.getJiraCardHistoryByCount(baseUrl, "1", 0, 100, token)) .thenReturn(CARD_HISTORY_MULTI_RESPONSE_BUILDER().build()); - when(jiraFeignClient.getJiraCardHistory(baseUrl, "2", token)) + when(jiraFeignClient.getJiraCardHistoryByCount(baseUrl, "2", 0, 100, token)) .thenReturn(CARD_HISTORY_RESPONSE_BUILDER().build()); when(jiraFeignClient.getTargetField(baseUrl, "PLL", token)).thenReturn(ALL_FIELD_RESPONSE_BUILDER().build()); when(systemUtil.getEnvMap()).thenReturn(envMap); @@ -403,6 +674,23 @@ void shouldThrowExceptionWhenGetJiraConfigurationThrowsUnExpectedException() { .hasMessageContaining("UnExpected Exception"); } + @Test + void shouldThrowExceptionWhenGetJiraInfoThrowsUnExpectedException() { + URI baseUrl = URI.create(SITE_ATLASSIAN_NET); + String token = "token"; + BoardRequestParam boardRequestParam = BOARD_REQUEST_BUILDER().build(); + when(jiraFeignClient.getJiraBoardConfiguration(any(URI.class), any(), any())) + .thenThrow(new CompletionException(new Exception("UnExpected Exception"))); + when(urlGenerator.getUri(any())).thenReturn(URI.create(SITE_ATLASSIAN_NET)); + when(jiraFeignClient.getProject(baseUrl, "project key", token)) + .thenReturn(JiraBoardProject.builder().style("next-gen").build()); + when(jiraFeignClient.getTargetField(baseUrl, "project key", "token")) + .thenReturn(FIELD_RESPONSE_BUILDER().build()); + assertThatThrownBy(() -> jiraService.getInfo(boardTypeJira, boardRequestParam)) + .isInstanceOf(InternalServerErrorException.class) + .hasMessageContaining("UnExpected Exception"); + } + @Test void shouldReturnAssigneeNameFromDoneCardWhenGetAssigneeSet() throws JsonProcessingException { JiraBoardConfigDTO jiraBoardConfigDTO = JIRA_BOARD_CONFIG_RESPONSE_BUILDER().build(); @@ -420,8 +708,8 @@ void shouldReturnAssigneeNameFromDoneCardWhenGetAssigneeSet() throws JsonProcess when(jiraFeignClient.getColumnStatusCategory(baseUrl, COLUM_SELF_ID_1, token)).thenReturn(doneStatusSelf); when(jiraFeignClient.getColumnStatusCategory(baseUrl, COLUM_SELF_ID_2, token)).thenReturn(doingStatusSelf); when(jiraFeignClient.getJiraCards(baseUrl, BOARD_ID, QUERY_COUNT, 0, jql, token)).thenReturn(allDoneCards); - when(jiraFeignClient.getJiraCardHistory(any(), any(), any())) - .thenReturn(new CardHistoryResponseDTO(Collections.emptyList())); + when(jiraFeignClient.getJiraCardHistoryByCount(any(), any(), anyInt(), anyInt(), any())) + .thenReturn(new CardHistoryResponseDTO(true, Collections.emptyList())); when(jiraFeignClient.getTargetField(baseUrl, "project key", token)) .thenReturn(FIELD_RESPONSE_BUILDER().build()); @@ -431,6 +719,36 @@ void shouldReturnAssigneeNameFromDoneCardWhenGetAssigneeSet() throws JsonProcess assertThat(boardConfigDTO.getUsers().get(0)).isEqualTo("Zhang San"); } + @Test + void shouldReturnAssigneeNameFromDoneCardWhenGetBoardInfoAndGetAssigneeSet() throws JsonProcessingException { + JiraBoardConfigDTO jiraBoardConfigDTO = JIRA_BOARD_CONFIG_RESPONSE_BUILDER().build(); + StatusSelfDTO doneStatusSelf = DONE_STATUS_SELF_RESPONSE_BUILDER().build(); + StatusSelfDTO doingStatusSelf = DOING_STATUS_SELF_RESPONSE_BUILDER().build(); + URI baseUrl = URI.create(SITE_ATLASSIAN_NET); + String token = "token"; + BoardRequestParam boardRequestParam = BOARD_REQUEST_BUILDER().build(); + String jql = String.format(ALL_CARDS_JQL, boardRequestParam.getStartTime(), boardRequestParam.getEndTime()); + String allDoneCards = objectMapper.writeValueAsString(ALL_DONE_CARDS_RESPONSE_FOR_STORY_POINT_BUILDER().build()) + .replaceAll("storyPoints", "customfield_10016"); + + doReturn(jiraBoardConfigDTO).when(jiraFeignClient).getJiraBoardConfiguration(baseUrl, BOARD_ID, token); + when(urlGenerator.getUri(any())).thenReturn(URI.create(SITE_ATLASSIAN_NET)); + when(jiraFeignClient.getProject(baseUrl, "project key", token)) + .thenReturn(JiraBoardProject.builder().style("next-gen").build()); + when(jiraFeignClient.getColumnStatusCategory(baseUrl, COLUM_SELF_ID_1, token)).thenReturn(doneStatusSelf); + when(jiraFeignClient.getColumnStatusCategory(baseUrl, COLUM_SELF_ID_2, token)).thenReturn(doingStatusSelf); + when(jiraFeignClient.getJiraCards(baseUrl, BOARD_ID, QUERY_COUNT, 0, jql, token)).thenReturn(allDoneCards); + when(jiraFeignClient.getJiraCardHistoryByCount(any(), any(), anyInt(), anyInt(), any())) + .thenReturn(new CardHistoryResponseDTO(true, Collections.emptyList())); + when(jiraFeignClient.getTargetField(baseUrl, "project key", token)) + .thenReturn(FIELD_RESPONSE_BUILDER().build()); + + BoardConfigDTO boardConfigDTO = jiraService.getInfo(boardTypeJira, boardRequestParam); + + assertThat(boardConfigDTO.getUsers()).hasSize(1); + assertThat(boardConfigDTO.getUsers().get(0)).isEqualTo("Zhang San"); + } + @Test void shouldThrowExceptionWhenGetTargetFieldFailed() { URI baseUrl = URI.create(SITE_ATLASSIAN_NET); @@ -446,6 +764,23 @@ void shouldThrowExceptionWhenGetTargetFieldFailed() { .hasMessageContaining("exception"); } + @Test + void shouldThrowExceptionWhenGetBoardInfoAndGetTargetFieldFailed() { + URI baseUrl = URI.create(SITE_ATLASSIAN_NET); + String token = "token"; + BoardRequestParam boardRequestParam = BOARD_REQUEST_BUILDER().build(); + + when(urlGenerator.getUri(any())).thenReturn(URI.create(SITE_ATLASSIAN_NET)); + when(jiraFeignClient.getProject(baseUrl, "project key", token)) + .thenReturn(JiraBoardProject.builder().style("next-gen").build()); + when(jiraFeignClient.getTargetField(baseUrl, boardRequestParam.getProjectKey(), token)) + .thenThrow(new CustomFeignClientException(500, "exception")); + + assertThatThrownBy(() -> jiraService.getInfo(boardTypeJira, BOARD_REQUEST_BUILDER().build())) + .isInstanceOf(Exception.class) + .hasMessageContaining("exception"); + } + @Test void shouldThrowExceptionWhenGetTargetFieldReturnNull() { URI baseUrl = URI.create(SITE_ATLASSIAN_NET); @@ -460,6 +795,22 @@ void shouldThrowExceptionWhenGetTargetFieldReturnNull() { .hasMessageContaining("There is no enough permission."); } + @Test + void shouldThrowExceptionWhenGetBoardInfoAndGetTargetFieldReturnNull() { + URI baseUrl = URI.create(SITE_ATLASSIAN_NET); + String token = "token"; + BoardRequestParam boardRequestParam = BOARD_REQUEST_BUILDER().build(); + + when(urlGenerator.getUri(any())).thenReturn(URI.create(SITE_ATLASSIAN_NET)); + when(jiraFeignClient.getProject(baseUrl, "project key", token)) + .thenReturn(JiraBoardProject.builder().style("next-gen").build()); + when(jiraFeignClient.getTargetField(baseUrl, boardRequestParam.getProjectKey(), token)).thenReturn(null); + + assertThatThrownBy(() -> jiraService.getInfo(boardTypeJira, BOARD_REQUEST_BUILDER().build())) + .isInstanceOf(PermissionDenyException.class) + .hasMessageContaining("There is no enough permission."); + } + @Test void shouldThrowExceptionWhenGetTargetFieldReturnEmpty() { URI baseUrl = URI.create(SITE_ATLASSIAN_NET); @@ -478,6 +829,26 @@ void shouldThrowExceptionWhenGetTargetFieldReturnEmpty() { .hasMessageContaining("There is no enough permission."); } + @Test + void shouldThrowExceptionWhenGetBoardInfoAndGetTargetFieldReturnEmpty() { + URI baseUrl = URI.create(SITE_ATLASSIAN_NET); + String token = "token"; + BoardRequestParam boardRequestParam = BOARD_REQUEST_BUILDER().build(); + FieldResponseDTO emptyProjectFieldResponse = FieldResponseDTO.builder() + .projects(Collections.emptyList()) + .build(); + + when(urlGenerator.getUri(any())).thenReturn(URI.create(SITE_ATLASSIAN_NET)); + when(jiraFeignClient.getProject(baseUrl, "project key", token)) + .thenReturn(JiraBoardProject.builder().style("next-gen").build()); + when(jiraFeignClient.getTargetField(baseUrl, boardRequestParam.getProjectKey(), token)) + .thenReturn(emptyProjectFieldResponse); + + assertThatThrownBy(() -> jiraService.getInfo(boardTypeJira, BOARD_REQUEST_BUILDER().build())) + .isInstanceOf(PermissionDenyException.class) + .hasMessageContaining("There is no enough permission."); + } + @Test void shouldThrowCustomExceptionWhenGetJiraBoardConfig() { when(urlGenerator.getUri(any())).thenReturn(URI.create(SITE_ATLASSIAN_NET)); @@ -487,6 +858,19 @@ void shouldThrowCustomExceptionWhenGetJiraBoardConfig() { .hasMessageContaining("There is no enough permission."); } + @Test + void shouldThrowCustomExceptionWhenGetJiraBoardInfo() { + URI baseUrl = URI.create(SITE_ATLASSIAN_NET); + String token = "token"; + when(urlGenerator.getUri(any())).thenReturn(URI.create(SITE_ATLASSIAN_NET)); + when(jiraFeignClient.getProject(baseUrl, "project key", token)) + .thenReturn(JiraBoardProject.builder().style("next-gen").build()); + + assertThatThrownBy(() -> jiraService.getInfo(boardTypeJira, BOARD_REQUEST_BUILDER().build())) + .isInstanceOf(PermissionDenyException.class) + .hasMessageContaining("There is no enough permission."); + } + @Test void shouldThrowCustomExceptionWhenCallJiraFeignClientToGetBoardConfigFailed() { URI baseUrl = URI.create(SITE_ATLASSIAN_NET); @@ -500,6 +884,23 @@ void shouldThrowCustomExceptionWhenCallJiraFeignClientToGetBoardConfigFailed() { .hasMessageContaining("exception"); } + @Test + void shouldThrowCustomExceptionWhenCallJiraFeignClientToGetBoardInfoFailed() { + URI baseUrl = URI.create(SITE_ATLASSIAN_NET); + String token = "token"; + when(urlGenerator.getUri(any())).thenReturn(URI.create(SITE_ATLASSIAN_NET)); + when(jiraFeignClient.getProject(baseUrl, "project key", token)) + .thenReturn(JiraBoardProject.builder().style("next-gen").build()); + when(jiraFeignClient.getJiraBoardConfiguration(any(), any(), any())) + .thenThrow(new CustomFeignClientException(400, "exception")); + when(jiraFeignClient.getTargetField(baseUrl, "project key", "token")) + .thenReturn(FIELD_RESPONSE_BUILDER().build()); + + assertThatThrownBy(() -> jiraService.getInfo(boardTypeJira, BoardRequestParam.builder().build())) + .isInstanceOf(Exception.class) + .hasMessageContaining("exception"); + } + @Test void shouldGetCardsWhenCallGetStoryPointsAndCycleTime() throws JsonProcessingException { URI baseUrl = URI.create(SITE_ATLASSIAN_NET); @@ -515,9 +916,9 @@ void shouldGetCardsWhenCallGetStoryPointsAndCycleTime() throws JsonProcessingExc when(urlGenerator.getUri(any())).thenReturn(baseUrl); when(jiraFeignClient.getJiraCards(baseUrl, BOARD_ID, QUERY_COUNT, 0, jql, boardRequestParam.getToken())) .thenReturn(allDoneCards); - when(jiraFeignClient.getJiraCardHistory(baseUrl, "1", token)) + when(jiraFeignClient.getJiraCardHistoryByCount(baseUrl, "1", 0, 100, token)) .thenReturn(CARD_HISTORY_MULTI_RESPONSE_BUILDER().build()); - when(jiraFeignClient.getJiraCardHistory(baseUrl, "2", token)) + when(jiraFeignClient.getJiraCardHistoryByCount(baseUrl, "2", 0, 100, token)) .thenReturn(CARD_HISTORY_RESPONSE_BUILDER().build()); when(jiraFeignClient.getTargetField(baseUrl, "PLL", token)).thenReturn(ALL_FIELD_RESPONSE_BUILDER().build()); @@ -532,7 +933,7 @@ void shouldGetCardsWhenCallGetStoryPointsAndCycleTime() throws JsonProcessingExc @Test void shouldGetCardsWhenCallGetStoryPointsAndCycleTimeWhenBoardTypeIsClassicJira() throws JsonProcessingException { - // given + URI baseUrl = URI.create(SITE_ATLASSIAN_NET); String token = "token"; @@ -545,25 +946,24 @@ void shouldGetCardsWhenCallGetStoryPointsAndCycleTimeWhenBoardTypeIsClassicJira( .replaceAll("flagged", "customfield_10021") .replaceAll("development", "customfield_10000"); - // when when(urlGenerator.getUri(any())).thenReturn(baseUrl); when(jiraFeignClient.getJiraCards(any(), any(), anyInt(), anyInt(), any(), any())).thenReturn(allDoneCards); - when(jiraFeignClient.getJiraCardHistory(baseUrl, "1", token)) + when(jiraFeignClient.getJiraCardHistoryByCount(baseUrl, "1", 0, 100, token)) .thenReturn(CARD_HISTORY_MULTI_RESPONSE_BUILDER().build()); - when(jiraFeignClient.getJiraCardHistory(baseUrl, "2", token)) + when(jiraFeignClient.getJiraCardHistoryByCount(baseUrl, "2", 0, 100, token)) .thenReturn(CARD_HISTORY_RESPONSE_BUILDER().build()); when(jiraFeignClient.getTargetField(baseUrl, "PLL", token)).thenReturn(ALL_FIELD_RESPONSE_BUILDER().build()); - // then CardCollection cardCollection = jiraService.getStoryPointsAndCycleTimeForDoneCards( storyPointsAndCycleTimeRequest, jiraBoardSetting.getBoardColumns(), List.of("Zhang San"), ""); + assertThat(cardCollection.getCardsNumber()).isEqualTo(1); } @Test void shouldGetCardsWhenCallGetStoryPointsAndCycleTimeWhenDoneTimeGreaterThanSelectedEndTime() throws JsonProcessingException { - // given + URI baseUrl = URI.create(SITE_ATLASSIAN_NET); String token = "token"; @@ -571,30 +971,26 @@ void shouldGetCardsWhenCallGetStoryPointsAndCycleTimeWhenDoneTimeGreaterThanSele StoryPointsAndCycleTimeRequest storyPointsAndCycleTimeRequest = CLASSIC_JIRA_STORY_POINTS_FORM_ALL_DONE_CARD() .build(); String allDoneCards = objectMapper.writeValueAsString(NEED_FILTERED_ALL_DONE_CARDS_BUILDER().build()); - - // when when(urlGenerator.getUri(any())).thenReturn(baseUrl); when(jiraFeignClient.getJiraCards(any(), any(), anyInt(), anyInt(), any(), any())).thenReturn(allDoneCards); - when(jiraFeignClient.getJiraCardHistory(baseUrl, "1", token)) + when(jiraFeignClient.getJiraCardHistoryByCount(baseUrl, "1", 0, 100, token)) .thenReturn(CARD_HISTORY_MULTI_RESPONSE_BUILDER().build()); - when(jiraFeignClient.getJiraCardHistory(baseUrl, "2", token)) + when(jiraFeignClient.getJiraCardHistoryByCount(baseUrl, "2", 0, 100, token)) .thenReturn(CARD_HISTORY_DONE_TIME_GREATER_THAN_END_TIME_BUILDER().build()); when(jiraFeignClient.getTargetField(baseUrl, "PLL", token)).thenReturn(ALL_FIELD_RESPONSE_BUILDER().build()); - // then CardCollection cardCollection = jiraService.getStoryPointsAndCycleTimeForDoneCards( storyPointsAndCycleTimeRequest, jiraBoardSetting.getBoardColumns(), List.of("Zhang San"), ""); + assertThat(cardCollection.getCardsNumber()).isEqualTo(1); } @Test void shouldReturnBadRequestExceptionWhenBoardTypeIsNotCorrect() { - // given JiraBoardSetting jiraBoardSetting = INCORRECT_JIRA_BOARD_SETTING_BUILD().build(); StoryPointsAndCycleTimeRequest storyPointsAndCycleTimeRequest = INCORRECT_JIRA_STORY_POINTS_FORM_ALL_DONE_CARD() .build(); - // then assertThatThrownBy(() -> jiraService.getStoryPointsAndCycleTimeForDoneCards(storyPointsAndCycleTimeRequest, jiraBoardSetting.getBoardColumns(), List.of("Zhang San"), null)) @@ -612,7 +1008,7 @@ public void shouldProcessCustomFieldsForCardsWhenCallGetStoryPointsAndCycleTime( when(jiraFeignClient.getJiraCards(any(), any(), anyInt(), anyInt(), any(), any())).thenReturn( "{\"total\":1,\"issues\":[{\"expand\":\"expand\",\"id\":\"1\",\"self\":\"https:xxxx/issue/1\",\"key\":\"ADM-455\",\"fields\":{\"customfield_10020\":[{\"id\":16,\"name\":\"Tool Sprint 11\",\"state\":\"closed\",\"boardId\":2,\"goal\":\"goals\",\"startDate\":\"2023-05-15T03:09:23.000Z\",\"endDate\":\"2023-05-28T16:00:00.000Z\",\"completeDate\":\"2023-05-29T03:51:24.898Z\"}],\"customfield_10021\":[{\"self\":\"https:xxxx/10019\",\"value\":\"Impediment\",\"id\":\"10019\"}],\"customfield_10016\":1,\"assignee\":{\"displayName\":\"Zhang San\"}}}]}"); when(jiraFeignClient.getTargetField(any(), any(), any())).thenReturn(FIELD_RESPONSE_BUILDER().build()); - when(jiraFeignClient.getJiraCardHistory(any(), any(), any())) + when(jiraFeignClient.getJiraCardHistoryByCount(any(), any(), anyInt(), anyInt(), any())) .thenReturn(CARD_HISTORY_MULTI_RESPONSE_BUILDER().build()); when(boardUtil.getCycleTimeInfos(any(), any(), any())).thenReturn(CYCLE_TIME_INFO_LIST()); when(boardUtil.getOriginCycleTimeInfos(any())).thenReturn(CYCLE_TIME_INFO_LIST()); @@ -633,7 +1029,7 @@ public void shouldReturnNullWhenCallGetStoryPointsAndCycleTimeAndHistoryISNull() when(jiraFeignClient.getJiraCards(any(), any(), anyInt(), anyInt(), any(), any())).thenReturn( "{\"total\":1,\"issues\":[{\"expand\":\"expand\",\"id\":\"1\",\"self\":\"https:xxxx/issue/1\",\"key\":\"ADM-455\",\"fields\":{\"customfield_10020\":[{\"id\":16,\"name\":\"Tool Sprint 11\",\"state\":\"closed\",\"boardId\":2,\"goal\":\"goals\",\"startDate\":\"2023-05-15T03:09:23.000Z\",\"endDate\":\"2023-05-28T16:00:00.000Z\",\"completeDate\":\"2023-05-29T03:51:24.898Z\"}],\"customfield_10021\":[{\"self\":\"https:xxxx/10019\",\"value\":\"Impediment\",\"id\":\"10019\"}],\"customfield_10016\":1,\"assignee\":{\"displayName\":\"Zhang San\"}}}]}"); when(jiraFeignClient.getTargetField(any(), any(), any())).thenReturn(FIELD_RESPONSE_BUILDER().build()); - when(jiraFeignClient.getJiraCardHistory(any(), any(), any())) + when(jiraFeignClient.getJiraCardHistoryByCount(any(), any(), anyInt(), anyInt(), any())) .thenReturn(CARD_HISTORY_RESPONSE_BUILDER_TO_DONE().build()); CardCollection doneCards = jiraService.getStoryPointsAndCycleTimeForDoneCards(storyPointsAndCycleTimeRequest, @@ -676,7 +1072,7 @@ public void shouldReturnCardsWhenCallGetStoryPointsAndCycleTimeForNonDoneCardsFo when(urlGenerator.getUri(any())).thenReturn(baseUrl); when(jiraFeignClient.getJiraCards(any(), any(), anyInt(), anyInt(), any(), any())) .thenReturn(objectMapper.writeValueAsString(ALL_NON_DONE_CARDS_RESPONSE_FOR_STORY_POINT_BUILDER().build())); - when(jiraFeignClient.getJiraCardHistory(any(), any(), any())) + when(jiraFeignClient.getJiraCardHistoryByCount(any(), any(), anyInt(), anyInt(), any())) .thenReturn(CARD_HISTORY_MULTI_RESPONSE_BUILDER().build()); when(jiraFeignClient.getTargetField(any(), any(), any())).thenReturn(ALL_FIELD_RESPONSE_BUILDER().build()); @@ -698,7 +1094,7 @@ public void shouldReturnCardsWhenCallGetStoryPointsAndCycleTimeForNonDoneCardsFo when(jiraFeignClient.getJiraCards(any(), any(), anyInt(), anyInt(), any(), any())) .thenReturn(objectMapper.writeValueAsString(ALL_NON_DONE_CARDS_RESPONSE_FOR_STORY_POINT_BUILDER().build())); when(jiraFeignClient.getTargetField(any(), any(), any())).thenReturn(ALL_FIELD_RESPONSE_BUILDER().build()); - when(jiraFeignClient.getJiraCardHistory(any(), any(), any())) + when(jiraFeignClient.getJiraCardHistoryByCount(any(), any(), anyInt(), anyInt(), any())) .thenReturn(CARD_HISTORY_MULTI_RESPONSE_BUILDER().build()); CardCollection nonDoneCards = jiraService.getStoryPointsAndCycleTimeForNonDoneCards( @@ -728,7 +1124,7 @@ public void shouldReturnCardsWhenCallGetStoryPointsAndCycleTimeForNonDoneCardsFo .thenReturn(objectMapper.writeValueAsString(ALL_NON_DONE_CARDS_RESPONSE_FOR_STORY_POINT_BUILDER().build())); when(jiraFeignClient.getTargetField(any(), any(), any())).thenReturn(ALL_FIELD_RESPONSE_BUILDER().build()); - when(jiraFeignClient.getJiraCardHistory(any(), any(), any())) + when(jiraFeignClient.getJiraCardHistoryByCount(any(), any(), anyInt(), anyInt(), any())) .thenReturn(CARD_HISTORY_MULTI_RESPONSE_BUILDER().build()); CardCollection nonDoneCards = jiraService.getStoryPointsAndCycleTimeForNonDoneCards( @@ -757,7 +1153,7 @@ public void shouldReturnCardsWhenCallGetStoryPointsAndCycleTimeForNonDoneCardsFo .thenReturn(objectMapper.writeValueAsString(ALL_NON_DONE_CARDS_RESPONSE_FOR_STORY_POINT_BUILDER().build())); when(jiraFeignClient.getTargetField(any(), any(), any())).thenReturn(ALL_FIELD_RESPONSE_BUILDER().build()); - when(jiraFeignClient.getJiraCardHistory(any(), any(), any())) + when(jiraFeignClient.getJiraCardHistoryByCount(any(), any(), anyInt(), anyInt(), any())) .thenReturn(CARD_HISTORY_MULTI_RESPONSE_BUILDER().build()); CardCollection nonDoneCards = jiraService.getStoryPointsAndCycleTimeForNonDoneCards( @@ -780,12 +1176,12 @@ public void shouldReturnJiraBoardConfigDTOWhenCallGetJiraBoardConfig() { } @Test - void shouldGetRealDoneCardWhenCallGetStoryPointsAndCycleTimeWhenUseHistoricalAssigneeFilterMethod() + void shouldGetRealDoneCardGivenCallGetStoryPointsAndCycleTimeWhenUseHistoricalAssigneeFilter() throws JsonProcessingException { - // given + URI baseUrl = URI.create(SITE_ATLASSIAN_NET); String token = "token"; - String assigneeFilter = "historicalAssignee"; + String assigneeFilter = AssigneeFilterMethod.HISTORICAL_ASSIGNEE.getDescription(); // request param JiraBoardSetting jiraBoardSetting = JIRA_BOARD_SETTING_WITH_HISTORICAL_ASSIGNEE_FILTER_METHOD().build(); @@ -793,60 +1189,69 @@ void shouldGetRealDoneCardWhenCallGetStoryPointsAndCycleTimeWhenUseHistoricalAss // return value String allDoneCards = objectMapper - .writeValueAsString(ALL_DONE_CARDS_RESPONSE_FOR_ASSIGNEE_FILTER_METHOD_TEST().build()) + .writeValueAsString(ALL_DONE_CARDS_RESPONSE_FOR_ASSIGNEE_FILTER_TEST().build()) .replaceAll("sprint", "customfield_10020") .replaceAll("partner", "customfield_10037") .replaceAll("flagged", "customfield_10021") .replaceAll("development", "customfield_10000"); - - // when when(urlGenerator.getUri(any())).thenReturn(baseUrl); when(jiraFeignClient.getJiraCards(any(), any(), anyInt(), anyInt(), any(), any())).thenReturn(allDoneCards); - when(jiraFeignClient.getJiraCardHistory(baseUrl, "ADM-475", token)) - .thenReturn(CARD1_HISTORY_FOR_HISTORICAL_ASSIGNEE_FILTER_METHOD().build()); - when(jiraFeignClient.getJiraCardHistory(baseUrl, "ADM-524", token)) - .thenReturn(CARD2_HISTORY_FOR_HISTORICAL_ASSIGNEE_FILTER_METHOD().build()); + when(jiraFeignClient.getJiraCardHistoryByCount(baseUrl, "ADM-475", 0, 100, token)) + .thenReturn(CARD1_HISTORY_FOR_HISTORICAL_ASSIGNEE_FILTER().build()); + when(jiraFeignClient.getJiraCardHistoryByCount(baseUrl, "ADM-524", 0, 100, token)) + .thenReturn(CARD2_HISTORY_FOR_HISTORICAL_ASSIGNEE_FILTER().build()); + when(jiraFeignClient.getJiraCardHistoryByCount(baseUrl, "ADM-520", 0, 100, token)) + .thenReturn(CARD3_HISTORY_FOR_HISTORICAL_ASSIGNEE_FILTER().build()); when(jiraFeignClient.getTargetField(baseUrl, "PLL", token)).thenReturn(ALL_FIELD_RESPONSE_BUILDER().build()); - // then - CardCollection cardCollection = jiraService.getStoryPointsAndCycleTimeForDoneCards(request, - jiraBoardSetting.getBoardColumns(), List.of("da pei"), assigneeFilter); - assertThat(cardCollection.getCardsNumber()).isEqualTo(2); + CardCollection cardCollection1 = jiraService.getStoryPointsAndCycleTimeForDoneCards(request, + jiraBoardSetting.getBoardColumns(), List.of("Da Pei"), assigneeFilter); + CardCollection cardCollection2 = jiraService.getStoryPointsAndCycleTimeForDoneCards(request, + jiraBoardSetting.getBoardColumns(), List.of("song"), assigneeFilter); + + assertThat(cardCollection1.getCardsNumber()).isEqualTo(0); + assertThat(cardCollection2.getCardsNumber()).isEqualTo(1); + assertThat(cardCollection2.getJiraCardDTOList().get(0).getBaseInfo().getKey()).isEqualTo("ADM-475"); + } @Test - void shouldGetRealDoneCardWhenCallGetStoryPointsAndCycleTimeWhenUseLastAssigneeFilterMethod() + void shouldGetRealDoneCardGivenCallGetStoryPointsAndCycleTimeWhenUseLastAssigneeFilter() throws JsonProcessingException { - // given + URI baseUrl = URI.create(SITE_ATLASSIAN_NET); String token = "token"; - String assigneeFilter = "lastAssignee"; - + String assigneeFilter = AssigneeFilterMethod.LAST_ASSIGNEE.getDescription(); // request param JiraBoardSetting jiraBoardSetting = JIRA_BOARD_SETTING_WITH_HISTORICAL_ASSIGNEE_FILTER_METHOD().build(); StoryPointsAndCycleTimeRequest request = STORY_POINTS_REQUEST_WITH_ASSIGNEE_FILTER_METHOD().build(); // return value String allDoneCards = objectMapper - .writeValueAsString(ALL_DONE_CARDS_RESPONSE_FOR_ASSIGNEE_FILTER_METHOD_TEST().build()) + .writeValueAsString(ALL_DONE_CARDS_RESPONSE_FOR_ASSIGNEE_FILTER_TEST().build()) .replaceAll("sprint", "customfield_10020") .replaceAll("partner", "customfield_10037") .replaceAll("flagged", "customfield_10021") .replaceAll("development", "customfield_10000"); - - // when when(urlGenerator.getUri(any())).thenReturn(baseUrl); when(jiraFeignClient.getJiraCards(any(), any(), anyInt(), anyInt(), any(), any())).thenReturn(allDoneCards); - when(jiraFeignClient.getJiraCardHistory(baseUrl, "ADM-475", token)) - .thenReturn(CARD1_HISTORY_FOR_HISTORICAL_ASSIGNEE_FILTER_METHOD().build()); - when(jiraFeignClient.getJiraCardHistory(baseUrl, "ADM-524", token)) - .thenReturn(CARD2_HISTORY_FOR_HISTORICAL_ASSIGNEE_FILTER_METHOD().build()); + when(jiraFeignClient.getJiraCardHistoryByCount(baseUrl, "ADM-475", 0, 100, token)) + .thenReturn(CARD1_HISTORY_FOR_HISTORICAL_ASSIGNEE_FILTER().build()); + when(jiraFeignClient.getJiraCardHistoryByCount(baseUrl, "ADM-524", 0, 100, token)) + .thenReturn(CARD2_HISTORY_FOR_HISTORICAL_ASSIGNEE_FILTER().build()); + when(jiraFeignClient.getJiraCardHistoryByCount(baseUrl, "ADM-520", 0, 100, token)) + .thenReturn(CARD3_HISTORY_FOR_HISTORICAL_ASSIGNEE_FILTER().build()); when(jiraFeignClient.getTargetField(baseUrl, "PLL", token)).thenReturn(ALL_FIELD_RESPONSE_BUILDER().build()); - // then - CardCollection cardCollection = jiraService.getStoryPointsAndCycleTimeForDoneCards(request, - jiraBoardSetting.getBoardColumns(), List.of("da pei"), assigneeFilter); - assertThat(cardCollection.getCardsNumber()).isEqualTo(1); + CardCollection cardCollection1 = jiraService.getStoryPointsAndCycleTimeForDoneCards(request, + jiraBoardSetting.getBoardColumns(), List.of("yun"), assigneeFilter); + CardCollection cardCollection2 = jiraService.getStoryPointsAndCycleTimeForDoneCards(request, + jiraBoardSetting.getBoardColumns(), List.of("Da Pei"), assigneeFilter); + + assertThat(cardCollection1.getCardsNumber()).isEqualTo(1); + assertThat(cardCollection1.getJiraCardDTOList().get(0).getBaseInfo().getKey()).isEqualTo("ADM-520"); + assertThat(cardCollection2.getCardsNumber()).isEqualTo(1); + assertThat(cardCollection2.getJiraCardDTOList().get(0).getBaseInfo().getKey()).isEqualTo("ADM-475"); } @Test @@ -871,7 +1276,7 @@ void shouldFilterOutUnreasonableTargetField() throws JsonProcessingException { when(jiraFeignClient.getColumnStatusCategory(baseUrl, COLUM_SELF_ID_3, token)).thenReturn(completeStatusSelf); when(jiraFeignClient.getColumnStatusCategory(baseUrl, COLUM_SELF_ID_2, token)).thenReturn(doingStatusSelf); when(jiraFeignClient.getJiraCards(baseUrl, BOARD_ID, QUERY_COUNT, 0, jql, token)).thenReturn(allDoneCards); - when(jiraFeignClient.getJiraCardHistory(any(), any(), any())) + when(jiraFeignClient.getJiraCardHistoryByCount(any(), any(), anyInt(), anyInt(), any())) .thenReturn(CARD_HISTORY_RESPONSE_BUILDER().build()); when(jiraFeignClient.getTargetField(baseUrl, "project key", token)) .thenReturn(INCLUDE_UNREASONABLE_FIELD_RESPONSE_BUILDER().build()); @@ -887,10 +1292,50 @@ void shouldFilterOutUnreasonableTargetField() throws JsonProcessingException { assertThat(boardConfigDTO.getTargetFields()).isEqualTo(expectTargetField); } + @Test + void shouldFilterOutUnreasonableTargetFieldWhenGetBoardInfo() throws JsonProcessingException { + JiraBoardConfigDTO jiraBoardConfigDTO = CLASSIC_JIRA_BOARD_CONFIG_RESPONSE_BUILDER().build(); + StatusSelfDTO doneStatusSelf = DONE_STATUS_SELF_RESPONSE_BUILDER().build(); + StatusSelfDTO completeStatusSelf = COMPLETE_STATUS_SELF_RESPONSE_BUILDER().build(); + StatusSelfDTO doingStatusSelf = DOING_STATUS_SELF_RESPONSE_BUILDER().build(); + URI baseUrl = URI.create(SITE_ATLASSIAN_NET); + String token = "token"; + BoardRequestParam boardRequestParam = BOARD_REQUEST_BUILDER().build(); + String jql = String.format(ALL_CARDS_JQL, boardRequestParam.getStartTime(), boardRequestParam.getEndTime()); + List expectTargetField = List.of(new TargetField("customfield_10021", "Flagged", false), + new TargetField("priority", "Priority", false), + new TargetField("timetracking", "Time tracking", false)); + String allDoneCards = objectMapper.writeValueAsString(ALL_DONE_CARDS_RESPONSE_FOR_STORY_POINT_BUILDER().build()) + .replaceAll("storyPoints", "customfield_10016"); + + doReturn(jiraBoardConfigDTO).when(jiraFeignClient).getJiraBoardConfiguration(baseUrl, BOARD_ID, token); + when(urlGenerator.getUri(any())).thenReturn(URI.create(SITE_ATLASSIAN_NET)); + when(jiraFeignClient.getProject(baseUrl, "project key", token)) + .thenReturn(JiraBoardProject.builder().style("classic").build()); + when(jiraFeignClient.getColumnStatusCategory(baseUrl, COLUM_SELF_ID_1, token)).thenReturn(doneStatusSelf); + when(jiraFeignClient.getColumnStatusCategory(baseUrl, COLUM_SELF_ID_3, token)).thenReturn(completeStatusSelf); + when(jiraFeignClient.getColumnStatusCategory(baseUrl, COLUM_SELF_ID_2, token)).thenReturn(doingStatusSelf); + when(jiraFeignClient.getJiraCards(baseUrl, BOARD_ID, QUERY_COUNT, 0, jql, token)).thenReturn(allDoneCards); + when(jiraFeignClient.getJiraCardHistoryByCount(any(), any(), anyInt(), anyInt(), any())) + .thenReturn(CARD_HISTORY_RESPONSE_BUILDER().build()); + when(jiraFeignClient.getTargetField(baseUrl, "project key", token)) + .thenReturn(INCLUDE_UNREASONABLE_FIELD_RESPONSE_BUILDER().build()); + + BoardConfigDTO boardConfigDTO = jiraService.getInfo(boardTypeJira, boardRequestParam); + + assertThat(boardConfigDTO.getTargetFields()).hasSize(3); + assertThat( + boardConfigDTO.getTargetFields().contains(new TargetField("customfield_10000", "Development", false))) + .isFalse(); + assertThat(boardConfigDTO.getTargetFields().contains(new TargetField("customfield_10019", "Rank", false))) + .isFalse(); + assertThat(boardConfigDTO.getTargetFields()).isEqualTo(expectTargetField); + } + @Test void shouldGetRealDoneCardsGivenMultipleStatuesMappingToDoneStatusWhenCallGetStoryPointsAndCycleTime() throws JsonProcessingException { - // given + URI baseUrl = URI.create(SITE_ATLASSIAN_NET); String token = "token"; String assigneeFilter = "lastAssignee"; @@ -900,26 +1345,23 @@ void shouldGetRealDoneCardsGivenMultipleStatuesMappingToDoneStatusWhenCallGetSto StoryPointsAndCycleTimeRequest request = STORY_POINTS_REQUEST_WITH_MULTIPLE_REAL_DONE_STATUSES().build(); // return value - String allDoneCards = objectMapper - .writeValueAsString(ALL_DONE_CARDS_RESPONSE_FOR_ASSIGNEE_FILTER_METHOD_TEST().build()) + String allDoneCards = objectMapper.writeValueAsString(ALL_DONE_CARDS_RESPONSE_FOR_MULTIPLE_STATUS().build()) .replaceAll("sprint", "customfield_10020") .replaceAll("partner", "customfield_10037") .replaceAll("flagged", "customfield_10021") .replaceAll("development", "customfield_10000"); - // when - when(urlGenerator.getUri(any())).thenReturn(baseUrl); when(jiraFeignClient.getJiraCards(any(), any(), anyInt(), anyInt(), any(), any())).thenReturn(allDoneCards); - when(jiraFeignClient.getJiraCardHistory(baseUrl, "ADM-475", token)) + when(jiraFeignClient.getJiraCardHistoryByCount(baseUrl, "ADM-475", 0, 100, token)) .thenReturn(CARD1_HISTORY_FOR_MULTIPLE_STATUSES().build()); - when(jiraFeignClient.getJiraCardHistory(baseUrl, "ADM-524", token)) + when(jiraFeignClient.getJiraCardHistoryByCount(baseUrl, "ADM-524", 0, 100, token)) .thenReturn(CARD2_HISTORY_FOR_MULTIPLE_STATUSES().build()); when(jiraFeignClient.getTargetField(baseUrl, "PLL", token)).thenReturn(ALL_FIELD_RESPONSE_BUILDER().build()); - // then CardCollection cardCollection = jiraService.getStoryPointsAndCycleTimeForDoneCards(request, - jiraBoardSetting.getBoardColumns(), List.of("da pei"), assigneeFilter); + jiraBoardSetting.getBoardColumns(), List.of("Da Pei"), assigneeFilter); + assertThat(cardCollection.getCardsNumber()).isEqualTo(1); assertThat(cardCollection.getJiraCardDTOList().get(0).getBaseInfo().getKey()).isEqualTo("ADM-475"); } @@ -927,7 +1369,7 @@ void shouldGetRealDoneCardsGivenMultipleStatuesMappingToDoneStatusWhenCallGetSto @Test void shouldGetRealDoneCardsGivenHistoryWithNoStatusFieldWhenCallGetStoryPointsAndCycleTime() throws JsonProcessingException { - // given + URI baseUrl = URI.create(SITE_ATLASSIAN_NET); String token = "token"; String assigneeFilter = "lastAssignee"; @@ -937,26 +1379,23 @@ void shouldGetRealDoneCardsGivenHistoryWithNoStatusFieldWhenCallGetStoryPointsAn StoryPointsAndCycleTimeRequest request = STORY_POINTS_REQUEST_WITH_MULTIPLE_REAL_DONE_STATUSES().build(); // return value - String allDoneCards = objectMapper - .writeValueAsString(ALL_DONE_CARDS_RESPONSE_FOR_ASSIGNEE_FILTER_METHOD_TEST().build()) + String allDoneCards = objectMapper.writeValueAsString(ALL_DONE_CARDS_RESPONSE_FOR_MULTIPLE_STATUS().build()) .replaceAll("sprint", "customfield_10020") .replaceAll("partner", "customfield_10037") .replaceAll("flagged", "customfield_10021") .replaceAll("development", "customfield_10000"); - // when - when(urlGenerator.getUri(any())).thenReturn(baseUrl); when(jiraFeignClient.getJiraCards(any(), any(), anyInt(), anyInt(), any(), any())).thenReturn(allDoneCards); - when(jiraFeignClient.getJiraCardHistory(baseUrl, "ADM-475", token)) + when(jiraFeignClient.getJiraCardHistoryByCount(baseUrl, "ADM-475", 0, 100, token)) .thenReturn(CARD2_HISTORY_FOR_MULTIPLE_STATUSES().build()); - when(jiraFeignClient.getJiraCardHistory(baseUrl, "ADM-524", token)) + when(jiraFeignClient.getJiraCardHistoryByCount(baseUrl, "ADM-524", 0, 100, token)) .thenReturn(CARD_HISTORY_WITH_NO_STATUS_FIELD().build()); when(jiraFeignClient.getTargetField(baseUrl, "PLL", token)).thenReturn(ALL_FIELD_RESPONSE_BUILDER().build()); - // then CardCollection cardCollection = jiraService.getStoryPointsAndCycleTimeForDoneCards(request, - jiraBoardSetting.getBoardColumns(), List.of("da pei"), assigneeFilter); + jiraBoardSetting.getBoardColumns(), List.of("Da Pei"), assigneeFilter); + assertThat(cardCollection.getCardsNumber()).isZero(); } diff --git a/backend/src/test/java/heartbeat/service/report/WorkDayTest.java b/backend/src/test/java/heartbeat/service/report/WorkDayTest.java index 5d721bde82..596712b8fc 100644 --- a/backend/src/test/java/heartbeat/service/report/WorkDayTest.java +++ b/backend/src/test/java/heartbeat/service/report/WorkDayTest.java @@ -11,11 +11,11 @@ import org.mockito.junit.jupiter.MockitoExtension; import java.time.LocalDate; -import java.time.Year; import java.time.ZoneOffset; import java.time.format.DateTimeFormatter; import java.util.List; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) @@ -29,7 +29,6 @@ class WorkDayTest { @Test void shouldReturnDayIsHoliday() { - String year = Year.now().toString(); List holidayDTOList = List.of( HolidayDTO.builder().date("2023-01-01").name("元旦").isOffDay(true).build(), HolidayDTO.builder().date("2023-01-28").name("春节").isOffDay(false).build()); @@ -43,7 +42,8 @@ void shouldReturnDayIsHoliday() { .atStartOfDay(ZoneOffset.UTC) .toInstant() .toEpochMilli(); - when(holidayFeignClient.getHolidays(year)) + + when(holidayFeignClient.getHolidays(any())) .thenReturn(HolidaysResponseDTO.builder().days(holidayDTOList).build()); workDay.changeConsiderHolidayMode(true); diff --git a/backend/src/test/java/heartbeat/service/source/github/GithubServiceTest.java b/backend/src/test/java/heartbeat/service/source/github/GithubServiceTest.java index a20ddeb43c..da07d3658a 100644 --- a/backend/src/test/java/heartbeat/service/source/github/GithubServiceTest.java +++ b/backend/src/test/java/heartbeat/service/source/github/GithubServiceTest.java @@ -29,12 +29,20 @@ import org.mockito.quality.Strictness; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import static heartbeat.TestFixtures.GITHUB_REPOSITORY; +import static heartbeat.TestFixtures.GITHUB_TOKEN; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.util.ArrayList; @@ -158,30 +166,52 @@ public ThreadPoolTaskExecutor getTaskExecutor() { } @Test + @Deprecated void shouldReturnNonRedundantGithubReposWhenCallGithubFeignClientApi() { - String githubToken = "123456"; + String githubToken = GITHUB_TOKEN; String token = "token " + githubToken; when(gitHubFeignClient.getAllRepos(token)).thenReturn(List.of(GitHubRepo.builder().htmlUrl("11111").build(), GitHubRepo.builder().htmlUrl("22222").build(), GitHubRepo.builder().htmlUrl("33333").build())); - when(gitHubFeignClient.getGithubOrganizationsInfo(token)) .thenReturn(List.of(GitHubOrganizationsInfo.builder().login("org1").build(), GitHubOrganizationsInfo.builder().login("org2").build())); - when(gitHubFeignClient.getReposByOrganizationName("org1", token)) .thenReturn(List.of(GitHubRepo.builder().htmlUrl("22222").build(), GitHubRepo.builder().htmlUrl("33333").build(), GitHubRepo.builder().htmlUrl("44444").build())); final var response = githubService.verifyToken(githubToken); githubService.shutdownExecutor(); + assertThat(response.getGithubRepos()).hasSize(4); assertThat(response.getGithubRepos()) .isEqualTo(new LinkedHashSet<>(List.of("11111", "22222", "33333", "44444"))); } @Test + void shouldReturnGithubTokenIsVerifyWhenVerifyToken() { + String githubToken = GITHUB_TOKEN; + String token = "token " + githubToken; + + doNothing().when(gitHubFeignClient).verifyToken(token); + + assertDoesNotThrow(() -> githubService.verifyTokenV2(githubToken)); + } + + @Test + void shouldReturnGithubBranchIsVerifyWhenVerifyBranch() { + String githubToken = GITHUB_TOKEN; + String token = "token " + githubToken; + doNothing().when(gitHubFeignClient).verifyCanReadTargetBranch(any(), any(), any()); + + githubService.verifyCanReadTargetBranch(GITHUB_REPOSITORY, "main", githubToken); + + verify(gitHubFeignClient, times(1)).verifyCanReadTargetBranch("fake/repo", "main", token); + } + + @Test + @Deprecated void shouldReturnUnauthorizedStatusWhenCallGithubFeignClientApiWithWrongToken() { - String wrongGithubToken = "123456"; + String wrongGithubToken = GITHUB_TOKEN; String token = "token " + wrongGithubToken; when(gitHubFeignClient.getAllRepos(token)) @@ -206,8 +236,9 @@ void shouldThrowExceptionWhenVerifyGitHubThrowUnExpectedException() { } @Test + @Deprecated void shouldGithubReturnEmptyWhenVerifyGithubThrowGithubRepoEmptyException() { - String githubEmptyToken = "123456"; + String githubEmptyToken = GITHUB_TOKEN; when(gitHubFeignClient.getReposByOrganizationName("org1", githubEmptyToken)).thenReturn(new ArrayList<>()); assertThatThrownBy(() -> githubService.verifyToken(githubEmptyToken)) @@ -215,11 +246,68 @@ void shouldGithubReturnEmptyWhenVerifyGithubThrowGithubRepoEmptyException() { .hasMessageContaining("No GitHub repositories found."); } + @Test + void shouldThrowExceptionWhenGithubReturnUnExpectedException() { + String githubEmptyToken = GITHUB_TOKEN; + doThrow(new UnauthorizedException("Failed to get GitHub info_status: 401 UNAUTHORIZED, reason: ...")) + .when(gitHubFeignClient) + .verifyToken("token " + githubEmptyToken); + + var exception = assertThrows(UnauthorizedException.class, () -> githubService.verifyTokenV2(githubEmptyToken)); + assertEquals("Failed to get GitHub info_status: 401 UNAUTHORIZED, reason: ...", exception.getMessage()); + } + + @Test + void shouldThrowExceptionGivenGithubReturnUnExpectedExceptionWhenVerifyBranch() { + String githubEmptyToken = GITHUB_TOKEN; + doThrow(new UnauthorizedException("Failed to get GitHub info_status: 401 UNAUTHORIZED, reason: ...")) + .when(gitHubFeignClient) + .verifyCanReadTargetBranch("fake/repo", "main", "token " + githubEmptyToken); + + var exception = assertThrows(UnauthorizedException.class, + () -> githubService.verifyCanReadTargetBranch(GITHUB_REPOSITORY, "main", githubEmptyToken)); + assertEquals("Failed to get GitHub info_status: 401 UNAUTHORIZED, reason: ...", exception.getMessage()); + } + + @Test + void shouldThrowExceptionGivenGithubReturnPermissionDenyExceptionWhenVerifyBranch() { + String githubEmptyToken = GITHUB_TOKEN; + doThrow(new NotFoundException("Failed to get GitHub info_status: 404, reason: ...")).when(gitHubFeignClient) + .verifyCanReadTargetBranch("fake/repo", "main", "token " + githubEmptyToken); + + var exception = assertThrows(PermissionDenyException.class, + () -> githubService.verifyCanReadTargetBranch(GITHUB_REPOSITORY, "main", githubEmptyToken)); + assertEquals("Unable to read target branch: main", exception.getMessage()); + } + + @Test + void shouldThrowExceptionGivenGithubReturnCompletionExceptionExceptionWhenVerifyToken() { + String githubEmptyToken = GITHUB_TOKEN; + doThrow(new CompletionException(new Exception("UnExpected Exception"))).when(gitHubFeignClient) + .verifyToken("token " + githubEmptyToken); + + var exception = assertThrows(InternalServerErrorException.class, + () -> githubService.verifyTokenV2(githubEmptyToken)); + assertEquals("Failed to call GitHub with token_error: UnExpected Exception", exception.getMessage()); + } + + @Test + void shouldThrowExceptionGivenGithubReturnUnauthorizedExceptionWhenVerifyBranch() { + String githubEmptyToken = GITHUB_TOKEN; + doThrow(new CompletionException(new Exception("UnExpected Exception"))).when(gitHubFeignClient) + .verifyCanReadTargetBranch("fake/repo", "main", "token " + githubEmptyToken); + + var exception = assertThrows(InternalServerErrorException.class, + () -> githubService.verifyCanReadTargetBranch(GITHUB_REPOSITORY, "main", githubEmptyToken)); + assertEquals("Failed to call GitHub branch: main with error: UnExpected Exception", exception.getMessage()); + } + @Test void shouldReturnNullWhenMergeTimeIsNull() { PullRequestInfo pullRequestInfo = PullRequestInfo.builder().build(); DeployInfo deployInfo = DeployInfo.builder().build(); CommitInfo commitInfo = CommitInfo.builder().build(); + LeadTime result = githubService.mapLeadTimeWithInfo(pullRequestInfo, deployInfo, commitInfo); assertNull(result); @@ -227,7 +315,6 @@ void shouldReturnNullWhenMergeTimeIsNull() { @Test void shouldReturnLeadTimeWhenMergedTimeIsNotNull() { - LeadTime result = githubService.mapLeadTimeWithInfo(pullRequestInfo, deployInfo, commitInfo); LeadTime expect = LeadTime.builder() .commitId("111") .prCreatedTime(1658548980000L) @@ -241,13 +328,14 @@ void shouldReturnLeadTimeWhenMergedTimeIsNotNull() { .totalTime(180000) .build(); + LeadTime result = githubService.mapLeadTimeWithInfo(pullRequestInfo, deployInfo, commitInfo); + assertEquals(expect, result); } @Test void CommitTimeInPrShouldBeZeroWhenCommitInfoIsNull() { commitInfo = CommitInfo.builder().build(); - LeadTime result = githubService.mapLeadTimeWithInfo(pullRequestInfo, deployInfo, commitInfo); LeadTime expect = LeadTime.builder() .commitId("111") .prCreatedTime(1658548980000L) @@ -261,13 +349,14 @@ void CommitTimeInPrShouldBeZeroWhenCommitInfoIsNull() { .totalTime(180000) .build(); + LeadTime result = githubService.mapLeadTimeWithInfo(pullRequestInfo, deployInfo, commitInfo); + assertEquals(expect, result); } @Test void shouldReturnFirstCommitTimeInPrZeroWhenCommitInfoIsNull() { commitInfo = CommitInfo.builder().build(); - LeadTime result = githubService.mapLeadTimeWithInfo(pullRequestInfo, deployInfo, commitInfo); LeadTime expect = LeadTime.builder() .commitId("111") .prCreatedTime(1658548980000L) @@ -281,17 +370,18 @@ void shouldReturnFirstCommitTimeInPrZeroWhenCommitInfoIsNull() { .totalTime(180000L) .build(); + LeadTime result = githubService.mapLeadTimeWithInfo(pullRequestInfo, deployInfo, commitInfo); + assertEquals(expect, result); } @Test void shouldReturnPipeLineLeadTimeWhenDeployITimesIsNotEmpty() { String mockToken = "mockToken"; - when(gitHubFeignClient.getPullRequestListInfo(any(), any(), any())).thenReturn(List.of(pullRequestInfo)); - when(gitHubFeignClient.getPullRequestCommitInfo(any(), any(), any())).thenReturn(List.of(commitInfo)); when(gitHubFeignClient.getCommitInfo(any(), any(), any())).thenReturn(commitInfo); + List result = githubService.fetchPipelinesLeadTime(deployTimes, repositoryMap, mockToken); assertEquals(pipelineLeadTimes, result); @@ -300,14 +390,13 @@ void shouldReturnPipeLineLeadTimeWhenDeployITimesIsNotEmpty() { @Test void shouldReturnEmptyLeadTimeWhenDeployTimesIsEmpty() { String mockToken = "mockToken"; - + List expect = List.of(PipelineLeadTime.builder().build()); when(gitHubFeignClient.getPullRequestListInfo(any(), any(), any())).thenReturn(List.of(pullRequestInfo)); - when(gitHubFeignClient.getPullRequestCommitInfo(any(), any(), any())).thenReturn(List.of(commitInfo)); List emptyDeployTimes = List.of(DeployTimes.builder().build()); + List result = githubService.fetchPipelinesLeadTime(emptyDeployTimes, repositoryMap, mockToken); - List expect = List.of(PipelineLeadTime.builder().build()); assertEquals(expect, result); } @@ -315,14 +404,6 @@ void shouldReturnEmptyLeadTimeWhenDeployTimesIsEmpty() { @Test void shouldReturnEmptyMergeLeadTimeWhenPullRequestInfoIsEmpty() { String mockToken = "mockToken"; - - when(gitHubFeignClient.getPullRequestListInfo(any(), any(), any())).thenReturn(List.of()); - - when(gitHubFeignClient.getPullRequestCommitInfo(any(), any(), any())).thenReturn(List.of()); - when(gitHubFeignClient.getCommitInfo(any(), any(), any())).thenReturn(new CommitInfo()); - - List result = githubService.fetchPipelinesLeadTime(deployTimes, repositoryMap, mockToken); - List expect = List.of(PipelineLeadTime.builder() .pipelineStep("Step") .pipelineName("Name") @@ -335,6 +416,11 @@ void shouldReturnEmptyMergeLeadTimeWhenPullRequestInfoIsEmpty() { .totalTime(120000) .build())) .build()); + when(gitHubFeignClient.getPullRequestListInfo(any(), any(), any())).thenReturn(List.of()); + when(gitHubFeignClient.getPullRequestCommitInfo(any(), any(), any())).thenReturn(List.of()); + when(gitHubFeignClient.getCommitInfo(any(), any(), any())).thenReturn(new CommitInfo()); + + List result = githubService.fetchPipelinesLeadTime(deployTimes, repositoryMap, mockToken); assertEquals(expect, result); } @@ -342,14 +428,6 @@ void shouldReturnEmptyMergeLeadTimeWhenPullRequestInfoIsEmpty() { @Test void shouldReturnEmptyMergeLeadTimeWhenPullRequestInfoGot404Error() { String mockToken = "mockToken"; - - when(gitHubFeignClient.getPullRequestListInfo(any(), any(), any())).thenThrow(new NotFoundException("")); - - when(gitHubFeignClient.getPullRequestCommitInfo(any(), any(), any())).thenReturn(List.of()); - when(gitHubFeignClient.getCommitInfo(any(), any(), any())).thenReturn(new CommitInfo()); - - List result = githubService.fetchPipelinesLeadTime(deployTimes, repositoryMap, mockToken); - List expect = List.of(PipelineLeadTime.builder() .pipelineStep("Step") .pipelineName("Name") @@ -362,6 +440,11 @@ void shouldReturnEmptyMergeLeadTimeWhenPullRequestInfoGot404Error() { .totalTime(120000) .build())) .build()); + when(gitHubFeignClient.getPullRequestListInfo(any(), any(), any())).thenThrow(new NotFoundException("")); + when(gitHubFeignClient.getPullRequestCommitInfo(any(), any(), any())).thenReturn(List.of()); + when(gitHubFeignClient.getCommitInfo(any(), any(), any())).thenReturn(new CommitInfo()); + + List result = githubService.fetchPipelinesLeadTime(deployTimes, repositoryMap, mockToken); assertEquals(expect, result); } @@ -370,12 +453,6 @@ void shouldReturnEmptyMergeLeadTimeWhenPullRequestInfoGot404Error() { void shouldReturnEmptyMergeLeadTimeWhenMergeTimeIsEmpty() { String mockToken = "mockToken"; pullRequestInfo.setMergedAt(null); - when(gitHubFeignClient.getPullRequestListInfo(any(), any(), any())).thenReturn(List.of(pullRequestInfo)); - - when(gitHubFeignClient.getPullRequestCommitInfo(any(), any(), any())).thenReturn(List.of()); - when(gitHubFeignClient.getCommitInfo(any(), any(), any())).thenReturn(new CommitInfo()); - List result = githubService.fetchPipelinesLeadTime(deployTimes, repositoryMap, mockToken); - List expect = List.of(PipelineLeadTime.builder() .pipelineStep("Step") .pipelineName("Name") @@ -388,6 +465,11 @@ void shouldReturnEmptyMergeLeadTimeWhenMergeTimeIsEmpty() { .totalTime(120000) .build())) .build()); + when(gitHubFeignClient.getPullRequestListInfo(any(), any(), any())).thenReturn(List.of(pullRequestInfo)); + when(gitHubFeignClient.getPullRequestCommitInfo(any(), any(), any())).thenReturn(List.of()); + when(gitHubFeignClient.getCommitInfo(any(), any(), any())).thenReturn(new CommitInfo()); + + List result = githubService.fetchPipelinesLeadTime(deployTimes, repositoryMap, mockToken); assertEquals(expect, result); } @@ -419,7 +501,7 @@ void shouldThrowCompletableExceptionIfGetPullRequestListInfoHasExceptionWhenFetc } @Test - public void shouldFetchCommitInfo() { + void shouldFetchCommitInfo() { CommitInfo commitInfo = CommitInfo.builder() .commit(Commit.builder() .author(Author.builder().name("XXXX").email("XXX@test.com").date("2023-05-10T06:43:02.653Z").build()) @@ -435,7 +517,7 @@ public void shouldFetchCommitInfo() { } @Test - public void shouldThrowPermissionDenyExceptionWhenFetchCommitInfo403Forbidden() { + void shouldThrowPermissionDenyExceptionWhenFetchCommitInfo403Forbidden() { when(gitHubFeignClient.getCommitInfo(anyString(), anyString(), anyString())) .thenThrow(new PermissionDenyException("request forbidden")); @@ -445,7 +527,7 @@ public void shouldThrowPermissionDenyExceptionWhenFetchCommitInfo403Forbidden() } @Test - public void shouldThrowInternalServerErrorExceptionWhenFetchCommitInfo500Exception() { + void shouldThrowInternalServerErrorExceptionWhenFetchCommitInfo500Exception() { when(gitHubFeignClient.getCommitInfo(anyString(), anyString(), anyString())).thenReturn(null); assertThatThrownBy(() -> githubService.fetchCommitInfo("12344", "", "")) @@ -454,7 +536,7 @@ public void shouldThrowInternalServerErrorExceptionWhenFetchCommitInfo500Excepti } @Test - public void shouldReturnNullWhenFetchCommitInfo404Exception() { + void shouldReturnNullWhenFetchCommitInfo404Exception() { when(gitHubFeignClient.getCommitInfo(anyString(), anyString(), anyString())).thenThrow(new NotFoundException("")); assertNull(githubService.fetchCommitInfo("12344", "", "")); @@ -463,12 +545,11 @@ public void shouldReturnNullWhenFetchCommitInfo404Exception() { @Test void shouldReturnPipeLineLeadTimeWhenDeployITimesIsNotEmptyAndCommitInfoError() { String mockToken = "mockToken"; - when(gitHubFeignClient.getPullRequestListInfo(any(), any(), any())).thenReturn(List.of(pullRequestInfo)); - when(gitHubFeignClient.getPullRequestCommitInfo(any(), any(), any())).thenReturn(List.of(commitInfo)); when(gitHubFeignClient.getCommitInfo(any(), any(), any())) .thenThrow(new NotFoundException("Failed to get commit")); + List result = githubService.fetchPipelinesLeadTime(deployTimes, repositoryMap, mockToken); assertEquals(pipelineLeadTimes, result); @@ -477,14 +558,12 @@ void shouldReturnPipeLineLeadTimeWhenDeployITimesIsNotEmptyAndCommitInfoError() @Test void shouldReturnPipeLineLeadTimeWhenDeployCommitShaIsDifferent() { String mockToken = "mockToken"; - pullRequestInfo = PullRequestInfo.builder() .mergedAt("2022-07-23T04:04:00.000+00:00") .createdAt("2022-07-23T04:03:00.000+00:00") .mergeCommitSha("222") .number(1) .build(); - pipelineLeadTimes = List.of(PipelineLeadTime.builder() .pipelineName("Name") .pipelineStep("Step") @@ -498,12 +577,11 @@ void shouldReturnPipeLineLeadTimeWhenDeployCommitShaIsDifferent() { .totalTime(120000) .build())) .build()); - when(gitHubFeignClient.getPullRequestListInfo(any(), any(), any())).thenReturn(List.of(pullRequestInfo)); - when(gitHubFeignClient.getPullRequestCommitInfo(any(), any(), any())).thenReturn(List.of(commitInfo)); when(gitHubFeignClient.getCommitInfo(any(), any(), any())) .thenThrow(new NotFoundException("Failed to get commit")); + List result = githubService.fetchPipelinesLeadTime(deployTimes, repositoryMap, mockToken); assertEquals(pipelineLeadTimes, result); diff --git a/docs/src/content/docs/en/spikes/tech-spikes-split-verification-of-board.mdx b/docs/src/content/docs/en/spikes/tech-spikes-split-verification-of-board.mdx index 273de3d7c8..7cadb5a80e 100644 --- a/docs/src/content/docs/en/spikes/tech-spikes-split-verification-of-board.mdx +++ b/docs/src/content/docs/en/spikes/tech-spikes-split-verification-of-board.mdx @@ -75,8 +75,9 @@ request: { "site": "...", "token(email+token)": "..." } -responses: - Status Code: 200 +responses: { + "project": "ADM" +} ``` - Exception Handler @@ -250,30 +251,38 @@ participant Jira ' group Verify the validity of the token FrontEnd -> JiraController: api/v1/boards/{boardType}/info activate JiraController - JiraController -> JiraService: get board + JiraController -> JiraService: get board info activate JiraService - JiraService -> JiraFeignClient: get board config info + JiraService -> JiraFeignClient: get board project for style activate JiraFeignClient - JiraFeignClient -> Jira: /rest/agile/1.0/board/{boardId}/configuration + JiraFeignClient -> Jira: rest/api/2/project/{projectIdOrKey} activate Jira Jira --> JiraFeignClient deactivate Jira JiraFeignClient --> JiraService deactivate JiraFeignClient - loop columns and status in column - JiraService -> JiraFeignClient: get jira column result + group get ignored and needed target fields + JiraService -> JiraFeignClient: get target fields activate JiraFeignClient - JiraFeignClient -> Jira: /rest/api/2/status/{statusNum} + JiraFeignClient -> Jira: /rest/api/2/issue/createmeta?projectKeys={projectKey}&expand=projects.issuetypes.fields activate Jira Jira --> JiraFeignClient deactivate Jira JiraFeignClient --> JiraService deactivate JiraFeignClient end - group get target fields - JiraService -> JiraFeignClient: get needed target fields + JiraService -> JiraFeignClient: get board config info + activate JiraFeignClient + JiraFeignClient -> Jira: /rest/agile/1.0/board/{boardId}/configuration + activate Jira + Jira --> JiraFeignClient + deactivate Jira + JiraFeignClient --> JiraService + deactivate JiraFeignClient + loop columns and status in column + JiraService -> JiraFeignClient: get jira column result activate JiraFeignClient - JiraFeignClient -> Jira: /rest/api/2/issue/createmeta?projectKeys={projectKey}&expand=projects.issuetypes.fields + JiraFeignClient -> Jira: /rest/api/2/status/{statusNum} activate Jira Jira --> JiraFeignClient deactivate Jira diff --git a/frontend/__tests__/src/components/Common/NotificationButton/NotificationButton.test.tsx b/frontend/__tests__/src/components/Common/NotificationButton/NotificationButton.test.tsx index b7177593f4..9a3cae106b 100644 --- a/frontend/__tests__/src/components/Common/NotificationButton/NotificationButton.test.tsx +++ b/frontend/__tests__/src/components/Common/NotificationButton/NotificationButton.test.tsx @@ -1,4 +1,4 @@ -import { cleanup, render, renderHook, waitFor } from '@testing-library/react' +import { render, renderHook, waitFor, screen } from '@testing-library/react' import { NotificationButton } from '@src/components/Common/NotificationButton' import React from 'react' import { useNotificationLayoutEffect } from '@src/hooks/useNotificationLayoutEffect' @@ -7,7 +7,6 @@ import userEvent from '@testing-library/user-event' const notificationIcon = 'NotificationIcon' describe('NotificationButton', () => { - afterEach(cleanup) const closeNotificationProps = { open: false, title: 'NotificationPopper', closeAutomatically: false } const openNotificationProps = { open: true, title: 'NotificationPopper', closeAutomatically: false } const { result } = renderHook(() => useNotificationLayoutEffect()) @@ -18,13 +17,12 @@ describe('NotificationButton', () => { expect(getByTestId(notificationIcon)).toBeInTheDocument() }) - it('should show NotificationPopper when clicking the component given the "open" value is true', () => { + it('should show NotificationPopper when clicking the component given the "open" value is true', async () => { act(() => { result.current.notificationProps = openNotificationProps }) - const { getByTestId, getByText } = render() - - userEvent.click(getByTestId(notificationIcon)) + const { getByText } = render() + await userEvent.click(screen.getByTestId(notificationIcon)) expect(getByText('NotificationPopper')).toBeInTheDocument() }) @@ -33,7 +31,6 @@ describe('NotificationButton', () => { result.current.notificationProps = closeNotificationProps }) const { getByTestId, queryByText } = render() - await userEvent.click(getByTestId(notificationIcon)) expect(queryByText('NotificationPopper')).not.toBeInTheDocument() @@ -56,7 +53,6 @@ describe('NotificationButton', () => { expect(getByRole('tooltip')).toBeInTheDocument() const content = await waitFor(() => getByText('OutSideSection')) - await userEvent.click(content) expect(result.current.updateProps).toBeCalledTimes(1) @@ -107,7 +103,6 @@ describe('NotificationButton', () => { expect(getByTestId(notificationIcon)).toBeInTheDocument() const content = await waitFor(() => getByText('OutSideSection')) - await userEvent.click(content) expect(result.current.updateProps).not.toBeCalled() diff --git a/frontend/__tests__/src/components/ErrorContent/index.test.tsx b/frontend/__tests__/src/components/ErrorContent/index.test.tsx index e617e6fe1d..aeeba5fda1 100644 --- a/frontend/__tests__/src/components/ErrorContent/index.test.tsx +++ b/frontend/__tests__/src/components/ErrorContent/index.test.tsx @@ -1,6 +1,6 @@ import { render } from '@testing-library/react' import ErrorPage from '@src/pages/ErrorPage' -import { ERROR_PAGE_MESSAGE, BASE_PAGE_ROUTE, RETRY_BUTTON } from '../../fixtures' +import { BASE_PAGE_ROUTE, ERROR_PAGE_MESSAGE, RETRY_BUTTON } from '../../fixtures' import React from 'react' import { BrowserRouter } from 'react-router-dom' import userEvent from '@testing-library/user-event' @@ -31,7 +31,6 @@ describe('error content', () => { ) - await userEvent.click(getByText(RETRY_BUTTON)) expect(navigateMock).toHaveBeenCalledWith(BASE_PAGE_ROUTE) diff --git a/frontend/__tests__/src/components/HomeGuide/HomeGuide.test.tsx b/frontend/__tests__/src/components/HomeGuide/HomeGuide.test.tsx index 86ae3ead69..f149221aba 100644 --- a/frontend/__tests__/src/components/HomeGuide/HomeGuide.test.tsx +++ b/frontend/__tests__/src/components/HomeGuide/HomeGuide.test.tsx @@ -1,5 +1,5 @@ import { HomeGuide } from '@src/components/HomeGuide' -import { fireEvent, render, waitFor } from '@testing-library/react' +import { fireEvent, render, waitFor, screen } from '@testing-library/react' import { setupStore } from '../../utils/setupStoreUtil' import { Provider } from 'react-redux' import { @@ -30,7 +30,7 @@ const setup = () => { ) } -const setupInputFile = (configJson: object) => { +const setupInputFile = async (configJson: object) => { const { queryByText, getByTestId } = setup() const file = new File([`${JSON.stringify(configJson)}`], 'test.json', { type: 'file', @@ -42,7 +42,7 @@ const setupInputFile = (configJson: object) => { value: [file], }) - fireEvent.change(input) + await fireEvent.change(input) return queryByText } @@ -63,12 +63,11 @@ describe('HomeGuide', () => { }) it('should render input when click guide button', async () => { - const { getByText, getByTestId } = setup() + const { getByTestId } = setup() const fileInput = getByTestId('testInput') const clickSpy = jest.spyOn(fileInput, 'click') - await userEvent.click(getByText(IMPORT_PROJECT_FROM_FILE)) - + await userEvent.click(screen.getByText(IMPORT_PROJECT_FROM_FILE)) expect(clickSpy).toHaveBeenCalled() }) @@ -94,10 +93,8 @@ describe('HomeGuide', () => { }) it('should go to Metrics page when click create a new project button', async () => { - const { getByText } = setup() - - await userEvent.click(getByText(CREATE_NEW_PROJECT)) - + setup() + await userEvent.click(screen.getByText(CREATE_NEW_PROJECT)) expect(navigateMock).toHaveBeenCalledTimes(1) expect(navigateMock).toHaveBeenCalledWith(METRICS_PAGE_ROUTE) }) @@ -105,7 +102,7 @@ describe('HomeGuide', () => { describe('isValidImportedConfig', () => { it('should show warning message when no projectName dateRange metrics all exist', async () => { const emptyConfig = {} - const queryByText = setupInputFile(emptyConfig) + const queryByText = await setupInputFile(emptyConfig) await waitFor(() => { expect(mockedUseAppDispatch).toHaveBeenCalledTimes(0) @@ -114,7 +111,7 @@ describe('HomeGuide', () => { }) it('should no display warning message when projectName dateRange metrics all exist', async () => { - const queryByText = setupInputFile(IMPORTED_NEW_CONFIG_FIXTURE) + const queryByText = await setupInputFile(IMPORTED_NEW_CONFIG_FIXTURE) await waitFor(() => { expect(mockedUseAppDispatch).toHaveBeenCalledTimes(0) @@ -128,7 +125,7 @@ describe('HomeGuide', () => { ['endDate', { projectName: '', metrics: [], dateRange: { startDate: '', endDate: '2023-02-01' } }], ['metrics', { projectName: '', metrics: ['Metric 1', 'Metric 2'], dateRange: {} }], ])('should not display warning message when only %s exists', async (_, validConfig) => { - const queryByText = setupInputFile(validConfig) + const queryByText = await setupInputFile(validConfig) await waitFor(() => { expect(mockedUseAppDispatch).toHaveBeenCalledTimes(0) diff --git a/frontend/__tests__/src/components/Metrics/ConfigStep/MetricsTypeCheckbox.test.tsx b/frontend/__tests__/src/components/Metrics/ConfigStep/MetricsTypeCheckbox.test.tsx index c56810930f..c921be903a 100644 --- a/frontend/__tests__/src/components/Metrics/ConfigStep/MetricsTypeCheckbox.test.tsx +++ b/frontend/__tests__/src/components/Metrics/ConfigStep/MetricsTypeCheckbox.test.tsx @@ -1,23 +1,24 @@ import { + ALL, + CHANGE_FAILURE_RATE, + CLASSIFICATION, CONFIG_TITLE, + CYCLE_TIME, + DEPLOYMENT_FREQUENCY, + LEAD_TIME_FOR_CHANGES, + MEAN_TIME_TO_RECOVERY, REQUIRED_DATA, REQUIRED_DATA_LIST, VELOCITY, - LEAD_TIME_FOR_CHANGES, - CYCLE_TIME, - ALL, - MEAN_TIME_TO_RECOVERY, - CLASSIFICATION, - DEPLOYMENT_FREQUENCY, - CHANGE_FAILURE_RATE, } from '../../../fixtures' -import { act, render, within } from '@testing-library/react' +import { act, fireEvent, render, waitFor, within, screen } from '@testing-library/react' import { MetricsTypeCheckbox } from '@src/components/Metrics/ConfigStep/MetricsTypeCheckbox' import { Provider } from 'react-redux' import { setupStore } from '../../../utils/setupStoreUtil' import userEvent from '@testing-library/user-event' import { SELECTED_VALUE_SEPARATOR } from '@src/constants/commons' import BasicInfo from '@src/components/Metrics/ConfigStep/BasicInfo' + let store = null describe('MetricsTypeCheckbox', () => { @@ -45,9 +46,7 @@ describe('MetricsTypeCheckbox', () => { it('should show detail options when click require data button', async () => { const { getByRole } = setup() - await act(async () => { - await userEvent.click(getByRole('button', { name: REQUIRED_DATA })) - }) + await userEvent.click(screen.getByRole('button', { name: REQUIRED_DATA })) const listBox = within(getByRole('listbox')) const options = listBox.getAllByRole('option') const optionValue = options.map((li) => li.getAttribute('data-value')) @@ -57,9 +56,7 @@ describe('MetricsTypeCheckbox', () => { it('should show multiple selections when multiple options are selected', async () => { const { getByRole, getByText } = setup() - await act(async () => { - await userEvent.click(getByRole('button', { name: REQUIRED_DATA })) - }) + await userEvent.click(screen.getByRole('button', { name: REQUIRED_DATA })) const listBox = within(getByRole('listbox')) await act(async () => { await userEvent.click(listBox.getByRole('option', { name: VELOCITY })) @@ -108,7 +105,9 @@ describe('MetricsTypeCheckbox', () => { it('should be checked of All selected option when click any other options', async () => { const { getByRole } = setup() - await userEvent.click(getByRole('button', { name: REQUIRED_DATA })) + await act(async () => { + await userEvent.click(getByRole('button', { name: REQUIRED_DATA })) + }) const listBox = within(getByRole('listbox')) const optionsToClick = [ @@ -120,10 +119,12 @@ describe('MetricsTypeCheckbox', () => { listBox.getByRole('option', { name: CHANGE_FAILURE_RATE }), listBox.getByRole('option', { name: MEAN_TIME_TO_RECOVERY }), ] - await Promise.all(optionsToClick.map((opt) => userEvent.click(opt))) + await Promise.all(optionsToClick.map((opt) => fireEvent.click(opt))) - expect(listBox.getByRole('option', { name: ALL })).toHaveAttribute('aria-selected', 'true') - }, 50000) + await waitFor(() => { + expect(listBox.getByRole('option', { name: ALL })).toHaveAttribute('aria-selected', 'true') + }) + }) it('should show some selections when click all option and then click velocity selection', async () => { const { getByRole, getByText } = setup() @@ -144,7 +145,7 @@ describe('MetricsTypeCheckbox', () => { expect(listBox.getByRole('option', { name: MEAN_TIME_TO_RECOVERY })).toHaveAttribute('aria-selected', 'false') expect(listBox.getByRole('option', { name: ALL })).toHaveAttribute('aria-selected', 'false') expect(getByText(displayedDataList.join(SELECTED_VALUE_SEPARATOR))).toBeInTheDocument() - }, 50000) + }) it('should show none selection when double click all option', async () => { const { getByRole, getByText } = setup() @@ -154,7 +155,7 @@ describe('MetricsTypeCheckbox', () => { await userEvent.click(getByRole('listbox', { name: REQUIRED_DATA })) const errorMessage = getByText('Metrics is required') - expect(errorMessage).toBeInTheDocument() + await waitFor(() => expect(errorMessage).toBeInTheDocument()) }) it('should show error message when require data is null', async () => { diff --git a/frontend/__tests__/src/components/Metrics/ConfigStep/PipelineTool.test.tsx b/frontend/__tests__/src/components/Metrics/ConfigStep/PipelineTool.test.tsx index 61e1263fdc..70b0344d25 100644 --- a/frontend/__tests__/src/components/Metrics/ConfigStep/PipelineTool.test.tsx +++ b/frontend/__tests__/src/components/Metrics/ConfigStep/PipelineTool.test.tsx @@ -20,7 +20,6 @@ import { setupServer } from 'msw/node' import { rest } from 'msw' import userEvent from '@testing-library/user-event' import { HttpStatusCode } from 'axios' -import { act } from 'react-dom/test-utils' export const fillPipelineToolFieldsInformation = async () => { const mockInfo = 'bkua_mockTokenMockTokenMockTokenMockToken1234' @@ -86,23 +85,24 @@ describe('PipelineTool', () => { }) it('should clear other fields information when change pipelineTool Field selection', async () => { - const { getByRole, getByText } = setup() + setup() const tokenInput = screen.getByTestId('pipelineToolTextField').querySelector('input') as HTMLInputElement await fillPipelineToolFieldsInformation() + await userEvent.click(screen.getByRole('button', { name: 'Pipeline Tool' })) - await userEvent.click(getByRole('button', { name: 'Pipeline Tool' })) - await userEvent.click(getByText(PIPELINE_TOOL_TYPES.GO_CD)) + await userEvent.click(screen.getByText(PIPELINE_TOOL_TYPES.GO_CD)) expect(tokenInput.value).toEqual('') - }, 50000) + }) it('should clear all fields information when click reset button', async () => { - const { getByRole, getByText, queryByRole } = setup() + const { getByText, queryByRole } = setup() const tokenInput = screen.getByTestId('pipelineToolTextField').querySelector('input') as HTMLInputElement await fillPipelineToolFieldsInformation() - await userEvent.click(getByText(VERIFY)) - await userEvent.click(getByRole('button', { name: RESET })) + await userEvent.click(screen.getByText(VERIFY)) + + await userEvent.click(screen.getByRole('button', { name: RESET })) expect(tokenInput.value).toEqual('') expect(getByText(PIPELINE_TOOL_TYPES.BUILD_KITE)).toBeInTheDocument() @@ -112,7 +112,7 @@ describe('PipelineTool', () => { it('should show detail options when click pipelineTool fields', async () => { const { getByRole } = setup() - await userEvent.click(getByRole('button', { name: 'Pipeline Tool' })) + await userEvent.click(screen.getByRole('button', { name: 'Pipeline Tool' })) const listBox = within(getByRole('listbox')) const options = listBox.getAllByRole('option') const optionValue = options.map((li) => li.getAttribute('data-value')) @@ -136,7 +136,6 @@ describe('PipelineTool', () => { await fillPipelineToolFieldsInformation() const mockInfo = 'mockToken' const tokenInput = screen.getByTestId('pipelineToolTextField').querySelector('input') as HTMLInputElement - await userEvent.type(tokenInput, mockInfo) await userEvent.clear(tokenInput) @@ -148,7 +147,6 @@ describe('PipelineTool', () => { const { getByText } = setup() const mockInfo = 'mockToken' const tokenInput = screen.getByTestId('pipelineToolTextField').querySelector('input') as HTMLInputElement - await userEvent.type(tokenInput, mockInfo) expect(tokenInput.value).toEqual(mockInfo) @@ -160,11 +158,9 @@ describe('PipelineTool', () => { it('should show reset button and verified button when verify succeed ', async () => { const { getByText } = setup() await fillPipelineToolFieldsInformation() - await userEvent.click(getByText(VERIFY)) - act(() => { - expect(getByText(RESET)).toBeVisible() - }) + await userEvent.click(screen.getByText(VERIFY)) + expect(screen.getByText(RESET)).toBeVisible() await waitFor(() => { expect(getByText(VERIFIED)).toBeTruthy() @@ -172,9 +168,9 @@ describe('PipelineTool', () => { }) it('should called verifyPipelineTool method once when click verify button', async () => { - const { getByRole, getByText } = setup() + const { getByText } = setup() await fillPipelineToolFieldsInformation() - await userEvent.click(getByRole('button', { name: VERIFY })) + await userEvent.click(screen.getByRole('button', { name: VERIFY })) expect(getByText('Verified')).toBeInTheDocument() }) @@ -183,7 +179,6 @@ describe('PipelineTool', () => { const { getByRole, container } = setup() await fillPipelineToolFieldsInformation() fireEvent.click(getByRole('button', { name: VERIFY })) - await waitFor(() => { expect(container.getElementsByTagName('span')[0].getAttribute('role')).toEqual('progressbar') }) @@ -195,10 +190,10 @@ describe('PipelineTool', () => { res(ctx.status(HttpStatusCode.Unauthorized), ctx.json({ hintInfo: VERIFY_ERROR_MESSAGE.UNAUTHORIZED })) ) ) - const { getByText, getByRole } = setup() + const { getByText } = setup() await fillPipelineToolFieldsInformation() - await userEvent.click(getByRole('button', { name: VERIFY })) + await userEvent.click(screen.getByRole('button', { name: VERIFY })) expect( getByText(`${MOCK_PIPELINE_VERIFY_REQUEST_PARAMS.type} ${VERIFY_FAILED}: ${VERIFY_ERROR_MESSAGE.UNAUTHORIZED}`) ).toBeInTheDocument() diff --git a/frontend/__tests__/src/components/Metrics/MetricsStep/DeploymentFrequencySettings/DeploymentFrequencySettings.test.tsx b/frontend/__tests__/src/components/Metrics/MetricsStep/DeploymentFrequencySettings/DeploymentFrequencySettings.test.tsx index 3f9686dfbe..69ef47dad3 100644 --- a/frontend/__tests__/src/components/Metrics/MetricsStep/DeploymentFrequencySettings/DeploymentFrequencySettings.test.tsx +++ b/frontend/__tests__/src/components/Metrics/MetricsStep/DeploymentFrequencySettings/DeploymentFrequencySettings.test.tsx @@ -1,4 +1,4 @@ -import { render, within } from '@testing-library/react' +import { render, within, screen } from '@testing-library/react' import { Provider } from 'react-redux' import { store } from '@src/store' import userEvent from '@testing-library/user-event' @@ -48,14 +48,13 @@ jest.mock('@src/hooks/useMetricsStepValidationCheckContext', () => ({ useMetricsStepValidationCheckContext: () => mockValidationCheckContext, })) -const setup = () => - render( - - - - ) - describe('DeploymentFrequencySettings', () => { + const setup = () => + render( + + + + ) afterEach(() => { jest.clearAllMocks() }) @@ -68,25 +67,22 @@ describe('DeploymentFrequencySettings', () => { }) it('should call addADeploymentFrequencySetting function when click add another pipeline button', async () => { - const { getByTestId } = await setup() - - await userEvent.click(getByTestId('AddIcon')) + setup() + await userEvent.click(screen.getByTestId('AddIcon')) expect(addADeploymentFrequencySetting).toHaveBeenCalledTimes(1) }) it('should call deleteADeploymentFrequencySetting function when click remove pipeline button', async () => { - const { getAllByRole } = await setup() - - await userEvent.click(getAllByRole('button', { name: REMOVE_BUTTON })[0]) + setup() + await userEvent.click(screen.getAllByRole('button', { name: REMOVE_BUTTON })[0]) expect(deleteADeploymentFrequencySetting).toHaveBeenCalledTimes(1) }) it('should call updateDeploymentFrequencySetting function and clearErrorMessages function when select organization', async () => { - const { getAllByRole, getByRole } = setup() - - await userEvent.click(getAllByRole('button', { name: LIST_OPEN })[0]) + const { getByRole } = setup() + await userEvent.click(screen.getAllByRole('button', { name: LIST_OPEN })[0]) const listBox = within(getByRole('listbox')) await userEvent.click(listBox.getByText('mockOrgName')) diff --git a/frontend/__tests__/src/components/Metrics/MetricsStep/MetricsStep.test.tsx b/frontend/__tests__/src/components/Metrics/MetricsStep/MetricsStep.test.tsx index 6466071a2a..abb74a2fac 100644 --- a/frontend/__tests__/src/components/Metrics/MetricsStep/MetricsStep.test.tsx +++ b/frontend/__tests__/src/components/Metrics/MetricsStep/MetricsStep.test.tsx @@ -149,6 +149,7 @@ describe('MetricsStep', () => { const columnsArray = within(cycleTimeSettingsSection).getAllByRole('button', { name: LIST_OPEN }) await userEvent.click(columnsArray[1]) + const options = within(getByRole('listbox')).getAllByRole('option') await userEvent.click(options[1]) @@ -162,8 +163,8 @@ describe('MetricsStep', () => { expect(realDoneSettingSection).not.toHaveTextContent(SELECT_CONSIDER_AS_DONE_MESSAGE) const columnsArray = within(cycleTimeSettingsSection).getAllByRole('button', { name: LIST_OPEN }) - await userEvent.click(columnsArray[2]) + const options = within(getByRole('listbox')).getAllByRole('option') await userEvent.click(options[options.length - 1]) @@ -179,10 +180,12 @@ describe('MetricsStep', () => { const columnsArray = within(cycleTimeSettingsSection).getAllByRole('button', { name: LIST_OPEN }) await userEvent.click(columnsArray[1]) + const options1 = within(getByRole('listbox')).getAllByRole('option') await userEvent.click(options1[1]) await userEvent.click(columnsArray[4]) + const options2 = within(getByRole('listbox')).getAllByRole('option') await userEvent.click(options2[1]) diff --git a/frontend/__tests__/src/components/Metrics/ReportStep/ExpiredDialog.test.tsx b/frontend/__tests__/src/components/Metrics/ReportStep/ExpiredDialog.test.tsx index d663bcc48e..db0d7709e5 100644 --- a/frontend/__tests__/src/components/Metrics/ReportStep/ExpiredDialog.test.tsx +++ b/frontend/__tests__/src/components/Metrics/ReportStep/ExpiredDialog.test.tsx @@ -1,4 +1,4 @@ -import { getByText, queryByText, render, waitFor } from '@testing-library/react' +import { render, screen, waitFor } from '@testing-library/react' import { setupStore } from '../../../utils/setupStoreUtil' import { ExpiredDialog } from '@src/components/Metrics/ReportStep/ExpiredDialog' import { Provider } from 'react-redux' @@ -17,8 +17,7 @@ describe('ExpiredDialog', () => { expect(getByText(EXPORT_EXPIRED_CSV_MESSAGE)).toBeInTheDocument() - await userEvent.click(getByText('No')) - + await userEvent.click(screen.getByText('No')) await waitFor(() => { expect(queryByText(EXPORT_EXPIRED_CSV_MESSAGE)).not.toBeInTheDocument() }) @@ -47,7 +46,7 @@ describe('ExpiredDialog', () => { expect(getByText(EXPORT_EXPIRED_CSV_MESSAGE)).toBeInTheDocument() - await userEvent.click(getByText('Yes')) + await userEvent.click(screen.getByText('Yes')) expect(handleOkFn).toBeCalledTimes(1) }) }) diff --git a/frontend/__tests__/src/components/Metrics/ReportStep/ReportStep.test.tsx b/frontend/__tests__/src/components/Metrics/ReportStep/ReportStep.test.tsx index 2dd1e73ac8..d64c552035 100644 --- a/frontend/__tests__/src/components/Metrics/ReportStep/ReportStep.test.tsx +++ b/frontend/__tests__/src/components/Metrics/ReportStep/ReportStep.test.tsx @@ -1,4 +1,4 @@ -import { render, renderHook } from '@testing-library/react' +import { render, renderHook, screen } from '@testing-library/react' import ReportStep from '@src/components/Metrics/ReportStep' import { BACK, @@ -28,7 +28,7 @@ import { useNotificationLayoutEffect } from '@src/hooks/useNotificationLayoutEff import React from 'react' import { useExportCsvEffect } from '@src/hooks/useExportCsvEffect' import { MESSAGE } from '@src/constants/resources' -import { formatMillisecondsToHours } from '@src/utils/util' +import { act } from 'react-dom/test-utils' jest.mock('@src/context/stepper/StepperSlice', () => ({ ...jest.requireActual('@src/context/stepper/StepperSlice'), @@ -205,7 +205,6 @@ describe('Report Step', () => { const back = getByText(BACK) await userEvent.click(back) - expect(backStep).toHaveBeenCalledTimes(1) }) @@ -214,7 +213,6 @@ describe('Report Step', () => { const save = getByText(SAVE) await userEvent.click(save) - expect(handleSaveMock).toHaveBeenCalledTimes(1) }) @@ -227,7 +225,7 @@ describe('Report Step', () => { expect(navigateMock).toHaveBeenCalledWith(ERROR_PAGE_ROUTE) }) - it('should call resetProps and updateProps when remaining time is less than or equal to 5 minutes', () => { + it('should call resetProps and updateProps when remaining time is less than or equal to 5 minutes', async () => { const initExportValidityTimeMin = 30 React.useState = jest.fn().mockReturnValue([ initExportValidityTimeMin, @@ -249,16 +247,17 @@ describe('Report Step', () => { title: MESSAGE.EXPIRE_IN_FIVE_MINUTES, closeAutomatically: true, }) - - jest.advanceTimersByTime(500000) - + await act(async () => { + return jest.advanceTimersByTime(500000) + }) expect(updateProps).not.toBeCalledWith({ open: true, title: MESSAGE.EXPIRE_IN_FIVE_MINUTES, closeAutomatically: true, }) - - jest.advanceTimersByTime(1000000) + await act(async () => { + return jest.advanceTimersByTime(1000000) + }) expect(updateProps).toBeCalledWith({ open: true, @@ -297,7 +296,6 @@ describe('Report Step', () => { const exportButton = getByText(EXPORT_PIPELINE_DATA) expect(exportButton).toBeInTheDocument() await userEvent.click(exportButton) - expect(result.current.fetchExportData).toBeCalledWith({ csvTimeStamp: 0, dataType: 'pipeline', @@ -345,14 +343,6 @@ describe('Report Step', () => { }) describe('export metric data', () => { - it('should show errorMessage when clicking export metric button given csv not exist', () => { - const { getByText } = setup(['']) - - userEvent.click(getByText(EXPORT_METRIC_DATA)) - - expect(getByText('Export metric data')).toBeInTheDocument() - }) - it('should show export metric button when visiting this page', () => { const { getByText } = setup(['']) @@ -363,9 +353,9 @@ describe('Report Step', () => { it('should call fetchExportData when clicking "Export metric data"', async () => { const { result } = renderHook(() => useExportCsvEffect()) - const { getByText } = setup(['']) + setup(['']) - const exportButton = getByText(EXPORT_METRIC_DATA) + const exportButton = screen.getByText(EXPORT_METRIC_DATA) expect(exportButton).toBeInTheDocument() await userEvent.click(exportButton) @@ -376,5 +366,11 @@ describe('Report Step', () => { startDate: '', }) }) + + it('should show errorMessage when clicking export metric button given csv not exist', async () => { + setup(['']) + await userEvent.click(screen.getByText(EXPORT_METRIC_DATA)) + expect(screen.getByText('Export metric data')).toBeInTheDocument() + }) }) }) diff --git a/frontend/__tests__/src/hooks/useExportCsvEffect.test.tsx b/frontend/__tests__/src/hooks/useExportCsvEffect.test.tsx index d6ffae5b07..d0dfaca730 100644 --- a/frontend/__tests__/src/hooks/useExportCsvEffect.test.tsx +++ b/frontend/__tests__/src/hooks/useExportCsvEffect.test.tsx @@ -4,6 +4,7 @@ import { useExportCsvEffect } from '@src/hooks/useExportCsvEffect' import { ERROR_MESSAGE_TIME_DURATION, MOCK_EXPORT_CSV_REQUEST_PARAMS } from '../fixtures' import { InternalServerException } from '@src/exceptions/InternalServerException' import { NotFoundException } from '@src/exceptions/NotFoundException' +import { HttpStatusCode } from 'axios' describe('use export csv effect', () => { afterEach(() => { @@ -27,7 +28,7 @@ describe('use export csv effect', () => { it('should set error message when export csv response status 500', async () => { csvClient.exportCSVData = jest.fn().mockImplementation(() => { - throw new InternalServerException('error message') + throw new InternalServerException('error message', HttpStatusCode.InternalServerError) }) const { result } = renderHook(() => useExportCsvEffect()) @@ -40,7 +41,7 @@ describe('use export csv effect', () => { it('should set error message when export csv response status 404', async () => { csvClient.exportCSVData = jest.fn().mockImplementation(() => { - throw new NotFoundException('error message') + throw new NotFoundException('error message', HttpStatusCode.NotFound) }) const { result } = renderHook(() => useExportCsvEffect()) diff --git a/frontend/__tests__/src/hooks/useGenerateReportEffect.test.tsx b/frontend/__tests__/src/hooks/useGenerateReportEffect.test.tsx index 063aa14b1a..26adf0e76e 100644 --- a/frontend/__tests__/src/hooks/useGenerateReportEffect.test.tsx +++ b/frontend/__tests__/src/hooks/useGenerateReportEffect.test.tsx @@ -1,4 +1,4 @@ -import { renderHook, waitFor } from '@testing-library/react' +import { act, renderHook, waitFor } from '@testing-library/react' import { useGenerateReportEffect } from '@src/hooks/useGenerateReportEffect' import { ERROR_MESSAGE_TIME_DURATION, @@ -43,8 +43,9 @@ describe('use generate report effect', () => { result.current.startToRequestBoardData(MOCK_GENERATE_REPORT_REQUEST_PARAMS) expect(result.current.errorMessage).toEqual('generate report: error') }) - - jest.advanceTimersByTime(ERROR_MESSAGE_TIME_DURATION) + act(() => { + jest.advanceTimersByTime(ERROR_MESSAGE_TIME_DURATION) + }) await waitFor(() => { expect(result.current.errorMessage).toEqual('') @@ -53,7 +54,7 @@ describe('use generate report effect', () => { it('should set error message when generate report response status 404', async () => { reportClient.retrieveReportByUrl = jest.fn().mockImplementation(async () => { - throw new NotFoundException('error message') + throw new NotFoundException('error message', HttpStatusCode.NotFound) }) const { result } = renderHook(() => useGenerateReportEffect()) @@ -62,9 +63,9 @@ describe('use generate report effect', () => { result.current.startToRequestBoardData(MOCK_GENERATE_REPORT_REQUEST_PARAMS) expect(result.current.errorMessage).toEqual('generate report: error message') }) - - jest.advanceTimersByTime(ERROR_MESSAGE_TIME_DURATION) - + act(() => { + jest.advanceTimersByTime(ERROR_MESSAGE_TIME_DURATION) + }) await waitFor(() => { expect(result.current.errorMessage).toEqual('') }) @@ -72,7 +73,7 @@ describe('use generate report effect', () => { it('should set error message when generate report response status 500', async () => { reportClient.retrieveReportByUrl = jest.fn().mockImplementation(async () => { - throw new InternalServerException(INTERNAL_SERVER_ERROR_MESSAGE) + throw new InternalServerException(INTERNAL_SERVER_ERROR_MESSAGE, HttpStatusCode.InternalServerError) }) const { result } = renderHook(() => useGenerateReportEffect()) @@ -98,7 +99,7 @@ describe('use generate report effect', () => { it('should return error message when calling startToRequestBoardData given pollingReport response return 5xx ', async () => { reportClient.pollingReport = jest.fn().mockImplementation(async () => { - throw new InternalServerException('error') + throw new InternalServerException('error', HttpStatusCode.InternalServerError) }) reportClient.retrieveReportByUrl = jest .fn() @@ -115,7 +116,7 @@ describe('use generate report effect', () => { it('should return error message when calling startToRequestBoardData given pollingReport response return 4xx ', async () => { reportClient.pollingReport = jest.fn().mockImplementation(async () => { - throw new NotFoundException('file not found') + throw new NotFoundException('file not found', HttpStatusCode.NotFound) }) reportClient.retrieveReportByUrl = jest .fn() @@ -128,8 +129,9 @@ describe('use generate report effect', () => { result.current.startToRequestBoardData(MOCK_GENERATE_REPORT_REQUEST_PARAMS) expect(result.current.errorMessage).toEqual('generate report: file not found') }) - - jest.advanceTimersByTime(ERROR_MESSAGE_TIME_DURATION) + act(() => { + jest.advanceTimersByTime(ERROR_MESSAGE_TIME_DURATION) + }) await waitFor(() => { expect(result.current.errorMessage).toEqual('') @@ -171,8 +173,9 @@ describe('use generate report effect', () => { await waitFor(() => { result.current.startToRequestBoardData(MOCK_GENERATE_REPORT_REQUEST_PARAMS) }) - - jest.advanceTimersByTime(10000) + act(() => { + jest.advanceTimersByTime(10000) + }) await waitFor(() => { expect(reportClient.pollingReport).toHaveBeenCalledTimes(2) @@ -212,8 +215,9 @@ describe('use generate report effect', () => { result.current.startToRequestDoraData(MOCK_GENERATE_REPORT_REQUEST_PARAMS) expect(result.current.errorMessage).toEqual('generate report: error') }) - - jest.advanceTimersByTime(ERROR_MESSAGE_TIME_DURATION) + act(() => { + jest.advanceTimersByTime(ERROR_MESSAGE_TIME_DURATION) + }) await waitFor(() => { expect(result.current.errorMessage).toEqual('') @@ -222,7 +226,7 @@ describe('use generate report effect', () => { it('should set error message when generate report response status 404', async () => { reportClient.retrieveReportByUrl = jest.fn().mockImplementation(async () => { - throw new NotFoundException('error message') + throw new NotFoundException('error message', HttpStatusCode.NotFound) }) const { result } = renderHook(() => useGenerateReportEffect()) @@ -231,8 +235,9 @@ describe('use generate report effect', () => { result.current.startToRequestDoraData(MOCK_GENERATE_REPORT_REQUEST_PARAMS) expect(result.current.errorMessage).toEqual('generate report: error message') }) - - jest.advanceTimersByTime(ERROR_MESSAGE_TIME_DURATION) + act(() => { + jest.advanceTimersByTime(ERROR_MESSAGE_TIME_DURATION) + }) await waitFor(() => { expect(result.current.errorMessage).toEqual('') @@ -241,7 +246,7 @@ describe('use generate report effect', () => { it('should set error message when generate report response status 500', async () => { reportClient.retrieveReportByUrl = jest.fn().mockImplementation(async () => { - throw new InternalServerException(INTERNAL_SERVER_ERROR_MESSAGE) + throw new InternalServerException(INTERNAL_SERVER_ERROR_MESSAGE, HttpStatusCode.NotFound) }) const { result } = renderHook(() => useGenerateReportEffect()) @@ -267,7 +272,7 @@ describe('use generate report effect', () => { it('should return error message when calling startToRequestDoraData given pollingReport response return 5xx ', async () => { reportClient.pollingReport = jest.fn().mockImplementation(async () => { - throw new InternalServerException('error') + throw new InternalServerException('error', HttpStatusCode.InternalServerError) }) reportClient.retrieveReportByUrl = jest @@ -285,7 +290,7 @@ describe('use generate report effect', () => { it('should return error message when calling startToRequestDoraData given pollingReport response return 4xx ', async () => { reportClient.pollingReport = jest.fn().mockImplementation(async () => { - throw new NotFoundException('file not found') + throw new NotFoundException('file not found', HttpStatusCode.NotFound) }) reportClient.retrieveReportByUrl = jest .fn() @@ -299,7 +304,9 @@ describe('use generate report effect', () => { expect(result.current.errorMessage).toEqual('generate report: file not found') }) - jest.advanceTimersByTime(ERROR_MESSAGE_TIME_DURATION) + act(() => { + jest.advanceTimersByTime(ERROR_MESSAGE_TIME_DURATION) + }) await waitFor(() => { expect(result.current.errorMessage).toEqual('') diff --git a/frontend/__tests__/src/hooks/useGetMetricsStepsEffect.test.tsx b/frontend/__tests__/src/hooks/useGetMetricsStepsEffect.test.tsx index 3de6ede47b..49d1457054 100644 --- a/frontend/__tests__/src/hooks/useGetMetricsStepsEffect.test.tsx +++ b/frontend/__tests__/src/hooks/useGetMetricsStepsEffect.test.tsx @@ -3,6 +3,7 @@ import { useGetMetricsStepsEffect } from '@src/hooks/useGetMetricsStepsEffect' import { metricsClient } from '@src/clients/MetricsClient' import { InternalServerException } from '@src/exceptions/InternalServerException' import { ERROR_MESSAGE_TIME_DURATION, MOCK_GET_STEPS_PARAMS } from '../fixtures' +import { HttpStatusCode } from 'axios' describe('use get steps effect', () => { const { params, buildId, organizationId, pipelineType, token } = MOCK_GET_STEPS_PARAMS @@ -31,7 +32,7 @@ describe('use get steps effect', () => { it('should set error message when get steps response status 500', async () => { metricsClient.getSteps = jest.fn().mockImplementation(() => { - throw new InternalServerException('error message') + throw new InternalServerException('error message', HttpStatusCode.InternalServerError) }) const { result } = renderHook(() => useGetMetricsStepsEffect()) diff --git a/frontend/__tests__/src/hooks/useNotificationLayoutEffect.test.tsx b/frontend/__tests__/src/hooks/useNotificationLayoutEffect.test.tsx index f84bec84c1..9db07332ba 100644 --- a/frontend/__tests__/src/hooks/useNotificationLayoutEffect.test.tsx +++ b/frontend/__tests__/src/hooks/useNotificationLayoutEffect.test.tsx @@ -52,8 +52,9 @@ describe('useNotificationLayoutEffect', () => { result.current.notificationProps = defaultProps result.current.updateProps?.(mockProps) }) - - jest.advanceTimersByTime(DURATION.NOTIFICATION_TIME) + act(() => { + jest.advanceTimersByTime(DURATION.NOTIFICATION_TIME) + }) await waitFor(() => { expect(result.current.notificationProps).toEqual({ @@ -89,8 +90,9 @@ describe('useNotificationLayoutEffect', () => { await waitFor(() => { expect(result.current.notificationProps).not.toEqual(expectedProps) }) - - jest.advanceTimersByTime(expectedTime) + act(() => { + jest.advanceTimersByTime(expectedTime) + }) await waitFor(() => { expect(result.current.notificationProps).toEqual(expectedProps) diff --git a/frontend/__tests__/src/hooks/useVerifyBoardEffect.test.tsx b/frontend/__tests__/src/hooks/useVerifyBoardEffect.test.tsx index d0620b382d..be63591fef 100644 --- a/frontend/__tests__/src/hooks/useVerifyBoardEffect.test.tsx +++ b/frontend/__tests__/src/hooks/useVerifyBoardEffect.test.tsx @@ -3,6 +3,7 @@ import { useVerifyBoardEffect } from '@src/hooks/useVerifyBoardEffect' import { boardClient } from '@src/clients/board/BoardClient' import { ERROR_MESSAGE_TIME_DURATION, MOCK_BOARD_VERIFY_REQUEST_PARAMS, VERIFY_FAILED } from '../fixtures' import { InternalServerException } from '@src/exceptions/InternalServerException' +import { HttpStatusCode } from 'axios' describe('use verify board state', () => { it('should initial data state when render hook', async () => { @@ -28,7 +29,7 @@ describe('use verify board state', () => { }) it('should set error message when get verify board response status 500', async () => { boardClient.getVerifyBoard = jest.fn().mockImplementation(() => { - throw new InternalServerException('error message') + throw new InternalServerException('error message', HttpStatusCode.InternalServerError) }) const { result } = renderHook(() => useVerifyBoardEffect()) diff --git a/frontend/__tests__/src/hooks/useVerifyPipelineToolEffect.test.tsx b/frontend/__tests__/src/hooks/useVerifyPipelineToolEffect.test.tsx index 38aa4b7167..b886f708ef 100644 --- a/frontend/__tests__/src/hooks/useVerifyPipelineToolEffect.test.tsx +++ b/frontend/__tests__/src/hooks/useVerifyPipelineToolEffect.test.tsx @@ -3,6 +3,7 @@ import { useVerifyPipelineToolEffect } from '@src/hooks/useVerifyPipelineToolEff import { pipelineToolClient } from '@src/clients/pipeline/PipelineToolClient' import { ERROR_MESSAGE_TIME_DURATION, MOCK_PIPELINE_VERIFY_REQUEST_PARAMS, VERIFY_FAILED } from '../fixtures' import { InternalServerException } from '@src/exceptions/InternalServerException' +import { HttpStatusCode } from 'axios' describe('use verify pipelineTool state', () => { it('should initial data state when render hook', async () => { @@ -29,7 +30,7 @@ describe('use verify pipelineTool state', () => { it('should set error message when get verify pipeline response status 500', async () => { pipelineToolClient.verifyPipelineTool = jest.fn().mockImplementation(() => { - throw new InternalServerException('error message') + throw new InternalServerException('error message', HttpStatusCode.InternalServerError) }) const { result } = renderHook(() => useVerifyPipelineToolEffect()) diff --git a/frontend/__tests__/src/hooks/useVerifySourceControlEffect.test.tsx b/frontend/__tests__/src/hooks/useVerifySourceControlEffect.test.tsx index 54e445cb78..4db96316d4 100644 --- a/frontend/__tests__/src/hooks/useVerifySourceControlEffect.test.tsx +++ b/frontend/__tests__/src/hooks/useVerifySourceControlEffect.test.tsx @@ -3,6 +3,7 @@ import { useVerifySourceControlEffect } from '@src/hooks/useVeritySourceControlE import { sourceControlClient } from '@src/clients/sourceControl/SourceControlClient' import { ERROR_MESSAGE_TIME_DURATION, MOCK_SOURCE_CONTROL_VERIFY_REQUEST_PARAMS, VERIFY_FAILED } from '../fixtures' import { InternalServerException } from '@src/exceptions/InternalServerException' +import { HttpStatusCode } from 'axios' describe('use verify sourceControl state', () => { it('should initial data state when render hook', async () => { @@ -30,7 +31,7 @@ describe('use verify sourceControl state', () => { it('should set error message when get verify sourceControl response status 500', async () => { sourceControlClient.getVerifySourceControl = jest.fn().mockImplementation(() => { - throw new InternalServerException('error message') + throw new InternalServerException('error message', HttpStatusCode.InternalServerError) }) const { result } = renderHook(() => useVerifySourceControlEffect()) diff --git a/frontend/jest.config.json b/frontend/jest.config.json index 1d86c5b5b0..19a8c46d58 100644 --- a/frontend/jest.config.json +++ b/frontend/jest.config.json @@ -27,5 +27,5 @@ "statements": 100 } }, - "testTimeout": 10000 + "testTimeout": 50000 } diff --git a/frontend/src/clients/Httpclient.ts b/frontend/src/clients/Httpclient.ts index c90b9949e5..bf2ecaa5f9 100644 --- a/frontend/src/clients/Httpclient.ts +++ b/frontend/src/clients/Httpclient.ts @@ -25,17 +25,17 @@ export class HttpClient { const errorMessage = data?.hintInfo ?? statusText switch (status) { case HttpStatusCode.BadRequest: - throw new BadRequestException(errorMessage) + throw new BadRequestException(errorMessage, status) case HttpStatusCode.Unauthorized: - throw new UnauthorizedException(errorMessage) + throw new UnauthorizedException(errorMessage, status) case HttpStatusCode.NotFound: - throw new NotFoundException(errorMessage) + throw new NotFoundException(errorMessage, status) case HttpStatusCode.Forbidden: - throw new ForbiddenException(errorMessage) + throw new ForbiddenException(errorMessage, status) case HttpStatusCode.InternalServerError: - throw new InternalServerException(errorMessage) + throw new InternalServerException(errorMessage, status) case HttpStatusCode.ServiceUnavailable: - throw new TimeoutException(errorMessage) + throw new TimeoutException(errorMessage, status) default: throw new UnknownException() } diff --git a/frontend/src/components/HomeGuide/index.tsx b/frontend/src/components/HomeGuide/index.tsx index 56b6f4ce55..872c19f79a 100644 --- a/frontend/src/components/HomeGuide/index.tsx +++ b/frontend/src/components/HomeGuide/index.tsx @@ -1,5 +1,3 @@ -import Stack from '@mui/material/Stack' - import { useNavigate } from 'react-router-dom' import { useAppDispatch } from '@src/hooks/useAppDispatch' import { resetImportedData, updateBasicConfigState, updateProjectCreatedState } from '@src/context/config/configSlice' diff --git a/frontend/src/components/Metrics/ReportButtonGroup/style.tsx b/frontend/src/components/Metrics/ReportButtonGroup/style.tsx index 432e0c320a..6a1745465f 100644 --- a/frontend/src/components/Metrics/ReportButtonGroup/style.tsx +++ b/frontend/src/components/Metrics/ReportButtonGroup/style.tsx @@ -29,7 +29,6 @@ export const StyledExportButton = styled(Button)({ whiteSpace: 'nowrap', backgroundColor: theme.main.backgroundColor, color: theme.main.color, - fontFamily: 'Roboto', '&:hover': { ...basicButtonStyle, backgroundColor: theme.main.backgroundColor, diff --git a/frontend/src/exceptions/BadRequestException.ts b/frontend/src/exceptions/BadRequestException.ts index cc83e728bc..aaf7ae0237 100644 --- a/frontend/src/exceptions/BadRequestException.ts +++ b/frontend/src/exceptions/BadRequestException.ts @@ -1,5 +1,9 @@ -export class BadRequestException extends Error { - constructor(message: string) { +import { IHeartBeatException } from '@src/exceptions/ExceptionType' + +export class BadRequestException extends Error implements IHeartBeatException { + code: number + constructor(message: string, status: number) { super(message) + this.code = status } } diff --git a/frontend/src/exceptions/ExceptionType.ts b/frontend/src/exceptions/ExceptionType.ts new file mode 100644 index 0000000000..9b90d52236 --- /dev/null +++ b/frontend/src/exceptions/ExceptionType.ts @@ -0,0 +1,4 @@ +export interface IHeartBeatException { + code?: number + message: string +} diff --git a/frontend/src/exceptions/ForbiddenException.ts b/frontend/src/exceptions/ForbiddenException.ts index 5eb38bf330..a66db5de61 100644 --- a/frontend/src/exceptions/ForbiddenException.ts +++ b/frontend/src/exceptions/ForbiddenException.ts @@ -1,5 +1,9 @@ -export class ForbiddenException extends Error { - constructor(message: string) { +import { IHeartBeatException } from '@src/exceptions/ExceptionType' + +export class ForbiddenException extends Error implements IHeartBeatException { + code: number + constructor(message: string, status: number) { super(message) + this.code = status } } diff --git a/frontend/src/exceptions/InternalServerException.ts b/frontend/src/exceptions/InternalServerException.ts index 3f689a9492..9a7e55b3b4 100644 --- a/frontend/src/exceptions/InternalServerException.ts +++ b/frontend/src/exceptions/InternalServerException.ts @@ -1,5 +1,9 @@ -export class InternalServerException extends Error { - constructor(message: string) { +import { IHeartBeatException } from '@src/exceptions/ExceptionType' + +export class InternalServerException extends Error implements IHeartBeatException { + code: number + constructor(message: string, status: number) { super(message) + this.code = status } } diff --git a/frontend/src/exceptions/NotFoundException.ts b/frontend/src/exceptions/NotFoundException.ts index d535c8b2a5..db9766a26d 100644 --- a/frontend/src/exceptions/NotFoundException.ts +++ b/frontend/src/exceptions/NotFoundException.ts @@ -1,5 +1,9 @@ -export class NotFoundException extends Error { - constructor(message: string) { +import { IHeartBeatException } from '@src/exceptions/ExceptionType' + +export class NotFoundException extends Error implements IHeartBeatException { + code: number + constructor(message: string, status: number) { super(message) + this.code = status } } diff --git a/frontend/src/exceptions/TimeoutException.ts b/frontend/src/exceptions/TimeoutException.ts index e29690cb4e..079885e90a 100644 --- a/frontend/src/exceptions/TimeoutException.ts +++ b/frontend/src/exceptions/TimeoutException.ts @@ -1,5 +1,9 @@ -export class TimeoutException extends Error { - constructor(message: string) { +import { IHeartBeatException } from '@src/exceptions/ExceptionType' + +export class TimeoutException extends Error implements IHeartBeatException { + code: number + constructor(message: string, status: number) { super(message) + this.code = status } } diff --git a/frontend/src/exceptions/UnauthorizedException.ts b/frontend/src/exceptions/UnauthorizedException.ts index d3a4eb83dc..42a93c78b7 100644 --- a/frontend/src/exceptions/UnauthorizedException.ts +++ b/frontend/src/exceptions/UnauthorizedException.ts @@ -1,5 +1,9 @@ -export class UnauthorizedException extends Error { - constructor(message: string) { +import { IHeartBeatException } from '@src/exceptions/ExceptionType' + +export class UnauthorizedException extends Error implements IHeartBeatException { + code: number + constructor(message: string, status: number) { super(message) + this.code = status } } diff --git a/frontend/src/exceptions/UnkonwException.ts b/frontend/src/exceptions/UnkonwException.ts index db4597056a..0fe5d58209 100644 --- a/frontend/src/exceptions/UnkonwException.ts +++ b/frontend/src/exceptions/UnkonwException.ts @@ -1,6 +1,7 @@ import { MESSAGE } from '@src/constants/resources' +import { IHeartBeatException } from '@src/exceptions/ExceptionType' -export class UnknownException extends Error { +export class UnknownException extends Error implements IHeartBeatException { constructor() { super(MESSAGE.UNKNOWN_ERROR) } diff --git a/frontend/src/exceptions/index.ts b/frontend/src/exceptions/index.ts new file mode 100644 index 0000000000..1f7cb18a5c --- /dev/null +++ b/frontend/src/exceptions/index.ts @@ -0,0 +1,18 @@ +import { BadRequestException } from '@src/exceptions/BadRequestException' +import { UnauthorizedException } from '@src/exceptions/UnauthorizedException' +import { ForbiddenException } from '@src/exceptions/ForbiddenException' +import { NotFoundException } from '@src/exceptions/NotFoundException' +import { InternalServerException } from '@src/exceptions/InternalServerException' +import { TimeoutException } from '@src/exceptions/TimeoutException' +import { UnknownException } from '@src/exceptions/UnkonwException' + +export const isHeartBeatException = (o: unknown) => + [ + BadRequestException, + UnauthorizedException, + ForbiddenException, + NotFoundException, + InternalServerException, + TimeoutException, + UnknownException, + ].some((excptionClass) => o instanceof excptionClass) diff --git a/stubs/backend/jira/jsons/jira.issue.PLL-1320.activityfeed.json b/stubs/backend/jira/jsons/jira.issue.PLL-1320.activityfeed.json index 1181e81fd5..df9404a9d0 100644 --- a/stubs/backend/jira/jsons/jira.issue.PLL-1320.activityfeed.json +++ b/stubs/backend/jira/jsons/jira.issue.PLL-1320.activityfeed.json @@ -1,7 +1,7 @@ { - "total": 116, + "total": 100, "nextPageStartAt": 100, - "isLast": false, + "isLast": true, "items": [ { "actor": { diff --git a/stubs/backend/jira/jsons/jira.issue.PLL-1341.activityfeed.json b/stubs/backend/jira/jsons/jira.issue.PLL-1341.activityfeed.json index 1439ba2e59..fbaf3a8f29 100644 --- a/stubs/backend/jira/jsons/jira.issue.PLL-1341.activityfeed.json +++ b/stubs/backend/jira/jsons/jira.issue.PLL-1341.activityfeed.json @@ -1,7 +1,7 @@ { - "total": 106, + "total": 100, "nextPageStartAt": 100, - "isLast": false, + "isLast": true, "items": [ { "actor": {