On branch main
new file: main_dc/yalarba/api_yal/cmd/testrunner/README.md new file: main_dc/yalarba/api_yal/cmd/testrunner/main.go new file: main_dc/yalarba/api_yal/cmd/testrunner/runner.go deleted: main_dc/yalarba/api_yal/test/intergration/auth_integration_test.go deleted: main_dc/yalarba/api_yal/test/intergration/objects_integration_test.go deleted: main_dc/yalarba/api_yal/test/intergration/setup_test.go deleted: main_dc/yalarba/api_yal/test/setup_test.go new file: main_dc/yalarba/api_yal/tests/integration/account_test.go new file: main_dc/yalarba/api_yal/tests/integration/appeal_test.go new file: main_dc/yalarba/api_yal/tests/integration/auth_test.go new file: main_dc/yalarba/api_yal/tests/integration/comment_test.go new file: main_dc/yalarba/api_yal/tests/integration/feedback_test.go new file: main_dc/yalarba/api_yal/tests/integration/object_test.go new file: main_dc/yalarba/api_yal/tests/integration/rating_test.go new file: main_dc/yalarba/api_yal/tests/testutils/client.go new file: main_dc/yalarba/api_yal/tests/testutils/fixtures.go new file: main_dc/yalarba/api_yal/tests/testutils/setup.go write tests
This commit is contained in:
@@ -0,0 +1,37 @@
|
|||||||
|
# Создаем директорию для тестового раннера
|
||||||
|
mkdir -p cmd/testrunner
|
||||||
|
|
||||||
|
# Сохраняем файл main.go в cmd/testrunner/
|
||||||
|
|
||||||
|
# Запуск в обычном режиме
|
||||||
|
go run cmd/testrunner/main.go
|
||||||
|
|
||||||
|
# Запуск с параметрами
|
||||||
|
go run cmd/testrunner/main.go -suite auth -verbose
|
||||||
|
go run cmd/testrunner/main.go -suite all -coverage
|
||||||
|
go run cmd/testrunner/main.go -suite all -parallel
|
||||||
|
|
||||||
|
# Интерактивный режим
|
||||||
|
go run cmd/testrunner/main.go interactive
|
||||||
|
|
||||||
|
# Запуск бенчмарков
|
||||||
|
go run cmd/testrunner/main.go bench
|
||||||
|
|
||||||
|
# Мониторинг сервера
|
||||||
|
go run cmd/testrunner/main.go watch
|
||||||
|
|
||||||
|
# Сборка для текущей платформы
|
||||||
|
go build -o testrunner.exe cmd/testrunner/main.go
|
||||||
|
|
||||||
|
# Запуск собранного бинарника
|
||||||
|
./testrunner.exe -suite all -verbose
|
||||||
|
|
||||||
|
# Кроссплатформенная сборка
|
||||||
|
# Windows
|
||||||
|
GOOS=windows GOARCH=amd64 go build -o testrunner-windows.exe cmd/testrunner/main.go
|
||||||
|
|
||||||
|
# Linux
|
||||||
|
GOOS=linux GOARCH=amd64 go build -o testrunner-linux cmd/testrunner/main.go
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
GOOS=darwin GOARCH=amd64 go build -o testrunner-macos cmd/testrunner/main.go
|
||||||
@@ -0,0 +1,574 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Конфигурация
|
||||||
|
type Config struct {
|
||||||
|
ServerURL string
|
||||||
|
ServerPort string
|
||||||
|
TestTimeout time.Duration
|
||||||
|
Verbose bool
|
||||||
|
Parallel bool
|
||||||
|
Coverage bool
|
||||||
|
TestSuite string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Цвета для вывода
|
||||||
|
type Colors struct {
|
||||||
|
Reset string
|
||||||
|
Red string
|
||||||
|
Green string
|
||||||
|
Yellow string
|
||||||
|
Blue string
|
||||||
|
Cyan string
|
||||||
|
}
|
||||||
|
|
||||||
|
var colors Colors
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// Определяем поддержку цветов
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
// На Windows включаем ANSI colors для новых версий
|
||||||
|
colors = Colors{
|
||||||
|
Reset: "\033[0m",
|
||||||
|
Red: "\033[31m",
|
||||||
|
Green: "\033[32m",
|
||||||
|
Yellow: "\033[33m",
|
||||||
|
Blue: "\033[34m",
|
||||||
|
Cyan: "\033[36m",
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
colors = Colors{
|
||||||
|
Reset: "\033[0m",
|
||||||
|
Red: "\033[31m",
|
||||||
|
Green: "\033[32m",
|
||||||
|
Yellow: "\033[33m",
|
||||||
|
Blue: "\033[34m",
|
||||||
|
Cyan: "\033[36m",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Тестовый набор
|
||||||
|
type TestSuite struct {
|
||||||
|
Name string
|
||||||
|
Pattern string
|
||||||
|
Timeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// Результат теста
|
||||||
|
type TestResult struct {
|
||||||
|
Suite string
|
||||||
|
Passed bool
|
||||||
|
Duration time.Duration
|
||||||
|
Output string
|
||||||
|
Error error
|
||||||
|
Coverage float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
config := parseFlags()
|
||||||
|
|
||||||
|
printBanner()
|
||||||
|
|
||||||
|
// Проверяем сервер
|
||||||
|
if !checkServer(config) {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Очищаем кэш тестов
|
||||||
|
cleanTestCache()
|
||||||
|
|
||||||
|
// Запускаем тесты
|
||||||
|
results := runTests(config)
|
||||||
|
|
||||||
|
// Выводим результаты
|
||||||
|
printResults(results)
|
||||||
|
|
||||||
|
// Завершаем с соответствующим кодом
|
||||||
|
if hasFailures(results) {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseFlags() *Config {
|
||||||
|
config := &Config{}
|
||||||
|
|
||||||
|
flag.StringVar(&config.ServerURL, "server-url", "http://localhost:8088", "API server URL")
|
||||||
|
flag.StringVar(&config.ServerPort, "server-port", "8088", "API server port")
|
||||||
|
flag.DurationVar(&config.TestTimeout, "timeout", 30*time.Minute, "Test timeout")
|
||||||
|
flag.BoolVar(&config.Verbose, "verbose", false, "Verbose output")
|
||||||
|
flag.BoolVar(&config.Parallel, "parallel", false, "Run tests in parallel")
|
||||||
|
flag.BoolVar(&config.Coverage, "coverage", false, "Generate coverage report")
|
||||||
|
flag.StringVar(&config.TestSuite, "suite", "all", "Test suite to run (all, auth, account, object, feedback, comment, rating, appeal)")
|
||||||
|
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
func printBanner() {
|
||||||
|
fmt.Printf("%s╔════════════════════════════════════════════════════════════════╗%s\n", colors.Cyan, colors.Reset)
|
||||||
|
fmt.Printf("%s║ Go Test Runner for API ║%s\n", colors.Cyan, colors.Reset)
|
||||||
|
fmt.Printf("%s║ Version 1.0.0 ║%s\n", colors.Cyan, colors.Reset)
|
||||||
|
fmt.Printf("%s╚════════════════════════════════════════════════════════════════╝%s\n", colors.Cyan, colors.Reset)
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkServer(config *Config) bool {
|
||||||
|
fmt.Printf("%s🔍 Checking server status...%s\n", colors.Yellow, colors.Reset)
|
||||||
|
|
||||||
|
client := &http.Client{Timeout: 5 * time.Second}
|
||||||
|
resp, err := client.Get(fmt.Sprintf("%s/health", config.ServerURL))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("%s❌ Server is not running on %s%s\n", colors.Red, config.ServerURL, colors.Reset)
|
||||||
|
fmt.Printf("%s💡 Please start the server first: go run cmd/main.go%s\n", colors.Yellow, colors.Reset)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode == http.StatusOK {
|
||||||
|
fmt.Printf("%s✅ Server is running on %s%s\n\n", colors.Green, config.ServerURL, colors.Reset)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("%s❌ Server returned status: %d%s\n", colors.Red, resp.StatusCode, colors.Reset)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanTestCache() {
|
||||||
|
fmt.Printf("%s🧹 Cleaning test cache...%s\n", colors.Yellow, colors.Reset)
|
||||||
|
cmd := exec.Command("go", "clean", "-testcache")
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
fmt.Printf("%s⚠️ Warning: failed to clean test cache: %v%s\n", colors.Yellow, err, colors.Reset)
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTestSuites() []TestSuite {
|
||||||
|
return []TestSuite{
|
||||||
|
{Name: "All", Pattern: "", Timeout: 30 * time.Minute},
|
||||||
|
{Name: "Auth", Pattern: "TestAuthFlow", Timeout: 5 * time.Minute},
|
||||||
|
{Name: "Account", Pattern: "TestAccountEndpoints", Timeout: 5 * time.Minute},
|
||||||
|
{Name: "Object", Pattern: "TestObjectEndpoints", Timeout: 5 * time.Minute},
|
||||||
|
{Name: "Feedback", Pattern: "TestFeedbackEndpoints", Timeout: 5 * time.Minute},
|
||||||
|
{Name: "Comment", Pattern: "TestCommentEndpoints", Timeout: 5 * time.Minute},
|
||||||
|
{Name: "Rating", Pattern: "TestRatingEndpoints", Timeout: 5 * time.Minute},
|
||||||
|
{Name: "Appeal", Pattern: "TestAppealEndpoints", Timeout: 5 * time.Minute},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runTests(config *Config) []TestResult {
|
||||||
|
suites := getTestSuites()
|
||||||
|
var selectedSuites []TestSuite
|
||||||
|
|
||||||
|
if config.TestSuite == "all" {
|
||||||
|
selectedSuites = suites
|
||||||
|
} else {
|
||||||
|
for _, suite := range suites {
|
||||||
|
if strings.EqualFold(suite.Name, config.TestSuite) {
|
||||||
|
selectedSuites = []TestSuite{suite}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(selectedSuites) == 0 {
|
||||||
|
fmt.Printf("%s❌ Unknown test suite: %s%s\n", colors.Red, config.TestSuite, colors.Reset)
|
||||||
|
fmt.Printf("%s📚 Available suites: all, auth, account, object, feedback, comment, rating, appeal%s\n", colors.Yellow, colors.Reset)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var results []TestResult
|
||||||
|
|
||||||
|
if config.Parallel && len(selectedSuites) > 1 {
|
||||||
|
results = runTestsParallel(selectedSuites, config)
|
||||||
|
} else {
|
||||||
|
for _, suite := range selectedSuites {
|
||||||
|
if suite.Name == "All" {
|
||||||
|
result := runTestSuite(suite, config)
|
||||||
|
results = append(results, result)
|
||||||
|
} else {
|
||||||
|
result := runTestSuite(suite, config)
|
||||||
|
results = append(results, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
func runTestsParallel(suites []TestSuite, config *Config) []TestResult {
|
||||||
|
fmt.Printf("%s🚀 Running tests in parallel mode...%s\n\n", colors.Cyan, colors.Reset)
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
results := make([]TestResult, len(suites))
|
||||||
|
|
||||||
|
// Пропускаем "All" suite в параллельном режиме
|
||||||
|
var filteredSuites []TestSuite
|
||||||
|
for _, suite := range suites {
|
||||||
|
if suite.Name != "All" {
|
||||||
|
filteredSuites = append(filteredSuites, suite)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, suite := range filteredSuites {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(idx int, s TestSuite) {
|
||||||
|
defer wg.Done()
|
||||||
|
results[idx] = runTestSuite(s, config)
|
||||||
|
}(i, suite)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
func runTestSuite(suite TestSuite, config *Config) TestResult {
|
||||||
|
startTime := time.Now()
|
||||||
|
|
||||||
|
fmt.Printf("%s📦 Running %s tests...%s\n", colors.Blue, suite.Name, colors.Reset)
|
||||||
|
|
||||||
|
cmd := exec.Command("go", "test")
|
||||||
|
|
||||||
|
if config.Verbose {
|
||||||
|
cmd.Args = append(cmd.Args, "-v")
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Args = append(cmd.Args, "-timeout", suite.Timeout.String())
|
||||||
|
|
||||||
|
if suite.Pattern != "" {
|
||||||
|
cmd.Args = append(cmd.Args, "-run", suite.Pattern)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Coverage && suite.Name == "All" {
|
||||||
|
cmd.Args = append(cmd.Args, "-coverprofile=coverage.out")
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Args = append(cmd.Args, "./tests/integration/...")
|
||||||
|
|
||||||
|
// Захват вывода
|
||||||
|
var stdout, stderr bytes.Buffer
|
||||||
|
cmd.Stdout = &stdout
|
||||||
|
cmd.Stderr = &stderr
|
||||||
|
|
||||||
|
// Запуск с таймаутом
|
||||||
|
_, cancel := context.WithTimeout(context.Background(), suite.Timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
err := cmd.Run()
|
||||||
|
duration := time.Since(startTime)
|
||||||
|
|
||||||
|
result := TestResult{
|
||||||
|
Suite: suite.Name,
|
||||||
|
Duration: duration,
|
||||||
|
Output: stdout.String(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
result.Passed = false
|
||||||
|
result.Error = err
|
||||||
|
if stderr.Len() > 0 {
|
||||||
|
result.Output += "\n" + stderr.String()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.Passed = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Извлечение покрытия, если нужно
|
||||||
|
if config.Coverage && suite.Name == "All" && result.Passed {
|
||||||
|
result.Coverage = extractCoverage()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Вывод результата
|
||||||
|
if result.Passed {
|
||||||
|
fmt.Printf("%s✅ %s tests passed %s(%v)%s\n", colors.Green, suite.Name, colors.Green, duration, colors.Reset)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("%s❌ %s tests failed %s(%v)%s\n", colors.Red, suite.Name, colors.Red, duration, colors.Reset)
|
||||||
|
if config.Verbose {
|
||||||
|
fmt.Println(result.Output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractCoverage() float64 {
|
||||||
|
// Проверяем наличие файла покрытия
|
||||||
|
if _, err := os.Stat("coverage.out"); os.IsNotExist(err) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Запускаем go tool cover для получения процента покрытия
|
||||||
|
cmd := exec.Command("go", "tool", "cover", "-func=coverage.out")
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Парсим вывод для получения total
|
||||||
|
lines := strings.Split(string(output), "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
if strings.Contains(line, "total:") {
|
||||||
|
// Извлекаем процент
|
||||||
|
parts := strings.Fields(line)
|
||||||
|
if len(parts) >= 3 {
|
||||||
|
percentage := strings.TrimSuffix(parts[2], "%")
|
||||||
|
var coverage float64
|
||||||
|
fmt.Sscanf(percentage, "%f", &coverage)
|
||||||
|
return coverage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func printResults(results []TestResult) {
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Printf("%s╔════════════════════════════════════════════════════════════════╗%s\n", colors.Cyan, colors.Reset)
|
||||||
|
fmt.Printf("%s║ Test Results ║%s\n", colors.Cyan, colors.Reset)
|
||||||
|
fmt.Printf("%s╚════════════════════════════════════════════════════════════════╝%s\n", colors.Cyan, colors.Reset)
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
// Таблица результатов
|
||||||
|
fmt.Printf("%-15s %-10s %-15s %s\n", "Suite", "Status", "Duration", "Coverage")
|
||||||
|
fmt.Println(strings.Repeat("-", 60))
|
||||||
|
|
||||||
|
var totalTests int
|
||||||
|
var passedTests int
|
||||||
|
var totalDuration time.Duration
|
||||||
|
|
||||||
|
for _, result := range results {
|
||||||
|
status := fmt.Sprintf("%s✓ PASSED%s", colors.Green, colors.Reset)
|
||||||
|
if !result.Passed {
|
||||||
|
status = fmt.Sprintf("%s✗ FAILED%s", colors.Red, colors.Reset)
|
||||||
|
}
|
||||||
|
|
||||||
|
coverage := "-"
|
||||||
|
if result.Coverage > 0 {
|
||||||
|
coverage = fmt.Sprintf("%.1f%%", result.Coverage)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("%-15s %-10s %-15v %s\n",
|
||||||
|
result.Suite,
|
||||||
|
status,
|
||||||
|
result.Duration.Round(time.Millisecond),
|
||||||
|
coverage)
|
||||||
|
|
||||||
|
totalTests++
|
||||||
|
if result.Passed {
|
||||||
|
passedTests++
|
||||||
|
}
|
||||||
|
totalDuration += result.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(strings.Repeat("-", 60))
|
||||||
|
fmt.Printf("%-15s %d/%d passed\n", "TOTAL:", passedTests, totalTests)
|
||||||
|
fmt.Printf("%-15s %v\n", "TIME:", totalDuration.Round(time.Millisecond))
|
||||||
|
|
||||||
|
// Генерация HTML отчета при покрытии
|
||||||
|
if len(results) > 0 && results[0].Coverage > 0 {
|
||||||
|
generateHTMLReport()
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateHTMLReport() {
|
||||||
|
fmt.Printf("%s📊 Generating HTML coverage report...%s\n", colors.Yellow, colors.Reset)
|
||||||
|
|
||||||
|
cmd := exec.Command("go", "tool", "cover", "-html=coverage.out", "-o", "coverage.html")
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
fmt.Printf("%s⚠️ Failed to generate HTML report: %v%s\n", colors.Yellow, err, colors.Reset)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("%s✅ Coverage report generated: coverage.html%s\n", colors.Green, colors.Reset)
|
||||||
|
|
||||||
|
// Открываем отчет в браузере
|
||||||
|
var openCmd *exec.Cmd
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "windows":
|
||||||
|
openCmd = exec.Command("cmd", "/c", "start", "coverage.html")
|
||||||
|
case "darwin":
|
||||||
|
openCmd = exec.Command("open", "coverage.html")
|
||||||
|
default:
|
||||||
|
openCmd = exec.Command("xdg-open", "coverage.html")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := openCmd.Run(); err != nil {
|
||||||
|
fmt.Printf("%s⚠️ Could not open browser: %v%s\n", colors.Yellow, err, colors.Reset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasFailures(results []TestResult) bool {
|
||||||
|
for _, result := range results {
|
||||||
|
if !result.Passed {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Интерактивный режим
|
||||||
|
func interactiveMode() {
|
||||||
|
reader := bufio.NewReader(os.Stdin)
|
||||||
|
|
||||||
|
for {
|
||||||
|
printMenu()
|
||||||
|
|
||||||
|
fmt.Print("Enter your choice: ")
|
||||||
|
input, _ := reader.ReadString('\n')
|
||||||
|
input = strings.TrimSpace(input)
|
||||||
|
|
||||||
|
switch input {
|
||||||
|
case "1":
|
||||||
|
runWithConfig(&Config{TestSuite: "all", Verbose: true})
|
||||||
|
case "2":
|
||||||
|
runWithConfig(&Config{TestSuite: "auth", Verbose: true})
|
||||||
|
case "3":
|
||||||
|
runWithConfig(&Config{TestSuite: "account", Verbose: true})
|
||||||
|
case "4":
|
||||||
|
runWithConfig(&Config{TestSuite: "object", Verbose: true})
|
||||||
|
case "5":
|
||||||
|
runWithConfig(&Config{TestSuite: "feedback", Verbose: true})
|
||||||
|
case "6":
|
||||||
|
runWithConfig(&Config{TestSuite: "comment", Verbose: true})
|
||||||
|
case "7":
|
||||||
|
runWithConfig(&Config{TestSuite: "rating", Verbose: true})
|
||||||
|
case "8":
|
||||||
|
runWithConfig(&Config{TestSuite: "appeal", Verbose: true})
|
||||||
|
case "9":
|
||||||
|
runWithConfig(&Config{TestSuite: "all", Coverage: true, Verbose: false})
|
||||||
|
case "10":
|
||||||
|
runWithConfig(&Config{TestSuite: "all", Parallel: true, Verbose: false})
|
||||||
|
case "0":
|
||||||
|
fmt.Printf("%s👋 Goodbye!%s\n", colors.Yellow, colors.Reset)
|
||||||
|
os.Exit(0)
|
||||||
|
default:
|
||||||
|
fmt.Printf("%s❌ Invalid choice! Please try again.%s\n", colors.Red, colors.Reset)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Print("\nPress Enter to continue...")
|
||||||
|
reader.ReadString('\n')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func printMenu() {
|
||||||
|
fmt.Printf("\n%s╔════════════════════════════════════════════════════════════════╗%s\n", colors.Cyan, colors.Reset)
|
||||||
|
fmt.Printf("%s║ Interactive Menu ║%s\n", colors.Cyan, colors.Reset)
|
||||||
|
fmt.Printf("%s╚════════════════════════════════════════════════════════════════╝%s\n", colors.Cyan, colors.Reset)
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Printf("%s1.%s Run all tests\n", colors.Green, colors.Reset)
|
||||||
|
fmt.Printf("%s2.%s Run auth tests only\n", colors.Green, colors.Reset)
|
||||||
|
fmt.Printf("%s3.%s Run account tests only\n", colors.Green, colors.Reset)
|
||||||
|
fmt.Printf("%s4.%s Run object tests only\n", colors.Green, colors.Reset)
|
||||||
|
fmt.Printf("%s5.%s Run feedback tests only\n", colors.Green, colors.Reset)
|
||||||
|
fmt.Printf("%s6.%s Run comment tests only\n", colors.Green, colors.Reset)
|
||||||
|
fmt.Printf("%s7.%s Run rating tests only\n", colors.Green, colors.Reset)
|
||||||
|
fmt.Printf("%s8.%s Run appeal tests only\n", colors.Green, colors.Reset)
|
||||||
|
fmt.Printf("%s9.%s Run all tests with coverage\n", colors.Green, colors.Reset)
|
||||||
|
fmt.Printf("%s10.%s Run all tests in parallel\n", colors.Green, colors.Reset)
|
||||||
|
fmt.Printf("%s0.%s Exit\n", colors.Red, colors.Reset)
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
|
||||||
|
func runWithConfig(config *Config) {
|
||||||
|
config.ServerURL = "http://localhost:8088"
|
||||||
|
config.TestTimeout = 30 * time.Minute
|
||||||
|
|
||||||
|
printBanner()
|
||||||
|
|
||||||
|
if !checkServer(config) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanTestCache()
|
||||||
|
results := runTests(config)
|
||||||
|
printResults(results)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция для мониторинга сервера
|
||||||
|
func watchServer(config *Config) {
|
||||||
|
fmt.Printf("%s👁️ Starting server health monitor...%s\n", colors.Yellow, colors.Reset)
|
||||||
|
|
||||||
|
ticker := time.NewTicker(10 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for range ticker.C {
|
||||||
|
if !checkServer(config) {
|
||||||
|
fmt.Printf("%s⚠️ Server is down!%s\n", colors.Red, colors.Reset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Структура для бенчмарков
|
||||||
|
type BenchmarkResult struct {
|
||||||
|
Name string
|
||||||
|
Ops int64
|
||||||
|
NsPerOp time.Duration
|
||||||
|
Allocs int64
|
||||||
|
Bytes int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func runBenchmarks() {
|
||||||
|
fmt.Printf("%s🏃 Running benchmarks...%s\n\n", colors.Yellow, colors.Reset)
|
||||||
|
|
||||||
|
cmd := exec.Command("go", "test", "-bench=.", "-benchmem", "./tests/integration/...")
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("%s❌ Benchmarks failed: %v%s\n", colors.Red, err, colors.Reset)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Точка входа с поддержкой аргументов командной строки
|
||||||
|
func mainWithArgs() {
|
||||||
|
if len(os.Args) > 1 && os.Args[1] == "interactive" {
|
||||||
|
interactiveMode()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(os.Args) > 1 && os.Args[1] == "bench" {
|
||||||
|
runBenchmarks()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(os.Args) > 1 && os.Args[1] == "watch" {
|
||||||
|
config := parseFlags()
|
||||||
|
watchServer(config)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обычный режим с флагами
|
||||||
|
config := parseFlags()
|
||||||
|
printBanner()
|
||||||
|
|
||||||
|
if !checkServer(config) {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanTestCache()
|
||||||
|
results := runTests(config)
|
||||||
|
printResults(results)
|
||||||
|
|
||||||
|
if hasFailures(results) {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
// cmd/testrunner/runner.go - Дополнительные утилиты
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Сохранение результатов в JSON
|
||||||
|
func saveResultsToJSON(results []TestResult, filename string) error {
|
||||||
|
type JSONResult struct {
|
||||||
|
Timestamp string `json:"timestamp"`
|
||||||
|
Results []TestResult `json:"results"`
|
||||||
|
Summary struct {
|
||||||
|
Total int `json:"total"`
|
||||||
|
Passed int `json:"passed"`
|
||||||
|
Failed int `json:"failed"`
|
||||||
|
PassRate float64 `json:"pass_rate"`
|
||||||
|
} `json:"summary"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var jsonResult JSONResult
|
||||||
|
jsonResult.Timestamp = time.Now().Format(time.RFC3339)
|
||||||
|
jsonResult.Results = results
|
||||||
|
|
||||||
|
for _, r := range results {
|
||||||
|
jsonResult.Summary.Total++
|
||||||
|
if r.Passed {
|
||||||
|
jsonResult.Summary.Passed++
|
||||||
|
} else {
|
||||||
|
jsonResult.Summary.Failed++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if jsonResult.Summary.Total > 0 {
|
||||||
|
jsonResult.Summary.PassRate = float64(jsonResult.Summary.Passed) / float64(jsonResult.Summary.Total) * 100
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.MarshalIndent(jsonResult, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return os.WriteFile(filename, data, 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создание отчета в формате JUnit XML (для CI/CD)
|
||||||
|
func generateJUnitReport(results []TestResult, filename string) error {
|
||||||
|
type TestCase struct {
|
||||||
|
Name string `xml:"name,attr"`
|
||||||
|
Classname string `xml:"classname,attr"`
|
||||||
|
Time float64 `xml:"time,attr"`
|
||||||
|
Failure *string `xml:"failure,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TestSuite struct {
|
||||||
|
Name string `xml:"name,attr"`
|
||||||
|
Tests int `xml:"tests,attr"`
|
||||||
|
Failures int `xml:"failures,attr"`
|
||||||
|
Time float64 `xml:"time,attr"`
|
||||||
|
TestCases []TestCase `xml:"testcase"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TestSuites struct {
|
||||||
|
XMLName string `xml:"testsuites"`
|
||||||
|
TestSuites []TestSuite `xml:"testsuite"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var suites TestSuites
|
||||||
|
|
||||||
|
for _, result := range results {
|
||||||
|
suite := TestSuite{
|
||||||
|
Name: result.Suite,
|
||||||
|
Tests: 1,
|
||||||
|
Time: result.Duration.Seconds(),
|
||||||
|
}
|
||||||
|
|
||||||
|
testCase := TestCase{
|
||||||
|
Name: fmt.Sprintf("%s Suite", result.Suite),
|
||||||
|
Classname: "api.tests",
|
||||||
|
Time: result.Duration.Seconds(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if !result.Passed {
|
||||||
|
suite.Failures = 1
|
||||||
|
failureMsg := result.Error.Error()
|
||||||
|
testCase.Failure = &failureMsg
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.TestCases = append(suite.TestCases, testCase)
|
||||||
|
suites.TestSuites = append(suites.TestSuites, suite)
|
||||||
|
}
|
||||||
|
|
||||||
|
xmlData, err := xml.MarshalIndent(suites, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return os.WriteFile(filename, append([]byte(xml.Header), xmlData...), 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция для запуска определенных тестов по тегам
|
||||||
|
func runTestsByTag(tag string, config *Config) []TestResult {
|
||||||
|
fmt.Printf("%s🏷️ Running tests with tag: %s%s\n", colors.Yellow, tag, colors.Reset)
|
||||||
|
|
||||||
|
cmd := exec.Command("go", "test", "-v", "-tags", tag, "./tests/integration/...")
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
|
||||||
|
result := TestResult{
|
||||||
|
Suite: fmt.Sprintf("Tag: %s", tag),
|
||||||
|
Output: string(output),
|
||||||
|
Passed: err == nil,
|
||||||
|
Error: err,
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.Passed {
|
||||||
|
fmt.Printf("%s✅ Tests with tag '%s' passed%s\n", colors.Green, tag, colors.Reset)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("%s❌ Tests with tag '%s' failed%s\n", colors.Red, tag, colors.Reset)
|
||||||
|
}
|
||||||
|
|
||||||
|
return []TestResult{result}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создание скрипта для CI/CD
|
||||||
|
func generateCIScript() {
|
||||||
|
ciScript := `#!/bin/bash
|
||||||
|
# CI/CD script for API tests
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "Starting API Tests in CI environment"
|
||||||
|
|
||||||
|
# Запуск сервера в фоне
|
||||||
|
go run cmd/main.go &
|
||||||
|
SERVER_PID=$!
|
||||||
|
|
||||||
|
# Ждем запуска сервера
|
||||||
|
sleep 5
|
||||||
|
|
||||||
|
# Запуск тестов
|
||||||
|
go run cmd/testrunner/main.go -suite all -coverage
|
||||||
|
|
||||||
|
# Остановка сервера
|
||||||
|
kill $SERVER_PID
|
||||||
|
|
||||||
|
echo "Tests completed"
|
||||||
|
`
|
||||||
|
|
||||||
|
if err := os.WriteFile("scripts/ci_test.sh", []byte(ciScript), 0755); err != nil {
|
||||||
|
fmt.Printf("Failed to generate CI script: %v\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция для сравнения результатов тестов
|
||||||
|
func compareResults(current, previous []TestResult) {
|
||||||
|
fmt.Printf("%s📊 Test Result Comparison%s\n", colors.Cyan, colors.Reset)
|
||||||
|
fmt.Println(strings.Repeat("-", 50))
|
||||||
|
|
||||||
|
currentMap := make(map[string]TestResult)
|
||||||
|
for _, r := range current {
|
||||||
|
currentMap[r.Suite] = r
|
||||||
|
}
|
||||||
|
|
||||||
|
prevMap := make(map[string]TestResult)
|
||||||
|
for _, r := range previous {
|
||||||
|
prevMap[r.Suite] = r
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, currentResult := range currentMap {
|
||||||
|
prevResult, exists := prevMap[name]
|
||||||
|
if !exists {
|
||||||
|
fmt.Printf("%s➕ New test suite: %s%s\n", colors.Green, name, colors.Reset)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentResult.Passed != prevResult.Passed {
|
||||||
|
if currentResult.Passed {
|
||||||
|
fmt.Printf("%s✅ Fixed: %s (was failing)%s\n", colors.Green, name, colors.Reset)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("%s❌ Regression: %s (was passing)%s\n", colors.Red, name, colors.Reset)
|
||||||
|
}
|
||||||
|
} else if currentResult.Passed {
|
||||||
|
// Сравнение времени выполнения
|
||||||
|
diff := currentResult.Duration - prevResult.Duration
|
||||||
|
if diff > 0 {
|
||||||
|
fmt.Printf("%s⚠️ Slower: %s (+%v)%s\n", colors.Yellow, name, diff.Round(time.Millisecond), colors.Reset)
|
||||||
|
} else if diff < 0 {
|
||||||
|
fmt.Printf("%s⚡ Faster: %s (%v)%s\n", colors.Green, name, (-diff).Round(time.Millisecond), colors.Reset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
package integration
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestAuthIntegration_Register(t *testing.T) {
|
|
||||||
// Очищаем БД перед тестом
|
|
||||||
CleanDatabase(t)
|
|
||||||
|
|
||||||
testCases := []struct {
|
|
||||||
name string
|
|
||||||
payload map[string]interface{}
|
|
||||||
wantStatus int
|
|
||||||
wantError bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Успешная регистрация",
|
|
||||||
payload: map[string]interface{}{
|
|
||||||
"email": "test@example.com",
|
|
||||||
"password": "password123",
|
|
||||||
"name": "Test User",
|
|
||||||
},
|
|
||||||
wantStatus: http.StatusCreated,
|
|
||||||
wantError: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Регистрация с существующим email",
|
|
||||||
payload: map[string]interface{}{
|
|
||||||
"email": "test@example.com", // тот же email
|
|
||||||
"password": "password123",
|
|
||||||
"name": "Another User",
|
|
||||||
},
|
|
||||||
wantStatus: http.StatusConflict,
|
|
||||||
wantError: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Невалидные данные",
|
|
||||||
payload: map[string]interface{}{
|
|
||||||
"email": "invalid-email",
|
|
||||||
},
|
|
||||||
wantStatus: http.StatusBadRequest,
|
|
||||||
wantError: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range testCases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
body, _ := json.Marshal(tc.payload)
|
|
||||||
req := httptest.NewRequest("POST", "/api/v1/auth/register", bytes.NewBuffer(body))
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
rr := httptest.NewRecorder()
|
|
||||||
|
|
||||||
TestRouter.ServeHTTP(rr, req)
|
|
||||||
|
|
||||||
assert.Equal(t, tc.wantStatus, rr.Code)
|
|
||||||
|
|
||||||
if !tc.wantError {
|
|
||||||
var response map[string]interface{}
|
|
||||||
err := json.Unmarshal(rr.Body.Bytes(), &response)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Contains(t, response, "user_id")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAuthIntegration_Login(t *testing.T) {
|
|
||||||
CleanDatabase(t)
|
|
||||||
|
|
||||||
// Сначала создаем пользователя
|
|
||||||
registerPayload := map[string]interface{}{
|
|
||||||
"email": "logintest@example.com",
|
|
||||||
"password": "testpass123",
|
|
||||||
"name": "Login Test User",
|
|
||||||
}
|
|
||||||
body, _ := json.Marshal(registerPayload)
|
|
||||||
req := httptest.NewRequest("POST", "/api/v1/auth/register", bytes.NewBuffer(body))
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
rr := httptest.NewRecorder()
|
|
||||||
TestRouter.ServeHTTP(rr, req)
|
|
||||||
assert.Equal(t, http.StatusCreated, rr.Code)
|
|
||||||
|
|
||||||
// Теперь логинимся
|
|
||||||
loginPayload := map[string]interface{}{
|
|
||||||
"email": "logintest@example.com",
|
|
||||||
"password": "testpass123",
|
|
||||||
}
|
|
||||||
body, _ = json.Marshal(loginPayload)
|
|
||||||
req = httptest.NewRequest("POST", "/api/v1/auth/login", bytes.NewBuffer(body))
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
rr = httptest.NewRecorder()
|
|
||||||
|
|
||||||
TestRouter.ServeHTTP(rr, req)
|
|
||||||
|
|
||||||
assert.Equal(t, http.StatusOK, rr.Code)
|
|
||||||
|
|
||||||
var response map[string]interface{}
|
|
||||||
err := json.Unmarshal(rr.Body.Bytes(), &response)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Проверяем наличие токенов
|
|
||||||
assert.Contains(t, response, "access_token")
|
|
||||||
assert.Contains(t, response, "refresh_token")
|
|
||||||
}
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
package integration
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestObjectsIntegration_CreateObject(t *testing.T) {
|
|
||||||
CleanDatabase(t)
|
|
||||||
|
|
||||||
// Сначала создаем пользователя и получаем токен
|
|
||||||
token := createTestUserAndGetToken(t)
|
|
||||||
|
|
||||||
testObject := map[string]interface{}{
|
|
||||||
"name": "Test Object",
|
|
||||||
"description": "Test Description",
|
|
||||||
"latitude": 55.751244,
|
|
||||||
"longitude": 37.618423,
|
|
||||||
"type": "museum",
|
|
||||||
}
|
|
||||||
|
|
||||||
body, _ := json.Marshal(testObject)
|
|
||||||
req := httptest.NewRequest("POST", "/api/v1/objects", bytes.NewBuffer(body))
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
req.Header.Set("Authorization", "Bearer "+token)
|
|
||||||
rr := httptest.NewRecorder()
|
|
||||||
|
|
||||||
TestRouter.ServeHTTP(rr, req)
|
|
||||||
|
|
||||||
assert.Equal(t, http.StatusCreated, rr.Code)
|
|
||||||
|
|
||||||
var response map[string]interface{}
|
|
||||||
err := json.Unmarshal(rr.Body.Bytes(), &response)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.NotEmpty(t, response["id"])
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestObjectsIntegration_GetNearbyObjects(t *testing.T) {
|
|
||||||
CleanDatabase(t)
|
|
||||||
|
|
||||||
// Создаем тестовые объекты
|
|
||||||
createTestObjects(t)
|
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "/api/v1/objects/nearby?lat=55.75&lon=37.61&radius=1000", nil)
|
|
||||||
rr := httptest.NewRecorder()
|
|
||||||
|
|
||||||
TestRouter.ServeHTTP(rr, req)
|
|
||||||
|
|
||||||
assert.Equal(t, http.StatusOK, rr.Code)
|
|
||||||
|
|
||||||
var response map[string]interface{}
|
|
||||||
err := json.Unmarshal(rr.Body.Bytes(), &response)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
objects, ok := response["objects"].([]interface{})
|
|
||||||
assert.True(t, ok)
|
|
||||||
assert.GreaterOrEqual(t, len(objects), 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Вспомогательные функции
|
|
||||||
func createTestUserAndGetToken(t *testing.T) string {
|
|
||||||
// Регистрация
|
|
||||||
registerPayload := map[string]interface{}{
|
|
||||||
"email": "testuser@example.com",
|
|
||||||
"password": "testpass123",
|
|
||||||
"name": "Test User",
|
|
||||||
}
|
|
||||||
body, _ := json.Marshal(registerPayload)
|
|
||||||
req := httptest.NewRequest("POST", "/api/v1/auth/register", bytes.NewBuffer(body))
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
rr := httptest.NewRecorder()
|
|
||||||
TestRouter.ServeHTTP(rr, req)
|
|
||||||
|
|
||||||
// Логин
|
|
||||||
loginPayload := map[string]interface{}{
|
|
||||||
"email": "testuser@example.com",
|
|
||||||
"password": "testpass123",
|
|
||||||
}
|
|
||||||
body, _ = json.Marshal(loginPayload)
|
|
||||||
req = httptest.NewRequest("POST", "/api/v1/auth/login", bytes.NewBuffer(body))
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
rr = httptest.NewRecorder()
|
|
||||||
TestRouter.ServeHTTP(rr, req)
|
|
||||||
|
|
||||||
var response map[string]interface{}
|
|
||||||
json.Unmarshal(rr.Body.Bytes(), &response)
|
|
||||||
return response["access_token"].(string)
|
|
||||||
}
|
|
||||||
|
|
||||||
func createTestObjects(t *testing.T) {
|
|
||||||
objects := []map[string]interface{}{
|
|
||||||
{
|
|
||||||
"name": "Museum 1",
|
|
||||||
"latitude": 55.751244,
|
|
||||||
"longitude": 37.618423,
|
|
||||||
"type": "museum",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Park 1",
|
|
||||||
"latitude": 55.755814,
|
|
||||||
"longitude": 37.617635,
|
|
||||||
"type": "park",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, obj := range objects {
|
|
||||||
body, _ := json.Marshal(obj)
|
|
||||||
req := httptest.NewRequest("POST", "/api/v1/objects", bytes.NewBuffer(body))
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
rr := httptest.NewRecorder()
|
|
||||||
TestRouter.ServeHTTP(rr, req)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
package integration
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
|
||||||
"github.com/ory/dockertest/v3"
|
|
||||||
"gorm.io/driver/postgres"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
TestDB *gorm.DB
|
|
||||||
TestRouter *chi.Mux
|
|
||||||
)
|
|
||||||
|
|
||||||
// TestMain - запускается один раз перед всеми тестами в пакете
|
|
||||||
func TestMain(m *testing.M) {
|
|
||||||
// Поднимаем PostgreSQL в Docker
|
|
||||||
db, cleanup := setupTestDatabase()
|
|
||||||
TestDB = db
|
|
||||||
|
|
||||||
// Инициализируем роутер с тестовой БД
|
|
||||||
TestRouter = setupTestRouter(TestDB)
|
|
||||||
|
|
||||||
// Запускаем тесты
|
|
||||||
code := m.Run()
|
|
||||||
|
|
||||||
// Очищаем ресурсы
|
|
||||||
cleanup()
|
|
||||||
|
|
||||||
os.Exit(code)
|
|
||||||
}
|
|
||||||
|
|
||||||
func setupTestDatabase() (*gorm.DB, func()) {
|
|
||||||
// Используем dockertest
|
|
||||||
pool, err := dockertest.NewPool("")
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Sprintf("Could not connect to Docker: %s", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Запускаем PostgreSQL контейнер
|
|
||||||
resource, err := pool.Run("postgres", "15-alpine", []string{
|
|
||||||
"POSTGRES_PASSWORD=testpass",
|
|
||||||
"POSTGRES_USER=testuser",
|
|
||||||
"POSTGRES_DB=testdb",
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Sprintf("Could not start PostgreSQL container: %s", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ждем пока база поднимется
|
|
||||||
var db *gorm.DB
|
|
||||||
if err := pool.Retry(func() error {
|
|
||||||
var err error
|
|
||||||
dsn := fmt.Sprintf("host=localhost port=%s user=testuser password=testpass dbname=testdb sslmode=disable",
|
|
||||||
resource.GetPort("5432/tcp"))
|
|
||||||
db, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
sqlDB, err := db.DB()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return sqlDB.Ping()
|
|
||||||
}); err != nil {
|
|
||||||
panic(fmt.Sprintf("Could not connect to database: %s", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Выполняем миграции (как в вашем main.go)
|
|
||||||
if err := database.MigrateModels(db); err != nil {
|
|
||||||
panic(fmt.Sprintf("Could not migrate: %s", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Очищаем базу после тестов
|
|
||||||
cleanup := func() {
|
|
||||||
if err := pool.Purge(resource); err != nil {
|
|
||||||
fmt.Printf("Could not purge resource: %s\n", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return db, cleanup
|
|
||||||
}
|
|
||||||
|
|
||||||
func setupTestRouter(db *gorm.DB) *chi.Mux {
|
|
||||||
// Инициализируем ваш роутер с тестовой БД
|
|
||||||
appConfig := &config.Config{
|
|
||||||
Environment: "test",
|
|
||||||
AppPort: "8089",
|
|
||||||
// ... другие настройки
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ваша функция настройки роутера
|
|
||||||
return router.SetupRouter(db, appConfig)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Вспомогательная функция для очистки таблиц между тестами
|
|
||||||
func CleanDatabase(t *testing.T) {
|
|
||||||
tables := []string{
|
|
||||||
"users",
|
|
||||||
"accounts",
|
|
||||||
"objects",
|
|
||||||
"feedbacks",
|
|
||||||
"comments",
|
|
||||||
"ratings",
|
|
||||||
"appeals",
|
|
||||||
// Добавьте все ваши таблицы
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, table := range tables {
|
|
||||||
if err := TestDB.Exec(fmt.Sprintf("TRUNCATE TABLE %s RESTART IDENTITY CASCADE", table)).Error; err != nil {
|
|
||||||
t.Logf("Warning: could not truncate table %s: %v", table, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
package test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"api_yal/internal/config"
|
|
||||||
"api_yal/internal/models"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
|
||||||
"github.com/ory/dockertest/v3"
|
|
||||||
"gorm.io/driver/postgres"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"gorm.io/gorm/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
TestDB *gorm.DB
|
|
||||||
TestRouter *chi.Mux
|
|
||||||
TestConfig *config.Config
|
|
||||||
)
|
|
||||||
|
|
||||||
func setupTestDatabase() (*gorm.DB, func()) {
|
|
||||||
pool, err := dockertest.NewPool("")
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Sprintf("Could not connect to Docker: %s", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Запускаем PostgreSQL контейнер
|
|
||||||
resource, err := pool.Run("postgres", "15-alpine", []string{
|
|
||||||
"POSTGRES_PASSWORD=testpass",
|
|
||||||
"POSTGRES_USER=testuser",
|
|
||||||
"POSTGRES_DB=testdb",
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Sprintf("Could not start PostgreSQL: %s", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ждем пока база поднимется
|
|
||||||
var db *gorm.DB
|
|
||||||
if err := pool.Retry(func() error {
|
|
||||||
var err error
|
|
||||||
dsn := fmt.Sprintf("host=localhost port=%s user=testuser password=testpass dbname=testdb sslmode=disable",
|
|
||||||
resource.GetPort("5432/tcp"))
|
|
||||||
db, err = gorm.Open(postgres.Open(dsn), &gorm.Config{
|
|
||||||
Logger: logger.Default.LogMode(logger.Silent), // Отключаем логи GORM в тестах
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
sqlDB, err := db.DB()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return sqlDB.Ping()
|
|
||||||
}); err != nil {
|
|
||||||
panic(fmt.Sprintf("Could not connect to DB: %s", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Выполняем миграции
|
|
||||||
models := []interface{}{
|
|
||||||
&models.Account{},
|
|
||||||
&models.UpdateHistory{},
|
|
||||||
&models.Object{},
|
|
||||||
&models.RatingVote{},
|
|
||||||
&models.VoteBreakdown{},
|
|
||||||
&models.Rating{},
|
|
||||||
&models.Feedback{},
|
|
||||||
&models.Comment{},
|
|
||||||
&models.Appeal{},
|
|
||||||
&models.AppealHistory{},
|
|
||||||
&models.PasswordReset{},
|
|
||||||
}
|
|
||||||
for _, model := range models {
|
|
||||||
db.AutoMigrate(model)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Очищаем базу после тестов
|
|
||||||
cleanup := func() {
|
|
||||||
if err := pool.Purge(resource); err != nil {
|
|
||||||
fmt.Printf("Could not purge resource: %s\n", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return db, cleanup
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExecuteRequest выполняет HTTP запрос и возвращает ответ
|
|
||||||
func ExecuteRequest(req *http.Request) *httptest.ResponseRecorder {
|
|
||||||
rr := httptest.NewRecorder()
|
|
||||||
TestRouter.ServeHTTP(rr, req)
|
|
||||||
return rr
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAuthHeader возвращает заголовок авторизации
|
|
||||||
func GetAuthHeader(token string) http.Header {
|
|
||||||
headers := http.Header{}
|
|
||||||
headers.Set("Authorization", "Bearer "+token)
|
|
||||||
headers.Set("Content-Type", "application/json")
|
|
||||||
return headers
|
|
||||||
}
|
|
||||||
|
|
||||||
// CleanDatabase очищает все таблицы между тестами
|
|
||||||
func CleanDatabase(t *testing.T) {
|
|
||||||
tables := []string{
|
|
||||||
"users", "accounts", "objects", "feedbacks",
|
|
||||||
"comments", "ratings", "appeals", "appeal_histories",
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, table := range tables {
|
|
||||||
if err := TestDB.Exec(fmt.Sprintf("TRUNCATE TABLE %s RESTART IDENTITY CASCADE", table)).Error; err != nil {
|
|
||||||
t.Logf("Warning: could not truncate %s: %v", table, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"api_yal/tests/testutils"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAccountEndpoints(t *testing.T) {
|
||||||
|
config := testutils.NewTestConfig()
|
||||||
|
|
||||||
|
t.Run("GetProfile", func(t *testing.T) {
|
||||||
|
user := config.CreateTestUser(t)
|
||||||
|
defer config.CleanupTestUser(t, user)
|
||||||
|
|
||||||
|
resp, err := config.Request("GET", "/account/profile", nil, user.Token)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get profile: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var profile map[string]interface{}
|
||||||
|
if err := config.ParseResponse(resp, &profile); err != nil {
|
||||||
|
t.Fatalf("Failed to parse response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedFields := []string{"id", "email", "full_name", "first_name", "last_name", "role", "stats"}
|
||||||
|
for _, field := range expectedFields {
|
||||||
|
if _, ok := profile[field]; !ok {
|
||||||
|
t.Errorf("Expected field %s not found", field)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetOwnAccount", func(t *testing.T) {
|
||||||
|
user := config.CreateTestUser(t)
|
||||||
|
defer config.CleanupTestUser(t, user)
|
||||||
|
|
||||||
|
resp, err := config.Request("GET", "/account", nil, user.Token)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get account: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("UpdateAccount", func(t *testing.T) {
|
||||||
|
user := config.CreateTestUser(t)
|
||||||
|
defer config.CleanupTestUser(t, user)
|
||||||
|
|
||||||
|
updateData := map[string]interface{}{
|
||||||
|
"phone": "+79998887766",
|
||||||
|
"city": "Moscow",
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := config.Request("PUT", "/account", updateData, user.Token)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to update account: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var updatedAccount map[string]interface{}
|
||||||
|
if err := config.ParseResponse(resp, &updatedAccount); err != nil {
|
||||||
|
t.Fatalf("Failed to parse response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if phone, ok := updatedAccount["phone"].(string); !ok || phone != "+79998887766" {
|
||||||
|
t.Error("Phone number not updated correctly")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ChangePassword", func(t *testing.T) {
|
||||||
|
user := config.CreateTestUser(t)
|
||||||
|
defer config.CleanupTestUser(t, user)
|
||||||
|
|
||||||
|
newPassword := "newpass123"
|
||||||
|
resp, err := config.Request("POST", "/account/change-password", map[string]interface{}{
|
||||||
|
"current_password": user.Password,
|
||||||
|
"new_password": newPassword,
|
||||||
|
}, user.Token)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to change password: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, что можно войти с новым паролем
|
||||||
|
newToken, err := config.GetAuthToken(user.Email, newPassword)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Failed to login with new password: %v", err)
|
||||||
|
}
|
||||||
|
if newToken == "" {
|
||||||
|
t.Error("Failed to get token with new password")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ChangePasswordWrongCurrent", func(t *testing.T) {
|
||||||
|
user := config.CreateTestUser(t)
|
||||||
|
defer config.CleanupTestUser(t, user)
|
||||||
|
|
||||||
|
resp, err := config.Request("POST", "/account/change-password", map[string]interface{}{
|
||||||
|
"current_password": "wrongpassword",
|
||||||
|
"new_password": "newpass123",
|
||||||
|
}, user.Token)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to change password: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != 400 && resp.StatusCode != 401 {
|
||||||
|
t.Errorf("Expected status 400 or 401, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("DeleteAccount", func(t *testing.T) {
|
||||||
|
user := config.CreateTestUser(t)
|
||||||
|
|
||||||
|
resp, err := config.Request("DELETE", "/account", nil, user.Token)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to delete account: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, что больше нельзя войти
|
||||||
|
_, err = config.GetAuthToken(user.Email, user.Password)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Should not be able to login after account deletion")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("UnauthorizedAccess", func(t *testing.T) {
|
||||||
|
resp, err := config.Request("GET", "/account/profile", nil, "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to make request: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != 401 {
|
||||||
|
t.Errorf("Expected status 401, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"api_yal/tests/testutils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAppealEndpoints(t *testing.T) {
|
||||||
|
config := testutils.NewTestConfig()
|
||||||
|
|
||||||
|
t.Run("CreateAppeal", func(t *testing.T) {
|
||||||
|
user := config.CreateTestUser(t)
|
||||||
|
defer config.CleanupTestUser(t, user)
|
||||||
|
|
||||||
|
appealData := map[string]interface{}{
|
||||||
|
"type": "complaint",
|
||||||
|
"title": "Test Appeal",
|
||||||
|
"message": "This is a test appeal message for testing purposes",
|
||||||
|
"priority": "high",
|
||||||
|
"contact_name": "Test User",
|
||||||
|
"contact_email": user.Email,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := config.Request("POST", "/appeals", appealData, user.Token)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create appeal: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != 201 {
|
||||||
|
t.Errorf("Expected status 201, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var createdAppeal map[string]interface{}
|
||||||
|
if err := config.ParseResponse(resp, &createdAppeal); err != nil {
|
||||||
|
t.Fatalf("Failed to parse response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := createdAppeal["id"]; !ok {
|
||||||
|
t.Error("Appeal ID not found")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetAppealByID", func(t *testing.T) {
|
||||||
|
user := config.CreateTestUser(t)
|
||||||
|
defer config.CleanupTestUser(t, user)
|
||||||
|
|
||||||
|
// Создаем обращение
|
||||||
|
appealData := map[string]interface{}{
|
||||||
|
"type": "question",
|
||||||
|
"title": "Test Question",
|
||||||
|
"message": "This is a test question",
|
||||||
|
"contact_email": user.Email,
|
||||||
|
}
|
||||||
|
resp, err := config.Request("POST", "/appeals", appealData, user.Token)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create appeal: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
var createdAppeal map[string]interface{}
|
||||||
|
if err := config.ParseResponse(resp, &createdAppeal); err != nil {
|
||||||
|
t.Fatalf("Failed to parse response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
appealID := createdAppeal["id"].(float64)
|
||||||
|
|
||||||
|
getResp, err := config.Request("GET", "/appeals/"+string(rune(appealID)), nil, user.Token)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get appeal: %v", err)
|
||||||
|
}
|
||||||
|
defer getResp.Body.Close()
|
||||||
|
|
||||||
|
if getResp.StatusCode != 200 {
|
||||||
|
t.Errorf("Expected status 200, got %d", getResp.StatusCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("UpdateAppeal", func(t *testing.T) {
|
||||||
|
user := config.CreateTestUser(t)
|
||||||
|
defer config.CleanupTestUser(t, user)
|
||||||
|
|
||||||
|
// Создаем обращение
|
||||||
|
appealData := map[string]interface{}{
|
||||||
|
"type": "suggestion",
|
||||||
|
"title": "Original Title",
|
||||||
|
"message": "Original message for testing",
|
||||||
|
"contact_email": user.Email,
|
||||||
|
}
|
||||||
|
resp, err := config.Request("POST", "/appeals", appealData, user.Token)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create appeal: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
var createdAppeal map[string]interface{}
|
||||||
|
if err := config.ParseResponse(resp, &createdAppeal); err != nil {
|
||||||
|
t.Fatalf("Failed to parse response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
appealID := createdAppeal["id"].(float64)
|
||||||
|
|
||||||
|
updateData := map[string]interface{}{
|
||||||
|
"title": "Updated Title",
|
||||||
|
"message": "Updated message content",
|
||||||
|
"priority": "critical",
|
||||||
|
}
|
||||||
|
|
||||||
|
updateResp, err := config.Request("PUT", "/appeals/"+string(rune(appealID)), updateData, user.Token)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to update appeal: %v", err)
|
||||||
|
}
|
||||||
|
defer updateResp.Body.Close()
|
||||||
|
|
||||||
|
if updateResp.StatusCode != 200 {
|
||||||
|
t.Errorf("Expected status 200, got %d", updateResp.StatusCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("MyAppeals", func(t *testing.T) {
|
||||||
|
user := config.CreateTestUser(t)
|
||||||
|
defer config.CleanupTestUser(t, user)
|
||||||
|
|
||||||
|
// Создаем несколько обращений
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
appealData := map[string]interface{}{
|
||||||
|
"type": "other",
|
||||||
|
"title": "Test Appeal",
|
||||||
|
"message": "Test message content",
|
||||||
|
}
|
||||||
|
_, err := config.Request("POST", "/appeals", appealData, user.Token)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create appeal: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := config.Request("GET", "/appeals/me", nil, user.Token)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get my appeals: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("DeleteAppeal", func(t *testing.T) {
|
||||||
|
user := config.CreateTestUser(t)
|
||||||
|
defer config.CleanupTestUser(t, user)
|
||||||
|
|
||||||
|
// Создаем обращение
|
||||||
|
appealData := map[string]interface{}{
|
||||||
|
"type": "question",
|
||||||
|
"title": "To Delete",
|
||||||
|
"message": "This appeal will be deleted",
|
||||||
|
}
|
||||||
|
resp, err := config.Request("POST", "/appeals", appealData, user.Token)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create appeal: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
var createdAppeal map[string]interface{}
|
||||||
|
if err := config.ParseResponse(resp, &createdAppeal); err != nil {
|
||||||
|
t.Fatalf("Failed to parse response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
appealID := createdAppeal["id"].(float64)
|
||||||
|
|
||||||
|
deleteResp, err := config.Request("DELETE", "/appeals/"+string(rune(appealID)), nil, user.Token)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to delete appeal: %v", err)
|
||||||
|
}
|
||||||
|
defer deleteResp.Body.Close()
|
||||||
|
|
||||||
|
if deleteResp.StatusCode != 204 {
|
||||||
|
t.Errorf("Expected status 204, got %d", deleteResp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, что обращение удалено
|
||||||
|
getResp, err := config.Request("GET", "/appeals/"+string(rune(appealID)), nil, user.Token)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get appeal: %v", err)
|
||||||
|
}
|
||||||
|
defer getResp.Body.Close()
|
||||||
|
|
||||||
|
if getResp.StatusCode != 404 {
|
||||||
|
t.Errorf("Expected status 404 for deleted appeal, got %d", getResp.StatusCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"api_yal/tests/testutils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAuthFlow(t *testing.T) {
|
||||||
|
config := testutils.NewTestConfig()
|
||||||
|
|
||||||
|
t.Run("Register", func(t *testing.T) {
|
||||||
|
testData := map[string]interface{}{
|
||||||
|
"email": "testflow@example.com",
|
||||||
|
"password": "test123456",
|
||||||
|
"first_name": "Flow",
|
||||||
|
"last_name": "Test",
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := config.Request("POST", "/auth/register", testData, "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to register: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != 201 {
|
||||||
|
t.Errorf("Expected status 201, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var result map[string]interface{}
|
||||||
|
if err := config.ParseResponse(resp, &result); err != nil {
|
||||||
|
t.Fatalf("Failed to parse response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := result["token"]; !ok {
|
||||||
|
t.Error("Token not found in response")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := result["user"]; !ok {
|
||||||
|
t.Error("User data not found in response")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Login", func(t *testing.T) {
|
||||||
|
// Сначала создаем пользователя
|
||||||
|
user := config.CreateTestUser(t)
|
||||||
|
defer config.CleanupTestUser(t, user)
|
||||||
|
|
||||||
|
// Тестируем логин
|
||||||
|
resp, err := config.Request("POST", "/auth/login", map[string]interface{}{
|
||||||
|
"email": user.Email,
|
||||||
|
"password": user.Password,
|
||||||
|
}, "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to login: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var result map[string]interface{}
|
||||||
|
if err := config.ParseResponse(resp, &result); err != nil {
|
||||||
|
t.Fatalf("Failed to parse response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := result["token"]; !ok {
|
||||||
|
t.Error("Token not found in response")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, что refresh token установлен в cookie
|
||||||
|
cookies := resp.Cookies()
|
||||||
|
foundRefreshToken := false
|
||||||
|
for _, cookie := range cookies {
|
||||||
|
if cookie.Name == "refresh_token" {
|
||||||
|
foundRefreshToken = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !foundRefreshToken {
|
||||||
|
t.Error("Refresh token cookie not set")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("RefreshToken", func(t *testing.T) {
|
||||||
|
user := config.CreateTestUser(t)
|
||||||
|
defer config.CleanupTestUser(t, user)
|
||||||
|
|
||||||
|
// Логинимся для получения refresh token в cookie
|
||||||
|
_, err := config.Request("POST", "/auth/login", map[string]interface{}{
|
||||||
|
"email": user.Email,
|
||||||
|
"password": user.Password,
|
||||||
|
}, "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to login: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновляем токен
|
||||||
|
resp, err := config.Request("POST", "/auth/refresh", nil, "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to refresh token: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Logout", func(t *testing.T) {
|
||||||
|
user := config.CreateTestUser(t)
|
||||||
|
defer config.CleanupTestUser(t, user)
|
||||||
|
|
||||||
|
resp, err := config.Request("POST", "/auth/logout", nil, user.Token)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to logout: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var result map[string]interface{}
|
||||||
|
if err := config.ParseResponse(resp, &result); err != nil {
|
||||||
|
t.Fatalf("Failed to parse response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if msg, ok := result["message"]; !ok || msg != "Successfully logged out" {
|
||||||
|
t.Error("Logout message not as expected")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("PasswordReset", func(t *testing.T) {
|
||||||
|
user := config.CreateTestUser(t)
|
||||||
|
defer config.CleanupTestUser(t, user)
|
||||||
|
|
||||||
|
// Запрос сброса пароля
|
||||||
|
resp, err := config.Request("POST", "/auth/password-reset/request", map[string]interface{}{
|
||||||
|
"email": user.Email,
|
||||||
|
}, "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to request password reset: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var result map[string]interface{}
|
||||||
|
if err := config.ParseResponse(resp, &result); err != nil {
|
||||||
|
t.Fatalf("Failed to parse response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resetToken, ok := result["token"].(string)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("Reset token not found in response")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Подтверждение сброса пароля
|
||||||
|
newPassword := "newpassword789"
|
||||||
|
resp, err = config.Request("POST", "/auth/password-reset/confirm", map[string]interface{}{
|
||||||
|
"token": resetToken,
|
||||||
|
"new_password": newPassword,
|
||||||
|
}, "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to confirm password reset: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, что можно войти с новым паролем
|
||||||
|
newToken, err := config.GetAuthToken(user.Email, newPassword)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Failed to login with new password: %v", err)
|
||||||
|
}
|
||||||
|
if newToken == "" {
|
||||||
|
t.Error("Failed to get token with new password")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("MobileLogin", func(t *testing.T) {
|
||||||
|
user := config.CreateTestUser(t)
|
||||||
|
defer config.CleanupTestUser(t, user)
|
||||||
|
|
||||||
|
resp, err := config.Request("POST", "/auth/mobile/login", map[string]interface{}{
|
||||||
|
"email": user.Email,
|
||||||
|
"password": user.Password,
|
||||||
|
}, "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to mobile login: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var result map[string]interface{}
|
||||||
|
if err := config.ParseResponse(resp, &result); err != nil {
|
||||||
|
t.Fatalf("Failed to parse response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
requiredFields := []string{"access_token", "refresh_token", "expires_at", "user"}
|
||||||
|
for _, field := range requiredFields {
|
||||||
|
if _, ok := result[field]; !ok {
|
||||||
|
t.Errorf("Required field %s not found in response", field)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,223 @@
|
|||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"api_yal/tests/testutils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCommentEndpoints(t *testing.T) {
|
||||||
|
config := testutils.NewTestConfig()
|
||||||
|
|
||||||
|
t.Run("CreateComment", func(t *testing.T) {
|
||||||
|
user := config.CreateTestUser(t)
|
||||||
|
defer config.CleanupTestUser(t, user)
|
||||||
|
|
||||||
|
objectID := config.CreateTestObject(t, user.Token)
|
||||||
|
feedbackID := config.CreateTestFeedback(t, user.Token, objectID)
|
||||||
|
|
||||||
|
commentData := testutils.CreateCommentRequest{
|
||||||
|
FeedbackID: feedbackID,
|
||||||
|
Text: "Great review! I agree completely.",
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := config.Request("POST", "/comments", commentData, user.Token)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create comment: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != 201 {
|
||||||
|
t.Errorf("Expected status 201, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var createdComment map[string]interface{}
|
||||||
|
if err := config.ParseResponse(resp, &createdComment); err != nil {
|
||||||
|
t.Fatalf("Failed to parse response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := createdComment["id"]; !ok {
|
||||||
|
t.Error("Comment ID not found")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetCommentByID", func(t *testing.T) {
|
||||||
|
user := config.CreateTestUser(t)
|
||||||
|
defer config.CleanupTestUser(t, user)
|
||||||
|
|
||||||
|
objectID := config.CreateTestObject(t, user.Token)
|
||||||
|
feedbackID := config.CreateTestFeedback(t, user.Token, objectID)
|
||||||
|
|
||||||
|
// Создаем комментарий
|
||||||
|
commentData := testutils.CreateCommentRequest{
|
||||||
|
FeedbackID: feedbackID,
|
||||||
|
Text: "Test comment",
|
||||||
|
}
|
||||||
|
resp, err := config.Request("POST", "/comments", commentData, user.Token)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create comment: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
var createdComment map[string]interface{}
|
||||||
|
if err := config.ParseResponse(resp, &createdComment); err != nil {
|
||||||
|
t.Fatalf("Failed to parse response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
commentID := createdComment["id"].(float64)
|
||||||
|
|
||||||
|
getResp, err := config.Request("GET", "/comments/"+string(rune(commentID)), nil, "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get comment: %v", err)
|
||||||
|
}
|
||||||
|
defer getResp.Body.Close()
|
||||||
|
|
||||||
|
if getResp.StatusCode != 200 {
|
||||||
|
t.Errorf("Expected status 200, got %d", getResp.StatusCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("UpdateComment", func(t *testing.T) {
|
||||||
|
user := config.CreateTestUser(t)
|
||||||
|
defer config.CleanupTestUser(t, user)
|
||||||
|
|
||||||
|
objectID := config.CreateTestObject(t, user.Token)
|
||||||
|
feedbackID := config.CreateTestFeedback(t, user.Token, objectID)
|
||||||
|
|
||||||
|
// Создаем комментарий
|
||||||
|
commentData := testutils.CreateCommentRequest{
|
||||||
|
FeedbackID: feedbackID,
|
||||||
|
Text: "Original comment",
|
||||||
|
}
|
||||||
|
resp, err := config.Request("POST", "/comments", commentData, user.Token)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create comment: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
var createdComment map[string]interface{}
|
||||||
|
if err := config.ParseResponse(resp, &createdComment); err != nil {
|
||||||
|
t.Fatalf("Failed to parse response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
commentID := createdComment["id"].(float64)
|
||||||
|
|
||||||
|
updateData := map[string]interface{}{
|
||||||
|
"text": "Updated comment text",
|
||||||
|
}
|
||||||
|
|
||||||
|
updateResp, err := config.Request("PUT", "/comments/"+string(rune(commentID)), updateData, user.Token)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to update comment: %v", err)
|
||||||
|
}
|
||||||
|
defer updateResp.Body.Close()
|
||||||
|
|
||||||
|
if updateResp.StatusCode != 200 {
|
||||||
|
t.Errorf("Expected status 200, got %d", updateResp.StatusCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ListComments", func(t *testing.T) {
|
||||||
|
resp, err := config.Request("GET", "/comments?page=1&page_size=20", nil, "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to list comments: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("MyComments", func(t *testing.T) {
|
||||||
|
user := config.CreateTestUser(t)
|
||||||
|
defer config.CleanupTestUser(t, user)
|
||||||
|
|
||||||
|
resp, err := config.Request("GET", "/comments/my", nil, user.Token)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get my comments: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("CommentsByFeedback", func(t *testing.T) {
|
||||||
|
user := config.CreateTestUser(t)
|
||||||
|
defer config.CleanupTestUser(t, user)
|
||||||
|
|
||||||
|
objectID := config.CreateTestObject(t, user.Token)
|
||||||
|
feedbackID := config.CreateTestFeedback(t, user.Token, objectID)
|
||||||
|
|
||||||
|
resp, err := config.Request("GET", "/comments/feedback/"+string(rune(feedbackID)), nil, "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get comments by feedback: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("CommentStats", func(t *testing.T) {
|
||||||
|
resp, err := config.Request("GET", "/comments/stats", nil, "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get comment stats: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var stats map[string]interface{}
|
||||||
|
if err := config.ParseResponse(resp, &stats); err != nil {
|
||||||
|
t.Fatalf("Failed to parse response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedFields := []string{"total_comments", "verified_comments", "unverified_comments"}
|
||||||
|
for _, field := range expectedFields {
|
||||||
|
if _, ok := stats[field]; !ok {
|
||||||
|
t.Errorf("Expected field %s not found", field)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("DeleteComment", func(t *testing.T) {
|
||||||
|
user := config.CreateTestUser(t)
|
||||||
|
defer config.CleanupTestUser(t, user)
|
||||||
|
|
||||||
|
objectID := config.CreateTestObject(t, user.Token)
|
||||||
|
feedbackID := config.CreateTestFeedback(t, user.Token, objectID)
|
||||||
|
|
||||||
|
// Создаем комментарий
|
||||||
|
commentData := testutils.CreateCommentRequest{
|
||||||
|
FeedbackID: feedbackID,
|
||||||
|
Text: "Comment to delete",
|
||||||
|
}
|
||||||
|
resp, err := config.Request("POST", "/comments", commentData, user.Token)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create comment: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
var createdComment map[string]interface{}
|
||||||
|
if err := config.ParseResponse(resp, &createdComment); err != nil {
|
||||||
|
t.Fatalf("Failed to parse response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
commentID := createdComment["id"].(float64)
|
||||||
|
|
||||||
|
deleteResp, err := config.Request("DELETE", "/comments/"+string(rune(commentID)), nil, user.Token)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to delete comment: %v", err)
|
||||||
|
}
|
||||||
|
defer deleteResp.Body.Close()
|
||||||
|
|
||||||
|
if deleteResp.StatusCode != 200 {
|
||||||
|
t.Errorf("Expected status 200, got %d", deleteResp.StatusCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,217 @@
|
|||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"api_yal/tests/testutils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFeedbackEndpoints(t *testing.T) {
|
||||||
|
config := testutils.NewTestConfig()
|
||||||
|
|
||||||
|
t.Run("CreateFeedback", func(t *testing.T) {
|
||||||
|
user := config.CreateTestUser(t)
|
||||||
|
defer config.CleanupTestUser(t, user)
|
||||||
|
|
||||||
|
objectID := config.CreateTestObject(t, user.Token)
|
||||||
|
|
||||||
|
feedbackData := testutils.CreateFeedbackRequest{
|
||||||
|
ObjectID: objectID,
|
||||||
|
Rating: 5,
|
||||||
|
Text: "Excellent place! Very recommended.",
|
||||||
|
Platform: "tourist",
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := config.Request("POST", "/feedbacks", feedbackData, user.Token)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create feedback: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != 201 {
|
||||||
|
t.Errorf("Expected status 201, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var createdFeedback map[string]interface{}
|
||||||
|
if err := config.ParseResponse(resp, &createdFeedback); err != nil {
|
||||||
|
t.Fatalf("Failed to parse response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := createdFeedback["id"]; !ok {
|
||||||
|
t.Error("Feedback ID not found")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetFeedbackByID", func(t *testing.T) {
|
||||||
|
user := config.CreateTestUser(t)
|
||||||
|
defer config.CleanupTestUser(t, user)
|
||||||
|
|
||||||
|
objectID := config.CreateTestObject(t, user.Token)
|
||||||
|
feedbackID := config.CreateTestFeedback(t, user.Token, objectID)
|
||||||
|
|
||||||
|
resp, err := config.Request("GET", "/feedbacks/"+string(rune(feedbackID)), nil, "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get feedback: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("UpdateFeedback", func(t *testing.T) {
|
||||||
|
user := config.CreateTestUser(t)
|
||||||
|
defer config.CleanupTestUser(t, user)
|
||||||
|
|
||||||
|
objectID := config.CreateTestObject(t, user.Token)
|
||||||
|
feedbackID := config.CreateTestFeedback(t, user.Token, objectID)
|
||||||
|
|
||||||
|
updateData := map[string]interface{}{
|
||||||
|
"rating": 4,
|
||||||
|
"text": "Updated feedback text",
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := config.Request("PUT", "/feedbacks/"+string(rune(feedbackID)), updateData, user.Token)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to update feedback: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var updatedFeedback map[string]interface{}
|
||||||
|
if err := config.ParseResponse(resp, &updatedFeedback); err != nil {
|
||||||
|
t.Fatalf("Failed to parse response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if text, ok := updatedFeedback["text"].(string); !ok || text != "Updated feedback text" {
|
||||||
|
t.Error("Feedback text not updated")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ListFeedbacks", func(t *testing.T) {
|
||||||
|
user := config.CreateTestUser(t)
|
||||||
|
defer config.CleanupTestUser(t, user)
|
||||||
|
|
||||||
|
resp, err := config.Request("GET", "/feedbacks?offset=0&limit=20", nil, "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to list feedbacks: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("MyFeedbacks", func(t *testing.T) {
|
||||||
|
user := config.CreateTestUser(t)
|
||||||
|
defer config.CleanupTestUser(t, user)
|
||||||
|
|
||||||
|
resp, err := config.Request("GET", "/feedbacks/my", nil, user.Token)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get my feedbacks: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("FeedbacksByObject", func(t *testing.T) {
|
||||||
|
user := config.CreateTestUser(t)
|
||||||
|
defer config.CleanupTestUser(t, user)
|
||||||
|
|
||||||
|
objectID := config.CreateTestObject(t, user.Token)
|
||||||
|
|
||||||
|
resp, err := config.Request("GET", "/feedbacks/object/"+string(rune(objectID)), nil, "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get object feedbacks: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("FeedbacksByPlatform", func(t *testing.T) {
|
||||||
|
resp, err := config.Request("GET", "/feedbacks/platform/tourist", nil, "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get platform feedbacks: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("SearchFeedbacks", func(t *testing.T) {
|
||||||
|
resp, err := config.Request("GET", "/feedbacks/search?q=excellent", nil, "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to search feedbacks: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("FeedbackStats", func(t *testing.T) {
|
||||||
|
resp, err := config.Request("GET", "/feedbacks/stats", nil, "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get feedback stats: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var stats map[string]interface{}
|
||||||
|
if err := config.ParseResponse(resp, &stats); err != nil {
|
||||||
|
t.Fatalf("Failed to parse response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedFields := []string{"total_feedbacks", "average_rating", "rating_distribution", "platform_stats"}
|
||||||
|
for _, field := range expectedFields {
|
||||||
|
if _, ok := stats[field]; !ok {
|
||||||
|
t.Errorf("Expected field %s not found", field)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("DeleteFeedback", func(t *testing.T) {
|
||||||
|
user := config.CreateTestUser(t)
|
||||||
|
defer config.CleanupTestUser(t, user)
|
||||||
|
|
||||||
|
objectID := config.CreateTestObject(t, user.Token)
|
||||||
|
feedbackID := config.CreateTestFeedback(t, user.Token, objectID)
|
||||||
|
|
||||||
|
resp, err := config.Request("DELETE", "/feedbacks/"+string(rune(feedbackID)), nil, user.Token)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to delete feedback: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != 204 {
|
||||||
|
t.Errorf("Expected status 204, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, что отзыв удален
|
||||||
|
getResp, err := config.Request("GET", "/feedbacks/"+string(rune(feedbackID)), nil, "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get feedback: %v", err)
|
||||||
|
}
|
||||||
|
defer getResp.Body.Close()
|
||||||
|
|
||||||
|
if getResp.StatusCode != 404 {
|
||||||
|
t.Errorf("Expected status 404 for deleted feedback, got %d", getResp.StatusCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,243 @@
|
|||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"api_yal/tests/testutils"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestObjectEndpoints(t *testing.T) {
|
||||||
|
config := testutils.NewTestConfig()
|
||||||
|
|
||||||
|
t.Run("CreateObject", func(t *testing.T) {
|
||||||
|
user := config.CreateTestUser(t)
|
||||||
|
defer config.CleanupTestUser(t, user)
|
||||||
|
|
||||||
|
objectData := testutils.CreateObjectRequest{
|
||||||
|
ShortName: "Test Cafe",
|
||||||
|
LongName: "Test Cafe on Main Street",
|
||||||
|
Type: "cafe",
|
||||||
|
Phone: "+71234567890",
|
||||||
|
Email: "cafe@test.com",
|
||||||
|
Site: "https://cafe.test.com",
|
||||||
|
ShortDescription: "A test cafe",
|
||||||
|
Description: "This is a test cafe for API testing",
|
||||||
|
Address: "123 Test Street",
|
||||||
|
Latitude: 55.751244,
|
||||||
|
Longitude: 37.618423,
|
||||||
|
IsActive: true,
|
||||||
|
IsVerified: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := config.Request("POST", "/objects", objectData, user.Token)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create object: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != 201 {
|
||||||
|
t.Errorf("Expected status 201, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var createdObject map[string]interface{}
|
||||||
|
if err := config.ParseResponse(resp, &createdObject); err != nil {
|
||||||
|
t.Fatalf("Failed to parse response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := createdObject["id"]; !ok {
|
||||||
|
t.Error("Object ID not found in response")
|
||||||
|
}
|
||||||
|
|
||||||
|
if name, ok := createdObject["short_name"].(string); !ok || name != "Test Cafe" {
|
||||||
|
t.Error("Object name mismatch")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetObjectByID", func(t *testing.T) {
|
||||||
|
user := config.CreateTestUser(t)
|
||||||
|
defer config.CleanupTestUser(t, user)
|
||||||
|
|
||||||
|
objectID := config.CreateTestObject(t, user.Token)
|
||||||
|
|
||||||
|
resp, err := config.Request("GET", "/objects/"+string(rune(objectID)), nil, "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get object: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var object map[string]interface{}
|
||||||
|
if err := config.ParseResponse(resp, &object); err != nil {
|
||||||
|
t.Fatalf("Failed to parse response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if id, ok := object["id"].(float64); !ok || uint(id) != objectID {
|
||||||
|
t.Errorf("Object ID mismatch: expected %d, got %v", objectID, id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetNonExistentObject", func(t *testing.T) {
|
||||||
|
resp, err := config.Request("GET", "/objects/999999", nil, "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get object: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != 404 {
|
||||||
|
t.Errorf("Expected status 404, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("UpdateObject", func(t *testing.T) {
|
||||||
|
user := config.CreateTestUser(t)
|
||||||
|
defer config.CleanupTestUser(t, user)
|
||||||
|
|
||||||
|
objectID := config.CreateTestObject(t, user.Token)
|
||||||
|
|
||||||
|
updateData := map[string]interface{}{
|
||||||
|
"short_name": "Updated Cafe Name",
|
||||||
|
"description": "Updated description",
|
||||||
|
"short_description": "Updated short desc",
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := config.Request("PUT", "/objects/"+string(rune(objectID)), updateData, user.Token)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to update object: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var updatedObject map[string]interface{}
|
||||||
|
if err := config.ParseResponse(resp, &updatedObject); err != nil {
|
||||||
|
t.Fatalf("Failed to parse response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if name, ok := updatedObject["short_name"].(string); !ok || name != "Updated Cafe Name" {
|
||||||
|
t.Error("Object name not updated")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("UpdateObjectUnauthorized", func(t *testing.T) {
|
||||||
|
user1 := config.CreateTestUser(t)
|
||||||
|
defer config.CleanupTestUser(t, user1)
|
||||||
|
|
||||||
|
user2 := config.CreateTestUser(t)
|
||||||
|
defer config.CleanupTestUser(t, user2)
|
||||||
|
|
||||||
|
objectID := config.CreateTestObject(t, user1.Token)
|
||||||
|
|
||||||
|
resp, err := config.Request("PUT", "/objects/"+string(rune(objectID)), map[string]interface{}{
|
||||||
|
"short_name": "Hacked Name",
|
||||||
|
}, user2.Token)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to update object: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != 403 {
|
||||||
|
t.Errorf("Expected status 403, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ListObjects", func(t *testing.T) {
|
||||||
|
user := config.CreateTestUser(t)
|
||||||
|
defer config.CleanupTestUser(t, user)
|
||||||
|
|
||||||
|
// Создаем несколько объектов
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
config.CreateTestObject(t, user.Token)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := config.Request("GET", "/objects?page=1&page_size=10", nil, "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to list objects: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var listResponse map[string]interface{}
|
||||||
|
if err := config.ParseResponse(resp, &listResponse); err != nil {
|
||||||
|
t.Fatalf("Failed to parse response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := listResponse["items"]; !ok {
|
||||||
|
t.Error("Items field not found in response")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("SearchObjects", func(t *testing.T) {
|
||||||
|
user := config.CreateTestUser(t)
|
||||||
|
defer config.CleanupTestUser(t, user)
|
||||||
|
|
||||||
|
objectID := config.CreateTestObject(t, user.Token)
|
||||||
|
|
||||||
|
resp, err := config.Request("GET", "/objects/search?q=Test", nil, "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to search objects: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var searchResults map[string]interface{}
|
||||||
|
if err := config.ParseResponse(resp, &searchResults); err != nil {
|
||||||
|
t.Fatalf("Failed to parse response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Search results: %+v", searchResults)
|
||||||
|
_ = objectID
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("NearbyObjects", func(t *testing.T) {
|
||||||
|
user := config.CreateTestUser(t)
|
||||||
|
defer config.CleanupTestUser(t, user)
|
||||||
|
|
||||||
|
resp, err := config.Request("GET", "/objects/nearby?lat=55.751244&lng=37.618423&radius=1000", nil, "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get nearby objects: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("DeleteObject", func(t *testing.T) {
|
||||||
|
user := config.CreateTestUser(t)
|
||||||
|
defer config.CleanupTestUser(t, user)
|
||||||
|
|
||||||
|
objectID := config.CreateTestObject(t, user.Token)
|
||||||
|
|
||||||
|
resp, err := config.Request("DELETE", "/objects/"+string(rune(objectID)), nil, user.Token)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to delete object: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != 204 {
|
||||||
|
t.Errorf("Expected status 204, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, что объект удален
|
||||||
|
getResp, err := config.Request("GET", "/objects/"+string(rune(objectID)), nil, "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get object: %v", err)
|
||||||
|
}
|
||||||
|
defer getResp.Body.Close()
|
||||||
|
|
||||||
|
if getResp.StatusCode != 404 {
|
||||||
|
t.Errorf("Expected status 404 for deleted object, got %d", getResp.StatusCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,240 @@
|
|||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"api_yal/tests/testutils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRatingEndpoints(t *testing.T) {
|
||||||
|
config := testutils.NewTestConfig()
|
||||||
|
|
||||||
|
t.Run("CreateRating", func(t *testing.T) {
|
||||||
|
user := config.CreateTestUser(t)
|
||||||
|
defer config.CleanupTestUser(t, user)
|
||||||
|
|
||||||
|
objectID := config.CreateTestObject(t, user.Token)
|
||||||
|
|
||||||
|
ratingData := map[string]interface{}{
|
||||||
|
"object_id": objectID,
|
||||||
|
"platform": "tourist",
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := config.Request("POST", "/ratings", ratingData, user.Token)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create rating: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != 201 {
|
||||||
|
t.Errorf("Expected status 201, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("CastVote", func(t *testing.T) {
|
||||||
|
user := config.CreateTestUser(t)
|
||||||
|
defer config.CleanupTestUser(t, user)
|
||||||
|
|
||||||
|
objectID := config.CreateTestObject(t, user.Token)
|
||||||
|
|
||||||
|
// Сначала создаем рейтинг
|
||||||
|
ratingData := map[string]interface{}{
|
||||||
|
"object_id": objectID,
|
||||||
|
"platform": "tourist",
|
||||||
|
}
|
||||||
|
resp, err := config.Request("POST", "/ratings", ratingData, user.Token)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create rating: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
var createdRating map[string]interface{}
|
||||||
|
if err := config.ParseResponse(resp, &createdRating); err != nil {
|
||||||
|
t.Fatalf("Failed to parse response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ratingID := createdRating["id"].(float64)
|
||||||
|
|
||||||
|
// Голосуем
|
||||||
|
voteData := map[string]interface{}{
|
||||||
|
"score": 5,
|
||||||
|
}
|
||||||
|
|
||||||
|
voteResp, err := config.Request("POST", "/ratings/"+string(rune(ratingID))+"/vote/tourist", voteData, user.Token)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to cast vote: %v", err)
|
||||||
|
}
|
||||||
|
defer voteResp.Body.Close()
|
||||||
|
|
||||||
|
if voteResp.StatusCode != 200 {
|
||||||
|
t.Errorf("Expected status 200, got %d", voteResp.StatusCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetMyVote", func(t *testing.T) {
|
||||||
|
user := config.CreateTestUser(t)
|
||||||
|
defer config.CleanupTestUser(t, user)
|
||||||
|
|
||||||
|
objectID := config.CreateTestObject(t, user.Token)
|
||||||
|
|
||||||
|
// Создаем рейтинг и голосуем
|
||||||
|
ratingData := map[string]interface{}{
|
||||||
|
"object_id": objectID,
|
||||||
|
"platform": "tourist",
|
||||||
|
}
|
||||||
|
resp, err := config.Request("POST", "/ratings", ratingData, user.Token)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create rating: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
var createdRating map[string]interface{}
|
||||||
|
if err := config.ParseResponse(resp, &createdRating); err != nil {
|
||||||
|
t.Fatalf("Failed to parse response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ratingID := createdRating["id"].(float64)
|
||||||
|
|
||||||
|
voteData := map[string]interface{}{
|
||||||
|
"score": 5,
|
||||||
|
}
|
||||||
|
_, err = config.Request("POST", "/ratings/"+string(rune(ratingID))+"/vote/tourist", voteData, user.Token)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to cast vote: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем мой голос
|
||||||
|
myVoteResp, err := config.Request("GET", "/ratings/"+string(rune(ratingID))+"/my-vote/tourist", nil, user.Token)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get my vote: %v", err)
|
||||||
|
}
|
||||||
|
defer myVoteResp.Body.Close()
|
||||||
|
|
||||||
|
if myVoteResp.StatusCode != 200 {
|
||||||
|
t.Errorf("Expected status 200, got %d", myVoteResp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var voteInfo map[string]interface{}
|
||||||
|
if err := config.ParseResponse(myVoteResp, &voteInfo); err != nil {
|
||||||
|
t.Fatalf("Failed to parse response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasVoted, ok := voteInfo["has_voted"].(bool); !ok || !hasVoted {
|
||||||
|
t.Error("has_voted should be true")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("UpdateMyVote", func(t *testing.T) {
|
||||||
|
user := config.CreateTestUser(t)
|
||||||
|
defer config.CleanupTestUser(t, user)
|
||||||
|
|
||||||
|
objectID := config.CreateTestObject(t, user.Token)
|
||||||
|
|
||||||
|
ratingData := map[string]interface{}{
|
||||||
|
"object_id": objectID,
|
||||||
|
"platform": "tourist",
|
||||||
|
}
|
||||||
|
resp, err := config.Request("POST", "/ratings", ratingData, user.Token)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create rating: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
var createdRating map[string]interface{}
|
||||||
|
if err := config.ParseResponse(resp, &createdRating); err != nil {
|
||||||
|
t.Fatalf("Failed to parse response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ratingID := createdRating["id"].(float64)
|
||||||
|
|
||||||
|
// Голосуем
|
||||||
|
voteData := map[string]interface{}{
|
||||||
|
"score": 5,
|
||||||
|
}
|
||||||
|
_, err = config.Request("POST", "/ratings/"+string(rune(ratingID))+"/vote/tourist", voteData, user.Token)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to cast vote: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновляем голос
|
||||||
|
updateData := map[string]interface{}{
|
||||||
|
"score": 4,
|
||||||
|
}
|
||||||
|
updateResp, err := config.Request("PUT", "/ratings/"+string(rune(ratingID))+"/my-vote/tourist", updateData, user.Token)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to update vote: %v", err)
|
||||||
|
}
|
||||||
|
defer updateResp.Body.Close()
|
||||||
|
|
||||||
|
if updateResp.StatusCode != 200 {
|
||||||
|
t.Errorf("Expected status 200, got %d", updateResp.StatusCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetRatingStats", func(t *testing.T) {
|
||||||
|
resp, err := config.Request("GET", "/ratings/stats", nil, "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get rating stats: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var stats map[string]interface{}
|
||||||
|
if err := config.ParseResponse(resp, &stats); err != nil {
|
||||||
|
t.Fatalf("Failed to parse response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedFields := []string{"total_ratings", "total_votes", "platform_distribution"}
|
||||||
|
for _, field := range expectedFields {
|
||||||
|
if _, ok := stats[field]; !ok {
|
||||||
|
t.Errorf("Expected field %s not found", field)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("DeleteMyVote", func(t *testing.T) {
|
||||||
|
user := config.CreateTestUser(t)
|
||||||
|
defer config.CleanupTestUser(t, user)
|
||||||
|
|
||||||
|
objectID := config.CreateTestObject(t, user.Token)
|
||||||
|
|
||||||
|
ratingData := map[string]interface{}{
|
||||||
|
"object_id": objectID,
|
||||||
|
"platform": "tourist",
|
||||||
|
}
|
||||||
|
resp, err := config.Request("POST", "/ratings", ratingData, user.Token)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create rating: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
var createdRating map[string]interface{}
|
||||||
|
if err := config.ParseResponse(resp, &createdRating); err != nil {
|
||||||
|
t.Fatalf("Failed to parse response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ratingID := createdRating["id"].(float64)
|
||||||
|
|
||||||
|
// Голосуем
|
||||||
|
voteData := map[string]interface{}{
|
||||||
|
"score": 5,
|
||||||
|
}
|
||||||
|
_, err = config.Request("POST", "/ratings/"+string(rune(ratingID))+"/vote/tourist", voteData, user.Token)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to cast vote: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Удаляем голос
|
||||||
|
deleteResp, err := config.Request("DELETE", "/ratings/"+string(rune(ratingID))+"/my-vote/tourist", nil, user.Token)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to delete vote: %v", err)
|
||||||
|
}
|
||||||
|
defer deleteResp.Body.Close()
|
||||||
|
|
||||||
|
if deleteResp.StatusCode != 204 {
|
||||||
|
t.Errorf("Expected status 204, got %d", deleteResp.StatusCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
package testutils
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
package testutils
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
type CreateObjectRequest struct {
|
||||||
|
ShortName string `json:"short_name"`
|
||||||
|
LongName string `json:"long_name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Phone string `json:"phone"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Site string `json:"site"`
|
||||||
|
ShortDescription string `json:"short_description"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Address string `json:"address"`
|
||||||
|
Latitude float64 `json:"latitude"`
|
||||||
|
Longitude float64 `json:"longitude"`
|
||||||
|
IsActive bool `json:"is_active"`
|
||||||
|
IsVerified bool `json:"is_verified"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateFeedbackRequest struct {
|
||||||
|
ObjectID uint `json:"object_id"`
|
||||||
|
Rating int `json:"rating"`
|
||||||
|
Text string `json:"text"`
|
||||||
|
Platform string `json:"platform"`
|
||||||
|
MediaURLs []string `json:"media_urls"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateCommentRequest struct {
|
||||||
|
FeedbackID uint `json:"feedback_id"`
|
||||||
|
Text string `json:"text"`
|
||||||
|
ParentID *uint `json:"parent_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TestConfig) CreateTestObject(t *testing.T, token string) uint {
|
||||||
|
req := CreateObjectRequest{
|
||||||
|
ShortName: "Test Object",
|
||||||
|
LongName: "Test Object for Testing",
|
||||||
|
Type: "cafe",
|
||||||
|
Phone: "+71234567890",
|
||||||
|
Email: "test@object.com",
|
||||||
|
Site: "https://test.com",
|
||||||
|
ShortDescription: "Test description",
|
||||||
|
Description: "Full test description",
|
||||||
|
Address: "Test Address 123",
|
||||||
|
Latitude: 55.751244,
|
||||||
|
Longitude: 37.618423,
|
||||||
|
IsActive: true,
|
||||||
|
IsVerified: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.Request("POST", "/objects", req, token)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create test object: %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)
|
||||||
|
}
|
||||||
|
|
||||||
|
if id, ok := result["id"].(float64); ok {
|
||||||
|
return uint(id)
|
||||||
|
}
|
||||||
|
t.Fatal("Failed to get object ID")
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TestConfig) CreateTestFeedback(t *testing.T, token string, objectID uint) uint {
|
||||||
|
req := CreateFeedbackRequest{
|
||||||
|
ObjectID: objectID,
|
||||||
|
Rating: 5,
|
||||||
|
Text: "Test feedback content",
|
||||||
|
Platform: "tourist",
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.Request("POST", "/feedbacks", req, token)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create test feedback: %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)
|
||||||
|
}
|
||||||
|
|
||||||
|
if id, ok := result["id"].(float64); ok {
|
||||||
|
return uint(id)
|
||||||
|
}
|
||||||
|
t.Fatal("Failed to get feedback ID")
|
||||||
|
return 0
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
package testutils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/cookiejar"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TestConfig struct {
|
||||||
|
BaseURL string
|
||||||
|
Client *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
type TestUser struct {
|
||||||
|
Email string
|
||||||
|
Password string
|
||||||
|
FirstName string
|
||||||
|
LastName string
|
||||||
|
Token string
|
||||||
|
UserID uint
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTestConfig() *TestConfig {
|
||||||
|
jar, _ := cookiejar.New(nil)
|
||||||
|
return &TestConfig{
|
||||||
|
BaseURL: "http://localhost:8088/api/v1",
|
||||||
|
Client: &http.Client{
|
||||||
|
Jar: jar,
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Client.Do(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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",
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user