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  }