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