Repository: grafana/grafana-infinity-datasource

Issue:
[Chore] Fix tests for digest authentication
https://github.com/grafana/grafana-infinity-datasource/pull/1232#discussion_r2071255548

Hints from issue comments:

Solution patch:
diff --git a/go.mod b/go.mod
index c51726d..852b9e2 100644
--- a/go.mod
+++ b/go.mod
@@ -16,13 +16,18 @@ require (
 	github.com/grafana/infinity-libs/lib/go/transformations v1.0.6
 	github.com/grafana/infinity-libs/lib/go/xmlframer v1.0.3
 	github.com/icholy/digest v0.1.22
-	github.com/stretchr/testify v1.10.0
 	go.opentelemetry.io/otel v1.36.0
 	go.opentelemetry.io/otel/trace v1.36.0
 	golang.org/x/oauth2 v0.30.0
 	moul.io/http2curl v1.0.0
 )
 
+// tests related deps
+require (
+	github.com/abbot/go-http-auth v0.4.0
+	github.com/stretchr/testify v1.10.0
+)
+
 require (
 	github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.2 // indirect
 	github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2 // indirect
diff --git a/go.sum b/go.sum
index c6c97d6..439b70b 100644
--- a/go.sum
+++ b/go.sum
@@ -15,6 +15,8 @@ github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1/go.mod h1:wP83
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
 github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
+github.com/abbot/go-http-auth v0.4.0 h1:QjmvZ5gSC7jm3Zg54DqWE/T5m1t2AfDu6QlXJT0EVT0=
+github.com/abbot/go-http-auth v0.4.0/go.mod h1:Cz6ARTIzApMJDzh5bRMSUou6UMSp0IEXg9km/ci7TJM=
 github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
 github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
 github.com/apache/arrow-go/v18 v18.3.0 h1:Xq4A6dZj9Nu33sqZibzn012LNnewkTUlfKVUFD/RX/I=
diff --git a/pkg/testsuite/golden/digest-auth-ok.jsonc b/pkg/testsuite/golden/digest-auth-ok.jsonc
new file mode 100644
index 0000000..b916cb9
--- /dev/null
+++ b/pkg/testsuite/golden/digest-auth-ok.jsonc
@@ -0,0 +1,167 @@
+//  🌟 This was machine generated.  Do not edit. 🌟
+//  
+//  Frame[0] {
+//      "typeVersion": [
+//          0,
+//          0
+//      ],
+//      "custom": {
+//          "query": {
+//              "refId": "",
+//              "type": "json",
+//              "format": "",
+//              "source": "url",
+//              "url": "http://127.0.0.1:8080",
+//              "url_options": {
+//                  "method": "GET",
+//                  "params": null,
+//                  "headers": null,
+//                  "data": "",
+//                  "body_type": "",
+//                  "body_content_type": "",
+//                  "body_form": null,
+//                  "body_graphql_query": "",
+//                  "body_graphql_variables": ""
+//              },
+//              "data": "",
+//              "parser": "backend",
+//              "filterExpression": "",
+//              "summarizeExpression": "",
+//              "summarizeBy": "",
+//              "summarizeAlias": "",
+//              "uql": "",
+//              "groq": "",
+//              "csv_options": {
+//                  "delimiter": "",
+//                  "skip_empty_lines": false,
+//                  "skip_lines_with_error": false,
+//                  "relax_column_count": false,
+//                  "columns": "",
+//                  "comment": ""
+//              },
+//              "json_options": {
+//                  "root_is_not_array": false,
+//                  "columnar": false
+//              },
+//              "root_selector": "",
+//              "columns": [],
+//              "computed_columns": [],
+//              "filters": null,
+//              "seriesCount": 0,
+//              "expression": "",
+//              "alias": "",
+//              "dataOverrides": null,
+//              "global_query_id": "",
+//              "query_mode": ""
+//          },
+//          "data": {
+//              "message": "OK"
+//          },
+//          "responseCodeFromServer": 200,
+//          "duration": 123,
+//          "error": ""
+//      },
+//      "executedQueryString": "###############\n## URL\n###############\n\nhttp://127.0.0.1:8080\n\n###############\n## Curl Command\n###############\n\ncurl -X 'GET' -H 'Accept: application/json;q=0.9,text/plain' -H 'Accept-Encoding: gzip' 'http://127.0.0.1:8080'"
+//  }
+//  Name: response
+//  Dimensions: 1 Fields by 1 Rows
+//  +-----------------+
+//  | Name: message   |
+//  | Labels:         |
+//  | Type: []*string |
+//  +-----------------+
+//  | OK              |
+//  +-----------------+
+//  
+//  
+//  🌟 This was machine generated.  Do not edit. 🌟
+{
+  "status": 200,
+  "frames": [
+    {
+      "schema": {
+        "name": "response",
+        "meta": {
+          "typeVersion": [
+            0,
+            0
+          ],
+          "custom": {
+            "query": {
+              "refId": "",
+              "type": "json",
+              "format": "",
+              "source": "url",
+              "url": "http://127.0.0.1:8080",
+              "url_options": {
+                "method": "GET",
+                "params": null,
+                "headers": null,
+                "data": "",
+                "body_type": "",
+                "body_content_type": "",
+                "body_form": null,
+                "body_graphql_query": "",
+                "body_graphql_variables": ""
+              },
+              "data": "",
+              "parser": "backend",
+              "filterExpression": "",
+              "summarizeExpression": "",
+              "summarizeBy": "",
+              "summarizeAlias": "",
+              "uql": "",
+              "groq": "",
+              "csv_options": {
+                "delimiter": "",
+                "skip_empty_lines": false,
+                "skip_lines_with_error": false,
+                "relax_column_count": false,
+                "columns": "",
+                "comment": ""
+              },
+              "json_options": {
+                "root_is_not_array": false,
+                "columnar": false
+              },
+              "root_selector": "",
+              "columns": [],
+              "computed_columns": [],
+              "filters": null,
+              "seriesCount": 0,
+              "expression": "",
+              "alias": "",
+              "dataOverrides": null,
+              "global_query_id": "",
+              "query_mode": ""
+            },
+            "data": {
+              "message": "OK"
+            },
+            "responseCodeFromServer": 200,
+            "duration": 123,
+            "error": ""
+          },
+          "executedQueryString": "###############\n## URL\n###############\n\nhttp://127.0.0.1:8080\n\n###############\n## Curl Command\n###############\n\ncurl -X 'GET' -H 'Accept: application/json;q=0.9,text/plain' -H 'Accept-Encoding: gzip' 'http://127.0.0.1:8080'"
+        },
+        "fields": [
+          {
+            "name": "message",
+            "type": "string",
+            "typeInfo": {
+              "frame": "string",
+              "nullable": true
+            }
+          }
+        ]
+      },
+      "data": {
+        "values": [
+          [
+            "OK"
+          ]
+        ]
+      }
+    }
+  ]
+}
\ No newline at end of file


Test patch:
diff --git a/pkg/testsuite/handler_querydata_test.go b/pkg/testsuite/handler_querydata_test.go
index eae7af4..717e0a7 100644
--- a/pkg/testsuite/handler_querydata_test.go
+++ b/pkg/testsuite/handler_querydata_test.go
@@ -5,11 +5,16 @@ import (
 	"encoding/base64"
 	"fmt"
 	"io"
+	"net"
 	"net/http"
 	"net/http/httptest"
 	"strings"
 	"testing"
 
+	"crypto/md5"
+	"encoding/hex"
+
+	auth "github.com/abbot/go-http-auth"
 	"github.com/grafana/grafana-infinity-datasource/pkg/infinity"
 	"github.com/grafana/grafana-infinity-datasource/pkg/models"
 	"github.com/grafana/grafana-infinity-datasource/pkg/pluginhost"
@@ -30,7 +35,7 @@ func queryData(t *testing.T, ctx context.Context, backendQuery backend.DataQuery
 func TestAuthentication(t *testing.T) {
 	t.Run("should throw error when allowed hosts not configured", func(t *testing.T) {
 		server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-			fmt.Fprintf(w, `{ "message" : "OK" }`)
+			_, _ = fmt.Fprintf(w, `{ "message" : "OK" }`)
 		}))
 		defer server.Close()
 		client, err := infinity.NewClient(context.TODO(), models.InfinitySettings{AuthenticationMethod: models.AuthenticationMethodApiKey, ApiKeyKey: "foo", ApiKeyValue: "bar"})
@@ -54,7 +59,7 @@ func TestAuthentication(t *testing.T) {
 				assert.Equal(t, http.MethodGet, r.Method)
 				assert.Equal(t, "Basic "+base64.StdEncoding.EncodeToString([]byte("infinityUser:myPassword")), r.Header.Get(infinity.HeaderKeyAuthorization))
 				assert.Equal(t, "", r.Header.Get(infinity.HeaderKeyIdToken))
-				fmt.Fprintf(w, `{ "message" : "OK" }`)
+				_, _ = fmt.Fprintf(w, `{ "message" : "OK" }`)
 			}))
 			defer server.Close()
 			client, err := infinity.NewClient(context.TODO(), models.InfinitySettings{
@@ -85,11 +90,11 @@ func TestAuthentication(t *testing.T) {
 				assert.Equal(t, http.MethodGet, r.Method)
 				assert.Equal(t, "", r.Header.Get(infinity.HeaderKeyIdToken))
 				if r.Header.Get(infinity.HeaderKeyAuthorization) == "Basic "+base64.StdEncoding.EncodeToString([]byte("infinityUser:myPassword")) {
-					fmt.Fprintf(w, "OK")
+					_, _ = fmt.Fprintf(w, "OK")
 					return
 				}
 				w.WriteHeader(http.StatusUnauthorized)
-				fmt.Fprintf(w, "UnAuthorized")
+				_, _ = fmt.Fprintf(w, "UnAuthorized")
 			}))
 			defer server.Close()
 			client, err := infinity.NewClient(context.TODO(), models.InfinitySettings{
@@ -122,7 +127,7 @@ func TestAuthentication(t *testing.T) {
 				assert.Equal(t, http.MethodGet, r.Method)
 				assert.Equal(t, "foo", r.Header.Get(infinity.HeaderKeyAuthorization))
 				assert.Equal(t, "bar", r.Header.Get(infinity.HeaderKeyIdToken))
-				fmt.Fprintf(w, `{ "message" : "OK" }`)
+				_, _ = fmt.Fprintf(w, `{ "message" : "OK" }`)
 			}))
 			defer server.Close()
 			client, err := infinity.NewClient(context.TODO(), models.InfinitySettings{
@@ -146,7 +151,7 @@ func TestAuthentication(t *testing.T) {
 				assert.Equal(t, http.MethodGet, r.Method)
 				assert.Equal(t, "", r.Header.Get(infinity.HeaderKeyAuthorization))
 				assert.Equal(t, "", r.Header.Get(infinity.HeaderKeyIdToken))
-				fmt.Fprintf(w, `{ "message" : "OK" }`)
+				_, _ = fmt.Fprintf(w, `{ "message" : "OK" }`)
 			}))
 			defer server.Close()
 			client, err := infinity.NewClient(context.TODO(), models.InfinitySettings{
@@ -257,7 +262,7 @@ func TestAuthentication(t *testing.T) {
 			server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 				assert.Equal(t, http.MethodGet, r.Method)
 				assert.Equal(t, "", r.Header.Get(infinity.HeaderKeyIdToken))
-				fmt.Fprintf(w, `{ "message" : "OK" }`)
+				_, _ = fmt.Fprintf(w, `{ "message" : "OK" }`)
 			}))
 			server.TLS = getServerCertificate(server.URL)
 			assert.NotNil(t, server.TLS)
@@ -289,7 +294,7 @@ func TestAuthentication(t *testing.T) {
 			server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 				assert.Equal(t, http.MethodGet, r.Method)
 				assert.Equal(t, "", r.Header.Get(infinity.HeaderKeyIdToken))
-				fmt.Fprintf(w, `{ "message" : "OK" }`)
+				_, _ = fmt.Fprintf(w, `{ "message" : "OK" }`)
 			}))
 			server.TLS = getServerCertificate(server.URL)
 			assert.NotNil(t, server.TLS)
