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