549 lines
15 KiB
Go
549 lines
15 KiB
Go
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)
|
||
})
|
||
}
|