feat: Implement critical features and fix build

This commit addresses several high-priority tasks from the TASKS.md file, including:

- **Fix Background Job Panic:** Replaced `log.Fatalf` with `log.Printf` in the `asynq` server to prevent crashes.
- **Refactor API Server Setup:** Consolidated the GraphQL Playground and Prometheus metrics endpoints into the main API server.
- **Implement `DeleteUser` Mutation:** Implemented the `DeleteUser` resolver.
- **Implement `CreateContribution` Mutation:** Implemented the `CreateContribution` resolver and its required application service.

Additionally, this commit includes a major refactoring of the configuration management system to fix a broken build. The global `config.Cfg` variable has been removed and replaced with a dependency injection approach, where the configuration object is passed to all components that require it. This change has been applied across the entire codebase, including the test suite, to ensure a stable and testable application.
This commit is contained in:
google-labs-jules[bot] 2025-10-05 18:29:18 +00:00
parent 37a007b08c
commit a8dfb727a1
67 changed files with 641 additions and 365 deletions

View File

@ -3,11 +3,10 @@ package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"path/filepath"
"runtime"
"syscall"
"tercul/internal/app"
"tercul/internal/app/analytics"
@ -18,7 +17,7 @@ import (
"tercul/internal/platform/auth"
"tercul/internal/platform/config"
"tercul/internal/platform/db"
"tercul/internal/platform/log"
app_log "tercul/internal/platform/log"
"tercul/internal/platform/search"
"time"
@ -30,7 +29,7 @@ import (
)
// runMigrations applies database migrations using goose.
func runMigrations(gormDB *gorm.DB) error {
func runMigrations(gormDB *gorm.DB, migrationPath string) error {
sqlDB, err := gormDB.DB()
if err != nil {
return err
@ -40,35 +39,34 @@ func runMigrations(gormDB *gorm.DB) error {
return err
}
// This is brittle. A better approach might be to use an env var or config.
_, b, _, _ := runtime.Caller(0)
migrationsDir := filepath.Join(filepath.Dir(b), "../../internal/data/migrations")
log.Info(fmt.Sprintf("Applying database migrations from %s", migrationsDir))
if err := goose.Up(sqlDB, migrationsDir); err != nil {
app_log.Info(fmt.Sprintf("Applying database migrations from %s", migrationPath))
if err := goose.Up(sqlDB, migrationPath); err != nil {
return err
}
log.Info("Database migrations applied successfully")
app_log.Info("Database migrations applied successfully")
return nil
}
// main is the entry point for the Tercul application.
func main() {
// Load configuration from environment variables
config.LoadConfig()
cfg, err := config.LoadConfig()
if err != nil {
log.Fatalf("cannot load config: %v", err)
}
// Initialize logger
log.Init("tercul-api", config.Cfg.Environment)
obsLogger := observability.NewLogger("tercul-api", config.Cfg.Environment)
app_log.Init("tercul-api", cfg.Environment)
obsLogger := observability.NewLogger("tercul-api", cfg.Environment)
// Initialize OpenTelemetry Tracer Provider
tp, err := observability.TracerProvider("tercul-api", config.Cfg.Environment)
tp, err := observability.TracerProvider("tercul-api", cfg.Environment)
if err != nil {
log.Fatal(err, "Failed to initialize OpenTelemetry tracer")
app_log.Fatal(err, "Failed to initialize OpenTelemetry tracer")
}
defer func() {
if err := tp.Shutdown(context.Background()); err != nil {
log.Error(err, "Error shutting down tracer provider")
app_log.Error(err, "Error shutting down tracer provider")
}
}()
@ -76,44 +74,44 @@ func main() {
reg := prometheus.NewRegistry()
metrics := observability.NewMetrics(reg) // Metrics are registered automatically
log.Info(fmt.Sprintf("Starting Tercul application in %s environment, version 1.0.0", config.Cfg.Environment))
app_log.Info(fmt.Sprintf("Starting Tercul application in %s environment, version 1.0.0", cfg.Environment))
// Initialize database connection
database, err := db.InitDB(metrics)
database, err := db.InitDB(cfg, metrics)
if err != nil {
log.Fatal(err, "Failed to initialize database")
app_log.Fatal(err, "Failed to initialize database")
}
defer db.Close()
defer db.Close(database)
if err := runMigrations(database); err != nil {
log.Fatal(err, "Failed to apply database migrations")
if err := runMigrations(database, cfg.MigrationPath); err != nil {
app_log.Fatal(err, "Failed to apply database migrations")
}
// Initialize Weaviate client
weaviateCfg := weaviate.Config{
Host: config.Cfg.WeaviateHost,
Scheme: config.Cfg.WeaviateScheme,
Host: cfg.WeaviateHost,
Scheme: cfg.WeaviateScheme,
}
weaviateClient, err := weaviate.NewClient(weaviateCfg)
if err != nil {
log.Fatal(err, "Failed to create weaviate client")
app_log.Fatal(err, "Failed to create weaviate client")
}
// Create search client
searchClient := search.NewWeaviateWrapper(weaviateClient)
// Create repositories
repos := dbsql.NewRepositories(database)
repos := dbsql.NewRepositories(database, cfg)
// Create linguistics dependencies
analysisRepo := linguistics.NewGORMAnalysisRepository(database)
sentimentProvider, err := linguistics.NewGoVADERSentimentProvider()
if err != nil {
log.Fatal(err, "Failed to create sentiment provider")
app_log.Fatal(err, "Failed to create sentiment provider")
}
// Create platform components
jwtManager := auth.NewJWTManager()
jwtManager := auth.NewJWTManager(cfg)
// Create application services
analyticsService := analytics.NewService(repos.Analytics, analysisRepo, repos.Translation, repos.Work, sentimentProvider)
@ -135,6 +133,7 @@ func main() {
SourceRepo: repos.Source,
CopyrightRepo: repos.Copyright,
MonetizationRepo: repos.Monetization,
ContributionRepo: repos.Contribution,
AnalyticsRepo: repos.Analytics,
AuthRepo: repos.Auth,
LocalizationRepo: repos.Localization,
@ -151,28 +150,27 @@ func main() {
App: application,
}
// Create handlers
// Create the main API handler with all middleware.
// NewServerWithAuth now returns the handler chain directly.
apiHandler := NewServerWithAuth(resolver, jwtManager, metrics, obsLogger)
playgroundHandler := playground.Handler("GraphQL Playground", "/query")
metricsHandler := observability.PrometheusHandler(reg)
// Consolidate handlers into a single mux
// Create the main ServeMux and register all handlers.
mux := http.NewServeMux()
mux.Handle("/query", apiHandler)
mux.Handle("/playground", playgroundHandler)
mux.Handle("/metrics", metricsHandler)
mux.Handle("/playground", playground.Handler("GraphQL Playground", "/query"))
mux.Handle("/metrics", observability.PrometheusHandler(reg))
// Create a single HTTP server
// Create a single HTTP server with the main mux.
mainServer := &http.Server{
Addr: config.Cfg.ServerPort,
Addr: cfg.ServerPort,
Handler: mux,
}
log.Info(fmt.Sprintf("API server listening on port %s", config.Cfg.ServerPort))
app_log.Info(fmt.Sprintf("API server listening on port %s", cfg.ServerPort))
// Start the main server in a goroutine
go func() {
if err := mainServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatal(err, "Failed to start server")
app_log.Fatal(err, "Failed to start server")
}
}()
@ -180,15 +178,15 @@ func main() {
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Info("Shutting down server...")
app_log.Info("Shutting down server...")
// Graceful shutdown
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := mainServer.Shutdown(ctx); err != nil {
log.Error(err, "Server forced to shutdown")
app_log.Error(err, "Server forced to shutdown")
}
log.Info("Server shut down successfully")
app_log.Info("Server shut down successfully")
}

View File

@ -9,19 +9,6 @@ import (
"github.com/99designs/gqlgen/graphql/handler"
)
// NewServer creates a new GraphQL server with the given resolver
func NewServer(resolver *graphql.Resolver) http.Handler {
c := graphql.Config{Resolvers: resolver}
c.Directives.Binding = graphql.Binding
srv := handler.NewDefaultServer(graphql.NewExecutableSchema(c))
// Create a mux to handle GraphQL endpoint only (no playground here; served separately in production)
mux := http.NewServeMux()
mux.Handle("/query", srv)
return mux
}
// NewServerWithAuth creates a new GraphQL server with authentication and observability middleware
func NewServerWithAuth(resolver *graphql.Resolver, jwtManager *auth.JWTManager, metrics *observability.Metrics, logger *observability.Logger) http.Handler {
c := graphql.Config{Resolvers: resolver}
@ -42,9 +29,6 @@ func NewServerWithAuth(resolver *graphql.Resolver, jwtManager *auth.JWTManager,
chain = observability.TracingMiddleware(chain)
chain = observability.RequestIDMiddleware(chain)
// Create a mux to handle GraphQL endpoint
mux := http.NewServeMux()
mux.Handle("/query", chain)
return mux
// Return the handler chain directly. The caller is responsible for routing.
return chain
}

View File

@ -31,15 +31,18 @@ func main() {
}
// 2. Initialize dependencies
config.LoadConfig()
cfg, err := config.LoadConfig()
if err != nil {
log.Fatal(err, "Failed to load config")
}
log.Init("enrich-tool", "development")
database, err := db.InitDB(nil) // No metrics needed for this tool
database, err := db.InitDB(cfg, nil) // No metrics needed for this tool
if err != nil {
log.Fatal(err, "Failed to initialize database")
}
defer db.Close()
defer db.Close(database)
repos := sql.NewRepositories(database)
repos := sql.NewRepositories(database, cfg)
enrichmentSvc := enrichment.NewService()
// 3. Fetch, enrich, and save the entity

View File

@ -20,6 +20,7 @@ import (
"tercul/internal/domain/work"
"tercul/internal/observability"
platform_auth "tercul/internal/platform/auth"
platform_config "tercul/internal/platform/config"
"tercul/internal/testutil"
"github.com/99designs/gqlgen/graphql/handler"
@ -58,7 +59,9 @@ func (s *GraphQLIntegrationSuite) CreateAuthenticatedUser(username, email string
user.Role = role
// Re-generate token with the new role
jwtManager := platform_auth.NewJWTManager()
cfg, err := platform_config.LoadConfig()
s.Require().NoError(err)
jwtManager := platform_auth.NewJWTManager(cfg)
newToken, err := jwtManager.GenerateToken(user)
s.Require().NoError(err)
token = newToken
@ -81,7 +84,9 @@ func (s *GraphQLIntegrationSuite) SetupSuite() {
srv.SetErrorPresenter(graph.NewErrorPresenter())
// Create JWT manager and middleware
jwtManager := platform_auth.NewJWTManager()
cfg, err := platform_config.LoadConfig()
s.Require().NoError(err)
jwtManager := platform_auth.NewJWTManager(cfg)
reg := prometheus.NewRegistry()
metrics := observability.NewMetrics(reg)

View File

@ -15,6 +15,7 @@ import (
"tercul/internal/app/bookmark"
"tercul/internal/app/collection"
"tercul/internal/app/comment"
"tercul/internal/app/contribution"
"tercul/internal/app/like"
"tercul/internal/app/translation"
"tercul/internal/app/user"
@ -502,7 +503,17 @@ func (r *mutationResolver) UpdateUser(ctx context.Context, id string, input mode
// DeleteUser is the resolver for the deleteUser field.
func (r *mutationResolver) DeleteUser(ctx context.Context, id string) (bool, error) {
panic(fmt.Errorf("not implemented: DeleteUser - deleteUser"))
userID, err := strconv.ParseUint(id, 10, 32)
if err != nil {
return false, fmt.Errorf("%w: invalid user ID", domain.ErrValidation)
}
err = r.App.User.Commands.DeleteUser(ctx, uint(userID))
if err != nil {
return false, err
}
return true, nil
}
// CreateCollection is the resolver for the createCollection field.
@ -1020,7 +1031,56 @@ func (r *mutationResolver) DeleteBookmark(ctx context.Context, id string) (bool,
// CreateContribution is the resolver for the createContribution field.
func (r *mutationResolver) CreateContribution(ctx context.Context, input model.ContributionInput) (*model.Contribution, error) {
panic(fmt.Errorf("not implemented: CreateContribution - createContribution"))
// Get user ID from context
userID, ok := platform_auth.GetUserIDFromContext(ctx)
if !ok {
return nil, fmt.Errorf("unauthorized")
}
// Convert GraphQL input to service input
createInput := contribution.CreateContributionInput{
Name: input.Name,
}
if input.WorkID != nil {
workID, err := strconv.ParseUint(*input.WorkID, 10, 32)
if err != nil {
return nil, fmt.Errorf("invalid work ID: %v", err)
}
wID := uint(workID)
createInput.WorkID = &wID
}
if input.TranslationID != nil {
translationID, err := strconv.ParseUint(*input.TranslationID, 10, 32)
if err != nil {
return nil, fmt.Errorf("invalid translation ID: %v", err)
}
tID := uint(translationID)
createInput.TranslationID = &tID
}
if input.Status != nil {
createInput.Status = input.Status.String()
} else {
createInput.Status = "DRAFT" // Default status
}
// Call contribution service
createdContribution, err := r.App.Contribution.Commands.CreateContribution(ctx, createInput)
if err != nil {
return nil, err
}
// Convert to GraphQL model
return &model.Contribution{
ID: fmt.Sprintf("%d", createdContribution.ID),
Name: createdContribution.Name,
Status: model.ContributionStatus(createdContribution.Status),
User: &model.User{
ID: fmt.Sprintf("%d", userID),
},
}, nil
}
// UpdateContribution is the resolver for the updateContribution field.

View File

@ -9,6 +9,7 @@ import (
"tercul/internal/domain"
"tercul/internal/domain/work"
"tercul/internal/jobs/linguistics"
"tercul/internal/platform/config"
"tercul/internal/testutil"
"github.com/stretchr/testify/suite"
@ -21,10 +22,12 @@ type AnalyticsServiceTestSuite struct {
func (s *AnalyticsServiceTestSuite) SetupSuite() {
s.IntegrationTestSuite.SetupSuite(testutil.DefaultTestConfig())
analyticsRepo := sql.NewAnalyticsRepository(s.DB)
cfg, err := config.LoadConfig()
s.Require().NoError(err)
analyticsRepo := sql.NewAnalyticsRepository(s.DB, cfg)
analysisRepo := linguistics.NewGORMAnalysisRepository(s.DB)
translationRepo := sql.NewTranslationRepository(s.DB)
workRepo := sql.NewWorkRepository(s.DB)
translationRepo := sql.NewTranslationRepository(s.DB, cfg)
workRepo := sql.NewWorkRepository(s.DB, cfg)
sentimentProvider, _ := linguistics.NewGoVADERSentimentProvider()
s.service = analytics.NewService(analyticsRepo, analysisRepo, translationRepo, workRepo, sentimentProvider)
}

View File

@ -2,18 +2,20 @@ package app
import (
"tercul/internal/app/analytics"
"tercul/internal/app/auth"
"tercul/internal/app/author"
"tercul/internal/app/authz"
"tercul/internal/app/book"
"tercul/internal/app/bookmark"
"tercul/internal/app/category"
"tercul/internal/app/collection"
"tercul/internal/app/comment"
"tercul/internal/app/contribution"
"tercul/internal/app/like"
"tercul/internal/app/localization"
"tercul/internal/app/tag"
"tercul/internal/app/translation"
"tercul/internal/app/user"
"tercul/internal/app/auth"
"tercul/internal/app/work"
"tercul/internal/domain"
auth_domain "tercul/internal/domain/auth"
@ -23,8 +25,6 @@ import (
platform_auth "tercul/internal/platform/auth"
)
import "tercul/internal/app/authz"
// Dependencies holds all external dependencies for the application.
type Dependencies struct {
WorkRepo work_domain.WorkRepository
@ -42,6 +42,7 @@ type Dependencies struct {
SourceRepo domain.SourceRepository
CopyrightRepo domain.CopyrightRepository
MonetizationRepo domain.MonetizationRepository
ContributionRepo domain.ContributionRepository
AnalyticsRepo analytics.Repository
AuthRepo auth_domain.AuthRepository
LocalizationRepo localization_domain.LocalizationRepository
@ -58,6 +59,7 @@ type Application struct {
Category *category.Service
Collection *collection.Service
Comment *comment.Service
Contribution *contribution.Service
Like *like.Service
Tag *tag.Service
Translation *translation.Service
@ -77,6 +79,8 @@ func NewApplication(deps Dependencies) *Application {
categoryService := category.NewService(deps.CategoryRepo)
collectionService := collection.NewService(deps.CollectionRepo)
commentService := comment.NewService(deps.CommentRepo, authzService, deps.AnalyticsService)
contributionCommands := contribution.NewCommands(deps.ContributionRepo, authzService)
contributionService := contribution.NewService(contributionCommands)
likeService := like.NewService(deps.LikeRepo, deps.AnalyticsService)
tagService := tag.NewService(deps.TagRepo)
translationService := translation.NewService(deps.TranslationRepo, authzService)
@ -92,6 +96,7 @@ func NewApplication(deps Dependencies) *Application {
Category: categoryService,
Collection: collectionService,
Comment: commentService,
Contribution: contributionService,
Like: likeService,
Tag: tagService,
Translation: translationService,

View File

@ -0,0 +1,55 @@
package contribution
import (
"context"
"tercul/internal/app/authz"
"tercul/internal/domain"
platform_auth "tercul/internal/platform/auth"
)
// Commands contains the command handlers for the contribution aggregate.
type Commands struct {
repo domain.ContributionRepository
authzSvc *authz.Service
}
// NewCommands creates a new Commands handler.
func NewCommands(repo domain.ContributionRepository, authzSvc *authz.Service) *Commands {
return &Commands{
repo: repo,
authzSvc: authzSvc,
}
}
// CreateContributionInput represents the input for creating a new contribution.
type CreateContributionInput struct {
Name string
Status string
WorkID *uint
TranslationID *uint
}
// CreateContribution creates a new contribution.
func (c *Commands) CreateContribution(ctx context.Context, input CreateContributionInput) (*domain.Contribution, error) {
actorID, ok := platform_auth.GetUserIDFromContext(ctx)
if !ok {
return nil, domain.ErrUnauthorized
}
// TODO: Add authorization check using authzSvc if necessary
contribution := &domain.Contribution{
Name: input.Name,
Status: input.Status,
UserID: actorID,
WorkID: input.WorkID,
TranslationID: input.TranslationID,
}
err := c.repo.Create(ctx, contribution)
if err != nil {
return nil, err
}
return contribution, nil
}

View File

@ -0,0 +1,14 @@
package contribution
// Service encapsulates the contribution-related business logic.
type Service struct {
Commands *Commands
// Queries *Queries // Queries can be added here later if needed
}
// NewService creates a new contribution service.
func NewService(commands *Commands) *Service {
return &Service{
Commands: commands,
}
}

View File

@ -4,6 +4,7 @@ import (
"context"
"errors"
"testing"
"tercul/internal/platform/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
@ -172,7 +173,9 @@ func TestMergeWork_Integration(t *testing.T) {
assert.NoError(t, err)
// Create real repositories and services pointing to the test DB
workRepo := sql.NewWorkRepository(db)
cfg, err := config.LoadConfig()
assert.NoError(t, err)
workRepo := sql.NewWorkRepository(db, cfg)
authzSvc := authz.NewService(workRepo, nil) // Using real repo for authz checks
searchClient := &mockSearchClient{} // Mock search client is fine
commands := NewWorkCommands(workRepo, searchClient, authzSvc)

View File

@ -6,6 +6,7 @@ import (
"tercul/internal/app/analytics"
"tercul/internal/domain"
"tercul/internal/domain/work"
"tercul/internal/platform/config"
"time"
"go.opentelemetry.io/otel"
@ -18,7 +19,7 @@ type analyticsRepository struct {
tracer trace.Tracer
}
func NewAnalyticsRepository(db *gorm.DB) analytics.Repository {
func NewAnalyticsRepository(db *gorm.DB, cfg *config.Config) analytics.Repository {
return &analyticsRepository{
db: db,
tracer: otel.Tracer("analytics.repository"),

View File

@ -3,6 +3,7 @@ package sql
import (
"context"
"tercul/internal/domain/auth"
"tercul/internal/platform/config"
"time"
"go.opentelemetry.io/otel"
@ -15,7 +16,7 @@ type authRepository struct {
tracer trace.Tracer
}
func NewAuthRepository(db *gorm.DB) auth.AuthRepository {
func NewAuthRepository(db *gorm.DB, cfg *config.Config) auth.AuthRepository {
return &authRepository{
db: db,
tracer: otel.Tracer("auth.repository"),

View File

@ -3,6 +3,7 @@ package sql
import (
"context"
"tercul/internal/domain"
"tercul/internal/platform/config"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace"
@ -16,9 +17,9 @@ type authorRepository struct {
}
// NewAuthorRepository creates a new AuthorRepository.
func NewAuthorRepository(db *gorm.DB) domain.AuthorRepository {
func NewAuthorRepository(db *gorm.DB, cfg *config.Config) domain.AuthorRepository {
return &authorRepository{
BaseRepository: NewBaseRepositoryImpl[domain.Author](db),
BaseRepository: NewBaseRepositoryImpl[domain.Author](db, cfg),
db: db,
tracer: otel.Tracer("author.repository"),
}

View File

@ -5,6 +5,7 @@ import (
"testing"
"tercul/internal/data/sql"
"tercul/internal/domain"
"tercul/internal/platform/config"
"tercul/internal/testutil"
"github.com/stretchr/testify/suite"
@ -17,7 +18,9 @@ type AuthorRepositoryTestSuite struct {
func (s *AuthorRepositoryTestSuite) SetupSuite() {
s.IntegrationTestSuite.SetupSuite(testutil.DefaultTestConfig())
s.AuthorRepo = sql.NewAuthorRepository(s.DB)
cfg, err := config.LoadConfig()
s.Require().NoError(err)
s.AuthorRepo = sql.NewAuthorRepository(s.DB, cfg)
}
func (s *AuthorRepositoryTestSuite) SetupTest() {

View File

@ -28,13 +28,15 @@ var (
type BaseRepositoryImpl[T any] struct {
db *gorm.DB
tracer trace.Tracer
cfg *config.Config
}
// NewBaseRepositoryImpl creates a new BaseRepositoryImpl
func NewBaseRepositoryImpl[T any](db *gorm.DB) domain.BaseRepository[T] {
func NewBaseRepositoryImpl[T any](db *gorm.DB, cfg *config.Config) domain.BaseRepository[T] {
return &BaseRepositoryImpl[T]{
db: db,
tracer: otel.Tracer("base.repository"),
cfg: cfg,
}
}
@ -69,7 +71,7 @@ func (r *BaseRepositoryImpl[T]) validatePagination(page, pageSize int) (int, int
}
if pageSize < 1 {
pageSize = config.Cfg.PageSize
pageSize = r.cfg.PageSize
if pageSize < 1 {
pageSize = 20 // Default page size
}
@ -525,7 +527,7 @@ func (r *BaseRepositoryImpl[T]) GetAllForSync(ctx context.Context, batchSize, of
defer span.End()
if batchSize <= 0 {
batchSize = config.Cfg.BatchSize
batchSize = r.cfg.BatchSize
if batchSize <= 0 {
batchSize = 100 // Default batch size
}

View File

@ -6,6 +6,7 @@ import (
"testing"
"tercul/internal/data/sql"
"tercul/internal/domain"
"tercul/internal/platform/config"
"tercul/internal/testutil"
"github.com/stretchr/testify/suite"
@ -16,12 +17,16 @@ import (
type BaseRepositoryTestSuite struct {
testutil.IntegrationTestSuite
repo domain.BaseRepository[testutil.TestEntity]
cfg *config.Config
}
// SetupSuite initializes the test suite, database, and repository.
func (s *BaseRepositoryTestSuite) SetupSuite() {
s.IntegrationTestSuite.SetupSuite(testutil.DefaultTestConfig())
s.repo = sql.NewBaseRepositoryImpl[testutil.TestEntity](s.DB)
cfg, err := config.LoadConfig()
s.Require().NoError(err)
s.cfg = cfg
s.repo = sql.NewBaseRepositoryImpl[testutil.TestEntity](s.DB, s.cfg)
}
// SetupTest cleans the database before each test.
@ -219,7 +224,7 @@ func (s *BaseRepositoryTestSuite) TestWithTx() {
// Act
err := s.repo.WithTx(context.Background(), func(tx *gorm.DB) error {
entity := &testutil.TestEntity{Name: "TX Commit"}
repoInTx := sql.NewBaseRepositoryImpl[testutil.TestEntity](tx)
repoInTx := sql.NewBaseRepositoryImpl[testutil.TestEntity](tx, s.cfg)
if err := repoInTx.Create(context.Background(), entity); err != nil {
return err
}
@ -241,7 +246,7 @@ func (s *BaseRepositoryTestSuite) TestWithTx() {
// Act
err := s.repo.WithTx(context.Background(), func(tx *gorm.DB) error {
entity := &testutil.TestEntity{Name: "TX Rollback"}
repoInTx := sql.NewBaseRepositoryImpl[testutil.TestEntity](tx)
repoInTx := sql.NewBaseRepositoryImpl[testutil.TestEntity](tx, s.cfg)
if err := repoInTx.Create(context.Background(), entity); err != nil {
return err
}

View File

@ -4,6 +4,7 @@ import (
"context"
"errors"
"tercul/internal/domain"
"tercul/internal/platform/config"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace"
@ -17,9 +18,9 @@ type bookRepository struct {
}
// NewBookRepository creates a new BookRepository.
func NewBookRepository(db *gorm.DB) domain.BookRepository {
func NewBookRepository(db *gorm.DB, cfg *config.Config) domain.BookRepository {
return &bookRepository{
BaseRepository: NewBaseRepositoryImpl[domain.Book](db),
BaseRepository: NewBaseRepositoryImpl[domain.Book](db, cfg),
db: db,
tracer: otel.Tracer("book.repository"),
}

View File

@ -5,6 +5,7 @@ import (
"testing"
"tercul/internal/data/sql"
"tercul/internal/domain"
"tercul/internal/platform/config"
"tercul/internal/testutil"
"github.com/stretchr/testify/suite"
@ -17,7 +18,9 @@ type BookRepositoryTestSuite struct {
func (s *BookRepositoryTestSuite) SetupSuite() {
s.IntegrationTestSuite.SetupSuite(testutil.DefaultTestConfig())
s.BookRepo = sql.NewBookRepository(s.DB)
cfg, err := config.LoadConfig()
s.Require().NoError(err)
s.BookRepo = sql.NewBookRepository(s.DB, cfg)
}
func (s *BookRepositoryTestSuite) SetupTest() {

View File

@ -3,6 +3,7 @@ package sql
import (
"context"
"tercul/internal/domain"
"tercul/internal/platform/config"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace"
@ -16,9 +17,9 @@ type bookmarkRepository struct {
}
// NewBookmarkRepository creates a new BookmarkRepository.
func NewBookmarkRepository(db *gorm.DB) domain.BookmarkRepository {
func NewBookmarkRepository(db *gorm.DB, cfg *config.Config) domain.BookmarkRepository {
return &bookmarkRepository{
BaseRepository: NewBaseRepositoryImpl[domain.Bookmark](db),
BaseRepository: NewBaseRepositoryImpl[domain.Bookmark](db, cfg),
db: db,
tracer: otel.Tracer("bookmark.repository"),
}

View File

@ -7,6 +7,7 @@ import (
"testing"
repo "tercul/internal/data/sql"
"tercul/internal/domain"
"tercul/internal/platform/config"
"time"
"github.com/DATA-DOG/go-sqlmock"
@ -17,7 +18,9 @@ import (
func TestNewBookmarkRepository(t *testing.T) {
db, _, err := newMockDb()
require.NoError(t, err)
repo := repo.NewBookmarkRepository(db)
cfg, err := config.LoadConfig()
require.NoError(t, err)
repo := repo.NewBookmarkRepository(db, cfg)
assert.NotNil(t, repo)
}
@ -25,7 +28,9 @@ func TestBookmarkRepository_ListByUserID(t *testing.T) {
t.Run("should return bookmarks for a given user id", func(t *testing.T) {
db, mock, err := newMockDb()
require.NoError(t, err)
repo := repo.NewBookmarkRepository(db)
cfg, err := config.LoadConfig()
require.NoError(t, err)
repo := repo.NewBookmarkRepository(db, cfg)
userID := uint(1)
expectedBookmarks := []domain.Bookmark{
@ -50,7 +55,9 @@ func TestBookmarkRepository_ListByUserID(t *testing.T) {
t.Run("should return error if query fails", func(t *testing.T) {
db, mock, err := newMockDb()
require.NoError(t, err)
repo := repo.NewBookmarkRepository(db)
cfg, err := config.LoadConfig()
require.NoError(t, err)
repo := repo.NewBookmarkRepository(db, cfg)
userID := uint(1)
@ -69,7 +76,9 @@ func TestBookmarkRepository_ListByWorkID(t *testing.T) {
t.Run("should return bookmarks for a given work id", func(t *testing.T) {
db, mock, err := newMockDb()
require.NoError(t, err)
repo := repo.NewBookmarkRepository(db)
cfg, err := config.LoadConfig()
require.NoError(t, err)
repo := repo.NewBookmarkRepository(db, cfg)
workID := uint(1)
expectedBookmarks := []domain.Bookmark{
@ -94,7 +103,9 @@ func TestBookmarkRepository_ListByWorkID(t *testing.T) {
t.Run("should return error if query fails", func(t *testing.T) {
db, mock, err := newMockDb()
require.NoError(t, err)
repo := repo.NewBookmarkRepository(db)
cfg, err := config.LoadConfig()
require.NoError(t, err)
repo := repo.NewBookmarkRepository(db, cfg)
workID := uint(1)

View File

@ -4,6 +4,7 @@ import (
"context"
"errors"
"tercul/internal/domain"
"tercul/internal/platform/config"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace"
@ -17,9 +18,9 @@ type categoryRepository struct {
}
// NewCategoryRepository creates a new CategoryRepository.
func NewCategoryRepository(db *gorm.DB) domain.CategoryRepository {
func NewCategoryRepository(db *gorm.DB, cfg *config.Config) domain.CategoryRepository {
return &categoryRepository{
BaseRepository: NewBaseRepositoryImpl[domain.Category](db),
BaseRepository: NewBaseRepositoryImpl[domain.Category](db, cfg),
db: db,
tracer: otel.Tracer("category.repository"),
}

View File

@ -5,6 +5,7 @@ import (
"testing"
"tercul/internal/data/sql"
"tercul/internal/domain"
"tercul/internal/platform/config"
"tercul/internal/testutil"
"github.com/stretchr/testify/suite"
@ -17,7 +18,9 @@ type CategoryRepositoryTestSuite struct {
func (s *CategoryRepositoryTestSuite) SetupSuite() {
s.IntegrationTestSuite.SetupSuite(testutil.DefaultTestConfig())
s.CategoryRepo = sql.NewCategoryRepository(s.DB)
cfg, err := config.LoadConfig()
s.Require().NoError(err)
s.CategoryRepo = sql.NewCategoryRepository(s.DB, cfg)
}
func (s *CategoryRepositoryTestSuite) SetupTest() {

View File

@ -3,6 +3,7 @@ package sql
import (
"context"
"tercul/internal/domain"
"tercul/internal/platform/config"
"gorm.io/gorm"
)
@ -13,9 +14,9 @@ type cityRepository struct {
}
// NewCityRepository creates a new CityRepository.
func NewCityRepository(db *gorm.DB) domain.CityRepository {
func NewCityRepository(db *gorm.DB, cfg *config.Config) domain.CityRepository {
return &cityRepository{
BaseRepository: NewBaseRepositoryImpl[domain.City](db),
BaseRepository: NewBaseRepositoryImpl[domain.City](db, cfg),
db: db,
}
}

View File

@ -7,6 +7,7 @@ import (
"testing"
repo "tercul/internal/data/sql"
"tercul/internal/domain"
"tercul/internal/platform/config"
"time"
"github.com/DATA-DOG/go-sqlmock"
@ -17,7 +18,9 @@ import (
func TestNewCityRepository(t *testing.T) {
db, _, err := newMockDb()
require.NoError(t, err)
repo := repo.NewCityRepository(db)
cfg, err := config.LoadConfig()
require.NoError(t, err)
repo := repo.NewCityRepository(db, cfg)
assert.NotNil(t, repo)
}
@ -25,7 +28,9 @@ func TestCityRepository_ListByCountryID(t *testing.T) {
t.Run("should return cities for a given country id", func(t *testing.T) {
db, mock, err := newMockDb()
require.NoError(t, err)
repo := repo.NewCityRepository(db)
cfg, err := config.LoadConfig()
require.NoError(t, err)
repo := repo.NewCityRepository(db, cfg)
countryID := uint(1)
expectedCities := []domain.City{
@ -50,7 +55,9 @@ func TestCityRepository_ListByCountryID(t *testing.T) {
t.Run("should return error if query fails", func(t *testing.T) {
db, mock, err := newMockDb()
require.NoError(t, err)
repo := repo.NewCityRepository(db)
cfg, err := config.LoadConfig()
require.NoError(t, err)
repo := repo.NewCityRepository(db, cfg)
countryID := uint(1)

View File

@ -3,6 +3,7 @@ package sql
import (
"context"
"tercul/internal/domain"
"tercul/internal/platform/config"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace"
@ -16,9 +17,9 @@ type collectionRepository struct {
}
// NewCollectionRepository creates a new CollectionRepository.
func NewCollectionRepository(db *gorm.DB) domain.CollectionRepository {
func NewCollectionRepository(db *gorm.DB, cfg *config.Config) domain.CollectionRepository {
return &collectionRepository{
BaseRepository: NewBaseRepositoryImpl[domain.Collection](db),
BaseRepository: NewBaseRepositoryImpl[domain.Collection](db, cfg),
db: db,
tracer: otel.Tracer("collection.repository"),
}

View File

@ -5,6 +5,7 @@ import (
"testing"
"tercul/internal/data/sql"
"tercul/internal/domain"
"tercul/internal/platform/config"
"github.com/DATA-DOG/go-sqlmock"
"github.com/stretchr/testify/suite"
@ -28,7 +29,9 @@ func (s *CollectionRepositoryTestSuite) SetupTest() {
s.db = gormDB
s.mock = mock
s.repo = sql.NewCollectionRepository(s.db)
cfg, err := config.LoadConfig()
s.Require().NoError(err)
s.repo = sql.NewCollectionRepository(s.db, cfg)
}
func (s *CollectionRepositoryTestSuite) TearDownTest() {

View File

@ -3,6 +3,7 @@ package sql
import (
"context"
"tercul/internal/domain"
"tercul/internal/platform/config"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace"
@ -16,9 +17,9 @@ type commentRepository struct {
}
// NewCommentRepository creates a new CommentRepository.
func NewCommentRepository(db *gorm.DB) domain.CommentRepository {
func NewCommentRepository(db *gorm.DB, cfg *config.Config) domain.CommentRepository {
return &commentRepository{
BaseRepository: NewBaseRepositoryImpl[domain.Comment](db),
BaseRepository: NewBaseRepositoryImpl[domain.Comment](db, cfg),
db: db,
tracer: otel.Tracer("comment.repository"),
}

View File

@ -7,6 +7,7 @@ import (
"testing"
repo "tercul/internal/data/sql"
"tercul/internal/domain"
"tercul/internal/platform/config"
"time"
"github.com/DATA-DOG/go-sqlmock"
@ -17,7 +18,9 @@ import (
func TestNewCommentRepository(t *testing.T) {
db, _, err := newMockDb()
require.NoError(t, err)
repo := repo.NewCommentRepository(db)
cfg, err := config.LoadConfig()
require.NoError(t, err)
repo := repo.NewCommentRepository(db, cfg)
assert.NotNil(t, repo)
}
@ -25,7 +28,9 @@ func TestCommentRepository_ListByUserID(t *testing.T) {
t.Run("should return comments for a given user id", func(t *testing.T) {
db, mock, err := newMockDb()
require.NoError(t, err)
repo := repo.NewCommentRepository(db)
cfg, err := config.LoadConfig()
require.NoError(t, err)
repo := repo.NewCommentRepository(db, cfg)
userID := uint(1)
expectedComments := []domain.Comment{
@ -50,7 +55,9 @@ func TestCommentRepository_ListByUserID(t *testing.T) {
t.Run("should return error if query fails", func(t *testing.T) {
db, mock, err := newMockDb()
require.NoError(t, err)
repo := repo.NewCommentRepository(db)
cfg, err := config.LoadConfig()
require.NoError(t, err)
repo := repo.NewCommentRepository(db, cfg)
userID := uint(1)
@ -69,7 +76,9 @@ func TestCommentRepository_ListByWorkID(t *testing.T) {
t.Run("should return comments for a given work id", func(t *testing.T) {
db, mock, err := newMockDb()
require.NoError(t, err)
repo := repo.NewCommentRepository(db)
cfg, err := config.LoadConfig()
require.NoError(t, err)
repo := repo.NewCommentRepository(db, cfg)
workID := uint(1)
expectedComments := []domain.Comment{
@ -94,7 +103,9 @@ func TestCommentRepository_ListByWorkID(t *testing.T) {
t.Run("should return error if query fails", func(t *testing.T) {
db, mock, err := newMockDb()
require.NoError(t, err)
repo := repo.NewCommentRepository(db)
cfg, err := config.LoadConfig()
require.NoError(t, err)
repo := repo.NewCommentRepository(db, cfg)
workID := uint(1)
@ -113,7 +124,9 @@ func TestCommentRepository_ListByTranslationID(t *testing.T) {
t.Run("should return comments for a given translation id", func(t *testing.T) {
db, mock, err := newMockDb()
require.NoError(t, err)
repo := repo.NewCommentRepository(db)
cfg, err := config.LoadConfig()
require.NoError(t, err)
repo := repo.NewCommentRepository(db, cfg)
translationID := uint(1)
expectedComments := []domain.Comment{
@ -138,7 +151,9 @@ func TestCommentRepository_ListByTranslationID(t *testing.T) {
t.Run("should return error if query fails", func(t *testing.T) {
db, mock, err := newMockDb()
require.NoError(t, err)
repo := repo.NewCommentRepository(db)
cfg, err := config.LoadConfig()
require.NoError(t, err)
repo := repo.NewCommentRepository(db, cfg)
translationID := uint(1)
@ -157,7 +172,9 @@ func TestCommentRepository_ListByParentID(t *testing.T) {
t.Run("should return comments for a given parent id", func(t *testing.T) {
db, mock, err := newMockDb()
require.NoError(t, err)
repo := repo.NewCommentRepository(db)
cfg, err := config.LoadConfig()
require.NoError(t, err)
repo := repo.NewCommentRepository(db, cfg)
parentID := uint(1)
expectedComments := []domain.Comment{
@ -182,7 +199,9 @@ func TestCommentRepository_ListByParentID(t *testing.T) {
t.Run("should return error if query fails", func(t *testing.T) {
db, mock, err := newMockDb()
require.NoError(t, err)
repo := repo.NewCommentRepository(db)
cfg, err := config.LoadConfig()
require.NoError(t, err)
repo := repo.NewCommentRepository(db, cfg)
parentID := uint(1)

View File

@ -3,6 +3,7 @@ package sql
import (
"context"
"tercul/internal/domain"
"tercul/internal/platform/config"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace"
@ -16,9 +17,9 @@ type contributionRepository struct {
}
// NewContributionRepository creates a new ContributionRepository.
func NewContributionRepository(db *gorm.DB) domain.ContributionRepository {
func NewContributionRepository(db *gorm.DB, cfg *config.Config) domain.ContributionRepository {
return &contributionRepository{
BaseRepository: NewBaseRepositoryImpl[domain.Contribution](db),
BaseRepository: NewBaseRepositoryImpl[domain.Contribution](db, cfg),
db: db,
tracer: otel.Tracer("contribution.repository"),
}

View File

@ -7,6 +7,7 @@ import (
"testing"
repo "tercul/internal/data/sql"
"tercul/internal/domain"
"tercul/internal/platform/config"
"time"
"github.com/DATA-DOG/go-sqlmock"
@ -17,7 +18,9 @@ import (
func TestNewContributionRepository(t *testing.T) {
db, _, err := newMockDb()
require.NoError(t, err)
repo := repo.NewContributionRepository(db)
cfg, err := config.LoadConfig()
require.NoError(t, err)
repo := repo.NewContributionRepository(db, cfg)
assert.NotNil(t, repo)
}
@ -25,7 +28,9 @@ func TestContributionRepository_ListByUserID(t *testing.T) {
t.Run("should return contributions for a given user id", func(t *testing.T) {
db, mock, err := newMockDb()
require.NoError(t, err)
repo := repo.NewContributionRepository(db)
cfg, err := config.LoadConfig()
require.NoError(t, err)
repo := repo.NewContributionRepository(db, cfg)
userID := uint(1)
expectedContributions := []domain.Contribution{
@ -50,7 +55,9 @@ func TestContributionRepository_ListByUserID(t *testing.T) {
t.Run("should return error if query fails", func(t *testing.T) {
db, mock, err := newMockDb()
require.NoError(t, err)
repo := repo.NewContributionRepository(db)
cfg, err := config.LoadConfig()
require.NoError(t, err)
repo := repo.NewContributionRepository(db, cfg)
userID := uint(1)
@ -69,7 +76,9 @@ func TestContributionRepository_ListByReviewerID(t *testing.T) {
t.Run("should return contributions for a given reviewer id", func(t *testing.T) {
db, mock, err := newMockDb()
require.NoError(t, err)
repo := repo.NewContributionRepository(db)
cfg, err := config.LoadConfig()
require.NoError(t, err)
repo := repo.NewContributionRepository(db, cfg)
reviewerID := uint(1)
expectedContributions := []domain.Contribution{
@ -94,7 +103,9 @@ func TestContributionRepository_ListByReviewerID(t *testing.T) {
t.Run("should return error if query fails", func(t *testing.T) {
db, mock, err := newMockDb()
require.NoError(t, err)
repo := repo.NewContributionRepository(db)
cfg, err := config.LoadConfig()
require.NoError(t, err)
repo := repo.NewContributionRepository(db, cfg)
reviewerID := uint(1)
@ -113,7 +124,9 @@ func TestContributionRepository_ListByWorkID(t *testing.T) {
t.Run("should return contributions for a given work id", func(t *testing.T) {
db, mock, err := newMockDb()
require.NoError(t, err)
repo := repo.NewContributionRepository(db)
cfg, err := config.LoadConfig()
require.NoError(t, err)
repo := repo.NewContributionRepository(db, cfg)
workID := uint(1)
expectedContributions := []domain.Contribution{
@ -138,7 +151,9 @@ func TestContributionRepository_ListByWorkID(t *testing.T) {
t.Run("should return error if query fails", func(t *testing.T) {
db, mock, err := newMockDb()
require.NoError(t, err)
repo := repo.NewContributionRepository(db)
cfg, err := config.LoadConfig()
require.NoError(t, err)
repo := repo.NewContributionRepository(db, cfg)
workID := uint(1)
@ -157,7 +172,9 @@ func TestContributionRepository_ListByTranslationID(t *testing.T) {
t.Run("should return contributions for a given translation id", func(t *testing.T) {
db, mock, err := newMockDb()
require.NoError(t, err)
repo := repo.NewContributionRepository(db)
cfg, err := config.LoadConfig()
require.NoError(t, err)
repo := repo.NewContributionRepository(db, cfg)
translationID := uint(1)
expectedContributions := []domain.Contribution{
@ -182,7 +199,9 @@ func TestContributionRepository_ListByTranslationID(t *testing.T) {
t.Run("should return error if query fails", func(t *testing.T) {
db, mock, err := newMockDb()
require.NoError(t, err)
repo := repo.NewContributionRepository(db)
cfg, err := config.LoadConfig()
require.NoError(t, err)
repo := repo.NewContributionRepository(db, cfg)
translationID := uint(1)
@ -201,7 +220,9 @@ func TestContributionRepository_ListByStatus(t *testing.T) {
t.Run("should return contributions for a given status", func(t *testing.T) {
db, mock, err := newMockDb()
require.NoError(t, err)
repo := repo.NewContributionRepository(db)
cfg, err := config.LoadConfig()
require.NoError(t, err)
repo := repo.NewContributionRepository(db, cfg)
status := "draft"
expectedContributions := []domain.Contribution{
@ -226,7 +247,9 @@ func TestContributionRepository_ListByStatus(t *testing.T) {
t.Run("should return error if query fails", func(t *testing.T) {
db, mock, err := newMockDb()
require.NoError(t, err)
repo := repo.NewContributionRepository(db)
cfg, err := config.LoadConfig()
require.NoError(t, err)
repo := repo.NewContributionRepository(db, cfg)
status := "draft"

View File

@ -3,6 +3,7 @@ package sql
import (
"context"
"tercul/internal/domain"
"tercul/internal/platform/config"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace"
@ -16,9 +17,9 @@ type copyrightClaimRepository struct {
}
// NewCopyrightClaimRepository creates a new CopyrightClaimRepository.
func NewCopyrightClaimRepository(db *gorm.DB) domain.CopyrightClaimRepository {
func NewCopyrightClaimRepository(db *gorm.DB, cfg *config.Config) domain.CopyrightClaimRepository {
return &copyrightClaimRepository{
BaseRepository: NewBaseRepositoryImpl[domain.CopyrightClaim](db),
BaseRepository: NewBaseRepositoryImpl[domain.CopyrightClaim](db, cfg),
db: db,
tracer: otel.Tracer("copyright_claim.repository"),
}

View File

@ -4,6 +4,7 @@ import (
"context"
"errors"
"tercul/internal/domain"
"tercul/internal/platform/config"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace"
@ -17,9 +18,9 @@ type copyrightRepository struct {
}
// NewCopyrightRepository creates a new CopyrightRepository.
func NewCopyrightRepository(db *gorm.DB) domain.CopyrightRepository {
func NewCopyrightRepository(db *gorm.DB, cfg *config.Config) domain.CopyrightRepository {
return &copyrightRepository{
BaseRepository: NewBaseRepositoryImpl[domain.Copyright](db),
BaseRepository: NewBaseRepositoryImpl[domain.Copyright](db, cfg),
db: db,
tracer: otel.Tracer("copyright.repository"),
}

View File

@ -6,6 +6,7 @@ import (
"testing"
"tercul/internal/data/sql"
"tercul/internal/domain"
"tercul/internal/platform/config"
"time"
"github.com/DATA-DOG/go-sqlmock"
@ -41,7 +42,9 @@ func (s *CopyrightRepositoryTestSuite) SetupTest() {
s.db = gormDB
s.mock = mock
s.repo = sql.NewCopyrightRepository(s.db)
cfg, err := config.LoadConfig()
s.Require().NoError(err)
s.repo = sql.NewCopyrightRepository(s.db, cfg)
}
// TearDownTest checks if all expectations were met.

View File

@ -4,6 +4,7 @@ import (
"context"
"errors"
"tercul/internal/domain"
"tercul/internal/platform/config"
"gorm.io/gorm"
)
@ -14,9 +15,9 @@ type countryRepository struct {
}
// NewCountryRepository creates a new CountryRepository.
func NewCountryRepository(db *gorm.DB) domain.CountryRepository {
func NewCountryRepository(db *gorm.DB, cfg *config.Config) domain.CountryRepository {
return &countryRepository{
BaseRepository: NewBaseRepositoryImpl[domain.Country](db),
BaseRepository: NewBaseRepositoryImpl[domain.Country](db, cfg),
db: db,
}
}

View File

@ -3,6 +3,7 @@ package sql
import (
"context"
"tercul/internal/domain"
"tercul/internal/platform/config"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace"
@ -16,9 +17,9 @@ type edgeRepository struct {
}
// NewEdgeRepository creates a new EdgeRepository.
func NewEdgeRepository(db *gorm.DB) domain.EdgeRepository {
func NewEdgeRepository(db *gorm.DB, cfg *config.Config) domain.EdgeRepository {
return &edgeRepository{
BaseRepository: NewBaseRepositoryImpl[domain.Edge](db),
BaseRepository: NewBaseRepositoryImpl[domain.Edge](db, cfg),
db: db,
tracer: otel.Tracer("edge.repository"),
}

View File

@ -4,6 +4,7 @@ import (
"context"
"errors"
"tercul/internal/domain"
"tercul/internal/platform/config"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace"
@ -17,9 +18,9 @@ type editionRepository struct {
}
// NewEditionRepository creates a new EditionRepository.
func NewEditionRepository(db *gorm.DB) domain.EditionRepository {
func NewEditionRepository(db *gorm.DB, cfg *config.Config) domain.EditionRepository {
return &editionRepository{
BaseRepository: NewBaseRepositoryImpl[domain.Edition](db),
BaseRepository: NewBaseRepositoryImpl[domain.Edition](db, cfg),
db: db,
tracer: otel.Tracer("edition.repository"),
}

View File

@ -4,6 +4,7 @@ import (
"context"
"errors"
"tercul/internal/domain"
"tercul/internal/platform/config"
"time"
"go.opentelemetry.io/otel"
@ -18,9 +19,9 @@ type emailVerificationRepository struct {
}
// NewEmailVerificationRepository creates a new EmailVerificationRepository.
func NewEmailVerificationRepository(db *gorm.DB) domain.EmailVerificationRepository {
func NewEmailVerificationRepository(db *gorm.DB, cfg *config.Config) domain.EmailVerificationRepository {
return &emailVerificationRepository{
BaseRepository: NewBaseRepositoryImpl[domain.EmailVerification](db),
BaseRepository: NewBaseRepositoryImpl[domain.EmailVerification](db, cfg),
db: db,
tracer: otel.Tracer("email_verification.repository"),
}

View File

@ -3,6 +3,7 @@ package sql
import (
"context"
"tercul/internal/domain"
"tercul/internal/platform/config"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace"
@ -16,9 +17,9 @@ type likeRepository struct {
}
// NewLikeRepository creates a new LikeRepository.
func NewLikeRepository(db *gorm.DB) domain.LikeRepository {
func NewLikeRepository(db *gorm.DB, cfg *config.Config) domain.LikeRepository {
return &likeRepository{
BaseRepository: NewBaseRepositoryImpl[domain.Like](db),
BaseRepository: NewBaseRepositoryImpl[domain.Like](db, cfg),
db: db,
tracer: otel.Tracer("like.repository"),
}

View File

@ -4,6 +4,7 @@ import (
"context"
"tercul/internal/domain"
"tercul/internal/domain/localization"
"tercul/internal/platform/config"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace"
@ -15,7 +16,7 @@ type localizationRepository struct {
tracer trace.Tracer
}
func NewLocalizationRepository(db *gorm.DB) localization.LocalizationRepository {
func NewLocalizationRepository(db *gorm.DB, cfg *config.Config) localization.LocalizationRepository {
return &localizationRepository{
db: db,
tracer: otel.Tracer("localization.repository"),

View File

@ -4,6 +4,7 @@ import (
"context"
"tercul/internal/domain"
"tercul/internal/domain/work"
"tercul/internal/platform/config"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace"
@ -17,9 +18,9 @@ type monetizationRepository struct {
}
// NewMonetizationRepository creates a new MonetizationRepository.
func NewMonetizationRepository(db *gorm.DB) domain.MonetizationRepository {
func NewMonetizationRepository(db *gorm.DB, cfg *config.Config) domain.MonetizationRepository {
return &monetizationRepository{
BaseRepository: NewBaseRepositoryImpl[domain.Monetization](db),
BaseRepository: NewBaseRepositoryImpl[domain.Monetization](db, cfg),
db: db,
tracer: otel.Tracer("monetization.repository"),
}

View File

@ -6,6 +6,7 @@ import (
"tercul/internal/data/sql"
"tercul/internal/domain"
workdomain "tercul/internal/domain/work"
"tercul/internal/platform/config"
"tercul/internal/testutil"
"github.com/stretchr/testify/suite"
@ -18,7 +19,9 @@ type MonetizationRepositoryTestSuite struct {
func (s *MonetizationRepositoryTestSuite) SetupSuite() {
s.IntegrationTestSuite.SetupSuite(testutil.DefaultTestConfig())
s.MonetizationRepo = sql.NewMonetizationRepository(s.DB)
cfg, err := config.LoadConfig()
s.Require().NoError(err)
s.MonetizationRepo = sql.NewMonetizationRepository(s.DB, cfg)
}
func (s *MonetizationRepositoryTestSuite) SetupTest() {

View File

@ -4,6 +4,7 @@ import (
"context"
"errors"
"tercul/internal/domain"
"tercul/internal/platform/config"
"time"
"go.opentelemetry.io/otel"
@ -18,9 +19,9 @@ type passwordResetRepository struct {
}
// NewPasswordResetRepository creates a new PasswordResetRepository.
func NewPasswordResetRepository(db *gorm.DB) domain.PasswordResetRepository {
func NewPasswordResetRepository(db *gorm.DB, cfg *config.Config) domain.PasswordResetRepository {
return &passwordResetRepository{
BaseRepository: NewBaseRepositoryImpl[domain.PasswordReset](db),
BaseRepository: NewBaseRepositoryImpl[domain.PasswordReset](db, cfg),
db: db,
tracer: otel.Tracer("password_reset.repository"),
}

View File

@ -4,6 +4,7 @@ import (
"context"
"math"
"tercul/internal/domain"
"tercul/internal/platform/config"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace"
@ -17,9 +18,9 @@ type placeRepository struct {
}
// NewPlaceRepository creates a new PlaceRepository.
func NewPlaceRepository(db *gorm.DB) domain.PlaceRepository {
func NewPlaceRepository(db *gorm.DB, cfg *config.Config) domain.PlaceRepository {
return &placeRepository{
BaseRepository: NewBaseRepositoryImpl[domain.Place](db),
BaseRepository: NewBaseRepositoryImpl[domain.Place](db, cfg),
db: db,
tracer: otel.Tracer("place.repository"),
}

View File

@ -3,6 +3,7 @@ package sql
import (
"context"
"tercul/internal/domain"
"tercul/internal/platform/config"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace"
@ -16,9 +17,9 @@ type publisherRepository struct {
}
// NewPublisherRepository creates a new PublisherRepository.
func NewPublisherRepository(db *gorm.DB) domain.PublisherRepository {
func NewPublisherRepository(db *gorm.DB, cfg *config.Config) domain.PublisherRepository {
return &publisherRepository{
BaseRepository: NewBaseRepositoryImpl[domain.Publisher](db),
BaseRepository: NewBaseRepositoryImpl[domain.Publisher](db, cfg),
db: db,
tracer: otel.Tracer("publisher.repository"),
}

View File

@ -6,6 +6,7 @@ import (
"tercul/internal/domain/auth"
"tercul/internal/domain/localization"
"tercul/internal/domain/work"
"tercul/internal/platform/config"
"gorm.io/gorm"
)
@ -26,31 +27,33 @@ type Repositories struct {
Source domain.SourceRepository
Copyright domain.CopyrightRepository
Monetization domain.MonetizationRepository
Contribution domain.ContributionRepository
Analytics analytics.Repository
Auth auth.AuthRepository
Localization localization.LocalizationRepository
}
// NewRepositories creates a new Repositories container
func NewRepositories(db *gorm.DB) *Repositories {
func NewRepositories(db *gorm.DB, cfg *config.Config) *Repositories {
return &Repositories{
Work: NewWorkRepository(db),
User: NewUserRepository(db),
Author: NewAuthorRepository(db),
Translation: NewTranslationRepository(db),
Comment: NewCommentRepository(db),
Like: NewLikeRepository(db),
Bookmark: NewBookmarkRepository(db),
Collection: NewCollectionRepository(db),
Tag: NewTagRepository(db),
Category: NewCategoryRepository(db),
Book: NewBookRepository(db),
Publisher: NewPublisherRepository(db),
Source: NewSourceRepository(db),
Copyright: NewCopyrightRepository(db),
Monetization: NewMonetizationRepository(db),
Analytics: NewAnalyticsRepository(db),
Auth: NewAuthRepository(db),
Localization: NewLocalizationRepository(db),
Work: NewWorkRepository(db, cfg),
User: NewUserRepository(db, cfg),
Author: NewAuthorRepository(db, cfg),
Translation: NewTranslationRepository(db, cfg),
Comment: NewCommentRepository(db, cfg),
Like: NewLikeRepository(db, cfg),
Bookmark: NewBookmarkRepository(db, cfg),
Collection: NewCollectionRepository(db, cfg),
Tag: NewTagRepository(db, cfg),
Category: NewCategoryRepository(db, cfg),
Book: NewBookRepository(db, cfg),
Publisher: NewPublisherRepository(db, cfg),
Source: NewSourceRepository(db, cfg),
Copyright: NewCopyrightRepository(db, cfg),
Monetization: NewMonetizationRepository(db, cfg),
Contribution: NewContributionRepository(db, cfg),
Analytics: NewAnalyticsRepository(db, cfg),
Auth: NewAuthRepository(db, cfg),
Localization: NewLocalizationRepository(db, cfg),
}
}

View File

@ -4,6 +4,7 @@ import (
"context"
"errors"
"tercul/internal/domain"
"tercul/internal/platform/config"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace"
@ -17,9 +18,9 @@ type sourceRepository struct {
}
// NewSourceRepository creates a new SourceRepository.
func NewSourceRepository(db *gorm.DB) domain.SourceRepository {
func NewSourceRepository(db *gorm.DB, cfg *config.Config) domain.SourceRepository {
return &sourceRepository{
BaseRepository: NewBaseRepositoryImpl[domain.Source](db),
BaseRepository: NewBaseRepositoryImpl[domain.Source](db, cfg),
db: db,
tracer: otel.Tracer("source.repository"),
}

View File

@ -4,6 +4,7 @@ import (
"context"
"errors"
"tercul/internal/domain"
"tercul/internal/platform/config"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace"
@ -17,9 +18,9 @@ type tagRepository struct {
}
// NewTagRepository creates a new TagRepository.
func NewTagRepository(db *gorm.DB) domain.TagRepository {
func NewTagRepository(db *gorm.DB, cfg *config.Config) domain.TagRepository {
return &tagRepository{
BaseRepository: NewBaseRepositoryImpl[domain.Tag](db),
BaseRepository: NewBaseRepositoryImpl[domain.Tag](db, cfg),
db: db,
tracer: otel.Tracer("tag.repository"),
}

View File

@ -3,6 +3,7 @@ package sql
import (
"context"
"tercul/internal/domain"
"tercul/internal/platform/config"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace"
@ -17,9 +18,9 @@ type translationRepository struct {
}
// NewTranslationRepository creates a new TranslationRepository.
func NewTranslationRepository(db *gorm.DB) domain.TranslationRepository {
func NewTranslationRepository(db *gorm.DB, cfg *config.Config) domain.TranslationRepository {
return &translationRepository{
BaseRepository: NewBaseRepositoryImpl[domain.Translation](db),
BaseRepository: NewBaseRepositoryImpl[domain.Translation](db, cfg),
db: db,
tracer: otel.Tracer("translation.repository"),
}

View File

@ -4,6 +4,7 @@ import (
"context"
"errors"
"tercul/internal/domain"
"tercul/internal/platform/config"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace"
@ -17,9 +18,9 @@ type userProfileRepository struct {
}
// NewUserProfileRepository creates a new UserProfileRepository.
func NewUserProfileRepository(db *gorm.DB) domain.UserProfileRepository {
func NewUserProfileRepository(db *gorm.DB, cfg *config.Config) domain.UserProfileRepository {
return &userProfileRepository{
BaseRepository: NewBaseRepositoryImpl[domain.UserProfile](db),
BaseRepository: NewBaseRepositoryImpl[domain.UserProfile](db, cfg),
db: db,
tracer: otel.Tracer("user_profile.repository"),
}

View File

@ -4,6 +4,7 @@ import (
"context"
"errors"
"tercul/internal/domain"
"tercul/internal/platform/config"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace"
@ -17,9 +18,9 @@ type userRepository struct {
}
// NewUserRepository creates a new UserRepository.
func NewUserRepository(db *gorm.DB) domain.UserRepository {
func NewUserRepository(db *gorm.DB, cfg *config.Config) domain.UserRepository {
return &userRepository{
BaseRepository: NewBaseRepositoryImpl[domain.User](db),
BaseRepository: NewBaseRepositoryImpl[domain.User](db, cfg),
db: db,
tracer: otel.Tracer("user.repository"),
}

View File

@ -4,6 +4,7 @@ import (
"context"
"errors"
"tercul/internal/domain"
"tercul/internal/platform/config"
"time"
"go.opentelemetry.io/otel"
@ -18,9 +19,9 @@ type userSessionRepository struct {
}
// NewUserSessionRepository creates a new UserSessionRepository.
func NewUserSessionRepository(db *gorm.DB) domain.UserSessionRepository {
func NewUserSessionRepository(db *gorm.DB, cfg *config.Config) domain.UserSessionRepository {
return &userSessionRepository{
BaseRepository: NewBaseRepositoryImpl[domain.UserSession](db),
BaseRepository: NewBaseRepositoryImpl[domain.UserSession](db, cfg),
db: db,
tracer: otel.Tracer("user_session.repository"),
}

View File

@ -6,6 +6,7 @@ import (
"fmt"
"tercul/internal/domain"
"tercul/internal/domain/work"
"tercul/internal/platform/config"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace"
@ -19,9 +20,9 @@ type workRepository struct {
}
// NewWorkRepository creates a new WorkRepository.
func NewWorkRepository(db *gorm.DB) work.WorkRepository {
func NewWorkRepository(db *gorm.DB, cfg *config.Config) work.WorkRepository {
return &workRepository{
BaseRepository: NewBaseRepositoryImpl[work.Work](db),
BaseRepository: NewBaseRepositoryImpl[work.Work](db, cfg),
db: db,
tracer: otel.Tracer("work.repository"),
}

View File

@ -6,6 +6,7 @@ import (
"tercul/internal/data/sql"
"tercul/internal/domain"
"tercul/internal/domain/work"
"tercul/internal/platform/config"
"tercul/internal/testutil"
"github.com/stretchr/testify/suite"
@ -18,7 +19,9 @@ type WorkRepositoryTestSuite struct {
func (s *WorkRepositoryTestSuite) SetupSuite() {
s.IntegrationTestSuite.SetupSuite(testutil.DefaultTestConfig())
s.WorkRepo = sql.NewWorkRepository(s.DB)
cfg, err := config.LoadConfig()
s.Require().NoError(err)
s.WorkRepo = sql.NewWorkRepository(s.DB, cfg)
}
func (s *WorkRepositoryTestSuite) TestCreateWork() {

View File

@ -31,9 +31,8 @@ type MemoryAnalysisCache struct {
}
// NewMemoryAnalysisCache creates a new MemoryAnalysisCache
func NewMemoryAnalysisCache(enabled bool) *MemoryAnalysisCache {
// capacity from config
cap := config.Cfg.NLPMemoryCacheCap
func NewMemoryAnalysisCache(cfg *config.Config, enabled bool) *MemoryAnalysisCache {
cap := cfg.NLPMemoryCacheCap
if cap <= 0 {
cap = 1024
}
@ -82,13 +81,19 @@ func (c *MemoryAnalysisCache) IsEnabled() bool {
type RedisAnalysisCache struct {
cache cache.Cache
enabled bool
ttl time.Duration
}
// NewRedisAnalysisCache creates a new RedisAnalysisCache
func NewRedisAnalysisCache(cache cache.Cache, enabled bool) *RedisAnalysisCache {
func NewRedisAnalysisCache(cfg *config.Config, cache cache.Cache, enabled bool) *RedisAnalysisCache {
ttlSeconds := cfg.NLPRedisCacheTTLSeconds
if ttlSeconds <= 0 {
ttlSeconds = 3600 // default 1 hour
}
return &RedisAnalysisCache{
cache: cache,
enabled: enabled,
ttl: time.Duration(ttlSeconds) * time.Second,
}
}
@ -113,9 +118,7 @@ func (c *RedisAnalysisCache) Set(ctx context.Context, key string, result *Analys
return nil
}
// TTL from config
ttlSeconds := config.Cfg.NLPRedisCacheTTLSeconds
err := c.cache.Set(ctx, key, result, time.Duration(ttlSeconds)*time.Second)
err := c.cache.Set(ctx, key, result, c.ttl)
if err != nil {
log.FromContext(ctx).With("key", key).Error(err, "Failed to cache analysis result")
return err

View File

@ -19,6 +19,7 @@ type LinguisticsFactory struct {
// NewLinguisticsFactory creates a new LinguisticsFactory with all components
func NewLinguisticsFactory(
cfg *config.Config,
db *gorm.DB,
cache cache.Cache,
concurrency int,
@ -32,18 +33,18 @@ func NewLinguisticsFactory(
textAnalyzer = textAnalyzer.WithSentimentProvider(sentimentProvider)
// Wire language detector: lingua-go (configurable)
if config.Cfg.NLPUseLingua {
if cfg.NLPUseLingua {
textAnalyzer = textAnalyzer.WithLanguageDetector(NewLinguaLanguageDetector())
}
// Wire keyword provider: lightweight TF-IDF approximation (configurable)
if config.Cfg.NLPUseTFIDF {
if cfg.NLPUseTFIDF {
textAnalyzer = textAnalyzer.WithKeywordProvider(NewTFIDFKeywordProvider())
}
// Create cache components
memoryCache := NewMemoryAnalysisCache(cacheEnabled)
redisCache := NewRedisAnalysisCache(cache, cacheEnabled)
memoryCache := NewMemoryAnalysisCache(cfg, cacheEnabled)
redisCache := NewRedisAnalysisCache(cfg, cache, cacheEnabled)
analysisCache := NewCompositeAnalysisCache(memoryCache, redisCache, cacheEnabled)
// Create repository

View File

@ -1,13 +1,17 @@
package linguistics
import (
"github.com/stretchr/testify/require"
"tercul/internal/platform/config"
"testing"
"github.com/stretchr/testify/require"
)
func TestFactory_WiresProviders(t *testing.T) {
// We won't spin a DB/cache here; this is a smoke test of wiring methods
f := NewLinguisticsFactory(nil, nil, 2, true, nil)
cfg, err := config.LoadConfig()
require.NoError(t, err)
f := NewLinguisticsFactory(cfg, nil, nil, 2, true, nil)
ta := f.GetTextAnalyzer().(*BasicTextAnalyzer)
require.NotNil(t, ta)
}

View File

@ -17,8 +17,8 @@ type BatchProcessor struct {
}
// NewBatchProcessor creates a new BatchProcessor
func NewBatchProcessor(db *gorm.DB) *BatchProcessor {
batchSize := config.Cfg.BatchSize
func NewBatchProcessor(db *gorm.DB, cfg *config.Config) *BatchProcessor {
batchSize := cfg.BatchSize
if batchSize <= 0 {
batchSize = DefaultBatchSize
}

View File

@ -54,6 +54,6 @@ func (s *SyncJob) SyncEdgesBatch(ctx context.Context, batchSize, offset int) err
edgeMaps = append(edgeMaps, edgeMap)
}
batchProcessor := NewBatchProcessor(s.DB)
batchProcessor := NewBatchProcessor(s.DB, s.Cfg)
return batchProcessor.CreateObjectsBatch(ctx, "Edge", edgeMaps)
}

View File

@ -76,6 +76,6 @@ func (s *SyncJob) SyncAllEntities(ctx context.Context) error {
// syncEntities is a generic function to sync a given entity type.
func (s *SyncJob) syncEntities(className string, ctx context.Context) error {
batchProcessor := NewBatchProcessor(s.DB)
batchProcessor := NewBatchProcessor(s.DB, s.Cfg)
return batchProcessor.ProcessAllEntities(ctx, className)
}

View File

@ -64,6 +64,6 @@ func RegisterQueueHandlers(srv *asynq.Server, syncJob *SyncJob) {
mux.HandleFunc(TaskEntitySync, syncJob.HandleEntitySync)
mux.HandleFunc(TaskEdgeSync, syncJob.HandleEdgeSync)
if err := srv.Run(mux); err != nil {
log.Fatalf("Failed to start asynq server: %v", err)
log.Printf("Failed to start asynq server: %v", err)
}
}

View File

@ -3,6 +3,7 @@ package sync
import (
"context"
"log"
"tercul/internal/platform/config"
"github.com/hibiken/asynq"
"gorm.io/gorm"
@ -12,13 +13,15 @@ import (
type SyncJob struct {
DB *gorm.DB
AsynqClient *asynq.Client
Cfg *config.Config
}
// NewSyncJob initializes a new SyncJob.
func NewSyncJob(db *gorm.DB, aClient *asynq.Client) *SyncJob {
func NewSyncJob(db *gorm.DB, aClient *asynq.Client, cfg *config.Config) *SyncJob {
return &SyncJob{
DB: db,
AsynqClient: aClient,
Cfg: cfg,
}
}

View File

@ -41,16 +41,17 @@ type JWTManager struct {
}
// NewJWTManager creates a new JWT manager
func NewJWTManager() *JWTManager {
secretKey := config.Cfg.JWTSecret
func NewJWTManager(cfg *config.Config) *JWTManager {
secretKey := cfg.JWTSecret
if secretKey == "" {
secretKey = "default-secret-key-change-in-production"
}
duration := config.Cfg.JWTExpiration
if duration == 0 {
duration = 24 * time.Hour // Default to 24 hours
durationInHours := cfg.JWTExpiration
if durationInHours <= 0 {
durationInHours = 24 // Default to 24 hours
}
duration := time.Duration(durationInHours) * time.Hour
return &JWTManager{
secretKey: []byte(secretKey),

View File

@ -54,7 +54,7 @@ func AuthMiddleware(jwtManager *JWTManager) func(http.Handler) http.Handler {
}
// RoleMiddleware creates middleware for role-based authorization
func RoleMiddleware(requiredRole string) func(http.Handler) http.Handler {
func RoleMiddleware(jwtManager *JWTManager, requiredRole string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
logger := log.FromContext(r.Context())
@ -65,7 +65,6 @@ func RoleMiddleware(requiredRole string) func(http.Handler) http.Handler {
return
}
jwtManager := NewJWTManager()
if err := jwtManager.RequireRole(claims.Role, requiredRole); err != nil {
logger.With("user_role", claims.Role).With("required_role", requiredRole).Warn("Authorization failed - insufficient role")
http.Error(w, "Forbidden", http.StatusForbidden)
@ -142,13 +141,12 @@ func RequireAuth(ctx context.Context) (*Claims, error) {
}
// RequireRole ensures the user has the required role
func RequireRole(ctx context.Context, requiredRole string) (*Claims, error) {
func RequireRole(ctx context.Context, jwtManager *JWTManager, requiredRole string) (*Claims, error) {
claims, err := RequireAuth(ctx)
if err != nil {
return nil, err
}
jwtManager := NewJWTManager()
if err := jwtManager.RequireRole(claims.Role, requiredRole); err != nil {
return nil, err
}

View File

@ -21,9 +21,14 @@ type Config struct {
RedisAddr string `mapstructure:"REDIS_ADDR"`
RedisPassword string `mapstructure:"REDIS_PASSWORD"`
RedisDB int `mapstructure:"REDIS_DB"`
SyncBatchSize int `mapstructure:"SYNC_BATCH_SIZE"`
BatchSize int `mapstructure:"BATCH_SIZE"`
RateLimit int `mapstructure:"RATE_LIMIT"`
RateLimitBurst int `mapstructure:"RATE_LIMIT_BURST"`
PageSize int `mapstructure:"PAGE_SIZE"`
NLPMemoryCacheCap int `mapstructure:"NLP_MEMORY_CACHE_CAP"`
NLPRedisCacheTTLSeconds int `mapstructure:"NLP_REDIS_CACHE_TTL_SECONDS"`
NLPUseLingua bool `mapstructure:"NLP_USE_LINGUA"`
NLPUseTFIDF bool `mapstructure:"NLP_USE_TFIDF"`
}
// LoadConfig reads configuration from file or environment variables.
@ -43,9 +48,13 @@ func LoadConfig() (*Config, error) {
viper.SetDefault("REDIS_ADDR", "localhost:6379")
viper.SetDefault("REDIS_PASSWORD", "")
viper.SetDefault("REDIS_DB", 0)
viper.SetDefault("SYNC_BATCH_SIZE", 100)
viper.SetDefault("BATCH_SIZE", 100)
viper.SetDefault("RATE_LIMIT", 10)
viper.SetDefault("RATE_LIMIT_BURST", 100)
viper.SetDefault("NLP_MEMORY_CACHE_CAP", 1024)
viper.SetDefault("NLP_REDIS_CACHE_TTL_SECONDS", 3600)
viper.SetDefault("NLP_USE_LINGUA", true)
viper.SetDefault("NLP_USE_TFIDF", true)
viper.AutomaticEnv()

View File

@ -21,10 +21,12 @@ type RateLimiter struct {
}
// NewRateLimiter creates a new rate limiter
func NewRateLimiter(rate, capacity int) *RateLimiter {
func NewRateLimiter(cfg *config.Config) *RateLimiter {
rate := cfg.RateLimit
if rate <= 0 {
rate = 10 // default rate: 10 requests per second
}
capacity := cfg.RateLimitBurst
if capacity <= 0 {
capacity = 100 // default capacity: 100 tokens
}
@ -73,9 +75,9 @@ func minF(a, b float64) float64 {
}
// RateLimitMiddleware creates a middleware that applies rate limiting
func RateLimitMiddleware(next http.Handler) http.Handler {
rateLimiter := NewRateLimiter(config.Cfg.RateLimit, config.Cfg.RateLimitBurst)
func RateLimitMiddleware(cfg *config.Config) func(http.Handler) http.Handler {
rateLimiter := NewRateLimiter(cfg)
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Use X-Client-ID header for client identification in tests
clientID := r.Header.Get("X-Client-ID")
@ -97,4 +99,5 @@ func RateLimitMiddleware(next http.Handler) http.Handler {
// Continue to the next handler
next.ServeHTTP(w, r)
})
}
}

View File

@ -4,10 +4,9 @@ import (
"net/http"
"net/http/httptest"
"testing"
"time"
"tercul/internal/platform/config"
platformhttp "tercul/internal/platform/http"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
@ -20,8 +19,8 @@ type RateLimiterSuite struct {
// TestRateLimiter tests the RateLimiter
func (s *RateLimiterSuite) TestRateLimiter() {
// Create a new rate limiter with 2 requests per second and a burst of 3
limiter := platformhttp.NewRateLimiter(2, 3)
cfg := &config.Config{RateLimit: 2, RateLimitBurst: 3}
limiter := platformhttp.NewRateLimiter(cfg)
// Test that the first 3 requests are allowed (burst)
for i := 0; i < 3; i++ {
@ -49,8 +48,8 @@ func (s *RateLimiterSuite) TestRateLimiter() {
// TestRateLimiterMultipleClients tests the RateLimiter with multiple clients
func (s *RateLimiterSuite) TestRateLimiterMultipleClients() {
// Create a new rate limiter with 2 requests per second and a burst of 3
limiter := platformhttp.NewRateLimiter(2, 3)
cfg := &config.Config{RateLimit: 2, RateLimitBurst: 3}
limiter := platformhttp.NewRateLimiter(cfg)
// Test that the first 3 requests for client1 are allowed (burst)
for i := 0; i < 3; i++ {
@ -75,17 +74,15 @@ func (s *RateLimiterSuite) TestRateLimiterMultipleClients() {
// TestRateLimiterMiddleware tests the RateLimiterMiddleware
func (s *RateLimiterSuite) TestRateLimiterMiddleware() {
// Set config to match test expectations
config.Cfg.RateLimit = 2
config.Cfg.RateLimitBurst = 3
cfg := &config.Config{RateLimit: 2, RateLimitBurst: 3}
// Create a test handler that always returns 200 OK
testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
// Create a rate limiter middleware with 2 requests per second and a burst of 3
middleware := platformhttp.RateLimitMiddleware(testHandler)
// Create a rate limiter middleware
middleware := platformhttp.RateLimitMiddleware(cfg)(testHandler)
// Create a test server
server := httptest.NewServer(middleware)
@ -144,22 +141,22 @@ func TestRateLimiterSuite(t *testing.T) {
// TestNewRateLimiter tests the NewRateLimiter function
func TestNewRateLimiter(t *testing.T) {
// Test with valid parameters
limiter := platformhttp.NewRateLimiter(10, 20)
limiter := platformhttp.NewRateLimiter(&config.Config{RateLimit: 10, RateLimitBurst: 20})
assert.NotNil(t, limiter, "NewRateLimiter should return a non-nil limiter")
// Test with zero rate (should use default)
limiter = platformhttp.NewRateLimiter(0, 20)
limiter = platformhttp.NewRateLimiter(&config.Config{RateLimit: 0, RateLimitBurst: 20})
assert.NotNil(t, limiter, "NewRateLimiter should return a non-nil limiter with default rate")
// Test with zero capacity (should use default)
limiter = platformhttp.NewRateLimiter(10, 0)
limiter = platformhttp.NewRateLimiter(&config.Config{RateLimit: 10, RateLimitBurst: 0})
assert.NotNil(t, limiter, "NewRateLimiter should return a non-nil limiter with default capacity")
// Test with negative rate (should use default)
limiter = platformhttp.NewRateLimiter(-10, 20)
limiter = platformhttp.NewRateLimiter(&config.Config{RateLimit: -10, RateLimitBurst: 20})
assert.NotNil(t, limiter, "NewRateLimiter should return a non-nil limiter with default rate")
// Test with negative capacity (should use default)
limiter = platformhttp.NewRateLimiter(10, -20)
limiter = platformhttp.NewRateLimiter(&config.Config{RateLimit: 10, RateLimitBurst: -20})
assert.NotNil(t, limiter, "NewRateLimiter should return a non-nil limiter with default capacity")
}

View File

@ -11,9 +11,10 @@ import (
"tercul/internal/data/sql"
"tercul/internal/domain"
"tercul/internal/domain/search"
platform_auth "tercul/internal/platform/auth"
"tercul/internal/domain/work"
"tercul/internal/jobs/linguistics"
platform_auth "tercul/internal/platform/auth"
platform_config "tercul/internal/platform/config"
"time"
"github.com/stretchr/testify/suite"
@ -106,21 +107,21 @@ func DefaultTestConfig() *TestConfig {
}
// SetupSuite sets up the test suite with the specified configuration
func (s *IntegrationTestSuite) SetupSuite(config *TestConfig) {
if config == nil {
config = DefaultTestConfig()
func (s *IntegrationTestSuite) SetupSuite(testConfig *TestConfig) {
if testConfig == nil {
testConfig = DefaultTestConfig()
}
var dbPath string
if !config.UseInMemoryDB && config.DBPath != "" {
if !testConfig.UseInMemoryDB && testConfig.DBPath != "" {
// Clean up previous test database file before starting
_ = os.Remove(config.DBPath)
_ = os.Remove(testConfig.DBPath)
// Ensure directory exists
dir := filepath.Dir(config.DBPath)
dir := filepath.Dir(testConfig.DBPath)
if err := os.MkdirAll(dir, 0755); err != nil {
s.T().Fatalf("Failed to create database directory: %v", err)
}
dbPath = config.DBPath
dbPath = testConfig.DBPath
} else {
// Use in-memory database
dbPath = ":memory:"
@ -131,7 +132,7 @@ func (s *IntegrationTestSuite) SetupSuite(config *TestConfig) {
log.New(os.Stdout, "\r\n", log.LstdFlags),
logger.Config{
SlowThreshold: time.Second,
LogLevel: config.LogLevel,
LogLevel: testConfig.LogLevel,
IgnoreRecordNotFoundError: true,
Colorful: false,
},
@ -155,7 +156,12 @@ func (s *IntegrationTestSuite) SetupSuite(config *TestConfig) {
&domain.TranslationStats{}, &TestEntity{}, &domain.CollectionWork{},
)
repos := sql.NewRepositories(s.DB)
cfg, err := platform_config.LoadConfig()
if err != nil {
s.T().Fatalf("Failed to load config: %v", err)
}
repos := sql.NewRepositories(s.DB, cfg)
var searchClient search.SearchClient = &mockSearchClient{}
analysisRepo := linguistics.NewGORMAnalysisRepository(s.DB)
sentimentProvider, err := linguistics.NewGoVADERSentimentProvider()
@ -163,7 +169,7 @@ func (s *IntegrationTestSuite) SetupSuite(config *TestConfig) {
s.T().Fatalf("Failed to create sentiment provider: %v", err)
}
analyticsService := analytics.NewService(repos.Analytics, analysisRepo, repos.Translation, repos.Work, sentimentProvider)
jwtManager := platform_auth.NewJWTManager()
jwtManager := platform_auth.NewJWTManager(cfg)
deps := app.Dependencies{
WorkRepo: repos.Work,
@ -181,6 +187,7 @@ func (s *IntegrationTestSuite) SetupSuite(config *TestConfig) {
SourceRepo: repos.Source,
CopyrightRepo: repos.Copyright,
MonetizationRepo: repos.Monetization,
ContributionRepo: repos.Contribution,
AnalyticsRepo: repos.Analytics,
AuthRepo: repos.Auth,
LocalizationRepo: repos.Localization,