Skip to content

Commit 23be412

Browse files
authored
ci: add test workflow
Add `test.yml` for verifying HTTPS and WebSocket support
2 parents a8acc02 + 3e6ea24 commit 23be412

13 files changed

Lines changed: 1132 additions & 373 deletions

File tree

.github/workflows/test.yml

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
name: Test
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
- dev
8+
- release-*
9+
- feat-*
10+
- ci-*
11+
- refactor-*
12+
- fix-*
13+
- test-*
14+
paths:
15+
- '.github/workflows/test.yml'
16+
- '**/Cargo.toml'
17+
- '**/*.rs'
18+
- '**/*.hurl'
19+
- 'run_tests.sh'
20+
- 'tests/**'
21+
pull_request:
22+
branches: [ main ]
23+
24+
env:
25+
CARGO_TERM_COLOR: always
26+
RUST_BACKTRACE: 1
27+
28+
jobs:
29+
test:
30+
name: Test Suite
31+
runs-on: ubuntu-latest
32+
strategy:
33+
matrix:
34+
rust: [1.90.0]
35+
36+
steps:
37+
- name: Checkout code
38+
uses: actions/checkout@v5
39+
40+
- name: Start httpbin
41+
run: |
42+
# Use official httpbin image
43+
docker run -d \
44+
--name httpbin \
45+
-p 8888:80 \
46+
kennethreitz/httpbin:latest
47+
48+
# Wait for service to be ready
49+
echo "Waiting for httpbin to be ready..."
50+
for i in {1..30}; do
51+
if curl -f http://localhost:8888/get > /dev/null 2>&1; then
52+
echo "✅ httpbin is ready!"
53+
break
54+
fi
55+
if [ $i -eq 30 ]; then
56+
echo "❌ httpbin failed to start"
57+
docker logs httpbin
58+
exit 1
59+
fi
60+
sleep 2
61+
done
62+
63+
- name: Start json-server
64+
run: |
65+
# Use Node.js official image to run json-server
66+
docker run -d \
67+
--name json-api \
68+
-p 8889:3000 \
69+
-v ${{ github.workspace }}/tests/mock-data:/data:ro \
70+
-w /data \
71+
node:18-alpine \
72+
sh -c "npm install -g json-server@0.17.4 && json-server --host 0.0.0.0 --port 3000 db.json"
73+
74+
# Wait for service to be ready
75+
echo "Waiting for json-api to be ready..."
76+
for i in {1..60}; do
77+
if curl -f http://localhost:8889/posts > /dev/null 2>&1; then
78+
echo "✅ json-api is ready!"
79+
break
80+
fi
81+
if [ $i -eq 60 ]; then
82+
echo "❌ json-api failed to start"
83+
docker logs json-api
84+
exit 1
85+
fi
86+
sleep 2
87+
done
88+
89+
- name: Start WebSocket echo server
90+
run: |
91+
# Use ultra-robust Python WebSocket echo server with manual message loop
92+
cat > ws-echo.py << 'EOF'
93+
import asyncio
94+
import websockets
95+
import logging
96+
import sys
97+
98+
logging.basicConfig(
99+
level=logging.INFO,
100+
format='%(asctime)s - %(levelname)s - %(message)s',
101+
stream=sys.stdout
102+
)
103+
logger = logging.getLogger(__name__)
104+
105+
async def echo(websocket):
106+
client_addr = websocket.remote_address
107+
logger.info(f"✅ New connection from {client_addr}")
108+
109+
message_count = 0
110+
try:
111+
# Manual receive loop with explicit error checking
112+
while True:
113+
try:
114+
# Wait for message with timeout
115+
message = await asyncio.wait_for(
116+
websocket.recv(),
117+
timeout=60.0 # 60 second timeout
118+
)
119+
120+
message_count += 1
121+
msg_type = "binary" if isinstance(message, bytes) else "text"
122+
msg_size = len(message)
123+
124+
logger.info(f"📨 Received {msg_type} message #{message_count}: {msg_size} bytes from {client_addr}")
125+
126+
# Echo back immediately
127+
await websocket.send(message)
128+
logger.info(f"📤 Echoed {msg_type} message #{message_count}: {msg_size} bytes to {client_addr}")
129+
130+
except asyncio.TimeoutError:
131+
logger.warning(f"⏰ Timeout waiting for message from {client_addr}")
132+
continue
133+
except websockets.exceptions.ConnectionClosedOK:
134+
logger.info(f"✅ Connection closed normally by {client_addr} after {message_count} messages")
135+
break
136+
except websockets.exceptions.ConnectionClosedError as e:
137+
logger.warning(f"⚠️ Connection closed with error from {client_addr}: {e}")
138+
break
139+
140+
except Exception as e:
141+
logger.error(f"❌ Unexpected error handling {client_addr}: {e}", exc_info=True)
142+
finally:
143+
logger.info(f"🔚 Handler ended for {client_addr} (processed {message_count} messages)")
144+
145+
async def main():
146+
host = "0.0.0.0"
147+
port = 8890
148+
149+
logger.info(f"🚀 Starting WebSocket echo server on {host}:{port}")
150+
151+
# Start server with very permissive settings
152+
async with websockets.serve(
153+
echo,
154+
host,
155+
port,
156+
ping_interval=None, # Disable ping/pong (let client handle it)
157+
ping_timeout=None, # No ping timeout
158+
close_timeout=10, # 10 seconds for close handshake
159+
max_size=10 * 1024 * 1024, # 10MB max message
160+
max_queue=32, # Max 32 queued messages
161+
compression=None # Disable compression for simplicity
162+
):
163+
logger.info(f"✅ WebSocket echo server is ready and listening")
164+
await asyncio.Future() # Run forever
165+
166+
if __name__ == "__main__":
167+
try:
168+
asyncio.run(main())
169+
except KeyboardInterrupt:
170+
logger.info("🛑 Server stopped by user")
171+
sys.exit(0)
172+
EOF
173+
174+
docker run -d \
175+
--name ws-echo \
176+
-p 8890:8890 \
177+
-v ${{ github.workspace }}/ws-echo.py:/ws-echo.py:ro \
178+
python:3.11-alpine \
179+
sh -c "pip install websockets && python /ws-echo.py"
180+
181+
# Wait for service to be ready - WebSocket service starts quickly
182+
echo "Waiting for ws-echo to be ready..."
183+
sleep 10
184+
185+
# Check if container is running
186+
if docker ps | grep -q ws-echo; then
187+
echo "✅ ws-echo container is running!"
188+
else
189+
echo "❌ ws-echo container failed to start"
190+
docker logs ws-echo
191+
exit 1
192+
fi
193+
194+
- name: Install Rust-stable
195+
uses: actions-rust-lang/setup-rust-toolchain@v1
196+
with:
197+
toolchain: ${{ matrix.rust }}
198+
199+
- name: Cache Rust dependencies
200+
uses: Swatinem/rust-cache@v2
201+
with:
202+
cache-on-failure: true
203+
204+
- name: Install Hurl
205+
run: |
206+
VERSION="5.0.1"
207+
curl -LO https://github.com/Orange-OpenSource/hurl/releases/download/${VERSION}/hurl_${VERSION}_amd64.deb
208+
sudo dpkg -i hurl_${VERSION}_amd64.deb
209+
210+
- name: Install SQLite
211+
run: sudo apt-get update && sudo apt-get install -y sqlite3
212+
213+
- name: Verify services are running
214+
run: |
215+
echo "Checking httpbin..."
216+
curl -f http://localhost:8888/get
217+
218+
echo "Checking json-api..."
219+
curl -f http://localhost:8889/posts
220+
221+
echo "Checking ws-echo (WebSocket service - checking port)..."
222+
if timeout 5 bash -c 'cat < /dev/null > /dev/tcp/localhost/8890'; then
223+
echo "✅ ws-echo port 8890 is listening"
224+
else
225+
echo "❌ ws-echo port 8890 is not accessible"
226+
docker logs ws-echo
227+
exit 1
228+
fi
229+
230+
- name: Run tests
231+
env:
232+
USE_DOCKER_SERVICES: false # Services already running via GitHub Actions
233+
run: |
234+
chmod +x run_tests.sh
235+
./run_tests.sh
236+
237+
- name: Upload test results
238+
if: failure()
239+
uses: actions/upload-artifact@v4
240+
with:
241+
name: test-logs
242+
path: |
243+
*.log
244+
sessions.db
245+
246+
- name: Cleanup test services
247+
if: always()
248+
run: |
249+
docker stop httpbin || true
250+
docker rm httpbin || true
251+
docker stop json-api || true
252+
docker rm json-api || true
253+
docker stop ws-echo || true
254+
docker rm ws-echo || true

