@tldraw/sync-collaboration 0.0.0-bootstrap โ 5.3.0-canary.04044ed9e96d
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-cjs/comment-authorizers.js +37 -20
- package/dist-cjs/comment-authorizers.js.map +2 -2
- package/dist-cjs/index.d.ts +109 -18
- package/dist-cjs/index.js +1 -1
- package/dist-cjs/index.js.map +2 -2
- package/dist-esm/comment-authorizers.mjs +37 -20
- package/dist-esm/comment-authorizers.mjs.map +2 -2
- package/dist-esm/index.d.mts +109 -18
- package/dist-esm/index.mjs +4 -2
- package/dist-esm/index.mjs.map +2 -2
- package/package.json +5 -5
- package/src/comment-authorizers.test.ts +519 -2
- package/src/comment-authorizers.ts +166 -63
- package/src/index.ts +6 -1
|
@@ -28,8 +28,15 @@ const thread = createCommentThread({
|
|
|
28
28
|
createdBy: 'client-claims-alice',
|
|
29
29
|
})
|
|
30
30
|
|
|
31
|
-
function session(
|
|
32
|
-
|
|
31
|
+
function session(
|
|
32
|
+
userId: string | null,
|
|
33
|
+
isReadonly = false
|
|
34
|
+
): {
|
|
35
|
+
sessionId: string
|
|
36
|
+
isReadonly: boolean
|
|
37
|
+
meta: TestMeta
|
|
38
|
+
} {
|
|
39
|
+
return { sessionId: 's1', isReadonly, meta: { userId } }
|
|
33
40
|
}
|
|
34
41
|
|
|
35
42
|
describe('createCommentAuthorizers', () => {
|
|
@@ -405,4 +412,514 @@ describe('createCommentAuthorizers', () => {
|
|
|
405
412
|
).toBeNull()
|
|
406
413
|
})
|
|
407
414
|
})
|
|
415
|
+
|
|
416
|
+
describe('canComment', () => {
|
|
417
|
+
const comment = (authorId: string) =>
|
|
418
|
+
createComment({ threadId: thread.id, pageId, authorId, body: toRichText('hi') })
|
|
419
|
+
const makeThread = (createdBy: string) =>
|
|
420
|
+
createCommentThread({ pageId, anchor: { type: 'page' }, createdBy })
|
|
421
|
+
const makeReaction = (userId: string, emoji = '๐') =>
|
|
422
|
+
createCommentReaction({
|
|
423
|
+
commentId: createCommentId('c1'),
|
|
424
|
+
threadId: thread.id,
|
|
425
|
+
pageId,
|
|
426
|
+
userId,
|
|
427
|
+
emoji,
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
it('blocks all comment-record writes from canvas read-only sessions by default', () => {
|
|
431
|
+
const readonly = session('real-bob', true)
|
|
432
|
+
|
|
433
|
+
const commentPrev = comment('real-bob')
|
|
434
|
+
expect(
|
|
435
|
+
authorizers.comment!({
|
|
436
|
+
session: readonly,
|
|
437
|
+
type: 'create',
|
|
438
|
+
prev: null,
|
|
439
|
+
next: comment('real-bob'),
|
|
440
|
+
})
|
|
441
|
+
).toBeNull()
|
|
442
|
+
expect(
|
|
443
|
+
authorizers.comment!({
|
|
444
|
+
session: readonly,
|
|
445
|
+
type: 'update',
|
|
446
|
+
prev: commentPrev,
|
|
447
|
+
next: { ...commentPrev, body: toRichText('edited') },
|
|
448
|
+
})
|
|
449
|
+
).toBeNull()
|
|
450
|
+
expect(
|
|
451
|
+
authorizers.comment!({ session: readonly, type: 'delete', prev: commentPrev, next: null })
|
|
452
|
+
).toBeNull()
|
|
453
|
+
|
|
454
|
+
const threadPrev = makeThread('real-bob')
|
|
455
|
+
expect(
|
|
456
|
+
authorizers['comment-thread']!({
|
|
457
|
+
session: readonly,
|
|
458
|
+
type: 'create',
|
|
459
|
+
prev: null,
|
|
460
|
+
next: makeThread('real-bob'),
|
|
461
|
+
})
|
|
462
|
+
).toBeNull()
|
|
463
|
+
expect(
|
|
464
|
+
authorizers['comment-thread']!({
|
|
465
|
+
session: readonly,
|
|
466
|
+
type: 'update',
|
|
467
|
+
prev: threadPrev,
|
|
468
|
+
next: { ...threadPrev, resolved: { at: 1, by: 'real-bob' } },
|
|
469
|
+
})
|
|
470
|
+
).toBeNull()
|
|
471
|
+
expect(
|
|
472
|
+
authorizers['comment-thread']!({
|
|
473
|
+
session: readonly,
|
|
474
|
+
type: 'delete',
|
|
475
|
+
prev: threadPrev,
|
|
476
|
+
next: null,
|
|
477
|
+
})
|
|
478
|
+
).toBeNull()
|
|
479
|
+
|
|
480
|
+
const reactionPrev = makeReaction('real-bob')
|
|
481
|
+
expect(
|
|
482
|
+
authorizers['comment-reaction']!({
|
|
483
|
+
session: readonly,
|
|
484
|
+
type: 'create',
|
|
485
|
+
prev: null,
|
|
486
|
+
next: makeReaction('real-bob'),
|
|
487
|
+
})
|
|
488
|
+
).toBeNull()
|
|
489
|
+
expect(
|
|
490
|
+
authorizers['comment-reaction']!({
|
|
491
|
+
session: readonly,
|
|
492
|
+
type: 'update',
|
|
493
|
+
prev: reactionPrev,
|
|
494
|
+
next: { ...reactionPrev, createdAt: reactionPrev.createdAt + 1 },
|
|
495
|
+
})
|
|
496
|
+
).toBeNull()
|
|
497
|
+
expect(
|
|
498
|
+
authorizers['comment-reaction']!({
|
|
499
|
+
session: readonly,
|
|
500
|
+
type: 'delete',
|
|
501
|
+
prev: reactionPrev,
|
|
502
|
+
next: null,
|
|
503
|
+
})
|
|
504
|
+
).toBeNull()
|
|
505
|
+
})
|
|
506
|
+
|
|
507
|
+
it('still applies the per-type rules to read-write sessions', () => {
|
|
508
|
+
const result = authorizers.comment!({
|
|
509
|
+
session: session('real-bob', false),
|
|
510
|
+
type: 'create',
|
|
511
|
+
prev: null,
|
|
512
|
+
next: comment('client-claims-alice'),
|
|
513
|
+
}) as TLComment
|
|
514
|
+
expect(result.authorId).toBe('real-bob')
|
|
515
|
+
})
|
|
516
|
+
|
|
517
|
+
it('lets a custom canComment allow read-only sessions (comment-only setups)', () => {
|
|
518
|
+
const commentOnly = createCommentAuthorizers<TestMeta>({
|
|
519
|
+
getUserId: (session) => session.meta.userId,
|
|
520
|
+
canComment: () => true,
|
|
521
|
+
})
|
|
522
|
+
|
|
523
|
+
const result = commentOnly.comment!({
|
|
524
|
+
session: session('real-bob', true),
|
|
525
|
+
type: 'create',
|
|
526
|
+
prev: null,
|
|
527
|
+
next: comment('client-claims-alice'),
|
|
528
|
+
}) as TLComment
|
|
529
|
+
expect(result.authorId).toBe('real-bob')
|
|
530
|
+
|
|
531
|
+
expect(
|
|
532
|
+
commentOnly.comment!({
|
|
533
|
+
session: session(null, true),
|
|
534
|
+
type: 'create',
|
|
535
|
+
prev: null,
|
|
536
|
+
next: comment('anon'),
|
|
537
|
+
})
|
|
538
|
+
).toBeNull()
|
|
539
|
+
})
|
|
540
|
+
|
|
541
|
+
it('is asked before canModifyComment, so a blocked session is never asked about a record', () => {
|
|
542
|
+
let asked = 0
|
|
543
|
+
const blocked = createCommentAuthorizers<TestMeta>({
|
|
544
|
+
getUserId: (session) => session.meta.userId,
|
|
545
|
+
canComment: () => false,
|
|
546
|
+
canModifyComment: () => {
|
|
547
|
+
asked++
|
|
548
|
+
return true
|
|
549
|
+
},
|
|
550
|
+
})
|
|
551
|
+
const prev = comment('real-bob')
|
|
552
|
+
expect(
|
|
553
|
+
blocked.comment!({
|
|
554
|
+
session: session('real-bob'),
|
|
555
|
+
type: 'update',
|
|
556
|
+
prev,
|
|
557
|
+
next: { ...prev, isDeleted: true },
|
|
558
|
+
})
|
|
559
|
+
).toBeNull()
|
|
560
|
+
expect(asked).toBe(0)
|
|
561
|
+
})
|
|
562
|
+
|
|
563
|
+
it('lets a custom canComment block sessions on its own criteria', () => {
|
|
564
|
+
const bannable = createCommentAuthorizers<TestMeta>({
|
|
565
|
+
getUserId: (session) => session.meta.userId,
|
|
566
|
+
canComment: ({ meta }) => meta.userId !== 'banned',
|
|
567
|
+
})
|
|
568
|
+
|
|
569
|
+
expect(
|
|
570
|
+
bannable.comment!({
|
|
571
|
+
session: session('banned', false),
|
|
572
|
+
type: 'create',
|
|
573
|
+
prev: null,
|
|
574
|
+
next: comment('banned'),
|
|
575
|
+
})
|
|
576
|
+
).toBeNull()
|
|
577
|
+
|
|
578
|
+
const result = bannable.comment!({
|
|
579
|
+
session: session('real-bob', false),
|
|
580
|
+
type: 'create',
|
|
581
|
+
prev: null,
|
|
582
|
+
next: comment('client-claims-alice'),
|
|
583
|
+
}) as TLComment
|
|
584
|
+
expect(result.authorId).toBe('real-bob')
|
|
585
|
+
})
|
|
586
|
+
})
|
|
587
|
+
|
|
588
|
+
// The default is the owner-only rule the per-type suites above already cover in full. These
|
|
589
|
+
// exercise the option itself: what a callback can widen, and what stays fixed underneath it.
|
|
590
|
+
describe('canModifyComment', () => {
|
|
591
|
+
const comment = (authorId: string) =>
|
|
592
|
+
createComment({ threadId: thread.id, pageId, authorId, body: toRichText('hi') })
|
|
593
|
+
const makeThread = (createdBy: string) =>
|
|
594
|
+
createCommentThread({ pageId, anchor: { type: 'page' }, createdBy })
|
|
595
|
+
|
|
596
|
+
// The motivating case: a moderator takes down someone else's comment, but takes over
|
|
597
|
+
// nobody's voice โ editing stays the author's.
|
|
598
|
+
const moderated = createCommentAuthorizers<TestMeta>({
|
|
599
|
+
getUserId: (session) => session.meta.userId,
|
|
600
|
+
canModifyComment: (ctx) =>
|
|
601
|
+
(ctx.action !== 'edit-comment' && ctx.session.meta.userId === 'real-mod') ||
|
|
602
|
+
ctx.userId === ctx.ownerId,
|
|
603
|
+
})
|
|
604
|
+
|
|
605
|
+
it('lets a widened rule soft-delete another userโs comment', () => {
|
|
606
|
+
const prev = comment('real-bob')
|
|
607
|
+
const next = { ...prev, isDeleted: true }
|
|
608
|
+
expect(moderated.comment!({ session: session('real-mod'), type: 'update', prev, next })).toBe(
|
|
609
|
+
next
|
|
610
|
+
)
|
|
611
|
+
})
|
|
612
|
+
|
|
613
|
+
it('lets a widened rule soft-delete another userโs thread', () => {
|
|
614
|
+
const prev = makeThread('real-bob')
|
|
615
|
+
const next = { ...prev, isDeleted: true }
|
|
616
|
+
expect(
|
|
617
|
+
moderated['comment-thread']!({ session: session('real-mod'), type: 'update', prev, next })
|
|
618
|
+
).toBe(next)
|
|
619
|
+
})
|
|
620
|
+
|
|
621
|
+
it('keeps the edit and the delete separate: widening deletes grants no edits', () => {
|
|
622
|
+
const prev = comment('real-bob')
|
|
623
|
+
const next = { ...prev, body: toRichText('rewritten by the mod') }
|
|
624
|
+
expect(
|
|
625
|
+
moderated.comment!({ session: session('real-mod'), type: 'update', prev, next })
|
|
626
|
+
).toBeNull()
|
|
627
|
+
})
|
|
628
|
+
|
|
629
|
+
// The gap a delete-only permission would otherwise leave: flip the flag, rewrite the body,
|
|
630
|
+
// and the edit rides in on a write the delete gate allowed.
|
|
631
|
+
it('vetoes a delete that carries an edit past a delete-only permission', () => {
|
|
632
|
+
const prev = comment('real-bob')
|
|
633
|
+
const next = { ...prev, isDeleted: true, body: toRichText('rewritten on the way out') }
|
|
634
|
+
expect(
|
|
635
|
+
moderated.comment!({ session: session('real-mod'), type: 'update', prev, next })
|
|
636
|
+
).toBeNull()
|
|
637
|
+
})
|
|
638
|
+
|
|
639
|
+
it('still lets the owner delete and edit in one write', () => {
|
|
640
|
+
const prev = comment('real-bob')
|
|
641
|
+
const next = { ...prev, isDeleted: true, body: toRichText('last word') }
|
|
642
|
+
expect(moderated.comment!({ session: session('real-bob'), type: 'update', prev, next })).toBe(
|
|
643
|
+
next
|
|
644
|
+
)
|
|
645
|
+
})
|
|
646
|
+
|
|
647
|
+
it('leaves everyone else where the default left them', () => {
|
|
648
|
+
const prev = comment('real-bob')
|
|
649
|
+
expect(
|
|
650
|
+
moderated.comment!({
|
|
651
|
+
session: session('real-mallory'),
|
|
652
|
+
type: 'update',
|
|
653
|
+
prev,
|
|
654
|
+
next: { ...prev, isDeleted: true },
|
|
655
|
+
})
|
|
656
|
+
).toBeNull()
|
|
657
|
+
const threadPrev = makeThread('real-bob')
|
|
658
|
+
expect(
|
|
659
|
+
moderated['comment-thread']!({
|
|
660
|
+
session: session('real-mallory'),
|
|
661
|
+
type: 'update',
|
|
662
|
+
prev: threadPrev,
|
|
663
|
+
next: { ...threadPrev, isDeleted: true },
|
|
664
|
+
})
|
|
665
|
+
).toBeNull()
|
|
666
|
+
})
|
|
667
|
+
|
|
668
|
+
it('narrows as well as widens', () => {
|
|
669
|
+
const frozen = createCommentAuthorizers<TestMeta>({
|
|
670
|
+
getUserId: (session) => session.meta.userId,
|
|
671
|
+
canModifyComment: (ctx) => ctx.action !== 'edit-comment' && ctx.userId === ctx.ownerId,
|
|
672
|
+
})
|
|
673
|
+
const prev = comment('real-bob')
|
|
674
|
+
expect(
|
|
675
|
+
frozen.comment!({
|
|
676
|
+
session: session('real-bob'),
|
|
677
|
+
type: 'update',
|
|
678
|
+
prev,
|
|
679
|
+
next: { ...prev, body: toRichText('edited') },
|
|
680
|
+
})
|
|
681
|
+
).toBeNull()
|
|
682
|
+
// ...and the delete it didn't narrow still goes through
|
|
683
|
+
const deleted = { ...prev, isDeleted: true }
|
|
684
|
+
expect(
|
|
685
|
+
frozen.comment!({ session: session('real-bob'), type: 'update', prev, next: deleted })
|
|
686
|
+
).toBe(deleted)
|
|
687
|
+
})
|
|
688
|
+
|
|
689
|
+
it('is asked with the stored record, not the clientโs version of it', () => {
|
|
690
|
+
const seen: unknown[] = []
|
|
691
|
+
const spying = createCommentAuthorizers<TestMeta>({
|
|
692
|
+
getUserId: (session) => session.meta.userId,
|
|
693
|
+
canModifyComment: (ctx) => {
|
|
694
|
+
seen.push(ctx)
|
|
695
|
+
return true
|
|
696
|
+
},
|
|
697
|
+
})
|
|
698
|
+
const prev = comment('real-bob')
|
|
699
|
+
// mallory's push claims the comment is hers and already deleted; the context must describe
|
|
700
|
+
// the record the room holds, or an ownership rule would be reading the attacker's claim
|
|
701
|
+
const next = { ...prev, authorId: 'real-mallory', body: toRichText('edited') }
|
|
702
|
+
spying.comment!({ session: session('real-mallory'), type: 'update', prev, next })
|
|
703
|
+
// the attribution guard rejects this one before the option is asked at all
|
|
704
|
+
expect(seen).toEqual([])
|
|
705
|
+
|
|
706
|
+
spying.comment!({
|
|
707
|
+
session: session('real-mallory'),
|
|
708
|
+
type: 'update',
|
|
709
|
+
prev,
|
|
710
|
+
next: { ...prev, body: toRichText('edited') },
|
|
711
|
+
})
|
|
712
|
+
expect(seen).toEqual([
|
|
713
|
+
{
|
|
714
|
+
session: session('real-mallory'),
|
|
715
|
+
userId: 'real-mallory',
|
|
716
|
+
ownerId: 'real-bob',
|
|
717
|
+
action: 'edit-comment',
|
|
718
|
+
comment: prev,
|
|
719
|
+
},
|
|
720
|
+
])
|
|
721
|
+
})
|
|
722
|
+
|
|
723
|
+
it('reports the threadโs creator as the owner of a thread delete', () => {
|
|
724
|
+
const seen: unknown[] = []
|
|
725
|
+
const spying = createCommentAuthorizers<TestMeta>({
|
|
726
|
+
getUserId: (session) => session.meta.userId,
|
|
727
|
+
canModifyComment: (ctx) => {
|
|
728
|
+
seen.push(ctx)
|
|
729
|
+
return true
|
|
730
|
+
},
|
|
731
|
+
})
|
|
732
|
+
const prev = makeThread('real-bob')
|
|
733
|
+
spying['comment-thread']!({
|
|
734
|
+
session: session('real-mallory'),
|
|
735
|
+
type: 'update',
|
|
736
|
+
prev,
|
|
737
|
+
next: { ...prev, isDeleted: true },
|
|
738
|
+
})
|
|
739
|
+
expect(seen).toEqual([
|
|
740
|
+
{
|
|
741
|
+
session: session('real-mallory'),
|
|
742
|
+
userId: 'real-mallory',
|
|
743
|
+
ownerId: 'real-bob',
|
|
744
|
+
action: 'delete-thread',
|
|
745
|
+
thread: prev,
|
|
746
|
+
},
|
|
747
|
+
])
|
|
748
|
+
})
|
|
749
|
+
|
|
750
|
+
// Resolving and reopening aren't anyone's in particular, matching the client option: they're
|
|
751
|
+
// `canComment`'s to gate, and the option is never asked about them.
|
|
752
|
+
it('is not asked about resolving or reopening a thread', () => {
|
|
753
|
+
let asked = 0
|
|
754
|
+
const counting = createCommentAuthorizers<TestMeta>({
|
|
755
|
+
getUserId: (session) => session.meta.userId,
|
|
756
|
+
canModifyComment: () => {
|
|
757
|
+
asked++
|
|
758
|
+
return true
|
|
759
|
+
},
|
|
760
|
+
})
|
|
761
|
+
const prev = makeThread('real-bob')
|
|
762
|
+
const resolved = { ...prev, resolved: { at: 1, by: 'real-mallory' } }
|
|
763
|
+
expect(
|
|
764
|
+
counting['comment-thread']!({
|
|
765
|
+
session: session('real-mallory'),
|
|
766
|
+
type: 'update',
|
|
767
|
+
prev,
|
|
768
|
+
next: resolved,
|
|
769
|
+
})
|
|
770
|
+
).toBe(resolved)
|
|
771
|
+
const reopened = { ...resolved, resolved: null }
|
|
772
|
+
expect(
|
|
773
|
+
counting['comment-thread']!({
|
|
774
|
+
session: session('real-mallory'),
|
|
775
|
+
type: 'update',
|
|
776
|
+
prev: resolved,
|
|
777
|
+
next: reopened,
|
|
778
|
+
})
|
|
779
|
+
).toBe(reopened)
|
|
780
|
+
expect(asked).toBe(0)
|
|
781
|
+
})
|
|
782
|
+
|
|
783
|
+
it('is not asked about reactions', () => {
|
|
784
|
+
let asked = 0
|
|
785
|
+
const counting = createCommentAuthorizers<TestMeta>({
|
|
786
|
+
getUserId: (session) => session.meta.userId,
|
|
787
|
+
canModifyComment: () => {
|
|
788
|
+
asked++
|
|
789
|
+
return true
|
|
790
|
+
},
|
|
791
|
+
})
|
|
792
|
+
const prev = createCommentReaction({
|
|
793
|
+
commentId: createCommentId('c1'),
|
|
794
|
+
threadId: thread.id,
|
|
795
|
+
pageId,
|
|
796
|
+
userId: 'real-bob',
|
|
797
|
+
emoji: '๐',
|
|
798
|
+
})
|
|
799
|
+
// a permissive callback doesn't hand mallory someone else's reaction
|
|
800
|
+
expect(
|
|
801
|
+
counting['comment-reaction']!({
|
|
802
|
+
session: session('real-mallory'),
|
|
803
|
+
type: 'delete',
|
|
804
|
+
prev,
|
|
805
|
+
next: null,
|
|
806
|
+
})
|
|
807
|
+
).toBeNull()
|
|
808
|
+
expect(
|
|
809
|
+
counting['comment-reaction']!({
|
|
810
|
+
session: session('real-bob'),
|
|
811
|
+
type: 'update',
|
|
812
|
+
prev,
|
|
813
|
+
next: { ...prev, createdAt: prev.createdAt + 1 },
|
|
814
|
+
})
|
|
815
|
+
).not.toBeNull()
|
|
816
|
+
expect(asked).toBe(0)
|
|
817
|
+
})
|
|
818
|
+
|
|
819
|
+
// The point of asking after the structural rules: a host can hand out these three writes
|
|
820
|
+
// without handing out the invariants underneath them.
|
|
821
|
+
describe('the structural rules hold however permissive the callback', () => {
|
|
822
|
+
const permissive = createCommentAuthorizers<TestMeta>({
|
|
823
|
+
getUserId: (session) => session.meta.userId,
|
|
824
|
+
canModifyComment: () => true,
|
|
825
|
+
})
|
|
826
|
+
|
|
827
|
+
it('still vetoes clearing a soft-delete', () => {
|
|
828
|
+
const prev = { ...comment('real-bob'), isDeleted: true }
|
|
829
|
+
expect(
|
|
830
|
+
permissive.comment!({
|
|
831
|
+
session: session('real-mod'),
|
|
832
|
+
type: 'update',
|
|
833
|
+
prev,
|
|
834
|
+
next: { ...prev, isDeleted: false },
|
|
835
|
+
})
|
|
836
|
+
).toBeNull()
|
|
837
|
+
})
|
|
838
|
+
|
|
839
|
+
it('still vetoes every client hard-delete', () => {
|
|
840
|
+
const prev = comment('real-bob')
|
|
841
|
+
expect(
|
|
842
|
+
permissive.comment!({ session: session('real-mod'), type: 'delete', prev, next: null })
|
|
843
|
+
).toBeNull()
|
|
844
|
+
const threadPrev = makeThread('real-bob')
|
|
845
|
+
expect(
|
|
846
|
+
permissive['comment-thread']!({
|
|
847
|
+
session: session('real-mod'),
|
|
848
|
+
type: 'delete',
|
|
849
|
+
prev: threadPrev,
|
|
850
|
+
next: null,
|
|
851
|
+
})
|
|
852
|
+
).toBeNull()
|
|
853
|
+
})
|
|
854
|
+
|
|
855
|
+
it('still vetoes changing a commentโs author', () => {
|
|
856
|
+
const prev = comment('real-bob')
|
|
857
|
+
expect(
|
|
858
|
+
permissive.comment!({
|
|
859
|
+
session: session('real-mod'),
|
|
860
|
+
type: 'update',
|
|
861
|
+
prev,
|
|
862
|
+
next: { ...prev, authorId: 'real-mod' },
|
|
863
|
+
})
|
|
864
|
+
).toBeNull()
|
|
865
|
+
})
|
|
866
|
+
|
|
867
|
+
it('still vetoes re-parenting and back-dating a comment', () => {
|
|
868
|
+
const prev = comment('real-bob')
|
|
869
|
+
expect(
|
|
870
|
+
permissive.comment!({
|
|
871
|
+
session: session('real-mod'),
|
|
872
|
+
type: 'update',
|
|
873
|
+
prev,
|
|
874
|
+
next: { ...prev, threadId: 'comment-thread:other' as TLCommentThread['id'] },
|
|
875
|
+
})
|
|
876
|
+
).toBeNull()
|
|
877
|
+
expect(
|
|
878
|
+
permissive.comment!({
|
|
879
|
+
session: session('real-mod'),
|
|
880
|
+
type: 'update',
|
|
881
|
+
prev,
|
|
882
|
+
next: { ...prev, createdAt: 1 },
|
|
883
|
+
})
|
|
884
|
+
).toBeNull()
|
|
885
|
+
})
|
|
886
|
+
|
|
887
|
+
it('still vetoes a create with the soft-delete flag already set', () => {
|
|
888
|
+
expect(
|
|
889
|
+
permissive.comment!({
|
|
890
|
+
session: session('real-mod'),
|
|
891
|
+
type: 'create',
|
|
892
|
+
prev: null,
|
|
893
|
+
next: { ...comment('real-mod'), isDeleted: true },
|
|
894
|
+
})
|
|
895
|
+
).toBeNull()
|
|
896
|
+
})
|
|
897
|
+
|
|
898
|
+
it('still vetoes a resolution attributed to someone else', () => {
|
|
899
|
+
const prev = makeThread('real-bob')
|
|
900
|
+
expect(
|
|
901
|
+
permissive['comment-thread']!({
|
|
902
|
+
session: session('real-mod'),
|
|
903
|
+
type: 'update',
|
|
904
|
+
prev,
|
|
905
|
+
next: { ...prev, resolved: { at: 1, by: 'real-alice' } },
|
|
906
|
+
})
|
|
907
|
+
).toBeNull()
|
|
908
|
+
})
|
|
909
|
+
|
|
910
|
+
// An anonymous session has no identity to check a record against, so the default withholds
|
|
911
|
+
// all three writes from it. A callback that returns true regardless is taken at its word โ
|
|
912
|
+
// worth knowing before writing one that ignores `userId`.
|
|
913
|
+
it('takes a callback at its word about an anonymous session', () => {
|
|
914
|
+
const prev = comment('real-bob')
|
|
915
|
+
const next = { ...prev, isDeleted: true }
|
|
916
|
+
expect(permissive.comment!({ session: session(null), type: 'update', prev, next })).toBe(
|
|
917
|
+
next
|
|
918
|
+
)
|
|
919
|
+
expect(
|
|
920
|
+
authorizers.comment!({ session: session(null), type: 'update', prev, next })
|
|
921
|
+
).toBeNull()
|
|
922
|
+
})
|
|
923
|
+
})
|
|
924
|
+
})
|
|
408
925
|
})
|