diff --git a/api/middleware_test.go b/api/middleware_test.go new file mode 100644 index 0000000..22c54a1 --- /dev/null +++ b/api/middleware_test.go @@ -0,0 +1,168 @@ +package api + +import ( + "net/http" + "net/http/httptest" + "testing" +) + +func TestIsIPAllowed(t *testing.T) { + tests := []struct { + name string + clientIP string + allowedIPs []string + expected bool + }{ + { + name: "exact match", + clientIP: "192.168.1.100", + allowedIPs: []string{"192.168.1.100"}, + expected: true, + }, + { + name: "no match", + clientIP: "192.168.1.100", + allowedIPs: []string{"192.168.1.101"}, + expected: false, + }, + { + name: "wildcard", + clientIP: "192.168.1.100", + allowedIPs: []string{"*"}, + expected: true, + }, + { + name: "CIDR match", + clientIP: "192.168.1.100", + allowedIPs: []string{"192.168.1.0/24"}, + expected: true, + }, + { + name: "CIDR no match", + clientIP: "192.168.2.100", + allowedIPs: []string{"192.168.1.0/24"}, + expected: false, + }, + { + name: "multiple IPs with match", + clientIP: "192.168.1.100", + allowedIPs: []string{"10.0.0.1", "192.168.1.100", "172.16.0.1"}, + expected: true, + }, + { + name: "localhost", + clientIP: "127.0.0.1", + allowedIPs: []string{"127.0.0.1"}, + expected: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := isIPAllowed(tt.clientIP, tt.allowedIPs) + if result != tt.expected { + t.Errorf("isIPAllowed(%q, %v) = %v, want %v", + tt.clientIP, tt.allowedIPs, result, tt.expected) + } + }) + } +} + +func TestAuthMiddleware_NoAuth(t *testing.T) { + // Create a test handler + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte("OK")) + }) + + // Create request + req := httptest.NewRequest("GET", "/api/v1/tasks", nil) + rec := httptest.NewRecorder() + + // Apply middleware + authMiddleware(handler).ServeHTTP(rec, req) + + // When no token is configured, request should succeed or be unauthorized + if rec.Code != http.StatusOK && rec.Code != http.StatusUnauthorized { + t.Errorf("Expected status 200 or 401, got %d", rec.Code) + } +} + +func TestAuthMiddleware_HealthCheck(t *testing.T) { + // Create a test handler + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte("OK")) + }) + + // Create request to health endpoint + req := httptest.NewRequest("GET", "/health", nil) + rec := httptest.NewRecorder() + + // Apply middleware + authMiddleware(handler).ServeHTTP(rec, req) + + // Health check should always work without auth + if rec.Code != http.StatusOK { + t.Errorf("Expected status 200 for health check, got %d", rec.Code) + } +} + +func TestGetClientIP(t *testing.T) { + tests := []struct { + name string + remoteAddr string + xForwardedFor string + xRealIP string + expectedIP string + }{ + { + name: "RemoteAddr only", + remoteAddr: "192.168.1.100:12345", + expectedIP: "192.168.1.100", + }, + { + name: "X-Forwarded-For single", + remoteAddr: "192.168.1.100:12345", + xForwardedFor: "10.0.0.1", + expectedIP: "10.0.0.1", + }, + { + name: "X-Forwarded-For multiple", + remoteAddr: "192.168.1.100:12345", + xForwardedFor: "10.0.0.1, 10.0.0.2, 10.0.0.3", + expectedIP: "10.0.0.1", + }, + { + name: "X-Real-IP", + remoteAddr: "192.168.1.100:12345", + xRealIP: "10.0.0.1", + expectedIP: "10.0.0.1", + }, + { + name: "X-Forwarded-For takes precedence", + remoteAddr: "192.168.1.100:12345", + xForwardedFor: "10.0.0.1", + xRealIP: "10.0.0.2", + expectedIP: "10.0.0.1", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req := httptest.NewRequest("GET", "/test", nil) + req.RemoteAddr = tt.remoteAddr + if tt.xForwardedFor != "" { + req.Header.Set("X-Forwarded-For", tt.xForwardedFor) + } + if tt.xRealIP != "" { + req.Header.Set("X-Real-IP", tt.xRealIP) + } + + result := getClientIP(req) + if result != tt.expectedIP { + t.Errorf("getClientIP() = %q, want %q", result, tt.expectedIP) + } + }) + } +} diff --git a/test_api.sh b/test_api.sh new file mode 100755 index 0000000..96c5490 --- /dev/null +++ b/test_api.sh @@ -0,0 +1,105 @@ +#!/bin/bash + +# API Test Script for SaveAny-Bot HTTP API + +API_URL="http://localhost:18080" +TOKEN="test-token-12345" +HEADERS=(-H "Authorization: Bearer ${TOKEN}" -H "Content-Type: application/json") + +echo "=== Testing SaveAny-Bot HTTP API ===" +echo + +# Test 1: Health Check (no auth required) +echo "1. Testing health check endpoint..." +RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" "${API_URL}/health") +HTTP_STATUS=$(echo "$RESPONSE" | grep "HTTP_STATUS" | cut -d: -f2) +BODY=$(echo "$RESPONSE" | sed '/HTTP_STATUS/d') + +if [ "$HTTP_STATUS" = "200" ]; then + echo "✓ Health check passed" + echo " Response: $BODY" +else + echo "✗ Health check failed (HTTP $HTTP_STATUS)" + echo " Response: $BODY" +fi +echo + +# Test 2: Unauthorized request (missing token) +echo "2. Testing unauthorized request..." +RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" "${API_URL}/api/v1/tasks") +HTTP_STATUS=$(echo "$RESPONSE" | grep "HTTP_STATUS" | cut -d: -f2) + +if [ "$HTTP_STATUS" = "401" ]; then + echo "✓ Correctly rejected unauthorized request" +else + echo "✗ Unexpected response (HTTP $HTTP_STATUS)" +fi +echo + +# Test 3: Invalid token +echo "3. Testing invalid token..." +RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" \ + -H "Authorization: Bearer wrong-token" \ + "${API_URL}/api/v1/tasks") +HTTP_STATUS=$(echo "$RESPONSE" | grep "HTTP_STATUS" | cut -d: -f2) + +if [ "$HTTP_STATUS" = "401" ]; then + echo "✓ Correctly rejected invalid token" +else + echo "✗ Unexpected response (HTTP $HTTP_STATUS)" +fi +echo + +# Test 4: List tasks (authorized) +echo "4. Testing list tasks endpoint..." +RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" \ + "${HEADERS[@]}" \ + "${API_URL}/api/v1/tasks") +HTTP_STATUS=$(echo "$RESPONSE" | grep "HTTP_STATUS" | cut -d: -f2) +BODY=$(echo "$RESPONSE" | sed '/HTTP_STATUS/d') + +if [ "$HTTP_STATUS" = "200" ]; then + echo "✓ List tasks successful" + echo " Response: $BODY" +else + echo "✗ List tasks failed (HTTP $HTTP_STATUS)" + echo " Response: $BODY" +fi +echo + +# Test 5: Create task with missing parameters +echo "5. Testing create task with missing parameters..." +RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" \ + -X POST \ + "${HEADERS[@]}" \ + -d '{"telegram_url":""}' \ + "${API_URL}/api/v1/tasks") +HTTP_STATUS=$(echo "$RESPONSE" | grep "HTTP_STATUS" | cut -d: -f2) + +if [ "$HTTP_STATUS" = "400" ]; then + echo "✓ Correctly rejected invalid request" +else + echo "✗ Unexpected response (HTTP $HTTP_STATUS)" +fi +echo + +# Test 6: Get non-existent task +echo "6. Testing get non-existent task..." +RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" \ + "${HEADERS[@]}" \ + "${API_URL}/api/v1/tasks/nonexistent") +HTTP_STATUS=$(echo "$RESPONSE" | grep "HTTP_STATUS" | cut -d: -f2) + +if [ "$HTTP_STATUS" = "404" ]; then + echo "✓ Correctly returned 404 for non-existent task" +else + echo "✗ Unexpected response (HTTP $HTTP_STATUS)" +fi +echo + +echo "=== API Tests Complete ===" +echo +echo "Note: Full integration testing requires:" +echo " - Valid Telegram bot token in config" +echo " - Bot running and connected to Telegram" +echo " - Valid Telegram message URL to test downloads"