Skip to content

Commit 7118f2d

Browse files
committed
Add SSH tunnel test infrastructure
- Add SSH server container (linuxserver/openssh-server) with TCP forwarding - Add postgres-ssh container as tunnel target - Add SSH connection fixtures and test helpers - Add SSH tunnel integration tests - Configure MaxStartups for high connection throughput in tests
1 parent fa34527 commit 7118f2d

5 files changed

Lines changed: 234 additions & 2 deletions

File tree

docker-compose.test.yml

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,3 +99,60 @@ services:
9999
timeout: 5s
100100
retries: 10
101101
start_period: 20s
102+
103+
turso:
104+
image: ghcr.io/tursodatabase/libsql-server:latest
105+
container_name: sqlit-test-turso
106+
ports:
107+
- "8081:8080"
108+
healthcheck:
109+
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
110+
interval: 5s
111+
timeout: 5s
112+
retries: 10
113+
start_period: 10s
114+
115+
# SSH server for testing SSH tunnel connections
116+
# Connects to postgres container on docker network
117+
ssh:
118+
image: linuxserver/openssh-server:latest
119+
container_name: sqlit-test-ssh
120+
environment:
121+
PUID: 1000
122+
PGID: 1000
123+
USER_NAME: testuser
124+
USER_PASSWORD: testpass
125+
PASSWORD_ACCESS: "true"
126+
# Enable TCP forwarding for SSH tunnels
127+
DOCKER_MODS: "linuxserver/mods:openssh-server-ssh-tunnel"
128+
ports:
129+
- "2222:2222"
130+
volumes:
131+
- ./tests/fixtures/99-fix-sshd-config.sh:/custom-cont-init.d/99-fix-sshd-config
132+
healthcheck:
133+
test: ["CMD", "nc", "-z", "localhost", "2222"]
134+
interval: 5s
135+
timeout: 5s
136+
retries: 10
137+
start_period: 10s
138+
depends_on:
139+
- postgres-ssh
140+
141+
# PostgreSQL for SSH tunnel tests (separate instance accessible via SSH)
142+
postgres-ssh:
143+
image: postgres:16-alpine
144+
container_name: sqlit-test-postgres-ssh
145+
environment:
146+
POSTGRES_USER: "testuser"
147+
POSTGRES_PASSWORD: "TestPassword123!"
148+
POSTGRES_DB: "test_sqlit"
149+
ports:
150+
- "5433:5432"
151+
healthcheck:
152+
test: ["CMD-SHELL", "pg_isready -U testuser -d test_sqlit"]
153+
interval: 5s
154+
timeout: 5s
155+
retries: 10
156+
start_period: 10s
157+
tmpfs:
158+
- /var/lib/postgresql/data

tests/conftest.py

Lines changed: 131 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1200,6 +1200,132 @@ def cockroachdb_connection(cockroachdb_db: str) -> str:
12001200
cleanup_connection(connection_name)
12011201

12021202

