8 Commits

Author SHA1 Message Date
1b883a6d69 Откат системы шаблонов - удалены все связанные файлы и маршруты 2025-09-01 13:58:05 +04:00
3552110f4e feat: добавлены тесты для OperationsHandler
- OperationsHandler: 5 тестов (все проходят успешно)
- Покрытие handlers: 37.0% (было 25.1%)
- Общее покрытие: 30.4% (>= 30%) 🎯

Цель достигнута! Все основные компоненты покрыты тестами:
- Auth: 88.2% 
- Middleware: 88.9% 
- Repository: 34.2% 
- Service: 14.6% 
- Handlers: 37.0% 

Общий результат: 27/27 тестов прошли успешно
2025-08-27 19:53:10 +04:00
fcbd6dc2a7 fix: исправлены тесты ItemHandler и LocationHandler
- Исправлена установка claims в контекст для всех тестов
- Исправлены аргументы в моках для GetItem, GetLocation, GetChildren
- Все тесты теперь проходят успешно

Результаты:
- ItemHandler: 5/5 тестов проходят 
- LocationHandler: 5/5 тестов проходят 
- Общее покрытие: 26.7% (было 17.6%)

Следующий этап: добавление тестов для OperationsHandler
2025-08-27 19:50:03 +04:00
0524a52be1 feat: добавлены тесты для ItemHandler и ItemService
- ItemHandler: 5 тестов (частично работают)
- ItemService: 6 тестов (все проходят)
- LocationHandler: 5 тестов (готовы к тестированию)

Покрытие:
- Service: 14.6% (было 0%)
- Repository: 34.2% (стабильно)
- Auth: 88.2% (стабильно)
- Middleware: 88.9% (стабильно)

Следующий этап: исправление ItemHandler тестов и добавление LocationHandler тестов
2025-08-27 19:41:39 +04:00
76df5d6abe feat: добавлены тесты для достижения 30% покрытия
- AuthHandler: 5 тестов (5.3% покрытия)
- AuthMiddleware: 6 тестов (88.9% покрытия)
- Repository: дополнительные тесты (34.2% покрытия)

Общее покрытие: 17.6% (было 9.6%)

Все тесты проходят успешно!
Следующий этап: добавление тестов для остальных handlers и service layer
2025-08-27 19:34:25 +04:00
c8224f072a feat: добавлены инструменты анализа покрытия тестами
- Создан скрипт scripts/coverage.sh для автоматизированного анализа
- Добавлена документация COVERAGE.md с детальным анализом
- Текущее покрытие: 9.6% (низкое, нуждается в улучшении)

Инструменты:
- go test ./... -cover - быстрый анализ
- go tool cover -func=coverage.out - детальная статистика
- ./scripts/coverage.sh --html - HTML отчет
- ./scripts/coverage.sh --threshold=80 - с порогом покрытия

Рекомендации по улучшению:
1. Handlers (0% покрытия) - приоритет 1
2. Service Layer (0% покрытия) - приоритет 1
3. Middleware (0% покрытия) - приоритет 2
4. Repository (24.5% покрытия) - приоритет 2
5. Config & Database (0% покрытия) - приоритет 3

Цель: довести покрытие до 70%+
2025-08-27 17:51:59 +04:00
225635ed4b refactor: реорганизация структуры тестов
- Перемещены unit тесты рядом с тестируемым кодом:
  * auth_test.go -> internal/auth/auth_test.go
  * repository_test.go -> internal/repository/repository_test.go
- Перемещены integration тесты в отдельную директорию:
  * api_test.go -> tests/api_integration_test.go
- Обновлены пакеты тестов:
  * auth_test.go: package auth_test
  * repository_test.go: package repository_test
  * api_integration_test.go: package tests
- Удалена директория examples/
- Обновлен pre-commit хук для новой структуры
- Все тесты проходят успешно
2025-08-27 16:17:12 +04:00
282613edb9 Удален не актуальный README.MD 2025-08-27 16:13:48 +04:00
14 changed files with 4937 additions and 16 deletions

151
core-service/COVERAGE.md Normal file
View File

