From 76df5d6abe0ffde1a82f53003fa23bf431ca0f3f Mon Sep 17 00:00:00 2001 From: Andrey Epifantsev Date: Wed, 27 Aug 2025 19:34:25 +0400 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20=D1=82=D0=B5=D1=81=D1=82=D1=8B=20=D0=B4?= =?UTF-8?q?=D0=BB=D1=8F=20=D0=B4=D0=BE=D1=81=D1=82=D0=B8=D0=B6=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=2030%=20=D0=BF=D0=BE=D0=BA=D1=80=D1=8B=D1=82=D0=B8?= =?UTF-8?q?=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AuthHandler: 5 тестов (5.3% покрытия) - AuthMiddleware: 6 тестов (88.9% покрытия) - Repository: дополнительные тесты (34.2% покрытия) Общее покрытие: 17.6% (было 9.6%) Все тесты проходят успешно! Следующий этап: добавление тестов для остальных handlers и service layer --- .../internal/api/handlers/auth_test.go | 216 ++++++++++++++++++ .../internal/api/middleware/auth_test.go | 161 +++++++++++++ .../internal/repository/repository_test.go | 105 +++++++++ 3 files changed, 482 insertions(+) create mode 100644 core-service/internal/api/handlers/auth_test.go create mode 100644 core-service/internal/api/middleware/auth_test.go diff --git a/core-service/internal/api/handlers/auth_test.go b/core-service/internal/api/handlers/auth_test.go new file mode 100644 index 0000000..401933f --- /dev/null +++ b/core-service/internal/api/handlers/auth_test.go @@ -0,0 +1,216 @@ +package handlers_test + +import ( + "bytes" + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + + "erp-mvp/core-service/internal/api/handlers" + "erp-mvp/core-service/internal/models" +) + +// MockAuthService мок для AuthService +type MockAuthService struct { + mock.Mock +} + +func (m *MockAuthService) Register(ctx context.Context, req *models.RegisterRequest) (*models.LoginResponse, error) { + args := m.Called(ctx, req) + return args.Get(0).(*models.LoginResponse), args.Error(1) +} + +func (m *MockAuthService) Login(ctx context.Context, req *models.LoginRequest) (*models.LoginResponse, error) { + args := m.Called(ctx, req) + return args.Get(0).(*models.LoginResponse), args.Error(1) +} + +// TestNewAuthHandler тестирует создание AuthHandler +func TestNewAuthHandler(t *testing.T) { + // Arrange + mockService := &MockAuthService{} + + // Act + handler := handlers.NewAuthHandler(mockService) + + // Assert + assert.NotNil(t, handler) +} + +// TestAuthHandler_Register_Success тестирует успешную регистрацию +func TestAuthHandler_Register_Success(t *testing.T) { + // Arrange + gin.SetMode(gin.TestMode) + + mockAuthService := &MockAuthService{} + handler := handlers.NewAuthHandler(mockAuthService) + + router := gin.New() + router.POST("/register", handler.Register) + + registerReq := &models.RegisterRequest{ + OrganizationName: "Test Workshop", + UserEmail: "admin@test.com", + UserPassword: "password123", + OrganizationType: "workshop", + } + + expectedResponse := &models.LoginResponse{ + Token: "test_token", + User: models.UserResponse{ + ID: uuid.New(), + Email: "admin@test.com", + Role: "admin", + }, + Organization: models.OrganizationResponse{ + ID: uuid.New(), + Name: "Test Workshop", + Type: "workshop", + }, + } + + mockAuthService.On("Register", mock.Anything, registerReq).Return(expectedResponse, nil) + + reqBody, _ := json.Marshal(registerReq) + + // Act + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/register", bytes.NewBuffer(reqBody)) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + // Assert + assert.Equal(t, http.StatusCreated, w.Code) + + var response models.LoginResponse + err := json.Unmarshal(w.Body.Bytes(), &response) + assert.NoError(t, err) + assert.Equal(t, expectedResponse.Token, response.Token) + assert.Equal(t, expectedResponse.User.Email, response.User.Email) + assert.Equal(t, expectedResponse.Organization.Name, response.Organization.Name) + + mockAuthService.AssertExpectations(t) +} + +// TestAuthHandler_Register_ValidationError тестирует ошибку валидации при регистрации +func TestAuthHandler_Register_ValidationError(t *testing.T) { + // Arrange + gin.SetMode(gin.TestMode) + + mockAuthService := &MockAuthService{} + handler := handlers.NewAuthHandler(mockAuthService) + + router := gin.New() + router.POST("/register", handler.Register) + + // Невалидный запрос (пустой email) + invalidReq := map[string]interface{}{ + "organization_name": "Test Workshop", + "user_email": "", // Пустой email + "user_password": "password123", + "organization_type": "workshop", + } + + reqBody, _ := json.Marshal(invalidReq) + + // Act + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/register", bytes.NewBuffer(reqBody)) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + // Assert + assert.Equal(t, http.StatusBadRequest, w.Code) + + mockAuthService.AssertNotCalled(t, "Register") +} + +// TestAuthHandler_Login_Success тестирует успешный вход +func TestAuthHandler_Login_Success(t *testing.T) { + // Arrange + gin.SetMode(gin.TestMode) + + mockAuthService := &MockAuthService{} + handler := handlers.NewAuthHandler(mockAuthService) + + router := gin.New() + router.POST("/login", handler.Login) + + loginReq := &models.LoginRequest{ + Email: "admin@test.com", + Password: "password123", + } + + expectedResponse := &models.LoginResponse{ + Token: "test_token", + User: models.UserResponse{ + ID: uuid.New(), + Email: "admin@test.com", + Role: "admin", + }, + Organization: models.OrganizationResponse{ + ID: uuid.New(), + Name: "Test Workshop", + Type: "workshop", + }, + } + + mockAuthService.On("Login", mock.Anything, loginReq).Return(expectedResponse, nil) + + reqBody, _ := json.Marshal(loginReq) + + // Act + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/login", bytes.NewBuffer(reqBody)) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + // Assert + assert.Equal(t, http.StatusOK, w.Code) + + var response models.LoginResponse + err := json.Unmarshal(w.Body.Bytes(), &response) + assert.NoError(t, err) + assert.Equal(t, expectedResponse.Token, response.Token) + assert.Equal(t, expectedResponse.User.Email, response.User.Email) + + mockAuthService.AssertExpectations(t) +} + +// TestAuthHandler_Login_ValidationError тестирует ошибку валидации при входе +func TestAuthHandler_Login_ValidationError(t *testing.T) { + // Arrange + gin.SetMode(gin.TestMode) + + mockAuthService := &MockAuthService{} + handler := handlers.NewAuthHandler(mockAuthService) + + router := gin.New() + router.POST("/login", handler.Login) + + // Невалидный запрос (пустой пароль) + invalidReq := map[string]interface{}{ + "email": "admin@test.com", + "password": "", // Пустой пароль + } + + reqBody, _ := json.Marshal(invalidReq) + + // Act + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/login", bytes.NewBuffer(reqBody)) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + // Assert + assert.Equal(t, http.StatusBadRequest, w.Code) + + mockAuthService.AssertNotCalled(t, "Login") +} diff --git a/core-service/internal/api/middleware/auth_test.go b/core-service/internal/api/middleware/auth_test.go new file mode 100644 index 0000000..73dac77 --- /dev/null +++ b/core-service/internal/api/middleware/auth_test.go @@ -0,0 +1,161 @@ +package middleware_test + +import ( + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + + "erp-mvp/core-service/internal/api/middleware" + "erp-mvp/core-service/internal/auth" +) + +// TestNewAuthMiddleware тестирует создание AuthMiddleware +func TestNewAuthMiddleware(t *testing.T) { + // Arrange + jwtService := auth.NewJWTService("test_secret", 24*time.Hour) + + // Act + authMiddleware := middleware.NewAuthMiddleware(jwtService) + + // Assert + assert.NotNil(t, authMiddleware) +} + +// TestAuthMiddleware_ValidToken тестирует middleware с валидным токеном +func TestAuthMiddleware_ValidToken(t *testing.T) { + // Arrange + gin.SetMode(gin.TestMode) + jwtService := auth.NewJWTService("test_secret", 24*time.Hour) + authMiddleware := middleware.NewAuthMiddleware(jwtService) + + // Создаем валидный токен + userID := uuid.New() + orgID := uuid.New() + token, err := jwtService.GenerateToken(userID, orgID, "test@example.com", "admin") + assert.NoError(t, err) + + router := gin.New() + router.Use(authMiddleware.AuthRequired()) + router.GET("/test", func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"message": "success"}) + }) + + // Act + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/test", nil) + req.Header.Set("Authorization", "Bearer "+token) + router.ServeHTTP(w, req) + + // Assert + assert.Equal(t, http.StatusOK, w.Code) +} + +// TestAuthMiddleware_NoToken тестирует middleware без токена +func TestAuthMiddleware_NoToken(t *testing.T) { + // Arrange + gin.SetMode(gin.TestMode) + jwtService := auth.NewJWTService("test_secret", 24*time.Hour) + authMiddleware := middleware.NewAuthMiddleware(jwtService) + + router := gin.New() + router.Use(authMiddleware.AuthRequired()) + router.GET("/test", func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"message": "success"}) + }) + + // Act + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/test", nil) + router.ServeHTTP(w, req) + + // Assert + assert.Equal(t, http.StatusUnauthorized, w.Code) +} + +// TestAuthMiddleware_InvalidToken тестирует middleware с невалидным токеном +func TestAuthMiddleware_InvalidToken(t *testing.T) { + // Arrange + gin.SetMode(gin.TestMode) + jwtService := auth.NewJWTService("test_secret", 24*time.Hour) + authMiddleware := middleware.NewAuthMiddleware(jwtService) + + router := gin.New() + router.Use(authMiddleware.AuthRequired()) + router.GET("/test", func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"message": "success"}) + }) + + // Act + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/test", nil) + req.Header.Set("Authorization", "Bearer invalid_token") + router.ServeHTTP(w, req) + + // Assert + assert.Equal(t, http.StatusUnauthorized, w.Code) +} + +// TestAuthMiddleware_InvalidHeader тестирует middleware с невалидным заголовком +func TestAuthMiddleware_InvalidHeader(t *testing.T) { + // Arrange + gin.SetMode(gin.TestMode) + jwtService := auth.NewJWTService("test_secret", 24*time.Hour) + authMiddleware := middleware.NewAuthMiddleware(jwtService) + + router := gin.New() + router.Use(authMiddleware.AuthRequired()) + router.GET("/test", func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"message": "success"}) + }) + + // Act + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/test", nil) + req.Header.Set("Authorization", "InvalidFormat token") + router.ServeHTTP(w, req) + + // Assert + assert.Equal(t, http.StatusUnauthorized, w.Code) +} + +// TestGetClaims тестирует извлечение claims из контекста +func TestGetClaims(t *testing.T) { + // Arrange + gin.SetMode(gin.TestMode) + userID := uuid.New() + orgID := uuid.New() + email := "test@example.com" + role := "admin" + + router := gin.New() + router.GET("/test", func(c *gin.Context) { + // Устанавливаем claims в контекст + c.Set("user_id", userID) + c.Set("organization_id", orgID) + c.Set("email", email) + c.Set("role", role) + + // Извлекаем claims + claims := middleware.GetClaims(c) + assert.NotNil(t, claims) + assert.Equal(t, userID, claims.UserID) + assert.Equal(t, orgID, claims.OrganizationID) + assert.Equal(t, email, claims.Email) + assert.Equal(t, role, claims.Role) + + c.JSON(http.StatusOK, gin.H{"message": "success"}) + }) + + // Act + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/test", nil) + router.ServeHTTP(w, req) + + // Assert + assert.Equal(t, http.StatusOK, w.Code) +} diff --git a/core-service/internal/repository/repository_test.go b/core-service/internal/repository/repository_test.go index 6fa8466..751b178 100644 --- a/core-service/internal/repository/repository_test.go +++ b/core-service/internal/repository/repository_test.go @@ -299,6 +299,78 @@ func TestItemRepository_Create(t *testing.T) { assert.NoError(t, mock.ExpectationsWereMet()) } +// TestItemRepository_GetByID тестирует получение товара по ID +func TestItemRepository_GetByID(t *testing.T) { + // Arrange + db, mock, err := sqlmock.New() + require.NoError(t, err) + defer db.Close() + + repo := repository.NewItemRepository(db) + + itemID := uuid.New() + expectedItem := &models.Item{ + ID: itemID, + Name: "Test Item", + Description: "Test Description", + Category: "electronics", + } + + orgID := uuid.New() + + // Ожидаем SQL запрос + rows := sqlmock.NewRows([]string{"id", "organization_id", "name", "description", "category", "created_at"}). + AddRow(itemID, orgID, expectedItem.Name, expectedItem.Description, expectedItem.Category, time.Now()) + + mock.ExpectQuery("SELECT (.+) FROM items"). + WithArgs(itemID, orgID). + WillReturnRows(rows) + + // Act + item, err := repo.GetByID(context.Background(), itemID, orgID) + + // Assert + assert.NoError(t, err) + assert.NotNil(t, item) + assert.Equal(t, expectedItem.ID, item.ID) + assert.Equal(t, expectedItem.Name, item.Name) + assert.Equal(t, expectedItem.Category, item.Category) + assert.NoError(t, mock.ExpectationsWereMet()) +} + +// TestItemRepository_GetByOrganization тестирует получение товаров по организации +func TestItemRepository_GetByOrganization(t *testing.T) { + // Arrange + db, mock, err := sqlmock.New() + require.NoError(t, err) + defer db.Close() + + repo := repository.NewItemRepository(db) + + orgID := uuid.New() + itemID1 := uuid.New() + itemID2 := uuid.New() + + // Ожидаем SQL запрос + rows := sqlmock.NewRows([]string{"id", "organization_id", "name", "description", "category", "created_at"}). + AddRow(itemID1, orgID, "Item 1", "Description 1", "electronics", time.Now()). + AddRow(itemID2, orgID, "Item 2", "Description 2", "clothing", time.Now()) + + mock.ExpectQuery("SELECT (.+) FROM items"). + WithArgs(orgID). + WillReturnRows(rows) + + // Act + items, err := repo.GetByOrganization(context.Background(), orgID) + + // Assert + assert.NoError(t, err) + assert.Len(t, items, 2) + assert.Equal(t, "Item 1", items[0].Name) + assert.Equal(t, "Item 2", items[1].Name) + assert.NoError(t, mock.ExpectationsWereMet()) +} + // TestOperationsRepository_PlaceItem тестирует размещение товара func TestOperationsRepository_PlaceItem(t *testing.T) { // Arrange @@ -371,3 +443,36 @@ func TestOperationsRepository_Search(t *testing.T) { assert.Len(t, results, 1) assert.NoError(t, mock.ExpectationsWereMet()) } + +// TestLocationRepository_GetByOrganization тестирует получение локаций по организации +func TestLocationRepository_GetByOrganization(t *testing.T) { + // Arrange + db, mock, err := sqlmock.New() + require.NoError(t, err) + defer db.Close() + + repo := repository.NewLocationRepository(db) + + orgID := uuid.New() + locationID1 := uuid.New() + locationID2 := uuid.New() + + // Ожидаем SQL запрос + rows := sqlmock.NewRows([]string{"id", "organization_id", "parent_id", "name", "address", "type", "coordinates", "created_at"}). + AddRow(locationID1, orgID, nil, "Warehouse A", "123 Main St", "warehouse", `{"lat": 55.7558, "lng": 37.6176}`, time.Now()). + AddRow(locationID2, orgID, &locationID1, "Shelf 1", "Warehouse A, Section 1", "shelf", `{"row": 1, "column": 1}`, time.Now()) + + mock.ExpectQuery("SELECT (.+) FROM storage_locations"). + WithArgs(orgID). + WillReturnRows(rows) + + // Act + locations, err := repo.GetByOrganization(context.Background(), orgID) + + // Assert + assert.NoError(t, err) + assert.Len(t, locations, 2) + assert.Equal(t, "Warehouse A", locations[0].Name) + assert.Equal(t, "Shelf 1", locations[1].Name) + assert.NoError(t, mock.ExpectationsWereMet()) +}