OCaml library for JSONfeed parsing and creation
1(** Tests for jsonfeed library *)
2
3open Jsonfeed
4
5(* Author tests *)
6
7let test_author_create_with_name () =
8 let author = Author.create ~name:"Jane Doe" () in
9 Alcotest.(check (option string)) "name" (Some "Jane Doe") (Author.name author);
10 Alcotest.(check (option string)) "url" None (Author.url author);
11 Alcotest.(check (option string)) "avatar" None (Author.avatar author);
12 Alcotest.(check bool) "is_valid" true (Author.is_valid author)
13
14let test_author_create_with_url () =
15 let author = Author.create ~url:"https://example.com" () in
16 Alcotest.(check (option string)) "name" None (Author.name author);
17 Alcotest.(check (option string)) "url" (Some "https://example.com") (Author.url author);
18 Alcotest.(check bool) "is_valid" true (Author.is_valid author)
19
20let test_author_create_with_all_fields () =
21 let author = Author.create
22 ~name:"Jane Doe"
23 ~url:"https://example.com"
24 ~avatar:"https://example.com/avatar.png"
25 () in
26 Alcotest.(check (option string)) "name" (Some "Jane Doe") (Author.name author);
27 Alcotest.(check (option string)) "url" (Some "https://example.com") (Author.url author);
28 Alcotest.(check (option string)) "avatar" (Some "https://example.com/avatar.png") (Author.avatar author);
29 Alcotest.(check bool) "is_valid" true (Author.is_valid author)
30
31let test_author_create_no_fields_fails () =
32 Alcotest.check_raises "no fields"
33 (Invalid_argument "Author.create: at least one field (name, url, or avatar) must be provided")
34 (fun () -> ignore (Author.create ()))
35
36let test_author_equal () =
37 let a1 = Author.create ~name:"Jane Doe" () in
38 let a2 = Author.create ~name:"Jane Doe" () in
39 let a3 = Author.create ~name:"John Smith" () in
40 Alcotest.(check bool) "equal same" true (Author.equal a1 a2);
41 Alcotest.(check bool) "equal different" false (Author.equal a1 a3)
42
43let test_author_pp () =
44 let author = Author.create ~name:"Jane Doe" ~url:"https://example.com" () in
45 let s = Format.asprintf "%a" Author.pp author in
46 Alcotest.(check string) "pp with name and url" "Jane Doe <https://example.com>" s
47
48let author_tests = [
49 "create with name", `Quick, test_author_create_with_name;
50 "create with url", `Quick, test_author_create_with_url;
51 "create with all fields", `Quick, test_author_create_with_all_fields;
52 "create with no fields fails", `Quick, test_author_create_no_fields_fails;
53 "equal", `Quick, test_author_equal;
54 "pp", `Quick, test_author_pp;
55]
56
57(* Attachment tests *)
58
59let test_attachment_create_minimal () =
60 let att = Attachment.create
61 ~url:"https://example.com/file.mp3"
62 ~mime_type:"audio/mpeg"
63 () in
64 Alcotest.(check string) "url" "https://example.com/file.mp3" (Attachment.url att);
65 Alcotest.(check string) "mime_type" "audio/mpeg" (Attachment.mime_type att);
66 Alcotest.(check (option string)) "title" None (Attachment.title att);
67 Alcotest.(check (option int64)) "size_in_bytes" None (Attachment.size_in_bytes att);
68 Alcotest.(check (option int)) "duration_in_seconds" None (Attachment.duration_in_seconds att)
69
70let test_attachment_create_complete () =
71 let att = Attachment.create
72 ~url:"https://example.com/episode.mp3"
73 ~mime_type:"audio/mpeg"
74 ~title:"Episode 1"
75 ~size_in_bytes:15_728_640L
76 ~duration_in_seconds:1800
77 () in
78 Alcotest.(check string) "url" "https://example.com/episode.mp3" (Attachment.url att);
79 Alcotest.(check string) "mime_type" "audio/mpeg" (Attachment.mime_type att);
80 Alcotest.(check (option string)) "title" (Some "Episode 1") (Attachment.title att);
81 Alcotest.(check (option int64)) "size_in_bytes" (Some 15_728_640L) (Attachment.size_in_bytes att);
82 Alcotest.(check (option int)) "duration_in_seconds" (Some 1800) (Attachment.duration_in_seconds att)
83
84let test_attachment_equal () =
85 let a1 = Attachment.create
86 ~url:"https://example.com/file.mp3"
87 ~mime_type:"audio/mpeg"
88 () in
89 let a2 = Attachment.create
90 ~url:"https://example.com/file.mp3"
91 ~mime_type:"audio/mpeg"
92 () in
93 let a3 = Attachment.create
94 ~url:"https://example.com/other.mp3"
95 ~mime_type:"audio/mpeg"
96 () in
97 Alcotest.(check bool) "equal same" true (Attachment.equal a1 a2);
98 Alcotest.(check bool) "equal different" false (Attachment.equal a1 a3)
99
100let attachment_tests = [
101 "create minimal", `Quick, test_attachment_create_minimal;
102 "create complete", `Quick, test_attachment_create_complete;
103 "equal", `Quick, test_attachment_equal;
104]
105
106(* Hub tests *)
107
108let test_hub_create () =
109 let hub = Hub.create ~type_:"WebSub" ~url:"https://example.com/hub" () in
110 Alcotest.(check string) "type_" "WebSub" (Hub.type_ hub);
111 Alcotest.(check string) "url" "https://example.com/hub" (Hub.url hub)
112
113let test_hub_equal () =
114 let h1 = Hub.create ~type_:"WebSub" ~url:"https://example.com/hub" () in
115 let h2 = Hub.create ~type_:"WebSub" ~url:"https://example.com/hub" () in
116 let h3 = Hub.create ~type_:"rssCloud" ~url:"https://example.com/hub" () in
117 Alcotest.(check bool) "equal same" true (Hub.equal h1 h2);
118 Alcotest.(check bool) "equal different" false (Hub.equal h1 h3)
119
120let hub_tests = [
121 "create", `Quick, test_hub_create;
122 "equal", `Quick, test_hub_equal;
123]
124
125(* Item tests *)
126
127let test_item_create_html () =
128 let item = Item.create
129 ~id:"https://example.com/1"
130 ~content:(`Html "<p>Hello</p>")
131 () in
132 Alcotest.(check string) "id" "https://example.com/1" (Item.id item);
133 Alcotest.(check (option string)) "content_html" (Some "<p>Hello</p>") (Item.content_html item);
134 Alcotest.(check (option string)) "content_text" None (Item.content_text item)
135
136let test_item_create_text () =
137 let item = Item.create
138 ~id:"https://example.com/2"
139 ~content:(`Text "Hello world")
140 () in
141 Alcotest.(check string) "id" "https://example.com/2" (Item.id item);
142 Alcotest.(check (option string)) "content_html" None (Item.content_html item);
143 Alcotest.(check (option string)) "content_text" (Some "Hello world") (Item.content_text item)
144
145let test_item_create_both () =
146 let item = Item.create
147 ~id:"https://example.com/3"
148 ~content:(`Both ("<p>Hello</p>", "Hello"))
149 () in
150 Alcotest.(check string) "id" "https://example.com/3" (Item.id item);
151 Alcotest.(check (option string)) "content_html" (Some "<p>Hello</p>") (Item.content_html item);
152 Alcotest.(check (option string)) "content_text" (Some "Hello") (Item.content_text item)
153
154let test_item_with_metadata () =
155 let item = Item.create
156 ~id:"https://example.com/4"
157 ~content:(`Html "<p>Test</p>")
158 ~title:"Test Post"
159 ~url:"https://example.com/posts/4"
160 ~tags:["test"; "example"]
161 () in
162 Alcotest.(check (option string)) "title" (Some "Test Post") (Item.title item);
163 Alcotest.(check (option string)) "url" (Some "https://example.com/posts/4") (Item.url item);
164 Alcotest.(check (option (list string))) "tags" (Some ["test"; "example"]) (Item.tags item)
165
166let test_item_equal () =
167 let i1 = Item.create ~id:"https://example.com/1" ~content:(`Text "test") () in
168 let i2 = Item.create ~id:"https://example.com/1" ~content:(`Html "<p>test</p>") () in
169 let i3 = Item.create ~id:"https://example.com/2" ~content:(`Text "test") () in
170 Alcotest.(check bool) "equal same id" true (Item.equal i1 i2);
171 Alcotest.(check bool) "equal different id" false (Item.equal i1 i3)
172
173let item_tests = [
174 "create with HTML content", `Quick, test_item_create_html;
175 "create with text content", `Quick, test_item_create_text;
176 "create with both contents", `Quick, test_item_create_both;
177 "create with metadata", `Quick, test_item_with_metadata;
178 "equal", `Quick, test_item_equal;
179]
180
181(* Jsonfeed tests *)
182
183let test_feed_create_minimal () =
184 let feed = Jsonfeed.create ~title:"Test Feed" ~items:[] () in
185 Alcotest.(check string) "title" "Test Feed" (Jsonfeed.title feed);
186 Alcotest.(check string) "version" "https://jsonfeed.org/version/1.1" (Jsonfeed.version feed);
187 Alcotest.(check int) "items length" 0 (List.length (Jsonfeed.items feed))
188
189let test_feed_create_with_items () =
190 let item = Item.create
191 ~id:"https://example.com/1"
192 ~content:(`Text "Hello")
193 () in
194 let feed = Jsonfeed.create
195 ~title:"Test Feed"
196 ~items:[item]
197 () in
198 Alcotest.(check int) "items length" 1 (List.length (Jsonfeed.items feed))
199
200let test_feed_validate_valid () =
201 let feed = Jsonfeed.create ~title:"Test" ~items:[] () in
202 match Jsonfeed.validate feed with
203 | Ok () -> ()
204 | Error errors ->
205 Alcotest.fail (Printf.sprintf "Validation should succeed: %s"
206 (String.concat "; " errors))
207
208let test_feed_validate_empty_title () =
209 let feed = Jsonfeed.create ~title:"" ~items:[] () in
210 match Jsonfeed.validate feed with
211 | Ok () -> Alcotest.fail "Should fail validation"
212 | Error errors ->
213 Alcotest.(check bool) "has error" true
214 (List.exists (fun s -> String.starts_with ~prefix:"title" s) errors)
215
216let contains_substring s sub =
217 try
218 let _ = Str.search_forward (Str.regexp_string sub) s 0 in
219 true
220 with Not_found -> false
221
222let test_feed_to_string () =
223 let feed = Jsonfeed.create ~title:"Test Feed" ~items:[] () in
224 match Jsonfeed.to_string feed with
225 | Ok json ->
226 Alcotest.(check bool) "contains version" true (contains_substring json "version");
227 Alcotest.(check bool) "contains title" true (contains_substring json "Test Feed")
228 | Error e ->
229 Alcotest.fail (Printf.sprintf "Serialization failed: %s" (Jsont.Error.to_string e))
230
231let test_feed_parse_minimal () =
232 let json = {|{
233 "version": "https://jsonfeed.org/version/1.1",
234 "title": "Test Feed",
235 "items": []
236 }|} in
237 match Jsonfeed.of_string json with
238 | Ok feed ->
239 Alcotest.(check string) "title" "Test Feed" (Jsonfeed.title feed);
240 Alcotest.(check int) "items" 0 (List.length (Jsonfeed.items feed))
241 | Error err ->
242 Alcotest.fail (Printf.sprintf "Parse failed: %s" (Jsont.Error.to_string err))
243
244let test_feed_parse_with_item () =
245 let json = {|{
246 "version": "https://jsonfeed.org/version/1.1",
247 "title": "Test Feed",
248 "items": [
249 {
250 "id": "https://example.com/1",
251 "content_html": "<p>Hello</p>"
252 }
253 ]
254 }|} in
255 match Jsonfeed.of_string json with
256 | Ok feed ->
257 let items = Jsonfeed.items feed in
258 Alcotest.(check int) "items count" 1 (List.length items);
259 (match items with
260 | [item] ->
261 Alcotest.(check string) "item id" "https://example.com/1" (Item.id item);
262 Alcotest.(check (option string)) "content_html" (Some "<p>Hello</p>") (Item.content_html item)
263 | _ -> Alcotest.fail "Expected 1 item")
264 | Error err ->
265 Alcotest.fail (Printf.sprintf "Parse failed: %s" (Jsont.Error.to_string err))
266
267let test_feed_roundtrip () =
268 let author = Author.create ~name:"Test Author" () in
269 let item = Item.create
270 ~id:"https://example.com/1"
271 ~title:"Test Item"
272 ~content:(`Html "<p>Hello, world!</p>")
273 ~date_published:(Jsonfeed.Rfc3339.parse "2024-11-01T10:00:00Z" |> Option.get)
274 ~tags:["test"; "example"]
275 () in
276
277 let feed1 = Jsonfeed.create
278 ~title:"Test Feed"
279 ~home_page_url:"https://example.com"
280 ~authors:[author]
281 ~items:[item]
282 () in
283
284 (* Serialize and parse *)
285 match Jsonfeed.to_string feed1 with
286 | Error e ->
287 Alcotest.fail (Printf.sprintf "Serialization failed: %s" (Jsont.Error.to_string e))
288 | Ok json ->
289 match Jsonfeed.of_string json with
290 | Ok feed2 ->
291 Alcotest.(check string) "title" (Jsonfeed.title feed1) (Jsonfeed.title feed2);
292 Alcotest.(check (option string)) "home_page_url"
293 (Jsonfeed.home_page_url feed1) (Jsonfeed.home_page_url feed2);
294 Alcotest.(check int) "items count"
295 (List.length (Jsonfeed.items feed1))
296 (List.length (Jsonfeed.items feed2))
297 | Error err ->
298 Alcotest.fail (Printf.sprintf "Round-trip parse failed: %s" (Jsont.Error.to_string err))
299
300let test_feed_parse_invalid_missing_content () =
301 let json = {|{
302 "version": "https://jsonfeed.org/version/1.1",
303 "title": "Test",
304 "items": [
305 {
306 "id": "1"
307 }
308 ]
309 }|} in
310 match Jsonfeed.of_string json with
311 | Ok _ -> Alcotest.fail "Should reject item without content"
312 | Error err ->
313 let err_str = Jsont.Error.to_string err in
314 Alcotest.(check bool) "has error" true
315 (contains_substring err_str "content")
316
317let jsonfeed_tests = [
318 "create minimal feed", `Quick, test_feed_create_minimal;
319 "create feed with items", `Quick, test_feed_create_with_items;
320 "validate valid feed", `Quick, test_feed_validate_valid;
321 "validate empty title", `Quick, test_feed_validate_empty_title;
322 "to_string", `Quick, test_feed_to_string;
323 "parse minimal feed", `Quick, test_feed_parse_minimal;
324 "parse feed with item", `Quick, test_feed_parse_with_item;
325 "round-trip", `Quick, test_feed_roundtrip;
326 "parse invalid missing content", `Quick, test_feed_parse_invalid_missing_content;
327]
328
329(* Main test suite *)
330
331let () =
332 Alcotest.run "jsonfeed" [
333 "Author", author_tests;
334 "Attachment", attachment_tests;
335 "Hub", hub_tests;
336 "Item", item_tests;
337 "Jsonfeed", jsonfeed_tests;
338 ]