alerting(it): RED tests for PER_EXCHANGE cross-field validation + empty targets

Three failing IT tests documenting the contract Task 3.3 will satisfy:
- createPerExchangeRule_withReNotifyMinutesNonZero_returns400
- createPerExchangeRule_withForDurationSecondsNonZero_returns400
- createAnyRule_withEmptyWebhooksAndTargets_returns400
This commit is contained in:
hsiegeln
2026-04-22 17:17:47 +02:00
parent e483e52eee
commit 377968eb53

View File

@@ -246,6 +246,61 @@ class AlertRuleControllerIT extends AbstractPostgresIT {
assertThat(preview.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(preview.getStatusCode()).isEqualTo(HttpStatus.OK);
} }
// --- PER_EXCHANGE cross-field validation + empty-targets validation ---
// RED tests: today's controller accepts these bodies; Task 3.3 adds the validator.
@Test
void createPerExchangeRule_withReNotifyMinutesNonZero_returns400() {
String body = perExchangeRuleBodyWithExtras(
"per-exchange-renotify",
/*reNotifyMinutes*/ 60,
/*forDurationSeconds*/ null);
ResponseEntity<String> resp = restTemplate.exchange(
"/api/v1/environments/" + envSlug + "/alerts/rules",
HttpMethod.POST,
new HttpEntity<>(body, securityHelper.authHeaders(operatorJwt)),
String.class);
assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
assertThat(resp.getBody()).contains("reNotifyMinutes");
}
@Test
void createPerExchangeRule_withForDurationSecondsNonZero_returns400() {
String body = perExchangeRuleBodyWithExtras(
"per-exchange-forduration",
/*reNotifyMinutes*/ null,
/*forDurationSeconds*/ 60);
ResponseEntity<String> resp = restTemplate.exchange(
"/api/v1/environments/" + envSlug + "/alerts/rules",
HttpMethod.POST,
new HttpEntity<>(body, securityHelper.authHeaders(operatorJwt)),
String.class);
assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
assertThat(resp.getBody()).contains("forDurationSeconds");
}
@Test
void createAnyRule_withEmptyWebhooksAndTargets_returns400() {
// baseValidPerExchangeRuleRequest() already produces no webhooks / no targets — that's
// precisely the "empty webhooks + empty targets" shape this test pins as a 400.
String body = baseValidPerExchangeRuleRequest("no-sinks");
ResponseEntity<String> resp = restTemplate.exchange(
"/api/v1/environments/" + envSlug + "/alerts/rules",
HttpMethod.POST,
new HttpEntity<>(body, securityHelper.authHeaders(operatorJwt)),
String.class);
assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
assertThat(resp.getBody()).satisfiesAnyOf(
s -> assertThat(s).contains("webhook"),
s -> assertThat(s).contains("target"));
}
// --- Unknown env returns 404 --- // --- Unknown env returns 404 ---
@Test @Test
@@ -275,4 +330,41 @@ class AlertRuleControllerIT extends AbstractPostgresIT {
"metric":"ERROR_RATE","comparator":"GT","threshold":0.05,"windowSeconds":60}} "metric":"ERROR_RATE","comparator":"GT","threshold":0.05,"windowSeconds":60}}
""".formatted(name); """.formatted(name);
} }
/**
* Produces a request body for a valid PER_EXCHANGE rule (baseline) — no webhooks,
* no targets, no reNotifyMinutes, no forDurationSeconds. The controller currently
* accepts this shape; Task 3.3 tightens that (empty sinks will 400).
*/
private static String baseValidPerExchangeRuleRequest(String name) {
return """
{"name":"%s","severity":"WARNING","conditionKind":"EXCHANGE_MATCH",
"condition":{"kind":"EXCHANGE_MATCH","scope":{},
"filter":{"status":"FAILED","attributes":{}},
"fireMode":"PER_EXCHANGE"}}
""".formatted(name);
}
/**
* Variant of {@link #baseValidPerExchangeRuleRequest(String)} that sets
* reNotifyMinutes and/or forDurationSeconds at the top-level request. Used to pin
* the PER_EXCHANGE cross-field validation contract (Task 3.3).
*/
private static String perExchangeRuleBodyWithExtras(String name,
Integer reNotifyMinutes,
Integer forDurationSeconds) {
StringBuilder extras = new StringBuilder();
if (reNotifyMinutes != null) {
extras.append(",\"reNotifyMinutes\":").append(reNotifyMinutes);
}
if (forDurationSeconds != null) {
extras.append(",\"forDurationSeconds\":").append(forDurationSeconds);
}
return """
{"name":"%s","severity":"WARNING","conditionKind":"EXCHANGE_MATCH",
"condition":{"kind":"EXCHANGE_MATCH","scope":{},
"filter":{"status":"FAILED","attributes":{}},
"fireMode":"PER_EXCHANGE"}%s}
""".formatted(name, extras.toString());
}
} }