package tempproxy import ( "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "time" ) func TestNewHTTPClient(t *testing.T) { client := NewHTTPClient("https://example.com/api", "test-token") if client.baseURL != "https://example.com/api" { t.Errorf("baseURL = %q, want https://example.com/api", client.baseURL) } if client.authToken != "test-token" { t.Errorf("authToken = %q, want test-token", client.authToken) } if client.httpClient == nil { t.Error("httpClient should not be nil") } if client.timeout != defaultTimeout { t.Errorf("timeout = %v, want %v", client.timeout, defaultTimeout) } } func TestNewHTTPClient_TrimsTrailingSlash(t *testing.T) { client := NewHTTPClient("https://example.com/api/", "token") if client.baseURL != "https://example.com/api" { t.Errorf("baseURL = %q, want trailing slash removed", client.baseURL) } } func TestHTTPClient_IsAvailable(t *testing.T) { tests := []struct { name string baseURL string token string expected bool }{ { name: "both configured", baseURL: "https://example.com", token: "token", expected: true, }, { name: "empty baseURL", baseURL: "", token: "token", expected: false, }, { name: "empty token", baseURL: "https://example.com", token: "", expected: false, }, { name: "both empty", baseURL: "", token: "", expected: false, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { client := NewHTTPClient(tc.baseURL, tc.token) if client.IsAvailable() != tc.expected { t.Errorf("IsAvailable() = %v, want %v", client.IsAvailable(), tc.expected) } }) } } func TestHTTPClient_GetTemperature_Success(t *testing.T) { // Create a test server server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Verify request if r.Method != http.MethodGet { t.Errorf("Method = %q, want GET", r.Method) } if !strings.HasPrefix(r.URL.Path, "/temps") { t.Errorf("Path = %q, want /temps", r.URL.Path) } if r.URL.Query().Get("node") != "node1" { t.Errorf("node query param = %q, want node1", r.URL.Query().Get("node")) } if r.Header.Get("Authorization") != "Bearer test-token" { t.Errorf("Authorization = %q, want Bearer test-token", r.Header.Get("Authorization")) } if r.Header.Get("Accept") != "application/json" { t.Errorf("Accept = %q, want application/json", r.Header.Get("Accept")) } // Return success response resp := struct { Node string `json:"node"` Temperature string `json:"temperature"` }{ Node: "node1", Temperature: `{"cpu": 45.0}`, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(resp) })) defer server.Close() client := NewHTTPClient(server.URL, "test-token") temp, err := client.GetTemperature("node1") if err != nil { t.Fatalf("GetTemperature() error = %v", err) } if temp != `{"cpu": 45.0}` { t.Errorf("GetTemperature() = %q, want {\"cpu\": 45.0}", temp) } } func TestHTTPClient_GetTemperature_NotConfigured(t *testing.T) { client := NewHTTPClient("", "") _, err := client.GetTemperature("node1") if err == nil { t.Fatal("Expected error for unconfigured client") } proxyErr, ok := err.(*ProxyError) if !ok { t.Fatalf("Expected *ProxyError, got %T", err) } if proxyErr.Type != ErrorTypeTransport { t.Errorf("Type = %v, want ErrorTypeTransport", proxyErr.Type) } if proxyErr.Retryable { t.Error("Should not be retryable") } } func TestHTTPClient_GetTemperature_Unauthorized(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusUnauthorized) })) defer server.Close() client := NewHTTPClient(server.URL, "bad-token") _, err := client.GetTemperature("node1") if err == nil { t.Fatal("Expected error for 401 response") } proxyErr, ok := err.(*ProxyError) if !ok { t.Fatalf("Expected *ProxyError, got %T", err) } if proxyErr.Type != ErrorTypeAuth { t.Errorf("Type = %v, want ErrorTypeAuth", proxyErr.Type) } if proxyErr.Retryable { t.Error("Auth errors should not be retryable") } } func TestHTTPClient_GetTemperature_Forbidden(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusForbidden) })) defer server.Close() client := NewHTTPClient(server.URL, "token") _, err := client.GetTemperature("node1") if err == nil { t.Fatal("Expected error for 403 response") } proxyErr, ok := err.(*ProxyError) if !ok { t.Fatalf("Expected *ProxyError, got %T", err) } if proxyErr.Type != ErrorTypeAuth { t.Errorf("Type = %v, want ErrorTypeAuth", proxyErr.Type) } if proxyErr.Message != "node not allowed by proxy" { t.Errorf("Message = %q, want 'node not allowed by proxy'", proxyErr.Message) } } func TestHTTPClient_GetTemperature_RateLimit(t *testing.T) { attempts := 0 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { attempts++ w.WriteHeader(http.StatusTooManyRequests) })) defer server.Close() client := NewHTTPClient(server.URL, "token") _, err := client.GetTemperature("node1") if err == nil { t.Fatal("Expected error for 429 response") } // Should have retried if attempts < 2 { t.Errorf("Expected retries, got %d attempts", attempts) } proxyErr, ok := err.(*ProxyError) if !ok { t.Fatalf("Expected *ProxyError, got %T", err) } if proxyErr.Type != ErrorTypeTransport { t.Errorf("Type = %v, want ErrorTypeTransport", proxyErr.Type) } } func TestHTTPClient_GetTemperature_ServerError(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusInternalServerError) w.Write([]byte("internal error")) })) defer server.Close() client := NewHTTPClient(server.URL, "token") _, err := client.GetTemperature("node1") if err == nil { t.Fatal("Expected error for 500 response") } proxyErr, ok := err.(*ProxyError) if !ok { t.Fatalf("Expected *ProxyError, got %T", err) } if proxyErr.Type != ErrorTypeTransport { t.Errorf("Type = %v, want ErrorTypeTransport", proxyErr.Type) } // 5xx errors should be retryable if !proxyErr.Retryable { t.Error("5xx errors should be retryable") } } func TestHTTPClient_GetTemperature_ClientError(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusBadRequest) w.Write([]byte("bad request")) })) defer server.Close() client := NewHTTPClient(server.URL, "token") _, err := client.GetTemperature("node1") if err == nil { t.Fatal("Expected error for 400 response") } proxyErr, ok := err.(*ProxyError) if !ok { t.Fatalf("Expected *ProxyError, got %T", err) } // 4xx errors (except 401, 403, 429) should not be retryable if proxyErr.Retryable { t.Error("4xx errors should not be retryable") } } func TestHTTPClient_GetTemperature_InvalidJSON(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Write([]byte("not valid json")) })) defer server.Close() client := NewHTTPClient(server.URL, "token") _, err := client.GetTemperature("node1") if err == nil { t.Fatal("Expected error for invalid JSON") } proxyErr, ok := err.(*ProxyError) if !ok { t.Fatalf("Expected *ProxyError, got %T", err) } if proxyErr.Type != ErrorTypeTransport { t.Errorf("Type = %v, want ErrorTypeTransport", proxyErr.Type) } if !strings.Contains(proxyErr.Message, "parse response JSON") { t.Errorf("Message = %q, should mention JSON parsing", proxyErr.Message) } } func TestHTTPClient_GetTemperature_URLEncoding(t *testing.T) { receivedNode := "" server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { receivedNode = r.URL.Query().Get("node") resp := struct { Node string `json:"node"` Temperature string `json:"temperature"` }{ Node: receivedNode, Temperature: "{}", } json.NewEncoder(w).Encode(resp) })) defer server.Close() client := NewHTTPClient(server.URL, "token") // Test with special characters that need URL encoding nodeWithSpaces := "node with spaces" _, err := client.GetTemperature(nodeWithSpaces) if err != nil { t.Fatalf("GetTemperature() error = %v", err) } if receivedNode != nodeWithSpaces { t.Errorf("Received node = %q, want %q", receivedNode, nodeWithSpaces) } } func TestHTTPClient_GetTemperature_RetryOnTransportError(t *testing.T) { attempts := 0 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { attempts++ if attempts < 3 { // Force connection close to simulate transport error hj, ok := w.(http.Hijacker) if ok { conn, _, _ := hj.Hijack() conn.Close() return } } // Success on third attempt resp := struct { Node string `json:"node"` Temperature string `json:"temperature"` }{ Node: "node1", Temperature: "{}", } json.NewEncoder(w).Encode(resp) })) defer server.Close() client := NewHTTPClient(server.URL, "token") _, _ = client.GetTemperature("node1") // Should have made multiple attempts if attempts < 2 { t.Errorf("Expected retries, got %d attempts", attempts) } } func TestHTTPClient_HealthCheck_Success(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/health" { t.Errorf("Path = %q, want /health", r.URL.Path) } if r.Header.Get("Authorization") != "Bearer test-token" { t.Errorf("Authorization header missing or incorrect") } w.WriteHeader(http.StatusOK) w.Write([]byte(`{"status":"ok"}`)) })) defer server.Close() client := NewHTTPClient(server.URL, "test-token") err := client.HealthCheck() if err != nil { t.Errorf("HealthCheck() error = %v", err) } } func TestHTTPClient_HealthCheck_NotConfigured(t *testing.T) { client := NewHTTPClient("", "") err := client.HealthCheck() if err == nil { t.Fatal("Expected error for unconfigured client") } proxyErr, ok := err.(*ProxyError) if !ok { t.Fatalf("Expected *ProxyError, got %T", err) } if proxyErr.Type != ErrorTypeTransport { t.Errorf("Type = %v, want ErrorTypeTransport", proxyErr.Type) } } func TestHTTPClient_HealthCheck_ServerError(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusServiceUnavailable) w.Write([]byte("service unavailable")) })) defer server.Close() client := NewHTTPClient(server.URL, "token") err := client.HealthCheck() if err == nil { t.Fatal("Expected error for 503 response") } proxyErr, ok := err.(*ProxyError) if !ok { t.Fatalf("Expected *ProxyError, got %T", err) } if proxyErr.Type != ErrorTypeTransport { t.Errorf("Type = %v, want ErrorTypeTransport", proxyErr.Type) } // 5xx errors should be retryable if !proxyErr.Retryable { t.Error("5xx errors should be retryable") } } func TestHTTPClient_HealthCheck_ConnectionRefused(t *testing.T) { // Use a URL that will refuse connection client := NewHTTPClient("http://127.0.0.1:1", "token") client.httpClient.Timeout = 100 * time.Millisecond err := client.HealthCheck() if err == nil { t.Fatal("Expected error for refused connection") } proxyErr, ok := err.(*ProxyError) if !ok { t.Fatalf("Expected *ProxyError, got %T", err) } if proxyErr.Type != ErrorTypeTransport { t.Errorf("Type = %v, want ErrorTypeTransport", proxyErr.Type) } if !proxyErr.Retryable { t.Error("Connection errors should be retryable") } } func TestHTTPClient_HealthCheck_LongBody(t *testing.T) { // Server returns a very long body that should be limited server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusBadGateway) // Write more than 1024 bytes for i := 0; i < 200; i++ { w.Write([]byte("error message ")) } })) defer server.Close() client := NewHTTPClient(server.URL, "token") err := client.HealthCheck() if err == nil { t.Fatal("Expected error") } proxyErr, ok := err.(*ProxyError) if !ok { t.Fatalf("Expected *ProxyError, got %T", err) } // Body should be limited to 1024 bytes if len(proxyErr.Message) > 1100 { // Some overhead for HTTP status prefix t.Errorf("Message too long: %d bytes", len(proxyErr.Message)) } } func TestHTTPClient_Fields(t *testing.T) { client := &HTTPClient{ baseURL: "https://test.example.com", authToken: "secret", timeout: 60 * time.Second, } if client.baseURL != "https://test.example.com" { t.Errorf("baseURL = %q, want https://test.example.com", client.baseURL) } if client.authToken != "secret" { t.Errorf("authToken = %q, want secret", client.authToken) } if client.timeout != 60*time.Second { t.Errorf("timeout = %v, want 60s", client.timeout) } }