.IMAGE — extensions removed from AllowedExtensions (scan gate), bookExtensions, MimeTypes and sync/format.go maps: .doc/.lit files are no longer scanned or indexed. classifyFormatGroup gains the RTF and PDB reflowable arms that were missing when those rungs shipped (rows only reclassify on creation). Scanner docs updated. DOC + LIT have no credible JS tooling (mammoth is docx-only; LIT needs LZX and its DRM variants are dead) and text-extraction-only support would misrepresent what the reader can do. Verified: probe .doc/.lit files watched but never scanned; probe .txt control scanned; TestClassifyFormatGroup extended and passing. Docs change granted in-session by the user (extensions + format docs).
155 lines
4.8 KiB
Go
155 lines
4.8 KiB
Go
package services
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"bookhoard/internal/database"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func setupTestDB(t *testing.T) *database.Queries {
|
|
return &database.Queries{}
|
|
}
|
|
|
|
func TestMarkDirectoryDirty(t *testing.T) {
|
|
db := setupTestDB(t)
|
|
scanner := NewMediaScanner(db)
|
|
scanner.folders = []string{"/test/folder"}
|
|
scanner.markDirectoryDirty("/test/folder/subdir")
|
|
scanner.dirtyDirsMu.RLock()
|
|
_, exists := scanner.dirtyDirs["/test/folder/subdir"]
|
|
scanner.dirtyDirsMu.RUnlock()
|
|
assert.True(t, exists, "Directory should be marked dirty")
|
|
}
|
|
|
|
func TestMarkDirectoryDirty_IgnoresNonWatchedPaths(t *testing.T) {
|
|
db := setupTestDB(t)
|
|
scanner := NewMediaScanner(db)
|
|
scanner.folders = []string{"/test/folder"}
|
|
scanner.markDirectoryDirty("/other/folder")
|
|
scanner.dirtyDirsMu.RLock()
|
|
_, exists := scanner.dirtyDirs["/other/folder"]
|
|
scanner.dirtyDirsMu.RUnlock()
|
|
assert.False(t, exists, "Non-watched directory should be ignored")
|
|
}
|
|
|
|
func TestMarkDirectoryDirty_SmartEventMerging(t *testing.T) {
|
|
db := setupTestDB(t)
|
|
scanner := NewMediaScanner(db)
|
|
scanner.folders = []string{"/test/folder"}
|
|
scanner.markDirectoryDirty("/test/folder/subdir1")
|
|
scanner.dirtyDirsMu.RLock()
|
|
_, exists1 := scanner.dirtyDirs["/test/folder/subdir1"]
|
|
scanner.dirtyDirsMu.RUnlock()
|
|
assert.True(t, exists1)
|
|
scanner.markDirectoryDirty("/test/folder")
|
|
scanner.dirtyDirsMu.RLock()
|
|
_, parentExists := scanner.dirtyDirs["/test/folder"]
|
|
_, childExists := scanner.dirtyDirs["/test/folder/subdir1"]
|
|
scanner.dirtyDirsMu.RUnlock()
|
|
assert.True(t, parentExists, "Parent should exist")
|
|
assert.False(t, childExists, "Child should be removed (consolidated)")
|
|
}
|
|
|
|
func TestWaitForFileStability_StableFile(t *testing.T) {
|
|
db := setupTestDB(t)
|
|
scanner := NewMediaScanner(db)
|
|
tmpDir := t.TempDir()
|
|
filePath := filepath.Join(tmpDir, "stable.epub")
|
|
err := os.WriteFile(filePath, []byte("test content"), 0644)
|
|
require.NoError(t, err)
|
|
assert.True(t, scanner.waitForFileStability(filePath))
|
|
}
|
|
|
|
func TestWaitForFileStability_UnstableFile(t *testing.T) {
|
|
db := setupTestDB(t)
|
|
scanner := NewMediaScanner(db)
|
|
tmpDir := t.TempDir()
|
|
filePath := filepath.Join(tmpDir, "unstable.epub")
|
|
file, err := os.Create(filePath)
|
|
require.NoError(t, err)
|
|
defer file.Close()
|
|
stableChan := make(chan bool)
|
|
go func() {
|
|
stableChan <- scanner.waitForFileStability(filePath)
|
|
}()
|
|
for i := 0; i < 3; i++ {
|
|
time.Sleep(100 * time.Millisecond)
|
|
file.WriteString("more data\n")
|
|
}
|
|
file.Close()
|
|
select {
|
|
case stable := <-stableChan:
|
|
assert.True(t, stable)
|
|
case <-time.After(7 * time.Second):
|
|
t.Fatal("waitForFileStability timeout")
|
|
}
|
|
}
|
|
|
|
func TestProcessDirtyDirectories_CollectsReadyDirs(t *testing.T) {
|
|
db := setupTestDB(t)
|
|
scanner := NewMediaScanner(db)
|
|
scanner.folders = []string{"/test/folder"}
|
|
|
|
scanner.dirtyDirsMu.Lock()
|
|
scanner.dirtyDirs["/test/folder/subdir"] = time.Now().Add(-15 * time.Second)
|
|
scanner.dirtyDirsMu.Unlock()
|
|
|
|
scanner.dirtyDirsMu.Lock()
|
|
now := time.Now()
|
|
readyDirs := make([]string, 0)
|
|
for dirPath, lastChange := range scanner.dirtyDirs {
|
|
if now.Sub(lastChange) >= 10*time.Second {
|
|
readyDirs = append(readyDirs, dirPath)
|
|
delete(scanner.dirtyDirs, dirPath)
|
|
}
|
|
}
|
|
scanner.dirtyDirsMu.Unlock()
|
|
|
|
assert.Equal(t, 1, len(readyDirs), "Should find one ready directory")
|
|
assert.Equal(t, "/test/folder/subdir", readyDirs[0])
|
|
|
|
scanner.dirtyDirsMu.RLock()
|
|
count := len(scanner.dirtyDirs)
|
|
scanner.dirtyDirsMu.RUnlock()
|
|
assert.Equal(t, 0, count, "Ready directory should be removed from dirty list")
|
|
}
|
|
|
|
func TestClassifyFormatGroup(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
ext string
|
|
epubFixed bool
|
|
group string
|
|
reflow bool
|
|
fixed bool
|
|
}{
|
|
{"EPUB reflowable", ".epub", false, "reflowable", true, false},
|
|
{"EPUB fixed-layout", ".epub", true, "fixed_layout", false, true},
|
|
{"MOBI", ".mobi", false, "reflowable", true, false},
|
|
{"FB2", ".fb2", false, "reflowable", true, false},
|
|
{"TXT", ".txt", false, "reflowable", true, false},
|
|
{"DOCX (was unknown before the fix)", ".docx", false, "reflowable", true, false},
|
|
{"RTF", ".rtf", false, "reflowable", true, false},
|
|
{"PDB (PalmDOC)", ".pdb", false, "reflowable", true, false},
|
|
{"PDF", ".pdf", false, "fixed_layout", false, true},
|
|
{"DJVU", ".djvu", false, "fixed_layout", false, true},
|
|
{"CBZ", ".cbz", false, "comic_archive", false, true},
|
|
{"unrecognized extension", ".xyz", false, "unknown", false, false},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
group, reflow, fixed := classifyFormatGroup(tt.ext, tt.epubFixed)
|
|
if group != tt.group || reflow != tt.reflow || fixed != tt.fixed {
|
|
t.Errorf("classifyFormatGroup(%q, %v) = (%q, %v, %v), want (%q, %v, %v)",
|
|
tt.ext, tt.epubFixed, group, reflow, fixed, tt.group, tt.reflow, tt.fixed)
|
|
}
|
|
})
|
|
}
|
|
}
|