diff --git a/tests/docker/asgi/Dockerfile b/tests/docker/asgi/Dockerfile new file mode 100644 index 00000000..57361aa6 --- /dev/null +++ b/tests/docker/asgi/Dockerfile @@ -0,0 +1,18 @@ +FROM python:3.11-slim + +WORKDIR /build + +# Copy gunicorn source +COPY . /build/ + +# Install gunicorn from source +RUN pip install --no-cache-dir -e . + +# Copy test app +WORKDIR /app +COPY tests/docker/asgi/app.py /app/ + +# Expose HTTP port +EXPOSE 8000 + +CMD ["gunicorn", "--worker-class", "asgi", "--bind", "0.0.0.0:8000", "app:app"] diff --git a/tests/docker/asgi/app.py b/tests/docker/asgi/app.py new file mode 100644 index 00000000..699d3577 --- /dev/null +++ b/tests/docker/asgi/app.py @@ -0,0 +1,45 @@ +"""Simple ASGI test application for HTTP protocol testing.""" + + +async def app(scope, receive, send): + """Simple ASGI application that echoes request info.""" + if scope["type"] == "lifespan": + while True: + message = await receive() + if message["type"] == "lifespan.startup": + await send({"type": "lifespan.startup.complete"}) + elif message["type"] == "lifespan.shutdown": + await send({"type": "lifespan.shutdown.complete"}) + return + + if scope["type"] != "http": + return + + # Read body + body = b"" + while True: + message = await receive() + body += message.get("body", b"") + if not message.get("more_body", False): + break + + # Build response + method = scope["method"] + path = scope["path"] + query = scope.get("query_string", b"").decode("utf-8") + + response_body = f"Method: {method}\nPath: {path}\nQuery: {query}\nBody: {body.decode('utf-8')}\n" + response_bytes = response_body.encode("utf-8") + + await send({ + "type": "http.response.start", + "status": 200, + "headers": [ + [b"content-type", b"text/plain"], + [b"content-length", str(len(response_bytes)).encode()], + ], + }) + await send({ + "type": "http.response.body", + "body": response_bytes, + }) diff --git a/tests/docker/asgi/docker-compose.yml b/tests/docker/asgi/docker-compose.yml new file mode 100644 index 00000000..9f1af22a --- /dev/null +++ b/tests/docker/asgi/docker-compose.yml @@ -0,0 +1,14 @@ +services: + gunicorn: + build: + context: ../../.. + dockerfile: tests/docker/asgi/Dockerfile + command: > + gunicorn + --worker-class asgi + --bind 0.0.0.0:8000 + --workers 1 + --log-level debug + app:app + ports: + - "8080:8000" diff --git a/tests/docker/asgi/test_asgi.sh b/tests/docker/asgi/test_asgi.sh new file mode 100755 index 00000000..41eccff5 --- /dev/null +++ b/tests/docker/asgi/test_asgi.sh @@ -0,0 +1,116 @@ +#!/bin/bash +# Integration test for ASGI HTTP protocol support +# +# This script tests that gunicorn's ASGI worker correctly handles +# HTTP requests directly (without uWSGI protocol). + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +# Use IPv4 explicitly to avoid Docker IPv6 issues +BASE_URL="http://127.0.0.1:8080" + +cleanup() { + echo "Cleaning up..." + docker compose down -v 2>/dev/null || true +} + +trap cleanup EXIT + +echo "=== Building and starting containers ===" +docker compose up -d --build + +echo "=== Waiting for services to be ready ===" +sleep 5 + +echo "=== Running tests ===" + +# Test 1: Simple GET request +echo "Test 1: Simple GET request" +RESPONSE=$(curl -s "$BASE_URL/") +if echo "$RESPONSE" | grep -q "Method: GET"; then + echo " PASS: GET request works" +else + echo " FAIL: GET request failed" + echo " Response: $RESPONSE" + exit 1 +fi + +# Test 2: GET with query string +echo "Test 2: GET with query string" +RESPONSE=$(curl -s "$BASE_URL/search?q=test&page=1") +if echo "$RESPONSE" | grep -q "Query: q=test&page=1"; then + echo " PASS: Query string works" +else + echo " FAIL: Query string failed" + echo " Response: $RESPONSE" + exit 1 +fi + +# Test 3: POST with body +echo "Test 3: POST with body" +RESPONSE=$(curl -s -X POST -d "hello=world" "$BASE_URL/submit") +if echo "$RESPONSE" | grep -q "Method: POST" && echo "$RESPONSE" | grep -q "Body: hello=world"; then + echo " PASS: POST with body works" +else + echo " FAIL: POST with body failed" + echo " Response: $RESPONSE" + exit 1 +fi + +# Test 4: Path handling +echo "Test 4: Path handling" +RESPONSE=$(curl -s "$BASE_URL/api/v1/users") +if echo "$RESPONSE" | grep -q "Path: /api/v1/users"; then + echo " PASS: Path handling works" +else + echo " FAIL: Path handling failed" + echo " Response: $RESPONSE" + exit 1 +fi + +# Test 5: Multiple requests (keepalive) +echo "Test 5: Multiple requests (keepalive)" +for i in 1 2 3; do + RESPONSE=$(curl -s "$BASE_URL/request/$i") + if ! echo "$RESPONSE" | grep -q "Path: /request/$i"; then + echo " FAIL: Request $i failed" + exit 1 + fi +done +echo " PASS: Multiple requests work" + +# Test 6: Large POST body +echo "Test 6: Large POST body" +LARGE_BODY=$(python3 -c "print('x' * 10000)") +RESPONSE=$(curl -s -X POST -d "$LARGE_BODY" "$BASE_URL/large") +if echo "$RESPONSE" | grep -q "Method: POST" && echo "$RESPONSE" | grep -c "x" | grep -q "10000"; then + echo " PASS: Large POST body works" +else + # Verify body length in response + BODY_LINE=$(echo "$RESPONSE" | grep "Body:") + BODY_LEN=${#BODY_LINE} + if [ "$BODY_LEN" -gt 10000 ]; then + echo " PASS: Large POST body works" + else + echo " FAIL: Large POST body failed" + echo " Response length: $BODY_LEN" + exit 1 + fi +fi + +# Test 7: HTTP headers +echo "Test 7: Custom headers" +RESPONSE=$(curl -s -H "X-Custom-Header: test-value" "$BASE_URL/headers") +if echo "$RESPONSE" | grep -q "Method: GET"; then + echo " PASS: Custom headers work" +else + echo " FAIL: Custom headers failed" + echo " Response: $RESPONSE" + exit 1 +fi + +echo "" +echo "=== All tests passed! ==="