bitchat/bitchatTests/Services/SafeRegexTests.swift
Vidit Kulshrestha 4ef5558d7b
Replace try! regex construction with a non-trapping SafeRegex helper (#1501)
MessageFormattingEngine and MessageDeduplicationService compiled eight
bundled regex literals with try!, so a bad pattern would crash the app
at startup - in the middle of the message-render path (#645).

Add SafeRegex.compile: it compiles the pattern normally, and on failure
logs through SecureLogger and returns a never-matching regex ('(?!)'),
so a broken pattern degrades that one formatting feature instead of
trapping. Pattern properties stay non-optional, so no call-site churn
across ChatMessageFormatter, MessageTextHelpers, and
ChatComposerCoordinator.

The compile-time guarantee try! provided moves into tests: each
production pattern is asserted to compile and match a known-good sample,
so a typo in a pattern now fails CI instead of crashing users.

Part of #645 (the remaining try! sites; NoiseSessionManager's
force-unwrap is addressed separately in #1456).
2026-07-30 17:56:45 +01:00

65 lines
2.6 KiB
Swift

//
// SafeRegexTests.swift
// bitchatTests
//
// SafeRegex must never trap: valid patterns compile normally, invalid ones
// degrade to a regex that matches nothing. The production-pattern test keeps
// the compile-time guarantee try! used to provide - a typo in any bundled
// pattern fails here instead of crashing the app at startup.
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import Foundation
import Testing
@testable import bitchat
struct SafeRegexTests {
private func matchCount(_ regex: NSRegularExpression, _ text: String) -> Int {
regex.numberOfMatches(in: text, options: [], range: NSRange(text.startIndex..., in: text))
}
@Test
func validPatternCompilesAndMatches() {
let regex = SafeRegex.compile("#([a-zA-Z0-9_]+)")
#expect(matchCount(regex, "tag #bitchat here") == 1)
}
@Test
func invalidPatternDegradesToNeverMatching() {
let regex = SafeRegex.compile("(unclosed")
#expect(matchCount(regex, "(unclosed anything") == 0)
#expect(matchCount(regex, "") == 0)
}
@Test
func productionPatternsCompileAndMatchTheirTargets() {
// A pattern that failed to compile would have degraded to
// never-matching, so each positive match proves the literal compiled.
#expect(matchCount(MessageFormattingEngine.Patterns.hashtag, "see #mesh") == 1)
#expect(matchCount(MessageFormattingEngine.Patterns.mention, "hi @alice#ab12") == 1)
let cashuToken = "cashuA" + String(repeating: "x", count: 45)
#expect(matchCount(MessageFormattingEngine.Patterns.cashu, cashuToken) == 1)
#expect(matchCount(MessageFormattingEngine.Patterns.quickCashuPresence, cashuToken) == 1)
let bolt11 = "lnbc1" + String(repeating: "q", count: 55)
#expect(matchCount(MessageFormattingEngine.Patterns.bolt11, bolt11) == 1)
let lnurl = "lnurl1" + String(repeating: "q", count: 25)
#expect(matchCount(MessageFormattingEngine.Patterns.lnurl, lnurl) == 1)
#expect(matchCount(MessageFormattingEngine.Patterns.lightningScheme, "pay lightning:abc123") == 1)
}
@Test
func contentNormalizerStillSimplifiesURLs() {
// Exercises ContentNormalizer's regex through its public entry point:
// same URL with different query strings must normalize identically.
let a = ContentNormalizer.normalizedKey("check https://example.com/page?q=1")
let b = ContentNormalizer.normalizedKey("check https://example.com/page?q=2")
#expect(a == b)
}
}