github.com/magnusbaeck/logstash-filter-verifier/v2@v2.0.0-pre.1/testcase/testcase_test.go (about)

     1  // Copyright (c) 2015-2018 Magnus Bäck <magnus@noun.se>
     2  
     3  package testcase
     4  
     5  import (
     6  	"bytes"
     7  	"encoding/json"
     8  	"io/ioutil"
     9  	"os"
    10  	"path/filepath"
    11  	"testing"
    12  
    13  	"github.com/imkira/go-observer"
    14  	"github.com/magnusbaeck/logstash-filter-verifier/logstash"
    15  	"github.com/stretchr/testify/assert"
    16  )
    17  
    18  func TestNew(t *testing.T) {
    19  	cases := []struct {
    20  		input    string
    21  		expected TestCaseSet
    22  	}{
    23  		// Happy flow relying on the default codec.
    24  		{
    25  			input: `{"fields": {"type": "mytype"}}`,
    26  			expected: TestCaseSet{
    27  				Codec: "line",
    28  				InputFields: logstash.FieldSet{
    29  					"type": "mytype",
    30  				},
    31  				IgnoredFields: []string{"@version"},
    32  			},
    33  		},
    34  		// Happy flow with a custom codec.
    35  		{
    36  			input: `{"fields": {"type": "mytype"}, "codec": "json_lines"}`,
    37  			expected: TestCaseSet{
    38  				Codec: "json_lines",
    39  				InputFields: logstash.FieldSet{
    40  					"type": "mytype",
    41  				},
    42  				IgnoredFields: []string{"@version"},
    43  			},
    44  		},
    45  		// Additional fields to ignore are appended to the default.
    46  		{
    47  			input: `{"ignore": ["foo"]}`,
    48  			expected: TestCaseSet{
    49  				Codec:         "line",
    50  				InputFields:   logstash.FieldSet{},
    51  				IgnoredFields: []string{"@version", "foo"},
    52  			},
    53  		},
    54  		// Fields with bracket notation
    55  		{
    56  			input: `{"fields": {"type": "mytype", "[log][file][path]": "/tmp/file.log"}}`,
    57  			expected: TestCaseSet{
    58  				Codec: "line",
    59  				InputFields: logstash.FieldSet{
    60  					"type": "mytype",
    61  					"log": map[string]interface{}{
    62  						"file": map[string]interface{}{
    63  							"path": "/tmp/file.log",
    64  						},
    65  					},
    66  				},
    67  				IgnoredFields: []string{"@version"},
    68  			},
    69  		},
    70  		// No handle input with bracket notation when codec is line
    71  		{
    72  			input: `{"input": ["{\"[test][path]\": \"test\"}"]}`,
    73  			expected: TestCaseSet{
    74  				Codec:         "line",
    75  				InputLines:    []string{"{\"[test][path]\": \"test\"}"},
    76  				IgnoredFields: []string{"@version"},
    77  				InputFields:   logstash.FieldSet{},
    78  			},
    79  		},
    80  		// handle input with bracket notation when codec is json_lines
    81  		{
    82  			input: `{"input": ["{\"[test][path]\": \"test\"}"], "codec": "json_lines"}`,
    83  			expected: TestCaseSet{
    84  				Codec:         "json_lines",
    85  				InputLines:    []string{"{\"test\":{\"path\":\"test\"}}"},
    86  				IgnoredFields: []string{"@version"},
    87  				InputFields:   logstash.FieldSet{},
    88  			},
    89  		},
    90  	}
    91  	for i, c := range cases {
    92  		tcs, err := New(bytes.NewReader([]byte(c.input)), "json")
    93  		if err != nil {
    94  			t.Errorf("Test %d: %q input: %s", i, c.input, err)
    95  			break
    96  		}
    97  		resultJSON := marshalTestCaseSet(t, tcs)
    98  		expectedJSON := marshalTestCaseSet(t, &c.expected)
    99  		if expectedJSON != resultJSON {
   100  			t.Errorf("Test %d:\nExpected:\n%s\nGot:\n%s", i, expectedJSON, resultJSON)
   101  		}
   102  	}
   103  }
   104  
   105  // TestNewFromFile smoketests NewFromFile and makes sure it returns
   106  // an absolute path even if a relative path was given as input.
   107  func TestNewFromFile(t *testing.T) {
   108  	filenames := []string{
   109  		"filename.json",
   110  		"filename.yml",
   111  		"filename.yaml",
   112  	}
   113  	for _, filename := range filenames {
   114  		tempdir, err := ioutil.TempDir("", "")
   115  		if err != nil {
   116  			t.Fatalf(err.Error())
   117  		}
   118  		defer os.RemoveAll(tempdir)
   119  		olddir, err := os.Getwd()
   120  		if err != nil {
   121  			t.Fatalf(err.Error())
   122  		}
   123  		defer os.Chdir(olddir)
   124  		if err = os.Chdir(tempdir); err != nil {
   125  			t.Fatalf(err.Error())
   126  		}
   127  
   128  		fullTestCasePath := filepath.Join(tempdir, filename)
   129  
   130  		// As it happens a valid JSON file is also a valid YAML file so
   131  		// the file we create can have the same contents regardless of
   132  		// the file format.
   133  		if err = ioutil.WriteFile(fullTestCasePath, []byte(`{"type": "test"}`), 0600); err != nil {
   134  			t.Fatal(err.Error())
   135  		}
   136  
   137  		tcs, err := NewFromFile(filename)
   138  		if err != nil {
   139  			t.Fatalf("NewFromFile() unexpectedly returned an error: %s", err)
   140  		}
   141  
   142  		if tcs.File != fullTestCasePath {
   143  			t.Fatalf("Expected test case path to be %q, got %q instead.", fullTestCasePath, tcs.File)
   144  		}
   145  	}
   146  }
   147  
   148  func TestCompare(t *testing.T) {
   149  	// Create an empty tempdir so that we can construct a path to
   150  	// a diff binary that's guaranteed to not exist.
   151  	tempdir, err := ioutil.TempDir("", "")
   152  	if err != nil {
   153  		t.Fatalf(err.Error())
   154  	}
   155  	defer os.RemoveAll(tempdir)
   156  
   157  	liveObserver := observer.NewProperty(nil)
   158  
   159  	cases := []struct {
   160  		testcase     *TestCaseSet
   161  		actualEvents []logstash.Event
   162  		diffCommand  []string
   163  		result       bool
   164  		err          error
   165  	}{
   166  		// Empty test case with no messages is okay.
   167  		{
   168  			&TestCaseSet{
   169  				File: "/path/to/filename.json",
   170  				InputFields: logstash.FieldSet{
   171  					"type": "test",
   172  				},
   173  				Codec:          "line",
   174  				InputLines:     []string{},
   175  				ExpectedEvents: []logstash.Event{},
   176  			},
   177  			[]logstash.Event{},
   178  			[]string{"diff"},
   179  			true,
   180  			nil,
   181  		},
   182  		// Too few messages received.
   183  		{
   184  			&TestCaseSet{
   185  				File: "/path/to/filename.json",
   186  				InputFields: logstash.FieldSet{
   187  					"type": "test",
   188  				},
   189  				Codec:      "line",
   190  				InputLines: []string{},
   191  				ExpectedEvents: []logstash.Event{
   192  					{
   193  						"a": "b",
   194  					},
   195  					{
   196  						"c": "d",
   197  					},
   198  				},
   199  			},
   200  			[]logstash.Event{
   201  				{
   202  					"a": "b",
   203  				},
   204  			},
   205  			[]string{"diff"},
   206  			false,
   207  			nil,
   208  		},
   209  		// Too many messages received.
   210  		{
   211  			&TestCaseSet{
   212  				File: "/path/to/filename.json",
   213  				InputFields: logstash.FieldSet{
   214  					"type": "test",
   215  				},
   216  				Codec:      "line",
   217  				InputLines: []string{},
   218  				ExpectedEvents: []logstash.Event{
   219  					{
   220  						"a": "b",
   221  					},
   222  				},
   223  			},
   224  			[]logstash.Event{
   225  				{
   226  					"a": "b",
   227  				},
   228  				{
   229  					"c": "d",
   230  				},
   231  			},
   232  			[]string{"diff"},
   233  			false,
   234  			nil,
   235  		},
   236  		// Different fields.
   237  		{
   238  			&TestCaseSet{
   239  				File: "/path/to/filename.json",
   240  				InputFields: logstash.FieldSet{
   241  					"type": "test",
   242  				},
   243  				Codec:      "line",
   244  				InputLines: []string{},
   245  				ExpectedEvents: []logstash.Event{
   246  					{
   247  						"a": "b",
   248  					},
   249  				},
   250  			},
   251  			[]logstash.Event{
   252  				{
   253  					"c": "d",
   254  				},
   255  			},
   256  			[]string{"diff"},
   257  			false,
   258  			nil,
   259  		},
   260  		// Same field with different values.
   261  		{
   262  			&TestCaseSet{
   263  				File: "/path/to/filename.json",
   264  				InputFields: logstash.FieldSet{
   265  					"type": "test",
   266  				},
   267  				Codec:      "line",
   268  				InputLines: []string{},
   269  				ExpectedEvents: []logstash.Event{
   270  					{
   271  						"a": "b",
   272  					},
   273  				},
   274  			},
   275  			[]logstash.Event{
   276  				{
   277  					"a": "B",
   278  				},
   279  			},
   280  			[]string{"diff"},
   281  			false,
   282  			nil,
   283  		},
   284  		// Ignored fields are ignored.
   285  		{
   286  			&TestCaseSet{
   287  				File: "/path/to/filename.json",
   288  				InputFields: logstash.FieldSet{
   289  					"type": "test",
   290  				},
   291  				Codec:         "line",
   292  				IgnoredFields: []string{"ignored"},
   293  				InputLines:    []string{},
   294  				ExpectedEvents: []logstash.Event{
   295  					{
   296  						"not_ignored": "value",
   297  					},
   298  				},
   299  			},
   300  			[]logstash.Event{
   301  				{
   302  					"ignored":     "ignoreme",
   303  					"not_ignored": "value",
   304  				},
   305  			},
   306  			[]string{"diff"},
   307  			true,
   308  			nil,
   309  		},
   310  		// Ignored fields with bracket notation are ignored
   311  		{
   312  			&TestCaseSet{
   313  				File: "/path/to/filename.json",
   314  				InputFields: logstash.FieldSet{
   315  					"type": "test",
   316  				},
   317  				Codec:         "line",
   318  				IgnoredFields: []string{"[file][log][path]"},
   319  				InputLines:    []string{},
   320  				ExpectedEvents: []logstash.Event{
   321  					{
   322  						"not_ignored": "value",
   323  						"file": map[string]interface{}{
   324  							"log": map[string]interface{}{
   325  								"line": "value",
   326  							},
   327  						},
   328  					},
   329  				},
   330  			},
   331  			[]logstash.Event{
   332  				{
   333  					"file": map[string]interface{}{
   334  						"log": map[string]interface{}{
   335  							"line": "value",
   336  							"path": "ignore_me",
   337  						},
   338  					},
   339  					"not_ignored": "value",
   340  				},
   341  			},
   342  			[]string{"diff"},
   343  			true,
   344  			nil,
   345  		},
   346  		// Ignored fields with bracket notation are ignored (when empty hash)
   347  		{
   348  			&TestCaseSet{
   349  				File: "/path/to/filename.json",
   350  				InputFields: logstash.FieldSet{
   351  					"type": "test",
   352  				},
   353  				Codec:         "line",
   354  				IgnoredFields: []string{"[file][log][path]"},
   355  				InputLines:    []string{},
   356  				ExpectedEvents: []logstash.Event{
   357  					{
   358  						"not_ignored": "value",
   359  					},
   360  				},
   361  			},
   362  			[]logstash.Event{
   363  				{
   364  					"file": map[string]interface{}{
   365  						"log": map[string]interface{}{
   366  							"path": "ignore_me",
   367  						},
   368  					},
   369  					"not_ignored": "value",
   370  				},
   371  			},
   372  			[]string{"diff"},
   373  			true,
   374  			nil,
   375  		},
   376  		// Diff command execution errors are propagated correctly.
   377  		{
   378  			&TestCaseSet{
   379  				File: "/path/to/filename.json",
   380  				InputFields: logstash.FieldSet{
   381  					"type": "test",
   382  				},
   383  				Codec:      "line",
   384  				InputLines: []string{},
   385  				ExpectedEvents: []logstash.Event{
   386  					{
   387  						"a": "b",
   388  					},
   389  				},
   390  			},
   391  			[]logstash.Event{
   392  				{
   393  					"a": "b",
   394  				},
   395  			},
   396  			[]string{filepath.Join(tempdir, "does-not-exist")},
   397  			false,
   398  			&os.PathError{},
   399  		},
   400  	}
   401  
   402  	for i, c := range cases {
   403  		actualResult, err := c.testcase.Compare(c.actualEvents, c.diffCommand, liveObserver)
   404  		if err != nil && c.err == nil {
   405  			t.Errorf("Test %d: Expected no error, got error: %s", i, err)
   406  		} else if c.err != nil && err == nil {
   407  			t.Errorf("Test %d: Expected error, got no error.", i)
   408  		} else if actualResult != c.result {
   409  			t.Errorf("Test %d: Expected %t, got %t.", i, c.result, actualResult)
   410  		}
   411  	}
   412  }
   413  
   414  func TestMarshalToFile(t *testing.T) {
   415  	tempdir, err := ioutil.TempDir("", "")
   416  	if err != nil {
   417  		t.Fatalf(err.Error())
   418  	}
   419  	defer os.RemoveAll(tempdir)
   420  
   421  	// Implicitly test that subdirectories are created as needed.
   422  	fullpath := filepath.Join(tempdir, "a", "b", "c.json")
   423  
   424  	if err = marshalToFile(logstash.Event{}, fullpath); err != nil {
   425  		t.Fatalf(err.Error())
   426  	}
   427  
   428  	// We won't verify the actual contents that was marshaled,
   429  	// we'll just check that it can be unmarshalled again and that
   430  	// the file ends with a newline.
   431  	buf, err := ioutil.ReadFile(fullpath)
   432  	if err != nil {
   433  		t.Fatalf(err.Error())
   434  	}
   435  	if len(buf) > 0 && buf[len(buf)-1] != '\n' {
   436  		t.Errorf("Expected non-empty file ending with a newline: %q", string(buf))
   437  	}
   438  	var event logstash.Event
   439  	if err = json.Unmarshal(buf, &event); err != nil {
   440  		t.Errorf("%s: %q", err, string(buf))
   441  	}
   442  }
   443  
   444  func marshalTestCaseSet(t *testing.T, tcs *TestCaseSet) string {
   445  	resultBuf, err := json.MarshalIndent(tcs, "", "  ")
   446  	if err != nil {
   447  		t.Errorf("Failed to marshal %+v as JSON: %s", tcs, err)
   448  		return ""
   449  	}
   450  	return string(resultBuf)
   451  }
   452  
   453  // TestConvertBracketFields tests that fields contain on fields, exclude and input
   454  // test cases are converted on sub structure if contain bracket notation.
   455  func TestConvertBracketFields(t *testing.T) {
   456  	testCase := &TestCaseSet{
   457  		File: "/path/to/filename.json",
   458  		InputFields: logstash.FieldSet{
   459  			"type":                "test",
   460  			"[log][file][path]":   "/tmp/file.log",
   461  			"[log][origin][file]": "test.java",
   462  		},
   463  		Codec: "json_lines",
   464  		InputLines: []string{
   465  			`{"message": "test", "[agent][hostname]": "localhost", "[log][level]": "info"}`,
   466  		},
   467  		ExpectedEvents: []logstash.Event{
   468  			{
   469  				"type":                "test",
   470  				"[log][file][path]":   "/tmp/file.log",
   471  				"[log][origin][file]": "test.java",
   472  			},
   473  		},
   474  	}
   475  
   476  	expected := &TestCaseSet{
   477  		File: "/path/to/filename.json",
   478  		InputFields: logstash.FieldSet{
   479  			"type": "test",
   480  			"log": map[string]interface{}{
   481  				"file": map[string]interface{}{
   482  					"path": "/tmp/file.log",
   483  				},
   484  				"origin": map[string]interface{}{
   485  					"file": "test.java",
   486  				},
   487  			},
   488  		},
   489  		Codec: "json_lines",
   490  		InputLines: []string{
   491  			`{"agent":{"hostname":"localhost"},"log":{"level":"info"},"message":"test"}`,
   492  		},
   493  		ExpectedEvents: []logstash.Event{
   494  			{
   495  				"type": "test",
   496  				"log": map[string]interface{}{
   497  					"file": map[string]interface{}{
   498  						"path": "/tmp/file.log",
   499  					},
   500  					"origin": map[string]interface{}{
   501  						"file": "test.java",
   502  					},
   503  				},
   504  			},
   505  		},
   506  	}
   507  
   508  	err := testCase.convertBracketFields()
   509  	assert.NoError(t, err)
   510  	assert.Equal(t, expected, testCase)
   511  }