v0.10.0: ports and adapters refactor
- Extract shared manifest types into internal/manifest/types leaf package. - Extract SQLite adapter into internal/manifest/sqlite. - Extract JSONL adapter into internal/manifest/jsonl. - Isolate modernc.org/sqlite import to sqlite/adapter.go. - Add adapter-backed registry with manifest.Default. - Adapter-agnostic ConvertManifest in types/. - MemoryAdapter for in-memory manifest testing. - CLI uses manifest.Default registry directly. - SQLite LogWriter type assertion moved into SQLiteAdapter. - Manifest interface includes Entries(); EntryReader removed. - No behavior changes. 100% coverage across all 6 packages.
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
_ "modernc.org/sqlite"
|
||||
|
||||
"gitea.k3s.k0.nu/tools/photocli/internal/manifest/types"
|
||||
)
|
||||
|
||||
type Adapter struct{}
|
||||
|
||||
func (Adapter) Format() types.Format { return types.FormatSQLite }
|
||||
func (Adapter) Aliases() []string { return []string{"db", "sqlite3"} }
|
||||
func (Adapter) Path(dir string) string { return Path(dir) }
|
||||
func (Adapter) Exists(dir string) bool { return FileExists(Path(dir)) }
|
||||
func (Adapter) Open(dir string) (types.Manifest, error) { return Load(dir) }
|
||||
func (Adapter) OpenLogWriter(m types.Manifest, dir string) (types.LogWriter, error) {
|
||||
if sm, ok := m.(*Store); ok && sm.DB() != nil {
|
||||
return NewLogWriter(sm.DB())
|
||||
}
|
||||
return types.NewFileLogWriter(types.LogPath(dir))
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
|
||||
"gitea.k3s.k0.nu/tools/photocli/internal/manifest/types"
|
||||
)
|
||||
|
||||
func TestAdapter(t *testing.T) {
|
||||
a := Adapter{}
|
||||
if a.Format() != types.FormatSQLite {
|
||||
t.Fatal("expected SQLite format")
|
||||
}
|
||||
if len(a.Aliases()) != 2 {
|
||||
t.Fatal("expected 2 aliases")
|
||||
}
|
||||
dir := t.TempDir()
|
||||
if a.Path(dir) != Path(dir) {
|
||||
t.Fatal("expected path match")
|
||||
}
|
||||
if a.Exists(dir) {
|
||||
t.Fatal("expected not to exist in empty dir")
|
||||
}
|
||||
m, err := a.Open(dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m.Close()
|
||||
w, err := a.OpenLogWriter(nil, dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
w.Close()
|
||||
}
|
||||
|
||||
func TestAdapterOpenLogWriterSQLite(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
m, err := Load(dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := m.OpenAppend(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer m.Close()
|
||||
w, err := Adapter{}.OpenLogWriter(m, dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer w.Close()
|
||||
if _, ok := w.(*LogWriter); !ok {
|
||||
t.Fatalf("expected sqlite log writer, got %T", w)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoreLoadEmpty(t *testing.T) {
|
||||
m, err := Load(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if m == nil {
|
||||
t.Fatal("expected non-nil store")
|
||||
}
|
||||
m.Close()
|
||||
}
|
||||
|
||||
func TestStoreLoadNonexistent(t *testing.T) {
|
||||
m, err := Load("/nonexistent/path")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if m == nil {
|
||||
t.Fatal("expected non-nil store")
|
||||
}
|
||||
m.Close()
|
||||
}
|
||||
|
||||
func TestStoreAddHasSaveClose(t *testing.T) {
|
||||
m, err := Load(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := m.OpenAppend(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m.Add("sid1", "sfile.jpg", 99, "azure")
|
||||
if !m.Has("sid1") {
|
||||
t.Fatal("expected Has to return true")
|
||||
}
|
||||
if m.Has("nope") {
|
||||
t.Fatal("expected Has to return false")
|
||||
}
|
||||
if err := m.Save(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m.Close()
|
||||
}
|
||||
|
||||
func TestStoreRoundTrip(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
m, err := Load(dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := m.OpenAppend(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m.Add("rid1", "rfile1.jpg", 10, "aws")
|
||||
m.Add("rid2", "rfile2.jpg", 20, "gcs")
|
||||
if err := m.Save(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m.Close()
|
||||
|
||||
m2, err := Load(dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := m2.OpenAppend(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer m2.Close()
|
||||
if !m2.Has("rid1") {
|
||||
t.Fatal("expected rid1 after reload")
|
||||
}
|
||||
if !m2.Has("rid2") {
|
||||
t.Fatal("expected rid2 after reload")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoreEntries(t *testing.T) {
|
||||
m, err := Load(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := m.OpenAppend(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer m.Close()
|
||||
m.Add("se1", "sf1.jpg", 1, "sc1")
|
||||
m.Add("se2", "sf2.jpg", 2, "sc2")
|
||||
entries := m.Entries()
|
||||
if len(entries) != 2 {
|
||||
t.Fatalf("expected 2 entries, got %d", len(entries))
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoreCloseIdempotent(t *testing.T) {
|
||||
m, err := Load(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := m.OpenAppend(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m.Close()
|
||||
m.Close()
|
||||
}
|
||||
|
||||
func TestStoreOpenAppendIdempotent(t *testing.T) {
|
||||
m, err := Load(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := m.OpenAppend(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := m.OpenAppend(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m.Close()
|
||||
}
|
||||
|
||||
func TestStoreManifestFormat(t *testing.T) {
|
||||
m, err := Load(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if m.ManifestFormat() != types.FormatSQLite {
|
||||
t.Fatal("expected SQLite format")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoreSave(t *testing.T) {
|
||||
m, err := Load(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := m.Save(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoreOpenAppendCreateDir(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
subdir := filepath.Join(dir, "subdir")
|
||||
m, err := Load(subdir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := m.OpenAppend(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m.Close()
|
||||
}
|
||||
|
||||
func TestStoreCloseWithoutOpen(t *testing.T) {
|
||||
m, err := Load(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m.Close()
|
||||
}
|
||||
|
||||
func TestStoreEntriesWithoutOpen(t *testing.T) {
|
||||
m, err := Load(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
entries := m.Entries()
|
||||
if entries != nil {
|
||||
t.Errorf("Entries without open should return nil, got %v", entries)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoreHasWithoutOpen(t *testing.T) {
|
||||
m, err := Load(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if m.Has("x") {
|
||||
t.Fatal("Has should return false without open")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoreAddEntryWithoutOpen(t *testing.T) {
|
||||
m, err := Load(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m.AddEntry(types.Entry{ID: "x", Filename: "f.jpg"})
|
||||
}
|
||||
|
||||
func TestSetOpener(t *testing.T) {
|
||||
old := SetOpener(func(driverName, dataSourceName string) (*sql.DB, error) {
|
||||
return nil, fmt.Errorf("test")
|
||||
})
|
||||
if old != nil {
|
||||
t.Fatal("expected nil old opener")
|
||||
}
|
||||
restore := SetOpener(old)
|
||||
if restore == nil {
|
||||
t.Fatal("expected non-nil restore")
|
||||
}
|
||||
SetOpener(nil)
|
||||
}
|
||||
|
||||
func TestFileExists(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
if FileExists(dir) {
|
||||
t.Fatal("expected false for directory")
|
||||
}
|
||||
if FileExists("/nonexistent/file") {
|
||||
t.Fatal("expected false for nonexistent")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogWriterClose(t *testing.T) {
|
||||
w := &LogWriter{}
|
||||
w.Close()
|
||||
}
|
||||
|
||||
func TestOpenerWithOverride(t *testing.T) {
|
||||
SetOpener(func(driverName, dataSourceName string) (*sql.DB, error) {
|
||||
return nil, nil
|
||||
})
|
||||
if opener() == nil {
|
||||
t.Fatal("expected override")
|
||||
}
|
||||
SetOpener(nil)
|
||||
}
|
||||
|
||||
func TestLogWriterCloseDirect(t *testing.T) {
|
||||
(&LogWriter{}).Close()
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"gitea.k3s.k0.nu/tools/photocli/internal/manifest/types"
|
||||
)
|
||||
|
||||
type LogWriter struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewLogWriter(db *sql.DB) (types.LogWriter, error) {
|
||||
_, err := db.Exec(`CREATE TABLE IF NOT EXISTS logs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
ts INTEGER NOT NULL,
|
||||
level TEXT NOT NULL,
|
||||
event TEXT NOT NULL,
|
||||
asset_id TEXT,
|
||||
album TEXT,
|
||||
filename TEXT,
|
||||
size INTEGER,
|
||||
cloud TEXT,
|
||||
duration_ms INTEGER,
|
||||
message TEXT
|
||||
)`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, _ = db.Exec(`CREATE INDEX IF NOT EXISTS idx_logs_ts ON logs(ts)`)
|
||||
_, _ = db.Exec(`CREATE INDEX IF NOT EXISTS idx_logs_event ON logs(event)`)
|
||||
return &LogWriter{db: db}, nil
|
||||
}
|
||||
|
||||
func (w *LogWriter) Log(e types.LogEntry) {
|
||||
if w.db == nil {
|
||||
return
|
||||
}
|
||||
w.db.Exec(`INSERT INTO logs (ts, level, event, asset_id, album, filename, size, cloud, duration_ms, message) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
e.Timestamp, e.Level, e.Event, e.AssetID, e.Album, e.Filename, e.Size, e.Cloud, e.DurationMs, e.Message)
|
||||
}
|
||||
|
||||
func (w *LogWriter) Close() { _ = w }
|
||||
@@ -0,0 +1,165 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"gitea.k3s.k0.nu/tools/photocli/internal/manifest/types"
|
||||
)
|
||||
|
||||
type OpenerFunc func(driverName, dataSourceName string) (*sql.DB, error)
|
||||
|
||||
var openerOverride OpenerFunc
|
||||
|
||||
func SetOpener(fn OpenerFunc) (old OpenerFunc) {
|
||||
old = openerOverride
|
||||
openerOverride = fn
|
||||
return old
|
||||
}
|
||||
|
||||
type Store struct {
|
||||
path string
|
||||
db *sql.DB
|
||||
open OpenerFunc
|
||||
execFunc func(query string, args ...any) (sql.Result, error)
|
||||
}
|
||||
|
||||
func Path(dir string) string {
|
||||
return filepath.Join(dir, "downloads.db")
|
||||
}
|
||||
|
||||
func Load(dir string) (*Store, error) {
|
||||
m := &Store{
|
||||
path: Path(dir),
|
||||
open: opener(),
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func opener() OpenerFunc {
|
||||
if openerOverride != nil {
|
||||
return openerOverride
|
||||
}
|
||||
return sql.Open
|
||||
}
|
||||
|
||||
func (m *Store) OpenAppend() error {
|
||||
if m.db != nil {
|
||||
return nil
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(m.path), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
opener := m.open
|
||||
if opener == nil {
|
||||
opener = sql.Open
|
||||
}
|
||||
db, err := opener("sqlite", m.path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open sqlite: %w", err)
|
||||
}
|
||||
execFn := m.execFunc
|
||||
if execFn == nil {
|
||||
execFn = db.Exec
|
||||
}
|
||||
_, err = execFn(`CREATE TABLE IF NOT EXISTS downloads (
|
||||
id TEXT PRIMARY KEY,
|
||||
filename TEXT NOT NULL DEFAULT '',
|
||||
path TEXT NOT NULL DEFAULT '',
|
||||
size INTEGER NOT NULL DEFAULT 0,
|
||||
cloud TEXT NOT NULL DEFAULT '',
|
||||
checksum TEXT NOT NULL DEFAULT '',
|
||||
exported INTEGER NOT NULL DEFAULT 0
|
||||
)`)
|
||||
if err != nil {
|
||||
db.Close()
|
||||
return fmt.Errorf("create table: %w", err)
|
||||
}
|
||||
_, _ = execFn(`ALTER TABLE downloads ADD COLUMN path TEXT NOT NULL DEFAULT ''`)
|
||||
_, _ = execFn(`ALTER TABLE downloads ADD COLUMN checksum TEXT NOT NULL DEFAULT ''`)
|
||||
_, err = execFn(`CREATE INDEX IF NOT EXISTS idx_downloads_id ON downloads(id)`)
|
||||
if err != nil {
|
||||
db.Close()
|
||||
return fmt.Errorf("create index: %w", err)
|
||||
}
|
||||
m.db = db
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Store) DB() *sql.DB { return m.db }
|
||||
func (m *Store) SetOpen(fn OpenerFunc) { m.open = fn }
|
||||
func (m *Store) SetExecFunc(fn func(query string, args ...any) (sql.Result, error)) {
|
||||
m.execFunc = fn
|
||||
}
|
||||
|
||||
func (m *Store) Has(id string) bool {
|
||||
if m.db == nil {
|
||||
return false
|
||||
}
|
||||
var count int
|
||||
err := m.db.QueryRow(`SELECT COUNT(*) FROM downloads WHERE id = ?`, id).Scan(&count)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return count > 0
|
||||
}
|
||||
|
||||
func (m *Store) ManifestFormat() types.Format {
|
||||
return types.FormatSQLite
|
||||
}
|
||||
|
||||
func (m *Store) Add(id string, filename string, size int64, cloud string) {
|
||||
m.AddEntry(types.NewEntry(id, filename, filename, size, cloud))
|
||||
}
|
||||
|
||||
func (m *Store) AddEntry(entry types.Entry) {
|
||||
if m.db == nil {
|
||||
return
|
||||
}
|
||||
if entry.Path == "" {
|
||||
entry.Path = entry.Filename
|
||||
}
|
||||
m.db.Exec(`INSERT OR REPLACE INTO downloads (id, filename, path, size, cloud, checksum, exported) VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
entry.ID, entry.Filename, entry.Path, entry.Size, entry.Cloud, entry.Checksum, entry.Exported)
|
||||
}
|
||||
|
||||
func (m *Store) Save() error { return nil }
|
||||
|
||||
func (m *Store) Close() {
|
||||
if m.db != nil {
|
||||
m.db.Close()
|
||||
m.db = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Store) Entries() map[string]types.Entry {
|
||||
if m.db == nil {
|
||||
return nil
|
||||
}
|
||||
out := make(map[string]types.Entry)
|
||||
rows, err := m.db.Query(`SELECT id, filename, path, size, cloud, checksum, exported FROM downloads`)
|
||||
if err != nil {
|
||||
return out
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var e types.Entry
|
||||
if err := rows.Scan(&e.ID, &e.Filename, &e.Path, &e.Size, &e.Cloud, &e.Checksum, &e.Exported); err == nil {
|
||||
if e.Path == "" {
|
||||
e.Path = e.Filename
|
||||
}
|
||||
out[e.ID] = e
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func FileExists(path string) bool {
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return !info.IsDir()
|
||||
}
|
||||
@@ -0,0 +1,323 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
|
||||
"gitea.k3s.k0.nu/tools/photocli/internal/manifest/types"
|
||||
)
|
||||
|
||||
func TestStoreOpenAppendSQLError(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
m, err := Load(dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m.SetOpen(func(driverName, dataSourceName string) (*sql.DB, error) {
|
||||
return nil, fmt.Errorf("simulated open error")
|
||||
})
|
||||
if err := m.OpenAppend(); err == nil {
|
||||
t.Error("expected error from sql.Open failure")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoreOpenAppendCreateTableError(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
m, err := Load(dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
realOpen := m.open
|
||||
m.SetOpen(func(driverName, dataSourceName string) (*sql.DB, error) {
|
||||
db, err := realOpen(driverName, dataSourceName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
db.Close()
|
||||
return db, nil
|
||||
})
|
||||
if err := m.OpenAppend(); err == nil {
|
||||
t.Error("expected error from closed DB CREATE TABLE")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoreHasAfterClose(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
m, err := Load(dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := m.OpenAppend(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m.Add("x1", "photo.jpg", 1024, "local")
|
||||
m.Close()
|
||||
if m.Has("x1") {
|
||||
t.Error("Has should return false after Close")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoreEntriesAfterClose(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
m, err := Load(dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := m.OpenAppend(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m.Add("x1", "photo.jpg", 1024, "local")
|
||||
m.Close()
|
||||
entries := m.Entries()
|
||||
if entries != nil {
|
||||
t.Errorf("Entries should return nil after Close, got %d entries", len(entries))
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoreOpenAppendMkdirAllError(t *testing.T) {
|
||||
m, err := Load("/proc/cannot-write")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := m.OpenAppend(); err == nil {
|
||||
t.Error("expected error creating dir under /proc")
|
||||
m.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoreOpenAppendNilOpener(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
m, err := Load(dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m.open = nil
|
||||
if err := m.OpenAppend(); err != nil {
|
||||
t.Errorf("expected nil opener to use sql.Open fallback, got err: %v", err)
|
||||
}
|
||||
m.Close()
|
||||
}
|
||||
|
||||
func TestStoreOpenAppendCreateIndexError(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
m, err := Load(dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
realOpen := m.open
|
||||
m.open = func(driverName, dataSourceName string) (*sql.DB, error) {
|
||||
db, err := realOpen(driverName, dataSourceName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
db.Close()
|
||||
return db, nil
|
||||
}
|
||||
if err := m.OpenAppend(); err == nil {
|
||||
t.Error("expected error from closed DB")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoreHasQueryError(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
m, err := Load(dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := m.OpenAppend(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m.Add("x1", "photo.jpg", 1024, "local")
|
||||
closedDB := m.DB()
|
||||
closedDB.Close()
|
||||
if m.Has("x1") {
|
||||
t.Error("Has should return false with broken DB connection")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoreEntriesQueryError(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
m, err := Load(dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := m.OpenAppend(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m.Add("x1", "photo.jpg", 1024, "local")
|
||||
closedDB := m.DB()
|
||||
closedDB.Close()
|
||||
entries := m.Entries()
|
||||
if entries == nil {
|
||||
t.Error("Entries should return non-nil map with broken DB connection")
|
||||
}
|
||||
if len(entries) != 0 {
|
||||
t.Errorf("Entries should be empty with broken DB connection, got %d", len(entries))
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoreHasQueryErrorWithOpenDB(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
m, err := Load(dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := m.OpenAppend(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m.Add("x1", "photo.jpg", 1024, "local")
|
||||
closedDB := m.DB()
|
||||
closedDB.Close()
|
||||
if m.Has("x1") {
|
||||
t.Error("Has should return false with closed DB")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoreEntriesQueryErrorWithOpenDB(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
m, err := Load(dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := m.OpenAppend(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m.Add("x1", "photo.jpg", 1024, "local")
|
||||
closedDB := m.DB()
|
||||
closedDB.Close()
|
||||
entries := m.Entries()
|
||||
if entries != nil && len(entries) != 0 {
|
||||
t.Errorf("Entries should be empty with closed DB, got %d", len(entries))
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoreCreateIndexError(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
m, err := Load(dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
realOpen := m.open
|
||||
m.open = func(driverName, dataSourceName string) (*sql.DB, error) {
|
||||
db, err := realOpen(driverName, dataSourceName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.SetExecFunc(func(query string, args ...any) (sql.Result, error) {
|
||||
if strings.Contains(query, "CREATE INDEX") {
|
||||
return nil, fmt.Errorf("injected CREATE INDEX error")
|
||||
}
|
||||
return db.Exec(query, args...)
|
||||
})
|
||||
return db, nil
|
||||
}
|
||||
if err := m.OpenAppend(); err == nil {
|
||||
t.Error("expected error from CREATE INDEX")
|
||||
m.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddEntryDefaultsPath(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
m, err := Load(dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := m.OpenAppend(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m.AddEntry(types.Entry{ID: "x1", Filename: "file.jpg", Size: 3, Cloud: "local", Checksum: "sha256:def"})
|
||||
if _, err := m.DB().Exec(`UPDATE downloads SET path = '' WHERE id = 'x1'`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sloaded := m.Entries()["x1"]
|
||||
if got := sloaded.Path; got != "file.jpg" {
|
||||
t.Fatalf("sqlite path = %q", got)
|
||||
}
|
||||
if sloaded.Checksum != "sha256:def" {
|
||||
t.Fatalf("sqlite checksum = %q", sloaded.Checksum)
|
||||
}
|
||||
m.Close()
|
||||
}
|
||||
|
||||
func TestNewLogWriter(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
dbPath := filepath.Join(dir, "test.db")
|
||||
db, err := sql.Open("sqlite", dbPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
lw, err := NewLogWriter(db)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
lw.Log(types.LogEntry{
|
||||
Timestamp: 1700000000,
|
||||
Level: "info",
|
||||
Event: "export_done",
|
||||
AssetID: "asset-1",
|
||||
Album: "Favorites",
|
||||
Filename: "photo.jpg",
|
||||
Size: 1024,
|
||||
Cloud: "local",
|
||||
DurationMs: 500,
|
||||
})
|
||||
lw.Log(types.LogEntry{
|
||||
Timestamp: 1700000001,
|
||||
Level: "error",
|
||||
Event: "export_fail",
|
||||
AssetID: "asset-2",
|
||||
Filename: "bad.jpg",
|
||||
Message: "timeout",
|
||||
})
|
||||
lw.Close()
|
||||
|
||||
var count int
|
||||
err = db.QueryRow(`SELECT COUNT(*) FROM logs`).Scan(&count)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if count != 2 {
|
||||
t.Errorf("expected 2 log entries, got %d", count)
|
||||
}
|
||||
|
||||
var event, level, assetID string
|
||||
err = db.QueryRow(`SELECT event, level, asset_id FROM logs WHERE asset_id = 'asset-1'`).Scan(&event, &level, &assetID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if event != "export_done" || level != "info" || assetID != "asset-1" {
|
||||
t.Errorf("unexpected row: event=%s level=%s asset_id=%s", event, level, assetID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogWriterNilDB(t *testing.T) {
|
||||
w := &LogWriter{db: nil}
|
||||
w.Log(types.LogEntry{Event: "test"})
|
||||
w.Close()
|
||||
}
|
||||
|
||||
func TestNewLogWriterError(t *testing.T) {
|
||||
db, err := sql.Open("sqlite", filepath.Join(t.TempDir(), "test.db"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := NewLogWriter(db); err == nil {
|
||||
t.Error("expected error for closed db")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogWriterCloseConcrete(t *testing.T) {
|
||||
(&LogWriter{}).Close()
|
||||
}
|
||||
Reference in New Issue
Block a user