add service
Some checks failed
Deploy Smart Search Backend / deploy (push) Failing after 1m54s

This commit is contained in:
vallyenfail
2026-01-17 20:41:37 +03:00
parent 635acd13ac
commit e2968722ed
70 changed files with 7542 additions and 463 deletions

View File

@@ -3,8 +3,8 @@ package grpc
import (
"context"
"smart-search-back/pkg/errors"
pb "smart-search-back/pkg/pb/api/proto/auth"
"git.techease.ru/Smart-search/smart-search-back/pkg/errors"
pb "git.techease.ru/Smart-search/smart-search-back/pkg/pb/auth"
"go.uber.org/zap"
)

View File

@@ -4,9 +4,9 @@ import (
"context"
"strconv"
"git.techease.ru/Smart-search/smart-search-back/pkg/errors"
pb "git.techease.ru/Smart-search/smart-search-back/pkg/pb/invite"
"google.golang.org/protobuf/types/known/timestamppb"
"smart-search-back/pkg/errors"
pb "smart-search-back/pkg/pb/api/proto/invite"
)
func (h *InviteHandler) Generate(ctx context.Context, req *pb.GenerateRequest) (*pb.GenerateResponse, error) {

View File

@@ -4,10 +4,10 @@ import (
"context"
"time"
"git.techease.ru/Smart-search/smart-search-back/pkg/errors"
pb "git.techease.ru/Smart-search/smart-search-back/pkg/pb/request"
"github.com/google/uuid"
"google.golang.org/protobuf/types/known/timestamppb"
"smart-search-back/pkg/errors"
pb "smart-search-back/pkg/pb/api/proto/request"
)
func (h *RequestHandler) CreateTZ(ctx context.Context, req *pb.CreateTZRequest) (*pb.CreateTZResponse, error) {

View File

@@ -1,14 +1,14 @@
package grpc
import (
"smart-search-back/internal/ai"
"smart-search-back/internal/repository"
"smart-search-back/internal/service"
authpb "smart-search-back/pkg/pb/api/proto/auth"
invitepb "smart-search-back/pkg/pb/api/proto/invite"
requestpb "smart-search-back/pkg/pb/api/proto/request"
supplierpb "smart-search-back/pkg/pb/api/proto/supplier"
userpb "smart-search-back/pkg/pb/api/proto/user"
"git.techease.ru/Smart-search/smart-search-back/internal/ai"
"git.techease.ru/Smart-search/smart-search-back/internal/repository"
"git.techease.ru/Smart-search/smart-search-back/internal/service"
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"
"github.com/jackc/pgx/v5/pgxpool"
"go.uber.org/zap"
@@ -53,13 +53,15 @@ func NewHandlers(pool *pgxpool.Pool, jwtSecret, cryptoSecret, openAIKey, perplex
supplierRepo := repository.NewSupplierRepository(pool)
tokenUsageRepo := repository.NewTokenUsageRepository(pool)
txManager := repository.NewTxManager(pool)
openAIClient := ai.NewOpenAIClient(openAIKey)
perplexityClient := ai.NewPerplexityClient(perplexityKey)
authService := service.NewAuthService(userRepo, sessionRepo, jwtSecret, cryptoSecret)
userService := service.NewUserService(userRepo, requestRepo, cryptoSecret)
inviteService := service.NewInviteService(inviteRepo, userRepo)
requestService := service.NewRequestService(requestRepo, supplierRepo, tokenUsageRepo, userRepo, openAIClient, perplexityClient)
inviteService := service.NewInviteService(inviteRepo, userRepo, txManager)
requestService := service.NewRequestService(requestRepo, supplierRepo, tokenUsageRepo, userRepo, openAIClient, perplexityClient, txManager)
supplierService := service.NewSupplierService(supplierRepo)
return &AuthHandler{authService: authService, logger: logger},

View File

@@ -3,9 +3,9 @@ package grpc
import (
"context"
"git.techease.ru/Smart-search/smart-search-back/pkg/errors"
pb "git.techease.ru/Smart-search/smart-search-back/pkg/pb/supplier"
"github.com/google/uuid"
"smart-search-back/pkg/errors"
pb "smart-search-back/pkg/pb/api/proto/supplier"
)
func (h *SupplierHandler) ExportExcel(ctx context.Context, req *pb.ExportExcelRequest) (*pb.ExportExcelResponse, error) {

View File

@@ -0,0 +1,158 @@
package tests
import (
"context"
authpb "git.techease.ru/Smart-search/smart-search-back/pkg/pb/auth"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func (s *IntegrationSuite) TestAuthHandler_LoginWithNonExistentUser() {
req := &authpb.LoginRequest{
Email: "nonexistent@example.com",
Password: "password123",
Ip: "127.0.0.1",
UserAgent: "test-agent",
}
resp, err := s.authClient.Login(context.Background(), req)
s.Error(err)
s.Nil(resp)
st, ok := status.FromError(err)
s.True(ok)
s.Equal(codes.NotFound, st.Code())
}
func (s *IntegrationSuite) TestAuthHandler_ValidateWithInvalidToken() {
req := &authpb.ValidateRequest{
AccessToken: "invalid-token",
}
resp, err := s.authClient.Validate(context.Background(), req)
s.NoError(err)
s.NotNil(resp)
s.False(resp.Valid)
s.Equal(int64(0), resp.UserId)
}
func (s *IntegrationSuite) TestAuthHandler_ValidateWithEmptyToken() {
req := &authpb.ValidateRequest{
AccessToken: "",
}
resp, err := s.authClient.Validate(context.Background(), req)
s.NoError(err)
s.NotNil(resp)
s.False(resp.Valid)
s.Equal(int64(0), resp.UserId)
}
func (s *IntegrationSuite) TestAuthHandler_RefreshWithInvalidToken() {
req := &authpb.RefreshRequest{
RefreshToken: "invalid-refresh-token",
}
resp, err := s.authClient.Refresh(context.Background(), req)
s.Error(err)
s.Nil(resp)
st, ok := status.FromError(err)
s.True(ok)
s.Equal(codes.Unauthenticated, st.Code())
}
func (s *IntegrationSuite) TestAuthHandler_LogoutWithInvalidToken() {
req := &authpb.LogoutRequest{
AccessToken: "invalid-token",
}
resp, err := s.authClient.Logout(context.Background(), req)
s.NoError(err)
s.NotNil(resp)
s.True(resp.Success)
}
func (s *IntegrationSuite) TestAuthHandler_RefreshTokenFlow() {
ctx := context.Background()
loginReq := &authpb.LoginRequest{
Email: "test@example.com",
Password: "testpassword",
Ip: "127.0.0.1",
UserAgent: "integration-test",
}
loginResp, err := s.authClient.Login(ctx, loginReq)
s.NoError(err)
s.NotNil(loginResp)
s.NotEmpty(loginResp.AccessToken)
s.NotEmpty(loginResp.RefreshToken)
validateReq := &authpb.ValidateRequest{
AccessToken: loginResp.AccessToken,
}
validateResp, err := s.authClient.Validate(ctx, validateReq)
s.NoError(err)
s.NotNil(validateResp)
s.True(validateResp.Valid)
refreshReq := &authpb.RefreshRequest{
RefreshToken: loginResp.RefreshToken,
Ip: "127.0.0.1",
UserAgent: "integration-test",
}
refreshResp, err := s.authClient.Refresh(ctx, refreshReq)
s.NoError(err)
s.NotNil(refreshResp)
s.NotEmpty(refreshResp.AccessToken)
logoutReq := &authpb.LogoutRequest{
AccessToken: refreshResp.AccessToken,
}
logoutResp, err := s.authClient.Logout(ctx, logoutReq)
s.NoError(err)
s.NotNil(logoutResp)
s.True(logoutResp.Success)
}
func (s *IntegrationSuite) TestAuthHandler_LogoutInvalidatesSession() {
ctx := context.Background()
loginReq := &authpb.LoginRequest{
Email: "test@example.com",
Password: "testpassword",
Ip: "127.0.0.1",
UserAgent: "integration-test",
}
loginResp, err := s.authClient.Login(ctx, loginReq)
s.NoError(err)
s.NotEmpty(loginResp.AccessToken)
logoutReq := &authpb.LogoutRequest{
AccessToken: loginResp.AccessToken,
}
logoutResp, err := s.authClient.Logout(ctx, logoutReq)
s.NoError(err)
s.True(logoutResp.Success)
validateReq := &authpb.ValidateRequest{
AccessToken: loginResp.AccessToken,
}
validateResp, err := s.authClient.Validate(ctx, validateReq)
s.NoError(err)
s.NotNil(validateResp)
s.False(validateResp.Valid)
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,285 @@
package tests
import (
"context"
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"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func (s *IntegrationSuite) TestEdgeCase_CreateTZWithEmptyRequestText() {
ctx := context.Background()
loginReq := &authpb.LoginRequest{
Email: "test@example.com",
Password: "testpassword",
Ip: "127.0.0.1",
UserAgent: "integration-test",
}
loginResp, err := s.authClient.Login(ctx, loginReq)
s.NoError(err)
validateReq := &authpb.ValidateRequest{
AccessToken: loginResp.AccessToken,
}
validateResp, err := s.authClient.Validate(ctx, validateReq)
s.NoError(err)
req := &requestpb.CreateTZRequest{
UserId: validateResp.UserId,
RequestTxt: "",
}
resp, err := s.requestClient.CreateTZ(ctx, req)
if err != nil {
st, ok := status.FromError(err)
s.True(ok)
s.Contains([]codes.Code{codes.InvalidArgument, codes.Internal}, st.Code())
return
}
s.NotNil(resp)
}
func (s *IntegrationSuite) TestEdgeCase_GenerateInviteWithZeroMaxUses() {
ctx := context.Background()
loginReq := &authpb.LoginRequest{
Email: "test@example.com",
Password: "testpassword",
Ip: "127.0.0.1",
UserAgent: "integration-test",
}
loginResp, err := s.authClient.Login(ctx, loginReq)
s.NoError(err)
validateReq := &authpb.ValidateRequest{
AccessToken: loginResp.AccessToken,
}
validateResp, err := s.authClient.Validate(ctx, validateReq)
s.NoError(err)
req := &invitepb.GenerateRequest{
UserId: validateResp.UserId,
TtlDays: 30,
MaxUses: 0,
}
resp, err := s.inviteClient.Generate(ctx, req)
if err != nil {
st, ok := status.FromError(err)
s.True(ok)
s.Contains([]codes.Code{codes.InvalidArgument, codes.Internal}, st.Code())
return
}
s.NotNil(resp)
}
func (s *IntegrationSuite) TestEdgeCase_GenerateInviteWithZeroTTL() {
ctx := context.Background()
loginReq := &authpb.LoginRequest{
Email: "test@example.com",
Password: "testpassword",
Ip: "127.0.0.1",
UserAgent: "integration-test",
}
loginResp, err := s.authClient.Login(ctx, loginReq)
s.NoError(err)
validateReq := &authpb.ValidateRequest{
AccessToken: loginResp.AccessToken,
}
validateResp, err := s.authClient.Validate(ctx, validateReq)
s.NoError(err)
req := &invitepb.GenerateRequest{
UserId: validateResp.UserId,
TtlDays: 0,
MaxUses: 10,
}
resp, err := s.inviteClient.Generate(ctx, req)
if err != nil {
st, ok := status.FromError(err)
s.True(ok)
s.Contains([]codes.Code{codes.InvalidArgument, codes.Internal}, st.Code())
return
}
s.NotNil(resp)
}
func (s *IntegrationSuite) TestEdgeCase_ApproveTZWithEmptyFinalTZ() {
ctx := context.Background()
loginReq := &authpb.LoginRequest{
Email: "test@example.com",
Password: "testpassword",
Ip: "127.0.0.1",
UserAgent: "integration-test",
}
loginResp, err := s.authClient.Login(ctx, loginReq)
s.NoError(err)
validateReq := &authpb.ValidateRequest{
AccessToken: loginResp.AccessToken,
}
validateResp, err := s.authClient.Validate(ctx, validateReq)
s.NoError(err)
createReq := &requestpb.CreateTZRequest{
UserId: validateResp.UserId,
RequestTxt: "Test request",
}
createResp, err := s.requestClient.CreateTZ(ctx, createReq)
if err != nil {
s.T().Skip("Cannot test ApproveTZ without CreateTZ")
return
}
approveReq := &requestpb.ApproveTZRequest{
RequestId: createResp.RequestId,
FinalTz: "",
UserId: validateResp.UserId,
}
approveResp, err := s.requestClient.ApproveTZ(ctx, approveReq)
if err != nil {
st, ok := status.FromError(err)
s.True(ok)
s.Contains([]codes.Code{codes.InvalidArgument, codes.Internal}, st.Code())
return
}
s.NotNil(approveResp)
}
func (s *IntegrationSuite) TestEdgeCase_DoubleLogout() {
ctx := context.Background()
loginReq := &authpb.LoginRequest{
Email: "test@example.com",
Password: "testpassword",
Ip: "127.0.0.1",
UserAgent: "integration-test",
}
loginResp, err := s.authClient.Login(ctx, loginReq)
s.NoError(err)
logoutReq := &authpb.LogoutRequest{
AccessToken: loginResp.AccessToken,
}
logoutResp1, err := s.authClient.Logout(ctx, logoutReq)
s.NoError(err)
s.True(logoutResp1.Success)
logoutResp2, err := s.authClient.Logout(ctx, logoutReq)
s.NoError(err)
s.True(logoutResp2.Success)
}
func (s *IntegrationSuite) TestEdgeCase_ValidateAfterLogout() {
ctx := context.Background()
loginReq := &authpb.LoginRequest{
Email: "test@example.com",
Password: "testpassword",
Ip: "127.0.0.1",
UserAgent: "integration-test",
}
loginResp, err := s.authClient.Login(ctx, loginReq)
s.NoError(err)
logoutReq := &authpb.LogoutRequest{
AccessToken: loginResp.AccessToken,
}
logoutResp, err := s.authClient.Logout(ctx, logoutReq)
s.NoError(err)
s.True(logoutResp.Success)
validateReq := &authpb.ValidateRequest{
AccessToken: loginResp.AccessToken,
}
validateResp, err := s.authClient.Validate(ctx, validateReq)
s.NoError(err)
s.False(validateResp.Valid)
s.Equal(int64(0), validateResp.UserId)
}
func (s *IntegrationSuite) TestEdgeCase_RefreshAfterLogout() {
ctx := context.Background()
loginReq := &authpb.LoginRequest{
Email: "test@example.com",
Password: "testpassword",
Ip: "127.0.0.1",
UserAgent: "integration-test",
}
loginResp, err := s.authClient.Login(ctx, loginReq)
s.NoError(err)
logoutReq := &authpb.LogoutRequest{
AccessToken: loginResp.AccessToken,
}
logoutResp, err := s.authClient.Logout(ctx, logoutReq)
s.NoError(err)
s.True(logoutResp.Success)
refreshReq := &authpb.RefreshRequest{
RefreshToken: loginResp.RefreshToken,
Ip: "127.0.0.1",
UserAgent: "integration-test",
}
refreshResp, err := s.authClient.Refresh(ctx, refreshReq)
s.Error(err)
s.Nil(refreshResp)
st, ok := status.FromError(err)
s.True(ok)
s.Equal(codes.Unauthenticated, st.Code())
}
func (s *IntegrationSuite) TestEdgeCase_LoginWithWrongPassword() {
ctx := context.Background()
loginReq := &authpb.LoginRequest{
Email: "test@example.com",
Password: "wrongpassword",
Ip: "127.0.0.1",
UserAgent: "integration-test",
}
loginResp, err := s.authClient.Login(ctx, loginReq)
s.Error(err)
s.Nil(loginResp)
st, ok := status.FromError(err)
s.True(ok)
s.Equal(codes.Unauthenticated, st.Code())
}

View File

@@ -0,0 +1,239 @@
package tests
import (
"context"
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"
)
func (s *IntegrationSuite) TestFullFlow_CompleteRequestLifecycle() {
ctx := context.Background()
loginReq := &authpb.LoginRequest{
Email: "test@example.com",
Password: "testpassword",
Ip: "127.0.0.1",
UserAgent: "integration-test",
}
loginResp, err := s.authClient.Login(ctx, loginReq)
s.NoError(err)
s.NotEmpty(loginResp.AccessToken)
s.NotEmpty(loginResp.RefreshToken)
validateReq := &authpb.ValidateRequest{
AccessToken: loginResp.AccessToken,
}
validateResp, err := s.authClient.Validate(ctx, validateReq)
s.NoError(err)
s.True(validateResp.Valid)
userID := validateResp.UserId
getUserInfoReq := &userpb.GetInfoRequest{
UserId: userID,
}
userInfoResp, err := s.userClient.GetInfo(ctx, getUserInfoReq)
s.NoError(err)
s.NotNil(userInfoResp)
s.Equal("test@example.com", userInfoResp.Email)
getBalanceReq := &userpb.GetBalanceRequest{
UserId: userID,
}
balanceResp, err := s.userClient.GetBalance(ctx, getBalanceReq)
s.NoError(err)
s.GreaterOrEqual(balanceResp.Balance, 0.0)
createTZReq := &requestpb.CreateTZRequest{
UserId: userID,
RequestTxt: "Нужны поставщики металлоконструкций",
}
createTZResp, err := s.requestClient.CreateTZ(ctx, createTZReq)
if err != nil {
s.T().Logf("CreateTZ failed: %v", err)
return
}
s.NotEmpty(createTZResp.RequestId)
s.NotEmpty(createTZResp.TzText)
requestID := createTZResp.RequestId
approveTZReq := &requestpb.ApproveTZRequest{
RequestId: requestID,
FinalTz: "Утвержденное ТЗ для поставщиков металлоконструкций",
UserId: userID,
}
approveTZResp, err := s.requestClient.ApproveTZ(ctx, approveTZReq)
if err != nil {
s.T().Logf("ApproveTZ failed: %v", err)
return
}
s.True(approveTZResp.Success)
getMailingListReq := &requestpb.GetMailingListRequest{
UserId: userID,
}
mailingListResp, err := s.requestClient.GetMailingList(ctx, getMailingListReq)
s.NoError(err)
s.NotNil(mailingListResp.Items)
s.GreaterOrEqual(len(mailingListResp.Items), 1)
getMailingListByIDReq := &requestpb.GetMailingListByIDRequest{
RequestId: requestID,
UserId: userID,
}
mailingListByIDResp, err := s.requestClient.GetMailingListByID(ctx, getMailingListByIDReq)
if err != nil {
s.T().Logf("GetMailingListByID failed: %v", err)
return
}
s.NotNil(mailingListByIDResp.Item)
s.Equal(requestID, mailingListByIDResp.Item.RequestId)
exportExcelReq := &supplierpb.ExportExcelRequest{
RequestId: requestID,
UserId: userID,
}
exportExcelResp, err := s.supplierClient.ExportExcel(ctx, exportExcelReq)
if err != nil {
s.T().Logf("ExportExcel failed (expected if no suppliers): %v", err)
} else {
s.NotNil(exportExcelResp)
s.NotEmpty(exportExcelResp.FileName)
}
getStatisticsReq := &userpb.GetStatisticsRequest{
UserId: userID,
}
statisticsResp, err := s.userClient.GetStatistics(ctx, getStatisticsReq)
s.NoError(err)
s.GreaterOrEqual(statisticsResp.TotalRequests, int32(0))
logoutReq := &authpb.LogoutRequest{
AccessToken: loginResp.AccessToken,
}
logoutResp, err := s.authClient.Logout(ctx, logoutReq)
s.NoError(err)
s.True(logoutResp.Success)
}
func (s *IntegrationSuite) TestFullFlow_InviteCodeLifecycle() {
ctx := context.Background()
loginReq := &authpb.LoginRequest{
Email: "test@example.com",
Password: "testpassword",
Ip: "127.0.0.1",
UserAgent: "integration-test",
}
loginResp, err := s.authClient.Login(ctx, loginReq)
s.NoError(err)
validateReq := &authpb.ValidateRequest{
AccessToken: loginResp.AccessToken,
}
validateResp, err := s.authClient.Validate(ctx, validateReq)
s.NoError(err)
userID := validateResp.UserId
generateInviteReq := &invitepb.GenerateRequest{
UserId: userID,
TtlDays: 7,
MaxUses: 5,
}
generateInviteResp, err := s.inviteClient.Generate(ctx, generateInviteReq)
s.NoError(err)
s.NotEmpty(generateInviteResp.Code)
s.Greater(generateInviteResp.MaxUses, int32(0))
inviteCode := generateInviteResp.Code
getInviteInfoReq := &invitepb.GetInfoRequest{
Code: inviteCode,
}
inviteInfoResp, err := s.inviteClient.GetInfo(ctx, getInviteInfoReq)
s.NoError(err)
s.Equal(inviteCode, inviteInfoResp.Code)
s.Equal(userID, inviteInfoResp.UserId)
s.Equal(generateInviteResp.MaxUses, inviteInfoResp.CanBeUsedCount)
s.Equal(int32(0), inviteInfoResp.UsedCount)
s.True(inviteInfoResp.IsActive)
logoutReq := &authpb.LogoutRequest{
AccessToken: loginResp.AccessToken,
}
logoutResp, err := s.authClient.Logout(ctx, logoutReq)
s.NoError(err)
s.True(logoutResp.Success)
}
func (s *IntegrationSuite) TestFullFlow_MultipleRefresh() {
ctx := context.Background()
loginReq := &authpb.LoginRequest{
Email: "test@example.com",
Password: "testpassword",
Ip: "127.0.0.1",
UserAgent: "integration-test",
}
loginResp, err := s.authClient.Login(ctx, loginReq)
s.NoError(err)
s.NotEmpty(loginResp.AccessToken)
s.NotEmpty(loginResp.RefreshToken)
refreshToken := loginResp.RefreshToken
refreshReq1 := &authpb.RefreshRequest{
RefreshToken: refreshToken,
Ip: "127.0.0.1",
UserAgent: "integration-test",
}
refreshResp1, err := s.authClient.Refresh(ctx, refreshReq1)
s.NoError(err)
s.NotEmpty(refreshResp1.AccessToken)
refreshReq2 := &authpb.RefreshRequest{
RefreshToken: refreshToken,
Ip: "127.0.0.1",
UserAgent: "integration-test",
}
refreshResp2, err := s.authClient.Refresh(ctx, refreshReq2)
s.NoError(err)
s.NotEmpty(refreshResp2.AccessToken)
validateReq := &authpb.ValidateRequest{
AccessToken: refreshResp2.AccessToken,
}
validateResp, err := s.authClient.Validate(ctx, validateReq)
s.NoError(err)
s.True(validateResp.Valid)
logoutReq := &authpb.LogoutRequest{
AccessToken: refreshResp2.AccessToken,
}
logoutResp, err := s.authClient.Logout(ctx, logoutReq)
s.NoError(err)
s.True(logoutResp.Success)
}

View File

@@ -0,0 +1,211 @@
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...")
err = database.RunMigrationsFromPath(connStr, "../../../migrations")
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...")
logger, _ := zap.NewDevelopment()
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) 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")
}

View File

@@ -0,0 +1,122 @@
package tests
import (
"context"
authpb "git.techease.ru/Smart-search/smart-search-back/pkg/pb/auth"
invitepb "git.techease.ru/Smart-search/smart-search-back/pkg/pb/invite"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func (s *IntegrationSuite) TestInviteHandler_GenerateWithNonExistentUser() {
req := &invitepb.GenerateRequest{
UserId: 999999,
TtlDays: 30,
MaxUses: 10,
}
resp, err := s.inviteClient.Generate(context.Background(), req)
s.Error(err)
s.Nil(resp)
st, ok := status.FromError(err)
s.True(ok)
s.Contains([]codes.Code{codes.NotFound, codes.Internal, codes.Unknown}, st.Code())
}
func (s *IntegrationSuite) TestInviteHandler_GetInfoWithInvalidCode() {
req := &invitepb.GetInfoRequest{
Code: "999999999",
}
resp, err := s.inviteClient.GetInfo(context.Background(), req)
s.Error(err)
s.Nil(resp)
st, ok := status.FromError(err)
s.True(ok)
s.Equal(codes.NotFound, st.Code())
}
func (s *IntegrationSuite) TestInviteHandler_GetInfoWithInvalidCodeFormat() {
req := &invitepb.GetInfoRequest{
Code: "invalid-code",
}
resp, err := s.inviteClient.GetInfo(context.Background(), req)
s.Error(err)
s.Nil(resp)
}
func (s *IntegrationSuite) TestInviteHandler_GenerateAndGetInfoFlow() {
ctx := context.Background()
loginReq := &authpb.LoginRequest{
Email: "test@example.com",
Password: "testpassword",
Ip: "127.0.0.1",
UserAgent: "integration-test",
}
loginResp, err := s.authClient.Login(ctx, loginReq)
s.NoError(err)
validateReq := &authpb.ValidateRequest{
AccessToken: loginResp.AccessToken,
}
validateResp, err := s.authClient.Validate(ctx, validateReq)
s.NoError(err)
s.True(validateResp.Valid)
generateReq := &invitepb.GenerateRequest{
UserId: validateResp.UserId,
TtlDays: 30,
MaxUses: 10,
}
generateResp, err := s.inviteClient.Generate(ctx, generateReq)
s.NoError(err)
s.NotNil(generateResp)
s.NotEmpty(generateResp.Code)
s.Greater(generateResp.MaxUses, int32(0))
s.NotNil(generateResp.ExpiresAt)
getInfoReq := &invitepb.GetInfoRequest{
Code: generateResp.Code,
}
infoResp, err := s.inviteClient.GetInfo(ctx, getInfoReq)
s.NoError(err)
s.NotNil(infoResp)
s.Equal(generateResp.Code, infoResp.Code)
s.Equal(validateResp.UserId, infoResp.UserId)
s.Equal(generateResp.MaxUses, infoResp.CanBeUsedCount)
s.Equal(int32(0), infoResp.UsedCount)
s.True(infoResp.IsActive)
}
func (s *IntegrationSuite) TestInviteHandler_GenerateWithInvalidTTL() {
ctx := context.Background()
req := &invitepb.GenerateRequest{
UserId: 1,
TtlDays: -1,
MaxUses: 10,
}
resp, err := s.inviteClient.Generate(ctx, req)
if err != nil {
st, ok := status.FromError(err)
s.True(ok)
s.Contains([]codes.Code{codes.InvalidArgument, codes.Internal, codes.Unknown}, st.Code())
return
}
s.NotNil(resp)
}

View File

@@ -0,0 +1,138 @@
package tests
import (
"context"
authpb "git.techease.ru/Smart-search/smart-search-back/pkg/pb/auth"
requestpb "git.techease.ru/Smart-search/smart-search-back/pkg/pb/request"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func (s *IntegrationSuite) TestRequestHandler_CreateTZWithNonExistentUser() {
req := &requestpb.CreateTZRequest{
UserId: 999999,
RequestTxt: "Нужны поставщики металлоконструкций",
}
resp, err := s.requestClient.CreateTZ(context.Background(), req)
s.Error(err)
s.Nil(resp)
st, ok := status.FromError(err)
s.True(ok)
s.Contains([]codes.Code{codes.NotFound, codes.Internal, codes.Unknown}, st.Code())
}
func (s *IntegrationSuite) TestRequestHandler_GetMailingListByIDWithNonExistent() {
req := &requestpb.GetMailingListByIDRequest{
RequestId: "999999",
UserId: 1,
}
resp, err := s.requestClient.GetMailingListByID(context.Background(), req)
s.Error(err)
s.Nil(resp)
st, ok := status.FromError(err)
s.True(ok)
s.Contains([]codes.Code{codes.NotFound, codes.Internal, codes.Unknown}, st.Code())
}
func (s *IntegrationSuite) TestRequestHandler_GetMailingListWithNonExistentUser() {
req := &requestpb.GetMailingListRequest{
UserId: 999999,
}
resp, err := s.requestClient.GetMailingList(context.Background(), req)
if err != nil {
st, ok := status.FromError(err)
s.True(ok)
s.Contains([]codes.Code{codes.NotFound, codes.Internal}, st.Code())
return
}
s.NotNil(resp)
s.Equal(0, len(resp.Items))
}
func (s *IntegrationSuite) TestRequestHandler_ApproveTZWithInvalidRequest() {
req := &requestpb.ApproveTZRequest{
RequestId: "999999",
FinalTz: "Approved TZ",
UserId: 1,
}
resp, err := s.requestClient.ApproveTZ(context.Background(), req)
s.Error(err)
s.Nil(resp)
st, ok := status.FromError(err)
s.True(ok)
s.Contains([]codes.Code{codes.NotFound, codes.Internal, codes.Unknown, codes.InvalidArgument}, st.Code())
}
func (s *IntegrationSuite) TestRequestHandler_CreateTZWithValidUser() {
ctx := context.Background()
loginReq := &authpb.LoginRequest{
Email: "test@example.com",
Password: "testpassword",
Ip: "127.0.0.1",
UserAgent: "integration-test",
}
loginResp, err := s.authClient.Login(ctx, loginReq)
s.NoError(err)
validateReq := &authpb.ValidateRequest{
AccessToken: loginResp.AccessToken,
}
validateResp, err := s.authClient.Validate(ctx, validateReq)
s.NoError(err)
req := &requestpb.CreateTZRequest{
UserId: validateResp.UserId,
RequestTxt: "Нужны поставщики металлоконструкций",
}
resp, err := s.requestClient.CreateTZ(ctx, req)
s.NoError(err)
s.NotNil(resp)
s.NotEmpty(resp.RequestId)
s.NotEmpty(resp.TzText)
}
func (s *IntegrationSuite) TestRequestHandler_GetMailingListWithValidUser() {
ctx := context.Background()
loginReq := &authpb.LoginRequest{
Email: "test@example.com",
Password: "testpassword",
Ip: "127.0.0.1",
UserAgent: "integration-test",
}
loginResp, err := s.authClient.Login(ctx, loginReq)
s.NoError(err)
validateReq := &authpb.ValidateRequest{
AccessToken: loginResp.AccessToken,
}
validateResp, err := s.authClient.Validate(ctx, validateReq)
s.NoError(err)
req := &requestpb.GetMailingListRequest{
UserId: validateResp.UserId,
}
resp, err := s.requestClient.GetMailingList(ctx, req)
s.NoError(err)
s.NotNil(resp)
}

View File

@@ -0,0 +1,95 @@
package tests
import (
"context"
authpb "git.techease.ru/Smart-search/smart-search-back/pkg/pb/auth"
requestpb "git.techease.ru/Smart-search/smart-search-back/pkg/pb/request"
supplierpb "git.techease.ru/Smart-search/smart-search-back/pkg/pb/supplier"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func (s *IntegrationSuite) TestSupplierHandler_ExportExcelWithNonExistentRequest() {
req := &supplierpb.ExportExcelRequest{
RequestId: "999999",
UserId: 1,
}
resp, err := s.supplierClient.ExportExcel(context.Background(), req)
s.Error(err)
s.Nil(resp)
st, ok := status.FromError(err)
s.True(ok)
s.Contains([]codes.Code{codes.NotFound, codes.Internal, codes.Unknown, codes.InvalidArgument}, st.Code())
}
func (s *IntegrationSuite) TestSupplierHandler_ExportExcelWithInvalidUser() {
req := &supplierpb.ExportExcelRequest{
RequestId: "1",
UserId: 999999,
}
resp, err := s.supplierClient.ExportExcel(context.Background(), req)
if err != nil {
st, ok := status.FromError(err)
s.True(ok)
s.Contains([]codes.Code{codes.NotFound, codes.PermissionDenied, codes.Internal, codes.Unknown, codes.InvalidArgument}, st.Code())
return
}
s.NotNil(resp)
}
func (s *IntegrationSuite) TestSupplierHandler_ExportExcelWithValidRequest() {
ctx := context.Background()
loginReq := &authpb.LoginRequest{
Email: "test@example.com",
Password: "testpassword",
Ip: "127.0.0.1",
UserAgent: "integration-test",
}
loginResp, err := s.authClient.Login(ctx, loginReq)
s.NoError(err)
validateReq := &authpb.ValidateRequest{
AccessToken: loginResp.AccessToken,
}
validateResp, err := s.authClient.Validate(ctx, validateReq)
s.NoError(err)
createReq := &requestpb.CreateTZRequest{
UserId: validateResp.UserId,
RequestTxt: "Нужны поставщики кирпича",
}
createResp, err := s.requestClient.CreateTZ(ctx, createReq)
if err != nil {
s.T().Skip("Cannot test ExportExcel without CreateTZ")
return
}
exportReq := &supplierpb.ExportExcelRequest{
RequestId: createResp.RequestId,
UserId: validateResp.UserId,
}
exportResp, err := s.supplierClient.ExportExcel(ctx, exportReq)
if err != nil {
st, ok := status.FromError(err)
s.True(ok)
s.Contains([]codes.Code{codes.NotFound, codes.Internal}, st.Code())
return
}
s.NotNil(exportResp)
s.NotEmpty(exportResp.FileName)
s.Equal("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", exportResp.MimeType)
}

View File

@@ -0,0 +1,174 @@
package tests
import (
"context"
authpb "git.techease.ru/Smart-search/smart-search-back/pkg/pb/auth"
userpb "git.techease.ru/Smart-search/smart-search-back/pkg/pb/user"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func (s *IntegrationSuite) TestUserHandler_GetInfoWithNonExistentUser() {
req := &userpb.GetInfoRequest{
UserId: 999999,
}
resp, err := s.userClient.GetInfo(context.Background(), req)
s.Error(err)
s.Nil(resp)
st, ok := status.FromError(err)
s.True(ok)
s.Equal(codes.NotFound, st.Code())
}
func (s *IntegrationSuite) TestUserHandler_GetBalanceWithNonExistentUser() {
req := &userpb.GetBalanceRequest{
UserId: 999999,
}
resp, err := s.userClient.GetBalance(context.Background(), req)
s.Error(err)
s.Nil(resp)
st, ok := status.FromError(err)
s.True(ok)
s.Equal(codes.NotFound, st.Code())
}
func (s *IntegrationSuite) TestUserHandler_GetStatisticsWithNonExistentUser() {
req := &userpb.GetStatisticsRequest{
UserId: 999999,
}
resp, err := s.userClient.GetStatistics(context.Background(), req)
if err != nil {
st, ok := status.FromError(err)
s.True(ok)
s.Equal(codes.NotFound, st.Code())
return
}
s.NotNil(resp)
s.Equal(int32(0), resp.TotalRequests)
}
func (s *IntegrationSuite) TestUserHandler_GetBalanceStatistics() {
req := &userpb.GetBalanceStatisticsRequest{
UserId: 1,
}
resp, err := s.userClient.GetBalanceStatistics(context.Background(), req)
if err != nil {
st, ok := status.FromError(err)
s.True(ok)
s.Equal(codes.NotFound, st.Code())
return
}
s.NotNil(resp)
s.GreaterOrEqual(resp.Balance, 0.0)
s.GreaterOrEqual(resp.TotalRequests, int32(0))
}
func (s *IntegrationSuite) TestUserHandler_GetInfoWithValidUser() {
ctx := context.Background()
loginReq := &authpb.LoginRequest{
Email: "test@example.com",
Password: "testpassword",
Ip: "127.0.0.1",
UserAgent: "integration-test",
}
loginResp, err := s.authClient.Login(ctx, loginReq)
s.NoError(err)
validateReq := &authpb.ValidateRequest{
AccessToken: loginResp.AccessToken,
}
validateResp, err := s.authClient.Validate(ctx, validateReq)
s.NoError(err)
req := &userpb.GetInfoRequest{
UserId: validateResp.UserId,
}
resp, err := s.userClient.GetInfo(ctx, req)
s.NoError(err)
s.NotNil(resp)
s.Equal("test@example.com", resp.Email)
s.Equal("Test User", resp.Name)
s.Equal("+1234567890", resp.Phone)
s.Equal("Test Company", resp.CompanyName)
s.Equal("active", resp.PaymentStatus)
}
func (s *IntegrationSuite) TestUserHandler_GetBalanceWithValidUser() {
ctx := context.Background()
loginReq := &authpb.LoginRequest{
Email: "test@example.com",
Password: "testpassword",
Ip: "127.0.0.1",
UserAgent: "integration-test",
}
loginResp, err := s.authClient.Login(ctx, loginReq)
s.NoError(err)
validateReq := &authpb.ValidateRequest{
AccessToken: loginResp.AccessToken,
}
validateResp, err := s.authClient.Validate(ctx, validateReq)
s.NoError(err)
req := &userpb.GetBalanceRequest{
UserId: validateResp.UserId,
}
resp, err := s.userClient.GetBalance(ctx, req)
s.NoError(err)
s.NotNil(resp)
s.GreaterOrEqual(resp.Balance, 0.0)
}
func (s *IntegrationSuite) TestUserHandler_GetStatisticsWithValidUser() {
ctx := context.Background()
loginReq := &authpb.LoginRequest{
Email: "test@example.com",
Password: "testpassword",
Ip: "127.0.0.1",
UserAgent: "integration-test",
}
loginResp, err := s.authClient.Login(ctx, loginReq)
s.NoError(err)
validateReq := &authpb.ValidateRequest{
AccessToken: loginResp.AccessToken,
}
validateResp, err := s.authClient.Validate(ctx, validateReq)
s.NoError(err)
req := &userpb.GetStatisticsRequest{
UserId: validateResp.UserId,
}
resp, err := s.userClient.GetStatistics(ctx, req)
s.NoError(err)
s.NotNil(resp)
s.GreaterOrEqual(resp.TotalRequests, int32(0))
s.GreaterOrEqual(resp.SuccessfulRequests, int32(0))
s.GreaterOrEqual(resp.FailedRequests, int32(0))
s.GreaterOrEqual(resp.TotalSpent, 0.0)
}

View File

@@ -3,8 +3,8 @@ package grpc
import (
"context"
"smart-search-back/pkg/errors"
pb "smart-search-back/pkg/pb/api/proto/user"
"git.techease.ru/Smart-search/smart-search-back/pkg/errors"
pb "git.techease.ru/Smart-search/smart-search-back/pkg/pb/user"
)
func (h *UserHandler) GetInfo(ctx context.Context, req *pb.GetInfoRequest) (*pb.GetInfoResponse, error) {