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