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}