diff --git a/documents/OJP_TESTCONTAINER_ANALYSIS.md b/documents/OJP_TESTCONTAINER_ANALYSIS.md new file mode 100644 index 000000000..58e24496c --- /dev/null +++ b/documents/OJP_TESTCONTAINER_ANALYSIS.md @@ -0,0 +1,1127 @@ +# OJP TestContainer Analysis & Implementation Guide + +## Executive Summary + +This document provides a comprehensive analysis for creating a TestContainer for Java integration tests that extends `org.testcontainers.containers.GenericContainer`. The goal is to publish a JAR to Maven Central containing this test container, which will run the OJP server internally, making it easier to produce integration tests that include OJP. + +## Current State Analysis + +### Existing Project Structure + +The OJP project is a multi-module Maven project with: + +1. **ojp-server** (Java 21) - The gRPC server managing HikariCP connection pools +2. **ojp-jdbc-driver** (Java 11) - JDBC driver implementation that connects to the server +3. **ojp-grpc-commons** (Java 11) - Shared gRPC contracts + +**Key Finding**: The project already uses TestContainers for SQL Server integration tests (see `SQLServerTestContainer.java`), demonstrating familiarity with the technology. + +### OJP Server Characteristics + +From analyzing `GrpcServer.java` and `ServerConfiguration.java`: + +- **Main Class**: `org.openjproxy.grpc.server.GrpcServer` +- **Default Port**: 1059 (gRPC) +- **Health Check**: Built-in gRPC health service +- **Configuration**: Environment-based configuration with defaults +- **Docker Image**: Already exists at `rrobetti/ojp:0.3.1-snapshot` +- **Shaded JAR**: Server produces a shaded JAR with all dependencies included + +### Current Testing Patterns + +Integration tests in `ojp-jdbc-driver` follow this pattern: +- Tests use `@ParameterizedTest` with `@CsvFileSource` +- Each database requires the OJP server to be running +- Tests connect via OJP JDBC URL format: `jdbc:ojp[localhost:1059]_://...` + +## Recommended Implementation Approach + +### 1. **Create as a Separate Module within this Repository** ✅ + +**Recommendation**: Create a new module `ojp-testcontainers` within the existing OJP repository. + +**Rationale**: +- ✅ Maintains version synchronization with OJP server +- ✅ Leverages existing CI/CD infrastructure +- ✅ Easier dependency management (can reference ojp-server artifacts) +- ✅ Single source of truth for issues and contributions +- ✅ Follows the existing multi-module pattern (ojp-server, ojp-jdbc-driver, ojp-grpc-commons) +- ✅ Simplifies release process (all artifacts released together) + +**Alternative Considered**: Separate repository +- ❌ Harder to keep versions in sync +- ❌ Additional CI/CD setup +- ❌ More complex release coordination +- ⚠️ Only consider if planning to support multiple OJP versions simultaneously + +### 1.1. **Licensing Considerations for Database TestContainers** ⚠️ + +**IMPORTANT**: The published `ojp-testcontainers` module to Maven Central will **only include support for open-source databases** due to licensing restrictions. + +**Open-Source Databases** (Can be published to Maven Central): +- ✅ PostgreSQL +- ✅ MySQL +- ✅ MariaDB +- ✅ H2 +- ✅ Other OSS databases with compatible licenses + +**Proprietary Databases** (Cannot be published to Maven Central): +- ❌ Oracle Database (requires accepting license) +- ❌ Microsoft SQL Server (requires accepting license) +- ❌ IBM DB2 (requires accepting license) +- ❌ Other proprietary databases + +**Solution for Proprietary Databases**: +Developers can create their own TestContainer implementations locally by following the patterns and documentation we provide. See [Section 8: Custom TestContainers for Proprietary Databases](#8-custom-testcontainers-for-proprietary-databases) for detailed guidance. + +### 2. Module Structure + +``` +ojp-testcontainers/ +├── pom.xml +├── src/ +│ ├── main/ +│ │ ├── java/ +│ │ │ └── org/openjproxy/testcontainers/ +│ │ │ ├── OJPContainer.java +│ │ │ ├── OJPContainerConfig.java +│ │ │ └── DatabaseConfig.java +│ │ └── resources/ +│ │ └── META-INF/ +│ │ └── MANIFEST.MF +│ └── test/ +│ ├── java/ +│ │ └── org/openjproxy/testcontainers/ +│ │ ├── OJPContainerTest.java +│ │ ├── PostgresIntegrationTest.java +│ │ └── MySQLIntegrationTest.java +│ └── resources/ +│ └── test-databases.yml +└── README.md +``` + +### 3. Implementation Design + +#### Core Class: OJPContainer + +```java +package org.openjproxy.testcontainers; + +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.utility.DockerImageName; + +/** + * TestContainer for OJP (Open J Proxy) server. + * Provides an easy way to run OJP server in integration tests. + * + * The OJP server acts as a proxy - it doesn't need database configuration at startup. + * Database connection details are passed through the JDBC URL when your tests connect. + * + * Example usage: + *
+ * {@code
+ * @Container
+ * static OJPContainer ojp = new OJPContainer();
+ * 
+ * // In your test - database config is in the JDBC URL
+ * String jdbcUrl = "jdbc:ojp[" + ojp.getHost() + ":" + ojp.getGrpcPort() + "]_" +
+ *                  "postgresql://localhost:5432/test";
+ * Connection conn = DriverManager.getConnection(jdbcUrl, "user", "pass");
+ * }
+ * 
+ */ +public class OJPContainer extends GenericContainer { + + private static final String DEFAULT_IMAGE_NAME = "rrobetti/ojp"; + private static final String DEFAULT_TAG = "0.3.1-snapshot"; + private static final int DEFAULT_GRPC_PORT = 1059; + private static final int DEFAULT_PROMETHEUS_PORT = 9159; + + private boolean telemetryEnabled = true; // Enabled by default + + public OJPContainer() { + this(DEFAULT_IMAGE_NAME + ":" + DEFAULT_TAG); + } + + public OJPContainer(String dockerImageName) { + super(DockerImageName.parse(dockerImageName)); + + // Expose default gRPC port and Prometheus port + // Both ports will be mapped to random available ports to avoid conflicts + withExposedPorts(DEFAULT_GRPC_PORT, DEFAULT_PROMETHEUS_PORT); + + // Wait for health check + waitingFor(Wait.forHealthcheck()); + } + + /** + * Get the gRPC connection string for OJP server. + * Use this to construct your JDBC URL. + * + * @return gRPC connection string (e.g., "localhost:32768") + */ + public String getGrpcUrl() { + return getHost() + ":" + getMappedPort(DEFAULT_GRPC_PORT); + } + + /** + * Get the mapped gRPC port. + * The port is randomly assigned to avoid conflicts. + * + * @return The host port mapped to the container's gRPC port + */ + public int getGrpcPort() { + return getMappedPort(DEFAULT_GRPC_PORT); + } + + /** + * Build an OJP JDBC URL from the original database JDBC URL. + * This is a convenience method to construct the proper OJP JDBC URL format. + * + * Example: + *
+     * String ojpUrl = ojp.buildJdbcUrl("jdbc:postgresql://localhost:5432/test");
+     * // Returns: "jdbc:ojp[localhost:32768]_postgresql://localhost:5432/test"
+     * 
+ * + * @param originalJdbcUrl The original database JDBC URL + * @return OJP-prefixed JDBC URL + */ + public String buildJdbcUrl(String originalJdbcUrl) { + // Remove "jdbc:" prefix from original URL + String dbUrl = originalJdbcUrl.startsWith("jdbc:") + ? originalJdbcUrl.substring(5) + : originalJdbcUrl; + + return "jdbc:ojp[" + getHost() + ":" + getMappedPort(DEFAULT_GRPC_PORT) + "]_" + dbUrl; + } + + /** + * Enable or disable telemetry/Prometheus metrics. + * Telemetry is enabled by default. + */ + public OJPContainer withTelemetryEnabled(boolean enabled) { + this.telemetryEnabled = enabled; + withEnv("ojp.opentelemetry.enabled", String.valueOf(enabled)); + return this; + } + + /** + * Get the Prometheus metrics endpoint URL. + * The Prometheus port is automatically mapped to a random available port + * to avoid conflicts when running multiple containers. + * + * @return Prometheus metrics URL (e.g., "http://localhost:54321/metrics") + */ + public String getPrometheusUrl() { + if (!telemetryEnabled) { + throw new IllegalStateException("Telemetry is disabled. Enable it with withTelemetryEnabled(true)"); + } + return "http://" + getHost() + ":" + getMappedPort(DEFAULT_PROMETHEUS_PORT) + "/metrics"; + } + + /** + * Get the mapped Prometheus port. + * The port is randomly assigned to avoid conflicts. + * + * @return The host port mapped to the container's Prometheus port + */ + public int getPrometheusPort() { + return getMappedPort(DEFAULT_PROMETHEUS_PORT); + } +} +``` + +#### Key Features to Implement + +1. **Fluent Configuration API** + - Easy database configuration + - Optional: Circuit breaker settings + - Optional: Connection pool settings + - Optional: IP whitelisting (for production-like testing) + - Optional: Telemetry/Prometheus configuration + +2. **Network Integration** + - Support for Testcontainers network (to connect to other database containers) + - Example: Link with PostgreSQL/MySQL containers + +3. **Health Checks** + - Use OJP's built-in gRPC health service + - Ensure container is ready before tests run + +4. **Resource Management** + - Proper cleanup on test completion + - Support for singleton pattern (shared across tests) + - Support for per-test instances + +5. **Port Management** + - Automatic port mapping for gRPC (1059) and Prometheus (9159) to random available ports + - Prevents conflicts when running multiple containers in parallel + - Both ports accessible via getMappedPort() methods + +### 4. Maven Configuration (pom.xml) + +```xml + + + 4.0.0 + + ojp-testcontainers + 0.3.1-snapshot + OJP TestContainers + TestContainers integration for OJP (Open J Proxy) + + + org.openjproxy + ojp-parent + 0.3.1-snapshot + ../pom.xml + + + + 1.20.4 + 11 + 11 + + + + + + org.testcontainers + testcontainers + ${testcontainers.version} + + + + + org.testcontainers + postgresql + ${testcontainers.version} + true + + + + org.testcontainers + mysql + ${testcontainers.version} + true + + + + + org.junit.jupiter + junit-jupiter + 5.12.1 + test + + + + + org.openjproxy + ojp-jdbc-driver + 0.3.1-snapshot + test + + + + + + + + org.apache.maven.plugins + maven-source-plugin + 3.2.1 + + + attach-sources + + jar + + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.6.3 + + + attach-javadocs + + jar + + + + + + + +``` + +### 5. Usage Examples + +#### Example 1: Basic Usage with H2 + +```java +import org.openjproxy.testcontainers.OJPContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +@Testcontainers +class MyIntegrationTest { + + @Container + static OJPContainer ojp = new OJPContainer(); + + @Test + void testDatabaseAccess() throws SQLException { + // Database connection info is in the JDBC URL + String ojpUrl = ojp.buildJdbcUrl("jdbc:h2:mem:test"); + + try (Connection conn = DriverManager.getConnection(ojpUrl, "sa", "")) { + // Your test code here + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT 1"); + assertTrue(rs.next()); + } + } +} +``` + +#### Example 2: With PostgreSQL Container + +```java +@Testcontainers +class PostgresIntegrationTest { + + static Network network = Network.newNetwork(); + + @Container + static PostgreSQLContainer postgres = new PostgreSQLContainer<>("postgres:15") + .withNetwork(network) + .withNetworkAliases("postgres"); + + @Container + static OJPContainer ojp = new OJPContainer() + .withNetwork(network) + .dependsOn(postgres); + + @Test + void testThroughOJP() throws SQLException { + // Build OJP URL with the database connection details + String ojpUrl = ojp.buildJdbcUrl(postgres.getJdbcUrl()); + + try (Connection conn = DriverManager.getConnection( + ojpUrl, + postgres.getUsername(), + postgres.getPassword())) { + + // Access PostgreSQL through OJP proxy + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT 1"); + assertTrue(rs.next()); + } + } +} +``` + +#### Example 3: Singleton Pattern (Shared Container) + +```java +public abstract class BaseOJPTest { + + protected static final OJPContainer OJP_CONTAINER; + + static { + OJP_CONTAINER = new OJPContainer() + .withReuse(true); // Enable container reuse + + OJP_CONTAINER.start(); + } +} + +class MyTest extends BaseOJPTest { + @Test + void test() throws SQLException { + // Build JDBC URL for your database + String ojpUrl = OJP_CONTAINER.buildJdbcUrl("jdbc:h2:mem:test"); + + try (Connection conn = DriverManager.getConnection(ojpUrl, "sa", "")) { + // Your test code + } + } +} +``` + +## Advanced Features to Consider + +### 1. Configuration Builder Pattern + +```java +OJPContainer ojp = new OJPContainer() + .withServerConfiguration(config -> config + .withCircuitBreakerTimeout(5000) + .withCircuitBreakerThreshold(10) + .withThreadPoolSize(50) + .withMaxRequestSize(4 * 1024 * 1024)); +``` + +### 2. Multi-Database Support + +The OJP server automatically supports multiple databases - just connect with different JDBC URLs through the same OJP container. No pre-configuration needed. + +```java +@Container +static OJPContainer ojp = new OJPContainer(); + +@Test +void testMultipleDatabases() throws SQLException { + // Connect to PostgreSQL through OJP + String pgUrl = ojp.buildJdbcUrl("jdbc:postgresql://localhost:5432/db1"); + try (Connection conn1 = DriverManager.getConnection(pgUrl, "user1", "pass1")) { + // Use PostgreSQL + } + + // Connect to MySQL through the same OJP instance + String mysqlUrl = ojp.buildJdbcUrl("jdbc:mysql://localhost:3306/db2"); + try (Connection conn2 = DriverManager.getConnection(mysqlUrl, "user2", "pass2")) { + // Use MySQL + } +} +``` + +### 3. Observability Support + +**Important**: Both the gRPC port (1059) and Prometheus port (9159) are automatically mapped to random available host ports to prevent conflicts when running multiple OJP containers. + +```java +OJPContainer ojp = new OJPContainer() + .withTelemetryEnabled(true) // Enabled by default + .withDatabaseConfig("db", ...); + +// Access metrics endpoint - port is automatically mapped to avoid conflicts +String metricsUrl = ojp.getPrometheusUrl(); // e.g., "http://localhost:54321/metrics" +int prometheusPort = ojp.getPrometheusPort(); // e.g., 54321 (random) + +// Disable telemetry if not needed +OJPContainer ojpNoMetrics = new OJPContainer() + .withTelemetryEnabled(false) + .withDatabaseConfig("db", ...); +``` + +**Port Mapping Strategy**: +- **gRPC Port (1059)**: Mapped to random host port (e.g., 32768) +- **Prometheus Port (9159)**: Mapped to random host port (e.g., 32769) +- This ensures no conflicts when running multiple containers in parallel tests + +### 4. Custom OJP Server Image + +```java +OJPContainer ojp = new OJPContainer("myregistry/custom-ojp:1.0.0") + .withDatabaseConfig(...); +``` + +## Questions to Address + +### Q1: Module location - Same repo or separate? +**Answer**: Same repository as a new module `ojp-testcontainers` + +**Reasoning**: +- Version synchronization +- Easier maintenance +- Single release process +- Follows existing multi-module pattern + +### Q2: Java Version Compatibility +**Answer**: Target Java 11 (LTS) for maximum compatibility + +**Reasoning**: +- OJP JDBC Driver uses Java 11 +- Most TestContainers users are on Java 11+ +- OJP Server runs in Docker, so its Java 21 requirement is internal + +### Q3: Maven Central Requirements +**Requirements for publication**: +- ✅ Sources JAR (add maven-source-plugin) +- ✅ Javadoc JAR (add maven-javadoc-plugin) +- ✅ GPG signing (already configured in parent) +- ✅ POM metadata (inherit from parent) +- ✅ Central Publishing Maven Plugin (already configured) + +### Q4: Container Image Strategy +**Answer**: Use existing Docker image by default, allow custom images + +**Options**: +1. **Use existing Docker image** (Recommended for v1) + - `rrobetti/ojp:0.3.1-snapshot` already exists + - Lightweight, fast startup + - Matches production usage + +2. **Build from shaded JAR** (Future option) + - Use `ojp-server` shaded JAR + - More flexible for development + - Requires base image selection + +### Q5: Configuration Approach +**Answer**: Hybrid - Environment variables + fluent API + +**Why**: +- OJP server already uses environment variables +- Fluent API provides better developer experience +- Environment variables work well with Docker + +### Q6: Health Check Implementation +**Answer**: Use gRPC health check protocol + +OJP Server already implements gRPC health service. TestContainer should: +```java +waitingFor(Wait.forHealthcheck()) +// OR +waitingFor(Wait.forLogMessage(".*OJP gRPC Server started successfully.*", 1)) +``` + +### Q7: Network Configuration +**Answer**: Support both standalone and networked modes + +- Default: No network (standalone) +- Advanced: Support Testcontainers Network for multi-container tests + +## Implementation Roadmap + +### Phase 1: MVP (Minimum Viable Product) +- [ ] Create `ojp-testcontainers` module +- [ ] Implement `OJPContainer` class +- [ ] Basic database configuration support +- [ ] Health check implementation +- [ ] Basic documentation +- [ ] Integration tests with H2 +- [ ] README with usage examples + +### Phase 2: Enhanced Features +- [ ] Multi-database configuration support +- [ ] Network integration examples +- [ ] PostgreSQL integration test +- [ ] MySQL integration test +- [ ] Advanced configuration options +- [ ] Singleton/shared container patterns + +### Phase 3: Production Ready +- [ ] Comprehensive Javadoc +- [ ] Performance testing +- [ ] CI/CD integration +- [ ] Maven Central publication +- [ ] Migration guide for existing tests +- [ ] Blog post / documentation + +### Phase 4: Advanced Features +- [ ] Telemetry/observability support +- [ ] Custom server configuration +- [ ] Support for slow query segregation feature +- [ ] Multi-node OJP setup support + +## Migration Path for Existing Tests + +Current pattern in `ojp-jdbc-driver` tests: +```java +// Before: Manual server startup required +mvn clean install +java -jar ojp-server/target/ojp-server-*-shaded.jar & +mvn test -pl ojp-jdbc-driver -DenableH2Tests=true +``` + +After TestContainer implementation: +```java +// After: Automatic server startup +@Container +static OJPContainer ojp = new OJPContainer() + .withDatabaseConfig("h2", "jdbc:h2:mem:test", "sa", ""); + +@Test +void test() { + // Server automatically started + Connection conn = DriverManager.getConnection( + ojp.getJdbcUrl("h2"), "sa", ""); +} +``` + +## Risks and Mitigations + +| Risk | Impact | Mitigation | +|------|--------|------------| +| Docker not available in test environment | High | Document requirements clearly, provide fallback instructions | +| Image size / startup time | Medium | Use existing optimized image, implement reuse pattern | +| Version synchronization issues | Medium | Keep in same repo, automated version bumping | +| Configuration complexity | Low | Start simple, add features incrementally | +| Network configuration confusion | Medium | Provide clear examples for both standalone and networked modes | + +## Success Criteria + +1. ✅ JAR published to Maven Central +2. ✅ Users can add single dependency and use OJP in tests +3. ✅ No manual server startup required +4. ✅ Documentation is clear and examples work +5. ✅ Compatible with JUnit 5 and TestContainers best practices +6. ✅ Supports common use cases (H2, PostgreSQL, MySQL) + +## Open Questions for Discussion + +1. **Naming**: Should it be `ojp-testcontainers` or `ojp-testcontainer` (singular)? + - Recommendation: `ojp-testcontainers` (matches TestContainers convention) + +2. **Package name**: `org.openjproxy.testcontainers` or `org.testcontainers.ojp`? + - Recommendation: `org.openjproxy.testcontainers` (maintains project ownership) + +3. **Should we support building OJP from source in the container?** + - Recommendation: No for MVP, use existing Docker image + +4. **Should we include database drivers in the testcontainer module?** + - Recommendation: No, keep them optional/test-scoped + +5. **Version strategy**: Same version as parent or independent? + - Recommendation: Same version (release together) + +## 8. Custom TestContainers for Proprietary Databases + +### Overview + +Due to licensing restrictions, the published `ojp-testcontainers` Maven artifact **cannot include** pre-built TestContainers for proprietary databases (Oracle, SQL Server, DB2). However, developers can easily create their own TestContainer implementations following the patterns documented here. + +### Why Custom Implementations are Needed + +**Licensing Restrictions**: +- Proprietary database containers require accepting specific license agreements +- These licenses cannot be automatically accepted in a published library +- Maven Central policies prohibit redistributing proprietary database drivers + +**Benefits of Custom Implementation**: +- ✅ Full control over database version and configuration +- ✅ Can use specific JDBC driver versions required by your project +- ✅ Can customize database settings for your use case +- ✅ Compliant with database vendor licensing requirements + +### Creating a Custom OJP TestContainer + +#### Step 1: Add Dependencies to Your Test Scope + +Add the OJP TestContainer dependency and the specific database container you need: + +```xml + + + + + org.openjproxy + ojp-testcontainers + 0.3.1-snapshot + test + + + + + org.testcontainers + oracle-xe + 1.20.4 + test + + + + + com.oracle.database.jdbc + ojdbc11 + 23.3.0.23.09 + test + + +``` + +#### Step 2: Create a Custom TestContainer Class + +Create a utility class in your test source directory: + +```java +// src/test/java/com/mycompany/testutil/OJPWithOracleTestContainer.java +package com.mycompany.testutil; + +import org.openjproxy.testcontainers.OJPContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.OracleContainer; +import org.testcontainers.lifecycle.Startables; + +import java.util.stream.Stream; + +/** + * Custom TestContainer setup that combines OJP with Oracle Database. + * This is a local implementation due to Oracle licensing restrictions. + */ +public class OJPWithOracleTestContainer { + + private static Network network; + private static OracleContainer oracleContainer; + private static OJPContainer ojpContainer; + private static boolean initialized = false; + + /** + * Initialize and start both Oracle and OJP containers. + * This method is idempotent - safe to call multiple times. + */ + public static synchronized void initialize() { + if (initialized) { + return; + } + + // Create shared network + network = Network.newNetwork(); + + // Start Oracle container + oracleContainer = new OracleContainer("gvenzl/oracle-xe:21-slim") + .withNetwork(network) + .withNetworkAliases("oracle-db") + .withReuse(true); // Optional: reuse container across test runs + + // Start OJP container (no database config needed) + ojpContainer = new OJPContainer() + .withNetwork(network) + .dependsOn(oracleContainer) + .withReuse(true); // Optional: reuse container across test runs + + // Start both containers in parallel + Startables.deepStart(Stream.of(oracleContainer, ojpContainer)) + .join(); + + initialized = true; + + // Add shutdown hook + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + if (ojpContainer != null) { + ojpContainer.stop(); + } + if (oracleContainer != null) { + oracleContainer.stop(); + } + if (network != null) { + network.close(); + } + })); + } + + /** + * Get OJP JDBC URL for Oracle database. + * The database connection details are embedded in the URL. + */ + public static String getOJPJdbcUrl() { + initialize(); + // Use the network alias for Oracle (oracle-db) since they're in the same network + return ojpContainer.buildJdbcUrl("jdbc:oracle:thin:@oracle-db:1521/XEPDB1"); + } + + /** + * Get direct Oracle JDBC URL (bypassing OJP). + */ + public static String getDirectOracleUrl() { + initialize(); + return oracleContainer.getJdbcUrl(); + } + + /** + * Get Oracle username. + */ + public static String getUsername() { + initialize(); + return oracleContainer.getUsername(); + } + + /** + * Get Oracle password. + */ + public static String getPassword() { + initialize(); + return oracleContainer.getPassword(); + } +} +``` + +#### Step 3: Use in Your Tests + +```java +import com.mycompany.testutil.OJPWithOracleTestContainer; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.Statement; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +class OracleIntegrationTest { + + @BeforeAll + static void setup() { + // Initialize containers (happens once for all tests) + OJPWithOracleTestContainer.initialize(); + } + + @Test + void testOracleViaOJP() throws Exception { + try (Connection conn = DriverManager.getConnection( + OJPWithOracleTestContainer.getOJPJdbcUrl(), + OJPWithOracleTestContainer.getUsername(), + OJPWithOracleTestContainer.getPassword())) { + + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT 1 FROM DUAL"); + assertTrue(rs.next()); + } + } +} +``` + +### Examples for Different Proprietary Databases + +#### SQL Server Example + +```java +package com.mycompany.testutil; + +import org.openjproxy.testcontainers.OJPContainer; +import org.testcontainers.containers.MSSQLServerContainer; +import org.testcontainers.containers.Network; + +public class OJPWithSQLServerTestContainer { + + private static Network network; + private static MSSQLServerContainer sqlServerContainer; + private static OJPContainer ojpContainer; + private static boolean initialized = false; + + public static synchronized void initialize() { + if (initialized) return; + + network = Network.newNetwork(); + + sqlServerContainer = new MSSQLServerContainer<>("mcr.microsoft.com/mssql/server:2022-latest") + .withNetwork(network) + .withNetworkAliases("sqlserver-db") + .acceptLicense(); + + ojpContainer = new OJPContainer() + .withNetwork(network) + .dependsOn(sqlServerContainer); + + sqlServerContainer.start(); + ojpContainer.start(); + + initialized = true; + } + + public static String getOJPJdbcUrl() { + initialize(); + // Use network alias in the JDBC URL + String sqlServerNetworkUrl = sqlServerContainer.getJdbcUrl() + .replace(sqlServerContainer.getHost(), "sqlserver-db"); + return ojpContainer.buildJdbcUrl(sqlServerNetworkUrl); + } + + public static String getUsername() { + initialize(); + return sqlServerContainer.getUsername(); + } + + public static String getPassword() { + initialize(); + return sqlServerContainer.getPassword(); + } +} +``` + +#### DB2 Example + +```java +package com.mycompany.testutil; + +import org.openjproxy.testcontainers.OJPContainer; +import org.testcontainers.containers.Db2Container; +import org.testcontainers.containers.Network; + +public class OJPWithDb2TestContainer { + + private static Network network; + private static Db2Container db2Container; + private static OJPContainer ojpContainer; + private static boolean initialized = false; + + public static synchronized void initialize() { + if (initialized) return; + + network = Network.newNetwork(); + + db2Container = new Db2Container("icr.io/db2_community/db2:11.5.9.0") + .withNetwork(network) + .withNetworkAliases("db2-db") + .acceptLicense(); + + ojpContainer = new OJPContainer() + .withNetwork(network) + .dependsOn(db2Container); + + db2Container.start(); + ojpContainer.start(); + + initialized = true; + } + + public static String getOJPJdbcUrl() { + initialize(); + // Use network alias in the JDBC URL + String db2NetworkUrl = db2Container.getJdbcUrl() + .replace(db2Container.getHost(), "db2-db"); + return ojpContainer.buildJdbcUrl(db2NetworkUrl); + } + + public static String getUsername() { + initialize(); + return db2Container.getUsername(); + } + + public static String getPassword() { + initialize(); + return db2Container.getPassword(); + } +} +``` + +### Testing with Specific JDBC Driver Versions + +To test with exact JDBC driver versions: + +```xml + + + + org.postgresql + postgresql + 42.7.3 + test + + +``` + +Then configure your test: + +```java +@Test +void testSpecificDriverVersion() throws Exception { + // The JDBC driver version in your classpath will be used + // OJP will proxy connections using this specific driver version + try (Connection conn = DriverManager.getConnection( + ojpContainer.getJdbcUrl("postgres"), "user", "pass")) { + + DatabaseMetaData meta = conn.getMetaData(); + System.out.println("Driver version: " + meta.getDriverVersion()); + + // Your test code + } +} +``` + +### Best Practices for Custom TestContainers + +1. **Create Once, Reuse**: Use singleton pattern to share containers across tests +2. **Use Networks**: Connect database and OJP containers via TestContainers Network +3. **Container Reuse**: Enable `.withReuse(true)` for faster test iterations +4. **Proper Cleanup**: Register shutdown hooks to clean up resources +5. **Documentation**: Document your custom setup in your project's README +6. **Version Control**: Commit your custom TestContainer classes to version control +7. **Team Sharing**: Share custom implementations across your team via internal repositories + +### Advanced: Using with Different Database Versions + +You can test against multiple database versions: + +```java +public class OJPWithMultiplePostgresVersions { + + public static OJPContainer createWithPostgres(String postgresVersion) { + Network network = Network.newNetwork(); + + PostgreSQLContainer postgres = new PostgreSQLContainer<>( + "postgres:" + postgresVersion) + .withNetwork(network) + .withNetworkAliases("postgres-db"); + + OJPContainer ojp = new OJPContainer() + .withNetwork(network) + .dependsOn(postgres) + .withDatabaseConfig("postgres", + postgres.getJdbcUrl(), + postgres.getUsername(), + postgres.getPassword()); + + postgres.start(); + ojp.start(); + + return ojp; + } +} + +// In your test +@ParameterizedTest +@ValueSource(strings = {"12", "13", "14", "15", "16"}) +void testAcrossPostgresVersions(String version) throws Exception { + OJPContainer ojp = OJPWithMultiplePostgresVersions.createWithPostgres(version); + + try (Connection conn = DriverManager.getConnection( + ojp.getJdbcUrl("postgres"), "test", "test")) { + // Test against specific version + } finally { + ojp.stop(); + } +} +``` + +### Summary: Licensing Approach + +| Database Type | Published in ojp-testcontainers | Custom Implementation Required | +|--------------|--------------------------------|-------------------------------| +| PostgreSQL | ✅ Yes | ❌ No (use published artifact) | +| MySQL/MariaDB | ✅ Yes | ❌ No (use published artifact) | +| H2 | ✅ Yes | ❌ No (use published artifact) | +| Oracle | ❌ No | ✅ Yes (create custom as shown above) | +| SQL Server | ❌ No | ✅ Yes (create custom as shown above) | +| DB2 | ❌ No | ✅ Yes (create custom as shown above) | + +This approach ensures: +- ✅ Compliance with all database licensing requirements +- ✅ Maven Central publication is legally sound +- ✅ Developers have full flexibility for proprietary databases +- ✅ Documentation provides clear guidance for all scenarios + +## References + +- TestContainers Documentation: https://www.testcontainers.org/ +- Existing SQL Server TestContainer: `ojp-jdbc-driver/src/test/java/openjproxy/jdbc/testutil/SQLServerTestContainer.java` +- OJP Server Main: `ojp-server/src/main/java/org/openjproxy/grpc/server/GrpcServer.java` +- OJP Server Configuration: `ojp-server/src/main/java/org/openjproxy/grpc/server/ServerConfiguration.java` +- Maven Central Requirements: https://central.sonatype.org/publish/requirements/ + +## Next Steps + +1. Review this analysis with maintainers +2. Address open questions +3. Create GitHub issue for tracking +4. Implement Phase 1 (MVP) +5. Iterate based on feedback + +--- + +**Document Version**: 1.0 +**Date**: 2025-12-17 +**Author**: GitHub Copilot Analysis +**Status**: Draft for Review diff --git a/documents/OJP_TESTCONTAINER_ARCHITECTURE.md b/documents/OJP_TESTCONTAINER_ARCHITECTURE.md new file mode 100644 index 000000000..f0d9bef26 --- /dev/null +++ b/documents/OJP_TESTCONTAINER_ARCHITECTURE.md @@ -0,0 +1,394 @@ +# OJP TestContainer Architecture + +## Current vs. Proposed Architecture + +### Current Testing Workflow (Manual) + +``` +┌─────────────────────────────────────────────────────────┐ +│ Developer │ +└───────────────────────────┬─────────────────────────────┘ + │ + ▼ + ┌────────────────────────────────┐ + │ 1. Build OJP Server │ + │ mvn clean install │ + └────────────┬───────────────────┘ + │ + ▼ + ┌────────────────────────────────┐ + │ 2. Start OJP Server Manually │ + │ java -jar ojp-server.jar │ + └────────────┬───────────────────┘ + │ + ▼ + ┌────────────────────────────────┐ + │ 3. Run Tests │ + │ mvn test │ + └────────────┬───────────────────┘ + │ + ▼ + ┌────────────────────────────────┐ + │ 4. Manually Stop Server │ + └────────────────────────────────┘ + +Problem: Manual steps, error-prone, slow feedback loop +``` + +### Proposed Testing Workflow (Automated with TestContainer) + +``` +┌─────────────────────────────────────────────────────────┐ +│ Developer │ +└───────────────────────────┬─────────────────────────────┘ + │ + ▼ + ┌────────────────────────────────┐ + │ 1. Run Tests │ + │ mvn test │ + │ (Everything automatic!) │ + └────────────┬───────────────────┘ + │ + ▼ + ✅ Done! + +Benefit: Automatic, reliable, fast feedback loop +``` + +## Component Architecture + +``` +┌────────────────────────────────────────────────────────────────────┐ +│ User's Test Code │ +│ │ +│ @Testcontainers │ +│ class MyTest { │ +│ @Container │ +│ static OJPContainer ojp = new OJPContainer() │ +│ .withDatabaseConfig("db1", ...) │ +│ } │ +└──────────────────────────────┬─────────────────────────────────────┘ + │ + │ Uses + ▼ +┌────────────────────────────────────────────────────────────────────┐ +│ ojp-testcontainers Module (NEW) │ +│ │ +│ ┌────────────────────────────────────────────┐ │ +│ │ OJPContainer extends GenericContainer │ │ +│ │ │ │ +│ │ + withDatabaseConfig(...) │ │ +│ │ + getJdbcUrl(...) │ │ +│ │ + getGrpcUrl() │ │ +│ │ + getPrometheusUrl() │ │ +│ │ + getPrometheusPort() │ │ +│ │ + withTelemetryEnabled(...) │ │ +│ │ + withServerConfiguration(...) │ │ +│ └────────────────────────────────────────────┘ │ +│ │ │ +│ │ Extends │ +│ ▼ │ +│ ┌────────────────────────────────────────────┐ │ +│ │ TestContainers GenericContainer │ │ +│ └────────────────────────────────────────────┘ │ +└──────────────────────────────┬─────────────────────────────────────┘ + │ + │ Starts + ▼ +┌────────────────────────────────────────────────────────────────────┐ +│ Docker Container │ +│ │ +│ ┌────────────────────────────────────────────────────────┐ │ +│ │ rrobetti/ojp:0.3.1-snapshot │ │ +│ │ │ │ +│ │ ┌──────────────────────────────────────────┐ │ │ +│ │ │ OJP gRPC Server (Java 21) │ │ │ +│ │ │ │ │ │ +│ │ │ - gRPC Port: 1059 → random (e.g. 32768)│ │ │ +│ │ │ - Prometheus: 9159 → random (e.g. 32769)│ │ │ +│ │ │ - Health Check: gRPC health service │ │ │ +│ │ │ - Configuration: ENV variables │ │ │ +│ │ │ │ │ │ +│ │ │ Note: Both ports mapped to random host │ │ │ +│ │ │ ports to prevent conflicts │ │ │ +│ │ └──────────────────────────────────────────┘ │ │ +│ │ │ │ +│ │ ┌──────────────────────────────────────────┐ │ │ +│ │ │ HikariCP Connection Pool │ │ │ +│ │ │ │ │ │ +│ │ │ - Database 1 Pool │ │ │ +│ │ │ - Database 2 Pool │ │ │ +│ │ │ - ... │ │ │ +│ │ └──────────────────────────────────────────┘ │ │ +│ └────────────────────────────────────────────────────────┘ │ +│ │ +└──────────────────────────────┬─────────────────────────────────────┘ + │ + │ Connects to + ▼ +┌────────────────────────────────────────────────────────────────────┐ +│ Actual Database Containers │ +│ │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ PostgreSQL │ │ MySQL │ │ H2 │ │ +│ │ Container │ │ Container │ │ (embedded) │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +└────────────────────────────────────────────────────────────────────┘ +``` + +## Network Integration Example + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ TestContainers Network │ +│ │ +│ ┌───────────────────┐ ┌──────────────────┐ │ +│ │ PostgreSQL │◄────────────────│ OJP Container │ │ +│ │ Container │ Internal │ │ │ +│ │ │ Network │ Port: 1059 │ │ +│ │ Alias: postgres │ Connection │ │ │ +│ │ Port: 5432 │ │ Config: │ │ +│ └───────────────────┘ │ DB URL = │ │ +│ ▲ │ postgres:5432 │ │ +│ │ └───────┬──────────┘ │ +│ │ │ │ +│ │ │ │ +└───────────┼────────────────────────────────────┼───────────────┘ + │ │ + │ Direct │ Via OJP + │ Access │ Proxy + │ │ + │ ▼ + ┌────┴────────────────────────────────────────┐ + │ Test Code │ + │ │ + │ Option 1: Direct access to postgres │ + │ Option 2: Access through OJP │ + └─────────────────────────────────────────────┘ +``` + +## Module Dependencies + +``` +ojp-parent (pom.xml) + │ + ├── ojp-grpc-commons (Java 11) + │ └── gRPC contracts + │ + ├── ojp-jdbc-driver (Java 11) + │ ├── depends on: ojp-grpc-commons + │ └── JDBC driver implementation + │ + ├── ojp-server (Java 21) + │ ├── depends on: ojp-grpc-commons + │ └── produces: Docker image + shaded JAR + │ + └── ojp-testcontainers (Java 11) ← NEW + ├── depends on: TestContainers + ├── uses: OJP Docker image + ├── test depends on: ojp-jdbc-driver + └── produces: JAR for Maven Central +``` + +## Data Flow in Tests + +``` +Test Code + │ + │ 1. Create Connection + ├──► DriverManager.getConnection(ojp.getJdbcUrl("db1")) + │ + │ 2. OJP JDBC URL + ├──► jdbc:ojp[localhost:12345]_postgresql://postgres:5432/test + │ │ + │ └─► Port mapped from container + │ + │ 3. gRPC Request + ├──► OJP Container (localhost:12345) + │ │ + │ │ 4. Execute SQL + │ ├──► HikariCP Pool + │ │ │ + │ │ │ 5. Real Connection + │ │ ├──► PostgreSQL Container + │ │ │ + │ │ │ 6. Results + │ │ ◄──── + │ │ + │ │ 7. gRPC Response + │ ◄──── + │ + │ 8. JDBC Results + ◄──── +``` + +## Class Hierarchy + +``` +java.lang.Object + │ + └── org.testcontainers.containers.GenericContainer + │ + └── org.openjproxy.testcontainers.OJPContainer + │ + ├── withDatabaseConfig(String name, String url, String user, String pass) + ├── withServerConfiguration(ServerConfigBuilder config) + ├── withTelemetryEnabled(boolean enabled) + ├── withPrometheusPort(int port) + ├── getJdbcUrl(String dbName) + ├── getGrpcUrl() + ├── getMetricsUrl() + └── getDatabaseConfig(String name) +``` + +## Lifecycle + +``` +Test Execution + │ + │ @BeforeAll / @Container annotation + │ + ├─► OJPContainer.start() + │ │ + │ ├─► Pull Docker image (if needed) + │ ├─► Start container + │ ├─► Wait for health check + │ └─► Container ready ✅ + │ + │ Test methods execute + │ + ├─► @Test test1() { ... } + ├─► @Test test2() { ... } + ├─► @Test test3() { ... } + │ + │ @AfterAll / automatic cleanup + │ + └─► OJPContainer.stop() + │ + └─► Stop and remove container +``` + +## Maven Central Publication Flow + +``` +Developer + │ + ├─► mvn clean deploy + │ + └─► Maven Central Publishing Plugin + │ + ├─► Build ojp-testcontainers.jar + ├─► Build ojp-testcontainers-sources.jar + ├─► Build ojp-testcontainers-javadoc.jar + ├─► Sign with GPG + │ + └─► Upload to Maven Central + │ + └─► Available for download + │ + └─► Users add dependency: + + org.openjproxy + ojp-testcontainers + 0.3.1-snapshot + test + +``` + +## Configuration Flow + +``` +User Configuration (Fluent API) + │ + │ OJPContainer ojp = new OJPContainer() + │ .withDatabaseConfig("db1", "jdbc:postgresql://...", "user", "pass") + │ .withCircuitBreakerTimeout(5000) + │ .withThreadPoolSize(50); + │ + ▼ +Environment Variables (Internal) + │ + │ OJP_DB_DB1_URL=jdbc:postgresql://... + │ OJP_DB_DB1_USERNAME=user + │ OJP_DB_DB1_PASSWORD=pass + │ OJP_CIRCUIT_BREAKER_TIMEOUT=5000 + │ OJP_THREAD_POOL_SIZE=50 + │ + ▼ +OJP Server (reads from environment) + │ + └─► ServerConfiguration.java + │ + └─► Creates connection pools, configures settings +``` + +--- + +## Database Support Strategy + +### Published JAR (Maven Central) + +``` +┌────────────────────────────────────────────────────┐ +│ ojp-testcontainers (Maven Central) │ +│ │ +│ ┌──────────────────────────────────────────┐ │ +│ │ Pre-configured Database Support │ │ +│ │ │ │ +│ │ ✅ PostgreSQL │ │ +│ │ ✅ MySQL / MariaDB │ │ +│ │ ✅ H2 │ │ +│ │ │ │ +│ │ License: Open-source, no restrictions │ │ +│ └──────────────────────────────────────────┘ │ +│ │ +│ ┌──────────────────────────────────────────┐ │ +│ │ Documentation for Custom Impl │ │ +│ │ │ │ +│ │ 📝 Oracle Database (example code) │ │ +│ │ 📝 SQL Server (example code) │ │ +│ │ 📝 DB2 (example code) │ │ +│ │ │ │ +│ │ Cannot publish due to licensing │ │ +│ └──────────────────────────────────────────┘ │ +└────────────────────────────────────────────────────┘ +``` + +### Custom Implementation (User's Project) + +``` +User's Project + │ + ├─► src/test/java/com/company/testutil/ + │ │ + │ ├─► OJPWithOracleContainer.java + │ │ └─► OracleContainer + OJPContainer + │ │ + │ ├─► OJPWithSQLServerContainer.java + │ │ └─► MSSQLServerContainer + OJPContainer + │ │ + │ └─► OJPWithDb2Container.java + │ └─► Db2Container + OJPContainer + │ + └─► pom.xml + │ + ├─► ojp-testcontainers (from Maven Central) + ├─► testcontainers-oracle (from Maven Central) + └─► oracle-jdbc (from Maven/vendor) +``` + +**Benefits**: +- ✅ Legal compliance with licensing +- ✅ Full flexibility for proprietary databases +- ✅ Can use exact driver versions needed +- ✅ Complete code examples provided + +--- + +This architecture enables: +✅ Automatic OJP server lifecycle management +✅ Isolated test environments +✅ Easy multi-database testing +✅ CI/CD integration +✅ No manual setup required +✅ License-compliant Maven Central publication diff --git a/documents/OJP_TESTCONTAINER_INDEX.md b/documents/OJP_TESTCONTAINER_INDEX.md new file mode 100644 index 000000000..6cd5e63a1 --- /dev/null +++ b/documents/OJP_TESTCONTAINER_INDEX.md @@ -0,0 +1,422 @@ +# OJP TestContainer Documentation Index + +> **Purpose**: Create a TestContainer for OJP (Open J Proxy) to simplify integration testing +> **Status**: Analysis Complete ✅ - Ready for Implementation +> **Date**: 2025-12-17 + +--- + +## 📚 Documentation Overview + +This analysis provides a complete blueprint for creating an OJP TestContainer that will be published to Maven Central, enabling developers to easily integrate OJP server into their integration tests with zero manual setup. + +**Total Documentation**: 4 comprehensive documents, 1,600+ lines, covering all aspects from high-level decisions to detailed implementation. + +--- + +## 🎯 Start Here + +### For Decision Makers & Reviewers +👉 **[Quick Reference Guide](OJP_TESTCONTAINER_QUICKREF.md)** - Best starting point for everyone + +### For Quick Overview +👉 **[Executive Summary](OJP_TESTCONTAINER_SUMMARY.md)** - 5-minute read with key recommendations + +### For Technical Details +👉 **[Technical Analysis](OJP_TESTCONTAINER_ANALYSIS.md)** - Complete technical specification + +### For Architecture Understanding +👉 **[Architecture Diagrams](OJP_TESTCONTAINER_ARCHITECTURE.md)** - Visual representations and data flows + +--- + +## 📖 Document Details + +### 1. Quick Reference Guide +**File**: [OJP_TESTCONTAINER_QUICKREF.md](OJP_TESTCONTAINER_QUICKREF.md) (449 lines) + +**Purpose**: One-stop reference for all TestContainer information + +**Contents**: +- What is this and why it's needed +- Quick start guide (for future users) +- Key decisions made +- Implementation plan with timeline +- Comprehensive FAQ (15+ questions) +- Implementation checklist +- Success metrics +- Status tracking + +**Best For**: Navigation, getting started, finding answers quickly + +--- + +### 2. Executive Summary +**File**: [OJP_TESTCONTAINER_SUMMARY.md](OJP_TESTCONTAINER_SUMMARY.md) (230 lines) + +**Purpose**: High-level overview for stakeholders and decision makers + +**Contents**: +- Quick summary of the proposal +- Key recommendations with rationale +- All important questions answered +- Minimal usage example +- Benefits analysis +- Risk assessment (LOW 🟢) +- Next actions + +**Best For**: Management, product owners, architects needing quick overview + +--- + +### 3. Technical Analysis +**File**: [OJP_TESTCONTAINER_ANALYSIS.md](OJP_TESTCONTAINER_ANALYSIS.md) (599 lines) + +**Purpose**: Complete technical blueprint for implementation + +**Contents**: +- Current state analysis + - Existing project structure + - OJP server characteristics + - Current testing patterns +- Recommended implementation approach + - Module location and structure + - Implementation design with code examples + - Maven configuration +- Usage examples + - Basic usage with H2 + - Integration with PostgreSQL container + - Singleton pattern for shared containers +- Advanced features + - Configuration builder pattern + - Multi-database support + - Observability integration +- Implementation roadmap (4 phases) +- Questions addressed (7 major questions) +- Risks and mitigations +- Success criteria +- Open questions for discussion + +**Best For**: Developers implementing the feature, technical architects + +--- + +### 4. Architecture Diagrams +**File**: [OJP_TESTCONTAINER_ARCHITECTURE.md](OJP_TESTCONTAINER_ARCHITECTURE.md) (325 lines) + +**Purpose**: Visual understanding of architecture and data flows + +**Contents**: +- Current vs. proposed workflow diagrams +- Component architecture diagram +- Network integration examples +- Module dependencies tree +- Data flow through system +- Class hierarchy +- Lifecycle management diagram +- Configuration flow +- Maven Central publication flow + +**Best For**: Visual learners, architects, developers new to the project + +--- + +## 🎯 Key Recommendations (TL;DR) + +| Decision | Recommendation | Why? | +|----------|---------------|------| +| **Location** | New module `ojp-testcontainers` in this repo | Version sync, single release, easier maintenance | +| **Strategy** | Use existing Docker image | Fast, tested, matches production | +| **Java Version** | Java 11 | Maximum compatibility | +| **API Design** | Fluent API + Environment variables | Great DX, works well with OJP | +| **Publication** | Maven Central | Using existing infrastructure | +| **Database Support** | Open-source only in published JAR | Licensing compliance; custom implementations for proprietary DBs | + +--- + +## 💡 What Problem Does This Solve? + +### Current Workflow (Manual - Pain Points ❌) + +```bash +# Step 1: Build OJP +mvn clean install + +# Step 2: Start OJP server manually +java -jar ojp-server/target/ojp-server-0.3.1-snapshot-shaded.jar & + +# Step 3: Run tests +mvn test -pl ojp-jdbc-driver -DenableH2Tests=true + +# Step 4: Remember to kill server +pkill -f ojp-server +``` + +**Problems**: +- 4 manual steps +- Easy to forget steps +- Server left running +- Not CI/CD friendly +- Slow feedback loop + +### Proposed Workflow (Automatic - Benefits ✅) + +```java +@Testcontainers +class MyTest { + @Container + static OJPContainer ojp = new OJPContainer(); + + @Test + void test() throws SQLException { + // Database config is in the JDBC URL + String ojpUrl = ojp.buildJdbcUrl("jdbc:postgresql://localhost:5432/test"); + try (Connection conn = DriverManager.getConnection(ojpUrl, "user", "pass")) { + // Everything automatic! + } + } +} +``` + +**Benefits**: +- Zero manual steps +- Automatic lifecycle +- CI/CD ready +- Isolated tests +- Fast feedback + +--- + +## 📋 Implementation Roadmap + +### Phase 1: MVP (2-3 weeks) +- Core `OJPContainer` class +- Basic database configuration +- Health checks +- H2 integration tests +- Documentation + +### Phase 2: Enhanced Features (1-2 weeks) +- Multi-database support +- Network integration +- PostgreSQL/MySQL examples +- Advanced configuration + +### Phase 3: Production Ready (1 week) +- Maven Central publication +- Comprehensive documentation +- Performance testing +- Release announcement + +### Phase 4: Advanced (Future) +- Telemetry support +- Custom server configuration +- Multi-node support + +**Total Estimated Time**: 4-6 weeks + +--- + +## ❓ Key Questions Answered + +| Question | Answer | Document | +|----------|--------|----------| +| Same repo or separate? | **Same repo** as new module | [Summary](OJP_TESTCONTAINER_SUMMARY.md#q-should-this-be-a-separate-repository) | +| Which Java version? | **Java 11** for compatibility | [Analysis](OJP_TESTCONTAINER_ANALYSIS.md#q2-java-version-compatibility) | +| Use existing Docker image? | **Yes**, `rrobetti/ojp:0.3.1-snapshot` | [Summary](OJP_TESTCONTAINER_SUMMARY.md#-decision-2-implementation-strategy) | +| Maven Central requirements? | **All met** in parent POM | [Analysis](OJP_TESTCONTAINER_ANALYSIS.md#q3-maven-central-requirements) | +| Configuration approach? | **Fluent API** + env vars | [Summary](OJP_TESTCONTAINER_SUMMARY.md#-decision-4-configuration-approach) | +| How to handle multiple databases? | **Built-in support** via config | [QuickRef](OJP_TESTCONTAINER_QUICKREF.md#q-can-i-use-multiple-databases-in-one-test) | +| Performance impact? | **2-3 seconds** startup (cached) | [QuickRef](OJP_TESTCONTAINER_QUICKREF.md#q-whats-the-performance-impact) | + +--- + +## 🎓 Usage Example + +After implementation, using OJP in tests will be this simple: + +```xml + + + org.openjproxy + ojp-testcontainers + 0.3.1-snapshot + test + +``` + +```java +// MyTest.java +import org.openjproxy.testcontainers.OJPContainer; + +@Testcontainers +class MyDatabaseTest { + + @Container + static OJPContainer ojp = new OJPContainer(); + + @Test + void testDatabaseAccess() throws SQLException { + // Build OJP JDBC URL from original database URL + String ojpUrl = ojp.buildJdbcUrl("jdbc:postgresql://localhost:5432/test"); + + try (Connection conn = DriverManager.getConnection( + ojpUrl, "user", "password")) { + + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT COUNT(*) FROM users"); + assertTrue(rs.next()); + } + } +} +``` + +**That's it!** OJP server automatically: +- ✅ Starts before tests +- ✅ Configures connection pools +- ✅ Provides connection URLs +- ✅ Stops after tests + +--- + +## 🔐 Database Licensing Strategy + +### Published to Maven Central (Open-Source Databases) + +The `ojp-testcontainers` artifact will include **pre-configured support for open-source databases only**: + +| Database | Status | Reason | +|----------|--------|--------| +| PostgreSQL | ✅ Included | Open-source, no license restrictions | +| MySQL / MariaDB | ✅ Included | Open-source, no license restrictions | +| H2 | ✅ Included | Open-source, no license restrictions | +| Oracle | 📝 Custom only | Proprietary license, cannot publish | +| SQL Server | 📝 Custom only | Proprietary license, cannot publish | +| DB2 | 📝 Custom only | Proprietary license, cannot publish | + +### Custom Implementations for Proprietary Databases + +Developers can create simple custom TestContainer implementations for proprietary databases following documented patterns: + +```java +// Your project: src/test/java/com/mycompany/testutil/OJPWithOracleContainer.java +public class OJPWithOracleContainer { + private static OracleContainer oracle = new OracleContainer("gvenzl/oracle-xe:21-slim") + .withNetworkAliases("oracle-db"); + private static OJPContainer ojp = new OJPContainer() + .withNetwork(network) + .dependsOn(oracle); + + public static String getOJPJdbcUrl() { + return ojp.buildJdbcUrl("jdbc:oracle:thin:@oracle-db:1521/XEPDB1"); + } +} +``` + +**Full Documentation**: Complete code examples for Oracle, SQL Server, and DB2 are provided in [Section 8 of the Technical Analysis](OJP_TESTCONTAINER_ANALYSIS.md#8-custom-testcontainers-for-proprietary-databases). + +**Benefits of This Approach**: +- ✅ Complies with Maven Central policies and database licensing +- ✅ Published artifact is legally sound +- ✅ Developers retain full flexibility for proprietary databases +- ✅ Can use exact JDBC driver versions needed +- ✅ Complete documentation and examples provided + +--- + +## 📊 Risk Assessment + +| Risk | Level | Mitigation | +|------|-------|------------| +| Technical feasibility | 🟢 Low | Pattern already proven with SQL Server TestContainer | +| Maintenance burden | 🟢 Low | Part of main repository | +| Implementation complexity | 🟡 Medium | Well-understood technology, clear design | +| Docker availability | 🟡 Medium | Clear documentation, graceful fallback | +| Port conflicts | 🟢 Low | Automatic random port mapping for gRPC (1059) and Prometheus (9159) | +| Community adoption | 🟢 Low | Solves real pain point | + +**Overall Risk**: 🟢 **LOW** - Safe to proceed + +--- + +## ✅ Success Criteria + +How we'll measure success: + +1. ✅ Published to Maven Central +2. ✅ Zero manual steps required for users +3. ✅ Works on all major CI/CD platforms +4. ✅ Clear documentation with working examples +5. ✅ Less than 5 lines of code to get started +6. ✅ Positive community feedback +7. ✅ Adopted in OJP's own integration tests + +--- + +## 🤔 Open Questions for Maintainers + +1. **Module Name**: Is `ojp-testcontainers` acceptable? +2. **Package Name**: Is `org.openjproxy.testcontainers` OK? +3. **Priority**: Which databases should we support first in examples? +4. **Timeline**: Any release deadline considerations? +5. **Migration**: Should we migrate existing OJP tests to use this? + +--- + +## 📞 Next Steps + +### For Maintainers +1. ✅ Review this analysis (all 4 documents) +2. ⏳ Discuss and answer open questions +3. ⏳ Approve or provide feedback +4. ⏳ Create GitHub issue for tracking + +### For Implementation +1. ⏳ Get approval +2. ⏳ Create module structure +3. ⏳ Implement Phase 1 (MVP) +4. ⏳ Test and iterate +5. ⏳ Publish to Maven Central + +--- + +## 🔗 Related Resources + +- **Existing Implementation**: [SQLServerTestContainer.java](../ojp-jdbc-driver/src/test/java/openjproxy/jdbc/testutil/SQLServerTestContainer.java) +- **OJP Server**: [GrpcServer.java](../ojp-server/src/main/java/org/openjproxy/grpc/server/GrpcServer.java) +- **Configuration**: [ServerConfiguration.java](../ojp-server/src/main/java/org/openjproxy/grpc/server/ServerConfiguration.java) +- **TestContainers Docs**: https://www.testcontainers.org/ +- **Maven Central**: https://central.sonatype.org/ + +--- + +## 📈 Document Statistics + +- **Total Documents**: 4 +- **Total Lines**: 1,600+ +- **Total Size**: ~55 KB +- **Code Examples**: 15+ +- **Diagrams**: 10+ +- **Questions Answered**: 20+ + +--- + +## 🎉 Conclusion + +This analysis provides a **complete, actionable blueprint** for creating an OJP TestContainer that will: + +1. **Simplify integration testing** - Zero manual server management +2. **Improve developer experience** - Simple fluent API +3. **Enable CI/CD adoption** - Works anywhere Docker runs +4. **Benefit the community** - Published to Maven Central +5. **Follow best practices** - Proven TestContainers patterns + +**Risk is LOW**, **value is HIGH**, and the **path forward is clear**. + +**Ready to proceed!** 🚀 + +--- + +**Analysis Completed By**: GitHub Copilot +**Date**: 2025-12-17 +**Status**: Awaiting Maintainer Review and Approval diff --git a/documents/OJP_TESTCONTAINER_QUICKREF.md b/documents/OJP_TESTCONTAINER_QUICKREF.md new file mode 100644 index 000000000..e7cba06fc --- /dev/null +++ b/documents/OJP_TESTCONTAINER_QUICKREF.md @@ -0,0 +1,536 @@ +# OJP TestContainer - Quick Reference Guide + +> **Status**: Analysis Complete - Ready for Implementation +> +> **Last Updated**: 2025-12-17 + +## 📋 Table of Contents + +1. [What is this?](#what-is-this) +2. [Quick Start Guide](#quick-start-guide) +3. [Key Decisions](#key-decisions) +4. [Implementation Plan](#implementation-plan) +5. [Related Documents](#related-documents) +6. [FAQ](#faq) + +--- + +## What is this? + +A plan to create an **OJP TestContainer** - a reusable Java library that makes it trivial to run OJP server in integration tests using TestContainers. + +### Problem it Solves + +**Current Workflow** (Manual): +```bash +# Step 1: Build OJP +mvn clean install + +# Step 2: Start OJP server manually +java -jar ojp-server/target/ojp-server-0.3.1-snapshot-shaded.jar & + +# Step 3: Run tests +mvn test -pl ojp-jdbc-driver -DenableH2Tests=true + +# Step 4: Kill server manually +pkill -f ojp-server +``` + +**Proposed Workflow** (Automatic): +```java +@Testcontainers +class MyTest { + @Container + static OJPContainer ojp = new OJPContainer() + .withDatabaseConfig("db", "jdbc:postgresql://...", "user", "pass"); + + @Test + void test() { + // OJP automatically started, tested, and stopped! + } +} +``` + +--- + +## Quick Start Guide + +### For End Users (After Implementation) + +**Step 1**: Add dependency +```xml + + org.openjproxy + ojp-testcontainers + 0.3.1-snapshot + test + +``` + +**Step 2**: Write test +```java +import org.openjproxy.testcontainers.OJPContainer; + +@Testcontainers +class DatabaseTest { + @Container + static OJPContainer ojp = new OJPContainer(); + + @Test + void testQuery() throws SQLException { + // Database config is in the JDBC URL + String ojpUrl = ojp.buildJdbcUrl("jdbc:postgresql://localhost:5432/test"); + + try (Connection conn = DriverManager.getConnection( + ojpUrl, "user", "password")) { + + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT 1"); + assertTrue(rs.next()); + } + } +} +``` + +**Step 3**: Run +```bash +mvn test +``` + +That's it! ✅ + +--- + +## Key Decisions + +### ✅ Decision 1: Module Location +**Create as new module in same repository**: `ojp-testcontainers/` + +**Why?** +- Version synchronization +- Single release process +- Easier dependency management +- Follows existing pattern + +### ✅ Decision 2: Implementation Strategy +**Use existing Docker image** (`rrobetti/ojp:0.3.1-snapshot`) + +**Why?** +- Image already exists and tested +- Fast startup +- Matches production usage + +### ✅ Decision 3: Java Version +**Target Java 11** + +**Why?** +- Maximum compatibility +- Matches `ojp-jdbc-driver` +- OJP server runs in Docker (its Java 21 requirement is internal) + +### ✅ Decision 4: Configuration Approach +**Fluent API + Environment Variables** + +**Example**: +```java +OJPContainer ojp = new OJPContainer() + .withDatabaseConfig("db1", jdbcUrl, user, pass) + .withCircuitBreakerTimeout(5000) + .withThreadPoolSize(50); +``` + +### ✅ Decision 5: Publication +**Maven Central** (using existing infrastructure) + +**Why?** +- Parent POM already configured +- Same process as other modules +- Wide accessibility + +### ✅ Decision 6: Database Licensing Strategy +**Published artifact: Open-source databases only** + +**Included in Maven Central**: +- ✅ PostgreSQL, MySQL, MariaDB, H2 + +**Custom implementations** (documentation provided): +- 📝 Oracle Database +- 📝 Microsoft SQL Server +- 📝 IBM DB2 + +**Why?** +- Licensing compliance with Maven Central policies +- Proprietary databases require accepting licenses +- Full documentation provided for custom implementations + +--- + +## Implementation Plan + +### Phase 1: MVP (Essential Features) + +**Module Setup**: +- [ ] Create `ojp-testcontainers/` directory +- [ ] Create `pom.xml` with parent reference +- [ ] Add to parent `` list +- [ ] Configure Maven Central publishing + +**Core Implementation**: +- [ ] `OJPContainer` class extending `GenericContainer` +- [ ] `withDatabaseConfig()` method +- [ ] `getJdbcUrl()` method +- [ ] Health check implementation +- [ ] Basic configuration support + +**Testing**: +- [ ] Unit tests for configuration +- [ ] Integration test with H2 +- [ ] Integration test with PostgreSQL container + +**Documentation**: +- [ ] README.md with examples +- [ ] Javadoc for public APIs +- [ ] Usage guide + +**Estimated Time**: 2-3 weeks + +### Phase 2: Enhanced Features + +**Features**: +- [ ] Multi-database configuration support +- [ ] Network integration examples +- [ ] MySQL integration test +- [ ] Advanced server configuration +- [ ] Container reuse patterns +- [ ] Performance optimization + +**Documentation**: +- [ ] Advanced usage examples +- [ ] Migration guide for existing tests +- [ ] Best practices guide + +**Estimated Time**: 1-2 weeks + +### Phase 3: Production Ready + +**Tasks**: +- [ ] Comprehensive Javadoc +- [ ] Performance testing +- [ ] CI/CD integration +- [ ] Maven Central publication +- [ ] Release notes +- [ ] Blog post / announcement + +**Estimated Time**: 1 week + +**Total Estimated Time**: 4-6 weeks + +--- + +## Related Documents + +| Document | Purpose | Audience | +|----------|---------|----------| +| [OJP_TESTCONTAINER_SUMMARY.md](OJP_TESTCONTAINER_SUMMARY.md) | Executive summary and quick decisions | Product owners, architects | +| [OJP_TESTCONTAINER_ANALYSIS.md](OJP_TESTCONTAINER_ANALYSIS.md) | Full technical analysis | Developers, implementers | +| [OJP_TESTCONTAINER_ARCHITECTURE.md](OJP_TESTCONTAINER_ARCHITECTURE.md) | Architecture diagrams and data flow | Developers, architects | +| This file | Quick reference and navigation | Everyone | + +--- + +## FAQ + +### Q: Why not a separate repository? + +**A**: Keeping it in the same repo ensures: +- Automatic version synchronization +- Single release process +- Easier dependency management +- Single issue tracker + +### Q: What databases will be supported? + +**A**: The published Maven Central artifact includes **open-source databases only** due to licensing: + +**Included in published JAR**: +- ✅ H2 (embedded, no Docker needed) +- ✅ PostgreSQL +- ✅ MySQL / MariaDB + +**Requires custom implementation** (full documentation provided): +- 📝 Oracle Database +- 📝 Microsoft SQL Server +- 📝 IBM DB2 + +For proprietary databases, you create a simple custom TestContainer in your test code following our documented patterns. See the [full analysis](OJP_TESTCONTAINER_ANALYSIS.md#8-custom-testcontainers-for-proprietary-databases) for complete examples. + +### Q: How do I use OJP with Oracle/SQL Server/DB2? + +**A**: Create a custom TestContainer in your test code: + +```java +// src/test/java/com/mycompany/testutil/OJPWithOracleContainer.java +public class OJPWithOracleContainer { + private static Network network = Network.newNetwork(); + + private static OracleContainer oracle = new OracleContainer("gvenzl/oracle-xe:21-slim") + .withNetwork(network) + .withNetworkAliases("oracle-db"); + + private static OJPContainer ojp = new OJPContainer() + .withNetwork(network) + .dependsOn(oracle); + + public static void initialize() { + oracle.start(); + ojp.start(); + } + + public static String getOJPJdbcUrl() { + // Use network alias in the JDBC URL + return ojp.buildJdbcUrl("jdbc:oracle:thin:@oracle-db:1521/XEPDB1"); + } + + public static String getUsername() { + return oracle.getUsername(); + } + + public static String getPassword() { + return oracle.getPassword(); + } +} +``` + +Full examples for Oracle, SQL Server, and DB2 are in the [technical analysis](OJP_TESTCONTAINER_ANALYSIS.md#8-custom-testcontainers-for-proprietary-databases). + +### Q: Will this work in CI/CD? + +**A**: Yes! TestContainers works anywhere Docker is available: +- GitHub Actions ✅ +- GitLab CI ✅ +- Jenkins ✅ +- CircleCI ✅ +- Local development ✅ + +### Q: What if Docker isn't available? + +**A**: Tests will be skipped gracefully with clear error messages. Documentation will explain: +- How to install Docker +- Alternative manual setup +- How to disable tests + +### Q: Can I use multiple databases in one test? + +**A**: Yes! OJP already supports multiple databases: +```java +OJPContainer ojp = new OJPContainer() + .withDatabaseConfig("postgres", postgresUrl, user, pass) + .withDatabaseConfig("mysql", mysqlUrl, user, pass); + +// Use both +String postgresUrl = ojp.getJdbcUrl("postgres"); +String mysqlUrl = ojp.getJdbcUrl("mysql"); +``` + +### Q: How does this relate to existing SQLServerTestContainer? + +**A**: This is a more general solution: +- **SQLServerTestContainer**: Manages SQL Server database container +- **OJPContainer**: Manages OJP server container +- **Together**: Create complete integration test environment + +Example combining both: +```java +@Container +static SQLServerTestContainer sqlServer = new SQLServerTestContainer(); + +@Container +static OJPContainer ojp = new OJPContainer() + .dependsOn(sqlServer) + .withDatabaseConfig("sqlserver", + sqlServer.getJdbcUrl(), + sqlServer.getUsername(), + sqlServer.getPassword()); +``` + +### Q: What's the performance impact? + +**A**: +- **First test run**: ~5-10 seconds (pull image + start) +- **Subsequent runs**: ~2-3 seconds (image cached) +- **With reuse**: <1 second (container reused) + +### Q: Can I customize OJP server settings? + +**A**: Yes! Phase 2 will add: +```java +OJPContainer ojp = new OJPContainer() + .withServerConfiguration(config -> config + .withCircuitBreakerTimeout(5000) + .withThreadPoolSize(50) + .withMaxRequestSize(4 * 1024 * 1024)) + .withTelemetryEnabled(true); // Enabled by default + +// Access Prometheus metrics (port automatically mapped to avoid conflicts) +String metricsUrl = ojp.getPrometheusUrl(); // e.g., http://localhost:54321/metrics +int prometheusPort = ojp.getPrometheusPort(); // e.g., 54321 (random) +``` + +### Q: How do I migrate existing tests? + +**A**: Replace manual server startup with container: + +**Before**: +```java +// Requires: java -jar ojp-server.jar & running in background + +@Test +void test() throws SQLException { + Connection conn = DriverManager.getConnection( + "jdbc:ojp[localhost:1059]_postgresql://...", "user", "pass"); + // test code +} +``` + +**After**: +```java +@Testcontainers +class Test { + @Container + static OJPContainer ojp = new OJPContainer() + .withDatabaseConfig("db", "jdbc:postgresql://...", "user", "pass"); + + @Test + void test() throws SQLException { + Connection conn = DriverManager.getConnection( + ojp.getJdbcUrl("db"), "user", "pass"); + // same test code + } +} +``` + +### Q: What Java versions are supported? + +**A**: +- **TestContainer module**: Java 11+ (for compatibility) +- **OJP Server** (in Docker): Java 21 (internal, doesn't affect users) +- **Test code**: Any version ≥ Java 11 + +### Q: How do I contribute? + +**A**: Once implementation starts: +1. Check GitHub issues for "good first issue" tags +2. Read CONTRIBUTING.md +3. Fork, implement, test, PR +4. Follow existing code patterns + +### Q: Will there be port conflicts when running multiple OJP containers? + +**A**: No! Both the gRPC port (1059) and Prometheus port (9159) are automatically mapped to random available host ports by TestContainers. + +```java +// Each container gets its own random ports +OJPContainer ojp1 = new OJPContainer(); +OJPContainer ojp2 = new OJPContainer(); + +// Ports are different for each container +int grpcPort1 = ojp1.getMappedPort(1059); // e.g., 32768 +int grpcPort2 = ojp2.getMappedPort(1059); // e.g., 32769 +int prometheusPort1 = ojp1.getPrometheusPort(); // e.g., 32770 +int prometheusPort2 = ojp2.getPrometheusPort(); // e.g., 32771 + +// Access metrics +String metricsUrl = ojp1.getPrometheusUrl(); // http://localhost:32770/metrics +``` + +This allows you to run multiple OJP containers in parallel without any conflicts. + +--- + +## Implementation Checklist + +Use this checklist when implementing: + +### Pre-Implementation +- [ ] Review all analysis documents +- [ ] Get maintainer approval +- [ ] Create GitHub issue for tracking +- [ ] Set up development branch + +### Module Setup +- [ ] Create `ojp-testcontainers/` directory structure +- [ ] Create `pom.xml` with dependencies +- [ ] Add module to parent POM +- [ ] Configure Maven plugins (sources, javadoc) + +### Core Development +- [ ] Implement `OJPContainer` class +- [ ] Implement `DatabaseConfig` class +- [ ] Implement configuration methods +- [ ] Add health check +- [ ] Add logging + +### Testing +- [ ] Write unit tests +- [ ] Write H2 integration test +- [ ] Write PostgreSQL integration test +- [ ] Test error scenarios +- [ ] Test concurrent usage + +### Documentation +- [ ] Write README.md +- [ ] Add Javadoc to all public methods +- [ ] Create usage examples +- [ ] Document troubleshooting + +### Quality Assurance +- [ ] Code review +- [ ] Test on different Java versions (11, 17, 21) +- [ ] Test on different OS (Linux, macOS, Windows) +- [ ] Performance testing +- [ ] Security review + +### Publication +- [ ] Test Maven Central deployment +- [ ] Create release notes +- [ ] Update main README +- [ ] Announce on social media / blog + +--- + +## Success Metrics + +How we'll know this is successful: + +1. ✅ Published to Maven Central +2. ✅ Zero manual steps to use in tests +3. ✅ Works on all major CI/CD platforms +4. ✅ Positive community feedback +5. ✅ Adopted in OJP's own integration tests +6. ✅ Documentation is clear and comprehensive +7. ✅ <5 lines of code to get started + +--- + +## Contact & Discussion + +- **GitHub Issues**: For tracking implementation +- **Pull Requests**: For code contributions +- **Discussions**: For questions and ideas +- **Discord**: For real-time chat (link in main README) + +--- + +## Status Updates + +| Date | Status | Notes | +|------|--------|-------| +| 2025-12-17 | Analysis Complete | Three documents created, ready for review | +| TBD | Approved | Awaiting maintainer approval | +| TBD | In Progress | Development started | +| TBD | Beta | Published to Maven Central (snapshot) | +| TBD | Released | Published to Maven Central (release) | + +--- + +**Ready to proceed?** Review the documents and let's build this! 🚀 diff --git a/documents/OJP_TESTCONTAINER_SUMMARY.md b/documents/OJP_TESTCONTAINER_SUMMARY.md new file mode 100644 index 000000000..cc265ee59 --- /dev/null +++ b/documents/OJP_TESTCONTAINER_SUMMARY.md @@ -0,0 +1,266 @@ +# OJP TestContainer - Executive Summary & Key Decisions + +## Quick Summary + +Creating an OJP TestContainer is **highly feasible and recommended**. The project already uses TestContainers for SQL Server tests, has a Docker image available, and follows patterns that make this a natural extension. + +## Key Recommendations + +### 1. Repository Location: **Same Repository** ✅ + +Create a new module `ojp-testcontainers` in the existing repository. + +**Why?** +- Version synchronization with OJP server +- Single release process +- Easier dependency management +- Follows existing multi-module pattern +- Single source for issues and contributions + +**Module structure**: +``` +ojp/ +├── ojp-server/ +├── ojp-jdbc-driver/ +├── ojp-grpc-commons/ +└── ojp-testcontainers/ ← NEW MODULE +``` + +### 2. Implementation Approach: **Docker Image Based** ✅ + +Use the existing Docker image `rrobetti/ojp:0.3.1-snapshot` by default. + +**Why?** +- Image already exists and is tested +- Fast startup time +- Matches production usage +- Simpler implementation + +**Important Licensing Note**: The published Maven Central artifact will only include pre-configured support for **open-source databases** (PostgreSQL, MySQL, MariaDB, H2). For proprietary databases (Oracle, SQL Server, DB2), developers can create custom TestContainer implementations following documented patterns. See the full analysis for detailed guidance. + +### 3. Target Java Version: **Java 11** ✅ + +**Why?** +- Maximum compatibility with test projects +- Matches `ojp-jdbc-driver` (Java 11) +- OJP server runs in Docker (its Java 21 requirement is internal) + +### 4. Configuration Strategy: **Fluent API + Environment Variables** ✅ + +```java +OJPContainer ojp = new OJPContainer() + .withDatabaseConfig("mydb", "jdbc:postgresql://...", "user", "pass") + .withNetworkMode(network); +``` + +**Why?** +- Better developer experience +- OJP server already uses environment variables +- Follows TestContainers conventions + +## Key Questions Answered + +### Q: Should this be a separate repository or a module? +**A: Module in the same repository** (`ojp-testcontainers`) + +Keeping it in the same repo ensures version sync and simplifies releases. + +### Q: What's the minimal implementation? +**A: Core features**: +1. `OJPContainer` class extending `GenericContainer` +2. Database configuration method (`withDatabaseConfig()`) +3. JDBC URL generation (`getJdbcUrl()`) +4. Health check implementation +5. Basic documentation and examples + +### Q: How complex is the configuration? +**A: Start simple, then enhance**: + +**Phase 1 (MVP)**: +- Basic database configuration +- Single database support +- Default OJP server settings + +**Phase 2**: +- Multiple databases +- Network configuration +- Advanced server settings + +### Q: What about Maven Central publication? +**A: Use existing infrastructure**: +- Parent POM already configured with Maven Central plugin +- Just need to add sources and javadoc plugins (already done in other modules) +- Follow same pattern as `ojp-jdbc-driver` + +### Q: What testing strategy? +**A: Multi-layered**: +1. Unit tests for container configuration +2. Integration tests with H2 (embedded) +3. Integration tests with PostgreSQL container +4. Integration tests with MySQL container + +### Q: How do users consume this? +**A: Single dependency**: + +```xml + + org.openjproxy + ojp-testcontainers + 0.3.1-snapshot + test + +``` + +## Usage Example (MVP) + +```java +import org.openjproxy.testcontainers.OJPContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.junit.jupiter.api.Test; + +@Testcontainers +class MyApplicationTest { + + @Container + static OJPContainer ojp = new OJPContainer(); + + @Test + void testDatabaseAccess() throws SQLException { + // Build OJP JDBC URL - database config is in the URL + String jdbcUrl = ojp.buildJdbcUrl("jdbc:postgresql://localhost:5432/test"); + + try (Connection conn = DriverManager.getConnection( + jdbcUrl, "user", "password")) { + // Your test code - database access goes through OJP + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT 1"); + assertTrue(rs.next()); + } + } +} +``` + +## Benefits + +1. **Simplified Testing**: No manual server startup +2. **Isolation**: Each test suite can have its own OJP instance +3. **CI/CD Ready**: Works in any environment with Docker +4. **Realistic**: Uses actual OJP Docker image +5. **Flexible**: Supports multiple databases and configurations +6. **Reusable**: Published to Maven Central for community use +7. **License Compliant**: Only open-source databases in published artifact; custom implementations available for proprietary databases +8. **Conflict-Free**: Automatic port mapping (gRPC and Prometheus) prevents conflicts in parallel tests + +## Licensing Strategy + +### Published to Maven Central (Open-Source Databases Only) + +The `ojp-testcontainers` Maven artifact will include pre-configured support for: +- ✅ PostgreSQL +- ✅ MySQL / MariaDB +- ✅ H2 +- ✅ Other open-source databases + +### Custom Implementations (Proprietary Databases) + +For licensing reasons, proprietary databases require custom implementations: +- ❌ Oracle Database - developers create custom TestContainer +- ❌ Microsoft SQL Server - developers create custom TestContainer +- ❌ IBM DB2 - developers create custom TestContainer + +**Documentation Provided**: Complete guide on creating custom TestContainers for proprietary databases, including code examples for Oracle, SQL Server, and DB2. See [Section 8 of the full analysis](OJP_TESTCONTAINER_ANALYSIS.md#8-custom-testcontainers-for-proprietary-databases). + +**Example Custom Implementation** (developers add to their test code): +```java +// src/test/java/com/mycompany/testutil/OJPWithOracleContainer.java +public class OJPWithOracleContainer { + private static OracleContainer oracle = new OracleContainer("gvenzl/oracle-xe:21-slim") + .withNetworkAliases("oracle-db"); + private static OJPContainer ojp = new OJPContainer() + .withNetwork(network) + .dependsOn(oracle); + + public static String getOJPJdbcUrl() { + return ojp.buildJdbcUrl("jdbc:oracle:thin:@oracle-db:1521/XEPDB1"); + } + // Container lifecycle methods... +} +``` + +## Implementation Roadmap + +### Phase 1: MVP (2-3 weeks) +- [ ] Create `ojp-testcontainers` module +- [ ] Implement `OJPContainer` class +- [ ] `buildJdbcUrl()` convenience method +- [ ] Health check +- [ ] H2 integration tests +- [ ] Documentation + +### Phase 2: Enhanced (1-2 weeks) +- [ ] Multi-database support +- [ ] Network integration +- [ ] PostgreSQL/MySQL examples +- [ ] Advanced configuration + +### Phase 3: Publication (1 week) +- [ ] Maven Central setup +- [ ] Comprehensive docs +- [ ] Release notes +- [ ] Blog post + +## Potential Challenges & Solutions + +| Challenge | Solution | +|-----------|----------| +| Docker not available | Document requirements, provide manual setup fallback | +| Image size/startup time | Use optimized image, support container reuse | +| Network configuration complexity | Provide clear examples for both standalone and networked modes | +| Version drift | Keep in same repo, release together | +| Configuration complexity | Start simple (MVP), add features incrementally | + +## Success Metrics + +1. ✅ Published to Maven Central +2. ✅ Users can start testing with <5 lines of code +3. ✅ No manual OJP server startup needed +4. ✅ Works with common databases (H2, PostgreSQL, MySQL) +5. ✅ Clear documentation with working examples + +## Risk Assessment: **LOW** 🟢 + +- **Technical Risk**: Low (pattern already proven with SQLServerTestContainer) +- **Maintenance Risk**: Low (part of main repository) +- **Adoption Risk**: Low (solves real pain point) +- **Implementation Risk**: Low (well-understood technology) + +## Next Actions + +1. **Review this analysis** with project maintainers +2. **Discuss open questions**: + - Naming convention preferences? + - Any specific configuration requirements? + - Priority features for MVP? +3. **Create implementation issue** in GitHub +4. **Begin Phase 1** implementation +5. **Iterate** based on feedback + +## Questions for Maintainers + +1. **Module Name**: `ojp-testcontainers` or another preference? +2. **Package Name**: `org.openjproxy.testcontainers` OK? +3. **Priority**: Which databases should be supported first? +4. **Timeline**: Any release deadlines to consider? +5. **Documentation**: Any specific documentation standards to follow? +6. **Testing**: Should we migrate existing tests to use this? + +## Additional Resources + +- Full Analysis: [OJP_TESTCONTAINER_ANALYSIS.md](OJP_TESTCONTAINER_ANALYSIS.md) +- TestContainers Docs: https://www.testcontainers.org/ +- Existing SQL Server TestContainer: `ojp-jdbc-driver/src/test/java/openjproxy/jdbc/testutil/SQLServerTestContainer.java` + +--- + +**Recommendation**: Proceed with implementation as new module in this repository. The benefits are clear, risks are low, and it aligns well with project goals and existing patterns. diff --git a/documents/README.md b/documents/README.md index 35a61d0e3..ab6a2e66a 100644 --- a/documents/README.md +++ b/documents/README.md @@ -44,6 +44,15 @@ Located in [configuration/](configuration/): - [OJP JDBC Configuration](configuration/ojp-jdbc-configuration.md) - JDBC driver configuration - [OJP Server Configuration](configuration/ojp-server-configuration.md) - Server configuration +## TestContainers Integration + +**OJP TestContainer Analysis** - Plan for creating a reusable TestContainer for OJP: +- [Quick Reference](OJP_TESTCONTAINER_QUICKREF.md) - Start here! Quick guide and navigation +- [Executive Summary](OJP_TESTCONTAINER_SUMMARY.md) - Key recommendations and decisions +- [Technical Analysis](OJP_TESTCONTAINER_ANALYSIS.md) - Full technical analysis and design +- [Architecture Diagrams](OJP_TESTCONTAINER_ARCHITECTURE.md) - Visual architecture and data flow +- [SQL Server TestContainer Guide](SQLSERVER_TESTCONTAINER_GUIDE.md) - Existing SQL Server implementation + ## Database Setup Guides Located in [environment-setup/](environment-setup/): @@ -155,5 +164,11 @@ documents/ ├── runnable-jar/ # JAR execution guides ├── targeted-problem/ # Problem statements ├── telemetry/ # Telemetry documentation -└── xa/ # XA transaction documentation +├── troubleshooting/ # Troubleshooting guides +├── xa/ # XA transaction documentation +├── OJP_TESTCONTAINER_QUICKREF.md # TestContainer quick reference +├── OJP_TESTCONTAINER_SUMMARY.md # TestContainer executive summary +├── OJP_TESTCONTAINER_ANALYSIS.md # TestContainer technical analysis +├── OJP_TESTCONTAINER_ARCHITECTURE.md # TestContainer architecture diagrams +└── SQLSERVER_TESTCONTAINER_GUIDE.md # SQL Server TestContainer guide ``` diff --git a/ojp-jdbc-driver/pom.xml b/ojp-jdbc-driver/pom.xml index b17525f04..2032d699d 100644 --- a/ojp-jdbc-driver/pom.xml +++ b/ojp-jdbc-driver/pom.xml @@ -124,6 +124,32 @@ test + + + + org.testcontainers + postgresql + 1.20.4 + test + + + + + + org.testcontainers + junit-jupiter + 1.20.4 + test + + + + + org.openjproxy + ojp-testcontainers + 0.3.1-snapshot + test + + diff --git a/ojp-jdbc-driver/src/test/java/openjproxy/jdbc/PostgresMultipleTypesIntegrationTest.java b/ojp-jdbc-driver/src/test/java/openjproxy/jdbc/PostgresMultipleTypesIntegrationTest.java index 268840599..7496c3878 100644 --- a/ojp-jdbc-driver/src/test/java/openjproxy/jdbc/PostgresMultipleTypesIntegrationTest.java +++ b/ojp-jdbc-driver/src/test/java/openjproxy/jdbc/PostgresMultipleTypesIntegrationTest.java @@ -1,10 +1,16 @@ package openjproxy.jdbc; +import openjproxy.jdbc.testutil.PostgreSQLConnectionProvider; +import openjproxy.jdbc.testutil.PostgreSQLTestContainer; import openjproxy.jdbc.testutil.TestDBUtils; import org.junit.Assert; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.condition.EnabledIf; import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvFileSource; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.openjproxy.testcontainers.OJPContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; import java.math.BigDecimal; import java.sql.Connection; @@ -19,19 +25,33 @@ import static org.junit.jupiter.api.Assumptions.assumeFalse; +@Testcontainers +@EnabledIf("openjproxy.jdbc.testutil.PostgreSQLTestContainer#isEnabled") public class PostgresMultipleTypesIntegrationTest { - private static boolean isTestEnabled; + private static boolean isTestDisabled; + + // OJP container that connects to the PostgreSQL container + @Container + static OJPContainer ojpContainer = new OJPContainer() + .withNetwork(PostgreSQLTestContainer.getNetwork()) + .dependsOn(PostgreSQLTestContainer.getInstance()); @BeforeAll public static void checkTestConfiguration() { - isTestEnabled = Boolean.parseBoolean(System.getProperty("enablePostgresTests", "false")); + isTestDisabled = !Boolean.parseBoolean(System.getProperty("enablePostgresTests", "false")); + + // Set the OJP proxy configuration for the connection provider + if (!isTestDisabled && ojpContainer.isRunning()) { + System.setProperty("ojp.proxy.host", ojpContainer.getHost()); + System.setProperty("ojp.proxy.port", String.valueOf(ojpContainer.getGrpcPort())); + } } @ParameterizedTest - @CsvFileSource(resources = "/postgres_connection.csv") - public void typesCoverageTestSuccessful(String driverClass, String url, String user, String pwd) throws SQLException, ClassNotFoundException, ParseException { - assumeFalse(!isTestEnabled, "Postgres tests are disabled"); + @ArgumentsSource(PostgreSQLConnectionProvider.class) + public void typesCoverageTestSuccessful(String driverClass, String url, String user, String pwd) throws SQLException, ParseException { + assumeFalse(isTestDisabled, "Postgres tests are disabled"); Connection conn = DriverManager.getConnection(url, user, pwd); @@ -134,9 +154,9 @@ public void typesCoverageTestSuccessful(String driverClass, String url, String u } @ParameterizedTest - @CsvFileSource(resources = "/postgres_connection.csv") - public void testPostgresSpecificTypes(String driverClass, String url, String user, String pwd) throws SQLException, ClassNotFoundException { - assumeFalse(!isTestEnabled, "Postgres tests are disabled"); + @ArgumentsSource(PostgreSQLConnectionProvider.class) + public void testPostgresSpecificTypes(String driverClass, String url, String user, String pwd) throws SQLException { + assumeFalse(isTestDisabled, "Postgres tests are disabled"); Connection conn = DriverManager.getConnection(url, user, pwd); @@ -197,4 +217,4 @@ private static byte[] hexStringToByteArray(String hex) { } return data; } -} \ No newline at end of file +} diff --git a/ojp-jdbc-driver/src/test/java/openjproxy/jdbc/testutil/PostgreSQLConnectionProvider.java b/ojp-jdbc-driver/src/test/java/openjproxy/jdbc/testutil/PostgreSQLConnectionProvider.java new file mode 100644 index 000000000..5e07e9f9c --- /dev/null +++ b/ojp-jdbc-driver/src/test/java/openjproxy/jdbc/testutil/PostgreSQLConnectionProvider.java @@ -0,0 +1,57 @@ +package openjproxy.jdbc.testutil; + +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; + +import java.util.stream.Stream; + +/** + * Custom ArgumentsProvider for PostgreSQL integration tests. + * Provides connection details from TestContainers when PostgreSQL tests are enabled. + * This allows tests to use TestContainers instead of external PostgreSQL instances. + * + * Note: Tests must manage their own OJPContainer instance using @Container annotation. + */ +public class PostgreSQLConnectionProvider implements ArgumentsProvider { + + // JDBC URL prefix to be removed when building OJP URL + private static final String JDBC_PREFIX = "jdbc:"; + + // OJP proxy server configuration - can be overridden via system property + private static final String OJP_PROXY_HOST = System.getProperty("ojp.proxy.host", "localhost"); + private static final String OJP_PROXY_PORT = System.getProperty("ojp.proxy.port", "1059"); + private static final String OJP_PROXY_ADDRESS = OJP_PROXY_HOST + ":" + OJP_PROXY_PORT; + + @Override + public Stream provideArguments(ExtensionContext context) { + if (!PostgreSQLTestContainer.isEnabled()) { + // Return empty stream when tests are disabled + return Stream.empty(); + } + + // Initialize and start the PostgreSQL TestContainer + PostgreSQLTestContainer.getInstance(); + + // Get PostgreSQL connection details + String postgresNetworkUrl = PostgreSQLTestContainer.getNetworkJdbcUrl(); + String username = PostgreSQLTestContainer.getUsername(); + String password = PostgreSQLTestContainer.getPassword(); + + // Build OJP JDBC URL from the PostgreSQL network URL + // Network URL format: jdbc:postgresql://postgres:5432/defaultdb + // OJP format: jdbc:ojp[localhost:1059]_postgresql://postgres:5432/defaultdb + String driverClass = "org.openjproxy.jdbc.Driver"; + + // Remove "jdbc:" prefix and add OJP wrapper + String urlWithoutPrefix = postgresNetworkUrl.startsWith(JDBC_PREFIX) + ? postgresNetworkUrl.substring(JDBC_PREFIX.length()) + : postgresNetworkUrl; + String ojpUrl = JDBC_PREFIX + "ojp[" + OJP_PROXY_ADDRESS + "]_" + urlWithoutPrefix; + + // Return a single set of arguments with the TestContainer connection details + return Stream.of( + Arguments.of(driverClass, ojpUrl, username, password) + ); + } +} diff --git a/ojp-jdbc-driver/src/test/java/openjproxy/jdbc/testutil/PostgreSQLTestContainer.java b/ojp-jdbc-driver/src/test/java/openjproxy/jdbc/testutil/PostgreSQLTestContainer.java new file mode 100644 index 000000000..5156d2ca1 --- /dev/null +++ b/ojp-jdbc-driver/src/test/java/openjproxy/jdbc/testutil/PostgreSQLTestContainer.java @@ -0,0 +1,140 @@ +package openjproxy.jdbc.testutil; + +import org.testcontainers.containers.Network; +import org.testcontainers.containers.PostgreSQLContainer; + +import java.util.concurrent.locks.ReentrantLock; + +/** + * Singleton PostgreSQL test container for all PostgreSQL integration tests. + * This ensures that all tests share the same PostgreSQL instance to improve + * test performance and reduce resource usage. + * + * The container is configured with max_prepared_transactions=100 to support + * distributed transaction testing. + * + * Note: OJP container is managed separately by each test class to allow flexibility. + */ +public class PostgreSQLTestContainer { + + // PostgreSQL Docker image version + private static final String POSTGRES_IMAGE = "postgres:17"; + + // Shared network for PostgreSQL and OJP containers + private static Network network; + private static PostgreSQLContainer postgresContainer; + private static boolean isStarted = false; + private static boolean shutdownHookRegistered = false; + private static ReentrantLock initLock = new ReentrantLock(); + + /** + * Gets or creates the shared PostgreSQL test container instance. + * The container is automatically started on first access. + * + * @return the shared PostgreSQLContainer instance + */ + public static PostgreSQLContainer getInstance() { + // Fast-path: if container already created and running, return it without locking + PostgreSQLContainer local = postgresContainer; + if (local != null && local.isRunning()) { + return local; + } + + initLock.lock(); + try { + if (network == null) { + network = Network.newNetwork(); + } + + if (postgresContainer == null) { + postgresContainer = new PostgreSQLContainer<>(POSTGRES_IMAGE) + .withNetwork(network) + .withNetworkAliases("postgres") + .withCommand("postgres", "-c", "max_prepared_transactions=100") + .withUsername("testuser") + .withPassword("testpassword") + .withDatabaseName("defaultdb"); + } + + if (!isStarted) { + postgresContainer.start(); + isStarted = true; + + // Add shutdown hook to stop container when JVM exits + if (!shutdownHookRegistered) { + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + if (postgresContainer != null && postgresContainer.isRunning()) { + postgresContainer.stop(); + } + if (network != null) { + network.close(); + } + })); + shutdownHookRegistered = true; + } + } + + return postgresContainer; + } finally { + initLock.unlock(); + } + } + + /** + * Gets the JDBC URL for connecting to the test container. + * This returns the host-accessible URL (not the network alias). + * + * @return JDBC URL string + */ + public static String getJdbcUrl() { + return getInstance().getJdbcUrl(); + } + + /** + * Gets the JDBC URL using the network alias for container-to-container communication. + * This is used to build the OJP URL that OJP container uses to connect to PostgreSQL. + * + * @return JDBC URL string with network alias + */ + public static String getNetworkJdbcUrl() { + getInstance(); // Ensure container is started + return "jdbc:postgresql://postgres:5432/defaultdb"; + } + + /** + * Gets the shared network for containers. + * + * @return Network instance + */ + public static Network getNetwork() { + getInstance(); // Ensure network is created + return network; + } + + /** + * Gets the username for connecting to the test container. + * + * @return username string + */ + public static String getUsername() { + return getInstance().getUsername(); + } + + /** + * Gets the password for connecting to the test container. + * + * @return password string + */ + public static String getPassword() { + return getInstance().getPassword(); + } + + /** + * Checks if PostgreSQL tests are enabled via system property. + * + * @return true if PostgreSQL tests should run + */ + public static boolean isEnabled() { + return Boolean.parseBoolean(System.getProperty("enablePostgresTests", "false")); + } +} diff --git a/ojp-testcontainers/README.md b/ojp-testcontainers/README.md new file mode 100644 index 000000000..963c0b5fc --- /dev/null +++ b/ojp-testcontainers/README.md @@ -0,0 +1,159 @@ +# OJP TestContainers + +TestContainers integration for OJP (Open J Proxy), providing an easy way to run OJP server in integration tests. + +## Features + +- 🚀 **Zero Configuration**: Just start the container, no database pre-configuration needed +- 🔌 **Automatic Port Management**: gRPC and Prometheus ports automatically mapped to avoid conflicts +- 🐳 **Docker-based**: Uses the official OJP Docker image +- 🧪 **Test-Ready**: Integrates seamlessly with JUnit 5 and TestContainers +- 📊 **Observability**: Built-in Prometheus metrics support + +## Usage + +### Add Dependency + +```xml + + org.openjproxy + ojp-testcontainers + 0.3.1-snapshot + test + +``` + +### Basic Example + +```java +import org.openjproxy.testcontainers.OJPContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +@Testcontainers +class MyIntegrationTest { + + @Container + static OJPContainer ojp = new OJPContainer(); + + @Test + void testDatabaseAccess() throws SQLException { + // Build OJP JDBC URL from original database URL + String ojpUrl = ojp.buildJdbcUrl("jdbc:postgresql://localhost:5432/test"); + + try (Connection conn = DriverManager.getConnection(ojpUrl, "user", "pass")) { + // Your test code here + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT 1"); + assertTrue(rs.next()); + } + } +} +``` + +## API Methods + +- `buildJdbcUrl(String originalJdbcUrl)` - Convenience method to build OJP JDBC URLs +- `getGrpcUrl()` - Get gRPC connection string +- `getGrpcPort()` - Get mapped gRPC port (random) +- `getPrometheusUrl()` - Get Prometheus metrics URL +- `getPrometheusPort()` - Get mapped Prometheus port (random) +- `withTelemetryEnabled(boolean)` - Control telemetry (enabled by default) + +## How It Works + +The OJP server is a **proxy** - it doesn't need database configuration at startup. Database connection details are passed through the JDBC URL when your application connects, following the format: + +``` +jdbc:ojp[ojp-host:port]_original-jdbc-url +``` + +The `buildJdbcUrl()` method constructs this format automatically for convenience. + +## Port Management + +Both the **gRPC port (1059)** and **Prometheus port (9159)** are automatically mapped to random available host ports to prevent conflicts when running multiple containers in parallel. + +```java +OJPContainer ojp = new OJPContainer(); + +// Ports are automatically mapped +String grpcUrl = ojp.getGrpcUrl(); // e.g., "localhost:32768" +String metricsUrl = ojp.getPrometheusUrl(); // e.g., "http://localhost:32769/metrics" +``` + +## Advanced Examples + +### With PostgreSQL Container + +```java +@Testcontainers +class PostgresIntegrationTest { + + static Network network = Network.newNetwork(); + + @Container + static PostgreSQLContainer postgres = new PostgreSQLContainer<>("postgres:15") + .withNetwork(network) + .withNetworkAliases("postgres"); + + @Container + static OJPContainer ojp = new OJPContainer() + .withNetwork(network) + .dependsOn(postgres); + + @Test + void testThroughOJP() throws SQLException { + // Build OJP URL with the database connection details + String ojpUrl = ojp.buildJdbcUrl(postgres.getJdbcUrl()); + + try (Connection conn = DriverManager.getConnection( + ojpUrl, + postgres.getUsername(), + postgres.getPassword())) { + + // Access PostgreSQL through OJP proxy + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT 1"); + assertTrue(rs.next()); + } + } +} +``` + +### Singleton Pattern (Shared Container) + +```java +public abstract class BaseOJPTest { + + protected static final OJPContainer OJP_CONTAINER; + + static { + OJP_CONTAINER = new OJPContainer() + .withReuse(true); // Enable container reuse + + OJP_CONTAINER.start(); + } +} + +class MyTest extends BaseOJPTest { + @Test + void test() throws SQLException { + String ojpUrl = OJP_CONTAINER.buildJdbcUrl("jdbc:h2:mem:test"); + + try (Connection conn = DriverManager.getConnection(ojpUrl, "sa", "")) { + // Your test code + } + } +} +``` + +## Requirements + +- Java 11 or higher +- Docker +- Maven or Gradle + +## License + +Apache License 2.0 diff --git a/ojp-testcontainers/pom.xml b/ojp-testcontainers/pom.xml new file mode 100644 index 000000000..fd2cd5ca2 --- /dev/null +++ b/ojp-testcontainers/pom.xml @@ -0,0 +1,100 @@ + + + 4.0.0 + + + org.openjproxy + ojp-parent + 0.3.1-snapshot + ../pom.xml + + + ojp-testcontainers + 0.3.1-snapshot + OJP TestContainers + TestContainers integration for OJP (Open J Proxy) + + + 1.20.4 + 5.12.1 + 11 + 11 + + + + + + org.testcontainers + testcontainers + ${testcontainers.version} + + + + + org.testcontainers + junit-jupiter + ${testcontainers.version} + test + + + + + org.junit.jupiter + junit-jupiter + ${junit.version} + test + + + + + com.h2database + h2 + 2.3.232 + test + + + + + + + + org.apache.maven.plugins + maven-source-plugin + 3.2.1 + + + attach-sources + + jar + + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.6.3 + + + attach-javadocs + + jar + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.2 + + + + diff --git a/ojp-testcontainers/src/main/java/org/openjproxy/testcontainers/OJPContainer.java b/ojp-testcontainers/src/main/java/org/openjproxy/testcontainers/OJPContainer.java new file mode 100644 index 000000000..4d8e6b117 --- /dev/null +++ b/ojp-testcontainers/src/main/java/org/openjproxy/testcontainers/OJPContainer.java @@ -0,0 +1,221 @@ +package org.openjproxy.testcontainers; + +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.images.builder.ImageFromDockerfile; +import org.testcontainers.utility.DockerImageName; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.Duration; +import java.util.stream.Stream; + +/** + * TestContainer for OJP (Open J Proxy) server. + * Provides an easy way to run OJP server in integration tests. + * + *

The OJP server acts as a proxy - it doesn't need database configuration at startup. + * Database connection details are passed through the JDBC URL when your tests connect.

+ * + *

Example usage:

+ *
+ * @Container
+ * static OJPContainer ojp = new OJPContainer();
+ * 
+ * // In your test - database config is in the JDBC URL
+ * String jdbcUrl = "jdbc:ojp[" + ojp.getHost() + ":" + ojp.getGrpcPort() + "]_" +
+ *                  "postgresql://localhost:5432/test";
+ * Connection conn = DriverManager.getConnection(jdbcUrl, "user", "pass");
+ * 
+ */ +public class OJPContainer extends GenericContainer { + + private static final int DEFAULT_GRPC_PORT = 1059; + private static final int DEFAULT_PROMETHEUS_PORT = 9159; + + private boolean telemetryEnabled = true; // Enabled by default + + /** + * Creates an OJP container by building a Docker image from the local ojp-server JAR. + * This eliminates the need for pre-published Docker images. + * + *

Prerequisites: Run 'mvn clean install' to build the ojp-server JAR before running tests.

+ */ + public OJPContainer() { + this(buildImageFromLocalJar()); + } + + /** + * Creates an OJP container with a custom Docker image. + * + * @param dockerImageName the Docker image name (e.g., "myregistry/ojp:1.0.0") + */ + public OJPContainer(String dockerImageName) { + super(DockerImageName.parse(dockerImageName)); + commonSetup(); + } + + /** + * Creates an OJP container from a dynamically built image. + * + * @param imageFromDockerfile the image builder + */ + private OJPContainer(ImageFromDockerfile imageFromDockerfile) { + super(imageFromDockerfile); + commonSetup(); + } + + /** + * Common setup for all constructors. + */ + private void commonSetup() { + // Expose default gRPC port and Prometheus port + // Both ports will be mapped to random available ports to avoid conflicts + withExposedPorts(DEFAULT_GRPC_PORT, DEFAULT_PROMETHEUS_PORT); + + // Wait for health check with timeout + waitingFor(Wait.forHealthcheck().withStartupTimeout(Duration.ofSeconds(60))); + } + + /** + * Builds a Docker image from the local ojp-server JAR file. + * + * @return ImageFromDockerfile that builds the OJP container image + * @throws IllegalStateException if the ojp-server JAR cannot be found + */ + private static ImageFromDockerfile buildImageFromLocalJar() { + Path ojpServerJar = findOjpServerJar(); + + return new ImageFromDockerfile() + .withDockerfileFromBuilder(builder -> builder + .from("eclipse-temurin:21-jre-alpine") + .copy("ojp-server.jar", "/app/ojp-server.jar") + .workDir("/app") + .expose(DEFAULT_GRPC_PORT, DEFAULT_PROMETHEUS_PORT) + .entryPoint("java", "-jar", "ojp-server.jar") + .build()) + .withFileFromPath("ojp-server.jar", ojpServerJar); + } + + /** + * Finds the ojp-server shaded JAR file in the Maven build output. + * + * @return Path to the ojp-server JAR + * @throws IllegalStateException if the JAR cannot be found + */ + private static Path findOjpServerJar() { + // Try common locations relative to the test module + Path[] searchPaths = { + Paths.get("../ojp-server/target"), + Paths.get("../../ojp-server/target"), + Paths.get("ojp-server/target"), + Paths.get("target") + }; + + for (Path searchPath : searchPaths) { + if (Files.exists(searchPath) && Files.isDirectory(searchPath)) { + try (Stream files = Files.walk(searchPath, 1)) { + Path jarFile = files + .filter(path -> path.getFileName().toString().matches("ojp-server-.*-shaded\\.jar")) + .findFirst() + .orElse(null); + + if (jarFile != null && Files.exists(jarFile)) { + return jarFile.toAbsolutePath(); + } + } catch (IOException e) { + // Continue searching + } + } + } + + throw new IllegalStateException( + "Cannot find ojp-server-*-shaded.jar. " + + "Please run 'mvn clean install' to build the OJP server JAR before running tests. " + + "Searched paths: " + String.join(", ", + Stream.of(searchPaths).map(Path::toString).toArray(String[]::new)) + ); + } + + /** + * Get the gRPC connection string for OJP server. + * Use this to construct your JDBC URL. + * + * @return gRPC connection string (e.g., "localhost:32768") + */ + public String getGrpcUrl() { + return getHost() + ":" + getMappedPort(DEFAULT_GRPC_PORT); + } + + /** + * Get the mapped gRPC port. + * The port is randomly assigned to avoid conflicts. + * + * @return The host port mapped to the container's gRPC port + */ + public int getGrpcPort() { + return getMappedPort(DEFAULT_GRPC_PORT); + } + + /** + * Build an OJP JDBC URL from the original database JDBC URL. + * This is a convenience method to construct the proper OJP JDBC URL format. + * + *

Example:

+ *
+     * String ojpUrl = ojp.buildJdbcUrl("jdbc:postgresql://localhost:5432/test");
+     * // Returns: "jdbc:ojp[localhost:32768]_postgresql://localhost:5432/test"
+     * 
+ * + * @param originalJdbcUrl The original database JDBC URL + * @return OJP-prefixed JDBC URL + */ + public String buildJdbcUrl(String originalJdbcUrl) { + // Remove "jdbc:" prefix from original URL + String dbUrl = originalJdbcUrl.startsWith("jdbc:") + ? originalJdbcUrl.substring(5) + : originalJdbcUrl; + + return "jdbc:ojp[" + getHost() + ":" + getMappedPort(DEFAULT_GRPC_PORT) + "]_" + dbUrl; + } + + /** + * Enable or disable telemetry/Prometheus metrics. + * Telemetry is enabled by default. + * + * @param enabled true to enable telemetry, false to disable + * @return this container instance for method chaining + */ + public OJPContainer withTelemetryEnabled(boolean enabled) { + this.telemetryEnabled = enabled; + withEnv("ojp.opentelemetry.enabled", String.valueOf(enabled)); + return this; + } + + /** + * Get the Prometheus metrics endpoint URL. + * The Prometheus port is automatically mapped to a random available port + * to avoid conflicts when running multiple containers. + * + * @return Prometheus metrics URL (e.g., "http://localhost:54321/metrics") + * @throws IllegalStateException if telemetry is disabled + */ + public String getPrometheusUrl() { + if (!telemetryEnabled) { + throw new IllegalStateException("Telemetry is disabled. Enable it with withTelemetryEnabled(true)"); + } + return "http://" + getHost() + ":" + getMappedPort(DEFAULT_PROMETHEUS_PORT) + "/metrics"; + } + + /** + * Get the mapped Prometheus port. + * The port is randomly assigned to avoid conflicts. + * + * @return The host port mapped to the container's Prometheus port + */ + public int getPrometheusPort() { + return getMappedPort(DEFAULT_PROMETHEUS_PORT); + } +} diff --git a/ojp-testcontainers/src/test/java/org/openjproxy/testcontainers/OJPContainerTest.java b/ojp-testcontainers/src/test/java/org/openjproxy/testcontainers/OJPContainerTest.java new file mode 100644 index 000000000..92f001ad9 --- /dev/null +++ b/ojp-testcontainers/src/test/java/org/openjproxy/testcontainers/OJPContainerTest.java @@ -0,0 +1,70 @@ +package org.openjproxy.testcontainers; + +import org.junit.jupiter.api.Test; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Integration test for OJPContainer. + * This test verifies that the OJP container can be started and provides + * the expected configuration methods. + * + * Note: Full JDBC integration tests are in ojp-jdbc-driver module to avoid + * cyclic dependencies. + */ +@Testcontainers +class OJPContainerTest { + + @Container + static OJPContainer ojp = new OJPContainer(); + + @Test + void testContainerStarts() { + // Verify container is running + assertTrue(ojp.isRunning(), "OJP container should be running"); + + // Verify gRPC port is mapped + assertTrue(ojp.getGrpcPort() > 0, "gRPC port should be mapped"); + + // Verify Prometheus port is mapped + assertTrue(ojp.getPrometheusPort() > 0, "Prometheus port should be mapped"); + } + + @Test + void testGetGrpcUrl() { + String grpcUrl = ojp.getGrpcUrl(); + assertNotNull(grpcUrl); + assertTrue(grpcUrl.contains(":"), "gRPC URL should contain host and port"); + } + + @Test + void testBuildJdbcUrl() { + String originalUrl = "jdbc:h2:mem:test"; + String ojpUrl = ojp.buildJdbcUrl(originalUrl); + + assertNotNull(ojpUrl); + assertTrue(ojpUrl.startsWith("jdbc:ojp["), "OJP URL should start with jdbc:ojp["); + assertTrue(ojpUrl.contains("]_h2:mem:test"), "OJP URL should contain the original database URL"); + } + + @Test + void testGetPrometheusUrl() { + String prometheusUrl = ojp.getPrometheusUrl(); + + assertNotNull(prometheusUrl); + assertTrue(prometheusUrl.startsWith("http://"), "Prometheus URL should start with http://"); + assertTrue(prometheusUrl.endsWith("/metrics"), "Prometheus URL should end with /metrics"); + } + + @Test + void testWithTelemetryDisabled() { + OJPContainer ojpNoTelemetry = new OJPContainer() + .withTelemetryEnabled(false); + + // Should throw exception when trying to get Prometheus URL with telemetry disabled + assertThrows(IllegalStateException.class, ojpNoTelemetry::getPrometheusUrl, + "Should throw exception when telemetry is disabled"); + } +} diff --git a/pom.xml b/pom.xml index d9790ddf2..8af970be0 100644 --- a/pom.xml +++ b/pom.xml @@ -16,6 +16,7 @@ ojp-grpc-commons ojp-jdbc-driver ojp-server + ojp-testcontainers