add service
All checks were successful
Deploy Smart Search Backend Test / deploy (push) Successful in 2m55s

This commit is contained in:
vallyenfail
2026-01-19 17:20:21 +03:00
parent b56b833680
commit 33b70d1ee4
10 changed files with 117 additions and 83 deletions

View File

@@ -2,13 +2,14 @@ package main
import ( import (
"context" "context"
"log" "os"
"github.com/jackc/pgx/v5/pgxpool" "github.com/jackc/pgx/v5/pgxpool"
_ "github.com/jackc/pgx/v5/stdlib" _ "github.com/jackc/pgx/v5/stdlib"
rkboot "github.com/rookie-ninja/rk-boot/v2" rkboot "github.com/rookie-ninja/rk-boot/v2"
rkentry "github.com/rookie-ninja/rk-entry/v2/entry" rkentry "github.com/rookie-ninja/rk-entry/v2/entry"
rkgrpc "github.com/rookie-ninja/rk-grpc/v2/boot" rkgrpc "github.com/rookie-ninja/rk-grpc/v2/boot"
"go.uber.org/zap"
"google.golang.org/grpc" "google.golang.org/grpc"
"git.techease.ru/Smart-search/smart-search-back/internal/config" "git.techease.ru/Smart-search/smart-search-back/internal/config"
@@ -19,50 +20,51 @@ import (
) )
func main() { func main() {
cfg, err := config.Load("config/config.yaml")
if err != nil {
log.Fatalf("Failed to load config: %v", err)
}
ctx := context.Background()
if err := database.RunMigrations(cfg.DatabaseURL()); err != nil {
log.Fatalf("Failed to run migrations: %v", err)
}
pool, err := pgxpool.New(ctx, cfg.DatabaseURL())
if err != nil {
log.Fatalf("Failed to connect to database: %v", err)
}
defer pool.Close()
if err := pool.Ping(ctx); err != nil {
log.Fatalf("Failed to ping database: %v", err)
}
log.Println("Successfully connected to database")
boot := rkboot.NewBoot(rkboot.WithBootConfigPath("config/boot.yaml", nil)) boot := rkboot.NewBoot(rkboot.WithBootConfigPath("config/boot.yaml", nil))
grpcEntry := rkgrpc.GetGrpcEntry("smart-search-service") loggerEntry := rkentry.GlobalAppCtx.GetLoggerEntry("smart-search-logger")
if grpcEntry == nil {
log.Fatal("Failed to get gRPC entry from rk-boot")
}
loggerEntry := rkentry.GlobalAppCtx.GetLoggerEntry("smart-search-service")
if loggerEntry == nil { if loggerEntry == nil {
loggerEntry = rkentry.GlobalAppCtx.GetLoggerEntryDefault() loggerEntry = rkentry.GlobalAppCtx.GetLoggerEntryDefault()
} }
logger := loggerEntry.Logger logger := loggerEntry.Logger
cfg, err := config.Load("config/config.yaml")
if err != nil {
logger.Fatal("Failed to load config", zap.Error(err))
}
ctx := context.Background()
if err := database.RunMigrations(cfg.DatabaseURL(), logger); err != nil {
logger.Fatal("Failed to run migrations", zap.Error(err))
}
pool, err := pgxpool.New(ctx, cfg.DatabaseURL())
if err != nil {
logger.Fatal("Failed to connect to database", zap.Error(err))
}
defer pool.Close()
if err := pool.Ping(ctx); err != nil {
logger.Fatal("Failed to ping database", zap.Error(err))
}
logger.Info("Successfully connected to database")
grpcEntry := rkgrpc.GetGrpcEntry("smart-search-service")
if grpcEntry == nil {
logger.Fatal("Failed to get gRPC entry from rk-boot")
os.Exit(1)
}
sessionRepo := repository.NewSessionRepository(pool) sessionRepo := repository.NewSessionRepository(pool)
inviteRepo := repository.NewInviteRepository(pool) inviteRepo := repository.NewInviteRepository(pool)
sessionCleaner := worker.NewSessionCleaner(ctx, sessionRepo) sessionCleaner := worker.NewSessionCleaner(ctx, sessionRepo, logger)
sessionCleaner.Start() sessionCleaner.Start()
defer sessionCleaner.Stop() defer sessionCleaner.Stop()
inviteCleaner := worker.NewInviteCleaner(ctx, inviteRepo) inviteCleaner := worker.NewInviteCleaner(ctx, inviteRepo, logger)
inviteCleaner.Start() inviteCleaner.Start()
defer inviteCleaner.Stop() defer inviteCleaner.Stop()
@@ -81,9 +83,9 @@ func main() {
boot.Bootstrap(ctx) boot.Bootstrap(ctx)
log.Println("gRPC server started via rk-boot") logger.Info("gRPC server started via rk-boot")
boot.WaitForShutdownSig(ctx) boot.WaitForShutdownSig(ctx)
log.Println("Server stopped gracefully") logger.Info("Server stopped gracefully")
} }

View File

@@ -1,16 +1,12 @@
--- ---
logger: logger:
- name: smart-search-logger - name: smart-search-logger
description: "Application logger for smart-search service"
default: true default: true
zap: zap:
level: error level: info
development: false
encoding: console encoding: console
outputPaths: ["stdout"] outputPaths: ["stdout"]
errorOutputPaths: ["stderr"] errorOutputPaths: ["stderr"]
disableCaller: false
disableStacktrace: false
grpc: grpc:
- name: smart-search-service - name: smart-search-service
@@ -19,16 +15,17 @@ grpc:
enableReflection: true enableReflection: true
enableRkGwOption: true enableRkGwOption: true
loggerEntry: smart-search-logger loggerEntry: smart-search-logger
eventEntry: smart-search-logger
middleware: middleware:
logging: logging:
enabled: true enabled: true
loggerEncoding: "console" loggerEncoding: console
loggerOutputPaths: ["stdout"] loggerOutputPaths: ["stdout"]
eventEncoding: console
eventOutputPaths: []
meta: meta:
enabled: true enabled: false
trace: trace:
enabled: true enabled: false
prometheus: prometheus:
enabled: true enabled: true
auth: auth:

3
go.mod
View File

@@ -17,6 +17,7 @@ require (
github.com/testcontainers/testcontainers-go/modules/postgres v0.40.0 github.com/testcontainers/testcontainers-go/modules/postgres v0.40.0
github.com/xuri/excelize/v2 v2.10.0 github.com/xuri/excelize/v2 v2.10.0
go.uber.org/zap v1.27.1 go.uber.org/zap v1.27.1
google.golang.org/genproto/googleapis/rpc v0.0.0-20260114163908-3f89685c29c3
google.golang.org/grpc v1.78.0 google.golang.org/grpc v1.78.0
google.golang.org/protobuf v1.36.11 google.golang.org/protobuf v1.36.11
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
@@ -95,7 +96,6 @@ require (
github.com/spf13/cast v1.10.0 // indirect github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect github.com/spf13/pflag v1.0.10 // indirect
github.com/spf13/viper v1.21.0 // indirect github.com/spf13/viper v1.21.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/subosito/gotenv v1.6.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect
github.com/tiendc/go-deepcopy v1.7.1 // indirect github.com/tiendc/go-deepcopy v1.7.1 // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect
@@ -126,7 +126,6 @@ require (
golang.org/x/sys v0.40.0 // indirect golang.org/x/sys v0.40.0 // indirect
golang.org/x/text v0.33.0 // indirect golang.org/x/text v0.33.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20251029180050-ab9386a59fda // indirect google.golang.org/genproto/googleapis/api v0.0.0-20251029180050-ab9386a59fda // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260114163908-3f89685c29c3 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
nhooyr.io/websocket v1.8.6 // indirect nhooyr.io/websocket v1.8.6 // indirect

View File

@@ -7,13 +7,14 @@ import (
_ "github.com/jackc/pgx/v5/stdlib" _ "github.com/jackc/pgx/v5/stdlib"
"github.com/pressly/goose/v3" "github.com/pressly/goose/v3"
"go.uber.org/zap"
) )
func RunMigrations(databaseURL string) error { func RunMigrations(databaseURL string, logger *zap.Logger) error {
return RunMigrationsFromPath(databaseURL, "migrations") return RunMigrationsFromPath(databaseURL, "migrations", logger)
} }
func RunMigrationsFromPath(databaseURL, migrationsDir string) error { func RunMigrationsFromPath(databaseURL, migrationsDir string, logger *zap.Logger) error {
db, err := sql.Open("pgx", databaseURL) db, err := sql.Open("pgx", databaseURL)
if err != nil { if err != nil {
return fmt.Errorf("failed to open database connection for migrations: %w", err) return fmt.Errorf("failed to open database connection for migrations: %w", err)
@@ -28,6 +29,8 @@ func RunMigrationsFromPath(databaseURL, migrationsDir string) error {
return fmt.Errorf("failed to set goose dialect: %w", err) return fmt.Errorf("failed to set goose dialect: %w", err)
} }
goose.SetLogger(&gooseLogger{logger: logger})
absPath, err := filepath.Abs(migrationsDir) absPath, err := filepath.Abs(migrationsDir)
if err != nil { if err != nil {
return fmt.Errorf("failed to resolve migrations path: %w", err) return fmt.Errorf("failed to resolve migrations path: %w", err)
@@ -39,3 +42,15 @@ func RunMigrationsFromPath(databaseURL, migrationsDir string) error {
return nil return nil
} }
type gooseLogger struct {
logger *zap.Logger
}
func (l *gooseLogger) Fatalf(format string, v ...interface{}) {
l.logger.Fatal(fmt.Sprintf(format, v...))
}
func (l *gooseLogger) Printf(format string, v ...interface{}) {
l.logger.Info(fmt.Sprintf(format, v...))
}

View File

@@ -2,9 +2,10 @@ package worker
import ( import (
"context" "context"
"log"
"time" "time"
"go.uber.org/zap"
"git.techease.ru/Smart-search/smart-search-back/internal/repository" "git.techease.ru/Smart-search/smart-search-back/internal/repository"
) )
@@ -13,13 +14,15 @@ type InviteCleaner struct {
ctx context.Context ctx context.Context
ticker *time.Ticker ticker *time.Ticker
done chan bool done chan bool
logger *zap.Logger
} }
func NewInviteCleaner(ctx context.Context, inviteRepo repository.InviteRepository) *InviteCleaner { func NewInviteCleaner(ctx context.Context, inviteRepo repository.InviteRepository, logger *zap.Logger) *InviteCleaner {
return &InviteCleaner{ return &InviteCleaner{
inviteRepo: inviteRepo, inviteRepo: inviteRepo,
ctx: ctx, ctx: ctx,
done: make(chan bool), done: make(chan bool),
logger: logger,
} }
} }
@@ -36,13 +39,13 @@ func (w *InviteCleaner) Start() {
case <-w.done: case <-w.done:
return return
case <-w.ctx.Done(): case <-w.ctx.Done():
log.Println("Invite cleaner context cancelled, stopping worker") w.logger.Info("Invite cleaner context cancelled, stopping worker")
return return
} }
} }
}() }()
log.Println("Invite cleaner worker started (runs every 6 hours)") w.logger.Info("Invite cleaner worker started (runs every 6 hours)")
} }
func (w *InviteCleaner) Stop() { func (w *InviteCleaner) Stop() {
@@ -53,17 +56,17 @@ func (w *InviteCleaner) Stop() {
case w.done <- true: case w.done <- true:
default: default:
} }
log.Println("Invite cleaner worker stopped") w.logger.Info("Invite cleaner worker stopped")
} }
func (w *InviteCleaner) deactivateExpiredInvites() { func (w *InviteCleaner) deactivateExpiredInvites() {
count, err := w.inviteRepo.DeactivateExpired(w.ctx) count, err := w.inviteRepo.DeactivateExpired(w.ctx)
if err != nil { if err != nil {
log.Printf("Error deactivating expired invites: %v", err) w.logger.Error("Error deactivating expired invites", zap.Error(err))
return return
} }
if count > 0 { if count > 0 {
log.Printf("Deactivated %d expired invite codes", count) w.logger.Info("Deactivated expired invite codes", zap.Int("count", count))
} }
} }

