0
0
mirror of https://github.com/PostHog/posthog.git synced 2024-11-21 13:39:22 +01:00

feat: livestream service tests (#24209)

This commit is contained in:
James Greenhill 2024-08-06 15:08:34 -07:00 committed by GitHub
parent 6ec9210a6b
commit fc8953cc6f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 1002 additions and 15 deletions

22
.github/workflows/go.yml vendored Normal file
View File

@ -0,0 +1,22 @@
name: Go Test (for livestream service)
on:
pull_request:
paths:
- 'livestream/**'
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.22
- name: Run tests
run: cd livestream && go test -v

12
livestream/.mockery.yaml Normal file
View File

@ -0,0 +1,12 @@
quiet: False
with-expecter: True
dir: mocks/{{ replaceAll .InterfaceDirRelative "internal" "internal_" }}
mockname: '{{.InterfaceName}}'
outpkg: '{{.PackageName}}'
filename: '{{.InterfaceName}}.go'
all: True
packages:
github.com/posthog/posthog/livestream:
config:
recursive: True
outpkg: mocks

View File

@ -20,7 +20,7 @@ func loadConfigs() {
err := viper.ReadInConfig()
if err != nil {
sentry.CaptureException(err)
log.Fatalf("fatal error config file: %w", err)
log.Fatalf("fatal error config file: %v", err)
}
viper.OnConfigChange(func(e fsnotify.Event) {

58
livestream/event_test.go Normal file
View File

@ -0,0 +1,58 @@
package main
import (
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestEventWriteTo(t *testing.T) {
tests := []struct {
name string
event Event
expected string
}{
{
name: "Full event",
event: Event{
ID: []byte("1"),
Data: []byte("test data"),
Event: []byte("message"),
Retry: []byte("3000"),
},
expected: "id: 1\ndata: test data\nevent: message\nretry: 3000\n\n",
},
{
name: "Event with multiline data",
event: Event{
ID: []byte("2"),
Data: []byte("line1\nline2\nline3"),
},
expected: "id: 2\ndata: line1\ndata: line2\ndata: line3\n\n",
},
{
name: "Event with comment only",
event: Event{
Comment: []byte("This is a comment"),
},
expected: ": This is a comment\n\n",
},
{
name: "Empty event",
event: Event{},
expected: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
w := httptest.NewRecorder()
err := tt.event.WriteTo(w)
require.NoError(t, err, "WriteTo() should not return an error")
assert.Equal(t, tt.expected, w.Body.String(), "WriteTo() output should match expected")
})
}
}

183
livestream/filter_test.go Normal file
View File

@ -0,0 +1,183 @@
package main
import (
"testing"
"time"
"sync/atomic"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewFilter(t *testing.T) {
subChan := make(chan Subscription)
unSubChan := make(chan Subscription)
inboundChan := make(chan PostHogEvent)
filter := NewFilter(subChan, unSubChan, inboundChan)
assert.NotNil(t, filter)
assert.Equal(t, subChan, filter.subChan)
assert.Equal(t, unSubChan, filter.unSubChan)
assert.Equal(t, inboundChan, filter.inboundChan)
assert.Empty(t, filter.subs)
}
func TestRemoveSubscription(t *testing.T) {
subs := []Subscription{
{ClientId: "1"},
{ClientId: "2"},
{ClientId: "3"},
}
result := removeSubscription("2", subs)
assert.Len(t, result, 2)
assert.Equal(t, "1", result[0].ClientId)
assert.Equal(t, "3", result[1].ClientId)
}
func TestUuidFromDistinctId(t *testing.T) {
result1 := uuidFromDistinctId(1, "user1")
result2 := uuidFromDistinctId(1, "user1")
result3 := uuidFromDistinctId(2, "user1")
assert.NotEmpty(t, result1)
assert.Equal(t, result1, result2)
assert.NotEqual(t, result1, result3)
assert.Empty(t, uuidFromDistinctId(0, "user1"))
assert.Empty(t, uuidFromDistinctId(1, ""))
}
func TestConvertToResponseGeoEvent(t *testing.T) {
event := PostHogEvent{
Lat: 40.7128,
Lng: -74.0060,
}
result := convertToResponseGeoEvent(event)
assert.Equal(t, 40.7128, result.Lat)
assert.Equal(t, -74.0060, result.Lng)
assert.Equal(t, uint(1), result.Count)
}
func TestConvertToResponsePostHogEvent(t *testing.T) {
event := PostHogEvent{
Uuid: "123",
Timestamp: "2023-01-01T00:00:00Z",
DistinctId: "user1",
Event: "pageview",
Properties: map[string]interface{}{"url": "https://example.com"},
}
result := convertToResponsePostHogEvent(event, 1)
assert.Equal(t, "123", result.Uuid)
assert.Equal(t, "2023-01-01T00:00:00Z", result.Timestamp)
assert.Equal(t, "user1", result.DistinctId)
assert.NotEmpty(t, result.PersonId)
assert.Equal(t, "pageview", result.Event)
assert.Equal(t, "https://example.com", result.Properties["url"])
}
func TestFilterRun(t *testing.T) {
subChan := make(chan Subscription)
unSubChan := make(chan Subscription)
inboundChan := make(chan PostHogEvent)
filter := NewFilter(subChan, unSubChan, inboundChan)
go filter.Run()
// Test subscription
eventChan := make(chan interface{}, 1)
sub := Subscription{
ClientId: "1",
TeamId: 1,
Token: "token1",
DistinctId: "user1",
EventTypes: []string{"pageview"},
EventChan: eventChan,
ShouldClose: &atomic.Bool{},
}
subChan <- sub
// Wait for subscription to be processed
time.Sleep(10 * time.Millisecond)
// Test event filtering
event := PostHogEvent{
Uuid: "123",
Timestamp: "2023-01-01T00:00:00Z",
DistinctId: "user1",
Token: "token1",
Event: "pageview",
Properties: map[string]interface{}{"url": "https://example.com"},
}
inboundChan <- event
// Wait for event to be processed
select {
case receivedEvent := <-eventChan:
responseEvent, ok := receivedEvent.(ResponsePostHogEvent)
require.True(t, ok)
assert.Equal(t, "123", responseEvent.Uuid)
assert.Equal(t, "user1", responseEvent.DistinctId)
assert.Equal(t, "pageview", responseEvent.Event)
case <-time.After(100 * time.Millisecond):
t.Fatal("Timed out waiting for event")
}
// Test unsubscription
unSubChan <- sub
// Wait for unsubscription to be processed
time.Sleep(10 * time.Millisecond)
assert.Empty(t, filter.subs)
}
func TestFilterRunWithGeoEvent(t *testing.T) {
subChan := make(chan Subscription)
unSubChan := make(chan Subscription)
inboundChan := make(chan PostHogEvent)
filter := NewFilter(subChan, unSubChan, inboundChan)
go filter.Run()
// Test subscription with Geo enabled
eventChan := make(chan interface{}, 1)
sub := Subscription{
ClientId: "1",
TeamId: 1,
Geo: true,
EventChan: eventChan,
ShouldClose: &atomic.Bool{},
}
subChan <- sub
// Wait for subscription to be processed
time.Sleep(10 * time.Millisecond)
// Test geo event filtering
event := PostHogEvent{
Lat: 40.7128,
Lng: -74.0060,
}
inboundChan <- event
// Wait for event to be processed
select {
case receivedEvent := <-eventChan:
geoEvent, ok := receivedEvent.(ResponseGeoEvent)
require.True(t, ok)
assert.Equal(t, 40.7128, geoEvent.Lat)
assert.Equal(t, -74.0060, geoEvent.Lng)
assert.Equal(t, uint(1), geoEvent.Count)
case <-time.After(100 * time.Millisecond):
t.Fatal("Timed out waiting for geo event")
}
}

View File

@ -7,22 +7,26 @@ import (
"github.com/oschwald/maxminddb-golang"
)
type GeoLocator struct {
type MaxMindLocator struct {
db *maxminddb.Reader
}
func NewGeoLocator(dbPath string) (*GeoLocator, error) {
type GeoLocator interface {
Lookup(ipString string) (float64, float64, error)
}
func NewMaxMindGeoLocator(dbPath string) (*MaxMindLocator, error) {
db, err := maxminddb.Open(dbPath)
if err != nil {
return nil, err
}
return &GeoLocator{
return &MaxMindLocator{
db: db,
}, nil
}
func (g *GeoLocator) Lookup(ipString string) (float64, float64, error) {
func (g *MaxMindLocator) Lookup(ipString string) (float64, float64, error) {
ip := net.ParseIP(ipString)
if ip == nil {
return 0, 0, errors.New("invalid IP address")

55
livestream/geoip_test.go Normal file
View File

@ -0,0 +1,55 @@
package main
import (
"errors"
"testing"
"github.com/posthog/posthog/livestream/mocks"
"github.com/stretchr/testify/assert"
)
func TestMaxMindLocator_Lookup_Success(t *testing.T) {
mockLocator := mocks.NewGeoLocator(t)
mockLocator.EXPECT().Lookup("192.0.2.1").Return(40.7128, -74.0060, nil)
latitude, longitude, err := mockLocator.Lookup("192.0.2.1")
assert.NoError(t, err)
assert.Equal(t, 40.7128, latitude)
assert.Equal(t, -74.0060, longitude)
}
func TestMaxMindLocator_Lookup_InvalidIP(t *testing.T) {
mockLocator := mocks.NewGeoLocator(t)
mockLocator.EXPECT().Lookup("invalid_ip").Return(0.0, 0.0, errors.New("invalid IP address"))
latitude, longitude, err := mockLocator.Lookup("invalid_ip")
assert.Error(t, err)
assert.Equal(t, "invalid IP address", err.Error())
assert.Equal(t, 0.0, latitude)
assert.Equal(t, 0.0, longitude)
}
func TestMaxMindLocator_Lookup_DatabaseError(t *testing.T) {
mockLocator := mocks.NewGeoLocator(t)
mockLocator.EXPECT().Lookup("192.0.2.1").Return(0.0, 0.0, errors.New("database error"))
latitude, longitude, err := mockLocator.Lookup("192.0.2.1")
assert.Error(t, err)
assert.Equal(t, "database error", err.Error())
assert.Equal(t, 0.0, latitude)
assert.Equal(t, 0.0, longitude)
}
func TestNewMaxMindGeoLocator_Success(t *testing.T) {
// This test would require mocking the maxminddb.Open function, which is not possible with the current setup.
// In a real scenario, you might use a test database file or mock the file system.
t.Skip("Skipping NewMaxMindGeoLocator test as it requires filesystem interaction")
}
func TestNewMaxMindGeoLocator_Error(t *testing.T) {
// Similar to the success case, this test would require mocking filesystem operations.
t.Skip("Skipping NewMaxMindGeoLocator error test as it requires filesystem interaction")
}

View File

@ -1,4 +1,4 @@
module github.com/posthog/livestream
module github.com/posthog/posthog/livestream
go 1.22.2
@ -13,11 +13,13 @@ require (
github.com/labstack/echo/v4 v4.12.0
github.com/oschwald/maxminddb-golang v1.12.0
github.com/spf13/viper v1.18.2
github.com/stretchr/testify v1.9.0
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a
)
require (
github.com/aws/aws-sdk-go-v2/config v1.26.5 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/docker/docker v25.0.5+incompatible // indirect
github.com/docker/docker-credential-helpers v0.8.1 // indirect
github.com/emicklei/go-restful/v3 v3.11.2 // indirect
@ -36,6 +38,7 @@ require (
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_golang v1.18.0 // indirect
github.com/prometheus/client_model v0.5.0 // indirect
github.com/prometheus/common v0.46.0 // indirect
@ -47,6 +50,7 @@ require (
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect

View File

@ -309,6 +309,8 @@ github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMV
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=

99
livestream/jwt_test.go Normal file
View File

@ -0,0 +1,99 @@
package main
import (
"testing"
"time"
"github.com/golang-jwt/jwt"
"github.com/spf13/viper"
)
func TestDecodeAuthToken(t *testing.T) {
// Set up a mock secret for testing
viper.Set("jwt.secret", "test-secret")
tests := []struct {
name string
authHeader string
expectError bool
expectedAud string
}{
{
name: "Valid token",
authHeader: "Bearer " + createValidToken(ExpectedScope),
expectError: false,
expectedAud: ExpectedScope,
},
{
name: "Invalid token format",
authHeader: "InvalidToken",
expectError: true,
},
{
name: "Missing Bearer prefix",
authHeader: createValidToken(ExpectedScope),
expectError: true,
},
{
name: "Invalid audience",
authHeader: "Bearer " + createValidToken("invalid:scope"),
expectError: true,
},
{
name: "Expired token",
authHeader: "Bearer " + createExpiredToken(),
expectError: true,
},
{
name: "Invalid signature",
authHeader: "Bearer " + createTokenWithInvalidSignature(),
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
claims, err := decodeAuthToken(tt.authHeader)
if tt.expectError {
if err == nil {
t.Errorf("Expected an error, but got nil")
}
} else {
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if claims["aud"] != tt.expectedAud {
t.Errorf("Expected audience %s, but got %s", tt.expectedAud, claims["aud"])
}
}
})
}
}
func createValidToken(audience string) string {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"aud": audience,
"exp": time.Now().Add(time.Hour).Unix(),
})
tokenString, _ := token.SignedString([]byte(viper.GetString("jwt.secret")))
return tokenString
}
func createExpiredToken() string {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"aud": ExpectedScope,
"exp": time.Now().Add(-time.Hour).Unix(),
})
tokenString, _ := token.SignedString([]byte(viper.GetString("jwt.secret")))
return tokenString
}
func createTokenWithInvalidSignature() string {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"aud": ExpectedScope,
"exp": time.Now().Add(time.Hour).Unix(),
})
tokenString, _ := token.SignedString([]byte("wrong-secret"))
return tokenString
}

View File

@ -28,15 +28,26 @@ type PostHogEvent struct {
Lng float64
}
type KafkaConsumer struct {
consumer *kafka.Consumer
type KafkaConsumerInterface interface {
SubscribeTopics(topics []string, rebalanceCb kafka.RebalanceCb) error
ReadMessage(timeout time.Duration) (*kafka.Message, error)
Close() error
}
type KafkaConsumer interface {
Consume()
Close()
}
type PostHogKafkaConsumer struct {
consumer KafkaConsumerInterface
topic string
geolocator *GeoLocator
geolocator GeoLocator
outgoingChan chan PostHogEvent
statsChan chan PostHogEvent
}
func NewKafkaConsumer(brokers string, securityProtocol string, groupID string, topic string, geolocator *GeoLocator, outgoingChan chan PostHogEvent, statsChan chan PostHogEvent) (*KafkaConsumer, error) {
func NewPostHogKafkaConsumer(brokers string, securityProtocol string, groupID string, topic string, geolocator GeoLocator, outgoingChan chan PostHogEvent, statsChan chan PostHogEvent) (*PostHogKafkaConsumer, error) {
config := &kafka.ConfigMap{
"bootstrap.servers": brokers,
"group.id": groupID,
@ -50,7 +61,7 @@ func NewKafkaConsumer(brokers string, securityProtocol string, groupID string, t
return nil, err
}
return &KafkaConsumer{
return &PostHogKafkaConsumer{
consumer: consumer,
topic: topic,
geolocator: geolocator,
@ -59,7 +70,7 @@ func NewKafkaConsumer(brokers string, securityProtocol string, groupID string, t
}, nil
}
func (c *KafkaConsumer) Consume() {
func (c *PostHogKafkaConsumer) Consume() {
err := c.consumer.SubscribeTopics([]string{c.topic}, nil)
if err != nil {
sentry.CaptureException(err)
@ -126,6 +137,6 @@ func (c *KafkaConsumer) Consume() {
}
}
func (c *KafkaConsumer) Close() {
func (c *PostHogKafkaConsumer) Close() {
c.consumer.Close()
}

98
livestream/kafka_test.go Normal file
View File

@ -0,0 +1,98 @@
package main
import (
"encoding/json"
"errors"
"testing"
"time"
"github.com/confluentinc/confluent-kafka-go/v2/kafka"
"github.com/posthog/posthog/livestream/mocks"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
func TestPostHogKafkaConsumer_Consume(t *testing.T) {
// Create mock objects
mockConsumer := new(mocks.KafkaConsumerInterface)
mockGeoLocator := new(mocks.GeoLocator)
// Create channels
outgoingChan := make(chan PostHogEvent, 1)
statsChan := make(chan PostHogEvent, 1)
// Create PostHogKafkaConsumer
consumer := &PostHogKafkaConsumer{
consumer: mockConsumer,
topic: "test-topic",
geolocator: mockGeoLocator,
outgoingChan: outgoingChan,
statsChan: statsChan,
}
// Mock SubscribeTopics
mockConsumer.On("SubscribeTopics", []string{"test-topic"}, mock.AnythingOfType("kafka.RebalanceCb")).Return(nil)
// Create a test message
testWrapper := PostHogEventWrapper{
Uuid: "test-uuid",
DistinctId: "test-distinct-id",
Ip: "192.0.2.1",
Data: `{"event": "test-event", "properties": {"token": "test-token"}}`,
}
testMessageValue, _ := json.Marshal(testWrapper)
testMessage := &kafka.Message{
Value: testMessageValue,
}
// Mock ReadMessage
mockConsumer.On("ReadMessage", mock.AnythingOfType("time.Duration")).Return(testMessage, nil).Maybe()
// Mock GeoLocator Lookup
mockGeoLocator.On("Lookup", "192.0.2.1").Return(37.7749, -122.4194, nil)
// Run Consume in a goroutine
go consumer.Consume()
// Wait for the message to be processed
select {
case event := <-outgoingChan:
assert.Equal(t, "test-uuid", event.Uuid)
assert.Equal(t, "test-distinct-id", event.DistinctId)
assert.Equal(t, "test-event", event.Event)
assert.Equal(t, "test-token", event.Token)
assert.Equal(t, 37.7749, event.Lat)
assert.Equal(t, -122.4194, event.Lng)
case <-time.After(time.Second):
t.Fatal("Timed out waiting for message")
}
// Check if the message was also sent to statsChan
select {
case <-statsChan:
// Message received in statsChan
case <-time.After(time.Second):
t.Fatal("Timed out waiting for stats message")
}
// Test error handling
mockConsumer.On("ReadMessage", mock.AnythingOfType("time.Duration")).Return(nil, errors.New("read error")).Maybe()
time.Sleep(time.Millisecond * 100) // Give some time for the error to be processed
// Assert that all expectations were met
mockConsumer.AssertExpectations(t)
mockGeoLocator.AssertExpectations(t)
}
func TestPostHogKafkaConsumer_Close(t *testing.T) {
mockConsumer := new(mocks.KafkaConsumerInterface)
consumer := &PostHogKafkaConsumer{
consumer: mockConsumer,
}
mockConsumer.On("Close").Return(nil)
consumer.Close()
mockConsumer.AssertExpectations(t)
}

View File

@ -57,7 +57,7 @@ func main() {
log.Fatal("kafka.group_id must be set")
}
geolocator, err := NewGeoLocator(mmdb)
geolocator, err := NewMaxMindGeoLocator(mmdb)
if err != nil {
sentry.CaptureException(err)
log.Fatalf("Failed to open MMDB: %v", err)
@ -78,7 +78,7 @@ func main() {
if !isProd {
kafkaSecurityProtocol = "PLAINTEXT"
}
consumer, err := NewKafkaConsumer(brokers, kafkaSecurityProtocol, groupID, topic, geolocator, phEventChan, statsChan)
consumer, err := NewPostHogKafkaConsumer(brokers, kafkaSecurityProtocol, groupID, topic, geolocator, phEventChan, statsChan)
if err != nil {
sentry.CaptureException(err)
log.Fatalf("Failed to create Kafka consumer: %v", err)

61
livestream/main_test.go Normal file
View File

@ -0,0 +1,61 @@
// go:generate mockery
package main
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/hashicorp/golang-lru/v2/expirable"
"github.com/labstack/echo/v4"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestIndex(t *testing.T) {
e := echo.New()
req := httptest.NewRequest(http.MethodGet, "/", nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
if assert.NoError(t, index(c)) {
assert.Equal(t, http.StatusOK, rec.Code)
assert.Equal(t, "RealTime Hog 3000", rec.Body.String())
}
}
func TestStatsHandler(t *testing.T) {
e := echo.New()
req := httptest.NewRequest(http.MethodGet, "/stats", nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
// Mock the authorization header
req.Header.Set("Authorization", "Bearer mock_token")
// Create a mock TeamStats
teamStats := &TeamStats{
Store: make(map[string]*expirable.LRU[string, string]),
}
teamStats.Store["mock_token"] = expirable.NewLRU[string, string](100, nil, time.Minute)
teamStats.Store["mock_token"].Add("user1", "data1")
// Add the teamStats to the context
c.Set("teamStats", teamStats)
handler := func(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]interface{}{
"users_on_product": teamStats.Store["mock_token"].Len(),
})
}
if assert.NoError(t, handler(c)) {
assert.Equal(t, http.StatusOK, rec.Code)
var response map[string]int
err := json.Unmarshal(rec.Body.Bytes(), &response)
require.NoError(t, err)
assert.Equal(t, 1, response["users_on_product"])
}
}

View File

@ -0,0 +1,95 @@
// Code generated by mockery v2.44.1. DO NOT EDIT.
package mocks
import mock "github.com/stretchr/testify/mock"
// GeoLocator is an autogenerated mock type for the GeoLocator type
type GeoLocator struct {
mock.Mock
}
type GeoLocator_Expecter struct {
mock *mock.Mock
}
func (_m *GeoLocator) EXPECT() *GeoLocator_Expecter {
return &GeoLocator_Expecter{mock: &_m.Mock}
}
// Lookup provides a mock function with given fields: ipString
func (_m *GeoLocator) Lookup(ipString string) (float64, float64, error) {
ret := _m.Called(ipString)
if len(ret) == 0 {
panic("no return value specified for Lookup")
}
var r0 float64
var r1 float64
var r2 error
if rf, ok := ret.Get(0).(func(string) (float64, float64, error)); ok {
return rf(ipString)
}
if rf, ok := ret.Get(0).(func(string) float64); ok {
r0 = rf(ipString)
} else {
r0 = ret.Get(0).(float64)
}
if rf, ok := ret.Get(1).(func(string) float64); ok {
r1 = rf(ipString)
} else {
r1 = ret.Get(1).(float64)
}
if rf, ok := ret.Get(2).(func(string) error); ok {
r2 = rf(ipString)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// GeoLocator_Lookup_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Lookup'
type GeoLocator_Lookup_Call struct {
*mock.Call
}
// Lookup is a helper method to define mock.On call
// - ipString string
func (_e *GeoLocator_Expecter) Lookup(ipString interface{}) *GeoLocator_Lookup_Call {
return &GeoLocator_Lookup_Call{Call: _e.mock.On("Lookup", ipString)}
}
func (_c *GeoLocator_Lookup_Call) Run(run func(ipString string)) *GeoLocator_Lookup_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string))
})
return _c
}
func (_c *GeoLocator_Lookup_Call) Return(_a0 float64, _a1 float64, _a2 error) *GeoLocator_Lookup_Call {
_c.Call.Return(_a0, _a1, _a2)
return _c
}
func (_c *GeoLocator_Lookup_Call) RunAndReturn(run func(string) (float64, float64, error)) *GeoLocator_Lookup_Call {
_c.Call.Return(run)
return _c
}
// NewGeoLocator creates a new instance of GeoLocator. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewGeoLocator(t interface {
mock.TestingT
Cleanup(func())
}) *GeoLocator {
mock := &GeoLocator{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View File

@ -0,0 +1,96 @@
// Code generated by mockery v2.44.1. DO NOT EDIT.
package mocks
import mock "github.com/stretchr/testify/mock"
// KafkaConsumer is an autogenerated mock type for the KafkaConsumer type
type KafkaConsumer struct {
mock.Mock
}
type KafkaConsumer_Expecter struct {
mock *mock.Mock
}
func (_m *KafkaConsumer) EXPECT() *KafkaConsumer_Expecter {
return &KafkaConsumer_Expecter{mock: &_m.Mock}
}
// Close provides a mock function with given fields:
func (_m *KafkaConsumer) Close() {
_m.Called()
}
// KafkaConsumer_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close'
type KafkaConsumer_Close_Call struct {
*mock.Call
}
// Close is a helper method to define mock.On call
func (_e *KafkaConsumer_Expecter) Close() *KafkaConsumer_Close_Call {
return &KafkaConsumer_Close_Call{Call: _e.mock.On("Close")}
}
func (_c *KafkaConsumer_Close_Call) Run(run func()) *KafkaConsumer_Close_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *KafkaConsumer_Close_Call) Return() *KafkaConsumer_Close_Call {
_c.Call.Return()
return _c
}
func (_c *KafkaConsumer_Close_Call) RunAndReturn(run func()) *KafkaConsumer_Close_Call {
_c.Call.Return(run)
return _c
}
// Consume provides a mock function with given fields:
func (_m *KafkaConsumer) Consume() {
_m.Called()
}
// KafkaConsumer_Consume_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Consume'
type KafkaConsumer_Consume_Call struct {
*mock.Call
}
// Consume is a helper method to define mock.On call
func (_e *KafkaConsumer_Expecter) Consume() *KafkaConsumer_Consume_Call {
return &KafkaConsumer_Consume_Call{Call: _e.mock.On("Consume")}
}
func (_c *KafkaConsumer_Consume_Call) Run(run func()) *KafkaConsumer_Consume_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *KafkaConsumer_Consume_Call) Return() *KafkaConsumer_Consume_Call {
_c.Call.Return()
return _c
}
func (_c *KafkaConsumer_Consume_Call) RunAndReturn(run func()) *KafkaConsumer_Consume_Call {
_c.Call.Return(run)
return _c
}
// NewKafkaConsumer creates a new instance of KafkaConsumer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewKafkaConsumer(t interface {
mock.TestingT
Cleanup(func())
}) *KafkaConsumer {
mock := &KafkaConsumer{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View File

@ -0,0 +1,187 @@
// Code generated by mockery v2.44.1. DO NOT EDIT.
package mocks
import (
kafka "github.com/confluentinc/confluent-kafka-go/v2/kafka"
mock "github.com/stretchr/testify/mock"
time "time"
)
// KafkaConsumerInterface is an autogenerated mock type for the KafkaConsumerInterface type
type KafkaConsumerInterface struct {
mock.Mock
}
type KafkaConsumerInterface_Expecter struct {
mock *mock.Mock
}
func (_m *KafkaConsumerInterface) EXPECT() *KafkaConsumerInterface_Expecter {
return &KafkaConsumerInterface_Expecter{mock: &_m.Mock}
}
// Close provides a mock function with given fields:
func (_m *KafkaConsumerInterface) Close() error {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for Close")
}
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
} else {
r0 = ret.Error(0)
}
return r0
}
// KafkaConsumerInterface_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close'
type KafkaConsumerInterface_Close_Call struct {
*mock.Call
}
// Close is a helper method to define mock.On call
func (_e *KafkaConsumerInterface_Expecter) Close() *KafkaConsumerInterface_Close_Call {
return &KafkaConsumerInterface_Close_Call{Call: _e.mock.On("Close")}
}
func (_c *KafkaConsumerInterface_Close_Call) Run(run func()) *KafkaConsumerInterface_Close_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *KafkaConsumerInterface_Close_Call) Return(_a0 error) *KafkaConsumerInterface_Close_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *KafkaConsumerInterface_Close_Call) RunAndReturn(run func() error) *KafkaConsumerInterface_Close_Call {
_c.Call.Return(run)
return _c
}
// ReadMessage provides a mock function with given fields: timeout
func (_m *KafkaConsumerInterface) ReadMessage(timeout time.Duration) (*kafka.Message, error) {
ret := _m.Called(timeout)
if len(ret) == 0 {
panic("no return value specified for ReadMessage")
}
var r0 *kafka.Message
var r1 error
if rf, ok := ret.Get(0).(func(time.Duration) (*kafka.Message, error)); ok {
return rf(timeout)
}
if rf, ok := ret.Get(0).(func(time.Duration) *kafka.Message); ok {
r0 = rf(timeout)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*kafka.Message)
}
}
if rf, ok := ret.Get(1).(func(time.Duration) error); ok {
r1 = rf(timeout)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// KafkaConsumerInterface_ReadMessage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ReadMessage'
type KafkaConsumerInterface_ReadMessage_Call struct {
*mock.Call
}
// ReadMessage is a helper method to define mock.On call
// - timeout time.Duration
func (_e *KafkaConsumerInterface_Expecter) ReadMessage(timeout interface{}) *KafkaConsumerInterface_ReadMessage_Call {
return &KafkaConsumerInterface_ReadMessage_Call{Call: _e.mock.On("ReadMessage", timeout)}
}
func (_c *KafkaConsumerInterface_ReadMessage_Call) Run(run func(timeout time.Duration)) *KafkaConsumerInterface_ReadMessage_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(time.Duration))
})
return _c
}
func (_c *KafkaConsumerInterface_ReadMessage_Call) Return(_a0 *kafka.Message, _a1 error) *KafkaConsumerInterface_ReadMessage_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *KafkaConsumerInterface_ReadMessage_Call) RunAndReturn(run func(time.Duration) (*kafka.Message, error)) *KafkaConsumerInterface_ReadMessage_Call {
_c.Call.Return(run)
return _c
}
// SubscribeTopics provides a mock function with given fields: topics, rebalanceCb
func (_m *KafkaConsumerInterface) SubscribeTopics(topics []string, rebalanceCb kafka.RebalanceCb) error {
ret := _m.Called(topics, rebalanceCb)
if len(ret) == 0 {
panic("no return value specified for SubscribeTopics")
}
var r0 error
if rf, ok := ret.Get(0).(func([]string, kafka.RebalanceCb) error); ok {
r0 = rf(topics, rebalanceCb)
} else {
r0 = ret.Error(0)
}
return r0
}
// KafkaConsumerInterface_SubscribeTopics_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SubscribeTopics'
type KafkaConsumerInterface_SubscribeTopics_Call struct {
*mock.Call
}
// SubscribeTopics is a helper method to define mock.On call
// - topics []string
// - rebalanceCb kafka.RebalanceCb
func (_e *KafkaConsumerInterface_Expecter) SubscribeTopics(topics interface{}, rebalanceCb interface{}) *KafkaConsumerInterface_SubscribeTopics_Call {
return &KafkaConsumerInterface_SubscribeTopics_Call{Call: _e.mock.On("SubscribeTopics", topics, rebalanceCb)}
}
func (_c *KafkaConsumerInterface_SubscribeTopics_Call) Run(run func(topics []string, rebalanceCb kafka.RebalanceCb)) *KafkaConsumerInterface_SubscribeTopics_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].([]string), args[1].(kafka.RebalanceCb))
})
return _c
}
func (_c *KafkaConsumerInterface_SubscribeTopics_Call) Return(_a0 error) *KafkaConsumerInterface_SubscribeTopics_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *KafkaConsumerInterface_SubscribeTopics_Call) RunAndReturn(run func([]string, kafka.RebalanceCb) error) *KafkaConsumerInterface_SubscribeTopics_Call {
_c.Call.Return(run)
return _c
}
// NewKafkaConsumerInterface creates a new instance of KafkaConsumerInterface. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewKafkaConsumerInterface(t interface {
mock.TestingT
Cleanup(func())
}) *KafkaConsumerInterface {
mock := &KafkaConsumerInterface{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}