main
  1package flux
  2
  3import (
  4	"context"
  5	"os"
  6	"path/filepath"
  7	"testing"
  8	"time"
  9)
 10
 11const testBookmarksOrg = `#+title: Bookmarks
 12#+filetags: :bookmarks:_export:
 13
 14Links worth keeping.
 15
 16* [[https://tonsky.me/blog/syntax-highlighting/][Syntax highlighting is wrong]]  :emacs:design:
 17
 18<2026-04-01 Tue>
 19
 20The essay that changed how I think about code colors.
 21
 22* [[https://beathagenlocher.com/stream/][Beat's /stream/]]  :web:design:inspiration:
 23
 24<2026-04-01 Tue>
 25
 26The original inspiration for this flux stream.
 27
 28* [[https://stagex.tools][StageX]]  :containers:nix:
 29
 30<2026-03-20 Thu>
 31
 32Interesting approach to base images.
 33
 34* Plain text heading without link
 35
 36<2026-03-15 Fri>
 37
 38This one has no URL.
 39`
 40
 41func TestBookmarksFetch(t *testing.T) {
 42	dir := t.TempDir()
 43	path := filepath.Join(dir, "bookmarks.org")
 44	if err := os.WriteFile(path, []byte(testBookmarksOrg), 0644); err != nil {
 45		t.Fatal(err)
 46	}
 47
 48	src := &BookmarkSource{File: path}
 49	entries, err := src.Fetch(context.Background(), time.Time{})
 50	if err != nil {
 51		t.Fatalf("Fetch: %v", err)
 52	}
 53
 54	if len(entries) != 4 {
 55		t.Fatalf("got %d entries, want 4", len(entries))
 56	}
 57
 58	// First entry — link with tags
 59	e := entries[0]
 60	if e.Title != "Syntax highlighting is wrong" {
 61		t.Errorf("title = %q", e.Title)
 62	}
 63	if e.URL != "https://tonsky.me/blog/syntax-highlighting/" {
 64		t.Errorf("url = %q", e.URL)
 65	}
 66	if e.Kind != KindBookmark {
 67		t.Errorf("kind = %s, want bookmark", e.Kind)
 68	}
 69	if len(e.Tags) != 2 {
 70		t.Errorf("tags = %v, want [emacs design]", e.Tags)
 71	}
 72	if e.Body == "" {
 73		t.Error("body is empty")
 74	}
 75	t.Logf("entry 0: %q → %q (%s)", e.Title, e.URL, e.Body)
 76
 77	// Second entry
 78	e2 := entries[1]
 79	if e2.URL != "https://beathagenlocher.com/stream/" {
 80		t.Errorf("url = %q", e2.URL)
 81	}
 82	if len(e2.Tags) != 3 {
 83		t.Errorf("tags = %v, want 3", e2.Tags)
 84	}
 85
 86	// Fourth entry — no link
 87	e4 := entries[3]
 88	if e4.URL != "" {
 89		t.Errorf("expected no URL, got %q", e4.URL)
 90	}
 91	if e4.Title != "Plain text heading without link" {
 92		t.Errorf("title = %q", e4.Title)
 93	}
 94}
 95
 96func TestBookmarksFetchSince(t *testing.T) {
 97	dir := t.TempDir()
 98	path := filepath.Join(dir, "bookmarks.org")
 99	if err := os.WriteFile(path, []byte(testBookmarksOrg), 0644); err != nil {
100		t.Fatal(err)
101	}
102
103	src := &BookmarkSource{File: path}
104	since := time.Date(2026, 3, 25, 0, 0, 0, 0, time.UTC)
105	entries, err := src.Fetch(context.Background(), since)
106	if err != nil {
107		t.Fatalf("Fetch: %v", err)
108	}
109
110	if len(entries) != 2 {
111		t.Fatalf("got %d entries since %s, want 2", len(entries), since.Format("2006-01-02"))
112	}
113}
114
115func TestBookmarksFetchMissing(t *testing.T) {
116	src := &BookmarkSource{File: "/nonexistent/bookmarks.org"}
117	entries, err := src.Fetch(context.Background(), time.Time{})
118	if err != nil {
119		t.Fatalf("expected nil error, got: %v", err)
120	}
121	if len(entries) != 0 {
122		t.Errorf("expected 0 entries, got %d", len(entries))
123	}
124}
125
126func TestBookmarksRealFile(t *testing.T) {
127	home := os.Getenv("HOME")
128	path := filepath.Join(home, "desktop", "org", "bookmarks.org")
129	if _, err := os.Stat(path); err != nil {
130		t.Skipf("bookmarks.org not found: %v", err)
131	}
132
133	src := &BookmarkSource{File: path}
134	entries, err := src.Fetch(context.Background(), time.Time{})
135	if err != nil {
136		t.Fatalf("Fetch: %v", err)
137	}
138
139	t.Logf("Found %d bookmark entries", len(entries))
140	for _, e := range entries {
141		t.Logf("  [%s] %s → %s", e.Date.Format("2006-01-02"), e.Title, e.URL)
142	}
143}
144
145func TestParseBookmarkHeading(t *testing.T) {
146	tests := []struct {
147		line  string
148		title string
149		url   string
150		tags  int
151	}{
152		{"* [[https://example.com][Example]]  :web:cool:", "Example", "https://example.com", 2},
153		{"* [[https://example.com][Example]]", "Example", "https://example.com", 0},
154		{"* Plain heading  :tag:", "Plain heading", "", 1},
155		{"* Plain heading", "Plain heading", "", 0},
156	}
157
158	for _, tt := range tests {
159		be := parseBookmarkHeading(tt.line)
160		if be.title != tt.title {
161			t.Errorf("parseBookmarkHeading(%q).title = %q, want %q", tt.line, be.title, tt.title)
162		}
163		if be.url != tt.url {
164			t.Errorf("parseBookmarkHeading(%q).url = %q, want %q", tt.line, be.url, tt.url)
165		}
166		if len(be.tags) != tt.tags {
167			t.Errorf("parseBookmarkHeading(%q).tags = %v, want %d tags", tt.line, be.tags, tt.tags)
168		}
169	}
170}