View File

@@ -2,9 +2,10 @@ package worker
import ( import (
"context" "context"
"log"
"time" "time"
"go.uber.org/zap"
"git.techease.ru/Smart-search/smart-search-back/internal/repository" "git.techease.ru/Smart-search/smart-search-back/internal/repository"
) )
@@ -13,13 +14,15 @@ type SessionCleaner struct {
ctx context.Context ctx context.Context
ticker *time.Ticker ticker *time.Ticker
done chan bool done chan bool
logger *zap.Logger
} }
func NewSessionCleaner(ctx context.Context, sessionRepo repository.SessionRepository) *SessionCleaner { func NewSessionCleaner(ctx context.Context, sessionRepo repository.SessionRepository, logger *zap.Logger) *SessionCleaner {
return &SessionCleaner{ return &SessionCleaner{
sessionRepo: sessionRepo, sessionRepo: sessionRepo,
ctx: ctx, ctx: ctx,
done: make(chan bool), done: make(chan bool),
logger: logger,
} }
} }
@@ -36,13 +39,13 @@ func (w *SessionCleaner) Start() {
case <-w.done: case <-w.done:
return return
case <-w.ctx.Done(): case <-w.ctx.Done():
log.Println("Session cleaner context cancelled, stopping worker") w.logger.Info("Session cleaner context cancelled, stopping worker")
return return
} }
} }
}() }()
log.Println("Session cleaner worker started (runs every hour)") w.logger.Info("Session cleaner worker started (runs every hour)")
} }
func (w *SessionCleaner) Stop() { func (w *SessionCleaner) Stop() {
@@ -53,17 +56,17 @@ func (w *SessionCleaner) Stop() {
case w.done <- true: case w.done <- true:
default: default:
} }
log.Println("Session cleaner worker stopped") w.logger.Info("Session cleaner worker stopped")
} }
func (w *SessionCleaner) cleanExpiredSessions() { func (w *SessionCleaner) cleanExpiredSessions() {
count, err := w.sessionRepo.DeleteExpired(w.ctx) count, err := w.sessionRepo.DeleteExpired(w.ctx)
if err != nil { if err != nil {
log.Printf("Error cleaning expired sessions: %v", err) w.logger.Error("Error cleaning expired sessions", zap.Error(err))
return return
} }
if count > 0 { if count > 0 {
log.Printf("Cleaned %d expired sessions", count) w.logger.Info("Cleaned expired sessions", zap.Int("count", count))
} }
} }

