diff --git a/.editorconfig b/.editorconfig index f45b19b..2c0ed4d 100644 --- a/.editorconfig +++ b/.editorconfig @@ -5,4 +5,7 @@ indent_style = space indent_size = 4 charset = utf-8 trim_trailing_whitespace = true -insert_final_newline = false \ No newline at end of file +insert_final_newline = true + +[*.yaml] +indent_size = 2 diff --git a/check/base.go b/check/base.go index 32c9836..548dacd 100644 --- a/check/base.go +++ b/check/base.go @@ -16,16 +16,18 @@ type FTWCheck struct { } // NewCheck creates a new FTWCheck, allowing to inject the configuration -func NewCheck(c *config.FTWConfiguration) *FTWCheck { - //TODO: check error - ll, _ := waflog.NewFTWLogLines(c) +func NewCheck(c *config.FTWConfiguration) (*FTWCheck, error) { + ll, err := waflog.NewFTWLogLines(c) + if err != nil { + return nil, err + } check := &FTWCheck{ log: ll, cfg: c, expected: &test.Output{}, } - return check + return check, nil } // SetExpectTestOutput sets the combined expected output from this test diff --git a/check/base_test.go b/check/base_test.go index 9ee2017..f29d28f 100644 --- a/check/base_test.go +++ b/check/base_test.go @@ -4,35 +4,57 @@ import ( "sort" "testing" - "github.com/stretchr/testify/assert" + "github.com/coreruleset/go-ftw/utils" + + "github.com/stretchr/testify/suite" "github.com/coreruleset/go-ftw/config" "github.com/coreruleset/go-ftw/test" ) -var yamlApacheConfig = `--- -logfile: 'tests/logs/modsec2-apache/apache2/error.log' -` - -var yamlNginxConfig = `--- +var configMap = map[string]string{ + "TestNewCheck": `--- logfile: 'tests/logs/modsec3-nginx/nginx/error.log' testoverride: ignore: '942200-1': 'Ignore Me' -` +`, "TestForced": `--- +testoverride: + ignore: + '942200-1': 'Ignore Me' + forcepass: + '1245': 'Forced Pass' + forcefail: + '6789': 'Forced Fail' +`, "TestCloudMode": `--- +mode: "cloud"`, +} -var yamlCloudConfig = `--- -mode: "cloud" -` +type checkBaseTestSuite struct { + suite.Suite + cfg *config.FTWConfiguration +} -func TestNewCheck(t *testing.T) { - cfg, err := config.NewConfigFromString(yamlNginxConfig) - assert.NoError(t, err) +func (s *checkBaseTestSuite) BeforeTest(_, name string) { + var err error + var logName string + s.cfg, err = config.NewConfigFromString(configMap[name]) + s.NoError(err) + logName, err = utils.CreateTempFileWithContent(logText, "test-*.log") + s.NoError(err) + s.cfg.WithLogfile(logName) +} - c := NewCheck(cfg) +func TestCheckBaseTestSuite(t *testing.T) { + suite.Run(t, new(checkBaseTestSuite)) +} + +func (s *checkBaseTestSuite) TestNewCheck() { + c, err := NewCheck(s.cfg) + s.NoError(err) for _, text := range c.cfg.TestOverride.Ignore { - assert.Equal(t, text, "Ignore Me", "Well, didn't match Ignore Me") + s.Equal(text, "Ignore Me", "Well, didn't match Ignore Me") } to := test.Output{ @@ -44,33 +66,37 @@ func TestNewCheck(t *testing.T) { } c.SetExpectTestOutput(&to) - assert.True(t, c.expected.ExpectError, "Problem setting expected output") + s.True(c.expected.ExpectError, "Problem setting expected output") c.SetNoLogContains("nologcontains") - assert.Equal(t, c.expected.NoLogContains, "nologcontains", "Problem setting nologcontains") + s.Equal(c.expected.NoLogContains, "nologcontains", "Problem setting nologcontains") } -func TestForced(t *testing.T) { - cfg, err := config.NewConfigFromString(yamlNginxConfig) - assert.NoError(t, err) +func (s *checkBaseTestSuite) TestForced() { + c, err := NewCheck(s.cfg) + s.NoError(err) - c := NewCheck(cfg) + s.True(c.ForcedIgnore("942200-1"), "Can't find ignored value") - assert.True(t, c.ForcedIgnore("942200-1"), "Can't find ignored value") + s.False(c.ForcedFail("1245"), "Value should not be found") - assert.False(t, c.ForcedFail("1245"), "Value should not be found") + s.False(c.ForcedPass("1234"), "Value should not be found") - assert.False(t, c.ForcedPass("1245"), "Value should not be found") -} + s.True(c.ForcedPass("1245"), "Value should be found") + + s.True(c.ForcedFail("6789"), "Value should be found") + + s.cfg.TestOverride.Ignore = make(map[*config.FTWRegexp]string) + s.Falsef(c.ForcedIgnore("anything"), "Should not find ignored value in empty map") -func TestCloudMode(t *testing.T) { - cfg, err := config.NewConfigFromString(yamlCloudConfig) - assert.NoError(t, err) +} - c := NewCheck(cfg) +func (s *checkBaseTestSuite) TestCloudMode() { + c, err := NewCheck(s.cfg) + s.NoError(err) - assert.True(t, c.CloudMode(), "couldn't detect cloud mode") + s.True(c.CloudMode(), "couldn't detect cloud mode") status := []int{200, 301} c.SetExpectStatus(status) @@ -81,7 +107,7 @@ func TestCloudMode(t *testing.T) { cloudStatus := c.expected.Status sort.Ints(cloudStatus) res := sort.SearchInts(cloudStatus, 403) - assert.Equalf(t, 2, res, "couldn't find expected 403 status in %#v -> %d", cloudStatus, res) + s.Equalf(2, res, "couldn't find expected 403 status in %#v -> %d", cloudStatus, res) c.SetLogContains("") c.SetNoLogContains("no log contains") @@ -96,6 +122,16 @@ func TestCloudMode(t *testing.T) { found = true } } - assert.True(t, found, "couldn't find expected 200 status") + s.True(found, "couldn't find expected 200 status") + +} + +func (s *checkBaseTestSuite) TestSetMarkers() { + c, err := NewCheck(s.cfg) + s.NoError(err) + c.SetStartMarker([]byte("TesTingStArtMarKer")) + c.SetEndMarker([]byte("TestIngEnDMarkeR")) + s.Equal([]byte("testingstartmarker"), c.log.StartMarker, "Couldn't set start marker") + s.Equal([]byte("testingendmarker"), c.log.EndMarker, "Couldn't set end marker") } diff --git a/check/error_test.go b/check/error_test.go index 15c7406..b49df57 100644 --- a/check/error_test.go +++ b/check/error_test.go @@ -4,7 +4,9 @@ import ( "errors" "testing" - "github.com/stretchr/testify/assert" + "github.com/coreruleset/go-ftw/utils" + + "github.com/stretchr/testify/suite" "github.com/coreruleset/go-ftw/config" ) @@ -25,25 +27,38 @@ var expectedFailTests = []struct { {errors.New("a"), false}, } -func TestAssertResponseErrorOK(t *testing.T) { - cfg, err := config.NewConfigFromString(yamlApacheConfig) - assert.NoError(t, err) +type checkErrorTestSuite struct { + suite.Suite + cfg *config.FTWConfiguration +} + +func TestCheckErrorTestSuite(t *testing.T) { + suite.Run(t, new(checkErrorTestSuite)) +} - c := NewCheck(cfg) +func (s *checkErrorTestSuite) SetupTest() { + var err error + s.cfg = config.NewDefaultConfig() + + logName, err := utils.CreateTempFileWithContent(logText, "test-*.log") + s.NoError(err) + s.cfg.WithLogfile(logName) +} +func (s *checkErrorTestSuite) TestAssertResponseErrorOK() { + c, err := NewCheck(s.cfg) + s.NoError(err) for _, e := range expectedOKTests { c.SetExpectError(e.expected) - assert.Equal(t, e.expected, c.AssertExpectError(e.err)) + s.Equal(e.expected, c.AssertExpectError(e.err)) } } -func TestAssertResponseFail(t *testing.T) { - cfg, err := config.NewConfigFromString(yamlApacheConfig) - assert.NoError(t, err) - - c := NewCheck(cfg) +func (s *checkErrorTestSuite) TestAssertResponseFail() { + c, err := NewCheck(s.cfg) + s.NoError(err) for _, e := range expectedFailTests { c.SetExpectError(e.expected) - assert.False(t, c.AssertExpectError(e.err)) + s.False(c.AssertExpectError(e.err)) } } diff --git a/check/logs_test.go b/check/logs_test.go index 746d660..b7a7b83 100644 --- a/check/logs_test.go +++ b/check/logs_test.go @@ -4,7 +4,7 @@ import ( "os" "testing" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" "github.com/coreruleset/go-ftw/config" "github.com/coreruleset/go-ftw/utils" @@ -16,24 +16,50 @@ var logText = `[Tue Jan 05 02:21:09.637165 2021] [:error] [pid 76:tid 1396834345 [Tue Jan 05 02:21:09.647668 2021] [:error] [pid 76:tid 139683434571520] [client 172.23.0.1:58998] [client 172.23.0.1] ModSecurity: Warning. Operator GE matched 5 at TX:inbound_anomaly_score. [file "/etc/modsecurity.d/owasp-crs/rules/RESPONSE-980-CORRELATION.conf"] [line "87"] [id "980130"] [msg "Inbound Anomaly Score Exceeded (Total Inbound Score: 5 - SQLI=0,XSS=0,RFI=0,LFI=0,RCE=0,PHPI=0,HTTP=0,SESS=0): individual paranoia level scores: 3, 2, 0, 0"] [ver "OWASP_CRS/3.3.0"] [tag "event-correlation"] [hostname "localhost"] [uri "/"] [unique_id "X-PNFSe1VwjCgYRI9FsbHgAAAIY"] ` -func TestAssertLogContainsOK(t *testing.T) { - cfg, err := config.NewConfigFromString(yamlApacheConfig) - assert.NoError(t, err) +type checkLogsTestSuite struct { + suite.Suite + cfg *config.FTWConfiguration + logName string +} - logName, _ := utils.CreateTempFileWithContent(logText, "test-*.log") - defer os.Remove(logName) - cfg.WithLogfile(logName) +func TestCheckLogsTestSuite(t *testing.T) { + suite.Run(t, new(checkLogsTestSuite)) +} - c := NewCheck(cfg) +func (s *checkLogsTestSuite) SetupTest() { + var err error + s.cfg = config.NewDefaultConfig() + + s.logName, err = utils.CreateTempFileWithContent(logText, "test-*.log") + s.NoError(err) + s.cfg.WithLogfile(s.logName) +} + +func (s *checkLogsTestSuite) TearDownTest() { + err := os.Remove(s.logName) + s.NoError(err) +} +func (s *checkLogsTestSuite) TestAssertLogContainsOK() { + c, err := NewCheck(s.cfg) + s.NoError(err) c.SetLogContains(`id "920300"`) - assert.True(t, c.AssertLogContains(), "did not find expected content 'id \"920300\"'") + s.True(c.AssertLogContains(), "did not find expected content 'id \"920300\"'") + c.SetLogContains(`SOMETHING`) - assert.False(t, c.AssertLogContains(), "found something that is not there") + s.False(c.AssertLogContains(), "found something that is not there") + s.True(c.LogContainsRequired(), "if LogContains is not empty it should return true") + + c.SetLogContains("") + s.False(c.AssertLogContains(), "empty LogContains should return false") c.SetNoLogContains("SOMETHING") - assert.True(t, c.AssertNoLogContains(), "found something that is not there") + s.True(c.AssertNoLogContains(), "found something that is not there") + c.SetNoLogContains(`id "920300"`) - assert.False(t, c.AssertNoLogContains(), "did not find expected content") + s.False(c.AssertNoLogContains(), "did not find expected content") + c.SetNoLogContains("") + s.False(c.AssertNoLogContains(), "should return false when empty string is passed") + s.False(c.NoLogContainsRequired(), "if NoLogContains is an empty string is passed should return false") } diff --git a/check/response_test.go b/check/response_test.go index a723cda..c03a384 100644 --- a/check/response_test.go +++ b/check/response_test.go @@ -3,7 +3,9 @@ package check import ( "testing" - "github.com/stretchr/testify/assert" + "github.com/coreruleset/go-ftw/utils" + + "github.com/stretchr/testify/suite" "github.com/coreruleset/go-ftw/config" ) @@ -20,37 +22,57 @@ var expectedResponseFailTests = []struct { expected string }{ {``, "not found"}, + {``, `empty should return false`}, +} + +type checkResponseTestSuite struct { + suite.Suite + cfg *config.FTWConfiguration +} + +func TestCheckResponseTestSuite(t *testing.T) { + suite.Run(t, new(checkResponseTestSuite)) } -func TestAssertResponseTextErrorOK(t *testing.T) { - cfg, err := config.NewConfigFromString(yamlApacheConfig) - assert.NoError(t, err) +func (s *checkResponseTestSuite) SetupTest() { + var err error + s.cfg = config.NewDefaultConfig() + logName, err := utils.CreateTempFileWithContent(logText, "test-*.log") + s.NoError(err) + s.cfg.WithLogfile(logName) +} - c := NewCheck(cfg) +func (s *checkResponseTestSuite) TestAssertResponseTextErrorOK() { + c, err := NewCheck(s.cfg) + s.NoError(err) for _, e := range expectedResponseOKTests { c.SetExpectResponse(e.expected) - assert.Truef(t, c.AssertResponseContains(e.response), "unexpected response: %v", e.response) + s.Truef(c.AssertResponseContains(e.response), "unexpected response: %v", e.response) } } -func TestAssertResponseTextFailOK(t *testing.T) { - cfg, err := config.NewConfigFromString(yamlApacheConfig) - assert.NoError(t, err) - - c := NewCheck(cfg) +func (s *checkResponseTestSuite) TestAssertResponseTextFailOK() { + c, err := NewCheck(s.cfg) + s.NoError(err) for _, e := range expectedResponseFailTests { c.SetExpectResponse(e.expected) - assert.Falsef(t, c.AssertResponseContains(e.response), "response shouldn't contain text %v", e.response) + s.Falsef(c.AssertResponseContains(e.response), "response shouldn't contain text %v", e.response) } } -func TestAssertResponseTextChecksFullResponseOK(t *testing.T) { - cfg, err := config.NewConfigFromString(yamlApacheConfig) - assert.NoError(t, err) - - c := NewCheck(cfg) +func (s *checkResponseTestSuite) TestAssertResponseTextChecksFullResponseOK() { + c, err := NewCheck(s.cfg) + s.NoError(err) for _, e := range expectedResponseOKTests { c.SetExpectResponse(e.expected) - assert.Truef(t, c.AssertResponseContains(e.response), "unexpected response: %v", e.response) + s.Truef(c.AssertResponseContains(e.response), "unexpected response: %v", e.response) } } + +func (s *checkResponseTestSuite) TestAssertResponseContainsRequired() { + c, err := NewCheck(s.cfg) + s.NoError(err) + c.SetExpectResponse("") + s.False(c.AssertResponseContains(""), "response shouldn't contain text") + s.False(c.ResponseContainsRequired(), "response shouldn't contain text") +} diff --git a/check/status_test.go b/check/status_test.go index 95fbb06..f8861d8 100644 --- a/check/status_test.go +++ b/check/status_test.go @@ -3,7 +3,9 @@ package check import ( "testing" - "github.com/stretchr/testify/assert" + "github.com/coreruleset/go-ftw/utils" + + "github.com/stretchr/testify/suite" "github.com/coreruleset/go-ftw/config" ) @@ -25,26 +27,47 @@ var statusFailTests = []struct { {200, []int{0}}, } -func TestStatusOK(t *testing.T) { - cfg, err := config.NewConfigFromString(yamlApacheConfig) - assert.NoError(t, err) +type checkStatusTestSuite struct { + suite.Suite + cfg *config.FTWConfiguration +} + +func (s *checkStatusTestSuite) SetupTest() { + var err error + s.cfg = config.NewDefaultConfig() + logName, err := utils.CreateTempFileWithContent(logText, "test-*.log") + s.NoError(err) + s.cfg.WithLogfile(logName) +} + +func TestCheckStatusTestSuite(t *testing.T) { + suite.Run(t, new(checkStatusTestSuite)) +} - c := NewCheck(cfg) +func (s *checkStatusTestSuite) TestStatusOK() { + c, err := NewCheck(s.cfg) + s.NoError(err) for _, expected := range statusOKTests { c.SetExpectStatus(expected.expectedStatus) - assert.True(t, c.AssertStatus(expected.status)) + s.True(c.AssertStatus(expected.status)) } } -func TestStatusFail(t *testing.T) { - cfg, err := config.NewConfigFromString(yamlApacheConfig) - assert.NoError(t, err) - - c := NewCheck(cfg) +func (s *checkStatusTestSuite) TestStatusFail() { + c, err := NewCheck(s.cfg) + s.NoError(err) for _, expected := range statusFailTests { c.SetExpectStatus(expected.expectedStatus) - assert.False(t, c.AssertStatus(expected.status)) + s.False(c.AssertStatus(expected.status)) } } + +func (s *checkStatusTestSuite) TestStatusCodeRequired() { + c, err := NewCheck(s.cfg) + s.NoError(err) + + c.SetExpectStatus([]int{200}) + s.True(c.StatusCodeRequired(), "status code should be required") +} diff --git a/cmd/check.go b/cmd/check.go index 2152453..a9a848d 100644 --- a/cmd/check.go +++ b/cmd/check.go @@ -2,12 +2,10 @@ package cmd import ( "fmt" - "os" + "github.com/coreruleset/go-ftw/test" "github.com/rs/zerolog/log" "github.com/spf13/cobra" - - "github.com/coreruleset/go-ftw/test" ) // NewCheckCmd represents the check command @@ -16,25 +14,22 @@ func NewCheckCommand() *cobra.Command { Use: "check", Short: "Checks ftw test files for syntax errors.", Long: ``, - Run: func(cmd *cobra.Command, args []string) { + RunE: func(cmd *cobra.Command, args []string) error { dir, _ := cmd.Flags().GetString("dir") - checkFiles(dir) + return checkFiles(dir) + }, } checkCmd.Flags().StringP("dir", "d", ".", "recursively find yaml tests in this directory") return checkCmd } -func checkFiles(dir string) { - var exit int +func checkFiles(dir string) error { files := fmt.Sprintf("%s/**/*.yaml", dir) log.Trace().Msgf("ftw/check: checking files using glob pattern: %s", files) tests, err := test.GetTestsFromFiles(files) - if err != nil { - exit = 1 - } else { + if err == nil { fmt.Printf("ftw/check: checked %d files, everything looks good!\n", len(tests)) - exit = 0 } - os.Exit(exit) + return err } diff --git a/cmd/check_test.go b/cmd/check_test.go new file mode 100644 index 0000000..9b8abe4 --- /dev/null +++ b/cmd/check_test.go @@ -0,0 +1,76 @@ +package cmd + +import ( + "context" + "io/fs" + "os" + "testing" + + "github.com/spf13/cobra" + "github.com/stretchr/testify/suite" +) + +var checkFileContents = `--- +meta: + author: "go-ftw" + enabled: true + name: "mock-TestRunTests_Run.yaml" + description: "Test file for go-ftw" +tests: + - # Standard GET request + test_title: 1234 + stages: + - stage: + input: + dest_addr: "127.0.0.1" + method: "GET" + port: 1234 + headers: + User-Agent: "OWASP CRS test agent" + Host: "localhost" + Accept: "*/*" + protocol: "http" + uri: "/" + version: "HTTP/1.1" + output: + status: [200] +` + +type checkCmdTestSuite struct { + suite.Suite + tempDir string + rootCmd *cobra.Command +} + +func (s *checkCmdTestSuite) SetupTest() { + tempDir, err := os.MkdirTemp("", "go-ftw-tests") + s.NoError(err) + s.tempDir = tempDir + + err = os.MkdirAll(s.tempDir, fs.ModePerm) + s.NoError(err) + testFileContents, err := os.CreateTemp(s.tempDir, "mock-test-*.yaml") + s.NoError(err) + n, err := testFileContents.WriteString(checkFileContents) + s.NoError(err) + s.Equal(len(checkFileContents), n) + + s.rootCmd = NewRootCommand() + s.rootCmd.AddCommand(NewCheckCommand()) +} + +func (s *checkCmdTestSuite) TearDownTest() { + err := os.RemoveAll(s.tempDir) + s.NoError(err) +} + +func TestCheckChoreTestSuite(t *testing.T) { + suite.Run(t, new(checkCmdTestSuite)) +} + +func (s *checkCmdTestSuite) TestCheckCommand() { + s.rootCmd.SetArgs([]string{"check", "-d", s.tempDir}) + cmd, err := s.rootCmd.ExecuteContextC(context.Background()) + s.NoError(err, "check command should not return an error") + s.Equal("check", cmd.Name(), "check command should have the name 'check'") +} diff --git a/cmd/root.go b/cmd/root.go index 6c7f5f9..4f83204 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -2,9 +2,7 @@ package cmd import ( "context" - "errors" "log" - "os" "github.com/rs/zerolog" "github.com/spf13/cobra" @@ -37,19 +35,13 @@ func NewRootCommand() *cobra.Command { // Execute adds all child commands to the root command and sets flags appropriately. // This is called by main.main(). It only needs to happen once to the rootCmd. -func Execute(version string) { +func Execute(version string) error { rootCmd := NewRootCommand() rootCmd.AddCommand(NewCheckCommand()) rootCmd.AddCommand(NewRunCommand()) rootCmd.Version = version - if err := rootCmd.ExecuteContext(context.Background()); err != nil { - if errors.Is(err, context.DeadlineExceeded) { - os.Exit(2) - } - - os.Exit(1) - } + return rootCmd.ExecuteContext(context.Background()) } func init() { diff --git a/cmd/root_test.go b/cmd/root_test.go new file mode 100644 index 0000000..58ee436 --- /dev/null +++ b/cmd/root_test.go @@ -0,0 +1,22 @@ +package cmd + +import ( + "testing" + + "github.com/stretchr/testify/suite" +) + +type rootCmdTestSuite struct { + suite.Suite +} + +func TestRootChoreTestSuite(t *testing.T) { + suite.Run(t, new(rootCmdTestSuite)) +} + +func (s *rootCmdTestSuite) TestRootCommand() { + rootCmd := NewRootCommand() + rootCmd.SetArgs([]string{"help"}) + err := Execute("v1.0.0") + s.NoError(err) +} diff --git a/cmd/run_test.go b/cmd/run_test.go index 38af34d..4a796cf 100644 --- a/cmd/run_test.go +++ b/cmd/run_test.go @@ -21,7 +21,7 @@ var testFileContentsTemplate = `--- meta: author: "go-ftw" enabled: true - name: "mock-test.yaml" + name: "mock-TestRunTests_Run.yaml" description: "Test file for go-ftw" tests: - # Standard GET request diff --git a/config/config.go b/config/config.go index e0f0c45..10f9e81 100644 --- a/config/config.go +++ b/config/config.go @@ -24,6 +24,14 @@ func NewDefaultConfig() *FTWConfiguration { return cfg } +// NewCloudConfig initializes the configuration with cloud values +func NewCloudConfig() *FTWConfiguration { + cfg := NewDefaultConfig() + cfg.RunMode = CloudRunMode + + return cfg +} + // NewConfigFromFile reads configuration information from the config file if it exists, // or uses `.ftw.yaml` as default file func NewConfigFromFile(cfgFile string) (*FTWConfiguration, error) { @@ -134,6 +142,6 @@ func (c *FTWConfiguration) WithMaxMarkerRetries(retries int) { } // WithMaxMarkerLogLines sets the new amount of lines we go back in the logfile attempting to find markers. -func (c *FTWConfiguration) WithMaxMarkerLogLines(retries int) { - c.MaxMarkerLogLines = retries +func (c *FTWConfiguration) WithMaxMarkerLogLines(amount int) { + c.MaxMarkerLogLines = amount } diff --git a/config/config_test.go b/config/config_test.go index e33c9cd..89deb52 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -5,12 +5,14 @@ import ( "regexp" "testing" + "github.com/stretchr/testify/suite" + "github.com/coreruleset/go-ftw/test" "github.com/coreruleset/go-ftw/utils" - "github.com/stretchr/testify/assert" ) -var yamlConfig = `--- +var testData = map[string]string{ + "TestNewConfigFromFile": `--- logfile: 'tests/logs/modsec2-apache/apache2/error.log' testoverride: input: @@ -18,148 +20,186 @@ testoverride: port: '1234' ignore: '920400-1$': 'This test must be ignored' -` - -var yamlCloudConfig = `--- +`, + "TestNewConfigFromFileRunMode": `--- mode: 'cloud' -` - -var yamlBadConfig = ` +`, + "bad": ` --- logfile: 'tests/logs/modsec2-apache/apache2/error.log' doesNotExist: "" -` +`, + "jsonConfig": ` +{"test": "type"} +`, +} -var jsonConfig = `{"test": "type"}` +type fileTestSuite struct { + suite.Suite + filename string + cfg *FTWConfiguration +} -func TestNewDefaultConfig(t *testing.T) { - cfg := NewDefaultConfig() - assert.Equal(t, DefaultLogMarkerHeaderName, cfg.LogMarkerHeaderName) - assert.Equal(t, DefaultRunMode, cfg.RunMode) - assert.Equal(t, "", cfg.LogFile) +type envTestSuite struct { + suite.Suite } -func TestNewConfigBadFileConfig(t *testing.T) { - filename, _ := utils.CreateTempFileWithContent(jsonConfig, "test-*.yaml") - defer os.Remove(filename) - cfg, err := NewConfigFromFile(filename) - assert.NoError(t, err) - assert.NotNil(t, cfg) +type baseTestSuite struct { + suite.Suite } -func TestNewConfigConfig(t *testing.T) { - filename, _ := utils.CreateTempFileWithContent(yamlConfig, "test-*.yaml") +func TestConfigTestSuite(t *testing.T) { + suite.Run(t, new(baseTestSuite)) + suite.Run(t, new(fileTestSuite)) + suite.Run(t, new(envTestSuite)) +} - cfg, err := NewConfigFromFile(filename) +func (s *fileTestSuite) SetupTest() { +} - assert.NoError(t, err) - assert.NotNil(t, cfg) - assert.NotEmpty(t, cfg.TestOverride.Overrides, "Ignore list must not be empty") +func (s *envTestSuite) SetupTest() { +} + +func (s *fileTestSuite) BeforeTest(_, name string) { + var err error + s.filename, _ = utils.CreateTempFileWithContent(testData[name], "test-*.yaml") + s.cfg, err = NewConfigFromFile(s.filename) + s.NoError(err) + s.NotNil(s.cfg) +} - for id, text := range cfg.TestOverride.Ignore { - assert.Contains(t, (*regexp.Regexp)(id).String(), "920400-1$", "Looks like we could not find item to ignore") - assert.Equal(t, "This test must be ignored", text, "Text doesn't match") +func (s *fileTestSuite) TearDownTest() { + if s.filename != "" { + err := os.Remove(s.filename) + s.NoError(err) + s.filename = "" } +} + +func (s *baseTestSuite) TestBaseUnmarshalText() { + var ftwRegexp FTWRegexp + err := ftwRegexp.UnmarshalText([]byte("test")) + s.NoError(err) + s.NotNil(ftwRegexp) + s.True(ftwRegexp.MatchString("This is a test for unmarshalling"), "looks like we could not match string") +} + +func (s *baseTestSuite) TestBaseNewFTWRegexpText() { + ftwRegexp, err := NewFTWRegexp("test") + s.NoError(err) + s.NotNil(ftwRegexp) + s.True(ftwRegexp.MatchString("This is a test"), "looks like we could not match string") +} - overrides := cfg.TestOverride.Overrides - assert.NotNil(t, overrides.DestAddr, "Looks like we are not overriding destination address") - assert.Equal(t, "httpbingo.org", *overrides.DestAddr, "Looks like we are not overriding destination address") +func (s *baseTestSuite) TestNewCloudConfig() { + cfg := NewCloudConfig() + s.Equal(CloudRunMode, cfg.RunMode) + s.Equal("", cfg.LogFile) } -func TestNewConfigBadConfig(t *testing.T) { - filename, _ := utils.CreateTempFileWithContent(yamlBadConfig, "test-*.yaml") +func (s *baseTestSuite) TestNewDefaultConfig() { + cfg := NewDefaultConfig() + s.Equal(DefaultLogMarkerHeaderName, cfg.LogMarkerHeaderName) + s.Equal(DefaultRunMode, cfg.RunMode) + s.Equal("", cfg.LogFile) +} + +func (s *fileTestSuite) TestNewConfigBadFileConfig() { + filename, _ := utils.CreateTempFileWithContent(testData["jsonConfig"], "test-*.yaml") defer os.Remove(filename) cfg, err := NewConfigFromFile(filename) + s.NoError(err) + s.NotNil(cfg) +} + +func (s *fileTestSuite) TestNewConfigFromFile() { + s.NotEmpty(s.cfg.TestOverride.Overrides, "Ignore list must not be empty") + + for id, text := range s.cfg.TestOverride.Ignore { + s.Contains((*regexp.Regexp)(id).String(), "920400-1$", "Looks like we could not find item to ignore") + s.Equal("This test must be ignored", text, "Text doesn't match") + } - assert.NoError(t, err) - assert.NotNil(t, cfg) + overrides := s.cfg.TestOverride.Overrides + s.NotNil(overrides.DestAddr, "Looks like we are not overriding destination address") + s.Equal("httpbingo.org", *overrides.DestAddr, "Looks like we are not overriding destination address") } -func TestNewConfigDefaultConfig(t *testing.T) { +func (s *fileTestSuite) TestNewConfigBadConfig() { + // contents come from `bad` YAML config + s.NotNil(s.cfg) +} + +func (s *fileTestSuite) TestNewConfigDefaultConfig() { // For this test we need a local .ftw.yaml file - fileName := ".ftw.yaml" - _ = os.WriteFile(fileName, []byte(yamlConfig), 0644) - t.Cleanup(func() { - os.Remove(fileName) - }) + s.filename = ".ftw.yaml" + _ = os.WriteFile(s.filename, []byte(testData["ok"]), 0644) cfg, err := NewConfigFromFile("") - assert.NoError(t, err) - assert.NotNil(t, cfg) + s.NoError(err) + s.NotNil(cfg) } -func TestNewConfigFromString(t *testing.T) { - cfg, err := NewConfigFromString(yamlConfig) - assert.NoError(t, err) - assert.NotNil(t, cfg) +func (s *fileTestSuite) TestNewConfigFromString() { + cfg, err := NewConfigFromString(testData["ok"]) + s.NoError(err) + s.NotNil(cfg) } -func TestNewEnvConfigFromString(t *testing.T) { - cfg, err := NewConfigFromString(yamlConfig) - assert.NoError(t, err) - assert.NotNil(t, cfg) +func (s *fileTestSuite) TestNewConfigFromNoneExistingFile() { + cfg, err := NewConfigFromFile("nonsense") + s.Error(err) + s.Nil(cfg) } -func TestNewConfigFromEnv(t *testing.T) { +func (s *fileTestSuite) TestNewConfigFromEnv() { // Set some environment so it gets merged with conf os.Setenv("FTW_LOGFILE", "koanf") cfg, err := NewConfigFromEnv() - assert.NoError(t, err) - assert.NotNil(t, cfg) - assert.Equal(t, "koanf", cfg.LogFile) + s.NoError(err) + s.NotNil(cfg) + s.Equal("koanf", cfg.LogFile) } -func TestNewConfigFromEnvHasDefaults(t *testing.T) { +func (s *fileTestSuite) TestNewConfigFromEnvHasDefaults() { cfg, err := NewConfigFromEnv() - assert.NoError(t, err) - assert.NotNil(t, cfg) + s.NoError(err) + s.NotNil(cfg) - assert.Equalf(t, DefaultRunMode, cfg.RunMode, + s.Equalf(DefaultRunMode, cfg.RunMode, "unexpected default value '%s' for run mode", cfg.RunMode) - assert.Equalf(t, DefaultLogMarkerHeaderName, cfg.LogMarkerHeaderName, + s.Equalf(DefaultLogMarkerHeaderName, cfg.LogMarkerHeaderName, "unexpected default value '%s' for logmarkerheadername", cfg.LogMarkerHeaderName) } -func TestNewConfigFromFileHasDefaults(t *testing.T) { - filename, _ := utils.CreateTempFileWithContent(yamlConfig, "test-*.yaml") - defer os.Remove(filename) - - cfg, err := NewConfigFromFile(filename) - assert.NoError(t, err) - assert.NotNil(t, cfg) - assert.Equalf(t, DefaultRunMode, cfg.RunMode, - "unexpected default value '%s' for run mode", cfg.RunMode) - assert.Equalf(t, DefaultLogMarkerHeaderName, cfg.LogMarkerHeaderName, - "unexpected default value '%s' for logmarkerheadername", cfg.LogMarkerHeaderName) +func (s *fileTestSuite) TestNewConfigFromFileHasDefaults() { + s.Equalf(DefaultRunMode, s.cfg.RunMode, + "unexpected default value '%s' for run mode", s.cfg.RunMode) + s.Equalf(DefaultLogMarkerHeaderName, s.cfg.LogMarkerHeaderName, + "unexpected default value '%s' for logmarkerheadername", s.cfg.LogMarkerHeaderName) } -func TestNewConfigFromStringHasDefaults(t *testing.T) { +func (s *fileTestSuite) TestNewConfigFromStringHasDefaults() { cfg, err := NewConfigFromString("") - assert.NoError(t, err) - assert.NotNil(t, cfg) - assert.Equalf(t, DefaultRunMode, cfg.RunMode, + s.NoError(err) + s.NotNil(cfg) + s.Equalf(DefaultRunMode, cfg.RunMode, "unexpected default value '%s' for run mode", cfg.RunMode) - assert.Equalf(t, DefaultLogMarkerHeaderName, cfg.LogMarkerHeaderName, - "unexpected default value '%s' for logmarkerheadername", cfg.LogMarkerHeaderName) + s.Equalf(DefaultLogMarkerHeaderName, cfg.LogMarkerHeaderName, + "unexpected default value '%s' for logmarkerheadername", s.cfg.LogMarkerHeaderName) } -func TestNewConfigFromFileRunMode(t *testing.T) { - filename, _ := utils.CreateTempFileWithContent(yamlCloudConfig, "test-*.yaml") - defer os.Remove(filename) - - cfg, err := NewConfigFromFile(filename) - assert.NoError(t, err) - assert.NotNil(t, cfg) - assert.Equalf(t, CloudRunMode, cfg.RunMode, - "unexpected value '%s' for run mode, expected '%s;", cfg.RunMode, CloudRunMode) +func (s *fileTestSuite) TestNewConfigFromFileRunMode() { + s.Equalf(CloudRunMode, s.cfg.RunMode, + "unexpected value '%s' for run mode, expected '%s;", s.cfg.RunMode, CloudRunMode) } -func TestNewDefaultConfigWithParams(t *testing.T) { +func (s *fileTestSuite) TestNewDefaultConfigWithParams() { cfg := NewDefaultConfig() cfg.WithLogfile("mylogfile.log") - assert.Equal(t, "mylogfile.log", cfg.LogFile) + s.Equal("mylogfile.log", cfg.LogFile) overrides := FTWTestOverride{ Overrides: test.Overrides{}, Ignore: nil, @@ -167,9 +207,18 @@ func TestNewDefaultConfigWithParams(t *testing.T) { ForceFail: nil, } cfg.WithOverrides(overrides) - assert.Equal(t, overrides, cfg.TestOverride) + s.Equal(overrides, cfg.TestOverride) cfg.WithLogMarkerHeaderName("NEW-MARKER-TEST") - assert.Equal(t, "NEW-MARKER-TEST", cfg.LogMarkerHeaderName) + s.Equal("NEW-MARKER-TEST", cfg.LogMarkerHeaderName) cfg.WithRunMode(CloudRunMode) - assert.Equal(t, CloudRunMode, cfg.RunMode) + s.Equal(CloudRunMode, cfg.RunMode) +} + +func (s *baseTestSuite) TestWithMaxMarker() { + cfg := NewDefaultConfig() + cfg.WithMaxMarkerRetries(19) + s.Equal(19, cfg.MaxMarkerRetries) + cfg.WithMaxMarkerLogLines(111) + s.Equal(111, cfg.MaxMarkerLogLines) + } diff --git a/config/types.go b/config/types.go index f8d1dff..435fcee 100644 --- a/config/types.go +++ b/config/types.go @@ -52,8 +52,10 @@ type FTWTestOverride struct { ForceFail map[*FTWRegexp]string `koanf:"forcefail"` } +// FTWRegexp is a wrapper around regexp.Regexp that implements the Unmarshaler interface type FTWRegexp regexp.Regexp +// UnmarshalText implements the Unmarshaler interface func (r *FTWRegexp) UnmarshalText(b []byte) error { re, err := regexp.Compile(string(b)) if err != nil { @@ -63,6 +65,16 @@ func (r *FTWRegexp) UnmarshalText(b []byte) error { return nil } +// MatchString implements the MatchString method of the regexp.Regexp struct func (r *FTWRegexp) MatchString(s string) bool { return (*regexp.Regexp)(r).MatchString(s) } + +// NewFTWRegexp creates a new FTWRegexp from a string +func NewFTWRegexp(s string) (*FTWRegexp, error) { + re, err := regexp.Compile(s) + if err != nil { + return nil, fmt.Errorf("invalid regexp: %w", err) + } + return (*FTWRegexp)(re), nil +} diff --git a/ftwhttp/client.go b/ftwhttp/client.go index 413d7ce..9025c54 100644 --- a/ftwhttp/client.go +++ b/ftwhttp/client.go @@ -2,6 +2,7 @@ package ftwhttp import ( "crypto/tls" + "crypto/x509" "fmt" "net" "net/http/cookiejar" @@ -34,6 +35,12 @@ func NewClient(config ClientConfig) (*Client, error) { return c, nil } +// SetRootCAs sets the root CAs for the client. +// This can be used if you are using internal certificates and for testing purposes. +func (c *Client) SetRootCAs(cas *x509.CertPool) { + c.config.RootCAs = cas +} + // NewConnection creates a new Connection based on a Destination func (c *Client) NewConnection(d Destination) error { if c.Transport != nil && c.Transport.connection != nil { @@ -82,7 +89,15 @@ func (c *Client) dial(d Destination) (net.Conn, error) { // strings.HasSuffix(err.String(), "connection refused") { if strings.ToLower(d.Protocol) == "https" { // Commenting InsecureSkipVerify: true. - return tls.DialWithDialer(&net.Dialer{Timeout: c.config.ConnectTimeout}, "tcp", hostPort, &tls.Config{MinVersion: tls.VersionTLS12}) + return tls.DialWithDialer( + &net.Dialer{ + Timeout: c.config.ConnectTimeout, + }, + "tcp", hostPort, + &tls.Config{ + MinVersion: tls.VersionTLS12, + RootCAs: c.config.RootCAs, + }) } return net.DialTimeout("tcp", hostPort, c.config.ConnectTimeout) diff --git a/ftwhttp/client_test.go b/ftwhttp/client_test.go index 223e7ed..dbce638 100644 --- a/ftwhttp/client_test.go +++ b/ftwhttp/client_test.go @@ -1,63 +1,107 @@ package ftwhttp import ( + "bytes" + "fmt" + "net/http" + "net/http/httptest" "testing" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" ) -func TestNewClient(t *testing.T) { - c, err := NewClient(NewClientConfig()) - assert.NoError(t, err) +const ( + secureServer = true + insecureServer = false +) - assert.NotNil(t, c.Jar, "Error creating Client") +type clientTestSuite struct { + suite.Suite + client *Client + ts *httptest.Server } -func TestConnectDestinationHTTPS(t *testing.T) { - d := &Destination{ - DestAddr: "example.com", - Port: 443, - Protocol: "https", - } +func TestClientTestSuite(t *testing.T) { + suite.Run(t, new(clientTestSuite)) +} - c, err := NewClient(NewClientConfig()) - assert.NoError(t, err) +func (s *clientTestSuite) SetupTest() { + var err error + s.client, err = NewClient(NewClientConfig()) + s.NoError(err) + s.Nil(s.client.Transport, "Transport not expected to be initialized yet") +} - err = c.NewConnection(*d) - assert.NoError(t, err, "This should not error") - assert.Equal(t, "https", c.Transport.protocol, "Error connecting to example.com using https") +func (s *clientTestSuite) TearDownTest() { + if s.ts != nil { + s.ts.Close() + } } -func TestDoRequest(t *testing.T) { - d := &Destination{ - DestAddr: "httpbin.org", - Port: 443, - Protocol: "https", +func (s *clientTestSuite) httpHandler() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/not-found" { + w.WriteHeader(http.StatusNotFound) + } else { + w.WriteHeader(http.StatusOK) + } + resp := new(bytes.Buffer) + for key, value := range r.Header { + _, err := fmt.Fprintf(resp, "%s=%s,", key, value) + s.NoError(err) + } + + _, err := w.Write(resp.Bytes()) + s.NoError(err) } +} - c, err := NewClient(NewClientConfig()) - assert.NoError(t, err) +func (s *clientTestSuite) httpTestServer(secure bool) { + s.HTTPStatusCode(s.httpHandler(), http.MethodGet, "/", nil, http.StatusOK) + s.HTTPStatusCode(s.httpHandler(), http.MethodGet, "/not-found", nil, http.StatusNotFound) - req := generateBaseRequestForTesting() + if secure { + s.ts = httptest.NewTLSServer(s.httpHandler()) + } else { + s.ts = httptest.NewServer(s.httpHandler()) + } +} - err = c.NewConnection(*d) - assert.NoError(t, err, "This should not error") +func (s *clientTestSuite) TestNewClient() { + s.NotNil(s.client.Jar, "Error creating Client") +} - _, err = c.Do(*req) +func (s *clientTestSuite) TestConnectDestinationHTTPS() { + s.httpTestServer(secureServer) + d, err := DestinationFromString(s.ts.URL) + s.NoError(err, "This should not error") + s.client.SetRootCAs(s.ts.Client().Transport.(*http.Transport).TLSClientConfig.RootCAs) + err = s.client.NewConnection(*d) + s.NoError(err, "This should not error") + s.Equal("https", s.client.Transport.protocol, "Error connecting to example.com using https") +} - assert.Error(t, err, "This should return error") +func (s *clientTestSuite) TestDoRequest() { + s.httpTestServer(secureServer) + d, err := DestinationFromString(s.ts.URL) + s.NoError(err, "This should not error") + s.client.SetRootCAs(s.ts.Client().Transport.(*http.Transport).TLSClientConfig.RootCAs) + req := generateBaseRequestForTesting() + req.requestLine.URI = "/not-found" + err = s.client.NewConnection(*d) + s.NoError(err, "This should not error") + response, err := s.client.Do(*req) + s.NoError(err, "This should error") + s.Equal(http.StatusNotFound, response.Parsed.StatusCode, "Error in calling website") } -func TestGetTrackedTime(t *testing.T) { +func (s *clientTestSuite) TestGetTrackedTime() { d := &Destination{ DestAddr: "httpbingo.org", Port: 443, Protocol: "https", } - c, err := NewClient(NewClientConfig()) - assert.NoError(t, err) - rl := &RequestLine{ Method: "POST", URI: "/post", @@ -69,33 +113,28 @@ func TestGetTrackedTime(t *testing.T) { data := []byte(`test=me&one=two&one=twice`) req := NewRequest(rl, h, data, true) - err = c.NewConnection(*d) - assert.NoError(t, err, "This should not error") - - c.StartTrackingTime() + err := s.client.NewConnection(*d) + s.NoError(err, "This should not error") - resp, err := c.Do(*req) + s.client.StartTrackingTime() - c.StopTrackingTime() + resp, err := s.client.Do(*req) - assert.NoError(t, err, "This should not error") + s.client.StopTrackingTime() - assert.Equal(t, 200, resp.Parsed.StatusCode, "Error in calling website") + s.NoError(err, "This should not error") + s.Equal(http.StatusOK, resp.Parsed.StatusCode, "Error in calling website") - rtt := c.GetRoundTripTime() - - assert.GreaterOrEqual(t, int(rtt.RoundTripDuration()), 0, "Error getting RTT") + rtt := s.client.GetRoundTripTime() + s.GreaterOrEqual(int(rtt.RoundTripDuration()), 0, "Error getting RTT") } -func TestClientMultipartFormDataRequest(t *testing.T) { - d := &Destination{ - DestAddr: "httpbingo.org", - Port: 443, - Protocol: "https", - } +func (s *clientTestSuite) TestClientMultipartFormDataRequest() { + s.httpTestServer(secureServer) + d, err := DestinationFromString(s.ts.URL) + s.NoError(err, "This should not error") - c, err := NewClient(NewClientConfig()) - assert.NoError(t, err) + s.client.SetRootCAs(s.ts.Client().Transport.(*http.Transport).TLSClientConfig.RootCAs) rl := &RequestLine{ Method: "POST", @@ -117,67 +156,55 @@ Some-file-test-here req := NewRequest(rl, h, data, true) - err = c.NewConnection(*d) - assert.NoError(t, err, "This should not error") + err = s.client.NewConnection(*d) + s.NoError(err, "This should not error") - c.StartTrackingTime() + s.client.StartTrackingTime() - resp, err := c.Do(*req) + resp, err := s.client.Do(*req) - c.StopTrackingTime() + s.client.StopTrackingTime() - assert.NoError(t, err, "This should not error") - assert.Equal(t, 200, resp.Parsed.StatusCode, "Error in calling website") + s.NoError(err, "This should not error") + s.Equal(http.StatusOK, resp.Parsed.StatusCode, "Error in calling website") } -func TestNewConnectionCreatesTransport(t *testing.T) { - c, err := NewClient(NewClientConfig()) - assert.NoError(t, err) - assert.Nil(t, c.Transport, "Transport not expected to initialized yet") - - server := testServer() - d, err := DestinationFromString(server.URL) - assert.NoError(t, err, "Failed to construct destination from test server") - - err = c.NewConnection(*d) - assert.NoError(t, err, "Failed to create new connection") - assert.NotNil(t, c.Transport, "Transport expected to be initialized") - assert.NotNil(t, c.Transport.connection, "Connection expected to be initialized") +func (s *clientTestSuite) TestNewConnectionCreatesTransport() { + s.httpTestServer(secureServer) + d, err := DestinationFromString(s.ts.URL) + s.NoError(err, "Failed to construct destination from test server") + s.client.SetRootCAs(s.ts.Client().Transport.(*http.Transport).TLSClientConfig.RootCAs) + err = s.client.NewConnection(*d) + s.NoError(err, "Failed to create new connection") + s.NotNil(s.client.Transport, "Transport expected to be initialized") + s.NotNil(s.client.Transport.connection, "Connection expected to be initialized") } -func TestNewOrReusedConnectionCreatesTransport(t *testing.T) { - c, err := NewClient(NewClientConfig()) - assert.NoError(t, err) - assert.Nil(t, c.Transport, "Transport not expected to initialized yet") - - server := testServer() - d, err := DestinationFromString(server.URL) - assert.NoError(t, err, "Failed to construct destination from test server") - - err = c.NewOrReusedConnection(*d) - assert.NoError(t, err, "Failed to create new or to reuse connection") - assert.NotNil(t, c.Transport, "Transport expected to be initialized") - assert.NotNil(t, c.Transport.connection, "Connection expected to be initialized") +func (s *clientTestSuite) TestNewOrReusedConnectionCreatesTransport() { + s.httpTestServer(secureServer) + d, err := DestinationFromString(s.ts.URL) + s.NoError(err, "Failed to construct destination from test server") + s.client.SetRootCAs(s.ts.Client().Transport.(*http.Transport).TLSClientConfig.RootCAs) + err = s.client.NewOrReusedConnection(*d) + s.NoError(err, "Failed to create new or to reuse connection") + s.NotNil(s.client.Transport, "Transport expected to be initialized") + s.NotNil(s.client.Transport.connection, "Connection expected to be initialized") } -func TestNewOrReusedConnectionReusesTransport(t *testing.T) { - c, err := NewClient(NewClientConfig()) - assert.NoError(t, err) - assert.Nil(t, c.Transport, "Transport not expected to initialized yet") - - server := testServer() - d, err := DestinationFromString(server.URL) - assert.NoError(t, err, "Failed to construct destination from test server") +func (s *clientTestSuite) TestNewOrReusedConnectionReusesTransport() { + s.httpTestServer(insecureServer) + d, err := DestinationFromString(s.ts.URL) + s.NoError(err, "Failed to construct destination from test server") - err = c.NewOrReusedConnection(*d) - assert.NoError(t, err, "Failed to create new or to reuse connection") - assert.NotNil(t, c.Transport, "Transport expected to be initialized") - assert.NotNil(t, c.Transport.connection, "Connection expected to be initialized") + err = s.client.NewOrReusedConnection(*d) + s.NoError(err, "Failed to create new or to reuse connection") + s.NotNil(s.client.Transport, "Transport expected to be initialized") + s.NotNil(s.client.Transport.connection, "Connection expected to be initialized") - begin := c.Transport.duration.begin - err = c.NewOrReusedConnection(*d) - assert.NoError(t, err, "Failed to reuse connection") + begin := s.client.Transport.duration.begin + err = s.client.NewOrReusedConnection(*d) + s.NoError(err, "Failed to reuse connection") - assert.Equal(t, begin, c.Transport.duration.begin, "Transport must not be reinitialized when reusing connection") + s.Equal(begin, s.client.Transport.duration.begin, "Transport must not be reinitialized when reusing connection") } diff --git a/ftwhttp/connection_test.go b/ftwhttp/connection_test.go index b5d25ce..6334ecb 100644 --- a/ftwhttp/connection_test.go +++ b/ftwhttp/connection_test.go @@ -3,13 +3,26 @@ package ftwhttp import ( "testing" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" ) -func TestDestinationFromString(t *testing.T) { +type connectionTestSuite struct { + suite.Suite +} + +func TestConnectionTestSuite(t *testing.T) { + suite.Run(t, new(connectionTestSuite)) +} +func (s *connectionTestSuite) TestDestinationFromString() { + d, err := DestinationFromString("http://example.com:80") + s.NoError(err, "This should not error") + s.Equal("example.com", d.DestAddr, "Error parsing destination") + s.Equal(80, d.Port, "Error parsing destination") + s.Equal("http", d.Protocol, "Error parsing destination") } -func TestMultipleRequestTypes(t *testing.T) { + +func (s *connectionTestSuite) TestMultipleRequestTypes() { var req *Request rl := &RequestLine{ @@ -23,5 +36,5 @@ func TestMultipleRequestTypes(t *testing.T) { data := []byte(`test=me&one=two`) req = NewRequest(rl, h, data, true) - assert.True(t, req.WithAutoCompleteHeaders(), "Set Autocomplete headers error ") + s.True(req.WithAutoCompleteHeaders(), "Set Autocomplete headers error ") } diff --git a/ftwhttp/header.go b/ftwhttp/header.go index ccd7ea3..2f19e46 100644 --- a/ftwhttp/header.go +++ b/ftwhttp/header.go @@ -4,6 +4,8 @@ import ( "bytes" "io" "sort" + + "github.com/rs/zerolog/log" ) const ( @@ -91,18 +93,21 @@ func (h Header) Write(w io.Writer) error { } // WriteBytes writes a header in a ByteWriter. -func (h Header) WriteBytes(b *bytes.Buffer) error { +func (h Header) WriteBytes(b *bytes.Buffer) (int, error) { sorted := h.getSortedHeadersByName() - + count := 0 for _, key := range sorted { // we want all headers "as-is" s := key + ": " + h[key] + "\r\n" - if _, err := b.Write([]byte(s)); err != nil { - return err + log.Info().Msgf("Writing header: %s", s) + n, err := b.Write([]byte(s)) + count += n + if err != nil { + return count, err } } - return nil + return count, nil } diff --git a/ftwhttp/header_test.go b/ftwhttp/header_test.go index 5869e5b..291c56b 100644 --- a/ftwhttp/header_test.go +++ b/ftwhttp/header_test.go @@ -10,10 +10,11 @@ package ftwhttp import ( "bytes" + "errors" "io" "testing" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" ) var headerWriteTests = []struct { @@ -50,31 +51,82 @@ var headerWriteTests = []struct { }, } -func TestHeaderWriteBytes(t *testing.T) { - var buf bytes.Buffer +type BadWriter struct { + err error +} + +func (bw BadWriter) Write(_ []byte) (n int, err error) { + return 0, bw.err +} + +type headerTestSuite struct { + suite.Suite +} + +func TestHeaderTestSuite(t *testing.T) { + suite.Run(t, new(headerTestSuite)) +} + +func (s *headerTestSuite) TestHeaderWrite() { + for _, test := range headerWriteTests { + err := test.h.Write(io.Discard) + s.NoError(err) + err = test.h.Write(BadWriter{err: errors.New("fake error")}) + if len(test.h) > 0 { + s.EqualErrorf(err, "fake error", "Write: got %v, want %v", err, "fake error") + } else { + s.NoErrorf(err, "Write: got %v", err) + } + } +} + +func (s *headerTestSuite) TestHeaderWriteBytes() { for i, test := range headerWriteTests { - _ = test.h.WriteBytes(&buf) - assert.Equalf(t, test.expected, buf.String(), "#%d:\n got: %q\nwant: %q", i, buf.String(), test.expected) + var buf bytes.Buffer + + n, err := test.h.WriteBytes(&buf) + w := buf.String() + s.Lenf(w, n, "#%d: WriteBytes: got %d, want %d", i, n, len(w)) + s.NoErrorf(err, "#%d: WriteBytes: got %v", i, err) + s.Equalf(test.expected, w, "#%d: WriteBytes: got %q, want %q", i, w, test.expected) buf.Reset() } } -func TestHeaderWrite(t *testing.T) { - for _, test := range headerWriteTests { - _ = test.h.Write(io.Discard) +func (s *headerTestSuite) TestHeaderWriteString() { + sw := stringWriter{io.Discard} + + for i, test := range headerWriteTests { + expected := test.h.Get("Content-Type") + n, err := sw.WriteString(expected) + s.NoErrorf(err, "#%d: WriteString: %v", i, err) + s.Equalf(len(expected), n, "#%d: WriteString: got %d, want %d", i, n, len(expected)) } } -func TestHeaderSetGet(t *testing.T) { +func (s *headerTestSuite) TestHeaderSetGet() { h := Header{ "Custom": "Value", } h.Add("Other", "Value") value := h.Get("Other") - assert.Equalf(t, "Value", value, "got: %s, want: %s\n", value, "Value") + s.Equalf("Value", value, "got: %s, want: %s\n", value, "Value") +} + +func (s *headerTestSuite) TestHeaderDel() { + for i, test := range headerWriteTests { + // we clone it because we are modifying the original + headerCopy := test.h.Clone() + expected := headerCopy.Get("Content-Type") + if expected != "" { + headerCopy.Del("Content-Type") + value := headerCopy.Get("Content-Type") + s.Equalf("", value, "#%d: got: %s, want: %s\n", i, value, "") + } + } } -func TestHeaderClone(t *testing.T) { +func (s *headerTestSuite) TestHeaderClone() { h := Header{ "Custom": "Value", } @@ -83,7 +135,7 @@ func TestHeaderClone(t *testing.T) { value := clone.Get("Custom") - assert.Equalf(t, "Value", value, "got: %s, want: %s\n", value, "Value") + s.Equalf("Value", value, "got: %s, want: %s\n", value, "Value") } diff --git a/ftwhttp/request.go b/ftwhttp/request.go index 9e3d9bf..69ab888 100644 --- a/ftwhttp/request.go +++ b/ftwhttp/request.go @@ -170,25 +170,32 @@ func buildRequest(r *Request) ([]byte, error) { r.AddStandardHeaders() } - err = r.Headers().WriteBytes(&b) + _, err := r.Headers().WriteBytes(&b) if err != nil { log.Debug().Msgf("ftw/http: error writing to buffer: %s", err.Error()) return nil, err } // TODO: handle cookies - // if c.Jar != nil { - // for _, cookie := range c.Jar.Cookies(req.URL) { + // if client.Jar != nil { + // for _, cookie := range client.Jar.Cookies(req.URL) { // req.AddCookie(cookie) // } // } // After headers, we need one blank line _, err = fmt.Fprintf(&b, "\r\n") - + if err != nil { + log.Debug().Msgf("ftw/http: error writing to buffer: %s", err.Error()) + return nil, err + } // Now the body, if anything if utils.IsNotEmpty(r.data) { _, err = fmt.Fprintf(&b, "%s", r.data) + if err != nil { + log.Debug().Msgf("ftw/http: error writing to buffer: %s", err.Error()) + return nil, err + } } } else { dumpRawData(&b, r.raw) diff --git a/ftwhttp/request_test.go b/ftwhttp/request_test.go index 85f2245..dc70513 100644 --- a/ftwhttp/request_test.go +++ b/ftwhttp/request_test.go @@ -4,90 +4,98 @@ import ( "errors" "testing" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" ) +type requestTestSuite struct { + suite.Suite +} + +func TestRequestTestSuite(t *testing.T) { + suite.Run(t, new(requestTestSuite)) +} + func generateBaseRequestForTesting() *Request { var req *Request rl := &RequestLine{ Method: "UNEXISTENT", URI: "/this/path", - Version: "1.4", + Version: "HTTP/1.4", } - h := Header{"This": "Header", "Connection": "Not-Closed"} + h := Header{"Host": "localhost", "This": "Header", "Connection": "Not-Closed"} req = NewRequest(rl, h, []byte("Data"), true) return req } -func TestAddStandardHeadersWhenConnectionHeaderIsPresent(t *testing.T) { +func (s *requestTestSuite) TestAddStandardHeadersWhenConnectionHeaderIsPresent() { req := NewRequest(&RequestLine{}, Header{"Connection": "Not-Closed"}, []byte("Data"), true) req.AddStandardHeaders() - assert.Equal(t, req.headers.Get("Connection"), "Not-Closed") + s.Equal(req.headers.Get("Connection"), "Not-Closed") } -func TestAddStandardHeadersWhenConnectionHeaderIsEmpty(t *testing.T) { +func (s *requestTestSuite) TestAddStandardHeadersWhenConnectionHeaderIsEmpty() { req := NewRequest(&RequestLine{}, Header{}, []byte("Data"), true) req.AddStandardHeaders() - assert.Equal(t, req.headers.Get("Connection"), "close") + s.Equal(req.headers.Get("Connection"), "close") } -func TestAddStandardHeadersWhenNoData(t *testing.T) { +func (s *requestTestSuite) TestAddStandardHeadersWhenNoData() { req := NewRequest(&RequestLine{Method: "GET"}, Header{}, []byte(""), true) req.AddStandardHeaders() - assert.Equal(t, req.headers.Get("Content-Length"), "") + s.Equal(req.headers.Get("Content-Length"), "") } -func TestAddStandardHeadersWhenGetMethod(t *testing.T) { +func (s *requestTestSuite) TestAddStandardHeadersWhenGetMethod() { req := NewRequest(&RequestLine{Method: "GET"}, Header{}, []byte("Data"), true) req.AddStandardHeaders() - assert.Equal(t, req.headers.Get("Content-Length"), "4") + s.Equal(req.headers.Get("Content-Length"), "4") } -func TestAddStandardHeadersWhenPostMethod(t *testing.T) { +func (s *requestTestSuite) TestAddStandardHeadersWhenPostMethod() { req := NewRequest(&RequestLine{Method: "POST"}, Header{}, []byte("Data"), true) req.AddStandardHeaders() - assert.Equal(t, req.headers.Get("Content-Length"), "4") + s.Equal(req.headers.Get("Content-Length"), "4") } -func TestAddStandardHeadersWhenPutMethod(t *testing.T) { +func (s *requestTestSuite) TestAddStandardHeadersWhenPutMethod() { req := NewRequest(&RequestLine{Method: "PUT"}, Header{}, []byte("Data"), true) req.AddStandardHeaders() - assert.Equal(t, req.headers.Get("Content-Length"), "4") + s.Equal(req.headers.Get("Content-Length"), "4") } -func TestAddStandardHeadersWhenPatchMethod(t *testing.T) { +func (s *requestTestSuite) TestAddStandardHeadersWhenPatchMethod() { req := NewRequest(&RequestLine{Method: "PATCH"}, Header{}, []byte("Data"), true) req.AddStandardHeaders() - assert.Equal(t, req.headers.Get("Content-Length"), "4") + s.Equal(req.headers.Get("Content-Length"), "4") } -func TestAddStandardHeadersWhenDeleteMethod(t *testing.T) { +func (s *requestTestSuite) TestAddStandardHeadersWhenDeleteMethod() { req := NewRequest(&RequestLine{Method: "DELETE"}, Header{}, []byte("Data"), true) req.AddStandardHeaders() - assert.Equal(t, req.headers.Get("Content-Length"), "4") + s.Equal(req.headers.Get("Content-Length"), "4") } -func TestMultipartFormDataRequest(t *testing.T) { +func (s *requestTestSuite) TestMultipartFormDataRequest() { var req *Request rl := &RequestLine{ @@ -109,7 +117,7 @@ Some-file-test-here ----------397236876--`) req = NewRequest(rl, h, data, true) - assert.False(t, req.isRaw()) + s.False(req.isRaw()) } func generateBaseRawRequestForTesting() *Request { @@ -127,7 +135,7 @@ User-Agent: ModSecurity CRS 3 Tests return req } -func TestGenerateBaseRawRequestForTesting(t *testing.T) { +func (s *requestTestSuite) TestGenerateBaseRawRequestForTesting() { var req *Request raw := []byte(`POST / HTTP/1.1 @@ -139,41 +147,41 @@ User-Agent: ModSecurity CRS 3 Tests `) req = NewRawRequest(raw, false) - assert.False(t, req.autoCompleteHeaders) + s.False(req.autoCompleteHeaders) } -func TestRequestLine(t *testing.T) { +func (s *requestTestSuite) TestRequestLine() { rl := &RequestLine{ Method: "UNEXISTENT", URI: "/this/path", Version: "1.4", } - s := rl.ToString() + str := rl.ToString() - assert.Equal(t, "UNEXISTENT /this/path 1.4\r\n", s) + s.Equal("UNEXISTENT /this/path 1.4\r\n", str) } -func TestDestination(t *testing.T) { +func (s *requestTestSuite) TestDestination() { d := &Destination{ DestAddr: "192.168.1.1", Port: 443, Protocol: "https", } - assert.Equal(t, "192.168.1.1", d.DestAddr) - assert.Equal(t, 443, d.Port) - assert.Equal(t, "https", d.Protocol) + s.Equal("192.168.1.1", d.DestAddr) + s.Equal(443, d.Port) + s.Equal("https", d.Protocol) } -func TestRequestNew(t *testing.T) { +func (s *requestTestSuite) TestRequestNew() { req := generateBaseRequestForTesting() head := req.Headers() - assert.Equal(t, "Header", head.Get("This")) - assert.Equal(t, []byte("Data"), req.Data(), "Failed to set data") + s.Equal("Header", head.Get("This")) + s.Equal([]byte("Data"), req.Data(), "Failed to set data") } -func TestWithAutocompleteRequest(t *testing.T) { +func (s *requestTestSuite) TestWithAutocompleteRequest() { var req *Request rl := &RequestLine{ @@ -187,10 +195,10 @@ func TestWithAutocompleteRequest(t *testing.T) { data := []byte(`test=me&one=two`) req = NewRequest(rl, h, data, true) - assert.True(t, req.WithAutoCompleteHeaders(), "Set Autocomplete headers error ") + s.True(req.WithAutoCompleteHeaders(), "Set Autocomplete headers error ") } -func TestWithoutAutocompleteRequest(t *testing.T) { +func (s *requestTestSuite) TestWithoutAutocompleteRequest() { var req *Request rl := &RequestLine{ @@ -204,92 +212,87 @@ func TestWithoutAutocompleteRequest(t *testing.T) { data := []byte(`test=me&one=two`) req = NewRequest(rl, h, data, false) - assert.False(t, req.WithAutoCompleteHeaders(), "Set Autocomplete headers error ") + s.False(req.WithAutoCompleteHeaders(), "Set Autocomplete headers error ") } -func TestRequestHeadersSet(t *testing.T) { +func (s *requestTestSuite) TestRequestHeadersSet() { req := generateBaseRequestForTesting() newH := Header{"X-New-Header": "Value"} req.SetHeaders(newH) - if req.headers.Get("X-New-Header") == "Value" { - t.Logf("Success !") - } else { - t.Errorf("Failed !") - } - + s.Equal("Value", req.headers.Get("X-New-Header"), "Failed to set headers") req.AddHeader("X-New-Header2", "Value") head := req.Headers() - assert.Equal(t, "Value", head.Get("X-New-Header2")) + s.Equal("Value", head.Get("X-New-Header2")) } -func TestRequestAutoCompleteHeaders(t *testing.T) { +func (s *requestTestSuite) TestRequestAutoCompleteHeaders() { req := generateBaseRequestForTesting() req.SetAutoCompleteHeaders(true) - assert.True(t, req.WithAutoCompleteHeaders(), "Set Autocomplete headers error ") + s.True(req.WithAutoCompleteHeaders(), "Set Autocomplete headers error ") } -func TestRequestData(t *testing.T) { +func (s *requestTestSuite) TestRequestData() { req := generateBaseRequestForTesting() err := req.SetData([]byte("This is the data now")) - assert.NoError(t, err) - assert.Equal(t, []byte("This is the data now"), req.Data(), "failed to set data") + s.NoError(err) + s.Equal([]byte("This is the data now"), req.Data(), "failed to set data") } -func TestRequestSettingRawDataWhenThereIsData(t *testing.T) { +func (s *requestTestSuite) TestRequestSettingRawDataWhenThereIsData() { req := generateBaseRequestForTesting() err := req.SetRawData([]byte("This is the data now")) expectedError := errors.New("ftw/http: data field is already present in this request") - assert.Error(t, err) - assert.Equal(t, expectedError, err) + s.Error(err) + s.Equal(expectedError, err) } -func TestRequestRawData(t *testing.T) { +func (s *requestTestSuite) TestRequestRawData() { req := generateBaseRawRequestForTesting() err := req.SetRawData([]byte("This is the RAW data now")) - assert.NoError(t, err) + s.NoError(err) - assert.Equal(t, []byte("This is the RAW data now"), req.RawData()) + s.Equal([]byte("This is the RAW data now"), req.RawData()) } -func TestRequestSettingDataaWhenThereIsRawData(t *testing.T) { +func (s *requestTestSuite) TestRequestSettingDataaWhenThereIsRawData() { req := generateBaseRawRequestForTesting() err := req.SetData([]byte("This is the data now")) expectedError := errors.New("ftw/http: raw field is already present in this request") - assert.Error(t, err) - assert.Equal(t, expectedError, err) + s.Error(err) + s.Equal(expectedError, err) } -func TestRequestURLParse(t *testing.T) { +func (s *requestTestSuite) TestRequestURLParse() { req := generateBaseRequestForTesting() h := req.Headers() h.Add(ContentTypeHeader, "application/x-www-form-urlencoded") // Test adding semicolons to test parse err := req.SetData([]byte("test=This&test=nothing")) - assert.NoError(t, err) + s.NoError(err) } -func TestRequestURLParseFail(t *testing.T) { +func (s *requestTestSuite) TestRequestURLParseFail() { req := generateBaseRequestForTesting() h := req.Headers() h.Add(ContentTypeHeader, "application/x-www-form-urlencoded") // Test adding semicolons to test parse err := req.SetData([]byte("test=This&that=but with;;;;;; data now")) - assert.NoError(t, err) + s.NoError(err) } -func TestRequestEncodesPostData(t *testing.T) { +func (s *requestTestSuite) TestRequestEncodesPostData() { tests := []struct { raw string encoded string @@ -312,8 +315,8 @@ func TestRequestEncodesPostData(t *testing.T) { }, { // Test adding semicolons to test parse - raw: `c4= ;c3=t;c2=a;c1=c;a1=/;a2=e;a3=t;a4=c;a5=/;a6=p;a7=a;a8=s;a9=s;a10=w;a11=d;$c1$c2$c3$c4$a1$a2$a3$a4$a5$a6$a7$a8$a9$a10$a11`, - encoded: "c4=+%3Bc3%3Dt%3Bc2%3Da%3Bc1%3Dc%3Ba1%3D%2F%3Ba2%3De%3Ba3%3Dt%3Ba4%3Dc%3Ba5%3D%2F%3Ba6%3Dp%3Ba7%3Da%3Ba8%3Ds%3Ba9%3Ds%3Ba10%3Dw%3Ba11%3Dd%3B%24c1%24c2%24c3%24c4%24a1%24a2%24a3%24a4%24a5%24a6%24a7%24a8%24a9%24a10%24a11", + raw: `c4= ;c3=t;c2=a;c1=client;a1=/;a2=e;a3=t;a4=client;a5=/;a6=p;a7=a;a8=s;a9=s;a10=w;a11=d;$c1$c2$c3$c4$a1$a2$a3$a4$a5$a6$a7$a8$a9$a10$a11`, + encoded: `c4=+%3Bc3%3Dt%3Bc2%3Da%3Bc1%3Dclient%3Ba1%3D%2F%3Ba2%3De%3Ba3%3Dt%3Ba4%3Dclient%3Ba5%3D%2F%3Ba6%3Dp%3Ba7%3Da%3Ba8%3Ds%3Ba9%3Ds%3Ba10%3Dw%3Ba11%3Dd%3B%24c1%24c2%24c3%24c4%24a1%24a2%24a3%24a4%24a5%24a6%24a7%24a8%24a9%24a10%24a11`, }, { // Already encoded @@ -324,25 +327,19 @@ func TestRequestEncodesPostData(t *testing.T) { for _, tc := range tests { tt := tc - t.Run(tt.raw, func(t *testing.T) { + s.Run(tt.raw, func() { req := generateBaseRequestForTesting() h := req.Headers() h.Add(ContentTypeHeader, "application/x-www-form-urlencoded") err := req.SetData([]byte(tt.raw)) - if err != nil { - t.Errorf("Failed !") - } + s.NoError(err) result, err := encodeDataParameters(h, req.Data()) - if err != nil { - t.Errorf("Failed to encode %s", req.Data()) - } + s.NoError(err, "Failed to encode %s", req.Data()) expected := tt.encoded actual := string(result) - if actual != expected { - t.Errorf("Unexpected URL encoded payload, expected %s, got %s", expected, actual) - } + s.Equal(expected, actual, "Unexpected URL encoded payload") }) } } diff --git a/ftwhttp/response_test.go b/ftwhttp/response_test.go index 1269ac7..81f6c50 100644 --- a/ftwhttp/response_test.go +++ b/ftwhttp/response_test.go @@ -8,9 +8,19 @@ import ( "testing" "time" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" ) +type responseTestSuite struct { + suite.Suite + client *Client + ts *httptest.Server +} + +func TestHResponseTestSuite(t *testing.T) { + suite.Run(t, new(responseTestSuite)) +} + func generateRequestForTesting(keepalive bool) *Request { var req *Request var connection string @@ -58,114 +68,103 @@ func generateRequestWithCookiesForTesting() *Request { return req } -// Error checking omitted for brevity -func testServer() (server *httptest.Server) { - - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, "Hello, client") - })) - - return ts +func (s *responseTestSuite) helloClient(w http.ResponseWriter, r *http.Request) { + n, err := fmt.Fprintln(w, "Hello, client") + s.NoError(err) + s.Equal(14, n) } -// Error checking omitted for brevity -func testEchoServer(t *testing.T) (server *httptest.Server) { - - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("X-Powered-By", "go-ftw") - w.WriteHeader(http.StatusOK) - resp := new(bytes.Buffer) - for key, value := range r.Header { - _, err := fmt.Fprintf(resp, "%s=%s,", key, value) - assert.NoError(t, err) - } - - _, err := w.Write(resp.Bytes()) - assert.NoError(t, err) - })) - - return ts +func (s *responseTestSuite) testEchoServer(w http.ResponseWriter, r *http.Request) { + w.Header().Set("X-Powered-By", "go-ftw") + w.WriteHeader(http.StatusOK) + resp := new(bytes.Buffer) + for key, value := range r.Header { + _, err := fmt.Fprintf(resp, "%s=%s,", key, value) + s.NoError(err) + } + _, err := w.Write(resp.Bytes()) + s.NoError(err) } -func testServerWithCookies() (server *httptest.Server) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - expiration := time.Now().Add(365 * 24 * time.Hour) - cookie := http.Cookie{Name: "username", Value: "go-ftw", Expires: expiration} - http.SetCookie(w, &cookie) - fmt.Fprintln(w, "Setting Cookies!") - })) +func (s *responseTestSuite) responseWithCookies(w http.ResponseWriter, r *http.Request) { + expiration := time.Now().Add(365 * 24 * time.Hour) + cookie := http.Cookie{Name: "username", Value: "go-ftw", Expires: expiration} + http.SetCookie(w, &cookie) + n, err := fmt.Fprintln(w, "Setting Cookies!") + s.NoError(err) + s.Equal(17, n) +} - return ts +func (s *responseTestSuite) SetupTest() { + var err error + s.client, err = NewClient(NewClientConfig()) + s.NoError(err) } -func TestResponse(t *testing.T) { - server := testServer() +func (s *responseTestSuite) TearDownTest() { + s.ts.Close() +} - defer server.Close() +func (s *responseTestSuite) BeforeTest(_, testName string) { + var f http.HandlerFunc + switch testName { + case "TestResponse": + f = s.helloClient + case "TestResponseWithCookies": + f = s.responseWithCookies + case "TestResponseChecksFullResponse": + f = s.testEchoServer + default: + f = s.testEchoServer + } + s.ts = httptest.NewServer(f) +} - d, err := DestinationFromString(server.URL) - assert.NoError(t, err) +func (s *responseTestSuite) TestResponse() { + d, err := DestinationFromString(s.ts.URL) + s.NoError(err) req := generateRequestForTesting(true) - client, err := NewClient(NewClientConfig()) - assert.NoError(t, err) - err = client.NewConnection(*d) - assert.NoError(t, err) + err = s.client.NewConnection(*d) + s.NoError(err) - response, err := client.Do(*req) - assert.NoError(t, err) + response, err := s.client.Do(*req) + s.NoError(err) - assert.Contains(t, response.GetFullResponse(), "Hello, client\n") + s.Contains(response.GetFullResponse(), "Hello, client\n") } -func TestResponseWithCookies(t *testing.T) { - server := testServerWithCookies() - - defer server.Close() - - d, err := DestinationFromString(server.URL) - assert.NoError(t, err) +func (s *responseTestSuite) TestResponseWithCookies() { + d, err := DestinationFromString(s.ts.URL) + s.NoError(err) req := generateRequestForTesting(true) - client, err := NewClient(NewClientConfig()) - assert.NoError(t, err) - err = client.NewConnection(*d) - - assert.NoError(t, err) - - response, err := client.Do(*req) + err = s.client.NewConnection(*d) + s.NoError(err) - assert.NoError(t, err) + response, err := s.client.Do(*req) + s.NoError(err) - assert.Contains(t, response.GetFullResponse(), "Setting Cookies!\n") + s.Contains(response.GetFullResponse(), "Setting Cookies!\n") cookiereq := generateRequestWithCookiesForTesting() - _, err = client.Do(*cookiereq) - - assert.NoError(t, err) + _, err = s.client.Do(*cookiereq) + s.NoError(err) } -func TestResponseChecksFullResponse(t *testing.T) { - server := testEchoServer(t) - - defer server.Close() - - d, err := DestinationFromString(server.URL) - assert.NoError(t, err) +func (s *responseTestSuite) TestResponseChecksFullResponse() { + d, err := DestinationFromString(s.ts.URL) + s.NoError(err) req := generateRequestForTesting(true) - client, err := NewClient(NewClientConfig()) - assert.NoError(t, err) - err = client.NewConnection(*d) - - assert.NoError(t, err) - - response, err := client.Do(*req) + err = s.client.NewConnection(*d) + s.NoError(err) - assert.NoError(t, err) + response, err := s.client.Do(*req) + s.NoError(err) - assert.Contains(t, response.GetFullResponse(), "X-Powered-By: go-ftw") - assert.Contains(t, response.GetFullResponse(), "User-Agent=[Go Tests]") + s.Contains(response.GetFullResponse(), "X-Powered-By: go-ftw") + s.Contains(response.GetFullResponse(), "User-Agent=[Go Tests]") } diff --git a/ftwhttp/types.go b/ftwhttp/types.go index de29c47..7682927 100644 --- a/ftwhttp/types.go +++ b/ftwhttp/types.go @@ -1,6 +1,7 @@ package ftwhttp import ( + "crypto/x509" "net" "net/http" "time" @@ -12,6 +13,8 @@ type ClientConfig struct { ConnectTimeout time.Duration // ReadTimeout is the timeout for reading a response. ReadTimeout time.Duration + // RootCAs is the set of root CA certificates that is used to verify server + RootCAs *x509.CertPool } // Client is the top level abstraction in http diff --git a/main.go b/main.go index 9313e84..1a30281 100644 --- a/main.go +++ b/main.go @@ -3,6 +3,8 @@ package main import ( + "context" + "errors" "fmt" "os" _ "time/tzdata" @@ -28,10 +30,15 @@ func main() { // Default level for this example is info, unless debug flag is present zerolog.SetGlobalLevel(zerolog.InfoLevel) - cmd.Execute( + err := cmd.Execute( buildVersion(version, commit, date, builtBy), ) + if errors.Is(err, context.DeadlineExceeded) { + os.Exit(2) + } else if err != nil { + os.Exit(1) + } } func buildVersion(version, commit, date, builtBy string) string { diff --git a/output/output_test.go b/output/output_test.go index bded3c2..a3c9415 100644 --- a/output/output_test.go +++ b/output/output_test.go @@ -3,6 +3,8 @@ package output import ( "bytes" "testing" + + "github.com/stretchr/testify/suite" ) var testString = "test" @@ -25,41 +27,44 @@ var outputTest = []struct { {"json", `{"level":"notice","message":"This is the test"}`}, } -func TestOutput(t *testing.T) { +type outputTestSuite struct { + suite.Suite +} + +func TestOutputTestSuite(t *testing.T) { + suite.Run(t, new(outputTestSuite)) +} + +func (s *outputTestSuite) TestOutput() { var b bytes.Buffer for i, test := range outputTest { o := NewOutput(test.oType, &b) - if err := o.Printf(format, testString); err != nil { - t.Fatalf("Error! in test %d", i) - } + err := o.Printf(format, testString) + s.NoError(err, "Error! in test %d", i) } } -func TestNormalCatalogOutput(t *testing.T) { +func (s *outputTestSuite) TestNormalCatalogOutput() { var b bytes.Buffer normal := NewOutput("normal", &b) for _, v := range normalCatalog { normal.RawPrint(v) - if b.String() != v { - t.Error("output is not equal") - } + s.Equal(b.String(), v, "output is not equal") // reset buffer b.Reset() } } -func TestPlainCatalogOutput(t *testing.T) { +func (s *outputTestSuite) TestPlainCatalogOutput() { var b bytes.Buffer normal := NewOutput("normal", &b) for _, v := range createPlainCatalog(normalCatalog) { normal.RawPrint(v) - if b.String() != v { - t.Error("plain output is not equal") - } + s.Equal(b.String(), v, "output is not equal") // reset buffer b.Reset() } diff --git a/runner/run.go b/runner/run.go index 6895c80..27a714f 100644 --- a/runner/run.go +++ b/runner/run.go @@ -93,7 +93,10 @@ func RunTest(runContext *TestRunContext, ftwTest test.FTWTest) error { } // Iterate over stages for _, stage := range testCase.Stages { - ftwCheck := check.NewCheck(runContext.Config) + ftwCheck, err := check.NewCheck(runContext.Config) + if err != nil { + return err + } if err := RunStage(runContext, ftwCheck, testCase, stage.Stage); err != nil { return err } diff --git a/runner/run_cloud_test.go b/runner/run_cloud_test.go new file mode 100644 index 0000000..1fd3d5e --- /dev/null +++ b/runner/run_cloud_test.go @@ -0,0 +1,112 @@ +package runner + +import ( + "fmt" + "net/http" + "net/http/httptest" + "os" + "strconv" + "testing" + "text/template" + + "github.com/coreruleset/go-ftw/config" + "github.com/coreruleset/go-ftw/ftwhttp" + "github.com/coreruleset/go-ftw/output" + "github.com/coreruleset/go-ftw/test" + "github.com/rs/zerolog/log" + "github.com/stretchr/testify/suite" +) + +type runCloudTestSuite struct { + suite.Suite + cfg *config.FTWConfiguration + ftwTests []test.FTWTest + out *output.Output + ts *httptest.Server + dest *ftwhttp.Destination + tempFileName string +} + +func TestRunCloudTestSuite(t *testing.T) { + suite.Run(t, new(runCloudTestSuite)) +} + +func (s *runCloudTestSuite) SetupTest() { + s.newTestCloudServer() + s.out = output.NewOutput("normal", os.Stdout) +} + +func (s *runCloudTestSuite) TearDownTest() { + s.ts.Close() + if s.tempFileName != "" { + err := os.Remove(s.tempFileName) + s.NoError(err, "cannot remove test file") + s.tempFileName = "" + } +} + +func (s *runCloudTestSuite) BeforeTest(_ string, name string) { + var err error + + // if we have a destination for this test, use it + // else use the default destination + if s.dest == nil { + s.dest, err = ftwhttp.DestinationFromString(destinationMap[name]) + s.NoError(err) + } + + log.Info().Msgf("Using port %d and addr '%s'", s.dest.Port, s.dest.DestAddr) + + // set up variables for template + vars := map[string]interface{}{ + "TestPort": s.dest.Port, + "TestAddr": s.dest.DestAddr, + } + + s.cfg = config.NewCloudConfig() + // get tests template from file + tmpl, err := template.ParseFiles(fmt.Sprintf("testdata/%s.yaml", name)) + s.NoError(err) + // create a temporary file to hold the test + testFileContents, err := os.CreateTemp("testdata", "mock-test-*.yaml") + s.NoError(err, "cannot create temporary file") + err = tmpl.Execute(testFileContents, vars) + s.NoError(err, "cannot execute template") + // get tests from file + s.ftwTests, err = test.GetTestsFromFiles(testFileContents.Name()) + s.NoError(err, "cannot get tests from file") + // save the name of the temporary file so we can delete it later + s.tempFileName = testFileContents.Name() +} + +// Error checking omitted for brevity +func (s *runCloudTestSuite) newTestCloudServer() { + var err error + + s.ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + statusCode := http.StatusOK + if r.URL.Path != "/" { + statusCode, err = strconv.Atoi(r.URL.Path[1:]) + if err != nil { + statusCode = http.StatusBadRequest + } + log.Debug().Msgf("Mock cloud server returning status code: %d", statusCode) + } + w.WriteHeader(statusCode) + _, _ = w.Write([]byte("Hello, client")) + })) + + s.dest, err = ftwhttp.DestinationFromString((s.ts).URL) + s.Require().NoError(err, "cannot get destination from string") +} + +func (s *runCloudTestSuite) TestCloudRun() { + s.Run("don't show time and execute all", func() { + res, err := Run(s.cfg, s.ftwTests, RunnerConfig{ + ShowTime: true, + Output: output.Quiet, + }, s.out) + s.NoError(err) + s.Equalf(res.Stats.TotalFailed(), 0, "Oops, %d tests failed to run!", res.Stats.TotalFailed()) + }) +} diff --git a/runner/run_input_override_test.go b/runner/run_input_override_test.go new file mode 100644 index 0000000..5363725 --- /dev/null +++ b/runner/run_input_override_test.go @@ -0,0 +1,319 @@ +package runner + +import ( + "bytes" + "errors" + "runtime" + "strconv" + "strings" + "testing" + "text/template" + + "github.com/Masterminds/sprig" + "github.com/coreruleset/go-ftw/config" + "github.com/coreruleset/go-ftw/ftwhttp" + "github.com/coreruleset/go-ftw/test" + "github.com/stretchr/testify/suite" +) + +type inputOverrideTestSuite struct { + suite.Suite + cfg *config.FTWConfiguration + logFilePath string +} + +var configTemplate = ` +--- +testoverride: + input: + {{ with .StopMagic }}stop_magic: {{ . }}{{ end }} + {{ with .BrokenConfig }}this_does_not_exist: "test"{{ end }} + {{ with .Port }}port: {{ . }}{{ end }} + {{ with .DestAddr }}dest_addr: {{ . }}{{ end }} + {{ with .Version }}version: {{ . }}{{ end }} + {{ with .URI }}uri: {{ . }}{{ end }} + {{ with .Method }}method: {{ . }}{{ end }} + {{ with .Protocol }}protocol: {{ . }}{{ end }} + {{ with .Data }}data: {{ . }}{{ end }} + {{ with .EncodedRequest }}encoded_request: {{ . }}{{ end }} + {{ with .RawRequest }}raw_request: {{ . }}{{ end }} + {{ with .Headers }} + headers: + {{ with .Host }}Host: {{ . }}{{ end }} + {{ with .UniqueID }}unique_id: {{ . }}{{ end }} + {{ end }} + {{ with .OverrideEmptyHostHeader }}override_empty_host_header: {{ . }}{{ end }} +` + +var overrideConfigMap = map[string]interface{}{ + "TestSetHostFromDestAddr": map[string]interface{}{ + "DestAddr": "address.org", + "Port": 80, + }, + "TestSetHostFromHostHeaderOverride": map[string]interface{}{ + "DestAddr": "wrong.org", + "Headers": map[string]string{ + "Host": "override.com", + }, + "OverrideEmptyHostHeader": true, + }, + "TestSetHeaderOverridingExistingOne": map[string]interface{}{ + "Headers": map[string]string{ + "Host": "address.org", + "UniqueID": "override", + }, + }, + "TestApplyInputOverrides": map[string]interface{}{ + "Headers": map[string]string{ + "Host": "address.org", + "UniqueID": "override", + }, + }, + "TestApplyInputOverrideURI": map[string]interface{}{ + "URI": "/override", + }, + "TestApplyInputOverrideVersion": map[string]interface{}{ + "Version": "HTTP/1.1", + }, + "TestApplyInputOverrideMethod": map[string]interface{}{ + "Method": "MERGE", + }, + "TestApplyInputOverrideData": map[string]interface{}{ + "Data": "override", + }, + "TestApplyInputOverrideEncodedRequest": map[string]interface{}{ + "EncodedRequest": "overrideb64", + }, + "TestApplyInputOverrideRAWRequest": map[string]interface{}{ + "RawRequest": "overrideraw", + }, + "TestApplyInputOverrideProtocol": map[string]interface{}{ + "Protocol": "HTTP/1.1", + }, + "TestApplyInputOverrideStopMagic": map[string]interface{}{ + "StopMagic": "true", + }, +} + +// getOverrideConfigValue is useful to not repeat the text in the test itself +func getOverrideConfigValue(key string) (string, error) { + pc, _, _, ok := runtime.Caller(1) + details := runtime.FuncForPC(pc) + if ok && details != nil { + caller := strings.Split(details.Name(), ".") + name := caller[len(caller)-1] + if overrideConfigMap[name] == nil { + return "", errors.New("cannot get override config value: be sure the caller is a test function, and the key is correct") + } + if strings.Contains(key, ".") { + keyParts := strings.Split(key, ".") + return overrideConfigMap[name].(map[string]interface{})[keyParts[0]].(map[string]string)[keyParts[1]], nil + } + return overrideConfigMap[name].(map[string]interface{})[key].(string), nil + } + return "", errors.New("failed to determine calling function") +} + +func TestInputOverrideTestSuite(t *testing.T) { + suite.Run(t, new(inputOverrideTestSuite)) +} + +func (s *inputOverrideTestSuite) SetupTest() { +} + +func (s *inputOverrideTestSuite) BeforeTest(_ string, name string) { + var err error + + // set up configuration from template + tmpl := template.New("input-override").Funcs(sprig.TxtFuncMap()) + configTmpl, err := tmpl.Parse(configTemplate) + s.NoError(err, "cannot parse template") + buf := &bytes.Buffer{} + err = configTmpl.Execute(buf, overrideConfigMap[name]) + s.NoError(err, "cannot execute template") + s.cfg, err = config.NewConfigFromString(buf.String()) + s.NoError(err, "cannot get config from string") + if s.logFilePath != "" { + s.cfg.WithLogfile(s.logFilePath) + } +} + +func (s *inputOverrideTestSuite) TestSetHostFromDestAddr() { + originalHost := "original.com" + overrideHost, err := getOverrideConfigValue("DestAddr") + s.NoError(err, "cannot get override value") + + testInput := test.Input{ + DestAddr: &originalHost, + } + cfg := &config.FTWConfiguration{ + TestOverride: config.FTWTestOverride{ + Overrides: test.Overrides{ + DestAddr: &overrideHost, + OverrideEmptyHostHeader: true, + }, + }, + } + + err = applyInputOverride(cfg.TestOverride, &testInput) + s.NoError(err, "Failed to apply input overrides") + + s.Equal(overrideHost, *testInput.DestAddr, "`dest_addr` should have been overridden") + + s.NotNil(testInput.Headers, "Header map must exist after overriding `dest_addr`") + + hostHeader := testInput.Headers.Get("Host") + s.NotEqual("", hostHeader, "Host header must be set after overriding `dest_addr`") + s.Equal(overrideHost, hostHeader, "Host header must be identical to `dest_addr` after overrding `dest_addr`") +} + +func (s *inputOverrideTestSuite) TestSetHostFromHostHeaderOverride() { + originalDestAddr := "original.com" + overrideHostHeader, err := getOverrideConfigValue("Headers.Host") + s.NoError(err, "cannot get override value") + + testInput := test.Input{ + DestAddr: &originalDestAddr, + } + + err = applyInputOverride(s.cfg.TestOverride, &testInput) + s.NoError(err, "Failed to apply input overrides") + + hostHeader := testInput.Headers.Get("Host") + s.NotEqual("", hostHeader, "Host header must be set after overriding the `Host` header") + if hostHeader == overrideHostHeader { + s.Equal(overrideHostHeader, hostHeader, "Host header override must take precence over OverrideEmptyHostHeader") + } else { + s.Equal(overrideHostHeader, hostHeader, "Host header must be identical to overridden `Host` header.") + } +} + +func (s *inputOverrideTestSuite) TestSetHeaderOverridingExistingOne() { + originalHeaderValue := "original" + overrideHeaderValue, err := getOverrideConfigValue("Headers.UniqueID") + s.NoError(err, "cannot get override value") + + testInput := test.Input{ + Headers: ftwhttp.Header{"unique_id": originalHeaderValue}, + } + + s.NotNil(testInput.Headers, "Header map must exist before overriding any header") + + err = applyInputOverride(s.cfg.TestOverride, &testInput) + s.NoError(err, "Failed to apply input overrides") + + overriddenHeader := testInput.Headers.Get("unique_id") + s.NotEqual("", overriddenHeader, "unique_id header must be set after overriding it") + s.Equal(overrideHeaderValue, overriddenHeader, "Host header must be identical to overridden `Host` header.") +} + +func (s *inputOverrideTestSuite) TestApplyInputOverrides() { + originalHeaderValue := "original" + overrideHeaderValue, err := getOverrideConfigValue("Headers.UniqueID") + s.NoError(err, "cannot get override value") + + testInput := test.Input{ + Headers: ftwhttp.Header{"unique_id": originalHeaderValue}, + } + + s.NotNil(testInput.Headers, "Header map must exist before overriding any header") + + err = applyInputOverride(s.cfg.TestOverride, &testInput) + s.NoError(err, "Failed to apply input overrides") + + overriddenHeader := testInput.Headers.Get("unique_id") + s.NotEqual("", overriddenHeader, "unique_id header must be set after overriding it") + s.Equal(overrideHeaderValue, overriddenHeader, "Host header must be identical to overridden `Host` header.") +} + +func (s *inputOverrideTestSuite) TestApplyInputOverrideURI() { + originalURI := "/original" + overrideURI, err := getOverrideConfigValue("URI") + s.NoError(err, "cannot get override value") + + testInput := test.Input{ + URI: &originalURI, + } + + err = applyInputOverride(s.cfg.TestOverride, &testInput) + s.NoError(err, "Failed to apply input overrides") + s.Equal(overrideURI, *testInput.URI, "`URI` should have been overridden") +} + +func (s *inputOverrideTestSuite) TestApplyInputOverrideVersion() { + originalVersion := "HTTP/0.9" + overrideVersion, err := getOverrideConfigValue("Version") + s.NoError(err, "cannot get override value") + + testInput := test.Input{ + Version: &originalVersion, + } + err = applyInputOverride(s.cfg.TestOverride, &testInput) + s.NoError(err, "Failed to apply input overrides") + s.Equal(overrideVersion, *testInput.Version, "`Version` should have been overridden") +} + +func (s *inputOverrideTestSuite) TestApplyInputOverrideMethod() { + originalMethod := "POST" + overrideMethod, err := getOverrideConfigValue("Method") + s.NoError(err, "cannot get override value") + + testInput := test.Input{ + Method: &originalMethod, + } + err = applyInputOverride(s.cfg.TestOverride, &testInput) + s.NoError(err, "Failed to apply input overrides") + s.Equal(overrideMethod, *testInput.Method, "`Method` should have been overridden") +} + +func (s *inputOverrideTestSuite) TestApplyInputOverrideData() { + originalData := "data" + overrideData, err := getOverrideConfigValue("Data") + s.NoError(err, "cannot get override value") + + testInput := test.Input{ + Data: &originalData, + } + err = applyInputOverride(s.cfg.TestOverride, &testInput) + s.NoError(err, "Failed to apply input overrides") + s.Equal(overrideData, *testInput.Data, "`Data` should have been overridden") +} + +func (s *inputOverrideTestSuite) TestApplyInputOverrideStopMagic() { + stopMagicBool, err := getOverrideConfigValue("StopMagic") + s.NoError(err, "cannot get override value") + overrideStopMagic, err := strconv.ParseBool(stopMagicBool) + s.NoError(err, "Failed to parse `StopMagic` override value") + testInput := test.Input{ + StopMagic: false, + } + err = applyInputOverride(s.cfg.TestOverride, &testInput) + s.NoError(err, "Failed to apply input overrides") + s.Equal(overrideStopMagic, testInput.StopMagic, "`StopMagic` should have been overridden") +} + +func (s *inputOverrideTestSuite) TestApplyInputOverrideEncodedRequest() { + originalEncodedRequest := "originalbase64" + overrideEncodedRequest, err := getOverrideConfigValue("EncodedRequest") + s.NoError(err, "cannot get override value") + testInput := test.Input{ + EncodedRequest: originalEncodedRequest, + } + err = applyInputOverride(s.cfg.TestOverride, &testInput) + s.NoError(err, "Failed to apply input overrides") + s.Equal(overrideEncodedRequest, testInput.EncodedRequest, "`EncodedRequest` should have been overridden") +} + +func (s *inputOverrideTestSuite) TestApplyInputOverrideRAWRequest() { + originalRAWRequest := "original" + overrideRAWRequest, err := getOverrideConfigValue("RawRequest") + s.NoError(err, "cannot get override value") + + testInput := test.Input{ + RAWRequest: originalRAWRequest, + } + + err = applyInputOverride(s.cfg.TestOverride, &testInput) + s.NoError(err, "Failed to apply input overrides") + s.Equal(overrideRAWRequest, testInput.RAWRequest, "`RAWRequest` should have been overridden") +} diff --git a/runner/run_test.go b/runner/run_test.go index 95ab8e0..6d7421d 100644 --- a/runner/run_test.go +++ b/runner/run_test.go @@ -1,1026 +1,346 @@ package runner import ( + "bytes" "fmt" "net/http" "net/http/httptest" "os" "regexp" "testing" - - "github.com/coreruleset/go-ftw/output" - - "github.com/stretchr/testify/assert" + "text/template" "github.com/rs/zerolog/log" + "github.com/stretchr/testify/suite" - "github.com/coreruleset/go-ftw/check" "github.com/coreruleset/go-ftw/config" "github.com/coreruleset/go-ftw/ftwhttp" + "github.com/coreruleset/go-ftw/output" "github.com/coreruleset/go-ftw/test" ) -var yamlConfig = ` ---- +var logText = `[Tue Jan 05 02:21:09.637165 2021] [:error] [pid 76:tid 139683434571520] [client 172.23.0.1:58998] [client 172.23.0.1] ModSecurity: Warning. Pattern match "\\\\b(?:keep-alive|close),\\\\s?(?:keep-alive|close)\\\\b" at REQUEST_HEADERS:Connection. [file "/etc/modsecurity.d/owasp-crs/rules/REQUEST-920-PROTOCOL-ENFORCEMENT.conf"] [line "339"] [id "920210"] [msg "Multiple/Conflicting Connection Header Data Found"] [data "close,close"] [severity "WARNING"] [ver "OWASP_CRS/3.3.0"] [tag "application-multi"] [tag "language-multi"] [tag "platform-multi"] [tag "attack-protocol"] [tag "paranoia-level/1"] [tag "OWASP_CRS"] [tag "capec/1000/210/272"] [hostname "localhost"] [uri "/"] [unique_id "X-PNFSe1VwjCgYRI9FsbHgAAAIY"] +[Tue Jan 05 02:21:09.637731 2021] [:error] [pid 76:tid 139683434571520] [client 172.23.0.1:58998] [client 172.23.0.1] ModSecurity: Warning. Match of "pm AppleWebKit Android" against "REQUEST_HEADERS:User-Agent" required. [file "/etc/modsecurity.d/owasp-crs/rules/REQUEST-920-PROTOCOL-ENFORCEMENT.conf"] [line "1230"] [id "920300"] [msg "Request Missing an Accept Header"] [severity "NOTICE"] [ver "OWASP_CRS/3.3.0"] [tag "application-multi"] [tag "language-multi"] [tag "platform-multi"] [tag "attack-protocol"] [tag "OWASP_CRS"] [tag "capec/1000/210/272"] [tag "PCI/6.5.10"] [tag "paranoia-level/2"] [hostname "localhost"] [uri "/"] [unique_id "X-PNFSe1VwjCgYRI9FsbHgAAAIY"] +[Tue Jan 05 02:21:09.638572 2021] [:error] [pid 76:tid 139683434571520] [client 172.23.0.1:58998] [client 172.23.0.1] ModSecurity: Warning. Operator GE matched 5 at TX:anomaly_score. [file "/etc/modsecurity.d/owasp-crs/rules/REQUEST-949-BLOCKING-EVALUATION.conf"] [line "91"] [id "949110"] [msg "Inbound Anomaly Score Exceeded (Total Score: 5)"] [severity "CRITICAL"] [ver "OWASP_CRS/3.3.0"] [tag "application-multi"] [tag "language-multi"] [tag "platform-multi"] [tag "attack-generic"] [hostname "localhost"] [uri "/"] [unique_id "X-PNFSe1VwjCgYRI9FsbHgAAAIY"] +[Tue Jan 05 02:21:09.647668 2021] [:error] [pid 76:tid 139683434571520] [client 172.23.0.1:58998] [client 172.23.0.1] ModSecurity: Warning. Operator GE matched 5 at TX:inbound_anomaly_score. [file "/etc/modsecurity.d/owasp-crs/rules/RESPONSE-980-CORRELATION.conf"] [line "87"] [id "980130"] [msg "Inbound Anomaly Score Exceeded (Total Inbound Score: 5 - SQLI=0,XSS=0,RFI=0,LFI=0,RCE=0,PHPI=0,HTTP=0,SESS=0): individual paranoia level scores: 3, 2, 0, 0"] [ver "OWASP_CRS/3.3.0"] [tag "event-correlation"] [hostname "localhost"] [uri "/"] [unique_id "X-PNFSe1VwjCgYRI9FsbHgAAAIY"]` + +var testConfigMap = map[string]string{ + "BaseConfig": `--- testoverride: ignore: "920400-1": "This test result must be ignored" -` - -var yamlConfigPortOverride = ` ---- -testoverride: - input: - dest_addr: "TEST_ADDR" - port: %d - protocol: "http" -` - -var yamlConfigEmptyHostHeaderOverride = ` ---- -testoverride: - input: - dest_addr: %s - headers: - Host: %s - override_empty_host_header: true -` - -var yamlConfigHostHeaderOverride = ` ---- +`, + "TestDisabledRun": `--- +mode: 'cloud' +`, + "TestBrokenOverrideRun": `--- testoverride: input: - dest_addr: address.org - headers: - unique_id: %s -` - -var yamlConfigHeaderOverride = ` ---- -testoverride: - input: - dest_addr: address.org - headers: - unique_id: %s -` - -var yamlConfigURIOverride = ` ---- -testoverride: - input: - uri: %s -` - -var yamlConfigVersionOverride = ` ---- + dest_addr: "{{ .TestAddr }}" + port: {{ .TestPort }} + this_does_not_exist: "test" +`, + "TestBrokenPortOverrideRun": `--- testoverride: input: - version: %s -` - -var yamlConfigMethodOverride = ` ---- + dest_addr: "{{ .TestAddr }}" + port: {{ .TestPort }} + protocol: "http"`, + "TestIgnoredTestsRun": `--- testoverride: - input: - method: %s -` - -var yamlConfigDataOverride = ` ---- + ignore: + "001": "This test result must be ignored" + forcefail: + "008": "This test should pass, but it is going to fail" + forcepass: + "099": "This test failed, but it shall pass!" +`, + "TestOverrideRun": `--- testoverride: input: - data: %s -` - -var yamlConfigStopMagicOverride = ` ---- + dest_addr: "{{ .TestAddr }}" + port: {{ .TestPort }} + protocol: "http" +`, + "TestApplyInputOverrideMethod": `--- testoverride: input: - stop_magic: %t -` - -var yamlConfigEncodedRequestOverride = ` ---- + method: %s +`, + "TestApplyInputOverrideData": `--- testoverride: input: - encoded_request: %s -` - -var yamlConfigRAWRequestOverride = ` ---- + data: %s +`, + "TestApplyInputOverrideStopMagic": `--- testoverride: input: - raw_request: %s -` - -var yamlConfigOverride = ` ---- + stop_magic: %t +`, + "TestApplyInputOverrideEncodedRequest": `--- testoverride: input: - dest_addr: "TEST_ADDR" - # -1 designates port value must be replaced by test setup - port: -1 - protocol: "http" -` - -var yamlBrokenConfigOverride = ` ---- + encoded_request: %s +`, + "TestApplyInputOverrideRAWRequest": `--- testoverride: input: - dest_addr: "TEST_ADDR" - # -1 designates port value must be replaced by test setup - port: -1 - this_does_not_exist: "test" -` - -var yamlConfigIgnoreTests = ` ---- -testoverride: - ignore: - "001": "This test result must be ignored" - forcefail: - "008": "This test should pass, but it is going to fail" - forcepass: - "099": "This test failed, but it shall pass!" -` - -var yamlCloudConfig = ` ---- -mode: cloud -` - -var logText = ` -[Tue Jan 05 02:21:09.637165 2021] [:error] [pid 76:tid 139683434571520] [client 172.23.0.1:58998] [client 172.23.0.1] ModSecurity: Warning. Pattern match "\\\\b(?:keep-alive|close),\\\\s?(?:keep-alive|close)\\\\b" at REQUEST_HEADERS:Connection. [file "/etc/modsecurity.d/owasp-crs/rules/REQUEST-920-PROTOCOL-ENFORCEMENT.conf"] [line "339"] [id "920210"] [msg "Multiple/Conflicting Connection Header Data Found"] [data "close,close"] [severity "WARNING"] [ver "OWASP_CRS/3.3.0"] [tag "application-multi"] [tag "language-multi"] [tag "platform-multi"] [tag "attack-protocol"] [tag "paranoia-level/1"] [tag "OWASP_CRS"] [tag "capec/1000/210/272"] [hostname "localhost"] [uri "/"] [unique_id "X-PNFSe1VwjCgYRI9FsbHgAAAIY"] -[Tue Jan 05 02:21:09.637731 2021] [:error] [pid 76:tid 139683434571520] [client 172.23.0.1:58998] [client 172.23.0.1] ModSecurity: Warning. Match of "pm AppleWebKit Android" against "REQUEST_HEADERS:User-Agent" required. [file "/etc/modsecurity.d/owasp-crs/rules/REQUEST-920-PROTOCOL-ENFORCEMENT.conf"] [line "1230"] [id "920300"] [msg "Request Missing an Accept Header"] [severity "NOTICE"] [ver "OWASP_CRS/3.3.0"] [tag "application-multi"] [tag "language-multi"] [tag "platform-multi"] [tag "attack-protocol"] [tag "OWASP_CRS"] [tag "capec/1000/210/272"] [tag "PCI/6.5.10"] [tag "paranoia-level/2"] [hostname "localhost"] [uri "/"] [unique_id "X-PNFSe1VwjCgYRI9FsbHgAAAIY"] -[Tue Jan 05 02:21:09.638572 2021] [:error] [pid 76:tid 139683434571520] [client 172.23.0.1:58998] [client 172.23.0.1] ModSecurity: Warning. Operator GE matched 5 at TX:anomaly_score. [file "/etc/modsecurity.d/owasp-crs/rules/REQUEST-949-BLOCKING-EVALUATION.conf"] [line "91"] [id "949110"] [msg "Inbound Anomaly Score Exceeded (Total Score: 5)"] [severity "CRITICAL"] [ver "OWASP_CRS/3.3.0"] [tag "application-multi"] [tag "language-multi"] [tag "platform-multi"] [tag "attack-generic"] [hostname "localhost"] [uri "/"] [unique_id "X-PNFSe1VwjCgYRI9FsbHgAAAIY"] -[Tue Jan 05 02:21:09.647668 2021] [:error] [pid 76:tid 139683434571520] [client 172.23.0.1:58998] [client 172.23.0.1] ModSecurity: Warning. Operator GE matched 5 at TX:inbound_anomaly_score. [file "/etc/modsecurity.d/owasp-crs/rules/RESPONSE-980-CORRELATION.conf"] [line "87"] [id "980130"] [msg "Inbound Anomaly Score Exceeded (Total Inbound Score: 5 - SQLI=0,XSS=0,RFI=0,LFI=0,RCE=0,PHPI=0,HTTP=0,SESS=0): individual paranoia level scores: 3, 2, 0, 0"] [ver "OWASP_CRS/3.3.0"] [tag "event-correlation"] [hostname "localhost"] [uri "/"] [unique_id "X-PNFSe1VwjCgYRI9FsbHgAAAIY"] -` - -var yamlTest = `--- -meta: - author: "tester" - enabled: true - name: "gotest-ftw.yaml" - description: "Example Test" -tests: - - test_title: "001" - description: "access real external site" - stages: - - stage: - input: - dest_addr: "TEST_ADDR" - # -1 designates port value must be replaced by test setup - port: -1 - headers: - User-Agent: "ModSecurity CRS 3 Tests" - Accept: "*/*" - Host: "TEST_ADDR" - output: - expect_error: False - status: [200] - - test_title: "008" - description: "this test is number 8" - stages: - - stage: - input: - dest_addr: "TEST_ADDR" - # -1 designates port value must be replaced by test setup - port: -1 - headers: - User-Agent: "ModSecurity CRS 3 Tests" - Accept: "*/*" - Host: "localhost" - output: - status: [200] - - test_title: "010" - stages: - - stage: - input: - dest_addr: "TEST_ADDR" - # -1 designates port value must be replaced by test setup - port: -1 - version: "HTTP/1.1" - method: "OTHER" - headers: - User-Agent: "ModSecurity CRS 3 Tests" - Accept: "*/*" - Host: "localhost" - output: - response_contains: "Hello, client" - - test_title: "101" - description: "this tests exceptions (connection timeout)" - stages: - - stage: - input: - dest_addr: "TEST_ADDR" - port: 8090 - headers: - User-Agent: "ModSecurity CRS 3 Tests" - Accept: "*/*" - Host: "none.host" - output: - expect_error: True - - test_title: "102" - description: "this tests exceptions (connection timeout)" - stages: - - stage: - input: - dest_addr: "TEST_ADDR" - port: 8090 - headers: - User-Agent: "ModSecurity CRS 3 Tests" - Host: "none.host" - Accept: "*/*" - encoded_request: "UE9TVCAvaW5kZXguaHRtbCBIVFRQLzEuMQ0KSG9zdDogMTkyLjE2OC4xLjIzDQpVc2VyLUFnZW50OiBjdXJsLzcuNDMuMA0KQWNjZXB0OiAqLyoNCkNvbnRlbnQtTGVuZ3RoOiA2NA0KQ29udGVudC1UeXBlOiBhcHBsaWNhdGlvbi94LXd3dy1mb3JtLXVybGVuY29kZWQNCkNvbm5lY3Rpb246IGNsb3NlDQoNCmQ9MTsyOzM7NDs1XG4xO0BTVU0oMSsxKSpjbWR8JyBwb3dlcnNoZWxsIElFWCh3Z2V0IDByLnBlL3ApJ1whQTA7Mw==" - output: - expect_error: True -` - -var yamlTestMultipleMatches = `--- -meta: - author: "tester" - enabled: true - name: "gotest-ftw.yaml" - description: "Example Test with multiple expected outputs per single rule" -tests: - - test_title: "001" - description: "access real external site" - stages: - - stage: - input: - dest_addr: "TEST_ADDR" - # -1 designates port value must be replaced by test setup - port: -1 - headers: - User-Agent: "ModSecurity CRS 3 Tests" - Accept: "*/*" - Host: "TEST_ADDR" - output: - status: [200] - response_contains: "Not contains this" -` - -var yamlTestOverride = ` ---- -meta: - author: "tester" - enabled: true - name: "gotest-ftw.yaml" - description: "Example Override Test" -tests: - - - test_title: "001" - description: "access real external site" - stages: - - - stage: - input: - dest_addr: "TEST_ADDR" - # -1 designates port value must be replaced by test setup - port: -1 - headers: - User-Agent: "ModSecurity CRS 3 Tests" - Host: "TEST_ADDR" - output: - expect_error: False - status: [200] -` - -var yamlTestOverrideWithNoPort = ` ---- -meta: - author: "tester" - enabled: true - name: "gotest-ftw.yaml" - description: "Example Override Test" -tests: - - - test_title: "001" - description: "access real external site" - stages: - - - stage: - input: - dest_addr: "TEST_ADDR" - headers: - User-Agent: "ModSecurity CRS 3 Tests" - Host: "TEST_ADDR" - output: - expect_error: False - status: [200] -` - -var yamlDisabledTest = ` ---- -meta: - author: "tester" - enabled: false - name: "we do not care, this test is disabled" - description: "Example Test" -tests: - - - test_title: "001" - description: "access real external site" - stages: - - - stage: - input: - dest_addr: "TEST_ADDR" - # -1 designates port value must be replaced by test setup - port: -1 - headers: - User-Agent: "ModSecurity CRS 3 Tests" - Host: "TEST_ADDR" - output: - status: [1234] -` + raw_request: %s +`, +} -var yamlTestLogs = `--- -meta: - author: "tester" - enabled: true - name: "gotest-ftw.yaml" - description: "Example Test" -tests: - - test_title: "200" - stages: - - stage: - input: - dest_addr: "TEST_ADDR" - # -1 designates port value must be replaced by test setup - port: -1 - headers: - User-Agent: "ModSecurity CRS 3 Tests" - Accept: "*/*" - Host: "localhost" - output: - log_contains: id \"949110\" - - test_title: "201" - stages: - - stage: - input: - dest_addr: "TEST_ADDR" - # -1 designates port value must be replaced by test setup - port: -1 - headers: - User-Agent: "ModSecurity CRS 3 Tests" - Accept: "*/*" - Host: "localhost" - output: - no_log_contains: ABCDE -` +var destinationMap = map[string]string{ + "TestBrokenOverrideRun": "http://example.com:1234", + "TestDisabledRun": "http://example.com:1234", +} -var yamlFailedTest = `--- -meta: - author: "tester" - enabled: true - name: "gotest-ftw.yaml" - description: "Example Test" -tests: - - test_title: "990" - description: test that fails - stages: - - stage: - input: - dest_addr: "TEST_ADDR" - # -1 designates port value must be replaced by test setup - port: -1 - headers: - User-Agent: "ModSecurity CRS 3 Tests" - Accept: "*/*" - Host: "none.host" - output: - status: [413] -` +type runTestSuite struct { + suite.Suite + cfg *config.FTWConfiguration + ftwTests []test.FTWTest + logFilePath string + out *output.Output + ts *httptest.Server + dest *ftwhttp.Destination + tempFileName string +} // Error checking omitted for brevity -func newTestServer(t *testing.T, cfg *config.FTWConfiguration, logLines string) (destination *ftwhttp.Destination, logFilePath string) { - logFilePath = setUpLogFileForTestServer(t, cfg) +func (s *runTestSuite) newTestServer(logLines string) { + var err error + s.setUpLogFileForTestServer() - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + s.ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte("Hello, client")) - writeTestServerLog(t, cfg, logLines, logFilePath, r) - })) - - // close server after test - t.Cleanup(ts.Close) - - dest, err := ftwhttp.DestinationFromString(ts.URL) - if err != nil { - assert.FailNow(t, "cannot get destination from string", err.Error()) - } - return dest, logFilePath -} - -// Error checking omitted for brevity -func newTestServerForCloudTest(t *testing.T, responseStatus int) (server *httptest.Server, destination *ftwhttp.Destination) { - server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(responseStatus) - _, _ = w.Write([]byte("Hello, client")) + s.writeTestServerLog(logLines, r) })) - // close server after test - t.Cleanup(server.Close) - - dest, err := ftwhttp.DestinationFromString(server.URL) - if err != nil { - assert.FailNow(t, "cannot get destination from string", err.Error()) - } - - return server, dest + s.dest, err = ftwhttp.DestinationFromString((s.ts).URL) + s.Require().NoError(err, "cannot get destination from string") } -func setUpLogFileForTestServer(t *testing.T, cfg *config.FTWConfiguration) (logFilePath string) { +func (s *runTestSuite) setUpLogFileForTestServer() { // log to the configured file - if cfg.RunMode == config.DefaultRunMode { - logFilePath = cfg.LogFile + if s.cfg != nil && s.cfg.RunMode == config.DefaultRunMode { + s.logFilePath = s.cfg.LogFile } // if no file has been configured, create one and handle cleanup - if logFilePath == "" { + if s.logFilePath == "" { file, err := os.CreateTemp("", "go-ftw-test-*.log") - assert.NoError(t, err) - logFilePath = file.Name() - t.Cleanup(func() { - _ = os.Remove(logFilePath) - log.Info().Msgf("Deleting temporary file '%s'", logFilePath) - }) + s.NoError(err) + s.logFilePath = file.Name() } - return logFilePath } -func writeTestServerLog(t *testing.T, cfg *config.FTWConfiguration, logLines string, logFilePath string, r *http.Request) { +func (s *runTestSuite) writeTestServerLog(logLines string, r *http.Request) { // write supplied log lines, emulating the output of the rule engine logMessage := logLines // if the request has the special test header, log the request instead // this emulates the log marker rule - if r.Header.Get(cfg.LogMarkerHeaderName) != "" { + if r.Header.Get(s.cfg.LogMarkerHeaderName) != "" { logMessage = fmt.Sprintf("request line: %s %s %s, headers: %s\n", r.Method, r.RequestURI, r.Proto, r.Header) } - file, err := os.OpenFile(logFilePath, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0600) - if err != nil { - assert.FailNow(t, "cannot open file", err.Error()) - } + file, err := os.OpenFile(s.logFilePath, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0600) + s.NoError(err, "cannot open file") + defer file.Close() - _, err = file.WriteString(logMessage) - if err != nil { - assert.FailNow(t, "cannot write log message to file", err.Error()) - } + n, err := file.WriteString(logMessage) + s.Len(logMessage, n, "cannot write log message to file") + s.NoError(err, "cannot write log message to file") } -func replaceDestinationInTest(ftwTest *test.FTWTest, d ftwhttp.Destination) { - // This function doesn't use `range` because we want to modify the struct in place. - // Range (and assignments in general) create copies of structs, not references. - // Maps, slices, etc. on the other hand, are assigned as references. - for testIndex := 0; testIndex < len(ftwTest.Tests); testIndex++ { - testCase := &ftwTest.Tests[testIndex] - for stageIndex := 0; stageIndex < len(testCase.Stages); stageIndex++ { - input := &testCase.Stages[stageIndex].Stage.Input - - if *input.DestAddr == "TEST_ADDR" { - input.DestAddr = &d.DestAddr - } - if input.Headers.Get("Host") == "TEST_ADDR" { - input.Headers.Set("Host", d.DestAddr) - } - if input.Port != nil && *input.Port == -1 { - input.Port = &d.Port - } - } +func (s *runTestSuite) SetupTest() { + s.cfg = config.NewDefaultConfig() + // setup test webserver (not a waf) + s.newTestServer(logText) + if s.logFilePath != "" { + s.cfg.WithLogfile(s.logFilePath) } + + s.out = output.NewOutput("normal", os.Stdout) } -func replaceDestinationInConfiguration(override *config.FTWTestOverride, dest ftwhttp.Destination) { - replaceableAddress := "TEST_ADDR" - replaceablePort := -1 +func (s *runTestSuite) TearDownTest() { + s.ts.Close() +} - overriddenInputs := &override.Overrides - if overriddenInputs.DestAddr != nil && *overriddenInputs.DestAddr == replaceableAddress { - overriddenInputs.DestAddr = &dest.DestAddr +func (s *runTestSuite) BeforeTest(_ string, name string) { + var err error + var cfg string + var ok bool + + // if we have a configuration for this test, use it + // else use the default configuration + if cfg, ok = testConfigMap[name]; !ok { + cfg = testConfigMap["BaseConfig"] } - if overriddenInputs.Port != nil && *overriddenInputs.Port == replaceablePort { - overriddenInputs.Port = &dest.Port + + // if we have a destination for this test, use it + // else use the default destination + if s.dest == nil { + s.dest, err = ftwhttp.DestinationFromString(destinationMap[name]) + s.NoError(err) } -} -func TestRun(t *testing.T) { - cfg, err := config.NewConfigFromString(yamlConfig) - assert.NoError(t, err) + log.Info().Msgf("Using port %d and addr '%s'", s.dest.Port, s.dest.DestAddr) - out := output.NewOutput("normal", os.Stdout) + // set up variables for template + vars := map[string]interface{}{ + "TestPort": s.dest.Port, + "TestAddr": s.dest.DestAddr, + } - // setup test webserver (not a waf) - dest, logFilePath := newTestServer(t, cfg, logText) - cfg.WithLogfile(logFilePath) - ftwTest, err := test.GetTestFromYaml([]byte(yamlTest)) - assert.NoError(t, err) + // set up configuration from template + configTmpl, err := template.New("config-test").Parse(cfg) + s.NoError(err, "cannot parse template") + buf := &bytes.Buffer{} + err = configTmpl.Execute(buf, vars) + s.NoError(err, "cannot execute template") + s.cfg, err = config.NewConfigFromString(buf.String()) + s.NoError(err, "cannot get config from string") + if s.logFilePath != "" { + s.cfg.WithLogfile(s.logFilePath) + } + // get tests template from file + tmpl, err := template.ParseFiles(fmt.Sprintf("testdata/%s.yaml", name)) + s.NoError(err) + // create a temporary file to hold the test + testFileContents, err := os.CreateTemp("testdata", "mock-test-*.yaml") + s.NoError(err, "cannot create temporary file") + err = tmpl.Execute(testFileContents, vars) + s.NoError(err, "cannot execute template") + // get tests from file + s.ftwTests, err = test.GetTestsFromFiles(testFileContents.Name()) + s.NoError(err, "cannot get tests from file") + // save the name of the temporary file so we can delete it later + s.tempFileName = testFileContents.Name() +} - replaceDestinationInTest(&ftwTest, *dest) +func (s *runTestSuite) AfterTest(_ string, _ string) { + err := os.Remove(s.logFilePath) + s.NoError(err, "cannot remove log file") + log.Info().Msgf("Deleting temporary file '%s'", s.logFilePath) + if s.tempFileName != "" { + err = os.Remove(s.tempFileName) + s.NoError(err, "cannot remove test file") + s.tempFileName = "" + } +} + +func TestRunTestsTestSuite(t *testing.T) { + suite.Run(t, new(runTestSuite)) +} - t.Run("show time and execute all", func(t *testing.T) { - res, err := Run(cfg, []test.FTWTest{ftwTest}, RunnerConfig{ +func (s *runTestSuite) TestRunTests_Run() { + s.Run("show time and execute all", func() { + res, err := Run(s.cfg, s.ftwTests, RunnerConfig{ ShowTime: true, Output: output.Quiet, - }, out) - assert.NoError(t, err) - assert.Equalf(t, res.Stats.TotalFailed(), 0, "Oops, %d tests failed to run!", res.Stats.TotalFailed()) + }, s.out) + s.NoError(err) + s.Equalf(res.Stats.TotalFailed(), 0, "Oops, %d tests failed to run!", res.Stats.TotalFailed()) }) - t.Run("be verbose and execute all", func(t *testing.T) { - res, err := Run(cfg, []test.FTWTest{ftwTest}, RunnerConfig{ + s.Run("be verbose and execute all", func() { + res, err := Run(s.cfg, s.ftwTests, RunnerConfig{ Include: regexp.MustCompile("0*"), ShowTime: true, - }, out) - assert.NoError(t, err) - assert.Equal(t, res.Stats.TotalFailed(), 0, "verbose and execute all failed") + }, s.out) + s.NoError(err) + s.Equal(res.Stats.TotalFailed(), 0, "verbose and execute all failed") }) - t.Run("don't show time and execute all", func(t *testing.T) { - res, err := Run(cfg, []test.FTWTest{ftwTest}, RunnerConfig{ + s.Run("don't show time and execute all", func() { + res, err := Run(s.cfg, s.ftwTests, RunnerConfig{ Include: regexp.MustCompile("0*"), - }, out) - assert.NoError(t, err) - assert.Equal(t, res.Stats.TotalFailed(), 0, "do not show time and execute all failed") + }, s.out) + s.NoError(err) + s.Equal(res.Stats.TotalFailed(), 0, "do not show time and execute all failed") }) - t.Run("execute only test 008 but exclude all", func(t *testing.T) { - res, err := Run(cfg, []test.FTWTest{ftwTest}, RunnerConfig{ + s.Run("execute only test 008 but exclude all", func() { + res, err := Run(s.cfg, s.ftwTests, RunnerConfig{ Include: regexp.MustCompile("008"), Exclude: regexp.MustCompile("0*"), - }, out) - assert.NoError(t, err) - assert.Equal(t, res.Stats.TotalFailed(), 0, "do not show time and execute all failed") + }, s.out) + s.NoError(err) + s.Equal(res.Stats.TotalFailed(), 0, "do not show time and execute all failed") }) - t.Run("exclude test 010", func(t *testing.T) { - res, err := Run(cfg, []test.FTWTest{ftwTest}, RunnerConfig{ + s.Run("exclude test 010", func() { + res, err := Run(s.cfg, s.ftwTests, RunnerConfig{ Exclude: regexp.MustCompile("010"), - }, out) - assert.NoError(t, err) - assert.Equal(t, res.Stats.TotalFailed(), 0, "failed to exclude test") + }, s.out) + s.NoError(err) + s.Equal(res.Stats.TotalFailed(), 0, "failed to exclude test") }) - t.Run("test exceptions 1", func(t *testing.T) { - res, err := Run(cfg, []test.FTWTest{ftwTest}, RunnerConfig{ + s.Run("test exceptions 1", func() { + res, err := Run(s.cfg, s.ftwTests, RunnerConfig{ Include: regexp.MustCompile("1*"), Exclude: regexp.MustCompile("0*"), Output: output.Quiet, - }, out) - assert.NoError(t, err) - assert.Equal(t, res.Stats.TotalFailed(), 0, "failed to test exceptions") + }, s.out) + s.NoError(err) + s.Equal(res.Stats.TotalFailed(), 0, "failed to test exceptions") }) } -func TestRunMultipleMatches(t *testing.T) { - cfg, err := config.NewConfigFromString(yamlConfig) - assert.NoError(t, err) - - out := output.NewOutput("normal", os.Stdout) - - dest, logFilePath := newTestServer(t, cfg, logText) - cfg.WithLogfile(logFilePath) - ftwTest, err := test.GetTestFromYaml([]byte(yamlTestMultipleMatches)) - assert.NoError(t, err) - - replaceDestinationInTest(&ftwTest, *dest) - - t.Run("execute multiple...test", func(t *testing.T) { - res, err := Run(cfg, []test.FTWTest{ftwTest}, RunnerConfig{ +func (s *runTestSuite) TestRunMultipleMatches() { + s.Run("execute multiple...test", func() { + res, err := Run(s.cfg, s.ftwTests, RunnerConfig{ Output: output.Quiet, - }, out) - assert.NoError(t, err) - assert.Equalf(t, res.Stats.TotalFailed(), 1, "Oops, %d tests failed to run! Expected 1 failing test", res.Stats.TotalFailed()) + }, s.out) + s.NoError(err) + s.Equalf(res.Stats.TotalFailed(), 1, "Oops, %d tests failed to run! Expected 1 failing test", res.Stats.TotalFailed()) }) } -func TestOverrideRun(t *testing.T) { - // setup test webserver (not a waf) - cfg, err := config.NewConfigFromString(yamlConfigOverride) - assert.NoError(t, err) - - out := output.NewOutput("normal", os.Stdout) - - dest, logFilePath := newTestServer(t, cfg, logText) - - replaceDestinationInConfiguration(&cfg.TestOverride, *dest) - cfg.WithLogfile(logFilePath) - - // replace host and port with values that can be overridden by config - fakeDestination, err := ftwhttp.DestinationFromString("http://example.com:1234") - if err != nil { - assert.FailNow(t, err.Error(), "Failed to parse fake destination") - } - - ftwTest, err := test.GetTestFromYaml([]byte(yamlTestOverride)) - assert.NoError(t, err) - - replaceDestinationInTest(&ftwTest, *fakeDestination) - - res, err := Run(cfg, []test.FTWTest{ftwTest}, RunnerConfig{ +func (s *runTestSuite) TestOverrideRun() { + res, err := Run(s.cfg, s.ftwTests, RunnerConfig{ Output: output.Quiet, - }, out) - assert.NoError(t, err) - assert.LessOrEqual(t, 0, res.Stats.TotalFailed(), "Oops, test run failed!") + }, s.out) + s.NoError(err) + s.LessOrEqual(0, res.Stats.TotalFailed(), "Oops, test run failed!") } -func TestBrokenOverrideRun(t *testing.T) { - cfg, err := config.NewConfigFromString(yamlBrokenConfigOverride) - assert.NoError(t, err) - - out := output.NewOutput("normal", os.Stdout) - - dest, logFilePath := newTestServer(t, cfg, logText) - cfg.WithLogfile(logFilePath) - - replaceDestinationInConfiguration(&cfg.TestOverride, *dest) - - // replace host and port with values that can be overridden by config - fakeDestination, err := ftwhttp.DestinationFromString("http://example.com:1234") - if err != nil { - assert.FailNow(t, err.Error(), "Failed to parse fake destination") - } - - ftwTest, err := test.GetTestFromYaml([]byte(yamlTestOverride)) - assert.NoError(t, err) - - replaceDestinationInTest(&ftwTest, *fakeDestination) - +func (s *runTestSuite) TestBrokenOverrideRun() { // the test should succeed, despite the unknown override property - res, err := Run(cfg, []test.FTWTest{ftwTest}, RunnerConfig{}, out) - assert.NoError(t, err) - assert.LessOrEqual(t, 0, res.Stats.TotalFailed(), "Oops, test run failed!") + res, err := Run(s.cfg, s.ftwTests, RunnerConfig{}, s.out) + s.NoError(err) + s.LessOrEqual(0, res.Stats.TotalFailed(), "Oops, test run failed!") } -func TestBrokenPortOverrideRun(t *testing.T) { - defaultConfig := config.NewDefaultConfig() - // TestServer initialized first to retrieve the correct port number - dest, logFilePath := newTestServer(t, defaultConfig, logText) - // replace destination port inside the yaml with the retrieved one - cfg, err := config.NewConfigFromString(fmt.Sprintf(yamlConfigPortOverride, dest.Port)) - assert.NoError(t, err) - - out := output.NewOutput("normal", os.Stdout) - - replaceDestinationInConfiguration(&cfg.TestOverride, *dest) - cfg.WithLogfile(logFilePath) - - // replace host and port with values that can be overridden by config - fakeDestination, err := ftwhttp.DestinationFromString("http://example.com:1234") - if err != nil { - assert.FailNow(t, err.Error(), "Failed to parse fake destination") - } - - ftwTest, err := test.GetTestFromYaml([]byte(yamlTestOverrideWithNoPort)) - assert.NoError(t, err) - - replaceDestinationInTest(&ftwTest, *fakeDestination) - +func (s *runTestSuite) TestBrokenPortOverrideRun() { // the test should succeed, despite the unknown override property - res, err := Run(cfg, []test.FTWTest{ftwTest}, RunnerConfig{}, out) - assert.NoError(t, err) - assert.LessOrEqual(t, 0, res.Stats.TotalFailed(), "Oops, test run failed!") -} - -func TestDisabledRun(t *testing.T) { - cfg, err := config.NewConfigFromString(yamlCloudConfig) - assert.NoError(t, err) - out := output.NewOutput("normal", os.Stdout) - - fakeDestination, err := ftwhttp.DestinationFromString("http://example.com:1234") - if err != nil { - assert.FailNow(t, err.Error(), "Failed to parse fake destination") - } - - ftwTest, err := test.GetTestFromYaml([]byte(yamlDisabledTest)) - assert.NoError(t, err) - replaceDestinationInTest(&ftwTest, *fakeDestination) - - res, err := Run(cfg, []test.FTWTest{ftwTest}, RunnerConfig{}, out) - assert.NoError(t, err) - assert.LessOrEqual(t, 0, res.Stats.TotalFailed(), "Oops, test run failed!") -} - -func TestLogsRun(t *testing.T) { - cfg, err := config.NewConfigFromString(yamlConfig) - assert.NoError(t, err) - // setup test webserver (not a waf) - dest, logFilePath := newTestServer(t, cfg, logText) - - replaceDestinationInConfiguration(&cfg.TestOverride, *dest) - cfg.WithLogfile(logFilePath) - - out := output.NewOutput("normal", os.Stdout) - - ftwTest, err := test.GetTestFromYaml([]byte(yamlTestLogs)) - assert.NoError(t, err) - replaceDestinationInTest(&ftwTest, *dest) - - res, err := Run(cfg, []test.FTWTest{ftwTest}, RunnerConfig{}, out) - assert.NoError(t, err) - assert.LessOrEqual(t, 0, res.Stats.TotalFailed(), "Oops, test run failed!") -} - -func TestCloudRun(t *testing.T) { - cfg, err := config.NewConfigFromString(yamlCloudConfig) - assert.NoError(t, err) - out := output.NewOutput("normal", os.Stdout) - stats := NewRunStats() - - ftwTestDummy, err := test.GetTestFromYaml([]byte(yamlTestLogs)) - assert.NoError(t, err) - - t.Run("don't show time and execute all", func(t *testing.T) { - for testCaseIndex, testCaseDummy := range ftwTestDummy.Tests { - for stageIndex := range testCaseDummy.Stages { - // Read the tests for every stage, so we can replace the destination - // in each run. The server needs to be configured for each stage - // individually. - ftwTest, err := test.GetTestFromYaml([]byte(yamlTestLogs)) - assert.NoError(t, err) - testCase := &ftwTest.Tests[testCaseIndex] - stage := &testCase.Stages[stageIndex].Stage - - ftwCheck := check.NewCheck(cfg) - - // this mirrors check.SetCloudMode() - responseStatus := 200 - if stage.Output.LogContains != "" { - responseStatus = 403 - } else if stage.Output.NoLogContains != "" { - responseStatus = 405 - } - server, dest := newTestServerForCloudTest(t, responseStatus) - - replaceDestinationInConfiguration(&cfg.TestOverride, *dest) - - replaceDestinationInTest(&ftwTest, *dest) - assert.NoError(t, err) - client, err := ftwhttp.NewClient(ftwhttp.NewClientConfig()) - assert.NoError(t, err) - runContext := TestRunContext{ - Config: cfg, - Include: nil, - Exclude: nil, - ShowTime: false, - Stats: stats, - Output: out, - Client: client, - LogLines: nil, - } - - err = RunStage(&runContext, ftwCheck, *testCase, *stage) - assert.NoError(t, err) - assert.LessOrEqual(t, 0, runContext.Stats.TotalFailed(), "Oops, test run failed!") - - server.Close() - } - } - }) -} - -func TestFailedTestsRun(t *testing.T) { - cfg, err := config.NewConfigFromString(yamlConfig) - assert.NoError(t, err) - dest, logFilePath := newTestServer(t, cfg, logText) - - out := output.NewOutput("normal", os.Stdout) - replaceDestinationInConfiguration(&cfg.TestOverride, *dest) - cfg.WithLogfile(logFilePath) - - ftwTest, err := test.GetTestFromYaml([]byte(yamlFailedTest)) - assert.NoError(t, err) - replaceDestinationInTest(&ftwTest, *dest) - - res, err := Run(cfg, []test.FTWTest{ftwTest}, RunnerConfig{}, out) - assert.NoError(t, err) - assert.Equal(t, 1, res.Stats.TotalFailed()) -} - -func TestApplyInputOverrideHostFromDestAddr(t *testing.T) { - originalHost := "original.com" - overrideHost := "override.com" - testInput := test.Input{ - DestAddr: &originalHost, - } - cfg := &config.FTWConfiguration{ - TestOverride: config.FTWTestOverride{ - Overrides: test.Overrides{ - DestAddr: &overrideHost, - }, - }, - } - - err := applyInputOverride(cfg.TestOverride, &testInput) - assert.NoError(t, err, "Failed to apply input overrides") - - assert.Equal(t, overrideHost, *testInput.DestAddr, "`dest_addr` should have been overridden") - - assert.NotNil(t, testInput.Headers, "Header map must exist after overriding `dest_addr`") - - hostHeader := testInput.Headers.Get("Host") - assert.Equal(t, "", hostHeader, "Without OverrideEmptyHostHeader, Host header must not be set after overriding `dest_addr`") + res, err := Run(s.cfg, s.ftwTests, RunnerConfig{}, s.out) + s.NoError(err) + s.LessOrEqual(0, res.Stats.TotalFailed(), "Oops, test run failed!") } -func TestApplyInputOverrideEmptyHostHeaderSetHostFromDestAddr(t *testing.T) { - originalHost := "original.com" - overrideHost := "override.com" - testInput := test.Input{ - DestAddr: &originalHost, - } - cfg := &config.FTWConfiguration{ - TestOverride: config.FTWTestOverride{ - Overrides: test.Overrides{ - DestAddr: &overrideHost, - OverrideEmptyHostHeader: true, - }, - }, - } - - err := applyInputOverride(cfg.TestOverride, &testInput) - assert.NoError(t, err, "Failed to apply input overrides") - - assert.Equal(t, overrideHost, *testInput.DestAddr, "`dest_addr` should have been overridden") - - assert.NotNil(t, testInput.Headers, "Header map must exist after overriding `dest_addr`") - - hostHeader := testInput.Headers.Get("Host") - assert.NotEqual(t, "", hostHeader, "Host header must be set after overriding `dest_addr` and setting `override_empty_host_header` to `true`") - assert.Equal(t, overrideHost, hostHeader, "Host header must be identical to `dest_addr` after overriding `dest_addr` and setting `override_emtpy_host_header` to `true`") -} - -func TestApplyInputOverrideSetHostFromHostHeaderOverride(t *testing.T) { - originalDestAddr := "original.com" - overrideDestAddress := "wrong.org" - overrideHostHeader := "override.com" - - cfg, err1 := config.NewConfigFromString(fmt.Sprintf(yamlConfigEmptyHostHeaderOverride, overrideDestAddress, overrideHostHeader)) - assert.NoError(t, err1) - - testInput := test.Input{ - DestAddr: &originalDestAddr, - } - - err := applyInputOverride(cfg.TestOverride, &testInput) - assert.NoError(t, err, "Failed to apply input overrides") - - hostHeader := testInput.Headers.Get("Host") - assert.NotEqual(t, "", hostHeader, "Host header must be set after overriding the `Host` header") - if hostHeader == overrideDestAddress { - assert.Equal(t, overrideHostHeader, hostHeader, "Host header override must take precence over OverrideEmptyHostHeader") - } else { - assert.Equal(t, overrideHostHeader, hostHeader, "Host header must be identical to overridden `Host` header.") - } +func (s *runTestSuite) TestDisabledRun() { + res, err := Run(s.cfg, s.ftwTests, RunnerConfig{}, s.out) + s.NoError(err) + s.LessOrEqual(0, res.Stats.TotalFailed(), "Oops, test run failed!") } -func TestApplyInputOverrideSetHeaderOverridingExistingOne(t *testing.T) { - originalHeaderValue := "original" - overrideHeaderValue := "override" - cfg, err1 := config.NewConfigFromString(fmt.Sprintf(yamlConfigHostHeaderOverride, overrideHeaderValue)) - assert.NoError(t, err1) - - testInput := test.Input{ - Headers: ftwhttp.Header{"unique_id": originalHeaderValue}, - } - - assert.NotNil(t, testInput.Headers, "Header map must exist before overriding any header") - - err := applyInputOverride(cfg.TestOverride, &testInput) - assert.NoError(t, err, "Failed to apply input overrides") - - overriddenHeader := testInput.Headers.Get("unique_id") - assert.NotEqual(t, "", overriddenHeader, "unique_id header must be set after overriding it") - assert.Equal(t, overrideHeaderValue, overriddenHeader, "Host header must be identical to overridden `Host` header.") +func (s *runTestSuite) TestLogsRun() { + res, err := Run(s.cfg, s.ftwTests, RunnerConfig{}, s.out) + s.NoError(err) + s.LessOrEqual(0, res.Stats.TotalFailed(), "Oops, test run failed!") } -func TestApplyInputOverrides(t *testing.T) { - originalHeaderValue := "original" - overrideHeaderValue := "override" - cfg, err1 := config.NewConfigFromString(fmt.Sprintf(yamlConfigHeaderOverride, overrideHeaderValue)) - assert.NoError(t, err1) - - testInput := test.Input{ - Headers: ftwhttp.Header{"unique_id": originalHeaderValue}, - } - - assert.NotNil(t, testInput.Headers, "Header map must exist before overriding any header") - - err := applyInputOverride(cfg.TestOverride, &testInput) - assert.NoError(t, err, "Failed to apply input overrides") - - overriddenHeader := testInput.Headers.Get("unique_id") - assert.NotEqual(t, "", overriddenHeader, "unique_id header must be set after overriding it") - assert.Equal(t, overrideHeaderValue, overriddenHeader, "Host header must be identical to overridden `Host` header.") +func (s *runTestSuite) TestFailedTestsRun() { + res, err := Run(s.cfg, s.ftwTests, RunnerConfig{}, s.out) + s.NoError(err) + s.Equal(1, res.Stats.TotalFailed()) } -func TestApplyInputOverrideURI(t *testing.T) { - originalURI := "original.com" - overrideURI := "override.com" - testInput := test.Input{ - URI: &originalURI, - } - - cfg, err1 := config.NewConfigFromString(fmt.Sprintf(yamlConfigURIOverride, overrideURI)) - assert.NoError(t, err1) - err := applyInputOverride(cfg.TestOverride, &testInput) - assert.NoError(t, err, "Failed to apply input overrides") - assert.Equal(t, overrideURI, *testInput.URI, "`URI` should have been overridden") -} - -func TestApplyInputOverrideVersion(t *testing.T) { - originalVersion := "HTTP/0.9" - overrideVersion := "HTTP/1.1" - testInput := test.Input{ - Version: &originalVersion, - } - cfg, err1 := config.NewConfigFromString(fmt.Sprintf(yamlConfigVersionOverride, overrideVersion)) - assert.NoError(t, err1) - err := applyInputOverride(cfg.TestOverride, &testInput) - assert.NoError(t, err, "Failed to apply input overrides") - assert.Equal(t, overrideVersion, *testInput.Version, "`Version` should have been overridden") -} - -func TestApplyInputOverrideMethod(t *testing.T) { - originalMethod := "original.com" - overrideMethod := "override.com" - testInput := test.Input{ - Method: &originalMethod, - } - cfg, err1 := config.NewConfigFromString(fmt.Sprintf(yamlConfigMethodOverride, overrideMethod)) - assert.NoError(t, err1) - err := applyInputOverride(cfg.TestOverride, &testInput) - assert.NoError(t, err, "Failed to apply input overrides") - assert.Equal(t, overrideMethod, *testInput.Method, "`Method` should have been overridden") -} - -func TestApplyInputOverrideData(t *testing.T) { - originalData := "data" - overrideData := "new data" - testInput := test.Input{ - Data: &originalData, - } - cfg, err1 := config.NewConfigFromString(fmt.Sprintf(yamlConfigDataOverride, overrideData)) - assert.NoError(t, err1) - err := applyInputOverride(cfg.TestOverride, &testInput) - assert.NoError(t, err, "Failed to apply input overrides") - assert.Equal(t, overrideData, *testInput.Data, "`Data` should have been overridden") -} - -func TestApplyInputOverrideStopMagic(t *testing.T) { - overrideStopMagic := true - testInput := test.Input{ - StopMagic: false, - } - cfg, err1 := config.NewConfigFromString(fmt.Sprintf(yamlConfigStopMagicOverride, overrideStopMagic)) - assert.NoError(t, err1) - err := applyInputOverride(cfg.TestOverride, &testInput) - assert.NoError(t, err, "Failed to apply input overrides") - assert.Equal(t, overrideStopMagic, testInput.StopMagic, "`StopMagic` should have been overridden") -} - -func TestApplyInputOverrideEncodedRequest(t *testing.T) { - originalEncodedRequest := "originalbase64" - overrideEncodedRequest := "modifiedbase64" - testInput := test.Input{ - EncodedRequest: originalEncodedRequest, - } - cfg, err1 := config.NewConfigFromString(fmt.Sprintf(yamlConfigEncodedRequestOverride, overrideEncodedRequest)) - assert.NoError(t, err1) - err := applyInputOverride(cfg.TestOverride, &testInput) - assert.NoError(t, err, "Failed to apply input overrides") - assert.Equal(t, overrideEncodedRequest, testInput.EncodedRequest, "`EncodedRequest` should have been overridden") -} - -func TestApplyInputOverrideRAWRequest(t *testing.T) { - originalRAWRequest := "original" - overrideRAWRequest := "override" - testInput := test.Input{ - RAWRequest: originalRAWRequest, - } - cfg, err1 := config.NewConfigFromString(fmt.Sprintf(yamlConfigRAWRequestOverride, overrideRAWRequest)) - assert.NoError(t, err1) - err := applyInputOverride(cfg.TestOverride, &testInput) - assert.NoError(t, err, "Failed to apply input overrides") - assert.Equal(t, overrideRAWRequest, testInput.RAWRequest, "`RAWRequest` should have been overridden") -} - -func TestIgnoredTestsRun(t *testing.T) { - cfg, err := config.NewConfigFromString(yamlConfigIgnoreTests) - dest, logFilePath := newTestServer(t, cfg, logText) - assert.NoError(t, err) - - out := output.NewOutput("normal", os.Stdout) - - replaceDestinationInConfiguration(&cfg.TestOverride, *dest) - cfg.WithLogfile(logFilePath) - - ftwTest, err := test.GetTestFromYaml([]byte(yamlTest)) - assert.NoError(t, err) - - replaceDestinationInTest(&ftwTest, *dest) - - res, err := Run(cfg, []test.FTWTest{ftwTest}, RunnerConfig{}, out) - assert.NoError(t, err) - assert.Equal(t, res.Stats.TotalFailed(), 1, "Oops, test run failed!") +func (s *runTestSuite) TestIgnoredTestsRun() { + res, err := Run(s.cfg, s.ftwTests, RunnerConfig{}, s.out) + s.NoError(err) + s.Equal(res.Stats.TotalFailed(), 1, "Oops, test run failed!") } diff --git a/runner/testdata/TestBrokenOverrideRun.yaml b/runner/testdata/TestBrokenOverrideRun.yaml new file mode 100644 index 0000000..ab6b84b --- /dev/null +++ b/runner/testdata/TestBrokenOverrideRun.yaml @@ -0,0 +1,19 @@ +--- +meta: + author: "tester" + enabled: true + name: "TestBrokenOverrideRun.yaml" + description: "Example Override Test" +tests: + - test_title: "001" + description: "access real external site" + stages: + - stage: + input: + dest_addr: "{{ .TestAddr }}" + headers: + User-Agent: "ModSecurity CRS 3 Tests" + Host: "{{ .TestAddr }}" + output: + expect_error: False + status: [200] diff --git a/runner/testdata/TestBrokenPortOverrideRun.yaml b/runner/testdata/TestBrokenPortOverrideRun.yaml new file mode 100644 index 0000000..b5dd26a --- /dev/null +++ b/runner/testdata/TestBrokenPortOverrideRun.yaml @@ -0,0 +1,19 @@ +--- +meta: + author: "tester" + enabled: true + name: "TestBrokenPortOverrideRun.yaml" + description: "Example Override Test" +tests: + - test_title: "001" + description: "access real external site" + stages: + - stage: + input: + dest_addr: "{{ .TestAddr }}" + headers: + User-Agent: "ModSecurity CRS 3 Tests" + Host: "{{ .TestAddr }}" + output: + expect_error: False + status: [200] diff --git a/runner/testdata/TestCloudRun.yaml b/runner/testdata/TestCloudRun.yaml new file mode 100644 index 0000000..7d7dedc --- /dev/null +++ b/runner/testdata/TestCloudRun.yaml @@ -0,0 +1,106 @@ +--- +meta: + author: "tester" + enabled: true + name: "TestCloudRun.yaml" + description: "Example Test" +tests: + - test_title: "001" + description: "access real external site" + stages: + - stage: + input: + uri: "/200" + dest_addr: "{{ .TestAddr }}" + port: {{ .TestPort }} + headers: + User-Agent: "ModSecurity CRS 3 Tests" + Accept: "*/*" + Host: "{{ .TestAddr }}" + output: + expect_error: False + status: [200] + - test_title: "403" + description: "using log_contains should return a 403 response." + stages: + - stage: + input: + uri: "/403" + dest_addr: "{{ .TestAddr }}" + port: {{ .TestPort }} + headers: + User-Agent: "ModSecurity CRS 3 Tests" + Accept: "*/*" + Host: "{{ .TestAddr }}" + output: + log_contains: "ModSecurity: Access denied with code 403" + - test_title: "405" + description: "using no_log_contains should return a 405 response." + stages: + - stage: + input: + uri: "/405" + dest_addr: "{{ .TestAddr }}" + port: {{ .TestPort }} + headers: + User-Agent: "ModSecurity CRS 3 Tests" + Accept: "*/*" + Host: "{{ .TestAddr }}" + output: + no_log_contains: "ModSecurity: Access denied with code 403" + - test_title: "008" + description: "this test is number 8" + stages: + - stage: + input: + uri: "/200" + dest_addr: "{{ .TestAddr }}" + port: {{ .TestPort }} + headers: + User-Agent: "ModSecurity CRS 3 Tests" + Accept: "*/*" + Host: "localhost" + output: + status: [200] + - test_title: "010" + stages: + - stage: + input: + uri: "/200" + dest_addr: "{{ .TestAddr }}" + port: {{ .TestPort }} + version: "HTTP/1.1" + method: "OTHER" + headers: + User-Agent: "ModSecurity CRS 3 Tests" + Accept: "*/*" + Host: "localhost" + output: + response_contains: "Hello, client" + - test_title: "101" + description: "this tests exceptions (connection timeout)" + stages: + - stage: + input: + dest_addr: "{{ .TestAddr }}" + port: 8090 + headers: + User-Agent: "ModSecurity CRS 3 Tests" + Accept: "*/*" + Host: "none.host" + output: + expect_error: True + - test_title: "102" + description: "this tests exceptions (connection timeout)" + stages: + - stage: + input: + dest_addr: "{{ .TestAddr }}" + port: 8090 + headers: + User-Agent: "ModSecurity CRS 3 Tests" + Host: "none.host" + Accept: "*/*" + encoded_request: "UE9TVCAvaW5kZXguaHRtbCBIVFRQLzEuMQ0KSG9zdDogMTkyLjE2OC4xLjIzDQpVc2VyLUFnZW50OiBjdXJsLzcuNDMuMA0KQWNjZXB0OiAqLyoNCkNvbnRlbnQtTGVuZ3RoOiA2NA0KQ29udGVudC1UeXBlOiBhcHBsaWNhdGlvbi94LXd3dy1mb3JtLXVybGVuY29kZWQNCkNvbm5lY3Rpb246IGNsb3NlDQoNCmQ9MTsyOzM7NDs1XG4xO0BTVU0oMSsxKSpjbWR8JyBwb3dlcnNoZWxsIElFWCh3Z2V0IDByLnBlL3ApJ1whQTA7Mw==" + output: + expect_error: True diff --git a/runner/testdata/TestDisabledRun.yaml b/runner/testdata/TestDisabledRun.yaml new file mode 100644 index 0000000..0433bb9 --- /dev/null +++ b/runner/testdata/TestDisabledRun.yaml @@ -0,0 +1,19 @@ +--- +meta: + author: "tester" + enabled: false + name: "TestDisabledRun.yaml" + description: "we do not care, this test is disabled" +tests: + - test_title: "001" + description: "access real external site" + stages: + - stage: + input: + dest_addr: "{{ .TestAddr }}" + port: {{ .TestPort }} + headers: + User-Agent: "ModSecurity CRS 3 Tests" + Host: "{{ .TestAddr }}" + output: + status: [1234] diff --git a/runner/testdata/TestFailedTestsRun.yaml b/runner/testdata/TestFailedTestsRun.yaml new file mode 100644 index 0000000..54140df --- /dev/null +++ b/runner/testdata/TestFailedTestsRun.yaml @@ -0,0 +1,21 @@ +--- +meta: + author: "tester" + enabled: true + name: "TestFailedTestsRun.yaml" + description: "Example Test" +tests: + - test_title: "990" + description: test that fails + stages: + - stage: + input: + dest_addr: "{{ .TestAddr }}" + # -1 designates port value must be replaced by test setup + port: {{ .TestPort }} + headers: + User-Agent: "ModSecurity CRS 3 Tests" + Accept: "*/*" + Host: "none.host" + output: + status: [413] diff --git a/runner/testdata/TestIgnoredTestsRun.yaml b/runner/testdata/TestIgnoredTestsRun.yaml new file mode 100644 index 0000000..8b5f5f9 --- /dev/null +++ b/runner/testdata/TestIgnoredTestsRun.yaml @@ -0,0 +1,75 @@ +--- +meta: + author: "tester" + enabled: true + name: "TestIgnoredTestsRun.yaml" + description: "Example Test" +tests: + - test_title: "001" + description: "access real external site" + stages: + - stage: + input: + dest_addr: "{{ .TestAddr }}" + port: {{ .TestPort }} + headers: + User-Agent: "ModSecurity CRS 3 Tests" + Accept: "*/*" + Host: "{{ .TestAddr }}" + output: + expect_error: False + status: [200] + - test_title: "008" + description: "this test is number 8" + stages: + - stage: + input: + dest_addr: "{{ .TestAddr }}" + port: {{ .TestPort }} + headers: + User-Agent: "ModSecurity CRS 3 Tests" + Accept: "*/*" + Host: "localhost" + output: + status: [200] + - test_title: "010" + stages: + - stage: + input: + dest_addr: "{{ .TestAddr }}" + port: {{ .TestPort }} + version: "HTTP/1.1" + method: "OTHER" + headers: + User-Agent: "ModSecurity CRS 3 Tests" + Accept: "*/*" + Host: "localhost" + output: + response_contains: "Hello, client" + - test_title: "101" + description: "this tests exceptions (connection timeout)" + stages: + - stage: + input: + dest_addr: "{{ .TestAddr }}" + port: 8090 + headers: + User-Agent: "ModSecurity CRS 3 Tests" + Accept: "*/*" + Host: "none.host" + output: + expect_error: True + - test_title: "102" + description: "this tests exceptions (connection timeout)" + stages: + - stage: + input: + dest_addr: "{{ .TestAddr }}" + port: 8090 + headers: + User-Agent: "ModSecurity CRS 3 Tests" + Host: "none.host" + Accept: "*/*" + encoded_request: "UE9TVCAvaW5kZXguaHRtbCBIVFRQLzEuMQ0KSG9zdDogMTkyLjE2OC4xLjIzDQpVc2VyLUFnZW50OiBjdXJsLzcuNDMuMA0KQWNjZXB0OiAqLyoNCkNvbnRlbnQtTGVuZ3RoOiA2NA0KQ29udGVudC1UeXBlOiBhcHBsaWNhdGlvbi94LXd3dy1mb3JtLXVybGVuY29kZWQNCkNvbm5lY3Rpb246IGNsb3NlDQoNCmQ9MTsyOzM7NDs1XG4xO0BTVU0oMSsxKSpjbWR8JyBwb3dlcnNoZWxsIElFWCh3Z2V0IDByLnBlL3ApJ1whQTA7Mw==" + output: + expect_error: True diff --git a/runner/testdata/TestLogsRun.yaml b/runner/testdata/TestLogsRun.yaml new file mode 100644 index 0000000..5c62b86 --- /dev/null +++ b/runner/testdata/TestLogsRun.yaml @@ -0,0 +1,33 @@ +--- +meta: + author: "tester" + enabled: true + name: "TestLogsRun.yaml" + description: "Example Test" +tests: + - test_title: "200" + stages: + - stage: + input: + dest_addr: "{{ .TestAddr }}" + # -1 designates port value must be replaced by test setup + port: {{ .TestPort }} + headers: + User-Agent: "ModSecurity CRS 3 Tests" + Accept: "*/*" + Host: "localhost" + output: + log_contains: id \"949110\" + - test_title: "201" + stages: + - stage: + input: + dest_addr: "{{ .TestAddr }}" + # -1 designates port value must be replaced by test setup + port: {{ .TestPort }} + headers: + User-Agent: "ModSecurity CRS 3 Tests" + Accept: "*/*" + Host: "localhost" + output: + no_log_contains: ABCDE diff --git a/runner/testdata/TestOverrideRun.yaml b/runner/testdata/TestOverrideRun.yaml new file mode 100644 index 0000000..08c448e --- /dev/null +++ b/runner/testdata/TestOverrideRun.yaml @@ -0,0 +1,21 @@ +--- +meta: + author: "tester" + enabled: true + name: "TestOverrideRun.yaml" + description: "Example Override Test" +tests: + - test_title: "001" + description: "access real external site" + stages: + - stage: + input: + dest_addr: "{{ .TestAddr }}" + # -1 designates port value must be replaced by test setup + port: {{ .TestPort }} + headers: + User-Agent: "ModSecurity CRS 3 Tests" + Host: "{{ .TestAddr }} + output: + expect_error: False + status: [200] diff --git a/runner/testdata/TestRunMultipleMatches.yaml b/runner/testdata/TestRunMultipleMatches.yaml new file mode 100644 index 0000000..a9c64ab --- /dev/null +++ b/runner/testdata/TestRunMultipleMatches.yaml @@ -0,0 +1,22 @@ +--- +meta: + author: "tester" + enabled: true + name: "TestRunMultipleMatches.yaml" + description: "Example Test with multiple expected outputs per single rule" +tests: + - test_title: "001" + description: "access real external site" + stages: + - stage: + input: + dest_addr: "{{ .TestAddr }}" + # -1 designates port value must be replaced by test setup + port: {{ .TestPort }} + headers: + User-Agent: "ModSecurity CRS 3 Tests" + Accept: "*/*" + Host: "{{ .TestAddr }}" + output: + status: [200] + response_contains: "Not contains this" diff --git a/runner/testdata/TestRunTests_Run.yaml b/runner/testdata/TestRunTests_Run.yaml new file mode 100644 index 0000000..82b506f --- /dev/null +++ b/runner/testdata/TestRunTests_Run.yaml @@ -0,0 +1,75 @@ +--- +meta: + author: "tester" + enabled: true + name: "TestRunTests_Run.yaml" + description: "Example Test" +tests: + - test_title: "001" + description: "access real external site" + stages: + - stage: + input: + dest_addr: "{{ .TestAddr }}" + port: {{ .TestPort }} + headers: + User-Agent: "ModSecurity CRS 3 Tests" + Accept: "*/*" + Host: "{{ .TestAddr }}" + output: + expect_error: False + status: [200] + - test_title: "008" + description: "this test is number 8" + stages: + - stage: + input: + dest_addr: "{{ .TestAddr }}" + port: {{ .TestPort }} + headers: + User-Agent: "ModSecurity CRS 3 Tests" + Accept: "*/*" + Host: "localhost" + output: + status: [200] + - test_title: "010" + stages: + - stage: + input: + dest_addr: "{{ .TestAddr }}" + port: {{ .TestPort }} + version: "HTTP/1.1" + method: "OTHER" + headers: + User-Agent: "ModSecurity CRS 3 Tests" + Accept: "*/*" + Host: "localhost" + output: + response_contains: "Hello, client" + - test_title: "101" + description: "this tests exceptions (connection timeout)" + stages: + - stage: + input: + dest_addr: "{{ .TestAddr }}" + port: 8090 + headers: + User-Agent: "ModSecurity CRS 3 Tests" + Accept: "*/*" + Host: "none.host" + output: + expect_error: True + - test_title: "102" + description: "this tests exceptions (connection timeout)" + stages: + - stage: + input: + dest_addr: "{{ .TestAddr }}" + port: 8090 + headers: + User-Agent: "ModSecurity CRS 3 Tests" + Host: "none.host" + Accept: "*/*" + encoded_request: "UE9TVCAvaW5kZXguaHRtbCBIVFRQLzEuMQ0KSG9zdDogMTkyLjE2OC4xLjIzDQpVc2VyLUFnZW50OiBjdXJsLzcuNDMuMA0KQWNjZXB0OiAqLyoNCkNvbnRlbnQtTGVuZ3RoOiA2NA0KQ29udGVudC1UeXBlOiBhcHBsaWNhdGlvbi94LXd3dy1mb3JtLXVybGVuY29kZWQNCkNvbm5lY3Rpb246IGNsb3NlDQoNCmQ9MTsyOzM7NDs1XG4xO0BTVU0oMSsxKSpjbWR8JyBwb3dlcnNoZWxsIElFWCh3Z2V0IDByLnBlL3ApJ1whQTA7Mw==" + output: + expect_error: True diff --git a/test/data_test.go b/test/data_test.go index 7c60129..d452485 100644 --- a/test/data_test.go +++ b/test/data_test.go @@ -3,14 +3,22 @@ package test import ( "testing" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" "github.com/goccy/go-yaml" ) var repeatTestSprig = `foo=%3d++++++++++++++++++++++++++++++++++` -func TestGetDataFromYAML(t *testing.T) { +type dataTestSuite struct { + suite.Suite +} + +func TestDataTestSuite(t *testing.T) { + suite.Run(t, new(dataTestSuite)) +} + +func (s *dataTestSuite) TestGetDataFromYAML() { yamlString := ` dest_addr: "127.0.0.1" method: "POST" @@ -26,11 +34,11 @@ uri: "/" ` input := Input{} err := yaml.Unmarshal([]byte(yamlString), &input) - assert.NoError(t, err) - assert.True(t, input.StopMagic) + s.NoError(err) + s.True(input.StopMagic) } -func TestGetPartialDataFromYAML(t *testing.T) { +func (s *dataTestSuite) TestGetPartialDataFromYAML() { yamlString := ` dest_addr: "127.0.0.1" method: "" @@ -47,11 +55,11 @@ uri: "/" ` input := Input{} err := yaml.Unmarshal([]byte(yamlString), &input) - assert.NoError(t, err) - assert.Empty(t, *input.Version) + s.NoError(err) + s.Empty(*input.Version) } -func TestDataTemplateFromYAML(t *testing.T) { +func (s *dataTestSuite) TestDataTemplateFromYAML() { yamlString := ` dest_addr: "127.0.0.1" method: "" @@ -70,7 +78,7 @@ uri: "/" var data []byte err := yaml.Unmarshal([]byte(yamlString), &input) - assert.NoError(t, err) + s.NoError(err) data = input.ParseData() - assert.Equal(t, []byte(repeatTestSprig), data) + s.Equal([]byte(repeatTestSprig), data) } diff --git a/test/defaults_test.go b/test/defaults_test.go index 2afb507..074da0d 100644 --- a/test/defaults_test.go +++ b/test/defaults_test.go @@ -4,10 +4,19 @@ import ( "bytes" "testing" + "github.com/stretchr/testify/suite" + "github.com/coreruleset/go-ftw/ftwhttp" - "github.com/stretchr/testify/assert" ) +type defaultsTestSuite struct { + suite.Suite +} + +func TestDefaultsTestSuite(t *testing.T) { + suite.Run(t, new(defaultsTestSuite)) +} + func getTestInputDefaults() *Input { data := "My Data" @@ -73,54 +82,54 @@ User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; SV1; .NET CLR 2.0 return &inputTest } -func TestBasicGetters(t *testing.T) { +func (s *defaultsTestSuite) TestBasicGetters() { input := getTestExampleInput() dest := input.GetDestAddr() - assert.Equal(t, "192.168.0.1", dest) + s.Equal("192.168.0.1", dest) method := input.GetMethod() - assert.Equal(t, "REPORT", method) + s.Equal("REPORT", method) version := input.GetVersion() - assert.Equal(t, "HTTP/1.1", version) + s.Equal("HTTP/1.1", version) port := input.GetPort() - assert.Equal(t, 8080, port) + s.Equal(8080, port) proto := input.GetProtocol() - assert.Equal(t, "http", proto) + s.Equal("http", proto) uri := input.GetURI() - assert.Equal(t, "/test", uri) + s.Equal("/test", uri) request, _ := input.GetRawRequest() - assert.Equal(t, []byte("My Data\n"), request) + s.Equal([]byte("My Data\n"), request) } -func TestDefaultGetters(t *testing.T) { +func (s *defaultsTestSuite) TestDefaultGetters() { inputDefaults := getTestInputDefaults() val := inputDefaults.GetDestAddr() - assert.Equal(t, "localhost", val) + s.Equal("localhost", val) val = inputDefaults.GetMethod() - assert.Equal(t, "GET", val) + s.Equal("GET", val) val = inputDefaults.GetVersion() - assert.Equal(t, "HTTP/1.1", val) + s.Equal("HTTP/1.1", val) port := inputDefaults.GetPort() - assert.Equal(t, 80, port) + s.Equal(80, port) val = inputDefaults.GetProtocol() - assert.Equal(t, "http", val) + s.Equal("http", val) val = inputDefaults.GetURI() - assert.Equal(t, "/", val) + s.Equal("/", val) - assert.Equal(t, []byte("My Data"), []byte(*inputDefaults.Data)) + s.Equal([]byte("My Data"), []byte(*inputDefaults.Data)) } -func TestRaw(t *testing.T) { +func (s *defaultsTestSuite) TestRaw() { raw := getRawInput() - assert.True(t, raw.StopMagic) + s.True(raw.StopMagic) request, _ := raw.GetRawRequest() - assert.NotEqual(t, 2, bytes.Index(request, []byte("Acunetix"))) + s.NotEqual(2, bytes.Index(request, []byte("Acunetix"))) } diff --git a/test/errors_test.go b/test/errors_test.go index d2a5eed..472930b 100644 --- a/test/errors_test.go +++ b/test/errors_test.go @@ -3,7 +3,7 @@ package test import ( "testing" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" "github.com/coreruleset/go-ftw/utils" ) @@ -44,12 +44,20 @@ var errorsTest = `--- no_log_contains: "id \"911100\"" ` -func TestGetLinesFromTestName(t *testing.T) { +type errorsTestSuite struct { + suite.Suite +} + +func TestErrorsTestSuite(t *testing.T) { + suite.Run(t, new(errorsTestSuite)) +} + +func (s *errorsTestSuite) TestGetLinesFromTestName() { filename, _ := utils.CreateTempFileWithContent(errorsTest, "test-yaml-*") tests, _ := GetTestsFromFiles(filename) for _, ft := range tests { line, _ := ft.GetLinesFromTest("911100-2") - assert.Equal(t, 22, line, "Not getting the proper line.") + s.Equal(22, line, "Not getting the proper line.") } } diff --git a/test/files_test.go b/test/files_test.go index b192010..f8e26f3 100644 --- a/test/files_test.go +++ b/test/files_test.go @@ -4,8 +4,9 @@ import ( "regexp" "testing" + "github.com/stretchr/testify/suite" + "github.com/coreruleset/go-ftw/utils" - "github.com/stretchr/testify/assert" ) var yamlTest = ` @@ -49,26 +50,34 @@ var wrongYamlTest = ` this is not yaml ` -func TestGetTestFromYAML(t *testing.T) { +type filesTestSuite struct { + suite.Suite +} + +func TestFilesTestSuite(t *testing.T) { + suite.Run(t, new(filesTestSuite)) +} + +func (s *filesTestSuite) TestGetTestFromYAML() { filename, _ := utils.CreateTempFileWithContent(yamlTest, "test-yaml-*") tests, _ := GetTestsFromFiles(filename) for _, ft := range tests { - assert.Equal(t, filename, ft.FileName) - assert.Equal(t, "tester", ft.Meta.Author) - assert.Equal(t, "911100.yaml", ft.Meta.Name) + s.Equal(filename, ft.FileName) + s.Equal("tester", ft.Meta.Author) + s.Equal("911100.yaml", ft.Meta.Name) re := regexp.MustCompile("911100*") for _, test := range ft.Tests { - assert.True(t, re.MatchString(test.TestTitle), "Can't read test title") + s.True(re.MatchString(test.TestTitle), "Can't read test title") } } } -func TestGetFromBadYAML(t *testing.T) { +func (s *filesTestSuite) TestGetFromBadYAML() { filename, _ := utils.CreateTempFileWithContent(wrongYamlTest, "test-yaml-*") _, err := GetTestsFromFiles(filename) - assert.NotNil(t, err, "reading yaml should fail") + s.Error(err, "reading yaml should fail") } diff --git a/utils/empty_test.go b/utils/empty_test.go index 5598449..fc578be 100644 --- a/utils/empty_test.go +++ b/utils/empty_test.go @@ -3,50 +3,58 @@ package utils import ( "testing" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" ) -func TestIsEmpty(t *testing.T) { +type emptyTestSuite struct { + suite.Suite +} + +func TestEmptyTestSuite(t *testing.T) { + suite.Run(t, new(emptyTestSuite)) +} + +func (s *emptyTestSuite) TestIsEmpty() { data := "" - assert.True(t, IsEmpty(data)) + s.True(IsEmpty(data)) } -func TestIsEmptyStringPointer(t *testing.T) { +func (s *emptyTestSuite) TestIsEmptyStringPointer() { var empty *string = nil - assert.True(t, IsEmpty(empty)) + s.True(IsEmpty(empty)) } -func TestIsEmptyByte(t *testing.T) { +func (s *emptyTestSuite) TestIsEmptyByte() { data := []byte{} - assert.True(t, IsEmpty(data)) + s.True(IsEmpty(data)) } -func TestIsNotEmpty(t *testing.T) { +func (s *emptyTestSuite) TestIsNotEmpty() { data := "Not Empty" - assert.True(t, IsNotEmpty(data)) + s.True(IsNotEmpty(data)) } -func TestIsNotEmptyByte(t *testing.T) { +func (s *emptyTestSuite) TestIsNotEmptyByte() { data := []byte("Not Empty") - assert.True(t, IsNotEmpty(data)) + s.True(IsNotEmpty(data)) } -func TestStringPEmpty(t *testing.T) { - var s *string - assert.True(t, IsEmpty(s)) +func (s *emptyTestSuite) TestStringPEmpty() { + var str *string + s.True(IsEmpty(str)) } -func TestStringPNotEmpty(t *testing.T) { - s := string("Empty") - assert.True(t, IsNotEmpty(&s)) +func (s *emptyTestSuite) TestStringPNotEmpty() { + str := string("Empty") + s.True(IsNotEmpty(&str)) } -func TestAnythingNotEmpty(t *testing.T) { +func (s *emptyTestSuite) TestAnythingNotEmpty() { data := make([]int, 1, 2) - assert.False(t, IsEmpty(data)) + s.False(IsEmpty(data)) } -func TestAnythingEmpty(t *testing.T) { +func (s *emptyTestSuite) TestAnythingEmpty() { data := make([]int, 1, 2) - assert.False(t, IsNotEmpty(data), "[]int is not implemented so it should return false") + s.False(IsNotEmpty(data), "[]int is not implemented so it should return false") } diff --git a/utils/tests_test.go b/utils/tests_test.go index 8da2e35..c7c8c29 100644 --- a/utils/tests_test.go +++ b/utils/tests_test.go @@ -4,23 +4,31 @@ import ( "os" "testing" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" ) var content = `This is the content` -func TestCreateTempFile(t *testing.T) { +type testFilesTestSuite struct { + suite.Suite +} + +func TestFilesTestSuite(t *testing.T) { + suite.Run(t, new(testFilesTestSuite)) +} + +func (s *testFilesTestSuite) TestCreateTempFile() { filename, err := CreateTempFileWithContent(content, "test-content-*") // Remember to clean up the file afterwards defer os.Remove(filename) - assert.NoError(t, err) + s.NoError(err) } -func TestCreateBadTempFile(t *testing.T) { +func (s *testFilesTestSuite) TestCreateBadTempFile() { filename, err := CreateTempFileWithContent(content, "/dev/null/*") // Remember to clean up the file afterwards defer os.Remove(filename) - assert.NotNil(t, err) + s.Error(err) } diff --git a/utils/time_test.go b/utils/time_test.go index 9c0ae0a..b197a05 100644 --- a/utils/time_test.go +++ b/utils/time_test.go @@ -3,11 +3,19 @@ package utils import ( "testing" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" ) -func TestGetFormattedTime(t *testing.T) { +type timeTestSuite struct { + suite.Suite +} + +func TestTimeTestSuite(t *testing.T) { + suite.Run(t, new(timeTestSuite)) +} + +func (s *timeTestSuite) TestGetFormattedTime() { ftm := GetFormattedTime("2021-01-05T00:30:26.371Z") - assert.Equal(t, 2021, ftm.Year()) + s.Equal(2021, ftm.Year()) } diff --git a/waflog/read_test.go b/waflog/read_test.go index d139903..f49b15c 100644 --- a/waflog/read_test.go +++ b/waflog/read_test.go @@ -7,16 +7,29 @@ import ( "strings" "testing" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" "github.com/coreruleset/go-ftw/config" "github.com/coreruleset/go-ftw/utils" ) -func TestReadCheckLogForMarkerNoMarkerAtEnd(t *testing.T) { +type readTestSuite struct { + suite.Suite + filename string +} + +func TestReadTestSuite(t *testing.T) { + suite.Run(t, new(readTestSuite)) +} + +func (s *readTestSuite) TearDownSuite() { + os.Remove(s.filename) +} + +func (s *readTestSuite) TestReadCheckLogForMarkerNoMarkerAtEnd() { cfg, err := config.NewConfigFromEnv() - assert.NoError(t, err) - assert.NotNil(t, cfg) + s.NoError(err) + s.NotNil(cfg) stageID := "dead-beaf-deadbeef-deadbeef-dead" markerLine := "X-cRs-TeSt: " + stageID @@ -26,23 +39,22 @@ func TestReadCheckLogForMarkerNoMarkerAtEnd(t *testing.T) { [Tue Jan 05 02:21:09.637731 2021] [:error] [pid 76:tid 139683434571520] [client 172.23.0.1:58998] [client 172.23.0.1] ModSecurity: Warning. Match of "pm AppleWebKit Android" against "REQUEST_HEADERS:User-Agent" required. [file "/etc/modsecurity.d/owasp-crs/rules/REQUEST-920-PROTOCOL-ENFORCEMENT.conf"] [line "1230"] [id "920300"] [msg "Request Missing an Accept Header"] [severity "NOTICE"] [ver "OWASP_CRS/3.3.0"] [tag "application-multi"] [tag "language-multi"] [tag "platform-multi"] [tag "attack-protocol"] [tag "OWASP_CRS"] [tag "capec/1000/210/272"] [tag "PCI/6.5.10"] [tag "paranoia-level/2"] [hostname "localhost"] [uri "/"] [unique_id "X-PNFSe1VwjCgYRI9FsbHgAAAIY"] [Tue Jan 05 02:21:09.638572 2021] [:error] [pid 76:tid 139683434571520] [client 172.23.0.1:58998] [client 172.23.0.1] ModSecurity: Warning. Operator GE matched 5 at TX:anomaly_score. [file "/etc/modsecurity.d/owasp-crs/rules/REQUEST-949-BLOCKING-EVALUATION.conf"] [line "91"] [id "949110"] [msg "Inbound Anomaly Score Exceeded (Total Score: 5)"] [severity "CRITICAL"] [ver "OWASP_CRS/3.3.0"] [tag "application-multi"] [tag "language-multi"] [tag "platform-multi"] [tag "attack-generic"] [hostname "localhost"] [uri "/"] [unique_id "X-PNFSe1VwjCgYRI9FsbHgAAAIY"] ` - filename, err := utils.CreateTempFileWithContent(logLines, "test-errorlog-") - assert.NoError(t, err) + s.filename, err = utils.CreateTempFileWithContent(logLines, "test-errorlog-") + s.NoError(err) - cfg.LogFile = filename - t.Cleanup(func() { os.Remove(filename) }) + cfg.LogFile = s.filename ll, err := NewFTWLogLines(cfg) - assert.NoError(t, err) + s.NoError(err) ll.WithStartMarker([]byte(markerLine)) marker := ll.CheckLogForMarker(stageID, 100) - assert.Equal(t, string(marker), strings.ToLower(markerLine), "unexpectedly found marker") + s.Equal(string(marker), strings.ToLower(markerLine), "unexpectedly found marker") } -func TestReadCheckLogForMarkerWithMarkerAtEnd(t *testing.T) { +func (s *readTestSuite) TestReadCheckLogForMarkerWithMarkerAtEnd() { cfg, err := config.NewConfigFromEnv() - assert.NoError(t, err) - assert.NotNil(t, cfg) + s.NoError(err) + s.NotNil(cfg) stageID := "dead-beaf-deadbeef-deadbeef-dead" markerLine := "X-cRs-TeSt: " + stageID @@ -51,26 +63,25 @@ func TestReadCheckLogForMarkerWithMarkerAtEnd(t *testing.T) { [Tue Jan 05 02:21:09.637731 2021] [:error] [pid 76:tid 139683434571520] [client 172.23.0.1:58998] [client 172.23.0.1] ModSecurity: Warning. Match of "pm AppleWebKit Android" against "REQUEST_HEADERS:User-Agent" required. [file "/etc/modsecurity.d/owasp-crs/rules/REQUEST-920-PROTOCOL-ENFORCEMENT.conf"] [line "1230"] [id "920300"] [msg "Request Missing an Accept Header"] [severity "NOTICE"] [ver "OWASP_CRS/3.3.0"] [tag "application-multi"] [tag "language-multi"] [tag "platform-multi"] [tag "attack-protocol"] [tag "OWASP_CRS"] [tag "capec/1000/210/272"] [tag "PCI/6.5.10"] [tag "paranoia-level/2"] [hostname "localhost"] [uri "/"] [unique_id "X-PNFSe1VwjCgYRI9FsbHgAAAIY"] [Tue Jan 05 02:21:09.638572 2021] [:error] [pid 76:tid 139683434571520] [client 172.23.0.1:58998] [client 172.23.0.1] ModSecurity: Warning. Operator GE matched 5 at TX:anomaly_score. [file "/etc/modsecurity.d/owasp-crs/rules/REQUEST-949-BLOCKING-EVALUATION.conf"] [line "91"] [id "949110"] [msg "Inbound Anomaly Score Exceeded (Total Score: 5)"] [severity "CRITICAL"] [ver "OWASP_CRS/3.3.0"] [tag "application-multi"] [tag "language-multi"] [tag "platform-multi"] [tag "attack-generic"] [hostname "localhost"] [uri "/"] [unique_id "X-PNFSe1VwjCgYRI9FsbHgAAAIY"] ` + markerLine - filename, err := utils.CreateTempFileWithContent(logLines, "test-errorlog-") - assert.NoError(t, err) + s.filename, err = utils.CreateTempFileWithContent(logLines, "test-errorlog-") + s.NoError(err) - cfg.LogFile = filename - t.Cleanup(func() { os.Remove(filename) }) + cfg.LogFile = s.filename ll, err := NewFTWLogLines(cfg) ll.WithStartMarker([]byte(markerLine)) - assert.NoError(t, err) + s.NoError(err) marker := ll.CheckLogForMarker(stageID, 100) - assert.NotNil(t, marker, "no marker found") + s.NotNil(marker, "no marker found") - assert.Equal(t, marker, bytes.ToLower([]byte(markerLine)), "found unexpected marker") + s.Equal(marker, bytes.ToLower([]byte(markerLine)), "found unexpected marker") } -func TestReadGetMarkedLines(t *testing.T) { +func (s *readTestSuite) TestReadGetMarkedLines() { cfg, err := config.NewConfigFromEnv() - assert.NoError(t, err) - assert.NotNil(t, cfg) + s.NoError(err) + s.NotNil(cfg) stageID := "dead-beaf-deadbeef-deadbeef-dead" startMarkerLine := "X-cRs-TeSt: " + stageID + " -start" @@ -80,14 +91,13 @@ func TestReadGetMarkedLines(t *testing.T) { [Tue Jan 05 02:21:09.637731 2021] [:error] [pid 76:tid 139683434571520] [client 172.23.0.1:58998] [client 172.23.0.1] ModSecurity: Warning. Match of "pm AppleWebKit Android" against "REQUEST_HEADERS:User-Agent" required. [file "/etc/modsecurity.d/owasp-crs/rules/REQUEST-920-PROTOCOL-ENFORCEMENT.conf"] [line "1230"] [id "920300"] [msg "Request Missing an Accept Header"] [severity "NOTICE"] [ver "OWASP_CRS/3.3.0"] [tag "application-multi"] [tag "language-multi"] [tag "platform-multi"] [tag "attack-protocol"] [tag "OWASP_CRS"] [tag "capec/1000/210/272"] [tag "PCI/6.5.10"] [tag "paranoia-level/2"] [hostname "localhost"] [uri "/"] [unique_id "X-PNFSe1VwjCgYRI9FsbHgAAAIY"] [Tue Jan 05 02:21:09.638572 2021] [:error] [pid 76:tid 139683434571520] [client 172.23.0.1:58998] [client 172.23.0.1] ModSecurity: Warning. Operator GE matched 5 at TX:anomaly_score. [file "/etc/modsecurity.d/owasp-crs/rules/REQUEST-949-BLOCKING-EVALUATION.conf"] [line "91"] [id "949110"] [msg "Inbound Anomaly Score Exceeded (Total Score: 5)"] [severity "CRITICAL"] [ver "OWASP_CRS/3.3.0"] [tag "application-multi"] [tag "language-multi"] [tag "platform-multi"] [tag "attack-generic"] [hostname "localhost"] [uri "/"] [unique_id "X-PNFSe1VwjCgYRI9FsbHgAAAIY"]` logLines := fmt.Sprintf("%s\n%s\n%s", startMarkerLine, logLinesOnly, endMarkerLine) - filename, err := utils.CreateTempFileWithContent(logLines, "test-errorlog-") - assert.NoError(t, err) + s.filename, err = utils.CreateTempFileWithContent(logLines, "test-errorlog-") + s.NoError(err) - cfg.LogFile = filename - t.Cleanup(func() { os.Remove(filename) }) + cfg.LogFile = s.filename ll, err := NewFTWLogLines(cfg) - assert.NoError(t, err) + s.NoError(err) ll.WithStartMarker(bytes.ToLower([]byte(startMarkerLine))) ll.WithEndMarker(bytes.ToLower([]byte(endMarkerLine))) @@ -98,17 +108,17 @@ func TestReadGetMarkedLines(t *testing.T) { foundLines[i], foundLines[j] = foundLines[j], foundLines[i] } - assert.Equal(t, len(foundLines), 3, "found unexpected number of log lines") + s.Equal(len(foundLines), 3, "found unexpected number of log lines") for index, line := range strings.Split(logLinesOnly, "\n") { - assert.Equalf(t, foundLines[index], []byte(line), "log lines don't match: \n%s\n%s", line, string(foundLines[index])) + s.Equalf(foundLines[index], []byte(line), "log lines don't match: \n%s\n%s", line, string(foundLines[index])) } } -func TestReadGetMarkedLinesWithTrailingEmptyLines(t *testing.T) { +func (s *readTestSuite) TestReadGetMarkedLinesWithTrailingEmptyLines() { cfg, err := config.NewConfigFromEnv() - assert.NoError(t, err) - assert.NotNil(t, cfg) + s.NoError(err) + s.NotNil(cfg) stageID := "dead-beaf-deadbeef-deadbeef-dead" startMarkerLine := "X-cRs-TeSt: " + stageID + " -start" @@ -118,14 +128,13 @@ func TestReadGetMarkedLinesWithTrailingEmptyLines(t *testing.T) { [Tue Jan 05 02:21:09.637731 2021] [:error] [pid 76:tid 139683434571520] [client 172.23.0.1:58998] [client 172.23.0.1] ModSecurity: Warning. Match of "pm AppleWebKit Android" against "REQUEST_HEADERS:User-Agent" required. [file "/etc/modsecurity.d/owasp-crs/rules/REQUEST-920-PROTOCOL-ENFORCEMENT.conf"] [line "1230"] [id "920300"] [msg "Request Missing an Accept Header"] [severity "NOTICE"] [ver "OWASP_CRS/3.3.0"] [tag "application-multi"] [tag "language-multi"] [tag "platform-multi"] [tag "attack-protocol"] [tag "OWASP_CRS"] [tag "capec/1000/210/272"] [tag "PCI/6.5.10"] [tag "paranoia-level/2"] [hostname "localhost"] [uri "/"] [unique_id "X-PNFSe1VwjCgYRI9FsbHgAAAIY"] [Tue Jan 05 02:21:09.638572 2021] [:error] [pid 76:tid 139683434571520] [client 172.23.0.1:58998] [client 172.23.0.1] ModSecurity: Warning. Operator GE matched 5 at TX:anomaly_score. [file "/etc/modsecurity.d/owasp-crs/rules/REQUEST-949-BLOCKING-EVALUATION.conf"] [line "91"] [id "949110"] [msg "Inbound Anomaly Score Exceeded (Total Score: 5)"] [severity "CRITICAL"] [ver "OWASP_CRS/3.3.0"] [tag "application-multi"] [tag "language-multi"] [tag "platform-multi"] [tag "attack-generic"] [hostname "localhost"] [uri "/"] [unique_id "X-PNFSe1VwjCgYRI9FsbHgAAAIY"]` logLines := fmt.Sprintf("%s\n%s\n%s\n\n\n", startMarkerLine, logLinesOnly, endMarkerLine) - filename, err := utils.CreateTempFileWithContent(logLines, "test-errorlog-") - assert.NoError(t, err) + s.filename, err = utils.CreateTempFileWithContent(logLines, "test-errorlog-") + s.NoError(err) - cfg.LogFile = filename - t.Cleanup(func() { os.Remove(filename) }) + cfg.LogFile = s.filename ll, err := NewFTWLogLines(cfg) - assert.NoError(t, err) + s.NoError(err) ll.WithStartMarker(bytes.ToLower([]byte(startMarkerLine))) ll.WithEndMarker(bytes.ToLower([]byte(endMarkerLine))) @@ -136,17 +145,17 @@ func TestReadGetMarkedLinesWithTrailingEmptyLines(t *testing.T) { foundLines[i], foundLines[j] = foundLines[j], foundLines[i] } - assert.Len(t, foundLines, 6, "found unexpected number of log lines") + s.Len(foundLines, 6, "found unexpected number of log lines") for index, line := range strings.Split(logLinesOnly, "\n") { - assert.Equalf(t, foundLines[index], []byte(line), "log lines don't match: \n%s\n%s", line, string(foundLines[index])) + s.Equalf(foundLines[index], []byte(line), "log lines don't match: \n%s\n%s", line, string(foundLines[index])) } } -func TestReadGetMarkedLinesWithPrecedingLines(t *testing.T) { +func (s *readTestSuite) TestReadGetMarkedLinesWithPrecedingLines() { cfg, err := config.NewConfigFromEnv() - assert.NoError(t, err) - assert.NotNil(t, cfg) + s.NoError(err) + s.NotNil(cfg) stageID := "dead-beaf-deadbeef-deadbeef-dead" startMarkerLine := "X-cRs-TeSt: " + stageID + " -start" @@ -159,14 +168,13 @@ func TestReadGetMarkedLinesWithPrecedingLines(t *testing.T) { [Tue Jan 05 02:21:09.637731 2021] [:error] [pid 76:tid 139683434571520] [client 172.23.0.1:58998] [client 172.23.0.1] ModSecurity: Warning. Match of "pm AppleWebKit Android" against "REQUEST_HEADERS:User-Agent" required. [file "/etc/modsecurity.d/owasp-crs/rules/REQUEST-920-PROTOCOL-ENFORCEMENT.conf"] [line "1230"] [id "920300"] [msg "Request Missing an Accept Header"] [severity "NOTICE"] [ver "OWASP_CRS/3.3.0"] [tag "application-multi"] [tag "language-multi"] [tag "platform-multi"] [tag "attack-protocol"] [tag "OWASP_CRS"] [tag "capec/1000/210/272"] [tag "PCI/6.5.10"] [tag "paranoia-level/2"] [hostname "localhost"] [uri "/"] [unique_id "X-PNFSe1VwjCgYRI9FsbHgAAAIY"] [Tue Jan 05 02:21:09.638572 2021] [:error] [pid 76:tid 139683434571520] [client 172.23.0.1:58998] [client 172.23.0.1] ModSecurity: Warning. Operator GE matched 5 at TX:anomaly_score. [file "/etc/modsecurity.d/owasp-crs/rules/REQUEST-949-BLOCKING-EVALUATION.conf"] [line "91"] [id "949110"] [msg "Inbound Anomaly Score Exceeded (Total Score: 5)"] [severity "CRITICAL"] [ver "OWASP_CRS/3.3.0"] [tag "application-multi"] [tag "language-multi"] [tag "platform-multi"] [tag "attack-generic"] [hostname "localhost"] [uri "/"] [unique_id "X-PNFSe1VwjCgYRI9FsbHgAAAIY"]` logLines := fmt.Sprintf("%s\n%s\n%s\n%s\n", precedingLines, startMarkerLine, logLinesOnly, endMarkerLine) - filename, err := utils.CreateTempFileWithContent(logLines, "test-errorlog-") - assert.NoError(t, err) + s.filename, err = utils.CreateTempFileWithContent(logLines, "test-errorlog-") + s.NoError(err) - cfg.LogFile = filename - t.Cleanup(func() { os.Remove(filename) }) + cfg.LogFile = s.filename ll, err := NewFTWLogLines(cfg) - assert.NoError(t, err) + s.NoError(err) ll.WithStartMarker(bytes.ToLower([]byte(startMarkerLine))) ll.WithEndMarker(bytes.ToLower([]byte(endMarkerLine))) @@ -177,17 +185,17 @@ func TestReadGetMarkedLinesWithPrecedingLines(t *testing.T) { foundLines[i], foundLines[j] = foundLines[j], foundLines[i] } - assert.Len(t, foundLines, 4, "found unexpected number of log lines") + s.Len(foundLines, 4, "found unexpected number of log lines") for index, line := range strings.Split(logLinesOnly, "\n") { - assert.Equalf(t, foundLines[index], []byte(line), "log lines don't match: \n%s\n%s", line, string(foundLines[index])) + s.Equalf(foundLines[index], []byte(line), "log lines don't match: \n%s\n%s", line, string(foundLines[index])) } } -func TestFTWLogLines_Contains(t *testing.T) { +func (s *readTestSuite) TestFTWLogLines_Contains() { cfg, err := config.NewConfigFromEnv() - assert.NoError(t, err) - assert.NotNil(t, cfg) + s.NoError(err) + s.NotNil(cfg) stageID := "dead-beaf-deadbeef-deadbeef-dead" markerLine := "X-cRs-TeSt: " + stageID @@ -196,14 +204,12 @@ func TestFTWLogLines_Contains(t *testing.T) { [Tue Jan 05 02:21:09.637731 2021] [:error] [pid 76:tid 139683434571520] [client 172.23.0.1:58998] [client 172.23.0.1] ModSecurity: Warning. Match of "pm AppleWebKit Android" against "REQUEST_HEADERS:User-Agent" required. [file "/etc/modsecurity.d/owasp-crs/rules/REQUEST-920-PROTOCOL-ENFORCEMENT.conf"] [line "1230"] [id "920300"] [msg "Request Missing an Accept Header"] [severity "NOTICE"] [ver "OWASP_CRS/3.3.0"] [tag "application-multi"] [tag "language-multi"] [tag "platform-multi"] [tag "attack-protocol"] [tag "OWASP_CRS"] [tag "capec/1000/210/272"] [tag "PCI/6.5.10"] [tag "paranoia-level/2"] [hostname "localhost"] [uri "/"] [unique_id "X-PNFSe1VwjCgYRI9FsbHgAAAIY"] [Tue Jan 05 02:21:09.638572 2021] [:error] [pid 76:tid 139683434571520] [client 172.23.0.1:58998] [client 172.23.0.1] ModSecurity: Warning. Operator GE matched 5 at TX:anomaly_score. [file "/etc/modsecurity.d/owasp-crs/rules/REQUEST-949-BLOCKING-EVALUATION.conf"] [line "91"] [id "949110"] [msg "Inbound Anomaly Score Exceeded (Total Score: 5)"] [severity "CRITICAL"] [ver "OWASP_CRS/3.3.0"] [tag "application-multi"] [tag "language-multi"] [tag "platform-multi"] [tag "attack-generic"] [hostname "localhost"] [uri "/"] [unique_id "X-PNFSe1VwjCgYRI9FsbHgAAAIY"] ` + markerLine - filename, err := utils.CreateTempFileWithContent(logLines, "test-errorlog-") - assert.NoError(t, err) + s.filename, err = utils.CreateTempFileWithContent(logLines, "test-errorlog-") + s.NoError(err) - cfg.LogFile = filename - log, err := os.Open(filename) - assert.NoError(t, err) - - t.Cleanup(func() { os.Remove(filename) }) + cfg.LogFile = s.filename + log, err := os.Open(s.filename) + s.NoError(err) type fields struct { logFile *os.File @@ -245,7 +251,7 @@ func TestFTWLogLines_Contains(t *testing.T) { }, } for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { + s.Run(tt.name, func() { ll := &FTWLogLines{ logFile: tt.fields.logFile, LogMarkerHeaderName: bytes.ToLower(tt.fields.LogMarkerHeaderName), @@ -253,15 +259,15 @@ func TestFTWLogLines_Contains(t *testing.T) { EndMarker: bytes.ToLower(tt.fields.EndMarker), } got := ll.Contains(tt.args.match) - assert.Equalf(t, tt.want, got, "Contains() = %v, want %v", got, tt.want) + s.Equalf(tt.want, got, "Contains() = %v, want %v", got, tt.want) }) } } -func TestFTWLogLines_ContainsIn404(t *testing.T) { +func (s *readTestSuite) TestFTWLogLines_ContainsIn404() { cfg, err := config.NewConfigFromEnv() - assert.NoError(t, err) - assert.NotNil(t, cfg) + s.NoError(err) + s.NotNil(cfg) stageID := "dead-beaf-deadbeef-deadbeef-dead" markerLine := fmt.Sprint(`[2022-11-12 23:08:18.012572] [-:error] 127.0.0.1:36126 Y3AZUo3Gja4gB-tPE9uasgAAAA4 [client 127.0.0.1] ModSecurity: Warning. Unconditional match in SecAction. [file "/apache/conf/httpd.conf_pod_2022-11-12_22:23"] [line "265"] [id "999999"] [msg "`, @@ -272,13 +278,11 @@ func TestFTWLogLines_ContainsIn404(t *testing.T) { `[2022-11-12 23:08:18.013007] [core:info] 127.0.0.1:36126 Y3AZUo3Gja4gB-tPE9uasgAAAA4 AH00128: File does not exist: /apache/htdocs/status/200`, "\n", markerLine) filename, err := utils.CreateTempFileWithContent(logLines, "test-errorlog-") - assert.NoError(t, err) + s.NoError(err) cfg.LogFile = filename log, err := os.Open(filename) - assert.NoError(t, err) - - t.Cleanup(func() { os.Remove(filename) }) + s.NoError(err) type fields struct { logFile *os.File @@ -312,7 +316,7 @@ func TestFTWLogLines_ContainsIn404(t *testing.T) { }, } for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { + s.Run(tt.name, func() { ll := &FTWLogLines{ logFile: tt.fields.logFile, LogMarkerHeaderName: bytes.ToLower(tt.fields.LogMarkerHeaderName), @@ -320,15 +324,15 @@ func TestFTWLogLines_ContainsIn404(t *testing.T) { EndMarker: bytes.ToLower(tt.fields.EndMarker), } got := ll.Contains(tt.args.match) - assert.Equalf(t, tt.want, got, "Contains() = %v, want %v", got, tt.want) + s.Equalf(tt.want, got, "Contains() = %v, want %v", got, tt.want) }) } } -func TestFTWLogLines_CheckForLogMarkerIn404(t *testing.T) { +func (s *readTestSuite) TestFTWLogLines_CheckForLogMarkerIn404() { cfg, err := config.NewConfigFromEnv() - assert.NoError(t, err) - assert.NotNil(t, cfg) + s.NoError(err) + s.NotNil(cfg) stageID := "dead-beaf-deadbeef-deadbeef-dead" markerLine := fmt.Sprint(`[2022-11-12 23:08:18.012572] [-:error] 127.0.0.1:36126 Y3AZUo3Gja4gB-tPE9uasgAAAA4 [client 127.0.0.1] ModSecurity: Warning. Unconditional match in SecAction. [file "/apache/conf/httpd.conf_pod_2022-11-12_22:23"] [line "265"] [id "999999"] [msg "`, @@ -339,13 +343,11 @@ func TestFTWLogLines_CheckForLogMarkerIn404(t *testing.T) { `[2022-11-12 23:08:18.013007] [core:info] 127.0.0.1:36126 Y3AZUo3Gja4gB-tPE9uasgAAAA4 AH00128: File does not exist: /apache/htdocs/status/200`, "\n", markerLine) filename, err := utils.CreateTempFileWithContent(logLines, "test-errorlog-") - assert.NoError(t, err) + s.NoError(err) cfg.LogFile = filename log, err := os.Open(filename) - assert.NoError(t, err) - - t.Cleanup(func() { os.Remove(filename) }) + s.NoError(err) ll := &FTWLogLines{ logFile: log, @@ -354,5 +356,5 @@ func TestFTWLogLines_CheckForLogMarkerIn404(t *testing.T) { EndMarker: bytes.ToLower([]byte(markerLine)), } foundMarker := ll.CheckLogForMarker(stageID, 100) - assert.Equal(t, strings.ToLower(markerLine), strings.ToLower(string(foundMarker))) + s.Equal(strings.ToLower(markerLine), strings.ToLower(string(foundMarker))) } diff --git a/waflog/waflog_test.go b/waflog/waflog_test.go index 3495237..ae77405 100644 --- a/waflog/waflog_test.go +++ b/waflog/waflog_test.go @@ -3,22 +3,30 @@ package waflog import ( "testing" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" "github.com/coreruleset/go-ftw/config" ) -func TestNewFTWLogLines(t *testing.T) { +type waflogTestSuite struct { + suite.Suite +} + +func TestWafLogTestSuite(t *testing.T) { + suite.Run(t, new(waflogTestSuite)) +} + +func (s *waflogTestSuite) TestNewFTWLogLines() { cfg := config.NewDefaultConfig() - assert.NotNil(t, cfg) + s.NotNil(cfg) // Don't call NewFTWLogLines to avoid opening the file. ll := &FTWLogLines{} ll.WithStartMarker([]byte("#")) ll.WithEndMarker([]byte("#")) - assert.NotNil(t, ll.StartMarker, "Failed! StartMarker must be set") - assert.NotNil(t, ll.EndMarker, "Failed! EndMarker must be set") + s.NotNil(ll.StartMarker, "Failed! StartMarker must be set") + s.NotNil(ll.EndMarker, "Failed! EndMarker must be set") err := ll.Cleanup() - assert.NoError(t, err) + s.NoError(err) }