package main import ( "context" "database/sql" "fmt" "io/ioutil" "log" "net" "os" "path/filepath" "tailly_clips/internal/moderation" "testing" "time" "tailly_clips/internal/ffmpeg" "tailly_clips/internal/handler" "tailly_clips/internal/repository" "tailly_clips/internal/service" "tailly_clips/internal/storage" "tailly_clips/proto" "github.com/caarlos0/env/v8" "github.com/joho/godotenv" _ "github.com/lib/pq" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" ) type RealTestSuite struct { grpcClient proto.ClipServiceClient grpcConn *grpc.ClientConn grpcServer *grpc.Server testUserID int32 } type Config struct { DBHost string `env:"DB_HOST" env-default:"localhost"` DBPort string `env:"DB_PORT" env-default:"5432"` DBUser string `env:"DB_USER" env-default:"postgres"` DBPassword string `env:"DB_PASSWORD" env-default:"postgres"` DBName string `env:"DB_NAME" env-default:"clip_service"` S3Endpoint string `env:"S3_ENDPOINT,required"` S3Bucket string `env:"S3_BUCKET,required"` S3Region string `env:"S3_REGION,required"` S3AccessKey string `env:"S3_ACCESS_KEY,required"` S3SecretKey string `env:"S3_SECRET_KEY,required"` ModerationAddr string `env:"MODERATION_ADDR" env-default:"localhost:50051"` } func loadConfig() (*Config, error) { // Загружаем .env файл (игнорируем ошибку если файла нет) _ = godotenv.Load() cfg := &Config{} if err := env.Parse(cfg); err != nil { return nil, err } return cfg, nil } func initDatabase(cfg *Config) (*sql.DB, error) { connStr := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", cfg.DBHost, cfg.DBPort, cfg.DBUser, cfg.DBPassword, cfg.DBName) db, err := sql.Open("postgres", connStr) if err != nil { return nil, fmt.Errorf("failed to open database: %w", err) } if err = db.Ping(); err != nil { return nil, fmt.Errorf("failed to ping database: %w", err) } db.SetMaxOpenConns(25) db.SetMaxIdleConns(25) db.SetConnMaxLifetime(5 * time.Minute) log.Println("Database connected successfully") return db, nil } func setupRealTestSuite(t *testing.T) *RealTestSuite { // Загружаем конфигурацию из основного .env файла cfg, err := loadConfig() if err != nil { log.Fatalf("Failed to load config: %v", err) } // Инициализация базы данных db, err := initDatabase(cfg) if err != nil { log.Fatalf("Failed to initialize database: %v", err) } // Инициализация S3 storage s3Storage := storage.NewS3Storage(storage.S3Config{ Endpoint: cfg.S3Endpoint, Bucket: cfg.S3Bucket, Region: cfg.S3Region, AccessKey: cfg.S3AccessKey, SecretKey: cfg.S3SecretKey, }) modClient, err := moderation.NewModerationClient(cfg.ModerationAddr) if err != nil { log.Fatalf("Failed to create moderation client: %v", err) } // Инициализация сервисов videoProcessor := ffmpeg.NewVideoProcessor() clipRepo := repository.NewClipRepository(db) likeRepo := repository.NewLikeRepository(db) commentRepo := repository.NewCommentRepository(db) clipService := service.NewClipService(clipRepo, s3Storage, videoProcessor, modClient) likeService := service.NewLikeService(likeRepo, clipRepo) commentService := service.NewCommentService(commentRepo, clipRepo) grpcHandler := handler.NewGRPCHandler(clipService, likeService, commentService) // Запускаем gRPC сервер на случайном порту listener, err := net.Listen("tcp", ":0") if err != nil { t.Fatalf("Failed to listen: %v", err) } server := grpc.NewServer( grpc.MaxRecvMsgSize(50*1024*1024), // 50MB grpc.MaxSendMsgSize(50*1024*1024), // 50MB ) proto.RegisterClipServiceServer(server, grpcHandler) go func() { if err := server.Serve(listener); err != nil { log.Printf("Server exited with error: %v", err) } }() // Подключаемся к серверу с увеличенным размером сообщения conn, err := grpc.Dial(listener.Addr().String(), grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithDefaultCallOptions( grpc.MaxCallRecvMsgSize(50*1024*1024), grpc.MaxCallSendMsgSize(50*1024*1024), ), ) if err != nil { t.Fatalf("Failed to dial: %v", err) } client := proto.NewClipServiceClient(conn) // Ждем немного для инициализации сервера time.Sleep(100 * time.Millisecond) return &RealTestSuite{ grpcClient: client, grpcConn: conn, grpcServer: server, testUserID: 9999, // Используем специальный ID для тестов } } func (s *RealTestSuite) cleanup() { if s.grpcServer != nil { s.grpcServer.Stop() } if s.grpcConn != nil { s.grpcConn.Close() } } func getRealVideoFiles(t *testing.T) []string { // Укажите путь к директории с реальными видео файлами videoDir := "./test_videos" // Измените на ваш путь if _, err := os.Stat(videoDir); os.IsNotExist(err) { t.Skipf("Video directory %s does not exist, skipping test", videoDir) return nil } files, err := ioutil.ReadDir(videoDir) if err != nil { t.Fatalf("Failed to read video directory: %v", err) } var videoFiles []string for _, file := range files { if !file.IsDir() { ext := filepath.Ext(file.Name()) if ext == ".mp4" || ext == ".mov" || ext == ".avi" || ext == ".mkv" { videoFiles = append(videoFiles, filepath.Join(videoDir, file.Name())) } } } if len(videoFiles) == 0 { t.Skip("No video files found in test directory") } // Ограничиваем максимум 10 файлами для теста if len(videoFiles) > 10 { videoFiles = videoFiles[:10] } return videoFiles } func TestRealIntegration_UploadMultipleVideos(t *testing.T) { suite := setupRealTestSuite(t) defer suite.cleanup() ctx := context.Background() // Получаем реальные видео файлы videoFiles := getRealVideoFiles(t) if videoFiles == nil { return } t.Logf("Found %d video files for testing", len(videoFiles)) // Загружаем каждое видео for i, videoPath := range videoFiles { t.Run(fmt.Sprintf("Upload_%s", filepath.Base(videoPath)), func(t *testing.T) { // Читаем видео файл videoData, err := ioutil.ReadFile(videoPath) if err != nil { t.Fatalf("Failed to read video file %s: %v", videoPath, err) } // Создаем уникальное название для избежания конфликтов uniqueTitle := fmt.Sprintf("Test Clip %d - %s - %d", i+1, filepath.Base(videoPath), time.Now().Unix()) // Создаем запрос req := &proto.CreateClipRequest{ UserId: suite.testUserID, Title: uniqueTitle, VideoData: videoData, FileName: filepath.Base(videoPath), ContentType: "video/mp4", } t.Logf("Uploading %s (%d bytes)", req.FileName, len(videoData)) // Вызываем gRPC метод startTime := time.Now() resp, err := suite.grpcClient.CreateClip(ctx, req) uploadTime := time.Since(startTime) if err != nil { t.Fatalf("Failed to create clip: %v", err) } t.Logf("Successfully uploaded in %v: %s (ID: %d)", uploadTime, resp.Clip.Title, resp.Clip.Id) // Проверяем результат if resp.Clip == nil { t.Fatal("Expected clip in response") } if resp.Clip.Title != uniqueTitle { t.Errorf("Expected title %s, got %s", uniqueTitle, resp.Clip.Title) } if resp.Clip.AuthorId != suite.testUserID { t.Errorf("Expected author ID %d, got %d", suite.testUserID, resp.Clip.AuthorId) } if resp.Clip.Duration <= 0 { t.Error("Expected positive duration") } if resp.Clip.VideoUrl == "" { t.Error("Expected video URL") } if resp.Clip.ThumbnailUrl == "" { t.Error("Expected thumbnail URL") } t.Logf("Video URL: %s", resp.Clip.VideoUrl) t.Logf("Thumbnail URL: %s", resp.Clip.ThumbnailUrl) t.Logf("Duration: %d seconds", resp.Clip.Duration) }) } } func TestRealIntegration_ClipOperations(t *testing.T) { suite := setupRealTestSuite(t) defer suite.cleanup() ctx := context.Background() // Сначала загружаем тестовое видео videoFiles := getRealVideoFiles(t) if videoFiles == nil || len(videoFiles) == 0 { t.Skip("No video files available for testing") return } videoData, err := ioutil.ReadFile(videoFiles[0]) if err != nil { t.Fatalf("Failed to read test video: %v", err) } // Создаем клип createResp, err := suite.grpcClient.CreateClip(ctx, &proto.CreateClipRequest{ UserId: suite.testUserID, Title: fmt.Sprintf("Integration Test Clip - %d", time.Now().Unix()), VideoData: videoData, FileName: filepath.Base(videoFiles[0]), ContentType: "video/mp4", }) if err != nil { t.Fatalf("Failed to create clip: %v", err) } clipID := createResp.Clip.Id t.Logf("Created test clip with ID: %d", clipID) // Тест 1: Получение клипа t.Run("Get_clip", func(t *testing.T) { resp, err := suite.grpcClient.GetClip(ctx, &proto.GetClipRequest{ClipId: clipID}) if err != nil { t.Fatalf("Failed to get clip: %v", err) } if resp.Clip.Id != clipID { t.Errorf("Expected clip ID %d, got %d", clipID, resp.Clip.Id) } t.Logf("Successfully retrieved clip: %s", resp.Clip.Title) }) // Тест 2: Лайк клипа t.Run("Like_clip", func(t *testing.T) { _, err := suite.grpcClient.LikeClip(ctx, &proto.LikeClipRequest{ ClipId: clipID, UserId: suite.testUserID, }) if err != nil { t.Fatalf("Failed to like clip: %v", err) } // Проверяем что лайк добавился checkResp, err := suite.grpcClient.CheckIfLiked(ctx, &proto.CheckIfLikedRequest{ ClipId: clipID, UserId: suite.testUserID, }) if err != nil { t.Fatalf("Failed to check like: %v", err) } if !checkResp.IsLiked { t.Error("Expected clip to be liked") } t.Logf("Successfully liked clip %d", clipID) }) // Тест 3: Комментарий t.Run("Add_comment", func(t *testing.T) { commentText := fmt.Sprintf("Test comment at %s", time.Now().Format(time.RFC3339)) commentResp, err := suite.grpcClient.CreateComment(ctx, &proto.CreateCommentRequest{ ClipId: clipID, UserId: suite.testUserID, Content: commentText, }) if err != nil { t.Fatalf("Failed to add comment: %v", err) } if commentResp.Comment.Content != commentText { t.Errorf("Expected comment '%s', got '%s'", commentText, commentResp.Comment.Content) } t.Logf("Successfully added comment with ID: %d", commentResp.Comment.Id) }) // Тест 4: Получение комментариев t.Run("Get_comments", func(t *testing.T) { resp, err := suite.grpcClient.GetClipComments(ctx, &proto.GetClipCommentsRequest{ ClipId: clipID, Limit: 10, Offset: 0, }) if err != nil { t.Fatalf("Failed to get comments: %v", err) } if len(resp.Comments) == 0 { t.Error("Expected at least one comment") } else { t.Logf("Found %d comments for clip %d", len(resp.Comments), clipID) } }) // Тест 5: Получение списка клипов пользователя t.Run("Get_user_clips", func(t *testing.T) { resp, err := suite.grpcClient.GetUserClips(ctx, &proto.GetUserClipsRequest{ UserId: suite.testUserID, Limit: 10, Offset: 0, }) if err != nil { t.Fatalf("Failed to get user clips: %v", err) } t.Logf("User %d has %d clips (total: %d)", suite.testUserID, len(resp.Clips), resp.TotalCount) }) // Тест 6: Получение всех клипов t.Run("Get_all_clips", func(t *testing.T) { resp, err := suite.grpcClient.GetClips(ctx, &proto.GetClipsRequest{ Limit: 20, Offset: 0, }) if err != nil { t.Fatalf("Failed to get all clips: %v", err) } t.Logf("Total clips in system: %d", resp.TotalCount) if len(resp.Clips) > 0 { t.Logf("Retrieved %d clips", len(resp.Clips)) } }) // NOTE: Удаление клипа закомментировано, так как данные не должны удаляться /* // Тест 7: Удаление клипа t.Run("Delete_clip", func(t *testing.T) { _, err := suite.grpcClient.DeleteClip(ctx, &proto.DeleteClipRequest{ ClipId: clipID, UserId: suite.testUserID, }) if err != nil { t.Fatalf("Failed to delete clip: %v", err) } t.Logf("Successfully deleted clip %d", clipID) }) */ } func TestRealIntegration_PerformanceTest(t *testing.T) { suite := setupRealTestSuite(t) defer suite.cleanup() ctx := context.Background() videoFiles := getRealVideoFiles(t) if videoFiles == nil || len(videoFiles) < 2 { t.Skip("Not enough video files for performance test") return } // Тестируем производительность на первом видео videoPath := videoFiles[0] videoData, err := ioutil.ReadFile(videoPath) if err != nil { t.Fatalf("Failed to read test video: %v", err) } t.Run("Upload_performance", func(t *testing.T) { // Замеряем время загрузки startTime := time.Now() _, err := suite.grpcClient.CreateClip(ctx, &proto.CreateClipRequest{ UserId: suite.testUserID, Title: fmt.Sprintf("Performance Test - %d", time.Now().UnixNano()), VideoData: videoData, FileName: filepath.Base(videoPath), ContentType: "video/mp4", }) uploadTime := time.Since(startTime) if err != nil { t.Fatalf("Upload failed: %v", err) } t.Logf("Upload performance: %v for %d bytes (%.2f MB/s)", uploadTime, len(videoData), float64(len(videoData))/1024/1024/uploadTime.Seconds()) }) t.Run("Concurrent_uploads", func(t *testing.T) { // Тестируем конкурентные загрузки (ограниченное количество) concurrentUploads := 3 if len(videoFiles) < concurrentUploads { concurrentUploads = len(videoFiles) } results := make(chan time.Duration, concurrentUploads) errors := make(chan error, concurrentUploads) for i := 0; i < concurrentUploads; i++ { go func(idx int) { videoData, err := ioutil.ReadFile(videoFiles[idx]) if err != nil { errors <- err return } startTime := time.Now() _, err = suite.grpcClient.CreateClip(ctx, &proto.CreateClipRequest{ UserId: suite.testUserID, Title: fmt.Sprintf("Concurrent %d - %d", idx, time.Now().UnixNano()), VideoData: videoData, FileName: filepath.Base(videoFiles[idx]), ContentType: "video/mp4", }) uploadTime := time.Since(startTime) if err != nil { errors <- err return } results <- uploadTime }(i) } // Собираем результаты var totalTime time.Duration for i := 0; i < concurrentUploads; i++ { select { case duration := <-results: totalTime += duration t.Logf("Concurrent upload %d: %v", i+1, duration) case err := <-errors: t.Errorf("Concurrent upload failed: %v", err) case <-time.After(2 * time.Minute): t.Error("Concurrent upload timeout") } } avgTime := totalTime / time.Duration(concurrentUploads) t.Logf("Average concurrent upload time: %v", avgTime) }) }