mirror of
https://github.com/adityachandelgit/BookLore.git
synced 2026-02-11 05:19:21 -06:00
Improve version checking
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user