package processing import ( "m3u8-downloader/pkg/config" "os" "path/filepath" "runtime" "strings" "testing" "time" ) func createTestConfig(tempDir string) *config.Config { return &config.Config{ Core: config.CoreConfig{ WorkerCount: 2, RefreshDelay: 1 * time.Second, }, NAS: config.NASConfig{ OutputPath: filepath.Join(tempDir, "nas"), Username: "testuser", Password: "testpass", Timeout: 10 * time.Second, RetryLimit: 2, EnableTransfer: false, // Disable to avoid NAS connection }, Processing: config.ProcessingConfig{ Enabled: true, AutoProcess: true, WorkerCount: 1, FFmpegPath: "echo", // Use echo command for testing }, Paths: config.PathsConfig{ LocalOutput: filepath.Join(tempDir, "data"), ProcessOutput: filepath.Join(tempDir, "out"), ManifestDir: filepath.Join(tempDir, "data"), PersistenceFile: filepath.Join(tempDir, "queue.json"), }, } } func TestNewProcessingService_Success(t *testing.T) { tempDir, err := os.MkdirTemp("", "processing_test_*") if err != nil { t.Fatalf("Failed to create temp dir: %v", err) } defer os.RemoveAll(tempDir) cfg := createTestConfig(tempDir) cfg.NAS.EnableTransfer = false // Disable NAS to avoid connection // We can't test actual NAS connection, so we'll skip the constructor test // that requires NAS connectivity. Instead, test the configuration handling. if cfg.Processing.FFmpegPath != "echo" { t.Errorf("Expected FFmpegPath='echo', got '%s'", cfg.Processing.FFmpegPath) } } func TestNewProcessingService_NilConfig(t *testing.T) { _, err := NewProcessingService("test-event", nil) if err == nil { t.Error("Expected error for nil config") } if !strings.Contains(err.Error(), "configuration is required") { t.Errorf("Expected 'configuration is required' error, got: %v", err) } } func TestProcessingService_GetEventDirs(t *testing.T) { tempDir, err := os.MkdirTemp("", "processing_test_*") if err != nil { t.Fatalf("Failed to create temp dir: %v", err) } defer os.RemoveAll(tempDir) cfg := createTestConfig(tempDir) // Create mock NAS directory structure nasDir := cfg.NAS.OutputPath os.MkdirAll(filepath.Join(nasDir, "event1"), 0755) os.MkdirAll(filepath.Join(nasDir, "event2"), 0755) os.MkdirAll(filepath.Join(nasDir, "event3"), 0755) // Create a file (should be ignored) os.WriteFile(filepath.Join(nasDir, "not_a_dir.txt"), []byte("test"), 0644) ps := &ProcessingService{ config: cfg, eventName: "", // Empty to test directory discovery } dirs, err := ps.GetEventDirs() if err != nil { t.Fatalf("GetEventDirs() failed: %v", err) } if len(dirs) != 3 { t.Errorf("Expected 3 event directories, got %d", len(dirs)) } expectedDirs := []string{"event1", "event2", "event3"} for _, expected := range expectedDirs { found := false for _, actual := range dirs { if actual == expected { found = true break } } if !found { t.Errorf("Expected to find directory '%s' in results: %v", expected, dirs) } } } func TestProcessingService_GetEventDirs_WithEventName(t *testing.T) { cfg := createTestConfig("/tmp") eventName := "specific-event" ps := &ProcessingService{ config: cfg, eventName: eventName, } dirs, err := ps.GetEventDirs() if err != nil { t.Fatalf("GetEventDirs() failed: %v", err) } if len(dirs) != 1 { t.Errorf("Expected 1 directory, got %d", len(dirs)) } if dirs[0] != eventName { t.Errorf("Expected directory '%s', got '%s'", eventName, dirs[0]) } } func TestProcessingService_GetResolutions(t *testing.T) { tempDir, err := os.MkdirTemp("", "processing_test_*") if err != nil { t.Fatalf("Failed to create temp dir: %v", err) } defer os.RemoveAll(tempDir) cfg := createTestConfig(tempDir) eventName := "test-event" // Create mock event directory with quality subdirectories eventPath := filepath.Join(cfg.NAS.OutputPath, eventName) os.MkdirAll(filepath.Join(eventPath, "1080p"), 0755) os.MkdirAll(filepath.Join(eventPath, "720p"), 0755) os.MkdirAll(filepath.Join(eventPath, "480p"), 0755) os.MkdirAll(filepath.Join(eventPath, "not_resolution"), 0755) // Should be ignored os.WriteFile(filepath.Join(eventPath, "file.txt"), []byte("test"), 0644) // Should be ignored ps := &ProcessingService{ config: cfg, eventName: eventName, } resolutions, err := ps.GetResolutions() if err != nil { t.Fatalf("GetResolutions() failed: %v", err) } expectedResolutions := []string{"1080p", "720p", "480p"} if len(resolutions) != len(expectedResolutions) { t.Errorf("Expected %d resolutions, got %d: %v", len(expectedResolutions), len(resolutions), resolutions) } for _, expected := range expectedResolutions { found := false for _, actual := range resolutions { if actual == expected { found = true break } } if !found { t.Errorf("Expected to find resolution '%s' in results: %v", expected, resolutions) } } } func TestProcessingService_AggregateSegmentInfo(t *testing.T) { ps := &ProcessingService{} // Create test channel with segments ch := make(chan SegmentInfo, 5) // Add segments with different qualities for same sequence ch <- SegmentInfo{Name: "seg_1001.ts", SeqNo: 1001, Resolution: "720p"} ch <- SegmentInfo{Name: "seg_1001.ts", SeqNo: 1001, Resolution: "1080p"} // Higher quality, should win ch <- SegmentInfo{Name: "seg_1002.ts", SeqNo: 1002, Resolution: "480p"} ch <- SegmentInfo{Name: "seg_1003.ts", SeqNo: 1003, Resolution: "1080p"} ch <- SegmentInfo{Name: "seg_1001.ts", SeqNo: 1001, Resolution: "540p"} // Lower than 1080p, should not replace close(ch) segmentMap, err := ps.AggregateSegmentInfo(ch) if err != nil { t.Fatalf("AggregateSegmentInfo() failed: %v", err) } // Should have 3 unique sequence numbers if len(segmentMap) != 3 { t.Errorf("Expected 3 unique segments, got %d", len(segmentMap)) } // Check sequence 1001 has the highest quality (1080p) seg1001, exists := segmentMap[1001] if !exists { t.Fatal("Segment 1001 should exist") } if seg1001.Resolution != "1080p" { t.Errorf("Expected segment 1001 to have resolution '1080p', got '%s'", seg1001.Resolution) } // Check sequence 1002 has 480p seg1002, exists := segmentMap[1002] if !exists { t.Fatal("Segment 1002 should exist") } if seg1002.Resolution != "480p" { t.Errorf("Expected segment 1002 to have resolution '480p', got '%s'", seg1002.Resolution) } // Check sequence 1003 has 1080p seg1003, exists := segmentMap[1003] if !exists { t.Fatal("Segment 1003 should exist") } if seg1003.Resolution != "1080p" { t.Errorf("Expected segment 1003 to have resolution '1080p', got '%s'", seg1003.Resolution) } } func TestProcessingService_WriteConcatFile(t *testing.T) { tempDir, err := os.MkdirTemp("", "processing_test_*") if err != nil { t.Fatalf("Failed to create temp dir: %v", err) } defer os.RemoveAll(tempDir) cfg := createTestConfig(tempDir) eventName := "test-event" ps := &ProcessingService{ config: cfg, eventName: eventName, } // Create test segment map segmentMap := map[int]SegmentInfo{ 1003: {Name: "seg_1003.ts", SeqNo: 1003, Resolution: "1080p"}, 1001: {Name: "seg_1001.ts", SeqNo: 1001, Resolution: "720p"}, 1002: {Name: "seg_1002.ts", SeqNo: 1002, Resolution: "1080p"}, } concatFilePath, err := ps.WriteConcatFile(segmentMap) if err != nil { t.Fatalf("WriteConcatFile() failed: %v", err) } // Verify file was created if _, err := os.Stat(concatFilePath); os.IsNotExist(err) { t.Fatalf("Concat file was not created: %s", concatFilePath) } // Read and verify content content, err := os.ReadFile(concatFilePath) if err != nil { t.Fatalf("Failed to read concat file: %v", err) } contentStr := string(content) lines := strings.Split(strings.TrimSpace(contentStr), "\n") if len(lines) != 3 { t.Errorf("Expected 3 lines in concat file, got %d", len(lines)) } // Verify segments are sorted by sequence number expectedOrder := []string{"seg_1001.ts", "seg_1002.ts", "seg_1003.ts"} for i, line := range lines { if !strings.Contains(line, expectedOrder[i]) { t.Errorf("Line %d should contain '%s', got: %s", i, expectedOrder[i], line) } if !strings.HasPrefix(line, "file '") { t.Errorf("Line %d should start with 'file ', got: %s", i, line) } } } func TestProcessingService_getFFmpegPath(t *testing.T) { cfg := createTestConfig("/tmp") tests := []struct { name string ffmpegPath string shouldFind bool expectedError string }{ { name: "echo command (should be found in PATH)", ffmpegPath: "echo", shouldFind: true, }, { name: "absolute path test", ffmpegPath: func() string { if runtime.GOOS == "windows" { return "C:\\Windows\\System32\\cmd.exe" } return "/bin/echo" }(), shouldFind: true, }, { name: "nonexistent command", ffmpegPath: "nonexistent_ffmpeg_command_12345", shouldFind: false, expectedError: "FFmpeg not found", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { testCfg := *cfg testCfg.Processing.FFmpegPath = tt.ffmpegPath ps := &ProcessingService{ config: &testCfg, eventName: "test", } path, err := ps.getFFmpegPath() if tt.shouldFind { if err != nil { t.Errorf("Expected to find FFmpeg, but got error: %v", err) } if path == "" { t.Error("Expected non-empty path") } } else { if err == nil { t.Error("Expected error for nonexistent FFmpeg") } if tt.expectedError != "" && !strings.Contains(err.Error(), tt.expectedError) { t.Errorf("Expected error containing '%s', got: %v", tt.expectedError, err) } } }) } } func TestSegmentInfo_Structure(t *testing.T) { segment := SegmentInfo{ Name: "test_segment.ts", SeqNo: 1001, Resolution: "1080p", } if segment.Name != "test_segment.ts" { t.Errorf("Expected Name='test_segment.ts', got '%s'", segment.Name) } if segment.SeqNo != 1001 { t.Errorf("Expected SeqNo=1001, got %d", segment.SeqNo) } if segment.Resolution != "1080p" { t.Errorf("Expected Resolution='1080p', got '%s'", segment.Resolution) } } func TestProcessJob_Structure(t *testing.T) { job := ProcessJob{ EventName: "test-event", } if job.EventName != "test-event" { t.Errorf("Expected EventName='test-event', got '%s'", job.EventName) } }