View File

@@ -8,6 +8,7 @@ import (
"github.com/gojuno/minimock/v3" "github.com/gojuno/minimock/v3"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"go.uber.org/zap"
"git.techease.ru/Smart-search/smart-search-back/internal/mocks" "git.techease.ru/Smart-search/smart-search-back/internal/mocks"
) )
@@ -17,6 +18,7 @@ type WorkerSuite struct {
ctx context.Context ctx context.Context
cancel context.CancelFunc cancel context.CancelFunc
ctrl *minimock.Controller ctrl *minimock.Controller
logger *zap.Logger
} }
func TestWorkerSuite(t *testing.T) { func TestWorkerSuite(t *testing.T) {
@@ -26,6 +28,7 @@ func TestWorkerSuite(t *testing.T) {
func (s *WorkerSuite) SetupTest() { func (s *WorkerSuite) SetupTest() {
s.ctx, s.cancel = context.WithCancel(context.Background()) s.ctx, s.cancel = context.WithCancel(context.Background())
s.ctrl = minimock.NewController(s.T()) s.ctrl = minimock.NewController(s.T())
s.logger = zap.NewNop()
} }
func (s *WorkerSuite) TearDownTest() { func (s *WorkerSuite) TearDownTest() {
@@ -42,7 +45,7 @@ func (s *WorkerSuite) TestSessionCleaner_StartStop() {
return 5, nil return 5, nil
}) })
cleaner := NewSessionCleaner(s.ctx, sessionRepo) cleaner := NewSessionCleaner(s.ctx, sessionRepo, s.logger)
cleaner.Start() cleaner.Start()
@@ -62,7 +65,7 @@ func (s *WorkerSuite) TestSessionCleaner_ContextCancellation() {
return 0, nil return 0, nil
}) })
cleaner := NewSessionCleaner(s.ctx, sessionRepo) cleaner := NewSessionCleaner(s.ctx, sessionRepo, s.logger)
cleaner.Start() cleaner.Start()
@@ -84,7 +87,7 @@ func (s *WorkerSuite) TestInviteCleaner_StartStop() {
return 3, nil return 3, nil
}) })
cleaner := NewInviteCleaner(s.ctx, inviteRepo) cleaner := NewInviteCleaner(s.ctx, inviteRepo, s.logger)
cleaner.Start() cleaner.Start()
@@ -104,7 +107,7 @@ func (s *WorkerSuite) TestInviteCleaner_ContextCancellation() {
return 0, nil return 0, nil
}) })
cleaner := NewInviteCleaner(s.ctx, inviteRepo) cleaner := NewInviteCleaner(s.ctx, inviteRepo, s.logger)
cleaner.Start() cleaner.Start()
@@ -124,7 +127,7 @@ func (s *WorkerSuite) TestSessionCleaner_ConcurrentStops() {
return 0, nil return 0, nil
}) })
cleaner := NewSessionCleaner(s.ctx, sessionRepo) cleaner := NewSessionCleaner(s.ctx, sessionRepo, s.logger)
cleaner.Start() cleaner.Start()
@@ -151,7 +154,7 @@ func (s *WorkerSuite) TestInviteCleaner_ConcurrentStops() {
return 0, nil return 0, nil
}) })
cleaner := NewInviteCleaner(s.ctx, inviteRepo) cleaner := NewInviteCleaner(s.ctx, inviteRepo, s.logger)
cleaner.Start() cleaner.Start()
@@ -180,7 +183,7 @@ func (s *WorkerSuite) TestSessionCleaner_MultipleStartStop() {
return 2, nil return 2, nil
}) })
cleaner := NewSessionCleaner(s.ctx, sessionRepo) cleaner := NewSessionCleaner(s.ctx, sessionRepo, s.logger)
for i := 0; i < 3; i++ { for i := 0; i < 3; i++ {
cleaner.Start() cleaner.Start()
@@ -200,7 +203,7 @@ func (s *WorkerSuite) TestInviteCleaner_MultipleStartStop() {
return 1, nil return 1, nil
}) })
cleaner := NewInviteCleaner(s.ctx, inviteRepo) cleaner := NewInviteCleaner(s.ctx, inviteRepo, s.logger)
for i := 0; i < 3; i++ { for i := 0; i < 3; i++ {
cleaner.Start() cleaner.Start()

View File

@@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"go.uber.org/zap" "go.uber.org/zap"
"google.golang.org/genproto/googleapis/rpc/errdetails"
"google.golang.org/grpc/codes" "google.golang.org/grpc/codes"
"google.golang.org/grpc/status" "google.golang.org/grpc/status"
) )
@@ -88,20 +89,31 @@ func ToGRPCError(err error, zapLogger *zap.Logger, method string) error {
return status.Error(codes.Internal, "internal server error") return status.Error(codes.Internal, "internal server error")
} }
var grpcCode codes.Code
switch appErr.Code { switch appErr.Code {
case AuthInvalidCredentials, AuthMissing, AuthInvalidToken, RefreshInvalid: case AuthInvalidCredentials, AuthMissing, AuthInvalidToken, RefreshInvalid:
return status.Error(codes.Unauthenticated, appErr.Message) grpcCode = codes.Unauthenticated
case PermissionDenied: case PermissionDenied:
return status.Error(codes.PermissionDenied, appErr.Message) grpcCode = codes.PermissionDenied
case InviteLimitReached: case InviteLimitReached:
return status.Error(codes.ResourceExhausted, appErr.Message) grpcCode = codes.ResourceExhausted
case InsufficientBalance, InviteInvalidOrExpired: case InsufficientBalance:
return status.Error(codes.FailedPrecondition, appErr.Message) grpcCode = codes.FailedPrecondition
case InviteInvalidOrExpired:
grpcCode = codes.NotFound
case EmailAlreadyExists: case EmailAlreadyExists:
return status.Error(codes.AlreadyExists, appErr.Message) grpcCode = codes.AlreadyExists
case UserNotFound, RequestNotFound: case UserNotFound, RequestNotFound:
return status.Error(codes.NotFound, appErr.Message) grpcCode = codes.NotFound
default: default:
return status.Error(codes.Unknown, appErr.Message) grpcCode = codes.Unknown
} }
st, err := status.New(grpcCode, appErr.Message).WithDetails(&errdetails.ErrorInfo{
Reason: appErr.Code,
})
if err != nil {
return status.Error(grpcCode, appErr.Message)
}
return st.Err()
} }

