fix(outbound): wire rulesReferencing to AlertRuleRepository (Plan 01 gate)

Replaces the Plan 01 stub that returned [] with a real call through
AlertRuleRepository.findRuleIdsByOutboundConnectionId. Adds AlertingBeanConfig
exposing the AlertRuleRepository bean; widens OutboundBeanConfig constructor
to inject it. Delete and narrow-envs guards now correctly block when rules
reference a connection.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-19 18:51:36 +02:00
parent f80bc006c1
commit 930ac20d11
4 changed files with 107 additions and 4 deletions

View File

@@ -0,0 +1,17 @@
package com.cameleer.server.app.alerting.config;
import com.cameleer.server.app.alerting.storage.PostgresAlertRuleRepository;
import com.cameleer.server.core.alerting.AlertRuleRepository;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
@Configuration
public class AlertingBeanConfig {
@Bean
public AlertRuleRepository alertRuleRepository(JdbcTemplate jdbc, ObjectMapper om) {
return new PostgresAlertRuleRepository(jdbc, om);
}
}

View File

@@ -1,5 +1,6 @@
package com.cameleer.server.app.outbound;
import com.cameleer.server.core.alerting.AlertRuleRepository;
import com.cameleer.server.core.outbound.OutboundConnection;
import com.cameleer.server.core.outbound.OutboundConnectionRepository;
import com.cameleer.server.core.outbound.OutboundConnectionService;
@@ -13,10 +14,15 @@ import java.util.UUID;
public class OutboundConnectionServiceImpl implements OutboundConnectionService {
private final OutboundConnectionRepository repo;
private final AlertRuleRepository ruleRepo;
private final String tenantId;
public OutboundConnectionServiceImpl(OutboundConnectionRepository repo, String tenantId) {
public OutboundConnectionServiceImpl(
OutboundConnectionRepository repo,
AlertRuleRepository ruleRepo,
String tenantId) {
this.repo = repo;
this.ruleRepo = ruleRepo;
this.tenantId = tenantId;
}
@@ -91,8 +97,7 @@ public class OutboundConnectionServiceImpl implements OutboundConnectionService
@Override
public List<UUID> rulesReferencing(UUID id) {
// Plan 01 stub. Plan 02 will wire this to AlertRuleRepository.
return List.of();
return ruleRepo.findRuleIdsByOutboundConnectionId(id);
}
private void assertNameUnique(String name, UUID excludingId) {

View File

@@ -3,6 +3,7 @@ package com.cameleer.server.app.outbound.config;
import com.cameleer.server.app.outbound.OutboundConnectionServiceImpl;
import com.cameleer.server.app.outbound.crypto.SecretCipher;
import com.cameleer.server.app.outbound.storage.PostgresOutboundConnectionRepository;
import com.cameleer.server.core.alerting.AlertRuleRepository;
import com.cameleer.server.core.outbound.OutboundConnectionRepository;
import com.cameleer.server.core.outbound.OutboundConnectionService;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -29,7 +30,8 @@ public class OutboundBeanConfig {
@Bean
public OutboundConnectionService outboundConnectionService(
OutboundConnectionRepository repo,
AlertRuleRepository ruleRepo,
@Value("${cameleer.server.tenant.id:default}") String tenantId) {
return new OutboundConnectionServiceImpl(repo, tenantId);
return new OutboundConnectionServiceImpl(repo, ruleRepo, tenantId);
}
}

View File

@@ -0,0 +1,79 @@
package com.cameleer.server.app.outbound;
import com.cameleer.server.app.AbstractPostgresIT;
import com.cameleer.server.app.alerting.storage.PostgresAlertRuleRepository;
import com.cameleer.server.core.alerting.*;
import com.cameleer.server.core.http.TrustMode;
import com.cameleer.server.core.outbound.*;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.server.ResponseStatusException;
import java.time.Instant;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
class OutboundConnectionServiceRulesReferencingIT extends AbstractPostgresIT {
@Autowired OutboundConnectionService service;
@Autowired OutboundConnectionRepository repo;
private UUID envId;
private UUID connId;
private UUID ruleId;
private PostgresAlertRuleRepository ruleRepo;
@BeforeEach
void seed() {
ruleRepo = new PostgresAlertRuleRepository(jdbcTemplate, new ObjectMapper());
envId = UUID.randomUUID();
jdbcTemplate.update(
"INSERT INTO environments (id, slug, display_name) VALUES (?, ?, ?)",
envId, "env-" + UUID.randomUUID(), "Test Env");
jdbcTemplate.update(
"INSERT INTO users (user_id, provider, email) VALUES ('u-ref', 'local', 'a@b.test')" +
" ON CONFLICT (user_id) DO NOTHING");
var c = repo.save(new OutboundConnection(
UUID.randomUUID(), "default", "conn-" + UUID.randomUUID(), null,
"https://example.test", OutboundMethod.POST,
Map.of(), null, TrustMode.SYSTEM_DEFAULT, List.of(), null,
new OutboundAuth.None(), List.of(),
Instant.now(), "u-ref", Instant.now(), "u-ref"));
connId = c.id();
ruleId = UUID.randomUUID();
var rule = new AlertRule(
ruleId, envId, "r", null, AlertSeverity.WARNING, true,
ConditionKind.AGENT_STATE,
new AgentStateCondition(new AlertScope(null, null, null), "DEAD", 60),
60, 0, 60, "t", "m",
List.of(new WebhookBinding(UUID.randomUUID(), connId, null, Map.of())),
List.of(), Instant.now(), null, null, Map.of(),
Instant.now(), "u-ref", Instant.now(), "u-ref");
ruleRepo.save(rule);
}
@AfterEach
void cleanup() {
jdbcTemplate.update("DELETE FROM alert_rules WHERE id = ?", ruleId);
jdbcTemplate.update("DELETE FROM outbound_connections WHERE id = ?", connId);
jdbcTemplate.update("DELETE FROM environments WHERE id = ?", envId);
jdbcTemplate.update("DELETE FROM users WHERE user_id = 'u-ref'");
}
@Test
void deleteConnectionReferencedByRuleReturns409() {
assertThat(service.rulesReferencing(connId)).hasSize(1);
assertThatThrownBy(() -> service.delete(connId, "u-ref"))
.isInstanceOf(ResponseStatusException.class)
.hasMessageContaining("referenced by rules");
}
}