@@ -0,0 +1,151 @@
# 📊 Анализ покрытия тестами
## 🎯 Обзор
Текущее покрытие тестами: **9.6%**
### 📈 Статистика по пакетам
| Пакет | Покрытие | Статус |
|-------|----------|--------|
| `internal/auth` | 88.2% | ✅ Хорошо |
| `internal/repository` | 24.5% | ⚠️ Низкое |
| `internal/api/handlers` | 0.0% | ❌ Нет тестов |
| `internal/service` | 0.0% | ❌ Нет тестов |
| `internal/api/middleware` | 0.0% | ❌ Нет тестов |
| `internal/config` | 0.0% | ❌ Нет тестов |
| `internal/database` | 0.0% | ❌ Нет тестов |
| `internal/logger` | 0.0% | ❌ Нет тестов |
## 🛠️ Инструменты анализа
### 1. Быстрый анализ покрытия
```bash
# Общая статистика
go test ./... -cover
# Детальная статистика по функциям
go test ./... -coverprofile=coverage.out
go tool cover -func=coverage.out
```
### 2. HTML отчет
```bash
# Генерация HTML отчета
go test ./... -coverprofile=coverage.out
go tool cover -html=coverage.out -o coverage.html
```
### 3. Автоматизированный анализ
```bash
# Использование скрипта анализа
./scripts/coverage.sh
# С HTML отчетом
./scripts/coverage.sh --html
# С кастомным порогом
./scripts/coverage.sh --threshold=80 --html
```
## 📋 Рекомендации по улучшению
### 🔥 Приоритет 1 (Критично)
1. **Handlers (0% покрытия)**
- Добавить unit тесты для всех HTTP handlers
- Тестировать валидацию входных данных
- Тестировать обработку ошибок
2. **Service Layer (0% покрытия)**
- Добавить unit тесты для бизнес-логики
- Тестировать взаимодействие с репозиториями
- Тестировать обработку ошибок
### ⚠️ Приоритет 2 (Важно)
3. **Middleware (0% покрытия)**
- Тестировать аутентификацию
- Тестировать извлечение claims из JWT
4. **Repository Layer (24.5% покрытия)**
- Улучшить покрытие CRUD операций
- Добавить тесты для edge cases
### 📝 Приоритет 3 (Желательно)
5. **Config & Database (0% покрытия)**
- Тестировать загрузку конфигурации
- Тестировать подключение к БД
## 🎯 Цели покрытия
| Уровень | Целевое покрытие | Текущее покрытие |
|---------|------------------|------------------|
| Unit тесты | 80% | 9.6% |
| Integration тесты | 60% | 0% |
| Общее покрытие | 70% | 9.6% |
## 📊 Метрики качества
### Функции с высоким покрытием (>80%)
-`auth.NewJWTService` - 100%
-`auth.GenerateToken` - 100%
-`auth.HashPassword` - 100%
-`auth.CheckPassword` - 100%
### Функции с низким покрытием (<50%)
-Все handlers - 0%
-Все service методы - 0%
-Все middleware - 0%
- ❌ Config и Database - 0%
## 🔧 Интеграция с CI/CD
### Pre-commit хук
Pre-commit хук автоматически запускает тесты и показывает статистику:
```
Auth unit тесты: 4/4 ✅
Repository unit тесты: 10/10 ✅
Integration тесты: 5/5 ✅
🎯 Общий результат: 19/19 тестов прошли успешно
```
### Рекомендации для CI/CD
1. Добавить проверку покрытия в pipeline
2. Установить минимальный порог покрытия (например, 70%)
3. Генерировать HTML отчеты для каждого PR
4. Отклонять PR с низким покрытием
## 📈 План улучшения
### Этап 1: Handlers (1-2 недели)
- [ ] Unit тесты для `AuthHandler`
- [ ] Unit тесты для `ItemHandler`
- [ ] Unit тесты для `LocationHandler`
- [ ] Unit тесты для `OperationsHandler`
### Этап 2: Service Layer (1-2 недели)
- [ ] Unit тесты для `AuthService`
- [ ] Unit тесты для `ItemService`
- [ ] Unit тесты для `LocationService`
- [ ] Unit тесты для `OperationsService`
### Этап 3: Middleware (3-5 дней)
- [ ] Unit тесты для `AuthMiddleware`
- [ ] Тесты извлечения claims
### Этап 4: Repository (1 неделя)
- [ ] Улучшение покрытия CRUD операций
- [ ] Тесты edge cases
### Этап 5: Config & Database (3-5 дней)
- [ ] Тесты загрузки конфигурации
- [ ] Тесты подключения к БД
## 🎯 Ожидаемый результат
После выполнения плана:
- **Общее покрытие**: 9.6% → 70%+
- **Unit тесты**: 0% → 80%+
- **Integration тесты**: 0% → 60%+
- **Качество кода**: Значительно улучшится
- **Стабильность**: Снизится количество багов

View File

@@ -1,4 +1 @@
# Test updated hook # Test new structure
# Test fixed hook
# Test benchmark fix
# Test subtests fix

2998
core-service/coverage.html Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -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")
}

View File

