Files
bitchat/bitchatTests/MessageFormattingEngineTests.swift
jack b6dd31b285 Extract MessageFormattingEngine from ChatViewModel
Refactor message formatting logic into a dedicated, testable class:

- Create MessageFormattingEngine with Patterns enum (precompiled regexes)
- Create MessageFormattingContext protocol for dependency injection
- Extract extractMentions(), containsCashuToken(), and pattern matchers
- Add 28 comprehensive tests for formatting utilities
- Update ChatViewModel to conform to MessageFormattingContext

This prepares for further message formatting extraction and makes
regex patterns and utility functions testable in isolation.
2025-11-24 11:44:59 -10:00

224 lines
8.8 KiB
Swift

//
// MessageFormattingEngineTests.swift
// bitchatTests
//
// Tests for MessageFormattingEngine regex patterns and utility functions.
// This is free and unencumbered software released into the public domain.
//
import Testing
import Foundation
import SwiftUI
@testable import bitchat
struct MessageFormattingEngineTests {
// MARK: - Mention Extraction Tests
@Test func extractMentions_singleMention() {
let content = "Hello @alice how are you?"
let mentions = MessageFormattingEngine.extractMentions(from: content)
#expect(mentions == ["alice"])
}
@Test func extractMentions_multipleMentions() {
let content = "@alice and @bob are chatting with @charlie"
let mentions = MessageFormattingEngine.extractMentions(from: content)
#expect(mentions.count == 3)
#expect(mentions.contains("alice"))
#expect(mentions.contains("bob"))
#expect(mentions.contains("charlie"))
}
@Test func extractMentions_mentionWithSuffix() {
let content = "Hey @alice#a1b2 check this out"
let mentions = MessageFormattingEngine.extractMentions(from: content)
#expect(mentions == ["alice#a1b2"])
}
@Test func extractMentions_noMentions() {
let content = "Just a regular message with no mentions"
let mentions = MessageFormattingEngine.extractMentions(from: content)
#expect(mentions.isEmpty)
}
@Test func extractMentions_unicodeNickname() {
let content = "Hello @日本語 and @émile"
let mentions = MessageFormattingEngine.extractMentions(from: content)
#expect(mentions.count == 2)
#expect(mentions.contains("日本語"))
#expect(mentions.contains("émile"))
}
@Test func extractMentions_mentionWithUnderscore() {
let content = "Thanks @user_name_123"
let mentions = MessageFormattingEngine.extractMentions(from: content)
#expect(mentions == ["user_name_123"])
}
@Test func extractMentions_emailNotCaptured() {
// Email addresses should not be captured as mentions
let content = "Contact me at test@example.com"
let mentions = MessageFormattingEngine.extractMentions(from: content)
// The regex will capture "example" after @ in email - this is expected behavior
// as the regex doesn't distinguish email addresses
#expect(mentions.count == 1)
}
// MARK: - Cashu Token Detection Tests
@Test func containsCashuToken_validTokenA() {
let content = "Here's a token: cashuAeyJwcm9vZnMiOiJIZWxsbyBXb3JsZCEgVGhpcyBpcyBhIHRlc3QgdG9rZW4i"
#expect(MessageFormattingEngine.containsCashuToken(content))
}
@Test func containsCashuToken_validTokenB() {
let content = "Payment: cashuBeyJwcm9vZnMiOiJIZWxsbyBXb3JsZCEgVGhpcyBpcyBhIHRlc3QgdG9rZW4i"
#expect(MessageFormattingEngine.containsCashuToken(content))
}
@Test func containsCashuToken_noToken() {
let content = "Just a regular message about cashews"
#expect(!MessageFormattingEngine.containsCashuToken(content))
}
@Test func containsCashuToken_tooShort() {
let content = "Invalid: cashuAshort"
#expect(!MessageFormattingEngine.containsCashuToken(content))
}
// MARK: - Regex Pattern Tests
@Test func hashtagPattern_standaloneHashtag() {
let content = "#bitcoin is great"
let nsContent = content as NSString
let range = NSRange(location: 0, length: nsContent.length)
let matches = MessageFormattingEngine.Patterns.hashtag.matches(in: content, options: [], range: range)
#expect(matches.count == 1)
}
@Test func hashtagPattern_multipleHashtags() {
let content = "#bitcoin #lightning #nostr"
let nsContent = content as NSString
let range = NSRange(location: 0, length: nsContent.length)
let matches = MessageFormattingEngine.Patterns.hashtag.matches(in: content, options: [], range: range)
#expect(matches.count == 3)
}
@Test func hashtagPattern_hashInMiddleOfWord() {
let content = "test#notahashtag"
let nsContent = content as NSString
let range = NSRange(location: 0, length: nsContent.length)
let matches = MessageFormattingEngine.Patterns.hashtag.matches(in: content, options: [], range: range)
// This will match because the regex doesn't check for word boundaries
#expect(matches.count == 1)
}
@Test func bolt11Pattern_mainnet() {
let content = "Pay this: lnbc10u1pjexampleinvoice0000000000000000000000000000000000000000000"
let nsContent = content as NSString
let range = NSRange(location: 0, length: nsContent.length)
let matches = MessageFormattingEngine.Patterns.bolt11.matches(in: content, options: [], range: range)
#expect(matches.count == 1)
}
@Test func bolt11Pattern_testnet() {
let content = "Test: lntb10u1pjexampleinvoice0000000000000000000000000000000000000000000"
let nsContent = content as NSString
let range = NSRange(location: 0, length: nsContent.length)
let matches = MessageFormattingEngine.Patterns.bolt11.matches(in: content, options: [], range: range)
#expect(matches.count == 1)
}
@Test func lnurlPattern_valid() {
let content = "LNURL: lnurl1dp68gurn8ghj7um9wfmxjcm99e3k7mf0v9cxj0m385ekvcenxc6r2c35xvukxefcv5mkvv34x5ekzd3ev56nyd3hxqurzepexejxxepnxscrvwfnv9nxzcn9xq6xyefhvgcxxcmyxymnserx"
let nsContent = content as NSString
let range = NSRange(location: 0, length: nsContent.length)
let matches = MessageFormattingEngine.Patterns.lnurl.matches(in: content, options: [], range: range)
#expect(matches.count == 1)
}
@Test func lightningSchemePattern_valid() {
let content = "Click: lightning:lnbc10u1example"
let nsContent = content as NSString
let range = NSRange(location: 0, length: nsContent.length)
let matches = MessageFormattingEngine.Patterns.lightningScheme.matches(in: content, options: [], range: range)
#expect(matches.count == 1)
}
@Test func cashuPattern_valid() {
let content = "Token: cashuAeyJwcm9vZnMiOlt7ImlkIjoiMDAwMDAwMDAwMDAwMDAwMCJ9XX0="
let nsContent = content as NSString
let range = NSRange(location: 0, length: nsContent.length)
let matches = MessageFormattingEngine.Patterns.cashu.matches(in: content, options: [], range: range)
#expect(matches.count == 1)
}
// MARK: - URL Detection Tests
@Test func linkDetector_httpURL() {
let content = "Check out http://example.com"
let nsContent = content as NSString
let range = NSRange(location: 0, length: nsContent.length)
let matches = MessageFormattingEngine.Patterns.linkDetector?.matches(in: content, options: [], range: range) ?? []
#expect(matches.count == 1)
}
@Test func linkDetector_httpsURL() {
let content = "Visit https://example.com/path?query=value"
let nsContent = content as NSString
let range = NSRange(location: 0, length: nsContent.length)
let matches = MessageFormattingEngine.Patterns.linkDetector?.matches(in: content, options: [], range: range) ?? []
#expect(matches.count == 1)
}
@Test func linkDetector_multipleURLs() {
let content = "See https://a.com and http://b.com"
let nsContent = content as NSString
let range = NSRange(location: 0, length: nsContent.length)
let matches = MessageFormattingEngine.Patterns.linkDetector?.matches(in: content, options: [], range: range) ?? []
#expect(matches.count == 2)
}
// MARK: - String Extension Tests
@Test func splitSuffix_withSuffix() {
let name = "alice#a1b2"
let (base, suffix) = name.splitSuffix()
#expect(base == "alice")
#expect(suffix == "#a1b2")
}
@Test func splitSuffix_withoutSuffix() {
let name = "alice"
let (base, suffix) = name.splitSuffix()
#expect(base == "alice")
#expect(suffix == "")
}
@Test func splitSuffix_withAtPrefix() {
let name = "@alice#a1b2"
let (base, suffix) = name.splitSuffix()
#expect(base == "alice")
#expect(suffix == "#a1b2")
}
@Test func hasVeryLongToken_noLongToken() {
let content = "Short words only here"
#expect(!content.hasVeryLongToken(threshold: 50))
}
@Test func hasVeryLongToken_withLongToken() {
let longToken = String(repeating: "a", count: 100)
let content = "Here is a \(longToken) token"
#expect(content.hasVeryLongToken(threshold: 50))
}
@Test func hasVeryLongToken_exactThreshold() {
let exactToken = String(repeating: "a", count: 50)
let content = "Token: \(exactToken)"
// Exactly at threshold DOES trigger (uses >= comparison)
#expect(content.hasVeryLongToken(threshold: 50))
}
}