Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ADM-727:[backend][docs]feat: Merge two API into one #897

Merged
merged 7 commits into from
Jan 10, 2024
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
ADM-741:[backend]feat: remove plain text token
LEI WANG committed Jan 9, 2024
commit aceabbf6438b20c6de41368ba0811d92443b0490
8 changes: 8 additions & 0 deletions backend/src/main/java/heartbeat/config/WebConfig.java
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@

import heartbeat.controller.board.dto.request.BoardType;
import heartbeat.controller.report.dto.request.DataType;
import heartbeat.controller.report.dto.request.ReportType;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.format.FormatterRegistry;
@@ -25,6 +26,13 @@ public DataType convert(String source) {
return DataType.fromValue(source);
}
});

registry.addConverter(new Converter<String, ReportType>() {
@Override
public ReportType convert(String type) {
return ReportType.fromValue(type);
}
});
}

}
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package heartbeat.controller.report;

import heartbeat.controller.report.dto.request.DataType;
import heartbeat.controller.report.dto.request.ExportCSVRequest;
import heartbeat.controller.report.dto.request.GenerateBoardReportRequest;
import heartbeat.controller.report.dto.request.ReportType;
import heartbeat.controller.report.dto.request.GenerateDoraReportRequest;
import heartbeat.controller.report.dto.request.GenerateBoardReportRequest;
import heartbeat.controller.report.dto.request.GenerateReportRequest;
import heartbeat.controller.report.dto.request.ExportCSVRequest;
import heartbeat.controller.report.dto.response.CallbackResponse;
import heartbeat.controller.report.dto.response.ReportResponse;
import heartbeat.exception.BadRequestException;
import heartbeat.exception.BaseException;
import heartbeat.handler.AsyncExceptionHandler;
import heartbeat.service.report.GenerateReporterService;
@@ -17,12 +20,12 @@
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.GetMapping;
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;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.PostMapping;

import java.util.concurrent.CompletableFuture;

@@ -62,7 +65,7 @@
return ResponseEntity.status(HttpStatus.OK).body(reportResponse);
}

@PostMapping("/board")
@PostMapping("/v1/board")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

