tailly_clips/clips_test.go
admin 57eba68496
Some checks failed
continuous-integration/drone/push Build is failing
v.0.0.1 Создан сервис клипов
2025-09-02 11:58:10 +03:00

549 lines
15 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

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

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