Files
ghostgram/submodules/TextFormat/Tests/SelfReferentialLinkTests.swift
ichmagmaus111andClaude Opus 5.5 0622057a4f Ghostgram: open-source release
Ghostgram for iOS, rebuilt from scratch on Telegram iOS 13.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 17:57:09 +01:00

179 lines
9.2 KiB
Swift

import XCTest
import Postbox
import TelegramCore
import TextFormat
final class SelfReferentialLinkTests: XCTestCase {
// MARK: - The predicate
func test_exactMatch_isSelfReferential() {
XCTAssertTrue(chatInputLinkIsSelfReferential(text: "https://example.com/foo", url: "https://example.com/foo"))
}
func test_differentTarget_isNotSelfReferential() {
XCTAssertFalse(chatInputLinkIsSelfReferential(text: "https://a.com", url: "https://b.com"))
}
func test_labelledLink_isNotSelfReferential() {
XCTAssertFalse(chatInputLinkIsSelfReferential(text: "click here", url: "https://example.com"))
}
/// The markdown parser adds the scheme to a bare `www.` autolink.
func test_schemeAddedByParser_isSelfReferential() {
XCTAssertTrue(chatInputLinkIsSelfReferential(text: "www.example.com", url: "http://www.example.com"))
XCTAssertTrue(chatInputLinkIsSelfReferential(text: "www.example.com", url: "https://www.example.com"))
}
/// The markdown parser turns a bare email into a `mailto:` link.
func test_mailtoPrefix_isSelfReferential() {
XCTAssertTrue(chatInputLinkIsSelfReferential(text: "user@example.com", url: "mailto:user@example.com"))
}
/// A non-ASCII path is percent-encoded in the URL but not in the display text.
func test_percentEncodedPath_isSelfReferential() {
XCTAssertTrue(chatInputLinkIsSelfReferential(
text: "https://ru.wikipedia.org/wiki/Привет",
url: "https://ru.wikipedia.org/wiki/%D0%9F%D1%80%D0%B8%D0%B2%D0%B5%D1%82"
))
}
func test_trailingSlashAddedByParser_isSelfReferential() {
XCTAssertTrue(chatInputLinkIsSelfReferential(text: "https://example.com", url: "https://example.com/"))
}
/// A different scheme is a different destination, even when the rest matches.
func test_schemeMismatchOnAnExplicitlySchemedText_isNotSelfReferential() {
XCTAssertFalse(chatInputLinkIsSelfReferential(text: "http://example.com", url: "https://example.com"))
}
func test_emptyOrWhitespaceText_isNotSelfReferential() {
XCTAssertFalse(chatInputLinkIsSelfReferential(text: "", url: "https://example.com"))
XCTAssertFalse(chatInputLinkIsSelfReferential(text: " ", url: "https://example.com"))
}
/// A Telegram deep-link marker (mention / date) is never a plain URL a user typed.
func test_telegramMarkers_areNotSelfReferential() {
XCTAssertFalse(chatInputLinkIsSelfReferential(text: "Alice", url: "tg://user?id=1"))
}
// MARK: - Stripping over a ChatInputContent
private func urlAttributes(_ url: String) -> ChatInputInlineAttributes {
var attributes = ChatInputInlineAttributes()
attributes.entity = .url(url)
return attributes
}
private func paragraph(_ runs: [ChatInputRun], style: ChatInputParagraphStyle = .body) -> ChatInputBlock {
return .paragraph(ChatInputParagraph(style: style, list: nil, runs: runs))
}
func test_strip_removesASelfReferentialURLEntity() {
let content = ChatInputContent(blocks: [paragraph([
ChatInputRun(text: "https://example.com", attributes: urlAttributes("https://example.com"))
])])
let stripped = chatInputContentStrippingSelfReferentialLinks(content)
guard case let .paragraph(result) = stripped.blocks[0] else { return XCTFail("expected a paragraph") }
XCTAssertEqual(result.runs.count, 1)
XCTAssertEqual(result.runs[0].text, "https://example.com")
XCTAssertEqual(result.runs[0].attributes, ChatInputInlineAttributes())
}
func test_strip_keepsAGenuineTextLink() {
let content = ChatInputContent(blocks: [paragraph([
ChatInputRun(text: "click here", attributes: urlAttributes("https://example.com"))
])])
let stripped = chatInputContentStrippingSelfReferentialLinks(content)
XCTAssertEqual(stripped, content)
}
func test_strip_preservesOtherInlineAttributesOnTheStrippedRun() {
var attributes = urlAttributes("https://example.com")
attributes.bold = true
let content = ChatInputContent(blocks: [paragraph([
ChatInputRun(text: "https://example.com", attributes: attributes)
])])
let stripped = chatInputContentStrippingSelfReferentialLinks(content)
guard case let .paragraph(result) = stripped.blocks[0] else { return XCTFail("expected a paragraph") }
XCTAssertNil(result.runs[0].attributes.entity)
XCTAssertTrue(result.runs[0].attributes.bold)
}
/// The link text can be split over several runs (e.g. a formatted span inside the label); the whole
/// span, not each run, is what must equal the URL.
func test_strip_joinsAdjacentRunsSharingTheSameURL() {
let attributes = urlAttributes("https://example.com/foo")
let content = ChatInputContent(blocks: [paragraph([
ChatInputRun(text: "https://example.com", attributes: attributes),
ChatInputRun(text: "/foo", attributes: attributes)
])])
let stripped = chatInputContentStrippingSelfReferentialLinks(content)
guard case let .paragraph(result) = stripped.blocks[0] else { return XCTFail("expected a paragraph") }
XCTAssertNil(result.runs[0].attributes.entity)
XCTAssertNil(result.runs[1].attributes.entity)
}
func test_strip_doesNotJoinRunsOfTwoDifferentLinks() {
let content = ChatInputContent(blocks: [paragraph([
ChatInputRun(text: "https://a.com", attributes: urlAttributes("https://a.com")),
ChatInputRun(text: " and ", attributes: ChatInputInlineAttributes()),
ChatInputRun(text: "label", attributes: urlAttributes("https://b.com"))
])])
let stripped = chatInputContentStrippingSelfReferentialLinks(content)
guard case let .paragraph(result) = stripped.blocks[0] else { return XCTFail("expected a paragraph") }
XCTAssertNil(result.runs[0].attributes.entity)
XCTAssertEqual(result.runs[2].attributes.entity, .url("https://b.com"))
}
func test_strip_leavesMentionAndDateEntitiesAlone() {
var mention = ChatInputInlineAttributes()
mention.entity = .mention(EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value(1)))
let content = ChatInputContent(blocks: [paragraph([ChatInputRun(text: "Alice", attributes: mention)])])
XCTAssertEqual(chatInputContentStrippingSelfReferentialLinks(content), content)
}
func test_strip_recursesIntoNestedContainers() {
let selfLink = ChatInputRun(text: "https://example.com", attributes: urlAttributes("https://example.com"))
let content = ChatInputContent(blocks: [
.code(ChatInputCode(language: nil, runs: [selfLink])),
.pullQuote(ChatInputPullQuote(runs: [selfLink], author: [selfLink])),
.blockQuote(ChatInputBlockQuote(content: ChatInputContent(blocks: [paragraph([selfLink])]), collapsed: false)),
.table(ChatInputTable(columns: [], rows: [ChatInputTableRow(cells: [ChatInputTableCell(runs: [selfLink])])])),
.media(ChatInputMedia(items: [], caption: [selfLink]))
])
let stripped = chatInputContentStrippingSelfReferentialLinks(content)
guard case let .code(code) = stripped.blocks[0] else { return XCTFail("expected code") }
XCTAssertNil(code.runs[0].attributes.entity)
guard case let .pullQuote(pullQuote) = stripped.blocks[1] else { return XCTFail("expected a pull quote") }
XCTAssertNil(pullQuote.runs[0].attributes.entity)
XCTAssertNil(pullQuote.author[0].attributes.entity)
guard case let .blockQuote(blockQuote) = stripped.blocks[2] else { return XCTFail("expected a block quote") }
guard case let .paragraph(inner) = blockQuote.content.blocks[0] else { return XCTFail("expected a paragraph") }
XCTAssertNil(inner.runs[0].attributes.entity)
guard case let .table(table) = stripped.blocks[3] else { return XCTFail("expected a table") }
XCTAssertNil(table.rows[0].cells[0].runs[0].attributes.entity)
guard case let .media(media) = stripped.blocks[4] else { return XCTFail("expected media") }
XCTAssertNil(media.caption[0].attributes.entity)
}
// MARK: - The paste gate
/// The bug: a pasted bare URL parses as a markdown autolink, and the link entity alone made the paste
/// look "richer than plain" — so it was routed to the rich paste path and landed as a text link.
func test_gate_bareURLParagraph_isNotRicherThanPlain_afterStripping() {
let content = ChatInputContent(blocks: [paragraph([
ChatInputRun(text: "https://example.com/foo", attributes: urlAttributes("https://example.com/foo"))
])])
XCTAssertTrue(pastedMarkdownContentIsRicherThanPlain(content), "precondition: the link entity is what makes it rich")
XCTAssertFalse(pastedMarkdownContentIsRicherThanPlain(chatInputContentStrippingSelfReferentialLinks(content)))
}
func test_gate_genuineTextLink_staysRicherThanPlain() {
let content = ChatInputContent(blocks: [paragraph([
ChatInputRun(text: "click here", attributes: urlAttributes("https://example.com"))
])])
XCTAssertTrue(pastedMarkdownContentIsRicherThanPlain(chatInputContentStrippingSelfReferentialLinks(content)))
}
}