public ResponseEntity<CallbackResponse> generateBoardReport(@RequestBody GenerateBoardReportRequest request) {
log.info(
"Start to generate board report, _metrics: {}, _considerHoliday: {}, _startTime: {}, _endTime: {}, _boardReportId: {}",
@@ -93,7 +96,7 @@
.body(CallbackResponse.builder().callbackUrl(callbackUrl).interval(interval).build());
}

@PostMapping("/dora")
@PostMapping("/v1/dora")
public ResponseEntity<CallbackResponse> generateDoraReport(@RequestBody GenerateDoraReportRequest request) {
log.info(
"Start to generate dora report, _metrics: {}, _considerHoliday: {}, _startTime: {}, _endTime: {}, _doraReportId: {}",
@@ -123,4 +126,18 @@
.body(CallbackResponse.builder().callbackUrl(callbackUrl).interval(interval).build());
}

@PostMapping("{reportType}")
public ResponseEntity<CallbackResponse> generateReport(@PathVariable ReportType reportType,
@RequestBody GenerateReportRequest request) {
CompletableFuture.runAsync(() -> {
switch (reportType) {

Check warning on line 133 in backend/src/main/java/heartbeat/controller/report/GenerateReportController.java

Codacy Production / Codacy Static Code Analysis

backend/src/main/java/heartbeat/controller/report/GenerateReportController.java#L133

Switch statements should be exhaustive, add a default case (or missing enum branches)
case BOARD -> generateReporterService.generateBoardReport(request);
case DORA -> generateReporterService.generateDoraReport(request);
}
});
String callbackUrl = "/reports/" + request.getCsvTimeStamp();
return ResponseEntity.status(HttpStatus.ACCEPTED)
.body(CallbackResponse.builder().callbackUrl(callbackUrl).interval(interval).build());
}

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package heartbeat.controller.report.dto.request;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.google.gson.Gson;
import heartbeat.util.MetricsUtil;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Builder;
@@ -35,4 +37,23 @@ public class GenerateReportRequest {
@NotBlank
private String csvTimeStamp;

public GenerateReportRequest convertToPipelineRequest(GenerateReportRequest request) {
List<String> pipelineMetrics = MetricsUtil
.getPipelineMetrics(request.getMetrics().stream().map(String::toLowerCase).toList());
Gson gson = new Gson();
GenerateReportRequest result = gson.fromJson(gson.toJson(request), GenerateReportRequest.class);

result.setMetrics(pipelineMetrics);
return result;
}

public GenerateReportRequest convertToCodeBaseRequest(GenerateReportRequest request) {
List<String> codebaseMetrics = MetricsUtil
.getCodeBaseMetrics(request.getMetrics().stream().map(String::toLowerCase).toList());
Gson gson = new Gson();
GenerateReportRequest result = gson.fromJson(gson.toJson(request), GenerateReportRequest.class);
result.setMetrics(codebaseMetrics);
return result;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package heartbeat.controller.report.dto.request;

public enum ReportType {

BOARD, DORA;

public static ReportType fromValue(String type) {
return switch (type) {
case "board" -> BOARD;
case "dora" -> DORA;
default -> throw new IllegalArgumentException("ReportType not found!");
};
}

}
Original file line number Diff line number Diff line change
@@ -71,16 +71,19 @@
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import heartbeat.util.IdUtil;
import heartbeat.util.MetricsUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import lombok.val;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.core.io.InputStreamResource;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

@@ -127,21 +130,6 @@ public class GenerateReporterService {

private final AsyncExceptionHandler asyncExceptionHandler;

private final List<String> kanbanMetrics = Stream
.of(RequireDataEnum.VELOCITY, RequireDataEnum.CYCLE_TIME, RequireDataEnum.CLASSIFICATION)
.map(RequireDataEnum::getValue)
.toList();

private final List<String> buildKiteMetrics = Stream
.of(RequireDataEnum.CHANGE_FAILURE_RATE, RequireDataEnum.DEPLOYMENT_FREQUENCY,
RequireDataEnum.MEAN_TIME_TO_RECOVERY)
.map(RequireDataEnum::getValue)
.toList();

private final List<String> codebaseMetrics = Stream.of(RequireDataEnum.LEAD_TIME_FOR_CHANGES)
.map(RequireDataEnum::getValue)
.toList();

private static StoryPointsAndCycleTimeRequest buildStoryPointsAndCycleTimeRequest(JiraBoardSetting jiraBoardSetting,
String startTime, String endTime) {
return StoryPointsAndCycleTimeRequest.builder()
@@ -213,17 +201,81 @@ else if (jsonObject.has("key")) {
return result;
}

public void generateBoardReport(GenerateReportRequest request) {
initializeMetricsDataReadyInHandler(request.getCsvTimeStamp(), request.getMetrics());

CompletableFuture.runAsync(() -> {
try {
saveReporterInHandler(generateReporter(request), IdUtil.getBoardReportId(request.getCsvTimeStamp()));
updateMetricsDataReadyInHandler(request.getCsvTimeStamp(), request.getMetrics());
log.info(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

where is the log of start to do something...

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

"Successfully generate board report, _metrics: {}, _considerHoliday: {}, _startTime: {}, _endTime: {}, _boardReportId: {}",
request.getMetrics(), request.getConsiderHoliday(), request.getStartTime(),
request.getEndTime(), IdUtil.getBoardReportId(request.getCsvTimeStamp()));
}
catch (BaseException e) {
asyncExceptionHandler.put(IdUtil.getBoardReportId(request.getCsvTimeStamp()), e);
}
});
}

public void generateDoraReport(GenerateReportRequest request) {
MetricsDataReady metricsDataStatus = getMetricsStatus(request.getMetrics(), Boolean.TRUE);
initializeMetricsDataReadyInHandler(request.getCsvTimeStamp(), request.getMetrics());
if (Objects.nonNull(metricsDataStatus.isSourceControlMetricsReady())
&& metricsDataStatus.isSourceControlMetricsReady()) {
generateCodeBaseReport(request);
}
if (Objects.nonNull(metricsDataStatus.isPipelineMetricsReady()) && metricsDataStatus.isPipelineMetricsReady()) {
generatePipelineReport(request);
}
generateCsvForDora(request);
}

private void generatePipelineReport(GenerateReportRequest request) {
CompletableFuture.runAsync(() -> {
try {
GenerateReportRequest pipelineRequest = request.convertToPipelineRequest(request);
saveReporterInHandler(generateReporter(pipelineRequest),
IdUtil.getDoraReportId(pipelineRequest.getCsvTimeStamp()));
updateMetricsDataReadyInHandler(pipelineRequest.getCsvTimeStamp(), pipelineRequest.getMetrics());
log.info(
"Successfully generate pipeline report, _metrics: {}, _considerHoliday: {}, _startTime: {}, _endTime: {}, _pipelineReportId: {}",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

where is the start to do something,....

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

pipelineRequest.getMetrics(), pipelineRequest.getConsiderHoliday(),
pipelineRequest.getStartTime(), pipelineRequest.getEndTime(),
IdUtil.getDoraReportId(request.getCsvTimeStamp()));
}
catch (BaseException e) {
asyncExceptionHandler.put(IdUtil.getDoraReportId(request.getCsvTimeStamp()), e);
}
});
}

private void generateCodeBaseReport(GenerateReportRequest request) {
CompletableFuture.runAsync(() -> {
try {
GenerateReportRequest codebaseRequest = request.convertToCodeBaseRequest(request);
saveReporterInHandler(generateReporter(codebaseRequest),
IdUtil.getCodeBaseReportId(codebaseRequest.getCsvTimeStamp()));
updateMetricsDataReadyInHandler(codebaseRequest.getCsvTimeStamp(), codebaseRequest.getMetrics());
log.info(
"Successfully generate codebase report, _metrics: {}, _considerHoliday: {}, _startTime: {}, _endTime: {}, _codeBaseReportId: {}",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

where is the start to do something,....

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

codebaseRequest.getMetrics(), codebaseRequest.getConsiderHoliday(),
codebaseRequest.getStartTime(), codebaseRequest.getEndTime(),
IdUtil.getCodeBaseReportId(request.getCsvTimeStamp()));
}
catch (BaseException e) {
asyncExceptionHandler.put(IdUtil.getCodeBaseReportId(request.getCsvTimeStamp()), e);
}
});
}

public synchronized ReportResponse generateReporter(GenerateReportRequest request) {
workDay.changeConsiderHolidayMode(request.getConsiderHoliday());
// fetch data for calculate
List<String> lowMetrics = request.getMetrics().stream().map(String::toLowerCase).toList();
FetchedData fetchedData = fetchOriginalData(request, lowMetrics);

if (lowMetrics.stream().anyMatch(this.codebaseMetrics::contains)
|| lowMetrics.stream().anyMatch(this.buildKiteMetrics::contains)) {
generateCSVForPipeline(request, fetchedData.getBuildKiteData());
}

ReportResponse reportResponse = new ReportResponse(EXPORT_CSV_VALIDITY_TIME);
JiraBoardSetting jiraBoardSetting = request.getJiraBoardSetting();

@@ -258,21 +310,21 @@ public synchronized ReportResponse generateReporter(GenerateReportRequest reques
private FetchedData fetchOriginalData(GenerateReportRequest request, List<String> lowMetrics) {
FetchedData fetchedData = new FetchedData();

if (lowMetrics.stream().anyMatch(this.kanbanMetrics::contains)) {
if (CollectionUtils.isNotEmpty(MetricsUtil.getBoardMetrics(lowMetrics))) {
if (request.getJiraBoardSetting() == null)
throw new BadRequestException("Failed to fetch Jira info due to Jira board setting is null.");
CardCollectionInfo cardCollectionInfo = fetchDataFromKanban(request);
fetchedData.setCardCollectionInfo(cardCollectionInfo);
}

if (lowMetrics.stream().anyMatch(this.codebaseMetrics::contains)) {
if (CollectionUtils.isNotEmpty(MetricsUtil.getCodeBaseMetrics(lowMetrics))) {
if (request.getCodebaseSetting() == null)
throw new BadRequestException("Failed to fetch Github info due to code base setting is null.");
BuildKiteData buildKiteData = fetchGithubData(request);
fetchedData.setBuildKiteData(buildKiteData);
}

if (lowMetrics.stream().anyMatch(this.buildKiteMetrics::contains)) {
if (CollectionUtils.isNotEmpty(MetricsUtil.getPipelineMetrics(lowMetrics))) {
if (request.getBuildKiteSetting() == null)
throw new BadRequestException("Failed to fetch BuildKite info due to BuildKite setting is null.");
FetchedData.BuildKiteData buildKiteData = fetchBuildKiteInfo(request);
@@ -287,6 +339,14 @@ private FetchedData fetchOriginalData(GenerateReportRequest request, List<String
return fetchedData;
}

public void generateCsvForDora(GenerateReportRequest request) {
List<String> lowMetrics = request.getMetrics().stream().map(String::toLowerCase).toList();
FetchedData fetchedData = fetchOriginalData(request, lowMetrics);

generateCSVForPipeline(request, fetchedData.getBuildKiteData());

}

private CardCollection fetchRealDoneCardCollection(GenerateReportRequest request) {
JiraBoardSetting jiraBoardSetting = request.getJiraBoardSetting();
StoryPointsAndCycleTimeRequest storyPointsAndCycleTimeRequest = buildStoryPointsAndCycleTimeRequest(
@@ -638,13 +698,10 @@ private Boolean checkCurrentMetricsReadyState(Boolean exist, Boolean previousVal
}

private MetricsDataReady getMetricsStatus(List<String> metrics, Boolean flag) {
boolean boardMetricsExist = metrics.stream().map(String::toLowerCase).anyMatch(this.kanbanMetrics::contains);
boolean codebaseMetricsExist = metrics.stream()
.map(String::toLowerCase)
.anyMatch(this.codebaseMetrics::contains);
boolean buildKiteMetricsExist = metrics.stream()
.map(String::toLowerCase)
.anyMatch(this.buildKiteMetrics::contains);
List<String> lowerMetrics = metrics.stream().map(String::toLowerCase).collect(Collectors.toList());
boolean boardMetricsExist = CollectionUtils.isNotEmpty(MetricsUtil.getBoardMetrics(lowerMetrics));
boolean codebaseMetricsExist = CollectionUtils.isNotEmpty(MetricsUtil.getCodeBaseMetrics(lowerMetrics));
boolean buildKiteMetricsExist = CollectionUtils.isNotEmpty(MetricsUtil.getPipelineMetrics(lowerMetrics));
Boolean isBoardMetricsReady = boardMetricsExist ? flag : null;
Boolean isCodebaseMetricsReady = codebaseMetricsExist ? flag : null;
Boolean isBuildKiteMetricsReady = buildKiteMetricsExist ? flag : null;
@@ -691,10 +748,6 @@ private List<PipelineCSVInfo> generateCSVForPipelineWithCodebase(CodebaseSetting
String endTime, BuildKiteData buildKiteData, List<DeploymentEnvironment> deploymentEnvironments) {
List<PipelineCSVInfo> pipelineCSVInfos = new ArrayList<>();

if (codebaseSetting == null && CollectionUtils.isEmpty(deploymentEnvironments)) {
return pipelineCSVInfos;
}

Map<String, String> repoIdMap = getRepoMap(deploymentEnvironments);
for (DeploymentEnvironment deploymentEnvironment : deploymentEnvironments) {
String repoId = GithubUtil.getGithubUrlFullName(repoIdMap.get(deploymentEnvironment.getId()));
@@ -806,6 +859,7 @@ public ReportResponse getReportFromHandler(String reportId) {
public ReportResponse getComposedReportResponse(String reportId, boolean isReportReady) {
ReportResponse boardReportResponse = getReportFromHandler(IdUtil.getBoardReportId(reportId));
ReportResponse doraReportResponse = getReportFromHandler(IdUtil.getDoraReportId(reportId));
ReportResponse codebaseReportResponse = getReportFromHandler(IdUtil.getCodeBaseReportId(reportId));
MetricsDataReady metricsDataReady = asyncReportRequestHandler.getMetricsDataReady(reportId);
ReportResponse response = Optional.ofNullable(boardReportResponse).orElse(doraReportResponse);

@@ -817,7 +871,7 @@ public ReportResponse getComposedReportResponse(String reportId, boolean isRepor
.deploymentFrequency(getValueOrNull(doraReportResponse, ReportResponse::getDeploymentFrequency))
.changeFailureRate(getValueOrNull(doraReportResponse, ReportResponse::getChangeFailureRate))
.meanTimeToRecovery(getValueOrNull(doraReportResponse, ReportResponse::getMeanTimeToRecovery))
.leadTimeForChanges(getValueOrNull(doraReportResponse, ReportResponse::getLeadTimeForChanges))
.leadTimeForChanges(getValueOrNull(codebaseReportResponse, ReportResponse::getLeadTimeForChanges))
.isBoardMetricsReady(getValueOrNull(metricsDataReady, MetricsDataReady::isBoardMetricsReady))
.isPipelineMetricsReady(getValueOrNull(metricsDataReady, MetricsDataReady::isPipelineMetricsReady))
.isSourceControlMetricsReady(
6 changes: 6 additions & 0 deletions backend/src/main/java/heartbeat/util/IdUtil.java
Original file line number Diff line number Diff line change
@@ -6,6 +6,8 @@ public interface IdUtil {

String DORA_REPORT_PREFIX = "dora-";

String CODE_BASE_PREFIX = "github-";

static String getBoardReportId(String timeStamp) {
return BOARD_REPORT_PREFIX + timeStamp;
}
@@ -14,6 +16,10 @@ static String getDoraReportId(String timeStamp) {
return DORA_REPORT_PREFIX + timeStamp;
}

static String getCodeBaseReportId(String timeStamp) {
return CODE_BASE_PREFIX + timeStamp;
}

static String getTimeStampFromReportId(String reportId) {
String[] splitResult = reportId.split("\\s*\\-|\\.\\s*");
return splitResult[1];
39 changes: 39 additions & 0 deletions backend/src/main/java/heartbeat/util/MetricsUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package heartbeat.util;

import heartbeat.controller.report.dto.request.RequireDataEnum;

import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public interface MetricsUtil {

List<String> kanbanMetrics = Stream
.of(RequireDataEnum.VELOCITY, RequireDataEnum.CYCLE_TIME, RequireDataEnum.CLASSIFICATION)
.map(RequireDataEnum::getValue)
.toList();

List<String> buildKiteMetrics = Stream
.of(RequireDataEnum.CHANGE_FAILURE_RATE, RequireDataEnum.DEPLOYMENT_FREQUENCY,
RequireDataEnum.MEAN_TIME_TO_RECOVERY)
.map(RequireDataEnum::getValue)
.toList();

List<String> codebaseMetrics = Stream.of(RequireDataEnum.LEAD_TIME_FOR_CHANGES)
.map(RequireDataEnum::getValue)
.toList();

static List<String> getPipelineMetrics(List<String> metrics) {
return metrics.stream().filter(buildKiteMetrics::contains).collect(Collectors.toList());
}

static List<String> getCodeBaseMetrics(List<String> metrics) {
return metrics.stream().filter(codebaseMetrics::contains).collect(Collectors.toList());

}

static List<String> getBoardMetrics(List<String> metrics) {
return metrics.stream().filter(kanbanMetrics::contains).collect(Collectors.toList());
}

}
Original file line number Diff line number Diff line change
@@ -5,6 +5,7 @@
import heartbeat.controller.report.dto.request.ExportCSVRequest;
import heartbeat.controller.report.dto.request.GenerateBoardReportRequest;
import heartbeat.controller.report.dto.request.GenerateDoraReportRequest;
import heartbeat.controller.report.dto.request.GenerateReportRequest;
import heartbeat.controller.report.dto.response.AvgDeploymentFrequency;
import heartbeat.controller.report.dto.response.DeploymentFrequency;
import heartbeat.controller.report.dto.response.ReportResponse;
@@ -28,7 +29,6 @@

import java.io.ByteArrayInputStream;
import java.io.File;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
@@ -161,7 +161,7 @@ void shouldReturnAcceptedStatusAndCallbackUrlAndIntervalWhenCallBoardReports() t
doNothing().when(generateReporterService).updateMetricsDataReadyInHandler(any(), any());

MockHttpServletResponse response = mockMvc
.perform(post("/reports/board").contentType(MediaType.APPLICATION_JSON)
.perform(post("/reports/v1/board").contentType(MediaType.APPLICATION_JSON)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove v1

is there any design for the new endpoint? i think we should follow the design

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

.content(mapper.writeValueAsString(request)))
.andExpect(status().isAccepted())
.andReturn()
@@ -189,7 +189,7 @@ void shouldGetExceptionAndPutInExceptionMapWhenCallBoardReport() throws Exceptio
doNothing().when(generateReporterService).updateMetricsDataReadyInHandler(any(), any());

MockHttpServletResponse response = mockMvc
.perform(post("/reports/board").contentType(MediaType.APPLICATION_JSON)
.perform(post("/reports/v1/board").contentType(MediaType.APPLICATION_JSON)
.content(mapper.writeValueAsString(request)))
.andExpect(status().isAccepted())
.andReturn()
@@ -201,12 +201,12 @@ void shouldGetExceptionAndPutInExceptionMapWhenCallBoardReport() throws Exceptio
assertEquals("10", interval);

Thread.sleep(2000L);
verify(generateReporterService).initializeMetricsDataReadyInHandler(request.getCsvTimeStamp(),
verify(generateReporterService, times(1)).initializeMetricsDataReadyInHandler(request.getCsvTimeStamp(),
request.getMetrics());
verify(generateReporterService, times(0)).saveReporterInHandler(any(), any());
verify(generateReporterService, times(0)).updateMetricsDataReadyInHandler(request.getCsvTimeStamp(),
request.getMetrics());
verify(asyncExceptionHandler).put(IdUtil.getBoardReportId(currentTimeStamp), requestFailedException);
verify(asyncExceptionHandler, times(1)).put(IdUtil.getBoardReportId(currentTimeStamp), requestFailedException);
}

@Test
@@ -225,7 +225,7 @@ void shouldGetExceptionAndPutInExceptionMapWhenCallDoraReport() throws Exception
doNothing().when(generateReporterService).updateMetricsDataReadyInHandler(any(), any());

MockHttpServletResponse response = mockMvc
.perform(post("/reports/dora").contentType(MediaType.APPLICATION_JSON)
.perform(post("/reports/v1/dora").contentType(MediaType.APPLICATION_JSON)
.content(mapper.writeValueAsString(request)))
.andExpect(status().isAccepted())
.andReturn()
@@ -237,12 +237,12 @@ void shouldGetExceptionAndPutInExceptionMapWhenCallDoraReport() throws Exception
assertEquals("10", interval);

Thread.sleep(2000L);
verify(generateReporterService).initializeMetricsDataReadyInHandler(request.getCsvTimeStamp(),
verify(generateReporterService, times(1)).initializeMetricsDataReadyInHandler(request.getCsvTimeStamp(),
request.getMetrics());
verify(generateReporterService, times(0)).saveReporterInHandler(any(), any());
verify(generateReporterService, times(0)).updateMetricsDataReadyInHandler(request.getCsvTimeStamp(),
request.getMetrics());
verify(asyncExceptionHandler).put(IdUtil.getDoraReportId(currentTimeStamp), requestFailedException);
verify(asyncExceptionHandler, times(1)).put(IdUtil.getDoraReportId(currentTimeStamp), requestFailedException);
}

@Test
@@ -265,13 +265,60 @@ void shouldReturnAcceptedStatusAndCallbackUrlAndIntervalWhenCallDoraReports() th

when(generateReporterService.generateReporter(request.convertToReportRequest())).thenReturn(expectedResponse);

MockHttpServletResponse response = mockMvc
.perform(post("/reports/v1/dora").contentType(MediaType.APPLICATION_JSON)
.content(mapper.writeValueAsString(request)))
.andExpect(status().isAccepted())
.andReturn()
.getResponse();

final var callbackUrl = JsonPath.parse(response.getContentAsString()).read("$.callbackUrl").toString();
final var interval = JsonPath.parse(response.getContentAsString()).read("$.interval").toString();
assertEquals("/reports/" + currentTimeStamp, callbackUrl);
assertEquals("10", interval);
}

@Test
void shouldReturnCallBackUrlWithAcceptedStatusAndInvokeGenerateBoardReportWhenReportTypeIsBoard() throws Exception {
ObjectMapper mapper = new ObjectMapper();
GenerateReportRequest request = mapper.readValue(new File(REQUEST_FILE_PATH), GenerateReportRequest.class);
String currentTimeStamp = "1685010080107";
request.setCsvTimeStamp(currentTimeStamp);

MockHttpServletResponse response = mockMvc
.perform(post("/reports/dora").contentType(MediaType.APPLICATION_JSON)
.content(mapper.writeValueAsString(request)))
.andExpect(status().isAccepted())
.andReturn()
.getResponse();

Thread.sleep(2000);
verify(generateReporterService, times(1)).generateDoraReport(request);
verify(generateReporterService, times(0)).generateBoardReport(request);

final var callbackUrl = JsonPath.parse(response.getContentAsString()).read("$.callbackUrl").toString();
final var interval = JsonPath.parse(response.getContentAsString()).read("$.interval").toString();
assertEquals("/reports/" + currentTimeStamp, callbackUrl);
assertEquals("10", interval);
}

@Test
void shouldReturnCallBackUrlWithAcceptedStatusAndInvokeGenerateDoraReportWhenReportTypeIsBoard() throws Exception {
ObjectMapper mapper = new ObjectMapper();
GenerateReportRequest request = mapper.readValue(new File(REQUEST_FILE_PATH), GenerateReportRequest.class);
String currentTimeStamp = "1685010080107";
request.setCsvTimeStamp(currentTimeStamp);

MockHttpServletResponse response = mockMvc
.perform(post("/reports/board").contentType(MediaType.APPLICATION_JSON)
.content(mapper.writeValueAsString(request)))
.andExpect(status().isAccepted())
.andReturn()
.getResponse();

verify(generateReporterService, times(0)).generateDoraReport(request);
verify(generateReporterService, times(1)).generateBoardReport(request);

final var callbackUrl = JsonPath.parse(response.getContentAsString()).read("$.callbackUrl").toString();
final var interval = JsonPath.parse(response.getContentAsString()).read("$.interval").toString();
assertEquals("/reports/" + currentTimeStamp, callbackUrl);
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package heartbeat.controller.report.dto.request;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.assertEquals;

class ReportTypeTest {

@Test
public void shouldConvertValueToType() {
ReportType boardType = ReportType.fromValue("board");
ReportType doraType = ReportType.fromValue("dora");

assertEquals(boardType, ReportType.BOARD);
assertEquals(doraType, ReportType.DORA);
}

@Test
public void shouldThrowExceptionWhenDateTypeNotSupported() {
assertThatThrownBy(() -> ReportType.fromValue("unknown")).isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("ReportType not found!");
}

}
Original file line number Diff line number Diff line change
@@ -9,6 +9,7 @@
],
"startTime": 1661702400000,
"endTime": 1662739199000,
"considerHoliday": true,
"buildKiteSetting": {
"type": "BuildKite",
"token": "0d6*********************************a57a",
@@ -36,6 +37,11 @@
}
]
},
"codebaseSetting": {
"type": "GitHub",
"token": "ghp*********************************FX",
"leadTime": []
},
"jiraBoardSetting": {
"type": "classic jira",
"token": "Basic exxxxxhhbmdxxxxvYixxxxxxZzlxxxxxx3Rqaxxxxxlodxxxxx9BRUU3",
Original file line number Diff line number Diff line change
@@ -103,13 +103,16 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;

import static heartbeat.TestFixtures.BUILDKITE_TOKEN;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import static heartbeat.TestFixtures.BUILDKITE_TOKEN;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.doThrow;
import static org.mockito.internal.verification.VerificationModeFactory.times;

@ExtendWith(MockitoExtension.class)
@@ -118,12 +121,19 @@ class GenerateReporterServiceTest {

public static final String SITE_ATLASSIAN_NET = "https://site.atlassian.net";

private static final String REQUEST_FILE_PATH = "src/test/java/heartbeat/controller/report/request.json";

private static final String RESPONSE_FILE_PATH = "src/test/java/heartbeat/controller/report/reportResponse.json";

@InjectMocks
GenerateReporterService generateReporterService;

@Mock
JiraService jiraService;

@Mock
IdUtil idUtil;

@Mock
WorkDay workDay;

@@ -577,7 +587,7 @@ void shouldGenerateCsvForPipelineWithPipelineMetricAndBuildInfoIsEmpty() throws
return null;
}).when(csvFileGenerator).convertPipelineDataToCSV(any(), any());

generateReporterService.generateReporter(request);
generateReporterService.generateDoraReport(request);

boolean isExists = Files.exists(csvFilePath);
Assertions.assertTrue(isExists);
@@ -627,7 +637,7 @@ void shouldGenerateCsvForPipelineWithPipelineMetric() throws IOException {
return null;
}).when(csvFileGenerator).convertPipelineDataToCSV(any(), any());

generateReporterService.generateReporter(request);
generateReporterService.generateDoraReport(request);

boolean isExists = Files.exists(mockPipelineCsvPath);
Assertions.assertTrue(isExists);
@@ -673,7 +683,7 @@ void shouldNotGenerateCsvForPipelineWithPipelineLeadTimeIsNull() throws IOExcept
when(gitHubService.fetchPipelinesLeadTime(any(), any(), any())).thenReturn(null);
when(buildKiteService.getStepsBeforeEndStep(any(), any())).thenReturn(List.of("xx"));

generateReporterService.generateReporter(request);
generateReporterService.generateDoraReport(request);

boolean isExists = Files.exists(mockPipelineCsvPath);
Assertions.assertFalse(isExists);
@@ -718,7 +728,7 @@ void shouldNotGenerateCsvForPipelineWithCommitIsNull() throws IOException {
when(gitHubService.fetchPipelinesLeadTime(any(), any(), any())).thenReturn(null);
when(buildKiteService.getStepsBeforeEndStep(any(), any())).thenReturn(List.of("xx"));

generateReporterService.generateReporter(request);
generateReporterService.generateDoraReport(request);

boolean isExists = Files.exists(mockPipelineCsvPath);
Assertions.assertFalse(isExists);
@@ -762,7 +772,7 @@ void shouldNotGenerateCsvForPipeline() throws IOException {
when(gitHubService.fetchPipelinesLeadTime(any(), any(), any())).thenReturn(null);
when(buildKiteService.getStepsBeforeEndStep(any(), any())).thenReturn(List.of("xx"));

generateReporterService.generateReporter(request);
generateReporterService.generateDoraReport(request);

boolean isExists = Files.exists(mockPipelineCsvPath);
Assertions.assertFalse(isExists);
@@ -1266,6 +1276,126 @@ void shouldPutReportInHandlerWhenCallSaveReporterInHandler() throws IOException
verify(asyncReportRequestHandler, times(1)).putReport(reportId, reportResponse);
}

@Test
void shouldPutBoardReportIntoHandlerWhenCallGenerateBoardReport() throws IOException, InterruptedException {
ObjectMapper mapper = new ObjectMapper();
GenerateReportRequest reportRequest = mapper.readValue(new File(REQUEST_FILE_PATH),
GenerateReportRequest.class);
ReportResponse reportResponse = mapper.readValue(new File(RESPONSE_FILE_PATH), ReportResponse.class);
reportRequest.setCsvTimeStamp("1683734399999");
MetricsDataReady previousMetricsReady = MetricsDataReady.builder()
.isBoardMetricsReady(true)
.isPipelineMetricsReady(false)
.isSourceControlMetricsReady(false)
.build();
GenerateReporterService spyGenerateReporterService = spy(generateReporterService);

doReturn(reportResponse).when(spyGenerateReporterService).generateReporter(reportRequest);
when(asyncReportRequestHandler.getMetricsDataReady(reportRequest.getCsvTimeStamp()))
.thenReturn(previousMetricsReady);

spyGenerateReporterService.generateBoardReport(reportRequest);

Thread.sleep(2000);
verify(spyGenerateReporterService, times(1)).generateReporter(reportRequest);
verify(spyGenerateReporterService, times(1))
.initializeMetricsDataReadyInHandler(reportRequest.getCsvTimeStamp(), reportRequest.getMetrics());
verify(spyGenerateReporterService, times(1)).saveReporterInHandler(
spyGenerateReporterService.generateReporter(reportRequest), reportRequest.getCsvTimeStamp());
verify(spyGenerateReporterService, times(1)).updateMetricsDataReadyInHandler(reportRequest.getCsvTimeStamp(),
reportRequest.getMetrics());
}

@Test
void shouldPutExceptionInHandlerWhenCallGenerateBoardReportThrowException()
throws IOException, InterruptedException {
ObjectMapper mapper = new ObjectMapper();
GenerateReportRequest reportRequest = mapper.readValue(new File(REQUEST_FILE_PATH),
GenerateReportRequest.class);
ReportResponse reportResponse = mapper.readValue(new File(RESPONSE_FILE_PATH), ReportResponse.class);
reportRequest.setCsvTimeStamp("1683734399999");
String boardTimeStamp = "board-1683734399999";
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is 1683734399999?
can we refactor with a readable name like board-20240109232359, even add milliseconds

must be changed with this comment

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

GenerateReporterService spyGenerateReporterService = spy(generateReporterService);
GenerateReportException e = new GenerateReportException(
"Failed to update metrics data ready through this timestamp.");

doReturn(reportResponse).when(spyGenerateReporterService).generateReporter(reportRequest);
doThrow(e).when(spyGenerateReporterService).updateMetricsDataReadyInHandler(any(), any());
when(asyncReportRequestHandler.getMetricsDataReady(reportRequest.getCsvTimeStamp())).thenReturn(null);

spyGenerateReporterService.generateBoardReport(reportRequest);

Thread.sleep(2000L);
verify(spyGenerateReporterService, times(1))
.initializeMetricsDataReadyInHandler(reportRequest.getCsvTimeStamp(), reportRequest.getMetrics());
verify(spyGenerateReporterService, times(1))
.saveReporterInHandler(spyGenerateReporterService.generateReporter(reportRequest), boardTimeStamp);
verify(asyncExceptionHandler, times(1)).put(boardTimeStamp, e);
}

@Test
void shouldGeneratePipelineReportAndUpdatePipelineMetricsReadyWhenCallGeneratePipelineReport()
throws IOException, InterruptedException {
ObjectMapper mapper = new ObjectMapper();
GenerateReportRequest reportRequest = mapper.readValue(new File(REQUEST_FILE_PATH),
GenerateReportRequest.class);
reportRequest.setMetrics(List.of("Deployment frequency"));
ReportResponse reportResponse = mapper.readValue(new File(RESPONSE_FILE_PATH), ReportResponse.class);
GenerateReporterService spyGenerateReporterService = spy(generateReporterService);
reportRequest.setCsvTimeStamp("1683734399999");
String doraTimeStamp = "dora-1683734399999";
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as board

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

MetricsDataReady previousMetricsReady = MetricsDataReady.builder()
.isBoardMetricsReady(null)
.isPipelineMetricsReady(false)
.isSourceControlMetricsReady(false)
.build();

doReturn(reportResponse).when(spyGenerateReporterService).generateReporter(reportRequest);
when(asyncReportRequestHandler.getMetricsDataReady(reportRequest.getCsvTimeStamp()))
.thenReturn(previousMetricsReady);

spyGenerateReporterService.generateDoraReport(reportRequest);

Thread.sleep(2000);
verify(spyGenerateReporterService, times(1)).generateReporter(any());
verify(spyGenerateReporterService, times(1)).initializeMetricsDataReadyInHandler(any(), any());
verify(spyGenerateReporterService, times(1))
.saveReporterInHandler(spyGenerateReporterService.generateReporter(any()), doraTimeStamp);
verify(spyGenerateReporterService, times(1)).updateMetricsDataReadyInHandler(any(), any());
}

@Test
void shouldGenerateCodebaseReportAndUpdateCodebaseMetricsReadyWhenCallGeneratePipelineReport()
throws IOException, InterruptedException {
ObjectMapper mapper = new ObjectMapper();
GenerateReportRequest reportRequest = mapper.readValue(new File(REQUEST_FILE_PATH),
GenerateReportRequest.class);
reportRequest.setMetrics(List.of("Lead time for changes"));
ReportResponse reportResponse = mapper.readValue(new File(RESPONSE_FILE_PATH), ReportResponse.class);
GenerateReporterService spyGenerateReporterService = spy(generateReporterService);
reportRequest.setCsvTimeStamp("1683734399999");
String codebaseTimeStamp = "github-1683734399999";
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as board

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

MetricsDataReady previousMetricsReady = MetricsDataReady.builder()
.isBoardMetricsReady(null)
.isPipelineMetricsReady(false)
.isSourceControlMetricsReady(false)
.build();

doReturn(reportResponse).when(spyGenerateReporterService).generateReporter(reportRequest);
when(asyncReportRequestHandler.getMetricsDataReady(reportRequest.getCsvTimeStamp()))
.thenReturn(previousMetricsReady);

spyGenerateReporterService.generateDoraReport(reportRequest);

Thread.sleep(2000);
verify(spyGenerateReporterService, times(1)).generateReporter(any());
verify(spyGenerateReporterService, times(1)).initializeMetricsDataReadyInHandler(any(), any());
verify(spyGenerateReporterService, times(1))
.saveReporterInHandler(spyGenerateReporterService.generateReporter(any()), codebaseTimeStamp);
verify(spyGenerateReporterService, times(1)).updateMetricsDataReadyInHandler(any(), any());

}

private JiraBoardSetting buildJiraBoardSetting() {
return JiraBoardSetting.builder()
.treatFlagCardAsBlock(true)

Unchanged files with check annotations Beta

import { getByText, queryByText, render, waitFor } from '@testing-library/react'

Check warning on line 1 in frontend/__tests__/src/components/Metrics/ReportStep/ExpiredDialog.test.tsx

GitHub Actions / frontend-check

'getByText' is defined but never used

Check warning on line 1 in frontend/__tests__/src/components/Metrics/ReportStep/ExpiredDialog.test.tsx

GitHub Actions / frontend-check

'queryByText' is defined but never used

Check warning on line 1 in frontend/__tests__/src/components/Metrics/ReportStep/ExpiredDialog.test.tsx

GitHub Actions / frontend-check

'getByText' is defined but never used

Check warning on line 1 in frontend/__tests__/src/components/Metrics/ReportStep/ExpiredDialog.test.tsx

GitHub Actions / frontend-check

'queryByText' is defined but never used
import { setupStore } from '../../../utils/setupStoreUtil'
import { ExpiredDialog } from '@src/components/Metrics/ReportStep/ExpiredDialog'
import { Provider } from 'react-redux'
import React from 'react'
import { useExportCsvEffect } from '@src/hooks/useExportCsvEffect'
import { MESSAGE } from '@src/constants/resources'
import { formatMillisecondsToHours } from '@src/utils/util'

Check warning on line 31 in frontend/__tests__/src/components/Metrics/ReportStep/ReportStep.test.tsx

GitHub Actions / frontend-check

'formatMillisecondsToHours' is defined but never used

Check warning on line 31 in frontend/__tests__/src/components/Metrics/ReportStep/ReportStep.test.tsx

GitHub Actions / frontend-check

'formatMillisecondsToHours' is defined but never used
jest.mock('@src/context/stepper/StepperSlice', () => ({
...jest.requireActual('@src/context/stepper/StepperSlice'),
// <reference types="cypress" />
import fs = require('fs')
module.exports = (on, config) => {

Check warning on line 4 in frontend/cypress/plugins/clearDownloadFile.ts

GitHub Actions / frontend-check

'config' is defined but never used

Check warning on line 4 in frontend/cypress/plugins/clearDownloadFile.ts

GitHub Actions / frontend-check

'config' is defined but never used
on('task', {
clearDownloads: () => {
const downloadsFolder = 'cypress/downloads'
})
}
const [_, __, ...args] = process.argv

Check warning on line 141 in frontend/scripts/runE2eWithServer.ts

GitHub Actions / frontend-check

'_' is assigned a value but never used

Check warning on line 141 in frontend/scripts/runE2eWithServer.ts

GitHub Actions / frontend-check

'__' is assigned a value but never used

Check warning on line 141 in frontend/scripts/runE2eWithServer.ts

GitHub Actions / frontend-check

'_' is assigned a value but never used

Check warning on line 141 in frontend/scripts/runE2eWithServer.ts

GitHub Actions / frontend-check

'__' is assigned a value but never used
main(args)
optionList: string[]
selectedOption: string[]
// There is an any because m-ui strictly define its type, but the parameters are not that strict. Maybe because of version diff
onChangeHandler: any

Check warning on line 10 in frontend/src/components/Common/MultiAutoComplete/index.tsx

GitHub Actions / frontend-check

Unexpected any. Specify a different type

Check warning on line 10 in frontend/src/components/Common/MultiAutoComplete/index.tsx

GitHub Actions / frontend-check

Unexpected any. Specify a different type
isSelectAll: boolean
textFieldLabel: string
isError: boolean
fontSize: '1rem',
fontWeight: '250',
fontFamily: theme.typography.fontFamily,
backgroundColor: theme.components!.tip.color,

Check warning on line 17 in frontend/src/components/Common/NotificationButton/style.tsx

GitHub Actions / frontend-check

Forbidden non-null assertion

Check warning on line 17 in frontend/src/components/Common/NotificationButton/style.tsx

GitHub Actions / frontend-check

Forbidden non-null assertion
'& .MuiTooltip-arrow': {
color: theme.components!.tip.color,

Check warning on line 19 in frontend/src/components/Common/NotificationButton/style.tsx

GitHub Actions / frontend-check

Forbidden non-null assertion

Check warning on line 19 in frontend/src/components/Common/NotificationButton/style.tsx

GitHub Actions / frontend-check

Forbidden non-null assertion
},
zIndex: Z_INDEX.TOOLTIP,
}
import Stack from '@mui/material/Stack'

Check warning on line 1 in frontend/src/components/HomeGuide/index.tsx

GitHub Actions / frontend-check

'Stack' is defined but never used

Check warning on line 1 in frontend/src/components/HomeGuide/index.tsx

GitHub Actions / frontend-check

'Stack' is defined but never used
import { useNavigate } from 'react-router-dom'
import { useAppDispatch } from '@src/hooks/useAppDispatch'