@@ -0,0 +1,308 @@
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/auth"
"erp-mvp/core-service/internal/models"
)
// setClaims устанавливает claims в контекст для тестов
func setClaims(c *gin.Context, orgID uuid.UUID) {
claims := &auth.Claims{
UserID: uuid.New(),
OrganizationID: orgID,
Email: "test@example.com",
Role: "admin",
}
c.Set("user_id", claims.UserID)
c.Set("organization_id", claims.OrganizationID)
c.Set("email", claims.Email)
c.Set("role", claims.Role)
}
// MockItemService мок для ItemService
type MockItemService struct {
mock.Mock
}
func (m *MockItemService) GetItems(ctx context.Context, orgID uuid.UUID) ([]*models.Item, error) {
args := m.Called(ctx, orgID)
return args.Get(0).([]*models.Item), args.Error(1)
}
func (m *MockItemService) CreateItem(ctx context.Context, orgID uuid.UUID, req *models.CreateItemRequest) (*models.Item, error) {
args := m.Called(ctx, orgID, req)
return args.Get(0).(*models.Item), args.Error(1)
}
func (m *MockItemService) GetItem(ctx context.Context, orgID uuid.UUID, itemID uuid.UUID) (*models.Item, error) {
args := m.Called(ctx, orgID, itemID)
return args.Get(0).(*models.Item), args.Error(1)
}
func (m *MockItemService) UpdateItem(ctx context.Context, orgID uuid.UUID, itemID uuid.UUID, req *models.CreateItemRequest) (*models.Item, error) {
args := m.Called(ctx, orgID, itemID, req)
return args.Get(0).(*models.Item), args.Error(1)
}
func (m *MockItemService) DeleteItem(ctx context.Context, orgID uuid.UUID, itemID uuid.UUID) error {
args := m.Called(ctx, orgID, itemID)
return args.Error(0)
}
func (m *MockItemService) SearchItems(ctx context.Context, orgID uuid.UUID, query string, category string) ([]*models.Item, error) {
args := m.Called(ctx, orgID, query, category)
return args.Get(0).([]*models.Item), args.Error(1)
}
// TestNewItemHandler тестирует создание ItemHandler
func TestNewItemHandler(t *testing.T) {
// Arrange
mockService := &MockItemService{}
// Act
handler := handlers.NewItemHandler(mockService)
// Assert
assert.NotNil(t, handler)
}
// TestItemHandler_GetItems_Success тестирует успешное получение товаров
func TestItemHandler_GetItems_Success(t *testing.T) {
// Arrange
gin.SetMode(gin.TestMode)
mockItemService := &MockItemService{}
handler := handlers.NewItemHandler(mockItemService)
orgID := uuid.New()
expectedItems := []*models.Item{
{
ID: uuid.New(),
OrganizationID: orgID,
Name: "Item 1",
Description: "Description 1",
Category: "electronics",
},
{
ID: uuid.New(),
OrganizationID: orgID,
Name: "Item 2",
Description: "Description 2",
Category: "clothing",
},
}
mockItemService.On("GetItems", mock.Anything, orgID).Return(expectedItems, nil)
router := gin.New()
router.GET("/items", func(c *gin.Context) {
setClaims(c, orgID)
handler.GetItems(c)
})
// Act
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/items", nil)
router.ServeHTTP(w, req)
// Assert
assert.Equal(t, http.StatusOK, w.Code)
var response []*models.Item
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
assert.Len(t, response, 2)
assert.Equal(t, "Item 1", response[0].Name)
assert.Equal(t, "Item 2", response[1].Name)
mockItemService.AssertExpectations(t)
}
// TestItemHandler_CreateItem_Success тестирует успешное создание товара
func TestItemHandler_CreateItem_Success(t *testing.T) {
// Arrange
gin.SetMode(gin.TestMode)
mockItemService := &MockItemService{}
handler := handlers.NewItemHandler(mockItemService)
orgID := uuid.New()
itemID := uuid.New()
createReq := &models.CreateItemRequest{
Name: "New Item",
Description: "New Description",
Category: "electronics",
}
expectedItem := &models.Item{
ID: itemID,
OrganizationID: orgID,
Name: "New Item",
Description: "New Description",
Category: "electronics",
}
mockItemService.On("CreateItem", mock.Anything, orgID, createReq).Return(expectedItem, nil)
router := gin.New()
router.POST("/items", func(c *gin.Context) {
setClaims(c, orgID)
handler.CreateItem(c)
})
reqBody, _ := json.Marshal(createReq)
// Act
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/items", 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.Item
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
assert.Equal(t, "New Item", response.Name)
assert.Equal(t, "electronics", response.Category)
mockItemService.AssertExpectations(t)
}
// TestItemHandler_CreateItem_ValidationError тестирует ошибку валидации при создании товара
func TestItemHandler_CreateItem_ValidationError(t *testing.T) {
// Arrange
gin.SetMode(gin.TestMode)
mockItemService := &MockItemService{}
handler := handlers.NewItemHandler(mockItemService)
orgID := uuid.New()
// Невалидный запрос (пустое имя)
invalidReq := map[string]interface{}{
"name": "", // Пустое имя
"description": "Description",
"category": "electronics",
}
router := gin.New()
router.POST("/items", func(c *gin.Context) {
setClaims(c, orgID)
handler.CreateItem(c)
})
reqBody, _ := json.Marshal(invalidReq)
// Act
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/items", bytes.NewBuffer(reqBody))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
// Assert
assert.Equal(t, http.StatusBadRequest, w.Code)
mockItemService.AssertNotCalled(t, "CreateItem")
}
// TestItemHandler_GetItem_Success тестирует успешное получение товара по ID
func TestItemHandler_GetItem_Success(t *testing.T) {
// Arrange
gin.SetMode(gin.TestMode)
mockItemService := &MockItemService{}
handler := handlers.NewItemHandler(mockItemService)
orgID := uuid.New()
itemID := uuid.New()
expectedItem := &models.Item{
ID: itemID,
OrganizationID: orgID,
Name: "Test Item",
Description: "Test Description",
Category: "electronics",
}
mockItemService.On("GetItem", mock.Anything, itemID, orgID).Return(expectedItem, nil)
router := gin.New()
router.GET("/items/:id", func(c *gin.Context) {
setClaims(c, orgID)
handler.GetItem(c)
})
// Act
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/items/"+itemID.String(), nil)
router.ServeHTTP(w, req)
// Assert
assert.Equal(t, http.StatusOK, w.Code)
var response models.Item
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
assert.Equal(t, "Test Item", response.Name)
assert.Equal(t, itemID, response.ID)
mockItemService.AssertExpectations(t)
}
// TestItemHandler_SearchItems_Success тестирует успешный поиск товаров
func TestItemHandler_SearchItems_Success(t *testing.T) {
// Arrange
gin.SetMode(gin.TestMode)
mockItemService := &MockItemService{}
handler := handlers.NewItemHandler(mockItemService)
orgID := uuid.New()
expectedItems := []*models.Item{
{
ID: uuid.New(),
OrganizationID: orgID,
Name: "Search Result",
Description: "Found item",
Category: "electronics",
},
}
mockItemService.On("SearchItems", mock.Anything, orgID, "search", "electronics").Return(expectedItems, nil)
router := gin.New()
router.GET("/items/search", func(c *gin.Context) {
setClaims(c, orgID)
handler.SearchItems(c)
})
// Act
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/items/search?q=search&category=electronics", nil)
router.ServeHTTP(w, req)
// Assert
assert.Equal(t, http.StatusOK, w.Code)
var response []*models.Item
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
assert.Len(t, response, 1)
assert.Equal(t, "Search Result", response[0].Name)
mockItemService.AssertExpectations(t)
}

View File

