package tests import ( "context" "fmt" "net" "testing" "time" "github.com/jackc/pgx/v5/pgxpool" "github.com/stretchr/testify/suite" "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/postgres" "github.com/testcontainers/testcontainers-go/wait" "go.uber.org/zap" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/test/bufconn" "git.techease.ru/Smart-search/smart-search-back/internal/database" grpchandlers "git.techease.ru/Smart-search/smart-search-back/internal/grpc" "git.techease.ru/Smart-search/smart-search-back/pkg/crypto" authpb "git.techease.ru/Smart-search/smart-search-back/pkg/pb/auth" invitepb "git.techease.ru/Smart-search/smart-search-back/pkg/pb/invite" requestpb "git.techease.ru/Smart-search/smart-search-back/pkg/pb/request" supplierpb "git.techease.ru/Smart-search/smart-search-back/pkg/pb/supplier" userpb "git.techease.ru/Smart-search/smart-search-back/pkg/pb/user" ) const ( testJWTSecret = "test-jwt-secret-key-for-integration-tests" testCryptoSecret = "test-crypto-secret-key-for-integration" bufSize = 1024 * 1024 ) type IntegrationSuite struct { suite.Suite ctx context.Context cancel context.CancelFunc pgContainer *postgres.PostgresContainer pool *pgxpool.Pool grpcServer *grpc.Server listener *bufconn.Listener authClient authpb.AuthServiceClient userClient userpb.UserServiceClient inviteClient invitepb.InviteServiceClient requestClient requestpb.RequestServiceClient supplierClient supplierpb.SupplierServiceClient testUserEmail string testUserPassword string testAccessToken string testRefreshToken string } func TestIntegrationSuite(t *testing.T) { suite.Run(t, new(IntegrationSuite)) } func (s *IntegrationSuite) SetupSuite() { s.ctx, s.cancel = context.WithCancel(context.Background()) s.T().Log("Starting PostgreSQL container...") pgContainer, err := postgres.Run(s.ctx, "postgres:15-alpine", postgres.WithDatabase("test_db"), postgres.WithUsername("test_user"), postgres.WithPassword("test_password"), testcontainers.WithWaitStrategy( wait.ForLog("database system is ready to accept connections"). WithOccurrence(2). WithStartupTimeout(60*time.Second)), ) s.Require().NoError(err) s.pgContainer = pgContainer connStr, err := pgContainer.ConnectionString(s.ctx, "sslmode=disable") s.Require().NoError(err) s.T().Logf("PostgreSQL connection string: %s", connStr) s.T().Log("Running migrations...") logger, _ := zap.NewDevelopment() err = database.RunMigrationsFromPath(connStr, "../migrations", logger) s.Require().NoError(err) s.T().Log("Creating connection pool...") poolConfig, err := pgxpool.ParseConfig(connStr) s.Require().NoError(err) poolConfig.MaxConns = 10 pool, err := pgxpool.NewWithConfig(s.ctx, poolConfig) s.Require().NoError(err) s.pool = pool err = pool.Ping(s.ctx) s.Require().NoError(err) s.T().Log("Creating gRPC server...") authHandler, userHandler, inviteHandler, requestHandler, supplierHandler := grpchandlers.NewHandlers( pool, testJWTSecret, testCryptoSecret, "", "", logger, ) s.listener = bufconn.Listen(bufSize) s.grpcServer = grpc.NewServer() grpchandlers.RegisterServices(s.grpcServer, authHandler, userHandler, inviteHandler, requestHandler, supplierHandler) go func() { if err := s.grpcServer.Serve(s.listener); err != nil { s.T().Logf("gRPC server error: %v", err) } }() s.T().Log("Creating gRPC clients...") conn, err := grpc.NewClient("passthrough://bufnet", grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) { return s.listener.Dial() }), grpc.WithTransportCredentials(insecure.NewCredentials()), ) s.Require().NoError(err) s.authClient = authpb.NewAuthServiceClient(conn) s.userClient = userpb.NewUserServiceClient(conn) s.inviteClient = invitepb.NewInviteServiceClient(conn) s.requestClient = requestpb.NewRequestServiceClient(conn) s.supplierClient = supplierpb.NewSupplierServiceClient(conn) s.testUserEmail = fmt.Sprintf("test_%d@example.com", time.Now().Unix()) s.testUserPassword = "testpassword123" s.T().Log("Creating test user...") s.createTestUser("test@example.com", "testpassword") s.T().Log("Integration suite setup completed") } func (s *IntegrationSuite) createTestUser(email, password string) { cryptoHelper := crypto.NewCrypto(testCryptoSecret) encryptedEmail, err := cryptoHelper.Encrypt(email) s.Require().NoError(err) encryptedPhone, err := cryptoHelper.Encrypt("+1234567890") s.Require().NoError(err) encryptedUserName, err := cryptoHelper.Encrypt("Test User") s.Require().NoError(err) emailHash := cryptoHelper.EmailHash(email) passwordHash := crypto.PasswordHash(password) query := ` INSERT INTO users (email, email_hash, password_hash, phone, user_name, company_name, balance, payment_status, invites_issued, invites_limit) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) ON CONFLICT (email_hash) DO NOTHING ` _, err = s.pool.Exec(s.ctx, query, encryptedEmail, emailHash, passwordHash, encryptedPhone, encryptedUserName, "Test Company", 1000.0, "active", 0, 10, ) s.Require().NoError(err) } func (s *IntegrationSuite) createActiveInviteCode(canBeUsedCount int) int64 { var inviteCode int64 query := ` INSERT INTO invite_codes (user_id, code, can_be_used_count, is_active, expires_at) VALUES (1, FLOOR(100000 + RANDOM() * 900000)::bigint, $1, true, NOW() + INTERVAL '30 days') RETURNING code ` err := s.pool.QueryRow(s.ctx, query, canBeUsedCount).Scan(&inviteCode) s.Require().NoError(err) return inviteCode } func (s *IntegrationSuite) createExpiredInviteCode() int64 { var inviteCode int64 query := ` INSERT INTO invite_codes (user_id, code, can_be_used_count, is_active, expires_at) VALUES (1, FLOOR(100000 + RANDOM() * 900000)::bigint, 5, true, NOW() - INTERVAL '1 day') RETURNING code ` err := s.pool.QueryRow(s.ctx, query).Scan(&inviteCode) s.Require().NoError(err) return inviteCode } func (s *IntegrationSuite) TearDownSuite() { s.T().Log("Tearing down integration suite...") if s.grpcServer != nil { s.grpcServer.Stop() } if s.pool != nil { s.pool.Close() } if s.pgContainer != nil { if err := s.pgContainer.Terminate(s.ctx); err != nil { s.T().Logf("Failed to terminate PostgreSQL container: %v", err) } } if s.cancel != nil { s.cancel() } s.T().Log("Integration suite teardown completed") } func (s *IntegrationSuite) TearDownTest() { s.testAccessToken = "" s.testRefreshToken = "" _, _ = s.pool.Exec(s.ctx, "DELETE FROM sessions") _, _ = s.pool.Exec(s.ctx, "DELETE FROM invite_codes") _, _ = s.pool.Exec(s.ctx, "DELETE FROM suppliers") _, _ = s.pool.Exec(s.ctx, "DELETE FROM requests_for_suppliers") } func (s *IntegrationSuite) createSecondTestUser() (email string, password string, userID int64) { email = "second_user@example.com" password = "secondpassword" cryptoHelper := crypto.NewCrypto(testCryptoSecret) encryptedEmail, err := cryptoHelper.Encrypt(email) s.Require().NoError(err) encryptedPhone, err := cryptoHelper.Encrypt("+9876543210") s.Require().NoError(err) encryptedUserName, err := cryptoHelper.Encrypt("Second User") s.Require().NoError(err) emailHash := cryptoHelper.EmailHash(email) passwordHash := crypto.PasswordHash(password) query := ` INSERT INTO users (email, email_hash, password_hash, phone, user_name, company_name, balance, payment_status, invites_issued, invites_limit) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) ON CONFLICT (email_hash) DO UPDATE SET balance = $7 RETURNING id ` err = s.pool.QueryRow(s.ctx, query, encryptedEmail, emailHash, passwordHash, encryptedPhone, encryptedUserName, "Second Company", 1000.0, "active", 0, 10, ).Scan(&userID) s.Require().NoError(err) return email, password, userID } func (s *IntegrationSuite) getInviteCodeUsageCount(code int64) int { var count int err := s.pool.QueryRow(s.ctx, "SELECT can_be_used_count FROM invite_codes WHERE code = $1", code, ).Scan(&count) if err != nil { return -1 } return count } func (s *IntegrationSuite) getRequestSuppliersCount(requestID string) int { var count int err := s.pool.QueryRow(s.ctx, "SELECT COUNT(*) FROM suppliers WHERE request_id = $1::uuid", requestID, ).Scan(&count) if err != nil { return -1 } return count } func (s *IntegrationSuite) getUserBalance(userID int) float64 { var balance float64 err := s.pool.QueryRow(s.ctx, "SELECT balance FROM users WHERE id = $1", userID, ).Scan(&balance) if err != nil { return -1 } return balance } func (s *IntegrationSuite) getTokenUsageCount(requestID string) int { var count int err := s.pool.QueryRow(s.ctx, "SELECT COUNT(*) FROM request_token_usage WHERE request_id = $1::uuid", requestID, ).Scan(&count) if err != nil { return -1 } return count } func (s *IntegrationSuite) createUniqueTestUser(suffix string, balance float64) (email string, password string, userID int) { email = fmt.Sprintf("user_%s_%d@example.com", suffix, time.Now().UnixNano()) password = "testpassword" cryptoHelper := crypto.NewCrypto(testCryptoSecret) encryptedEmail, err := cryptoHelper.Encrypt(email) s.Require().NoError(err) encryptedPhone, err := cryptoHelper.Encrypt(fmt.Sprintf("+1%d", time.Now().UnixNano()%10000000000)) s.Require().NoError(err) encryptedUserName, err := cryptoHelper.Encrypt(fmt.Sprintf("User %s", suffix)) s.Require().NoError(err) emailHash := cryptoHelper.EmailHash(email) passwordHash := crypto.PasswordHash(password) query := ` INSERT INTO users (email, email_hash, password_hash, phone, user_name, company_name, balance, payment_status, invites_issued, invites_limit) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING id ` err = s.pool.QueryRow(s.ctx, query, encryptedEmail, emailHash, passwordHash, encryptedPhone, encryptedUserName, "Test Company", balance, "active", 0, 10, ).Scan(&userID) s.Require().NoError(err) return email, password, userID } func (s *IntegrationSuite) isInviteCodeActive(code int64) bool { var isActive bool err := s.pool.QueryRow(s.ctx, "SELECT is_active FROM invite_codes WHERE code = $1", code, ).Scan(&isActive) if err != nil { return false } return isActive }