feat(01-03): add test infrastructure, protocol version interceptor, and app bootstrap
- AbstractClickHouseIT base class with Testcontainers ClickHouse and schema init - ProtocolVersionInterceptor validates X-Cameleer-Protocol-Version:1 on data/agent paths - WebConfig registers interceptor with path patterns, excludes health/docs - Cameleer3ServerApplication with @EnableScheduling and component scanning - application-test.yml with small buffer config for tests Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
package com.cameleer3.server.app;
|
||||
|
||||
import com.cameleer3.server.app.config.IngestionConfig;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
/**
|
||||
* Main entry point for the Cameleer3 Server application.
|
||||
* <p>
|
||||
* Scans {@code com.cameleer3.server.app} and {@code com.cameleer3.server.core} packages.
|
||||
*/
|
||||
@SpringBootApplication(scanBasePackages = {
|
||||
"com.cameleer3.server.app",
|
||||
"com.cameleer3.server.core"
|
||||
})
|
||||
@EnableScheduling
|
||||
@EnableConfigurationProperties(IngestionConfig.class)
|
||||
public class Cameleer3ServerApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(Cameleer3ServerApplication.class, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.cameleer3.server.app.config;
|
||||
|
||||
import com.cameleer3.server.app.interceptor.ProtocolVersionInterceptor;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
/**
|
||||
* Web MVC configuration.
|
||||
* <p>
|
||||
* Registers the {@link ProtocolVersionInterceptor} on data and agent endpoint paths,
|
||||
* excluding health, API docs, and Swagger UI paths that do not require protocol versioning.
|
||||
*/
|
||||
@Configuration
|
||||
public class WebConfig implements WebMvcConfigurer {
|
||||
|
||||
private final ProtocolVersionInterceptor protocolVersionInterceptor;
|
||||
|
||||
public WebConfig(ProtocolVersionInterceptor protocolVersionInterceptor) {
|
||||
this.protocolVersionInterceptor = protocolVersionInterceptor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
registry.addInterceptor(protocolVersionInterceptor)
|
||||
.addPathPatterns("/api/v1/data/**", "/api/v1/agents/**")
|
||||
.excludePathPatterns(
|
||||
"/api/v1/health",
|
||||
"/api/v1/api-docs/**",
|
||||
"/api/v1/swagger-ui/**",
|
||||
"/api/v1/swagger-ui.html"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.cameleer3.server.app.interceptor;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Validates that all requests to data and agent endpoints include the
|
||||
* {@code X-Cameleer-Protocol-Version} header with value {@code "1"}.
|
||||
* <p>
|
||||
* Requests missing the header or using an unsupported version receive a 400 response
|
||||
* with a JSON error body.
|
||||
*/
|
||||
@Component
|
||||
public class ProtocolVersionInterceptor implements HandlerInterceptor {
|
||||
|
||||
private static final String HEADER_NAME = "X-Cameleer-Protocol-Version";
|
||||
private static final String SUPPORTED_VERSION = "1";
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public ProtocolVersionInterceptor(ObjectMapper objectMapper) {
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
|
||||
Object handler) throws Exception {
|
||||
String version = request.getHeader(HEADER_NAME);
|
||||
|
||||
if (version == null || !SUPPORTED_VERSION.equals(version)) {
|
||||
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
|
||||
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
|
||||
objectMapper.writeValue(response.getWriter(),
|
||||
Map.of("error", "Missing or unsupported X-Cameleer-Protocol-Version header"));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.cameleer3.server.app;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.context.DynamicPropertyRegistry;
|
||||
import org.springframework.test.context.DynamicPropertySource;
|
||||
import org.testcontainers.clickhouse.ClickHouseContainer;
|
||||
import org.testcontainers.junit.jupiter.Container;
|
||||
import org.testcontainers.junit.jupiter.Testcontainers;
|
||||
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.sql.Connection;
|
||||
import java.sql.DriverManager;
|
||||
import java.sql.Statement;
|
||||
|
||||
/**
|
||||
* Base class for integration tests requiring a ClickHouse instance.
|
||||
* <p>
|
||||
* Uses Testcontainers to spin up a ClickHouse server and initializes the schema
|
||||
* from {@code clickhouse/init/01-schema.sql} before the first test runs.
|
||||
* Subclasses get a {@link JdbcTemplate} for direct database assertions.
|
||||
*/
|
||||
@Testcontainers
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||
@ActiveProfiles("test")
|
||||
public abstract class AbstractClickHouseIT {
|
||||
|
||||
@Container
|
||||
protected static final ClickHouseContainer CLICKHOUSE =
|
||||
new ClickHouseContainer("clickhouse/clickhouse-server:25.3");
|
||||
|
||||
@Autowired
|
||||
protected JdbcTemplate jdbcTemplate;
|
||||
|
||||
@DynamicPropertySource
|
||||
static void overrideProperties(DynamicPropertyRegistry registry) {
|
||||
registry.add("spring.datasource.url", CLICKHOUSE::getJdbcUrl);
|
||||
registry.add("spring.datasource.username", CLICKHOUSE::getUsername);
|
||||
registry.add("spring.datasource.password", CLICKHOUSE::getPassword);
|
||||
}
|
||||
|
||||
@BeforeAll
|
||||
static void initSchema() throws Exception {
|
||||
String sql = Files.readString(
|
||||
Path.of("clickhouse/init/01-schema.sql"), StandardCharsets.UTF_8);
|
||||
|
||||
try (Connection conn = DriverManager.getConnection(
|
||||
CLICKHOUSE.getJdbcUrl(),
|
||||
CLICKHOUSE.getUsername(),
|
||||
CLICKHOUSE.getPassword());
|
||||
Statement stmt = conn.createStatement()) {
|
||||
// Execute each statement separately (separated by semicolons)
|
||||
for (String statement : sql.split(";")) {
|
||||
String trimmed = statement.trim();
|
||||
if (!trimmed.isEmpty()) {
|
||||
stmt.execute(trimmed);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
cameleer3-server-app/src/test/resources/application-test.yml
Normal file
11
cameleer3-server-app/src/test/resources/application-test.yml
Normal file
@@ -0,0 +1,11 @@
|
||||
spring:
|
||||
datasource:
|
||||
url: jdbc:ch://placeholder:8123/cameleer3
|
||||
username: default
|
||||
password: ""
|
||||
driver-class-name: com.clickhouse.jdbc.ClickHouseDriver
|
||||
|
||||
ingestion:
|
||||
buffer-capacity: 100
|
||||
batch-size: 10
|
||||
flush-interval-ms: 100
|
||||
Reference in New Issue
Block a user