README.md

Lines changed: 88 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,14 @@ A high-performance proxy server built with Rust, supporting HTTP/HTTPS and WebSo
3131
- [🛡️ Server Status](#️-server-status)
3232
- [📊 Error Handling](#-error-handling)
3333
- [🧪 Testing](#-testing)
34+
- [Prerequisites](#prerequisites)
35+
- [Running Tests](#running-tests)
36+
- [Option 1: With Docker Services (Recommended)](#option-1-with-docker-services-recommended)
37+
- [Option 2: Manual Service Management](#option-2-manual-service-management)
38+
- [Option 3: Individual Test Suites](#option-3-individual-test-suites)
39+
- [Test Services](#test-services)
40+
- [Benefits of Docker-Based Testing](#benefits-of-docker-based-testing)
41+
- [CI/CD](#cicd)
3442
- [📝 Logging](#-logging)
3543
- [🛠️ Development Guide](#️-development-guide)
3644
- [Code Linting and Formatting](#code-linting-and-formatting)
@@ -341,14 +349,91 @@ Other statuses (such as `inactive`) will return `503 Service Unavailable`.
341349

342350
## 🧪 Testing
343351

352+
### Prerequisites
353+
354+
- **Docker and Docker Compose** (for test services)
355+
- **Hurl** (for API testing)
356+
- **Rust toolchain**
357+
358+
### Running Tests
359+
360+
#### Option 1: With Docker Services (Recommended)
361+
362+
This uses local Docker containers for all test dependencies, eliminating reliance on unstable external services:
363+
344364
```bash
345-
# Run tests
365+
# Run all tests with Docker services (automatically starts/stops services)
366+
./run_tests.sh
367+
368+
# Or with custom port
369+
TEST_PORT=10086 ./run_tests.sh
370+
```
371+
372+
The test script will:
373+
1. Build the project
374+
2. Start Docker test services (httpbin, json-api, ws-echo)
375+
3. Initialize the test database
376+
4. Run Hurl API tests
377+
5. Run Rust integration tests
378+
6. Automatically stop services on completion
379+
380+
#### Option 2: Manual Service Management
381+
382+
```bash
383+
# Start test services
384+
./scripts/start-test-services.sh
385+
386+
# Run tests (skips Docker service management)
387+
USE_DOCKER_SERVICES=false ./run_tests.sh
388+
389+
# Stop test services
390+
./scripts/stop-test-services.sh
391+
```
392+
393+
#### Option 3: Individual Test Suites
394+
395+
```bash
396+
# Rust unit tests only
346397
cargo test
347398

348-
# View test coverage
349-
cargo test --verbose
399+
# Rust integration tests only (includes HTTP + WebSocket tests)
400+
cargo test --test integration
401+
402+
# Hurl HTTP API tests only (requires services running)
403+
hurl --test --variable port=8080 tests/http.hurl
350404
```
351405

406+
**Note:** WebSocket tests are only available in Rust integration tests (`tests/integration.rs`), as Hurl doesn't support WebSocket message protocol.
407+
408+
### Test Services
409+
410+
The test suite uses the following Docker services (all run locally):
411+
412+
| Service | Port | Purpose | Replaces |
413+
|---------|------|---------|----------|
414+
| **httpbin** | 8888 | HTTP testing service | httpbin.org |
415+
| **json-api** | 8889 | REST API testing service | jsonplaceholder.typicode.com |
416+
| **ws-echo** | 8890 | WebSocket echo service | echo.websocket.org |
417+
418+
All services are automatically managed by `run_tests.sh` when `USE_DOCKER_SERVICES=true` (default).
419+
420+
### Benefits of Docker-Based Testing
421+
422+
**Stable**: No dependency on external services
423+
**Fast**: Local network, no internet latency
424+
**Reliable**: Consistent test environment
425+
**Offline**: Tests work without internet connection
426+
**CI/CD Ready**: GitHub Actions integration included
427+
428+
### CI/CD
429+
430+
The project includes a GitHub Actions workflow (`.github/workflows/test.yml`) that automatically:
431+
- Runs all tests on push/PR
432+
- Uses service containers for test dependencies
433+
- Caches Rust dependencies for faster builds
434+
- Runs linting and formatting checks
435+
- Builds binaries for multiple platforms
436+
352437
## 📝 Logging
353438

354439
Set environment variables to control log level:

0 commit comments

Comments
 (0)