Files
troubleshoot/pkg/collect/http_test.go
ada mancini eacff7112f support adding a CA cert to http collector (#1624)
* add a TLS parameter for cacert

* pass a ca cert into http request

* test preflight

* make schemas

* log extra information from http request

* pass a proxy into the collector spec

* hitting a segfault; breakpoint

* accept a dir, file, or a string-literal as CA

* move tls params into get, put, post methods

* test for cert untrusted response

* make generate

* make schemas

* more test cases

* make schemas

* dont include system certs

* make generate && make schemas

* resolve gosec G402 warning

* remove old check for system certs

* ignore errcheck "return value not checked" linter errors
2024-10-23 18:15:08 -04:00

476 lines
12 KiB
Go

package collect
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
"github.com/stretchr/testify/assert"
)
type Headers struct {
ContentLength string `json:"Content-Length"`
ContentType string `json:"Content-Type"`
Date string `json:"Date,omitempty"`
}
type Response struct {
Status int `json:"status"`
Body string `json:"body"`
Headers Headers `json:"headers"`
}
type ResponseData struct {
Response Response `json:"response"`
}
type args struct {
progressChan chan<- interface{}
}
type CollectorTest struct {
name string
httpServer *http.Server
isHttps bool
Collector *troubleshootv1beta2.HTTP
args args
checkTimeout bool
want CollectorResult
wantErr bool
}
type ErrorResponse struct {
Error HTTPError `json:"error"`
}
func (r ResponseData) ToJSONbytes() ([]byte, error) {
return json.Marshal(r)
}
func (r ErrorResponse) ToJSONbytes() ([]byte, error) {
return json.Marshal(r)
}
func TestCollectHTTP_Collect(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/get", func(res http.ResponseWriter, req *http.Request) {
res.Header().Set("Content-Type", "application/json; charset=utf-8")
res.WriteHeader(http.StatusOK)
res.Write([]byte("{\"status\": \"healthy\"}"))
})
mux.HandleFunc("/post", func(res http.ResponseWriter, req *http.Request) {
res.Header().Set("Content-Type", "text/plain; charset=utf-8")
res.WriteHeader(http.StatusOK)
res.Write([]byte("Hello, POST!"))
})
mux.HandleFunc("/put", func(res http.ResponseWriter, req *http.Request) {
res.Header().Set("Content-Type", "text/plain; charset=utf-8")
res.WriteHeader(http.StatusOK)
res.Write([]byte("Hello, PUT!"))
})
mux.HandleFunc("/error", func(res http.ResponseWriter, req *http.Request) {
time.Sleep(1 * time.Millisecond)
fmt.Println("Sleeping for 2 seconds on /error call")
res.Header().Set("Content-Type", "application/json; charset=utf-8")
res.WriteHeader(http.StatusInternalServerError)
res.Write([]byte("{\"error\": { \"message\": \"context deadline exceeded\"}}"))
})
mux.HandleFunc("/certificate-mismatch", func(res http.ResponseWriter, req *http.Request) {
time.Sleep(1 * time.Millisecond)
fmt.Println("Sleeping for 2 seconds on /error call")
res.Header().Set("Content-Type", "application/json; charset=utf-8")
res.WriteHeader(http.StatusInternalServerError)
res.Write([]byte("{\"error\": { \"message\": \"Request failed: proxyconnect tcp: tls: failed to verify certificate: x509: \"10.0.0.254\" certificate is not trusted\"}}"))
})
sample_get_response := &ResponseData{
Response: Response{
Status: 200,
Body: "{\"status\": \"healthy\"}",
Headers: Headers{
ContentLength: "21",
ContentType: "application/json; charset=utf-8",
},
},
}
sample_get_bytes, _ := sample_get_response.ToJSONbytes()
sample_post_response := &ResponseData{
Response: Response{
Status: 200,
Body: "Hello, POST!",
Headers: Headers{
ContentLength: "12",
ContentType: "text/plain; charset=utf-8",
},
},
}
sample_post_bytes, _ := sample_post_response.ToJSONbytes()
sample_put_response := &ResponseData{
Response: Response{
Status: 200,
Body: "Hello, PUT!",
Headers: Headers{
ContentLength: "11",
ContentType: "text/plain; charset=utf-8",
},
},
}
sample_put_bytes, _ := sample_put_response.ToJSONbytes()
sample_error_response := &ErrorResponse{
Error: HTTPError{
Message: "context deadline exceeded",
},
}
sample_error_bytes, _ := sample_error_response.ToJSONbytes()
sample_certificate_untrusted := &ErrorResponse{
Error: HTTPError{
Message: "Request failed: proxyconnect tcp: tls: failed to verify certificate: x509: \"10.0.0.254\" certificate is not trusted",
},
}
sample_certificate_untrusted_bytes, _ := sample_certificate_untrusted.ToJSONbytes()
tests := []CollectorTest{
{
// check valid file path when CollectorName is not supplied
name: "GET: collector name unset",
Collector: &troubleshootv1beta2.HTTP{
CollectorMeta: troubleshootv1beta2.CollectorMeta{
CollectorName: "",
},
Get: &troubleshootv1beta2.Get{},
},
args: args{
progressChan: nil,
},
want: CollectorResult{
"result.json": sample_get_bytes,
},
checkTimeout: false,
wantErr: false,
isHttps: false,
},
{
// check valid file path when CollectorName is supplied
name: "GET: valid collect",
Collector: &troubleshootv1beta2.HTTP{
CollectorMeta: troubleshootv1beta2.CollectorMeta{
CollectorName: "example-com",
},
Get: &troubleshootv1beta2.Get{},
},
args: args{
progressChan: nil,
},
want: CollectorResult{
"example-com.json": sample_get_bytes,
},
checkTimeout: false,
wantErr: false,
isHttps: false,
},
{
// check valid file path when CollectorName is supplied
name: "POST: valid collect",
Collector: &troubleshootv1beta2.HTTP{
CollectorMeta: troubleshootv1beta2.CollectorMeta{
CollectorName: "example-com",
},
Post: &troubleshootv1beta2.Post{
InsecureSkipVerify: true,
Body: `{"id": 123, "name": "John Doe"}`,
Headers: map[string]string{"Content-Type": "application/json"},
},
},
args: args{
progressChan: nil,
},
want: CollectorResult{
"example-com.json": sample_post_bytes,
},
checkTimeout: false,
wantErr: false,
isHttps: false,
},
{
// check valid file path when CollectorName is supplied
name: "PUT: valid collect",
Collector: &troubleshootv1beta2.HTTP{
CollectorMeta: troubleshootv1beta2.CollectorMeta{
CollectorName: "example-com",
},
Put: &troubleshootv1beta2.Put{
Body: `{"id": 123, "name": "John Doe"}`,
Headers: map[string]string{"Content-Type": "application/json"},
},
},
args: args{
progressChan: nil,
},
want: CollectorResult{
"example-com.json": sample_put_bytes,
},
checkTimeout: false,
wantErr: false,
isHttps: false,
},
{
name: "ERROR: check request timeout < server delay (exit early)",
Collector: &troubleshootv1beta2.HTTP{
CollectorMeta: troubleshootv1beta2.CollectorMeta{
CollectorName: "example-com",
},
Get: &troubleshootv1beta2.Get{
Timeout: "200ns",
},
},
args: args{
progressChan: nil,
},
want: CollectorResult{
"example-com.json": sample_error_bytes,
},
checkTimeout: true,
wantErr: true,
},
{
name: "GET: check request timeout > server delay",
Collector: &troubleshootv1beta2.HTTP{
CollectorMeta: troubleshootv1beta2.CollectorMeta{
CollectorName: "",
},
Get: &troubleshootv1beta2.Get{
Timeout: "300ms",
},
},
args: args{
progressChan: nil,
},
want: CollectorResult{
"result.json": sample_get_bytes,
},
checkTimeout: true,
wantErr: false,
},
{
name: "TLS: certificate is not trusted",
Collector: &troubleshootv1beta2.HTTP{
CollectorMeta: troubleshootv1beta2.CollectorMeta{
CollectorName: "",
},
Get: &troubleshootv1beta2.Get{
Timeout: "300ms",
},
},
args: args{
progressChan: nil,
},
want: CollectorResult{
"result.json": sample_certificate_untrusted_bytes,
},
checkTimeout: true,
wantErr: true,
},
}
for _, tt := range tests {
var ts *httptest.Server
if tt.isHttps {
ts = httptest.NewTLSServer(mux)
} else {
ts = httptest.NewServer(mux)
}
url := ts.URL
defer ts.Close()
t.Run(tt.name, func(t *testing.T) {
c := &CollectHTTP{
Collector: tt.Collector,
}
switch {
case c.Collector.Get != nil:
if tt.checkTimeout && tt.wantErr {
c.Collector.Get.URL = fmt.Sprintf("%s%s", url, "/error")
response_data := sample_error_response
response_data.testCollectHTTP(t, &tt, c)
c.Collector.Get.URL = fmt.Sprintf("%s%s", url, "/certificate-mismatch")
response_data = sample_certificate_untrusted
response_data.testCollectHTTP(t, &tt, c)
} else {
c.Collector.Get.URL = fmt.Sprintf("%s%s", url, "/get")
response_data := sample_get_response
response_data.testCollectHTTP(t, &tt, c)
}
case c.Collector.Post != nil:
c.Collector.Post.URL = fmt.Sprintf("%s%s", url, "/post")
response_data := sample_post_response
response_data.testCollectHTTP(t, &tt, c)
case c.Collector.Put != nil:
c.Collector.Put.URL = fmt.Sprintf("%s%s", url, "/put")
response_data := sample_put_response
response_data.testCollectHTTP(t, &tt, c)
default:
t.Errorf("ERR: Method not supported")
}
})
}
}
func (rd *ResponseData) testCollectHTTP(t *testing.T, tt *CollectorTest, c *CollectHTTP) {
got, err := c.Collect(tt.args.progressChan)
if (err != nil) != tt.wantErr {
t.Errorf("CollectHTTP.Collect() error = %v, wantErr %v", err, tt.wantErr)
return
}
var expected_filename string
if c.Collector.CollectorName == "" {
expected_filename = "result.json"
} else {
expected_filename = c.Collector.CollectorName + ".json"
}
var resp ResponseData
if err := json.Unmarshal(got[expected_filename], &resp); err != nil {
t.Errorf("CollectHTTP.Collect() error = %v, wantErr %v", err, tt.wantErr)
return
}
// Correct format of the collected data (JSON data)
assert.Equal(t, rd.Response.Status, resp.Response.Status)
assert.Equal(t, rd.Response.Body, resp.Response.Body)
assert.Equal(t, rd.Response.Headers.ContentLength, resp.Response.Headers.ContentLength)
}
func (er *ErrorResponse) testCollectHTTP(t *testing.T, tt *CollectorTest, c *CollectHTTP) {
got, err := c.Collect(tt.args.progressChan)
if err != nil {
t.Errorf("CollectHTTP.Collect() error = %v, wantErr %v", err, tt.wantErr)
return
}
var expected_filename string
if c.Collector.CollectorName == "" {
expected_filename = "result.json"
} else {
expected_filename = c.Collector.CollectorName + ".json"
}
// assert er.Error.Message is not nil
assert.NotNil(t, er.Error.Message)
if err := json.Unmarshal(got[expected_filename], &er); err != nil {
t.Errorf("CollectHTTP.Collect() error = %v, wantErr %v", err, tt.wantErr)
return
}
if strings.Contains(strings.TrimSpace(er.Error.Message), "context deadline exceeded") != tt.wantErr {
t.Errorf("CollectHTTP.Collect() response = %v, wantErr %v", er.Error.Message, tt.wantErr)
}
}
func Test_parseTimeout(t *testing.T) {
tests := []struct {
name string
input string
want time.Duration
wantErr bool
}{
{
name: "1s timeout",
input: "1s",
want: time.Second,
},
{
name: "empty timeout",
input: "",
want: 0,
},
{
name: "negative timeout",
input: "-1s",
want: 0,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := parseTimeout(tt.input)
assert.Equal(t, (err != nil), tt.wantErr)
assert.Equal(t, got, tt.want)
})
}
}
func Test_responseToOutput(t *testing.T) {
tests := []struct {
name string
response *http.Response
err error
want []byte
wantErr bool
}{
{
name: "valid JSON response",
response: &http.Response{
Body: io.NopCloser(bytes.NewBufferString(`{"ok": false, "error": "invalid_auth"}`)),
Header: http.Header{"Content-Type": []string{"application/json; charset=utf-8"}},
StatusCode: http.StatusOK,
},
err: nil,
want: []byte(`
{
"response":
{
"status":200,
"body":"{\"ok\": false, \"error\": \"invalid_auth\"}",
"headers":{"Content-Type":"application/json; charset=utf-8"},
"raw_json":{"ok":false,"error":"invalid_auth"}
}
}`),
wantErr: false,
},
{
name: "invalid JSON response",
response: &http.Response{
Body: io.NopCloser(bytes.NewBufferString(`foobar`)),
Header: http.Header{"Content-Type": []string{"text/html; charset=utf-8"}},
StatusCode: http.StatusOK,
},
err: nil,
want: []byte(`
{
"response":
{
"status":200,
"body":"foobar",
"headers":{"Content-Type":"text/html; charset=utf-8"}
}
}`),
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := responseToOutput(tt.response, tt.err)
if tt.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assert.JSONEq(t, string(got), string(tt.want))
})
}
}