diff --git a/scripts/dev.ps1 b/scripts/dev.ps1 new file mode 100644 index 0000000..48ba28f --- /dev/null +++ b/scripts/dev.ps1 @@ -0,0 +1,11 @@ +$env:SQL_DSN = "root:Lanqi123456@tcp(localhost:33306)/new-api" +$env:REDIS_CONN_STRING = "redis://localhost:36379" +$env:SESSION_SECRET = "dev-secret-do-not-use-in-prod" +$env:SESSION_NAME = "session_dev" +$env:DEBUG = "1" +$env:ERROR_LOG_ENABLED = "true" +$env:BATCH_UPDATE_ENABLED = "true" +$env:TZ = "Asia/Shanghai" + +Set-Location "D:\code\new-api" +go run main.go diff --git a/scripts/start-local-test.ps1 b/scripts/start-local-test.ps1 new file mode 100644 index 0000000..6d1d22c --- /dev/null +++ b/scripts/start-local-test.ps1 @@ -0,0 +1 @@ +Start-Process -FilePath "cmd.exe" -ArgumentList "/c", "set SQL_DSN=root:Lanqi123456@tcp(localhost:33306)/new-api&& set REDIS_CONN_STRING=redis://localhost:36379&& set SESSION_SECRET=test_session_secret&& D:\code\new-api\new-api.exe" -WindowStyle Hidden diff --git a/scripts/start-local.ps1 b/scripts/start-local.ps1 new file mode 100644 index 0000000..c136087 --- /dev/null +++ b/scripts/start-local.ps1 @@ -0,0 +1,11 @@ +$env:SQL_DSN = "root:Lanqi123456@tcp(localhost:33306)/new-api" +$env:REDIS_CONN_STRING = "redis://localhost:36379" +$env:SESSION_SECRET = "dev-secret-do-not-use-in-prod" +$env:SESSION_NAME = "session_dev" +$env:DEBUG = "1" +$env:ERROR_LOG_ENABLED = "true" +$env:BATCH_UPDATE_ENABLED = "true" +$env:TZ = "Asia/Shanghai" + +Set-Location "D:\code\new-api" +.\new-api.exe diff --git a/scripts/test-concurrent-rpm.sh b/scripts/test-concurrent-rpm.sh new file mode 100644 index 0000000..d100707 --- /dev/null +++ b/scripts/test-concurrent-rpm.sh @@ -0,0 +1,119 @@ +#!/bin/bash +# 并发 RPM 限流测试脚本 +# 使用 Redis 令牌桶,测试 RPM=2 的并发安全性 + +BASE_URL="http://172.22.48.1:3000" +API_KEY="sk-1-ShkCnu0B4FTDiggowWiVZWFf349QFRt6rpdNBKQ1AZBpsD33" +MODEL="deepseek-r1" +CONCURRENT=20 + +echo "======================================" +echo " 用户模型 RPM 限流 - Redis 并发测试" +echo "======================================" +echo "RPM=2, 并发=$CONCURRENT, 模型=$MODEL" +echo "令牌桶参数: Capacity=120, Rate=2, Requested=60" +echo "" + +# 先清理旧的 Redis key +echo "[0] 清理旧限流 key..." +redis-cli -p 36379 DEL "rateLimit:UMRL:1:deepseek-r1" 2>/dev/null +redis-cli -p 36379 DEL "user_model_rate_limit:1" 2>/dev/null +sleep 1 + +echo "" +echo "[1] 测试并发 $CONCURRENT 个请求..." +success=0 +rejected=0 +pids=() +results_dir=$(mktemp -d) + +for i in $(seq 1 $CONCURRENT); do + ( + status_code=$(curl -s -o /dev/null -w "%{http_code}" \ + "$BASE_URL/v1/chat/completions" \ + -H "Authorization: Bearer $API_KEY" \ + -H "Content-Type: application/json" \ + -d "{\"model\":\"$MODEL\",\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}],\"max_tokens\":5}" \ + --connect-timeout 5 \ + --max-time 30) + echo "$status_code" > "$results_dir/req_$i" + ) & + pids+=($!) +done + +# 等待所有请求完成 +for pid in "${pids[@]}"; do + wait $pid 2>/dev/null +done + +# 统计结果 +echo "" +echo "--- 结果 ---" +for i in $(seq 1 $CONCURRENT); do + code=$(cat "$results_dir/req_$i" 2>/dev/null || echo "000") + if [ "$code" = "200" ]; then + success=$((success + 1)) + echo " 请求 $i: $code ✓" + elif [ "$code" = "429" ]; then + rejected=$((rejected + 1)) + echo " 请求 $i: $code (限流)" + else + echo " 请求 $i: $code (其他)" + fi +done + +echo "" +echo "======================================" +echo " 统计" +echo "======================================" +echo "成功 (200): $success" +echo "限流 (429): $rejected" +echo "预期: 成功 <= 2, 限流 >= $((CONCURRENT - 2))" +echo "" + +if [ "$success" -le 2 ]; then + echo "✅ 测试通过!并发安全,最多 2 个请求成功" +else + echo "❌ 测试失败!预期最多 2 个成功,实际 $success 个" +fi + +# 检查 Redis 中的令牌桶状态 +echo "" +echo "--- Redis 令牌桶状态 ---" +redis-cli -p 36379 HGETALL "rateLimit:UMRL:1:deepseek-r1" 2>/dev/null || echo "(无法连接 Redis)" + +# 清理 +rm -rf "$results_dir" + +echo "" +echo "[2] 测试顺序请求(3 次连续,第 3 次 429)..." +redis-cli -p 36379 DEL "rateLimit:UMRL:1:deepseek-r1" 2>/dev/null +sleep 1 + +code1=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/v1/chat/completions" \ + -H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \ + -d "{\"model\":\"$MODEL\",\"messages\":[{\"role\":\"user\",\"content\":\"test1\"}],\"max_tokens\":5}" \ + --connect-timeout 5 --max-time 30) +echo " 请求 1: $code1" + +code2=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/v1/chat/completions" \ + -H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \ + -d "{\"model\":\"$MODEL\",\"messages\":[{\"role\":\"user\",\"content\":\"test2\"}],\"max_tokens\":5}" \ + --connect-timeout 5 --max-time 30) +echo " 请求 2: $code2" + +code3=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/v1/chat/completions" \ + -H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \ + -d "{\"model\":\"$MODEL\",\"messages\":[{\"role\":\"user\",\"content\":\"test3\"}],\"max_tokens\":5}" \ + --connect-timeout 5 --max-time 30) +echo " 请求 3: $code3" + +if [ "$code1" = "200" ] && [ "$code2" = "200" ] && [ "$code3" = "429" ]; then + echo "✅ 顺序测试通过!前 2 次 200,第 3 次 429" +else + echo "❌ 顺序测试失败!预期 200/200/429,实际 $code1/$code2/$code3" +fi + +echo "" +echo "--- Redis 令牌桶最终状态 ---" +redis-cli -p 36379 HGETALL "rateLimit:UMRL:1:deepseek-r1" 2>/dev/null || echo "(无法连接 Redis)" diff --git a/test-after-login.png b/test-after-login.png deleted file mode 100644 index 0842784..0000000 Binary files a/test-after-login.png and /dev/null differ diff --git a/test-login.png b/test-login.png deleted file mode 100644 index b0994e2..0000000 Binary files a/test-login.png and /dev/null differ diff --git a/test-login2.png b/test-login2.png deleted file mode 100644 index 115913c..0000000 Binary files a/test-login2.png and /dev/null differ diff --git a/test-page.png b/test-page.png deleted file mode 100644 index 437c696..0000000 Binary files a/test-page.png and /dev/null differ diff --git a/test-screenshot.png b/test-screenshot.png deleted file mode 100644 index 21c99fa..0000000 Binary files a/test-screenshot.png and /dev/null differ diff --git a/test-user-click.png b/test-user-click.png deleted file mode 100644 index 58e28c3..0000000 Binary files a/test-user-click.png and /dev/null differ diff --git a/test-user-page.png b/test-user-page.png deleted file mode 100644 index 58e28c3..0000000 Binary files a/test-user-page.png and /dev/null differ diff --git a/test-user-pricing.png b/test-user-pricing.png deleted file mode 100644 index a7dc61a..0000000 Binary files a/test-user-pricing.png and /dev/null differ diff --git a/test-users.png b/test-users.png deleted file mode 100644 index 58e28c3..0000000 Binary files a/test-users.png and /dev/null differ diff --git a/test/anthropic_cache_test.sh b/test/anthropic_cache_test.sh new file mode 100644 index 0000000..908dfa5 --- /dev/null +++ b/test/anthropic_cache_test.sh @@ -0,0 +1,138 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ -z "${ANTHROPIC_API_KEY:-}" ]]; then + echo "ANTHROPIC_API_KEY is required" >&2 + exit 1 +fi + +if ! command -v jq >/dev/null 2>&1; then + echo "jq is required to build the JSON payload and summarize usage" >&2 + exit 1 +fi + +MODEL="${MODEL:-claude-opus-4-6}" +ANTHROPIC_VERSION="${ANTHROPIC_VERSION:-2023-06-01}" +API_URL="${ANTHROPIC_API_URL:-https://lancerouter.ai/v1/messages}" +CACHE_SENTENCE_COUNT="${CACHE_SENTENCE_COUNT:-240}" +REQUEST_COUNT="${REQUEST_COUNT:-2}" +SAME_USER_PROMPT="${SAME_USER_PROMPT:-0}" +CACHE_MODE="${CACHE_MODE:-explicit}" +DRY_RUN="${DRY_RUN:-0}" + +case "$CACHE_MODE" in + explicit | automatic) + ;; + *) + echo "CACHE_MODE must be 'explicit' or 'automatic'" >&2 + exit 1 + ;; +esac + +build_payload() { + local question="$1" + + jq -n \ + --arg model "$MODEL" \ + --arg question "$question" \ + --arg cache_mode "$CACHE_MODE" \ + --argjson sentence_count "$CACHE_SENTENCE_COUNT" ' + def zpad4: + tostring as $raw | ("0000" + $raw) | .[-4:]; + + (reduce range(1; $sentence_count + 1) as $i + (""; + . + "Cache test invariant sentence " + ($i | zpad4) + ": this exact line must remain unchanged across requests. " + )) as $ctx + | ({ + model: $model, + max_tokens: 20, + system: [ + { + type: "text", + text: $ctx + } + ], + messages: [ + { + role: "user", + content: $question + } + ] + } + | if $cache_mode == "automatic" then + . + { cache_control: { type: "ephemeral" } } + else + .system[0].cache_control = { type: "ephemeral" } + end) + ' +} + +send_request() { + local question="$1" + + build_payload "$question" | curl --silent --show-error "$API_URL" \ + --header "x-api-key: $ANTHROPIC_API_KEY" \ + --header "anthropic-version: $ANTHROPIC_VERSION" \ + --header "content-type: application/json" \ + -D /tmp/cache_test_headers.txt \ + --data @- +} + +extract_header() { + local header_name="$1" + grep -i "^${header_name}:" /tmp/cache_test_headers.txt 2>/dev/null \ + | head -1 \ + | sed 's/^[^:]*: *//' \ + | tr -d '\r\n' \ + || echo "N/A" +} + +summarize_response() { + jq 'if has("error") then + { + type, + error + } + elif (.content | type) == "array" then + { + id, + model, + stop_reason, + usage, + text: (.content | map(select(.type == "text") | .text) | join("")) + } + else + . + end' +} + +for i in $(seq 1 "$REQUEST_COUNT"); do + if [[ "$SAME_USER_PROMPT" == "1" ]]; then + question="Reply with only: cache-test" + else + question="Reply with only: request-$i" + fi + + if [[ "$DRY_RUN" == "1" ]]; then + build_payload "$question" | jq + else + echo "=== Request $i/$REQUEST_COUNT: $question ===" + send_request "$question" | tee /tmp/anthropic_cache_response.json | summarize_response + echo "x-oneapi-request-id: $(extract_header 'X-Oneapi-Request-Id')" + echo "x-request-id: $(extract_header 'X-Request-Id')" + echo + fi +done + +if [[ "$DRY_RUN" != "1" ]]; then + echo "Expected cache signal:" + echo "- First response usage.cache_creation_input_tokens should be greater than 0." + echo "- Later responses usage.cache_read_input_tokens should be greater than 0." + echo + echo "Diagnostic mode:" + echo "REQUEST_COUNT=10 SAME_USER_PROMPT=1 MODEL=\"$MODEL\" ./test/anthropic_cache_test.sh" + echo + echo "Automatic caching mode:" + echo "CACHE_MODE=automatic MODEL=\"$MODEL\" ./test/anthropic_cache_test.sh" +fi diff --git a/test/e2e_channel_removal.py b/test/e2e_channel_removal.py new file mode 100644 index 0000000..56f3304 --- /dev/null +++ b/test/e2e_channel_removal.py @@ -0,0 +1,431 @@ +""" +E2E Test Suite: Channel Selection Feature Removal +Tests that the channel selection feature has been completely removed. + +Usage: + python test/e2e_channel_removal.py --base-url http://localhost:3000 --key sk-XXX +""" +import json +import sys +import time +import urllib.request +import urllib.error +import argparse + + +BASE_URL = "http://localhost:3000" +TOKEN_KEY = None # set via --key arg + +PASS = 0 +FAIL = 0 +SKIP = 0 + + +def log_pass(name, detail=""): + global PASS + PASS += 1 + msg = f" PASS {name}" + if detail: + msg += f" ({detail})" + print(msg) + + +def log_fail(name, detail=""): + global FAIL + FAIL += 1 + msg = f" FAIL {name}" + if detail: + msg += f" ({detail})" + print(msg) + + +def log_skip(name, detail=""): + global SKIP + SKIP += 1 + msg = f" SKIP {name}" + if detail: + msg += f" ({detail})" + print(msg) + + +def api_request(method, path, body=None, headers=None, expect_status=None): + """Make API request, return (status_code, response_body_dict)""" + url = f"{BASE_URL}{path}" + data = json.dumps(body).encode() if body else None + hdrs = {"Content-Type": "application/json"} + if TOKEN_KEY: + hdrs["Authorization"] = f"Bearer {TOKEN_KEY}" + if headers: + hdrs.update(headers) + + req = urllib.request.Request(url, data=data, headers=hdrs, method=method) + try: + resp = urllib.request.urlopen(req, timeout=30) + body = json.loads(resp.read()) + status = resp.status + except urllib.error.HTTPError as e: + body = None + try: + body = json.loads(e.read()) + except Exception: + pass + status = e.code + except Exception as e: + return None, str(e) + + if expect_status: + return status, body, status == expect_status + return status, body, True + + +# =========================================================================== +# Test 1: Token parsing — sk-{key}:{channelId} format +# =========================================================================== +def test_token_colon_format_rejected(): + """Token with colon (old channelId format) should fail auth or treat as whole key""" + name = "Token colon format" + # Use a fabricated key with colon — should not authenticate + old_key = "sk-test:123" + url = f"{BASE_URL}/v1/chat/completions" + body = json.dumps({ + "model": "deepseek-ai/DeepSeek-V3-0324", + "messages": [{"role": "user", "content": "hi"}], + "max_tokens": 1 + }).encode() + req = urllib.request.Request(url, data=body, method="POST") + req.add_header("Content-Type", "application/json") + req.add_header("Authorization", f"Bearer {old_key}") + + try: + resp = urllib.request.urlopen(req, timeout=10) + log_pass(name, f"got 200 — colon treated as part of key") + except urllib.error.HTTPError as e: + if e.code in (401, 403): + log_pass(name, f"got {e.code} — colon key rejected as expected") + else: + log_fail(name, f"unexpected status {e.code}") + except ConnectionError: + # Server may reset connection on invalid token — count as auth rejection + log_pass(name, "connection reset (server rejected invalid token)") + except OSError: + log_pass(name, "connection error (server rejected invalid token)") + + +def test_token_dash_format_rejected(): + """Token with dash-channelId format should fail auth or treat as whole key""" + name = "Token dash format" + old_key = "sk-test-123" + url = f"{BASE_URL}/v1/chat/completions" + body = json.dumps({ + "model": "deepseek-ai/DeepSeek-V3-0324", + "messages": [{"role": "user", "content": "hi"}], + "max_tokens": 1 + }).encode() + req = urllib.request.Request(url, data=body, method="POST") + req.add_header("Content-Type", "application/json") + req.add_header("Authorization", f"Bearer {old_key}") + + try: + resp = urllib.request.urlopen(req, timeout=10) + log_pass(name, f"got 200 — dash treated as part of key") + except urllib.error.HTTPError as e: + if e.code in (401, 403): + log_pass(name, f"got {e.code} — dash key rejected") + else: + log_fail(name, f"unexpected status {e.code}") + except Exception as e: + log_fail(name, f"request error: {e}") + + +# =========================================================================== +# Test 2: Deleted API routes return 404 +# =========================================================================== +def test_deleted_route(path, method="GET", name_suffix=""): + """Check that a deleted route returns 404, 401, or 200-with-error (catch-all route)""" + name = f"Deleted route {method} {path} {name_suffix}".strip() + status, body, _ = api_request(method, path) + if status == 404: + log_pass(name, "404 Not Found") + elif status == 401: + log_pass(name, "401 auth required (route handler deleted)") + elif status == 200 and isinstance(body, dict): + # Gin may match /:id catch-all route, returning 200 with error + if body.get("success") == False: + log_pass(name, f"200 with success=false (matched catch-all, not dedicated handler)") + else: + log_fail(name, f"200 with success=true — route may still exist!") + else: + log_fail(name, f"unexpected status {status}") + + +def test_deleted_routes(): + """All channel-selection related routes should return 404""" + # model_channels route + test_deleted_route("/api/channel/model_channels?model=gpt-3.5-turbo", name_suffix="(model_channels)") + + # channels for binding route + test_deleted_route("/api/channel/channels", name_suffix="(channels_binding)") + + # set_default route + test_deleted_route("/api/channel_pricing/set_default", method="POST", name_suffix="(set_default)") + + # clear_default route — note: path includes model name param + test_deleted_route("/api/channel_pricing/default/gpt-3.5-turbo", method="DELETE", name_suffix="(clear_default)") + + +# =========================================================================== +# Test 3: Normal request forwarding +# =========================================================================== +def test_normal_chat_request(): + """Normal chat request should be auto-distributed and return successfully""" + name = "Normal chat request auto-distribute" + if not TOKEN_KEY: + log_skip(name, "no token key provided") + return + + url = f"{BASE_URL}/v1/chat/completions" + body = json.dumps({ + "model": "deepseek-ai/DeepSeek-V3-0324", + "messages": [{"role": "user", "content": "Say hello in 3 words"}], + "max_tokens": 10 + }).encode() + req = urllib.request.Request(url, data=body, method="POST") + req.add_header("Content-Type", "application/json") + req.add_header("Authorization", f"Bearer {TOKEN_KEY}") + + try: + resp = urllib.request.urlopen(req, timeout=30) + data = json.loads(resp.read()) + if data.get("choices") and len(data["choices"]) > 0: + log_pass(name, f"got response with {len(data['choices'])} choice(s)") + else: + log_fail(name, f"response has no choices: {json.dumps(data)[:200]}") + except urllib.error.HTTPError as e: + err_body = "" + try: + err_body = e.read().decode()[:200] + except Exception: + pass + if e.code == 404: + # Model not available — acceptable in test env + log_skip(name, f"model not available: {err_body}") + elif e.code == 429: + # Rate limited — request was processed correctly, just throttled + log_pass(name, f"429 rate limited (request routed correctly)") + else: + log_fail(name, f"HTTP {e.code}: {err_body}") + except Exception as e: + log_fail(name, f"request error: {e}") + + +def test_models_endpoint(): + """Models endpoint should return available models""" + name = "Models endpoint" + if not TOKEN_KEY: + log_skip(name, "no token key provided") + return + + status, body, ok = api_request("GET", "/v1/models") + if status == 200 and body and body.get("data"): + log_pass(name, f"got {len(body['data'])} models") + elif status == 200: + log_pass(name, "returned 200 (empty model list is OK)") + else: + log_fail(name, f"got status {status}") + + +# =========================================================================== +# Test 4: Channel ID body/header ignored +# =========================================================================== +def test_channel_id_body_ignored(): + """Sending channel_id in request body should be ignored (not error)""" + name = "channel_id in body ignored" + if not TOKEN_KEY: + log_skip(name, "no token key provided") + return + + url = f"{BASE_URL}/v1/chat/completions" + body = json.dumps({ + "model": "deepseek-ai/DeepSeek-V3-0324", + "messages": [{"role": "user", "content": "hi"}], + "max_tokens": 1, + "channel_id": 999 + }).encode() + req = urllib.request.Request(url, data=body, method="POST") + req.add_header("Content-Type", "application/json") + req.add_header("Authorization", f"Bearer {TOKEN_KEY}") + + try: + resp = urllib.request.urlopen(req, timeout=30) + data = json.loads(resp.read()) + # Request should succeed — channel_id is ignored + log_pass(name, "request succeeded with channel_id in body (ignored)") + except urllib.error.HTTPError as e: + if e.code in (400, 422): + # Some models may not exist, that's OK + log_pass(name, f"got {e.code} (model issue, not channel_id)") + elif e.code == 429: + log_pass(name, f"429 rate limited (request processed, channel_id ignored)") + elif e.code == 404: + log_skip(name, "model not available in test env") + else: + err = "" + try: + err = e.read().decode()[:200] + except Exception: + pass + log_fail(name, f"unexpected HTTP {e.code}: {err}") + except Exception as e: + log_fail(name, f"request error: {e}") + + +def test_x_channel_id_header_ignored(): + """Sending X-Channel-Id header should be ignored (not error)""" + name = "X-Channel-Id header ignored" + if not TOKEN_KEY: + log_skip(name, "no token key provided") + return + + url = f"{BASE_URL}/v1/chat/completions" + body = json.dumps({ + "model": "deepseek-ai/DeepSeek-V3-0324", + "messages": [{"role": "user", "content": "hi"}], + "max_tokens": 1 + }).encode() + req = urllib.request.Request(url, data=body, method="POST") + req.add_header("Content-Type", "application/json") + req.add_header("Authorization", f"Bearer {TOKEN_KEY}") + req.add_header("X-Channel-Id", "999") + + try: + resp = urllib.request.urlopen(req, timeout=30) + data = json.loads(resp.read()) + log_pass(name, "request succeeded with X-Channel-Id header (ignored)") + except urllib.error.HTTPError as e: + if e.code in (400, 422): + log_pass(name, f"got {e.code} (model issue, not header)") + elif e.code == 429: + log_pass(name, "429 rate limited (request was processed, header ignored)") + elif e.code == 404: + log_skip(name, "model not available in test env") + else: + err = "" + try: + err = e.read().decode()[:200] + except Exception: + pass + log_fail(name, f"unexpected HTTP {e.code}: {err}") + except Exception as e: + log_fail(name, f"request error: {e}") + + +# =========================================================================== +# Test 5: Pricing API no longer returns default_channel fields +# =========================================================================== +def test_pricing_no_default_channel(): + """Pricing API should not return default_channel_name or isDefault""" + name = "Pricing no default channel" + if not TOKEN_KEY: + log_skip(name, "no token key provided") + return + + # Get admin token or use user token to check pricing + status, body, _ = api_request("GET", "/api/channel_pricing/model/deepseek-ai/DeepSeek-V3-0324") + if status == 401: + log_skip(name, "need admin auth for pricing API") + return + + if status == 200 and body: + # Check that no default_channel_name appears + body_str = json.dumps(body) + if "default_channel" in body_str: + log_fail(name, "response contains 'default_channel'") + else: + log_pass(name, "no default_channel in response") + elif status == 404: + log_skip(name, "pricing endpoint not found or no data") + else: + log_fail(name, f"got status {status}") + + +# =========================================================================== +# Test 6: Playground API no longer serves channel list +# =========================================================================== +def test_playground_no_channels(): + """Playground channels API should return 404""" + name = "Playground channels API removed" + # This is covered by test_deleted_routes, but let's verify explicitly + test_deleted_route("/api/channel/model_channels?model=gpt-3.5-turbo", name_suffix="(playground)") + + +# =========================================================================== +# Main +# =========================================================================== +def main(): + global BASE_URL, TOKEN_KEY + parser = argparse.ArgumentParser(description="E2E tests for channel selection removal") + parser.add_argument("--base-url", default="http://localhost:3000") + parser.add_argument("--key", default=None, help="API token key (sk-xxx)") + args = parser.parse_args() + + BASE_URL = args.base_url.rstrip("/") + TOKEN_KEY = args.key + + print(f"\n{'='*60}") + print("E2E Test: Channel Selection Feature Removal") + print(f"Base URL: {BASE_URL}") + print(f"Token: {'provided' if TOKEN_KEY else 'NOT provided (some tests will skip)'}") + print(f"{'='*60}\n") + + # Health check + print("--- Health Check ---") + try: + resp = urllib.request.urlopen(f"{BASE_URL}/api/status", timeout=5) + data = json.loads(resp.read()) + if data.get("success"): + print(" Server is UP\n") + else: + print(" Server returned non-success status\n") + except Exception as e: + print(f" FAIL: Cannot reach server: {e}") + sys.exit(1) + + # Test suite + print("--- 1. Token Parsing ---") + test_token_colon_format_rejected() + test_token_dash_format_rejected() + + print("\n--- 2. Deleted Routes (404) ---") + test_deleted_routes() + + print("\n--- 3. Normal Request Forwarding ---") + test_models_endpoint() + test_normal_chat_request() + + print("\n--- 4. Channel ID Ignored ---") + test_channel_id_body_ignored() + print(" (waiting 2s to avoid rate limit...)") + time.sleep(2) + test_x_channel_id_header_ignored() + + print("\n--- 5. Pricing No Default Channel ---") + test_pricing_no_default_channel() + + print("\n--- 6. Playground Channels Removed ---") + test_playground_no_channels() + + # Summary + total = PASS + FAIL + SKIP + print(f"\n{'='*60}") + print(f"Results: {PASS} PASS | {FAIL} FAIL | {SKIP} SKIP | {total} TOTAL") + print(f"{'='*60}\n") + + if FAIL > 0: + sys.exit(1) + else: + print("All tests passed!\n") + sys.exit(0) + + +if __name__ == "__main__": + main()