internal/output/formatter_test.go (view raw)
1package output
2
3import (
4 "encoding/json"
5 "strings"
6 "testing"
7 "time"
8
9 "llm_aggregator/internal/aggregator"
10)
11
12func TestNewFormatter(t *testing.T) {
13 tests := []struct {
14 name string
15 format string
16 wantErr bool
17 wantNil bool
18 }{
19 {"text format", "text", false, false},
20 {"json format", "json", false, false},
21 {"markdown format", "markdown", false, false},
22 {"uppercase text", "TEXT", false, false},
23 {"mixed case json", "Json", false, false},
24 {"unsupported format", "xml", true, true},
25 {"empty format", "", true, true},
26 {"unknown format", "csv", true, true},
27 }
28
29 for _, tt := range tests {
30 t.Run(tt.name, func(t *testing.T) {
31 formatter, err := NewFormatter(tt.format)
32
33 if tt.wantErr && err == nil {
34 t.Error("Expected error, got nil")
35 }
36 if !tt.wantErr && err != nil {
37 t.Errorf("Unexpected error: %v", err)
38 }
39 if tt.wantNil && formatter != nil {
40 t.Error("Expected nil formatter")
41 }
42 if !tt.wantNil && formatter == nil {
43 t.Error("Expected non-nil formatter")
44 }
45 })
46 }
47}
48
49func TestFormatDataJSON(t *testing.T) {
50 formatter, err := NewFormatter("json")
51 if err != nil {
52 t.Fatalf("Failed to create formatter: %v", err)
53 }
54
55 data := map[string]any{
56 "title": "Test Summary",
57 "prompt": "Summarise the news",
58 "model": "deepseek-chat",
59 "articles_count": 5,
60 "timestamp": "2024-01-15T10:00:00Z",
61 "summary": "This is a test summary.",
62 }
63
64 output, err := formatter.FormatData(data)
65 if err != nil {
66 t.Errorf("FormatData failed: %v", err)
67 }
68
69 // Verify it's valid JSON
70 var parsed map[string]any
71 if err := json.Unmarshal([]byte(output), &parsed); err != nil {
72 t.Errorf("Output is not valid JSON: %v", err)
73 }
74
75 // Verify expected fields
76 if parsed["title"] != "Test Summary" {
77 t.Errorf("Expected title 'Test Summary', got %v", parsed["title"])
78 }
79 if parsed["summary"] != "This is a test summary." {
80 t.Errorf("Expected summary 'This is a test summary.', got %v", parsed["summary"])
81 }
82}
83
84func TestFormatDataMarkdown(t *testing.T) {
85 formatter, err := NewFormatter("markdown")
86 if err != nil {
87 t.Fatalf("Failed to create formatter: %v", err)
88 }
89
90 data := map[string]any{
91 "title": "Markdown Test",
92 "prompt": "Test prompt",
93 "model": "gpt-4",
94 "articles_count": 3,
95 "summary": "Markdown summary content.",
96 }
97
98 output, err := formatter.FormatData(data)
99 if err != nil {
100 t.Errorf("FormatData failed: %v", err)
101 }
102
103 // Check for markdown structure
104 if !strings.Contains(output, "# Markdown Test") {
105 t.Error("Expected title in markdown format")
106 }
107 if !strings.Contains(output, "## Metadata") {
108 t.Error("Expected metadata section")
109 }
110 if !strings.Contains(output, "## Summary") {
111 t.Error("Expected summary section")
112 }
113 if !strings.Contains(output, "Markdown summary content.") {
114 t.Error("Expected summary content")
115 }
116}
117
118func TestFormatDataText(t *testing.T) {
119 formatter, err := NewFormatter("text")
120 if err != nil {
121 t.Fatalf("Failed to create formatter: %v", err)
122 }
123
124 data := map[string]any{
125 "title": "Text Output Test",
126 "prompt": "Test prompt",
127 "model": "claude-3",
128 "articles_count": 2,
129 "summary": "Plain text summary.",
130 }
131
132 output, err := formatter.FormatData(data)
133 if err != nil {
134 t.Errorf("FormatData failed: %v", err)
135 }
136
137 // Check for text formatting elements
138 if !strings.Contains(output, "====") {
139 t.Error("Expected separator lines")
140 }
141 if !strings.Contains(output, "Text Output Test") {
142 t.Error("Expected title in output")
143 }
144 if !strings.Contains(output, "METADATA") {
145 t.Error("Expected metadata header")
146 }
147 if !strings.Contains(output, "SUMMARY") {
148 t.Error("Expected summary header")
149 }
150}
151
152func TestFormatDataWithArticles(t *testing.T) {
153 formatter, err := NewFormatter("text")
154 if err != nil {
155 t.Fatalf("Failed to create formatter: %v", err)
156 }
157
158 articles := []map[string]any{
159 {
160 "title": "Article One",
161 "source_feed": "News Feed",
162 "author": "John Doe",
163 "link": "https://example.com/1",
164 "published": time.Date(2024, 1, 15, 10, 0, 0, 0, time.UTC),
165 },
166 {
167 "title": "Article Two",
168 "source_feed": "Tech Feed",
169 "author": "Jane Smith",
170 "link": "https://example.com/2",
171 },
172 }
173
174 data := map[string]any{
175 "title": "With Articles",
176 "summary": "Summary with articles",
177 "articles_count": 2,
178 "articles": articles,
179 }
180
181 output, err := formatter.FormatData(data)
182 if err != nil {
183 t.Errorf("FormatData failed: %v", err)
184 }
185
186 // Check articles are included
187 if !strings.Contains(output, "Article One") {
188 t.Error("Expected Article One in output")
189 }
190 if !strings.Contains(output, "Article Two") {
191 t.Error("Expected Article Two in output")
192 }
193 if !strings.Contains(output, "News Feed") {
194 t.Error("Expected feed name in output")
195 }
196}
197
198func TestFormatDataMarkdownWithArticles(t *testing.T) {
199 formatter, err := NewFormatter("markdown")
200 if err != nil {
201 t.Fatalf("Failed to create formatter: %v", err)
202 }
203
204 articles := []map[string]any{
205 {
206 "title": "MD Article",
207 "source_feed": "MD Feed",
208 "published": time.Date(2024, 1, 15, 10, 0, 0, 0, time.UTC),
209 "link": "https://example.com/md",
210 },
211 }
212
213 data := map[string]any{
214 "title": "Markdown with Articles",
215 "summary": "Test summary",
216 "articles_count": 1,
217 "articles": articles,
218 }
219
220 output, err := formatter.FormatData(data)
221 if err != nil {
222 t.Errorf("FormatData failed: %v", err)
223 }
224
225 // Check markdown article format
226 if !strings.Contains(output, "### Article 1: MD Article") {
227 t.Error("Expected markdown article header")
228 }
229 if !strings.Contains(output, "**Source**: MD Feed") {
230 t.Error("Expected source field in markdown")
231 }
232 if !strings.Contains(output, "[https://example.com/md](https://example.com/md)") {
233 t.Error("Expected link in markdown format")
234 }
235}
236
237func TestFormatDataDefaultValues(t *testing.T) {
238 formatter, err := NewFormatter("text")
239 if err != nil {
240 t.Fatalf("Failed to create formatter: %v", err)
241 }
242
243 // Data with missing fields - should use defaults
244 data := map[string]any{}
245
246 output, err := formatter.FormatData(data)
247 if err != nil {
248 t.Errorf("FormatData failed: %v", err)
249 }
250
251 // Check default values are used
252 if !strings.Contains(output, "LLM Aggregator Summary") {
253 t.Error("Expected default title")
254 }
255 if !strings.Contains(output, "No summary available.") {
256 t.Error("Expected default summary message")
257 }
258}
259
260func TestGetStringHelper(t *testing.T) {
261 tests := []struct {
262 name string
263 data map[string]any
264 key string
265 defaultValue string
266 want string
267 }{
268 {"existing string", map[string]any{"key": "value"}, "key", "default", "value"},
269 {"missing key", map[string]any{}, "key", "default", "default"},
270 {"nil value", map[string]any{"key": nil}, "key", "default", "default"},
271 {"wrong type", map[string]any{"key": 123}, "key", "default", "default"},
272 }
273
274 for _, tt := range tests {
275 t.Run(tt.name, func(t *testing.T) {
276 got := getString(tt.data, tt.key, tt.defaultValue)
277 if got != tt.want {
278 t.Errorf("getString() = %q, want %q", got, tt.want)
279 }
280 })
281 }
282}
283
284func TestGetIntHelper(t *testing.T) {
285 tests := []struct {
286 name string
287 data map[string]any
288 key string
289 defaultValue int
290 want int
291 }{
292 {"existing int", map[string]any{"count": 42}, "count", 0, 42},
293 {"existing float64", map[string]any{"count": float64(42.5)}, "count", 0, 42},
294 {"missing key", map[string]any{}, "count", 10, 10},
295 {"nil value", map[string]any{"count": nil}, "count", 10, 10},
296 {"wrong type", map[string]any{"count": "string"}, "count", 10, 10},
297 }
298
299 for _, tt := range tests {
300 t.Run(tt.name, func(t *testing.T) {
301 got := getInt(tt.data, tt.key, tt.defaultValue)
302 if got != tt.want {
303 t.Errorf("getInt() = %d, want %d", got, tt.want)
304 }
305 })
306 }
307}
308
309func TestCenterText(t *testing.T) {
310 tests := []struct {
311 name string
312 text string
313 width int
314 expect string
315 }{
316 {"short text", "Hi", 10, " Hi "}, // 3 left, 2 right (floored)
317 {"exact width", "Hello", 5, "Hello"},
318 {"longer than width", "Hello World", 5, "Hello World"},
319 {"even padding", "Test", 12, " Test "}, // 4 left, 3 right (floored)
320 }
321
322 for _, tt := range tests {
323 t.Run(tt.name, func(t *testing.T) {
324 got := centerText(tt.text, tt.width)
325 if got != tt.expect {
326 t.Errorf("centerText(%q, %d) = %q, want %q", tt.text, tt.width, got, tt.expect)
327 }
328 })
329 }
330}
331
332func TestFormatArticleList(t *testing.T) {
333 articles := []*aggregator.Article{
334 {
335 Title: "Test Article",
336 Link: "https://example.com/test",
337 Content: "Article content here",
338 SourceFeed: "Test Feed",
339 Author: "Test Author",
340 },
341 }
342
343 t.Run("text format", func(t *testing.T) {
344 output, err := FormatArticleList(articles, "text", false)
345 if err != nil {
346 t.Errorf("FormatArticleList failed: %v", err)
347 }
348 if !strings.Contains(output, "Test Article") {
349 t.Error("Expected article title in output")
350 }
351 })
352
353 t.Run("markdown format", func(t *testing.T) {
354 output, err := FormatArticleList(articles, "markdown", false)
355 if err != nil {
356 t.Errorf("FormatArticleList failed: %v", err)
357 }
358 if !strings.Contains(output, "Articles List") {
359 t.Error("Expected articles list header")
360 }
361 })
362
363 t.Run("json format", func(t *testing.T) {
364 output, err := FormatArticleList(articles, "json", false)
365 if err != nil {
366 t.Errorf("FormatArticleList failed: %v", err)
367 }
368 if !strings.Contains(output, "Test Article") {
369 t.Error("Expected article title in JSON")
370 }
371 })
372
373 t.Run("include content", func(t *testing.T) {
374 output, err := FormatArticleList(articles, "text", true)
375 if err != nil {
376 t.Errorf("FormatArticleList failed: %v", err)
377 }
378 if !strings.Contains(output, "Article content here") {
379 t.Error("Expected content in output when includeContent=true")
380 }
381 })
382}
383
384func TestJSONIndentation(t *testing.T) {
385 formatter, err := NewFormatter("json")
386 if err != nil {
387 t.Fatalf("Failed to create formatter: %v", err)
388 }
389
390 data := map[string]any{
391 "nested": map[string]any{
392 "key1": "value1",
393 "key2": 42,
394 },
395 "array": []int{1, 2, 3},
396 }
397
398 output, err := formatter.FormatData(data)
399 if err != nil {
400 t.Errorf("FormatData failed: %v", err)
401 }
402
403 // Check for proper indentation (two spaces)
404 lines := strings.Split(output, "\n")
405 if len(lines) < 3 {
406 t.Error("Expected multiple lines in formatted JSON")
407 }
408 if !strings.HasPrefix(lines[1], " ") {
409 t.Error("Expected two-space indentation")
410 }
411}