cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists 馃崈
charm
leaflet
readability
golang
1package ui
2
3import (
4 "bytes"
5 "context"
6 "fmt"
7 "strings"
8 "testing"
9 "time"
10
11 "github.com/stormlightlabs/noteleaf/internal/models"
12 "github.com/stormlightlabs/noteleaf/internal/repo"
13)
14
15type mockBookRepository struct {
16 books []*models.Book
17 err error
18}
19
20func (m *mockBookRepository) List(ctx context.Context, options repo.BookListOptions) ([]*models.Book, error) {
21 if m.err != nil {
22 return nil, m.err
23 }
24
25 var filtered []*models.Book
26 for _, book := range m.books {
27 if options.Status != "" && book.Status != options.Status {
28 continue
29 }
30
31 if options.Search != "" && !strings.Contains(strings.ToLower(book.Title), strings.ToLower(options.Search)) {
32 continue
33 }
34
35 filtered = append(filtered, book)
36
37 if options.Limit > 0 && len(filtered) >= options.Limit {
38 break
39 }
40 }
41
42 return filtered, nil
43}
44
45func TestBookAdapter(t *testing.T) {
46 now := time.Now()
47 book := &models.Book{
48 ID: 1,
49 Title: "Test Book",
50 Author: "Test Author",
51 Status: "reading",
52 Progress: 45,
53 Pages: 250,
54 Rating: 4.5,
55 Notes: "Great book so far",
56 Added: now,
57 Started: &now,
58 }
59
60 t.Run("BookRecord", func(t *testing.T) {
61 record := &BookRecord{Book: book}
62
63 t.Run("GetField", func(t *testing.T) {
64 tests := []struct {
65 field string
66 expected any
67 name string
68 }{
69 {"id", int64(1), "should return book ID"},
70 {"title", "Test Book", "should return book title"},
71 {"author", "Test Author", "should return book author"},
72 {"status", "reading", "should return book status"},
73 {"progress", 45, "should return book progress"},
74 {"pages", 250, "should return page count"},
75 {"rating", 4.5, "should return rating"},
76 {"notes", "Great book so far", "should return notes"},
77 {"added", now, "should return added time"},
78 {"started", &now, "should return started time"},
79 {"unknown", "", "should return empty string for unknown field"},
80 }
81
82 for _, tt := range tests {
83 t.Run(tt.name, func(t *testing.T) {
84 result := record.GetField(tt.field)
85
86 switch expected := tt.expected.(type) {
87 case time.Time:
88 if resultTime, ok := result.(time.Time); !ok || !resultTime.Equal(expected) {
89 t.Errorf("GetField(%q) = %v, want %v", tt.field, result, tt.expected)
90 }
91 case *time.Time:
92 resultPtr, ok := result.(*time.Time)
93 if !ok || (expected == nil && resultPtr != nil) || (expected != nil && (resultPtr == nil || !resultPtr.Equal(*expected))) {
94 t.Errorf("GetField(%q) = %v, want %v", tt.field, result, tt.expected)
95 }
96 default:
97 if result != tt.expected {
98 t.Errorf("GetField(%q) = %v, want %v", tt.field, result, tt.expected)
99 }
100 }
101 })
102 }
103 })
104
105 t.Run("ListItem methods", func(t *testing.T) {
106 if record.GetTitle() != "Test Book" {
107 t.Errorf("GetTitle() = %q, want 'Test Book'", record.GetTitle())
108 }
109
110 description := record.GetDescription()
111 if !strings.Contains(description, "Test Author") {
112 t.Errorf("GetDescription() should contain author, got: %s", description)
113 }
114 if !strings.Contains(description, "Reading") {
115 t.Errorf("GetDescription() should contain status, got: %s", description)
116 }
117 if !strings.Contains(description, "250 pages") {
118 t.Errorf("GetDescription() should contain page count, got: %s", description)
119 }
120 if !strings.Contains(description, "45%") {
121 t.Errorf("GetDescription() should contain progress, got: %s", description)
122 }
123
124 filterValue := record.GetFilterValue()
125 if !strings.Contains(filterValue, "Test Book") || !strings.Contains(filterValue, "Test Author") {
126 t.Errorf("GetFilterValue() should contain title and author, got: %s", filterValue)
127 }
128 if !strings.Contains(filterValue, "Great book so far") {
129 t.Errorf("GetFilterValue() should contain notes, got: %s", filterValue)
130 }
131 })
132
133 t.Run("Model interface", func(t *testing.T) {
134 if record.GetID() != 1 {
135 t.Errorf("GetID() = %d, want 1", record.GetID())
136 }
137
138 if record.GetTableName() != "books" {
139 t.Errorf("GetTableName() = %q, want 'books'", record.GetTableName())
140 }
141 })
142 })
143
144 t.Run("BookDataSource", func(t *testing.T) {
145 books := []*models.Book{
146 {
147 ID: 1,
148 Title: "Go Programming",
149 Author: "John Doe",
150 Status: "reading",
151 },
152 {
153 ID: 2,
154 Title: "Python Guide",
155 Author: "Jane Smith",
156 Status: "queued",
157 },
158 {
159 ID: 3,
160 Title: "Advanced Go",
161 Author: "Bob Johnson",
162 Status: "finished",
163 },
164 }
165
166 t.Run("Load", func(t *testing.T) {
167 repo := &mockBookRepository{books: books}
168 source := &BookDataSource{repo: repo}
169
170 items, err := source.Load(context.Background(), ListOptions{})
171 if err != nil {
172 t.Fatalf("Load() failed: %v", err)
173 }
174
175 if len(items) != 3 {
176 t.Errorf("Load() returned %d items, want 3", len(items))
177 }
178
179 if items[0].GetTitle() != "Go Programming" {
180 t.Errorf("First item title = %q, want 'Go Programming'", items[0].GetTitle())
181 }
182 })
183
184 t.Run("Load with status filter", func(t *testing.T) {
185 repo := &mockBookRepository{books: books}
186 source := &BookDataSource{repo: repo, status: "reading"}
187
188 items, err := source.Load(context.Background(), ListOptions{})
189 if err != nil {
190 t.Fatalf("Load() failed: %v", err)
191 }
192
193 if len(items) != 1 {
194 t.Errorf("Load() with status filter returned %d items, want 1", len(items))
195 }
196 if items[0].GetTitle() != "Go Programming" {
197 t.Errorf("Filtered item title = %q, want 'Go Programming'", items[0].GetTitle())
198 }
199 })
200
201 t.Run("Search", func(t *testing.T) {
202 repo := &mockBookRepository{books: books}
203 source := &BookDataSource{repo: repo}
204
205 items, err := source.Search(context.Background(), "Python", ListOptions{})
206 if err != nil {
207 t.Fatalf("Search() failed: %v", err)
208 }
209
210 if len(items) != 1 {
211 t.Errorf("Search() returned %d items, want 1", len(items))
212 }
213 if items[0].GetTitle() != "Python Guide" {
214 t.Errorf("Search result title = %q, want 'Python Guide'", items[0].GetTitle())
215 }
216 })
217
218 t.Run("Load error", func(t *testing.T) {
219 testErr := fmt.Errorf("test error")
220 repo := &mockBookRepository{err: testErr}
221 source := &BookDataSource{repo: repo}
222
223 if _, err := source.Load(context.Background(), ListOptions{}); err != testErr {
224 t.Errorf("Load() error = %v, want %v", err, testErr)
225 }
226 })
227
228 t.Run("Count", func(t *testing.T) {
229 repo := &mockBookRepository{books: books}
230 source := &BookDataSource{repo: repo}
231
232 count, err := source.Count(context.Background(), ListOptions{})
233 if err != nil {
234 t.Fatalf("Count() failed: %v", err)
235 }
236
237 if count != 3 {
238 t.Errorf("Count() = %d, want 3", count)
239 }
240 })
241
242 t.Run("Count error", func(t *testing.T) {
243 testErr := fmt.Errorf("test error")
244 repo := &mockBookRepository{err: testErr}
245 source := &BookDataSource{repo: repo}
246
247 _, err := source.Count(context.Background(), ListOptions{})
248 if err != testErr {
249 t.Errorf("Count() error = %v, want %v", err, testErr)
250 }
251 })
252 })
253
254 t.Run("NewBookDataList", func(t *testing.T) {
255 repo := &mockBookRepository{
256 books: []*models.Book{
257 {
258 ID: 1,
259 Title: "Test Book",
260 Author: "Test Author",
261 Status: "reading",
262 },
263 },
264 }
265
266 opts := DataListOptions{
267 Output: &bytes.Buffer{},
268 Input: strings.NewReader("q\n"),
269 Static: true,
270 }
271
272 list := NewBookDataList(repo, opts, "")
273 if list == nil {
274 t.Fatal("NewBookDataList() returned nil")
275 }
276
277 err := list.Browse(context.Background())
278 if err != nil {
279 t.Errorf("Browse() failed: %v", err)
280 }
281 })
282
283 t.Run("NewBookListFromList", func(t *testing.T) {
284 repo := &mockBookRepository{
285 books: []*models.Book{
286 {
287 ID: 1,
288 Title: "Test Book",
289 Author: "Test Author",
290 Status: "reading",
291 },
292 },
293 }
294
295 output := &bytes.Buffer{}
296 input := strings.NewReader("q\n")
297
298 list := NewBookListFromList(repo, output, input, true, "")
299 if list == nil {
300 t.Fatal("NewBookListFromList() returned nil")
301 }
302
303 err := list.Browse(context.Background())
304 if err != nil {
305 t.Errorf("Browse() failed: %v", err)
306 }
307
308 outputStr := output.String()
309 if !strings.Contains(outputStr, "Books") {
310 t.Error("Output should contain 'Books' title")
311 }
312 if !strings.Contains(outputStr, "Test Book") {
313 t.Error("Output should contain book title")
314 }
315 })
316
317 t.Run("Format Book for View", func(t *testing.T) {
318 now := time.Now()
319 started := now.Add(-time.Hour)
320
321 book := &models.Book{
322 ID: 1,
323 Title: "Test Book Title",
324 Author: "Test Author",
325 Status: "reading",
326 Progress: 75,
327 Pages: 250,
328 Rating: 4.5,
329 Notes: "This is a great book with detailed explanations.",
330 Added: now,
331 Started: &started,
332 }
333
334 result := formatBookForView(book)
335
336 if !strings.Contains(result, "Test Book Title") {
337 t.Error("Formatted view should contain book title")
338 }
339 if !strings.Contains(result, "Test Author") {
340 t.Error("Formatted view should contain author")
341 }
342 if !strings.Contains(result, "Reading") {
343 t.Error("Formatted view should contain status")
344 }
345 if !strings.Contains(result, "75%") {
346 t.Error("Formatted view should contain progress")
347 }
348 if !strings.Contains(result, "250") {
349 t.Error("Formatted view should contain page count")
350 }
351 if !strings.Contains(result, "4.5/5") {
352 t.Error("Formatted view should contain rating")
353 }
354 if !strings.Contains(result, "Added:") {
355 t.Error("Formatted view should contain added date")
356 }
357 if !strings.Contains(result, "Started:") {
358 t.Error("Formatted view should contain started date")
359 }
360 if !strings.Contains(result, "great book with detailed") {
361 t.Error("Formatted view should contain notes")
362 }
363 })
364}