From 72ea8c4e7f69333a970289a6b60da12d4d37c96f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 24 May 2026 00:11:50 +0000 Subject: [PATCH] Count and sort projects by open issues only Agent-Logs-Url: https://github.com/conorheffron/ironoc/sessions/14e51559-eece-4bdb-80e4-1dbab4a8143c Co-authored-by: conorheffron <8218626+conorheffron@users.noreply.github.com> --- .../controller/GitProjectsController.java | 15 +++++- .../GitProjectsControllerIntegrationTest.java | 47 +++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/src/main/java/net/ironoc/portfolio/controller/GitProjectsController.java b/src/main/java/net/ironoc/portfolio/controller/GitProjectsController.java index 89fc1f90..823abe30 100644 --- a/src/main/java/net/ironoc/portfolio/controller/GitProjectsController.java +++ b/src/main/java/net/ironoc/portfolio/controller/GitProjectsController.java @@ -30,6 +30,7 @@ public class GitProjectsController extends AbstractLogger { private final GitDetailsService gitDetailsService; protected static final String IRONOC_GIT_USER = "conorheffron"; + private static final String OPEN_ISSUE_STATE = "open"; // Cache for issue counts: key is "username/repo" private final ConcurrentHashMap issueCountCache = new ConcurrentHashMap<>(); @@ -104,7 +105,7 @@ private ResponseEntity> getReposSortedAndWithIssueC Integer cachedCount = issueCountCache.get(cacheKey); if (cachedCount == null) { List issues = gitDetailsService.getIssues(userId, domain.getName(), false); - cachedCount = issues != null ? issues.size() : 0; + cachedCount = countOpenIssues(issues); issueCountCache.put(cacheKey, cachedCount); } domain.setIssueCount(cachedCount); @@ -152,6 +153,18 @@ private List sanitizeValues(String... values) { return List.of(sanitizedValueUserId, sanitizedValueRepo); } + private int countOpenIssues(List issues) { + if (issues == null) { + return 0; + } + return (int) issues.stream() + .filter(Objects::nonNull) + .map(RepositoryIssueDto::getState) + .filter(Objects::nonNull) + .filter(OPEN_ISSUE_STATE::equalsIgnoreCase) + .count(); + } + private String sanitizeValue(String value) { // trim leading and trailing whitespace String sanitizedValue = value.trim(); diff --git a/src/test/java/net/ironoc/portfolio/controller/GitProjectsControllerIntegrationTest.java b/src/test/java/net/ironoc/portfolio/controller/GitProjectsControllerIntegrationTest.java index dfec2c9b..3100855e 100644 --- a/src/test/java/net/ironoc/portfolio/controller/GitProjectsControllerIntegrationTest.java +++ b/src/test/java/net/ironoc/portfolio/controller/GitProjectsControllerIntegrationTest.java @@ -2,6 +2,7 @@ import module java.base; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import net.ironoc.portfolio.dto.RepositoryDetailDto; import net.ironoc.portfolio.dto.RepositoryIssueDto; @@ -169,6 +170,52 @@ public void test_getReposByUsernameReqParam_success() throws Exception { assertThat(response.getContentAsString(), is(JSON_RESPONSE)); } + @Test + public void test_getReposByUsernameReqParam_counts_only_open_issues_and_sorts_by_open_count() throws Exception { + // given + List dtos = List.of( + RepositoryDetailDto.builder() + .name("repo-open-1") + .fullName("conorheffron/repo-open-1") + .htmlUrl("https://github.com/conorheffron/repo-open-1") + .build(), + RepositoryDetailDto.builder() + .name("repo-open-2") + .fullName("conorheffron/repo-open-2") + .htmlUrl("https://github.com/conorheffron/repo-open-2") + .build() + ); + + when(gitDetailsServiceMock.getRepoDetails("conorheffron", false)).thenReturn(dtos); + when(gitDetailsServiceMock.mapRepositoriesToResponse(anyList())) + .thenReturn(new GitDetailsService(null, null, null, null, null).mapRepositoriesToResponse(dtos)); + when(gitDetailsServiceMock.getIssues("conorheffron", "repo-open-1", false)) + .thenReturn(List.of( + RepositoryIssueDto.builder().state("open").build(), + RepositoryIssueDto.builder().state("closed").build(), + RepositoryIssueDto.builder().state("CLOSED").build() + )); + when(gitDetailsServiceMock.getIssues("conorheffron", "repo-open-2", false)) + .thenReturn(List.of( + RepositoryIssueDto.builder().state("OPEN").build(), + RepositoryIssueDto.builder().state("open").build(), + RepositoryIssueDto.builder().state("closed").build() + )); + + // when + MockHttpServletResponse response = mockMvc.perform(get("/api/get-repo-detail?username=conorheffron") + .accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk()) + .andReturn().getResponse(); + + // then + assertThat(response.getStatus(), is(HttpStatus.OK.value())); + JsonNode responseJson = objectMapper.readTree(response.getContentAsString()); + assertThat(responseJson.get(0).get("name").asText(), is("repo-open-2")); + assertThat(responseJson.get(0).get("issueCount").asInt(), is(2)); + assertThat(responseJson.get(1).get("name").asText(), is("repo-open-1")); + assertThat(responseJson.get(1).get("issueCount").asInt(), is(1)); + } + @Test public void test_getReposByUsernameReqParam_empty_response_success() throws Exception { // when