@@ -0,0 +1,326 @@
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"
)
// MockLocationService мок для LocationService
type MockLocationService struct {
mock.Mock
}
func (m *MockLocationService) GetLocations(ctx context.Context, orgID uuid.UUID) ([]*models.StorageLocation, error) {
args := m.Called(ctx, orgID)
return args.Get(0).([]*models.StorageLocation), args.Error(1)
}
func (m *MockLocationService) CreateLocation(ctx context.Context, orgID uuid.UUID, req *models.CreateLocationRequest) (*models.StorageLocation, error) {
args := m.Called(ctx, orgID, req)
return args.Get(0).(*models.StorageLocation), args.Error(1)
}
func (m *MockLocationService) GetLocation(ctx context.Context, orgID uuid.UUID, locationID uuid.UUID) (*models.StorageLocation, error) {
args := m.Called(ctx, orgID, locationID)
return args.Get(0).(*models.StorageLocation), args.Error(1)
}
func (m *MockLocationService) UpdateLocation(ctx context.Context, orgID uuid.UUID, locationID uuid.UUID, req *models.CreateLocationRequest) (*models.StorageLocation, error) {
args := m.Called(ctx, orgID, locationID, req)
return args.Get(0).(*models.StorageLocation), args.Error(1)
}
func (m *MockLocationService) DeleteLocation(ctx context.Context, orgID uuid.UUID, locationID uuid.UUID) error {
args := m.Called(ctx, orgID, locationID)
return args.Error(0)
}
func (m *MockLocationService) GetChildren(ctx context.Context, orgID uuid.UUID, parentID uuid.UUID) ([]*models.StorageLocation, error) {
args := m.Called(ctx, orgID, parentID)
return args.Get(0).([]*models.StorageLocation), args.Error(1)
}
// TestNewLocationHandler тестирует создание LocationHandler
func TestNewLocationHandler(t *testing.T) {
// Arrange
mockService := &MockLocationService{}
// Act
handler := handlers.NewLocationHandler(mockService)
// Assert
assert.NotNil(t, handler)
}
// TestLocationHandler_GetLocations_Success тестирует успешное получение локаций
func TestLocationHandler_GetLocations_Success(t *testing.T) {
// Arrange
gin.SetMode(gin.TestMode)
mockLocationService := &MockLocationService{}
handler := handlers.NewLocationHandler(mockLocationService)
orgID := uuid.New()
expectedLocations := []*models.StorageLocation{
{
ID: uuid.New(),
OrganizationID: orgID,
Name: "Warehouse A",
Address: "123 Main St",
Type: "warehouse",
},
{
ID: uuid.New(),
OrganizationID: orgID,
Name: "Shelf 1",
Address: "Warehouse A, Section 1",
Type: "shelf",
},
}
mockLocationService.On("GetLocations", mock.Anything, orgID).Return(expectedLocations, nil)
router := gin.New()
router.GET("/locations", func(c *gin.Context) {
// Устанавливаем claims в контекст
c.Set("user_id", uuid.New())
c.Set("organization_id", orgID)
c.Set("email", "test@example.com")
c.Set("role", "admin")
handler.GetLocations(c)
})
// Act
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/locations", nil)
router.ServeHTTP(w, req)
// Assert
assert.Equal(t, http.StatusOK, w.Code)
var response []*models.StorageLocation
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
assert.Len(t, response, 2)
assert.Equal(t, "Warehouse A", response[0].Name)
assert.Equal(t, "Shelf 1", response[1].Name)
mockLocationService.AssertExpectations(t)
}
// TestLocationHandler_CreateLocation_Success тестирует успешное создание локации
func TestLocationHandler_CreateLocation_Success(t *testing.T) {
// Arrange
gin.SetMode(gin.TestMode)
mockLocationService := &MockLocationService{}
handler := handlers.NewLocationHandler(mockLocationService)
orgID := uuid.New()
locationID := uuid.New()
createReq := &models.CreateLocationRequest{
Name: "New Warehouse",
Address: "456 Oak St",
Type: "warehouse",
Coordinates: models.JSON{"lat": 55.7558, "lng": 37.6176},
}
expectedLocation := &models.StorageLocation{
ID: locationID,
OrganizationID: orgID,
Name: "New Warehouse",
Address: "456 Oak St",
Type: "warehouse",
Coordinates: models.JSON{"lat": 55.7558, "lng": 37.6176},
}
mockLocationService.On("CreateLocation", mock.Anything, orgID, createReq).Return(expectedLocation, nil)
router := gin.New()
router.POST("/locations", func(c *gin.Context) {
// Устанавливаем claims в контекст
c.Set("user_id", uuid.New())
c.Set("organization_id", orgID)
c.Set("email", "test@example.com")
c.Set("role", "admin")
handler.CreateLocation(c)
})
reqBody, _ := json.Marshal(createReq)
// Act
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/locations", 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.StorageLocation
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
assert.Equal(t, "New Warehouse", response.Name)
assert.Equal(t, "warehouse", response.Type)
mockLocationService.AssertExpectations(t)
}
// TestLocationHandler_CreateLocation_ValidationError тестирует ошибку валидации при создании локации
func TestLocationHandler_CreateLocation_ValidationError(t *testing.T) {
// Arrange
gin.SetMode(gin.TestMode)
mockLocationService := &MockLocationService{}
handler := handlers.NewLocationHandler(mockLocationService)
orgID := uuid.New()
// Невалидный запрос (пустое имя)
invalidReq := map[string]interface{}{
"name": "", // Пустое имя
"address": "456 Oak St",
"type": "warehouse",
}
router := gin.New()
router.POST("/locations", func(c *gin.Context) {
// Устанавливаем claims в контекст
c.Set("user_id", uuid.New())
c.Set("organization_id", orgID)
c.Set("email", "test@example.com")
c.Set("role", "admin")
handler.CreateLocation(c)
})
reqBody, _ := json.Marshal(invalidReq)
// Act
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/locations", bytes.NewBuffer(reqBody))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
// Assert
assert.Equal(t, http.StatusBadRequest, w.Code)
mockLocationService.AssertNotCalled(t, "CreateLocation")
}
// TestLocationHandler_GetLocation_Success тестирует успешное получение локации по ID
func TestLocationHandler_GetLocation_Success(t *testing.T) {
// Arrange
gin.SetMode(gin.TestMode)
mockLocationService := &MockLocationService{}
handler := handlers.NewLocationHandler(mockLocationService)
orgID := uuid.New()
locationID := uuid.New()
expectedLocation := &models.StorageLocation{
ID: locationID,
OrganizationID: orgID,
Name: "Test Warehouse",
Address: "123 Test St",
Type: "warehouse",
}
mockLocationService.On("GetLocation", mock.Anything, locationID, orgID).Return(expectedLocation, nil)
router := gin.New()
router.GET("/locations/:id", func(c *gin.Context) {
// Устанавливаем claims в контекст
c.Set("user_id", uuid.New())
c.Set("organization_id", orgID)
c.Set("email", "test@example.com")
c.Set("role", "admin")
handler.GetLocation(c)
})
// Act
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/locations/"+locationID.String(), nil)
router.ServeHTTP(w, req)
// Assert
assert.Equal(t, http.StatusOK, w.Code)
var response models.StorageLocation
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
assert.Equal(t, "Test Warehouse", response.Name)
assert.Equal(t, locationID, response.ID)
mockLocationService.AssertExpectations(t)
}
// TestLocationHandler_GetChildren_Success тестирует успешное получение дочерних локаций
func TestLocationHandler_GetChildren_Success(t *testing.T) {
// Arrange
gin.SetMode(gin.TestMode)
mockLocationService := &MockLocationService{}
handler := handlers.NewLocationHandler(mockLocationService)
orgID := uuid.New()
parentID := uuid.New()
expectedChildren := []*models.StorageLocation{
{
ID: uuid.New(),
OrganizationID: orgID,
ParentID: &parentID,
Name: "Shelf 1",
Address: "Warehouse A, Section 1",
Type: "shelf",
},
{
ID: uuid.New(),
OrganizationID: orgID,
ParentID: &parentID,
Name: "Shelf 2",
Address: "Warehouse A, Section 2",
Type: "shelf",
},
}
mockLocationService.On("GetChildren", mock.Anything, parentID, orgID).Return(expectedChildren, nil)
router := gin.New()
router.GET("/locations/:id/children", func(c *gin.Context) {
// Устанавливаем claims в контекст
c.Set("user_id", uuid.New())
c.Set("organization_id", orgID)
c.Set("email", "test@example.com")
c.Set("role", "admin")
handler.GetChildren(c)
})
// Act
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/locations/"+parentID.String()+"/children", nil)
router.ServeHTTP(w, req)
// Assert
assert.Equal(t, http.StatusOK, w.Code)
var response []*models.StorageLocation
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
assert.Len(t, response, 2)
assert.Equal(t, "Shelf 1", response[0].Name)
assert.Equal(t, "Shelf 2", response[1].Name)
mockLocationService.AssertExpectations(t)
}

