test(deploy): lock in FAILED→null snapshot for health-check-fail path

Existing IT only exercises the startContainer-throws path, where the
exception bypasses the entire try block. Add a test where startContainer
succeeds but getContainerStatus never returns healthy — this covers the
early-exit at the HEALTH_CHECK stage, which is the common real-world
failure shape and closest to the snapshot-write point.

Shortens healthchecktimeout to 2s via @TestPropertySource so the test
completes in a few seconds.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-23 00:37:37 +02:00
parent 703bd412ed
commit ffdaeabc9f

View File

@@ -20,6 +20,7 @@ import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.test.context.TestPropertySource;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
@@ -34,8 +35,10 @@ import static org.mockito.Mockito.when;
/**
* Verifies that DeploymentExecutor writes DeploymentConfigSnapshot on successful
* RUNNING transition and does NOT write it on a FAILED path.
* RUNNING transition and does NOT write it on a FAILED path (both the
* startContainer-throws path and the health-check-fails path).
*/
@TestPropertySource(properties = "cameleer.server.runtime.healthchecktimeout=2")
class DeploymentSnapshotIT extends AbstractPostgresIT {
@MockBean
@@ -189,6 +192,53 @@ class DeploymentSnapshotIT extends AbstractPostgresIT {
assertThat(failed.deployedConfigSnapshot()).isNull();
}
// -----------------------------------------------------------------------
// Test 3: snapshot is NOT populated when the health check never passes.
// This exercises the early-exit path in DeploymentExecutor (line ~231) —
// startContainer succeeds, but no replica ever reports healthy, so
// waitForAnyHealthy returns 0 before the snapshot-write point.
// -----------------------------------------------------------------------
@Test
void snapshot_isNotPopulated_whenHealthCheckFails() throws Exception {
// --- given: container starts but never becomes healthy ---
String fakeContainerId = "fake-unhealthy-" + UUID.randomUUID();
when(runtimeOrchestrator.isEnabled()).thenReturn(true);
when(runtimeOrchestrator.startContainer(any())).thenReturn(fakeContainerId);
when(runtimeOrchestrator.getContainerStatus(fakeContainerId))
.thenReturn(new ContainerStatus("starting", true, 0, null));
String appSlug = "snap-unhealthy-" + UUID.randomUUID().toString().substring(0, 8);
post("/api/v1/environments/default/apps", String.format("""
{"slug": "%s", "displayName": "Snapshot Unhealthy App"}
""", appSlug), operatorJwt);
put("/api/v1/environments/default/apps/" + appSlug + "/container-config",
"""
{"runtimeType": "spring-boot", "appPort": 8081}
""", operatorJwt);
String versionId = uploadJar(appSlug, ("fake-jar-unhealthy-" + appSlug).getBytes());
// --- when: trigger deploy ---
JsonNode deployResponse = post(
"/api/v1/environments/default/apps/" + appSlug + "/deployments",
String.format("{\"appVersionId\": \"%s\"}", versionId), operatorJwt);
String deploymentId = deployResponse.path("id").asText();
// --- await FAILED (healthchecktimeout overridden to 2s in @TestPropertySource) ---
await().atMost(30, TimeUnit.SECONDS)
.pollInterval(500, TimeUnit.MILLISECONDS)
.untilAsserted(() -> {
Deployment d = deploymentRepository.findById(UUID.fromString(deploymentId))
.orElseThrow(() -> new AssertionError("Deployment not found: " + deploymentId));
assertThat(d.status()).isEqualTo(DeploymentStatus.FAILED);
});
// --- then: snapshot is null (snapshot-write is gated behind health check) ---
Deployment failed = deploymentRepository.findById(UUID.fromString(deploymentId)).orElseThrow();
assertThat(failed.deployedConfigSnapshot()).isNull();
}
// -----------------------------------------------------------------------
// Helpers
// -----------------------------------------------------------------------