github.com/ouraigua/jenkins-library@v0.0.0-20231028010029-fbeaf2f3aa9b/pkg/log/ansHook_test.go (about) 1 //go:build unit 2 // +build unit 3 4 package log 5 6 import ( 7 "bytes" 8 "encoding/json" 9 "fmt" 10 "github.com/SAP/jenkins-library/pkg/ans" 11 "github.com/sirupsen/logrus" 12 "github.com/stretchr/testify/assert" 13 "github.com/stretchr/testify/require" 14 "os" 15 "reflect" 16 "strconv" 17 "testing" 18 "time" 19 ) 20 21 func TestANSHook_Levels(t *testing.T) { 22 23 registrationUtil := createRegUtil() 24 25 t.Run("good", func(t *testing.T) { 26 t.Run("default hook levels", func(t *testing.T) { 27 registerANSHookIfConfigured(testCorrelationID, registrationUtil) 28 assert.Equal(t, []logrus.Level{logrus.WarnLevel, logrus.ErrorLevel, logrus.PanicLevel, logrus.FatalLevel}, registrationUtil.Hook.Levels()) 29 }) 30 }) 31 } 32 33 func TestANSHook_setupEventTemplate(t *testing.T) { 34 35 t.Run("good", func(t *testing.T) { 36 t.Run("setup event without customer template", func(t *testing.T) { 37 event, _ := setupEventTemplate("", defaultCorrelationID()) 38 assert.Equal(t, defaultEvent(), event, "unexpected event data") 39 }) 40 t.Run("setup event from default customer template", func(t *testing.T) { 41 event, _ := setupEventTemplate(customerEventString(), defaultCorrelationID()) 42 assert.Equal(t, defaultEvent(), event, "unexpected event data") 43 }) 44 t.Run("setup event with category", func(t *testing.T) { 45 event, _ := setupEventTemplate(customerEventString(map[string]interface{}{"Category": "ALERT"}), defaultCorrelationID()) 46 assert.Equal(t, "", event.Category, "unexpected category data") 47 }) 48 t.Run("setup event with severity", func(t *testing.T) { 49 event, _ := setupEventTemplate(customerEventString(map[string]interface{}{"Severity": "WARNING"}), defaultCorrelationID()) 50 assert.Equal(t, "", event.Severity, "unexpected severity data") 51 }) 52 t.Run("setup event with invalid category", func(t *testing.T) { 53 event, _ := setupEventTemplate(customerEventString(map[string]interface{}{"Category": "invalid"}), defaultCorrelationID()) 54 assert.Equal(t, "", event.Category, "unexpected category data") 55 }) 56 t.Run("setup event with priority", func(t *testing.T) { 57 event, _ := setupEventTemplate(customerEventString(map[string]interface{}{"Priority": "1"}), defaultCorrelationID()) 58 assert.Equal(t, 1, event.Priority, "unexpected priority data") 59 }) 60 t.Run("setup event with omitted priority 0", func(t *testing.T) { 61 event, err := setupEventTemplate(customerEventString(map[string]interface{}{"Priority": "0"}), defaultCorrelationID()) 62 assert.Equal(t, nil, err, "priority 0 must not fail") 63 assert.Equal(t, 0, event.Priority, "unexpected priority data") 64 }) 65 }) 66 67 t.Run("bad", func(t *testing.T) { 68 t.Run("setup event with invalid priority", func(t *testing.T) { 69 _, err := setupEventTemplate(customerEventString(map[string]interface{}{"Priority": "-1"}), defaultCorrelationID()) 70 assert.Contains(t, err.Error(), "Priority must be 1 or greater", "unexpected error text") 71 }) 72 t.Run("setup event with invalid variable name", func(t *testing.T) { 73 _, err := setupEventTemplate(customerEventString(map[string]interface{}{"Invalid": "invalid"}), defaultCorrelationID()) 74 assert.Contains(t, err.Error(), "could not be unmarshalled", "unexpected error text") 75 }) 76 }) 77 } 78 79 func TestANSHook_registerANSHook(t *testing.T) { 80 81 os.Setenv("PIPER_ansHookServiceKey", defaultServiceKeyJSON) 82 83 t.Run("good", func(t *testing.T) { 84 t.Run("No service key skips registration", func(t *testing.T) { 85 util := createRegUtil() 86 os.Setenv("PIPER_ansHookServiceKey", "") 87 assert.Nil(t, registerANSHookIfConfigured(testCorrelationID, util), "registration did not nil") 88 assert.Nil(t, util.Hook, "registration registered hook") 89 os.Setenv("PIPER_ansHookServiceKey", defaultServiceKeyJSON) 90 }) 91 t.Run("Default registration registers hook and secret", func(t *testing.T) { 92 util := createRegUtil() 93 assert.Nil(t, registerANSHookIfConfigured(testCorrelationID, util), "registration did not nil") 94 assert.NotNil(t, util.Hook, "registration didnt register hook") 95 assert.NotNil(t, util.Secret, "registration didnt register secret") 96 }) 97 t.Run("Registration with default template", func(t *testing.T) { 98 util := createRegUtil() 99 os.Setenv("PIPER_ansEventTemplate", customerEventString()) 100 assert.Nil(t, registerANSHookIfConfigured(testCorrelationID, util), "registration did not return nil") 101 assert.Equal(t, customerEvent(), util.Hook.eventTemplate, "unexpected event template data") 102 os.Setenv("PIPER_ansEventTemplate", "") 103 }) 104 t.Run("Registration with customized template", func(t *testing.T) { 105 util := createRegUtil() 106 os.Setenv("PIPER_ansEventTemplate", customerEventString(map[string]interface{}{"Priority": "123"})) 107 assert.Nil(t, registerANSHookIfConfigured(testCorrelationID, util), "registration did not return nil") 108 assert.Equal(t, 123, util.Hook.eventTemplate.Priority, "unexpected event template data") 109 os.Setenv("PIPER_ansEventTemplate", "") 110 }) 111 }) 112 113 t.Run("bad", func(t *testing.T) { 114 t.Run("Fails on check error", func(t *testing.T) { 115 util := createRegUtil(map[string]interface{}{"CheckErr": fmt.Errorf("check failed")}) 116 err := registerANSHookIfConfigured(testCorrelationID, util) 117 assert.Contains(t, err.Error(), "check failed", "unexpected error text") 118 }) 119 120 t.Run("Fails on validation error", func(t *testing.T) { 121 os.Setenv("PIPER_ansEventTemplate", customerEventString(map[string]interface{}{"Priority": "-1"})) 122 err := registerANSHookIfConfigured(testCorrelationID, createRegUtil()) 123 assert.Contains(t, err.Error(), "Priority must be 1 or greater", "unexpected error text") 124 os.Setenv("PIPER_ansEventTemplate", "") 125 }) 126 127 }) 128 os.Setenv("PIPER_ansHookServiceKey", "") 129 } 130 131 func TestANSHook_Fire(t *testing.T) { 132 registrationUtil := createRegUtil() 133 ansHook := &ANSHook{ 134 client: registrationUtil, 135 } 136 137 t.Run("Straight forward test", func(t *testing.T) { 138 ansHook.eventTemplate = defaultEvent() 139 require.NoError(t, ansHook.Fire(defaultLogrusEntry()), "error is not nil") 140 assert.Equal(t, defaultResultingEvent(), registrationUtil.Event, "error category tag is not as expected") 141 registrationUtil.clearEventTemplate() 142 }) 143 t.Run("Set error category", func(t *testing.T) { 144 SetErrorCategory(ErrorTest) 145 ansHook.eventTemplate = defaultEvent() 146 require.NoError(t, ansHook.Fire(defaultLogrusEntry()), "error is not nil") 147 assert.Equal(t, "test", registrationUtil.Event.Tags["cicd:errorCategory"], "error category tag is not as expected") 148 SetErrorCategory(ErrorUndefined) 149 registrationUtil.clearEventTemplate() 150 }) 151 t.Run("Event already set", func(t *testing.T) { 152 alreadySetEvent := ans.Event{EventType: "My event type", Subject: "My subject line", Tags: map[string]interface{}{"Some": 1.0, "Additional": "a string", "Tags": true}} 153 ansHook.eventTemplate = mergeEvents(t, defaultEvent(), alreadySetEvent) 154 require.NoError(t, ansHook.Fire(defaultLogrusEntry()), "error is not nil") 155 assert.Equal(t, mergeEvents(t, defaultResultingEvent(), alreadySetEvent), registrationUtil.Event, "event is not as expected") 156 registrationUtil.clearEventTemplate() 157 }) 158 t.Run("Log entries should not affect each other", func(t *testing.T) { 159 ansHook.eventTemplate = defaultEvent() 160 SetErrorCategory(ErrorTest) 161 require.NoError(t, ansHook.Fire(defaultLogrusEntry()), "error is not nil") 162 assert.Equal(t, "test", registrationUtil.Event.Tags["cicd:errorCategory"], "error category tag is not as expected") 163 SetErrorCategory(ErrorUndefined) 164 require.NoError(t, ansHook.Fire(defaultLogrusEntry()), "error is not nil") 165 assert.Nil(t, registrationUtil.Event.Tags["cicd:errorCategory"], "error category tag is not nil") 166 registrationUtil.clearEventTemplate() 167 }) 168 t.Run("White space messages should not send", func(t *testing.T) { 169 ansHook.eventTemplate = defaultEvent() 170 entryWithSpaceMessage := defaultLogrusEntry() 171 entryWithSpaceMessage.Message = " " 172 require.NoError(t, ansHook.Fire(entryWithSpaceMessage), "error is not nil") 173 assert.Equal(t, ans.Event{}, registrationUtil.Event, "event is not empty") 174 }) 175 t.Run("Should not fire twice", func(t *testing.T) { 176 ansHook.eventTemplate = defaultEvent() 177 ansHook.firing = true 178 require.EqualError(t, ansHook.Fire(defaultLogrusEntry()), "ANS hook has already been fired", "error message is not as expected") 179 ansHook.firing = false 180 }) 181 t.Run("No stepName set", func(t *testing.T) { 182 ansHook.eventTemplate = defaultEvent() 183 logrusEntryWithoutStepName := defaultLogrusEntry() 184 logrusEntryWithoutStepName.Data = map[string]interface{}{} 185 require.NoError(t, ansHook.Fire(logrusEntryWithoutStepName), "error is not nil") 186 assert.Equal(t, "n/a", registrationUtil.Event.Tags["cicd:stepName"], "event step name tag is not as expected.") 187 assert.Equal(t, "Step 'n/a' sends 'WARNING'", registrationUtil.Event.Subject, "event subject is not as expected") 188 registrationUtil.clearEventTemplate() 189 }) 190 } 191 192 const testCorrelationID = "1234" 193 const defaultServiceKeyJSON = `{"url": "https://my.test.backend", "client_id": "myTestClientID", "client_secret": "super secret", "oauth_url": "https://my.test.oauth.provider"}` 194 195 var defaultTime = time.Date(2001, 2, 3, 4, 5, 6, 7, time.UTC) 196 197 func defaultCorrelationID() string { 198 return testCorrelationID 199 } 200 201 func merge(base, overlay map[string]interface{}) map[string]interface{} { 202 203 result := map[string]interface{}{} 204 205 if base == nil { 206 base = map[string]interface{}{} 207 } 208 209 for key, value := range base { 210 result[key] = value 211 } 212 213 for key, value := range overlay { 214 if val, ok := value.(map[string]interface{}); ok { 215 if valBaseKey, ok := base[key].(map[string]interface{}); !ok { 216 result[key] = merge(map[string]interface{}{}, val) 217 } else { 218 result[key] = merge(valBaseKey, val) 219 } 220 } else { 221 result[key] = value 222 } 223 } 224 return result 225 } 226 227 func customerEvent(params ...interface{}) ans.Event { 228 event := ans.Event{} 229 json.Unmarshal([]byte(customerEventString(params)), &event) 230 return event 231 } 232 233 func customerEventString(params ...interface{}) string { 234 event := defaultEvent() 235 236 additionalFields := make(map[string]interface{}) 237 if len(params) > 0 { 238 for i := 0; i < len(params); i++ { 239 additionalFields = merge(additionalFields, pokeObject(&event, params[i])) 240 } 241 } 242 243 // create json string from Event 244 marshaled, err := json.Marshal(event) 245 if err != nil { 246 panic(fmt.Sprintf("cannot marshal customer event: %v", err)) 247 } 248 249 // add non Event members to json string 250 if len(additionalFields) > 0 { 251 closingBraceIdx := bytes.LastIndexByte(marshaled, '}') 252 for key, value := range additionalFields { 253 var entry string 254 switch value.(type) { 255 default: 256 panic(fmt.Sprintf("invalid key value type: %v", key)) 257 case string: 258 entry = `, "` + key + `": "` + value.(string) + `"` 259 case int: 260 entry = `, "` + key + `": "` + strconv.Itoa(value.(int)) + `"` 261 } 262 263 add := []byte(entry) 264 marshaled = append(marshaled[:closingBraceIdx], add...) 265 } 266 marshaled = append(marshaled, '}') 267 } 268 269 return string(marshaled) 270 } 271 272 type registrationUtilMock struct { 273 ans.Client 274 Event ans.Event 275 ServiceKey ans.ServiceKey 276 SendErr error 277 CheckErr error 278 Hook *ANSHook 279 Secret string 280 } 281 282 func (m *registrationUtilMock) Send(event ans.Event) error { 283 m.Event = event 284 return m.SendErr 285 } 286 287 func (m *registrationUtilMock) CheckCorrectSetup() error { 288 return m.CheckErr 289 } 290 291 func (m *registrationUtilMock) SetServiceKey(serviceKey ans.ServiceKey) { 292 m.ServiceKey = serviceKey 293 294 } 295 func (m *registrationUtilMock) registerHook(hook *ANSHook) { 296 m.Hook = hook 297 } 298 299 func (m *registrationUtilMock) registerSecret(secret string) { 300 m.Secret = secret 301 } 302 303 func (m *registrationUtilMock) clearEventTemplate() { 304 m.Event = ans.Event{} 305 } 306 307 func createRegUtil(params ...interface{}) *registrationUtilMock { 308 309 mock := registrationUtilMock{} 310 if len(params) > 0 { 311 for i := 0; i < len(params); i++ { 312 pokeObject(&mock, params[i]) 313 } 314 } 315 return &mock 316 } 317 318 func pokeObject(obj interface{}, param interface{}) map[string]interface{} { 319 320 additionalFields := make(map[string]interface{}) 321 322 switch t := param.(type) { 323 case map[string]interface{}: 324 { 325 m := param.(map[string]interface{}) 326 v := reflect.ValueOf(obj) 327 if v.Kind() == reflect.Ptr { 328 v = v.Elem() 329 } 330 for key, value := range m { 331 f := v.FieldByName(key) 332 333 if f != (reflect.Value{}) { 334 switch f.Kind() { 335 case reflect.String: 336 f.SetString(value.(string)) 337 case reflect.Int, reflect.Int64: 338 switch t := value.(type) { 339 case string: 340 v, _ := strconv.Atoi(value.(string)) 341 f.SetInt(int64(v)) 342 case int: 343 f.SetInt(int64((value).(int))) 344 case int64: 345 f.SetInt(value.(int64)) 346 default: 347 panic(fmt.Sprintf("unsupported value type: %v of key:%v value:%v\n", t, key, value)) 348 } 349 case reflect.Map: 350 switch value.(type) { 351 case map[string]string, map[string]interface{}: 352 if value != nil { 353 val := reflect.ValueOf(value) 354 f.Set(val) 355 } else { 356 f.Set(reflect.Zero(f.Type())) 357 } 358 } 359 case reflect.Interface: 360 if value != nil { 361 val := reflect.ValueOf(value) 362 f.Set(val) 363 } else { 364 f.Set(reflect.Zero(f.Type())) 365 } 366 default: 367 panic(fmt.Sprintf("unsupported field type: %v of key:%v value:%v\n", f.Kind(), key, value)) 368 } 369 } else { 370 additionalFields[key] = value 371 } 372 } 373 } 374 case []interface{}: 375 p := param.([]interface{}) 376 for i := 0; i < len(p); i++ { 377 pokeObject(obj, p[i]) 378 } 379 default: 380 panic(fmt.Sprintf("unsupported paramter type: %v", t)) 381 } 382 return additionalFields 383 } 384 385 func defaultEvent() ans.Event { 386 event := ans.Event{ 387 EventType: "Piper", 388 Tags: map[string]interface{}{ 389 "ans:correlationId": testCorrelationID, 390 "ans:sourceEventId": testCorrelationID, 391 }, 392 Resource: &ans.Resource{ 393 ResourceType: "Pipeline", 394 ResourceName: "Pipeline", 395 }, 396 } 397 return event 398 } 399 400 func defaultResultingEvent() ans.Event { 401 return customerEvent(map[string]interface{}{ 402 "EventTimestamp": defaultTime.Unix(), 403 "Severity": "WARNING", 404 "Category": "ALERT", 405 "Subject": "Step 'testStep' sends 'WARNING'", 406 "Body": "my log message", 407 "Tags": map[string]interface{}{ 408 "ans:correlationId": "1234", 409 "ans:sourceEventId": "1234", 410 "cicd:stepName": "testStep", 411 "cicd:logLevel": "warning", 412 }, 413 }) 414 } 415 416 func defaultLogrusEntry() *logrus.Entry { 417 return &logrus.Entry{ 418 Level: logrus.WarnLevel, 419 Time: defaultTime, 420 Message: "my log message", 421 Data: map[string]interface{}{"stepName": "testStep"}, 422 } 423 } 424 425 func mergeEvents(t *testing.T, event1, event2 ans.Event) ans.Event { 426 event2JSON, err := json.Marshal(event2) 427 require.NoError(t, err) 428 err = event1.MergeWithJSON(event2JSON) 429 require.NoError(t, err) 430 return event1 431 }