From 190a743b053a14ddfa2efa91253b5c87b18c6d6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20B=C3=A4umer?= Date: Mon, 28 Jan 2019 11:19:07 +0100 Subject: [PATCH 1/3] Add header validation in http rescource --- .gitignore | 1 + add.go | 13 ++ add_test.go | 21 +++ cmd/goss/goss.go | 4 + development/http_test_server.go | 25 ++++ docs/manual.md | 4 + .../goss/alpine3/goss-expected-q.yaml | 6 + .../goss/alpine3/goss-expected.yaml | 6 + .../goss/centos7/goss-expected-q.yaml | 6 + .../goss/centos7/goss-expected.yaml | 6 + integration-tests/goss/generate_goss.sh | 4 +- .../goss/precise/goss-expected-q.yaml | 6 + .../goss/precise/goss-expected.yaml | 6 + .../goss/wheezy/goss-expected-q.yaml | 6 + .../goss/wheezy/goss-expected.yaml | 6 + integration-tests/test.sh | 2 +- resource/gomega_test.go | 5 +- resource/http.go | 137 ++++++++++++++++-- resource/http_test.go | 136 +++++++++++++++++ resource/resource_list.go | 19 ++- resource/resource_list_test.go | 28 ++++ resource/validate.go | 1 + resource/validate_test.go | 1 + system/http.go | 17 +++ system/http_test.go | 59 ++++++++ system/mock_system/system_http_mock.go | 133 +++++++++++++++++ system/mock_system/system_system_mock.go | 48 ++++++ util/config.go | 1 + 28 files changed, 683 insertions(+), 24 deletions(-) create mode 100644 add_test.go create mode 100644 development/http_test_server.go create mode 100644 resource/http_test.go create mode 100644 resource/resource_list_test.go create mode 100644 system/http_test.go create mode 100644 system/mock_system/system_http_mock.go create mode 100644 system/mock_system/system_system_mock.go diff --git a/.gitignore b/.gitignore index 7b7b5dc..d756025 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ /vendor/ /integration-tests/**/goss-linux-386 /integration-tests/**/goss-linux-amd64 +development/http_test_server # Random stuff for my local testing/development that I don't want checked in tmp/ diff --git a/add.go b/add.go index 169c8a1..6fe12b1 100644 --- a/add.go +++ b/add.go @@ -15,6 +15,8 @@ import ( // Simple wrapper to add multiple resources func AddResources(fileName, resourceName string, keys []string, c *cli.Context) error { OutStoreFormat = getStoreFormatFromFileName(fileName) + header := extractHeaderArgument(c.String("header")) + config := util.Config{ IgnoreList: c.GlobalStringSlice("exclude-attr"), Timeout: int(c.Duration("timeout") / time.Millisecond), @@ -23,6 +25,7 @@ func AddResources(fileName, resourceName string, keys []string, c *cli.Context) Server: c.String("server"), Username: c.String("username"), Password: c.String("password"), + Header: header, } var gossConfig GossConfig @@ -44,6 +47,16 @@ func AddResources(fileName, resourceName string, keys []string, c *cli.Context) return nil } +func extractHeaderArgument(headerArg string) map[string][]string { + if headerArg == "" { + return make(map[string][]string) + } + rawHeaders := strings.Split(headerArg, ":") + headers := make(map[string][]string) + headers[rawHeaders[0]] = []string{strings.TrimSpace(rawHeaders[1])} + return headers +} + func AddResource(fileName string, gossConfig GossConfig, resourceName, key string, c *cli.Context, config util.Config, sys *system.System) error { // Need to figure out a good way to refactor this switch resourceName { diff --git a/add_test.go b/add_test.go new file mode 100644 index 0000000..5f062a2 --- /dev/null +++ b/add_test.go @@ -0,0 +1,21 @@ +package goss + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func Test_ExtractHeaderArgument(t *testing.T) { + expected := make(map[string][]string) + expected["Set-Cookie"] = []string{"something"} + + headerArgs := "Set-Cookie: something" + got := extractHeaderArgument(headerArgs) + + assert.Equal(t, expected, got) +} + +func Test_ExtractHeaderArgumentWithoutEmptyArg(t *testing.T) { + got := extractHeaderArgument("") + assert.Equal(t, make(map[string][]string), got) +} \ No newline at end of file diff --git a/cmd/goss/goss.go b/cmd/goss/goss.go index bcece80..23408fb 100644 --- a/cmd/goss/goss.go +++ b/cmd/goss/goss.go @@ -293,6 +293,10 @@ func main() { Name: "password, p", Usage: "Password for basic auth", }, + cli.StringFlag{ + Name: "header", + Usage: "Set-Cookie: Value", + }, }, Action: func(c *cli.Context) error { goss.AddResources(c.GlobalString("gossfile"), "HTTP", c.Args(), c) diff --git a/development/http_test_server.go b/development/http_test_server.go new file mode 100644 index 0000000..ea2bb70 --- /dev/null +++ b/development/http_test_server.go @@ -0,0 +1,25 @@ +package main + +import ( + "fmt" + "log" + "net/http" +) + +func handler(w http.ResponseWriter, r *http.Request) { + for k, v := range r.Header { + fmt.Println("key:", k, "val:", v) + } + + w.Header().Add("key", "value") + w.Header().Add("key", "value") +} + +func main() { + http.HandleFunc("/", handler) + err := http.ListenAndServe(":9090", nil) + + if err != nil { + log.Fatal("ListenAndServe: ", err) + } +} \ No newline at end of file diff --git a/docs/manual.md b/docs/manual.md index 728f719..24c96b3 100644 --- a/docs/manual.md +++ b/docs/manual.md @@ -578,6 +578,10 @@ http: body: [] # Check http response content for these patterns username: "" # username for basic auth password: "" # password for basic auth + headers: # Check for http headers, is not support for add command + key: + - value + - another value ``` diff --git a/integration-tests/goss/alpine3/goss-expected-q.yaml b/integration-tests/goss/alpine3/goss-expected-q.yaml index 7497a23..6e512b0 100644 --- a/integration-tests/goss/alpine3/goss-expected-q.yaml +++ b/integration-tests/goss/alpine3/goss-expected-q.yaml @@ -108,9 +108,15 @@ http: no-follow-redirects: true timeout: 5000 body: [] + headers: + Server: + - gws https://www.google.com: status: 200 allow-insecure: false no-follow-redirects: false timeout: 5000 body: [] + headers: + Server: + - gws \ No newline at end of file diff --git a/integration-tests/goss/alpine3/goss-expected.yaml b/integration-tests/goss/alpine3/goss-expected.yaml index 9d937a2..65fcadd 100644 --- a/integration-tests/goss/alpine3/goss-expected.yaml +++ b/integration-tests/goss/alpine3/goss-expected.yaml @@ -151,9 +151,15 @@ http: no-follow-redirects: true timeout: 5000 body: [] + headers: + Server: + - gws https://www.google.com: status: 200 allow-insecure: false no-follow-redirects: false timeout: 5000 body: [] + headers: + Server: + - gws \ No newline at end of file diff --git a/integration-tests/goss/centos7/goss-expected-q.yaml b/integration-tests/goss/centos7/goss-expected-q.yaml index 8cc9f32..4cbdd25 100644 --- a/integration-tests/goss/centos7/goss-expected-q.yaml +++ b/integration-tests/goss/centos7/goss-expected-q.yaml @@ -108,9 +108,15 @@ http: no-follow-redirects: true timeout: 5000 body: [] + headers: + Server: + - gws https://www.google.com: status: 200 allow-insecure: false no-follow-redirects: false timeout: 5000 body: [] + headers: + Server: + - gws \ No newline at end of file diff --git a/integration-tests/goss/centos7/goss-expected.yaml b/integration-tests/goss/centos7/goss-expected.yaml index f510a9e..373c6da 100644 --- a/integration-tests/goss/centos7/goss-expected.yaml +++ b/integration-tests/goss/centos7/goss-expected.yaml @@ -157,9 +157,15 @@ http: no-follow-redirects: true timeout: 5000 body: [] + headers: + Server: + - gws https://www.google.com: status: 200 allow-insecure: false no-follow-redirects: false timeout: 5000 body: [] + headers: + Server: + - gws \ No newline at end of file diff --git a/integration-tests/goss/generate_goss.sh b/integration-tests/goss/generate_goss.sh index e587741..fa37c9b 100755 --- a/integration-tests/goss/generate_goss.sh +++ b/integration-tests/goss/generate_goss.sh @@ -58,9 +58,9 @@ goss a "${args[@]}" kernel-param kernel.ostype goss a "${args[@]}" mount /dev -goss a "${args[@]}" http https://www.google.com +goss a "${args[@]}" http https://www.google.com --header "Server: gws" -goss a "${args[@]}" http http://google.com -r +goss a "${args[@]}" http http://google.com -r --header "Server: gws" # Auto-add # Validate that empty configs don't get created diff --git a/integration-tests/goss/precise/goss-expected-q.yaml b/integration-tests/goss/precise/goss-expected-q.yaml index 4b65adb..71e59dc 100644 --- a/integration-tests/goss/precise/goss-expected-q.yaml +++ b/integration-tests/goss/precise/goss-expected-q.yaml @@ -108,9 +108,15 @@ http: no-follow-redirects: true timeout: 5000 body: [] + headers: + Server: + - gws https://www.google.com: status: 200 allow-insecure: false no-follow-redirects: false timeout: 5000 body: [] + headers: + Server: + - gws diff --git a/integration-tests/goss/precise/goss-expected.yaml b/integration-tests/goss/precise/goss-expected.yaml index 9d02dbb..e9a44c2 100644 --- a/integration-tests/goss/precise/goss-expected.yaml +++ b/integration-tests/goss/precise/goss-expected.yaml @@ -157,9 +157,15 @@ http: no-follow-redirects: true timeout: 5000 body: [] + headers: + Server: + - gws https://www.google.com: status: 200 allow-insecure: false no-follow-redirects: false timeout: 5000 body: [] + headers: + Server: + - gws \ No newline at end of file diff --git a/integration-tests/goss/wheezy/goss-expected-q.yaml b/integration-tests/goss/wheezy/goss-expected-q.yaml index 2912d85..865979f 100644 --- a/integration-tests/goss/wheezy/goss-expected-q.yaml +++ b/integration-tests/goss/wheezy/goss-expected-q.yaml @@ -108,9 +108,15 @@ http: no-follow-redirects: true timeout: 5000 body: [] + headers: + Server: + - gws https://www.google.com: status: 200 allow-insecure: false no-follow-redirects: false timeout: 5000 body: [] + headers: + Server: + - gws diff --git a/integration-tests/goss/wheezy/goss-expected.yaml b/integration-tests/goss/wheezy/goss-expected.yaml index 8b2b3f1..fc80f9b 100644 --- a/integration-tests/goss/wheezy/goss-expected.yaml +++ b/integration-tests/goss/wheezy/goss-expected.yaml @@ -157,9 +157,15 @@ http: no-follow-redirects: true timeout: 5000 body: [] + headers: + Server: + - gws https://www.google.com: status: 200 allow-insecure: false no-follow-redirects: false timeout: 5000 body: [] + headers: + Server: + - gws \ No newline at end of file diff --git a/integration-tests/test.sh b/integration-tests/test.sh index 38114f6..d303c76 100755 --- a/integration-tests/test.sh +++ b/integration-tests/test.sh @@ -17,7 +17,7 @@ seccomp_opts() { cp "../release/goss-linux-$arch" "goss/$os/" # Run build if Dockerfile has changed but hasn't been pushed to dockerhub if ! md5sum -c "Dockerfile_${os}.md5"; then - docker build -t "simonbaeumer/goss_${os}:latest" - < "Dockerfile_$os" + docker build -q -t "simonbaeumer/goss_${os}:latest" - < "Dockerfile_$os" # Pull if image doesn't exist locally elif ! docker images | grep "SimonBaeumer/goss_$os";then docker pull "simonbaeumer/goss_$os" diff --git a/resource/gomega_test.go b/resource/gomega_test.go index 15527aa..3b098fc 100644 --- a/resource/gomega_test.go +++ b/resource/gomega_test.go @@ -2,7 +2,6 @@ package resource import ( "encoding/json" - "fmt" "reflect" "regexp" "testing" @@ -163,8 +162,8 @@ func gomegaEqual(g, w interface{}, negateTester bool) bool { } gotMessage = sanitizeMatcherText(gotMessage) wantMessage = sanitizeMatcherText(wantMessage) - fmt.Println("got:", gotMessage) - fmt.Println("want:", wantMessage) + //fmt.Println("got:", gotMessage) + //fmt.Println("want:", wantMessage) return gotT == wantT && gotMessage == wantMessage diff --git a/resource/http.go b/resource/http.go index 964d870..0744b1a 100644 --- a/resource/http.go +++ b/resource/http.go @@ -3,36 +3,46 @@ package resource import ( "github.com/SimonBaeumer/goss/system" "github.com/SimonBaeumer/goss/util" + "reflect" + "strings" + "time" ) +const TimeoutMS = 5000 + type HTTP struct { - Title string `json:"title,omitempty" yaml:"title,omitempty"` - Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"` - HTTP string `json:"-" yaml:"-"` - Status matcher `json:"status" yaml:"status"` - AllowInsecure bool `json:"allow-insecure" yaml:"allow-insecure"` - NoFollowRedirects bool `json:"no-follow-redirects" yaml:"no-follow-redirects"` - Timeout int `json:"timeout" yaml:"timeout"` - Body []string `json:"body" yaml:"body"` - Username string `json:"username,omitempty" yaml:"username,omitempty"` - Password string `json:"password,omitempty" yaml:"password,omitempty"` + Title string `json:"title,omitempty" yaml:"title,omitempty"` + Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"` + HTTP string `json:"-" yaml:"-"` + Status matcher `json:"status" yaml:"status"` + AllowInsecure bool `json:"allow-insecure" yaml:"allow-insecure"` + NoFollowRedirects bool `json:"no-follow-redirects" yaml:"no-follow-redirects"` + Timeout int `json:"timeout" yaml:"timeout"` + Body []string `json:"body" yaml:"body"` + Username string `json:"username,omitempty" yaml:"username,omitempty"` + Password string `json:"password,omitempty" yaml:"password,omitempty"` + Headers map[string][]string `json:"headers,omitempty" yaml:"headers,omitempty"` } func (u *HTTP) ID() string { return u.HTTP } func (u *HTTP) SetID(id string) { u.HTTP = id } -// FIXME: Can this be refactored? func (r *HTTP) GetTitle() string { return r.Title } func (r *HTTP) GetMeta() meta { return r.Meta } func (u *HTTP) Validate(sys *system.System) []TestResult { skip := false - if u.Timeout == 0 { - u.Timeout = 5000 - } - sysHTTP := sys.NewHTTP(u.HTTP, sys, util.Config{ + + conf := util.Config{ AllowInsecure: u.AllowInsecure, NoFollowRedirects: u.NoFollowRedirects, - Timeout: u.Timeout, Username: u.Username, Password: u.Password}) + Timeout: u.Timeout, Username: u.Username, Password: u.Password} + + sysHTTP := sys.NewHTTP( + u.HTTP, + sys, + conf, + ) + sysHTTP.SetAllowInsecure(u.AllowInsecure) sysHTTP.SetNoFollowRedirects(u.NoFollowRedirects) @@ -45,12 +55,22 @@ func (u *HTTP) Validate(sys *system.System) []TestResult { results = append(results, ValidateContains(u, "Body", u.Body, sysHTTP.Body, skip)) } + if len(u.Headers) > 0 { + headers, _ := sysHTTP.Headers() + results = append(results, validateHeader(u, "Headers", u.Headers, headers, skip)) + } + return results } func NewHTTP(sysHTTP system.HTTP, config util.Config) (*HTTP, error) { + if config.Timeout == 0 { + config.Timeout = TimeoutMS + } + http := sysHTTP.HTTP() status, err := sysHTTP.Status() + headers, _ := sysHTTP.Headers() u := &HTTP{ HTTP: http, Status: status, @@ -60,6 +80,91 @@ func NewHTTP(sysHTTP system.HTTP, config util.Config) (*HTTP, error) { Timeout: config.Timeout, Username: config.Username, Password: config.Password, + Headers: headers, } return u, err } + + +func validateHeader(res ResourceRead, property string, expectedHeaders map[string][]string, actualHeaders system.Header, skip bool) TestResult { + id := res.ID() + title := res.GetTitle() + meta := res.GetMeta() + typ := reflect.TypeOf(res) + typeS := strings.Split(typ.String(), ".")[1] + startTime := time.Now() + if skip { + return skipResult( + typeS, + Values, + id, + title, + meta, + property, + startTime, + ) + } + + var actualString string + for k, values := range actualHeaders { + for _, v := range values { + actualString += k + ": " + v + "\n" + } + } + + for expectedKey, expectedValues := range expectedHeaders { + if _, ok := actualHeaders[expectedKey]; !ok { + return TestResult{ + Successful: false, + Result: FAIL, + Title: title, + ResourceType: typeS, + ResourceId: id, + TestType: Header, + Property: property, + Err: nil, + Human: "Did not find header " + expectedKey + " got \n" + actualString, + Expected: []string{expectedKey}, + Found: []string{actualString}, + } + } + + actualValues := actualHeaders[expectedKey] + + for _, expectedValue := range expectedValues { + if !isInStringSlice(actualValues, expectedValue) { + return TestResult{ + Successful: false, + Result: FAIL, + ResourceType: typeS, + ResourceId: id, + TestType: Header, + Title: title, + Property: property, + Err: nil, + Human: "Did not find header " + expectedKey + ": " + expectedValue, + Expected: []string{expectedValue}, + Found: []string{actualString}, + } + } + } + } + + return TestResult{ + Successful: true, + Title: title, + ResourceId: id, + Result: SUCCESS, + ResourceType: typeS, + TestType: Header, + } +} + +func isInStringSlice(haystack []string, needle string) bool { + for _, value := range haystack { + if needle == value { + return true + } + } + return false +} \ No newline at end of file diff --git a/resource/http_test.go b/resource/http_test.go new file mode 100644 index 0000000..4674ce7 --- /dev/null +++ b/resource/http_test.go @@ -0,0 +1,136 @@ +package resource + +import ( + "github.com/SimonBaeumer/goss/system" + "github.com/SimonBaeumer/goss/system/mock_system" + "github.com/SimonBaeumer/goss/util" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" + "testing" +) + +const DoNotSkip = false +const ID = "" + +func TestNewHTTP(t *testing.T) { + ctrl := gomock.NewController(t) + + mockSystemHTTP := mock_system.NewMockHTTP(ctrl) + mockSystemHTTP.EXPECT().HTTP().Return("http://goss.rocks") + mockSystemHTTP.EXPECT().Status().Return(200, nil) + mockSystemHTTP.EXPECT().Headers().Return(system.Header{}, nil) + + got, _ := NewHTTP(mockSystemHTTP, util.Config{}) + + assert.IsType(t, new(HTTP), got) + assert.Equal(t, "http://goss.rocks", got.HTTP) +} + +func Test_validateHeader_ShouldFindKey(t *testing.T) { + fakeRes := &FakeResource{ID} + expected := make(map[string][]string) + expected["key"] = []string{"value"} + + actual := system.Header{} + actual["key"] = []string{"value"} + + got := validateHeader(fakeRes, "Headers", expected, actual, DoNotSkip) + + assert.IsType(t, new(TestResult), &got) + assert.Equal(t, SUCCESS, got.Result) + assert.True(t, got.Successful) +} + +func Test_validateHeader_ShouldNotFindKey(t *testing.T) { + fakeRes := &FakeResource{ID} + expected := make(map[string][]string) + expected["key"] = []string{"value"} + + actual := system.Header{} + actual["another"] = []string{"value"} + + got := validateHeader(fakeRes, "Headers", expected, actual, DoNotSkip) + + assert.IsType(t, new(TestResult), &got) + assert.Equal(t, FAIL, got.Result) + assert.False(t, got.Successful) + assert.Equal(t, "Did not find header key got \nanother: value\n", got.Human) +} + +func Test_validateHeader_ShouldFindValue(t *testing.T) { + fakeRes := &FakeResource{ID} + expected := make(map[string][]string) + expected["key"] = []string{"value", "value1"} + expected["another"] = []string{"value"} + + actual := make(system.Header) + actual["key"] = []string{"value", "value1"} + actual["another"] = []string{"value"} + + got := validateHeader(fakeRes, "Header", expected, actual, DoNotSkip) + + assert.Equal(t, SUCCESS, got.Result) + assert.True(t, got.Successful) +} + +func Test_validateHeader_ShouldNotFindValue(t *testing.T) { + fakeRes := &FakeResource{ID} + expected := make(map[string][]string) + expected["key"] = []string{"does not match"} + + actual := make(system.Header) + actual["key"] = []string{"value"} + + got := validateHeader(fakeRes, "Header", expected, actual, DoNotSkip) + + assert.Equal(t, FAIL, got.Result) + assert.False(t, got.Successful) + assert.Equal(t, "Did not find header key: does not match", got.Human) +} + +func Test_validateHeader_ShouldNotFindValue_GivenMultiple(t *testing.T) { + fakeRes := &FakeResource{ID} + expected := make(map[string][]string) + expected["key"] = []string{"value", "does not match"} + + actual := make(system.Header) + actual["key"] = []string{"value"} + + got := validateHeader(fakeRes, "Header", expected, actual, DoNotSkip) + + assert.Equal(t, FAIL, got.Result) + assert.False(t, got.Successful) + assert.Equal(t, "Did not find header key: does not match", got.Human) +} + +func Test_validateHeader_ShouldFindValuesInComplex(t *testing.T) { + fakeRes := &FakeResource{ID} + expected := make(map[string][]string) + expected["key"] = []string{"value0"} + expected["another"] = []string{"value1", "value2", "value3"} + expected["yet"] = []string{"my", "my1", "my2"} + + actual := make(system.Header) + actual["key"] = []string{"value0"} + actual["another"] = []string{"value1", "value2", "value3"} + actual["yet"] = []string{"my", "my1", "my2"} + + got := validateHeader(fakeRes, "Header", expected, actual, DoNotSkip) + + assert.Equal(t, SUCCESS, got.Result) + assert.True(t, got.Successful) +} + +func Test_isInSlice(t *testing.T) { + haystack := []string{"apple", "banana"} + + got := isInStringSlice(haystack, "banana") + assert.True(t, got) +} + +func Test_isNotInSlice(t *testing.T) { + haystack := []string{"apple", "banana"} + + got := isInStringSlice(haystack, "pear") + assert.False(t, got) +} \ No newline at end of file diff --git a/resource/resource_list.go b/resource/resource_list.go index b31b82e..01f0869 100644 --- a/resource/resource_list.go +++ b/resource/resource_list.go @@ -18,6 +18,7 @@ import ( //go:generate goimports -w resource_list.go resource_list.go type AddrMap map[string]*Addr +var BlacklistedAutoAddHeaders = [...]string{"Set-Cookie", "set-cookie", "Date", "date"} func (r AddrMap) AppendSysResource(sr string, sys *system.System, config util.Config) (*Addr, error) { sysres := sys.NewAddr(sr, sys, config) @@ -1419,20 +1420,34 @@ func (ret *InterfaceMap) UnmarshalYAML(unmarshal func(v interface{}) error) erro type HTTPMap map[string]*HTTP -func (r HTTPMap) AppendSysResource(sr string, sys *system.System, config util.Config) (*HTTP, error) { - sysres := sys.NewHTTP(sr, sys, config) +func (r HTTPMap) AppendSysResource(src string, sys *system.System, config util.Config) (*HTTP, error) { + sysres := sys.NewHTTP(src, sys, config) res, err := NewHTTP(sysres, config) if err != nil { return nil, err } + + // Remove headers because of the instability of the generated data + res.Headers = config.Header + if old_res, ok := r[res.ID()]; ok { res.Title = old_res.Title res.Meta = old_res.Meta } r[res.ID()] = res + return res, nil } +func (r HTTPMap) isBlacklistedHeader(key string) bool { + for _, listedKey := range BlacklistedAutoAddHeaders { + if listedKey == key { + return true + } + } + return false +} + func (r HTTPMap) AppendSysResourceIfExists(sr string, sys *system.System) (*HTTP, system.HTTP, bool) { sysres := sys.NewHTTP(sr, sys, util.Config{}) // FIXME: Do we want to be silent about errors? diff --git a/resource/resource_list_test.go b/resource/resource_list_test.go new file mode 100644 index 0000000..1b3db4a --- /dev/null +++ b/resource/resource_list_test.go @@ -0,0 +1,28 @@ +package resource + +import ( + "github.com/SimonBaeumer/goss/system" + "github.com/SimonBaeumer/goss/util" + "github.com/stretchr/testify/assert" + "testing" +) + +const SuccessStatusCode = 200 + + +func TestAddrMap_AppendSysResource(t *testing.T) { + conf := util.Config{} + systemMock := &system.System{ + NewHTTP: func(src string, sys *system.System, config util.Config) system.HTTP { + return system.NewDefHTTP("http://goss.rocks", nil, conf) + }, + } + httpMap := HTTPMap{} + + got, err := httpMap.AppendSysResource("http://goss.rocks", systemMock, conf) + + assert.Nil(t, err) + assert.Equal(t, "http://goss.rocks", got.HTTP) + assert.Equal(t, SuccessStatusCode, got.Status) + assert.Empty(t, got.Headers) +} diff --git a/resource/validate.go b/resource/validate.go index e01f5de..ce68bb5 100644 --- a/resource/validate.go +++ b/resource/validate.go @@ -17,6 +17,7 @@ const ( Value = iota Values Contains + Header ) const ( diff --git a/resource/validate_test.go b/resource/validate_test.go index efa348f..bcbd25f 100644 --- a/resource/validate_test.go +++ b/resource/validate_test.go @@ -139,3 +139,4 @@ func TestValidateContainsSkip(t *testing.T) { } } } + diff --git a/system/http.go b/system/http.go index 17c40f7..2f4be22 100644 --- a/system/http.go +++ b/system/http.go @@ -8,6 +8,8 @@ import ( "github.com/SimonBaeumer/goss/util" ) +type Header map[string][]string + type HTTP interface { HTTP() string Status() (int, error) @@ -15,6 +17,7 @@ type HTTP interface { Exists() (bool, error) SetAllowInsecure(bool) SetNoFollowRedirects(bool) + Headers() (Header, error) } type DefHTTP struct { @@ -40,6 +43,7 @@ func NewDefHTTP(http string, system *System, config util.Config) HTTP { } } +//The setup method configures the http client and sends the request. func (u *DefHTTP) setup() error { if u.loaded { return u.err @@ -110,3 +114,16 @@ func (u *DefHTTP) Body() (io.Reader, error) { return u.resp.Body, nil } + +func (u *DefHTTP) Headers() (Header, error) { + if err := u.setup(); err != nil { + return nil, err + } + + headers := make(Header) + for k, v := range u.resp.Header { + headers[k] = v + } + + return headers, nil +} \ No newline at end of file diff --git a/system/http_test.go b/system/http_test.go new file mode 100644 index 0000000..59bfc8f --- /dev/null +++ b/system/http_test.go @@ -0,0 +1,59 @@ +package system + +import ( + "github.com/SimonBaeumer/goss/util" + "github.com/stretchr/testify/assert" + "io" + "testing" +) + +const SuccessStatusCode = 200 + +func newTestingDefHTTP() HTTP { + return NewDefHTTP( + "http://goss.rocks", + &System{}, + util.Config{}, + ) +} + +func Test_NewDefHTTP(t *testing.T) { + system := &System{} + conf := util.Config{} + + got := NewDefHTTP( + "http://goss.rocks", + system, + conf, + ) + + assert.Implements(t, (*HTTP)(nil), got, "DefHTTP does not implement HTTP") + assert.Equal(t, got.HTTP(), "http://goss.rocks") +} + +func TestDefHTTP_Body(t *testing.T) { + http := newTestingDefHTTP() + + got, err := http.Body() + + assert.Nil(t, err) + assert.Implements(t, new(io.Reader), got) +} + +func TestDefHTTP_Status(t *testing.T) { + http := newTestingDefHTTP() + + got, err := http.Status() + + assert.Nil(t, err) + assert.Equal(t, SuccessStatusCode, got) +} + +func TestDefHTTP_Headers(t *testing.T) { + defHTTP := newTestingDefHTTP() + + got, err := defHTTP.Headers() + + assert.Nil(t, err) + assert.IsType(t, make(Header), got) +} diff --git a/system/mock_system/system_http_mock.go b/system/mock_system/system_http_mock.go new file mode 100644 index 0000000..8c3a6f8 --- /dev/null +++ b/system/mock_system/system_http_mock.go @@ -0,0 +1,133 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: http.go + +// Package mock_system is a generated GoMock package. +package mock_system + +import ( + system "github.com/SimonBaeumer/goss/system" + gomock "github.com/golang/mock/gomock" + io "io" + reflect "reflect" +) + +// MockHTTP is a mock of HTTP interface +type MockHTTP struct { + ctrl *gomock.Controller + recorder *MockHTTPMockRecorder +} + +// MockHTTPMockRecorder is the mock recorder for MockHTTP +type MockHTTPMockRecorder struct { + mock *MockHTTP +} + +// NewMockHTTP creates a new mock instance +func NewMockHTTP(ctrl *gomock.Controller) *MockHTTP { + mock := &MockHTTP{ctrl: ctrl} + mock.recorder = &MockHTTPMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockHTTP) EXPECT() *MockHTTPMockRecorder { + return m.recorder +} + +// HTTP mocks base method +func (m *MockHTTP) HTTP() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "HTTP") + ret0, _ := ret[0].(string) + return ret0 +} + +// HTTP indicates an expected call of HTTP +func (mr *MockHTTPMockRecorder) HTTP() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HTTP", reflect.TypeOf((*MockHTTP)(nil).HTTP)) +} + +// Status mocks base method +func (m *MockHTTP) Status() (int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Status") + ret0, _ := ret[0].(int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Status indicates an expected call of Status +func (mr *MockHTTPMockRecorder) Status() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Status", reflect.TypeOf((*MockHTTP)(nil).Status)) +} + +// Body mocks base method +func (m *MockHTTP) Body() (io.Reader, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Body") + ret0, _ := ret[0].(io.Reader) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Body indicates an expected call of Body +func (mr *MockHTTPMockRecorder) Body() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Body", reflect.TypeOf((*MockHTTP)(nil).Body)) +} + +// Exists mocks base method +func (m *MockHTTP) Exists() (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Exists") + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Exists indicates an expected call of Exists +func (mr *MockHTTPMockRecorder) Exists() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exists", reflect.TypeOf((*MockHTTP)(nil).Exists)) +} + +// SetAllowInsecure mocks base method +func (m *MockHTTP) SetAllowInsecure(arg0 bool) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetAllowInsecure", arg0) +} + +// SetAllowInsecure indicates an expected call of SetAllowInsecure +func (mr *MockHTTPMockRecorder) SetAllowInsecure(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetAllowInsecure", reflect.TypeOf((*MockHTTP)(nil).SetAllowInsecure), arg0) +} + +// SetNoFollowRedirects mocks base method +func (m *MockHTTP) SetNoFollowRedirects(arg0 bool) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetNoFollowRedirects", arg0) +} + +// SetNoFollowRedirects indicates an expected call of SetNoFollowRedirects +func (mr *MockHTTPMockRecorder) SetNoFollowRedirects(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetNoFollowRedirects", reflect.TypeOf((*MockHTTP)(nil).SetNoFollowRedirects), arg0) +} + +// Headers mocks base method +func (m *MockHTTP) Headers() (system.Header, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Headers") + ret0, _ := ret[0].(system.Header) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Headers indicates an expected call of Headers +func (mr *MockHTTPMockRecorder) Headers() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Headers", reflect.TypeOf((*MockHTTP)(nil).Headers)) +} diff --git a/system/mock_system/system_system_mock.go b/system/mock_system/system_system_mock.go new file mode 100644 index 0000000..54d3842 --- /dev/null +++ b/system/mock_system/system_system_mock.go @@ -0,0 +1,48 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: system.go + +// Package mock_system is a generated GoMock package. +package mock_system + +import ( + gomock "github.com/golang/mock/gomock" + reflect "reflect" +) + +// MockResource is a mock of Resource interface +type MockResource struct { + ctrl *gomock.Controller + recorder *MockResourceMockRecorder +} + +// MockResourceMockRecorder is the mock recorder for MockResource +type MockResourceMockRecorder struct { + mock *MockResource +} + +// NewMockResource creates a new mock instance +func NewMockResource(ctrl *gomock.Controller) *MockResource { + mock := &MockResource{ctrl: ctrl} + mock.recorder = &MockResourceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockResource) EXPECT() *MockResourceMockRecorder { + return m.recorder +} + +// Exists mocks base method +func (m *MockResource) Exists() (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Exists") + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Exists indicates an expected call of Exists +func (mr *MockResourceMockRecorder) Exists() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exists", reflect.TypeOf((*MockResource)(nil).Exists)) +} diff --git a/util/config.go b/util/config.go index e968eef..c4b2730 100644 --- a/util/config.go +++ b/util/config.go @@ -16,6 +16,7 @@ type Config struct { Server string Username string Password string + Header map[string][]string } type OutputConfig struct { From 6b2a4adda1ed01bea5ae44ec769aa7e713ca9ed7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20B=C3=A4umer?= Date: Thu, 31 Jan 2019 11:57:32 +0100 Subject: [PATCH 2/3] Add request-headers to config --- development/http_test_server.go | 7 ++-- resource/http.go | 67 +++++++++++++++++++-------------- resource/http_test.go | 25 +++++++++++- resource/validate_test.go | 9 ++--- system/http.go | 9 +++++ util/config.go | 5 +++ validate.go | 2 +- 7 files changed, 86 insertions(+), 38 deletions(-) diff --git a/development/http_test_server.go b/development/http_test_server.go index ea2bb70..bb344bd 100644 --- a/development/http_test_server.go +++ b/development/http_test_server.go @@ -7,12 +7,13 @@ import ( ) func handler(w http.ResponseWriter, r *http.Request) { - for k, v := range r.Header { - fmt.Println("key:", k, "val:", v) + for k, values := range r.Header { + for _, value := range values { + fmt.Println("key:", k, "val:", value) + } } w.Header().Add("key", "value") - w.Header().Add("key", "value") } func main() { diff --git a/resource/http.go b/resource/http.go index 0744b1a..a87a530 100644 --- a/resource/http.go +++ b/resource/http.go @@ -1,11 +1,11 @@ package resource import ( - "github.com/SimonBaeumer/goss/system" - "github.com/SimonBaeumer/goss/util" - "reflect" - "strings" - "time" + "github.com/SimonBaeumer/goss/system" + "github.com/SimonBaeumer/goss/util" + "reflect" + "strings" + "time" ) const TimeoutMS = 5000 @@ -22,6 +22,7 @@ type HTTP struct { Username string `json:"username,omitempty" yaml:"username,omitempty"` Password string `json:"password,omitempty" yaml:"password,omitempty"` Headers map[string][]string `json:"headers,omitempty" yaml:"headers,omitempty"` + RequestHeaders map[string][]string `json:"request-headers,omitempty" yaml:"request-headers,omitempty"` } func (u *HTTP) ID() string { return u.HTTP } @@ -34,8 +35,13 @@ func (u *HTTP) Validate(sys *system.System) []TestResult { skip := false conf := util.Config{ - AllowInsecure: u.AllowInsecure, NoFollowRedirects: u.NoFollowRedirects, - Timeout: u.Timeout, Username: u.Username, Password: u.Password} + AllowInsecure: u.AllowInsecure, + NoFollowRedirects: u.NoFollowRedirects, + Timeout: u.Timeout, + Username: u.Username, + Password: u.Password, + RequestHeaders: u.RequestHeaders, + } sysHTTP := sys.NewHTTP( u.HTTP, @@ -105,27 +111,22 @@ func validateHeader(res ResourceRead, property string, expectedHeaders map[strin ) } - var actualString string - for k, values := range actualHeaders { - for _, v := range values { - actualString += k + ": " + v + "\n" - } - } + actualString := convertHeaderMapToString(actualHeaders) for expectedKey, expectedValues := range expectedHeaders { if _, ok := actualHeaders[expectedKey]; !ok { return TestResult{ - Successful: false, - Result: FAIL, - Title: title, + Successful: false, + Result: FAIL, + Title: title, ResourceType: typeS, - ResourceId: id, - TestType: Header, - Property: property, - Err: nil, - Human: "Did not find header " + expectedKey + " got \n" + actualString, - Expected: []string{expectedKey}, - Found: []string{actualString}, + ResourceId: id, + TestType: Header, + Property: property, + Err: nil, + Human: "Did not find header " + expectedKey + " got \n" + actualString, + Expected: []string{expectedKey}, + Found: []string{actualString}, } } @@ -137,14 +138,14 @@ func validateHeader(res ResourceRead, property string, expectedHeaders map[strin Successful: false, Result: FAIL, ResourceType: typeS, - ResourceId: id, + ResourceId: id, TestType: Header, - Title: title, + Title: title, Property: property, Err: nil, Human: "Did not find header " + expectedKey + ": " + expectedValue, Expected: []string{expectedValue}, - Found: []string{actualString}, + Found: []string{actualString}, } } } @@ -152,7 +153,7 @@ func validateHeader(res ResourceRead, property string, expectedHeaders map[strin return TestResult{ Successful: true, - Title: title, + Title: title, ResourceId: id, Result: SUCCESS, ResourceType: typeS, @@ -160,6 +161,16 @@ func validateHeader(res ResourceRead, property string, expectedHeaders map[strin } } +func convertHeaderMapToString(actualHeaders system.Header) string { + var actualString string + for k, values := range actualHeaders { + for _, v := range values { + actualString += k + ": " + v + "\n" + } + } + return actualString +} + func isInStringSlice(haystack []string, needle string) bool { for _, value := range haystack { if needle == value { @@ -167,4 +178,4 @@ func isInStringSlice(haystack []string, needle string) bool { } } return false -} \ No newline at end of file +} diff --git a/resource/http_test.go b/resource/http_test.go index 4674ce7..293abf4 100644 --- a/resource/http_test.go +++ b/resource/http_test.go @@ -6,6 +6,7 @@ import ( "github.com/SimonBaeumer/goss/util" "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" + "gopkg.in/yaml.v2" "testing" ) @@ -133,4 +134,26 @@ func Test_isNotInSlice(t *testing.T) { got := isInStringSlice(haystack, "pear") assert.False(t, got) -} \ No newline at end of file +} + +func Test_ParseYAML(t *testing.T) { + configString := []byte(` +status: 200 +allow-insecure: true +no-follow-redirects: true +request-headers: + key: + - value + - value2 +`) + + got := new(HTTP) + err := yaml.Unmarshal(configString, got) + + assert.Nil(t, err) + assert.Equal(t, 200, got.Status) + assert.Equal(t, true, got.AllowInsecure) + assert.Equal(t, true, got.NoFollowRedirects) + assert.IsType(t, make(map[string][]string), got.RequestHeaders) + assert.Equal(t, []string{"value", "value2"}, got.RequestHeaders["key"]) +} diff --git a/resource/validate_test.go b/resource/validate_test.go index bcbd25f..8ab775d 100644 --- a/resource/validate_test.go +++ b/resource/validate_test.go @@ -1,10 +1,10 @@ package resource import ( - "fmt" - "io" - "strings" - "testing" + "fmt" + "io" + "strings" + "testing" ) type FakeResource struct { @@ -139,4 +139,3 @@ func TestValidateContainsSkip(t *testing.T) { } } } - diff --git a/system/http.go b/system/http.go index 2f4be22..6b9ee43 100644 --- a/system/http.go +++ b/system/http.go @@ -30,6 +30,7 @@ type DefHTTP struct { err error Username string Password string + RequestHeaders Header } func NewDefHTTP(http string, system *System, config util.Config) HTTP { @@ -40,6 +41,7 @@ func NewDefHTTP(http string, system *System, config util.Config) HTTP { Timeout: config.Timeout, Username: config.Username, Password: config.Password, + RequestHeaders: config.RequestHeaders, } } @@ -66,6 +68,13 @@ func (u *DefHTTP) setup() error { } req, err := http.NewRequest("GET", u.http, nil) + + for key, values := range u.RequestHeaders { + for _, value := range values { + req.Header.Add(key, value) + } + } + if err != nil { return u.err } diff --git a/util/config.go b/util/config.go index c4b2730..864ea2f 100644 --- a/util/config.go +++ b/util/config.go @@ -17,6 +17,11 @@ type Config struct { Username string Password string Header map[string][]string + RequestHeaders map[string][]string +} + +type Request struct { + } type OutputConfig struct { diff --git a/validate.go b/validate.go index 9658f8c..fbfd815 100644 --- a/validate.go +++ b/validate.go @@ -95,7 +95,7 @@ func Validate(c *cli.Context, startTime time.Time) { } func validate(sys *system.System, gossConfig GossConfig, maxConcurrent int) <-chan []resource.TestResult { - out := make(chan []resource.TestResult) + out := make(chan []resource.TestResult) in := make(chan resource.Resource) go func() { From 5ebb08030d2ba5cebd9e9600e1728d23d838ace0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20B=C3=A4umer?= Date: Thu, 31 Jan 2019 13:39:39 +0100 Subject: [PATCH 3/3] Add request-headers documentation --- docs/manual.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/manual.md b/docs/manual.md index 24c96b3..e9cfa00 100644 --- a/docs/manual.md +++ b/docs/manual.md @@ -582,6 +582,9 @@ http: key: - value - another value + request-headers: # define headers should be send within the request + key: + - value ```