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 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 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 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 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 ]