1203+
# =============================================================================
1204+
# Turso (libSQL) Fixtures
1205+
# =============================================================================
1206+
1207+
# Turso connection settings for Docker (libsql-server)
1208+
TURSO_HOST = os.environ.get("TURSO_HOST", "localhost")
1209+
TURSO_PORT = int(os.environ.get("TURSO_PORT", "8081"))
1210+
1211+
1212+
def turso_available() -> bool:
1213+
"""Check if Turso (libsql-server) is available."""
1214+
return is_port_open(TURSO_HOST, TURSO_PORT)
1215+
1216+
1217+
@pytest.fixture(scope="session")
1218+
def turso_server_ready() -> bool:
1219+
"""Check if Turso is ready and return True/False."""
1220+
if not turso_available():
1221+
return False
1222+
1223+
# Wait a bit for libsql-server to be fully ready
1224+
time.sleep(1)
1225+
return True
1226+
1227+
1228+
@pytest.fixture(scope="function")
1229+
def turso_db(turso_server_ready: bool) -> str:
1230+
"""Set up Turso test database."""
1231+
if not turso_server_ready:
1232+
pytest.skip("Turso (libsql-server) is not available")
1233+
1234+
try:
1235+
from libsql_client import create_client_sync
1236+
except ImportError:
1237+
pytest.skip("libsql-client is not installed")
1238+
1239+
turso_url = f"http://{TURSO_HOST}:{TURSO_PORT}"
1240+
1241+
try:
1242+
client = create_client_sync(turso_url)
1243+
1244+
# Drop tables if they exist and recreate
1245+
client.execute("DROP TABLE IF EXISTS test_users")
1246+
client.execute("DROP TABLE IF EXISTS test_products")
1247+
client.execute("DROP VIEW IF EXISTS test_user_emails")
1248+
1249+
# Create test tables
1250+
client.execute("""
1251+
CREATE TABLE test_users (
1252+
id INTEGER PRIMARY KEY,
1253+
name TEXT NOT NULL,
1254+
email TEXT UNIQUE
1255+
)
1256+
""")
1257+
1258+
client.execute("""
1259+
CREATE TABLE test_products (
1260+
id INTEGER PRIMARY KEY,
1261+
name TEXT NOT NULL,
1262+
price REAL NOT NULL,
1263+
stock INTEGER DEFAULT 0
1264+
)
1265+
""")
1266+
1267+
# Create test view
1268+
client.execute("""
1269+
CREATE VIEW test_user_emails AS
1270+
SELECT id, name, email FROM test_users WHERE email IS NOT NULL
1271+
""")
1272+
1273+
# Insert test data
1274+
client.execute("""
1275+
INSERT INTO test_users (id, name, email) VALUES
1276+
(1, 'Alice', 'alice@example.com'),
1277+
(2, 'Bob', 'bob@example.com'),
1278+
(3, 'Charlie', 'charlie@example.com')
1279+
""")
1280+
1281+
client.execute("""
1282+
INSERT INTO test_products (id, name, price, stock) VALUES
1283+
(1, 'Widget', 9.99, 100),
1284+
(2, 'Gadget', 19.99, 50),
1285+
(3, 'Gizmo', 29.99, 25)
1286+
""")
1287+
1288+
client.close()
1289+
1290+
except Exception as e:
1291+
pytest.skip(f"Failed to setup Turso database: {e}")
1292+
1293+
yield turso_url
1294+
1295+
# Cleanup: drop test tables
1296+
try:
1297+
client = create_client_sync(turso_url)
1298+
client.execute("DROP TABLE IF EXISTS test_users")
1299+
client.execute("DROP TABLE IF EXISTS test_products")
1300+
client.execute("DROP VIEW IF EXISTS test_user_emails")
1301+
client.close()
1302+
except Exception:
1303+
pass
1304+
1305+
1306+
@pytest.fixture(scope="function")
1307+
def turso_connection(turso_db: str) -> str:
1308+
"""Create a sqlit CLI connection for Turso and clean up after test."""
1309+
connection_name = f"test_turso_{os.getpid()}"
1310+
1311+
# Clean up any existing connection with this name
1312+
cleanup_connection(connection_name)
1313+
1314+
# Create the connection (no auth token needed for local libsql-server)
1315+
run_cli(
1316+
"connection", "create",
1317+
"--name", connection_name,
1318+
"--db-type", "turso",
1319+
"--server", turso_db,
1320+
"--password", "", # No auth token for local server
1321+
)
1322+
1323+
yield connection_name
1324+
1325+
# Cleanup
1326+
cleanup_connection(connection_name)
1327+
1328+
12031329
# =============================================================================
12041330
# SSH Tunnel Fixtures
12051331
# =============================================================================
@@ -1210,7 +1336,7 @@ def cockroachdb_connection(cockroachdb_db: str) -> str:
12101336
SSH_USER = os.environ.get("SSH_USER", "testuser")
12111337
SSH_PASSWORD = os.environ.get("SSH_PASSWORD", "testpass")
12121338
# The PostgreSQL host as seen from the SSH server (docker network)
1213-
SSH_REMOTE_DB_HOST = os.environ.get("SSH_REMOTE_DB_HOST", "postgres")
1339+
SSH_REMOTE_DB_HOST = os.environ.get("SSH_REMOTE_DB_HOST", "postgres-ssh")
12141340
SSH_REMOTE_DB_PORT = int(os.environ.get("SSH_REMOTE_DB_PORT", "5432"))
12151341

