diff --git a/cmd/backend/dependencies.go b/cmd/backend/dependencies.go index e133230..bbcc554 100644 --- a/cmd/backend/dependencies.go +++ b/cmd/backend/dependencies.go @@ -30,6 +30,7 @@ import ( "github.com/database-playground/backend-v2/internal/workers" "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" + "github.com/posthog/posthog-go" "github.com/redis/rueidis" "github.com/vektah/gqlparser/v2/ast" "go.uber.org/fx" @@ -50,6 +51,31 @@ func ApqCache(redisClient rueidis.Client) graphql.Cache[string] { return apq.NewCache(redisClient, 24*time.Hour) } +func PostHogClient(lifecycle fx.Lifecycle, cfg config.Config) (posthog.Client, error) { + if cfg.PostHog.APIKey == nil || cfg.PostHog.Host == nil { + slog.Warn("PostHog client is not initialized, because you did not configure a PostHog API key and a host.") + return nil, nil + } + + client, err := posthog.NewWithConfig( + *cfg.PostHog.APIKey, + posthog.Config{ + Endpoint: *cfg.PostHog.Host, + }, + ) + if err != nil { + return nil, err + } + + lifecycle.Append(fx.StopHook(func() { + if err := client.Close(); err != nil { + slog.Info("failed to close PostHog client", "error", err) + } + })) + + return client, nil +} + // GqlgenHandler creates a gqlgen handler. func GqlgenHandler( entClient *ent.Client, @@ -85,8 +111,8 @@ func UserAccountContext(entClient *ent.Client, storage auth.Storage, eventServic } // EventService creates an events.EventService. -func EventService(entClient *ent.Client) *events.EventService { - return events.NewEventService(entClient) +func EventService(entClient *ent.Client, posthogClient posthog.Client) *events.EventService { + return events.NewEventService(entClient, posthogClient) } // SubmissionService creates a submission.SubmissionService. diff --git a/cmd/backend/server.go b/cmd/backend/server.go index 7694b3d..63531a8 100644 --- a/cmd/backend/server.go +++ b/cmd/backend/server.go @@ -20,6 +20,7 @@ func main() { EventService, SubmissionService, ApqCache, + PostHogClient, AnnotateService(AuthService), GqlgenHandler, fx.Annotate( diff --git a/docs/config.md b/docs/config.md index a5337b9..5699dbb 100644 --- a/docs/config.md +++ b/docs/config.md @@ -57,3 +57,13 @@ Google OAuth 的「已授權的重新導向 URI」應包含 `https://HOST/api/au ## SQL Runner - `SQL_RUNNER_URI`:[SQL Runner API](https://github.com/database-playground/sqlrunner-v2) 的連線 URL,如 `https://sqlrunner.dbplay.app`。部署說明可參見 [Usage > Starting the service](https://github.com/database-playground/sqlrunner-v2/tree/main?tab=readme-ov-file#starting-the-service)。 + +## PostHog 設定 + +PostHog 是一個產品統計平台。這個專案使用 [posthog-go](https://posthog.com/docs/libraries/go) 做後端的 event 寫入。 + +如果不填寫 API Key 則代表不送出任何統計。 + +- `POSTHOG_API_KEY`: PostHog 的 API key。可以在 PostHog 的 Settings > Project > General > Project API key 中取得。 +- `POSTHOG_HOST`: PostHog API 的主機。可以在 PostHog 的 Settings > Project > General > Web snippet 中的 `api_host` 取得。 + - e.g. `https://us.i.posthog.com` diff --git a/go.mod b/go.mod index 9d42d3d..dbb8a85 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/jackc/pgx/v5 v5.7.6 github.com/joho/godotenv v1.5.1 github.com/mattn/go-sqlite3 v1.14.28 + github.com/posthog/posthog-go v1.6.10 github.com/redis/rueidis v1.0.66 github.com/samber/lo v1.51.0 github.com/stretchr/testify v1.11.1 diff --git a/go.sum b/go.sum index a012082..d1d20ee 100644 --- a/go.sum +++ b/go.sum @@ -215,6 +215,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posthog/posthog-go v1.6.10 h1:OA6bkiUg89rI7f5cSXbcrH5+wLinyS6hHplnD92Pu/M= +github.com/posthog/posthog-go v1.6.10/go.mod h1:LcC1Nu4AgvV22EndTtrMXTy+7RGVC0MhChSw7Qk5XkY= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= diff --git a/graph/user.resolvers_test.go b/graph/user.resolvers_test.go index faebeab..d21b150 100644 --- a/graph/user.resolvers_test.go +++ b/graph/user.resolvers_test.go @@ -54,7 +54,7 @@ func (m *mockAuthStorage) Peek(ctx context.Context, token string) (auth.TokenInf func NewTestResolver(t *testing.T, entClient *ent.Client, authStorage auth.Storage) *Resolver { t.Helper() - eventService := events.NewEventService(entClient) + eventService := events.NewEventService(entClient, nil) sqlrunner := testhelper.NewSQLRunnerClient(t) submissionService := submission.NewSubmissionService(entClient, eventService, sqlrunner) diff --git a/httpapi/auth/introspect_test.go b/httpapi/auth/introspect_test.go index 66663a3..ee6951f 100644 --- a/httpapi/auth/introspect_test.go +++ b/httpapi/auth/introspect_test.go @@ -54,7 +54,7 @@ func setupTestAuthServiceWithDatabase(t *testing.T) (*AuthService, *mockAuthStor storage := newMockAuthStorageForIntrospect() cfg := config.Config{} - eventService := events.NewEventService(entClient) + eventService := events.NewEventService(entClient, nil) useraccount := useraccount.NewContext(entClient, storage, eventService) authService := NewAuthService(entClient, storage, cfg, useraccount) diff --git a/httpapi/auth/revoke_test.go b/httpapi/auth/revoke_test.go index f65032c..14a4129 100644 --- a/httpapi/auth/revoke_test.go +++ b/httpapi/auth/revoke_test.go @@ -76,7 +76,7 @@ func setupTestAuthService(t *testing.T) (*AuthService, *mockAuthStorage) { entClient := testhelper.NewEntSqliteClient(t) storage := newMockAuthStorage() cfg := config.Config{} - eventService := events.NewEventService(entClient) + eventService := events.NewEventService(entClient, nil) useraccount := useraccount.NewContext(entClient, storage, eventService) authService := NewAuthService(entClient, storage, cfg, useraccount) diff --git a/internal/config/models.go b/internal/config/models.go index 3dc98b9..61dff60 100644 --- a/internal/config/models.go +++ b/internal/config/models.go @@ -17,6 +17,7 @@ type Config struct { GAuth GAuthConfig `envPrefix:"GAUTH_"` Server ServerConfig `envPrefix:"SERVER_"` SqlRunner SqlRunnerConfig `envPrefix:"SQL_RUNNER_"` + PostHog PostHogConfig `envPrefix:"POSTHOG_"` } func (c Config) Validate() error { @@ -32,6 +33,12 @@ func (c Config) Validate() error { if err := c.Server.Validate(); err != nil { return fmt.Errorf("SERVER: %w", err) } + if err := c.SqlRunner.Validate(); err != nil { + return fmt.Errorf("SQL_RUNNER: %w", err) + } + if err := c.PostHog.Validate(); err != nil { + return fmt.Errorf("POSTHOG: %w", err) + } return nil } @@ -147,3 +154,20 @@ func (c SqlRunnerConfig) Validate() error { return nil } + +type PostHogConfig struct { + APIKey *string `env:"API_KEY"` + Host *string `env:"HOST"` +} + +func (c PostHogConfig) Validate() error { + if c.APIKey != nil && *c.APIKey == "" { + return errors.New("POSTHOG_API_KEY cannot be empty") + } + + if c.Host != nil && *c.Host == "" { + return errors.New("POSTHOG_HOST cannot be empty") + } + + return nil +} diff --git a/internal/events/constants.go b/internal/events/constants.go index 9acde6b..3c4761c 100644 --- a/internal/events/constants.go +++ b/internal/events/constants.go @@ -9,4 +9,7 @@ const ( EventTypeLogoutAll EventType = "logout_all" EventTypeSubmitAnswer EventType = "submit_answer" + + // Internal usage + EventTypeGrantPoint EventType = "grant_point" ) diff --git a/internal/events/events.go b/internal/events/events.go index 5b8cd42..a395ca8 100644 --- a/internal/events/events.go +++ b/internal/events/events.go @@ -3,23 +3,27 @@ package events import ( "context" "log/slog" + "strconv" "time" "github.com/database-playground/backend-v2/ent" + "github.com/posthog/posthog-go" ) // EventService is the service for triggering events. type EventService struct { - entClient *ent.Client + entClient *ent.Client + posthogClient posthog.Client handlers []EventHandler } // NewEventService creates a new EventService. -func NewEventService(entClient *ent.Client) *EventService { +func NewEventService(entClient *ent.Client, posthogClient posthog.Client) *EventService { return &EventService{ - entClient: entClient, - handlers: []EventHandler{NewPointsGranter(entClient)}, + entClient: entClient, + posthogClient: posthogClient, + handlers: []EventHandler{NewPointsGranter(entClient, posthogClient)}, } } @@ -43,6 +47,16 @@ func (s *EventService) TriggerEvent(ctx context.Context, event Event) { if err != nil { slog.Error("failed to trigger event", "error", err) } + + if s.posthogClient != nil { + slog.Debug("sending event to PostHog", "event_type", event.Type, "user_id", event.UserID) + s.posthogClient.Enqueue(posthog.Capture{ + DistinctId: strconv.Itoa(event.UserID), + Event: string(event.Type), + Timestamp: time.Now(), + Properties: event.Payload, + }) + } } // triggerEvent triggers an event synchronously. diff --git a/internal/events/points.go b/internal/events/points.go index 459c1e9..86de3c6 100644 --- a/internal/events/points.go +++ b/internal/events/points.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "log/slog" + "strconv" "time" "github.com/database-playground/backend-v2/ent" @@ -12,6 +13,7 @@ import ( "github.com/database-playground/backend-v2/ent/question" "github.com/database-playground/backend-v2/ent/submission" "github.com/database-playground/backend-v2/ent/user" + "github.com/posthog/posthog-go" ) // startOfDay returns the start of the given day (midnight). @@ -45,13 +47,15 @@ const ( // PointsGranter determines if the criteria is met to grant points to a user. type PointsGranter struct { - entClient *ent.Client + entClient *ent.Client + posthogClient posthog.Client } // NewPointsGranter creates a new PointsGranter. -func NewPointsGranter(entClient *ent.Client) *PointsGranter { +func NewPointsGranter(entClient *ent.Client, posthogClient posthog.Client) *PointsGranter { return &PointsGranter{ - entClient: entClient, + entClient: entClient, + posthogClient: posthogClient, } } @@ -171,11 +175,7 @@ func (d *PointsGranter) GrantDailyLoginPoints(ctx context.Context, userID int) ( } // Grant the "daily login" points to the user. - err = d.entClient.Point.Create(). - SetUserID(userID). - SetDescription(PointDescriptionDailyLogin). - SetPoints(PointValueDailyLogin). - Exec(ctx) + err = d.grantPoint(ctx, userID, 0, PointDescriptionDailyLogin, PointValueDailyLogin) if err != nil { return false, err } @@ -221,14 +221,11 @@ func (d *PointsGranter) GrantWeeklyLoginPoints(ctx context.Context, userID int) } // Grant the "weekly login" points to the user. - err = d.entClient.Point.Create(). - SetUserID(userID). - SetDescription(PointDescriptionWeeklyLogin). - SetPoints(PointValueWeeklyLogin). - Exec(ctx) + err = d.grantPoint(ctx, userID, 0, PointDescriptionWeeklyLogin, PointValueWeeklyLogin) if err != nil { return false, err } + return true, nil } @@ -261,11 +258,7 @@ func (d *PointsGranter) GrantFirstAttemptPoints(ctx context.Context, userID int, } // Grant the "first attempt" points to the user. - err = d.entClient.Point.Create(). - SetUserID(userID). - SetDescription(description). - SetPoints(PointValueFirstAttempt). - Exec(ctx) + err = d.grantPoint(ctx, userID, questionID, description, PointValueFirstAttempt) if err != nil { return false, err } @@ -305,11 +298,7 @@ func (d *PointsGranter) GrantDailyAttemptPoints(ctx context.Context, userID int) } // Grant the "daily attempt" points to the user. - err = d.entClient.Point.Create(). - SetUserID(userID). - SetDescription(PointDescriptionDailyAttempt). - SetPoints(PointValueDailyAttempt). - Exec(ctx) + err = d.grantPoint(ctx, userID, 0, PointDescriptionDailyAttempt, PointValueDailyAttempt) if err != nil { return false, err } @@ -347,11 +336,7 @@ func (d *PointsGranter) GrantCorrectAnswerPoints(ctx context.Context, userID int } // Grant the "correct answer" points to the user. - err = d.entClient.Point.Create(). - SetUserID(userID). - SetDescription(description). - SetPoints(PointValueCorrectAnswer). - Exec(ctx) + err = d.grantPoint(ctx, userID, questionID, description, PointValueCorrectAnswer) if err != nil { return false, err } @@ -397,14 +382,49 @@ func (d *PointsGranter) GrantFirstPlacePoints(ctx context.Context, userID int, q } // Grant the "first place" points to the user. - err = d.entClient.Point.Create(). + err = d.grantPoint(ctx, userID, questionID, description, PointValueFirstPlace) + if err != nil { + return false, err + } + + return true, nil +} + +func (d *PointsGranter) grantPoint(ctx context.Context, userID int, questionID int, description string, points int) error { + err := d.entClient.Point.Create(). SetUserID(userID). SetDescription(description). - SetPoints(PointValueFirstPlace). + SetPoints(points). Exec(ctx) if err != nil { - return false, err + if d.posthogClient != nil { + d.posthogClient.Enqueue(posthog.NewDefaultException( + time.Now(), strconv.Itoa(userID), + "failed to grant point", err.Error(), + )) + } + + return err } - return true, nil + if d.posthogClient != nil { + properties := posthog.NewProperties(). + Set("description", description). + Set("points", points) + + if questionID != 0 { + properties.Set("questionID", strconv.Itoa(questionID)) + } + + slog.Debug("sending event to PostHog", "event_type", EventTypeGrantPoint, "user_id", userID) + + d.posthogClient.Enqueue(posthog.Capture{ + DistinctId: strconv.Itoa(userID), + Event: string(EventTypeGrantPoint), + Timestamp: time.Now(), + Properties: properties, + }) + } + + return nil } diff --git a/internal/events/points_test.go b/internal/events/points_test.go index e09f97d..c32ed31 100644 --- a/internal/events/points_test.go +++ b/internal/events/points_test.go @@ -70,7 +70,7 @@ func createPointsRecord(t *testing.T, client *ent.Client, userID int, descriptio func TestGrantDailyLoginPoints_Success(t *testing.T) { client := testhelper.NewEntSqliteClient(t) - granter := events.NewPointsGranter(client) + granter := events.NewPointsGranter(client, nil) userID := setupTestData(t, client) ctx := context.Background() @@ -96,7 +96,7 @@ func TestGrantDailyLoginPoints_Success(t *testing.T) { func TestGrantDailyLoginPoints_AlreadyGrantedToday(t *testing.T) { client := testhelper.NewEntSqliteClient(t) - granter := events.NewPointsGranter(client) + granter := events.NewPointsGranter(client, nil) userID := setupTestData(t, client) ctx := context.Background() @@ -124,7 +124,7 @@ func TestGrantDailyLoginPoints_AlreadyGrantedToday(t *testing.T) { func TestGrantDailyLoginPoints_NoLoginToday(t *testing.T) { client := testhelper.NewEntSqliteClient(t) - granter := events.NewPointsGranter(client) + granter := events.NewPointsGranter(client, nil) userID := setupTestData(t, client) ctx := context.Background() @@ -149,7 +149,7 @@ func TestGrantDailyLoginPoints_NoLoginToday(t *testing.T) { func TestGrantDailyLoginPoints_OldPointsRecordExists(t *testing.T) { client := testhelper.NewEntSqliteClient(t) - granter := events.NewPointsGranter(client) + granter := events.NewPointsGranter(client, nil) userID := setupTestData(t, client) ctx := context.Background() @@ -178,7 +178,7 @@ func TestGrantDailyLoginPoints_OldPointsRecordExists(t *testing.T) { func TestGrantWeeklyLoginPoints_Success(t *testing.T) { client := testhelper.NewEntSqliteClient(t) - granter := events.NewPointsGranter(client) + granter := events.NewPointsGranter(client, nil) userID := setupTestData(t, client) ctx := context.Background() @@ -207,7 +207,7 @@ func TestGrantWeeklyLoginPoints_Success(t *testing.T) { func TestGrantWeeklyLoginPoints_AlreadyGrantedThisWeek(t *testing.T) { client := testhelper.NewEntSqliteClient(t) - granter := events.NewPointsGranter(client) + granter := events.NewPointsGranter(client, nil) userID := setupTestData(t, client) ctx := context.Background() @@ -238,7 +238,7 @@ func TestGrantWeeklyLoginPoints_AlreadyGrantedThisWeek(t *testing.T) { func TestGrantWeeklyLoginPoints_InsufficientLoginDays(t *testing.T) { client := testhelper.NewEntSqliteClient(t) - granter := events.NewPointsGranter(client) + granter := events.NewPointsGranter(client, nil) userID := setupTestData(t, client) ctx := context.Background() @@ -266,7 +266,7 @@ func TestGrantWeeklyLoginPoints_InsufficientLoginDays(t *testing.T) { func TestGrantWeeklyLoginPoints_NoLoginEvents(t *testing.T) { client := testhelper.NewEntSqliteClient(t) - granter := events.NewPointsGranter(client) + granter := events.NewPointsGranter(client, nil) userID := setupTestData(t, client) ctx := context.Background() @@ -311,7 +311,7 @@ func TestGrantWeeklyLoginPoints_MultipleLoginsPerDay(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { client := testhelper.NewEntSqliteClient(t) - granter := events.NewPointsGranter(client) + granter := events.NewPointsGranter(client, nil) userID := setupTestData(t, client) ctx := context.Background() @@ -351,7 +351,7 @@ func TestGrantWeeklyLoginPoints_MultipleLoginsPerDay(t *testing.T) { func TestGrantWeeklyLoginPoints_OldWeeklyPointsRecordExists(t *testing.T) { client := testhelper.NewEntSqliteClient(t) - granter := events.NewPointsGranter(client) + granter := events.NewPointsGranter(client, nil) userID := setupTestData(t, client) ctx := context.Background() @@ -384,13 +384,13 @@ func TestGrantWeeklyLoginPoints_OldWeeklyPointsRecordExists(t *testing.T) { func TestNewPointsGranter(t *testing.T) { client := testhelper.NewEntSqliteClient(t) - granter := events.NewPointsGranter(client) + granter := events.NewPointsGranter(client, nil) require.NotNil(t, granter) } func TestGrantDailyLoginPoints_NonExistentUser(t *testing.T) { client := testhelper.NewEntSqliteClient(t) - granter := events.NewPointsGranter(client) + granter := events.NewPointsGranter(client, nil) ctx := context.Background() nonExistentUserID := 99999 @@ -404,7 +404,7 @@ func TestGrantDailyLoginPoints_NonExistentUser(t *testing.T) { func TestGrantWeeklyLoginPoints_NonExistentUser(t *testing.T) { client := testhelper.NewEntSqliteClient(t) - granter := events.NewPointsGranter(client) + granter := events.NewPointsGranter(client, nil) ctx := context.Background() nonExistentUserID := 99999 @@ -490,7 +490,7 @@ func createSubmitAnswerEvent(t *testing.T, client *ent.Client, userID int, submi func TestGrantFirstAttemptPoints_Success(t *testing.T) { client := testhelper.NewEntSqliteClient(t) - granter := events.NewPointsGranter(client) + granter := events.NewPointsGranter(client, nil) userID := setupTestData(t, client) ctx := context.Background() @@ -519,7 +519,7 @@ func TestGrantFirstAttemptPoints_Success(t *testing.T) { func TestGrantFirstAttemptPoints_AlreadyGranted(t *testing.T) { client := testhelper.NewEntSqliteClient(t) - granter := events.NewPointsGranter(client) + granter := events.NewPointsGranter(client, nil) userID := setupTestData(t, client) ctx := context.Background() @@ -551,7 +551,7 @@ func TestGrantFirstAttemptPoints_AlreadyGranted(t *testing.T) { func TestGrantFirstAttemptPoints_SecondSubmission(t *testing.T) { client := testhelper.NewEntSqliteClient(t) - granter := events.NewPointsGranter(client) + granter := events.NewPointsGranter(client, nil) userID := setupTestData(t, client) ctx := context.Background() @@ -579,7 +579,7 @@ func TestGrantFirstAttemptPoints_SecondSubmission(t *testing.T) { func TestGrantDailyAttemptPoints_Success(t *testing.T) { client := testhelper.NewEntSqliteClient(t) - granter := events.NewPointsGranter(client) + granter := events.NewPointsGranter(client, nil) userID := setupTestData(t, client) ctx := context.Background() @@ -609,7 +609,7 @@ func TestGrantDailyAttemptPoints_Success(t *testing.T) { func TestGrantDailyAttemptPoints_AlreadyGrantedToday(t *testing.T) { client := testhelper.NewEntSqliteClient(t) - granter := events.NewPointsGranter(client) + granter := events.NewPointsGranter(client, nil) userID := setupTestData(t, client) ctx := context.Background() @@ -641,7 +641,7 @@ func TestGrantDailyAttemptPoints_AlreadyGrantedToday(t *testing.T) { func TestGrantDailyAttemptPoints_NoSubmissionToday(t *testing.T) { client := testhelper.NewEntSqliteClient(t) - granter := events.NewPointsGranter(client) + granter := events.NewPointsGranter(client, nil) userID := setupTestData(t, client) ctx := context.Background() @@ -670,7 +670,7 @@ func TestGrantDailyAttemptPoints_NoSubmissionToday(t *testing.T) { func TestGrantCorrectAnswerPoints_Success(t *testing.T) { client := testhelper.NewEntSqliteClient(t) - granter := events.NewPointsGranter(client) + granter := events.NewPointsGranter(client, nil) userID := setupTestData(t, client) ctx := context.Background() @@ -698,7 +698,7 @@ func TestGrantCorrectAnswerPoints_Success(t *testing.T) { func TestGrantCorrectAnswerPoints_AlreadyGranted(t *testing.T) { client := testhelper.NewEntSqliteClient(t) - granter := events.NewPointsGranter(client) + granter := events.NewPointsGranter(client, nil) userID := setupTestData(t, client) ctx := context.Background() @@ -730,7 +730,7 @@ func TestGrantCorrectAnswerPoints_AlreadyGranted(t *testing.T) { func TestGrantCorrectAnswerPoints_NoSuccessfulSubmission(t *testing.T) { client := testhelper.NewEntSqliteClient(t) - granter := events.NewPointsGranter(client) + granter := events.NewPointsGranter(client, nil) userID := setupTestData(t, client) ctx := context.Background() @@ -757,7 +757,7 @@ func TestGrantCorrectAnswerPoints_NoSuccessfulSubmission(t *testing.T) { func TestGrantFirstPlacePoints_Success(t *testing.T) { client := testhelper.NewEntSqliteClient(t) - granter := events.NewPointsGranter(client) + granter := events.NewPointsGranter(client, nil) userID := setupTestData(t, client) ctx := context.Background() @@ -785,7 +785,7 @@ func TestGrantFirstPlacePoints_Success(t *testing.T) { func TestGrantFirstPlacePoints_NotFirstPlace(t *testing.T) { client := testhelper.NewEntSqliteClient(t) - granter := events.NewPointsGranter(client) + granter := events.NewPointsGranter(client, nil) ctx := context.Background() now := time.Now() @@ -827,7 +827,7 @@ func TestGrantFirstPlacePoints_NotFirstPlace(t *testing.T) { func TestGrantFirstPlacePoints_AlreadyGranted(t *testing.T) { client := testhelper.NewEntSqliteClient(t) - granter := events.NewPointsGranter(client) + granter := events.NewPointsGranter(client, nil) userID := setupTestData(t, client) ctx := context.Background() @@ -859,7 +859,7 @@ func TestGrantFirstPlacePoints_AlreadyGranted(t *testing.T) { func TestHandleSubmitAnswerEvent_SuccessfulSubmission(t *testing.T) { client := testhelper.NewEntSqliteClient(t) - granter := events.NewPointsGranter(client) + granter := events.NewPointsGranter(client, nil) userID := setupTestData(t, client) ctx := context.Background() @@ -903,7 +903,7 @@ func TestHandleSubmitAnswerEvent_SuccessfulSubmission(t *testing.T) { func TestHandleSubmitAnswerEvent_FailedSubmission(t *testing.T) { client := testhelper.NewEntSqliteClient(t) - granter := events.NewPointsGranter(client) + granter := events.NewPointsGranter(client, nil) userID := setupTestData(t, client) ctx := context.Background() @@ -947,7 +947,7 @@ func TestHandleSubmitAnswerEvent_FailedSubmission(t *testing.T) { func TestHandleSubmitAnswerEvent_SecondAttemptSuccess(t *testing.T) { client := testhelper.NewEntSqliteClient(t) - granter := events.NewPointsGranter(client) + granter := events.NewPointsGranter(client, nil) userID := setupTestData(t, client) ctx := context.Background() @@ -1006,7 +1006,7 @@ func TestHandleSubmitAnswerEvent_SecondAttemptSuccess(t *testing.T) { // TestGrantDailyLoginPoints_MidnightBoundary tests the edge case where events happen around midnight func TestGrantDailyLoginPoints_MidnightBoundary(t *testing.T) { client := testhelper.NewEntSqliteClient(t) - granter := events.NewPointsGranter(client) + granter := events.NewPointsGranter(client, nil) userID := setupTestData(t, client) ctx := context.Background() @@ -1043,7 +1043,7 @@ func TestGrantDailyLoginPoints_MidnightBoundary(t *testing.T) { // TestGrantDailyLoginPoints_SameDayDifferentTimes tests that multiple events on the same day only grant points once func TestGrantDailyLoginPoints_SameDayDifferentTimes(t *testing.T) { client := testhelper.NewEntSqliteClient(t) - granter := events.NewPointsGranter(client) + granter := events.NewPointsGranter(client, nil) userID := setupTestData(t, client) ctx := context.Background() @@ -1082,7 +1082,7 @@ func TestGrantDailyLoginPoints_SameDayDifferentTimes(t *testing.T) { // TestGrantDailyAttemptPoints_MidnightBoundary tests the edge case for daily attempts around midnight func TestGrantDailyAttemptPoints_MidnightBoundary(t *testing.T) { client := testhelper.NewEntSqliteClient(t) - granter := events.NewPointsGranter(client) + granter := events.NewPointsGranter(client, nil) userID := setupTestData(t, client) ctx := context.Background() @@ -1124,7 +1124,7 @@ func TestGrantDailyAttemptPoints_MidnightBoundary(t *testing.T) { // TestGrantWeeklyLoginPoints_MidnightBoundary tests that weekly login properly counts distinct calendar days func TestGrantWeeklyLoginPoints_MidnightBoundary(t *testing.T) { client := testhelper.NewEntSqliteClient(t) - granter := events.NewPointsGranter(client) + granter := events.NewPointsGranter(client, nil) userID := setupTestData(t, client) ctx := context.Background() @@ -1173,7 +1173,7 @@ func TestGrantWeeklyLoginPoints_MidnightBoundary(t *testing.T) { // TestGrantWeeklyLoginPoints_NotEnoughDistinctDays tests that multiple logins on the same day don't count as multiple days func TestGrantWeeklyLoginPoints_NotEnoughDistinctDays(t *testing.T) { client := testhelper.NewEntSqliteClient(t) - granter := events.NewPointsGranter(client) + granter := events.NewPointsGranter(client, nil) userID := setupTestData(t, client) ctx := context.Background() diff --git a/internal/submission/submission_test.go b/internal/submission/submission_test.go index 05596a7..34ce7e8 100644 --- a/internal/submission/submission_test.go +++ b/internal/submission/submission_test.go @@ -68,7 +68,7 @@ func setupTestData(t *testing.T, client *ent.Client) (userID, questionID, databa func TestSubmitAnswer_Success_MatchingAnswer(t *testing.T) { client := testhelper.NewEntSqliteClient(t) - eventService := eventsService.NewEventService(client) + eventService := eventsService.NewEventService(client, nil) sqlRunner := newTestSQLRunner(t) service := submissionService.NewSubmissionService(client, eventService, sqlRunner) @@ -106,7 +106,7 @@ func TestSubmitAnswer_Success_MatchingAnswer(t *testing.T) { func TestSubmitAnswer_Failed_NonMatchingAnswer(t *testing.T) { client := testhelper.NewEntSqliteClient(t) - eventService := eventsService.NewEventService(client) + eventService := eventsService.NewEventService(client, nil) sqlRunner := newTestSQLRunner(t) service := submissionService.NewSubmissionService(client, eventService, sqlRunner) @@ -135,7 +135,7 @@ func TestSubmitAnswer_Failed_NonMatchingAnswer(t *testing.T) { func TestSubmitAnswer_Failed_UserQueryError(t *testing.T) { client := testhelper.NewEntSqliteClient(t) - eventService := eventsService.NewEventService(client) + eventService := eventsService.NewEventService(client, nil) sqlRunner := newTestSQLRunner(t) service := submissionService.NewSubmissionService(client, eventService, sqlRunner) @@ -160,7 +160,7 @@ func TestSubmitAnswer_Failed_UserQueryError(t *testing.T) { func TestSubmitAnswer_Failed_ReferenceQueryError(t *testing.T) { client := testhelper.NewEntSqliteClient(t) - eventService := eventsService.NewEventService(client) + eventService := eventsService.NewEventService(client, nil) sqlRunner := newTestSQLRunner(t) service := submissionService.NewSubmissionService(client, eventService, sqlRunner) @@ -214,7 +214,7 @@ func TestSubmitAnswer_Failed_ReferenceQueryError(t *testing.T) { func TestSubmitAnswer_QuestionNotFound(t *testing.T) { client := testhelper.NewEntSqliteClient(t) - eventService := eventsService.NewEventService(client) + eventService := eventsService.NewEventService(client, nil) sqlRunner := newTestSQLRunner(t) service := submissionService.NewSubmissionService(client, eventService, sqlRunner) @@ -313,7 +313,7 @@ func TestCompareAnswer(t *testing.T) { func TestNewSubmissionService(t *testing.T) { client := testhelper.NewEntSqliteClient(t) - eventService := eventsService.NewEventService(client) + eventService := eventsService.NewEventService(client, nil) sqlRunner := newTestSQLRunner(t) service := submissionService.NewSubmissionService(client, eventService, sqlRunner) @@ -324,7 +324,7 @@ func TestNewSubmissionService(t *testing.T) { func TestSubmitAnswer_Integration_MultipleSubmissions(t *testing.T) { client := testhelper.NewEntSqliteClient(t) - eventService := eventsService.NewEventService(client) + eventService := eventsService.NewEventService(client, nil) sqlRunner := newTestSQLRunner(t) service := submissionService.NewSubmissionService(client, eventService, sqlRunner) @@ -394,7 +394,7 @@ func TestSubmitAnswer_Integration_MultipleSubmissions(t *testing.T) { func TestSubmitAnswer_EventAndSubmissionRecordGeneration(t *testing.T) { client := testhelper.NewEntSqliteClient(t) - eventService := eventsService.NewEventService(client) + eventService := eventsService.NewEventService(client, nil) sqlRunner := newTestSQLRunner(t) service := submissionService.NewSubmissionService(client, eventService, sqlRunner) diff --git a/internal/useraccount/delete_test.go b/internal/useraccount/delete_test.go index 869a4fc..518f652 100644 --- a/internal/useraccount/delete_test.go +++ b/internal/useraccount/delete_test.go @@ -36,7 +36,7 @@ func TestDeleteUser(t *testing.T) { t.Run(tt.name, func(t *testing.T) { client := setupTestDatabase(t) authStorage := newMockAuthStorage() - eventService := events.NewEventService(client) + eventService := events.NewEventService(client, nil) ctx := useraccount.NewContext(client, authStorage, eventService) var userID int @@ -78,7 +78,7 @@ func TestDeleteUser(t *testing.T) { func TestDeleteUser_Integration(t *testing.T) { client := setupTestDatabase(t) authStorage := newMockAuthStorage() - eventService := events.NewEventService(client) + eventService := events.NewEventService(client, nil) ctx := useraccount.NewContext(client, authStorage, eventService) // Get the unverified group diff --git a/internal/useraccount/register_flow_test.go b/internal/useraccount/register_flow_test.go index 3bf717d..b825271 100644 --- a/internal/useraccount/register_flow_test.go +++ b/internal/useraccount/register_flow_test.go @@ -15,7 +15,7 @@ import ( func TestGetOrRegister_NewUser(t *testing.T) { client := setupTestDatabase(t) authStorage := newMockAuthStorage() - eventService := events.NewEventService(client) + eventService := events.NewEventService(client, nil) ctx := useraccount.NewContext(client, authStorage, eventService) context := context.Background() @@ -47,7 +47,7 @@ func TestGetOrRegister_NewUser(t *testing.T) { func TestGetOrRegister_ExistingUser(t *testing.T) { client := setupTestDatabase(t) authStorage := newMockAuthStorage() - eventService := events.NewEventService(client) + eventService := events.NewEventService(client, nil) ctx := useraccount.NewContext(client, authStorage, eventService) context := context.Background() @@ -81,7 +81,7 @@ func TestGetOrRegister_MissingUnverifiedGroup(t *testing.T) { // Create a fresh database without setup client := testhelper.NewEntSqliteClient(t) authStorage := newMockAuthStorage() - eventService := events.NewEventService(client) + eventService := events.NewEventService(client, nil) ctx := useraccount.NewContext(client, authStorage, eventService) context := context.Background() @@ -98,7 +98,7 @@ func TestGetOrRegister_MissingUnverifiedGroup(t *testing.T) { func TestVerify_Success(t *testing.T) { client := setupTestDatabase(t) authStorage := newMockAuthStorage() - eventService := events.NewEventService(client) + eventService := events.NewEventService(client, nil) ctx := useraccount.NewContext(client, authStorage, eventService) context := context.Background() @@ -135,7 +135,7 @@ func TestVerify_Success(t *testing.T) { func TestVerify_UserNotFound(t *testing.T) { client := setupTestDatabase(t) authStorage := newMockAuthStorage() - eventService := events.NewEventService(client) + eventService := events.NewEventService(client, nil) ctx := useraccount.NewContext(client, authStorage, eventService) context := context.Background() @@ -147,7 +147,7 @@ func TestVerify_UserNotFound(t *testing.T) { func TestVerify_UserAlreadyVerified(t *testing.T) { client := setupTestDatabase(t) authStorage := newMockAuthStorage() - eventService := events.NewEventService(client) + eventService := events.NewEventService(client, nil) ctx := useraccount.NewContext(client, authStorage, eventService) context := context.Background() @@ -172,7 +172,7 @@ func TestVerify_MissingNewUserGroup(t *testing.T) { // Create a fresh database without setup client := testhelper.NewEntSqliteClient(t) authStorage := newMockAuthStorage() - eventService := events.NewEventService(client) + eventService := events.NewEventService(client, nil) ctx := useraccount.NewContext(client, authStorage, eventService) context := context.Background() @@ -199,7 +199,7 @@ func TestVerify_MissingNewUserGroup(t *testing.T) { func TestRegistrationFlow_Complete(t *testing.T) { client := setupTestDatabase(t) authStorage := newMockAuthStorage() - eventService := events.NewEventService(client) + eventService := events.NewEventService(client, nil) ctx := useraccount.NewContext(client, authStorage, eventService) context := context.Background() @@ -249,7 +249,7 @@ func TestRegistrationFlow_Complete(t *testing.T) { func TestRegistrationFlow_ExistingUser(t *testing.T) { client := setupTestDatabase(t) authStorage := newMockAuthStorage() - eventService := events.NewEventService(client) + eventService := events.NewEventService(client, nil) ctx := useraccount.NewContext(client, authStorage, eventService) context := context.Background() @@ -291,7 +291,7 @@ func TestRegistrationFlow_ErrorCases(t *testing.T) { // Create a fresh database without setup client := testhelper.NewEntSqliteClient(t) authStorage := newMockAuthStorage() - eventService := events.NewEventService(client) + eventService := events.NewEventService(client, nil) ctx := useraccount.NewContext(client, authStorage, eventService) context := context.Background() @@ -308,7 +308,7 @@ func TestRegistrationFlow_ErrorCases(t *testing.T) { t.Run("verify already verified user", func(t *testing.T) { client := setupTestDatabase(t) authStorage := newMockAuthStorage() - eventService := events.NewEventService(client) + eventService := events.NewEventService(client, nil) ctx := useraccount.NewContext(client, authStorage, eventService) context := context.Background() @@ -332,7 +332,7 @@ func TestRegistrationFlow_ErrorCases(t *testing.T) { t.Run("verify non-existent user", func(t *testing.T) { client := setupTestDatabase(t) authStorage := newMockAuthStorage() - eventService := events.NewEventService(client) + eventService := events.NewEventService(client, nil) ctx := useraccount.NewContext(client, authStorage, eventService) context := context.Background() diff --git a/internal/useraccount/token_test.go b/internal/useraccount/token_test.go index f5dfb1d..b913ea8 100644 --- a/internal/useraccount/token_test.go +++ b/internal/useraccount/token_test.go @@ -18,7 +18,7 @@ import ( func TestGrantToken_Success(t *testing.T) { client := setupTestDatabase(t) authStorage := newMockAuthStorage() - eventService := events_pkg.NewEventService(client) + eventService := events_pkg.NewEventService(client, nil) ctx := useraccount.NewContext(client, authStorage, eventService) context := context.Background() @@ -55,7 +55,7 @@ func TestGrantToken_Success(t *testing.T) { func TestGrantToken_Impersonation(t *testing.T) { client := setupTestDatabase(t) authStorage := newMockAuthStorage() - eventService := events_pkg.NewEventService(client) + eventService := events_pkg.NewEventService(client, nil) ctx := useraccount.NewContext(client, authStorage, eventService) context := context.Background() @@ -90,7 +90,7 @@ func TestGrantToken_Impersonation(t *testing.T) { func TestGrantToken_DefaultFlow(t *testing.T) { client := setupTestDatabase(t) authStorage := newMockAuthStorage() - eventService := events_pkg.NewEventService(client) + eventService := events_pkg.NewEventService(client, nil) ctx := useraccount.NewContext(client, authStorage, eventService) context := context.Background() @@ -119,7 +119,7 @@ func TestGrantToken_DefaultFlow(t *testing.T) { func TestGrantToken_NewUserScopes(t *testing.T) { client := setupTestDatabase(t) authStorage := newMockAuthStorage() - eventService := events_pkg.NewEventService(client) + eventService := events_pkg.NewEventService(client, nil) ctx := useraccount.NewContext(client, authStorage, eventService) context := context.Background() @@ -153,7 +153,7 @@ func TestGrantToken_NewUserScopes(t *testing.T) { func TestGrantToken_UserWithoutScopeSet(t *testing.T) { client := setupTestDatabase(t) authStorage := newMockAuthStorage() - eventService := events_pkg.NewEventService(client) + eventService := events_pkg.NewEventService(client, nil) ctx := useraccount.NewContext(client, authStorage, eventService) context := context.Background() @@ -187,7 +187,7 @@ func TestGrantToken_UserWithoutScopeSet(t *testing.T) { func TestRevokeToken_Success(t *testing.T) { client := setupTestDatabase(t) authStorage := newMockAuthStorage() - eventService := events_pkg.NewEventService(client) + eventService := events_pkg.NewEventService(client, nil) ctx := useraccount.NewContext(client, authStorage, eventService) context := context.Background() @@ -219,7 +219,7 @@ func TestRevokeToken_Success(t *testing.T) { func TestRevokeAllTokens_Success(t *testing.T) { client := setupTestDatabase(t) authStorage := newMockAuthStorage() - eventService := events_pkg.NewEventService(client) + eventService := events_pkg.NewEventService(client, nil) ctx := useraccount.NewContext(client, authStorage, eventService) context := context.Background() @@ -262,7 +262,7 @@ func TestRevokeAllTokens_Success(t *testing.T) { func TestGrantToken_LoginEventTriggered(t *testing.T) { client := setupTestDatabase(t) authStorage := newMockAuthStorage() - eventService := events_pkg.NewEventService(client) + eventService := events_pkg.NewEventService(client, nil) ctx := useraccount.NewContext(client, authStorage, eventService) context := context.Background() @@ -305,7 +305,7 @@ func TestGrantToken_LoginEventTriggered(t *testing.T) { func TestGrantToken_ImpersonationEventTriggered(t *testing.T) { client := setupTestDatabase(t) authStorage := newMockAuthStorage() - eventService := events_pkg.NewEventService(client) + eventService := events_pkg.NewEventService(client, nil) ctx := useraccount.NewContext(client, authStorage, eventService) context := context.Background() @@ -369,7 +369,7 @@ func TestGrantToken_ImpersonationEventTriggered(t *testing.T) { func TestGrantToken_MultipleTokensCreateMultipleEvents(t *testing.T) { client := setupTestDatabase(t) authStorage := newMockAuthStorage() - eventService := events_pkg.NewEventService(client) + eventService := events_pkg.NewEventService(client, nil) ctx := useraccount.NewContext(client, authStorage, eventService) context := context.Background() @@ -437,7 +437,7 @@ func TestGrantToken_MultipleTokensCreateMultipleEvents(t *testing.T) { func TestRevokeToken_LogoutEventTriggered(t *testing.T) { client := setupTestDatabase(t) authStorage := newMockAuthStorage() - eventService := events_pkg.NewEventService(client) + eventService := events_pkg.NewEventService(client, nil) ctx := useraccount.NewContext(client, authStorage, eventService) context := context.Background() @@ -482,7 +482,7 @@ func TestRevokeToken_LogoutEventTriggered(t *testing.T) { func TestRevokeAllTokens_LogoutAllEventTriggered(t *testing.T) { client := setupTestDatabase(t) authStorage := newMockAuthStorage() - eventService := events_pkg.NewEventService(client) + eventService := events_pkg.NewEventService(client, nil) ctx := useraccount.NewContext(client, authStorage, eventService) context := context.Background() diff --git a/internal/useraccount/useraccount_test.go b/internal/useraccount/useraccount_test.go index 1c93357..1211984 100644 --- a/internal/useraccount/useraccount_test.go +++ b/internal/useraccount/useraccount_test.go @@ -72,7 +72,7 @@ func setupTestDatabase(t *testing.T) *ent.Client { func TestNewContext(t *testing.T) { client := setupTestDatabase(t) authStorage := newMockAuthStorage() - eventService := events.NewEventService(client) + eventService := events.NewEventService(client, nil) ctx := useraccount.NewContext(client, authStorage, eventService) require.NotNil(t, ctx) @@ -81,7 +81,7 @@ func TestNewContext(t *testing.T) { func TestGetUser(t *testing.T) { client := setupTestDatabase(t) authStorage := newMockAuthStorage() - eventService := events.NewEventService(client) + eventService := events.NewEventService(client, nil) ctx := useraccount.NewContext(client, authStorage, eventService) context := context.Background()