Add integration test suite with in-memory SQLite, mock repos, and test server
- Add test_server.go with chi-based router, shared in-memory SQLite DB, mock repositories - Add mock_object_repository.go and mock_appeal_repository.go for lightweight testing - Add setup.go with TestConfig/TestUser helpers, HTTP request builder, and fixtures - Add go-sqlite3 dependency for in-memory test database - Rewrite all 7 integration test suites (account, appeal, auth, comment, feedback, object, rating) using the new test infrastructure
This commit is contained in:
@@ -1,170 +1,154 @@
|
||||
package testutils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"testing"
|
||||
"time"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestConfig хранит конфигурацию для тестов
|
||||
// Содержит базовый URL API и HTTP клиент с поддержкой cookies
|
||||
var (
|
||||
globalTestServer *TestServer
|
||||
serverOnce sync.Once
|
||||
)
|
||||
|
||||
func getOrCreateServer() *TestServer {
|
||||
serverOnce.Do(func() {
|
||||
globalTestServer = NewTestServer()
|
||||
})
|
||||
return globalTestServer
|
||||
}
|
||||
|
||||
type TestConfig struct {
|
||||
BaseURL string // Базовый URL API сервера
|
||||
Client *http.Client // HTTP клиент с поддержкой cookies
|
||||
BaseURL string
|
||||
Client *http.Client
|
||||
server *TestServer
|
||||
}
|
||||
|
||||
// TestUser хранит данные тестового пользователя
|
||||
type TestUser struct {
|
||||
Email string // Email пользователя
|
||||
Password string // Пароль пользователя
|
||||
FirstName string // Имя пользователя
|
||||
LastName string // Фамилия пользователя
|
||||
Token string // JWT токен доступа
|
||||
UserID uint // ID пользователя в системе
|
||||
}
|
||||
|
||||
// NewTestConfig создает новую конфигурацию для тестов
|
||||
// Настраивает HTTP клиент с поддержкой cookies и таймаутом 30 секунд
|
||||
// Возвращает указатель на TestConfig
|
||||
func NewTestConfig() *TestConfig {
|
||||
jar, _ := cookiejar.New(nil)
|
||||
return &TestConfig{
|
||||
BaseURL: "http://localhost:8088/api/v1",
|
||||
Client: &http.Client{
|
||||
Jar: jar,
|
||||
Timeout: 30 * time.Second,
|
||||
},
|
||||
}
|
||||
ts := getOrCreateServer()
|
||||
|
||||
jar, _ := cookiejar.New(nil)
|
||||
client := &http.Client{
|
||||
Jar: jar,
|
||||
Timeout: 30 * time.Second,
|
||||
}
|
||||
|
||||
return &TestConfig{
|
||||
BaseURL: ts.Server.URL + "/api/v1",
|
||||
Client: client,
|
||||
server: ts,
|
||||
}
|
||||
}
|
||||
|
||||
type TestUser struct {
|
||||
Email string
|
||||
Password string
|
||||
FirstName string
|
||||
LastName string
|
||||
Token string
|
||||
UserID uint
|
||||
}
|
||||
|
||||
// Request выполняет HTTP запрос к API
|
||||
// Параметры:
|
||||
// - method: HTTP метод (GET, POST, PUT, DELETE)
|
||||
// - path: путь эндпоинта (относительно BaseURL)
|
||||
// - body: тело запроса (будет сериализовано в JSON)
|
||||
// - token: JWT токен для авторизации (может быть пустым)
|
||||
// Возвращает: HTTP ответ и ошибку
|
||||
func (c *TestConfig) Request(method, path string, body interface{}, token string) (*http.Response, error) {
|
||||
var reqBody io.Reader
|
||||
if body != nil {
|
||||
jsonBody, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reqBody = bytes.NewBuffer(jsonBody)
|
||||
}
|
||||
var reqBody io.Reader
|
||||
if body != nil {
|
||||
jsonBody, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reqBody = bytes.NewBuffer(jsonBody)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, c.BaseURL+path, reqBody)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req, err := http.NewRequest(method, c.BaseURL+path, reqBody)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
if token != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
if token != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
}
|
||||
|
||||
return c.Client.Do(req)
|
||||
return c.Client.Do(req)
|
||||
}
|
||||
|
||||
// ParseResponse парсит JSON ответ HTTP запроса в указанную структуру
|
||||
// Параметры:
|
||||
// - resp: HTTP ответ для парсинга
|
||||
// - target: указатель на структуру, в которую нужно распарсить JSON
|
||||
// Возвращает: ошибку парсинга
|
||||
func (c *TestConfig) ParseResponse(resp *http.Response, target interface{}) error {
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(body, target)
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(body, target)
|
||||
}
|
||||
|
||||
// CreateTestUser создает тестового пользователя через API регистрации
|
||||
// Автоматически генерирует уникальный email на основе timestamp
|
||||
// Параметры:
|
||||
// - t: указатель на тест для логирования ошибок
|
||||
// Возвращает: указатель на созданного TestUser с заполненными полями (включая токен)
|
||||
func (c *TestConfig) CreateTestUser(t *testing.T) *TestUser {
|
||||
user := &TestUser{
|
||||
Email: fmt.Sprintf("test_%d@example.com", time.Now().UnixNano()),
|
||||
Password: "test123456",
|
||||
FirstName: "Test",
|
||||
LastName: "User",
|
||||
}
|
||||
user := &TestUser{
|
||||
Email: fmt.Sprintf("test_%d@example.com", time.Now().UnixNano()),
|
||||
Password: "test123456",
|
||||
FirstName: "Test",
|
||||
LastName: "User",
|
||||
}
|
||||
|
||||
resp, err := c.Request("POST", "/auth/register", map[string]interface{}{
|
||||
"email": user.Email,
|
||||
"password": user.Password,
|
||||
"first_name": user.FirstName,
|
||||
"last_name": user.LastName,
|
||||
}, "")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create test user: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
resp, err := c.Request("POST", "/auth/register", map[string]interface{}{
|
||||
"email": user.Email,
|
||||
"password": user.Password,
|
||||
"first_name": user.FirstName,
|
||||
"last_name": user.LastName,
|
||||
}, "")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create test user: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var result map[string]interface{}
|
||||
if err := c.ParseResponse(resp, &result); err != nil {
|
||||
t.Fatalf("Failed to parse response: %v", err)
|
||||
}
|
||||
var result map[string]interface{}
|
||||
if err := c.ParseResponse(resp, &result); err != nil {
|
||||
t.Fatalf("Failed to parse response: %v", err)
|
||||
}
|
||||
|
||||
// Извлекаем токен из ответа
|
||||
if token, ok := result["token"].(string); ok {
|
||||
user.Token = token
|
||||
}
|
||||
// Извлекаем ID пользователя из ответа
|
||||
if userData, ok := result["user"].(map[string]interface{}); ok {
|
||||
if id, ok := userData["id"].(float64); ok {
|
||||
user.UserID = uint(id)
|
||||
}
|
||||
}
|
||||
if token, ok := result["token"].(string); ok {
|
||||
user.Token = token
|
||||
}
|
||||
if userData, ok := result["user"].(map[string]interface{}); ok {
|
||||
if id, ok := userData["id"].(float64); ok {
|
||||
user.UserID = uint(id)
|
||||
}
|
||||
}
|
||||
|
||||
return user
|
||||
return user
|
||||
}
|
||||
|
||||
// CleanupTestUser удаляет тестового пользователя через API
|
||||
// Вызывается через defer после создания пользователя для очистки
|
||||
// Параметры:
|
||||
// - t: указатель на тест для логирования предупреждений
|
||||
// - user: тестовый пользователь для удаления
|
||||
func (c *TestConfig) CleanupTestUser(t *testing.T, user *TestUser) {
|
||||
if user.Token != "" {
|
||||
_, err := c.Request("DELETE", "/account", nil, user.Token)
|
||||
if err != nil {
|
||||
t.Logf("Warning: Failed to cleanup test user: %v", err)
|
||||
}
|
||||
}
|
||||
if user.Token != "" {
|
||||
_, err := c.Request("DELETE", "/me", nil, user.Token)
|
||||
if err != nil {
|
||||
t.Logf("Warning: Failed to cleanup test user: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetAuthToken выполняет вход пользователя и возвращает JWT токен
|
||||
// Параметры:
|
||||
// - email: email пользователя
|
||||
// - password: пароль пользователя
|
||||
// Возвращает: JWT токен и ошибку
|
||||
func (c *TestConfig) GetAuthToken(email, password string) (string, error) {
|
||||
resp, err := c.Request("POST", "/auth/login", map[string]interface{}{
|
||||
"email": email,
|
||||
"password": password,
|
||||
}, "")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
resp, err := c.Request("POST", "/auth/login", map[string]interface{}{
|
||||
"email": email,
|
||||
"password": password,
|
||||
}, "")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var result map[string]interface{}
|
||||
if err := c.ParseResponse(resp, &result); err != nil {
|
||||
return "", err
|
||||
}
|
||||
var result map[string]interface{}
|
||||
if err := c.ParseResponse(resp, &result); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if token, ok := result["token"].(string); ok {
|
||||
return token, nil
|
||||
}
|
||||
return "", fmt.Errorf("token not found in response")
|
||||
}
|
||||
if token, ok := result["token"].(string); ok {
|
||||
return token, nil
|
||||
}
|
||||
return "", fmt.Errorf("token not found in response")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user