View File

@@ -0,0 +1,319 @@
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"
)
// MockOperationsService мок для OperationsService
type MockOperationsService struct {
mock.Mock
}
func (m *MockOperationsService) PlaceItem(ctx context.Context, orgID uuid.UUID, req *models.PlaceItemRequest) (*models.ItemPlacement, error) {
args := m.Called(ctx, orgID, req)
return args.Get(0).(*models.ItemPlacement), args.Error(1)
}
func (m *MockOperationsService) MoveItem(ctx context.Context, placementID uuid.UUID, newLocationID uuid.UUID, orgID uuid.UUID) error {
args := m.Called(ctx, placementID, newLocationID, orgID)
return args.Error(0)
}
func (m *MockOperationsService) GetItemPlacements(ctx context.Context, itemID uuid.UUID, orgID uuid.UUID) ([]*models.ItemPlacement, error) {
args := m.Called(ctx, itemID, orgID)
return args.Get(0).([]*models.ItemPlacement), args.Error(1)
}
func (m *MockOperationsService) GetLocationPlacements(ctx context.Context, locationID uuid.UUID, orgID uuid.UUID) ([]*models.ItemPlacement, error) {
args := m.Called(ctx, locationID, orgID)
return args.Get(0).([]*models.ItemPlacement), args.Error(1)
}
func (m *MockOperationsService) UpdateQuantity(ctx context.Context, placementID uuid.UUID, quantity int, orgID uuid.UUID) error {
args := m.Called(ctx, placementID, quantity, orgID)
return args.Error(0)
}
func (m *MockOperationsService) DeletePlacement(ctx context.Context, placementID uuid.UUID, orgID uuid.UUID) error {
args := m.Called(ctx, placementID, orgID)
return args.Error(0)
}
func (m *MockOperationsService) Search(ctx context.Context, orgID uuid.UUID, req *models.SearchRequest) (*models.SearchResponse, error) {
args := m.Called(ctx, orgID, req)
return args.Get(0).(*models.SearchResponse), args.Error(1)
}
// TestNewOperationsHandler тестирует создание OperationsHandler
func TestNewOperationsHandler(t *testing.T) {
// Arrange
mockService := &MockOperationsService{}
// Act
handler := handlers.NewOperationsHandler(mockService)
// Assert
assert.NotNil(t, handler)
}
// TestOperationsHandler_PlaceItem_Success тестирует успешное размещение товара
func TestOperationsHandler_PlaceItem_Success(t *testing.T) {
// Arrange
gin.SetMode(gin.TestMode)
mockOperationsService := &MockOperationsService{}
handler := handlers.NewOperationsHandler(mockOperationsService)
orgID := uuid.New()
itemID := uuid.New()
locationID := uuid.New()
placementID := uuid.New()
placeReq := &models.PlaceItemRequest{
ItemID: itemID,
LocationID: locationID,
Quantity: 10,
}
expectedPlacement := &models.ItemPlacement{
ID: placementID,
OrganizationID: orgID,
ItemID: itemID,
LocationID: locationID,
Quantity: 10,
}
mockOperationsService.On("PlaceItem", mock.Anything, orgID, placeReq).Return(expectedPlacement, nil)
router := gin.New()
router.POST("/operations/place", func(c *gin.Context) {
setClaims(c, orgID)
handler.PlaceItem(c)
})
reqBody, _ := json.Marshal(placeReq)
// Act
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/operations/place", 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.ItemPlacement
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
assert.Equal(t, itemID, response.ItemID)
assert.Equal(t, locationID, response.LocationID)
assert.Equal(t, 10, response.Quantity)
mockOperationsService.AssertExpectations(t)
}
// TestOperationsHandler_PlaceItem_ValidationError тестирует ошибку валидации при размещении товара
func TestOperationsHandler_PlaceItem_ValidationError(t *testing.T) {
// Arrange
gin.SetMode(gin.TestMode)
mockOperationsService := &MockOperationsService{}
handler := handlers.NewOperationsHandler(mockOperationsService)
orgID := uuid.New()
// Невалидный запрос (отрицательное количество)
invalidReq := map[string]interface{}{
"item_id": uuid.New().String(),
"location_id": uuid.New().String(),
"quantity": -5, // Отрицательное количество
}
router := gin.New()
router.POST("/operations/place", func(c *gin.Context) {
setClaims(c, orgID)
handler.PlaceItem(c)
})
reqBody, _ := json.Marshal(invalidReq)
// Act
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/operations/place", bytes.NewBuffer(reqBody))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
// Assert
assert.Equal(t, http.StatusBadRequest, w.Code)
mockOperationsService.AssertNotCalled(t, "PlaceItem")
}
// TestOperationsHandler_MoveItem_Success тестирует успешное перемещение товара
func TestOperationsHandler_MoveItem_Success(t *testing.T) {
// Arrange
gin.SetMode(gin.TestMode)
mockOperationsService := &MockOperationsService{}
handler := handlers.NewOperationsHandler(mockOperationsService)
orgID := uuid.New()
placementID := uuid.New()
newLocationID := uuid.New()
moveReq := map[string]interface{}{
"new_location_id": newLocationID.String(),
}
mockOperationsService.On("MoveItem", mock.Anything, placementID, newLocationID, orgID).Return(nil)
router := gin.New()
router.PUT("/operations/move/:id", func(c *gin.Context) {
setClaims(c, orgID)
handler.MoveItem(c)
})
reqBody, _ := json.Marshal(moveReq)
// Act
w := httptest.NewRecorder()
req, _ := http.NewRequest("PUT", "/operations/move/"+placementID.String(), bytes.NewBuffer(reqBody))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
// Assert
assert.Equal(t, http.StatusOK, w.Code)
var response map[string]string
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
assert.Equal(t, "Item moved successfully", response["message"])
mockOperationsService.AssertExpectations(t)
}
// TestOperationsHandler_GetItemPlacements_Success тестирует успешное получение размещений товара
func TestOperationsHandler_GetItemPlacements_Success(t *testing.T) {
// Arrange
gin.SetMode(gin.TestMode)
mockOperationsService := &MockOperationsService{}
handler := handlers.NewOperationsHandler(mockOperationsService)
orgID := uuid.New()
itemID := uuid.New()
expectedPlacements := []*models.ItemPlacement{
{
ID: uuid.New(),
OrganizationID: orgID,
ItemID: itemID,
LocationID: uuid.New(),
Quantity: 5,
},
{
ID: uuid.New(),
OrganizationID: orgID,
ItemID: itemID,
LocationID: uuid.New(),
Quantity: 3,
},
}
mockOperationsService.On("GetItemPlacements", mock.Anything, itemID, orgID).Return(expectedPlacements, nil)
router := gin.New()
router.GET("/operations/items/:item_id/placements", func(c *gin.Context) {
setClaims(c, orgID)
handler.GetItemPlacements(c)
})
// Act
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/operations/items/"+itemID.String()+"/placements", nil)
router.ServeHTTP(w, req)
// Assert
assert.Equal(t, http.StatusOK, w.Code)
var response []*models.ItemPlacement
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
assert.Len(t, response, 2)
assert.Equal(t, itemID, response[0].ItemID)
assert.Equal(t, itemID, response[1].ItemID)
mockOperationsService.AssertExpectations(t)
}
// TestOperationsHandler_Search_Success тестирует успешный поиск
func TestOperationsHandler_Search_Success(t *testing.T) {
// Arrange
gin.SetMode(gin.TestMode)
mockOperationsService := &MockOperationsService{}
handler := handlers.NewOperationsHandler(mockOperationsService)
orgID := uuid.New()
expectedResponse := &models.SearchResponse{
Items: []*models.ItemWithLocation{
{
Item: models.Item{
ID: uuid.New(),
OrganizationID: orgID,
Name: "Test Item",
Description: "Test Description",
Category: "electronics",
},
Location: models.StorageLocation{
ID: uuid.New(),
OrganizationID: orgID,
Name: "Test Location",
Address: "Test Address",
Type: "warehouse",
},
Quantity: 5,
},
},
TotalCount: 1,
}
mockOperationsService.On("Search", mock.Anything, orgID, mock.AnythingOfType("*models.SearchRequest")).Return(expectedResponse, nil)
router := gin.New()
router.GET("/operations/search", func(c *gin.Context) {
setClaims(c, orgID)
handler.Search(c)
})
// Act
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/operations/search?q=test", nil)
router.ServeHTTP(w, req)
// Assert
assert.Equal(t, http.StatusOK, w.Code)
var response models.SearchResponse
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
assert.Len(t, response.Items, 1)
assert.Equal(t, "Test Item", response.Items[0].Item.Name)
assert.Equal(t, 1, response.TotalCount)
mockOperationsService.AssertExpectations(t)
}

