Compare commits
25 Commits
94bf3a6b86
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 1b883a6d69 | |||
| 3552110f4e | |||
| fcbd6dc2a7 | |||
| 0524a52be1 | |||
| 76df5d6abe | |||
| c8224f072a | |||
| 225635ed4b | |||
| 282613edb9 | |||
| 9d242d096d | |||
| 3bb9074a18 | |||
| 5c0052398e | |||
| a707c138fe | |||
| 71b45b0b60 | |||
| e4e56c577f | |||
| 3783283f92 | |||
| e251b73f41 | |||
| b19f6df20c | |||
| 508b57bf2d | |||
| c9d1a75f56 | |||
| e65863a782 | |||
| e490e49c93 | |||
| 79c3eed25b | |||
| 6f93a1f9bc | |||
| 91d651184a | |||
| 582353ecf1 |
151
core-service/COVERAGE.md
Normal file
151
core-service/COVERAGE.md
Normal 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%+
|
||||||
|
- **Качество кода**: Значительно улучшится
|
||||||
|
- **Стабильность**: Снизится количество багов
|
||||||
1
core-service/README.md
Normal file
1
core-service/README.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Test new structure
|
||||||
2998
core-service/coverage.html
Normal file
2998
core-service/coverage.html
Normal file
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,7 @@ module erp-mvp/core-service
|
|||||||
go 1.21
|
go 1.21
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/DATA-DOG/go-sqlmock v1.5.2
|
||||||
github.com/gin-gonic/gin v1.10.1
|
github.com/gin-gonic/gin v1.10.1
|
||||||
github.com/go-playground/validator/v10 v10.20.0
|
github.com/go-playground/validator/v10 v10.20.0
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0
|
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||||
@@ -10,6 +11,7 @@ require (
|
|||||||
github.com/joho/godotenv v1.4.0
|
github.com/joho/godotenv v1.4.0
|
||||||
github.com/lib/pq v1.10.9
|
github.com/lib/pq v1.10.9
|
||||||
github.com/sirupsen/logrus v1.9.3
|
github.com/sirupsen/logrus v1.9.3
|
||||||
|
github.com/stretchr/testify v1.11.1
|
||||||
golang.org/x/crypto v0.23.0
|
golang.org/x/crypto v0.23.0
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -18,6 +20,7 @@ require (
|
|||||||
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
@@ -30,6 +33,8 @@ require (
|
|||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/stretchr/objx v0.5.2 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||||
golang.org/x/arch v0.8.0 // indirect
|
golang.org/x/arch v0.8.0 // indirect
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
|
||||||
|
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
|
||||||
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
|
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
|
||||||
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
||||||
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
||||||
@@ -36,6 +38,7 @@ github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
|
|||||||
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
|
github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||||
@@ -60,6 +63,7 @@ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs
|
|||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
@@ -67,8 +71,9 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
|||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||||
|
|||||||
216
core-service/internal/api/handlers/auth_test.go
Normal file
216
core-service/internal/api/handlers/auth_test.go
Normal 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")
|
||||||
|
}
|
||||||
308
core-service/internal/api/handlers/items_test.go
Normal file
308
core-service/internal/api/handlers/items_test.go
Normal 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)
|
||||||
|
}
|
||||||
326
core-service/internal/api/handlers/locations_test.go
Normal file
326
core-service/internal/api/handlers/locations_test.go
Normal 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)
|
||||||
|
}
|
||||||
319
core-service/internal/api/handlers/operations_test.go
Normal file
319
core-service/internal/api/handlers/operations_test.go
Normal 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)
|
||||||
|
}
|
||||||
161
core-service/internal/api/middleware/auth_test.go
Normal file
161
core-service/internal/api/middleware/auth_test.go
Normal 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)
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
183
core-service/internal/auth/auth_test.go
Normal file
183
core-service/internal/auth/auth_test.go
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
package auth_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"erp-mvp/core-service/internal/auth"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestJWTService_GenerateToken тестирует генерацию JWT токенов
|
||||||
|
func TestJWTService_GenerateToken(t *testing.T) {
|
||||||
|
// Arrange
|
||||||
|
secret := "test_secret_key"
|
||||||
|
ttl := 24 * time.Hour
|
||||||
|
jwtService := auth.NewJWTService(secret, ttl)
|
||||||
|
userID := uuid.New()
|
||||||
|
orgID := uuid.New()
|
||||||
|
email := "test@example.com"
|
||||||
|
role := "admin"
|
||||||
|
|
||||||
|
// Act
|
||||||
|
token, err := jwtService.GenerateToken(userID, orgID, email, role)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, token)
|
||||||
|
|
||||||
|
// Проверяем, что токен можно декодировать
|
||||||
|
parsedToken, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) {
|
||||||
|
return []byte(secret), nil
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, parsedToken.Valid)
|
||||||
|
|
||||||
|
// Проверяем claims
|
||||||
|
claims, ok := parsedToken.Claims.(jwt.MapClaims)
|
||||||
|
require.True(t, ok)
|
||||||
|
assert.Equal(t, userID.String(), claims["user_id"])
|
||||||
|
assert.Equal(t, orgID.String(), claims["organization_id"])
|
||||||
|
assert.Equal(t, email, claims["email"])
|
||||||
|
assert.Equal(t, role, claims["role"])
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestJWTService_ValidateToken тестирует валидацию JWT токенов
|
||||||
|
func TestJWTService_ValidateToken(t *testing.T) {
|
||||||
|
// Arrange
|
||||||
|
secret := "test_secret_key"
|
||||||
|
ttl := 24 * time.Hour
|
||||||
|
jwtService := auth.NewJWTService(secret, ttl)
|
||||||
|
|
||||||
|
// Создаем валидный токен для тестирования
|
||||||
|
userID := uuid.New()
|
||||||
|
orgID := uuid.New()
|
||||||
|
validToken, err := jwtService.GenerateToken(userID, orgID, "test@example.com", "admin")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
secret string
|
||||||
|
token string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid token",
|
||||||
|
secret: secret,
|
||||||
|
token: validToken,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid signature",
|
||||||
|
secret: "wrong_secret",
|
||||||
|
token: validToken,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid token format",
|
||||||
|
secret: secret,
|
||||||
|
token: "invalid_token_format",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// Arrange
|
||||||
|
testJWTService := auth.NewJWTService(tt.secret, ttl)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
claims, err := testJWTService.ValidateToken(tt.token)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
if tt.wantErr {
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, claims)
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, claims)
|
||||||
|
assert.Equal(t, userID, claims.UserID)
|
||||||
|
assert.Equal(t, orgID, claims.OrganizationID)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestPasswordHashing тестирует хеширование и проверку паролей
|
||||||
|
func TestPasswordHashing(t *testing.T) {
|
||||||
|
// Arrange
|
||||||
|
password := "mySecurePassword123"
|
||||||
|
|
||||||
|
// Act - хешируем пароль
|
||||||
|
hashedPassword, err := auth.HashPassword(password)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, hashedPassword)
|
||||||
|
assert.NotEqual(t, password, hashedPassword)
|
||||||
|
|
||||||
|
// Act - проверяем правильный пароль
|
||||||
|
isValid := auth.CheckPassword(password, hashedPassword)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assert.True(t, isValid)
|
||||||
|
|
||||||
|
// Act - проверяем неправильный пароль
|
||||||
|
isValid = auth.CheckPassword("wrongPassword", hashedPassword)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assert.False(t, isValid)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestPasswordHashing_EmptyPassword тестирует обработку пустого пароля
|
||||||
|
func TestPasswordHashing_EmptyPassword(t *testing.T) {
|
||||||
|
// Arrange
|
||||||
|
password := ""
|
||||||
|
|
||||||
|
// Act
|
||||||
|
hashedPassword, err := auth.HashPassword(password)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, hashedPassword)
|
||||||
|
|
||||||
|
// Проверяем, что пустой пароль работает
|
||||||
|
isValid := auth.CheckPassword(password, hashedPassword)
|
||||||
|
assert.True(t, isValid)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BenchmarkPasswordHashing тестирует производительность хеширования
|
||||||
|
func BenchmarkPasswordHashing(b *testing.B) {
|
||||||
|
password := "benchmarkPassword123"
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_, err := auth.HashPassword(password)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BenchmarkJWTGeneration тестирует производительность генерации JWT
|
||||||
|
func BenchmarkJWTGeneration(b *testing.B) {
|
||||||
|
secret := "benchmark_secret_key"
|
||||||
|
ttl := 24 * time.Hour
|
||||||
|
jwtService := auth.NewJWTService(secret, ttl)
|
||||||
|
userID := uuid.New()
|
||||||
|
orgID := uuid.New()
|
||||||
|
email := "benchmark@example.com"
|
||||||
|
role := "admin"
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_, err := jwtService.GenerateToken(userID, orgID, email, role)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
478
core-service/internal/repository/repository_test.go
Normal file
478
core-service/internal/repository/repository_test.go
Normal file
@@ -0,0 +1,478 @@
|
|||||||
|
package repository_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/DATA-DOG/go-sqlmock"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"erp-mvp/core-service/internal/models"
|
||||||
|
"erp-mvp/core-service/internal/repository"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestOrganizationRepository_Create тестирует создание организации
|
||||||
|
func TestOrganizationRepository_Create(t *testing.T) {
|
||||||
|
// Arrange
|
||||||
|
db, mock, err := sqlmock.New()
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
repo := repository.NewOrganizationRepository(db)
|
||||||
|
|
||||||
|
orgID := uuid.New()
|
||||||
|
org := &models.Organization{
|
||||||
|
ID: orgID,
|
||||||
|
Name: "Test Organization",
|
||||||
|
Type: "workshop",
|
||||||
|
Settings: models.JSON{
|
||||||
|
"created_at": 1234567890,
|
||||||
|
},
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ожидаем SQL запрос
|
||||||
|
mock.ExpectExec("INSERT INTO organizations").
|
||||||
|
WithArgs(orgID, org.Name, org.Type, sqlmock.AnyArg(), sqlmock.AnyArg()).
|
||||||
|
WillReturnResult(sqlmock.NewResult(1, 1))
|
||||||
|
|
||||||
|
// Act
|
||||||
|
err = repo.Create(context.Background(), org)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NoError(t, mock.ExpectationsWereMet())
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestOrganizationRepository_GetByID тестирует получение организации по ID
|
||||||
|
func TestOrganizationRepository_GetByID(t *testing.T) {
|
||||||
|
// Arrange
|
||||||
|
db, mock, err := sqlmock.New()
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
repo := repository.NewOrganizationRepository(db)
|
||||||
|
|
||||||
|
orgID := uuid.New()
|
||||||
|
expectedOrg := &models.Organization{
|
||||||
|
ID: orgID,
|
||||||
|
Name: "Test Organization",
|
||||||
|
Type: "workshop",
|
||||||
|
Settings: models.JSON{
|
||||||
|
"created_at": 1234567890,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ожидаем SQL запрос
|
||||||
|
rows := sqlmock.NewRows([]string{"id", "name", "type", "settings", "created_at"}).
|
||||||
|
AddRow(orgID, expectedOrg.Name, expectedOrg.Type, `{"created_at":1234567890}`, time.Now())
|
||||||
|
|
||||||
|
mock.ExpectQuery("SELECT (.+) FROM organizations").
|
||||||
|
WithArgs(orgID).
|
||||||
|
WillReturnRows(rows)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
org, err := repo.GetByID(context.Background(), orgID)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, org)
|
||||||
|
assert.Equal(t, expectedOrg.ID, org.ID)
|
||||||
|
assert.Equal(t, expectedOrg.Name, org.Name)
|
||||||
|
assert.Equal(t, expectedOrg.Type, org.Type)
|
||||||
|
assert.NoError(t, mock.ExpectationsWereMet())
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestOrganizationRepository_GetByID_NotFound тестирует случай, когда организация не найдена
|
||||||
|
func TestOrganizationRepository_GetByID_NotFound(t *testing.T) {
|
||||||
|
// Arrange
|
||||||
|
db, mock, err := sqlmock.New()
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
repo := repository.NewOrganizationRepository(db)
|
||||||
|
orgID := uuid.New()
|
||||||
|
|
||||||
|
// Ожидаем SQL запрос с пустым результатом
|
||||||
|
mock.ExpectQuery("SELECT (.+) FROM organizations").
|
||||||
|
WithArgs(orgID).
|
||||||
|
WillReturnError(sql.ErrNoRows)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
org, err := repo.GetByID(context.Background(), orgID)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, org)
|
||||||
|
assert.Equal(t, "organization not found", err.Error())
|
||||||
|
assert.NoError(t, mock.ExpectationsWereMet())
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestUserRepository_Create тестирует создание пользователя
|
||||||
|
func TestUserRepository_Create(t *testing.T) {
|
||||||
|
// Arrange
|
||||||
|
db, mock, err := sqlmock.New()
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
repo := repository.NewUserRepository(db)
|
||||||
|
|
||||||
|
userID := uuid.New()
|
||||||
|
orgID := uuid.New()
|
||||||
|
user := &models.User{
|
||||||
|
ID: userID,
|
||||||
|
OrganizationID: orgID,
|
||||||
|
Email: "test@example.com",
|
||||||
|
PasswordHash: "hashed_password",
|
||||||
|
Role: "admin",
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
password := "hashed_password"
|
||||||
|
|
||||||
|
// Ожидаем SQL запрос
|
||||||
|
mock.ExpectExec("INSERT INTO users").
|
||||||
|
WithArgs(userID, orgID, user.Email, password, user.Role, sqlmock.AnyArg()).
|
||||||
|
WillReturnResult(sqlmock.NewResult(1, 1))
|
||||||
|
|
||||||
|
// Act
|
||||||
|
err = repo.Create(context.Background(), user, password)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NoError(t, mock.ExpectationsWereMet())
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestUserRepository_GetByEmail тестирует получение пользователя по email
|
||||||
|
func TestUserRepository_GetByEmail(t *testing.T) {
|
||||||
|
// Arrange
|
||||||
|
db, mock, err := sqlmock.New()
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
repo := repository.NewUserRepository(db)
|
||||||
|
|
||||||
|
userID := uuid.New()
|
||||||
|
orgID := uuid.New()
|
||||||
|
email := "test@example.com"
|
||||||
|
expectedUser := &models.User{
|
||||||
|
ID: userID,
|
||||||
|
OrganizationID: orgID,
|
||||||
|
Email: email,
|
||||||
|
PasswordHash: "hashed_password",
|
||||||
|
Role: "admin",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ожидаем SQL запрос
|
||||||
|
rows := sqlmock.NewRows([]string{"id", "organization_id", "email", "password_hash", "role", "created_at"}).
|
||||||
|
AddRow(userID, orgID, email, "hashed_password", "admin", time.Now())
|
||||||
|
|
||||||
|
mock.ExpectQuery("SELECT (.+) FROM users").
|
||||||
|
WithArgs(email).
|
||||||
|
WillReturnRows(rows)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
user, err := repo.GetByEmail(context.Background(), email)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, user)
|
||||||
|
assert.Equal(t, expectedUser.ID, user.ID)
|
||||||
|
assert.Equal(t, expectedUser.Email, user.Email)
|
||||||
|
assert.Equal(t, expectedUser.Role, user.Role)
|
||||||
|
assert.NoError(t, mock.ExpectationsWereMet())
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestLocationRepository_Create тестирует создание места хранения
|
||||||
|
func TestLocationRepository_Create(t *testing.T) {
|
||||||
|
// Arrange
|
||||||
|
db, mock, err := sqlmock.New()
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
repo := repository.NewLocationRepository(db)
|
||||||
|
|
||||||
|
locationID := uuid.New()
|
||||||
|
orgID := uuid.New()
|
||||||
|
location := &models.StorageLocation{
|
||||||
|
ID: locationID,
|
||||||
|
OrganizationID: orgID,
|
||||||
|
Name: "Test Location",
|
||||||
|
Address: "Test Address",
|
||||||
|
Type: "warehouse",
|
||||||
|
Coordinates: models.JSON{
|
||||||
|
"lat": 55.7558,
|
||||||
|
"lng": 37.6176,
|
||||||
|
},
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ожидаем SQL запрос
|
||||||
|
mock.ExpectExec("INSERT INTO storage_locations").
|
||||||
|
WithArgs(locationID, orgID, sqlmock.AnyArg(), location.Name, location.Address, location.Type, sqlmock.AnyArg(), sqlmock.AnyArg()).
|
||||||
|
WillReturnResult(sqlmock.NewResult(1, 1))
|
||||||
|
|
||||||
|
// Act
|
||||||
|
err = repo.Create(context.Background(), location)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NoError(t, mock.ExpectationsWereMet())
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestLocationRepository_GetByID тестирует получение места хранения по ID
|
||||||
|
func TestLocationRepository_GetByID(t *testing.T) {
|
||||||
|
// Arrange
|
||||||
|
db, mock, err := sqlmock.New()
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
repo := repository.NewLocationRepository(db)
|
||||||
|
|
||||||
|
locationID := uuid.New()
|
||||||
|
orgID := uuid.New()
|
||||||
|
expectedLocation := &models.StorageLocation{
|
||||||
|
ID: locationID,
|
||||||
|
OrganizationID: orgID,
|
||||||
|
Name: "Test Location",
|
||||||
|
Address: "Test Address",
|
||||||
|
Type: "warehouse",
|
||||||
|
Coordinates: models.JSON{
|
||||||
|
"lat": 55.7558,
|
||||||
|
"lng": 37.6176,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ожидаем SQL запрос
|
||||||
|
rows := sqlmock.NewRows([]string{"id", "organization_id", "parent_id", "name", "address", "type", "coordinates", "created_at"}).
|
||||||
|
AddRow(locationID, orgID, nil, expectedLocation.Name, expectedLocation.Address, expectedLocation.Type, `{"lat":55.7558,"lng":37.6176}`, time.Now())
|
||||||
|
|
||||||
|
mock.ExpectQuery("SELECT (.+) FROM storage_locations").
|
||||||
|
WithArgs(locationID, orgID).
|
||||||
|
WillReturnRows(rows)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
location, err := repo.GetByID(context.Background(), locationID, orgID)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, location)
|
||||||
|
assert.Equal(t, expectedLocation.ID, location.ID)
|
||||||
|
assert.Equal(t, expectedLocation.Name, location.Name)
|
||||||
|
assert.Equal(t, expectedLocation.Type, location.Type)
|
||||||
|
assert.NoError(t, mock.ExpectationsWereMet())
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestItemRepository_Create тестирует создание товара
|
||||||
|
func TestItemRepository_Create(t *testing.T) {
|
||||||
|
// Arrange
|
||||||
|
db, mock, err := sqlmock.New()
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
repo := repository.NewItemRepository(db)
|
||||||
|
|
||||||
|
itemID := uuid.New()
|
||||||
|
orgID := uuid.New()
|
||||||
|
item := &models.Item{
|
||||||
|
ID: itemID,
|
||||||
|
OrganizationID: orgID,
|
||||||
|
Name: "Test Item",
|
||||||
|
Description: "Test Description",
|
||||||
|
Category: "Test Category",
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ожидаем SQL запрос
|
||||||
|
mock.ExpectExec("INSERT INTO items").
|
||||||
|
WithArgs(itemID, orgID, item.Name, item.Description, item.Category, sqlmock.AnyArg()).
|
||||||
|
WillReturnResult(sqlmock.NewResult(1, 1))
|
||||||
|
|
||||||
|
// Act
|
||||||
|
err = repo.Create(context.Background(), item)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assert.NoError(t, err)
|
||||||
|
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
|
||||||
|
db, mock, err := sqlmock.New()
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
repo := repository.NewOperationsRepository(db)
|
||||||
|
|
||||||
|
placementID := uuid.New()
|
||||||
|
orgID := uuid.New()
|
||||||
|
itemID := uuid.New()
|
||||||
|
locationID := uuid.New()
|
||||||
|
placement := &models.ItemPlacement{
|
||||||
|
ID: placementID,
|
||||||
|
OrganizationID: orgID,
|
||||||
|
ItemID: itemID,
|
||||||
|
LocationID: locationID,
|
||||||
|
Quantity: 5,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ожидаем SQL запрос
|
||||||
|
mock.ExpectExec("INSERT INTO item_placements").
|
||||||
|
WithArgs(placementID, orgID, itemID, locationID, placement.Quantity, sqlmock.AnyArg()).
|
||||||
|
WillReturnResult(sqlmock.NewResult(1, 1))
|
||||||
|
|
||||||
|
// Act
|
||||||
|
err = repo.PlaceItem(context.Background(), placement)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NoError(t, mock.ExpectationsWereMet())
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestOperationsRepository_Search тестирует поиск товаров с местами размещения
|
||||||
|
func TestOperationsRepository_Search(t *testing.T) {
|
||||||
|
// Arrange
|
||||||
|
db, mock, err := sqlmock.New()
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
repo := repository.NewOperationsRepository(db)
|
||||||
|
|
||||||
|
orgID := uuid.New()
|
||||||
|
itemID := uuid.New()
|
||||||
|
locationID := uuid.New()
|
||||||
|
|
||||||
|
// Ожидаем SQL запрос с JOIN
|
||||||
|
rows := sqlmock.NewRows([]string{
|
||||||
|
"i.id", "i.organization_id", "i.name", "i.description", "i.category", "i.created_at",
|
||||||
|
"sl.id", "sl.organization_id", "sl.parent_id", "sl.name", "sl.address", "sl.type", "sl.coordinates", "sl.created_at",
|
||||||
|
"ip.quantity",
|
||||||
|
}).AddRow(
|
||||||
|
itemID, orgID, "Test Item", "Test Description", "Test Category", time.Now(),
|
||||||
|
locationID, orgID, nil, "Test Location", "Test Address", "warehouse", `{"lat":55.7558,"lng":37.6176}`, time.Now(),
|
||||||
|
5,
|
||||||
|
)
|
||||||
|
|
||||||
|
mock.ExpectQuery("SELECT (.+) FROM items i").
|
||||||
|
WithArgs(orgID, "%test%").
|
||||||
|
WillReturnRows(rows)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
results, err := repo.Search(context.Background(), orgID, "test", "", "")
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, results)
|
||||||
|
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())
|
||||||
|
}
|
||||||
245
core-service/internal/service/item_service_test.go
Normal file
245
core-service/internal/service/item_service_test.go
Normal 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)
|
||||||
|
}
|
||||||
Binary file not shown.
103
core-service/scripts/coverage.sh
Executable file
103
core-service/scripts/coverage.sh
Executable 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
|
||||||
348
core-service/tests/api_integration_test.go
Normal file
348
core-service/tests/api_integration_test.go
Normal file
@@ -0,0 +1,348 @@
|
|||||||
|
package tests
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"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/api/middleware"
|
||||||
|
"erp-mvp/core-service/internal/auth"
|
||||||
|
"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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestAuthHandler_Register тестирует endpoint регистрации
|
||||||
|
func TestAuthHandler_Register(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_Login тестирует endpoint входа
|
||||||
|
func TestAuthHandler_Login(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)
|
||||||
|
|
||||||
|
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
|
||||||
|
registerReq := map[string]interface{}{
|
||||||
|
"organization_name": "Test Workshop",
|
||||||
|
"user_password": "password123",
|
||||||
|
"organization_type": "workshop",
|
||||||
|
}
|
||||||
|
|
||||||
|
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.StatusBadRequest, w.Code)
|
||||||
|
|
||||||
|
var errorResponse map[string]interface{}
|
||||||
|
err := json.Unmarshal(w.Body.Bytes(), &errorResponse)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Contains(t, errorResponse["error"], "Validation failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestAuthMiddleware тестирует middleware аутентификации
|
||||||
|
func TestAuthMiddleware(t *testing.T) {
|
||||||
|
// Arrange
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
|
||||||
|
secret := "test_secret_key"
|
||||||
|
ttl := 24 * time.Hour
|
||||||
|
jwtService := auth.NewJWTService(secret, ttl)
|
||||||
|
authMiddleware := middleware.NewAuthMiddleware(jwtService)
|
||||||
|
|
||||||
|
router := gin.New()
|
||||||
|
router.Use(authMiddleware.AuthRequired())
|
||||||
|
|
||||||
|
router.GET("/protected", func(c *gin.Context) {
|
||||||
|
claims := middleware.GetClaims(c)
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"user_id": claims.UserID.String(),
|
||||||
|
"organization_id": claims.OrganizationID.String(),
|
||||||
|
"email": claims.Email,
|
||||||
|
"role": claims.Role,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Создаем валидный JWT токен
|
||||||
|
userID := uuid.New()
|
||||||
|
orgID := uuid.New()
|
||||||
|
validToken, err := jwtService.GenerateToken(userID, orgID, "test@example.com", "admin")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Act - тест с валидным токеном
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequest("GET", "/protected", nil)
|
||||||
|
req.Header.Set("Authorization", "Bearer "+validToken)
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assert.Equal(t, http.StatusOK, w.Code)
|
||||||
|
|
||||||
|
var response map[string]interface{}
|
||||||
|
err = json.Unmarshal(w.Body.Bytes(), &response)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, userID.String(), response["user_id"])
|
||||||
|
assert.Equal(t, "test@example.com", response["email"])
|
||||||
|
|
||||||
|
// Act - тест без токена
|
||||||
|
w = httptest.NewRecorder()
|
||||||
|
req, _ = http.NewRequest("GET", "/protected", nil)
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
||||||
|
|
||||||
|
// Act - тест с неверным токеном
|
||||||
|
w = httptest.NewRecorder()
|
||||||
|
req, _ = http.NewRequest("GET", "/protected", nil)
|
||||||
|
req.Header.Set("Authorization", "Bearer invalid_token")
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestLocationHandler_CreateLocation тестирует создание места хранения
|
||||||
|
func TestLocationHandler_CreateLocation(t *testing.T) {
|
||||||
|
// Arrange
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
|
||||||
|
mockLocationService := &MockLocationService{}
|
||||||
|
handler := handlers.NewLocationHandler(mockLocationService)
|
||||||
|
|
||||||
|
router := gin.New()
|
||||||
|
router.Use(mockAuthMiddleware())
|
||||||
|
router.POST("/locations", handler.CreateLocation)
|
||||||
|
|
||||||
|
locationReq := &models.CreateLocationRequest{
|
||||||
|
Name: "Test Location",
|
||||||
|
Address: "Test Address",
|
||||||
|
Type: "warehouse",
|
||||||
|
Coordinates: models.JSON{
|
||||||
|
"lat": 55.7558,
|
||||||
|
"lng": 37.6176,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedLocation := &models.StorageLocation{
|
||||||
|
ID: uuid.New(),
|
||||||
|
Name: "Test Location",
|
||||||
|
Address: "Test Address",
|
||||||
|
Type: "warehouse",
|
||||||
|
Coordinates: models.JSON{
|
||||||
|
"lat": 55.7558,
|
||||||
|
"lng": 37.6176,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
mockLocationService.On("CreateLocation", mock.Anything, mock.Anything, locationReq).Return(expectedLocation, nil)
|
||||||
|
|
||||||
|
reqBody, _ := json.Marshal(locationReq)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequest("POST", "/locations", bytes.NewBuffer(reqBody))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Authorization", "Bearer test_token")
|
||||||
|
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, expectedLocation.Name, response.Name)
|
||||||
|
assert.Equal(t, expectedLocation.Type, response.Type)
|
||||||
|
|
||||||
|
mockLocationService.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockLocationService мок для LocationService
|
||||||
|
type MockLocationService struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
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) 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) GetLocation(ctx context.Context, id, orgID uuid.UUID) (*models.StorageLocation, error) {
|
||||||
|
args := m.Called(ctx, id, orgID)
|
||||||
|
return args.Get(0).(*models.StorageLocation), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockLocationService) UpdateLocation(ctx context.Context, id, orgID uuid.UUID, req *models.CreateLocationRequest) (*models.StorageLocation, error) {
|
||||||
|
args := m.Called(ctx, id, orgID, req)
|
||||||
|
return args.Get(0).(*models.StorageLocation), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockLocationService) DeleteLocation(ctx context.Context, id, orgID uuid.UUID) error {
|
||||||
|
args := m.Called(ctx, id, orgID)
|
||||||
|
return args.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockLocationService) GetChildren(ctx context.Context, parentID, orgID uuid.UUID) ([]*models.StorageLocation, error) {
|
||||||
|
args := m.Called(ctx, parentID, orgID)
|
||||||
|
return args.Get(0).([]*models.StorageLocation), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// mockAuthMiddleware создает middleware для тестов
|
||||||
|
func mockAuthMiddleware() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
// Устанавливаем тестовые claims
|
||||||
|
claims := &auth.Claims{
|
||||||
|
UserID: uuid.New(),
|
||||||
|
OrganizationID: uuid.New(),
|
||||||
|
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)
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user