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 }