12161342

@@ -1240,9 +1366,12 @@ def ssh_postgres_db(ssh_server_ready: bool) -> str:
12401366
pytest.skip("psycopg2 is not installed")
12411367

12421368
# Connect directly to PostgreSQL to set up test data
1243-
# In CI, PostgreSQL is accessible directly on the host
1369+
# postgres-ssh container is accessible on port 5433
12441370
pg_host = os.environ.get("SSH_DIRECT_PG_HOST", "localhost")
12451371
pg_port = int(os.environ.get("SSH_DIRECT_PG_PORT", "5433"))
1372+
pg_user = POSTGRES_USER
1373+
pg_password = POSTGRES_PASSWORD
1374+
pg_database = POSTGRES_DATABASE
12461375

12471376
try:
12481377
conn = psycopg2.connect(
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#!/bin/bash
2+
# Increase MaxStartups and MaxSessions for testing
3+
echo "MaxStartups 100:30:200" >> /etc/ssh/sshd_config
4+
echo "MaxSessions 100" >> /etc/ssh/sshd_config
5+
# Reload sshd to pick up the changes
6+
pkill -HUP sshd 2>/dev/null || true

tests/fixtures/sshd_config

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Custom sshd_config for testing - allows high connection rate
2+
Port 2222
3+
AddressFamily any
4+
ListenAddress 0.0.0.0
5+
ListenAddress ::
6+
7+
# Host keys (linuxserver image puts them in /config)
8+
HostKey /config/ssh_host_keys/ssh_host_rsa_key
9+
HostKey /config/ssh_host_keys/ssh_host_ecdsa_key
10+
HostKey /config/ssh_host_keys/ssh_host_ed25519_key
11+
12+
# Authentication
13+
PermitRootLogin no
14+
PasswordAuthentication yes
15+
PubkeyAuthentication yes
16+
AuthorizedKeysFile .ssh/authorized_keys
17+
18+
# TCP forwarding for SSH tunnels
19+
AllowTcpForwarding yes
20+
GatewayPorts no
21+
X11Forwarding no
22+
23+
# Subsystems
24+
Subsystem sftp /usr/lib/openssh/sftp-server
25+
26+
# Increase connection limits for testing (no rate limiting)
27+
MaxStartups 100:30:200
28+
MaxSessions 100
29+
LoginGraceTime 120

tests/test_ssh.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from __future__ import annotations
44

55
import json
6+
import time
67

78
import pytest
89

@@ -16,6 +17,16 @@ class TestSSHTunnelIntegration:
1617
Tests are skipped if SSH server is not available.
1718
"""
1819

20+
@pytest.fixture(autouse=True)
21+
def slow_down_ssh_tests(self):
22+
"""Add a small delay between SSH tests to avoid overwhelming the server.
23+
24+
Note: SSH tests may fail when run together rapidly due to SSH server
25+
connection limits. Run individually with: pytest tests/test_ssh.py -k <test_name>
26+
"""
27+
time.sleep(1) # Wait before each test
28+
yield
29+
1930
def test_create_ssh_connection(self, ssh_postgres_db, cli_runner):
2031
"""Test creating a PostgreSQL connection with SSH tunnel via CLI."""
2132
from .conftest import (

0 commit comments

Comments
 (0)