@@ -309,4 +309,184 @@ describe("bookmarks service", () => {
309309 expect ( result . tags ) . toEqual ( [ "c++" , "c#" , "node.js" ] ) ;
310310 } ) ;
311311 } ) ;
312+
313+ describe ( "inbox items" , ( ) => {
314+ it ( "allows empty title when inbox is true" , ( ) => {
315+ const result = validateBookmark ( {
316+ url : "https://example.com" ,
317+ title : "" ,
318+ inbox : true ,
319+ } ) ;
320+
321+ expect ( result . inbox ) . toBe ( true ) ;
322+ expect ( result . title ) . toBe ( "" ) ;
323+ } ) ;
324+
325+ it ( "allows missing title when inbox is true" , ( ) => {
326+ const result = validateBookmark ( {
327+ url : "https://example.com" ,
328+ inbox : true ,
329+ } ) ;
330+
331+ expect ( result . inbox ) . toBe ( true ) ;
332+ expect ( result . title ) . toBe ( "" ) ;
333+ } ) ;
334+
335+ it ( "still requires title when inbox is false" , ( ) => {
336+ expect ( ( ) =>
337+ validateBookmark ( {
338+ url : "https://example.com" ,
339+ inbox : false ,
340+ } )
341+ ) . toThrow ( "Title is required" ) ;
342+ } ) ;
343+
344+ it ( "still requires title when inbox is not set" , ( ) => {
345+ expect ( ( ) =>
346+ validateBookmark ( {
347+ url : "https://example.com" ,
348+ } )
349+ ) . toThrow ( "Title is required" ) ;
350+ } ) ;
351+
352+ it ( "converts inbox to boolean" , ( ) => {
353+ const result1 = validateBookmark ( {
354+ url : "https://example.com" ,
355+ title : "Test" ,
356+ inbox : 1 ,
357+ } ) ;
358+ const result2 = validateBookmark ( {
359+ url : "https://example.com" ,
360+ title : "Test" ,
361+ inbox : 0 ,
362+ } ) ;
363+ const result3 = validateBookmark ( {
364+ url : "https://example.com" ,
365+ title : "Test" ,
366+ inbox : "yes" ,
367+ } ) ;
368+
369+ expect ( result1 . inbox ) . toBe ( true ) ;
370+ expect ( result2 . inbox ) . toBe ( false ) ;
371+ expect ( result3 . inbox ) . toBe ( true ) ;
372+ } ) ;
373+
374+ it ( "still validates URL for inbox items" , ( ) => {
375+ expect ( ( ) =>
376+ validateBookmark ( {
377+ url : "not a valid url" ,
378+ inbox : true ,
379+ } )
380+ ) . toThrow ( "Invalid URL format" ) ;
381+ } ) ;
382+ } ) ;
383+
384+ describe ( "favicon and preview fields" , ( ) => {
385+ it ( "passes through favicon value" , ( ) => {
386+ const result = validateBookmark ( {
387+ url : "https://example.com" ,
388+ title : "Test" ,
389+ favicon : "https://example.com/favicon.ico" ,
390+ } ) ;
391+
392+ expect ( result . favicon ) . toBe ( "https://example.com/favicon.ico" ) ;
393+ } ) ;
394+
395+ it ( "defaults favicon to null" , ( ) => {
396+ const result = validateBookmark ( {
397+ url : "https://example.com" ,
398+ title : "Test" ,
399+ } ) ;
400+
401+ expect ( result . favicon ) . toBeNull ( ) ;
402+ } ) ;
403+
404+ it ( "passes through preview value" , ( ) => {
405+ const result = validateBookmark ( {
406+ url : "https://example.com" ,
407+ title : "Test" ,
408+ preview : "https://example.com/preview.png" ,
409+ } ) ;
410+
411+ expect ( result . preview ) . toBe ( "https://example.com/preview.png" ) ;
412+ } ) ;
413+
414+ it ( "defaults preview to null" , ( ) => {
415+ const result = validateBookmark ( {
416+ url : "https://example.com" ,
417+ title : "Test" ,
418+ } ) ;
419+
420+ expect ( result . preview ) . toBeNull ( ) ;
421+ } ) ;
422+ } ) ;
423+
424+ describe ( "tag edge cases" , ( ) => {
425+ it ( "handles non-string values in tags array" , ( ) => {
426+ const result = validateBookmark ( {
427+ url : "https://example.com" ,
428+ title : "Test" ,
429+ tags : [ 123 , null , undefined , "valid" ] ,
430+ } ) ;
431+
432+ // Non-string tags should be converted to empty string and filtered
433+ expect ( result . tags ) . toContain ( "valid" ) ;
434+ expect ( result . tags . length ) . toBe ( 1 ) ;
435+ } ) ;
436+
437+ it ( "handles non-array tags gracefully" , ( ) => {
438+ const result = validateBookmark ( {
439+ url : "https://example.com" ,
440+ title : "Test" ,
441+ tags : "not-an-array" ,
442+ } ) ;
443+
444+ expect ( result . tags ) . toEqual ( [ ] ) ;
445+ } ) ;
446+
447+ it ( "trims and lowercases each tag" , ( ) => {
448+ const result = validateBookmark ( {
449+ url : "https://example.com" ,
450+ title : "Test" ,
451+ tags : [ " JavaScript " , "REACT" , " node.JS " ] ,
452+ } ) ;
453+
454+ expect ( result . tags ) . toEqual ( [ "javascript" , "react" , "node.js" ] ) ;
455+ } ) ;
456+
457+ it ( "deduplication is not enforced at validation level" , ( ) => {
458+ const result = validateBookmark ( {
459+ url : "https://example.com" ,
460+ title : "Test" ,
461+ tags : [ "dup" , "dup" , "dup" ] ,
462+ } ) ;
463+
464+ // validateBookmark does not deduplicate - it normalizes
465+ expect ( result . tags ) . toEqual ( [ "dup" , "dup" , "dup" ] ) ;
466+ } ) ;
467+ } ) ;
468+
469+ describe ( "normalizeUrl edge cases" , ( ) => {
470+ it ( "handles URLs with encoded characters" , ( ) => {
471+ const result = normalizeUrl ( "https://example.com/path%20with%20spaces" ) ;
472+ expect ( result ) . toContain ( "path%20with%20spaces" ) ;
473+ } ) ;
474+
475+ it ( "handles URLs with multiple query parameters of same key" , ( ) => {
476+ const result = normalizeUrl ( "https://example.com?a=1&a=2" ) ;
477+ expect ( result ) . toContain ( "a=1&a=2" ) ;
478+ } ) ;
479+
480+ it ( "handles internationalized domain names" , ( ) => {
481+ // punycode-encoded domains should work
482+ const result = normalizeUrl ( "https://xn--n3h.example.com" ) ;
483+ expect ( result ) . toContain ( "xn--n3h.example.com" ) ;
484+ } ) ;
485+
486+ it ( "handles very long paths" , ( ) => {
487+ const longPath = "/a" . repeat ( 500 ) ;
488+ const result = normalizeUrl ( `https://example.com${ longPath } ` ) ;
489+ expect ( result ) . toContain ( longPath ) ;
490+ } ) ;
491+ } ) ;
312492} ) ;
0 commit comments