add service

This commit is contained in:
vallyenfail
2026-01-17 17:39:33 +03:00
parent 1376ff9188
commit d959dcca96
82 changed files with 25041 additions and 1 deletions

309
TESTING.md Normal file
View File

@@ -0,0 +1,309 @@
# Руководство по тестированию
## Архитектура для тестирования
Проект спроектирован с учетом тестируемости:
### ✅ Интерфейсы для всех слоев
**Repository интерфейсы** (`internal/repository/interfaces.go`):
```go
type UserRepository interface {
FindByEmailHash(ctx context.Context, emailHash string) (*model.User, error)
FindByID(ctx context.Context, userID int) (*model.User, error)
Create(ctx context.Context, user *model.User) error
// ...
}
```
**Service интерфейсы** (`internal/service/interfaces.go`):
```go
type AuthService interface {
Login(ctx context.Context, email, password, ip, userAgent string) (string, string, error)
Refresh(ctx context.Context, refreshToken string) (string, error)
// ...
}
```
### ✅ Context пробрасывается через все слои
```
main.go (ctx) → Workers (ctx) → gRPC Handler (ctx) → Service (ctx) → Repository (ctx) → pgx
```
**Правило**: `context.Background()` создается **только один раз** в `main.go` и прокидывается через все компоненты.
Это позволяет:
- **Graceful shutdown**: при отмене context все workers и операции останавливаются
- Отменять долгие операции
- Передавать метаданные (trace ID, user ID)
- Контролировать таймауты
- Избежать потерянных goroutines
## Автоматическая генерация моков
Проект использует [minimock](https://github.com/gojuno/minimock) для автоматической генерации типизированных моков из интерфейсов.
### Генерация моков
Все моки генерируются в `internal/mocks/` одной командой:
```bash
make generate-mock
```
Эта команда генерирует моки для всех интерфейсов:
- **Repository интерфейсы**: `UserRepository`, `SessionRepository`, `InviteRepository`, `RequestRepository`, `SupplierRepository`, `TokenUsageRepository`
- **Service интерфейсы**: `AuthService`, `UserService`, `InviteService`, `RequestService`, `SupplierService`
### Использование сгенерированных моков
```go
import (
"testing"
"context"
"smart-search-back/internal/mocks"
"smart-search-back/internal/service"
"smart-search-back/internal/model"
"github.com/stretchr/testify/assert"
)
func TestAuthService_Login_Success(t *testing.T) {
// Создаем моки
mockUserRepo := mocks.NewUserRepositoryMock(t)
mockSessionRepo := mocks.NewSessionRepositoryMock(t)
// Настраиваем поведение мока
mockUserRepo.FindByEmailHashMock.Expect(context.Background(), "email_hash").Return(&model.User{
ID: 1,
PasswordHash: "b109f3bbbc244eb82441917ed06d618b9008dd09b3befd1b5e07394c706a8bb980b1d7785e5976ec049b46df5f1326af5a2ea6d103fd07c95385ffab0cacbc86",
}, nil)
mockSessionRepo.CreateMock.Expect(context.Background(), &model.Session{}).Return(nil)
// Создаем сервис с моками
authService := service.NewAuthService(mockUserRepo, mockSessionRepo)
// Выполняем тест
accessToken, refreshToken, err := authService.Login(
context.Background(),
"test@example.com",
"password",
"127.0.0.1",
"test-agent",
)
// Проверяем результат
assert.NoError(t, err)
assert.NotEmpty(t, accessToken)
assert.NotEmpty(t, refreshToken)
// Minimock автоматически проверит что все ожидания выполнены
}
```
### Преимущества minimock
**Типобезопасность** - моки генерируются из интерфейсов, ошибки компиляции при изменении сигнатур
**Автоматическая проверка** - проверяет что все ожидания выполнены
**Счетчики вызовов** - можно проверить сколько раз был вызван метод
**Inspection** - можно проверить аргументы вызовов
**Minimal boilerplate** - не нужно писать моки вручную
### Пример с проверкой вызовов
```go
func TestUserService_GetBalance(t *testing.T) {
mockUserRepo := mocks.NewUserRepositoryMock(t)
// Ожидаем вызов GetBalance с userID=123
mockUserRepo.GetBalanceMock.Expect(context.Background(), 123).Return(100.50, nil)
userService := service.NewUserService(mockUserRepo, nil)
balance, err := userService.GetBalance(context.Background(), 123)
assert.NoError(t, err)
assert.Equal(t, 100.50, balance)
// Minimock автоматически проверит:
// - что GetBalance был вызван ровно 1 раз
// - с правильными аргументами
// - и вернул правильное значение
}
```
### Ручные моки (legacy пример)
Для сравнения, старый пример с ручными моками (все еще работает, но не рекомендуется):
```go
// Mock репозитория (legacy - используйте minimock!)
type mockUserRepo struct {
findByEmailHashFunc func(ctx context.Context, emailHash string) (*model.User, error)
}
func (m *mockUserRepo) FindByEmailHash(ctx context.Context, emailHash string) (*model.User, error) {
if m.findByEmailHashFunc != nil {
return m.findByEmailHashFunc(ctx, emailHash)
}
return nil, nil
}
```
## Запуск тестов
```bash
# Все тесты
go test ./...
# С покрытием
go test ./... -cover
# Verbose режим
go test ./... -v
# Конкретный пакет
go test ./internal/service/tests/...
# С race detector
go test ./... -race
```
## Использование minimock
Проект использует [minimock](https://github.com/gojuno/minimock) для автоматической генерации типизированных моков.
### Генерация моков
Все моки генерируются в `internal/mocks/`:
```bash
make generate-mock
```
Генерируются моки для всех интерфейсов:
- **Repository**: `UserRepository`, `SessionRepository`, `InviteRepository`, `RequestRepository`, `SupplierRepository`, `TokenUsageRepository`
- **Service**: `AuthService`, `UserService`, `InviteService`, `RequestService`, `SupplierService`
### Использование сгенерированных моков
```go
import "smart-search-back/internal/mocks"
func TestAuthService_Login(t *testing.T) {
mockUserRepo := mocks.NewUserRepositoryMock(t)
mockSessionRepo := mocks.NewSessionRepositoryMock(t)
mockUserRepo.FindByEmailHashMock.Expect(ctx, "hash").Return(&model.User{...}, nil)
authService := service.NewAuthService(mockUserRepo, mockSessionRepo)
// ... тест
}
```
Подробнее см. раздел "Автоматическая генерация моков" выше.
## Структура тестов
```
internal/
├── service/
│ ├── auth.go
│ ├── interfaces.go # Интерфейсы сервисов
│ └── tests/
│ └── auth_test.go # Тесты с моками
├── repository/
│ ├── user.go
│ ├── interfaces.go # Интерфейсы репозиториев
│ └── tests/
│ └── user_test.go
```
## Best Practices
### 1. Используйте context.Background() в тестах
```go
ctx := context.Background()
result, err := service.SomeMethod(ctx, params)
```
### 2. Мокайте только то, что нужно
```go
mockRepo := &mockUserRepo{
findByIDFunc: func(ctx context.Context, id int) (*model.User, error) {
return &model.User{ID: id}, nil
},
// Остальные методы можно не реализовывать если не используются
}
```
### 3. Проверяйте вызовы
```go
var called bool
mockRepo := &mockUserRepo{
createFunc: func(ctx context.Context, user *model.User) error {
called = true
assert.Equal(t, "expected@email.com", user.Email)
return nil
},
}
// ... вызов сервиса
assert.True(t, called, "Create should have been called")
```
### 4. Тестируйте ошибки
```go
mockRepo := &mockUserRepo{
findByIDFunc: func(ctx context.Context, id int) (*model.User, error) {
return nil, errors.NewBusinessError(errors.UserNotFound, "user not found")
},
}
result, err := service.GetUser(ctx, 123)
assert.Error(t, err)
assert.Nil(t, result)
```
## Integration тесты
Для integration тестов с реальной БД можно использовать testcontainers:
```go
func TestUserRepository_Integration(t *testing.T) {
// Создать testcontainer с PostgreSQL
// Применить миграции
// Запустить тесты
}
```
## CI/CD
```yaml
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
with:
go-version: 1.21
- run: go test ./... -race -cover
```
## Хэши для тестов
При тестировании аутентификации используйте правильные хэши:
- Пароль: `"password"`
- SHA512 хэш: `"b109f3bbbc244eb82441917ed06d618b9008dd09b3befd1b5e07394c706a8bb980b1d7785e5976ec049b46df5f1326af5a2ea6d103fd07c95385ffab0cacbc86"`
Или используйте `crypto.PasswordHash("password")` прямо в тестах.