Mention.test.ts raw
1 import { describe, it, expect } from 'vitest'
2 import { Mention, MentionList } from './Mention'
3 import { Pubkey } from '../shared/value-objects/Pubkey'
4 import { RelayUrl } from '../shared/value-objects/RelayUrl'
5
6 describe('Mention', () => {
7 const pubkey1 = Pubkey.fromHex('a'.repeat(64))
8 const pubkey2 = Pubkey.fromHex('b'.repeat(64))
9 const relayUrl = RelayUrl.tryCreate('wss://relay.example.com')!
10
11 describe('factory methods', () => {
12 it('creates tag mention', () => {
13 const mention = Mention.tag(pubkey1)
14
15 expect(mention.pubkey).toEqual(pubkey1)
16 expect(mention.type).toBe('tag')
17 expect(mention.isExplicitTag).toBe(true)
18 expect(mention.isInline).toBe(false)
19 expect(mention.isFromContext).toBe(false)
20 })
21
22 it('creates tag mention with relay hint', () => {
23 const mention = Mention.tag(pubkey1, relayUrl)
24
25 expect(mention.relayHint).toEqual(relayUrl)
26 })
27
28 it('creates inline mention', () => {
29 const mention = Mention.inline(pubkey1, 'Alice')
30
31 expect(mention.type).toBe('inline')
32 expect(mention.displayName).toBe('Alice')
33 expect(mention.isInline).toBe(true)
34 })
35
36 it('creates reply author mention', () => {
37 const mention = Mention.replyAuthor(pubkey1, relayUrl)
38
39 expect(mention.type).toBe('reply_author')
40 expect(mention.isFromContext).toBe(true)
41 })
42
43 it('creates quote author mention', () => {
44 const mention = Mention.quoteAuthor(pubkey1)
45
46 expect(mention.type).toBe('quote_author')
47 expect(mention.isFromContext).toBe(true)
48 })
49 })
50
51 describe('parseFromContent', () => {
52 it('extracts npub mentions', () => {
53 const npub = pubkey1.npub
54 const content = `Hey nostr:${npub} check this out!`
55
56 const mentions = Mention.parseFromContent(content)
57
58 expect(mentions).toHaveLength(1)
59 expect(mentions[0].pubkey.hex).toBe(pubkey1.hex)
60 expect(mentions[0].isInline).toBe(true)
61 })
62
63 it('extracts multiple mentions', () => {
64 const npub1 = pubkey1.npub
65 const npub2 = pubkey2.npub
66 const content = `nostr:${npub1} and nostr:${npub2} are cool`
67
68 const mentions = Mention.parseFromContent(content)
69
70 expect(mentions).toHaveLength(2)
71 })
72
73 it('deduplicates mentions of same pubkey', () => {
74 const npub = pubkey1.npub
75 const content = `nostr:${npub} says nostr:${npub} is great`
76
77 const mentions = Mention.parseFromContent(content)
78
79 expect(mentions).toHaveLength(1)
80 })
81
82 it('handles invalid bech32 gracefully', () => {
83 const content = 'nostr:npub1invalid and nostr:npub1alsobad'
84
85 const mentions = Mention.parseFromContent(content)
86
87 expect(mentions).toHaveLength(0)
88 })
89
90 it('returns empty array for content without mentions', () => {
91 const content = 'Just a regular post without mentions'
92
93 const mentions = Mention.parseFromContent(content)
94
95 expect(mentions).toHaveLength(0)
96 })
97 })
98
99 describe('toNostrUri', () => {
100 it('returns npub URI without relay hint', () => {
101 const mention = Mention.inline(pubkey1)
102
103 const uri = mention.toNostrUri()
104
105 expect(uri).toBe(`nostr:${pubkey1.npub}`)
106 })
107
108 it('returns nprofile URI with relay hint', () => {
109 const mention = Mention.tag(pubkey1, relayUrl)
110
111 const uri = mention.toNostrUri()
112
113 expect(uri).toContain('nostr:nprofile1')
114 })
115 })
116
117 describe('toTag', () => {
118 it('generates p tag without relay', () => {
119 const mention = Mention.inline(pubkey1)
120
121 const tag = mention.toTag()
122
123 expect(tag).toEqual(['p', pubkey1.hex])
124 })
125
126 it('generates p tag with relay hint', () => {
127 const mention = Mention.tag(pubkey1, relayUrl)
128
129 const tag = mention.toTag()
130
131 expect(tag).toEqual(['p', pubkey1.hex, relayUrl.value])
132 })
133 })
134
135 describe('immutable modifications', () => {
136 it('withRelayHint returns new instance', () => {
137 const original = Mention.inline(pubkey1)
138 const modified = original.withRelayHint(relayUrl)
139
140 expect(original.relayHint).toBeNull()
141 expect(modified.relayHint).toEqual(relayUrl)
142 })
143
144 it('withDisplayName returns new instance', () => {
145 const original = Mention.inline(pubkey1)
146 const modified = original.withDisplayName('Bob')
147
148 expect(original.displayName).toBeNull()
149 expect(modified.displayName).toBe('Bob')
150 })
151 })
152
153 describe('equals', () => {
154 it('returns true for same pubkey', () => {
155 const a = Mention.tag(pubkey1)
156 const b = Mention.inline(pubkey1) // Different type but same pubkey
157
158 expect(a.equals(b)).toBe(true)
159 })
160
161 it('returns false for different pubkeys', () => {
162 const a = Mention.tag(pubkey1)
163 const b = Mention.tag(pubkey2)
164
165 expect(a.equals(b)).toBe(false)
166 })
167 })
168
169 describe('hasSamePubkey', () => {
170 it('returns true for matching pubkey', () => {
171 const mention = Mention.tag(pubkey1)
172
173 expect(mention.hasSamePubkey(pubkey1)).toBe(true)
174 })
175
176 it('returns false for different pubkey', () => {
177 const mention = Mention.tag(pubkey1)
178
179 expect(mention.hasSamePubkey(pubkey2)).toBe(false)
180 })
181 })
182 })
183
184 describe('MentionList', () => {
185 const pubkey1 = Pubkey.fromHex('a'.repeat(64))
186 const pubkey2 = Pubkey.fromHex('b'.repeat(64))
187 const pubkey3 = Pubkey.fromHex('c'.repeat(64))
188
189 describe('factory methods', () => {
190 it('creates empty list', () => {
191 const list = MentionList.empty()
192
193 expect(list.isEmpty).toBe(true)
194 expect(list.length).toBe(0)
195 })
196
197 it('creates from mentions with deduplication', () => {
198 const mentions = [
199 Mention.tag(pubkey1),
200 Mention.inline(pubkey2),
201 Mention.tag(pubkey1) // Duplicate
202 ]
203
204 const list = MentionList.from(mentions)
205
206 expect(list.length).toBe(2)
207 })
208 })
209
210 describe('add', () => {
211 it('adds new mention', () => {
212 const list = MentionList.empty().add(Mention.tag(pubkey1))
213
214 expect(list.length).toBe(1)
215 expect(list.contains(pubkey1)).toBe(true)
216 })
217
218 it('does not add duplicate', () => {
219 const list = MentionList.empty()
220 .add(Mention.tag(pubkey1))
221 .add(Mention.inline(pubkey1))
222
223 expect(list.length).toBe(1)
224 })
225 })
226
227 describe('remove', () => {
228 it('removes mention by pubkey', () => {
229 const list = MentionList.from([Mention.tag(pubkey1), Mention.tag(pubkey2)]).remove(
230 pubkey1
231 )
232
233 expect(list.length).toBe(1)
234 expect(list.contains(pubkey1)).toBe(false)
235 expect(list.contains(pubkey2)).toBe(true)
236 })
237 })
238
239 describe('contains', () => {
240 it('returns true if pubkey is in list', () => {
241 const list = MentionList.from([Mention.tag(pubkey1)])
242
243 expect(list.contains(pubkey1)).toBe(true)
244 expect(list.contains(pubkey2)).toBe(false)
245 })
246 })
247
248 describe('pubkeys', () => {
249 it('returns all pubkeys', () => {
250 const list = MentionList.from([Mention.tag(pubkey1), Mention.tag(pubkey2)])
251
252 const pubkeys = list.pubkeys
253
254 expect(pubkeys).toHaveLength(2)
255 expect(pubkeys.map((p) => p.hex)).toContain(pubkey1.hex)
256 expect(pubkeys.map((p) => p.hex)).toContain(pubkey2.hex)
257 })
258 })
259
260 describe('toTags', () => {
261 it('generates p tags for all mentions', () => {
262 const list = MentionList.from([Mention.tag(pubkey1), Mention.tag(pubkey2)])
263
264 const tags = list.toTags()
265
266 expect(tags).toHaveLength(2)
267 expect(tags[0][0]).toBe('p')
268 expect(tags[1][0]).toBe('p')
269 })
270 })
271
272 describe('merge', () => {
273 it('merges two lists with deduplication', () => {
274 const list1 = MentionList.from([Mention.tag(pubkey1), Mention.tag(pubkey2)])
275 const list2 = MentionList.from([Mention.tag(pubkey2), Mention.tag(pubkey3)])
276
277 const merged = list1.merge(list2)
278
279 expect(merged.length).toBe(3)
280 expect(merged.contains(pubkey1)).toBe(true)
281 expect(merged.contains(pubkey2)).toBe(true)
282 expect(merged.contains(pubkey3)).toBe(true)
283 })
284 })
285 })
286