Files
backend/tests/e2e/api_test.go
2025-12-13 22:34:01 +05:00

1362 lines
48 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package e2e
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"testing"
"time"
"git.kirlllll.ru/volontery/backend/internal/api"
"git.kirlllll.ru/volontery/backend/internal/config"
"git.kirlllll.ru/volontery/backend/internal/database"
"git.kirlllll.ru/volontery/backend/internal/pkg/jwt"
"git.kirlllll.ru/volontery/backend/internal/repository"
"git.kirlllll.ru/volontery/backend/internal/service"
"github.com/jackc/pgx/v5/pgtype"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
// APITestSuite - набор E2E тестов для API
type APITestSuite struct {
suite.Suite
server *api.Server
db *pgxpool.Pool
queries *database.Queries
cfg *config.Config
accessToken string
userID int64
}
// SetupSuite запускается один раз перед всеми тестами
func (suite *APITestSuite) SetupSuite() {
// Загрузка конфигурации
cfg, err := config.Load()
require.NoError(suite.T(), err)
suite.cfg = cfg
// Подключение к БД
ctx := context.Background()
db, err := pgxpool.New(ctx, cfg.DatabaseURL)
require.NoError(suite.T(), err)
suite.db = db
suite.queries = database.New(db)
// Инициализация зависимостей
jwtManager := jwt.NewManager(cfg.JWTSecret, cfg.JWTAccessTokenTTL, cfg.JWTRefreshTokenTTL)
require.NoError(suite.T(), err)
authRepo := repository.NewAuthRepository(suite.queries)
userRepo := repository.NewUserRepository(suite.queries)
rbacRepo := repository.NewRBACRepository(suite.queries)
authService := service.NewAuthService(userRepo, authRepo, rbacRepo, jwtManager)
userService := service.NewUserService(userRepo, rbacRepo)
requestRepo := repository.NewRequestRepository(suite.queries)
requestService := service.NewRequestService(requestRepo)
// Создание сервера
suite.server = api.NewServer(cfg, authService, userService, requestService, jwtManager)
}
// TearDownSuite запускается один раз после всех тестов
func (suite *APITestSuite) TearDownSuite() {
if suite.db != nil {
suite.db.Close()
}
}
// TestAPITestSuite - точка входа для запуска набора тестов
func TestAPITestSuite(t *testing.T) {
suite.Run(t, new(APITestSuite))
}
// Helper функции
// createModerator создает пользователя с ролью moderator
func (suite *APITestSuite) createModerator() (userID int64, token string) {
// Создаем пользователя
email := fmt.Sprintf("moderator_%d@example.com", time.Now().UnixNano())
registerReq := map[string]interface{}{
"email": email,
"password": "SecureP@ssw0rd123",
"first_name": "Moderator",
"last_name": "User",
}
_, regBody := suite.makeRequest("POST", "/api/v1/auth/register", registerReq, "")
var authResp map[string]interface{}
err := json.Unmarshal(regBody, &authResp)
require.NoError(suite.T(), err)
token = authResp["access_token"].(string)
user := authResp["user"].(map[string]interface{})
userID = int64(user["id"].(float64))
// Получаем роль moderator
role, err := suite.queries.GetRoleByName(context.Background(), "moderator")
require.NoError(suite.T(), err)
// Назначаем роль модератора
_, err = suite.queries.AssignRoleToUser(context.Background(), database.AssignRoleToUserParams{
UserID: userID,
RoleID: role.ID,
AssignedBy: pgtype.Int8{
Int64: userID,
Valid: true,
}, // self-assigned for testing
})
require.NoError(suite.T(), err)
return userID, token
}
// createApprovedRequest создает заявку и одобряет её
func (suite *APITestSuite) createApprovedRequest(requesterToken string) int64 {
// Получаем типы заявок
_, body := suite.makeRequest("GET", "/api/v1/request-types", nil, "")
var types []map[string]interface{}
json.Unmarshal(body, &types)
require.NotEmpty(suite.T(), types)
requestTypeID := int64(types[0]["id"].(float64))
// Создаем заявку
createReq := map[string]interface{}{
"request_type_id": requestTypeID,
"title": fmt.Sprintf("Test Request %d", time.Now().UnixNano()),
"description": "Test description",
"latitude": 55.751244,
"longitude": 37.618423,
"address": "Test Address",
"city": "Москва",
"urgency": "medium",
}
_, respBody := suite.makeRequest("POST", "/api/v1/requests", createReq, requesterToken)
var request map[string]interface{}
json.Unmarshal(respBody, &request)
requestID := int64(request["id"].(float64))
// Создаем модератора и одобряем заявку
moderatorID, moderatorToken := suite.createModerator()
url := fmt.Sprintf("/api/v1/moderation/requests/%d/approve", requestID)
approveReq := map[string]interface{}{
"comment": "Approved for testing",
}
resp, _ := suite.makeRequest("POST", url, approveReq, moderatorToken)
require.Equal(suite.T(), http.StatusOK, resp.StatusCode, "Failed to approve request, moderator_id=%d", moderatorID)
return requestID
}
// createVolunteerWithResponse создает волонтера и отклик
func (suite *APITestSuite) createVolunteerWithResponse(requestID int64) (volunteerID int64, responseID int64, token string) {
// Создаем волонтера
email := fmt.Sprintf("volunteer_%d@example.com", time.Now().UnixNano())
registerReq := map[string]interface{}{
"email": email,
"password": "SecureP@ssw0rd123",
"first_name": "Volunteer",
"last_name": "User",
}
_, regBody := suite.makeRequest("POST", "/api/v1/auth/register", registerReq, "")
var authResp map[string]interface{}
json.Unmarshal(regBody, &authResp)
token = authResp["access_token"].(string)
user := authResp["user"].(map[string]interface{})
volunteerID = int64(user["id"].(float64))
// Создаем отклик
responseReq := map[string]interface{}{
"message": "I can help with this request",
}
url := fmt.Sprintf("/api/v1/requests/%d/responses", requestID)
_, respBody := suite.makeRequest("POST", url, responseReq, token)
var response map[string]interface{}
json.Unmarshal(respBody, &response)
responseID = int64(response["id"].(float64))
return volunteerID, responseID, token
}
// checkModeratorAction проверяет наличие записи в moderator_actions
func (suite *APITestSuite) checkModeratorAction(moderatorID, requestID int64, actionType string) {
actions, err := suite.queries.GetModeratorActionsByRequest(context.Background(), pgtype.Int8{
Int64: requestID,
Valid: true,
})
require.NoError(suite.T(), err)
found := false
for _, action := range actions {
if action.ModeratorID == moderatorID && string(action.ActionType) == actionType {
found = true
break
}
}
assert.True(suite.T(), found, "Moderator action not found: moderator_id=%d, request_id=%d, action_type=%s", moderatorID, requestID, actionType)
}
// checkRequestStatusHistory проверяет историю статусов
func (suite *APITestSuite) checkRequestStatusHistory(requestID int64, expectedStatuses []string) {
history, err := suite.queries.GetRequestStatusHistory(context.Background(), requestID)
require.NoError(suite.T(), err)
// Получаем список статусов из истории (в обратном порядке - от новых к старым)
var actualStatuses []string
for i := len(history) - 1; i >= 0; i-- {
actualStatuses = append(actualStatuses, string(history[i].ToStatus))
}
// Проверяем, что все ожидаемые статусы присутствуют
for _, expectedStatus := range expectedStatuses {
found := false
for _, actualStatus := range actualStatuses {
if actualStatus == expectedStatus {
found = true
break
}
}
assert.True(suite.T(), found, "Expected status %s not found in history for request %d. Actual: %v", expectedStatus, requestID, actualStatuses)
}
}
func (suite *APITestSuite) makeRequest(method, path string, body interface{}, token string) (*http.Response, []byte) {
var bodyReader io.Reader
if body != nil {
jsonBody, err := json.Marshal(body)
require.NoError(suite.T(), err)
bodyReader = bytes.NewReader(jsonBody)
}
req := httptest.NewRequest(method, path, bodyReader)
if bodyReader != nil {
req.Header.Set("Content-Type", "application/json")
}
if token != "" {
req.Header.Set("Authorization", "Bearer "+token)
}
w := httptest.NewRecorder()
suite.server.ServeHTTP(w, req)
resp := w.Result()
respBody, err := io.ReadAll(resp.Body)
require.NoError(suite.T(), err)
defer resp.Body.Close()
return resp, respBody
}
// Test 1: Health Check
func (suite *APITestSuite) Test01_HealthCheck() {
suite.T().Log("Testing: GET /health")
resp, body := suite.makeRequest("GET", "/health", nil, "")
assert.Equal(suite.T(), http.StatusOK, resp.StatusCode)
assert.Equal(suite.T(), "OK", string(body))
}
// Test 2: User Registration
func (suite *APITestSuite) Test02_Register() {
suite.T().Log("Testing: POST /api/v1/auth/register")
registerReq := map[string]interface{}{
"email": fmt.Sprintf("test_%d@example.com", time.Now().Unix()),
"password": "SecureP@ssw0rd123",
"first_name": "Test",
"last_name": "User",
"phone": "+79001234567",
"latitude": 55.751244,
"longitude": 37.618423,
"city": "Москва",
"bio": "Test bio",
}
resp, body := suite.makeRequest("POST", "/api/v1/auth/register", registerReq, "")
assert.Equal(suite.T(), http.StatusCreated, resp.StatusCode)
assert.Equal(suite.T(), "application/json", resp.Header.Get("Content-Type"))
var authResp map[string]interface{}
err := json.Unmarshal(body, &authResp)
require.NoError(suite.T(), err)
assert.Contains(suite.T(), authResp, "access_token")
assert.Contains(suite.T(), authResp, "refresh_token")
assert.Contains(suite.T(), authResp, "expires_in")
assert.Contains(suite.T(), authResp, "user")
// Сохраняем токен для последующих тестов
suite.accessToken = authResp["access_token"].(string)
user := authResp["user"].(map[string]interface{})
suite.userID = int64(user["id"].(float64))
}
// Test 3: Login
func (suite *APITestSuite) Test03_Login() {
suite.T().Log("Testing: POST /api/v1/auth/login")
// Сначала регистрируем пользователя
email := fmt.Sprintf("login_test_%d@example.com", time.Now().Unix())
password := "SecureP@ssw0rd123"
registerReq := map[string]interface{}{
"email": email,
"password": password,
"first_name": "Login",
"last_name": "Test User",
}
suite.makeRequest("POST", "/api/v1/auth/register", registerReq, "")
// Теперь логинимся
loginReq := map[string]interface{}{
"email": email,
"password": password,
}
resp, body := suite.makeRequest("POST", "/api/v1/auth/login", loginReq, "")
assert.Equal(suite.T(), http.StatusOK, resp.StatusCode)
var authResp map[string]interface{}
err := json.Unmarshal(body, &authResp)
require.NoError(suite.T(), err)
assert.Contains(suite.T(), authResp, "access_token")
assert.Contains(suite.T(), authResp, "refresh_token")
}
// Test 4: Login with wrong credentials
func (suite *APITestSuite) Test04_Login_WrongCredentials() {
suite.T().Log("Testing: POST /api/v1/auth/login (invalid credentials)")
loginReq := map[string]interface{}{
"email": "nonexistent@example.com",
"password": "wrongpassword",
}
resp, body := suite.makeRequest("POST", "/api/v1/auth/login", loginReq, "")
assert.Equal(suite.T(), http.StatusUnauthorized, resp.StatusCode)
var errorResp map[string]interface{}
err := json.Unmarshal(body, &errorResp)
require.NoError(suite.T(), err)
assert.Contains(suite.T(), errorResp, "error")
}
// Test 5: Get current user info
func (suite *APITestSuite) Test05_AuthMe() {
suite.T().Log("Testing: GET /api/v1/auth/me")
resp, body := suite.makeRequest("GET", "/api/v1/auth/me", nil, suite.accessToken)
assert.Equal(suite.T(), http.StatusOK, resp.StatusCode)
var userInfo map[string]interface{}
err := json.Unmarshal(body, &userInfo)
require.NoError(suite.T(), err)
assert.Contains(suite.T(), userInfo, "id")
assert.Contains(suite.T(), userInfo, "email")
assert.Equal(suite.T(), float64(suite.userID), userInfo["id"].(float64))
}
// Test 6: Get current user profile
func (suite *APITestSuite) Test06_GetMyProfile() {
suite.T().Log("Testing: GET /api/v1/users/me")
resp, body := suite.makeRequest("GET", "/api/v1/users/me", nil, suite.accessToken)
assert.Equal(suite.T(), http.StatusOK, resp.StatusCode)
var profile map[string]interface{}
err := json.Unmarshal(body, &profile)
require.NoError(suite.T(), err)
assert.Contains(suite.T(), profile, "id")
assert.Contains(suite.T(), profile, "email")
assert.Contains(suite.T(), profile, "first_name")
assert.Contains(suite.T(), profile, "last_name")
assert.Contains(suite.T(), profile, "created_at")
}
// Test 7: Update profile
func (suite *APITestSuite) Test07_UpdateProfile() {
suite.T().Log("Testing: PATCH /api/v1/users/me")
updateReq := map[string]interface{}{
"first_name": "Updated",
"last_name": "Name",
"city": "Санкт-Петербург",
}
resp, body := suite.makeRequest("PATCH", "/api/v1/users/me", updateReq, suite.accessToken)
assert.Equal(suite.T(), http.StatusOK, resp.StatusCode)
var result map[string]interface{}
err := json.Unmarshal(body, &result)
require.NoError(suite.T(), err)
assert.Contains(suite.T(), result, "message")
}
// Test 8: Update location
func (suite *APITestSuite) Test08_UpdateLocation() {
suite.T().Log("Testing: POST /api/v1/users/me/location")
locationReq := map[string]interface{}{
"latitude": 59.9343,
"longitude": 30.3351,
}
resp, body := suite.makeRequest("POST", "/api/v1/users/me/location", locationReq, suite.accessToken)
assert.Equal(suite.T(), http.StatusOK, resp.StatusCode)
var result map[string]interface{}
err := json.Unmarshal(body, &result)
require.NoError(suite.T(), err)
assert.Contains(suite.T(), result, "message")
}
// Test 9: Get user roles
func (suite *APITestSuite) Test09_GetMyRoles() {
suite.T().Log("Testing: GET /api/v1/users/me/roles")
resp, body := suite.makeRequest("GET", "/api/v1/users/me/roles", nil, suite.accessToken)
assert.Equal(suite.T(), http.StatusOK, resp.StatusCode)
var roles []map[string]interface{}
err := json.Unmarshal(body, &roles)
require.NoError(suite.T(), err)
// Пользователь должен иметь хотя бы одну роль
assert.NotEmpty(suite.T(), roles)
}
// Test 10: Get user permissions
func (suite *APITestSuite) Test10_GetMyPermissions() {
suite.T().Log("Testing: GET /api/v1/users/me/permissions")
resp, body := suite.makeRequest("GET", "/api/v1/users/me/permissions", nil, suite.accessToken)
assert.Equal(suite.T(), http.StatusOK, resp.StatusCode)
var permissions []map[string]interface{}
err := json.Unmarshal(body, &permissions)
require.NoError(suite.T(), err)
// Пользователь должен иметь разрешения
assert.NotEmpty(suite.T(), permissions)
}
// Test 11: Get request types
func (suite *APITestSuite) Test11_GetRequestTypes() {
suite.T().Log("Testing: GET /api/v1/request-types")
resp, body := suite.makeRequest("GET", "/api/v1/request-types", nil, "")
assert.Equal(suite.T(), http.StatusOK, resp.StatusCode)
var types []map[string]interface{}
err := json.Unmarshal(body, &types)
require.NoError(suite.T(), err)
// Должны быть типы заявок в БД
assert.NotEmpty(suite.T(), types)
// Проверяем структуру
if len(types) > 0 {
assert.Contains(suite.T(), types[0], "id")
assert.Contains(suite.T(), types[0], "name")
}
}
// Test 12: Create request
func (suite *APITestSuite) Test12_CreateRequest() {
suite.T().Log("Testing: POST /api/v1/requests")
// Получаем типы заявок
_, body := suite.makeRequest("GET", "/api/v1/request-types", nil, "")
var types []map[string]interface{}
json.Unmarshal(body, &types)
require.NotEmpty(suite.T(), types)
requestTypeID := int64(types[0]["id"].(float64))
createReq := map[string]interface{}{
"request_type_id": requestTypeID,
"title": "Нужна помощь с покупкой продуктов",
"description": "Прошу помочь купить продукты в ближайшем магазине. Список прилагается.",
"latitude": 55.751244,
"longitude": 37.618423,
"address": "ул. Тверская, д. 1, кв. 10",
"city": "Москва",
"urgency": "high",
"contact_phone": "+79001234567",
"contact_notes": "Код домофона: 123",
"desired_completion_date": time.Now().Add(24 * time.Hour).Format(time.RFC3339),
}
resp, body := suite.makeRequest("POST", "/api/v1/requests", createReq, suite.accessToken)
assert.Equal(suite.T(), http.StatusCreated, resp.StatusCode)
var request map[string]interface{}
err := json.Unmarshal(body, &request)
require.NoError(suite.T(), err)
assert.Contains(suite.T(), request, "id")
assert.Contains(suite.T(), request, "title")
assert.Contains(suite.T(), request, "status")
}
// Test 13: Get my requests
func (suite *APITestSuite) Test13_GetMyRequests() {
suite.T().Log("Testing: GET /api/v1/requests/my")
resp, body := suite.makeRequest("GET", "/api/v1/requests/my?limit=10&offset=0", nil, suite.accessToken)
assert.Equal(suite.T(), http.StatusOK, resp.StatusCode)
var requests []map[string]interface{}
err := json.Unmarshal(body, &requests)
require.NoError(suite.T(), err)
// Должна быть хотя бы одна заявка (созданная в предыдущем тесте)
assert.NotEmpty(suite.T(), requests)
}
// Test 14: Find nearby requests
func (suite *APITestSuite) Test14_FindNearbyRequests() {
suite.T().Log("Testing: GET /api/v1/requests/nearby")
url := "/api/v1/requests/nearby?lat=55.751244&lon=37.618423&radius=5000&limit=10&offset=0"
resp, body := suite.makeRequest("GET", url, nil, suite.accessToken)
if resp.StatusCode != http.StatusOK {
suite.T().Logf("Error response body: %s", string(body))
}
assert.Equal(suite.T(), http.StatusOK, resp.StatusCode)
var requests []map[string]interface{}
err := json.Unmarshal(body, &requests)
require.NoError(suite.T(), err)
// Проверяем наличие distance_meters в ответе
if len(requests) > 0 {
assert.Contains(suite.T(), requests[0], "distance_meters")
}
}
// Test 15: Find requests in bounds
func (suite *APITestSuite) Test15_FindRequestsInBounds() {
suite.T().Log("Testing: GET /api/v1/requests/bounds")
url := "/api/v1/requests/bounds?min_lon=37.6&min_lat=55.7&max_lon=37.7&max_lat=55.8"
resp, body := suite.makeRequest("GET", url, nil, suite.accessToken)
assert.Equal(suite.T(), http.StatusOK, resp.StatusCode)
var requests []map[string]interface{}
err := json.Unmarshal(body, &requests)
require.NoError(suite.T(), err)
// Ответ должен быть массивом
assert.NotNil(suite.T(), requests)
}
// Test 16: Get request by ID
func (suite *APITestSuite) Test16_GetRequestByID() {
suite.T().Log("Testing: GET /api/v1/requests/{id}")
// Получаем список своих заявок
_, body := suite.makeRequest("GET", "/api/v1/requests/my", nil, suite.accessToken)
var requests []map[string]interface{}
json.Unmarshal(body, &requests)
require.NotEmpty(suite.T(), requests)
requestID := int64(requests[0]["id"].(float64))
// Получаем заявку по ID
url := fmt.Sprintf("/api/v1/requests/%d", requestID)
resp, body := suite.makeRequest("GET", url, nil, suite.accessToken)
assert.Equal(suite.T(), http.StatusOK, resp.StatusCode)
var request map[string]interface{}
err := json.Unmarshal(body, &request)
require.NoError(suite.T(), err)
assert.Equal(suite.T(), float64(requestID), request["id"].(float64))
assert.Contains(suite.T(), request, "title")
assert.Contains(suite.T(), request, "description")
}
// Test 17: Create volunteer response
func (suite *APITestSuite) Test17_CreateVolunteerResponse() {
suite.T().Log("Testing: POST /api/v1/requests/{id}/responses")
// Создаем нового волонтера
email := fmt.Sprintf("volunteer_%d@example.com", time.Now().Unix())
registerReq := map[string]interface{}{
"email": email,
"password": "SecureP@ssw0rd123",
"first_name": "Volunteer",
"last_name": "User",
}
_, regBody := suite.makeRequest("POST", "/api/v1/auth/register", registerReq, "")
var authResp map[string]interface{}
json.Unmarshal(regBody, &authResp)
volunteerToken := authResp["access_token"].(string)
// Получаем список заявок
_, body := suite.makeRequest("GET", "/api/v1/requests/my", nil, suite.accessToken)
var requests []map[string]interface{}
json.Unmarshal(body, &requests)
require.NotEmpty(suite.T(), requests)
requestID := int64(requests[0]["id"].(float64))
// Создаем отклик
responseReq := map[string]interface{}{
"message": "Готов помочь завтра после 15:00",
}
url := fmt.Sprintf("/api/v1/requests/%d/responses", requestID)
resp, body := suite.makeRequest("POST", url, responseReq, volunteerToken)
assert.Equal(suite.T(), http.StatusCreated, resp.StatusCode)
var response map[string]interface{}
err := json.Unmarshal(body, &response)
require.NoError(suite.T(), err)
assert.Contains(suite.T(), response, "id")
assert.Contains(suite.T(), response, "volunteer_id")
assert.Contains(suite.T(), response, "status")
}
// Test 18: Get request responses
func (suite *APITestSuite) Test18_GetRequestResponses() {
suite.T().Log("Testing: GET /api/v1/requests/{id}/responses")
// Получаем список своих заявок
_, body := suite.makeRequest("GET", "/api/v1/requests/my", nil, suite.accessToken)
var requests []map[string]interface{}
json.Unmarshal(body, &requests)
require.NotEmpty(suite.T(), requests)
requestID := int64(requests[0]["id"].(float64))
// Получаем отклики
url := fmt.Sprintf("/api/v1/requests/%d/responses", requestID)
resp, body := suite.makeRequest("GET", url, nil, suite.accessToken)
assert.Equal(suite.T(), http.StatusOK, resp.StatusCode)
var responses []map[string]interface{}
err := json.Unmarshal(body, &responses)
require.NoError(suite.T(), err)
// Должен быть хотя бы один отклик (созданный в предыдущем тесте)
assert.NotEmpty(suite.T(), responses)
if len(responses) > 0 {
assert.Contains(suite.T(), responses[0], "volunteer_id")
assert.Contains(suite.T(), responses[0], "status")
}
}
// Test 19: Refresh token
func (suite *APITestSuite) Test19_RefreshToken() {
suite.T().Log("Testing: POST /api/v1/auth/refresh")
// Регистрируем нового пользователя
email := fmt.Sprintf("refresh_test_%d@example.com", time.Now().Unix())
registerReq := map[string]interface{}{
"email": email,
"password": "SecureP@ssw0rd123",
"first_name": "Refresh",
"last_name": "Test User",
}
_, regBody := suite.makeRequest("POST", "/api/v1/auth/register", registerReq, "")
var authResp map[string]interface{}
json.Unmarshal(regBody, &authResp)
refreshToken := authResp["refresh_token"].(string)
// Обновляем токен
refreshReq := map[string]interface{}{
"refresh_token": refreshToken,
}
resp, body := suite.makeRequest("POST", "/api/v1/auth/refresh", refreshReq, "")
assert.Equal(suite.T(), http.StatusOK, resp.StatusCode)
var newAuthResp map[string]interface{}
err := json.Unmarshal(body, &newAuthResp)
require.NoError(suite.T(), err)
assert.Contains(suite.T(), newAuthResp, "access_token")
assert.Contains(suite.T(), newAuthResp, "refresh_token")
assert.NotEqual(suite.T(), refreshToken, newAuthResp["refresh_token"].(string))
}
// Test 20: Logout
func (suite *APITestSuite) Test20_Logout() {
suite.T().Log("Testing: POST /api/v1/auth/logout")
resp, body := suite.makeRequest("POST", "/api/v1/auth/logout", nil, suite.accessToken)
assert.Equal(suite.T(), http.StatusOK, resp.StatusCode)
var result map[string]interface{}
err := json.Unmarshal(body, &result)
require.NoError(suite.T(), err)
assert.Contains(suite.T(), result, "message")
}
// Test 21: Unauthorized access
func (suite *APITestSuite) Test21_UnauthorizedAccess() {
suite.T().Log("Testing: Unauthorized access to protected endpoint")
resp, body := suite.makeRequest("GET", "/api/v1/users/me", nil, "")
assert.Equal(suite.T(), http.StatusUnauthorized, resp.StatusCode)
var errorResp map[string]interface{}
err := json.Unmarshal(body, &errorResp)
require.NoError(suite.T(), err)
assert.Contains(suite.T(), errorResp, "error")
}
// Test 22: Invalid token
func (suite *APITestSuite) Test22_InvalidToken() {
suite.T().Log("Testing: Invalid token")
resp, body := suite.makeRequest("GET", "/api/v1/users/me", nil, "invalid.token.here")
assert.Equal(suite.T(), http.StatusUnauthorized, resp.StatusCode)
var errorResp map[string]interface{}
err := json.Unmarshal(body, &errorResp)
require.NoError(suite.T(), err)
assert.Contains(suite.T(), errorResp, "error")
}
// Test 23: Get user by ID
func (suite *APITestSuite) Test23_GetUserByID() {
suite.T().Log("Testing: GET /api/v1/users/{id}")
url := fmt.Sprintf("/api/v1/users/%d", suite.userID)
resp, body := suite.makeRequest("GET", url, nil, suite.accessToken)
assert.Equal(suite.T(), http.StatusOK, resp.StatusCode)
var profile map[string]interface{}
err := json.Unmarshal(body, &profile)
require.NoError(suite.T(), err)
assert.Equal(suite.T(), float64(suite.userID), profile["id"].(float64))
assert.Contains(suite.T(), profile, "email")
assert.Contains(suite.T(), profile, "first_name")
assert.Contains(suite.T(), profile, "last_name")
}
// Test 24: Invalid request body
func (suite *APITestSuite) Test24_InvalidRequestBody() {
suite.T().Log("Testing: Invalid request body")
// Пытаемся зарегистрироваться с невалидными данными
registerReq := map[string]interface{}{
"email": "invalid-email", // невалидный email
}
resp, body := suite.makeRequest("POST", "/api/v1/auth/register", registerReq, "")
assert.Equal(suite.T(), http.StatusBadRequest, resp.StatusCode)
var errorResp map[string]interface{}
err := json.Unmarshal(body, &errorResp)
require.NoError(suite.T(), err)
assert.Contains(suite.T(), errorResp, "error")
}
// Test 25: Pagination
func (suite *APITestSuite) Test25_Pagination() {
suite.T().Log("Testing: Pagination parameters")
// Тест с разными параметрами пагинации
testCases := []struct {
limit int
offset int
}{
{10, 0},
{5, 5},
{20, 0},
}
for _, tc := range testCases {
url := fmt.Sprintf("/api/v1/requests/my?limit=%d&offset=%d", tc.limit, tc.offset)
resp, _ := suite.makeRequest("GET", url, nil, suite.accessToken)
assert.Equal(suite.T(), http.StatusOK, resp.StatusCode)
}
}
// ============================================================================
// ТЕСТЫ МОДЕРАЦИИ (Test26-Test32)
// ============================================================================
// Test 26: Full moderation workflow
func (suite *APITestSuite) Test26_ModerationWorkflow() {
suite.T().Log("Testing: Full moderation workflow")
// 1. Создаем заявку (автоматически pending_moderation)
_, body := suite.makeRequest("GET", "/api/v1/request-types", nil, "")
var types []map[string]interface{}
json.Unmarshal(body, &types)
require.NotEmpty(suite.T(), types)
requestTypeID := int64(types[0]["id"].(float64))
createReq := map[string]interface{}{
"request_type_id": requestTypeID,
"title": "Moderation Test Request",
"description": "This request needs moderation",
"latitude": 55.751244,
"longitude": 37.618423,
"address": "Test Address",
}
_, respBody := suite.makeRequest("POST", "/api/v1/requests", createReq, suite.accessToken)
var request map[string]interface{}
json.Unmarshal(respBody, &request)
requestID := int64(request["id"].(float64))
// status - это объект NullRequestStatus {request_status: "...", valid: true}
statusObj := request["status"].(map[string]interface{})
assert.Equal(suite.T(), "pending_moderation", statusObj["request_status"])
// 2. Создаем модератора
moderatorID, moderatorToken := suite.createModerator()
// 3. Получаем список заявок на модерации
resp, body := suite.makeRequest("GET", "/api/v1/moderation/requests/pending?limit=10&offset=0", nil, moderatorToken)
assert.Equal(suite.T(), http.StatusOK, resp.StatusCode)
var pendingRequests []map[string]interface{}
json.Unmarshal(body, &pendingRequests)
assert.NotEmpty(suite.T(), pendingRequests)
// 4. Одобряем заявку
url := fmt.Sprintf("/api/v1/moderation/requests/%d/approve", requestID)
approveReq := map[string]interface{}{
"comment": "Request looks good",
}
resp, _ = suite.makeRequest("POST", url, approveReq, moderatorToken)
assert.Equal(suite.T(), http.StatusOK, resp.StatusCode)
// 5. Проверяем статус заявки
url = fmt.Sprintf("/api/v1/requests/%d", requestID)
resp, body = suite.makeRequest("GET", url, nil, suite.accessToken)
json.Unmarshal(body, &request)
statusObj = request["status"].(map[string]interface{})
assert.Equal(suite.T(), "approved", statusObj["request_status"])
// 6. Проверяем запись в moderator_actions
suite.checkModeratorAction(moderatorID, requestID, "approve_request")
// 7. Проверяем историю статусов
suite.checkRequestStatusHistory(requestID, []string{"pending_moderation", "approved"})
}
// Test 27: Approve request
func (suite *APITestSuite) Test27_ModerationApprove() {
suite.T().Log("Testing: POST /api/v1/moderation/requests/{id}/approve")
// Создаем заявку
_, body := suite.makeRequest("GET", "/api/v1/request-types", nil, "")
var types []map[string]interface{}
json.Unmarshal(body, &types)
requestTypeID := int64(types[0]["id"].(float64))
createReq := map[string]interface{}{
"request_type_id": requestTypeID,
"title": "Approve Test",
"description": "Test",
"latitude": 55.751244,
"longitude": 37.618423,
"address": "Test",
}
_, respBody := suite.makeRequest("POST", "/api/v1/requests", createReq, suite.accessToken)
var request map[string]interface{}
json.Unmarshal(respBody, &request)
requestID := int64(request["id"].(float64))
// Модератор одобряет
moderatorID, moderatorToken := suite.createModerator()
url := fmt.Sprintf("/api/v1/moderation/requests/%d/approve", requestID)
approveReq := map[string]interface{}{}
resp, body := suite.makeRequest("POST", url, approveReq, moderatorToken)
if resp.StatusCode != http.StatusOK {
suite.T().Logf("Approve failed with status %d, body: %s", resp.StatusCode, string(body))
}
assert.Equal(suite.T(), http.StatusOK, resp.StatusCode)
// Проверяем статус
url = fmt.Sprintf("/api/v1/requests/%d", requestID)
_, body = suite.makeRequest("GET", url, nil, suite.accessToken)
json.Unmarshal(body, &request)
statusObj := request["status"].(map[string]interface{})
assert.Equal(suite.T(), "approved", statusObj["request_status"])
suite.checkModeratorAction(moderatorID, requestID, "approve_request")
}
// Test 28: Reject request
func (suite *APITestSuite) Test28_ModerationReject() {
suite.T().Log("Testing: POST /api/v1/moderation/requests/{id}/reject")
// Создаем заявку
_, body := suite.makeRequest("GET", "/api/v1/request-types", nil, "")
var types []map[string]interface{}
json.Unmarshal(body, &types)
requestTypeID := int64(types[0]["id"].(float64))
createReq := map[string]interface{}{
"request_type_id": requestTypeID,
"title": "Reject Test",
"description": "Test",
"latitude": 55.751244,
"longitude": 37.618423,
"address": "Test",
}
_, respBody := suite.makeRequest("POST", "/api/v1/requests", createReq, suite.accessToken)
var request map[string]interface{}
json.Unmarshal(respBody, &request)
requestID := int64(request["id"].(float64))
// Модератор отклоняет
moderatorID, moderatorToken := suite.createModerator()
url := fmt.Sprintf("/api/v1/moderation/requests/%d/reject", requestID)
rejectReq := map[string]interface{}{
"comment": "Does not meet requirements",
}
resp, _ := suite.makeRequest("POST", url, rejectReq, moderatorToken)
assert.Equal(suite.T(), http.StatusOK, resp.StatusCode)
// Проверяем статус
url = fmt.Sprintf("/api/v1/requests/%d", requestID)
_, body = suite.makeRequest("GET", url, nil, suite.accessToken)
json.Unmarshal(body, &request)
statusObj := request["status"].(map[string]interface{})
assert.Equal(suite.T(), "rejected", statusObj["request_status"])
suite.checkModeratorAction(moderatorID, requestID, "reject_request")
}
// Test 29: Reject without comment (should fail)
func (suite *APITestSuite) Test29_ModerationRejectWithoutComment() {
suite.T().Log("Testing: Reject request without comment")
// Создаем заявку
_, body := suite.makeRequest("GET", "/api/v1/request-types", nil, "")
var types []map[string]interface{}
json.Unmarshal(body, &types)
requestTypeID := int64(types[0]["id"].(float64))
createReq := map[string]interface{}{
"request_type_id": requestTypeID,
"title": "Reject No Comment Test",
"description": "Test",
"latitude": 55.751244,
"longitude": 37.618423,
"address": "Test",
}
_, respBody := suite.makeRequest("POST", "/api/v1/requests", createReq, suite.accessToken)
var request map[string]interface{}
json.Unmarshal(respBody, &request)
requestID := int64(request["id"].(float64))
// Модератор пытается отклонить без комментария
_, moderatorToken := suite.createModerator()
url := fmt.Sprintf("/api/v1/moderation/requests/%d/reject", requestID)
rejectReq := map[string]interface{}{
"comment": "",
}
resp, _ := suite.makeRequest("POST", url, rejectReq, moderatorToken)
assert.Equal(suite.T(), http.StatusBadRequest, resp.StatusCode)
}
// Test 30: Moderation without permission (should fail)
func (suite *APITestSuite) Test30_ModerationWithoutPermission() {
suite.T().Log("Testing: Moderation without permission")
// Создаем заявку
_, body := suite.makeRequest("GET", "/api/v1/request-types", nil, "")
var types []map[string]interface{}
json.Unmarshal(body, &types)
requestTypeID := int64(types[0]["id"].(float64))
createReq := map[string]interface{}{
"request_type_id": requestTypeID,
"title": "No Permission Test",
"description": "Test",
"latitude": 55.751244,
"longitude": 37.618423,
"address": "Test",
}
_, respBody := suite.makeRequest("POST", "/api/v1/requests", createReq, suite.accessToken)
var request map[string]interface{}
json.Unmarshal(respBody, &request)
requestID := int64(request["id"].(float64))
// Обычный пользователь пытается модерировать
url := fmt.Sprintf("/api/v1/moderation/requests/%d/approve", requestID)
resp, _ := suite.makeRequest("POST", url, nil, suite.accessToken)
assert.Equal(suite.T(), http.StatusForbidden, resp.StatusCode)
}
// Test 31: Get pending moderation requests
func (suite *APITestSuite) Test31_GetPendingModerationRequests() {
suite.T().Log("Testing: GET /api/v1/moderation/requests/pending")
// Создаем несколько заявок
_, body := suite.makeRequest("GET", "/api/v1/request-types", nil, "")
var types []map[string]interface{}
json.Unmarshal(body, &types)
requestTypeID := int64(types[0]["id"].(float64))
for i := 0; i < 3; i++ {
createReq := map[string]interface{}{
"request_type_id": requestTypeID,
"title": fmt.Sprintf("Pending Request %d", i),
"description": "Test",
"latitude": 55.751244,
"longitude": 37.618423,
"address": "Test",
}
suite.makeRequest("POST", "/api/v1/requests", createReq, suite.accessToken)
}
// Модератор получает список
_, moderatorToken := suite.createModerator()
resp, body := suite.makeRequest("GET", "/api/v1/moderation/requests/pending?limit=10&offset=0", nil, moderatorToken)
assert.Equal(suite.T(), http.StatusOK, resp.StatusCode)
var requests []map[string]interface{}
json.Unmarshal(body, &requests)
assert.NotEmpty(suite.T(), requests)
// Проверяем, что все заявки в статусе pending_moderation
for _, req := range requests {
statusObj := req["status"].(map[string]interface{})
assert.Equal(suite.T(), "pending_moderation", statusObj["request_status"])
}
}
// Test 32: Get my moderated requests
func (suite *APITestSuite) Test32_GetMyModeratedRequests() {
suite.T().Log("Testing: GET /api/v1/moderation/requests/my")
// Создаем модератора
moderatorID, moderatorToken := suite.createModerator()
// Создаем и модерируем несколько заявок
_, body := suite.makeRequest("GET", "/api/v1/request-types", nil, "")
var types []map[string]interface{}
json.Unmarshal(body, &types)
requestTypeID := int64(types[0]["id"].(float64))
moderatedCount := 0
for i := 0; i < 2; i++ {
createReq := map[string]interface{}{
"request_type_id": requestTypeID,
"title": fmt.Sprintf("Moderated Request %d", i),
"description": "Test",
"latitude": 55.751244,
"longitude": 37.618423,
"address": "Test",
}
_, respBody := suite.makeRequest("POST", "/api/v1/requests", createReq, suite.accessToken)
var request map[string]interface{}
json.Unmarshal(respBody, &request)
requestID := int64(request["id"].(float64))
// Одобряем
url := fmt.Sprintf("/api/v1/moderation/requests/%d/approve", requestID)
approveReq := map[string]interface{}{}
resp, _ := suite.makeRequest("POST", url, approveReq, moderatorToken)
if resp.StatusCode == http.StatusOK {
moderatedCount++
}
}
// Получаем историю модерации
resp, body := suite.makeRequest("GET", "/api/v1/moderation/requests/my?limit=10&offset=0", nil, moderatorToken)
assert.Equal(suite.T(), http.StatusOK, resp.StatusCode)
var requests []map[string]interface{}
json.Unmarshal(body, &requests)
assert.NotEmpty(suite.T(), requests)
// Проверяем, что все заявки модерированы этим модератором
for _, req := range requests {
if req["moderated_by"] != nil {
assert.Equal(suite.T(), float64(moderatorID), req["moderated_by"].(float64))
}
}
}
// ============================================================================
// Тесты хранимых процедур
// ============================================================================
// Test33_AcceptVolunteerResponse проверяет успешное принятие отклика волонтера
func (suite *APITestSuite) Test33_AcceptVolunteerResponse() {
// Создаем заявку и одобряем её
requestID := suite.createApprovedRequest(suite.accessToken)
// Создаем волонтера и отклик
volunteerID, responseID, _ := suite.createVolunteerWithResponse(requestID)
// Принимаем отклик через процедуру
url := fmt.Sprintf("/api/v1/requests/%d/responses/%d/accept", requestID, responseID)
resp, body := suite.makeRequest("POST", url, nil, suite.accessToken)
if resp.StatusCode != http.StatusOK {
suite.T().Logf("Accept response failed. Status: %d, Body: %s", resp.StatusCode, string(body))
}
assert.Equal(suite.T(), http.StatusOK, resp.StatusCode)
var result map[string]interface{}
err := json.Unmarshal(body, &result)
require.NoError(suite.T(), err)
// Проверяем результат процедуры
assert.True(suite.T(), result["success"].(bool))
assert.Contains(suite.T(), result["message"].(string), "successfully")
assert.Equal(suite.T(), float64(requestID), result["request_id"].(float64))
assert.Equal(suite.T(), float64(volunteerID), result["volunteer_id"].(float64))
// Проверяем, что заявка перешла в статус in_progress
resp2, body2 := suite.makeRequest("GET", fmt.Sprintf("/api/v1/requests/%d", requestID), nil, suite.accessToken)
assert.Equal(suite.T(), http.StatusOK, resp2.StatusCode)
var request map[string]interface{}
json.Unmarshal(body2, &request)
statusObj := request["status"].(map[string]interface{})
assert.Equal(suite.T(), "in_progress", statusObj["request_status"])
assert.Equal(suite.T(), float64(volunteerID), request["assigned_volunteer_id"].(float64))
// Проверяем историю статусов
suite.checkRequestStatusHistory(requestID, []string{"approved", "in_progress"})
}
// Test34_AcceptVolunteerResponseValidation проверяет валидацию процедуры accept_volunteer_response
func (suite *APITestSuite) Test34_AcceptVolunteerResponseValidation() {
// Создаем заявку от первого пользователя
requestID := suite.createApprovedRequest(suite.accessToken)
// Создаем волонтера с откликом
_, responseID, _ := suite.createVolunteerWithResponse(requestID)
// Создаем второго пользователя (не владелец заявки)
email := fmt.Sprintf("other_user_%d@example.com", time.Now().UnixNano())
registerReq := map[string]interface{}{
"email": email,
"password": "SecureP@ssw0rd123",
"first_name": "Other",
"last_name": "User",
}
_, regBody := suite.makeRequest("POST", "/api/v1/auth/register", registerReq, "")
var authResp map[string]interface{}
json.Unmarshal(regBody, &authResp)
otherUserToken := authResp["access_token"].(string)
// Попытка принять отклик другим пользователем (не владельцем заявки)
url := fmt.Sprintf("/api/v1/requests/%d/responses/%d/accept", requestID, responseID)
resp, body := suite.makeRequest("POST", url, nil, otherUserToken)
// Должна быть ошибка, так как пользователь не владелец заявки
assert.Equal(suite.T(), http.StatusBadRequest, resp.StatusCode)
var result map[string]interface{}
json.Unmarshal(body, &result)
// Процедура возвращает success: false
if result["success"] != nil {
assert.False(suite.T(), result["success"].(bool))
assert.Contains(suite.T(), result["message"].(string), "not the owner")
} else {
// Или ошибка от обработчика
assert.Contains(suite.T(), result["error"].(string), "owner")
}
}
// Test35_CompleteRequestWithRating проверяет успешное завершение заявки с рейтингом
func (suite *APITestSuite) Test35_CompleteRequestWithRating() {
// Создаем заявку, одобряем, принимаем отклик волонтера
requestID := suite.createApprovedRequest(suite.accessToken)
volunteerID, responseID, _ := suite.createVolunteerWithResponse(requestID)
// Принимаем отклик
acceptURL := fmt.Sprintf("/api/v1/requests/%d/responses/%d/accept", requestID, responseID)
suite.makeRequest("POST", acceptURL, nil, suite.accessToken)
// Завершаем заявку с рейтингом
completeReq := map[string]interface{}{
"rating": 5,
"comment": "Отличная работа!",
}
completeURL := fmt.Sprintf("/api/v1/requests/%d/complete", requestID)
resp, body := suite.makeRequest("POST", completeURL, completeReq, suite.accessToken)
assert.Equal(suite.T(), http.StatusOK, resp.StatusCode)
var result map[string]interface{}
err := json.Unmarshal(body, &result)
require.NoError(suite.T(), err)
// Проверяем результат процедуры
assert.True(suite.T(), result["success"].(bool))
assert.Contains(suite.T(), result["message"].(string), "successfully")
assert.NotNil(suite.T(), result["rating_id"])
// Проверяем, что заявка завершена
resp2, body2 := suite.makeRequest("GET", fmt.Sprintf("/api/v1/requests/%d", requestID), nil, suite.accessToken)
assert.Equal(suite.T(), http.StatusOK, resp2.StatusCode)
var request map[string]interface{}
json.Unmarshal(body2, &request)
statusObj := request["status"].(map[string]interface{})
assert.Equal(suite.T(), "completed", statusObj["request_status"])
// completed_at может быть null, если поле не заполнилось
if request["completed_at"] == nil {
suite.T().Logf("Request response: %s", string(body2))
}
assert.NotNil(suite.T(), request["completed_at"])
// Проверяем, что рейтинг создан
ratingID := int64(result["rating_id"].(float64))
rating, err := suite.queries.GetRatingByResponseID(context.Background(), responseID)
require.NoError(suite.T(), err)
assert.Equal(suite.T(), ratingID, rating.ID)
assert.Equal(suite.T(), int32(5), rating.Rating)
assert.Equal(suite.T(), volunteerID, rating.VolunteerID)
// Проверяем историю статусов
suite.checkRequestStatusHistory(requestID, []string{"approved", "in_progress", "completed"})
}
// Test36_CompleteRequestWithRatingValidation проверяет валидацию рейтинга
func (suite *APITestSuite) Test36_CompleteRequestWithRatingValidation() {
// Создаем заявку в статусе in_progress
requestID := suite.createApprovedRequest(suite.accessToken)
_, responseID, _ := suite.createVolunteerWithResponse(requestID)
acceptURL := fmt.Sprintf("/api/v1/requests/%d/responses/%d/accept", requestID, responseID)
suite.makeRequest("POST", acceptURL, nil, suite.accessToken)
// Попытка завершить с неправильным рейтингом (вне диапазона 1-5)
completeReq := map[string]interface{}{
"rating": 10, // Неверный рейтинг
}
completeURL := fmt.Sprintf("/api/v1/requests/%d/complete", requestID)
resp, body := suite.makeRequest("POST", completeURL, completeReq, suite.accessToken)
assert.Equal(suite.T(), http.StatusBadRequest, resp.StatusCode)
var errorResp map[string]interface{}
json.Unmarshal(body, &errorResp)
assert.Contains(suite.T(), errorResp["error"].(string), "between 1 and 5")
// Попытка с рейтингом 0
completeReq2 := map[string]interface{}{
"rating": 0,
}
resp2, body2 := suite.makeRequest("POST", completeURL, completeReq2, suite.accessToken)
assert.Equal(suite.T(), http.StatusBadRequest, resp2.StatusCode)
var errorResp2 map[string]interface{}
json.Unmarshal(body2, &errorResp2)
assert.Contains(suite.T(), errorResp2["error"].(string), "between 1 and 5")
}
// Test37_CompleteRequestNotInProgress проверяет, что нельзя завершить заявку не в статусе in_progress
func (suite *APITestSuite) Test37_CompleteRequestNotInProgress() {
// Создаем заявку (будет в статусе pending_moderation)
_, body := suite.makeRequest("GET", "/api/v1/request-types", nil, "")
var types []map[string]interface{}
json.Unmarshal(body, &types)
requestTypeID := int64(types[0]["id"].(float64))
createReq := map[string]interface{}{
"request_type_id": requestTypeID,
"title": "Request to complete",
"description": "Test",
"latitude": 55.751244,
"longitude": 37.618423,
"address": "Test",
}
_, respBody := suite.makeRequest("POST", "/api/v1/requests", createReq, suite.accessToken)
var request map[string]interface{}
json.Unmarshal(respBody, &request)
requestID := int64(request["id"].(float64))
// Попытка завершить заявку в статусе pending_moderation
completeReq := map[string]interface{}{
"rating": 5,
}
completeURL := fmt.Sprintf("/api/v1/requests/%d/complete", requestID)
resp, body := suite.makeRequest("POST", completeURL, completeReq, suite.accessToken)
// Процедура должна вернуть ошибку
assert.Equal(suite.T(), http.StatusBadRequest, resp.StatusCode)
var result map[string]interface{}
json.Unmarshal(body, &result)
// Хранимая процедура возвращает success: false, которое обработчик преобразует в error
if result["error"] != nil {
// Проверяем сообщение об ошибке содержит информацию о статусе
assert.Contains(suite.T(), result["error"].(string), "in_progress")
} else {
suite.T().Logf("Response: %s", string(body))
suite.T().Fatal("Expected error in response")
}
}