package pubsub import ( "context" "fmt" "os" "sync" "testing" "time" "github.com/kolide/fleet/server/kolide" "github.com/kolide/fleet/server/test" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // waitTimeout waits for the waitgroup for the specified max timeout. // Returns true if waiting timed out. http://stackoverflow.com/a/32843750/491710 func waitTimeout(wg *sync.WaitGroup, timeout time.Duration) bool { c := make(chan struct{}) go func() { defer close(c) wg.Wait() }() select { case <-c: return false // completed normally case <-time.After(timeout): return true // timed out } } var testFunctions = [...]func(*testing.T, kolide.QueryResultStore){ testQueryResultsStore, testQueryResultsStoreErrors, } func TestRedis(t *testing.T) { if _, ok := os.LookupEnv("REDIS_TEST"); !ok { t.SkipNow() } store, teardown := setupRedis(t) defer teardown() for _, f := range testFunctions { t.Run(test.FunctionName(f), func(t *testing.T) { f(t, store) }) } } func TestInmem(t *testing.T) { for _, f := range testFunctions { t.Run(test.FunctionName(f), func(t *testing.T) { t.Parallel() store := NewInmemQueryResults() f(t, store) }) } } func setupRedis(t *testing.T) (store *redisQueryResults, teardown func()) { var ( addr = "127.0.0.1:6379" password = "" database = 0 useTLS = false ) if a, ok := os.LookupEnv("REDIS_PORT_6379_TCP_ADDR"); ok { addr = fmt.Sprintf("%s:6379", a) } store = NewRedisQueryResults(NewRedisPool(addr, password, database, useTLS)) _, err := store.pool.Get().Do("PING") require.Nil(t, err) teardown = func() { store.pool.Close() } return store, teardown } func testQueryResultsStoreErrors(t *testing.T, store kolide.QueryResultStore) { // Write with no subscriber err := store.WriteResult( kolide.DistributedQueryResult{ DistributedQueryCampaignID: 9999, Rows: []map[string]string{{"bing": "fds"}}, Host: kolide.Host{ ID: 4, UpdateCreateTimestamps: kolide.UpdateCreateTimestamps{ UpdateTimestamp: kolide.UpdateTimestamp{ UpdatedAt: time.Now().UTC(), }, }, DetailUpdateTime: time.Now().UTC(), }, }, ) assert.NotNil(t, err) castErr, ok := err.(Error) if assert.True(t, ok, "err should be pubsub.Error") { assert.True(t, castErr.NoSubscriber(), "NoSubscriber() should be true") } } func testQueryResultsStore(t *testing.T, store kolide.QueryResultStore) { // Test handling results for two campaigns in parallel campaign1 := kolide.DistributedQueryCampaign{ID: 1} ctx1, cancel1 := context.WithCancel(context.Background()) channel1, err := store.ReadChannel(ctx1, campaign1) assert.Nil(t, err) expected1 := []kolide.DistributedQueryResult{ kolide.DistributedQueryResult{ DistributedQueryCampaignID: 1, Rows: []map[string]string{{"foo": "bar"}}, Host: kolide.Host{ ID: 1, // Note these times need to be set to avoid // issues with roundtrip serializing the zero // time value. See https://goo.gl/CCEs8x UpdateCreateTimestamps: kolide.UpdateCreateTimestamps{ UpdateTimestamp: kolide.UpdateTimestamp{ UpdatedAt: time.Now().UTC(), }, CreateTimestamp: kolide.CreateTimestamp{ CreatedAt: time.Now().UTC(), }, }, DetailUpdateTime: time.Now().UTC(), SeenTime: time.Now().UTC(), }, }, kolide.DistributedQueryResult{ DistributedQueryCampaignID: 1, Rows: []map[string]string{{"whoo": "wahh"}}, Host: kolide.Host{ ID: 3, UpdateCreateTimestamps: kolide.UpdateCreateTimestamps{ UpdateTimestamp: kolide.UpdateTimestamp{ UpdatedAt: time.Now().UTC(), }, CreateTimestamp: kolide.CreateTimestamp{ CreatedAt: time.Now().UTC(), }, }, DetailUpdateTime: time.Now().UTC(), SeenTime: time.Now().UTC(), }, }, kolide.DistributedQueryResult{ DistributedQueryCampaignID: 1, Rows: []map[string]string{{"bing": "fds"}}, Host: kolide.Host{ ID: 4, UpdateCreateTimestamps: kolide.UpdateCreateTimestamps{ UpdateTimestamp: kolide.UpdateTimestamp{ UpdatedAt: time.Now().UTC(), }, CreateTimestamp: kolide.CreateTimestamp{ CreatedAt: time.Now().UTC(), }, }, DetailUpdateTime: time.Now().UTC(), SeenTime: time.Now().UTC(), }, }, } campaign2 := kolide.DistributedQueryCampaign{ID: 2} ctx2, cancel2 := context.WithCancel(context.Background()) channel2, err := store.ReadChannel(ctx2, campaign2) assert.Nil(t, err) expected2 := []kolide.DistributedQueryResult{ kolide.DistributedQueryResult{ DistributedQueryCampaignID: 2, Rows: []map[string]string{{"tim": "tom"}}, Host: kolide.Host{ ID: 1, UpdateCreateTimestamps: kolide.UpdateCreateTimestamps{ UpdateTimestamp: kolide.UpdateTimestamp{ UpdatedAt: time.Now().UTC(), }, CreateTimestamp: kolide.CreateTimestamp{ CreatedAt: time.Now().UTC(), }, }, DetailUpdateTime: time.Now().UTC(), SeenTime: time.Now().UTC(), }, }, kolide.DistributedQueryResult{ DistributedQueryCampaignID: 2, Rows: []map[string]string{{"slim": "slam"}}, Host: kolide.Host{ ID: 3, UpdateCreateTimestamps: kolide.UpdateCreateTimestamps{ UpdateTimestamp: kolide.UpdateTimestamp{ UpdatedAt: time.Now().UTC(), }, CreateTimestamp: kolide.CreateTimestamp{ CreatedAt: time.Now().UTC(), }, }, DetailUpdateTime: time.Now().UTC(), SeenTime: time.Now().UTC(), }, }, } var results1, results2 []kolide.DistributedQueryResult var readerWg, writerWg sync.WaitGroup readerWg.Add(1) go func() { defer readerWg.Done() for res := range channel1 { switch res := res.(type) { case kolide.DistributedQueryResult: results1 = append(results1, res) } } }() readerWg.Add(1) go func() { defer readerWg.Done() for res := range channel2 { switch res := res.(type) { case kolide.DistributedQueryResult: results2 = append(results2, res) } } }() // Wait to ensure subscriptions are activated before writing time.Sleep(100 * time.Millisecond) writerWg.Add(1) go func() { defer writerWg.Done() for _, res := range expected1 { assert.Nil(t, store.WriteResult(res)) } time.Sleep(300 * time.Millisecond) cancel1() }() writerWg.Add(1) go func() { defer writerWg.Done() for _, res := range expected2 { assert.Nil(t, store.WriteResult(res)) } time.Sleep(300 * time.Millisecond) cancel2() }() // wait with a timeout to ensure that the test can't hang if waitTimeout(&writerWg, 5*time.Second) { t.Error("Timed out waiting for writers to join") } if waitTimeout(&readerWg, 5*time.Second) { t.Error("Timed out waiting for readers to join") } assert.EqualValues(t, expected1, results1) assert.EqualValues(t, expected2, results2) }