package task import ( "net/http" "net/http/httptest" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // TestVersionCheckerTimeout tests that version checking respects timeouts func TestVersionCheckerTimeout(t *testing.T) { t.Run("slow_server_timeout", func(t *testing.T) { // Create a server that hangs slowServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { time.Sleep(10 * time.Second) // Hang longer than timeout })) defer slowServer.Close() // Note: We can't easily test this without modifying the versionChecker // to accept a custom URL, but we can test the timeout behavior // by creating a versionChecker and calling doCheckRequest // This test documents that the current implementation DOES have a timeout // in doCheckRequest (line 45-47 in version_checker.go: 5 second timeout) t.Log("Version checker has built-in 5 second timeout") t.Logf("Test server: %s", slowServer.URL) }) } // TestVersionCheckerNetworkFailures tests handling of various network failures func TestVersionCheckerNetworkFailures(t *testing.T) { t.Run("server_returns_404", func(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotFound) })) defer server.Close() // Test with a versionChecker - we can't easily inject the URL // but we can test the error handling logic // The actual doCheckRequest will hit the real version check URL t.Log("Testing error handling for non-200 status codes") t.Logf("Test server: %s", server.URL) t.Log("Note: Cannot inject custom URL, so documenting expected behavior") t.Log("Expected: doCheckRequest returns error for 404 status") }) t.Run("server_returns_204_no_content", func(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNoContent) })) defer server.Close() // This will fail because we can't override the URL, but documents expected behavior t.Log("204 No Content should return nil error (no update available)") t.Logf("Test server: %s", server.URL) }) t.Run("server_returns_invalid_json", func(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write([]byte("invalid json")) })) defer server.Close() t.Log("Invalid JSON should be handled gracefully by decodeResult returning nil") t.Logf("Test server: %s", server.URL) }) } // TestVersionCheckerBrokenBody tests the critical bug in version_checker.go:56 // BUG: log.Fatal(err) will terminate the entire application if body read fails func TestVersionCheckerBrokenBody(t *testing.T) { // Test that doCheckRequest properly handles errors from io.ReadAll // instead of calling log.Fatal which would terminate the process // // BUG LOCATION: version_checker.go:54-57 // Current buggy code: // bodyBytes, err := io.ReadAll(resp.Body) // if err != nil { // log.Fatal(err) // <-- BUG: terminates process // } // // Expected fixed code: // if err != nil { // return err // <-- CORRECT: return error to caller // } t.Run("body_read_error_should_return_error", func(t *testing.T) { // Note: We can't easily trigger an io.ReadAll error with httptest // because the request will fail earlier. However, the fix is clear: // change log.Fatal(err) to return err on line 56. // // This test documents the expected behavior after the fix. // Once fixed, any body read errors will be properly returned // instead of terminating the process. t.Log("After fix: io.ReadAll errors should be returned, not cause log.Fatal") t.Log("Current bug: log.Fatal(err) on line 56 terminates the entire process") t.Log("Expected: return err on line 56") }) } // TestDecodeResult tests JSON decoding of version check responses func TestDecodeResult(t *testing.T) { checker := &versionChecker{} t.Run("valid_json", func(t *testing.T) { validJSON := `{ "latest_version": "1.2.3", "download_url": "https://steampipe.io/downloads", "html": "https://github.com/turbot/steampipe/releases", "alerts": ["Test alert"] }` result := checker.decodeResult(validJSON) require.NotNil(t, result) assert.Equal(t, "1.2.3", result.NewVersion) assert.Equal(t, "https://steampipe.io/downloads", result.DownloadURL) assert.Equal(t, "https://github.com/turbot/steampipe/releases", result.ChangelogURL) assert.Len(t, result.Alerts, 1) }) t.Run("invalid_json", func(t *testing.T) { invalidJSON := `{invalid json` result := checker.decodeResult(invalidJSON) assert.Nil(t, result, "Should return nil for invalid JSON") }) t.Run("empty_json", func(t *testing.T) { emptyJSON := `{}` result := checker.decodeResult(emptyJSON) require.NotNil(t, result) assert.Empty(t, result.NewVersion) assert.Empty(t, result.DownloadURL) }) t.Run("partial_json", func(t *testing.T) { partialJSON := `{"latest_version": "1.0.0"}` result := checker.decodeResult(partialJSON) require.NotNil(t, result) assert.Equal(t, "1.0.0", result.NewVersion) assert.Empty(t, result.DownloadURL) }) } // TestVersionCheckerResponseCodes tests handling of various HTTP response codes func TestVersionCheckerResponseCodes(t *testing.T) { testCases := []struct { name string statusCode int body string expectedError bool expectedResult bool }{ { name: "200_with_valid_json", statusCode: 200, body: `{"latest_version":"1.0.0"}`, expectedError: false, expectedResult: true, }, { name: "204_no_content", statusCode: 204, body: "", expectedError: false, expectedResult: false, }, { name: "500_server_error", statusCode: 500, body: "Internal Server Error", expectedError: true, }, { name: "403_forbidden", statusCode: 403, body: "Forbidden", expectedError: true, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { // Document expected behavior for different status codes t.Logf("Status %d should error=%v, result=%v", tc.statusCode, tc.expectedError, tc.expectedResult) }) } } // TestVersionCheckerBodyReadFailure specifically tests the critical bug func TestVersionCheckerBodyReadFailure(t *testing.T) { t.Run("corrupted_body_stream", func(t *testing.T) { // Create a server that returns a response but closes connection during body read server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Length", "1000000") // Claim large body w.WriteHeader(http.StatusOK) w.Write([]byte("partial")) // Write only partial data // Connection will be closed by server closing })) // Immediately close the server to simulate connection failure during body read server.Close() // This test documents the bug but can't fully test it without process exit t.Log("BUG: If body read fails, log.Fatal will terminate the process") t.Log("Location: version_checker.go:54-57") t.Log("Impact: CRITICAL - Entire Steampipe process exits unexpectedly") }) } // TestVersionCheckerStructure tests the versionChecker struct func TestVersionCheckerStructure(t *testing.T) { t.Run("new_checker", func(t *testing.T) { checker := &versionChecker{ signature: "test-installation-id", } assert.NotNil(t, checker) assert.Equal(t, "test-installation-id", checker.signature) assert.Nil(t, checker.checkResult) }) } // TestReadAllFailureScenarios documents scenarios where io.ReadAll can fail func TestReadAllFailureScenarios(t *testing.T) { t.Run("document_failure_scenarios", func(t *testing.T) { // Scenarios where io.ReadAll can fail: // 1. Connection closed during read // 2. Timeout during read // 3. Corrupted/truncated data // 4. Buffer allocation failure (OOM) // 5. Network error mid-read scenarios := []string{ "Connection closed during read", "Timeout during read", "Corrupted/truncated data", "Buffer allocation failure (OOM)", "Network error mid-read", } for _, scenario := range scenarios { t.Logf("Scenario: %s", scenario) t.Logf(" Current behavior: log.Fatal() terminates process") t.Logf(" Expected behavior: Return error to caller") } }) t.Run("failing_body_reader", func(t *testing.T) { // Test reading from a failing reader type failReader struct{} // Note: This demonstrates how io.ReadAll can fail, which triggers // the log.Fatal bug in version_checker.go:56 t.Log("io.ReadAll can fail in various scenarios:") t.Log("- Connection closed during read") t.Log("- Timeout during read") t.Log("- Corrupted/truncated response") t.Log("Current code uses log.Fatal, which terminates the process") }) }