View File

@@ -208,7 +208,7 @@ func (s *IntegrationSuite) TestAuthHandler_RegisterInvalidInviteCode() {
st, ok := status.FromError(err) st, ok := status.FromError(err)
s.True(ok) s.True(ok)
s.Equal(codes.FailedPrecondition, st.Code()) s.Equal(codes.NotFound, st.Code())
} }
func (s *IntegrationSuite) TestAuthHandler_RegisterExpiredInviteCode() { func (s *IntegrationSuite) TestAuthHandler_RegisterExpiredInviteCode() {
@@ -232,7 +232,7 @@ func (s *IntegrationSuite) TestAuthHandler_RegisterExpiredInviteCode() {
st, ok := status.FromError(err) st, ok := status.FromError(err)
s.True(ok) s.True(ok)
s.Equal(codes.FailedPrecondition, st.Code()) s.Equal(codes.NotFound, st.Code())
} }
func (s *IntegrationSuite) TestAuthHandler_RegisterExhaustedInviteCode() { func (s *IntegrationSuite) TestAuthHandler_RegisterExhaustedInviteCode() {
@@ -270,7 +270,7 @@ func (s *IntegrationSuite) TestAuthHandler_RegisterExhaustedInviteCode() {
st, ok := status.FromError(err) st, ok := status.FromError(err)
s.True(ok) s.True(ok)
s.Equal(codes.FailedPrecondition, st.Code()) s.Equal(codes.NotFound, st.Code())
} }
func (s *IntegrationSuite) TestAuthHandler_RegisterDuplicateEmail() { func (s *IntegrationSuite) TestAuthHandler_RegisterDuplicateEmail() {

View File

@@ -78,7 +78,8 @@ func (s *IntegrationSuite) SetupSuite() {
s.T().Logf("PostgreSQL connection string: %s", connStr) s.T().Logf("PostgreSQL connection string: %s", connStr)
s.T().Log("Running migrations...") s.T().Log("Running migrations...")
err = database.RunMigrationsFromPath(connStr, "../migrations") logger, _ := zap.NewDevelopment()
err = database.RunMigrationsFromPath(connStr, "../migrations", logger)
s.Require().NoError(err) s.Require().NoError(err)
s.T().Log("Creating connection pool...") s.T().Log("Creating connection pool...")
@@ -94,7 +95,6 @@ func (s *IntegrationSuite) SetupSuite() {
s.Require().NoError(err) s.Require().NoError(err)
s.T().Log("Creating gRPC server...") s.T().Log("Creating gRPC server...")
logger, _ := zap.NewDevelopment()
authHandler, userHandler, inviteHandler, requestHandler, supplierHandler := grpchandlers.NewHandlers( authHandler, userHandler, inviteHandler, requestHandler, supplierHandler := grpchandlers.NewHandlers(
pool, pool,