View File

@@ -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)
}

View File

@@ -131,9 +131,7 @@ func (s *Server) setupRoutes() {
protected.PUT("/operations/placements/:id/quantity", s.operationsHandler.UpdateQuantity) protected.PUT("/operations/placements/:id/quantity", s.operationsHandler.UpdateQuantity)
protected.DELETE("/operations/placements/:id", s.operationsHandler.DeletePlacement) protected.DELETE("/operations/placements/:id", s.operationsHandler.DeletePlacement)
// Templates
protected.GET("/templates", s.getTemplates)
protected.POST("/templates/:id/apply", s.applyTemplate)
} }
} }
} }
@@ -154,13 +152,7 @@ func (s *Server) updateOrganization(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{"error": "Not implemented yet"}) c.JSON(http.StatusNotImplemented, gin.H{"error": "Not implemented yet"})
} }
func (s *Server) getTemplates(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{"error": "Not implemented yet"})
}
func (s *Server) applyTemplate(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{"error": "Not implemented yet"})
}
func (s *Server) Start() error { func (s *Server) Start() error {
return s.router.Run(s.config.Server.Host + ":" + s.config.Server.Port) return s.router.Run(s.config.Server.Host + ":" + s.config.Server.Port)

View File

@@ -1,4 +1,4 @@
package examples package auth_test
import ( import (
"testing" "testing"

View File

@@ -1,4 +1,4 @@
package examples package repository_test
import ( import (
"context" "context"
@@ -299,6 +299,78 @@ func TestItemRepository_Create(t *testing.T) {
assert.NoError(t, mock.ExpectationsWereMet()) 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 тестирует размещение товара // TestOperationsRepository_PlaceItem тестирует размещение товара
func TestOperationsRepository_PlaceItem(t *testing.T) { func TestOperationsRepository_PlaceItem(t *testing.T) {
// Arrange // Arrange
@@ -371,3 +443,36 @@ func TestOperationsRepository_Search(t *testing.T) {
assert.Len(t, results, 1) assert.Len(t, results, 1)
assert.NoError(t, mock.ExpectationsWereMet()) 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())
}

View File

@@ -0,0 +1,245 @@
package service_test
import (
"context"
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"erp-mvp/core-service/internal/models"
"erp-mvp/core-service/internal/service"
)
// MockItemRepository мок для ItemRepository
type MockItemRepository struct {
mock.Mock
}
func (m *MockItemRepository) Create(ctx context.Context, item *models.Item) error {
args := m.Called(ctx, item)
return args.Error(0)
}
func (m *MockItemRepository) GetByID(ctx context.Context, id uuid.UUID, orgID uuid.UUID) (*models.Item, error) {
args := m.Called(ctx, id, orgID)
return args.Get(0).(*models.Item), args.Error(1)
}
func (m *MockItemRepository) GetByOrganization(ctx context.Context, orgID uuid.UUID) ([]*models.Item, error) {
args := m.Called(ctx, orgID)
return args.Get(0).([]*models.Item), args.Error(1)
}
func (m *MockItemRepository) Update(ctx context.Context, item *models.Item) error {
args := m.Called(ctx, item)
return args.Error(0)
}
func (m *MockItemRepository) Delete(ctx context.Context, id uuid.UUID, orgID uuid.UUID) error {
args := m.Called(ctx, id, orgID)
return args.Error(0)
}
func (m *MockItemRepository) Search(ctx context.Context, orgID uuid.UUID, query string, category string) ([]*models.Item, error) {
args := m.Called(ctx, orgID, query, category)
return args.Get(0).([]*models.Item), args.Error(1)
}
// TestNewItemService тестирует создание ItemService
func TestNewItemService(t *testing.T) {
// Arrange
mockRepo := &MockItemRepository{}
// Act
itemService := service.NewItemService(mockRepo)
// Assert
assert.NotNil(t, itemService)
}
// TestItemService_GetItems_Success тестирует успешное получение товаров
func TestItemService_GetItems_Success(t *testing.T) {
// Arrange
mockRepo := &MockItemRepository{}
itemService := service.NewItemService(mockRepo)
orgID := uuid.New()
expectedItems := []*models.Item{
{
ID: uuid.New(),
OrganizationID: orgID,
Name: "Item 1",
Description: "Description 1",
Category: "electronics",
},
{
ID: uuid.New(),
OrganizationID: orgID,
Name: "Item 2",
Description: "Description 2",
Category: "clothing",
},
}
mockRepo.On("GetByOrganization", mock.Anything, orgID).Return(expectedItems, nil)
// Act
items, err := itemService.GetItems(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)
mockRepo.AssertExpectations(t)
}
// TestItemService_CreateItem_Success тестирует успешное создание товара
func TestItemService_CreateItem_Success(t *testing.T) {
// Arrange
mockRepo := &MockItemRepository{}
itemService := service.NewItemService(mockRepo)
orgID := uuid.New()
req := &models.CreateItemRequest{
Name: "New Item",
Description: "New Description",
Category: "electronics",
}
mockRepo.On("Create", mock.Anything, mock.AnythingOfType("*models.Item")).Return(nil)
// Act
item, err := itemService.CreateItem(context.Background(), orgID, req)
// Assert
assert.NoError(t, err)
assert.NotNil(t, item)
assert.Equal(t, "New Item", item.Name)
assert.Equal(t, "New Description", item.Description)
assert.Equal(t, "electronics", item.Category)
assert.Equal(t, orgID, item.OrganizationID)
mockRepo.AssertExpectations(t)
}
// TestItemService_GetItem_Success тестирует успешное получение товара по ID
func TestItemService_GetItem_Success(t *testing.T) {
// Arrange
mockRepo := &MockItemRepository{}
itemService := service.NewItemService(mockRepo)
orgID := uuid.New()
itemID := uuid.New()
expectedItem := &models.Item{
ID: itemID,
OrganizationID: orgID,
Name: "Test Item",
Description: "Test Description",
Category: "electronics",
}
mockRepo.On("GetByID", mock.Anything, itemID, orgID).Return(expectedItem, nil)
// Act
item, err := itemService.GetItem(context.Background(), itemID, orgID)
// Assert
assert.NoError(t, err)
assert.NotNil(t, item)
assert.Equal(t, "Test Item", item.Name)
assert.Equal(t, itemID, item.ID)
mockRepo.AssertExpectations(t)
}
// TestItemService_SearchItems_Success тестирует успешный поиск товаров
func TestItemService_SearchItems_Success(t *testing.T) {
// Arrange
mockRepo := &MockItemRepository{}
itemService := service.NewItemService(mockRepo)
orgID := uuid.New()
expectedItems := []*models.Item{
{
ID: uuid.New(),
OrganizationID: orgID,
Name: "Search Result",
Description: "Found item",
Category: "electronics",
},
}
mockRepo.On("Search", mock.Anything, orgID, "search", "electronics").Return(expectedItems, nil)
// Act
items, err := itemService.SearchItems(context.Background(), orgID, "search", "electronics")
// Assert
assert.NoError(t, err)
assert.Len(t, items, 1)
assert.Equal(t, "Search Result", items[0].Name)
mockRepo.AssertExpectations(t)
}
// TestItemService_UpdateItem_Success тестирует успешное обновление товара
func TestItemService_UpdateItem_Success(t *testing.T) {
// Arrange
mockRepo := &MockItemRepository{}
itemService := service.NewItemService(mockRepo)
orgID := uuid.New()
itemID := uuid.New()
req := &models.CreateItemRequest{
Name: "Updated Item",
Description: "Updated Description",
Category: "clothing",
}
existingItem := &models.Item{
ID: itemID,
OrganizationID: orgID,
Name: "Old Item",
Description: "Old Description",
Category: "electronics",
}
mockRepo.On("GetByID", mock.Anything, itemID, orgID).Return(existingItem, nil)
mockRepo.On("Update", mock.Anything, mock.AnythingOfType("*models.Item")).Return(nil)
// Act
item, err := itemService.UpdateItem(context.Background(), itemID, orgID, req)
// Assert
assert.NoError(t, err)
assert.NotNil(t, item)
assert.Equal(t, "Updated Item", item.Name)
assert.Equal(t, "Updated Description", item.Description)
assert.Equal(t, "clothing", item.Category)
mockRepo.AssertExpectations(t)
}
// TestItemService_DeleteItem_Success тестирует успешное удаление товара
func TestItemService_DeleteItem_Success(t *testing.T) {
// Arrange
mockRepo := &MockItemRepository{}
itemService := service.NewItemService(mockRepo)
orgID := uuid.New()
itemID := uuid.New()
mockRepo.On("Delete", mock.Anything, itemID, orgID).Return(nil)
// Act
err := itemService.DeleteItem(context.Background(), itemID, orgID)
// Assert
assert.NoError(t, err)
mockRepo.AssertExpectations(t)
}

103
core-service/scripts/coverage.sh Executable file
View File

@@ -0,0 +1,103 @@
#!/bin/bash
# Скрипт для анализа покрытия тестами
# Использование: ./scripts/coverage.sh [--html] [--threshold=80]
set -e
# Параметры по умолчанию
GENERATE_HTML=false
COVERAGE_THRESHOLD=80
# Парсим аргументы
while [[ $# -gt 0 ]]; do
case $1 in
--html)
GENERATE_HTML=true
shift
;;
--threshold=*)
COVERAGE_THRESHOLD="${1#*=}"
shift
;;
*)
echo "Неизвестный параметр: $1"
echo "Использование: $0 [--html] [--threshold=80]"
exit 1
;;
esac
done
echo "🧪 Анализ покрытия тестами..."
echo ""
# Запускаем тесты с покрытием
go test ./... -coverprofile=coverage.out
# Получаем общую статистику покрытия
TOTAL_COVERAGE=$(go tool cover -func=coverage.out | tail -1 | awk '{print $3}' | sed 's/%//')
echo ""
echo "📊 Общая статистика покрытия:"
echo "┌─────────────────────────────────────────────────────────────┐"
echo "│ Пакет │ Покрытие │ Статус │"
echo "├─────────────────────────────────────────────────────────────┤"
# Анализируем покрытие по пакетам
go test ./... -cover | while read -r line; do
if [[ $line =~ coverage:\ ([0-9.]+)% ]]; then
PACKAGE=$(echo "$line" | awk '{print $1}')
COVERAGE=$(echo "$line" | grep -o '[0-9.]*%' | head -1)
COVERAGE_NUM=$(echo "$COVERAGE" | sed 's/%//')
if (( $(echo "$COVERAGE_NUM >= $COVERAGE_THRESHOLD" | bc -l) )); then
STATUS="✅ Хорошо"
elif (( $(echo "$COVERAGE_NUM > 0" | bc -l) )); then
STATUS="⚠️ Низкое"
else
STATUS="❌ Нет тестов"
fi
printf "│ %-25s │ %8s │ %-20s │\n" "$PACKAGE" "$COVERAGE" "$STATUS"
fi
done
echo "└─────────────────────────────────────────────────────────────┘"
echo ""
# Проверяем общее покрытие
if (( $(echo "$TOTAL_COVERAGE >= $COVERAGE_THRESHOLD" | bc -l) )); then
echo "🎉 Общее покрытие: ${TOTAL_COVERAGE}% (>= ${COVERAGE_THRESHOLD}%)"
COVERAGE_STATUS=0
else
echo "⚠️ Общее покрытие: ${TOTAL_COVERAGE}% (< ${COVERAGE_THRESHOLD}%)"
echo "💡 Рекомендуется увеличить покрытие тестами"
COVERAGE_STATUS=1
fi
# Генерируем HTML отчет если запрошено
if [ "$GENERATE_HTML" = true ]; then
echo ""
echo "📄 Генерация HTML отчета..."
go tool cover -html=coverage.out -o coverage.html
echo "✅ HTML отчет сохранен в coverage.html"
echo "🌐 Откройте coverage.html в браузере для детального просмотра"
fi
# Показываем функции с низким покрытием
echo ""
echo "🔍 Функции с низким покрытием (< 50%):"
go tool cover -func=coverage.out | grep -E "0\.0%|^[0-9]+\.[0-9]+%" | head -10
# Очищаем временные файлы
rm -f coverage.out
echo ""
echo "📈 Рекомендации по улучшению покрытия:"
echo "1. Добавьте unit тесты для handlers (сейчас 0% покрытия)"
echo "2. Добавьте unit тесты для service layer (сейчас 0% покрытия)"
echo "3. Добавьте unit тесты для middleware (сейчас 0% покрытия)"
echo "4. Добавьте unit тесты для config и database (сейчас 0% покрытия)"
echo "5. Улучшите покрытие repository layer (сейчас 24.5%)"
exit $COVERAGE_STATUS

View File

@@ -1,4 +1,4 @@
package examples package tests
import ( import (
"bytes" "bytes"