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 let json = Jsonfeed.to_string feed in
225 Alcotest.(check bool) "contains version" true (contains_substring json "version");
226 Alcotest.(check bool) "contains title" true (contains_substring json "Test Feed")
227
228let test_feed_parse_minimal () =
229 let json = {|{
230 "version": "https://jsonfeed.org/version/1.1",
231 "title": "Test Feed",
232 "items": []
233 }|} in
234 match Jsonfeed.of_string json with
235 | Ok feed ->
236 Alcotest.(check string) "title" "Test Feed" (Jsonfeed.title feed);
237 Alcotest.(check int) "items" 0 (List.length (Jsonfeed.items feed))
238 | Error (`Msg err) ->
239 Alcotest.fail (Printf.sprintf "Parse failed: %s" err)
240
241let test_feed_parse_with_item () =
242 let json = {|{
243 "version": "https://jsonfeed.org/version/1.1",
244 "title": "Test Feed",
245 "items": [
246 {
247 "id": "https://example.com/1",
248 "content_html": "<p>Hello</p>"
249 }
250 ]
251 }|} in
252 match Jsonfeed.of_string json with
253 | Ok feed ->
254 let items = Jsonfeed.items feed in
255 Alcotest.(check int) "items count" 1 (List.length items);
256 (match items with
257 | [item] ->
258 Alcotest.(check string) "item id" "https://example.com/1" (Item.id item);
259 Alcotest.(check (option string)) "content_html" (Some "<p>Hello</p>") (Item.content_html item)
260 | _ -> Alcotest.fail "Expected 1 item")
261 | Error (`Msg err) ->
262 Alcotest.fail (Printf.sprintf "Parse failed: %s" err)
263
264let test_feed_roundtrip () =
265 let author = Author.create ~name:"Test Author" () in
266 let item = Item.create
267 ~id:"https://example.com/1"
268 ~title:"Test Item"
269 ~content:(`Html "<p>Hello, world!</p>")
270 ~date_published:(Jsonfeed.parse_rfc3339 "2024-11-01T10:00:00Z" |> Option.get)
271 ~tags:["test"; "example"]
272 () in
273
274 let feed1 = Jsonfeed.create
275 ~title:"Test Feed"
276 ~home_page_url:"https://example.com"
277 ~authors:[author]
278 ~items:[item]
279 () in
280
281 (* Serialize and parse *)
282 let json = Jsonfeed.to_string feed1 in
283 match Jsonfeed.of_string json with
284 | Ok feed2 ->
285 Alcotest.(check string) "title" (Jsonfeed.title feed1) (Jsonfeed.title feed2);
286 Alcotest.(check (option string)) "home_page_url"
287 (Jsonfeed.home_page_url feed1) (Jsonfeed.home_page_url feed2);
288 Alcotest.(check int) "items count"
289 (List.length (Jsonfeed.items feed1))
290 (List.length (Jsonfeed.items feed2))
291 | Error (`Msg err) ->
292 Alcotest.fail (Printf.sprintf "Round-trip failed: %s" err)
293
294let test_feed_parse_invalid_missing_content () =
295 let json = {|{
296 "version": "https://jsonfeed.org/version/1.1",
297 "title": "Test",
298 "items": [
299 {
300 "id": "1"
301 }
302 ]
303 }|} in
304 match Jsonfeed.of_string json with
305 | Ok _ -> Alcotest.fail "Should reject item without content"
306 | Error (`Msg err) ->
307 Alcotest.(check bool) "has error" true
308 (contains_substring err "content")
309
310let jsonfeed_tests = [
311 "create minimal feed", `Quick, test_feed_create_minimal;
312 "create feed with items", `Quick, test_feed_create_with_items;
313 "validate valid feed", `Quick, test_feed_validate_valid;
314 "validate empty title", `Quick, test_feed_validate_empty_title;
315 "to_string", `Quick, test_feed_to_string;
316 "parse minimal feed", `Quick, test_feed_parse_minimal;
317 "parse feed with item", `Quick, test_feed_parse_with_item;
318 "round-trip", `Quick, test_feed_roundtrip;
319 "parse invalid missing content", `Quick, test_feed_parse_invalid_missing_content;
320]
321
322(* Main test suite *)
323
324let () =
325 Alcotest.run "jsonfeed" [
326 "Author", author_tests;
327 "Attachment", attachment_tests;
328 "Hub", hub_tests;
329 "Item", item_tests;
330 "Jsonfeed", jsonfeed_tests;
331 ]