1
0
Fork 0
timelinize/datasources/firefox/firefox_test.go
Sergio Rubio 88eab7c50a
firefox: check sqlite database has the required table (#74)
* firefox: check sqlite database has the required table

This improves Firefox places.sqlite database recognition by making sure
it has the required moz_places table we need.

* Address linter issues

* Wait for the goroutine to exit before cancelling
2025-04-08 09:47:17 -06:00

253 lines
5.8 KiB
Go

/*
Timelinize
Copyright (c) 2025 Sergio Rubio
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package firefox
import (
"context"
"database/sql"
"os"
"path/filepath"
"sync"
"testing"
"time"
_ "github.com/mattn/go-sqlite3"
"github.com/timelinize/timelinize/timeline"
)
func TestFirefox_Recognize(t *testing.T) {
tests := []struct {
name string
filename string
want float64
valid bool
}{
{
name: "Places database",
filename: "places.sqlite",
want: 1,
valid: true,
},
{
name: "Invalid places database",
filename: "places.sqlite",
want: 0,
},
{
name: "Other SQLite file",
filename: "other.sqlite",
want: 0,
},
{
name: "Non-SQLite file",
filename: "test.txt",
want: 0,
},
}
f := new(Firefox)
ctx := context.Background()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
dbPath := filepath.Join(t.TempDir(), tt.filename)
_, err := createTestDatabase(dbPath, tt.valid)
if err != nil {
t.Fatalf("Failed to create test database: %v", err)
}
dirEntry := timeline.DirEntry{
DirEntry: testDirEntry{name: tt.filename},
FSRoot: dbPath,
}
recognition, err := f.Recognize(ctx, dirEntry, timeline.RecognizeParams{})
if err != nil {
t.Errorf("Recognize() error = %v", err)
return
}
if recognition.Confidence != tt.want {
t.Errorf("Recognize() confidence = %v, want %v", recognition.Confidence, tt.want)
}
})
}
}
func TestFirefox_FileImport(t *testing.T) {
tmpDir := t.TempDir()
dbFilename := "places.sqlite"
dbPath := filepath.Join(tmpDir, dbFilename)
db, err := createTestDatabase(dbPath, true)
if err != nil {
t.Fatalf("Failed to create test database: %v", err)
}
defer db.Close()
visitTime := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
err = insertTestData(db, "https://example.com", "Example Site", "Test description", visitTime)
if err != nil {
t.Fatalf("Failed to insert test data: %v", err)
}
err = insertTestData(db, "https://example.com", "Example Site", "Test description", visitTime)
if err != nil {
t.Fatalf("Failed to insert test data: %v", err)
}
itemChan := make(chan *timeline.Graph)
defer close(itemChan)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Run the import in a goroutine
wg := sync.WaitGroup{}
wg.Add(1)
go func() {
defer wg.Done()
f := new(Firefox)
dirEntry := timeline.DirEntry{
DirEntry: testDirEntry{name: dbFilename},
FSRoot: tmpDir,
Filename: dbFilename,
}
err := f.FileImport(ctx, dirEntry, timeline.ImportParams{
Pipeline: itemChan,
})
if err != nil {
t.Errorf("FileImport() error = %v", err)
}
}()
count := 0
for count < 2 {
select {
case graph := <-itemChan:
count++
if graph == nil || graph.Item == nil {
t.Fatal("Expected non-nil item")
}
item := graph.Item
if item.Classification.Name != timeline.ClassPageView.Name {
t.Errorf("Expected ClassPageView, got %v", item.Classification)
}
if !item.Timestamp.Equal(visitTime) {
t.Errorf("Expected timestamp %v, got %v", visitTime, item.Timestamp)
}
if url, ok := item.Metadata["URL"].(string); !ok || url != "https://example.com" {
t.Errorf("Expected URL https://example.com, got %v", url)
}
if title, ok := item.Metadata["Title"].(string); !ok || title != "Example Site" {
t.Errorf("Expected title 'Example Site', got %v", title)
}
if desc, ok := item.Metadata["Description"].(string); !ok || desc != "Test description" {
t.Errorf("Expected description 'Test description', got %v", desc)
}
case <-time.After(10 * time.Second):
t.Fatal("Timeout waiting for imported item")
}
}
wg.Wait()
if count != 2 {
t.Errorf("Expected count 2, got %d", count)
}
}
type testDirEntry struct {
name string
}
func (t testDirEntry) Name() string { return t.name }
func (t testDirEntry) IsDir() bool { return false }
func (t testDirEntry) Type() os.FileMode { return 0 }
func (t testDirEntry) Info() (os.FileInfo, error) { return nil, nil }
func createTestDatabase(path string, valid bool) (*sql.DB, error) {
db, err := sql.Open("sqlite3", path)
if err != nil {
return nil, err
}
query := "CREATE TABLE random (id INTEGER PRIMARY KEY);"
if valid {
query = `
CREATE TABLE moz_places (
id INTEGER PRIMARY KEY,
url TEXT NOT NULL,
title TEXT,
description TEXT
);
CREATE TABLE moz_historyvisits (
id INTEGER PRIMARY KEY,
place_id INTEGER NOT NULL,
visit_date INTEGER NOT NULL
);
`
}
_, err = db.Exec(query)
if err != nil {
db.Close()
return nil, err
}
return db, nil
}
func insertTestData(db *sql.DB, url, title, description string, visitTime time.Time) error {
tx, err := db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
// Insert place
res, err := tx.Exec(`
INSERT INTO moz_places (url, title, description)
VALUES (?, ?, ?)
`, url, title, description)
if err != nil {
return err
}
placeID, err := res.LastInsertId()
if err != nil {
return err
}
// Insert visit
_, err = tx.Exec(`
INSERT INTO moz_historyvisits (place_id, visit_date)
VALUES (?, ?)
`, placeID, visitTime.UnixNano()/1000) // Convert to microseconds
if err != nil {
return err
}
return tx.Commit()
}