diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 42d6aed..91426e9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,6 +56,12 @@ jobs: - name: Verify dependencies run: go mod verify + + - name: Validate Go packages + run: go run ./deployment/ci/validation_pkg/main.go + + - name: Check mocks + run: make check-mocks - name: Generate mocks run: make generate-mocks diff --git a/Makefile b/Makefile index c5a2825..1a582bb 100644 --- a/Makefile +++ b/Makefile @@ -19,9 +19,20 @@ NC=\033[0m # No Color all: help # Executar todos os checks do CI localmente -ci: lint security test build clean +ci: validate-pkg check-mocks test build lint security clean @echo "$(GREEN)✅ Todos os checks do CI passaram com sucesso!$(NC)" +# Gera mocks +generate-mocks: install-mockgen clean-mocks + go run deployment/ci/gen_mocks/main.go + @echo "$(GREEN)Mocks gerados com sucesso!$(NC)" + +# Valida pacotes Go +validate-pkg: + @echo "$(GREEN)Validando pacotes Go...$(NC)" + $(GO) run ./deployment/ci/validation_pkg/main.go + @echo "$(GREEN)Validação dos pacotes Go finalizada!$(NC)" + # Construir o aplicativo build: @echo "$(GREEN)Construindo aplicação...$(NC)" @@ -32,20 +43,25 @@ load-test: @echo "$(YELLOW)Executando teste de carga...$(NC)" @$(GO) run ./cmd/loadtest/loadtest.go -# Iniciar worker -run-worker: - @echo "$(BLUE)Iniciando serviço worker...$(NC)" - @$(GO) run ./cmd/api/main.go --service=worker +# Iniciar pubsub (API) +run-pubsub: + @echo "$(BLUE)Iniciando serviço pubsub (API)...$(NC)" + @$(GO) run ./cmd/api/main.go --scope=pubsub + +# Iniciar task (API) +run-task: + @echo "$(BLUE)Iniciando serviço task (API)...$(NC)" + @$(GO) run ./cmd/api/main.go --scope=task -# Iniciar webhook (API) -run-webhook: - @echo "$(BLUE)Iniciando serviço webhook (API)...$(NC)" - @$(GO) run ./cmd/api/main.go --service=webhook +# Iniciar backoffice (API) +run-backoffice: + @echo "$(BLUE)Iniciando serviço backoffice (API)...$(NC)" + @$(GO) run ./cmd/api/main.go --scope=backoffice # Iniciar ambos serviços run-all: @echo "$(BLUE)Iniciando todos os serviços (worker e webhook)...$(NC)" - @$(GO) run ./cmd/api/main.go --service=all + @$(GO) run ./cmd/api/main.go --scope=all # Limpar binários gerados clean: @@ -201,90 +217,22 @@ install-mockgen: @$(GO) install go.uber.org/mock/mockgen@latest generate-mocks: install-mockgen clean-mocks - @echo "$(GREEN)Gerando mocks...$(NC)" - @echo "$(BLUE)Gerando mock para DeadLetter...$(NC)" - @$(MOCKGEN) -source=internal/wtrhandler/deadletter_asynq_handle.go -destination=internal/wtrhandler/deadletter_mock.go -package=wtrhandler DeadLetterStore - @echo "$(BLUE)Gerando mock para Fetcher...$(NC)" - @$(MOCKGEN) -source=internal/wtrhandler/request_handle_asynq.go -destination=internal/wtrhandler/fetcher_mock.go -package=wtrhandler Fetcher - @echo "$(BLUE)Gerando mock para Publisher...$(NC)" - @$(MOCKGEN) -source=pkg/pubadapter/adapter.go -destination=pkg/publisher/publisher_task_mock.go -package=publisher - @echo "$(BLUE)Gerando mock para Publisher em pubadapter...$(NC)" - @$(MOCKGEN) -source=pkg/pubadapter/adapter.go -destination=pkg/pubadapter/publisher_task_mock.go -package=pubadapter - @echo "$(BLUE)Gerando mock para PublisherInsights...$(NC)" - # TODO: está sendo criado junto com outro por estar no mesmo arquivo, separar interfaces em arquivos diferentes - # @$(MOCKGEN) -source=tmp/publisher_insights.go -destination=internal/wtrhandler/mock_publisher_insights.go -package=wtrhandler - @echo "$(BLUE)Gerando mock para ConsumerInsights...$(NC)" - # TODO: está sendo criado junto com outro por estar no mesmo arquivo, separar interfaces em arquivos diferentes - # @$(MOCKGEN) -source=tmp/consumer_insights.go -destination=internal/wtrhandler/mock_consumer_insights.go -package=wtrhandler + @go run deployment/ci/gen_mocks/main.go @echo "$(GREEN)Mocks gerados com sucesso!$(NC)" update-mocks: generate-mocks + @$(MAKE) clean-mocks + @$(MAKE) generate-mocks @echo "$(GREEN)Mocks atualizados!$(NC)" check-mocks: - @echo "$(GREEN)Verificando se os mocks existem...$(NC)" - @if [ ! -f "internal/wtrhandler/repository_mock.go" ]; then \ - echo "$(YELLOW)⚠️ Repository mock não encontrado!$(NC)"; \ - echo "Execute 'make generate-mocks' para gerar os mocks"; \ - exit 1; \ - fi - @if [ ! -f "internal/wtrhandler/deadletter_mock.go" ]; then \ - echo "$(YELLOW)⚠️ DeadLetter mock não encontrado!$(NC)"; \ - echo "Execute 'make generate-mocks' para gerar os mocks"; \ - exit 1; \ - fi - @if [ ! -f "internal/wtrhandler/fetcher_mock.go" ]; then \ - echo "$(YELLOW)⚠️ Fetcher mock não encontrado!$(NC)"; \ - echo "Execute 'make generate-mocks' para gerar os mocks"; \ - exit 1; \ - fi - @if [ ! -f "pkg/cachemanager/cache_mock.go" ]; then \ - echo "$(YELLOW)⚠️ Cache mock não encontrado!$(NC)"; \ - echo "Execute 'make generate-mocks' para gerar os mocks"; \ - exit 1; \ - fi - @if [ ! -f "pkg/publisher/publisher_task_mock.go" ]; then \ - echo "$(YELLOW)⚠️ Publisher mock não encontrado!$(NC)"; \ - echo "Execute 'make generate-mocks' para gerar os mocks"; \ - exit 1; \ - fi - @if [ ! -f "pkg/pubadapter/publisher_task_mock.go" ]; then \ - echo "$(YELLOW)⚠️ Publisher mock em pubadapter não encontrado!$(NC)"; \ - echo "Execute 'make generate-mocks' para gerar os mocks"; \ - exit 1; \ - fi - # TODO: Descomentar quando os mocks de insights forem implementados - # @if [ ! -f "internal/wtrhandler/mock_publisher_insights.go" ]; then \ - # echo "$(YELLOW)⚠️ PublisherInsights mock não encontrado!$(NC)"; \ - # echo "Execute 'make generate-mocks' para gerar os mocks"; \ - # exit 1; \ - # fi - # @if [ ! -f "internal/wtrhandler/mock_consumer_insights.go" ]; then \ - # echo "$(YELLOW)⚠️ ConsumerInsights mock não encontrado!$(NC)"; \ - # echo "Execute 'make generate-mocks' para gerar os mocks"; \ - # exit 1; \ - # fi - @echo "$(GREEN)✅ Todos os mocks existem!$(NC)" + @echo "$(GREEN)Verificando se todas as interfaces possuem mocks...$(NC)" + @sh ./deployment/ci/validate_mock.sh @echo "$(BLUE)💡 Para regenerar todos os mocks, execute: make update-mocks$(NC)" clean-mocks: - @echo "$(GREEN)Removendo mocks...$(NC)" - @echo "$(BLUE)Removendo Repository mock...$(NC)" - @rm -f internal/wtrhandler/repository_mock.go - @echo "$(BLUE)Removendo DeadLetter mock...$(NC)" - @rm -f internal/wtrhandler/deadletter_mock.go - @echo "$(BLUE)Removendo Fetcher mock...$(NC)" - @rm -f internal/wtrhandler/fetcher_mock.go - @echo "$(BLUE)Removendo Cache mock...$(NC)" - @rm -f pkg/cachemanager/cache_mock.go - @echo "$(BLUE)Removendo Publisher mock...$(NC)" - @rm -f pkg/publisher/publisher_task_mock.go - @echo "$(BLUE)Removendo Publisher mock em pubadapter...$(NC)" - @rm -f pkg/pubadapter/publisher_task_mock.go - @echo "$(BLUE)Removendo PublisherInsights mock...$(NC)" - @rm -f internal/wtrhandler/mock_publisher_insights.go - @echo "$(BLUE)Removendo ConsumerInsights mock...$(NC)" - @rm -f internal/wtrhandler/mock_consumer_insights.go + @find . -type f -name '*_mock.go' -exec rm -f {} \; + @rm -rf ./mocks/** @echo "$(GREEN)Mocks removidos com sucesso!$(NC)" @echo "$(BLUE)💡 Para gerar novos mocks, execute: make generate-mocks$(NC)" diff --git a/cmd/api/main.go b/cmd/api/main.go index fe2d7f1..7abb3e5 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -10,25 +10,18 @@ import ( "syscall" "time" - "cloud.google.com/go/pubsub" - vkit "cloud.google.com/go/pubsub/apiv1" - "github.com/IsaacDSC/gqueue/internal/cfg" - "github.com/IsaacDSC/gqueue/internal/domain" - "github.com/IsaacDSC/gqueue/internal/storests" - "github.com/googleapis/gax-go/v2" - "google.golang.org/grpc/codes" - - "github.com/hibiken/asynq" - - "github.com/IsaacDSC/gqueue/cmd/setup" - "github.com/IsaacDSC/gqueue/cmd/setup/api" "github.com/IsaacDSC/gqueue/cmd/setup/backoffice" + "github.com/IsaacDSC/gqueue/cmd/setup/pubsub" + "github.com/IsaacDSC/gqueue/cmd/setup/task" + "github.com/IsaacDSC/gqueue/internal/cfg" + "github.com/IsaacDSC/gqueue/internal/fetcher" "github.com/IsaacDSC/gqueue/internal/interstore" + "github.com/IsaacDSC/gqueue/internal/storests" "github.com/redis/go-redis/v9" ) // waitForShutdown waits for SIGINT/SIGTERM and gracefully shuts down the provided servers. -func waitForShutdown(apiServer, backofficeServer *http.Server) { +func waitForShutdown(server *http.Server) { quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit @@ -38,14 +31,8 @@ func waitForShutdown(apiServer, backofficeServer *http.Server) { shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() - if apiServer != nil { - if err := apiServer.Shutdown(shutdownCtx); err != nil { - log.Printf("API server shutdown error: %v", err) - } - } - - if backofficeServer != nil { - if err := backofficeServer.Shutdown(shutdownCtx); err != nil { + if server != nil { + if err := server.Shutdown(shutdownCtx); err != nil { log.Printf("Backoffice server shutdown error: %v", err) } } @@ -53,110 +40,77 @@ func waitForShutdown(apiServer, backofficeServer *http.Server) { log.Println("Server shutdown complete") } -// TODO: rename to --scope=... -// go run . --service=all -// go run . --service=backoffice -// go run . --service=api -// go run . --service=archived-notification +// go run . --scope=all +// go run . --scope=backoffice +// go run . --scope=pubsub +// go run . --scope=task func main() { conf := cfg.Get() ctx := context.Background() - asynqClient := asynq.NewClient(asynq.RedisClientOpt{Addr: conf.Cache.CacheAddr}) - defer asynqClient.Close() - - // var highPerformancePublisher pubadapter.GenericPublisher - var pubsubClient *pubsub.Client - - if conf.WQ == cfg.WQGooglePubSub { - config := &pubsub.ClientConfig{ - PublisherCallOptions: &vkit.PublisherCallOptions{ - Publish: []gax.CallOption{ - gax.WithRetry(func() gax.Retryer { - return gax.OnCodes([]codes.Code{ - codes.Aborted, - codes.Canceled, - codes.Internal, - codes.ResourceExhausted, - codes.Unknown, - codes.Unavailable, - codes.DeadlineExceeded, - }, gax.Backoff{ - Initial: 250 * time.Millisecond, // default 100 milliseconds - Max: 5 * time.Second, // default 60 seconds - Multiplier: 1.45, // default 1.3 - }) - }), - }, - }, - } - - clientPubsub, err := pubsub.NewClientWithConfig(ctx, domain.ProjectID, config) - if err != nil { - log.Fatalf("Erro ao criar cliente: %v", err) - } - - pubsubClient = clientPubsub - - defer clientPubsub.Close() - } + scope := flag.String("scope", "all", "service to run") + flag.Parse() - cacheClient := redis.NewClient(&redis.Options{Addr: conf.Cache.CacheAddr}) - if err := cacheClient.Ping(ctx).Err(); err != nil { + redisClient := redis.NewClient(&redis.Options{Addr: conf.Cache.CacheAddr}) + if err := redisClient.Ping(ctx).Err(); err != nil { panic(err) } - storeInsights := storests.NewStore(cacheClient) + storeInsights := storests.NewStore(redisClient) store, err := interstore.NewPostgresStoreFromDSN(conf.ConfigDatabase.DbConn) if err != nil { panic(err) } - service := flag.String("service", "all", "service to run") - flag.Parse() - - if *service == "api" { - apiServer := api.Start( - ctx, + var servers []*http.Server + if scopeOrAll(*scope, "backoffice") { + backofficeServer := backoffice.Start( + redisClient, store, - asynqClient, - pubsubClient, storeInsights, ) - waitForShutdown(apiServer, nil) - return + servers = append(servers, backofficeServer) } - if *service == "backoffice" { - backofficeServer := backoffice.Start( - cacheClient, - store, - storeInsights, + var memStore *interstore.MemStore + var fetch *fetcher.Notification + // task and pubsub share some dependencies, so we initialize them here and pass to both services + if *scope == "pubsub" || *scope == "task" || *scope == "all" { + memStore = interstore.NewMemStore(store) + fetch = fetcher.NewNotification() + } + + if scopeOrAll(*scope, "pubsub") { + s := pubsub.New( + store, memStore, fetch, storeInsights, ) - waitForShutdown(nil, backofficeServer) - return + + s.Start(ctx, conf) + + defer s.Close() + + servers = append(servers, s.Server()) } - // TODO: adicionar graceful shutdown - if *service == "archived-notification" { - setup.StartArchivedNotify(ctx, store, cacheClient) - return + if scopeOrAll(*scope, "task") { + s := task.New( + store, memStore, fetch, storeInsights, + ) + + s.Start(ctx, conf) + + defer s.Close() + + servers = append(servers, s.Server()) } - backofficeServer := backoffice.Start( - cacheClient, - store, - storeInsights, - ) - - apiServer := api.Start( - ctx, - store, - asynqClient, - pubsubClient, - storeInsights, - ) - - waitForShutdown(apiServer, backofficeServer) + for _, server := range servers { + waitForShutdown(server) + } + +} + +func scopeOrAll(scope, expected string) bool { + return scope == "all" || scope == expected } diff --git a/cmd/setup/api/base.go b/cmd/setup/api/base.go deleted file mode 100644 index 94faff2..0000000 --- a/cmd/setup/api/base.go +++ /dev/null @@ -1,96 +0,0 @@ -package api - -import ( - "context" - "log" - "net/http" - - "cloud.google.com/go/pubsub" - "github.com/IsaacDSC/gqueue/cmd/setup/middleware" - "github.com/IsaacDSC/gqueue/internal/backoffice" - "github.com/IsaacDSC/gqueue/internal/cfg" - "github.com/IsaacDSC/gqueue/internal/domain" - "github.com/IsaacDSC/gqueue/internal/fetcher" - "github.com/IsaacDSC/gqueue/internal/storests" - "github.com/IsaacDSC/gqueue/internal/wtrhandler" - "github.com/IsaacDSC/gqueue/pkg/httpadapter" - "github.com/IsaacDSC/gqueue/pkg/pubadapter" - "github.com/hibiken/asynq" -) - -type PersistentRepository interface { - GetAllEvents(ctx context.Context) ([]domain.Event, error) - GetAllSchedulers(ctx context.Context, state string) ([]domain.Event, error) -} - -func Start( - ctx context.Context, - store PersistentRepository, - asynqClient *asynq.Client, - gcppubsubClient *pubsub.Client, - insightsStore *storests.Store, -) *http.Server { - fetch := fetcher.NewNotification() - - memStore := loadInMemStore(store) - - classificationResult := pubadapter.ClassificationPublisher( - pubadapter.NewPubSubGoogle(gcppubsubClient), - pubadapter.NewPublisher(asynqClient), - ) - - adaptpub := pubadapter.NewStrategy(&classificationResult) - - if gcppubsubClient != nil { - go startUsingGooglePubSub( - memStore, - gcppubsubClient, - adaptpub, - fetch, insightsStore, - ) - } - - if asynqClient != nil { - go startUsingAsynq(memStore, adaptpub, fetch, insightsStore) - } - - StartTaskSyncMemStore(ctx, store, memStore) - - mux := http.NewServeMux() - - routes := []httpadapter.HttpHandle{ - backoffice.GetHealthCheckHandler(), - wtrhandler.PublisherEvent(memStore, adaptpub, insightsStore), - } - - for _, route := range routes { - mux.HandleFunc(route.Path, route.Handler) - } - - // config := cfg.Get() - - // authorization := auth.NewBasicAuth(map[string]string{ - // config.ProjectID: config.SecretKey, - // }) - - handler := middleware.CORSMiddleware(middleware.LoggerMiddleware(mux)) - // h := authorization.Middleware(handler.ServeHTTP) - - env := cfg.Get() - port := env.ApiPort - - server := &http.Server{ - Addr: port.String(), - Handler: handler, - } - - log.Printf("Starting API server on :%d", port) - - go func() { - if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { - log.Printf("API server error: %v", err) - } - }() - - return server -} diff --git a/cmd/setup/api/sync_mem_store.go b/cmd/setup/api/sync_mem_store.go deleted file mode 100644 index 2cfb5e0..0000000 --- a/cmd/setup/api/sync_mem_store.go +++ /dev/null @@ -1,56 +0,0 @@ -package api - -import ( - "context" - "log" - "time" - - "github.com/IsaacDSC/gqueue/internal/interstore" - "github.com/IsaacDSC/gqueue/pkg/ctxlogger" -) - -func StartTaskSyncMemStore(ctx context.Context, store PersistentRepository, memStore *interstore.MemStore) { - l := ctxlogger.GetLogger(ctx) - - // refresh events in memory store every minute - go func() { - l.Info("Starting task to sync mem store with persistent store") - trigger := time.NewTicker(time.Minute) - for { - select { - case <-trigger.C: - events, err := store.GetAllEvents(ctx) - if err != nil { - l.Warn("error fetching events from persistent store", "error", err) - continue - } - - memStore.Refresh(ctx, events) - case <-ctx.Done(): - trigger.Stop() - return - } - } - }() - - // refresh schedulers in memory store every 5 minutes - go func() { - log.Println("Starting task to sync mem store with persistent store for schedulers") - trigger := time.NewTicker(5 * time.Minute) - for { - select { - case <-trigger.C: - events, err := store.GetAllSchedulers(ctx, "archived") - if err != nil { - continue - } - - memStore.RefreshRetryTopics(ctx, events) - case <-ctx.Done(): - trigger.Stop() - return - } - } - }() - -} diff --git a/cmd/setup/archived_notification.go b/cmd/setup/archived_notification.go deleted file mode 100644 index daa9404..0000000 --- a/cmd/setup/archived_notification.go +++ /dev/null @@ -1,24 +0,0 @@ -package setup - -import ( - "context" - "time" - - "github.com/IsaacDSC/gqueue/internal/asynqstore" - "github.com/IsaacDSC/gqueue/internal/asynqtask" - "github.com/IsaacDSC/gqueue/internal/fetcher" - "github.com/IsaacDSC/gqueue/internal/interstore" - "github.com/redis/go-redis/v9" -) - -func StartArchivedNotify(ctx context.Context, store interstore.Repository, cache *redis.Client) { - cacheManager := asynqstore.NewCache(cache) - fetch := fetcher.NewNotification() - svc := asynqtask.NewTaskManager(store, cacheManager, fetch) - - for { - svc.NotifyListeners(ctx) - time.Sleep(30 * time.Second) - } - -} diff --git a/cmd/setup/backoffice/httpserver.go b/cmd/setup/backoffice/httpserver.go index b2476fc..b24916a 100644 --- a/cmd/setup/backoffice/httpserver.go +++ b/cmd/setup/backoffice/httpserver.go @@ -6,7 +6,8 @@ import ( "net/http" "github.com/IsaacDSC/gqueue/cmd/setup/middleware" - "github.com/IsaacDSC/gqueue/internal/backoffice" + "github.com/IsaacDSC/gqueue/internal/app/backofficeapp" + "github.com/IsaacDSC/gqueue/internal/app/health" "github.com/IsaacDSC/gqueue/internal/cfg" "github.com/IsaacDSC/gqueue/internal/domain" "github.com/IsaacDSC/gqueue/internal/interstore" @@ -26,13 +27,13 @@ func Start( mux := http.NewServeMux() routes := []httpadapter.HttpHandle{ - backoffice.GetHealthCheckHandler(), - backoffice.PatchConsumer(store), - backoffice.GetEvent(store), - backoffice.GetEvents(store), - backoffice.GetRegisterTaskConsumerArchived(store), - backoffice.RemoveEvent(store), - backoffice.GetInsightsHandle(insightsStore), + health.GetHealthCheckHandler(), + backofficeapp.PatchConsumer(store), + backofficeapp.GetEvent(store), + backofficeapp.GetEvents(store), + backofficeapp.GetRegisterTaskConsumerArchived(store), + backofficeapp.RemoveEvent(store), + backofficeapp.GetInsightsHandle(insightsStore), } for _, route := range routes { @@ -49,14 +50,14 @@ func Start( // h := authorization.Middleware(handler.ServeHTTP) env := cfg.Get() - port := env.BackofficePort + port := env.BackofficeApiPort server := &http.Server{ Addr: port.String(), Handler: handler, } - log.Printf("Starting Backoffice server on :%d", port) + log.Printf("[*] Starting Backoffice server on :%d", port) go func() { if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { diff --git a/cmd/setup/pubsub/base.go b/cmd/setup/pubsub/base.go new file mode 100644 index 0000000..fbc9580 --- /dev/null +++ b/cmd/setup/pubsub/base.go @@ -0,0 +1,98 @@ +package pubsub + +import ( + "context" + "log" + "net/http" + "time" + + "cloud.google.com/go/pubsub" + vkit "cloud.google.com/go/pubsub/apiv1" + "github.com/IsaacDSC/gqueue/internal/cfg" + "github.com/IsaacDSC/gqueue/internal/domain" + "github.com/IsaacDSC/gqueue/internal/fetcher" + "github.com/IsaacDSC/gqueue/internal/interstore" + "github.com/IsaacDSC/gqueue/internal/storests" + "github.com/IsaacDSC/gqueue/pkg/pubadapter" + "github.com/googleapis/gax-go/v2" + "google.golang.org/grpc/codes" +) + +type PersistentRepository interface { + GetAllEvents(ctx context.Context) ([]domain.Event, error) + GetAllSchedulers(ctx context.Context, state string) ([]domain.Event, error) +} + +type Service struct { + pubsubClient *pubsub.Client + gcppublisher pubadapter.GenericPublisher + server *http.Server + // injectable dependencies + persistentStore PersistentRepository + memStore *interstore.MemStore + fetch *fetcher.Notification + insightsStore *storests.Store +} + +func New( + ps PersistentRepository, + ms *interstore.MemStore, + fetch *fetcher.Notification, + insightsStore *storests.Store, +) *Service { + return &Service{ + persistentStore: ps, + memStore: ms, + fetch: fetch, + insightsStore: insightsStore, + } +} + +func (s *Service) Start(ctx context.Context, env cfg.Config) { + config := &pubsub.ClientConfig{ + PublisherCallOptions: &vkit.PublisherCallOptions{ + Publish: []gax.CallOption{ + gax.WithRetry(func() gax.Retryer { + return gax.OnCodes([]codes.Code{ + codes.Aborted, + codes.Canceled, + codes.Internal, + codes.ResourceExhausted, + codes.Unknown, + codes.Unavailable, + codes.DeadlineExceeded, + }, gax.Backoff{ + Initial: 250 * time.Millisecond, // default 100 milliseconds + Max: 5 * time.Second, // default 60 seconds + Multiplier: 1.45, // default 1.3 + }) + }), + }, + }, + } + + clientPubsub, err := pubsub.NewClientWithConfig(ctx, domain.ProjectID, config) + if err != nil { + log.Fatalf("Erro ao criar cliente: %v", err) + } + + s.pubsubClient = clientPubsub + + // setup publisher + s.gcppublisher = pubadapter.NewPubSubGoogle(s.pubsubClient) + + // setup consumer depends on publisher + go s.consumer(ctx, env) + + // load mem store with events from persistent store + s.memStore.LoadInMemStore(ctx) + + // task refresh mem store + go s.syncMemStore(ctx) + + s.server = s.startHttpServer(ctx, env) +} + +func (s *Service) Close() { _ = s.pubsubClient.Close() } + +func (s *Service) Server() *http.Server { return s.server } diff --git a/cmd/setup/api/worker.go b/cmd/setup/pubsub/consumer.go similarity index 51% rename from cmd/setup/api/worker.go rename to cmd/setup/pubsub/consumer.go index cad252f..d7ff955 100644 --- a/cmd/setup/api/worker.go +++ b/cmd/setup/pubsub/consumer.go @@ -1,8 +1,7 @@ -package api +package pubsub import ( "context" - "errors" "log" "os" "os/signal" @@ -11,59 +10,25 @@ import ( "time" "cloud.google.com/go/pubsub" - "github.com/IsaacDSC/gqueue/cmd/setup/middleware" + "github.com/IsaacDSC/gqueue/internal/app/pubsubapp" "github.com/IsaacDSC/gqueue/internal/cfg" "github.com/IsaacDSC/gqueue/internal/domain" - "github.com/IsaacDSC/gqueue/internal/fetcher" - "github.com/IsaacDSC/gqueue/internal/storests" - "github.com/IsaacDSC/gqueue/pkg/asynqsvc" "github.com/IsaacDSC/gqueue/pkg/gpubsub" "github.com/IsaacDSC/gqueue/pkg/topicutils" - - "github.com/IsaacDSC/gqueue/internal/wtrhandler" - "github.com/IsaacDSC/gqueue/pkg/pubadapter" - - "github.com/IsaacDSC/gqueue/internal/interstore" - - "github.com/hibiken/asynq" ) -func loadInMemStore(store PersistentRepository) *interstore.MemStore { - memStore := interstore.NewMemStore() - - events, err := store.GetAllEvents(context.Background()) - if err != nil && !errors.Is(err, domain.EventNotFound) { - log.Printf("[!] Error loading events into in-memory store: %v", err) - return memStore - } - - memStore.Refresh(context.Background(), events) - - return memStore -} - -func startUsingGooglePubSub( - store *interstore.MemStore, - clientPubsub *pubsub.Client, - gcppublisher pubadapter.GenericPublisher, - fetch *fetcher.Notification, - insightsStore *storests.Store, -) { - ctx := context.Background() - +func (s *Service) consumer(ctx context.Context, env cfg.Config) { sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, os.Interrupt) ctx, cancel := context.WithCancel(ctx) defer cancel() - cfg := cfg.Get() - - concurrency := cfg.AsynqConfig.Concurrency + concurrency := env.AsynqConfig.Concurrency handlers := []gpubsub.Handle{ - wtrhandler.NewDeadLatterQueue(store, fetch).ToGPubSubHandler(gcppublisher), - wtrhandler.GetRequestHandle(fetch, insightsStore).ToGPubSubHandler(gcppublisher), + pubsubapp.NewDeadLatterQueue(s.memStore, s.fetch).ToGPubSubHandler(s.gcppublisher), + pubsubapp.GetRequestHandle(s.fetch, s.insightsStore).ToGPubSubHandler(s.gcppublisher), } var wg sync.WaitGroup @@ -77,7 +42,7 @@ func startUsingGooglePubSub( log.Printf("[*] Starting subscriber for topic: %s", topicName) // Register topic if not exists - topic := clientPubsub.Topic(topicName) + topic := s.pubsubClient.Topic(topicName) exists, err := topic.Exists(ctx) if err != nil { log.Printf("[!] Error checking if topic %s exists: %v", topicName, err) @@ -86,7 +51,7 @@ func startUsingGooglePubSub( if !exists { log.Printf("[*] Creating topic: %s", topicName) - topic, err = clientPubsub.CreateTopic(ctx, topicName) + topic, err = s.pubsubClient.CreateTopic(ctx, topicName) if err != nil { log.Printf("[!] Error creating topic %s: %v", topicName, err) return @@ -94,7 +59,7 @@ func startUsingGooglePubSub( } subscriptionName := topicutils.BuildSubscriptionName(topicName) - subscription := clientPubsub.Subscription(subscriptionName) + subscription := s.pubsubClient.Subscription(subscriptionName) subExists, err := subscription.Exists(ctx) if err != nil { @@ -105,7 +70,7 @@ func startUsingGooglePubSub( if !subExists { log.Printf("[*] Creating subscription: %s", subscriptionName) - subscription, err = clientPubsub.CreateSubscription(ctx, subscriptionName, pubsub.SubscriptionConfig{ + subscription, err = s.pubsubClient.CreateSubscription(ctx, subscriptionName, pubsub.SubscriptionConfig{ Topic: topic, AckDeadline: 20 * time.Second, // DeadLetterPolicy: &pubsub.DeadLetterPolicy{ @@ -159,52 +124,3 @@ func startUsingGooglePubSub( log.Println("[!] Timeout waiting for subscribers to stop, forcing shutdown") } } - -func startUsingAsynq( - store *interstore.MemStore, - pub pubadapter.GenericPublisher, - fetch *fetcher.Notification, - insightsStore *storests.Store, -) { - cfg := cfg.Get() - - asynqCfg := asynq.Config{ - Concurrency: cfg.AsynqConfig.Concurrency, - } - - srv := asynq.NewServer( - asynq.RedisClientOpt{Addr: cfg.Cache.CacheAddr}, - asynqCfg, - ) - - sigChan := make(chan os.Signal, 1) - signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, os.Interrupt) - - mux := asynq.NewServeMux() - mux.Use(middleware.AsynqLogger) - - events := []asynqsvc.AsynqHandle{ - wtrhandler.GetRequestHandle(fetch, insightsStore).ToAsynqHandler(), - } - - for _, event := range events { - topic := topicutils.BuildTopicName(domain.ProjectID, event.TopicName) - mux.HandleFunc(topic, event.Handler) - } - - log.Println("[*] starting worker with configs") - log.Println("[*] wq.concurrency", asynqCfg.Concurrency) - log.Println("[*] Asynq Worker started. Press Ctrl+C to gracefully shutdown...") - - go func() { - if err := srv.Run(mux); err != nil { - log.Printf("[!] Asynq server error: %v", err) - } - }() - - <-sigChan - log.Println("[*] Received shutdown signal, initiating graceful shutdown...") - - srv.Shutdown() - log.Println("[*] Asynq server stopped gracefully") -} diff --git a/cmd/setup/pubsub/http_api.go b/cmd/setup/pubsub/http_api.go new file mode 100644 index 0000000..737d02b --- /dev/null +++ b/cmd/setup/pubsub/http_api.go @@ -0,0 +1,53 @@ +package pubsub + +import ( + "context" + "log" + "net/http" + + "github.com/IsaacDSC/gqueue/cmd/setup/middleware" + "github.com/IsaacDSC/gqueue/internal/app/health" + "github.com/IsaacDSC/gqueue/internal/app/pubsubapp" + "github.com/IsaacDSC/gqueue/internal/cfg" + "github.com/IsaacDSC/gqueue/pkg/httpadapter" +) + +func (s *Service) startHttpServer(ctx context.Context, env cfg.Config) *http.Server { + + mux := http.NewServeMux() + + routes := []httpadapter.HttpHandle{ + health.GetHealthCheckHandler(), + pubsubapp.PublisherEvent(s.memStore, s.gcppublisher, s.insightsStore), + } + + for _, route := range routes { + mux.HandleFunc(route.Path, route.Handler) + } + + // config := cfg.Get() + + // authorization := auth.NewBasicAuth(map[string]string{ + // config.ProjectID: config.SecretKey, + // }) + + handler := middleware.CORSMiddleware(middleware.LoggerMiddleware(mux)) + // h := authorization.Middleware(handler.ServeHTTP) + + port := env.PubsubApiPort + + server := &http.Server{ + Addr: port.String(), + Handler: handler, + } + + log.Printf("[*] Starting Pubsub API server on :%d", port) + + go func() { + if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Printf("API server error: %v", err) + } + }() + + return server +} diff --git a/cmd/setup/pubsub/mem_sync.go b/cmd/setup/pubsub/mem_sync.go new file mode 100644 index 0000000..534104b --- /dev/null +++ b/cmd/setup/pubsub/mem_sync.go @@ -0,0 +1,27 @@ +package pubsub + +import ( + "context" + "time" + + "github.com/IsaacDSC/gqueue/pkg/ctxlogger" +) + +func (s *Service) syncMemStore(ctx context.Context) { + l := ctxlogger.GetLogger(ctx) + trigger := time.NewTicker(time.Minute) + for { + select { + case <-trigger.C: + if err := s.memStore.LoadInMemStore(ctx); err != nil { + l.Error("Error refreshing mem store with events from persistent store", "error", err) + continue + } + + l.Info("Executed periodic refresh of mem store with events from persistent store", "scope", "pubsub") + case <-ctx.Done(): + trigger.Stop() + return + } + } +} diff --git a/cmd/setup/task/base.go b/cmd/setup/task/base.go new file mode 100644 index 0000000..ecb7f2f --- /dev/null +++ b/cmd/setup/task/base.go @@ -0,0 +1,74 @@ +package task + +import ( + "context" + "net/http" + + "github.com/IsaacDSC/gqueue/internal/cfg" + "github.com/IsaacDSC/gqueue/internal/domain" + "github.com/IsaacDSC/gqueue/internal/fetcher" + "github.com/IsaacDSC/gqueue/internal/interstore" + "github.com/IsaacDSC/gqueue/internal/storests" + "github.com/IsaacDSC/gqueue/pkg/pubadapter" + "github.com/hibiken/asynq" +) + +type PersistentRepository interface { + GetAllEvents(ctx context.Context) ([]domain.Event, error) + GetAllSchedulers(ctx context.Context, state string) ([]domain.Event, error) +} + +type Service struct { + asynqClient *asynq.Client + asynqServer *asynq.Server + asynqPublisher pubadapter.GenericPublisher + server *http.Server + // injectable dependencies + persistentStore PersistentRepository + memStore *interstore.MemStore + fetch *fetcher.Notification + insightsStore *storests.Store +} + +func New( + ps PersistentRepository, + ms *interstore.MemStore, + fetch *fetcher.Notification, + insightsStore *storests.Store, +) *Service { + return &Service{ + persistentStore: ps, + memStore: ms, + fetch: fetch, + insightsStore: insightsStore, + } +} + +func (s *Service) Start(ctx context.Context, env cfg.Config) { + s.asynqClient = asynq.NewClient(asynq.RedisClientOpt{Addr: env.Cache.CacheAddr}) + + asynqCfg := asynq.Config{ + Concurrency: env.AsynqConfig.Concurrency, + } + + s.asynqServer = asynq.NewServer( + asynq.RedisClientOpt{Addr: env.Cache.CacheAddr}, + asynqCfg, + ) + + go s.consumer(ctx, env, asynqCfg) + + s.asynqPublisher = pubadapter.NewPublisher(s.asynqClient) + + // load mem store with events from persistent store + s.memStore.LoadInMemStore(ctx) + + // task refresh mem store + go s.syncMemStore(ctx) + + s.server = s.startHttpServer(ctx, env) +} + +func (s *Service) Close() { _ = s.asynqClient.Close() } + +func (s *Service) Server() *http.Server { return s.server } diff --git a/cmd/setup/task/consumer.go b/cmd/setup/task/consumer.go new file mode 100644 index 0000000..5c6afb9 --- /dev/null +++ b/cmd/setup/task/consumer.go @@ -0,0 +1,51 @@ +package task + +import ( + "context" + "log" + "os" + "os/signal" + "syscall" + + "github.com/IsaacDSC/gqueue/cmd/setup/middleware" + "github.com/IsaacDSC/gqueue/internal/app/taskapp" + "github.com/IsaacDSC/gqueue/internal/cfg" + "github.com/IsaacDSC/gqueue/internal/domain" + "github.com/IsaacDSC/gqueue/pkg/asynqsvc" + "github.com/IsaacDSC/gqueue/pkg/topicutils" + "github.com/hibiken/asynq" +) + +func (s *Service) consumer(ctx context.Context, env cfg.Config, asynqCfg asynq.Config) { + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, os.Interrupt) + + mux := asynq.NewServeMux() + mux.Use(middleware.AsynqLogger) + + events := []asynqsvc.AsynqHandle{ + taskapp.GetRequestHandle(s.fetch, s.insightsStore).ToAsynqHandler(), + } + + for _, event := range events { + topic := topicutils.BuildTopicName(domain.ProjectID, event.TopicName) + mux.HandleFunc(topic, event.Handler) + } + + log.Println("[*] starting worker with configs") + log.Println("[*] wq.concurrency", asynqCfg.Concurrency) + log.Println("[*] Asynq Worker started. Press Ctrl+C to gracefully shutdown...") + + go func() { + if err := s.asynqServer.Run(mux); err != nil { + log.Printf("[!] Asynq server error: %v", err) + } + }() + + <-sigChan + log.Println("[*] Received shutdown signal, initiating graceful shutdown...") + + s.asynqServer.Shutdown() + log.Println("[*] Asynq server stopped gracefully") + +} diff --git a/cmd/setup/task/http_api.go b/cmd/setup/task/http_api.go new file mode 100644 index 0000000..b69ebfa --- /dev/null +++ b/cmd/setup/task/http_api.go @@ -0,0 +1,53 @@ +package task + +import ( + "context" + "log" + "net/http" + + "github.com/IsaacDSC/gqueue/cmd/setup/middleware" + "github.com/IsaacDSC/gqueue/internal/app/health" + "github.com/IsaacDSC/gqueue/internal/app/taskapp" + "github.com/IsaacDSC/gqueue/internal/cfg" + "github.com/IsaacDSC/gqueue/pkg/httpadapter" +) + +func (s *Service) startHttpServer(ctx context.Context, env cfg.Config) *http.Server { + + mux := http.NewServeMux() + + routes := []httpadapter.HttpHandle{ + health.GetHealthCheckHandler(), + taskapp.PublisherEvent(s.memStore, s.asynqPublisher, s.insightsStore), + } + + for _, route := range routes { + mux.HandleFunc(route.Path, route.Handler) + } + + // config := cfg.Get() + + // authorization := auth.NewBasicAuth(map[string]string{ + // config.ProjectID: config.SecretKey, + // }) + + handler := middleware.CORSMiddleware(middleware.LoggerMiddleware(mux)) + // h := authorization.Middleware(handler.ServeHTTP) + + port := env.TaskApiPort + + server := &http.Server{ + Addr: port.String(), + Handler: handler, + } + + log.Printf("[*] Starting Task API server on :%d", port) + + go func() { + if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Printf("API server error: %v", err) + } + }() + + return server +} diff --git a/cmd/setup/task/mem_sync.go b/cmd/setup/task/mem_sync.go new file mode 100644 index 0000000..96b8558 --- /dev/null +++ b/cmd/setup/task/mem_sync.go @@ -0,0 +1,27 @@ +package task + +import ( + "context" + "time" + + "github.com/IsaacDSC/gqueue/pkg/ctxlogger" +) + +func (s *Service) syncMemStore(ctx context.Context) { + l := ctxlogger.GetLogger(ctx) + trigger := time.NewTicker(time.Minute) + for { + select { + case <-trigger.C: + if err := s.memStore.LoadInMemStore(ctx); err != nil { + l.Error("Error refreshing mem store with events from persistent store", "error", err) + continue + } + + l.Info("Executed periodic refresh of mem store with events from persistent store", "scope", "task") + case <-ctx.Done(): + trigger.Stop() + return + } + } +} diff --git a/deployment/ci/gen_mocks/main.go b/deployment/ci/gen_mocks/main.go new file mode 100644 index 0000000..cb6a24c --- /dev/null +++ b/deployment/ci/gen_mocks/main.go @@ -0,0 +1,126 @@ +package main + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "sync" + "time" +) + +type FileInfo struct { + Path string + Pkg string + FileName string +} + +func main() { + skipDirs := map[string]bool{ + "vendor": true, + "docs": true, + "mocks": true, + "tmp": true, + ".git": true, + "specs": true, + "deployment": true, + ".vscode": true, + ".idea": true, + "examples": true, + } + + timeStart := time.Now() + + // Channel to send found files to the workers + fileCh := make(chan FileInfo, 100) + var wgWalk sync.WaitGroup + var wgMock sync.WaitGroup + + const destDir = "./mocks" + const numWorkers = 5 // Adjust as needed + + // Start the mock generation workers + for i := 0; i < numWorkers; i++ { + wgMock.Add(1) + go func() { + defer wgMock.Done() + for f := range fileCh { + myPkg := fmt.Sprintf("mock%s", f.Pkg) + folderDir := fmt.Sprintf("%s/%s", destDir, myPkg) + completeDest := fmt.Sprintf("%s/%s/mock_%s.go", destDir, myPkg, f.FileName) + + os.Mkdir(folderDir, 0755) + + cmd := exec.Command("go", "run", "go.uber.org/mock/mockgen@latest", + "-source="+f.Path, + "-destination="+completeDest, + "-package="+myPkg, + ) + if err := cmd.Run(); err != nil { + fmt.Printf("Error generating mock for %s\n", f.FileName) + } else { + fmt.Printf("Mock generated: %s\n", completeDest) + } + } + }() + } + + // Walk in a goroutine to avoid blocking + wgWalk.Add(1) + go func() { + defer wgWalk.Done() + defer close(fileCh) // Close the channel when walk is done + + filepath.Walk(".", func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + if info.IsDir() { + if skipDirs[info.Name()] { + fmt.Printf("Skipping directory: %s\n", info.Name()) + return filepath.SkipDir + } + return nil + } + + if filepath.Ext(path) != ".go" { + return nil + } + + content, err := os.ReadFile(path) + if err != nil { + fmt.Fprintf(os.Stderr, "Error reading %s: %v\n", path, err) + return nil + } + + if strings.Contains(string(content), "interface {") { + fileCh <- FileInfo{ + Path: path, + Pkg: extractPackage(string(content)), + FileName: strings.ReplaceAll(filepath.Base(path), ".go", ""), + } + } + + return nil + }) + }() + + // Wait for the walk to finish (which closes the channel) + wgWalk.Wait() + // Wait for all workers to finish + wgMock.Wait() + + fmt.Printf("\nTotal execution time: %s\n", time.Since(timeStart)) +} + +func extractPackage(content string) string { + for _, line := range strings.Split(content, "\n") { + line = strings.TrimSpace(line) + if strings.HasPrefix(line, "package ") { + return strings.TrimPrefix(line, "package ") + } + } + return "" +} diff --git a/deployment/ci/validate_mock.sh b/deployment/ci/validate_mock.sh new file mode 100644 index 0000000..1eb3adb --- /dev/null +++ b/deployment/ci/validate_mock.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +# Lista todas as interfaces do projeto (nomes únicos) +interfaces=$(grep -rhoE 'type[[:space:]]+[A-Za-z0-9_]+[[:space:]]+interface[[:space:]]*\{' . --include="*.go" | sed -E 's/type[[:space:]]+([A-Za-z0-9_]+)[[:space:]]+interface.*/\1/' | sort | uniq) + +echo "Interfaces encontradas:" +echo "$interfaces" +echo + +missing=0 + +# Para cada interface, verifica se existe algum arquivo _mock.go que a referencia +for iface in $interfaces; do + if grep -r -w "Mock$iface" . --include="*mock_*.go" > /dev/null; then + echo "✅ Mock encontrado para interface: Mock$iface" + else + echo "❌ Mock NÃO encontrado para interface: Mock$iface" + missing=$((missing+1)) + fi +done + +if [ $missing -eq 0 ]; then + echo "\nTodas as interfaces possuem mocks gerados." + exit 0 +else + echo "\n$missing interface(s) sem mocks." + exit 1 +fi \ No newline at end of file diff --git a/deployment/ci/validation_pkg/main.go b/deployment/ci/validation_pkg/main.go new file mode 100644 index 0000000..db8f53a --- /dev/null +++ b/deployment/ci/validation_pkg/main.go @@ -0,0 +1,100 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + "strings" +) + +func main() { + skipDirs := map[string]bool{ + "vendor": true, + "docs": true, + "mocks": true, + "tmp": true, + ".git": true, + "specs": true, + "deployment": true, + ".vscode": true, + ".idea": true, + "examples": true, + } + + folders := make(map[string][]string) + hasError := false + + filepath.Walk(".", func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + if info.IsDir() { + if skipDirs[info.Name()] { + return filepath.SkipDir + } + + if info.Name() != "." { + folders[info.Name()] = append(folders[info.Name()], path) + } + return nil + } + + if filepath.Ext(path) != ".go" { + return nil + } + + // Ignora arquivos de teste + if strings.HasSuffix(filepath.Base(path), "_test.go") { + return nil + } + + content, err := os.ReadFile(path) + if err != nil { + fmt.Fprintf(os.Stderr, "Erro ao ler %s: %v\n", path, err) + return nil + } + + pkg := extractPackage(string(content)) + + // Ignora package main + if pkg == "main" { + return nil + } + + folderName := filepath.Base(filepath.Dir(path)) + if pkg != "" && folderName != "." && pkg != folderName { + fmt.Printf("ERRO: package '%s' não corresponde ao nome da pasta '%s' em: %s\n", pkg, folderName, path) + hasError = true + } + + return nil + }) + + // Valida pastas com o mesmo nome + for name, paths := range folders { + if len(paths) > 1 { + fmt.Printf("ERRO: pasta '%s' está duplicada em:\n", name) + for _, p := range paths { + fmt.Printf(" - %s\n", p) + } + hasError = true + } + } + + if hasError { + os.Exit(1) + } + + fmt.Println("Nenhum problema encontrado.") +} + +func extractPackage(content string) string { + for _, line := range strings.Split(content, "\n") { + line = strings.TrimSpace(line) + if strings.HasPrefix(line, "package ") { + return strings.TrimPrefix(line, "package ") + } + } + return "" +} diff --git a/example/example.md b/example/example.md index 0355fd6..3fadf37 100644 --- a/example/example.md +++ b/example/example.md @@ -24,7 +24,13 @@ curl -X GET \ ### Publisher data curl -X POST \ - http://localhost:8080/api/v1/event/publisher \ + http://localhost:8082/api/v1/pubsub \ + -H "Content-Type: application/json" \ + -H "Authorization: Basic YWRtaW46cGFzc3dvcmQ=" \ +-d @example/publisher_data.json + +curl -X POST \ + http://localhost:8083/api/v1/task \ -H "Content-Type: application/json" \ -H "Authorization: Basic YWRtaW46cGFzc3dvcmQ=" \ -d @example/publisher_data.json diff --git a/example/publisher_data.json b/example/publisher_data.json index 655bfe9..52e8065 100644 --- a/example/publisher_data.json +++ b/example/publisher_data.json @@ -2,7 +2,8 @@ "service_name": "my-app", "event_name": "payment.charged", "data": { - "key": "value" + "key": "value", + "w-queue":"pubsub" }, "metadata": { "correlation_id": "5e4dd662-9eba-4321-9c97-0b4ee0942f8b" diff --git a/internal/backoffice/get_event.go b/internal/app/backofficeapp/get_event.go similarity index 98% rename from internal/backoffice/get_event.go rename to internal/app/backofficeapp/get_event.go index fffb506..68eeab7 100644 --- a/internal/backoffice/get_event.go +++ b/internal/app/backofficeapp/get_event.go @@ -1,4 +1,4 @@ -package backoffice +package backofficeapp import ( "encoding/json" diff --git a/internal/backoffice/insights_handle.go b/internal/app/backofficeapp/insights_handle.go similarity index 96% rename from internal/backoffice/insights_handle.go rename to internal/app/backofficeapp/insights_handle.go index 85adda7..ec34dd4 100644 --- a/internal/backoffice/insights_handle.go +++ b/internal/app/backofficeapp/insights_handle.go @@ -1,4 +1,4 @@ -package backoffice +package backofficeapp import ( "context" diff --git a/internal/backoffice/interfaces.go b/internal/app/backofficeapp/interfaces.go similarity index 96% rename from internal/backoffice/interfaces.go rename to internal/app/backofficeapp/interfaces.go index 00afa9b..258af7a 100644 --- a/internal/backoffice/interfaces.go +++ b/internal/app/backofficeapp/interfaces.go @@ -1,4 +1,4 @@ -package backoffice +package backofficeapp import ( "context" diff --git a/internal/backoffice/register_consumer.go b/internal/app/backofficeapp/register_consumer.go similarity index 98% rename from internal/backoffice/register_consumer.go rename to internal/app/backofficeapp/register_consumer.go index 271c55a..bbbce71 100644 --- a/internal/backoffice/register_consumer.go +++ b/internal/app/backofficeapp/register_consumer.go @@ -1,4 +1,4 @@ -package backoffice +package backofficeapp import ( "encoding/json" diff --git a/internal/backoffice/register_event_consumer_archived.go b/internal/app/backofficeapp/register_event_consumer_archived.go similarity index 97% rename from internal/backoffice/register_event_consumer_archived.go rename to internal/app/backofficeapp/register_event_consumer_archived.go index d690162..5d5307a 100644 --- a/internal/backoffice/register_event_consumer_archived.go +++ b/internal/app/backofficeapp/register_event_consumer_archived.go @@ -1,4 +1,4 @@ -package backoffice +package backofficeapp import ( "encoding/json" diff --git a/internal/backoffice/remove_event_handle.go b/internal/app/backofficeapp/remove_event_handle.go similarity index 97% rename from internal/backoffice/remove_event_handle.go rename to internal/app/backofficeapp/remove_event_handle.go index da79d43..1536edf 100644 --- a/internal/backoffice/remove_event_handle.go +++ b/internal/app/backofficeapp/remove_event_handle.go @@ -1,4 +1,4 @@ -package backoffice +package backofficeapp import ( "net/http" diff --git a/internal/backoffice/health_check_handle.go b/internal/app/health/health_check_handle.go similarity index 94% rename from internal/backoffice/health_check_handle.go rename to internal/app/health/health_check_handle.go index c334eab..8d6be29 100644 --- a/internal/backoffice/health_check_handle.go +++ b/internal/app/health/health_check_handle.go @@ -1,4 +1,4 @@ -package backoffice +package health import ( "net/http" diff --git a/internal/app/pubsubapp/consumer_handle.go b/internal/app/pubsubapp/consumer_handle.go new file mode 100644 index 0000000..ba244be --- /dev/null +++ b/internal/app/pubsubapp/consumer_handle.go @@ -0,0 +1,61 @@ +package pubsubapp + +import ( + "context" + "fmt" + "time" + + "github.com/IsaacDSC/gqueue/internal/domain" + "github.com/IsaacDSC/gqueue/pkg/asyncadapter" + "github.com/IsaacDSC/gqueue/pkg/ctxlogger" +) + +type Fetcher interface { + Notify(ctx context.Context, data map[string]any, headers map[string]string, consumer domain.Consumer) error +} + +type ConsumerInsights interface { + Consumed(ctx context.Context, input domain.ConsumerMetric) error +} + +func GetRequestHandle(fetch Fetcher, insights ConsumerInsights) asyncadapter.Handle[RequestPayload] { + + insertInsights := func(ctx context.Context, payload RequestPayload, started time.Time, isSuccess bool) { + l := ctxlogger.GetLogger(ctx) + finished := time.Now() + if err := insights.Consumed(ctx, domain.ConsumerMetric{ + TopicName: payload.EventName, + ConsumerName: payload.Consumer.ServiceName, + TimeStarted: started, + TimeEnded: finished, + TimeDurationMs: finished.Sub(started).Milliseconds(), + ACK: isSuccess, + }); err != nil { + l.Warn("not save metric", "type", "consumer", "error", err.Error()) + } + + } + + return asyncadapter.Handle[RequestPayload]{ + EventName: domain.EventQueueRequestToExternal, + Handler: func(c asyncadapter.AsyncCtx[RequestPayload]) error { + started := time.Now() + ctx := c.Context() + + payload, err := c.Payload() + if err != nil { + return fmt.Errorf("get payload: %w", err) + } + + headers := payload.mergeHeaders(payload.Consumer.Headers) + if err := fetch.Notify(ctx, payload.Data, headers, payload.Consumer); err != nil { + insertInsights(ctx, payload, started, false) + return fmt.Errorf("fetch consumer: %w", err) + } + + insertInsights(ctx, payload, started, true) + + return nil + }, + } +} diff --git a/internal/wtrhandler/request_handle_asynq_test.go b/internal/app/pubsubapp/consumer_handle_test.go similarity index 72% rename from internal/wtrhandler/request_handle_asynq_test.go rename to internal/app/pubsubapp/consumer_handle_test.go index 7812f47..e222bce 100644 --- a/internal/wtrhandler/request_handle_asynq_test.go +++ b/internal/app/pubsubapp/consumer_handle_test.go @@ -1,4 +1,4 @@ -package wtrhandler +package pubsubapp_test import ( "context" @@ -8,7 +8,9 @@ import ( "os" "testing" + "github.com/IsaacDSC/gqueue/internal/app/pubsubapp" "github.com/IsaacDSC/gqueue/internal/domain" + "github.com/IsaacDSC/gqueue/mocks/mockpubsubapp" "github.com/IsaacDSC/gqueue/pkg/asyncadapter" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -27,104 +29,24 @@ func init() { // mockFetcher implements the Fetcher interface for testing type mockFetcher struct { - notifyTriggerFunc func(ctx context.Context, data map[string]any, headers map[string]string, consumer Consumer) error + notifyTriggerFunc func(ctx context.Context, data map[string]any, headers map[string]string, consumer domain.Consumer) error } -func (m *mockFetcher) Notify(ctx context.Context, data map[string]any, headers map[string]string, consumer Consumer) error { +func (m *mockFetcher) Notify(ctx context.Context, data map[string]any, headers map[string]string, consumer domain.Consumer) error { if m.notifyTriggerFunc != nil { return m.notifyTriggerFunc(ctx, data, headers, consumer) } return nil } -func TestRequestPayload_mergeHeaders(t *testing.T) { - tests := []struct { - name string - payload RequestPayload - inputHeaders map[string]string - expectedResult map[string]string - }{ - { - name: "merge_with_empty_payload_headers", - payload: RequestPayload{ - Headers: nil, - }, - inputHeaders: map[string]string{ - "Authorization": "Bearer token", - "Content-Type": "application/json", - }, - expectedResult: map[string]string{ - "Authorization": "Bearer token", - "Content-Type": "application/json", - }, - }, - { - name: "merge_with_existing_payload_headers", - payload: RequestPayload{ - Headers: map[string]string{ - "X-Custom-Header": "custom-value", - "User-Agent": "webhook-client", - }, - }, - inputHeaders: map[string]string{ - "Authorization": "Bearer token", - "Content-Type": "application/json", - }, - expectedResult: map[string]string{ - "X-Custom-Header": "custom-value", - "User-Agent": "webhook-client", - "Authorization": "Bearer token", - "Content-Type": "application/json", - }, - }, - { - name: "override_existing_headers", - payload: RequestPayload{ - Headers: map[string]string{ - "Content-Type": "text/plain", - "User-Agent": "webhook-client", - }, - }, - inputHeaders: map[string]string{ - "Content-Type": "application/json", - "Authorization": "Bearer token", - }, - expectedResult: map[string]string{ - "User-Agent": "webhook-client", - "Content-Type": "application/json", - "Authorization": "Bearer token", - }, - }, - { - name: "empty_input_headers", - payload: RequestPayload{ - Headers: map[string]string{ - "X-Custom-Header": "custom-value", - }, - }, - inputHeaders: map[string]string{}, - expectedResult: map[string]string{ - "X-Custom-Header": "custom-value", - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := tt.payload.mergeHeaders(tt.inputHeaders) - assert.Equal(t, tt.expectedResult, result) - }) - } -} - func TestGetRequestHandle(t *testing.T) { t.Run("returns_correct_queue_name_and_handler", func(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() mockFetch := &mockFetcher{} - mockInsights := NewMockConsumerInsights(ctrl) - handle := GetRequestHandle(mockFetch, mockInsights) + mockInsights := mockpubsubapp.NewMockConsumerInsights(ctrl) + handle := pubsubapp.GetRequestHandle(mockFetch, mockInsights) assert.Equal(t, "event-queue.request-to-external", handle.EventName) assert.NotNil(t, handle.Handler) @@ -157,17 +79,17 @@ func TestGetRequestHandle_Handler(t *testing.T) { tests := []struct { name string - payload RequestPayload + payload pubsubapp.RequestPayload mockFetcher *mockFetcher - setupMocks func(*MockConsumerInsights) + setupMocks func(*mockpubsubapp.MockConsumerInsights) expectedError bool expectedErrMsg string }{ { name: "successful_request", - payload: RequestPayload{ + payload: pubsubapp.RequestPayload{ EventName: "user.created", - Consumer: Consumer{ + Consumer: domain.Consumer{ ServiceName: "user-service", BaseUrl: testServer.URL, Path: "/webhook", @@ -184,11 +106,11 @@ func TestGetRequestHandle_Handler(t *testing.T) { }, }, mockFetcher: &mockFetcher{ - notifyTriggerFunc: func(ctx context.Context, data map[string]any, headers map[string]string, consumer Consumer) error { + notifyTriggerFunc: func(ctx context.Context, data map[string]any, headers map[string]string, consumer domain.Consumer) error { return nil }, }, - setupMocks: func(mockInsights *MockConsumerInsights) { + setupMocks: func(mockInsights *mockpubsubapp.MockConsumerInsights) { mockInsights.EXPECT(). Consumed(gomock.Any(), gomock.Any()). DoAndReturn(func(ctx context.Context, input domain.ConsumerMetric) error { @@ -210,20 +132,20 @@ func TestGetRequestHandle_Handler(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - mockInsights := NewMockConsumerInsights(ctrl) + mockInsights := mockpubsubapp.NewMockConsumerInsights(ctrl) if tt.setupMocks != nil { tt.setupMocks(mockInsights) } // Get the handler - handle := GetRequestHandle(tt.mockFetcher, mockInsights) + handle := pubsubapp.GetRequestHandle(tt.mockFetcher, mockInsights) // Create task payload taskPayload, err := json.Marshal(tt.payload) require.NoError(t, err) // Create AsyncCtx wrapper - asyncCtx := asyncadapter.NewAsyncCtx[RequestPayload](context.Background(), taskPayload) + asyncCtx := asyncadapter.NewAsyncCtx[pubsubapp.RequestPayload](context.Background(), taskPayload) // Execute handler err = handle.Handler(asyncCtx) @@ -245,11 +167,11 @@ func TestGetRequestHandle_Handler_InvalidPayload(t *testing.T) { defer ctrl.Finish() mockFetch := &mockFetcher{} - mockInsights := NewMockConsumerInsights(ctrl) - handle := GetRequestHandle(mockFetch, mockInsights) + mockInsights := mockpubsubapp.NewMockConsumerInsights(ctrl) + handle := pubsubapp.GetRequestHandle(mockFetch, mockInsights) // Create AsyncCtx wrapper with invalid payload - asyncCtx := asyncadapter.NewAsyncCtx[RequestPayload](context.Background(), []byte("invalid json")) + asyncCtx := asyncadapter.NewAsyncCtx[pubsubapp.RequestPayload](context.Background(), []byte("invalid json")) err := handle.Handler(asyncCtx) @@ -262,9 +184,9 @@ func TestRequestPayload_mergeHeaders_Integration(t *testing.T) { name string data map[string]any headers map[string]string - consumer Consumer - mockNotifyTriggerFn func(ctx context.Context, data map[string]any, headers map[string]string, consumer Consumer) error - setupMocks func(*MockConsumerInsights) + consumer domain.Consumer + mockNotifyTriggerFn func(ctx context.Context, data map[string]any, headers map[string]string, consumer domain.Consumer) error + setupMocks func(*mockpubsubapp.MockConsumerInsights) expectedError bool expectedErrMsg string }{ @@ -278,18 +200,18 @@ func TestRequestPayload_mergeHeaders_Integration(t *testing.T) { "Authorization": "Bearer token", "User-Agent": "webhook-client", }, - consumer: Consumer{ + consumer: domain.Consumer{ ServiceName: "user-service", BaseUrl: "http://example.com", Path: "/webhook", }, - mockNotifyTriggerFn: func(ctx context.Context, data map[string]any, headers map[string]string, consumer Consumer) error { + mockNotifyTriggerFn: func(ctx context.Context, data map[string]any, headers map[string]string, consumer domain.Consumer) error { // Verify the merged headers are passed correctly assert.Equal(t, "Bearer token", headers["Authorization"]) assert.Equal(t, "webhook-client", headers["User-Agent"]) return nil }, - setupMocks: func(mockInsights *MockConsumerInsights) { + setupMocks: func(mockInsights *mockpubsubapp.MockConsumerInsights) { mockInsights.EXPECT(). Consumed(gomock.Any(), gomock.Any()). DoAndReturn(func(ctx context.Context, input domain.ConsumerMetric) error { @@ -308,15 +230,15 @@ func TestRequestPayload_mergeHeaders_Integration(t *testing.T) { "user_id": "123", }, headers: map[string]string{}, - consumer: Consumer{ + consumer: domain.Consumer{ ServiceName: "user-service", BaseUrl: "http://example.com", Path: "/webhook", }, - mockNotifyTriggerFn: func(ctx context.Context, data map[string]any, headers map[string]string, consumer Consumer) error { + mockNotifyTriggerFn: func(ctx context.Context, data map[string]any, headers map[string]string, consumer domain.Consumer) error { return assert.AnError }, - setupMocks: func(mockInsights *MockConsumerInsights) { + setupMocks: func(mockInsights *mockpubsubapp.MockConsumerInsights) { mockInsights.EXPECT(). Consumed(gomock.Any(), gomock.Any()). DoAndReturn(func(ctx context.Context, input domain.ConsumerMetric) error { @@ -337,7 +259,7 @@ func TestRequestPayload_mergeHeaders_Integration(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - mockInsights := NewMockConsumerInsights(ctrl) + mockInsights := mockpubsubapp.NewMockConsumerInsights(ctrl) if tt.setupMocks != nil { tt.setupMocks(mockInsights) } @@ -346,9 +268,9 @@ func TestRequestPayload_mergeHeaders_Integration(t *testing.T) { notifyTriggerFunc: tt.mockNotifyTriggerFn, } - handle := GetRequestHandle(mockFetch, mockInsights) + handle := pubsubapp.GetRequestHandle(mockFetch, mockInsights) - payload := RequestPayload{ + payload := pubsubapp.RequestPayload{ EventName: "user.created", Consumer: tt.consumer, Data: tt.data, @@ -360,7 +282,7 @@ func TestRequestPayload_mergeHeaders_Integration(t *testing.T) { require.NoError(t, err) // Create AsyncCtx wrapper - asyncCtx := asyncadapter.NewAsyncCtx[RequestPayload](context.Background(), taskPayload) + asyncCtx := asyncadapter.NewAsyncCtx[pubsubapp.RequestPayload](context.Background(), taskPayload) // Execute handler err = handle.Handler(asyncCtx) @@ -395,7 +317,7 @@ func TestMockFetcher_ErrorScenarios(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - mockInsights := NewMockConsumerInsights(ctrl) + mockInsights := mockpubsubapp.NewMockConsumerInsights(ctrl) mockInsights.EXPECT(). Consumed(gomock.Any(), gomock.Any()). DoAndReturn(func(ctx context.Context, input domain.ConsumerMetric) error { @@ -407,16 +329,16 @@ func TestMockFetcher_ErrorScenarios(t *testing.T) { Times(1) mockFetch := &mockFetcher{ - notifyTriggerFunc: func(ctx context.Context, data map[string]any, headers map[string]string, consumer Consumer) error { + notifyTriggerFunc: func(ctx context.Context, data map[string]any, headers map[string]string, consumer domain.Consumer) error { return tt.mockError }, } - handle := GetRequestHandle(mockFetch, mockInsights) + handle := pubsubapp.GetRequestHandle(mockFetch, mockInsights) - payload := RequestPayload{ + payload := pubsubapp.RequestPayload{ EventName: "user.created", - Consumer: Consumer{ + Consumer: domain.Consumer{ ServiceName: "user-service", BaseUrl: "http://localhost:99999", // Unreachable port Path: "/webhook", @@ -430,7 +352,7 @@ func TestMockFetcher_ErrorScenarios(t *testing.T) { require.NoError(t, err) // Create AsyncCtx wrapper - asyncCtx := asyncadapter.NewAsyncCtx[RequestPayload](context.Background(), taskPayload) + asyncCtx := asyncadapter.NewAsyncCtx[pubsubapp.RequestPayload](context.Background(), taskPayload) err = handle.Handler(asyncCtx) assert.Error(t, err) @@ -446,7 +368,7 @@ func TestGetRequestHandle_HeaderMerging(t *testing.T) { // Track the headers received by the mock fetcher var receivedHeaders map[string]string - mockInsights := NewMockConsumerInsights(ctrl) + mockInsights := mockpubsubapp.NewMockConsumerInsights(ctrl) mockInsights.EXPECT(). Consumed(gomock.Any(), gomock.Any()). DoAndReturn(func(ctx context.Context, input domain.ConsumerMetric) error { @@ -458,17 +380,17 @@ func TestGetRequestHandle_HeaderMerging(t *testing.T) { Times(1) mockFetch := &mockFetcher{ - notifyTriggerFunc: func(ctx context.Context, data map[string]any, headers map[string]string, consumer Consumer) error { + notifyTriggerFunc: func(ctx context.Context, data map[string]any, headers map[string]string, consumer domain.Consumer) error { receivedHeaders = headers return nil }, } - handle := GetRequestHandle(mockFetch, mockInsights) + handle := pubsubapp.GetRequestHandle(mockFetch, mockInsights) - payload := RequestPayload{ + payload := pubsubapp.RequestPayload{ EventName: "user.created", - Consumer: Consumer{ + Consumer: domain.Consumer{ ServiceName: "user-service", BaseUrl: "http://example.com", Path: "/webhook", @@ -492,7 +414,7 @@ func TestGetRequestHandle_HeaderMerging(t *testing.T) { require.NoError(t, err) // Create AsyncCtx wrapper - asyncCtx := asyncadapter.NewAsyncCtx[RequestPayload](context.Background(), taskPayload) + asyncCtx := asyncadapter.NewAsyncCtx[pubsubapp.RequestPayload](context.Background(), taskPayload) err = handle.Handler(asyncCtx) require.NoError(t, err) @@ -510,7 +432,7 @@ func TestGetRequestHandle_DataPassing(t *testing.T) { // Track the data received by the mock fetcher var receivedData map[string]any - mockInsights := NewMockConsumerInsights(ctrl) + mockInsights := mockpubsubapp.NewMockConsumerInsights(ctrl) mockInsights.EXPECT(). Consumed(gomock.Any(), gomock.Any()). DoAndReturn(func(ctx context.Context, input domain.ConsumerMetric) error { @@ -522,13 +444,13 @@ func TestGetRequestHandle_DataPassing(t *testing.T) { Times(1) mockFetch := &mockFetcher{ - notifyTriggerFunc: func(ctx context.Context, data map[string]any, headers map[string]string, consumer Consumer) error { + notifyTriggerFunc: func(ctx context.Context, data map[string]any, headers map[string]string, consumer domain.Consumer) error { receivedData = data return nil }, } - handle := GetRequestHandle(mockFetch, mockInsights) + handle := pubsubapp.GetRequestHandle(mockFetch, mockInsights) expectedData := map[string]any{ "user_id": "123", @@ -538,9 +460,9 @@ func TestGetRequestHandle_DataPassing(t *testing.T) { "is_active": true, } - payload := RequestPayload{ + payload := pubsubapp.RequestPayload{ EventName: "user.created", - Consumer: Consumer{ + Consumer: domain.Consumer{ ServiceName: "user-service", BaseUrl: "http://example.com", Path: "/webhook", @@ -554,7 +476,7 @@ func TestGetRequestHandle_DataPassing(t *testing.T) { require.NoError(t, err) // Create AsyncCtx wrapper - asyncCtx := asyncadapter.NewAsyncCtx[RequestPayload](context.Background(), taskPayload) + asyncCtx := asyncadapter.NewAsyncCtx[pubsubapp.RequestPayload](context.Background(), taskPayload) err = handle.Handler(asyncCtx) require.NoError(t, err) diff --git a/internal/wtrhandler/deadletter_asynq_handle.go b/internal/app/pubsubapp/deadletter_handle.go similarity index 93% rename from internal/wtrhandler/deadletter_asynq_handle.go rename to internal/app/pubsubapp/deadletter_handle.go index d86448a..bf4ea21 100644 --- a/internal/wtrhandler/deadletter_asynq_handle.go +++ b/internal/app/pubsubapp/deadletter_handle.go @@ -1,4 +1,4 @@ -package wtrhandler +package pubsubapp import ( "context" @@ -45,9 +45,9 @@ func NewDeadLatterQueue(store DeadLetterStore, fetcher Fetcher) asyncadapter.Han "data": p.Data, "metadata": p.Attributes, "event_at": p.PublishTime, - }, consumer.Headers, Consumer{ + }, consumer.Headers, domain.Consumer{ ServiceName: consumer.ServiceName, - BaseUrl: consumer.Host, + BaseUrl: consumer.BaseUrl, Path: consumer.Path, Headers: consumer.Headers, }) diff --git a/internal/wtrhandler/deadletter_asynq_handle_test.go b/internal/app/pubsubapp/deadletter_handle_test.go similarity index 89% rename from internal/wtrhandler/deadletter_asynq_handle_test.go rename to internal/app/pubsubapp/deadletter_handle_test.go index 78c040a..11528ad 100644 --- a/internal/wtrhandler/deadletter_asynq_handle_test.go +++ b/internal/app/pubsubapp/deadletter_handle_test.go @@ -1,4 +1,4 @@ -package wtrhandler +package pubsubapp import ( "context" @@ -10,6 +10,7 @@ import ( "cloud.google.com/go/pubsub" "github.com/IsaacDSC/gqueue/internal/cfg" "github.com/IsaacDSC/gqueue/internal/domain" + "github.com/IsaacDSC/gqueue/mocks/mockpubsubapp" "github.com/IsaacDSC/gqueue/pkg/asyncadapter" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -26,16 +27,16 @@ func init() { type notifyCall struct { data map[string]any headers map[string]string - consumer Consumer + consumer domain.Consumer } // mockFetcherWithCalls wraps MockFetcher to track calls type mockFetcherWithCalls struct { - *MockFetcher + *mockpubsubapp.MockFetcher notifyCalls []notifyCall } -func (m *mockFetcherWithCalls) Notify(ctx context.Context, data map[string]any, headers map[string]string, consumer Consumer) error { +func (m *mockFetcherWithCalls) Notify(ctx context.Context, data map[string]any, headers map[string]string, consumer domain.Consumer) error { m.notifyCalls = append(m.notifyCalls, notifyCall{ data: data, headers: headers, @@ -49,8 +50,8 @@ func TestNewDeadLatterQueue(t *testing.T) { defer ctrl.Finish() t.Run("returns_correct_event_name_and_handler", func(t *testing.T) { - mockStore := NewMockDeadLetterStore(ctrl) - mockFetcher := NewMockFetcher(ctrl) + mockStore := mockpubsubapp.NewMockDeadLetterStore(ctrl) + mockFetcher := mockpubsubapp.NewMockFetcher(ctrl) handle := NewDeadLatterQueue(mockStore, mockFetcher) @@ -67,10 +68,10 @@ func TestNewDeadLatterQueue(t *testing.T) { }) t.Run("constructor_creates_different_instances", func(t *testing.T) { - mockStore1 := NewMockDeadLetterStore(ctrl) - mockFetcher1 := NewMockFetcher(ctrl) - mockStore2 := NewMockDeadLetterStore(ctrl) - mockFetcher2 := NewMockFetcher(ctrl) + mockStore1 := mockpubsubapp.NewMockDeadLetterStore(ctrl) + mockFetcher1 := mockpubsubapp.NewMockFetcher(ctrl) + mockStore2 := mockpubsubapp.NewMockDeadLetterStore(ctrl) + mockFetcher2 := mockpubsubapp.NewMockFetcher(ctrl) handle1 := NewDeadLatterQueue(mockStore1, mockFetcher1) handle2 := NewDeadLatterQueue(mockStore2, mockFetcher2) @@ -101,7 +102,7 @@ func TestDeadLetterQueue_Handler_Success(t *testing.T) { Consumers: []domain.Consumer{ { ServiceName: "notification-service", - Host: "http://localhost:8080", + BaseUrl: "http://localhost:8080", Path: "/webhook/user-created", Headers: map[string]string{ "Content-Type": "application/json", @@ -110,7 +111,7 @@ func TestDeadLetterQueue_Handler_Success(t *testing.T) { }, { ServiceName: "analytics-service", - Host: "http://localhost:8081", + BaseUrl: "http://localhost:8081", Path: "/analytics/event", Headers: map[string]string{ "X-API-Key": "analytics-key", @@ -125,7 +126,7 @@ func TestDeadLetterQueue_Handler_Success(t *testing.T) { Consumers: []domain.Consumer{ { ServiceName: "email-service", - Host: "http://localhost:8082", + BaseUrl: "http://localhost:8082", Path: "/send-confirmation", Headers: map[string]string{ "Content-Type": "application/json", @@ -135,14 +136,14 @@ func TestDeadLetterQueue_Handler_Success(t *testing.T) { }, } - mockStore := NewMockDeadLetterStore(ctrl) + mockStore := mockpubsubapp.NewMockDeadLetterStore(ctrl) mockStore.EXPECT(). GetAllSchedulers(gomock.Any(), "archived"). Return(mockEvents, nil). Times(1) mockFetcher := &mockFetcherWithCalls{ - MockFetcher: NewMockFetcher(ctrl), + MockFetcher: mockpubsubapp.NewMockFetcher(ctrl), notifyCalls: []notifyCall{}, } @@ -176,7 +177,7 @@ func TestDeadLetterQueue_Handler_Success(t *testing.T) { // Don't compare exact time due to marshaling precision loss assert.NotNil(t, firstCall.data["event_at"]) assert.Equal(t, map[string]string{"Content-Type": "application/json", "Authorization": "Bearer token"}, firstCall.headers) - assert.Equal(t, Consumer{ + assert.Equal(t, domain.Consumer{ ServiceName: "notification-service", BaseUrl: "http://localhost:8080", Path: "/webhook/user-created", @@ -191,7 +192,7 @@ func TestDeadLetterQueue_Handler_Success(t *testing.T) { assert.Equal(t, map[string]string{"source": "api", "version": "1.0"}, secondCall.data["metadata"]) assert.NotNil(t, secondCall.data["event_at"]) assert.Equal(t, map[string]string{"X-API-Key": "analytics-key"}, secondCall.headers) - assert.Equal(t, Consumer{ + assert.Equal(t, domain.Consumer{ ServiceName: "analytics-service", BaseUrl: "http://localhost:8081", Path: "/analytics/event", @@ -206,7 +207,7 @@ func TestDeadLetterQueue_Handler_Success(t *testing.T) { assert.Equal(t, map[string]string{"source": "api", "version": "1.0"}, thirdCall.data["metadata"]) assert.NotNil(t, thirdCall.data["event_at"]) assert.Equal(t, map[string]string{"Content-Type": "application/json"}, thirdCall.headers) - assert.Equal(t, Consumer{ + assert.Equal(t, domain.Consumer{ ServiceName: "email-service", BaseUrl: "http://localhost:8082", Path: "/send-confirmation", @@ -225,14 +226,14 @@ func TestDeadLetterQueue_Handler_EventNotFound(t *testing.T) { PublishTime: time.Now(), } - mockStore := NewMockDeadLetterStore(ctrl) + mockStore := mockpubsubapp.NewMockDeadLetterStore(ctrl) mockStore.EXPECT(). GetAllSchedulers(gomock.Any(), "archived"). Return(nil, domain.EventNotFound). Times(1) mockFetcher := &mockFetcherWithCalls{ - MockFetcher: NewMockFetcher(ctrl), + MockFetcher: mockpubsubapp.NewMockFetcher(ctrl), notifyCalls: []notifyCall{}, } @@ -265,14 +266,14 @@ func TestDeadLetterQueue_Handler_StoreError(t *testing.T) { expectedError := errors.New("database connection failed") - mockStore := NewMockDeadLetterStore(ctrl) + mockStore := mockpubsubapp.NewMockDeadLetterStore(ctrl) mockStore.EXPECT(). GetAllSchedulers(gomock.Any(), "archived"). Return(nil, expectedError). Times(1) mockFetcher := &mockFetcherWithCalls{ - MockFetcher: NewMockFetcher(ctrl), + MockFetcher: mockpubsubapp.NewMockFetcher(ctrl), notifyCalls: []notifyCall{}, } @@ -297,8 +298,8 @@ func TestDeadLetterQueue_Handler_InvalidPayload(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - mockStore := NewMockDeadLetterStore(ctrl) - mockFetcher := NewMockFetcher(ctrl) + mockStore := mockpubsubapp.NewMockDeadLetterStore(ctrl) + mockFetcher := mockpubsubapp.NewMockFetcher(ctrl) handle := NewDeadLatterQueue(mockStore, mockFetcher) @@ -321,14 +322,14 @@ func TestDeadLetterQueue_Handler_EmptyEvents(t *testing.T) { PublishTime: time.Now(), } - mockStore := NewMockDeadLetterStore(ctrl) + mockStore := mockpubsubapp.NewMockDeadLetterStore(ctrl) mockStore.EXPECT(). GetAllSchedulers(gomock.Any(), "archived"). Return([]domain.Event{}, nil). Times(1) mockFetcher := &mockFetcherWithCalls{ - MockFetcher: NewMockFetcher(ctrl), + MockFetcher: mockpubsubapp.NewMockFetcher(ctrl), notifyCalls: []notifyCall{}, } @@ -367,14 +368,14 @@ func TestDeadLetterQueue_Handler_EventsWithNoConsumers(t *testing.T) { }, } - mockStore := NewMockDeadLetterStore(ctrl) + mockStore := mockpubsubapp.NewMockDeadLetterStore(ctrl) mockStore.EXPECT(). GetAllSchedulers(gomock.Any(), "archived"). Return(mockEvents, nil). Times(1) mockFetcher := &mockFetcherWithCalls{ - MockFetcher: NewMockFetcher(ctrl), + MockFetcher: mockpubsubapp.NewMockFetcher(ctrl), notifyCalls: []notifyCall{}, } @@ -413,7 +414,7 @@ func TestDeadLetterQueue_Handler_FetcherError(t *testing.T) { Consumers: []domain.Consumer{ { ServiceName: "webhook-service", - Host: "http://localhost:8080", + BaseUrl: "http://localhost:8080", Path: "/webhook", Headers: map[string]string{"Content-Type": "application/json"}, }, @@ -421,7 +422,7 @@ func TestDeadLetterQueue_Handler_FetcherError(t *testing.T) { }, } - mockStore := NewMockDeadLetterStore(ctrl) + mockStore := mockpubsubapp.NewMockDeadLetterStore(ctrl) mockStore.EXPECT(). GetAllSchedulers(gomock.Any(), "archived"). Return(mockEvents, nil). @@ -429,7 +430,7 @@ func TestDeadLetterQueue_Handler_FetcherError(t *testing.T) { fetcherError := errors.New("webhook delivery failed") mockFetcher := &mockFetcherWithCalls{ - MockFetcher: NewMockFetcher(ctrl), + MockFetcher: mockpubsubapp.NewMockFetcher(ctrl), notifyCalls: []notifyCall{}, } @@ -474,7 +475,7 @@ func TestDeadLetterQueue_Handler_MultipleEventsWithMixedConsumers(t *testing.T) Consumers: []domain.Consumer{ { ServiceName: "inventory-service", - Host: "http://inventory.local", + BaseUrl: "http://inventory.local", Path: "/reserve", Headers: map[string]string{"Authorization": "Bearer inventory-token"}, }, @@ -493,13 +494,13 @@ func TestDeadLetterQueue_Handler_MultipleEventsWithMixedConsumers(t *testing.T) Consumers: []domain.Consumer{ { ServiceName: "logging-service", - Host: "http://logs.local", + BaseUrl: "http://logs.local", Path: "/audit", Headers: map[string]string{"X-Service": "audit"}, }, { ServiceName: "backup-service", - Host: "http://backup.local", + BaseUrl: "http://backup.local", Path: "/store", Headers: map[string]string{"X-Backup": "true"}, }, @@ -507,14 +508,14 @@ func TestDeadLetterQueue_Handler_MultipleEventsWithMixedConsumers(t *testing.T) }, } - mockStore := NewMockDeadLetterStore(ctrl) + mockStore := mockpubsubapp.NewMockDeadLetterStore(ctrl) mockStore.EXPECT(). GetAllSchedulers(gomock.Any(), "archived"). Return(mockEvents, nil). Times(1) mockFetcher := &mockFetcherWithCalls{ - MockFetcher: NewMockFetcher(ctrl), + MockFetcher: mockpubsubapp.NewMockFetcher(ctrl), notifyCalls: []notifyCall{}, } @@ -575,13 +576,13 @@ func TestDeadLetterQueue_Handler_VerifyState(t *testing.T) { PublishTime: time.Now(), } - mockStore := NewMockDeadLetterStore(ctrl) + mockStore := mockpubsubapp.NewMockDeadLetterStore(ctrl) mockStore.EXPECT(). GetAllSchedulers(gomock.Any(), "archived"). Return([]domain.Event{}, nil). Times(1) - mockFetcher := NewMockFetcher(ctrl) + mockFetcher := mockpubsubapp.NewMockFetcher(ctrl) handle := NewDeadLatterQueue(mockStore, mockFetcher) diff --git a/internal/app/pubsubapp/internal_payload_dto.go b/internal/app/pubsubapp/internal_payload_dto.go new file mode 100644 index 0000000..c8239c7 --- /dev/null +++ b/internal/app/pubsubapp/internal_payload_dto.go @@ -0,0 +1,38 @@ +package pubsubapp + +import ( + "encoding/json" + "fmt" +) + +type InternalPayload struct { + EventName string `json:"event_name"` + Data Data `json:"data"` + Metadata Metadata `json:"metadata"` +} + +func (p InternalPayload) Validate() error { + if p.EventName == "" { + return fmt.Errorf("event name is required") + } + + if p.Data == nil { + return fmt.Errorf("data is required") + } + + return nil +} + +type Metadata struct { + Source string `json:"source"` + Version string `json:"version"` + Environment string `json:"environment"` + Headers map[string]string `json:"headers"` +} + +type Data map[string]any + +func (d Data) ToBytes() []byte { + data, _ := json.Marshal(d) + return data +} diff --git a/internal/wtrhandler/external_handle_http.go b/internal/app/pubsubapp/publisher_handle_http.go similarity index 83% rename from internal/wtrhandler/external_handle_http.go rename to internal/app/pubsubapp/publisher_handle_http.go index c810977..f8fa86b 100644 --- a/internal/wtrhandler/external_handle_http.go +++ b/internal/app/pubsubapp/publisher_handle_http.go @@ -1,4 +1,4 @@ -package wtrhandler +package pubsubapp import ( "context" @@ -23,6 +23,25 @@ type Store interface { GetEvent(ctx context.Context, eventName string) (domain.Event, error) } +type RequestPayload struct { + EventName string `json:"event_name"` + Consumer domain.Consumer `json:"consumer"` + Data map[string]any `json:"data"` + Headers map[string]string `json:"headers,omitempty"` +} + +func (p RequestPayload) mergeHeaders(headers map[string]string) map[string]string { + if p.Headers == nil { + p.Headers = make(map[string]string) + } + + for key, value := range headers { + p.Headers[key] = value + } + + return p.Headers +} + func PublisherEvent( store Store, adaptpub pubadapter.GenericPublisher, @@ -46,7 +65,7 @@ func PublisherEvent( } return httpadapter.HttpHandle{ - Path: "POST /api/v1/event/publisher", + Path: "POST /api/v1/pubsub", Handler: func(w http.ResponseWriter, r *http.Request) { started := time.Now() ctx := r.Context() @@ -94,16 +113,16 @@ func PublisherEvent( EventName: event.Name, Data: payload.Data, Headers: payload.Metadata.Headers, - Consumer: Consumer{ + Consumer: domain.Consumer{ ServiceName: consumer.ServiceName, - BaseUrl: consumer.Host, + BaseUrl: consumer.BaseUrl, Path: consumer.Path, Headers: consumer.Headers, }, } topic := topicutils.BuildTopicName(domain.ProjectID, domain.EventQueueRequestToExternal) - opts := pubadapter.Opts{Attributes: make(map[string]string), AsynqOpts: config, Type: eventType} + opts := pubadapter.Opts{Attributes: make(map[string]string), AsynqOpts: config} if err = adaptpub.Publish(ctx, topic, input, opts); err != nil { err = fmt.Errorf("publish event: %w", err) l.Error("failed to publish event", "error", err.Error()) @@ -112,7 +131,7 @@ func PublisherEvent( } } - w.WriteHeader(http.StatusAccepted) + w.WriteHeader(http.StatusCreated) }, } } diff --git a/internal/asynqtask/archived_task.go b/internal/app/taskapp/archived_task.go similarity index 52% rename from internal/asynqtask/archived_task.go rename to internal/app/taskapp/archived_task.go index e8964ea..54bf8d8 100644 --- a/internal/asynqtask/archived_task.go +++ b/internal/app/taskapp/archived_task.go @@ -1,10 +1,9 @@ -package asynqtask +package taskapp import ( "context" "errors" "fmt" - "sync" "github.com/IsaacDSC/gqueue/internal/domain" "github.com/IsaacDSC/gqueue/pkg/ctxlogger" @@ -20,10 +19,6 @@ type CacheManager interface { SetArchivedTasks(ctx context.Context, events []domain.Event) error } -type Fetcher interface { - NotifyScheduler(ctx context.Context, url string, data any, headers map[string]string) error -} - type Storer interface { GetAllSchedulers(ctx context.Context, state string) ([]domain.Event, error) } @@ -31,91 +26,10 @@ type Storer interface { type TaskManager struct { store Storer cm CacheManager - fetch Fetcher -} - -func NewTaskManager(store Storer, cm CacheManager, fetch Fetcher) *TaskManager { - return &TaskManager{store: store, cm: cm, fetch: fetch} } -func (n TaskManager) NotifyListeners(ctx context.Context) error { - l := ctxlogger.GetLogger(ctx) - l.Debug("Starting notification process") - - events, err := n.cm.FindAllConsumers(ctx) - if errors.Is(err, ErrorNotFound) { - results, err := n.store.GetAllSchedulers(ctx, "archived") - if errors.Is(domain.EventNotFound, err) { - l.Warn("No consumers found", "tag", "TaskManager.NotifyListeners") - return nil - } - - if err != nil { - return fmt.Errorf("failed to get consumers on store: %w", err) - } - - if err := n.cm.SetArchivedTasks(ctx, results); err != nil { - l.Error("Failed to set archived tasks", "error", err) - } - - events = results - } - - if err != nil { - return fmt.Errorf("failed to get consumers on cache: %w", err) - } - - msgs, err := n.GetMsgsArchived(ctx) - if err != nil { - l.Error("Failed to get archived messages", "error", err) - return fmt.Errorf("failed to get archived messages: %w", err) - } - - var fetchMsgs []FetchMsg - for _, events := range events { - for _, msg := range msgs { - if events.Name == msg.Msg["event_name"] { - fetchMsgs = append(fetchMsgs, FetchMsg{ - ID: msg.ID, - QueueName: msg.Queue, - Tasks: msg.Tasks, - Data: msg.Msg["data"], - Schedulers: events.Consumers, - }) - } - } - } - - l.Debug("Fetched archived messages", "count", len(msgs)) - - var wg sync.WaitGroup - for _, msg := range fetchMsgs { - for _, scheduler := range msg.Schedulers { - wg.Add(1) - go func() { - defer wg.Done() - if err := n.fetch.NotifyScheduler(ctx, scheduler.Host, msg.Data, scheduler.Headers); err != nil { - l.Error("Failed to notify consumer", "consumer", scheduler.Host, "error", err) - } else { - if err := n.cm.RemoveMsgArchivedTask(ctx, msg.QueueName, msg.ID); err != nil { - l.Error("Failed to remove archived task", "queue", msg.QueueName, "task", msg.ID, "error", err) - } else { - if err := n.cm.RemoveItemsArchivedTasks(ctx, msg.QueueName, msg.Tasks...); err != nil { - l.Error("Failed to remove archived tasks", "queue", msg.QueueName, "tasks", msg.Tasks, "error", err) - } - } - } - }() - - } - - l.Info("Notifying schedulers", "queue", msg.QueueName, "schedulers", msg.Schedulers) - } - - wg.Wait() - l.Debug("Notification process completed") - - return nil +func NewTaskManager(store Storer, cm CacheManager) *TaskManager { + return &TaskManager{store: store, cm: cm} } func (n TaskManager) GetMsgsArchived(ctx context.Context) ([]TaskArchivedData, error) { diff --git a/internal/asynqstore/cache_store.go b/internal/app/taskapp/cache_store.go similarity index 86% rename from internal/asynqstore/cache_store.go rename to internal/app/taskapp/cache_store.go index 684615f..85fed4b 100644 --- a/internal/asynqstore/cache_store.go +++ b/internal/app/taskapp/cache_store.go @@ -1,4 +1,4 @@ -package asynqstore +package taskapp import ( "context" @@ -6,7 +6,6 @@ import ( "errors" "fmt" - "github.com/IsaacDSC/gqueue/internal/asynqtask" "github.com/IsaacDSC/gqueue/internal/cfg" "github.com/IsaacDSC/gqueue/internal/domain" "github.com/redis/go-redis/v9" @@ -14,9 +13,9 @@ import ( type Cacher interface { FindAllConsumers(ctx context.Context) ([]domain.Event, error) - FindAllQueues(ctx context.Context) ([]asynqtask.Queue, error) + FindAllQueues(ctx context.Context) ([]Queue, error) FindArchivedTasks(ctx context.Context, queue string) ([]string, error) - GetMsgArchivedTask(ctx context.Context, queue, task string) (asynqtask.RawMsg, error) + GetMsgArchivedTask(ctx context.Context, queue, task string) (RawMsg, error) RemoveMsgArchivedTask(ctx context.Context, queue, task string) error RemoveItemsArchivedTasks(ctx context.Context, queue string, tasks ...string) error SetArchivedTasks(ctx context.Context, events []domain.Event) error @@ -37,7 +36,7 @@ const archivedKey = "gqueue:consumers:schedule:archived" func (c Cache) FindAllConsumers(ctx context.Context) ([]domain.Event, error) { cResult, err := c.cache.Get(ctx, archivedKey).Result() if errors.Is(err, redis.Nil) { - return nil, asynqtask.ErrorNotFound + return nil, ErrorNotFound } if err != nil { @@ -52,21 +51,21 @@ func (c Cache) FindAllConsumers(ctx context.Context) ([]domain.Event, error) { return results, nil } -func (c Cache) FindAllQueues(ctx context.Context) ([]asynqtask.Queue, error) { +func (c Cache) FindAllQueues(ctx context.Context) ([]Queue, error) { const key = "asynq:queues" qResult, err := c.cache.SMembers(ctx, key).Result() if errors.Is(err, redis.Nil) { - return nil, asynqtask.ErrorNotFound + return nil, ErrorNotFound } if err != nil { return nil, fmt.Errorf("failed to get queues: %w", err) } - queues := make([]asynqtask.Queue, len(qResult)) + queues := make([]Queue, len(qResult)) for i, q := range qResult { - queues[i] = asynqtask.Queue(q) + queues[i] = Queue(q) } return queues, nil @@ -92,7 +91,7 @@ func (c Cache) RemoveItemsArchivedTasks(ctx context.Context, queue string, tasks return nil } -func (c Cache) GetMsgArchivedTask(ctx context.Context, queue, taskName string) (asynqtask.RawMsg, error) { +func (c Cache) GetMsgArchivedTask(ctx context.Context, queue, taskName string) (RawMsg, error) { const archivedTaskKey = "asynq:{%s}:t:%s" key := fmt.Sprintf(archivedTaskKey, queue, taskName) result, err := c.cache.HGet(ctx, key, "msg").Result() @@ -100,7 +99,7 @@ func (c Cache) GetMsgArchivedTask(ctx context.Context, queue, taskName string) ( return "", fmt.Errorf("failed to fetch DLQ task: %w", err) } - return asynqtask.RawMsg(result), nil + return RawMsg(result), nil } func (c Cache) RemoveMsgArchivedTask(ctx context.Context, queue, task string) error { diff --git a/internal/wtrhandler/request_handle_asynq.go b/internal/app/taskapp/consumer_handle.go similarity index 76% rename from internal/wtrhandler/request_handle_asynq.go rename to internal/app/taskapp/consumer_handle.go index 377dd3f..f9249c1 100644 --- a/internal/wtrhandler/request_handle_asynq.go +++ b/internal/app/taskapp/consumer_handle.go @@ -1,4 +1,4 @@ -package wtrhandler +package taskapp import ( "context" @@ -10,27 +10,8 @@ import ( "github.com/IsaacDSC/gqueue/pkg/ctxlogger" ) -type RequestPayload struct { - EventName string `json:"event_name"` - Consumer Consumer `json:"consumer"` - Data map[string]any `json:"data"` - Headers map[string]string `json:"headers,omitempty"` -} - -func (p RequestPayload) mergeHeaders(headers map[string]string) map[string]string { - if p.Headers == nil { - p.Headers = make(map[string]string) - } - - for key, value := range headers { - p.Headers[key] = value - } - - return p.Headers -} - type Fetcher interface { - Notify(ctx context.Context, data map[string]any, headers map[string]string, consumer Consumer) error + Notify(ctx context.Context, data map[string]any, headers map[string]string, consumer domain.Consumer) error } type ConsumerInsights interface { @@ -66,6 +47,10 @@ func GetRequestHandle(fetch Fetcher, insights ConsumerInsights) asyncadapter.Han return fmt.Errorf("get payload: %w", err) } + if err := payload.Validate(); err != nil { + return fmt.Errorf("validate payload: %w", err) + } + headers := payload.mergeHeaders(payload.Consumer.Headers) if err := fetch.Notify(ctx, payload.Data, headers, payload.Consumer); err != nil { insertInsights(ctx, payload, started, false) diff --git a/internal/app/taskapp/consumer_handle_test.go b/internal/app/taskapp/consumer_handle_test.go new file mode 100644 index 0000000..9f8d9c0 --- /dev/null +++ b/internal/app/taskapp/consumer_handle_test.go @@ -0,0 +1,488 @@ +package taskapp_test + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "os" + "testing" + + "github.com/IsaacDSC/gqueue/internal/app/taskapp" + "github.com/IsaacDSC/gqueue/internal/domain" + "github.com/IsaacDSC/gqueue/mocks/mocktaskapp" + "github.com/IsaacDSC/gqueue/pkg/asyncadapter" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" +) + +func init() { + // Set required environment variables for tests + os.Setenv("GO_ENV", "test") + os.Setenv("WQ_QUEUES", `{"internal.critical": 7, "internal.high": 5, "internal.medium": 3, "internal.low": 1, "external.critical": 7, "external.high": 5, "external.medium": 3, "external.low": 1}`) + os.Setenv("CACHE_ADDR", "localhost:6379") + os.Setenv("DB_DRIVER", "pg") + os.Setenv("DB_CONNECTION_STRING", "postgresql://test:test@localhost:5432/test?sslmode=disable") + os.Setenv("WQ_CONCURRENCY", "32") +} + +// mockFetcher implements the Fetcher interface for testing +type mockFetcher struct { + notifyTriggerFunc func(ctx context.Context, data map[string]any, headers map[string]string, consumer domain.Consumer) error +} + +func (m *mockFetcher) Notify(ctx context.Context, data map[string]any, headers map[string]string, consumer domain.Consumer) error { + if m.notifyTriggerFunc != nil { + return m.notifyTriggerFunc(ctx, data, headers, consumer) + } + return nil +} +func TestGetRequestHandle(t *testing.T) { + t.Run("returns_correct_queue_name_and_handler", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockFetch := &mockFetcher{} + mockInsights := mocktaskapp.NewMockConsumerInsights(ctrl) + handle := taskapp.GetRequestHandle(mockFetch, mockInsights) + + assert.Equal(t, "event-queue.request-to-external", handle.EventName) + assert.NotNil(t, handle.Handler) + }) +} + +func TestGetRequestHandle_Handler(t *testing.T) { + // Create a test server to mock external webhook endpoint + testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Verify request method + assert.Equal(t, http.MethodPost, r.Method) + + // Verify content type + assert.Equal(t, "application/json", r.Header.Get("Content-Type")) + + // Verify custom headers + assert.Equal(t, "Bearer token", r.Header.Get("Authorization")) + assert.Equal(t, "webhook-client", r.Header.Get("User-Agent")) + + // Read and verify body + var body map[string]interface{} + err := json.NewDecoder(r.Body).Decode(&body) + assert.NoError(t, err) + assert.Equal(t, "123", body["user_id"]) + assert.Equal(t, "test@example.com", body["email"]) + + w.WriteHeader(http.StatusOK) + })) + defer testServer.Close() + + tests := []struct { + name string + payload taskapp.RequestPayload + mockFetcher *mockFetcher + setupMocks func(*mocktaskapp.MockConsumerInsights) + expectedError bool + expectedErrMsg string + }{ + { + name: "successful_request", + payload: taskapp.RequestPayload{ + EventName: "user.created", + Consumer: domain.Consumer{ + ServiceName: "user-service", + BaseUrl: testServer.URL, + Path: "/webhook", + Headers: map[string]string{ + "User-Agent": "webhook-client", + }, + }, + Data: map[string]any{ + "user_id": "123", + "email": "test@example.com", + }, + Headers: map[string]string{ + "Authorization": "Bearer token", + }, + }, + mockFetcher: &mockFetcher{ + notifyTriggerFunc: func(ctx context.Context, data map[string]any, headers map[string]string, consumer domain.Consumer) error { + return nil + }, + }, + setupMocks: func(mockInsights *mocktaskapp.MockConsumerInsights) { + mockInsights.EXPECT(). + Consumed(gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, input domain.ConsumerMetric) error { + assert.Equal(t, "user.created", input.TopicName) + assert.Equal(t, "user-service", input.ConsumerName) + assert.True(t, input.ACK) + assert.NotZero(t, input.TimeStarted) + assert.NotZero(t, input.TimeEnded) + return nil + }). + Times(1) + }, + expectedError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockInsights := mocktaskapp.NewMockConsumerInsights(ctrl) + if tt.setupMocks != nil { + tt.setupMocks(mockInsights) + } + + // Get the handler + handle := taskapp.GetRequestHandle(tt.mockFetcher, mockInsights) + + // Create task payload + taskPayload, err := json.Marshal(tt.payload) + require.NoError(t, err) + + // Create AsyncCtx wrapper + asyncCtx := asyncadapter.NewAsyncCtx[taskapp.RequestPayload](context.Background(), taskPayload) + + // Execute handler + err = handle.Handler(asyncCtx) + + if tt.expectedError { + assert.Error(t, err) + if tt.expectedErrMsg != "" { + assert.Contains(t, err.Error(), tt.expectedErrMsg) + } + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestGetRequestHandle_Handler_InvalidPayload(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockFetch := &mockFetcher{} + mockInsights := mocktaskapp.NewMockConsumerInsights(ctrl) + handle := taskapp.GetRequestHandle(mockFetch, mockInsights) + + // Create AsyncCtx wrapper with invalid payload + asyncCtx := asyncadapter.NewAsyncCtx[taskapp.RequestPayload](context.Background(), []byte("invalid json")) + + err := handle.Handler(asyncCtx) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "unmarshal payload:") +} + +func TestRequestPayload_mergeHeaders_Integration(t *testing.T) { + tests := []struct { + name string + data map[string]any + headers map[string]string + consumer domain.Consumer + mockNotifyTriggerFn func(ctx context.Context, data map[string]any, headers map[string]string, consumer domain.Consumer) error + setupMocks func(*mocktaskapp.MockConsumerInsights) + expectedError bool + expectedErrMsg string + }{ + { + name: "successful_integration_test", + data: map[string]any{ + "user_id": "123", + "email": "test@example.com", + }, + headers: map[string]string{ + "Authorization": "Bearer token", + "User-Agent": "webhook-client", + }, + consumer: domain.Consumer{ + ServiceName: "user-service", + BaseUrl: "http://example.com", + Path: "/webhook", + }, + mockNotifyTriggerFn: func(ctx context.Context, data map[string]any, headers map[string]string, consumer domain.Consumer) error { + // Verify the merged headers are passed correctly + assert.Equal(t, "Bearer token", headers["Authorization"]) + assert.Equal(t, "webhook-client", headers["User-Agent"]) + return nil + }, + setupMocks: func(mockInsights *mocktaskapp.MockConsumerInsights) { + mockInsights.EXPECT(). + Consumed(gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, input domain.ConsumerMetric) error { + assert.Equal(t, "user.created", input.TopicName) + assert.Equal(t, "user-service", input.ConsumerName) + assert.True(t, input.ACK) + return nil + }). + Times(1) + }, + expectedError: false, + }, + { + name: "fetcher_returns_error", + data: map[string]any{ + "user_id": "123", + }, + headers: map[string]string{}, + consumer: domain.Consumer{ + ServiceName: "user-service", + BaseUrl: "http://example.com", + Path: "/webhook", + }, + mockNotifyTriggerFn: func(ctx context.Context, data map[string]any, headers map[string]string, consumer domain.Consumer) error { + return assert.AnError + }, + setupMocks: func(mockInsights *mocktaskapp.MockConsumerInsights) { + mockInsights.EXPECT(). + Consumed(gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, input domain.ConsumerMetric) error { + assert.Equal(t, "user.created", input.TopicName) + assert.Equal(t, "user-service", input.ConsumerName) + assert.False(t, input.ACK) + return nil + }). + Times(1) + }, + expectedError: true, + expectedErrMsg: "fetch consumer:", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockInsights := mocktaskapp.NewMockConsumerInsights(ctrl) + if tt.setupMocks != nil { + tt.setupMocks(mockInsights) + } + + mockFetch := &mockFetcher{ + notifyTriggerFunc: tt.mockNotifyTriggerFn, + } + + handle := taskapp.GetRequestHandle(mockFetch, mockInsights) + + payload := taskapp.RequestPayload{ + EventName: "user.created", + Consumer: tt.consumer, + Data: tt.data, + Headers: tt.headers, + } + + // Create task payload + taskPayload, err := json.Marshal(payload) + require.NoError(t, err) + + // Create AsyncCtx wrapper + asyncCtx := asyncadapter.NewAsyncCtx[taskapp.RequestPayload](context.Background(), taskPayload) + + // Execute handler + err = handle.Handler(asyncCtx) + + if tt.expectedError { + assert.Error(t, err) + if tt.expectedErrMsg != "" { + assert.Contains(t, err.Error(), tt.expectedErrMsg) + } + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestMockFetcher_ErrorScenarios(t *testing.T) { + tests := []struct { + name string + mockError error + expectedErrMsg string + }{ + { + name: "network_error_simulation", + mockError: assert.AnError, + expectedErrMsg: "fetch consumer:", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockInsights := mocktaskapp.NewMockConsumerInsights(ctrl) + mockInsights.EXPECT(). + Consumed(gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, input domain.ConsumerMetric) error { + assert.Equal(t, "user.created", input.TopicName) + assert.Equal(t, "user-service", input.ConsumerName) + assert.False(t, input.ACK) + return nil + }). + Times(1) + + mockFetch := &mockFetcher{ + notifyTriggerFunc: func(ctx context.Context, data map[string]any, headers map[string]string, consumer domain.Consumer) error { + return tt.mockError + }, + } + + handle := taskapp.GetRequestHandle(mockFetch, mockInsights) + + payload := taskapp.RequestPayload{ + EventName: "user.created", + Consumer: domain.Consumer{ + ServiceName: "user-service", + BaseUrl: "http://localhost:99999", // Unreachable port + Path: "/webhook", + }, + Data: map[string]any{"test": "value"}, + Headers: map[string]string{}, + } + + // Create task payload + taskPayload, err := json.Marshal(payload) + require.NoError(t, err) + + // Create AsyncCtx wrapper + asyncCtx := asyncadapter.NewAsyncCtx[taskapp.RequestPayload](context.Background(), taskPayload) + err = handle.Handler(asyncCtx) + + assert.Error(t, err) + assert.Contains(t, err.Error(), tt.expectedErrMsg) + }) + } +} + +func TestGetRequestHandle_HeaderMerging(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // Track the headers received by the mock fetcher + var receivedHeaders map[string]string + + mockInsights := mocktaskapp.NewMockConsumerInsights(ctrl) + mockInsights.EXPECT(). + Consumed(gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, input domain.ConsumerMetric) error { + assert.Equal(t, "user.created", input.TopicName) + assert.Equal(t, "user-service", input.ConsumerName) + assert.True(t, input.ACK) + return nil + }). + Times(1) + + mockFetch := &mockFetcher{ + notifyTriggerFunc: func(ctx context.Context, data map[string]any, headers map[string]string, consumer domain.Consumer) error { + receivedHeaders = headers + return nil + }, + } + + handle := taskapp.GetRequestHandle(mockFetch, mockInsights) + + payload := taskapp.RequestPayload{ + EventName: "user.created", + Consumer: domain.Consumer{ + ServiceName: "user-service", + BaseUrl: "http://example.com", + Path: "/webhook", + Headers: map[string]string{ + "X-Service": "webhook-service", + }, + }, + Data: map[string]any{ + "user_id": "123", + "email": "test@example.com", + }, + Headers: map[string]string{ + "Authorization": "Bearer token", + "User-Agent": "webhook-client", + "X-Custom": "custom-value", + }, + } + + // Create task payload + taskPayload, err := json.Marshal(payload) + require.NoError(t, err) + + // Create AsyncCtx wrapper + asyncCtx := asyncadapter.NewAsyncCtx[taskapp.RequestPayload](context.Background(), taskPayload) + err = handle.Handler(asyncCtx) + require.NoError(t, err) + + // Verify headers were merged correctly (Headers should override Trigger.Headers) + assert.Equal(t, "Bearer token", receivedHeaders["Authorization"]) + assert.Equal(t, "webhook-client", receivedHeaders["User-Agent"]) + assert.Equal(t, "custom-value", receivedHeaders["X-Custom"]) + assert.Equal(t, "webhook-service", receivedHeaders["X-Service"]) +} + +func TestGetRequestHandle_DataPassing(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // Track the data received by the mock fetcher + var receivedData map[string]any + + mockInsights := mocktaskapp.NewMockConsumerInsights(ctrl) + mockInsights.EXPECT(). + Consumed(gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, input domain.ConsumerMetric) error { + assert.Equal(t, "user.created", input.TopicName) + assert.Equal(t, "user-service", input.ConsumerName) + assert.True(t, input.ACK) + return nil + }). + Times(1) + + mockFetch := &mockFetcher{ + notifyTriggerFunc: func(ctx context.Context, data map[string]any, headers map[string]string, consumer domain.Consumer) error { + receivedData = data + return nil + }, + } + + handle := taskapp.GetRequestHandle(mockFetch, mockInsights) + + expectedData := map[string]any{ + "user_id": "123", + "email": "test@example.com", + "metadata": map[string]interface{}{"source": "api"}, + "count": 42, + "is_active": true, + } + + payload := taskapp.RequestPayload{ + EventName: "user.created", + Consumer: domain.Consumer{ + ServiceName: "user-service", + BaseUrl: "http://example.com", + Path: "/webhook", + }, + Data: expectedData, + Headers: map[string]string{}, + } + + // Create task payload + taskPayload, err := json.Marshal(payload) + require.NoError(t, err) + + // Create AsyncCtx wrapper + asyncCtx := asyncadapter.NewAsyncCtx[taskapp.RequestPayload](context.Background(), taskPayload) + err = handle.Handler(asyncCtx) + require.NoError(t, err) + + // Verify data was passed correctly + assert.Equal(t, "123", receivedData["user_id"]) + assert.Equal(t, "test@example.com", receivedData["email"]) + assert.Equal(t, map[string]interface{}{"source": "api"}, receivedData["metadata"]) + assert.Equal(t, float64(42), receivedData["count"]) // JSON numbers are float64 + assert.Equal(t, true, receivedData["is_active"]) +} diff --git a/internal/asynqtask/errors_data.go b/internal/app/taskapp/errors_data.go similarity index 77% rename from internal/asynqtask/errors_data.go rename to internal/app/taskapp/errors_data.go index 7f78858..37840e5 100644 --- a/internal/asynqtask/errors_data.go +++ b/internal/app/taskapp/errors_data.go @@ -1,4 +1,4 @@ -package asynqtask +package taskapp import "errors" diff --git a/internal/asynqtask/fetch_msg_data.go b/internal/app/taskapp/fetch_msg_data.go similarity index 96% rename from internal/asynqtask/fetch_msg_data.go rename to internal/app/taskapp/fetch_msg_data.go index 6c49b41..94f754b 100644 --- a/internal/asynqtask/fetch_msg_data.go +++ b/internal/app/taskapp/fetch_msg_data.go @@ -1,4 +1,4 @@ -package asynqtask +package taskapp import "github.com/IsaacDSC/gqueue/internal/domain" diff --git a/internal/wtrhandler/internal_payload.go b/internal/app/taskapp/internal_payload.go similarity index 97% rename from internal/wtrhandler/internal_payload.go rename to internal/app/taskapp/internal_payload.go index b49fef9..1a420af 100644 --- a/internal/wtrhandler/internal_payload.go +++ b/internal/app/taskapp/internal_payload.go @@ -1,4 +1,4 @@ -package wtrhandler +package taskapp import ( "encoding/json" diff --git a/internal/asynqtask/msg_data.go b/internal/app/taskapp/msg_data.go similarity index 98% rename from internal/asynqtask/msg_data.go rename to internal/app/taskapp/msg_data.go index 2eb4c7d..b0c6611 100644 --- a/internal/asynqtask/msg_data.go +++ b/internal/app/taskapp/msg_data.go @@ -1,4 +1,4 @@ -package asynqtask +package taskapp import ( "encoding/json" diff --git a/internal/app/taskapp/publisher_handle_http.go b/internal/app/taskapp/publisher_handle_http.go new file mode 100644 index 0000000..12fb5ee --- /dev/null +++ b/internal/app/taskapp/publisher_handle_http.go @@ -0,0 +1,149 @@ +package taskapp + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "time" + + "github.com/IsaacDSC/gqueue/internal/domain" + "github.com/IsaacDSC/gqueue/pkg/ctxlogger" + "github.com/IsaacDSC/gqueue/pkg/httpadapter" + "github.com/IsaacDSC/gqueue/pkg/pubadapter" + "github.com/IsaacDSC/gqueue/pkg/topicutils" +) + +type PublisherInsights interface { + Published(ctx context.Context, input domain.PublisherMetric) error +} + +type Store interface { + GetEvent(ctx context.Context, eventName string) (domain.Event, error) +} + +type RequestPayload struct { + EventName string `json:"event_name"` + Consumer domain.Consumer `json:"consumer"` + Data map[string]any `json:"data"` + Headers map[string]string `json:"headers,omitempty"` +} + +func (p RequestPayload) Validate() error { + if p.EventName == "" { + return errors.New("event_name is required") + } + + if p.Consumer.BaseUrl == "" { + return errors.New("consumer.base_url is required") + } + + return nil +} + +func (p RequestPayload) mergeHeaders(headers map[string]string) map[string]string { + if p.Headers == nil { + p.Headers = make(map[string]string) + } + + for key, value := range headers { + p.Headers[key] = value + } + + return p.Headers +} + +func PublisherEvent( + store Store, + adaptpub pubadapter.GenericPublisher, + insights PublisherInsights, +) httpadapter.HttpHandle { + + insertInsights := func(ctx context.Context, payload InternalPayload, started time.Time, isSuccess bool) { + l := ctxlogger.GetLogger(ctx) + finished := time.Now() + + if err := insights.Published(ctx, domain.PublisherMetric{ + TopicName: payload.EventName, + TimeStarted: started, + TimeEnded: finished, + TimeDurationMs: finished.Sub(started).Milliseconds(), + ACK: true, + }); err != nil { + l.Warn("not save metric", "type", "publisher", "error", err.Error()) + } + + } + + return httpadapter.HttpHandle{ + Path: "POST /api/v1/task", + Handler: func(w http.ResponseWriter, r *http.Request) { + started := time.Now() + ctx := r.Context() + l := ctxlogger.GetLogger(ctx) + + var payload InternalPayload + + defer r.Body.Close() + if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + if err := payload.Validate(); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + var err error + defer insertInsights(ctx, payload, started, err == nil) + + event, err := store.GetEvent(ctx, payload.EventName) + if errors.Is(err, domain.EventNotFound) { + err = fmt.Errorf("event not found: %w", err) + http.Error(w, "event not found", http.StatusNotFound) + return + } + if err != nil { + err = fmt.Errorf("get event: %w", err) + l.Error("failed to get event", "error", err.Error()) + http.Error(w, "failed to get event", http.StatusInternalServerError) + return + } + + eventType := event.Type.String() + if eventType == "" { + l.Warn("event type is empty, defaulting to internal", "event_name", event.Name) + eventType = domain.EventTypeInternal.String() + } + + config := event.Option.ToAsynqOptions() + for _, consumer := range event.Consumers { + + input := RequestPayload{ + EventName: event.Name, + Data: payload.Data, + Headers: payload.Metadata.Headers, + Consumer: domain.Consumer{ + ServiceName: consumer.ServiceName, + BaseUrl: consumer.BaseUrl, + Path: consumer.Path, + Headers: consumer.Headers, + }, + } + + topic := topicutils.BuildTopicName(domain.ProjectID, domain.EventQueueRequestToExternal) + opts := pubadapter.Opts{Attributes: make(map[string]string), AsynqOpts: config} + if err = adaptpub.Publish(ctx, topic, input, opts); err != nil { + err = fmt.Errorf("publish event: %w", err) + l.Error("failed to publish event", "error", err.Error()) + http.Error(w, "failed to publish event", http.StatusInternalServerError) + return + } + } + + w.WriteHeader(http.StatusAccepted) + }, + } +} diff --git a/internal/asynqtask/queue_consumer_data.go b/internal/app/taskapp/queue_consumer_data.go similarity index 89% rename from internal/asynqtask/queue_consumer_data.go rename to internal/app/taskapp/queue_consumer_data.go index dff9e60..1a66fb5 100644 --- a/internal/asynqtask/queue_consumer_data.go +++ b/internal/app/taskapp/queue_consumer_data.go @@ -1,4 +1,4 @@ -package asynqtask +package taskapp type Consumer struct { Host string `json:"host"` diff --git a/internal/asynqtask/queues_data.go b/internal/app/taskapp/queues_data.go similarity index 80% rename from internal/asynqtask/queues_data.go rename to internal/app/taskapp/queues_data.go index e72fc1a..616fb3a 100644 --- a/internal/asynqtask/queues_data.go +++ b/internal/app/taskapp/queues_data.go @@ -1,4 +1,4 @@ -package asynqtask +package taskapp type Queue string diff --git a/internal/cfg/env.go b/internal/cfg/env.go index 16ba08d..49bc7c3 100644 --- a/internal/cfg/env.go +++ b/internal/cfg/env.go @@ -54,11 +54,13 @@ type Config struct { ConfigDatabase ConfigDatabase Cache Cache AsynqConfig AsynqConfig - WQ WQ `env:"WQ"` - InternalBaseURL string `env:"INTERNAL_BASE_URL"` - InternalServiceName string `env:"INTERNAL_SERVICE_NAME"` - ApiPort ServerPort `env:"API_PORT" env-default:"8080"` - BackofficePort ServerPort `env:"BACKOFFICE_PORT" env-default:"8081"` + WQ WQ `env:"WQ"` + InternalBaseURL string `env:"INTERNAL_BASE_URL"` + InternalServiceName string `env:"INTERNAL_SERVICE_NAME"` + + PubsubApiPort ServerPort `env:"PUBSUB_API_PORT" env-default:"8082"` + TaskApiPort ServerPort `env:"TASK_API_PORT" env-default:"8082"` + BackofficeApiPort ServerPort `env:"BACKOFFICE_API_PORT" env-default:"8081"` } var cfg Config diff --git a/internal/domain/event.go b/internal/domain/event.go index b8d2135..0bd4f74 100644 --- a/internal/domain/event.go +++ b/internal/domain/event.go @@ -2,6 +2,7 @@ package domain import ( "fmt" + "strings" "time" "github.com/IsaacDSC/gqueue/pkg/intertime" @@ -34,11 +35,17 @@ func (e *Event) Validate() error { type Consumer struct { ServiceName string `json:"service_name" bson:"service_name"` - Host string `json:"host" bson:"host"` + BaseUrl string `json:"host" bson:"base_url"` Path string `json:"path" bson:"path"` Headers map[string]string `json:"headers" bson:"headers"` } +func (t *Consumer) GetUrl() string { + baseURL := strings.TrimSuffix(t.BaseUrl, "/") + path := strings.TrimPrefix(t.Path, "/") + return fmt.Sprintf("%s/%s", baseURL, path) +} + type Opt struct { // TODO: mover para uma conflig global ao invéz de usar por produtor Deadline *time.Time `json:"deadline" bson:"deadline"` diff --git a/internal/fetcher/notification.go b/internal/fetcher/notification.go index ae24839..85cc791 100644 --- a/internal/fetcher/notification.go +++ b/internal/fetcher/notification.go @@ -7,7 +7,7 @@ import ( "fmt" "net/http" - "github.com/IsaacDSC/gqueue/internal/wtrhandler" + "github.com/IsaacDSC/gqueue/internal/domain" "github.com/IsaacDSC/gqueue/pkg/httpclient" ) @@ -17,7 +17,7 @@ func NewNotification() *Notification { return &Notification{} } -func (n Notification) Notify(ctx context.Context, data map[string]any, headers map[string]string, consumer wtrhandler.Consumer) error { +func (n Notification) Notify(ctx context.Context, data map[string]any, headers map[string]string, consumer domain.Consumer) error { url := consumer.GetUrl() return fetch(ctx, url, data, headers) } diff --git a/internal/fetcher/notification_test.go b/internal/fetcher/notification_test.go index fa1923a..15f1447 100644 --- a/internal/fetcher/notification_test.go +++ b/internal/fetcher/notification_test.go @@ -10,7 +10,7 @@ import ( "os" "testing" - "github.com/IsaacDSC/gqueue/internal/wtrhandler" + "github.com/IsaacDSC/gqueue/internal/domain" ) func init() { @@ -28,7 +28,7 @@ func TestNotification_NotifyTrigger(t *testing.T) { name string data map[string]any headers map[string]string - consumer wtrhandler.Consumer + consumer domain.Consumer serverResponse serverResponse wantErr bool errContains string @@ -44,7 +44,7 @@ func TestNotification_NotifyTrigger(t *testing.T) { "Authorization": "Bearer token123", "X-Custom": "custom-value", }, - consumer: wtrhandler.Consumer{ + consumer: domain.Consumer{ ServiceName: "test-service", BaseUrl: "", Path: "/webhook", @@ -64,7 +64,7 @@ func TestNotification_NotifyTrigger(t *testing.T) { headers: map[string]string{ "Content-Type": "application/json", }, - consumer: wtrhandler.Consumer{ + consumer: domain.Consumer{ ServiceName: "user-service", BaseUrl: "", Path: "/users/webhook", @@ -81,7 +81,7 @@ func TestNotification_NotifyTrigger(t *testing.T) { "ping": "pong", }, headers: map[string]string{}, - consumer: wtrhandler.Consumer{ + consumer: domain.Consumer{ ServiceName: "ping-service", BaseUrl: "", Path: "/ping", @@ -98,7 +98,7 @@ func TestNotification_NotifyTrigger(t *testing.T) { "test": "boundary", }, headers: map[string]string{}, - consumer: wtrhandler.Consumer{ + consumer: domain.Consumer{ ServiceName: "boundary-service", BaseUrl: "", Path: "/boundary", @@ -115,7 +115,7 @@ func TestNotification_NotifyTrigger(t *testing.T) { "invalid": "data", }, headers: map[string]string{}, - consumer: wtrhandler.Consumer{ + consumer: domain.Consumer{ ServiceName: "validation-service", BaseUrl: "", Path: "/validate", @@ -133,7 +133,7 @@ func TestNotification_NotifyTrigger(t *testing.T) { "sensitive": "data", }, headers: map[string]string{}, - consumer: wtrhandler.Consumer{ + consumer: domain.Consumer{ ServiceName: "auth-service", BaseUrl: "", Path: "/secure", @@ -151,7 +151,7 @@ func TestNotification_NotifyTrigger(t *testing.T) { "event": "not-found", }, headers: map[string]string{}, - consumer: wtrhandler.Consumer{ + consumer: domain.Consumer{ ServiceName: "missing-service", BaseUrl: "", Path: "/missing", @@ -169,7 +169,7 @@ func TestNotification_NotifyTrigger(t *testing.T) { "event": "server-error", }, headers: map[string]string{}, - consumer: wtrhandler.Consumer{ + consumer: domain.Consumer{ ServiceName: "error-service", BaseUrl: "", Path: "/error", @@ -187,7 +187,7 @@ func TestNotification_NotifyTrigger(t *testing.T) { "redirect": "test", }, headers: map[string]string{}, - consumer: wtrhandler.Consumer{ + consumer: domain.Consumer{ ServiceName: "redirect-service", BaseUrl: "", Path: "/redirect", @@ -205,7 +205,7 @@ func TestNotification_NotifyTrigger(t *testing.T) { "complex": "url", }, headers: map[string]string{}, - consumer: wtrhandler.Consumer{ + consumer: domain.Consumer{ ServiceName: "complex-service", BaseUrl: "", Path: "/api/v1/webhooks", @@ -255,7 +255,7 @@ func TestNotification_NotifyTrigger_InvalidData(t *testing.T) { "channel": make(chan int), } - trigger := wtrhandler.Consumer{ + trigger := domain.Consumer{ ServiceName: "test-service", BaseUrl: "http://example.com", Path: "/webhook", diff --git a/internal/interstore/mem_store.go b/internal/interstore/mem_store.go index 63e7dfd..cae94e1 100644 --- a/internal/interstore/mem_store.go +++ b/internal/interstore/mem_store.go @@ -2,26 +2,49 @@ package interstore import ( "context" + "errors" + "fmt" "sync/atomic" "github.com/IsaacDSC/gqueue/internal/domain" "github.com/IsaacDSC/gqueue/pkg/ctxlogger" ) +type PersistentStore interface { + GetAllEvents(ctx context.Context) ([]domain.Event, error) +} + type MemStore struct { topicEvents atomic.Value retryTopics atomic.Value tag string + + persitentStore PersistentStore } -func NewMemStore() *MemStore { +func NewMemStore(persistentStore PersistentStore) *MemStore { ms := &MemStore{} ms.topicEvents.Store(make(map[string]domain.Event)) ms.retryTopics.Store(make(map[string]domain.Event)) ms.tag = "mem_store" + ms.persitentStore = persistentStore + return ms } +func (ms *MemStore) LoadInMemStore(ctx context.Context) error { + l := ctxlogger.GetLogger(ctx) + events, err := ms.persitentStore.GetAllEvents(ctx) + if err != nil && !errors.Is(err, domain.EventNotFound) { + l.Error("Error loading events into in-memory store", "error", err) + return fmt.Errorf("error loading events into in-memory store: %w", err) + } + + ms.Refresh(ctx, events) + + return nil +} + func (ms *MemStore) GetEvent(ctx context.Context, eventName string) (domain.Event, error) { l := ctxlogger.GetLogger(ctx) diff --git a/internal/interstore/mem_store_test.go b/internal/interstore/mem_store_test.go index 54785a2..27395cf 100644 --- a/internal/interstore/mem_store_test.go +++ b/internal/interstore/mem_store_test.go @@ -5,7 +5,9 @@ import ( "testing" "github.com/IsaacDSC/gqueue/internal/domain" + "github.com/IsaacDSC/gqueue/mocks/mockinterstore" "github.com/google/uuid" + "go.uber.org/mock/gomock" ) func TestMemStore_GetEvent(t *testing.T) { @@ -92,9 +94,12 @@ func TestMemStore_GetEvent(t *testing.T) { }, } + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockStore := mockinterstore.NewMockPersistentStore(ctrl) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - ms := NewMemStore() + ms := NewMemStore(mockStore) ctx := context.Background() ms.Refresh(ctx, tt.setupEvents) @@ -243,9 +248,12 @@ func TestMemStore_Refresh(t *testing.T) { }, } + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockStore := mockinterstore.NewMockPersistentStore(ctrl) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - ms := NewMemStore() + ms := NewMemStore(mockStore) ctx := context.Background() if len(tt.initialEvents) > 0 { @@ -276,7 +284,10 @@ func TestMemStore_Refresh(t *testing.T) { } func TestMemStore_DuplicateEventLastWins(t *testing.T) { - ms := NewMemStore() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockStore := mockinterstore.NewMockPersistentStore(ctrl) + ms := NewMemStore(mockStore) ctx := context.Background() events := []domain.Event{ diff --git a/internal/wtrhandler/external_handle_http_test.go b/internal/wtrhandler/external_handle_http_test.go deleted file mode 100644 index 0560330..0000000 --- a/internal/wtrhandler/external_handle_http_test.go +++ /dev/null @@ -1,454 +0,0 @@ -package wtrhandler - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "net/http" - "net/http/httptest" - "testing" - - "github.com/IsaacDSC/gqueue/internal/cfg" - "github.com/IsaacDSC/gqueue/internal/domain" - "github.com/IsaacDSC/gqueue/pkg/pubadapter" - "github.com/stretchr/testify/assert" - "go.uber.org/mock/gomock" -) - -func TestGetExternalHandle(t *testing.T) { - // Setup test configuration with valid queues - testConfig := cfg.Config{ - AsynqConfig: cfg.AsynqConfig{}, - } - cfg.SetConfig(testConfig) - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockPublisher := pubadapter.NewMockGenericPublisher(ctrl) - - tests := []struct { - name string - payload InternalPayload - setupMock func(*pubadapter.MockGenericPublisher) - setupStoreMock func(*MockStore) - expectedStatus int - expectedError bool - }{ - { - name: "successful_publish_minimal_payload", - payload: InternalPayload{ - EventName: "user.created", - Data: Data{ - "user_id": "123", - "email": "test@example.com", - }, - Metadata: Metadata{ - Source: "api", - Version: "1.0", - Environment: "test", - Headers: map[string]string{ - "Content-Type": "application/json", - }, - }, - }, - setupStoreMock: func(s *MockStore) { - s.EXPECT().GetEvent(gomock.Any(), "user.created").Return(domain.Event{ - Name: "user.created", - Type: domain.EventTypeInternal, - Consumers: []domain.Consumer{{ - ServiceName: "test-service", - Host: "http://localhost:8080", - Path: "/webhook", - }}, - }, nil).Times(1) - }, - setupMock: func(m *pubadapter.MockGenericPublisher) { - m.EXPECT(). - Publish( - gomock.Any(), - "your-project-id-event-queue-request-to-external", - gomock.AssignableToTypeOf(RequestPayload{}), - gomock.Any(), - ). - Do(func(ctx context.Context, eventName string, payload RequestPayload, opts pubadapter.Opts) { - // Validate payload content using assert - assert.Equal(t, "user.created", payload.EventName) - assert.Equal(t, "123", payload.Data["user_id"]) - assert.Equal(t, "test@example.com", payload.Data["email"]) - }). - Return(nil). - Times(1) - }, - expectedStatus: http.StatusAccepted, - expectedError: false, - }, - { - name: "successful_publish_full_payload", - payload: InternalPayload{ - EventName: "order.completed", - Data: Data{ - "order_id": "ord_123456", - "customer_id": "cust_789", - "amount": 99.99, - "items": []map[string]any{ - {"id": "item_1", "quantity": 2}, - {"id": "item_2", "quantity": 1}, - }, - }, - Metadata: Metadata{ - Source: "checkout-service", - Version: "2.1", - Environment: "production", - Headers: map[string]string{ - "Content-Type": "application/json", - "Authorization": "Bearer token123", - "X-Request-ID": "req_abc123", - "X-Correlation": "corr_def456", - }, - }, - }, - setupStoreMock: func(s *MockStore) { - s.EXPECT().GetEvent(gomock.Any(), "order.completed").Return(domain.Event{ - Name: "order.completed", - Type: domain.EventTypeInternal, - Consumers: []domain.Consumer{{ - ServiceName: "test-service", - Host: "http://localhost:8080", - Path: "/webhook", - }}, - }, nil).Times(1) - }, - setupMock: func(m *pubadapter.MockGenericPublisher) { - m.EXPECT(). - Publish( - gomock.Any(), - "your-project-id-event-queue-request-to-external", - gomock.AssignableToTypeOf(RequestPayload{}), - gomock.Any(), - ). - Do(func(ctx context.Context, eventName string, payload RequestPayload, opts pubadapter.Opts) { - // Validate payload content using assert - assert.Equal(t, "order.completed", payload.EventName) - assert.Equal(t, "ord_123456", payload.Data["order_id"]) - assert.Equal(t, 99.99, payload.Data["amount"]) - // Validate headers - assert.Equal(t, "Bearer token123", payload.Headers["Authorization"]) - }). - Return(nil). - Times(1) - }, - expectedStatus: http.StatusAccepted, - expectedError: false, - }, - { - name: "successful_publish_with_deadline", - payload: InternalPayload{ - EventName: "notification.send", - Data: Data{ - "user_id": "user_456", - "message": "Welcome to our platform!", - "channel": "email", - "template_id": "welcome_template", - }, - Metadata: Metadata{ - Source: "notification-service", - Version: "1.5", - Environment: "staging", - Headers: map[string]string{ - "Content-Type": "application/json", - "X-Service": "notification-worker", - }, - }, - }, - setupStoreMock: func(s *MockStore) { - s.EXPECT().GetEvent(gomock.Any(), "notification.send").Return(domain.Event{ - Name: "notification.send", - Type: domain.EventTypeInternal, - Consumers: []domain.Consumer{{ - ServiceName: "test-service", - Host: "http://localhost:8080", - Path: "/webhook", - }}, - }, nil).Times(1) - }, - setupMock: func(m *pubadapter.MockGenericPublisher) { - m.EXPECT(). - Publish( - gomock.Any(), - "your-project-id-event-queue-request-to-external", - gomock.AssignableToTypeOf(RequestPayload{}), - gomock.Any(), - ). - Do(func(ctx context.Context, eventName string, payload RequestPayload, opts pubadapter.Opts) { - // Validate payload content using assert - assert.Equal(t, "notification.send", payload.EventName) - assert.Equal(t, "user_456", payload.Data["user_id"]) - assert.Equal(t, "Welcome to our platform!", payload.Data["message"]) - assert.Equal(t, "email", payload.Data["channel"]) - // Validate headers - assert.Equal(t, "notification-worker", payload.Headers["X-Service"]) - }). - Return(nil). - Times(1) - }, - expectedStatus: http.StatusAccepted, - expectedError: false, - }, - { - name: "publisher_error", - payload: InternalPayload{ - EventName: "payment.failed", - Data: Data{ - "payment_id": "pay_error123", - "reason": "insufficient_funds", - }, - Metadata: Metadata{ - Source: "payment-service", - Version: "1.0", - Environment: "test", - }, - }, - setupStoreMock: func(s *MockStore) { - s.EXPECT().GetEvent(gomock.Any(), "payment.failed").Return(domain.Event{ - Name: "payment.failed", - Type: domain.EventTypeInternal, - Consumers: []domain.Consumer{{ - ServiceName: "test-service", - Host: "http://localhost:8080", - Path: "/webhook", - }}, - }, nil).Times(1) - }, - setupMock: func(m *pubadapter.MockGenericPublisher) { - m.EXPECT(). - Publish( - gomock.Any(), - "your-project-id-event-queue-request-to-external", - gomock.AssignableToTypeOf(RequestPayload{}), - gomock.Any(), - ). - Do(func(ctx context.Context, eventName string, payload RequestPayload, opts pubadapter.Opts) { - // Validate payload even on error scenario using assert - assert.Equal(t, "payment.failed", payload.EventName) - assert.Equal(t, "pay_error123", payload.Data["payment_id"]) - assert.Equal(t, "insufficient_funds", payload.Data["reason"]) - }). - Return(errors.New("publisher error")). - Times(1) - }, - expectedStatus: http.StatusInternalServerError, - expectedError: true, - }, - { - name: "empty_payload_with_defaults", - payload: InternalPayload{ - EventName: "system.ping", - Data: Data{}, - Metadata: Metadata{ - Source: "health-check", - Version: "1.0", - Environment: "test", - }, - }, - setupStoreMock: func(s *MockStore) { - s.EXPECT().GetEvent(gomock.Any(), "system.ping").Return(domain.Event{ - Name: "system.ping", - Type: domain.EventTypeInternal, - Consumers: []domain.Consumer{{ - ServiceName: "test-service", - Host: "http://localhost:8080", - Path: "/webhook", - }}, - }, nil).Times(1) - }, - setupMock: func(m *pubadapter.MockGenericPublisher) { - m.EXPECT(). - Publish( - gomock.Any(), - "your-project-id-event-queue-request-to-external", - gomock.AssignableToTypeOf(RequestPayload{}), - gomock.Any(), - ). - Do(func(ctx context.Context, eventName string, payload RequestPayload, opts pubadapter.Opts) { - // Validate empty/default payload using assert - assert.Equal(t, "system.ping", payload.EventName) - assert.Empty(t, payload.Data) - }). - Return(nil). - Times(1) - }, - expectedStatus: http.StatusAccepted, - expectedError: false, - }, - { - name: "get_event_returns_event_not_found", - payload: InternalPayload{ - EventName: "nonexistent.event", - Data: Data{ - "key": "value", - }, - Metadata: Metadata{ - Source: "test-service", - Version: "1.0", - Environment: "test", - }, - }, - setupStoreMock: func(s *MockStore) { - s.EXPECT().GetEvent(gomock.Any(), "nonexistent.event").Return(domain.Event{}, domain.EventNotFound).Times(1) - }, - setupMock: func(m *pubadapter.MockGenericPublisher) { - // No publish should be called when event is not found - }, - expectedStatus: http.StatusNotFound, - expectedError: true, - }, - { - name: "get_event_returns_other_error", - payload: InternalPayload{ - EventName: "error.event", - Data: Data{ - "key": "value", - }, - Metadata: Metadata{ - Source: "test-service", - Version: "1.0", - Environment: "test", - }, - }, - setupStoreMock: func(s *MockStore) { - s.EXPECT().GetEvent(gomock.Any(), "error.event").Return(domain.Event{}, errors.New("database connection error")).Times(1) - }, - setupMock: func(m *pubadapter.MockGenericPublisher) { - // No publish should be called when store returns error - }, - expectedStatus: http.StatusInternalServerError, - expectedError: true, - }, - { - name: "event_with_zero_consumers", - payload: InternalPayload{ - EventName: "no.consumers.event", - Data: Data{ - "key": "value", - }, - Metadata: Metadata{ - Source: "test-service", - Version: "1.0", - Environment: "test", - }, - }, - setupStoreMock: func(s *MockStore) { - s.EXPECT().GetEvent(gomock.Any(), "no.consumers.event").Return(domain.Event{ - Name: "no.consumers.event", - Type: domain.EventTypeInternal, - Consumers: []domain.Consumer{}, // Empty consumers - }, nil).Times(1) - }, - setupMock: func(m *pubadapter.MockGenericPublisher) { - // No publish should be called when there are no consumers - }, - expectedStatus: http.StatusAccepted, - expectedError: false, - }, - { - name: "event_with_multiple_consumers", - payload: InternalPayload{ - EventName: "multi.consumer.event", - Data: Data{ - "user_id": "456", - }, - Metadata: Metadata{ - Source: "multi-service", - Version: "1.0", - Environment: "test", - Headers: map[string]string{ - "Content-Type": "application/json", - }, - }, - }, - setupStoreMock: func(s *MockStore) { - s.EXPECT().GetEvent(gomock.Any(), "multi.consumer.event").Return(domain.Event{ - Name: "multi.consumer.event", - Type: domain.EventTypeInternal, - Consumers: []domain.Consumer{ - { - ServiceName: "service-one", - Host: "http://localhost:8081", - Path: "/webhook1", - }, - { - ServiceName: "service-two", - Host: "http://localhost:8082", - Path: "/webhook2", - }, - { - ServiceName: "service-three", - Host: "http://localhost:8083", - Path: "/webhook3", - }, - }, - }, nil).Times(1) - }, - setupMock: func(m *pubadapter.MockGenericPublisher) { - m.EXPECT(). - Publish( - gomock.Any(), - "your-project-id-event-queue-request-to-external", - gomock.AssignableToTypeOf(RequestPayload{}), - gomock.Any(), - ). - Return(nil). - Times(3) // Should be called once for each consumer - }, - expectedStatus: http.StatusAccepted, - expectedError: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Setup mock expectations - tt.setupMock(mockPublisher) - - // Create mocks for Store and PublisherInsights using generated mocks - store := NewMockStore(ctrl) - tt.setupStoreMock(store) - - insights := NewMockPublisherInsights(ctrl) - insights.EXPECT().Published(gomock.Any(), gomock.Any()).Return(nil).AnyTimes() - - // Create handler - httpHandle := PublisherEvent(store, mockPublisher, insights) - - // Prepare request - payloadBytes, err := json.Marshal(tt.payload) - if err != nil { - t.Fatalf("Failed to marshal payload: %v", err) - } - - req := httptest.NewRequest(http.MethodPost, "/webhook", bytes.NewReader(payloadBytes)) - req.Header.Set("Content-Type", "application/json") - - // Add context - ctx := context.WithValue(req.Context(), "test", true) - req = req.WithContext(ctx) - - // Create response recorder - rr := httptest.NewRecorder() - - // Execute handler - httpHandle.Handler(rr, req) // Verify status code - assert.Equal(t, tt.expectedStatus, rr.Code) - - // Verify error response body if expected - if tt.expectedError { - assert.NotEmpty(t, rr.Body.String(), "Expected error response body, but got empty body") - } else { - // Verify success response has no body (just status) - assert.Empty(t, rr.Body.String(), "Expected empty response body for success") - } - }) - } -} diff --git a/internal/wtrhandler/trigger.go b/internal/wtrhandler/trigger.go deleted file mode 100644 index 1a9ba2e..0000000 --- a/internal/wtrhandler/trigger.go +++ /dev/null @@ -1,19 +0,0 @@ -package wtrhandler - -import ( - "fmt" - "strings" -) - -type Consumer struct { - ServiceName string `json:"service_name"` - BaseUrl string `json:"base_url"` - Path string `json:"path"` - Headers map[string]string `bson:"headers"` -} - -func (t *Consumer) GetUrl() string { - baseURL := strings.TrimSuffix(t.BaseUrl, "/") - path := strings.TrimPrefix(t.Path, "/") - return fmt.Sprintf("%s/%s", baseURL, path) -} diff --git a/mocks/mockbackoffice/mock_httpserver.go b/mocks/mockbackoffice/mock_httpserver.go new file mode 100644 index 0000000..3eac7e9 --- /dev/null +++ b/mocks/mockbackoffice/mock_httpserver.go @@ -0,0 +1,57 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: cmd/setup/backoffice/httpserver.go +// +// Generated by this command: +// +// mockgen -source=cmd/setup/backoffice/httpserver.go -destination=./mocks/mockbackoffice/mock_httpserver.go -package=mockbackoffice +// + +// Package mockbackoffice is a generated GoMock package. +package mockbackoffice + +import ( + context "context" + reflect "reflect" + + domain "github.com/IsaacDSC/gqueue/internal/domain" + gomock "go.uber.org/mock/gomock" +) + +// MockInsightsStore is a mock of InsightsStore interface. +type MockInsightsStore struct { + ctrl *gomock.Controller + recorder *MockInsightsStoreMockRecorder + isgomock struct{} +} + +// MockInsightsStoreMockRecorder is the mock recorder for MockInsightsStore. +type MockInsightsStoreMockRecorder struct { + mock *MockInsightsStore +} + +// NewMockInsightsStore creates a new mock instance. +func NewMockInsightsStore(ctrl *gomock.Controller) *MockInsightsStore { + mock := &MockInsightsStore{ctrl: ctrl} + mock.recorder = &MockInsightsStoreMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockInsightsStore) EXPECT() *MockInsightsStoreMockRecorder { + return m.recorder +} + +// GetAll mocks base method. +func (m *MockInsightsStore) GetAll(ctx context.Context) (domain.Metrics, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAll", ctx) + ret0, _ := ret[0].(domain.Metrics) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAll indicates an expected call of GetAll. +func (mr *MockInsightsStoreMockRecorder) GetAll(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAll", reflect.TypeOf((*MockInsightsStore)(nil).GetAll), ctx) +} diff --git a/mocks/mockbackofficeapp/mock_insights_handle.go b/mocks/mockbackofficeapp/mock_insights_handle.go new file mode 100644 index 0000000..a7cc03c --- /dev/null +++ b/mocks/mockbackofficeapp/mock_insights_handle.go @@ -0,0 +1,57 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: internal/app/backofficeapp/insights_handle.go +// +// Generated by this command: +// +// mockgen -source=internal/app/backofficeapp/insights_handle.go -destination=./mocks/mockbackofficeapp/mock_insights_handle.go -package=mockbackofficeapp +// + +// Package mockbackofficeapp is a generated GoMock package. +package mockbackofficeapp + +import ( + context "context" + reflect "reflect" + + domain "github.com/IsaacDSC/gqueue/internal/domain" + gomock "go.uber.org/mock/gomock" +) + +// MockInsightsStore is a mock of InsightsStore interface. +type MockInsightsStore struct { + ctrl *gomock.Controller + recorder *MockInsightsStoreMockRecorder + isgomock struct{} +} + +// MockInsightsStoreMockRecorder is the mock recorder for MockInsightsStore. +type MockInsightsStoreMockRecorder struct { + mock *MockInsightsStore +} + +// NewMockInsightsStore creates a new mock instance. +func NewMockInsightsStore(ctrl *gomock.Controller) *MockInsightsStore { + mock := &MockInsightsStore{ctrl: ctrl} + mock.recorder = &MockInsightsStoreMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockInsightsStore) EXPECT() *MockInsightsStoreMockRecorder { + return m.recorder +} + +// GetAll mocks base method. +func (m *MockInsightsStore) GetAll(ctx context.Context) (domain.Metrics, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAll", ctx) + ret0, _ := ret[0].(domain.Metrics) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAll indicates an expected call of GetAll. +func (mr *MockInsightsStoreMockRecorder) GetAll(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAll", reflect.TypeOf((*MockInsightsStore)(nil).GetAll), ctx) +} diff --git a/internal/backoffice/repository_mock.go b/mocks/mockbackofficeapp/mock_interfaces.go similarity index 93% rename from internal/backoffice/repository_mock.go rename to mocks/mockbackofficeapp/mock_interfaces.go index 02d866c..cbdd3b4 100644 --- a/internal/backoffice/repository_mock.go +++ b/mocks/mockbackofficeapp/mock_interfaces.go @@ -1,13 +1,13 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: internal/backoffice/register_consumer.go +// Source: internal/app/backofficeapp/interfaces.go // // Generated by this command: // -// mockgen -source=internal/backoffice/register_consumer.go -destination=internal/backoffice/repository_mock.go -package=backoffice +// mockgen -source=internal/app/backofficeapp/interfaces.go -destination=./mocks/mockbackofficeapp/mock_interfaces.go -package=mockbackofficeapp // -// Package backoffice is a generated GoMock package. -package backoffice +// Package mockbackofficeapp is a generated GoMock package. +package mockbackofficeapp import ( context "context" diff --git a/mocks/mockinterstore/mock_mem_store.go b/mocks/mockinterstore/mock_mem_store.go new file mode 100644 index 0000000..072bdb5 --- /dev/null +++ b/mocks/mockinterstore/mock_mem_store.go @@ -0,0 +1,57 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: internal/interstore/mem_store.go +// +// Generated by this command: +// +// mockgen -source=internal/interstore/mem_store.go -destination=./mocks/mockinterstore/mock_mem_store.go -package=mockinterstore +// + +// Package mockinterstore is a generated GoMock package. +package mockinterstore + +import ( + context "context" + reflect "reflect" + + domain "github.com/IsaacDSC/gqueue/internal/domain" + gomock "go.uber.org/mock/gomock" +) + +// MockPersistentStore is a mock of PersistentStore interface. +type MockPersistentStore struct { + ctrl *gomock.Controller + recorder *MockPersistentStoreMockRecorder + isgomock struct{} +} + +// MockPersistentStoreMockRecorder is the mock recorder for MockPersistentStore. +type MockPersistentStoreMockRecorder struct { + mock *MockPersistentStore +} + +// NewMockPersistentStore creates a new mock instance. +func NewMockPersistentStore(ctrl *gomock.Controller) *MockPersistentStore { + mock := &MockPersistentStore{ctrl: ctrl} + mock.recorder = &MockPersistentStoreMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockPersistentStore) EXPECT() *MockPersistentStoreMockRecorder { + return m.recorder +} + +// GetAllEvents mocks base method. +func (m *MockPersistentStore) GetAllEvents(ctx context.Context) ([]domain.Event, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAllEvents", ctx) + ret0, _ := ret[0].([]domain.Event) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAllEvents indicates an expected call of GetAllEvents. +func (mr *MockPersistentStoreMockRecorder) GetAllEvents(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllEvents", reflect.TypeOf((*MockPersistentStore)(nil).GetAllEvents), ctx) +} diff --git a/mocks/mockinterstore/mock_store.go b/mocks/mockinterstore/mock_store.go new file mode 100644 index 0000000..116f15f --- /dev/null +++ b/mocks/mockinterstore/mock_store.go @@ -0,0 +1,160 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: internal/interstore/store.go +// +// Generated by this command: +// +// mockgen -source=internal/interstore/store.go -destination=./mocks/mockinterstore/mock_store.go -package=mockinterstore +// + +// Package mockinterstore is a generated GoMock package. +package mockinterstore + +import ( + context "context" + reflect "reflect" + + domain "github.com/IsaacDSC/gqueue/internal/domain" + uuid "github.com/google/uuid" + gomock "go.uber.org/mock/gomock" +) + +// MockRepository is a mock of Repository interface. +type MockRepository struct { + ctrl *gomock.Controller + recorder *MockRepositoryMockRecorder + isgomock struct{} +} + +// MockRepositoryMockRecorder is the mock recorder for MockRepository. +type MockRepositoryMockRecorder struct { + mock *MockRepository +} + +// NewMockRepository creates a new mock instance. +func NewMockRepository(ctrl *gomock.Controller) *MockRepository { + mock := &MockRepository{ctrl: ctrl} + mock.recorder = &MockRepositoryMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockRepository) EXPECT() *MockRepositoryMockRecorder { + return m.recorder +} + +// DisabledEvent mocks base method. +func (m *MockRepository) DisabledEvent(ctx context.Context, eventID uuid.UUID) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DisabledEvent", ctx, eventID) + ret0, _ := ret[0].(error) + return ret0 +} + +// DisabledEvent indicates an expected call of DisabledEvent. +func (mr *MockRepositoryMockRecorder) DisabledEvent(ctx, eventID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DisabledEvent", reflect.TypeOf((*MockRepository)(nil).DisabledEvent), ctx, eventID) +} + +// GetAllEvents mocks base method. +func (m *MockRepository) GetAllEvents(ctx context.Context) ([]domain.Event, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAllEvents", ctx) + ret0, _ := ret[0].([]domain.Event) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAllEvents indicates an expected call of GetAllEvents. +func (mr *MockRepositoryMockRecorder) GetAllEvents(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllEvents", reflect.TypeOf((*MockRepository)(nil).GetAllEvents), ctx) +} + +// GetAllSchedulers mocks base method. +func (m *MockRepository) GetAllSchedulers(ctx context.Context, state string) ([]domain.Event, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAllSchedulers", ctx, state) + ret0, _ := ret[0].([]domain.Event) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAllSchedulers indicates an expected call of GetAllSchedulers. +func (mr *MockRepositoryMockRecorder) GetAllSchedulers(ctx, state any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllSchedulers", reflect.TypeOf((*MockRepository)(nil).GetAllSchedulers), ctx, state) +} + +// GetEventByID mocks base method. +func (m *MockRepository) GetEventByID(ctx context.Context, eventID uuid.UUID) (domain.Event, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetEventByID", ctx, eventID) + ret0, _ := ret[0].(domain.Event) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetEventByID indicates an expected call of GetEventByID. +func (mr *MockRepositoryMockRecorder) GetEventByID(ctx, eventID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEventByID", reflect.TypeOf((*MockRepository)(nil).GetEventByID), ctx, eventID) +} + +// GetInternalEvent mocks base method. +func (m *MockRepository) GetInternalEvent(ctx context.Context, eventName string) (domain.Event, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetInternalEvent", ctx, eventName) + ret0, _ := ret[0].(domain.Event) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetInternalEvent indicates an expected call of GetInternalEvent. +func (mr *MockRepositoryMockRecorder) GetInternalEvent(ctx, eventName any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInternalEvent", reflect.TypeOf((*MockRepository)(nil).GetInternalEvent), ctx, eventName) +} + +// GetInternalEvents mocks base method. +func (m *MockRepository) GetInternalEvents(ctx context.Context, filters domain.FilterEvents) ([]domain.Event, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetInternalEvents", ctx, filters) + ret0, _ := ret[0].([]domain.Event) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetInternalEvents indicates an expected call of GetInternalEvents. +func (mr *MockRepositoryMockRecorder) GetInternalEvents(ctx, filters any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInternalEvents", reflect.TypeOf((*MockRepository)(nil).GetInternalEvents), ctx, filters) +} + +// UpdateEvent mocks base method. +func (m *MockRepository) UpdateEvent(ctx context.Context, event domain.Event) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateEvent", ctx, event) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdateEvent indicates an expected call of UpdateEvent. +func (mr *MockRepositoryMockRecorder) UpdateEvent(ctx, event any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateEvent", reflect.TypeOf((*MockRepository)(nil).UpdateEvent), ctx, event) +} + +// Upsert mocks base method. +func (m *MockRepository) Upsert(ctx context.Context, event domain.Event) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Upsert", ctx, event) + ret0, _ := ret[0].(error) + return ret0 +} + +// Upsert indicates an expected call of Upsert. +func (mr *MockRepositoryMockRecorder) Upsert(ctx, event any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Upsert", reflect.TypeOf((*MockRepository)(nil).Upsert), ctx, event) +} diff --git a/pkg/publisher/publisher_task_mock.go b/mocks/mockpubadapter/mock_adapter.go similarity index 93% rename from pkg/publisher/publisher_task_mock.go rename to mocks/mockpubadapter/mock_adapter.go index ca56bd0..16f092a 100644 --- a/pkg/publisher/publisher_task_mock.go +++ b/mocks/mockpubadapter/mock_adapter.go @@ -3,11 +3,11 @@ // // Generated by this command: // -// mockgen -source=pkg/pubadapter/adapter.go -destination=pkg/publisher/publisher_task_mock.go -package=publisher +// mockgen -source=pkg/pubadapter/adapter.go -destination=./mocks/mockpubadapter/mock_adapter.go -package=mockpubadapter // -// Package publisher is a generated GoMock package. -package publisher +// Package mockpubadapter is a generated GoMock package. +package mockpubadapter import ( context "context" diff --git a/mocks/mockpubsub/mock_base.go b/mocks/mockpubsub/mock_base.go new file mode 100644 index 0000000..a3b81b5 --- /dev/null +++ b/mocks/mockpubsub/mock_base.go @@ -0,0 +1,72 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: cmd/setup/pubsub/base.go +// +// Generated by this command: +// +// mockgen -source=cmd/setup/pubsub/base.go -destination=./mocks/mockpubsub/mock_base.go -package=mockpubsub +// + +// Package mockpubsub is a generated GoMock package. +package mockpubsub + +import ( + context "context" + reflect "reflect" + + domain "github.com/IsaacDSC/gqueue/internal/domain" + gomock "go.uber.org/mock/gomock" +) + +// MockPersistentRepository is a mock of PersistentRepository interface. +type MockPersistentRepository struct { + ctrl *gomock.Controller + recorder *MockPersistentRepositoryMockRecorder + isgomock struct{} +} + +// MockPersistentRepositoryMockRecorder is the mock recorder for MockPersistentRepository. +type MockPersistentRepositoryMockRecorder struct { + mock *MockPersistentRepository +} + +// NewMockPersistentRepository creates a new mock instance. +func NewMockPersistentRepository(ctrl *gomock.Controller) *MockPersistentRepository { + mock := &MockPersistentRepository{ctrl: ctrl} + mock.recorder = &MockPersistentRepositoryMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockPersistentRepository) EXPECT() *MockPersistentRepositoryMockRecorder { + return m.recorder +} + +// GetAllEvents mocks base method. +func (m *MockPersistentRepository) GetAllEvents(ctx context.Context) ([]domain.Event, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAllEvents", ctx) + ret0, _ := ret[0].([]domain.Event) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAllEvents indicates an expected call of GetAllEvents. +func (mr *MockPersistentRepositoryMockRecorder) GetAllEvents(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllEvents", reflect.TypeOf((*MockPersistentRepository)(nil).GetAllEvents), ctx) +} + +// GetAllSchedulers mocks base method. +func (m *MockPersistentRepository) GetAllSchedulers(ctx context.Context, state string) ([]domain.Event, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAllSchedulers", ctx, state) + ret0, _ := ret[0].([]domain.Event) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAllSchedulers indicates an expected call of GetAllSchedulers. +func (mr *MockPersistentRepositoryMockRecorder) GetAllSchedulers(ctx, state any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllSchedulers", reflect.TypeOf((*MockPersistentRepository)(nil).GetAllSchedulers), ctx, state) +} diff --git a/mocks/mockpubsubapp/mock_consumer_handle.go b/mocks/mockpubsubapp/mock_consumer_handle.go new file mode 100644 index 0000000..0d796e2 --- /dev/null +++ b/mocks/mockpubsubapp/mock_consumer_handle.go @@ -0,0 +1,94 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: internal/app/pubsubapp/consumer_handle.go +// +// Generated by this command: +// +// mockgen -source=internal/app/pubsubapp/consumer_handle.go -destination=./mocks/mockpubsubapp/mock_consumer_handle.go -package=mockpubsubapp +// + +// Package mockpubsubapp is a generated GoMock package. +package mockpubsubapp + +import ( + context "context" + reflect "reflect" + + domain "github.com/IsaacDSC/gqueue/internal/domain" + gomock "go.uber.org/mock/gomock" +) + +// MockFetcher is a mock of Fetcher interface. +type MockFetcher struct { + ctrl *gomock.Controller + recorder *MockFetcherMockRecorder + isgomock struct{} +} + +// MockFetcherMockRecorder is the mock recorder for MockFetcher. +type MockFetcherMockRecorder struct { + mock *MockFetcher +} + +// NewMockFetcher creates a new mock instance. +func NewMockFetcher(ctrl *gomock.Controller) *MockFetcher { + mock := &MockFetcher{ctrl: ctrl} + mock.recorder = &MockFetcherMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockFetcher) EXPECT() *MockFetcherMockRecorder { + return m.recorder +} + +// Notify mocks base method. +func (m *MockFetcher) Notify(ctx context.Context, data map[string]any, headers map[string]string, consumer domain.Consumer) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Notify", ctx, data, headers, consumer) + ret0, _ := ret[0].(error) + return ret0 +} + +// Notify indicates an expected call of Notify. +func (mr *MockFetcherMockRecorder) Notify(ctx, data, headers, consumer any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Notify", reflect.TypeOf((*MockFetcher)(nil).Notify), ctx, data, headers, consumer) +} + +// MockConsumerInsights is a mock of ConsumerInsights interface. +type MockConsumerInsights struct { + ctrl *gomock.Controller + recorder *MockConsumerInsightsMockRecorder + isgomock struct{} +} + +// MockConsumerInsightsMockRecorder is the mock recorder for MockConsumerInsights. +type MockConsumerInsightsMockRecorder struct { + mock *MockConsumerInsights +} + +// NewMockConsumerInsights creates a new mock instance. +func NewMockConsumerInsights(ctrl *gomock.Controller) *MockConsumerInsights { + mock := &MockConsumerInsights{ctrl: ctrl} + mock.recorder = &MockConsumerInsightsMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockConsumerInsights) EXPECT() *MockConsumerInsightsMockRecorder { + return m.recorder +} + +// Consumed mocks base method. +func (m *MockConsumerInsights) Consumed(ctx context.Context, input domain.ConsumerMetric) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Consumed", ctx, input) + ret0, _ := ret[0].(error) + return ret0 +} + +// Consumed indicates an expected call of Consumed. +func (mr *MockConsumerInsightsMockRecorder) Consumed(ctx, input any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Consumed", reflect.TypeOf((*MockConsumerInsights)(nil).Consumed), ctx, input) +} diff --git a/internal/wtrhandler/deadletter_mock.go b/mocks/mockpubsubapp/mock_deadletter_handle.go similarity index 85% rename from internal/wtrhandler/deadletter_mock.go rename to mocks/mockpubsubapp/mock_deadletter_handle.go index efe80a3..5a60d46 100644 --- a/internal/wtrhandler/deadletter_mock.go +++ b/mocks/mockpubsubapp/mock_deadletter_handle.go @@ -1,13 +1,13 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: internal/wtrhandler/deadletter_asynq_handle.go +// Source: internal/app/pubsubapp/deadletter_handle.go // // Generated by this command: // -// mockgen -source=internal/wtrhandler/deadletter_asynq_handle.go -destination=internal/wtrhandler/deadletter_mock.go -package=wtrhandler DeadLetterStore +// mockgen -source=internal/app/pubsubapp/deadletter_handle.go -destination=./mocks/mockpubsubapp/mock_deadletter_handle.go -package=mockpubsubapp // -// Package wtrhandler is a generated GoMock package. -package wtrhandler +// Package mockpubsubapp is a generated GoMock package. +package mockpubsubapp import ( context "context" diff --git a/internal/wtrhandler/external_handle_http_mock.go b/mocks/mockpubsubapp/mock_publisher_handle_http.go similarity index 90% rename from internal/wtrhandler/external_handle_http_mock.go rename to mocks/mockpubsubapp/mock_publisher_handle_http.go index 77046fe..6802f9c 100644 --- a/internal/wtrhandler/external_handle_http_mock.go +++ b/mocks/mockpubsubapp/mock_publisher_handle_http.go @@ -1,13 +1,13 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: internal/wtrhandler/external_handle_http.go +// Source: internal/app/pubsubapp/publisher_handle_http.go // // Generated by this command: // -// mockgen -source=internal/wtrhandler/external_handle_http.go -destination=internal/wtrhandler/external_handle_http_mock.go -package=wtrhandler Store,PublisherInsights +// mockgen -source=internal/app/pubsubapp/publisher_handle_http.go -destination=./mocks/mockpubsubapp/mock_publisher_handle_http.go -package=mockpubsubapp // -// Package wtrhandler is a generated GoMock package. -package wtrhandler +// Package mockpubsubapp is a generated GoMock package. +package mockpubsubapp import ( context "context" diff --git a/mocks/mocktask/mock_base.go b/mocks/mocktask/mock_base.go new file mode 100644 index 0000000..23696cb --- /dev/null +++ b/mocks/mocktask/mock_base.go @@ -0,0 +1,72 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: cmd/setup/task/base.go +// +// Generated by this command: +// +// mockgen -source=cmd/setup/task/base.go -destination=./mocks/mocktask/mock_base.go -package=mocktask +// + +// Package mocktask is a generated GoMock package. +package mocktask + +import ( + context "context" + reflect "reflect" + + domain "github.com/IsaacDSC/gqueue/internal/domain" + gomock "go.uber.org/mock/gomock" +) + +// MockPersistentRepository is a mock of PersistentRepository interface. +type MockPersistentRepository struct { + ctrl *gomock.Controller + recorder *MockPersistentRepositoryMockRecorder + isgomock struct{} +} + +// MockPersistentRepositoryMockRecorder is the mock recorder for MockPersistentRepository. +type MockPersistentRepositoryMockRecorder struct { + mock *MockPersistentRepository +} + +// NewMockPersistentRepository creates a new mock instance. +func NewMockPersistentRepository(ctrl *gomock.Controller) *MockPersistentRepository { + mock := &MockPersistentRepository{ctrl: ctrl} + mock.recorder = &MockPersistentRepositoryMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockPersistentRepository) EXPECT() *MockPersistentRepositoryMockRecorder { + return m.recorder +} + +// GetAllEvents mocks base method. +func (m *MockPersistentRepository) GetAllEvents(ctx context.Context) ([]domain.Event, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAllEvents", ctx) + ret0, _ := ret[0].([]domain.Event) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAllEvents indicates an expected call of GetAllEvents. +func (mr *MockPersistentRepositoryMockRecorder) GetAllEvents(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllEvents", reflect.TypeOf((*MockPersistentRepository)(nil).GetAllEvents), ctx) +} + +// GetAllSchedulers mocks base method. +func (m *MockPersistentRepository) GetAllSchedulers(ctx context.Context, state string) ([]domain.Event, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAllSchedulers", ctx, state) + ret0, _ := ret[0].([]domain.Event) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAllSchedulers indicates an expected call of GetAllSchedulers. +func (mr *MockPersistentRepositoryMockRecorder) GetAllSchedulers(ctx, state any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllSchedulers", reflect.TypeOf((*MockPersistentRepository)(nil).GetAllSchedulers), ctx, state) +} diff --git a/mocks/mocktaskapp/mock_archived_task.go b/mocks/mocktaskapp/mock_archived_task.go new file mode 100644 index 0000000..360e279 --- /dev/null +++ b/mocks/mocktaskapp/mock_archived_task.go @@ -0,0 +1,189 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: internal/app/taskapp/archived_task.go +// +// Generated by this command: +// +// mockgen -source=internal/app/taskapp/archived_task.go -destination=./mocks/mocktaskapp/mock_archived_task.go -package=mocktaskapp +// + +// Package mocktaskapp is a generated GoMock package. +package mocktaskapp + +import ( + context "context" + reflect "reflect" + + taskapp "github.com/IsaacDSC/gqueue/internal/app/taskapp" + domain "github.com/IsaacDSC/gqueue/internal/domain" + gomock "go.uber.org/mock/gomock" +) + +// MockCacheManager is a mock of CacheManager interface. +type MockCacheManager struct { + ctrl *gomock.Controller + recorder *MockCacheManagerMockRecorder + isgomock struct{} +} + +// MockCacheManagerMockRecorder is the mock recorder for MockCacheManager. +type MockCacheManagerMockRecorder struct { + mock *MockCacheManager +} + +// NewMockCacheManager creates a new mock instance. +func NewMockCacheManager(ctrl *gomock.Controller) *MockCacheManager { + mock := &MockCacheManager{ctrl: ctrl} + mock.recorder = &MockCacheManagerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockCacheManager) EXPECT() *MockCacheManagerMockRecorder { + return m.recorder +} + +// FindAllConsumers mocks base method. +func (m *MockCacheManager) FindAllConsumers(ctx context.Context) ([]domain.Event, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindAllConsumers", ctx) + ret0, _ := ret[0].([]domain.Event) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindAllConsumers indicates an expected call of FindAllConsumers. +func (mr *MockCacheManagerMockRecorder) FindAllConsumers(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindAllConsumers", reflect.TypeOf((*MockCacheManager)(nil).FindAllConsumers), ctx) +} + +// FindAllQueues mocks base method. +func (m *MockCacheManager) FindAllQueues(ctx context.Context) ([]taskapp.Queue, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindAllQueues", ctx) + ret0, _ := ret[0].([]taskapp.Queue) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindAllQueues indicates an expected call of FindAllQueues. +func (mr *MockCacheManagerMockRecorder) FindAllQueues(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindAllQueues", reflect.TypeOf((*MockCacheManager)(nil).FindAllQueues), ctx) +} + +// FindArchivedTasks mocks base method. +func (m *MockCacheManager) FindArchivedTasks(ctx context.Context, queue string) ([]string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindArchivedTasks", ctx, queue) + ret0, _ := ret[0].([]string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindArchivedTasks indicates an expected call of FindArchivedTasks. +func (mr *MockCacheManagerMockRecorder) FindArchivedTasks(ctx, queue any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindArchivedTasks", reflect.TypeOf((*MockCacheManager)(nil).FindArchivedTasks), ctx, queue) +} + +// GetMsgArchivedTask mocks base method. +func (m *MockCacheManager) GetMsgArchivedTask(ctx context.Context, queue, task string) (taskapp.RawMsg, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetMsgArchivedTask", ctx, queue, task) + ret0, _ := ret[0].(taskapp.RawMsg) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetMsgArchivedTask indicates an expected call of GetMsgArchivedTask. +func (mr *MockCacheManagerMockRecorder) GetMsgArchivedTask(ctx, queue, task any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMsgArchivedTask", reflect.TypeOf((*MockCacheManager)(nil).GetMsgArchivedTask), ctx, queue, task) +} + +// RemoveItemsArchivedTasks mocks base method. +func (m *MockCacheManager) RemoveItemsArchivedTasks(ctx context.Context, queue string, tasks ...string) error { + m.ctrl.T.Helper() + varargs := []any{ctx, queue} + for _, a := range tasks { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "RemoveItemsArchivedTasks", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// RemoveItemsArchivedTasks indicates an expected call of RemoveItemsArchivedTasks. +func (mr *MockCacheManagerMockRecorder) RemoveItemsArchivedTasks(ctx, queue any, tasks ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, queue}, tasks...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveItemsArchivedTasks", reflect.TypeOf((*MockCacheManager)(nil).RemoveItemsArchivedTasks), varargs...) +} + +// RemoveMsgArchivedTask mocks base method. +func (m *MockCacheManager) RemoveMsgArchivedTask(ctx context.Context, queue, task string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RemoveMsgArchivedTask", ctx, queue, task) + ret0, _ := ret[0].(error) + return ret0 +} + +// RemoveMsgArchivedTask indicates an expected call of RemoveMsgArchivedTask. +func (mr *MockCacheManagerMockRecorder) RemoveMsgArchivedTask(ctx, queue, task any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveMsgArchivedTask", reflect.TypeOf((*MockCacheManager)(nil).RemoveMsgArchivedTask), ctx, queue, task) +} + +// SetArchivedTasks mocks base method. +func (m *MockCacheManager) SetArchivedTasks(ctx context.Context, events []domain.Event) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetArchivedTasks", ctx, events) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetArchivedTasks indicates an expected call of SetArchivedTasks. +func (mr *MockCacheManagerMockRecorder) SetArchivedTasks(ctx, events any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetArchivedTasks", reflect.TypeOf((*MockCacheManager)(nil).SetArchivedTasks), ctx, events) +} + +// MockStorer is a mock of Storer interface. +type MockStorer struct { + ctrl *gomock.Controller + recorder *MockStorerMockRecorder + isgomock struct{} +} + +// MockStorerMockRecorder is the mock recorder for MockStorer. +type MockStorerMockRecorder struct { + mock *MockStorer +} + +// NewMockStorer creates a new mock instance. +func NewMockStorer(ctrl *gomock.Controller) *MockStorer { + mock := &MockStorer{ctrl: ctrl} + mock.recorder = &MockStorerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockStorer) EXPECT() *MockStorerMockRecorder { + return m.recorder +} + +// GetAllSchedulers mocks base method. +func (m *MockStorer) GetAllSchedulers(ctx context.Context, state string) ([]domain.Event, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAllSchedulers", ctx, state) + ret0, _ := ret[0].([]domain.Event) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAllSchedulers indicates an expected call of GetAllSchedulers. +func (mr *MockStorerMockRecorder) GetAllSchedulers(ctx, state any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllSchedulers", reflect.TypeOf((*MockStorer)(nil).GetAllSchedulers), ctx, state) +} diff --git a/mocks/mocktaskapp/mock_cache_store.go b/mocks/mocktaskapp/mock_cache_store.go new file mode 100644 index 0000000..5b79002 --- /dev/null +++ b/mocks/mocktaskapp/mock_cache_store.go @@ -0,0 +1,150 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: internal/app/taskapp/cache_store.go +// +// Generated by this command: +// +// mockgen -source=internal/app/taskapp/cache_store.go -destination=./mocks/mocktaskapp/mock_cache_store.go -package=mocktaskapp +// + +// Package mocktaskapp is a generated GoMock package. +package mocktaskapp + +import ( + context "context" + reflect "reflect" + + taskapp "github.com/IsaacDSC/gqueue/internal/app/taskapp" + domain "github.com/IsaacDSC/gqueue/internal/domain" + gomock "go.uber.org/mock/gomock" +) + +// MockCacher is a mock of Cacher interface. +type MockCacher struct { + ctrl *gomock.Controller + recorder *MockCacherMockRecorder + isgomock struct{} +} + +// MockCacherMockRecorder is the mock recorder for MockCacher. +type MockCacherMockRecorder struct { + mock *MockCacher +} + +// NewMockCacher creates a new mock instance. +func NewMockCacher(ctrl *gomock.Controller) *MockCacher { + mock := &MockCacher{ctrl: ctrl} + mock.recorder = &MockCacherMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockCacher) EXPECT() *MockCacherMockRecorder { + return m.recorder +} + +// FindAllConsumers mocks base method. +func (m *MockCacher) FindAllConsumers(ctx context.Context) ([]domain.Event, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindAllConsumers", ctx) + ret0, _ := ret[0].([]domain.Event) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindAllConsumers indicates an expected call of FindAllConsumers. +func (mr *MockCacherMockRecorder) FindAllConsumers(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindAllConsumers", reflect.TypeOf((*MockCacher)(nil).FindAllConsumers), ctx) +} + +// FindAllQueues mocks base method. +func (m *MockCacher) FindAllQueues(ctx context.Context) ([]taskapp.Queue, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindAllQueues", ctx) + ret0, _ := ret[0].([]taskapp.Queue) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindAllQueues indicates an expected call of FindAllQueues. +func (mr *MockCacherMockRecorder) FindAllQueues(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindAllQueues", reflect.TypeOf((*MockCacher)(nil).FindAllQueues), ctx) +} + +// FindArchivedTasks mocks base method. +func (m *MockCacher) FindArchivedTasks(ctx context.Context, queue string) ([]string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindArchivedTasks", ctx, queue) + ret0, _ := ret[0].([]string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindArchivedTasks indicates an expected call of FindArchivedTasks. +func (mr *MockCacherMockRecorder) FindArchivedTasks(ctx, queue any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindArchivedTasks", reflect.TypeOf((*MockCacher)(nil).FindArchivedTasks), ctx, queue) +} + +// GetMsgArchivedTask mocks base method. +func (m *MockCacher) GetMsgArchivedTask(ctx context.Context, queue, task string) (taskapp.RawMsg, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetMsgArchivedTask", ctx, queue, task) + ret0, _ := ret[0].(taskapp.RawMsg) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetMsgArchivedTask indicates an expected call of GetMsgArchivedTask. +func (mr *MockCacherMockRecorder) GetMsgArchivedTask(ctx, queue, task any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMsgArchivedTask", reflect.TypeOf((*MockCacher)(nil).GetMsgArchivedTask), ctx, queue, task) +} + +// RemoveItemsArchivedTasks mocks base method. +func (m *MockCacher) RemoveItemsArchivedTasks(ctx context.Context, queue string, tasks ...string) error { + m.ctrl.T.Helper() + varargs := []any{ctx, queue} + for _, a := range tasks { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "RemoveItemsArchivedTasks", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// RemoveItemsArchivedTasks indicates an expected call of RemoveItemsArchivedTasks. +func (mr *MockCacherMockRecorder) RemoveItemsArchivedTasks(ctx, queue any, tasks ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, queue}, tasks...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveItemsArchivedTasks", reflect.TypeOf((*MockCacher)(nil).RemoveItemsArchivedTasks), varargs...) +} + +// RemoveMsgArchivedTask mocks base method. +func (m *MockCacher) RemoveMsgArchivedTask(ctx context.Context, queue, task string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RemoveMsgArchivedTask", ctx, queue, task) + ret0, _ := ret[0].(error) + return ret0 +} + +// RemoveMsgArchivedTask indicates an expected call of RemoveMsgArchivedTask. +func (mr *MockCacherMockRecorder) RemoveMsgArchivedTask(ctx, queue, task any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveMsgArchivedTask", reflect.TypeOf((*MockCacher)(nil).RemoveMsgArchivedTask), ctx, queue, task) +} + +// SetArchivedTasks mocks base method. +func (m *MockCacher) SetArchivedTasks(ctx context.Context, events []domain.Event) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetArchivedTasks", ctx, events) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetArchivedTasks indicates an expected call of SetArchivedTasks. +func (mr *MockCacherMockRecorder) SetArchivedTasks(ctx, events any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetArchivedTasks", reflect.TypeOf((*MockCacher)(nil).SetArchivedTasks), ctx, events) +} diff --git a/internal/wtrhandler/fetcher_mock.go b/mocks/mocktaskapp/mock_consumer_handle.go similarity index 89% rename from internal/wtrhandler/fetcher_mock.go rename to mocks/mocktaskapp/mock_consumer_handle.go index 8d69992..1f91f27 100644 --- a/internal/wtrhandler/fetcher_mock.go +++ b/mocks/mocktaskapp/mock_consumer_handle.go @@ -1,13 +1,13 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: internal/wtrhandler/request_handle_asynq.go +// Source: internal/app/taskapp/consumer_handle.go // // Generated by this command: // -// mockgen -source=internal/wtrhandler/request_handle_asynq.go -destination=internal/wtrhandler/fetcher_mock.go -package=wtrhandler Fetcher +// mockgen -source=internal/app/taskapp/consumer_handle.go -destination=./mocks/mocktaskapp/mock_consumer_handle.go -package=mocktaskapp // -// Package wtrhandler is a generated GoMock package. -package wtrhandler +// Package mocktaskapp is a generated GoMock package. +package mocktaskapp import ( context "context" @@ -42,7 +42,7 @@ func (m *MockFetcher) EXPECT() *MockFetcherMockRecorder { } // Notify mocks base method. -func (m *MockFetcher) Notify(ctx context.Context, data map[string]any, headers map[string]string, consumer Consumer) error { +func (m *MockFetcher) Notify(ctx context.Context, data map[string]any, headers map[string]string, consumer domain.Consumer) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Notify", ctx, data, headers, consumer) ret0, _ := ret[0].(error) diff --git a/mocks/mocktaskapp/mock_publisher_handle_http.go b/mocks/mocktaskapp/mock_publisher_handle_http.go new file mode 100644 index 0000000..0a50ebb --- /dev/null +++ b/mocks/mocktaskapp/mock_publisher_handle_http.go @@ -0,0 +1,95 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: internal/app/taskapp/publisher_handle_http.go +// +// Generated by this command: +// +// mockgen -source=internal/app/taskapp/publisher_handle_http.go -destination=./mocks/mocktaskapp/mock_publisher_handle_http.go -package=mocktaskapp +// + +// Package mocktaskapp is a generated GoMock package. +package mocktaskapp + +import ( + context "context" + reflect "reflect" + + domain "github.com/IsaacDSC/gqueue/internal/domain" + gomock "go.uber.org/mock/gomock" +) + +// MockPublisherInsights is a mock of PublisherInsights interface. +type MockPublisherInsights struct { + ctrl *gomock.Controller + recorder *MockPublisherInsightsMockRecorder + isgomock struct{} +} + +// MockPublisherInsightsMockRecorder is the mock recorder for MockPublisherInsights. +type MockPublisherInsightsMockRecorder struct { + mock *MockPublisherInsights +} + +// NewMockPublisherInsights creates a new mock instance. +func NewMockPublisherInsights(ctrl *gomock.Controller) *MockPublisherInsights { + mock := &MockPublisherInsights{ctrl: ctrl} + mock.recorder = &MockPublisherInsightsMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockPublisherInsights) EXPECT() *MockPublisherInsightsMockRecorder { + return m.recorder +} + +// Published mocks base method. +func (m *MockPublisherInsights) Published(ctx context.Context, input domain.PublisherMetric) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Published", ctx, input) + ret0, _ := ret[0].(error) + return ret0 +} + +// Published indicates an expected call of Published. +func (mr *MockPublisherInsightsMockRecorder) Published(ctx, input any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Published", reflect.TypeOf((*MockPublisherInsights)(nil).Published), ctx, input) +} + +// MockStore is a mock of Store interface. +type MockStore struct { + ctrl *gomock.Controller + recorder *MockStoreMockRecorder + isgomock struct{} +} + +// MockStoreMockRecorder is the mock recorder for MockStore. +type MockStoreMockRecorder struct { + mock *MockStore +} + +// NewMockStore creates a new mock instance. +func NewMockStore(ctrl *gomock.Controller) *MockStore { + mock := &MockStore{ctrl: ctrl} + mock.recorder = &MockStoreMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockStore) EXPECT() *MockStoreMockRecorder { + return m.recorder +} + +// GetEvent mocks base method. +func (m *MockStore) GetEvent(ctx context.Context, eventName string) (domain.Event, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetEvent", ctx, eventName) + ret0, _ := ret[0].(domain.Event) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetEvent indicates an expected call of GetEvent. +func (mr *MockStoreMockRecorder) GetEvent(ctx, eventName any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEvent", reflect.TypeOf((*MockStore)(nil).GetEvent), ctx, eventName) +} diff --git a/pkg/pubadapter/base.go b/pkg/pubadapter/base.go index 218e25c..39ce6a7 100644 --- a/pkg/pubadapter/base.go +++ b/pkg/pubadapter/base.go @@ -1,22 +1,31 @@ package pubadapter import ( + "context" "fmt" ) -type WQType string +type PublisherStrategy struct { + TasksPublisher GenericPublisher + PubsubPublisher GenericPublisher +} -func (wt WQType) Validate() error { - switch wt { - case LowThroughput, HighThroughput, LowLatency: - return nil - default: - return fmt.Errorf("invalid WQType: %s", wt) +var _ GenericPublisher = (*PublisherStrategy)(nil) + +func NewStrategy(classificationResult *ClassificationResult) *PublisherStrategy { + return &PublisherStrategy{ + TasksPublisher: classificationResult.InternalPublisher, + PubsubPublisher: classificationResult.ExternalPublisher, } } -const ( - LowThroughput WQType = "low_throughput" - HighThroughput WQType = "high_throughput" - LowLatency WQType = "low_latency" -) +func (s *PublisherStrategy) Publish(ctx context.Context, eventName string, payload any, opts Opts) error { + switch opts.WQType { + case LowThroughput, HighThroughput: + return s.TasksPublisher.Publish(ctx, eventName, payload, opts) + case LowLatency: + return s.PubsubPublisher.Publish(ctx, eventName, payload, opts) + default: + return fmt.Errorf("invalid publish type: %s", opts.WQType) + } +} diff --git a/pkg/pubadapter/opts.go b/pkg/pubadapter/opts.go index e6441cd..c13ff2e 100644 --- a/pkg/pubadapter/opts.go +++ b/pkg/pubadapter/opts.go @@ -1,15 +1,35 @@ package pubadapter -import "github.com/hibiken/asynq" +import ( + "fmt" + + "github.com/hibiken/asynq" +) type Opts struct { Attributes map[string]string AsynqOpts []asynq.Option - // Type specifies the publisher type for routing ("internal" or "external"). - Type string + WQType WQType } var EmptyOpts = Opts{ Attributes: make(map[string]string), AsynqOpts: []asynq.Option{}, } + +type WQType string + +func (wt WQType) Validate() error { + switch wt { + case LowThroughput, HighThroughput, LowLatency: + return nil + default: + return fmt.Errorf("invalid WQType: %s", wt) + } +} + +const ( + LowThroughput WQType = "low_throughput" + HighThroughput WQType = "high_throughput" + LowLatency WQType = "low_latency" +) diff --git a/pkg/pubadapter/publisher_task.go b/pkg/pubadapter/pub_sub_asynq.go similarity index 91% rename from pkg/pubadapter/publisher_task.go rename to pkg/pubadapter/pub_sub_asynq.go index 885c5c1..d57d278 100644 --- a/pkg/pubadapter/publisher_task.go +++ b/pkg/pubadapter/pub_sub_asynq.go @@ -4,9 +4,9 @@ import ( "context" "encoding/json" "fmt" - "log" "time" + "github.com/IsaacDSC/gqueue/pkg/ctxlogger" "github.com/hibiken/asynq" ) @@ -21,6 +21,7 @@ func NewPublisher(client *asynq.Client) *Task { } func (t *Task) Publish(ctx context.Context, eventName string, payload any, opts Opts) error { + l := ctxlogger.GetLogger(ctx) defaultOpts := NewDefaultOpt() asynqopts := opts.AsynqOpts @@ -39,7 +40,7 @@ func (t *Task) Publish(ctx context.Context, eventName string, payload any, opts return fmt.Errorf("could not schedule task: %v", err) } - log.Printf("enqueued task: id=%s queue=%s", info.ID, info.Queue) + l.Info("enqueued task", "id", info.ID, "queue", info.Queue) return nil } diff --git a/pkg/pubadapter/publisher_task_mock.go b/pkg/pubadapter/pub_sub_asynq_test.go similarity index 100% rename from pkg/pubadapter/publisher_task_mock.go rename to pkg/pubadapter/pub_sub_asynq_test.go diff --git a/pkg/pubadapter/publisher_strategy.go b/pkg/pubadapter/publisher_strategy.go deleted file mode 100644 index 56fda99..0000000 --- a/pkg/pubadapter/publisher_strategy.go +++ /dev/null @@ -1,31 +0,0 @@ -package pubadapter - -import ( - "context" - "fmt" -) - -type PublisherStrategy struct { - InternalPublisher GenericPublisher - ExternalPublisher GenericPublisher -} - -var _ GenericPublisher = (*PublisherStrategy)(nil) - -func NewStrategy(classificationResult *ClassificationResult) *PublisherStrategy { - return &PublisherStrategy{ - InternalPublisher: classificationResult.InternalPublisher, - ExternalPublisher: classificationResult.ExternalPublisher, - } -} - -func (s *PublisherStrategy) Publish(ctx context.Context, eventName string, payload any, opts Opts) error { - switch opts.Type { - case "internal": - return s.InternalPublisher.Publish(ctx, eventName, payload, opts) - case "external": - return s.ExternalPublisher.Publish(ctx, eventName, payload, opts) - default: - return fmt.Errorf("invalid publish type: %s", opts.Type) - } -}