@@ -318,61 +323,81 @@ func TestAuthentication(t *testing.T) {
 		})
 	})
 	t.Run("digest auth", func(t *testing.T) {
-		t.Skip() // skipping due to transient errors in httpbin.org.
-		//TODO: Replace httpbin with local digeset auth server
+		realm := "test-realm"
+		username := "foo"
+		password := "bar"
+		secretProvider := func(user, realm string) string {
+			if user == username {
+				h := md5.New()
+				_, _ = h.Write([]byte(fmt.Sprintf("%s:%s:%s", username, realm, password)))
+				return hex.EncodeToString(h.Sum(nil))
+			}
+			return ""
+		}
+		authenticator := auth.NewDigestAuthenticator(realm, secretProvider)
+		server := httptest.NewUnstartedServer(http.HandlerFunc(authenticator.Wrap(func(w http.ResponseWriter, r *auth.AuthenticatedRequest) {
+			w.Header().Set("Content-Type", "application/json")
+			_, _ = w.Write([]byte(`{"message": "OK"}`))
+		})))
+		l, err := net.Listen("tcp", "127.0.0.1:8080")
+		require.Nil(t, err)
+		_ = server.Listener.Close()
+		server.Listener = l
+		server.Start()
+		defer server.Close()
+		query := backend.DataQuery{JSON: []byte(fmt.Sprintf(`{ "type": "json", "url":  "%s", "source": "url" }`, server.URL))}
 		t.Run("should be ok with correct credentials", func(t *testing.T) {
 			client, err := infinity.NewClient(context.TODO(), models.InfinitySettings{
+				IsMock:               true,
 				AuthenticationMethod: models.AuthenticationMethodDigestAuth,
-				UserName:             "foo",
-				Password:             "bar",
-				AllowedHosts:         []string{"http://httpbin.org/digest-auth"},
+				UserName:             username,
+				Password:             password,
+				AllowedHosts:         []string{server.URL},
 			})
 			require.Nil(t, err)
-			res := queryData(t, context.Background(), backend.DataQuery{
-				JSON: []byte(fmt.Sprintf(`{
-					"type": "json",
-					"url":  "%s",
-					"source": "url"
-				}`, "http://httpbin.org/digest-auth/auth/foo/bar/MD5")),
-			}, *client, map[string]string{}, backend.PluginContext{})
+			res := queryData(t, context.Background(), query, *client, map[string]string{}, backend.PluginContext{})
 			require.NotNil(t, res)
 			require.Nil(t, res.Error)
+			experimental.CheckGoldenJSONResponse(t, "golden", "digest-auth-ok", &res, UPDATE_GOLDEN_DATA)
 		})
-		t.Run("should fail with incorrect credentials", func(t *testing.T) {
+		t.Run("should fail with incorrect username", func(t *testing.T) {
 			client, err := infinity.NewClient(context.TODO(), models.InfinitySettings{
 				AuthenticationMethod: models.AuthenticationMethodDigestAuth,
-				UserName:             "foo",
-				Password:             "baz",
-				AllowedHosts:         []string{"http://httpbin.org/digest-auth"},
+				UserName:             "boo",
+				Password:             "bar",
+				AllowedHosts:         []string{server.URL},
 			})
 			require.Nil(t, err)
-			res := queryData(t, context.Background(), backend.DataQuery{
-				JSON: []byte(fmt.Sprintf(`{
-					"type": "json",
-					"url":  "%s",
-					"source": "url"
-				}`, "http://httpbin.org/digest-auth/auth/foo/bar/MD5")),
-			}, *client, map[string]string{}, backend.PluginContext{})
+			res := queryData(t, context.Background(), query, *client, map[string]string{}, backend.PluginContext{})
+			require.NotNil(t, res)
+			require.NotNil(t, res.Error)
+			require.Equal(t, "error while performing the infinity query. unsuccessful HTTP response code\nstatus code : 401 Unauthorized", res.Error.Error())
+		})
+		t.Run("should fail with incorrect password", func(t *testing.T) {
+			client, err := infinity.NewClient(context.TODO(), models.InfinitySettings{
+				AuthenticationMethod: models.AuthenticationMethodDigestAuth,
+				UserName:             "boo",
+				Password:             "bar",
+				AllowedHosts:         []string{server.URL},
+			})
+			require.Nil(t, err)
+			res := queryData(t, context.Background(), query, *client, map[string]string{}, backend.PluginContext{})
+			require.NotNil(t, res)
 			require.NotNil(t, res.Error)
-			require.Equal(t, "error while performing the infinity query. unsuccessful HTTP response. 401 UNAUTHORIZED", res.Error.Error())
+			require.Equal(t, "error while performing the infinity query. unsuccessful HTTP response code\nstatus code : 401 Unauthorized", res.Error.Error())
 		})
 		t.Run("should fail with incorrect auth method", func(t *testing.T) {
 			client, err := infinity.NewClient(context.TODO(), models.InfinitySettings{
 				AuthenticationMethod: models.AuthenticationMethodBasic,
-				UserName:             "foo",
-				Password:             "bar",
-				AllowedHosts:         []string{"http://httpbin.org/digest-auth"},
+				UserName:             username,
+				Password:             password,
+				AllowedHosts:         []string{server.URL},
 			})
 			require.Nil(t, err)
-			res := queryData(t, context.Background(), backend.DataQuery{
-				JSON: []byte(fmt.Sprintf(`{
-					"type": "json",
-					"url":  "%s",
-					"source": "url"
-				}`, "http://httpbin.org/digest-auth/auth/foo/bar/MD5")),
-			}, *client, map[string]string{}, backend.PluginContext{})
+			res := queryData(t, context.Background(), query, *client, map[string]string{}, backend.PluginContext{})
+			require.NotNil(t, res)
 			require.NotNil(t, res.Error)
-			require.Equal(t, "error while performing the infinity query. unsuccessful HTTP response. 401 UNAUTHORIZED", res.Error.Error())
+			require.Equal(t, "error while performing the infinity query. unsuccessful HTTP response code\nstatus code : 401 Unauthorized", res.Error.Error())
 		})
 	})
 }
@@ -382,7 +407,7 @@ func TestResponseFormats(t *testing.T) {
 		t.Run("should parse the response and send results", func(t *testing.T) {
 			server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 				assert.Equal(t, http.MethodGet, r.Method)
-				fmt.Fprintf(w, `{ "foo" : "bar" }`)
+				_, _ = fmt.Fprintf(w, `{ "foo" : "bar" }`)
 			}))
 			defer server.Close()
 			client, err := infinity.NewClient(context.TODO(), models.InfinitySettings{URL: server.URL})
@@ -407,7 +432,7 @@ func TestResponseFormats(t *testing.T) {
 		t.Run("should parse the response and send results", func(t *testing.T) {
 			server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 				assert.Equal(t, http.MethodGet, r.Method)
-				fmt.Fprintf(w, `{
+				_, _ = fmt.Fprintf(w, `{
 					"channel": {
 					  "id": 38629,
 					  "name": "Traffic Monitor",
@@ -526,7 +551,7 @@ func TestResponseFormats(t *testing.T) {
 		t.Run("should parse the response and send results", func(t *testing.T) {
 			server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 				assert.Equal(t, http.MethodGet, r.Method)
-				fmt.Fprintf(w, `{ "foo" : "bar" }`)
+				_, _ = fmt.Fprintf(w, `{ "foo" : "bar" }`)
 			}))
 			defer server.Close()
 			client, err := infinity.NewClient(context.TODO(), models.InfinitySettings{URL: server.URL})
@@ -552,7 +577,7 @@ func TestResponseFormats(t *testing.T) {
 			server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 				assert.Equal(t, http.MethodGet, r.Method)
 				w.Header().Add("Content-Type", "text/csv")
-				fmt.Fprintf(w, "a,b\na1,b1")
+				_, _ = fmt.Fprintf(w, "a,b\na1,b1")
 			}))
 			defer server.Close()
 			client, err := infinity.NewClient(context.TODO(), models.InfinitySettings{URL: server.URL})
@@ -578,7 +603,7 @@ func TestResponseFormats(t *testing.T) {
 			server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 				assert.Equal(t, http.MethodGet, r.Method)
 				w.Header().Add("Content-Type", "text/xml")
-				fmt.Fprintf(w, `<xml><User name="foo"></xml>`)
+				_, _ = fmt.Fprintf(w, `<xml><User name="foo"></xml>`)
 			}))
 			defer server.Close()
 			client, err := infinity.NewClient(context.TODO(), models.InfinitySettings{URL: server.URL})
@@ -604,7 +629,7 @@ func TestResponseFormats(t *testing.T) {
 			server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 				assert.Equal(t, http.MethodGet, r.Method)
 				w.Header().Add("Content-Type", "application/json")
-				fmt.Fprintf(w, `{ "foo" : "bar" }`)
+				_, _ = fmt.Fprintf(w, `{ "foo" : "bar" }`)
 			}))
 			defer server.Close()
 			client, err := infinity.NewClient(context.TODO(), models.InfinitySettings{URL: server.URL})
@@ -630,7 +655,7 @@ func TestResponseFormats(t *testing.T) {
 			server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 				assert.Equal(t, http.MethodGet, r.Method)
 				w.Header().Add("Content-Type", "application/json")
-				fmt.Fprintf(w, `{ "foo" : "bar" }`)
+				_, _ = fmt.Fprintf(w, `{ "foo" : "bar" }`)
 			}))
 			defer server.Close()
 			client, err := infinity.NewClient(context.TODO(), models.InfinitySettings{URL: server.URL})