main
  1package flux
  2
  3import (
  4	"context"
  5	"os"
  6	"path/filepath"
  7	"testing"
  8	"time"
  9)
 10
 11const testTilOrg = `#+title:      TIL — Today I Learned
 12#+date:       [2026-04-01 Tue]
 13#+filetags:   :til:_export:
 14
 15Short things I learned, discovered, or found interesting.
 16
 17* GitHub is a CVE Numbering Authority                           :github:security:
 18
 19<2026-03-12 Wed>
 20
 21GitHub can assign CVE IDs for any GitHub-hosted project. Most OSS projects don't need to
 22become their own CNA.
 23
 24The full flow stays within GitHub:
 25
 261. Someone submits a Private Vulnerability Report (PVR)
 272. Merge + publish is atomic
 28
 29* Fail2ban + push notifications = self-DoS                     :homelab:fail2ban:nixos:
 30
 31<2026-03-02 Sun>
 32
 33Push notification services (ntfy, Gotify) behind a reverse proxy with fail2ban create a
 34self-DoS loop.
 35
 36#+begin_warning
 37=ignoreIP= doesn't help if your IP changes.
 38#+end_warning
 39
 40* A TIL without tags
 41
 42<2026-01-15 Wed>
 43
 44This one has no tags, just content.
 45
 46More content in the second paragraph that should not appear in summary.
 47`
 48
 49func TestTILFetch(t *testing.T) {
 50	dir := t.TempDir()
 51	path := filepath.Join(dir, "til.org")
 52	if err := os.WriteFile(path, []byte(testTilOrg), 0644); err != nil {
 53		t.Fatal(err)
 54	}
 55
 56	src := &TILSource{File: path}
 57	entries, err := src.Fetch(context.Background(), time.Time{})
 58	if err != nil {
 59		t.Fatalf("Fetch: %v", err)
 60	}
 61
 62	if len(entries) != 3 {
 63		t.Fatalf("got %d entries, want 3", len(entries))
 64	}
 65
 66	// First entry
 67	e := entries[0]
 68	if e.Title != "GitHub is a CVE Numbering Authority" {
 69		t.Errorf("title = %q", e.Title)
 70	}
 71	if e.Date.Format("2006-01-02") != "2026-03-12" {
 72		t.Errorf("date = %s", e.Date)
 73	}
 74	if len(e.Tags) < 3 { // til + github + security
 75		t.Errorf("tags = %v, want at least 3", e.Tags)
 76	}
 77	if e.Body == "" {
 78		t.Error("body is empty")
 79	}
 80	// Body should be first paragraph only
 81	if len(e.Body) > 200 {
 82		t.Errorf("body too long (%d chars), should be first paragraph only: %q", len(e.Body), e.Body[:100])
 83	}
 84	t.Logf("entry 0: %q → %q", e.Title, e.Body)
 85
 86	// Second entry - check body stops before org block
 87	e2 := entries[1]
 88	if e2.Title != "Fail2ban + push notifications = self-DoS" {
 89		t.Errorf("title = %q", e2.Title)
 90	}
 91	if e2.Body == "" {
 92		t.Error("entry 2 body is empty")
 93	}
 94	t.Logf("entry 1: %q → %q", e2.Title, e2.Body)
 95
 96	// Third entry - no tags (except "til" added by source)
 97	e3 := entries[2]
 98	if e3.Title != "A TIL without tags" {
 99		t.Errorf("title = %q", e3.Title)
100	}
101	if len(e3.Tags) != 1 || e3.Tags[0] != "til" {
102		t.Errorf("tags = %v, want [til]", e3.Tags)
103	}
104	// Body should be first paragraph only, not second
105	if e3.Body != "This one has no tags, just content." {
106		t.Errorf("body = %q, want first paragraph only", e3.Body)
107	}
108	t.Logf("entry 2: %q → %q", e3.Title, e3.Body)
109}
110
111func TestTILFetchSince(t *testing.T) {
112	dir := t.TempDir()
113	path := filepath.Join(dir, "til.org")
114	if err := os.WriteFile(path, []byte(testTilOrg), 0644); err != nil {
115		t.Fatal(err)
116	}
117
118	src := &TILSource{File: path}
119	since := time.Date(2026, 3, 1, 0, 0, 0, 0, time.UTC)
120	entries, err := src.Fetch(context.Background(), since)
121	if err != nil {
122		t.Fatalf("Fetch: %v", err)
123	}
124
125	if len(entries) != 2 {
126		t.Fatalf("got %d entries since %s, want 2", len(entries), since.Format("2006-01-02"))
127	}
128}
129
130func TestTILFetchMissing(t *testing.T) {
131	src := &TILSource{File: "/nonexistent/til.org"}
132	entries, err := src.Fetch(context.Background(), time.Time{})
133	if err != nil {
134		t.Fatalf("expected nil error for missing file, got: %v", err)
135	}
136	if len(entries) != 0 {
137		t.Errorf("expected 0 entries, got %d", len(entries))
138	}
139}
140
141func TestSlugify(t *testing.T) {
142	tests := []struct {
143		in, want string
144	}{
145		{"GitHub is a CVE Numbering Authority", "github-is-a-cve-numbering-authority"},
146		{"Fail2ban + push = self-DoS", "fail2ban-push-self-dos"},
147		{"don't bind-mount over /nix/store", "don-t-bind-mount-over-nix-store"},
148		{`Paperless: let it auto-detect, don't "optimize"`, "paperless-let-it-auto-detect-don-t-optimize"},
149	}
150	for _, tt := range tests {
151		got := slugify(tt.in)
152		if got != tt.want {
153			t.Errorf("slugify(%q) = %q, want %q", tt.in, got, tt.want)
154		}
155	}
156}
157
158func TestCleanOrgMarkup(t *testing.T) {
159	tests := []struct {
160		in, want string
161	}{
162		{"*bold* text", "bold text"},
163		{"/italic/ text", "italic text"},
164		{"=code= and ~verbatim~", "code and verbatim"},
165		{"[[https://example.com][Example]]", "Example"},
166		{"plain text", "plain text"},
167	}
168	for _, tt := range tests {
169		got := cleanOrgMarkup(tt.in)
170		if got != tt.want {
171			t.Errorf("cleanOrgMarkup(%q) = %q, want %q", tt.in, got, tt.want)
172		}
173	}
174}
175
176func TestTILRealFile(t *testing.T) {
177	home := os.Getenv("HOME")
178	path := filepath.Join(home, "desktop", "org", "til.org")
179	if _, err := os.Stat(path); err != nil {
180		t.Skipf("til.org not found: %v", err)
181	}
182
183	src := &TILSource{File: path}
184	entries, err := src.Fetch(context.Background(), time.Time{})
185	if err != nil {
186		t.Fatalf("Fetch: %v", err)
187	}
188
189	t.Logf("Found %d TIL entries", len(entries))
190	for _, e := range entries {
191		t.Logf("  [%s] %s — %s", e.Date.Format("2006-01-02"), e.Title, truncate(e.Body, 80))
192	}
193}
194
195func truncate(s string, n int) string {
196	if len(s) <= n {
197		return s
198	}
199	return s[:n] + "…"
200}