Repository: vespa-engine/vespa (language: java)

Issue:
Support for headers in system tests executed by vespa test
Hello Vespa team!

I have come across with the need of creating system tests with http requests with headers.

I would usually execute them with `vespa test` and a json file describing the test configuration.
However, it seems that http requests with headers are not supported with `vespa test` and I have found that the only way to test them are with Java API tests, as stated in https://docs.vespa.ai/en/testing.html.
The documentation suggests the Java API tests are for complex tests in mind, but I think just adding a header to an http request doesn't count as a complex situation at all and just adds too much complexity to simple system tests that just require adding an additional header.

Would it be possible to add the ability of executing http requests with headers for `vespa test` system tests?

What I had in mind was a configuration similar to this to execute an http request with headers:

```json
"steps": [
        {
            "name": "get documents",
            "request": {
                "method": "GET",
                "uri": "/document/v1",
                "parameters": {
                    "cluster": "music",
                    "selection": "true"
                },
                "headers": {
                    "context": "{\"key\": \"value\"}"       
                }
            }
        }
]
```

Thank you.

Kind regards,

Sergio Moreno López.



Discussion / hints:

Fix patch:
diff --git a/client/go/internal/cli/cmd/root.go b/client/go/internal/cli/cmd/root.go
index ff11a2e53a2f..1f324658b695 100644
--- a/client/go/internal/cli/cmd/root.go
+++ b/client/go/internal/cli/cmd/root.go
@@ -473,6 +473,9 @@ func (c *CLI) createCloudTarget(targetType string, opts targetOptions, customURL
 		// Only setup API authentication if we're using "cloud" target, and not a direct URL
 		if customURL == "" {
 			apiAuth, err = c.cloudApiAuthenticator(deployment, system)
+			if err != nil {
+				return nil, err
+			}
 		}
 		deploymentTLSOptions = vespa.TLSOptions{}
 		if !opts.noCertificate {


Test patch:
diff --git a/client/go/internal/cli/cmd/test.go b/client/go/internal/cli/cmd/test.go
index f0432dd4f704..376611767d96 100644
--- a/client/go/internal/cli/cmd/test.go
+++ b/client/go/internal/cli/cmd/test.go
@@ -198,6 +198,14 @@ func verify(step step, defaultCluster string, defaultParameters map[string]strin
 		method = "GET"
 	}
 
+	header := http.Header{}
+	for k, v := range step.Request.Headers {
+		header.Set(k, v)
+	}
+	if header.Get("Content-Type") == "" { // Set default if not specified by test
+		header.Set("Content-Type", "application/json")
+	}
+
 	var service *vespa.Service
 	requestUri := step.Request.URI
 	if requestUri == "" {
@@ -238,9 +246,6 @@ func verify(step step, defaultCluster string, defaultParameters map[string]strin
 	}
 	requestUrl.RawQuery = query.Encode()
 
-	header := http.Header{}
-	header.Add("Content-Type", "application/json") // TODO: Not guaranteed to be true ...
-
 	request := &http.Request{
 		URL:    requestUrl,
 		Method: method,
@@ -464,11 +469,12 @@ type step struct {
 }
 
 type request struct {
-	Cluster       string          `json:"cluster"`
-	Method        string          `json:"method"`
-	URI           string          `json:"uri"`
-	ParametersRaw json.RawMessage `json:"parameters"`
-	BodyRaw       json.RawMessage `json:"body"`
+	Cluster       string            `json:"cluster"`
+	Method        string            `json:"method"`
+	URI           string            `json:"uri"`
+	Headers       map[string]string `json:"headers"`
+	ParametersRaw json.RawMessage   `json:"parameters"`
+	BodyRaw       json.RawMessage   `json:"body"`
 }
 
 type response struct {
diff --git a/client/go/internal/cli/cmd/test_test.go b/client/go/internal/cli/cmd/test_test.go
index 9237221f0038..1888db017d49 100644
--- a/client/go/internal/cli/cmd/test_test.go
+++ b/client/go/internal/cli/cmd/test_test.go
@@ -40,8 +40,8 @@ func TestSuite(t *testing.T) {
 	assert.Equal(t, "", stderr.String())
 	baseUrl := "http://127.0.0.1:8080"
 	urlWithQuery := baseUrl + "/search/?presentation.timing=true&query=artist%3A+foo&timeout=3.4s"
-	discoveryRequest := createSearchRequest("http://127.0.0.1:19071/application/v2/tenant/default/application/default/environment/prod/region/default/instance/default/serviceconverge")
-	requests := []*http.Request{discoveryRequest, createFeedRequest(baseUrl), createFeedRequest(baseUrl), createSearchRequest(urlWithQuery), createSearchRequest(urlWithQuery)}
+	discoveryRequest := createDiscoveryRequest()
+	requests := []*http.Request{discoveryRequest, createFeedRequest(baseUrl), createFeedRequest(baseUrl), createSearchRequest(urlWithQuery), createRequestWithCustomHeader(urlWithQuery)}
 	requests = append(requests, discoveryRequest)
 	requests = append(requests, createSearchRequest(baseUrl+"/search/"))
 	requests = append(requests, createSearchRequest(baseUrl+"/search/?foo=%2F"))
@@ -116,8 +116,8 @@ func TestSingleTest(t *testing.T) {
 
 	baseUrl := "http://127.0.0.1:8080"
 	rawUrl := baseUrl + "/search/?presentation.timing=true&query=artist%3A+foo&timeout=3.4s"
-	discoveryRequest := createSearchRequest("http://127.0.0.1:19071/application/v2/tenant/default/application/default/environment/prod/region/default/instance/default/serviceconverge")
-	assertRequests([]*http.Request{discoveryRequest, createFeedRequest(baseUrl), createFeedRequest(baseUrl), createSearchRequest(rawUrl), createSearchRequest(rawUrl)}, client, t)
+	discoveryRequest := createDiscoveryRequest()
+	assertRequests([]*http.Request{discoveryRequest, createFeedRequest(baseUrl), createFeedRequest(baseUrl), createSearchRequest(rawUrl), createRequestWithCustomHeader(rawUrl)}, client, t)
 }
 
 func TestSingleTestWithCloudAndEndpoints(t *testing.T) {
@@ -156,7 +156,7 @@ func TestSingleTestWithCloudAndEndpoints(t *testing.T) {
 
 	baseUrl := "https://url"
 	rawUrl := baseUrl + "/search/?presentation.timing=true&query=artist%3A+foo&timeout=3.4s"
-	assertRequests([]*http.Request{createFeedRequest(baseUrl), createFeedRequest(baseUrl), createSearchRequest(rawUrl), createSearchRequest(rawUrl)}, client, t)
+	assertRequests([]*http.Request{createFeedRequest(baseUrl), createFeedRequest(baseUrl), createSearchRequest(rawUrl), createRequestWithCustomHeader(rawUrl)}, client, t)
 }
 
 func createFeedRequest(urlPrefix string) *http.Request {
@@ -171,26 +171,40 @@ func createSearchRequest(rawUrl string) *http.Request {
 
 func createRequest(method string, uri string, body string) *http.Request {
 	requestUrl, _ := url.ParseRequestURI(uri)
-	return &http.Request{
+	r := &http.Request{
 		URL:    requestUrl,
 		Method: method,
-		Header: nil,
+		Header: http.Header{},
 		Body:   io.NopCloser(strings.NewReader(body)),
 	}
+	r.Header.Set("Content-Type", "application/json")
+	return r
+}
+
+func createRequestWithCustomHeader(url string) *http.Request {
+	r := createSearchRequest(url)
+	r.Header.Set("X-Foo", "bar")
+	return r
+}
+
+func createDiscoveryRequest() *http.Request {
+	r := createSearchRequest("http://127.0.0.1:19071/application/v2/tenant/default/application/default/environment/prod/region/default/instance/default/serviceconverge")
+	r.Header = http.Header{}
+	return r
 }
 
 func assertRequests(requests []*http.Request, client *mock.HTTPClient, t *testing.T) {
 	t.Helper()
-	if assert.Equal(t, len(requests), len(client.Requests)) {
-		for i, e := range requests {
-			a := client.Requests[i]
-			assert.Equal(t, e.URL.String(), a.URL.String())
-			assert.Equal(t, e.Method, a.Method)
-			actualBody := a.Body
-			if actualBody == nil {
-				actualBody = io.NopCloser(strings.NewReader(""))
-			}
-			assert.Equal(t, util.ReaderToJSON(e.Body), util.ReaderToJSON(actualBody))
+	require.Equal(t, len(requests), len(client.Requests))
+	for i, want := range requests {
+		got := client.Requests[i]
+		assert.Equal(t, want.URL.String(), got.URL.String())
+		assert.Equal(t, want.Method, got.Method)
+		assert.Equal(t, want.Header, got.Header)
+		actualBody := got.Body
+		if actualBody == nil {
+			actualBody = io.NopCloser(strings.NewReader(""))
 		}
+		assert.Equal(t, util.ReaderToJSON(want.Body), util.ReaderToJSON(actualBody))
 	}
 }
diff --git a/client/go/internal/cli/cmd/testdata/tests/system-test/test.json b/client/go/internal/cli/cmd/testdata/tests/system-test/test.json
index 2e327b5e5dfb..b338c111d2cd 100644
--- a/client/go/internal/cli/cmd/testdata/tests/system-test/test.json
+++ b/client/go/internal/cli/cmd/testdata/tests/system-test/test.json
@@ -19,7 +19,7 @@
             "name": "re-feed music",
             "request": {
                 "method": "POST",
-                "body": { 
+                "body": {
                     "fields": {
                         "artist": "Foo Fighters"
                     }
@@ -44,7 +44,10 @@
             "name": "query for foo again",
             "request": {
                 "uri": "/search/?presentation.timing=true",
-                "parameters": "foo/query.json"
+                "parameters": "foo/query.json",
+                "headers": {
+                    "X-Foo": "bar"
+                }
             },
             "response": {
                 "body": {