OCaml library for JSONfeed parsing and creation
at main 20 kB view raw
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)) 18 "url" (Some "https://example.com") (Author.url author); 19 Alcotest.(check bool) "is_valid" true (Author.is_valid author) 20 21let test_author_create_with_all_fields () = 22 let author = 23 Author.create ~name:"Jane Doe" ~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)) 28 "url" (Some "https://example.com") (Author.url author); 29 Alcotest.(check (option string)) 30 "avatar" (Some "https://example.com/avatar.png") (Author.avatar author); 31 Alcotest.(check bool) "is_valid" true (Author.is_valid author) 32 33let test_author_create_no_fields_fails () = 34 Alcotest.check_raises "no fields" 35 (Invalid_argument 36 "Author.create: at least one field (name, url, or avatar) must be \ 37 provided") (fun () -> ignore (Author.create ())) 38 39let test_author_equal () = 40 let a1 = Author.create ~name:"Jane Doe" () in 41 let a2 = Author.create ~name:"Jane Doe" () in 42 let a3 = Author.create ~name:"John Smith" () in 43 Alcotest.(check bool) "equal same" true (Author.equal a1 a2); 44 Alcotest.(check bool) "equal different" false (Author.equal a1 a3) 45 46let test_author_pp () = 47 let author = Author.create ~name:"Jane Doe" ~url:"https://example.com" () in 48 let s = Format.asprintf "%a" Author.pp author in 49 Alcotest.(check string) 50 "pp with name and url" "Jane Doe <https://example.com>" s 51 52let author_tests = 53 [ 54 ("create with name", `Quick, test_author_create_with_name); 55 ("create with url", `Quick, test_author_create_with_url); 56 ("create with all fields", `Quick, test_author_create_with_all_fields); 57 ("create with no fields fails", `Quick, test_author_create_no_fields_fails); 58 ("equal", `Quick, test_author_equal); 59 ("pp", `Quick, test_author_pp); 60 ] 61 62(* Attachment tests *) 63 64let test_attachment_create_minimal () = 65 let att = 66 Attachment.create ~url:"https://example.com/file.mp3" 67 ~mime_type:"audio/mpeg" () 68 in 69 Alcotest.(check string) 70 "url" "https://example.com/file.mp3" (Attachment.url att); 71 Alcotest.(check string) "mime_type" "audio/mpeg" (Attachment.mime_type att); 72 Alcotest.(check (option string)) "title" None (Attachment.title att); 73 Alcotest.(check (option int64)) 74 "size_in_bytes" None 75 (Attachment.size_in_bytes att); 76 Alcotest.(check (option int)) 77 "duration_in_seconds" None 78 (Attachment.duration_in_seconds att) 79 80let test_attachment_create_complete () = 81 let att = 82 Attachment.create ~url:"https://example.com/episode.mp3" 83 ~mime_type:"audio/mpeg" ~title:"Episode 1" ~size_in_bytes:15_728_640L 84 ~duration_in_seconds:1800 () 85 in 86 Alcotest.(check string) 87 "url" "https://example.com/episode.mp3" (Attachment.url att); 88 Alcotest.(check string) "mime_type" "audio/mpeg" (Attachment.mime_type att); 89 Alcotest.(check (option string)) 90 "title" (Some "Episode 1") (Attachment.title att); 91 Alcotest.(check (option int64)) 92 "size_in_bytes" (Some 15_728_640L) 93 (Attachment.size_in_bytes att); 94 Alcotest.(check (option int)) 95 "duration_in_seconds" (Some 1800) 96 (Attachment.duration_in_seconds att) 97 98let test_attachment_equal () = 99 let a1 = 100 Attachment.create ~url:"https://example.com/file.mp3" 101 ~mime_type:"audio/mpeg" () 102 in 103 let a2 = 104 Attachment.create ~url:"https://example.com/file.mp3" 105 ~mime_type:"audio/mpeg" () 106 in 107 let a3 = 108 Attachment.create ~url:"https://example.com/other.mp3" 109 ~mime_type:"audio/mpeg" () 110 in 111 Alcotest.(check bool) "equal same" true (Attachment.equal a1 a2); 112 Alcotest.(check bool) "equal different" false (Attachment.equal a1 a3) 113 114let attachment_tests = 115 [ 116 ("create minimal", `Quick, test_attachment_create_minimal); 117 ("create complete", `Quick, test_attachment_create_complete); 118 ("equal", `Quick, test_attachment_equal); 119 ] 120 121(* Hub tests *) 122 123let test_hub_create () = 124 let hub = Hub.create ~type_:"WebSub" ~url:"https://example.com/hub" () in 125 Alcotest.(check string) "type_" "WebSub" (Hub.type_ hub); 126 Alcotest.(check string) "url" "https://example.com/hub" (Hub.url hub) 127 128let test_hub_equal () = 129 let h1 = Hub.create ~type_:"WebSub" ~url:"https://example.com/hub" () in 130 let h2 = Hub.create ~type_:"WebSub" ~url:"https://example.com/hub" () in 131 let h3 = Hub.create ~type_:"rssCloud" ~url:"https://example.com/hub" () in 132 Alcotest.(check bool) "equal same" true (Hub.equal h1 h2); 133 Alcotest.(check bool) "equal different" false (Hub.equal h1 h3) 134 135let hub_tests = 136 [ ("create", `Quick, test_hub_create); ("equal", `Quick, test_hub_equal) ] 137 138(* Item tests *) 139 140let test_item_create_html () = 141 let item = 142 Item.create ~id:"https://example.com/1" ~content:(`Html "<p>Hello</p>") () 143 in 144 Alcotest.(check string) "id" "https://example.com/1" (Item.id item); 145 Alcotest.(check (option string)) 146 "content_html" (Some "<p>Hello</p>") (Item.content_html item); 147 Alcotest.(check (option string)) "content_text" None (Item.content_text item) 148 149let test_item_create_text () = 150 let item = 151 Item.create ~id:"https://example.com/2" ~content:(`Text "Hello world") () 152 in 153 Alcotest.(check string) "id" "https://example.com/2" (Item.id item); 154 Alcotest.(check (option string)) "content_html" None (Item.content_html item); 155 Alcotest.(check (option string)) 156 "content_text" (Some "Hello world") (Item.content_text item) 157 158let test_item_create_both () = 159 let item = 160 Item.create ~id:"https://example.com/3" 161 ~content:(`Both ("<p>Hello</p>", "Hello")) 162 () 163 in 164 Alcotest.(check string) "id" "https://example.com/3" (Item.id item); 165 Alcotest.(check (option string)) 166 "content_html" (Some "<p>Hello</p>") (Item.content_html item); 167 Alcotest.(check (option string)) 168 "content_text" (Some "Hello") (Item.content_text item) 169 170let test_item_with_metadata () = 171 let item = 172 Item.create ~id:"https://example.com/4" ~content:(`Html "<p>Test</p>") 173 ~title:"Test Post" ~url:"https://example.com/posts/4" 174 ~tags:[ "test"; "example" ] () 175 in 176 Alcotest.(check (option string)) "title" (Some "Test Post") (Item.title item); 177 Alcotest.(check (option string)) 178 "url" (Some "https://example.com/posts/4") (Item.url item); 179 Alcotest.(check (option (list string))) 180 "tags" 181 (Some [ "test"; "example" ]) 182 (Item.tags item) 183 184let test_item_equal () = 185 let i1 = Item.create ~id:"https://example.com/1" ~content:(`Text "test") () in 186 let i2 = 187 Item.create ~id:"https://example.com/1" ~content:(`Html "<p>test</p>") () 188 in 189 let i3 = Item.create ~id:"https://example.com/2" ~content:(`Text "test") () in 190 Alcotest.(check bool) "equal same id" true (Item.equal i1 i2); 191 Alcotest.(check bool) "equal different id" false (Item.equal i1 i3) 192 193let item_tests = 194 [ 195 ("create with HTML content", `Quick, test_item_create_html); 196 ("create with text content", `Quick, test_item_create_text); 197 ("create with both contents", `Quick, test_item_create_both); 198 ("create with metadata", `Quick, test_item_with_metadata); 199 ("equal", `Quick, test_item_equal); 200 ] 201 202(* Jsonfeed tests *) 203 204let test_feed_create_minimal () = 205 let feed = Jsonfeed.create ~title:"Test Feed" ~items:[] () in 206 Alcotest.(check string) "title" "Test Feed" (Jsonfeed.title feed); 207 Alcotest.(check string) 208 "version" "https://jsonfeed.org/version/1.1" (Jsonfeed.version feed); 209 Alcotest.(check int) "items length" 0 (List.length (Jsonfeed.items feed)) 210 211let test_feed_create_with_items () = 212 let item = 213 Item.create ~id:"https://example.com/1" ~content:(`Text "Hello") () 214 in 215 let feed = Jsonfeed.create ~title:"Test Feed" ~items:[ item ] () in 216 Alcotest.(check int) "items length" 1 (List.length (Jsonfeed.items feed)) 217 218let test_feed_validate_valid () = 219 let feed = Jsonfeed.create ~title:"Test" ~items:[] () in 220 match Jsonfeed.validate feed with 221 | Ok () -> () 222 | Error errors -> 223 Alcotest.fail 224 (Printf.sprintf "Validation should succeed: %s" 225 (String.concat "; " errors)) 226 227let test_feed_validate_empty_title () = 228 let feed = Jsonfeed.create ~title:"" ~items:[] () in 229 match Jsonfeed.validate feed with 230 | Ok () -> Alcotest.fail "Should fail validation" 231 | Error errors -> 232 Alcotest.(check bool) 233 "has error" true 234 (List.exists (fun s -> String.starts_with ~prefix:"title" s) errors) 235 236let contains_substring s sub = 237 try 238 let _ = Str.search_forward (Str.regexp_string sub) s 0 in 239 true 240 with Not_found -> false 241 242let test_feed_to_string () = 243 let feed = Jsonfeed.create ~title:"Test Feed" ~items:[] () in 244 match Jsonfeed.to_string feed with 245 | Ok json -> 246 Alcotest.(check bool) 247 "contains version" true 248 (contains_substring json "version"); 249 Alcotest.(check bool) 250 "contains title" true 251 (contains_substring json "Test Feed") 252 | Error e -> 253 Alcotest.fail 254 (Printf.sprintf "Serialization failed: %s" (Jsont.Error.to_string e)) 255 256let test_feed_parse_minimal () = 257 let json = 258 {|{ 259 "version": "https://jsonfeed.org/version/1.1", 260 "title": "Test Feed", 261 "items": [] 262 }|} 263 in 264 match Jsonfeed.of_string json with 265 | Ok feed -> 266 Alcotest.(check string) "title" "Test Feed" (Jsonfeed.title feed); 267 Alcotest.(check int) "items" 0 (List.length (Jsonfeed.items feed)) 268 | Error err -> 269 Alcotest.fail 270 (Printf.sprintf "Parse failed: %s" (Jsont.Error.to_string err)) 271 272let test_feed_parse_with_item () = 273 let json = 274 {|{ 275 "version": "https://jsonfeed.org/version/1.1", 276 "title": "Test Feed", 277 "items": [ 278 { 279 "id": "https://example.com/1", 280 "content_html": "<p>Hello</p>" 281 } 282 ] 283 }|} 284 in 285 match Jsonfeed.of_string json with 286 | Ok feed -> ( 287 let items = Jsonfeed.items feed in 288 Alcotest.(check int) "items count" 1 (List.length items); 289 match items with 290 | [ item ] -> 291 Alcotest.(check string) 292 "item id" "https://example.com/1" (Item.id item); 293 Alcotest.(check (option string)) 294 "content_html" (Some "<p>Hello</p>") (Item.content_html item) 295 | _ -> Alcotest.fail "Expected 1 item") 296 | Error err -> 297 Alcotest.fail 298 (Printf.sprintf "Parse failed: %s" (Jsont.Error.to_string err)) 299 300let test_feed_roundtrip () = 301 let author = Author.create ~name:"Test Author" () in 302 let item = 303 Item.create ~id:"https://example.com/1" ~title:"Test Item" 304 ~content:(`Html "<p>Hello, world!</p>") 305 ~date_published: 306 (Jsonfeed.Rfc3339.parse "2024-11-01T10:00:00Z" |> Option.get) 307 ~tags:[ "test"; "example" ] () 308 in 309 310 let feed1 = 311 Jsonfeed.create ~title:"Test Feed" ~home_page_url:"https://example.com" 312 ~authors:[ author ] ~items:[ item ] () 313 in 314 315 (* Serialize and parse *) 316 match Jsonfeed.to_string feed1 with 317 | Error e -> 318 Alcotest.fail 319 (Printf.sprintf "Serialization failed: %s" (Jsont.Error.to_string e)) 320 | Ok json -> ( 321 match Jsonfeed.of_string json with 322 | Ok feed2 -> 323 Alcotest.(check string) 324 "title" (Jsonfeed.title feed1) (Jsonfeed.title feed2); 325 Alcotest.(check (option string)) 326 "home_page_url" 327 (Jsonfeed.home_page_url feed1) 328 (Jsonfeed.home_page_url feed2); 329 Alcotest.(check int) 330 "items count" 331 (List.length (Jsonfeed.items feed1)) 332 (List.length (Jsonfeed.items feed2)) 333 | Error err -> 334 Alcotest.fail 335 (Printf.sprintf "Round-trip parse failed: %s" 336 (Jsont.Error.to_string err))) 337 338let test_feed_parse_invalid_missing_content () = 339 let json = 340 {|{ 341 "version": "https://jsonfeed.org/version/1.1", 342 "title": "Test", 343 "items": [ 344 { 345 "id": "1" 346 } 347 ] 348 }|} 349 in 350 match Jsonfeed.of_string json with 351 | Ok _ -> Alcotest.fail "Should reject item without content" 352 | Error err -> 353 let err_str = Jsont.Error.to_string err in 354 Alcotest.(check bool) 355 "has error" true 356 (contains_substring err_str "content") 357 358let jsonfeed_tests = 359 [ 360 ("create minimal feed", `Quick, test_feed_create_minimal); 361 ("create feed with items", `Quick, test_feed_create_with_items); 362 ("validate valid feed", `Quick, test_feed_validate_valid); 363 ("validate empty title", `Quick, test_feed_validate_empty_title); 364 ("to_string", `Quick, test_feed_to_string); 365 ("parse minimal feed", `Quick, test_feed_parse_minimal); 366 ("parse feed with item", `Quick, test_feed_parse_with_item); 367 ("round-trip", `Quick, test_feed_roundtrip); 368 ( "parse invalid missing content", 369 `Quick, 370 test_feed_parse_invalid_missing_content ); 371 ] 372 373(* Unknown fields preservation tests *) 374 375let test_author_unknown_roundtrip () = 376 let json = 377 {|{ 378 "name": "Test Author", 379 "custom_field": "custom value", 380 "another_extension": 42 381 }|} 382 in 383 match Jsont_bytesrw.decode_string' Author.jsont json with 384 | Error e -> 385 Alcotest.fail 386 (Printf.sprintf "Parse failed: %s" (Jsont.Error.to_string e)) 387 | Ok author -> ( 388 (* Check that unknown fields are preserved *) 389 let unknown = Author.unknown author in 390 Alcotest.(check bool) 391 "has unknown fields" false 392 (Jsonfeed.Unknown.is_empty unknown); 393 (* Encode and decode again *) 394 match Jsont_bytesrw.encode_string' Author.jsont author with 395 | Error e -> 396 Alcotest.fail 397 (Printf.sprintf "Encode failed: %s" (Jsont.Error.to_string e)) 398 | Ok json2 -> ( 399 match Jsont_bytesrw.decode_string' Author.jsont json2 with 400 | Error e -> 401 Alcotest.fail 402 (Printf.sprintf "Re-parse failed: %s" (Jsont.Error.to_string e)) 403 | Ok author2 -> 404 (* Verify unknown fields survive roundtrip *) 405 let unknown2 = Author.unknown author2 in 406 Alcotest.(check bool) 407 "unknown fields preserved" false 408 (Jsonfeed.Unknown.is_empty unknown2))) 409 410let test_item_unknown_roundtrip () = 411 let json = 412 {|{ 413 "id": "https://example.com/1", 414 "content_html": "<p>Test</p>", 415 "custom_metadata": "some custom data", 416 "x_custom_number": 123.45 417 }|} 418 in 419 match Jsont_bytesrw.decode_string' Item.jsont json with 420 | Error e -> 421 Alcotest.fail 422 (Printf.sprintf "Parse failed: %s" (Jsont.Error.to_string e)) 423 | Ok item -> ( 424 (* Check that unknown fields are preserved *) 425 let unknown = Item.unknown item in 426 Alcotest.(check bool) 427 "has unknown fields" false 428 (Jsonfeed.Unknown.is_empty unknown); 429 (* Encode and decode again *) 430 match Jsont_bytesrw.encode_string' Item.jsont item with 431 | Error e -> 432 Alcotest.fail 433 (Printf.sprintf "Encode failed: %s" (Jsont.Error.to_string e)) 434 | Ok json2 -> ( 435 match Jsont_bytesrw.decode_string' Item.jsont json2 with 436 | Error e -> 437 Alcotest.fail 438 (Printf.sprintf "Re-parse failed: %s" (Jsont.Error.to_string e)) 439 | Ok item2 -> 440 let unknown2 = Item.unknown item2 in 441 Alcotest.(check bool) 442 "unknown fields preserved" false 443 (Jsonfeed.Unknown.is_empty unknown2))) 444 445let test_feed_unknown_roundtrip () = 446 let json = 447 {|{ 448 "version": "https://jsonfeed.org/version/1.1", 449 "title": "Test Feed", 450 "items": [], 451 "custom_extension": "custom value", 452 "x_another_field": {"nested": "data"} 453 }|} 454 in 455 match Jsonfeed.of_string json with 456 | Error e -> 457 Alcotest.fail 458 (Printf.sprintf "Parse failed: %s" (Jsont.Error.to_string e)) 459 | Ok feed -> ( 460 (* Check that unknown fields are preserved *) 461 let unknown = Jsonfeed.unknown feed in 462 Alcotest.(check bool) 463 "has unknown fields" false 464 (Jsonfeed.Unknown.is_empty unknown); 465 (* Encode and decode again *) 466 match Jsonfeed.to_string feed with 467 | Error e -> 468 Alcotest.fail 469 (Printf.sprintf "Encode failed: %s" (Jsont.Error.to_string e)) 470 | Ok json2 -> ( 471 match Jsonfeed.of_string json2 with 472 | Error e -> 473 Alcotest.fail 474 (Printf.sprintf "Re-parse failed: %s" (Jsont.Error.to_string e)) 475 | Ok feed2 -> 476 let unknown2 = Jsonfeed.unknown feed2 in 477 Alcotest.(check bool) 478 "unknown fields preserved" false 479 (Jsonfeed.Unknown.is_empty unknown2))) 480 481let test_hub_unknown_roundtrip () = 482 let json = 483 {|{ 484 "type": "WebSub", 485 "url": "https://example.com/hub", 486 "custom_field": "test" 487 }|} 488 in 489 match Jsont_bytesrw.decode_string' Hub.jsont json with 490 | Error e -> 491 Alcotest.fail 492 (Printf.sprintf "Parse failed: %s" (Jsont.Error.to_string e)) 493 | Ok hub -> ( 494 let unknown = Hub.unknown hub in 495 Alcotest.(check bool) 496 "has unknown fields" false 497 (Jsonfeed.Unknown.is_empty unknown); 498 match Jsont_bytesrw.encode_string' Hub.jsont hub with 499 | Error e -> 500 Alcotest.fail 501 (Printf.sprintf "Encode failed: %s" (Jsont.Error.to_string e)) 502 | Ok json2 -> ( 503 match Jsont_bytesrw.decode_string' Hub.jsont json2 with 504 | Error e -> 505 Alcotest.fail 506 (Printf.sprintf "Re-parse failed: %s" (Jsont.Error.to_string e)) 507 | Ok hub2 -> 508 let unknown2 = Hub.unknown hub2 in 509 Alcotest.(check bool) 510 "unknown fields preserved" false 511 (Jsonfeed.Unknown.is_empty unknown2))) 512 513let test_attachment_unknown_roundtrip () = 514 let json = 515 {|{ 516 "url": "https://example.com/file.mp3", 517 "mime_type": "audio/mpeg", 518 "x_custom": "value" 519 }|} 520 in 521 match Jsont_bytesrw.decode_string' Attachment.jsont json with 522 | Error e -> 523 Alcotest.fail 524 (Printf.sprintf "Parse failed: %s" (Jsont.Error.to_string e)) 525 | Ok att -> ( 526 let unknown = Attachment.unknown att in 527 Alcotest.(check bool) 528 "has unknown fields" false 529 (Jsonfeed.Unknown.is_empty unknown); 530 match Jsont_bytesrw.encode_string' Attachment.jsont att with 531 | Error e -> 532 Alcotest.fail 533 (Printf.sprintf "Encode failed: %s" (Jsont.Error.to_string e)) 534 | Ok json2 -> ( 535 match Jsont_bytesrw.decode_string' Attachment.jsont json2 with 536 | Error e -> 537 Alcotest.fail 538 (Printf.sprintf "Re-parse failed: %s" (Jsont.Error.to_string e)) 539 | Ok att2 -> 540 let unknown2 = Attachment.unknown att2 in 541 Alcotest.(check bool) 542 "unknown fields preserved" false 543 (Jsonfeed.Unknown.is_empty unknown2))) 544 545let unknown_fields_tests = 546 [ 547 ("author unknown roundtrip", `Quick, test_author_unknown_roundtrip); 548 ("item unknown roundtrip", `Quick, test_item_unknown_roundtrip); 549 ("feed unknown roundtrip", `Quick, test_feed_unknown_roundtrip); 550 ("hub unknown roundtrip", `Quick, test_hub_unknown_roundtrip); 551 ("attachment unknown roundtrip", `Quick, test_attachment_unknown_roundtrip); 552 ] 553 554(* Main test suite *) 555 556let () = 557 Alcotest.run "jsonfeed" 558 [ 559 ("Author", author_tests); 560 ("Attachment", attachment_tests); 561 ("Hub", hub_tests); 562 ("Item", item_tests); 563 ("Jsonfeed", jsonfeed_tests); 564 ("Unknown Fields", unknown_fields_tests); 565 ]