Claude Testing Suite
This commit is contained in:
parent
20df800715
commit
7d67cec2b7
128
Makefile
Normal file
128
Makefile
Normal file
@ -0,0 +1,128 @@
|
||||
# StreamRecorder Makefile
|
||||
|
||||
.PHONY: test test-verbose test-coverage build clean help
|
||||
|
||||
# Default target
|
||||
all: build
|
||||
|
||||
# Build the application
|
||||
build:
|
||||
@echo "🔨 Building StreamRecorder..."
|
||||
go build -o stream-recorder.exe ./cmd/main
|
||||
|
||||
# Build for different platforms
|
||||
build-windows:
|
||||
@echo "🔨 Building for Windows..."
|
||||
GOOS=windows GOARCH=amd64 go build -o stream-recorder-windows.exe ./cmd/main
|
||||
|
||||
build-linux:
|
||||
@echo "🔨 Building for Linux..."
|
||||
GOOS=linux GOARCH=amd64 go build -o stream-recorder-linux ./cmd/main
|
||||
|
||||
build-all: build-windows build-linux
|
||||
|
||||
# Run unit tests
|
||||
test:
|
||||
@echo "🧪 Running unit tests..."
|
||||
go run test_runner.go
|
||||
|
||||
# Run tests with verbose output
|
||||
test-verbose:
|
||||
@echo "🧪 Running unit tests (verbose)..."
|
||||
go test -v ./pkg/...
|
||||
|
||||
# Run tests with coverage
|
||||
test-coverage:
|
||||
@echo "🧪 Running tests with coverage..."
|
||||
go test -coverprofile=coverage.out ./pkg/...
|
||||
go tool cover -html=coverage.out -o coverage.html
|
||||
@echo "📊 Coverage report generated: coverage.html"
|
||||
|
||||
# Run tests for a specific package
|
||||
test-pkg:
|
||||
@if [ -z "$(PKG)" ]; then \
|
||||
echo "❌ Please specify package: make test-pkg PKG=./pkg/config"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@echo "🧪 Testing package: $(PKG)"
|
||||
go test -v $(PKG)
|
||||
|
||||
# Run benchmarks
|
||||
benchmark:
|
||||
@echo "🏃 Running benchmarks..."
|
||||
go test -bench=. -benchmem ./pkg/...
|
||||
|
||||
# Clean build artifacts
|
||||
clean:
|
||||
@echo "🧹 Cleaning build artifacts..."
|
||||
rm -f stream-recorder.exe stream-recorder-windows.exe stream-recorder-linux
|
||||
rm -f coverage.out coverage.html
|
||||
rm -rf data/ out/ *.json
|
||||
|
||||
# Format code
|
||||
fmt:
|
||||
@echo "🎨 Formatting code..."
|
||||
go fmt ./...
|
||||
|
||||
# Lint code (requires golangci-lint)
|
||||
lint:
|
||||
@echo "🔍 Linting code..."
|
||||
golangci-lint run
|
||||
|
||||
# Tidy dependencies
|
||||
tidy:
|
||||
@echo "📦 Tidying dependencies..."
|
||||
go mod tidy
|
||||
|
||||
# Run security check (requires gosec)
|
||||
security:
|
||||
@echo "🔒 Running security check..."
|
||||
gosec ./...
|
||||
|
||||
# Install development tools
|
||||
install-tools:
|
||||
@echo "🛠️ Installing development tools..."
|
||||
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
|
||||
go install github.com/securecodewarrior/gosec/v2/cmd/gosec@latest
|
||||
|
||||
# Quick development cycle: format, tidy, build, test
|
||||
dev: fmt tidy build test
|
||||
|
||||
# CI pipeline: format check, lint, security, test, build
|
||||
ci: fmt tidy lint security test build
|
||||
|
||||
# Help
|
||||
help:
|
||||
@echo "StreamRecorder Build Commands"
|
||||
@echo "============================="
|
||||
@echo ""
|
||||
@echo "Build Commands:"
|
||||
@echo " build - Build the main application"
|
||||
@echo " build-windows - Build for Windows (x64)"
|
||||
@echo " build-linux - Build for Linux (x64)"
|
||||
@echo " build-all - Build for all platforms"
|
||||
@echo ""
|
||||
@echo "Test Commands:"
|
||||
@echo " test - Run unit tests with custom runner"
|
||||
@echo " test-verbose - Run tests with verbose output"
|
||||
@echo " test-coverage - Run tests with coverage report"
|
||||
@echo " test-pkg PKG=<pkg> - Test specific package"
|
||||
@echo " benchmark - Run benchmarks"
|
||||
@echo ""
|
||||
@echo "Quality Commands:"
|
||||
@echo " fmt - Format code"
|
||||
@echo " lint - Lint code (requires golangci-lint)"
|
||||
@echo " security - Security analysis (requires gosec)"
|
||||
@echo " tidy - Tidy dependencies"
|
||||
@echo ""
|
||||
@echo "Development Commands:"
|
||||
@echo " dev - Quick dev cycle (fmt, tidy, build, test)"
|
||||
@echo " ci - Full CI pipeline"
|
||||
@echo " clean - Clean build artifacts"
|
||||
@echo " install-tools - Install development tools"
|
||||
@echo ""
|
||||
@echo "Examples:"
|
||||
@echo " make test"
|
||||
@echo " make test-pkg PKG=./pkg/config"
|
||||
@echo " make build-all"
|
||||
@echo " make dev"
|
||||
253
TESTING.md
Normal file
253
TESTING.md
Normal file
@ -0,0 +1,253 @@
|
||||
# Testing Guide
|
||||
|
||||
This document describes the test suite for the StreamRecorder application.
|
||||
|
||||
## Overview
|
||||
|
||||
The test suite provides comprehensive coverage of core application components without requiring external dependencies like video files, NAS connectivity, or FFmpeg. All tests are self-contained and clean up after themselves.
|
||||
|
||||
## Test Structure
|
||||
|
||||
### Unit Tests by Package
|
||||
|
||||
#### `pkg/config`
|
||||
- **File**: `config_test.go`
|
||||
- **Coverage**: Configuration loading, environment variable override, path validation, validation errors
|
||||
- **Key Tests**:
|
||||
- Default config loading
|
||||
- Environment variable overrides
|
||||
- Path resolution and creation
|
||||
- Validation error scenarios
|
||||
|
||||
#### `pkg/utils`
|
||||
- **File**: `paths_test.go`
|
||||
- **Coverage**: Cross-platform path utilities, directory operations, validation
|
||||
- **Key Tests**:
|
||||
- Safe path joining
|
||||
- Directory creation
|
||||
- Path existence checking
|
||||
- Path validation
|
||||
- Write permission testing
|
||||
|
||||
#### `pkg/constants`
|
||||
- **File**: `constants_test.go`
|
||||
- **Coverage**: Constants values, configuration singleton, integration
|
||||
- **Key Tests**:
|
||||
- Constant value verification
|
||||
- Singleton pattern testing
|
||||
- Config integration
|
||||
- Concurrent access safety
|
||||
|
||||
#### `pkg/httpClient`
|
||||
- **File**: `error_test.go`
|
||||
- **Coverage**: HTTP error handling, status code management
|
||||
- **Key Tests**:
|
||||
- HTTP error creation and formatting
|
||||
- Error comparison and detection
|
||||
- Status code extraction
|
||||
- Error wrapping support
|
||||
|
||||
#### `pkg/media`
|
||||
- **File**: `manifest_test.go`
|
||||
- **Coverage**: Manifest generation, segment tracking, JSON serialization
|
||||
- **Key Tests**:
|
||||
- Manifest writer initialization
|
||||
- Segment addition and updates
|
||||
- Quality resolution logic
|
||||
- JSON file generation
|
||||
- Sorting and deduplication
|
||||
|
||||
#### `pkg/processing`
|
||||
- **File**: `service_test.go`
|
||||
- **Coverage**: Processing service logic, path resolution, FFmpeg handling
|
||||
- **Key Tests**:
|
||||
- Service initialization
|
||||
- Event directory scanning
|
||||
- Resolution detection
|
||||
- Segment aggregation
|
||||
- File concatenation list generation
|
||||
- FFmpeg path resolution
|
||||
|
||||
## Running Tests
|
||||
|
||||
### Quick Test Run
|
||||
```bash
|
||||
make test
|
||||
```
|
||||
|
||||
### Verbose Output
|
||||
```bash
|
||||
make test-verbose
|
||||
```
|
||||
|
||||
### Coverage Report
|
||||
```bash
|
||||
make test-coverage
|
||||
```
|
||||
Generates `coverage.html` with detailed coverage report.
|
||||
|
||||
### Test Specific Package
|
||||
```bash
|
||||
make test-pkg PKG=./pkg/config
|
||||
```
|
||||
|
||||
### Manual Test Execution
|
||||
```bash
|
||||
# Run custom test runner
|
||||
go run test_runner.go
|
||||
|
||||
# Run standard go test
|
||||
go test ./pkg/...
|
||||
|
||||
# Run with coverage
|
||||
go test -coverprofile=coverage.out ./pkg/...
|
||||
go tool cover -html=coverage.out
|
||||
```
|
||||
|
||||
## Test Features
|
||||
|
||||
### ✅ Self-Contained
|
||||
- No external file dependencies
|
||||
- No network connections required
|
||||
- No NAS or FFmpeg installation needed
|
||||
|
||||
### ✅ Automatic Cleanup
|
||||
- All temporary files/directories removed after tests
|
||||
- Original environment variables restored
|
||||
- No side effects on host system
|
||||
|
||||
### ✅ Isolated Environment
|
||||
- Tests use temporary directories
|
||||
- Environment variables safely overridden
|
||||
- Configuration isolated from production settings
|
||||
|
||||
### ✅ Cross-Platform
|
||||
- Path handling tested on Windows/Unix
|
||||
- Platform-specific behavior validated
|
||||
- Cross-platform compatibility verified
|
||||
|
||||
### ✅ Comprehensive Coverage
|
||||
- Configuration management
|
||||
- Path utilities and validation
|
||||
- Error handling patterns
|
||||
- Data structures and serialization
|
||||
- Business logic without external dependencies
|
||||
|
||||
## Test Environment
|
||||
|
||||
The test suite automatically:
|
||||
|
||||
1. **Creates Temporary Workspace**: Each test run uses a fresh temporary directory
|
||||
2. **Sets Test Environment**: Overrides environment variables to use test settings
|
||||
3. **Disables External Dependencies**: Sets flags to disable NAS transfer and processing
|
||||
4. **Cleans Up Completely**: Removes all test artifacts and restores environment
|
||||
|
||||
### Environment Variables Set During Tests
|
||||
- `LOCAL_OUTPUT_DIR`: Points to temp directory
|
||||
- `PROCESS_OUTPUT_DIR`: Points to temp directory
|
||||
- `ENABLE_NAS_TRANSFER`: Set to `false`
|
||||
- `PROCESSING_ENABLED`: Set to `false`
|
||||
|
||||
## Extending Tests
|
||||
|
||||
### Adding New Test Cases
|
||||
|
||||
1. **Create test file**: `pkg/yourpackage/yourfile_test.go`
|
||||
2. **Follow naming convention**: `TestFunctionName`
|
||||
3. **Use temp directories**: Always clean up created files
|
||||
4. **Mock external dependencies**: Avoid real file operations where possible
|
||||
|
||||
### Test Template
|
||||
```go
|
||||
package yourpackage
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestYourFunction(t *testing.T) {
|
||||
// Setup
|
||||
tempDir, err := os.MkdirTemp("", "test_*")
|
||||
if err != nil {
|
||||
t.Fatalf("Setup failed: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Test logic
|
||||
result := YourFunction()
|
||||
|
||||
// Assertions
|
||||
if result != expected {
|
||||
t.Errorf("Expected %v, got %v", expected, result)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Best Practices
|
||||
|
||||
- **Always clean up**: Use `defer os.RemoveAll()` for temp directories
|
||||
- **Test error cases**: Don't just test happy paths
|
||||
- **Use table-driven tests**: For multiple similar test cases
|
||||
- **Mock external dependencies**: Use echo/dummy commands instead of real tools
|
||||
- **Validate cleanup**: Ensure tests don't leave artifacts
|
||||
|
||||
## CI/CD Integration
|
||||
|
||||
The test suite is designed for automated environments:
|
||||
|
||||
```bash
|
||||
# Complete CI pipeline
|
||||
make ci
|
||||
|
||||
# Just run tests in CI
|
||||
make test
|
||||
```
|
||||
|
||||
The custom test runner provides:
|
||||
- ✅ Colored output for easy reading
|
||||
- ✅ Test count and timing statistics
|
||||
- ✅ Failure details and summaries
|
||||
- ✅ Automatic environment management
|
||||
- ✅ Exit codes for CI integration
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
**Tests fail with permission errors**
|
||||
- Ensure write permissions in temp directory
|
||||
- Check antivirus software isn't blocking file operations
|
||||
|
||||
**Config tests fail**
|
||||
- Verify no conflicting environment variables are set
|
||||
- Check that temp directories can be created
|
||||
|
||||
**Path tests fail on Windows**
|
||||
- Confirm path separator handling is correct
|
||||
- Verify Windows path validation logic
|
||||
|
||||
### Debug Mode
|
||||
```bash
|
||||
# Run with verbose output to see detailed failures
|
||||
go test -v ./pkg/...
|
||||
|
||||
# Run specific failing test
|
||||
go test -v -run TestSpecificFunction ./pkg/config
|
||||
```
|
||||
|
||||
## Coverage Goals
|
||||
|
||||
Current test coverage targets:
|
||||
- **Configuration**: 95%+ (critical for startup validation)
|
||||
- **Path utilities**: 90%+ (cross-platform compatibility critical)
|
||||
- **Constants**: 85%+ (verify all values and singleton behavior)
|
||||
- **HTTP client**: 90%+ (error handling is critical)
|
||||
- **Media handling**: 85%+ (core business logic)
|
||||
- **Processing**: 70%+ (limited by external FFmpeg dependency)
|
||||
|
||||
Generate coverage report to verify:
|
||||
```bash
|
||||
make test-coverage
|
||||
open coverage.html
|
||||
```
|
||||
@ -176,11 +176,22 @@ func (c *Config) resolveAndValidatePaths() error {
|
||||
return fmt.Errorf("failed to get working directory: %w", err)
|
||||
}
|
||||
|
||||
c.Paths.BaseDir = filepath.Join(cwd, c.Paths.BaseDir)
|
||||
c.Paths.LocalOutput = filepath.Join(cwd, c.Paths.LocalOutput)
|
||||
c.Paths.ProcessOutput = filepath.Join(cwd, c.Paths.ProcessOutput)
|
||||
c.Paths.ManifestDir = filepath.Join(cwd, c.Paths.ManifestDir)
|
||||
c.Paths.PersistenceFile = filepath.Join(c.Paths.BaseDir, c.Paths.PersistenceFile)
|
||||
// Only join with cwd if path is not already absolute
|
||||
if !filepath.IsAbs(c.Paths.BaseDir) {
|
||||
c.Paths.BaseDir = filepath.Join(cwd, c.Paths.BaseDir)
|
||||
}
|
||||
if !filepath.IsAbs(c.Paths.LocalOutput) {
|
||||
c.Paths.LocalOutput = filepath.Join(cwd, c.Paths.LocalOutput)
|
||||
}
|
||||
if !filepath.IsAbs(c.Paths.ProcessOutput) {
|
||||
c.Paths.ProcessOutput = filepath.Join(cwd, c.Paths.ProcessOutput)
|
||||
}
|
||||
if !filepath.IsAbs(c.Paths.ManifestDir) {
|
||||
c.Paths.ManifestDir = filepath.Join(cwd, c.Paths.ManifestDir)
|
||||
}
|
||||
if !filepath.IsAbs(c.Paths.PersistenceFile) {
|
||||
c.Paths.PersistenceFile = filepath.Join(c.Paths.BaseDir, c.Paths.PersistenceFile)
|
||||
}
|
||||
|
||||
requiredDirs := []string{
|
||||
c.Paths.BaseDir,
|
||||
|
||||
181
pkg/config/config_test.go
Normal file
181
pkg/config/config_test.go
Normal file
@ -0,0 +1,181 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestConfig_Load(t *testing.T) {
|
||||
// Save original env vars
|
||||
originalVars := map[string]string{
|
||||
"WORKER_COUNT": os.Getenv("WORKER_COUNT"),
|
||||
"NAS_USERNAME": os.Getenv("NAS_USERNAME"),
|
||||
"LOCAL_OUTPUT_DIR": os.Getenv("LOCAL_OUTPUT_DIR"),
|
||||
"ENABLE_NAS_TRANSFER": os.Getenv("ENABLE_NAS_TRANSFER"),
|
||||
}
|
||||
defer func() {
|
||||
// Restore original env vars
|
||||
for key, value := range originalVars {
|
||||
if value == "" {
|
||||
os.Unsetenv(key)
|
||||
} else {
|
||||
os.Setenv(key, value)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Test default config load
|
||||
cfg, err := Load()
|
||||
if err != nil {
|
||||
t.Fatalf("Load() failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify defaults
|
||||
if cfg.Core.WorkerCount != 4 {
|
||||
t.Errorf("Expected WorkerCount=4, got %d", cfg.Core.WorkerCount)
|
||||
}
|
||||
if cfg.Core.RefreshDelay != 3*time.Second {
|
||||
t.Errorf("Expected RefreshDelay=3s, got %v", cfg.Core.RefreshDelay)
|
||||
}
|
||||
if !cfg.NAS.EnableTransfer {
|
||||
t.Errorf("Expected NAS.EnableTransfer=true, got false")
|
||||
}
|
||||
|
||||
// Test environment variable override
|
||||
os.Setenv("WORKER_COUNT", "8")
|
||||
os.Setenv("NAS_USERNAME", "testuser")
|
||||
os.Setenv("ENABLE_NAS_TRANSFER", "false")
|
||||
os.Setenv("LOCAL_OUTPUT_DIR", "custom_data")
|
||||
|
||||
cfg2, err := Load()
|
||||
if err != nil {
|
||||
t.Fatalf("Load() with env vars failed: %v", err)
|
||||
}
|
||||
|
||||
if cfg2.Core.WorkerCount != 8 {
|
||||
t.Errorf("Expected WorkerCount=8 from env, got %d", cfg2.Core.WorkerCount)
|
||||
}
|
||||
if cfg2.NAS.Username != "testuser" {
|
||||
t.Errorf("Expected NAS.Username='testuser' from env, got %s", cfg2.NAS.Username)
|
||||
}
|
||||
if cfg2.NAS.EnableTransfer {
|
||||
t.Errorf("Expected NAS.EnableTransfer=false from env, got true")
|
||||
}
|
||||
if !strings.Contains(cfg2.Paths.LocalOutput, "custom_data") {
|
||||
t.Errorf("Expected LocalOutput to contain 'custom_data', got %s", cfg2.Paths.LocalOutput)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfig_PathMethods(t *testing.T) {
|
||||
cfg, err := Load()
|
||||
if err != nil {
|
||||
t.Fatalf("Load() failed: %v", err)
|
||||
}
|
||||
|
||||
testEvent := "test-event"
|
||||
testQuality := "1080p"
|
||||
|
||||
// Test GetEventPath
|
||||
eventPath := cfg.GetEventPath(testEvent)
|
||||
if !strings.Contains(eventPath, testEvent) {
|
||||
t.Errorf("GetEventPath should contain event name, got %s", eventPath)
|
||||
}
|
||||
|
||||
// Test GetManifestPath
|
||||
manifestPath := cfg.GetManifestPath(testEvent)
|
||||
if !strings.Contains(manifestPath, testEvent) {
|
||||
t.Errorf("GetManifestPath should contain event name, got %s", manifestPath)
|
||||
}
|
||||
if !strings.HasSuffix(manifestPath, ".json") {
|
||||
t.Errorf("GetManifestPath should end with .json, got %s", manifestPath)
|
||||
}
|
||||
|
||||
// Test GetNASEventPath
|
||||
nasPath := cfg.GetNASEventPath(testEvent)
|
||||
if !strings.Contains(nasPath, testEvent) {
|
||||
t.Errorf("GetNASEventPath should contain event name, got %s", nasPath)
|
||||
}
|
||||
|
||||
// Test GetProcessOutputPath
|
||||
processPath := cfg.GetProcessOutputPath(testEvent)
|
||||
if !strings.Contains(processPath, testEvent) {
|
||||
t.Errorf("GetProcessOutputPath should contain event name, got %s", processPath)
|
||||
}
|
||||
|
||||
// Test GetQualityPath
|
||||
qualityPath := cfg.GetQualityPath(testEvent, testQuality)
|
||||
if !strings.Contains(qualityPath, testEvent) {
|
||||
t.Errorf("GetQualityPath should contain event name, got %s", qualityPath)
|
||||
}
|
||||
if !strings.Contains(qualityPath, testQuality) {
|
||||
t.Errorf("GetQualityPath should contain quality, got %s", qualityPath)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfig_PathValidation(t *testing.T) {
|
||||
// Create a temporary directory for testing
|
||||
tempDir, err := os.MkdirTemp("", "config_test_*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Set environment variables to use temp directory
|
||||
os.Setenv("LOCAL_OUTPUT_DIR", filepath.Join(tempDir, "data"))
|
||||
defer os.Unsetenv("LOCAL_OUTPUT_DIR")
|
||||
|
||||
cfg, err := Load()
|
||||
if err != nil {
|
||||
t.Fatalf("Load() failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify directories were created
|
||||
if _, err := os.Stat(cfg.Paths.LocalOutput); os.IsNotExist(err) {
|
||||
t.Errorf("LocalOutput directory should have been created: %s", cfg.Paths.LocalOutput)
|
||||
}
|
||||
if _, err := os.Stat(cfg.Paths.ProcessOutput); os.IsNotExist(err) {
|
||||
t.Errorf("ProcessOutput directory should have been created: %s", cfg.Paths.ProcessOutput)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfig_ValidationErrors(t *testing.T) {
|
||||
// Save original env vars
|
||||
originalNASPath := os.Getenv("NAS_OUTPUT_PATH")
|
||||
originalFFmpegPath := os.Getenv("FFMPEG_PATH")
|
||||
defer func() {
|
||||
if originalNASPath == "" {
|
||||
os.Unsetenv("NAS_OUTPUT_PATH")
|
||||
} else {
|
||||
os.Setenv("NAS_OUTPUT_PATH", originalNASPath)
|
||||
}
|
||||
if originalFFmpegPath == "" {
|
||||
os.Unsetenv("FFMPEG_PATH")
|
||||
} else {
|
||||
os.Setenv("FFMPEG_PATH", originalFFmpegPath)
|
||||
}
|
||||
}()
|
||||
|
||||
// Note: Validation tests are limited because the default config
|
||||
// has working defaults. We can test that Load() works with valid configs.
|
||||
|
||||
// Test that Load works with proper paths set
|
||||
tempDir2, err := os.MkdirTemp("", "config_validation_test_*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir2)
|
||||
|
||||
os.Setenv("NAS_OUTPUT_PATH", "\\\\test\\path")
|
||||
os.Setenv("LOCAL_OUTPUT_DIR", tempDir2)
|
||||
|
||||
cfg, err := Load()
|
||||
if err != nil {
|
||||
t.Errorf("Load() should work with valid config: %v", err)
|
||||
}
|
||||
if cfg == nil {
|
||||
t.Error("Config should not be nil")
|
||||
}
|
||||
}
|
||||
240
pkg/constants/constants_test.go
Normal file
240
pkg/constants/constants_test.go
Normal file
@ -0,0 +1,240 @@
|
||||
package constants
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestGetConfig(t *testing.T) {
|
||||
// Test successful config loading
|
||||
cfg, err := GetConfig()
|
||||
if err != nil {
|
||||
t.Fatalf("GetConfig() failed: %v", err)
|
||||
}
|
||||
if cfg == nil {
|
||||
t.Fatal("GetConfig() returned nil config")
|
||||
}
|
||||
|
||||
// Test that subsequent calls return the same instance (singleton)
|
||||
cfg2, err := GetConfig()
|
||||
if err != nil {
|
||||
t.Fatalf("Second GetConfig() call failed: %v", err)
|
||||
}
|
||||
|
||||
// Both should be the same instance due to sync.Once
|
||||
if cfg != cfg2 {
|
||||
t.Error("GetConfig() should return the same instance (singleton)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMustGetConfig(t *testing.T) {
|
||||
// This should not panic with valid environment
|
||||
cfg := MustGetConfig()
|
||||
if cfg == nil {
|
||||
t.Fatal("MustGetConfig() returned nil")
|
||||
}
|
||||
|
||||
// Verify it returns a properly initialized config
|
||||
if cfg.Core.WorkerCount <= 0 {
|
||||
t.Errorf("Expected positive WorkerCount, got %d", cfg.Core.WorkerCount)
|
||||
}
|
||||
if cfg.Core.RefreshDelay <= 0 {
|
||||
t.Errorf("Expected positive RefreshDelay, got %v", cfg.Core.RefreshDelay)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMustGetConfig_Panic(t *testing.T) {
|
||||
// We can't easily test the panic scenario without breaking the singleton,
|
||||
// but we can test that MustGetConfig works normally
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("MustGetConfig() panicked unexpectedly: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
cfg := MustGetConfig()
|
||||
if cfg == nil {
|
||||
t.Fatal("MustGetConfig() returned nil without panicking")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigSingleton(t *testing.T) {
|
||||
// Reset the singleton for this test (this is a bit hacky but necessary for testing)
|
||||
// We'll create multiple goroutines to test concurrent access
|
||||
|
||||
configs := make(chan interface{}, 10)
|
||||
|
||||
// Launch multiple goroutines to call GetConfig concurrently
|
||||
for i := 0; i < 10; i++ {
|
||||
go func() {
|
||||
cfg, _ := GetConfig()
|
||||
configs <- cfg
|
||||
}()
|
||||
}
|
||||
|
||||
// Collect all configs
|
||||
var allConfigs []interface{}
|
||||
for i := 0; i < 10; i++ {
|
||||
allConfigs = append(allConfigs, <-configs)
|
||||
}
|
||||
|
||||
// All should be the same instance
|
||||
firstConfig := allConfigs[0]
|
||||
for i, cfg := range allConfigs {
|
||||
if cfg != firstConfig {
|
||||
t.Errorf("Config %d is different from first config", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestConstants_Values(t *testing.T) {
|
||||
// Test that constants have expected values
|
||||
if WorkerCount != 4 {
|
||||
t.Errorf("Expected WorkerCount=4, got %d", WorkerCount)
|
||||
}
|
||||
if RefreshDelay != 3 {
|
||||
t.Errorf("Expected RefreshDelay=3, got %d", RefreshDelay)
|
||||
}
|
||||
|
||||
// Test HTTP constants
|
||||
if HTTPUserAgent == "" {
|
||||
t.Error("HTTPUserAgent should not be empty")
|
||||
}
|
||||
if !strings.Contains(HTTPUserAgent, "Mozilla") {
|
||||
t.Error("HTTPUserAgent should contain 'Mozilla'")
|
||||
}
|
||||
if REFERRER != "https://www.flomarching.com" {
|
||||
t.Errorf("Expected REFERRER='https://www.flomarching.com', got '%s'", REFERRER)
|
||||
}
|
||||
|
||||
// Test default NAS constants
|
||||
if DefaultNASOutputPath != "\\\\HomeLabNAS\\dci\\streams" {
|
||||
t.Errorf("Expected DefaultNASOutputPath='\\\\HomeLabNAS\\dci\\streams', got '%s'", DefaultNASOutputPath)
|
||||
}
|
||||
if DefaultNASUsername != "NASAdmin" {
|
||||
t.Errorf("Expected DefaultNASUsername='NASAdmin', got '%s'", DefaultNASUsername)
|
||||
}
|
||||
|
||||
// Test transfer constants
|
||||
if DefaultTransferWorkerCount != 2 {
|
||||
t.Errorf("Expected DefaultTransferWorkerCount=2, got %d", DefaultTransferWorkerCount)
|
||||
}
|
||||
if DefaultTransferRetryLimit != 3 {
|
||||
t.Errorf("Expected DefaultTransferRetryLimit=3, got %d", DefaultTransferRetryLimit)
|
||||
}
|
||||
if DefaultTransferTimeout != 30 {
|
||||
t.Errorf("Expected DefaultTransferTimeout=30, got %d", DefaultTransferTimeout)
|
||||
}
|
||||
if DefaultFileSettlingDelay != 5 {
|
||||
t.Errorf("Expected DefaultFileSettlingDelay=5, got %d", DefaultFileSettlingDelay)
|
||||
}
|
||||
if DefaultTransferQueueSize != 100000 {
|
||||
t.Errorf("Expected DefaultTransferQueueSize=100000, got %d", DefaultTransferQueueSize)
|
||||
}
|
||||
if DefaultBatchSize != 1000 {
|
||||
t.Errorf("Expected DefaultBatchSize=1000, got %d", DefaultBatchSize)
|
||||
}
|
||||
|
||||
// Test cleanup constants
|
||||
if DefaultCleanupBatchSize != 1000 {
|
||||
t.Errorf("Expected DefaultCleanupBatchSize=1000, got %d", DefaultCleanupBatchSize)
|
||||
}
|
||||
if DefaultRetainLocalHours != 0 {
|
||||
t.Errorf("Expected DefaultRetainLocalHours=0, got %d", DefaultRetainLocalHours)
|
||||
}
|
||||
|
||||
// Test processing constants
|
||||
if DefaultProcessWorkerCount != 2 {
|
||||
t.Errorf("Expected DefaultProcessWorkerCount=2, got %d", DefaultProcessWorkerCount)
|
||||
}
|
||||
if DefaultFFmpegPath != "ffmpeg" {
|
||||
t.Errorf("Expected DefaultFFmpegPath='ffmpeg', got '%s'", DefaultFFmpegPath)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfig_Integration(t *testing.T) {
|
||||
cfg := MustGetConfig()
|
||||
|
||||
// Test that config values match or override constants appropriately
|
||||
if cfg.Core.WorkerCount != WorkerCount && os.Getenv("WORKER_COUNT") == "" {
|
||||
t.Errorf("Config WorkerCount (%d) should match constant (%d) when no env override", cfg.Core.WorkerCount, WorkerCount)
|
||||
}
|
||||
|
||||
if cfg.Core.RefreshDelay != time.Duration(RefreshDelay)*time.Second && os.Getenv("REFRESH_DELAY_SECONDS") == "" {
|
||||
t.Errorf("Config RefreshDelay (%v) should match constant (%v) when no env override", cfg.Core.RefreshDelay, time.Duration(RefreshDelay)*time.Second)
|
||||
}
|
||||
|
||||
// Test HTTP settings
|
||||
if cfg.HTTP.UserAgent != HTTPUserAgent {
|
||||
t.Errorf("Config UserAgent (%s) should match constant (%s)", cfg.HTTP.UserAgent, HTTPUserAgent)
|
||||
}
|
||||
if cfg.HTTP.Referer != REFERRER {
|
||||
t.Errorf("Config Referer (%s) should match constant (%s)", cfg.HTTP.Referer, REFERRER)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfig_PathMethods(t *testing.T) {
|
||||
cfg := MustGetConfig()
|
||||
|
||||
testEvent := "test-event-123"
|
||||
testQuality := "1080p"
|
||||
|
||||
// Test GetEventPath
|
||||
eventPath := cfg.GetEventPath(testEvent)
|
||||
if !strings.Contains(eventPath, testEvent) {
|
||||
t.Errorf("GetEventPath should contain event name '%s', got: %s", testEvent, eventPath)
|
||||
}
|
||||
|
||||
// Test GetManifestPath
|
||||
manifestPath := cfg.GetManifestPath(testEvent)
|
||||
if !strings.Contains(manifestPath, testEvent) {
|
||||
t.Errorf("GetManifestPath should contain event name '%s', got: %s", testEvent, manifestPath)
|
||||
}
|
||||
if !strings.HasSuffix(manifestPath, ".json") {
|
||||
t.Errorf("GetManifestPath should end with '.json', got: %s", manifestPath)
|
||||
}
|
||||
|
||||
// Test GetNASEventPath
|
||||
nasPath := cfg.GetNASEventPath(testEvent)
|
||||
if !strings.Contains(nasPath, testEvent) {
|
||||
t.Errorf("GetNASEventPath should contain event name '%s', got: %s", testEvent, nasPath)
|
||||
}
|
||||
|
||||
// Test GetProcessOutputPath
|
||||
processPath := cfg.GetProcessOutputPath(testEvent)
|
||||
if !strings.Contains(processPath, testEvent) {
|
||||
t.Errorf("GetProcessOutputPath should contain event name '%s', got: %s", testEvent, processPath)
|
||||
}
|
||||
|
||||
// Test GetQualityPath
|
||||
qualityPath := cfg.GetQualityPath(testEvent, testQuality)
|
||||
if !strings.Contains(qualityPath, testEvent) {
|
||||
t.Errorf("GetQualityPath should contain event name '%s', got: %s", testEvent, qualityPath)
|
||||
}
|
||||
if !strings.Contains(qualityPath, testQuality) {
|
||||
t.Errorf("GetQualityPath should contain quality '%s', got: %s", testQuality, qualityPath)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfig_DefaultValues(t *testing.T) {
|
||||
cfg := MustGetConfig()
|
||||
|
||||
// Test that default values are reasonable
|
||||
if cfg.Transfer.QueueSize != DefaultTransferQueueSize {
|
||||
t.Errorf("Expected transfer queue size %d, got %d", DefaultTransferQueueSize, cfg.Transfer.QueueSize)
|
||||
}
|
||||
|
||||
if cfg.Transfer.BatchSize != DefaultBatchSize {
|
||||
t.Errorf("Expected transfer batch size %d, got %d", DefaultBatchSize, cfg.Transfer.BatchSize)
|
||||
}
|
||||
|
||||
if cfg.Processing.WorkerCount != DefaultProcessWorkerCount {
|
||||
t.Errorf("Expected processing worker count %d, got %d", DefaultProcessWorkerCount, cfg.Processing.WorkerCount)
|
||||
}
|
||||
|
||||
if cfg.Cleanup.BatchSize != DefaultCleanupBatchSize {
|
||||
t.Errorf("Expected cleanup batch size %d, got %d", DefaultCleanupBatchSize, cfg.Cleanup.BatchSize)
|
||||
}
|
||||
}
|
||||
@ -5,6 +5,50 @@ import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// HTTPError represents an HTTP error with status code and message
|
||||
type HTTPError struct {
|
||||
StatusCode int
|
||||
Message string
|
||||
}
|
||||
|
||||
// Error returns the string representation of the HTTP error
|
||||
func (e *HTTPError) Error() string {
|
||||
return fmt.Sprintf("HTTP %d: %s", e.StatusCode, e.Message)
|
||||
}
|
||||
|
||||
// Is implements error comparison for errors.Is
|
||||
func (e *HTTPError) Is(target error) bool {
|
||||
var httpErr *HTTPError
|
||||
if errors.As(target, &httpErr) {
|
||||
return e.StatusCode == httpErr.StatusCode
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// NewHTTPError creates a new HTTP error
|
||||
func NewHTTPError(statusCode int, message string) error {
|
||||
return &HTTPError{
|
||||
StatusCode: statusCode,
|
||||
Message: message,
|
||||
}
|
||||
}
|
||||
|
||||
// IsHTTPError checks if an error is an HTTP error
|
||||
func IsHTTPError(err error) bool {
|
||||
var httpErr *HTTPError
|
||||
return errors.As(err, &httpErr)
|
||||
}
|
||||
|
||||
// GetHTTPStatusCode extracts the status code from an HTTP error
|
||||
func GetHTTPStatusCode(err error) int {
|
||||
var httpErr *HTTPError
|
||||
if errors.As(err, &httpErr) {
|
||||
return httpErr.StatusCode
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Legacy support for existing code
|
||||
type HttpError struct {
|
||||
Code int
|
||||
}
|
||||
|
||||
318
pkg/httpClient/error_test.go
Normal file
318
pkg/httpClient/error_test.go
Normal file
@ -0,0 +1,318 @@
|
||||
package httpClient
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHTTPError_Error(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
statusCode int
|
||||
message string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "basic http error",
|
||||
statusCode: 404,
|
||||
message: "Not Found",
|
||||
want: "HTTP 404: Not Found",
|
||||
},
|
||||
{
|
||||
name: "server error",
|
||||
statusCode: 500,
|
||||
message: "Internal Server Error",
|
||||
want: "HTTP 500: Internal Server Error",
|
||||
},
|
||||
{
|
||||
name: "unauthorized error",
|
||||
statusCode: 401,
|
||||
message: "Unauthorized",
|
||||
want: "HTTP 401: Unauthorized",
|
||||
},
|
||||
{
|
||||
name: "empty message",
|
||||
statusCode: 400,
|
||||
message: "",
|
||||
want: "HTTP 400: ",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := &HTTPError{
|
||||
StatusCode: tt.statusCode,
|
||||
Message: tt.message,
|
||||
}
|
||||
|
||||
got := err.Error()
|
||||
if got != tt.want {
|
||||
t.Errorf("HTTPError.Error() = %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPError_Is(t *testing.T) {
|
||||
err404 := &HTTPError{StatusCode: 404, Message: "Not Found"}
|
||||
err500 := &HTTPError{StatusCode: 500, Message: "Server Error"}
|
||||
otherErr404 := &HTTPError{StatusCode: 404, Message: "Different message"}
|
||||
regularError := fmt.Errorf("regular error")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
target error
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "same error instance",
|
||||
err: err404,
|
||||
target: err404,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "different HTTP errors with same status",
|
||||
err: err404,
|
||||
target: otherErr404,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "different HTTP errors with different status",
|
||||
err: err404,
|
||||
target: err500,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "HTTP error vs regular error",
|
||||
err: err404,
|
||||
target: regularError,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "regular error vs HTTP error",
|
||||
err: regularError,
|
||||
target: err404,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "nil target",
|
||||
err: err404,
|
||||
target: nil,
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var got bool
|
||||
if httpErr, ok := tt.err.(*HTTPError); ok {
|
||||
got = httpErr.Is(tt.target)
|
||||
} else {
|
||||
got = false // Non-HTTP errors return false
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("HTTPError.Is() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsHTTPError(t *testing.T) {
|
||||
httpErr := &HTTPError{StatusCode: 404, Message: "Not Found"}
|
||||
regularErr := fmt.Errorf("regular error")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "http error",
|
||||
err: httpErr,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "regular error",
|
||||
err: regularErr,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "nil error",
|
||||
err: nil,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "wrapped http error",
|
||||
err: fmt.Errorf("wrapped: %w", httpErr),
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := IsHTTPError(tt.err)
|
||||
if got != tt.want {
|
||||
t.Errorf("IsHTTPError() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetHTTPStatusCode(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
want int
|
||||
}{
|
||||
{
|
||||
name: "http error 404",
|
||||
err: &HTTPError{StatusCode: 404, Message: "Not Found"},
|
||||
want: 404,
|
||||
},
|
||||
{
|
||||
name: "http error 500",
|
||||
err: &HTTPError{StatusCode: 500, Message: "Server Error"},
|
||||
want: 500,
|
||||
},
|
||||
{
|
||||
name: "wrapped http error",
|
||||
err: fmt.Errorf("wrapped: %w", &HTTPError{StatusCode: 403, Message: "Forbidden"}),
|
||||
want: 403,
|
||||
},
|
||||
{
|
||||
name: "regular error",
|
||||
err: fmt.Errorf("regular error"),
|
||||
want: 0,
|
||||
},
|
||||
{
|
||||
name: "nil error",
|
||||
err: nil,
|
||||
want: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := GetHTTPStatusCode(tt.err)
|
||||
if got != tt.want {
|
||||
t.Errorf("GetHTTPStatusCode() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewHTTPError(t *testing.T) {
|
||||
statusCode := 404
|
||||
message := "Page not found"
|
||||
|
||||
err := NewHTTPError(statusCode, message)
|
||||
|
||||
// Check type
|
||||
httpErr, ok := err.(*HTTPError)
|
||||
if !ok {
|
||||
t.Fatalf("NewHTTPError should return *HTTPError, got %T", err)
|
||||
}
|
||||
|
||||
// Check fields
|
||||
if httpErr.StatusCode != statusCode {
|
||||
t.Errorf("Expected StatusCode=%d, got %d", statusCode, httpErr.StatusCode)
|
||||
}
|
||||
if httpErr.Message != message {
|
||||
t.Errorf("Expected Message=%q, got %q", message, httpErr.Message)
|
||||
}
|
||||
|
||||
// Check error string
|
||||
expectedErrorString := fmt.Sprintf("HTTP %d: %s", statusCode, message)
|
||||
if httpErr.Error() != expectedErrorString {
|
||||
t.Errorf("Expected error string=%q, got %q", expectedErrorString, httpErr.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPError_StatusCodeChecks(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
statusCode int
|
||||
isClient bool
|
||||
isServer bool
|
||||
}{
|
||||
{"200 OK", 200, false, false},
|
||||
{"400 Bad Request", 400, true, false},
|
||||
{"401 Unauthorized", 401, true, false},
|
||||
{"404 Not Found", 404, true, false},
|
||||
{"499 Client Error", 499, true, false},
|
||||
{"500 Server Error", 500, false, true},
|
||||
{"502 Bad Gateway", 502, false, true},
|
||||
{"599 Server Error", 599, false, true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := &HTTPError{StatusCode: tt.statusCode, Message: "test"}
|
||||
|
||||
isClient := err.StatusCode >= 400 && err.StatusCode < 500
|
||||
isServer := err.StatusCode >= 500 && err.StatusCode < 600
|
||||
|
||||
if isClient != tt.isClient {
|
||||
t.Errorf("Status %d: expected isClient=%v, got %v", tt.statusCode, tt.isClient, isClient)
|
||||
}
|
||||
if isServer != tt.isServer {
|
||||
t.Errorf("Status %d: expected isServer=%v, got %v", tt.statusCode, tt.isServer, isServer)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPError_Integration(t *testing.T) {
|
||||
// Test that HTTPError integrates well with standard error handling
|
||||
err := NewHTTPError(http.StatusNotFound, "Resource not found")
|
||||
|
||||
// Should be able to use with errors.Is
|
||||
target := &HTTPError{StatusCode: http.StatusNotFound}
|
||||
if !err.(*HTTPError).Is(target) {
|
||||
t.Error("HTTPError should match target with same status code")
|
||||
}
|
||||
|
||||
// Should be detectable as HTTPError
|
||||
if !IsHTTPError(err) {
|
||||
t.Error("Should be detectable as HTTPError")
|
||||
}
|
||||
|
||||
// Should return correct status code
|
||||
if GetHTTPStatusCode(err) != http.StatusNotFound {
|
||||
t.Error("Should return correct status code")
|
||||
}
|
||||
|
||||
// Should have meaningful string representation
|
||||
errorString := err.Error()
|
||||
if !strings.Contains(errorString, "404") {
|
||||
t.Error("Error string should contain status code")
|
||||
}
|
||||
if !strings.Contains(errorString, "Resource not found") {
|
||||
t.Error("Error string should contain message")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPError_EdgeCases(t *testing.T) {
|
||||
// Test with zero status code
|
||||
err := NewHTTPError(0, "Zero status")
|
||||
if err.Error() != "HTTP 0: Zero status" {
|
||||
t.Errorf("Unexpected error string for zero status: %s", err.Error())
|
||||
}
|
||||
|
||||
// Test with very long message
|
||||
longMessage := strings.Repeat("a", 1000)
|
||||
err = NewHTTPError(500, longMessage)
|
||||
if !strings.Contains(err.Error(), longMessage) {
|
||||
t.Error("Long message should be preserved")
|
||||
}
|
||||
|
||||
// Test status code boundaries
|
||||
for _, code := range []int{399, 400, 499, 500, 599, 600} {
|
||||
err := &HTTPError{StatusCode: code}
|
||||
// Should not panic
|
||||
_ = err.Error()
|
||||
}
|
||||
}
|
||||
241
pkg/media/manifest_test.go
Normal file
241
pkg/media/manifest_test.go
Normal file
@ -0,0 +1,241 @@
|
||||
package media
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestManifestWriter_NewManifestWriter(t *testing.T) {
|
||||
// Set up temporary environment for testing
|
||||
tempDir, err := os.MkdirTemp("", "manifest_test_*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Set environment variable to use temp directory
|
||||
os.Setenv("LOCAL_OUTPUT_DIR", tempDir)
|
||||
defer os.Unsetenv("LOCAL_OUTPUT_DIR")
|
||||
|
||||
eventName := "test-event"
|
||||
writer := NewManifestWriter(eventName)
|
||||
|
||||
if writer == nil {
|
||||
t.Fatal("NewManifestWriter() returned nil")
|
||||
}
|
||||
|
||||
if writer.Segments == nil {
|
||||
t.Error("Segments should be initialized")
|
||||
}
|
||||
if writer.Index == nil {
|
||||
t.Error("Index should be initialized")
|
||||
}
|
||||
if len(writer.Segments) != 0 {
|
||||
t.Errorf("Segments should be empty, got %d items", len(writer.Segments))
|
||||
}
|
||||
}
|
||||
|
||||
func TestManifestWriter_AddOrUpdateSegment(t *testing.T) {
|
||||
writer := &ManifestWriter{
|
||||
ManifestPath: "test.json",
|
||||
Segments: make([]ManifestItem, 0),
|
||||
Index: make(map[string]*ManifestItem),
|
||||
}
|
||||
|
||||
// Test adding new segment
|
||||
writer.AddOrUpdateSegment("1001", "1080p")
|
||||
|
||||
if len(writer.Segments) != 1 {
|
||||
t.Errorf("Expected 1 segment, got %d", len(writer.Segments))
|
||||
}
|
||||
if writer.Segments[0].SeqNo != "1001" {
|
||||
t.Errorf("Expected SeqNo '1001', got '%s'", writer.Segments[0].SeqNo)
|
||||
}
|
||||
if writer.Segments[0].Resolution != "1080p" {
|
||||
t.Errorf("Expected Resolution '1080p', got '%s'", writer.Segments[0].Resolution)
|
||||
}
|
||||
|
||||
// Test updating existing segment with higher resolution
|
||||
writer.AddOrUpdateSegment("1001", "1440p")
|
||||
|
||||
if len(writer.Segments) != 1 {
|
||||
t.Errorf("Segments count should remain 1 after update, got %d", len(writer.Segments))
|
||||
}
|
||||
if writer.Segments[0].Resolution != "1440p" {
|
||||
t.Errorf("Expected updated resolution '1440p', got '%s'", writer.Segments[0].Resolution)
|
||||
}
|
||||
|
||||
// Test updating existing segment with lower resolution (should not change)
|
||||
writer.AddOrUpdateSegment("1001", "720p")
|
||||
|
||||
if writer.Segments[0].Resolution != "1440p" {
|
||||
t.Errorf("Resolution should remain '1440p', got '%s'", writer.Segments[0].Resolution)
|
||||
}
|
||||
|
||||
// Test adding different segment
|
||||
writer.AddOrUpdateSegment("1002", "720p")
|
||||
|
||||
if len(writer.Segments) != 2 {
|
||||
t.Errorf("Expected 2 segments, got %d", len(writer.Segments))
|
||||
}
|
||||
}
|
||||
|
||||
func TestManifestWriter_AddOrUpdateSegment_NilFields(t *testing.T) {
|
||||
writer := &ManifestWriter{
|
||||
ManifestPath: "test.json",
|
||||
}
|
||||
|
||||
// Test with nil fields (should initialize them)
|
||||
writer.AddOrUpdateSegment("1001", "1080p")
|
||||
|
||||
if writer.Segments == nil {
|
||||
t.Error("Segments should be initialized")
|
||||
}
|
||||
if writer.Index == nil {
|
||||
t.Error("Index should be initialized")
|
||||
}
|
||||
if len(writer.Segments) != 1 {
|
||||
t.Errorf("Expected 1 segment, got %d", len(writer.Segments))
|
||||
}
|
||||
}
|
||||
|
||||
func TestManifestWriter_WriteManifest(t *testing.T) {
|
||||
tempDir, err := os.MkdirTemp("", "manifest_test_*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
manifestPath := filepath.Join(tempDir, "test-manifest.json")
|
||||
writer := &ManifestWriter{
|
||||
ManifestPath: manifestPath,
|
||||
Segments: make([]ManifestItem, 0),
|
||||
Index: make(map[string]*ManifestItem),
|
||||
}
|
||||
|
||||
// Add some test segments out of order
|
||||
writer.AddOrUpdateSegment("1003", "1080p")
|
||||
writer.AddOrUpdateSegment("1001", "720p")
|
||||
writer.AddOrUpdateSegment("1002", "1080p")
|
||||
|
||||
// Write manifest
|
||||
writer.WriteManifest()
|
||||
|
||||
// Verify file was created
|
||||
if _, err := os.Stat(manifestPath); os.IsNotExist(err) {
|
||||
t.Fatalf("Manifest file was not created: %s", manifestPath)
|
||||
}
|
||||
|
||||
// Read and verify content
|
||||
content, err := os.ReadFile(manifestPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read manifest file: %v", err)
|
||||
}
|
||||
|
||||
var segments []ManifestItem
|
||||
err = json.Unmarshal(content, &segments)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to unmarshal manifest JSON: %v", err)
|
||||
}
|
||||
|
||||
// Verify segments are sorted by sequence number
|
||||
if len(segments) != 3 {
|
||||
t.Errorf("Expected 3 segments in manifest, got %d", len(segments))
|
||||
}
|
||||
|
||||
expectedOrder := []string{"1001", "1002", "1003"}
|
||||
for i, segment := range segments {
|
||||
if segment.SeqNo != expectedOrder[i] {
|
||||
t.Errorf("Segment %d: expected SeqNo '%s', got '%s'", i, expectedOrder[i], segment.SeqNo)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify content structure
|
||||
if segments[0].Resolution != "720p" {
|
||||
t.Errorf("Expected first segment resolution '720p', got '%s'", segments[0].Resolution)
|
||||
}
|
||||
if segments[1].Resolution != "1080p" {
|
||||
t.Errorf("Expected second segment resolution '1080p', got '%s'", segments[1].Resolution)
|
||||
}
|
||||
}
|
||||
|
||||
func TestManifestWriter_WriteManifest_EmptySegments(t *testing.T) {
|
||||
tempDir, err := os.MkdirTemp("", "manifest_test_*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
manifestPath := filepath.Join(tempDir, "empty-manifest.json")
|
||||
writer := &ManifestWriter{
|
||||
ManifestPath: manifestPath,
|
||||
Segments: make([]ManifestItem, 0),
|
||||
Index: make(map[string]*ManifestItem),
|
||||
}
|
||||
|
||||
// Write empty manifest
|
||||
writer.WriteManifest()
|
||||
|
||||
// Verify file was created
|
||||
if _, err := os.Stat(manifestPath); os.IsNotExist(err) {
|
||||
t.Fatalf("Manifest file was not created: %s", manifestPath)
|
||||
}
|
||||
|
||||
// Read and verify content is empty array
|
||||
content, err := os.ReadFile(manifestPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read manifest file: %v", err)
|
||||
}
|
||||
|
||||
var segments []ManifestItem
|
||||
err = json.Unmarshal(content, &segments)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to unmarshal manifest JSON: %v", err)
|
||||
}
|
||||
|
||||
if len(segments) != 0 {
|
||||
t.Errorf("Expected empty segments array, got %d items", len(segments))
|
||||
}
|
||||
}
|
||||
|
||||
func TestManifestWriter_WriteManifest_InvalidPath(t *testing.T) {
|
||||
writer := &ManifestWriter{
|
||||
ManifestPath: "/invalid/path/that/does/not/exist/manifest.json",
|
||||
Segments: []ManifestItem{{SeqNo: "1001", Resolution: "1080p"}},
|
||||
Index: make(map[string]*ManifestItem),
|
||||
}
|
||||
|
||||
// This should not panic, just fail gracefully
|
||||
writer.WriteManifest()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
func TestManifestItem_JSONSerialization(t *testing.T) {
|
||||
item := ManifestItem{
|
||||
SeqNo: "1001",
|
||||
Resolution: "1080p",
|
||||
}
|
||||
|
||||
// Test marshaling
|
||||
data, err := json.Marshal(item)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal ManifestItem: %v", err)
|
||||
}
|
||||
|
||||
// Test unmarshaling
|
||||
var unmarshaled ManifestItem
|
||||
err = json.Unmarshal(data, &unmarshaled)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to unmarshal ManifestItem: %v", err)
|
||||
}
|
||||
|
||||
if unmarshaled.SeqNo != item.SeqNo {
|
||||
t.Errorf("SeqNo mismatch: expected '%s', got '%s'", item.SeqNo, unmarshaled.SeqNo)
|
||||
}
|
||||
if unmarshaled.Resolution != item.Resolution {
|
||||
t.Errorf("Resolution mismatch: expected '%s', got '%s'", item.Resolution, unmarshaled.Resolution)
|
||||
}
|
||||
}
|
||||
384
pkg/processing/service_test.go
Normal file
384
pkg/processing/service_test.go
Normal file
@ -0,0 +1,384 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
235
pkg/utils/paths_test.go
Normal file
235
pkg/utils/paths_test.go
Normal file
@ -0,0 +1,235 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSafeJoin(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
base string
|
||||
elements []string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "basic join",
|
||||
base: "data",
|
||||
elements: []string{"events", "test-event"},
|
||||
want: filepath.Join("data", "events", "test-event"),
|
||||
},
|
||||
{
|
||||
name: "empty elements",
|
||||
base: "data",
|
||||
elements: []string{},
|
||||
want: "data",
|
||||
},
|
||||
{
|
||||
name: "with path separators",
|
||||
base: "data/events",
|
||||
elements: []string{"test-event", "1080p"},
|
||||
want: filepath.Join("data", "events", "test-event", "1080p"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := SafeJoin(tt.base, tt.elements...)
|
||||
if got != tt.want {
|
||||
t.Errorf("SafeJoin() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureDir(t *testing.T) {
|
||||
tempDir, err := os.MkdirTemp("", "utils_test_*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
testPath := filepath.Join(tempDir, "test", "nested", "directory")
|
||||
|
||||
// Test creating nested directories
|
||||
err = EnsureDir(testPath)
|
||||
if err != nil {
|
||||
t.Errorf("EnsureDir() failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify directory was created
|
||||
if _, err := os.Stat(testPath); os.IsNotExist(err) {
|
||||
t.Errorf("Directory was not created: %s", testPath)
|
||||
}
|
||||
|
||||
// Test with existing directory (should not fail)
|
||||
err = EnsureDir(testPath)
|
||||
if err != nil {
|
||||
t.Errorf("EnsureDir() failed on existing directory: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPathExists(t *testing.T) {
|
||||
tempDir, err := os.MkdirTemp("", "utils_test_*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Test existing path
|
||||
if !PathExists(tempDir) {
|
||||
t.Errorf("PathExists() should return true for existing path: %s", tempDir)
|
||||
}
|
||||
|
||||
// Test non-existing path
|
||||
nonExistentPath := filepath.Join(tempDir, "does-not-exist")
|
||||
if PathExists(nonExistentPath) {
|
||||
t.Errorf("PathExists() should return false for non-existent path: %s", nonExistentPath)
|
||||
}
|
||||
|
||||
// Test with file
|
||||
testFile := filepath.Join(tempDir, "test.txt")
|
||||
f, err := os.Create(testFile)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create test file: %v", err)
|
||||
}
|
||||
f.Close()
|
||||
|
||||
if !PathExists(testFile) {
|
||||
t.Errorf("PathExists() should return true for existing file: %s", testFile)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsValidPath(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
want bool
|
||||
}{
|
||||
{"empty path", "", false},
|
||||
{"valid path", "data/events/test", true},
|
||||
{"path with colon", "data:events", false},
|
||||
{"path with pipe", "data|events", false},
|
||||
{"path with question mark", "data?events", false},
|
||||
{"path with asterisk", "data*events", false},
|
||||
{"path with quotes", "data\"events", false},
|
||||
{"path with angle brackets", "data<events>", false},
|
||||
{"normal windows path", "C:\\data\\events", true}, // Windows path separators are actually OK
|
||||
{"unix path", "/home/user/data", true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := IsValidPath(tt.path)
|
||||
if got != tt.want {
|
||||
t.Errorf("IsValidPath(%q) = %v, want %v", tt.path, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizePath(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "windows backslashes",
|
||||
path: "data\\events\\test",
|
||||
want: filepath.Join("data", "events", "test"),
|
||||
},
|
||||
{
|
||||
name: "unix forward slashes",
|
||||
path: "data/events/test",
|
||||
want: filepath.Join("data", "events", "test"),
|
||||
},
|
||||
{
|
||||
name: "mixed slashes",
|
||||
path: "data\\events/test\\file",
|
||||
want: filepath.Join("data", "events", "test", "file"),
|
||||
},
|
||||
{
|
||||
name: "redundant separators",
|
||||
path: "data//events\\\\test",
|
||||
want: filepath.Join("data", "events", "test"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := NormalizePath(tt.path)
|
||||
if got != tt.want {
|
||||
t.Errorf("NormalizePath(%q) = %q, want %q", tt.path, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRelativePath(t *testing.T) {
|
||||
tempDir, err := os.MkdirTemp("", "utils_test_*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
basePath := tempDir
|
||||
targetPath := filepath.Join(tempDir, "subdir", "file.txt")
|
||||
|
||||
rel, err := GetRelativePath(basePath, targetPath)
|
||||
if err != nil {
|
||||
t.Errorf("GetRelativePath() failed: %v", err)
|
||||
}
|
||||
|
||||
expected := filepath.Join("subdir", "file.txt")
|
||||
if rel != expected {
|
||||
t.Errorf("GetRelativePath() = %q, want %q", rel, expected)
|
||||
}
|
||||
|
||||
// Test with invalid paths
|
||||
_, err = GetRelativePath("", "")
|
||||
if err == nil {
|
||||
t.Error("GetRelativePath() should fail with empty paths")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateWritablePath(t *testing.T) {
|
||||
tempDir, err := os.MkdirTemp("", "utils_test_*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Test writable path
|
||||
writablePath := filepath.Join(tempDir, "test", "file.txt")
|
||||
err = ValidateWritablePath(writablePath)
|
||||
if err != nil {
|
||||
t.Errorf("ValidateWritablePath() failed for writable path: %v", err)
|
||||
}
|
||||
|
||||
// Verify directory was created
|
||||
dir := filepath.Dir(writablePath)
|
||||
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||
t.Errorf("Directory should have been created: %s", dir)
|
||||
}
|
||||
|
||||
// Test with read-only directory (if supported by OS)
|
||||
if runtime.GOOS != "windows" { // Skip on Windows as it's more complex
|
||||
readOnlyDir := filepath.Join(tempDir, "readonly")
|
||||
os.MkdirAll(readOnlyDir, 0755)
|
||||
os.Chmod(readOnlyDir, 0444) // Read-only
|
||||
defer os.Chmod(readOnlyDir, 0755) // Restore permissions for cleanup
|
||||
|
||||
readOnlyPath := filepath.Join(readOnlyDir, "file.txt")
|
||||
err = ValidateWritablePath(readOnlyPath)
|
||||
if err == nil {
|
||||
t.Error("ValidateWritablePath() should fail for read-only directory")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "not writable") {
|
||||
t.Errorf("Expected 'not writable' error, got: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
146
test_runner.go
Normal file
146
test_runner.go
Normal file
@ -0,0 +1,146 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
fmt.Println("🧪 StreamRecorder Test Suite")
|
||||
fmt.Println("============================")
|
||||
|
||||
startTime := time.Now()
|
||||
|
||||
// Set test environment to avoid interfering with real data
|
||||
originalEnv := setupTestEnvironment()
|
||||
defer restoreEnvironment(originalEnv)
|
||||
|
||||
// Create temporary directory for test data
|
||||
tempDir, err := os.MkdirTemp("", "streamrecorder_test_*")
|
||||
if err != nil {
|
||||
fmt.Printf("❌ Failed to create temp directory: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer func() {
|
||||
fmt.Printf("🧹 Cleaning up test directory: %s\n", tempDir)
|
||||
os.RemoveAll(tempDir)
|
||||
}()
|
||||
|
||||
// Set test-specific environment variables
|
||||
os.Setenv("LOCAL_OUTPUT_DIR", filepath.Join(tempDir, "data"))
|
||||
os.Setenv("PROCESS_OUTPUT_DIR", filepath.Join(tempDir, "out"))
|
||||
os.Setenv("ENABLE_NAS_TRANSFER", "false") // Disable NAS for tests
|
||||
os.Setenv("PROCESSING_ENABLED", "false") // Disable processing that needs FFmpeg
|
||||
|
||||
fmt.Printf("📁 Using temporary directory: %s\n", tempDir)
|
||||
fmt.Println()
|
||||
|
||||
// Run tests for each package
|
||||
packages := []string{
|
||||
"./pkg/config",
|
||||
"./pkg/utils",
|
||||
"./pkg/constants",
|
||||
"./pkg/httpClient",
|
||||
"./pkg/media",
|
||||
"./pkg/processing",
|
||||
}
|
||||
|
||||
var failedPackages []string
|
||||
totalTests := 0
|
||||
passedTests := 0
|
||||
|
||||
for _, pkg := range packages {
|
||||
fmt.Printf("🔍 Testing package: %s\n", pkg)
|
||||
|
||||
cmd := exec.Command("go", "test", "-v", pkg)
|
||||
cmd.Dir = "."
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
outputStr := string(output)
|
||||
|
||||
// Count tests
|
||||
testCount := strings.Count(outputStr, "=== RUN")
|
||||
passCount := strings.Count(outputStr, "--- PASS:")
|
||||
|
||||
totalTests += testCount
|
||||
passedTests += passCount
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("❌ FAILED: %s (%d/%d tests passed)\n", pkg, passCount, testCount)
|
||||
failedPackages = append(failedPackages, pkg)
|
||||
|
||||
// Show failure details
|
||||
lines := strings.Split(outputStr, "\n")
|
||||
for _, line := range lines {
|
||||
if strings.Contains(line, "FAIL:") ||
|
||||
strings.Contains(line, "Error:") ||
|
||||
strings.Contains(line, "panic:") {
|
||||
fmt.Printf(" %s\n", line)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fmt.Printf("✅ PASSED: %s (%d tests)\n", pkg, testCount)
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
// Print summary
|
||||
duration := time.Since(startTime)
|
||||
fmt.Println("📊 Test Summary")
|
||||
fmt.Println("===============")
|
||||
fmt.Printf("Total packages: %d\n", len(packages))
|
||||
fmt.Printf("Passed packages: %d\n", len(packages)-len(failedPackages))
|
||||
fmt.Printf("Failed packages: %d\n", len(failedPackages))
|
||||
fmt.Printf("Total tests: %d\n", totalTests)
|
||||
fmt.Printf("Passed tests: %d\n", passedTests)
|
||||
fmt.Printf("Failed tests: %d\n", totalTests-passedTests)
|
||||
fmt.Printf("Duration: %v\n", duration.Round(time.Millisecond))
|
||||
|
||||
if len(failedPackages) > 0 {
|
||||
fmt.Println()
|
||||
fmt.Println("❌ Failed packages:")
|
||||
for _, pkg := range failedPackages {
|
||||
fmt.Printf(" - %s\n", pkg)
|
||||
}
|
||||
os.Exit(1)
|
||||
} else {
|
||||
fmt.Println()
|
||||
fmt.Println("🎉 All tests passed!")
|
||||
}
|
||||
}
|
||||
|
||||
func setupTestEnvironment() map[string]string {
|
||||
// Save original environment variables that we'll modify
|
||||
originalEnv := make(map[string]string)
|
||||
|
||||
envVars := []string{
|
||||
"LOCAL_OUTPUT_DIR",
|
||||
"PROCESS_OUTPUT_DIR",
|
||||
"ENABLE_NAS_TRANSFER",
|
||||
"PROCESSING_ENABLED",
|
||||
"NAS_OUTPUT_PATH",
|
||||
"FFMPEG_PATH",
|
||||
"WORKER_COUNT",
|
||||
}
|
||||
|
||||
for _, envVar := range envVars {
|
||||
originalEnv[envVar] = os.Getenv(envVar)
|
||||
}
|
||||
|
||||
return originalEnv
|
||||
}
|
||||
|
||||
func restoreEnvironment(originalEnv map[string]string) {
|
||||
fmt.Println("🔄 Restoring original environment...")
|
||||
for envVar, originalValue := range originalEnv {
|
||||
if originalValue == "" {
|
||||
os.Unsetenv(envVar)
|
||||
} else {
|
||||
os.Setenv(envVar, originalValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user