feat(outbound): Postgres repository for outbound_connections
- PostgresOutboundConnectionRepository: JdbcTemplate impl of OutboundConnectionRepository; UUID arrays via ConnectionCallback, JSONB for headers/auth/ca-paths, enum casts for method/trust/auth-kind - OutboundBeanConfig: wires the repo + SecretCipher beans - PostgresOutboundConnectionRepositoryIT: 5 Testcontainers tests (save+read, unique-name, allowed-env-ids round-trip, tenant isolation, delete); validates V11 Flyway migration end-to-end - application-test.yml: add jwtsecret default so SecretCipher bean starts up in the Spring test context Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
package com.cameleer.server.app.outbound.storage;
|
||||
|
||||
import com.cameleer.server.app.AbstractPostgresIT;
|
||||
import com.cameleer.server.core.http.TrustMode;
|
||||
import com.cameleer.server.core.outbound.OutboundAuth;
|
||||
import com.cameleer.server.core.outbound.OutboundConnection;
|
||||
import com.cameleer.server.core.outbound.OutboundConnectionRepository;
|
||||
import com.cameleer.server.core.outbound.OutboundMethod;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
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 PostgresOutboundConnectionRepositoryIT extends AbstractPostgresIT {
|
||||
|
||||
@Autowired
|
||||
OutboundConnectionRepository repo;
|
||||
|
||||
private static final String TENANT = "default";
|
||||
private static final String USER = "test-alice";
|
||||
|
||||
@BeforeEach
|
||||
void seedUser() {
|
||||
jdbcTemplate.update(
|
||||
"INSERT INTO users (user_id, provider, email, display_name) VALUES (?, ?, ?, ?) ON CONFLICT (user_id) DO NOTHING",
|
||||
USER, "test", "alice@example.com", "Alice");
|
||||
jdbcTemplate.update("DELETE FROM outbound_connections WHERE tenant_id = ?", TENANT);
|
||||
}
|
||||
|
||||
private OutboundConnection draft(String name) {
|
||||
return new OutboundConnection(
|
||||
UUID.randomUUID(), TENANT, name, "desc",
|
||||
"https://hooks.slack.com/services/T/B/X", OutboundMethod.POST,
|
||||
Map.of("Content-Type", "application/json"), null,
|
||||
TrustMode.SYSTEM_DEFAULT, List.of(),
|
||||
null, new OutboundAuth.None(), List.of(),
|
||||
Instant.now(), USER, Instant.now(), USER);
|
||||
}
|
||||
|
||||
@Test
|
||||
void saveAndRead() {
|
||||
OutboundConnection c = draft("slack-ops");
|
||||
repo.save(c);
|
||||
OutboundConnection loaded = repo.findById(TENANT, c.id()).orElseThrow();
|
||||
assertThat(loaded.name()).isEqualTo("slack-ops");
|
||||
assertThat(loaded.defaultHeaders()).containsEntry("Content-Type", "application/json");
|
||||
assertThat(loaded.method()).isEqualTo(OutboundMethod.POST);
|
||||
assertThat(loaded.tlsTrustMode()).isEqualTo(TrustMode.SYSTEM_DEFAULT);
|
||||
assertThat(loaded.auth()).isInstanceOf(OutboundAuth.None.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void uniqueNamePerTenant() {
|
||||
OutboundConnection a = draft("slack-ops");
|
||||
repo.save(a);
|
||||
OutboundConnection b = draft("slack-ops");
|
||||
assertThatThrownBy(() -> repo.save(b))
|
||||
.isInstanceOf(org.springframework.dao.DuplicateKeyException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void allowedEnvironmentIdsRoundTrip() {
|
||||
UUID env1 = UUID.randomUUID();
|
||||
UUID env2 = UUID.randomUUID();
|
||||
OutboundConnection c = new OutboundConnection(
|
||||
UUID.randomUUID(), TENANT, "multi-env", null,
|
||||
"https://example.com", OutboundMethod.POST,
|
||||
Map.of(), null, TrustMode.SYSTEM_DEFAULT, List.of(),
|
||||
null, new OutboundAuth.None(), List.of(env1, env2),
|
||||
Instant.now(), USER, Instant.now(), USER);
|
||||
repo.save(c);
|
||||
OutboundConnection loaded = repo.findById(TENANT, c.id()).orElseThrow();
|
||||
assertThat(loaded.allowedEnvironmentIds()).containsExactly(env1, env2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void listByTenantOnlyReturnsCurrentTenant() {
|
||||
repo.save(draft("in-tenant"));
|
||||
// The outbound_connections.tenant_id is a plain varchar, so we can insert for a different tenant
|
||||
// without schema changes. Insert another tenant via the repo directly:
|
||||
UUID otherId = UUID.randomUUID();
|
||||
OutboundConnection other = new OutboundConnection(
|
||||
otherId, "other-tenant", "other-tenant-conn", null,
|
||||
"https://example.com", OutboundMethod.POST,
|
||||
Map.of(), null, TrustMode.SYSTEM_DEFAULT, List.of(),
|
||||
null, new OutboundAuth.None(), List.of(),
|
||||
Instant.now(), USER, Instant.now(), USER);
|
||||
repo.save(other);
|
||||
try {
|
||||
List<OutboundConnection> list = repo.listByTenant(TENANT);
|
||||
assertThat(list).extracting(OutboundConnection::name).containsExactly("in-tenant");
|
||||
} finally {
|
||||
repo.delete("other-tenant", otherId);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteRemovesRow() {
|
||||
OutboundConnection c = draft("to-delete");
|
||||
repo.save(c);
|
||||
repo.delete(TENANT, c.id());
|
||||
assertThat(repo.findById(TENANT, c.id())).isEmpty();
|
||||
}
|
||||
}
|
||||
@@ -16,3 +16,4 @@ cameleer:
|
||||
bootstraptoken: test-bootstrap-token
|
||||
bootstraptokenprevious: old-bootstrap-token
|
||||
infrastructureendpoints: true
|
||||
jwtsecret: test-jwt-secret-for-integration-tests-only
|
||||
|
||||
Reference in New Issue
Block a user