Replace direct error equality check with errors.Is() in media_scanner_hash_test. In analytics_test.go, improve defer patterns by capturing resp.Body as a named parameter to avoid stale references, and add require.NoError() checks on all json.NewDecoder().Decode() calls that were previously silently ignoring decode errors.
438 lines
10 KiB
Go
438 lines
10 KiB
Go
package services
|
|
|
|
import (
|
|
"bufio"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// TestCalculateFileSHA256 tests the SHA-256 calculation with streaming
|
|
func TestCalculateFileSHA256(t *testing.T) {
|
|
scanner := &MediaScanner{}
|
|
|
|
// Create a temporary test file
|
|
tmpDir := t.TempDir()
|
|
testFile := filepath.Join(tmpDir, "test.txt")
|
|
testContent := "The quick brown fox jumps over the lazy dog"
|
|
|
|
if err := os.WriteFile(testFile, []byte(testContent), 0644); err != nil {
|
|
t.Fatalf("Failed to create test file: %v", err)
|
|
}
|
|
|
|
// Calculate expected hash
|
|
hasher := sha256.New()
|
|
hasher.Write([]byte(testContent))
|
|
expectedHash := hex.EncodeToString(hasher.Sum(nil))
|
|
|
|
// Test the function
|
|
calculatedHash, err := scanner.calculateFileSHA256(testFile)
|
|
if err != nil {
|
|
t.Fatalf("calculateFileSHA256 failed: %v", err)
|
|
}
|
|
|
|
if calculatedHash != expectedHash {
|
|
t.Errorf("Expected hash %s, got %s", expectedHash, calculatedHash)
|
|
}
|
|
|
|
t.Logf("SHA-256 hash calculation successful: %s", calculatedHash)
|
|
}
|
|
|
|
// TestCalculateFileSHA256LargeFile tests streaming with large file
|
|
func TestCalculateFileSHA256LargeFile(t *testing.T) {
|
|
scanner := &MediaScanner{}
|
|
|
|
// Create a temporary test file with larger content
|
|
tmpDir := t.TempDir()
|
|
testFile := filepath.Join(tmpDir, "large_test.txt")
|
|
|
|
// Create a 10MB file
|
|
file, err := os.Create(testFile)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create test file: %v", err)
|
|
}
|
|
defer file.Close()
|
|
|
|
writer := bufio.NewWriter(file)
|
|
testLine := strings.Repeat("This is a test line for SHA-256 calculation\n", 100)
|
|
for i := 0; i < 1000; i++ {
|
|
if _, err := writer.WriteString(testLine); err != nil {
|
|
t.Fatalf("Failed to write to test file: %v", err)
|
|
}
|
|
}
|
|
writer.Flush()
|
|
|
|
// Calculate expected hash
|
|
file.Seek(0, 0)
|
|
hasher := sha256.New()
|
|
buf := make([]byte, 4096)
|
|
for {
|
|
n, err := file.Read(buf)
|
|
if err != nil && !errors.Is(err, bufio.ErrBufferFull) {
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
t.Fatalf("Failed to read file for expected hash: %v", err)
|
|
}
|
|
hasher.Write(buf[:n])
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
}
|
|
expectedHash := hex.EncodeToString(hasher.Sum(nil))
|
|
|
|
// Test the function
|
|
calculatedHash, err := scanner.calculateFileSHA256(testFile)
|
|
if err != nil {
|
|
t.Fatalf("calculateFileSHA256 failed for large file: %v", err)
|
|
}
|
|
|
|
if calculatedHash != expectedHash {
|
|
t.Errorf("Expected hash %s, got %s", expectedHash, calculatedHash)
|
|
}
|
|
|
|
t.Logf("Large file SHA-256 hash calculation successful")
|
|
}
|
|
|
|
// TestExtractISBNFromIdentifier tests ISBN extraction
|
|
func TestExtractISBNFromIdentifier(t *testing.T) {
|
|
scanner := &MediaScanner{}
|
|
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
expected string
|
|
}{
|
|
{
|
|
name: "ISBN with prefix",
|
|
input: "isbn:978-3-16-148410-0",
|
|
expected: "9783161484100",
|
|
},
|
|
{
|
|
name: "ISBN with hyphens",
|
|
input: "978-3-16-148410-0",
|
|
expected: "9783161484100",
|
|
},
|
|
{
|
|
name: "ISBN with spaces",
|
|
input: "978 3 16 148410 0",
|
|
expected: "9783161484100",
|
|
},
|
|
{
|
|
name: "ISBN-10",
|
|
input: "isbn:0-306-40615-2",
|
|
expected: "0306406152",
|
|
},
|
|
{
|
|
name: "Clean ISBN-13",
|
|
input: "9783161484100",
|
|
expected: "9783161484100",
|
|
},
|
|
{
|
|
name: "Invalid identifier",
|
|
input: "not-an-isbn",
|
|
expected: "",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := scanner.extractISBNFromIdentifier(tt.input)
|
|
if result != tt.expected {
|
|
t.Errorf("Expected %s, got %s", tt.expected, result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestIsValidUUID tests UUID validation
|
|
func TestIsValidUUID(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
expected bool
|
|
}{
|
|
{
|
|
name: "Valid UUID v4",
|
|
input: "550e8400-e29b-41d4-a716-446655440000",
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "Valid UUID with uppercase",
|
|
input: "550E8400-E29B-41D4-A716-446655440000",
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "Invalid UUID - missing dashes",
|
|
input: "550e8400e29b41d4a716446655440000",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "Invalid UUID - wrong format",
|
|
input: "not-a-uuid",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "Empty string",
|
|
input: "",
|
|
expected: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := isValidUUID(tt.input)
|
|
if result != tt.expected {
|
|
t.Errorf("Expected %v, got %v for input %s", tt.expected, result, tt.input)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestDetermineHashConfidence tests confidence level determination
|
|
func TestDetermineHashConfidence(t *testing.T) {
|
|
scanner := &MediaScanner{}
|
|
|
|
tests := []struct {
|
|
name string
|
|
uuid string
|
|
identifier string
|
|
expectedConf string
|
|
}{
|
|
{
|
|
name: "High confidence - valid UUID",
|
|
uuid: "550e8400-e29b-41d4-a716-446655440000",
|
|
identifier: "",
|
|
expectedConf: "high",
|
|
},
|
|
{
|
|
name: "Medium confidence - ISBN",
|
|
uuid: "",
|
|
identifier: "isbn:978-3-16-148410-0",
|
|
expectedConf: "medium",
|
|
},
|
|
{
|
|
name: "Medium confidence - long identifier",
|
|
uuid: "",
|
|
identifier: "some-long-identifier-string",
|
|
expectedConf: "medium",
|
|
},
|
|
{
|
|
name: "Low confidence - no identifiers",
|
|
uuid: "",
|
|
identifier: "",
|
|
expectedConf: "low",
|
|
},
|
|
{
|
|
name: "Low confidence - short identifier",
|
|
uuid: "",
|
|
identifier: "abc",
|
|
expectedConf: "low",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := scanner.determineHashConfidence(tt.uuid, tt.identifier)
|
|
if result != tt.expectedConf {
|
|
t.Errorf("Expected confidence %s, got %s", tt.expectedConf, result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestDetectFormatType tests format type detection
|
|
func TestDetectFormatType(t *testing.T) {
|
|
scanner := &MediaScanner{}
|
|
|
|
tests := []struct {
|
|
name string
|
|
filePath string
|
|
expectedType string
|
|
}{
|
|
{
|
|
name: "EPUB file",
|
|
filePath: "/path/to/book.epub",
|
|
expectedType: "epub",
|
|
},
|
|
{
|
|
name: "KEPUB file",
|
|
filePath: "/path/to/book.kepub.epub",
|
|
expectedType: "kepub",
|
|
},
|
|
{
|
|
name: "PDF file",
|
|
filePath: "/path/to/document.pdf",
|
|
expectedType: "pdf",
|
|
},
|
|
{
|
|
name: "CBZ file",
|
|
filePath: "/path/to/comic.cbz",
|
|
expectedType: "comic_archive",
|
|
},
|
|
{
|
|
name: "MOBI file",
|
|
filePath: "/path/to/book.mobi",
|
|
expectedType: "mobi",
|
|
},
|
|
{
|
|
name: "TXT file",
|
|
filePath: "/path/to/book.txt",
|
|
expectedType: "txt",
|
|
},
|
|
{
|
|
name: "Unknown format",
|
|
filePath: "/path/to/book.xyz",
|
|
expectedType: "unknown",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := scanner.detectFormatType(tt.filePath)
|
|
if result != tt.expectedType {
|
|
t.Errorf("Expected format type %s, got %s", tt.expectedType, result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestExtractHashInfo tests the complete hash extraction flow
|
|
func TestExtractHashInfo(t *testing.T) {
|
|
scanner := &MediaScanner{}
|
|
|
|
// Create a temporary test file
|
|
tmpDir := t.TempDir()
|
|
testFile := filepath.Join(tmpDir, "test_book.txt")
|
|
testContent := "Test book content for hash extraction"
|
|
|
|
if err := os.WriteFile(testFile, []byte(testContent), 0644); err != nil {
|
|
t.Fatalf("Failed to create test file: %v", err)
|
|
}
|
|
|
|
// Test hash extraction
|
|
hashInfo, formatInfo, err := scanner.extractHashInfo(testFile)
|
|
if err != nil {
|
|
t.Fatalf("extractHashInfo failed: %v", err)
|
|
}
|
|
|
|
// Verify hash info
|
|
if hashInfo == nil {
|
|
t.Fatal("hashInfo is nil")
|
|
}
|
|
|
|
if hashInfo.FileSHA256 == "" {
|
|
t.Error("FileSHA256 is empty")
|
|
}
|
|
|
|
// Verify format info
|
|
if formatInfo == nil {
|
|
t.Fatal("formatInfo is nil")
|
|
}
|
|
|
|
if formatInfo.FormatType != "txt" {
|
|
t.Errorf("Expected format type 'txt', got %s", formatInfo.FormatType)
|
|
}
|
|
|
|
if formatInfo.FileSHA256 != hashInfo.FileSHA256 {
|
|
t.Error("FormatInfo.FileSHA256 doesn't match HashInfo.FileSHA256")
|
|
}
|
|
|
|
t.Logf("Hash extraction test passed: SHA256=%s, Format=%s, Confidence=%s",
|
|
hashInfo.FileSHA256, formatInfo.FormatType, hashInfo.HashConfidence)
|
|
}
|
|
|
|
// BenchmarkCalculateFileSHA256 benchmarks SHA-256 calculation
|
|
func BenchmarkCalculateFileSHA256(b *testing.B) {
|
|
scanner := &MediaScanner{}
|
|
|
|
// Create a temporary test file
|
|
tmpDir := b.TempDir()
|
|
testFile := filepath.Join(tmpDir, "bench_test.txt")
|
|
testContent := strings.Repeat("Benchmark test content for SHA-256 calculation\n", 10000)
|
|
|
|
if err := os.WriteFile(testFile, []byte(testContent), 0644); err != nil {
|
|
b.Fatalf("Failed to create test file: %v", err)
|
|
}
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
_, _ = scanner.calculateFileSHA256(testFile)
|
|
}
|
|
}
|
|
|
|
// TestExtractHashInfoIntegration is an integration test that tests a realistic scenario
|
|
func TestExtractHashInfoIntegration(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("Skipping integration test in short mode")
|
|
}
|
|
|
|
scanner := &MediaScanner{}
|
|
tmpDir := t.TempDir()
|
|
|
|
// Test multiple file types
|
|
testFiles := []struct {
|
|
name string
|
|
filename string
|
|
content string
|
|
}{
|
|
{
|
|
name: "Text file",
|
|
filename: "test.txt",
|
|
content: "Plain text file",
|
|
},
|
|
{
|
|
name: "HTML file (simulating EPUB content)",
|
|
filename: "test.html",
|
|
content: "<html><body>Test content</body></html>",
|
|
},
|
|
}
|
|
|
|
for _, tf := range testFiles {
|
|
t.Run(tf.name, func(t *testing.T) {
|
|
testFile := filepath.Join(tmpDir, tf.filename)
|
|
if err := os.WriteFile(testFile, []byte(tf.content), 0644); err != nil {
|
|
t.Fatalf("Failed to create test file: %v", err)
|
|
}
|
|
|
|
hashInfo, formatInfo, err := scanner.extractHashInfo(testFile)
|
|
if err != nil {
|
|
t.Fatalf("extractHashInfo failed: %v", err)
|
|
}
|
|
|
|
// Verify SHA-256 is calculated
|
|
if hashInfo.FileSHA256 == "" {
|
|
t.Error("SHA-256 hash not calculated")
|
|
}
|
|
|
|
// Verify format detection
|
|
if formatInfo.FormatType == "unknown" {
|
|
t.Logf("Warning: Format detected as 'unknown' for %s", tf.filename)
|
|
}
|
|
|
|
t.Logf("Integration test passed for %s: SHA256=%s, Format=%s",
|
|
tf.name, hashInfo.FileSHA256, formatInfo.FormatType)
|
|
})
|
|
}
|
|
}
|
|
|
|
// Example usage
|
|
func ExampleMediaScanner_calculateFileSHA256() {
|
|
scanner := &MediaScanner{}
|
|
|
|
// Calculate SHA-256 hash of a file
|
|
hash, err := scanner.calculateFileSHA256("/path/to/ebook.epub")
|
|
if err != nil {
|
|
fmt.Printf("Error: %v\n", err)
|
|
return
|
|
}
|
|
|
|
fmt.Printf("SHA-256: %s\n", hash)
|
|
}
|