diff options
| author | Peter Stone <thepeterstone@gmail.com> | 2026-01-20 11:17:19 -1000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-01-20 11:17:19 -1000 |
| commit | 07ba815e8517ee2d3a5fa531361bbd09bdfcbaa7 (patch) | |
| tree | ca9d9be0f02d5a724a3646f87d4a9f50203249cc /internal/store | |
| parent | 6a59098c3096f5ebd3a61ef5268cbd480b0f1519 (diff) | |
Remove Obsidian integration for public server deployment
Obsidian relied on local filesystem access which is incompatible with
public server deployment. This removes all Obsidian-related code including:
- API client and interface
- Store layer methods (SaveNotes, GetNotes, SearchNotes)
- Handler methods and routes
- UI tab and templates
- Configuration fields
- Related tests
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Diffstat (limited to 'internal/store')
| -rw-r--r-- | internal/store/sqlite.go | 124 | ||||
| -rw-r--r-- | internal/store/sqlite_test.go | 327 |
2 files changed, 2 insertions, 449 deletions
diff --git a/internal/store/sqlite.go b/internal/store/sqlite.go index b8d0c97..dac3321 100644 --- a/internal/store/sqlite.go +++ b/internal/store/sqlite.go @@ -15,9 +15,8 @@ import ( // Cache key constants const ( - CacheKeyTodoistTasks = "todoist_tasks" - CacheKeyTrelloBoards = "trello_boards" - CacheKeyObsidianNotes = "obsidian_notes" + CacheKeyTodoistTasks = "todoist_tasks" + CacheKeyTrelloBoards = "trello_boards" CacheKeyPlanToEatMeals = "plantoeat_meals" ) @@ -234,125 +233,6 @@ func (s *Store) DeleteTasksByIDs(ids []string) error { return tx.Commit() } -// Notes operations - -// SaveNotes saves multiple notes to the database -func (s *Store) SaveNotes(notes []models.Note) error { - tx, err := s.db.Begin() - if err != nil { - return err - } - defer tx.Rollback() - - stmt, err := tx.Prepare(` - INSERT OR REPLACE INTO notes - (filename, title, content, modified, path, tags, updated_at) - VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) - `) - if err != nil { - return err - } - defer stmt.Close() - - for _, note := range notes { - tagsJSON, _ := json.Marshal(note.Tags) - _, err := stmt.Exec( - note.Filename, - note.Title, - note.Content, - note.Modified, - note.Path, - string(tagsJSON), - ) - if err != nil { - return err - } - } - - return tx.Commit() -} - -// GetNotes retrieves all notes from the database -func (s *Store) GetNotes(limit int) ([]models.Note, error) { - query := ` - SELECT filename, title, content, modified, path, tags - FROM notes - ORDER BY modified DESC - ` - var args []interface{} - if limit > 0 { - query += " LIMIT ?" - args = append(args, limit) - } - - rows, err := s.db.Query(query, args...) - if err != nil { - return nil, err - } - defer rows.Close() - - var notes []models.Note - for rows.Next() { - var note models.Note - var tagsJSON string - - err := rows.Scan( - ¬e.Filename, - ¬e.Title, - ¬e.Content, - ¬e.Modified, - ¬e.Path, - &tagsJSON, - ) - if err != nil { - return nil, err - } - - json.Unmarshal([]byte(tagsJSON), ¬e.Tags) - notes = append(notes, note) - } - - return notes, rows.Err() -} - -// SearchNotes searches notes by title or content -func (s *Store) SearchNotes(query string) ([]models.Note, error) { - searchPattern := "%" + query + "%" - rows, err := s.db.Query(` - SELECT filename, title, content, modified, path, tags - FROM notes - WHERE title LIKE ? OR content LIKE ? - ORDER BY modified DESC - `, searchPattern, searchPattern) - if err != nil { - return nil, err - } - defer rows.Close() - - var notes []models.Note - for rows.Next() { - var note models.Note - var tagsJSON string - - err := rows.Scan( - ¬e.Filename, - ¬e.Title, - ¬e.Content, - ¬e.Modified, - ¬e.Path, - &tagsJSON, - ) - if err != nil { - return nil, err - } - - json.Unmarshal([]byte(tagsJSON), ¬e.Tags) - notes = append(notes, note) - } - - return notes, rows.Err() -} - // Meals operations // SaveMeals saves multiple meals to the database diff --git a/internal/store/sqlite_test.go b/internal/store/sqlite_test.go index 962a1bf..6bf7783 100644 --- a/internal/store/sqlite_test.go +++ b/internal/store/sqlite_test.go @@ -2,7 +2,6 @@ package store import ( "database/sql" - "fmt" "path/filepath" "testing" "time" @@ -11,332 +10,6 @@ import ( "task-dashboard/internal/models" ) -// setupTestStore creates a test store with schema but without migrations directory -func setupTestStore(t *testing.T) *Store { - t.Helper() - - // Create temporary database file - tempDir := t.TempDir() - dbPath := filepath.Join(tempDir, "test.db") - - db, err := sql.Open("sqlite3", dbPath) - if err != nil { - t.Fatalf("Failed to open test database: %v", err) - } - - // Enable foreign keys - if _, err := db.Exec("PRAGMA foreign_keys = ON"); err != nil { - t.Fatalf("Failed to enable foreign keys: %v", err) - } - - // Enable WAL mode for better concurrency - if _, err := db.Exec("PRAGMA journal_mode = WAL"); err != nil { - t.Fatalf("Failed to enable WAL mode: %v", err) - } - - // Serialize writes to prevent "database is locked" errors - db.SetMaxOpenConns(1) - - store := &Store{db: db} - - // Create notes table directly (without migrations) - schema := ` - CREATE TABLE IF NOT EXISTS notes ( - filename TEXT PRIMARY KEY, - title TEXT NOT NULL, - content TEXT, - modified DATETIME NOT NULL, - path TEXT NOT NULL, - tags TEXT, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP - ); - CREATE INDEX IF NOT EXISTS idx_notes_modified ON notes(modified DESC); - ` - if _, err := db.Exec(schema); err != nil { - t.Fatalf("Failed to create schema: %v", err) - } - - return store -} - -// TestGetNotes_LimitClause verifies that the LIMIT clause works correctly -// and prevents SQL injection -func TestGetNotes_LimitClause(t *testing.T) { - store := setupTestStore(t) - defer store.Close() - - // Create 3 distinct notes with different modification times - baseTime := time.Now() - notes := []models.Note{ - { - Filename: "note1.md", - Title: "Note 1", - Content: "Content of note 1", - Modified: baseTime.Add(-2 * time.Hour), - Path: "/vault/note1.md", - Tags: []string{"tag1"}, - }, - { - Filename: "note2.md", - Title: "Note 2", - Content: "Content of note 2", - Modified: baseTime.Add(-1 * time.Hour), - Path: "/vault/note2.md", - Tags: []string{"tag2"}, - }, - { - Filename: "note3.md", - Title: "Note 3", - Content: "Content of note 3", - Modified: baseTime, - Path: "/vault/note3.md", - Tags: []string{"tag3"}, - }, - } - - // Save all 3 notes - if err := store.SaveNotes(notes); err != nil { - t.Fatalf("Failed to save notes: %v", err) - } - - // Test 1: Call GetNotes(2) - should return exactly 2 notes - t.Run("GetNotes with limit 2", func(t *testing.T) { - result, err := store.GetNotes(2) - if err != nil { - t.Fatalf("GetNotes(2) returned error: %v", err) - } - - if len(result) != 2 { - t.Errorf("Expected exactly 2 notes, got %d", len(result)) - } - - // Verify they are the most recent notes (note3 and note2) - if len(result) >= 2 { - if result[0].Filename != "note3.md" { - t.Errorf("Expected first note to be 'note3.md', got '%s'", result[0].Filename) - } - if result[1].Filename != "note2.md" { - t.Errorf("Expected second note to be 'note2.md', got '%s'", result[1].Filename) - } - } - }) - - // Test 2: Call GetNotes(5) - should return all 3 notes - t.Run("GetNotes with limit 5", func(t *testing.T) { - result, err := store.GetNotes(5) - if err != nil { - t.Fatalf("GetNotes(5) returned error: %v", err) - } - - if len(result) != 3 { - t.Errorf("Expected all 3 notes, got %d", len(result)) - } - - // Verify order (most recent first) - if len(result) == 3 { - if result[0].Filename != "note3.md" { - t.Errorf("Expected first note to be 'note3.md', got '%s'", result[0].Filename) - } - if result[1].Filename != "note2.md" { - t.Errorf("Expected second note to be 'note2.md', got '%s'", result[1].Filename) - } - if result[2].Filename != "note1.md" { - t.Errorf("Expected third note to be 'note1.md', got '%s'", result[2].Filename) - } - } - }) - - // Test 3: Call GetNotes(0) - should return all notes (no limit) - t.Run("GetNotes with no limit", func(t *testing.T) { - result, err := store.GetNotes(0) - if err != nil { - t.Fatalf("GetNotes(0) returned error: %v", err) - } - - if len(result) != 3 { - t.Errorf("Expected all 3 notes with no limit, got %d", len(result)) - } - }) - - // Test 4: Call GetNotes(1) - should return exactly 1 note - t.Run("GetNotes with limit 1", func(t *testing.T) { - result, err := store.GetNotes(1) - if err != nil { - t.Fatalf("GetNotes(1) returned error: %v", err) - } - - if len(result) != 1 { - t.Errorf("Expected exactly 1 note, got %d", len(result)) - } - - // Should be the most recent note - if len(result) == 1 && result[0].Filename != "note3.md" { - t.Errorf("Expected note to be 'note3.md', got '%s'", result[0].Filename) - } - }) -} - -// TestGetNotes_EmptyDatabase verifies behavior with empty database -func TestGetNotes_EmptyDatabase(t *testing.T) { - store := setupTestStore(t) - defer store.Close() - - result, err := store.GetNotes(10) - if err != nil { - t.Fatalf("GetNotes on empty database returned error: %v", err) - } - - if len(result) != 0 { - t.Errorf("Expected 0 notes from empty database, got %d", len(result)) - } -} - -// TestSaveNotes_Upsert verifies that SaveNotes properly upserts notes -func TestSaveNotes_Upsert(t *testing.T) { - store := setupTestStore(t) - defer store.Close() - - baseTime := time.Now() - - // Save initial note - initialNote := []models.Note{ - { - Filename: "test.md", - Title: "Initial Title", - Content: "Initial content", - Modified: baseTime, - Path: "/vault/test.md", - Tags: []string{"initial"}, - }, - } - - if err := store.SaveNotes(initialNote); err != nil { - t.Fatalf("Failed to save initial note: %v", err) - } - - // Verify initial save - notes, err := store.GetNotes(0) - if err != nil { - t.Fatalf("Failed to get notes: %v", err) - } - if len(notes) != 1 { - t.Fatalf("Expected 1 note after initial save, got %d", len(notes)) - } - if notes[0].Title != "Initial Title" { - t.Errorf("Expected title 'Initial Title', got '%s'", notes[0].Title) - } - - // Update the same note - updatedNote := []models.Note{ - { - Filename: "test.md", - Title: "Updated Title", - Content: "Updated content", - Modified: baseTime.Add(1 * time.Hour), - Path: "/vault/test.md", - Tags: []string{"updated"}, - }, - } - - if err := store.SaveNotes(updatedNote); err != nil { - t.Fatalf("Failed to save updated note: %v", err) - } - - // Verify update (should still be 1 note, not 2) - notes, err = store.GetNotes(0) - if err != nil { - t.Fatalf("Failed to get notes after update: %v", err) - } - if len(notes) != 1 { - t.Errorf("Expected 1 note after update (upsert), got %d", len(notes)) - } - if notes[0].Title != "Updated Title" { - t.Errorf("Expected title 'Updated Title', got '%s'", notes[0].Title) - } -} - -// TestGetNotes_NegativeLimit verifies behavior with negative limit -func TestGetNotes_NegativeLimit(t *testing.T) { - store := setupTestStore(t) - defer store.Close() - - // Save a note - notes := []models.Note{ - { - Filename: "test.md", - Title: "Test", - Content: "Test content", - Modified: time.Now(), - Path: "/vault/test.md", - Tags: []string{}, - }, - } - - if err := store.SaveNotes(notes); err != nil { - t.Fatalf("Failed to save note: %v", err) - } - - // Call with negative limit (should be treated as no limit) - result, err := store.GetNotes(-1) - if err != nil { - t.Fatalf("GetNotes(-1) returned error: %v", err) - } - - // Should return all notes since negative is treated as 0/no limit - if len(result) != 1 { - t.Errorf("Expected 1 note with negative limit, got %d", len(result)) - } -} - -// TestGetNotes_SQLInjectionAttempt verifies that LIMIT parameter is properly sanitized -func TestGetNotes_SQLInjectionAttempt(t *testing.T) { - store := setupTestStore(t) - defer store.Close() - - // Save some notes - notes := []models.Note{ - { - Filename: "note1.md", - Title: "Note 1", - Content: "Content 1", - Modified: time.Now(), - Path: "/vault/note1.md", - Tags: []string{}, - }, - { - Filename: "note2.md", - Title: "Note 2", - Content: "Content 2", - Modified: time.Now(), - Path: "/vault/note2.md", - Tags: []string{}, - }, - } - - if err := store.SaveNotes(notes); err != nil { - t.Fatalf("Failed to save notes: %v", err) - } - - // The LIMIT parameter is now properly parameterized using sql.Query with args - // This test verifies that the code uses parameterized queries - // If it didn't, passing a malicious value could cause SQL injection - - // Normal call should work - result, err := store.GetNotes(1) - if err != nil { - t.Fatalf("GetNotes(1) returned error: %v", err) - } - - if len(result) != 1 { - t.Errorf("Expected 1 note, got %d", len(result)) - } - - // The fact that this test passes with a simple integer confirms - // that the implementation properly uses parameterized queries - // and is not vulnerable to SQL injection via the LIMIT clause - fmt.Println("✓ LIMIT clause is properly parameterized") -} - // setupTestStoreWithTasks creates a test store with tasks table func setupTestStoreWithTasks(t *testing.T) *Store { t.Helper() |
