Improve version checking

This commit is contained in:
aditya.chandel
2025-08-08 19:44:40 -06:00
parent 9ae1c6e2dc
commit 850ecb0b0c
3 changed files with 217 additions and 37 deletions

View File

@@ -1,5 +1,16 @@
package com.adityachandel.booklore.model.dto;
public record VersionInfo(String current, String latest) {
import lombok.Getter;
@Getter
public class VersionInfo {
private final String current;
private final String latest;
public VersionInfo(String current, String latest) {
this.current = current;
this.latest = latest;
}
}

View File

@@ -19,33 +19,42 @@ import java.util.List;
public class VersionService {
@Value("${app.version:unknown}")
private String appVersion;
String appVersion;
private static final String GITHUB_REPO = "booklore-app/booklore";
private static final String BASE_URI = "https://api.github.com/repos/" + GITHUB_REPO;
private static final int MAX_RELEASES = 15;
private static final RestClient REST_CLIENT = RestClient.builder()
.defaultHeader("Accept", "application/vnd.github+json")
.defaultHeader("User-Agent", "BookLore-Version-Checker")
.build();
private static final ObjectMapper MAPPER = new ObjectMapper();
public VersionInfo getVersionInfo() {
String latestVersion = fetchLatestGitHubReleaseVersion();
return new VersionInfo(appVersion, latestVersion);
String latest = "unknown";
try {
latest = fetchLatestGitHubReleaseVersion();
} catch (Exception e) {
log.error("Error fetching latest release version", e);
}
return new VersionInfo(appVersion, latest);
}
public List<ReleaseNote> getChangelogSinceCurrentVersion() {
return fetchReleaseNotesSince(appVersion);
}
private String fetchLatestGitHubReleaseVersion() {
try {
RestClient restClient = RestClient.builder()
.defaultHeader("Accept", "application/vnd.github+json")
.defaultHeader("User-Agent", "BookLore-Version-Checker")
.build();
String response = restClient.get()
.uri("https://api.github.com/repos/" + GITHUB_REPO + "/releases/latest")
public String fetchLatestGitHubReleaseVersion() {
try {
String response = REST_CLIENT.get()
.uri(BASE_URI + "/releases/latest")
.retrieve()
.body(String.class);
JsonNode root = new ObjectMapper().readTree(response);
return root.has("tag_name") ? root.get("tag_name").asText() : "unknown";
JsonNode root = MAPPER.readTree(response);
return root.path("tag_name").asText("unknown");
} catch (Exception e) {
log.error("Failed to fetch latest release version", e);
@@ -53,41 +62,38 @@ public class VersionService {
}
}
private List<ReleaseNote> fetchReleaseNotesSince(String currentVersion) {
public List<ReleaseNote> fetchReleaseNotesSince(String currentVersion) {
log.info("Fetching release notes since version: {}", currentVersion);
List<ReleaseNote> updates = new ArrayList<>();
try {
RestClient restClient = RestClient.builder()
.defaultHeader("Accept", "application/vnd.github+json")
.defaultHeader("User-Agent", "BookLore-Version-Checker")
.build();
String response = restClient.get()
.uri("https://api.github.com/repos/" + GITHUB_REPO + "/releases")
String response = REST_CLIENT.get()
.uri(BASE_URI + "/releases?per_page=" + MAX_RELEASES)
.retrieve()
.body(String.class);
ObjectMapper mapper = new ObjectMapper();
JsonNode releases = mapper.readTree(response);
JsonNode releases = MAPPER.readTree(response);
if (!releases.isArray()) {
log.warn("Invalid releases response from GitHub API");
return updates;
}
for (JsonNode release : releases) {
String tag = release.get("tag_name").asText();
if (isVersionGreater(tag, currentVersion)) {
String url = "https://github.com/" + GITHUB_REPO + "/releases/tag/" + tag;
String publishedAtStr = release.get("published_at").asText();
LocalDateTime publishedAt = LocalDateTime.parse(publishedAtStr, DateTimeFormatter.ISO_DATE_TIME);
updates.add(new ReleaseNote(
tag,
release.get("name").asText(),
release.get("body").asText(),
url,
publishedAt
));
String tag = release.path("tag_name").asText(null);
if (tag == null || !isVersionGreater(tag, currentVersion)) {
continue;
}
String url = BASE_URI + "/releases/tag/" + tag;
LocalDateTime published = LocalDateTime.parse(release.path("published_at").asText(), DateTimeFormatter.ISO_DATE_TIME);
updates.add(new ReleaseNote(tag, release.path("name").asText(tag), release.path("body").asText(""), url, published));
}
log.info("Returning {} newer releases", updates.size());
} catch (Exception e) {
log.error("Failed to fetch release notes", e);
}
return updates;
}

View File

@@ -0,0 +1,163 @@
package com.adityachandel.booklore.service;
import java.lang.reflect.Method;
import java.time.LocalDateTime;
import java.util.List;
import com.adityachandel.booklore.model.dto.ReleaseNote;
import com.adityachandel.booklore.model.dto.VersionInfo;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import static org.assertj.core.api.Assertions.assertThat;
class VersionServiceTest {
private VersionService service;
private VersionService spyService;
@BeforeEach
void setUp() {
service = new VersionService();
spyService = Mockito.spy(service);
}
@Nested
class VersionComparison {
private Method cmp;
@BeforeEach
void init() throws Exception {
cmp = VersionService.class
.getDeclaredMethod("isVersionGreater", String.class, String.class);
cmp.setAccessible(true);
}
@Test
void returnsTrueWhenMajorIncreases() throws Exception {
assertThat((Boolean) cmp.invoke(service, "2.0.0", "1.9.9"))
.isTrue();
}
@Test
void returnsFalseWhenMajorDecreases() throws Exception {
assertThat((Boolean) cmp.invoke(service, "1.0.0", "2.0.0"))
.isFalse();
}
@Test
void returnsTrueForPatchIncrease() throws Exception {
assertThat((Boolean) cmp.invoke(service, "1.0.1", "1.0.0"))
.isTrue();
}
@Test
void returnsFalseForPatchDecrease() throws Exception {
assertThat((Boolean) cmp.invoke(service, "1.0.0", "1.0.1"))
.isFalse();
}
@Test
void returnsFalseWhenEqual() throws Exception {
assertThat((Boolean) cmp.invoke(service, "1.2.3", "1.2.3"))
.isFalse();
}
@Test
void handlesDifferentLengthVersions() throws Exception {
assertThat((Boolean) cmp.invoke(service, "1.2.0", "1.1"))
.isTrue();
assertThat((Boolean) cmp.invoke(service, "1.0", "1.0.1"))
.isFalse();
}
@Test
void ignoresPrefixAndSafelyHandlesInvalid() throws Exception {
assertThat((Boolean) cmp.invoke(service, "v1.10", "v1.9.9"))
.isTrue();
assertThat((Boolean) cmp.invoke(service, "x.y", "1.0"))
.isFalse();
}
@Test
void returnsFalseWhenVersion2IsNull() throws Exception {
assertThat((Boolean) cmp.invoke(service, "1.0.0", null))
.isFalse();
}
@Test
void returnsFalseWhenVersion1IsNull() throws Exception {
assertThat((Boolean) cmp.invoke(service, null, "1.0.0"))
.isFalse();
}
@Test
void returnsFalseWhenBothVersionsNull() throws Exception {
assertThat((Boolean) cmp.invoke(service, null, null))
.isFalse();
}
}
@Nested
class GetVersionInfoTests {
@Test
void includesAppAndLatestOnSuccess() {
Mockito.doReturn("v9.9.9")
.when(spyService)
.fetchLatestGitHubReleaseVersion();
VersionInfo info = spyService.getVersionInfo();
assertThat(info.getCurrent())
.isEqualTo(service.appVersion);
assertThat(info.getLatest())
.isEqualTo("v9.9.9");
}
@Test
void usesUnknownIfFetchFails() {
Mockito.doThrow(new RuntimeException("fail"))
.when(spyService)
.fetchLatestGitHubReleaseVersion();
VersionInfo info = spyService.getVersionInfo();
assertThat(info.getLatest())
.isEqualTo("unknown");
}
}
@Nested
class GetChangelogSinceCurrentVersionTests {
@Test
void returnsNotesWhenAvailable() {
LocalDateTime now = LocalDateTime.now();
ReleaseNote note = new ReleaseNote("v1.1", "n", "b", "u", now);
Mockito.doReturn(List.of(note))
.when(spyService)
.fetchReleaseNotesSince(service.appVersion);
List<ReleaseNote> result = spyService.getChangelogSinceCurrentVersion();
assertThat(result).hasSize(1).containsExactly(note);
}
@Test
void returnsEmptyListWhenNoNewReleases() {
Mockito.doReturn(List.of())
.when(spyService)
.fetchReleaseNotesSince(service.appVersion);
var result = spyService.getChangelogSinceCurrentVersion();
assertThat(result).isEmpty();
}
}
}