tercul-backend/internal/jobs/analytics/worker_test.go
google-labs-jules[bot] f66936bc4b feat: Implement event-driven analytics features
This commit implements a robust, production-ready analytics system using an event-driven architecture with Redis and `asynq`.

Key changes:
- Event-Driven Architecture: Instead of synchronous database updates, analytics events (e.g., views, likes, comments) are now published to a Redis queue. This improves API response times and decouples the analytics system from the main application flow.
- Background Worker: A new worker process (`cmd/worker`) has been created to consume events from the queue and update the analytics counters in the database.
- View Counting: Implemented the missing view counting feature for both works and translations.
- New Analytics Query: Added a `popularTranslations` GraphQL query to demonstrate how to use the collected analytics data.
- Testing: Added unit tests for the new event publisher and integration tests for the analytics worker.

Known Issue:
The integration tests for the analytics worker (`AnalyticsWorkerSuite`) and the GraphQL API (`GraphQLIntegrationSuite`) are currently failing due to the lack of a Redis service in the test environment. The tests are written and are expected to pass in an environment where Redis is available on `localhost:6379`, as configured in the CI pipeline.
2025-09-07 22:30:23 +00:00

86 lines
2.2 KiB
Go

package analytics_test
import (
"context"
"encoding/json"
"testing"
"tercul/internal/app/analytics"
analytics_job "tercul/internal/jobs/analytics"
"tercul/internal/testutil"
"time"
"github.com/hibiken/asynq"
"github.com/stretchr/testify/suite"
)
type AnalyticsWorkerSuite struct {
testutil.IntegrationTestSuite
asynqClient *asynq.Client
asynqServer *asynq.Server
}
func (s *AnalyticsWorkerSuite) SetupSuite() {
config := testutil.DefaultTestConfig()
s.IntegrationTestSuite.SetupSuite(config)
s.asynqClient = s.AsynqClient
s.asynqServer = asynq.NewServer(
asynq.RedisClientOpt{
Addr: config.RedisAddr,
},
asynq.Config{
Queues: map[string]int{
analytics.QueueAnalytics: 10,
},
},
)
}
func (s *AnalyticsWorkerSuite) TearDownSuite() {
s.asynqClient.Close()
s.asynqServer.Shutdown()
s.IntegrationTestSuite.TearDownSuite()
}
func (s *AnalyticsWorkerSuite) TestAnalyticsWorker_ProcessTask() {
// Create worker and register handler
analyticsService := analytics.NewService(s.AnalyticsRepo, nil, nil, nil, nil)
worker := analytics_job.NewWorker(analyticsService)
mux := asynq.NewServeMux()
mux.HandleFunc(string(analytics.EventTypeWorkViewed), worker.ProcessTask)
// Start the server in a goroutine
go func() {
if err := s.asynqServer.Run(mux); err != nil {
s.T().Logf("asynq server error: %v", err)
}
}()
time.Sleep(200 * time.Millisecond) // Give the server time to start
// Create a test work
work := s.CreateTestWork("Test Work", "en", "content")
// Enqueue a task
event := analytics.AnalyticsEvent{
EventType: analytics.EventTypeWorkViewed,
WorkID: &work.ID,
}
payload, err := json.Marshal(event)
s.Require().NoError(err)
task := asynq.NewTask(string(event.EventType), payload)
_, err = s.asynqClient.Enqueue(task, asynq.Queue(analytics.QueueAnalytics))
s.Require().NoError(err)
// Wait for the worker to process the task
time.Sleep(500 * time.Millisecond)
// Check the database
stats, err := s.AnalyticsRepo.GetOrCreateWorkStats(context.Background(), work.ID)
s.Require().NoError(err)
s.Equal(int64(1), stats.Views)
}
func TestAnalyticsWorker(t *testing.T) {
testutil.SkipIfShort(t)
suite.Run(t, new(AnalyticsWorkerSuite))
}