github.com/imyousuf/webhook-broker@v0.1.2/main_test.go (about) 1 package main 2 3 import ( 4 "bytes" 5 "context" 6 "errors" 7 "fmt" 8 "io/ioutil" 9 "net" 10 "net/http" 11 "os" 12 "path/filepath" 13 "sync" 14 "testing" 15 "time" 16 17 "github.com/rs/zerolog" 18 "github.com/rs/zerolog/log" 19 20 "github.com/imyousuf/webhook-broker/config" 21 "github.com/imyousuf/webhook-broker/controllers" 22 "github.com/imyousuf/webhook-broker/storage" 23 "github.com/imyousuf/webhook-broker/storage/data" 24 "github.com/imyousuf/webhook-broker/storage/mocks" 25 "github.com/stretchr/testify/assert" 26 "github.com/stretchr/testify/mock" 27 ) 28 29 const ( 30 configFilePath = "./testdatadir/webhook-broker.main.cfg" 31 configFilePath2 = "./testdatadir/webhook-broker.main-2.cfg" 32 notificationInitialContent = `[http] 33 listener=:12345 34 ` 35 notificationDifferentContent = `[http] 36 listener=:8080 37 ` 38 ) 39 40 func TestGetAppVersion(t *testing.T) { 41 assert.Equal(t, string(GetAppVersion()), "0.1.2") 42 } 43 44 var mainFunctionBreaker = func(stop *chan os.Signal) { 45 go func() { 46 waitForStatusEndpoint(":8080") 47 fmt.Println("Interrupt sent") 48 *stop <- os.Interrupt 49 }() 50 } 51 52 var waitForStatusEndpoint = func(portString string) { 53 var client = &http.Client{Timeout: time.Second * 10} 54 defer func() { 55 client.CloseIdleConnections() 56 }() 57 for { 58 response, err := client.Get("http://localhost" + portString + "/_status") 59 if err == nil { 60 if response.StatusCode == 200 { 61 break 62 } 63 } 64 } 65 } 66 67 var configChangeRestartMainFnBreaker = func(stop *chan os.Signal) { 68 go func() { 69 waitForStatusEndpoint(":12345") 70 ioutil.WriteFile(configFilePath, []byte(notificationDifferentContent), 0644) 71 time.Sleep(1 * time.Millisecond) 72 fmt.Println("called mainFnBreaker") 73 mainFunctionBreaker(stop) 74 }() 75 } 76 77 var panicExit = func(code int) { 78 panic(code) 79 } 80 81 func TestMainFunc(t *testing.T) { 82 os.Remove("./webhook-broker.sqlite3") 83 t.Run("GetAppErr", func(t *testing.T) { 84 oldExit := exit 85 oldArgs := os.Args 86 oldGetApp := getApp 87 getApp = func(httpServiceContainer *HTTPServiceContainer) (*data.App, error) { 88 serverShutdownContext, shutdownTimeoutCancelFunc := context.WithTimeout(context.Background(), 15*time.Second) 89 defer shutdownTimeoutCancelFunc() 90 httpServiceContainer.Server.Shutdown(serverShutdownContext) 91 return nil, errors.New("No App Error") 92 } 93 exit = panicExit 94 os.Args = []string{"webhook-broker", "-migrate", "./migration/sqls/"} 95 func() { 96 defer func() { 97 if r := recover(); r != nil { 98 log.Print(r) 99 assert.Equal(t, 4, r.(int)) 100 } else { 101 t.Fail() 102 } 103 }() 104 main() 105 }() 106 defer func() { 107 exit = oldExit 108 os.Args = oldArgs 109 getApp = oldGetApp 110 }() 111 }) 112 t.Run("StartInitRaceErrInBetween", func(t *testing.T) { 113 oldExit := exit 114 oldArgs := os.Args 115 oldStartAppInit := startAppInit 116 oldNotify := controllers.NotifyOnInterrupt 117 controllers.NotifyOnInterrupt = mainFunctionBreaker 118 startAppInit = func(httpServiceContainer *HTTPServiceContainer, seedData *config.SeedData) error { 119 return storage.ErrAppInitializing 120 } 121 exit = panicExit 122 os.Args = []string{"webhook-broker"} 123 main() 124 defer func() { 125 exit = oldExit 126 os.Args = oldArgs 127 startAppInit = oldStartAppInit 128 controllers.NotifyOnInterrupt = oldNotify 129 }() 130 }) 131 t.Run("StartInitRaceErrDuringUpdate", func(t *testing.T) { 132 oldExit := exit 133 oldArgs := os.Args 134 oldStartAppInit := startAppInit 135 oldNotify := controllers.NotifyOnInterrupt 136 controllers.NotifyOnInterrupt = mainFunctionBreaker 137 startAppInit = func(httpServiceContainer *HTTPServiceContainer, seedData *config.SeedData) error { 138 return storage.ErrOptimisticAppInit 139 } 140 exit = panicExit 141 os.Args = []string{"webhook-broker"} 142 main() 143 defer func() { 144 exit = oldExit 145 os.Args = oldArgs 146 startAppInit = oldStartAppInit 147 controllers.NotifyOnInterrupt = oldNotify 148 }() 149 }) 150 t.Run("SuccessRunWithAutoRestartOnConfigChange", func(t *testing.T) { 151 var buf bytes.Buffer 152 oldLogger := log.Logger 153 log.Logger = log.Output(&buf) 154 oldArgs := os.Args 155 ioutil.WriteFile(configFilePath, []byte(notificationInitialContent), 0644) 156 os.Args = []string{"webhook-broker", "-migrate", "./migration/sqls/", "-config", configFilePath} 157 oldNotify := controllers.NotifyOnInterrupt 158 controllers.NotifyOnInterrupt = configChangeRestartMainFnBreaker 159 defer func() { 160 log.Logger = oldLogger 161 os.Args = oldArgs 162 controllers.NotifyOnInterrupt = oldNotify 163 }() 164 main() 165 logString := buf.String() 166 assert.Contains(t, logString, "Webhook Broker") 167 assert.Contains(t, logString, string(GetAppVersion())) 168 t.Log(logString) 169 // Assert App initialization completed 170 configuration, _ := config.GetAutoConfiguration() 171 migrationConf := &storage.MigrationConfig{MigrationEnabled: false} 172 dataAccessor, _ := storage.GetNewDataAccessor(configuration, migrationConf, configuration) 173 app, err := dataAccessor.GetAppRepository().GetApp() 174 assert.Nil(t, err) 175 assert.Equal(t, data.Initialized, app.GetStatus()) 176 // Load and assert seed data 177 channel, err := dataAccessor.GetChannelRepository().Get("sample-channel") 178 assert.Nil(t, err) 179 assert.NotNil(t, channel) 180 producer, err := dataAccessor.GetProducerRepository().Get("sample-producer") 181 assert.Nil(t, err) 182 assert.NotNil(t, producer) 183 consumer, err := dataAccessor.GetConsumerRepository().Get("sample-channel", "sample-consumer") 184 assert.Nil(t, err) 185 assert.NotNil(t, consumer) 186 }) 187 t.Run("SuccessRunWithExitOnConfigChange", func(t *testing.T) { 188 ioutil.WriteFile(configFilePath2, []byte(notificationInitialContent), 0644) 189 oldArgs := os.Args 190 os.Args = []string{"webhook-broker", "-migrate", "./migration/sqls/", "-config", configFilePath2, "-stop-on-conf-change"} 191 defer func() { 192 os.Args = oldArgs 193 }() 194 var wg sync.WaitGroup 195 wg.Add(2) 196 go func() { 197 main() 198 wg.Done() 199 }() 200 go func() { 201 waitForStatusEndpoint(":12345") 202 ioutil.WriteFile(configFilePath2, []byte(notificationDifferentContent), 0644) 203 wg.Done() 204 }() 205 wg.Wait() 206 }) 207 t.Run("HelpError", func(t *testing.T) { 208 oldExit := exit 209 oldArgs := os.Args 210 oldConsole := consolePrintln 211 exit = panicExit 212 consolePrintln = func(output string) { 213 assert.Contains(t, output, "Usage of") 214 assert.Contains(t, output, "-config") 215 assert.Contains(t, output, "-migrate") 216 } 217 os.Args = []string{"webhook-broker", "-h"} 218 func() { 219 defer func() { 220 if r := recover(); r != nil { 221 assert.Equal(t, 1, r.(int)) 222 } else { 223 t.Fail() 224 } 225 }() 226 main() 227 }() 228 defer func() { 229 exit = oldExit 230 os.Args = oldArgs 231 consolePrintln = oldConsole 232 }() 233 }) 234 t.Run("ParseError", func(t *testing.T) { 235 oldExit := exit 236 oldArgs := os.Args 237 exit = panicExit 238 os.Args = []string{"webhook-broker", "-migrate1=test"} 239 func() { 240 defer func() { 241 if r := recover(); r != nil { 242 assert.Equal(t, 1, r.(int)) 243 } else { 244 t.Fail() 245 } 246 }() 247 main() 248 }() 249 defer func() { 250 exit = oldExit 251 os.Args = oldArgs 252 }() 253 }) 254 t.Run("NoWatcher", func(t *testing.T) { 255 oldExit := exit 256 oldArgs := os.Args 257 exit = panicExit 258 os.Args = []string{"webhook-broker", "-do-not-watch-conf-change"} 259 inConfig, _, cliCfgErr := parseArgs(os.Args[0], os.Args[1:]) 260 assert.Nil(t, cliCfgErr) 261 assert.True(t, inConfig.DoNotWatchConfigChange) 262 inConfig.NotifyOnConfigFileChange(func() { 263 t.FailNow() 264 }) 265 assert.False(t, inConfig.IsConfigWatcherStarted()) 266 defer func() { 267 exit = oldExit 268 os.Args = oldArgs 269 }() 270 }) 271 t.Run("ConfError", func(t *testing.T) { 272 ln, netErr := net.Listen("tcp", ":8080") 273 if netErr == nil { 274 defer ln.Close() 275 oldExit := exit 276 oldArgs := os.Args 277 exit = panicExit 278 os.Args = []string{"webhook-broker"} 279 func() { 280 defer func() { 281 if r := recover(); r != nil { 282 assert.Equal(t, 3, r.(int)) 283 } else { 284 t.Fail() 285 } 286 }() 287 main() 288 }() 289 defer func() { 290 exit = oldExit 291 os.Args = oldArgs 292 }() 293 } 294 }) 295 } 296 297 func TestParseArgs(t *testing.T) { 298 absPath, _ := filepath.Abs("./migration") 299 t.Run("FlagParseError", func(t *testing.T) { 300 t.Parallel() 301 _, _, err := parseArgs("webhook-broker", []string{"-migrate1", "no such path"}) 302 assert.NotNil(t, err) 303 }) 304 t.Run("NonExistentMigrationSource", func(t *testing.T) { 305 t.Parallel() 306 _, _, err := parseArgs("webhook-broker", []string{"-migrate", "no such path"}) 307 assert.NotNil(t, err) 308 }) 309 t.Run("MigrationSourceNotDir", func(t *testing.T) { 310 t.Parallel() 311 _, _, err := parseArgs("webhook-broker", []string{"-migrate", "./Makefile"}) 312 assert.NotNil(t, err) 313 assert.Equal(t, err, ErrMigrationSrcNotDir) 314 }) 315 t.Run("ValidMigrationSourceAbs", func(t *testing.T) { 316 t.Parallel() 317 cliConfig, _, err := parseArgs("webhook-broker", []string{"-migrate", "./migration"}) 318 assert.Nil(t, err) 319 assert.True(t, cliConfig.IsMigrationEnabled()) 320 assert.Equal(t, "file://"+absPath, cliConfig.MigrationSource) 321 }) 322 t.Run("ValidMigrationSourceRelative", func(t *testing.T) { 323 t.Parallel() 324 cliConfig, _, err := parseArgs("webhook-broker", []string{"-migrate", absPath}) 325 assert.Nil(t, err) 326 assert.True(t, cliConfig.IsMigrationEnabled()) 327 assert.Equal(t, "file://"+absPath, cliConfig.MigrationSource) 328 }) 329 } 330 331 func TestInitAppTime(t *testing.T) { 332 oldGetTimeoutTimer := getTimeoutTimer 333 oldGetApp := getApp 334 oldStartAppInit := startAppInit 335 oldGetWaitDuration := waitDuration 336 getTimeoutTimer = func() <-chan time.Time { 337 return time.After(time.Millisecond * 100) 338 } 339 waitDuration = 110 * time.Millisecond 340 getApp = func(httpServiceContainer *HTTPServiceContainer) (*data.App, error) { 341 seedData := &config.SeedData{} 342 seedData.DataHash = "TEST" 343 return data.NewApp(seedData, data.Initialized), nil 344 } 345 initErr := errors.New("test err") 346 startAppInit = func(httpServiceContainer *HTTPServiceContainer, seedData *config.SeedData) error { 347 return initErr 348 } 349 defaultConfig, err := config.GetAutoConfiguration() 350 if err != nil { 351 t.Fatal(err) 352 } 353 container := &HTTPServiceContainer{Configuration: defaultConfig} 354 initApp(container) 355 // Test finishing without error is itself success as no DB call is attempted 356 defer func() { 357 getTimeoutTimer = oldGetTimeoutTimer 358 getApp = oldGetApp 359 startAppInit = oldStartAppInit 360 waitDuration = oldGetWaitDuration 361 }() 362 } 363 364 const testLogFile = "./log-setup-test-output.log" 365 366 type MockLogConfig struct { 367 logLevel config.LogLevel 368 } 369 370 func (m MockLogConfig) GetLogLevel() config.LogLevel { return m.logLevel } 371 func (m MockLogConfig) GetLogFilename() string { return testLogFile } 372 func (m MockLogConfig) GetMaxLogFileSize() uint { return 10 } 373 func (m MockLogConfig) GetMaxLogBackups() uint { return 1 } 374 func (m MockLogConfig) GetMaxAgeForALogFile() uint { return 1 } 375 func (m MockLogConfig) IsCompressionEnabledOnLogBackups() bool { return true } 376 func (m MockLogConfig) IsLoggerConfigAvailable() bool { return true } 377 378 func TestSetupLog(t *testing.T) { 379 defer func() { 380 if r := recover(); r != nil { 381 t.Log(r) 382 } 383 }() 384 configs := make(map[config.LogLevel]func(), 4) 385 configs[config.Debug] = func() { 386 log.Print("unit test debug") 387 log.Info().Msg("unit test info") 388 log.Error().Msg("unit test error") 389 dat, err := ioutil.ReadFile(testLogFile) 390 assert.Nil(t, err) 391 assert.Contains(t, string(dat), "unit test debug") 392 assert.Contains(t, string(dat), "unit test info") 393 assert.Contains(t, string(dat), "unit test error") 394 } 395 configs[config.Info] = func() { 396 log.Print("unit test debug") 397 log.Info().Msg("unit test info") 398 log.Error().Msg("unit test error") 399 dat, err := ioutil.ReadFile(testLogFile) 400 assert.Nil(t, err) 401 assert.NotContains(t, string(dat), "unit test debug") 402 assert.Contains(t, string(dat), "unit test info") 403 assert.Contains(t, string(dat), "unit test error") 404 } 405 configs[config.Error] = func() { 406 log.Print("unit test debug") 407 log.Info().Msg("unit test info") 408 log.Error().Msg("unit test error") 409 dat, err := ioutil.ReadFile(testLogFile) 410 assert.Nil(t, err) 411 assert.NotContains(t, string(dat), "unit test debug") 412 assert.NotContains(t, string(dat), "unit test info") 413 assert.Contains(t, string(dat), "unit test error") 414 } 415 configs[config.Fatal] = func() { 416 assert.Equal(t, zerolog.FatalLevel, zerolog.GlobalLevel()) 417 } 418 for logLevel, testFunc := range configs { 419 _, err := os.Stat(testLogFile) 420 if err == nil { 421 os.Remove(testLogFile) 422 } 423 setupLogger(&MockLogConfig{logLevel: logLevel}) 424 testFunc() 425 } 426 } 427 428 func TestCreateSeedData_ErrorFlows(t *testing.T) { 429 var buf bytes.Buffer 430 oldLogger := log.Logger 431 log.Logger = log.Output(&buf) 432 expectedErr := errors.New("expected main seed data error") 433 defer func() { 434 assert.Contains(t, expectedErr.Error(), buf.String()) 435 assert.Contains(t, "Error creating producer", buf.String()) 436 assert.Contains(t, "Error creating channel", buf.String()) 437 assert.Contains(t, "Error creating consumer", buf.String()) 438 log.Logger = oldLogger 439 }() 440 configuration, _ := config.GetAutoConfiguration() 441 t.Run("StoreError", func(t *testing.T) { 442 t.Parallel() 443 dataAccessor := new(mocks.DataAccessor) 444 productRepo := new(mocks.ProducerRepository) 445 channelRepo := new(mocks.ChannelRepository) 446 consumerRepo := new(mocks.ConsumerRepository) 447 dataAccessor.On("GetProducerRepository").Return(productRepo) 448 dataAccessor.On("GetChannelRepository").Return(channelRepo) 449 dataAccessor.On("GetConsumerRepository").Return(consumerRepo) 450 productRepo.On("Store", mock.Anything).Return(nil, expectedErr) 451 channelRepo.On("Get", mock.Anything).Return(&data.Channel{}, nil) 452 channelRepo.On("Store", mock.Anything).Return(nil, expectedErr) 453 consumerRepo.On("Store", mock.Anything).Return(nil, expectedErr) 454 createSeedData(dataAccessor, configuration) 455 dataAccessor.AssertExpectations(t) 456 }) 457 t.Run("ChannelGetError", func(t *testing.T) { 458 t.Parallel() 459 dataAccessor := new(mocks.DataAccessor) 460 productRepo := new(mocks.ProducerRepository) 461 channelRepo := new(mocks.ChannelRepository) 462 dataAccessor.On("GetProducerRepository").Return(productRepo) 463 dataAccessor.On("GetChannelRepository").Return(channelRepo) 464 productRepo.On("Store", mock.Anything).Return(nil, expectedErr) 465 channelRepo.On("Get", mock.Anything).Return(&data.Channel{}, expectedErr) 466 channelRepo.On("Store", mock.Anything).Return(nil, expectedErr) 467 createSeedData(dataAccessor, configuration) 468 dataAccessor.AssertExpectations(t) 469 }) 470 }