Compare commits

...

6 Commits

Author SHA1 Message Date
Ciro Spaciari
9172749b25 more 2025-09-26 19:22:05 -07:00
Ciro Spaciari
708e2cb3ec more fixes 2025-09-26 18:43:10 -07:00
Ciro Spaciari
c8a6c6bd34 more fixes 2025-09-26 18:36:21 -07:00
Ciro Spaciari
eb69db1a4c fix more tests 2025-09-26 18:30:19 -07:00
Ciro Spaciari
486ba80fe8 we need to test passwords outside tls too 2025-09-26 17:56:20 -07:00
Ciro Spaciari
f585a453fd add more complex password 2025-09-26 17:53:25 -07:00
9 changed files with 120 additions and 69 deletions

View File

@@ -7,6 +7,7 @@ Docker Compose is a tool for defining and running multi-container Docker applica
### Why Use Docker Compose Instead of Plain Docker?
**Without Docker Compose (the old way):**
```javascript
// Each test file manages its own container
const container = await Bun.spawn({
@@ -21,6 +22,7 @@ const container = await Bun.spawn({
```
**With Docker Compose (the new way):**
```javascript
// All tests share managed containers
const postgres = await dockerCompose.ensure("postgres_plain");
@@ -34,26 +36,31 @@ const postgres = await dockerCompose.ensure("postgres_plain");
## Benefits of This Setup
### 1. **Speed** 🚀
- Containers start once and stay running
- Tests run 10-100x faster (no container startup overhead)
- Example: PostgreSQL tests went from 30s to 3s
### 2. **No Port Conflicts** 🔌
- Docker Compose assigns random available ports automatically
- No more "port already in use" errors
- Multiple developers can run tests simultaneously
### 3. **Centralized Configuration** 📝
- All services defined in one `docker-compose.yml` file
- Easy to update versions, add services, or change settings
- No need to hunt through test files to find container configs
### 4. **Lazy Loading** 💤
- Services only start when actually needed
- Running MySQL tests? Only MySQL starts
- Saves memory and CPU
### 5. **Better CI/CD** 🔄
- Predictable, reproducible test environments
- Same setup locally and in CI
- Easy to debug when things go wrong
@@ -63,6 +70,7 @@ const postgres = await dockerCompose.ensure("postgres_plain");
### The Setup
1. **docker-compose.yml** - Defines all test services:
```yaml
services:
postgres_plain:
@@ -70,11 +78,12 @@ services:
environment:
POSTGRES_HOST_AUTH_METHOD: trust
ports:
- target: 5432 # Container's port
published: 0 # 0 = let Docker pick a random port
- target: 5432 # Container's port
published: 0 # 0 = let Docker pick a random port
```
2. **index.ts** - TypeScript helper for managing services:
```typescript
// Start a service (if not already running)
const info = await dockerCompose.ensure("postgres_plain");
@@ -83,6 +92,7 @@ const info = await dockerCompose.ensure("postgres_plain");
```
3. **Test Integration**:
```typescript
import * as dockerCompose from "../../docker/index.ts";
@@ -90,7 +100,7 @@ test("database test", async () => {
const pg = await dockerCompose.ensure("postgres_plain");
const client = new PostgresClient({
host: pg.host,
port: pg.ports[5432], // Use the mapped port
port: pg.ports[5432], // Use the mapped port
});
// ... run tests
});
@@ -98,22 +108,24 @@ test("database test", async () => {
## Available Services
| Service | Description | Ports | Special Features |
|---------|-------------|-------|------------------|
| **PostgreSQL** | | | |
| `postgres_plain` | Basic PostgreSQL | 5432 | No auth required |
| `postgres_tls` | PostgreSQL with TLS | 5432 | SSL certificates included |
| `postgres_auth` | PostgreSQL with auth | 5432 | Username/password required |
| **MySQL** | | | |
| `mysql_plain` | Basic MySQL | 3306 | Root user, no password |
| `mysql_native_password` | MySQL with legacy auth | 3306 | For compatibility testing |
| `mysql_tls` | MySQL with TLS | 3306 | SSL certificates included |
| **Redis/Valkey** | | | |
| `redis_unified` | Redis with all features | 6379 (TCP), 6380 (TLS) | Persistence, Unix sockets, ACLs |
| **S3/MinIO** | | | |
| `minio` | S3-compatible storage | 9000 (API), 9001 (Console) | AWS S3 API testing |
| **WebSocket** | | | |
| `autobahn` | WebSocket test suite | 9002 | 517 conformance tests |
| Service | Description | Ports | Special Features |
| ---------------------------- | ----------------------- | -------------------------- | ------------------------------- |
| **PostgreSQL** | | | |
| `postgres_plain` | Basic PostgreSQL | 5432 | No auth required |
| `postgres_tls` | PostgreSQL with TLS | 5432 | SSL certificates included |
| `postgres_auth` | PostgreSQL with auth | 5432 | Username/password required |
| **MySQL** | | | |
| `mysql_plain` | Basic MySQL 8 | 3306 | Root user |
| `mysql_plain_empty_password` | Basic MySQL 8 | 3306 | Root user, no password |
| `mysql_native_password` | MySQL with legacy auth | 3306 | For compatibility testing |
| `mysql_tls` | MySQL with TLS | 3306 | SSL certificates included |
| `mysql_plain_9` | Basic MySQL 9 | 3306 | Root user |
| **Redis/Valkey** | | | |
| `redis_unified` | Redis with all features | 6379 (TCP), 6380 (TLS) | Persistence, Unix sockets, ACLs |
| **S3/MinIO** | | | |
| `minio` | S3-compatible storage | 9000 (API), 9001 (Console) | AWS S3 API testing |
| **WebSocket** | | | |
| `autobahn` | WebSocket test suite | 9002 | 517 conformance tests |
## Usage Examples
@@ -193,27 +205,32 @@ This is different from the old approach where every test started and stopped its
## Debugging
### View Running Services
```bash
cd test/docker
docker-compose ps
```
### Check Service Logs
```bash
docker-compose logs postgres_plain
```
### Stop All Services
```bash
docker-compose down
```
### Remove Everything (Including Data)
```bash
docker-compose down -v # -v removes volumes too
```
### Connection Issues?
```bash
# Check if service is healthy
docker-compose ps
@@ -238,12 +255,14 @@ const pg = await dockerCompose.ensure("postgres_plain");
### Persistent Data
Some services use volumes to persist data across container restarts:
- Redis: Uses volume for AOF persistence
- PostgreSQL/MySQL: Can be configured with volumes if needed
### Environment Variables
Control behavior with environment variables:
- `COMPOSE_PROJECT_NAME`: Prefix for container names (default: "bun-test-services")
- `BUN_DOCKER_COMPOSE_PATH`: Override docker-compose.yml location
@@ -258,6 +277,7 @@ If you're migrating tests from direct Docker usage:
5. **Cleanup**: Remove old Docker management code
Example migration:
```javascript
// OLD
const container = spawn(["docker", "run", "-d", "postgres"]);
@@ -298,13 +318,13 @@ A: This tells Docker to pick any available port, preventing conflicts.
## Troubleshooting
| Problem | Solution |
|---------|----------|
| "Connection refused" | Service might still be starting. Add `waitTcp()` or increase timeout |
| "Port already in use" | Another service using the port. Use dynamic ports (`published: 0`) |
| "Container not found" | Run `docker-compose up -d SERVICE_NAME` manually |
| Tests suddenly slow | Containers might have been stopped. Check with `docker-compose ps` |
| "Permission denied" | Docker daemon might require sudo. Check Docker installation |
| Problem | Solution |
| --------------------- | -------------------------------------------------------------------- |
| "Connection refused" | Service might still be starting. Add `waitTcp()` or increase timeout |
| "Port already in use" | Another service using the port. Use dynamic ports (`published: 0`) |
| "Container not found" | Run `docker-compose up -d SERVICE_NAME` manually |
| Tests suddenly slow | Containers might have been stopped. Check with `docker-compose ps` |
| "Permission denied" | Docker daemon might require sudo. Check Docker installation |
## Contributing
@@ -317,4 +337,4 @@ To add a new service:
5. Add example test
6. Submit PR
Remember: The goal is to make tests fast, reliable, and easy to run!
Remember: The goal is to make tests fast, reliable, and easy to run!

View File

@@ -77,6 +77,43 @@ services:
# MySQL Services
mysql_plain:
image: mysql:8.4
environment:
MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
MYSQL_ROOT_PASSWORD: "bun123456@#$%^&*()"
MYSQL_DATABASE: bun_sql_test
ports:
- target: 3306
published: 0
protocol: tcp
tmpfs:
- /var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 1h # Effectively disable after startup
timeout: 5s
retries: 30
start_period: 10s
mysql_plain_9:
image: mysql:9
environment:
MYSQL_ROOT_PASSWORD: "bun123456@#$%^&*()"
MYSQL_DATABASE: bun_sql_test
ports:
- target: 3306
published: 0
protocol: tcp
tmpfs:
- /var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 1h # Effectively disable after startup
timeout: 5s
retries: 30
start_period: 10s
mysql_plain_empty_password:
image: mysql:8.4
environment:
MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
@@ -97,7 +134,7 @@ services:
mysql_native_password:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: bun
MYSQL_ROOT_PASSWORD: "bun123456@#$%^&*()"
MYSQL_DATABASE: bun_sql_test
MYSQL_ROOT_HOST: "%"
command: --default-authentication-plugin=mysql_native_password
@@ -122,7 +159,7 @@ services:
MYSQL_VERSION: 8.4
image: bun-mysql-tls:local
environment:
MYSQL_ROOT_PASSWORD: bun
MYSQL_ROOT_PASSWORD: "bun123456@#$%^&*()"
MYSQL_DATABASE: bun_sql_test
ports:
- target: 3306

View File

@@ -11,6 +11,8 @@ export type ServiceName =
| "postgres_tls"
| "postgres_auth"
| "mysql_plain"
| "mysql_plain_empty_password"
| "mysql_plain_9"
| "mysql_native_password"
| "mysql_tls"
| "redis_plain"
@@ -41,14 +43,14 @@ class DockerComposeHelper {
private runningServices: Set<ServiceName> = new Set();
constructor(options: DockerComposeOptions = {}) {
this.projectName = options.projectName ||
this.projectName =
options.projectName ||
process.env.BUN_DOCKER_PROJECT_NAME ||
process.env.COMPOSE_PROJECT_NAME ||
"bun-test-services"; // Default project name for all test services
"bun-test-services"; // Default project name for all test services
this.composeFile = options.composeFile ||
process.env.BUN_DOCKER_COMPOSE_FILE ||
join(__dirname, "docker-compose.yml");
this.composeFile =
options.composeFile || process.env.BUN_DOCKER_COMPOSE_FILE || join(__dirname, "docker-compose.yml");
// Verify the compose file exists
const fs = require("fs");
@@ -70,10 +72,7 @@ class DockerComposeHelper {
stderr: "pipe",
});
const [stdout, stderr] = await Promise.all([
proc.stdout.text(),
proc.stderr.text(),
]);
const [stdout, stderr] = await Promise.all([proc.stdout.text(), proc.stderr.text()]);
const exitCode = await proc.exited;
@@ -151,12 +150,12 @@ class DockerComposeHelper {
try {
const socket = new net.Socket();
await new Promise<void>((resolve, reject) => {
socket.once('connect', () => {
socket.once("connect", () => {
socket.destroy();
resolve();
});
socket.once('error', reject);
socket.connect(port, '127.0.0.1');
socket.once("error", reject);
socket.connect(port, "127.0.0.1");
});
return;
} catch {
@@ -281,12 +280,14 @@ class DockerComposeHelper {
break;
case "mysql_plain":
case "mysql_plain_empty_password":
case "mysql_plain_9":
case "mysql_native_password":
case "mysql_tls":
env.MYSQL_HOST = info.host;
env.MYSQL_PORT = info.ports[3306].toString();
env.MYSQL_USER = "root";
env.MYSQL_PASSWORD = service === "mysql_plain" ? "" : "bun";
env.MYSQL_PASSWORD = service === "mysql_plain_empty_password" ? "" : "bun123456@#$%^&*()";
env.MYSQL_DATABASE = "bun_sql_test";
if (info.tls) {
@@ -449,7 +450,7 @@ export async function prepareImages(): Promise<void> {
// Higher-level wrappers for tests
export async function withPostgres(
opts: { variant?: "plain" | "tls" | "auth" },
fn: (info: ServiceInfo & { url: string }) => Promise<void>
fn: (info: ServiceInfo & { url: string }) => Promise<void>,
): Promise<void> {
const variant = opts.variant || "plain";
const serviceName = `postgres_${variant}` as ServiceName;
@@ -467,7 +468,7 @@ export async function withPostgres(
export async function withMySQL(
opts: { variant?: "plain" | "native_password" | "tls" },
fn: (info: ServiceInfo & { url: string }) => Promise<void>
fn: (info: ServiceInfo & { url: string }) => Promise<void>,
): Promise<void> {
const variant = opts.variant || "plain";
const serviceName = `mysql_${variant}` as ServiceName;
@@ -485,7 +486,7 @@ export async function withMySQL(
export async function withRedis(
opts: { variant?: "plain" | "unified" },
fn: (info: ServiceInfo & { url: string; tlsUrl?: string }) => Promise<void>
fn: (info: ServiceInfo & { url: string; tlsUrl?: string }) => Promise<void>,
): Promise<void> {
const variant = opts.variant || "plain";
const serviceName = `redis_${variant}` as ServiceName;
@@ -502,7 +503,7 @@ export async function withRedis(
}
export async function withMinio(
fn: (info: ServiceInfo & { endpoint: string; accessKeyId: string; secretAccessKey: string }) => Promise<void>
fn: (info: ServiceInfo & { endpoint: string; accessKeyId: string; secretAccessKey: string }) => Promise<void>,
): Promise<void> {
const info = await ensure("minio");
@@ -518,9 +519,7 @@ export async function withMinio(
}
}
export async function withAutobahn(
fn: (info: ServiceInfo & { url: string }) => Promise<void>
): Promise<void> {
export async function withAutobahn(fn: (info: ServiceInfo & { url: string }) => Promise<void>): Promise<void> {
const info = await ensure("autobahn");
try {
@@ -531,4 +530,4 @@ export async function withAutobahn(
} finally {
// Services persist - no teardown
}
}
}

View File

@@ -936,10 +936,10 @@ export async function describeWithContainer(
"postgres_tls": 5432,
"postgres_auth": 5432,
"mysql_plain": 3306,
"mysql_native_password": 3306,
"mysql_tls": 3306,
"mysql:8": 3306, // Map mysql:8 to mysql_plain
"mysql:9": 3306, // Map mysql:9 to mysql_native_password
"mysql:9": 3306, // Map mysql:9 to mysql_plain_9
"mysql_native_password": 3306, // mysql_native_password
"redis_plain": 6379,
"redis_unified": 6379,
"minio": 9000,
@@ -951,12 +951,12 @@ export async function describeWithContainer(
// Map mysql:8 and mysql:9 based on environment variables
let actualService = image;
if (image === "mysql:8" || image === "mysql:9") {
if (env.MYSQL_ROOT_PASSWORD === "bun") {
actualService = "mysql_native_password"; // Has password "bun"
} else if (env.MYSQL_ALLOW_EMPTY_PASSWORD === "yes") {
actualService = "mysql_plain"; // No password
if (env.MYSQL_ALLOW_EMPTY_PASSWORD === "yes") {
actualService = "mysql_plain_empty_password"; // No password
} else if (image === "mysql:9") {
actualService = "mysql_plain_9";
} else {
actualService = "mysql_plain"; // Default to no password
actualService = "mysql_plain";
}
}

View File

@@ -15,13 +15,10 @@ const tests: {
label: "mysql:8 with root user and password",
database: {
image: "mysql:8",
env: {
MYSQL_ROOT_PASSWORD: "bun",
},
},
client: {
user: "root",
password: "bun",
password: "bun123456@#$%^&*()",
},
},
{

View File

@@ -12,7 +12,8 @@ describeWithContainer(
},
container => {
// Create getters that will be evaluated when the test runs
const getUrl = () => `mysql://root:bun@${container.host}:${container.port}/bun_sql_test`;
const getUrl = () =>
`mysql://root:${encodeURIComponent("bun123456@#$%^&*()")}@${container.host}:${container.port}/bun_sql_test`;
test("should be able to connect with mysql_native_password auth plugin", async () => {
console.log("Container info in test:", container);

View File

@@ -13,7 +13,7 @@ describeWithContainer(
container => {
// Use a getter to avoid reading port/host at define time
const getOptions = () => ({
url: `mysql://root@${container.host}:${container.port}/bun_sql_test`,
url: `mysql://root:${encodeURIComponent("bun123456@#$%^&*()")}@${container.host}:${container.port}/bun_sql_test`,
max: 1,
bigint: true,
});

View File

@@ -24,9 +24,6 @@ if (isDockerEnabled()) {
process.arch === "x64" && {
name: "MySQL 9",
image: "mysql:9",
env: {
MYSQL_ROOT_PASSWORD: "bun",
},
},
].filter(Boolean);
@@ -40,9 +37,9 @@ if (isDockerEnabled()) {
},
container => {
let sql: SQL;
const password = image.image === "mysql_plain" ? "" : "bun";
const password = "bun123456@#$%^&*()";
const getOptions = (): Bun.SQL.Options => ({
url: `mysql://root:${password}@${container.host}:${container.port}/bun_sql_test`,
url: `mysql://root:${encodeURIComponent(password)}@${container.host}:${container.port}/bun_sql_test`,
max: 1,
tls:
image.name === "MySQL with TLS"

View File

@@ -12,7 +12,7 @@ describeWithContainer(
container => {
// Use a getter to avoid reading port/host at define time
const getOptions = () => ({
url: `mysql://root@${container.host}:${container.port}/bun_sql_test`,
url: `mysql://root:${encodeURIComponent("bun123456@#$%^&*()")}@${container.host}:${container.port}/bun_sql_test`,
max: 1,
bigint: true,
});