Correction problème mise à jour : l'application ne voyait pas les mises à jour quand on lançait docker après avoir push la dernière version.

Effectivement : au demarrage, docker ce mettait automatiquement sur la dernière version alors qu'il n'avait pas necessairement récupérer, ducoup comparaison faisait true et on arrivait pas à avoir la derniere version du code.
Push de la clé jwt publique : sinon pas incluse dans le jar finale et la section patreon n'apparaissait pas.
This commit is contained in:
2026-04-29 10:56:37 +02:00
parent fd14e1e572
commit cb9eb14054
11 changed files with 391 additions and 395 deletions

View File

@@ -1,17 +1,21 @@
package com.loremind.infrastructure.updates;
import com.loremind.application.licensing.LicenseService;
import com.loremind.infrastructure.updates.UpdateCheckService.ImageStatus;
import com.loremind.infrastructure.updates.UpdateCheckService.ImageStatusKind;
import com.loremind.infrastructure.updates.UpdateCheckService.TagsListResponse;
import com.loremind.infrastructure.updates.UpdateCheckService.UpdateStatus;
import org.junit.jupiter.api.Test;
import org.springframework.boot.info.BuildProperties;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.client.RestTemplate;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.List;
import java.util.Properties;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
@@ -19,69 +23,68 @@ import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;
/**
* Test unitaire pour UpdateCheckService.
* Tests UpdateCheckService - approche semver (post-refactor v0.8.x).
*
* Couvre les invariants critiques de la detection de MAJ :
* - feature desactivee si token absent
* - status UP_TO_DATE quand baseline == remote
* - status UPDATE_AVAILABLE quand baseline != remote
* - status UNKNOWN quand baseline manque (PAS d'alignement lazy — invariant
* central, regression historique)
* - status UNKNOWN quand remote impossible a fetcher
* - drapeaux top-level updateAvailable / anyUnknown coherents
* - back-compat : champ updateAvailable sur ImageStatus = (status == UPDATE_AVAILABLE)
* Couvre :
* - feature desactivee si WATCHTOWER_TOKEN absent
* - UP_TO_DATE quand version locale == max(tags remote)
* - UPDATE_AVAILABLE quand un tag plus eleve existe
* - UNKNOWN quand le registry echoue
* - UNKNOWN quand BuildProperties est absent (currentVersion = null)
* - parseSemver / findMaxSemver / compareSemver utilitaires
*/
public class UpdateCheckServiceTest {
private static UpdateCheckService newService(String token) {
// licensing.* params left empty + LicenseService null : la feature beta est
// desactivee dans ces tests, qui couvrent uniquement le canal stable.
private static UpdateCheckService newService(String token, String currentVersion) {
BuildProperties bp = null;
if (currentVersion != null) {
Properties p = new Properties();
p.setProperty("version", currentVersion);
bp = new BuildProperties(p);
}
// licenseService null : la beta est testee separement, ces tests
// couvrent uniquement le canal stable.
return new UpdateCheckService(
new RestTemplateBuilder(),
"ghcr.io",
"igmlcreation/loremind-core,igmlcreation/loremind-brain",
"latest",
"http://watchtower:8080",
token,
"",
"latest",
null
null,
bp
);
}
/**
* Injecte un RestTemplate moque dans le service deja construit, et pose
* directement les baselines pour eviter les vrais appels HTTP.
*/
@SuppressWarnings("unchecked")
private static void setBaselines(UpdateCheckService svc, Map<String, String> baselines) {
((Map<String, String>) ReflectionTestUtils.getField(svc, "baselineDigests")).putAll(baselines);
}
private static RestTemplate stubHttp(UpdateCheckService svc) {
RestTemplate http = mock(RestTemplate.class);
ReflectionTestUtils.setField(svc, "http", http);
return http;
}
private static void stubRemoteDigest(RestTemplate http, String image, String digest) {
HttpHeaders headers = new HttpHeaders();
if (digest != null) headers.add("Docker-Content-Digest", digest);
ResponseEntity<Void> resp = new ResponseEntity<>(headers, org.springframework.http.HttpStatus.OK);
when(http.exchange(eq("https://ghcr.io/v2/" + image + "/manifests/latest"),
eq(org.springframework.http.HttpMethod.HEAD), any(), eq(Void.class)))
private static void stubTags(RestTemplate http, String image, List<String> tags) {
TagsListResponse body = new TagsListResponse();
body.name = image;
body.tags = tags;
ResponseEntity<TagsListResponse> resp = new ResponseEntity<>(body, HttpStatus.OK);
when(http.exchange(eq("https://ghcr.io/v2/" + image + "/tags/list"),
eq(HttpMethod.GET), any(), eq(TagsListResponse.class)))
.thenReturn(resp);
}
private static void stubRemoteFailure(RestTemplate http, String image) {
when(http.exchange(eq("https://ghcr.io/v2/" + image + "/manifests/latest"),
eq(org.springframework.http.HttpMethod.HEAD), any(), eq(Void.class)))
private static void stubTagsFailure(RestTemplate http, String image) {
when(http.exchange(eq("https://ghcr.io/v2/" + image + "/tags/list"),
eq(HttpMethod.GET), any(), eq(TagsListResponse.class)))
.thenThrow(new RuntimeException("network down"));
}
// -----------------------------------------------------------------
// Comportement du service
// -----------------------------------------------------------------
@Test
void disabledWhenTokenMissing() {
UpdateCheckService svc = newService("");
UpdateCheckService svc = newService("", "0.8.0");
UpdateStatus status = svc.check();
assertFalse(status.enabled());
assertFalse(status.updateAvailable());
@@ -90,118 +93,153 @@ public class UpdateCheckServiceTest {
}
@Test
void upToDate_whenBaselineEqualsRemote() {
UpdateCheckService svc = newService("token");
ReflectionTestUtils.setField(svc, "baselineDigests", new ConcurrentHashMap<>());
setBaselines(svc, Map.of(
"igmlcreation/loremind-core", "sha256:aaa",
"igmlcreation/loremind-brain", "sha256:bbb"
));
void upToDate_whenCurrentEqualsMaxRemote() {
UpdateCheckService svc = newService("token", "0.8.0");
RestTemplate http = stubHttp(svc);
stubRemoteDigest(http, "igmlcreation/loremind-core", "sha256:aaa");
stubRemoteDigest(http, "igmlcreation/loremind-brain", "sha256:bbb");
stubTags(http, "igmlcreation/loremind-core",
List.of("0.7.0", "0.7.1", "0.7.2", "0.8.0", "latest"));
stubTags(http, "igmlcreation/loremind-brain",
List.of("0.7.0", "0.8.0", "latest"));
UpdateStatus status = svc.check();
assertTrue(status.enabled());
assertFalse(status.updateAvailable());
assertFalse(status.anyUnknown());
assertEquals("0.8.0", status.currentVersion());
for (ImageStatus img : status.images()) {
assertEquals(ImageStatusKind.UP_TO_DATE, img.status());
assertEquals("0.8.0", img.localVersion());
assertEquals("0.8.0", img.remoteVersion());
assertFalse(img.updateAvailable(), "back-compat bool");
}
}
@Test
void updateAvailable_whenRemoteDiffers() {
UpdateCheckService svc = newService("token");
ReflectionTestUtils.setField(svc, "baselineDigests", new ConcurrentHashMap<>());
setBaselines(svc, Map.of(
"igmlcreation/loremind-core", "sha256:OLD",
"igmlcreation/loremind-brain", "sha256:bbb"
));
void updateAvailable_whenRemoteHigher() {
UpdateCheckService svc = newService("token", "0.7.2");
RestTemplate http = stubHttp(svc);
stubRemoteDigest(http, "igmlcreation/loremind-core", "sha256:NEW");
stubRemoteDigest(http, "igmlcreation/loremind-brain", "sha256:bbb");
stubTags(http, "igmlcreation/loremind-core",
List.of("0.7.0", "0.7.1", "0.7.2", "0.8.0", "latest"));
stubTags(http, "igmlcreation/loremind-brain",
List.of("0.7.2", "latest"));
UpdateStatus status = svc.check();
assertTrue(status.updateAvailable());
assertFalse(status.anyUnknown());
ImageStatus core = status.images().stream()
.filter(i -> i.image().endsWith("core")).findFirst().orElseThrow();
assertEquals(ImageStatusKind.UPDATE_AVAILABLE, core.status());
assertEquals("0.7.2", core.localVersion());
assertEquals("0.8.0", core.remoteVersion());
assertTrue(core.updateAvailable(), "back-compat bool");
ImageStatus brain = status.images().stream()
.filter(i -> i.image().endsWith("brain")).findFirst().orElseThrow();
assertEquals(ImageStatusKind.UP_TO_DATE, brain.status());
}
@Test
void unknown_whenBaselineMissing_DOES_NOT_lazyAlign() {
// INVARIANT CENTRAL : si la baseline est absente (echec init au boot),
// on NE DOIT PAS aligner lazy sur le remote courant — sinon une MAJ
// pousse APRES le boot serait declaree "a jour" silencieusement.
UpdateCheckService svc = newService("token");
ReflectionTestUtils.setField(svc, "baselineDigests", new ConcurrentHashMap<>());
// baseline DELIBEREMENT vide
void unknown_whenRegistryFails() {
UpdateCheckService svc = newService("token", "0.8.0");
RestTemplate http = stubHttp(svc);
stubRemoteDigest(http, "igmlcreation/loremind-core", "sha256:remote-now");
stubRemoteDigest(http, "igmlcreation/loremind-brain", "sha256:remote-now-2");
stubTagsFailure(http, "igmlcreation/loremind-core");
stubTags(http, "igmlcreation/loremind-brain", List.of("0.8.0"));
UpdateStatus status = svc.check();
assertTrue(status.anyUnknown());
ImageStatus core = status.images().stream()
.filter(i -> i.image().endsWith("core")).findFirst().orElseThrow();
assertEquals(ImageStatusKind.UNKNOWN, core.status());
assertNull(core.remoteVersion());
assertEquals("0.8.0", core.localVersion());
}
@Test
void unknown_whenNoValidSemverTags() {
UpdateCheckService svc = newService("token", "0.8.0");
RestTemplate http = stubHttp(svc);
stubTags(http, "igmlcreation/loremind-core", List.of("latest", "stable", "main"));
stubTags(http, "igmlcreation/loremind-brain", List.of("0.8.0"));
UpdateStatus status = svc.check();
assertTrue(status.anyUnknown());
ImageStatus core = status.images().stream()
.filter(i -> i.image().endsWith("core")).findFirst().orElseThrow();
assertEquals(ImageStatusKind.UNKNOWN, core.status());
assertNull(core.remoteVersion());
}
@Test
void unknown_whenBuildPropertiesAbsent() {
// INVARIANT : pas de version courante => tout UNKNOWN, jamais "a jour"
// par defaut. Evite de declarer "a jour" un build dev sans build-info.
UpdateCheckService svc = newService("token", null);
RestTemplate http = stubHttp(svc);
// Meme si on stub des tags, le service doit bypass et renvoyer UNKNOWN
stubTags(http, "igmlcreation/loremind-core", List.of("0.8.0"));
UpdateStatus status = svc.check();
assertTrue(status.enabled());
assertFalse(status.updateAvailable());
assertTrue(status.anyUnknown());
assertNull(status.currentVersion());
for (ImageStatus img : status.images()) {
assertEquals(ImageStatusKind.UNKNOWN, img.status());
assertNull(img.localDigest());
assertNotNull(img.remoteDigest()); // remote OK, baseline manquante
}
}
// VERIFICATION CRITIQUE : la baseline ne doit PAS avoir ete posee.
@SuppressWarnings("unchecked")
Map<String, String> baselines = (Map<String, String>) ReflectionTestUtils.getField(svc, "baselineDigests");
assertTrue(baselines.isEmpty(),
"check() ne doit JAMAIS aligner lazy la baseline sur le remote — "
+ "regression de bug historique (faux negatif silencieux).");
// -----------------------------------------------------------------
// Utilitaires semver
// -----------------------------------------------------------------
@Test
void parseSemver_acceptsCommonFormats() {
assertArrayEquals(new int[]{0, 8, 0}, UpdateCheckService.parseSemver("0.8.0"));
assertArrayEquals(new int[]{0, 8, 0}, UpdateCheckService.parseSemver("v0.8.0"));
assertArrayEquals(new int[]{1, 0, 0}, UpdateCheckService.parseSemver("1.0.0"));
assertArrayEquals(new int[]{0, 8, 0}, UpdateCheckService.parseSemver("0.8.0-beta.1"));
assertArrayEquals(new int[]{0, 8, 0}, UpdateCheckService.parseSemver("0.8.0+build.42"));
}
@Test
void unknown_whenRemoteFetchFails() {
UpdateCheckService svc = newService("token");
ReflectionTestUtils.setField(svc, "baselineDigests", new ConcurrentHashMap<>());
setBaselines(svc, Map.of("igmlcreation/loremind-core", "sha256:aaa",
"igmlcreation/loremind-brain", "sha256:bbb"));
RestTemplate http = stubHttp(svc);
stubRemoteFailure(http, "igmlcreation/loremind-core");
stubRemoteDigest(http, "igmlcreation/loremind-brain", "sha256:bbb");
UpdateStatus status = svc.check();
assertFalse(status.updateAvailable());
assertTrue(status.anyUnknown());
ImageStatus core = status.images().stream()
.filter(i -> i.image().endsWith("core")).findFirst().orElseThrow();
assertEquals(ImageStatusKind.UNKNOWN, core.status());
assertNull(core.remoteDigest());
assertEquals("sha256:aaa", core.localDigest()); // baseline preservee
void parseSemver_rejectsInvalid() {
assertNull(UpdateCheckService.parseSemver(null));
assertNull(UpdateCheckService.parseSemver(""));
assertNull(UpdateCheckService.parseSemver("latest"));
assertNull(UpdateCheckService.parseSemver("stable"));
assertNull(UpdateCheckService.parseSemver("0.8.0.1.2"));
assertNull(UpdateCheckService.parseSemver("0.x.0"));
}
@Test
void mixedStatuses_anyUnknownAndAnyUpdateBothTrue() {
UpdateCheckService svc = newService("token");
ReflectionTestUtils.setField(svc, "baselineDigests", new ConcurrentHashMap<>());
setBaselines(svc, Map.of("igmlcreation/loremind-core", "sha256:OLD"));
// brain n'a pas de baseline -> UNKNOWN
RestTemplate http = stubHttp(svc);
stubRemoteDigest(http, "igmlcreation/loremind-core", "sha256:NEW");
stubRemoteFailure(http, "igmlcreation/loremind-brain");
void compareSemver_basic() {
assertTrue(UpdateCheckService.compareSemver("0.7.2", "0.8.0") < 0);
assertTrue(UpdateCheckService.compareSemver("0.8.0", "0.7.2") > 0);
assertEquals(0, UpdateCheckService.compareSemver("0.8.0", "0.8.0"));
assertEquals(0, UpdateCheckService.compareSemver("v0.8.0", "0.8.0"));
assertTrue(UpdateCheckService.compareSemver("0.8.0", "0.10.0") < 0);
assertTrue(UpdateCheckService.compareSemver("1.0.0", "0.99.99") > 0);
}
UpdateStatus status = svc.check();
@Test
void findMaxSemver_picksHighest() {
assertEquals("0.8.0", UpdateCheckService.findMaxSemver(
List.of("0.7.0", "0.7.1", "0.7.2", "0.8.0", "latest")));
assertEquals("0.10.0", UpdateCheckService.findMaxSemver(
List.of("0.8.0", "0.10.0", "0.9.5")));
assertEquals("v1.0.0", UpdateCheckService.findMaxSemver(
List.of("v0.8.0", "v1.0.0", "latest")));
}
assertTrue(status.updateAvailable(), "core a une MAJ disponible");
assertTrue(status.anyUnknown(), "brain est UNKNOWN");
@Test
void findMaxSemver_returnsNullWhenNoValidTag() {
assertNull(UpdateCheckService.findMaxSemver(List.of("latest", "stable", "main")));
assertNull(UpdateCheckService.findMaxSemver(List.of()));
}
}