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:
@@ -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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user