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  }