ContentFilter.test.ts raw

   1  import { describe, it, expect } from 'vitest'
   2  import { ContentFilter } from './ContentFilter'
   3  import type { Event } from 'nostr-tools'
   4  
   5  describe('ContentFilter', () => {
   6    // Helper to create mock events
   7    const createEvent = (overrides: Partial<Event> = {}): Event => ({
   8      id: 'a'.repeat(64),
   9      pubkey: 'b'.repeat(64),
  10      created_at: Math.floor(Date.now() / 1000),
  11      kind: 1,
  12      tags: [],
  13      content: 'test content',
  14      sig: 'c'.repeat(128),
  15      ...overrides
  16    })
  17  
  18    describe('factory methods', () => {
  19      it('creates default filter with sensible defaults', () => {
  20        const filter = ContentFilter.default()
  21  
  22        expect(filter.hideMutedUsers).toBe(true)
  23        expect(filter.hideContentMentioningMuted).toBe(true)
  24        expect(filter.hideUntrustedUsers).toBe(false)
  25        expect(filter.hideReplies).toBe(false)
  26        expect(filter.hideReposts).toBe(false)
  27        expect(filter.allowedKinds).toEqual([])
  28        expect(filter.nsfwPolicy).toBe('hide_content')
  29      })
  30  
  31      it('creates filter from preferences', () => {
  32        const filter = ContentFilter.fromPreferences({
  33          hideMutedUsers: false,
  34          hideReplies: true,
  35          nsfwPolicy: 'show'
  36        })
  37  
  38        expect(filter.hideMutedUsers).toBe(false)
  39        expect(filter.hideReplies).toBe(true)
  40        expect(filter.nsfwPolicy).toBe('show')
  41      })
  42  
  43      it('uses defaults for missing preferences', () => {
  44        const filter = ContentFilter.fromPreferences({})
  45  
  46        expect(filter.hideMutedUsers).toBe(true)
  47        expect(filter.nsfwPolicy).toBe('hide_content')
  48      })
  49    })
  50  
  51    describe('isKindAllowed', () => {
  52      it('allows all kinds when allowedKinds is empty', () => {
  53        const filter = ContentFilter.default()
  54  
  55        expect(filter.isKindAllowed(1)).toBe(true)
  56        expect(filter.isKindAllowed(6)).toBe(true)
  57        expect(filter.isKindAllowed(30023)).toBe(true)
  58      })
  59  
  60      it('only allows specified kinds', () => {
  61        const filter = ContentFilter.default().withAllowedKinds([1, 6])
  62  
  63        expect(filter.isKindAllowed(1)).toBe(true)
  64        expect(filter.isKindAllowed(6)).toBe(true)
  65        expect(filter.isKindAllowed(7)).toBe(false)
  66      })
  67    })
  68  
  69    describe('shouldShow', () => {
  70      const mutedPubkeys = new Set(['muted'.repeat(8)])
  71      const trustedPubkeys = new Set(['trusted'.repeat(8)])
  72      const deletedEventIds = new Set(['deleted'.repeat(8)])
  73  
  74      it('shows normal events', () => {
  75        const filter = ContentFilter.default()
  76        const event = createEvent()
  77        const context = { mutedPubkeys: new Set<string>() }
  78  
  79        const result = filter.shouldShow(event, context)
  80  
  81        expect(result.shouldShow).toBe(true)
  82      })
  83  
  84      it('hides events from muted authors', () => {
  85        const filter = ContentFilter.default()
  86        const event = createEvent({ pubkey: 'muted'.repeat(8) })
  87        const context = { mutedPubkeys }
  88  
  89        const result = filter.shouldShow(event, context)
  90  
  91        expect(result.shouldShow).toBe(false)
  92        expect(result.reason).toBe('muted_author')
  93      })
  94  
  95      it('shows events from muted authors when hideMutedUsers is false', () => {
  96        const filter = ContentFilter.default().withHideMutedUsers(false)
  97        const event = createEvent({ pubkey: 'muted'.repeat(8) })
  98        const context = { mutedPubkeys }
  99  
 100        const result = filter.shouldShow(event, context)
 101  
 102        expect(result.shouldShow).toBe(true)
 103      })
 104  
 105      it('hides events mentioning muted users', () => {
 106        const filter = ContentFilter.default()
 107        const event = createEvent({
 108          tags: [['p', 'muted'.repeat(8)]]
 109        })
 110        const context = { mutedPubkeys }
 111  
 112        const result = filter.shouldShow(event, context)
 113  
 114        expect(result.shouldShow).toBe(false)
 115        expect(result.reason).toBe('mentions_muted_user')
 116      })
 117  
 118      it('hides deleted events', () => {
 119        const filter = ContentFilter.default()
 120        const event = createEvent({ id: 'deleted'.repeat(8) })
 121        const context = { mutedPubkeys: new Set<string>(), deletedEventIds }
 122  
 123        const result = filter.shouldShow(event, context)
 124  
 125        expect(result.shouldShow).toBe(false)
 126        expect(result.reason).toBe('deleted')
 127      })
 128  
 129      it('hides untrusted authors when enabled', () => {
 130        const filter = ContentFilter.default().withHideUntrustedUsers(true)
 131        const event = createEvent({ pubkey: 'stranger'.repeat(8) })
 132        const context = { mutedPubkeys: new Set<string>(), trustedPubkeys }
 133  
 134        const result = filter.shouldShow(event, context)
 135  
 136        expect(result.shouldShow).toBe(false)
 137        expect(result.reason).toBe('untrusted_author')
 138      })
 139  
 140      it('shows trusted authors when hiding untrusted', () => {
 141        const filter = ContentFilter.default().withHideUntrustedUsers(true)
 142        const event = createEvent({ pubkey: 'trusted'.repeat(8) })
 143        const context = { mutedPubkeys: new Set<string>(), trustedPubkeys }
 144  
 145        const result = filter.shouldShow(event, context)
 146  
 147        expect(result.shouldShow).toBe(true)
 148      })
 149  
 150      it('hides replies when enabled', () => {
 151        const filter = ContentFilter.default().withHideReplies(true)
 152        const event = createEvent({
 153          tags: [['e', 'someevent'.repeat(8), '', 'reply']]
 154        })
 155        const context = { mutedPubkeys: new Set<string>() }
 156  
 157        const result = filter.shouldShow(event, context)
 158  
 159        expect(result.shouldShow).toBe(false)
 160        expect(result.reason).toBe('reply_filtered')
 161      })
 162  
 163      it('hides reposts when enabled', () => {
 164        const filter = ContentFilter.default().withHideReposts(true)
 165        const event = createEvent({ kind: 6 })
 166        const context = { mutedPubkeys: new Set<string>() }
 167  
 168        const result = filter.shouldShow(event, context)
 169  
 170        expect(result.shouldShow).toBe(false)
 171        expect(result.reason).toBe('repost_filtered')
 172      })
 173  
 174      it('hides events with disallowed kinds', () => {
 175        const filter = ContentFilter.default().withAllowedKinds([1])
 176        const event = createEvent({ kind: 6 })
 177        const context = { mutedPubkeys: new Set<string>() }
 178  
 179        const result = filter.shouldShow(event, context)
 180  
 181        expect(result.shouldShow).toBe(false)
 182        expect(result.reason).toBe('kind_not_allowed')
 183      })
 184  
 185      it('shows pinned events even from muted authors', () => {
 186        const filter = ContentFilter.default()
 187        const eventId = 'pinned'.repeat(8)
 188        const event = createEvent({ id: eventId, pubkey: 'muted'.repeat(8) })
 189        const context = {
 190          mutedPubkeys,
 191          pinnedEventIds: new Set([eventId])
 192        }
 193  
 194        const result = filter.shouldShow(event, context)
 195  
 196        expect(result.shouldShow).toBe(true)
 197      })
 198    })
 199  
 200    describe('immutable modifications', () => {
 201      it('withHideMutedUsers returns new instance', () => {
 202        const filter1 = ContentFilter.default()
 203        const filter2 = filter1.withHideMutedUsers(false)
 204  
 205        expect(filter1.hideMutedUsers).toBe(true)
 206        expect(filter2.hideMutedUsers).toBe(false)
 207      })
 208  
 209      it('withHideReplies returns new instance', () => {
 210        const filter1 = ContentFilter.default()
 211        const filter2 = filter1.withHideReplies(true)
 212  
 213        expect(filter1.hideReplies).toBe(false)
 214        expect(filter2.hideReplies).toBe(true)
 215      })
 216  
 217      it('withAllowedKinds returns new instance', () => {
 218        const filter1 = ContentFilter.default()
 219        const filter2 = filter1.withAllowedKinds([1, 6, 7])
 220  
 221        expect(filter1.allowedKinds).toEqual([])
 222        expect(filter2.allowedKinds).toEqual([1, 6, 7])
 223      })
 224  
 225      it('withNsfwPolicy returns new instance', () => {
 226        const filter1 = ContentFilter.default()
 227        const filter2 = filter1.withNsfwPolicy('show')
 228  
 229        expect(filter1.nsfwPolicy).toBe('hide_content')
 230        expect(filter2.nsfwPolicy).toBe('show')
 231      })
 232    })
 233  
 234    describe('equals', () => {
 235      it('returns true for identical filters', () => {
 236        const filter1 = ContentFilter.default()
 237        const filter2 = ContentFilter.default()
 238  
 239        expect(filter1.equals(filter2)).toBe(true)
 240      })
 241  
 242      it('returns false for different settings', () => {
 243        const filter1 = ContentFilter.default()
 244        const filter2 = ContentFilter.default().withHideReplies(true)
 245  
 246        expect(filter1.equals(filter2)).toBe(false)
 247      })
 248  
 249      it('returns false for different allowed kinds', () => {
 250        const filter1 = ContentFilter.default().withAllowedKinds([1])
 251        const filter2 = ContentFilter.default().withAllowedKinds([1, 6])
 252  
 253        expect(filter1.equals(filter2)).toBe(false)
 254      })
 255    })
 256  })
 257