From 8dfe7e8b4aee7d5347ba002b980bf56ad0eb7e7d Mon Sep 17 00:00:00 2001 From: valitovgaziz Date: Mon, 8 Jun 2026 01:44:23 +0500 Subject: [PATCH] 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 --- .../yalarba/api_yal/cmd/testrunner/README.md | 37 ++ .../yalarba/api_yal/cmd/testrunner/main.go | 574 ++++++++++++++++++ .../yalarba/api_yal/cmd/testrunner/runner.go | 199 ++++++ .../intergration/auth_integration_test.go | 112 ---- .../intergration/objects_integration_test.go | 119 ---- .../api_yal/test/intergration/setup_test.go | 118 ---- main_dc/yalarba/api_yal/test/setup_test.go | 116 ---- .../api_yal/tests/integration/account_test.go | 159 +++++ .../api_yal/tests/integration/appeal_test.go | 192 ++++++ .../api_yal/tests/integration/auth_test.go | 215 +++++++ .../api_yal/tests/integration/comment_test.go | 223 +++++++ .../tests/integration/feedback_test.go | 217 +++++++ .../api_yal/tests/integration/object_test.go | 243 ++++++++ .../api_yal/tests/integration/rating_test.go | 240 ++++++++ .../yalarba/api_yal/tests/testutils/client.go | 1 + .../api_yal/tests/testutils/fixtures.go | 94 +++ .../yalarba/api_yal/tests/testutils/setup.go | 135 ++++ 17 files changed, 2529 insertions(+), 465 deletions(-) create mode 100644 main_dc/yalarba/api_yal/cmd/testrunner/README.md create mode 100644 main_dc/yalarba/api_yal/cmd/testrunner/main.go create mode 100644 main_dc/yalarba/api_yal/cmd/testrunner/runner.go delete mode 100644 main_dc/yalarba/api_yal/test/intergration/auth_integration_test.go delete mode 100644 main_dc/yalarba/api_yal/test/intergration/objects_integration_test.go delete mode 100644 main_dc/yalarba/api_yal/test/intergration/setup_test.go delete mode 100644 main_dc/yalarba/api_yal/test/setup_test.go create mode 100644 main_dc/yalarba/api_yal/tests/integration/account_test.go create mode 100644 main_dc/yalarba/api_yal/tests/integration/appeal_test.go create mode 100644 main_dc/yalarba/api_yal/tests/integration/auth_test.go create mode 100644 main_dc/yalarba/api_yal/tests/integration/comment_test.go create mode 100644 main_dc/yalarba/api_yal/tests/integration/feedback_test.go create mode 100644 main_dc/yalarba/api_yal/tests/integration/object_test.go create mode 100644 main_dc/yalarba/api_yal/tests/integration/rating_test.go create mode 100644 main_dc/yalarba/api_yal/tests/testutils/client.go create mode 100644 main_dc/yalarba/api_yal/tests/testutils/fixtures.go create mode 100644 main_dc/yalarba/api_yal/tests/testutils/setup.go diff --git a/main_dc/yalarba/api_yal/cmd/testrunner/README.md b/main_dc/yalarba/api_yal/cmd/testrunner/README.md new file mode 100644 index 0000000..df27de4 --- /dev/null +++ b/main_dc/yalarba/api_yal/cmd/testrunner/README.md @@ -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 \ No newline at end of file diff --git a/main_dc/yalarba/api_yal/cmd/testrunner/main.go b/main_dc/yalarba/api_yal/cmd/testrunner/main.go new file mode 100644 index 0000000..f932d2d --- /dev/null +++ b/main_dc/yalarba/api_yal/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) + } +} diff --git a/main_dc/yalarba/api_yal/cmd/testrunner/runner.go b/main_dc/yalarba/api_yal/cmd/testrunner/runner.go new file mode 100644 index 0000000..cf25d62 --- /dev/null +++ b/main_dc/yalarba/api_yal/cmd/testrunner/runner.go @@ -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) + } + } + } +} \ No newline at end of file diff --git a/main_dc/yalarba/api_yal/test/intergration/auth_integration_test.go b/main_dc/yalarba/api_yal/test/intergration/auth_integration_test.go deleted file mode 100644 index c669950..0000000 --- a/main_dc/yalarba/api_yal/test/intergration/auth_integration_test.go +++ /dev/null @@ -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") -} \ No newline at end of file diff --git a/main_dc/yalarba/api_yal/test/intergration/objects_integration_test.go b/main_dc/yalarba/api_yal/test/intergration/objects_integration_test.go deleted file mode 100644 index 36ee382..0000000 --- a/main_dc/yalarba/api_yal/test/intergration/objects_integration_test.go +++ /dev/null @@ -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) - } -} \ No newline at end of file diff --git a/main_dc/yalarba/api_yal/test/intergration/setup_test.go b/main_dc/yalarba/api_yal/test/intergration/setup_test.go deleted file mode 100644 index b131a2a..0000000 --- a/main_dc/yalarba/api_yal/test/intergration/setup_test.go +++ /dev/null @@ -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) - } - } -} \ No newline at end of file diff --git a/main_dc/yalarba/api_yal/test/setup_test.go b/main_dc/yalarba/api_yal/test/setup_test.go deleted file mode 100644 index 72f1698..0000000 --- a/main_dc/yalarba/api_yal/test/setup_test.go +++ /dev/null @@ -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) - } - } -} diff --git a/main_dc/yalarba/api_yal/tests/integration/account_test.go b/main_dc/yalarba/api_yal/tests/integration/account_test.go new file mode 100644 index 0000000..b837f4b --- /dev/null +++ b/main_dc/yalarba/api_yal/tests/integration/account_test.go @@ -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) + } + }) +} diff --git a/main_dc/yalarba/api_yal/tests/integration/appeal_test.go b/main_dc/yalarba/api_yal/tests/integration/appeal_test.go new file mode 100644 index 0000000..68ca8a9 --- /dev/null +++ b/main_dc/yalarba/api_yal/tests/integration/appeal_test.go @@ -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) + } + }) +} \ No newline at end of file diff --git a/main_dc/yalarba/api_yal/tests/integration/auth_test.go b/main_dc/yalarba/api_yal/tests/integration/auth_test.go new file mode 100644 index 0000000..d0f57db --- /dev/null +++ b/main_dc/yalarba/api_yal/tests/integration/auth_test.go @@ -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) + } + } + }) +} \ No newline at end of file diff --git a/main_dc/yalarba/api_yal/tests/integration/comment_test.go b/main_dc/yalarba/api_yal/tests/integration/comment_test.go new file mode 100644 index 0000000..2ec49e6 --- /dev/null +++ b/main_dc/yalarba/api_yal/tests/integration/comment_test.go @@ -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) + } + }) +} \ No newline at end of file diff --git a/main_dc/yalarba/api_yal/tests/integration/feedback_test.go b/main_dc/yalarba/api_yal/tests/integration/feedback_test.go new file mode 100644 index 0000000..5c7b0f7 --- /dev/null +++ b/main_dc/yalarba/api_yal/tests/integration/feedback_test.go @@ -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) + } + }) +} \ No newline at end of file diff --git a/main_dc/yalarba/api_yal/tests/integration/object_test.go b/main_dc/yalarba/api_yal/tests/integration/object_test.go new file mode 100644 index 0000000..f9b539f --- /dev/null +++ b/main_dc/yalarba/api_yal/tests/integration/object_test.go @@ -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) + } + }) +} diff --git a/main_dc/yalarba/api_yal/tests/integration/rating_test.go b/main_dc/yalarba/api_yal/tests/integration/rating_test.go new file mode 100644 index 0000000..efb24a0 --- /dev/null +++ b/main_dc/yalarba/api_yal/tests/integration/rating_test.go @@ -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) + } + }) +} \ No newline at end of file diff --git a/main_dc/yalarba/api_yal/tests/testutils/client.go b/main_dc/yalarba/api_yal/tests/testutils/client.go new file mode 100644 index 0000000..8cbf20b --- /dev/null +++ b/main_dc/yalarba/api_yal/tests/testutils/client.go @@ -0,0 +1 @@ +package testutils \ No newline at end of file diff --git a/main_dc/yalarba/api_yal/tests/testutils/fixtures.go b/main_dc/yalarba/api_yal/tests/testutils/fixtures.go new file mode 100644 index 0000000..5e22bca --- /dev/null +++ b/main_dc/yalarba/api_yal/tests/testutils/fixtures.go @@ -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 +} \ No newline at end of file diff --git a/main_dc/yalarba/api_yal/tests/testutils/setup.go b/main_dc/yalarba/api_yal/tests/testutils/setup.go new file mode 100644 index 0000000..52fb9ef --- /dev/null +++ b/main_dc/yalarba/api_yal/tests/testutils/setup.go @@ -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") +} \ No newline at end of file