376 lines
15 KiB
Swift
376 lines
15 KiB
Swift
//
|
|
// Copyright 2023 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
import XCTest
|
|
import SignalCoreKit
|
|
import SignalServiceKit
|
|
import SignalUI
|
|
|
|
private extension LinkPreviewFetcher.State {
|
|
var isNone: Bool {
|
|
if case .none = self {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
var isLoaded: Bool {
|
|
if case .loaded = self {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
var isFailed: Bool {
|
|
if case .failed = self {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
var isLoading: Bool {
|
|
if case .loading = self {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
}
|
|
|
|
class LinkPreviewFetcherTest: XCTestCase {
|
|
|
|
private var mockLinkPreviewManager: MockLinkPreviewManager!
|
|
private var testScheduler: TestScheduler!
|
|
private var mockDB: MockDB!
|
|
|
|
override func setUp() {
|
|
super.setUp()
|
|
|
|
mockDB = MockDB()
|
|
mockLinkPreviewManager = MockLinkPreviewManager()
|
|
testScheduler = TestScheduler()
|
|
testScheduler.start()
|
|
}
|
|
|
|
func testUpdateLoaded() throws {
|
|
let linkPreviewFetcher = LinkPreviewFetcher(
|
|
db: mockDB,
|
|
linkPreviewManager: mockLinkPreviewManager,
|
|
schedulers: TestSchedulers(scheduler: testScheduler)
|
|
)
|
|
|
|
// Non-URL text shouldn't issue any fetches.
|
|
for textValue in ["a", "ab", "abc"] {
|
|
linkPreviewFetcher.update(.init(text: textValue, ranges: .empty))
|
|
XCTAssert(linkPreviewFetcher.currentState.isNone)
|
|
XCTAssertNil(linkPreviewFetcher.linkPreviewDraftIfLoaded)
|
|
XCTAssertNil(linkPreviewFetcher.currentUrl)
|
|
XCTAssertEqual(mockLinkPreviewManager.fetchedURLs, [])
|
|
}
|
|
|
|
// A valid URL should fetch only once, even if the text is modified.
|
|
let validURL = try XCTUnwrap(URL(string: "https://signal.org"))
|
|
mockLinkPreviewManager.fetchLinkPreviewBlock = { fetchedURL in
|
|
XCTAssertEqual(fetchedURL, validURL)
|
|
return .value(OWSLinkPreviewDraft(url: fetchedURL, title: "Website Title"))
|
|
}
|
|
for textValue in ["Check ou https://signal.org", "Check out https://signal.org"] {
|
|
linkPreviewFetcher.update(.init(text: textValue, ranges: .empty))
|
|
XCTAssert(linkPreviewFetcher.currentState.isLoaded)
|
|
XCTAssertEqual(linkPreviewFetcher.linkPreviewDraftIfLoaded?.url, validURL)
|
|
XCTAssertEqual(linkPreviewFetcher.currentUrl, validURL)
|
|
XCTAssertEqual(mockLinkPreviewManager.fetchedURLs, [validURL])
|
|
}
|
|
|
|
// An invalid URL should fetch only once, even if the text is modified.
|
|
let invalidURL = try XCTUnwrap(URL(string: "https://signal.org/not_found"))
|
|
mockLinkPreviewManager.fetchLinkPreviewBlock = { fetchedURL in
|
|
XCTAssertEqual(fetchedURL, invalidURL)
|
|
return Promise(error: OWSGenericError("Not found."))
|
|
}
|
|
for textValue in ["Check ou https://signal.org/not_found", "Check out https://signal.org/not_found"] {
|
|
linkPreviewFetcher.update(.init(text: textValue, ranges: .empty))
|
|
XCTAssert(linkPreviewFetcher.currentState.isFailed)
|
|
XCTAssertNil(linkPreviewFetcher.linkPreviewDraftIfLoaded)
|
|
XCTAssertEqual(linkPreviewFetcher.currentUrl, invalidURL)
|
|
XCTAssertEqual(mockLinkPreviewManager.fetchedURLs, [validURL, invalidURL])
|
|
}
|
|
|
|
// Removing the URL should clear the link preview.
|
|
for textValue in ["Check out", "Check ou"] {
|
|
linkPreviewFetcher.update(.init(text: textValue, ranges: .empty))
|
|
XCTAssert(linkPreviewFetcher.currentState.isNone)
|
|
XCTAssertNil(linkPreviewFetcher.linkPreviewDraftIfLoaded)
|
|
XCTAssertNil(linkPreviewFetcher.currentUrl)
|
|
XCTAssertEqual(mockLinkPreviewManager.fetchedURLs, [validURL, invalidURL])
|
|
}
|
|
}
|
|
|
|
func testUpdateLoading() throws {
|
|
let linkPreviewFetcher = LinkPreviewFetcher(
|
|
db: mockDB,
|
|
linkPreviewManager: mockLinkPreviewManager,
|
|
schedulers: TestSchedulers(scheduler: testScheduler)
|
|
)
|
|
|
|
let validURL = try XCTUnwrap(URL(string: "https://signal.org"))
|
|
mockLinkPreviewManager.fetchLinkPreviewBlock = { fetchedURL in
|
|
return .value(OWSLinkPreviewDraft(url: fetchedURL, title: "Website Title"))
|
|
}
|
|
|
|
testScheduler.stop()
|
|
|
|
linkPreviewFetcher.update(.init(text: "https://signal.org is a grea", ranges: .empty))
|
|
XCTAssert(linkPreviewFetcher.currentState.isLoading)
|
|
XCTAssertEqual(mockLinkPreviewManager.fetchedURLs, [validURL])
|
|
|
|
// If there's a request in flight, we shouldn't send a new request.
|
|
linkPreviewFetcher.update(.init(text: "https://signal.org is a great", ranges: .empty))
|
|
XCTAssert(linkPreviewFetcher.currentState.isLoading)
|
|
XCTAssertEqual(mockLinkPreviewManager.fetchedURLs, [validURL])
|
|
|
|
testScheduler.start()
|
|
|
|
XCTAssert(linkPreviewFetcher.currentState.isLoaded)
|
|
XCTAssertEqual(mockLinkPreviewManager.fetchedURLs, [validURL])
|
|
}
|
|
|
|
func testUpdateObsolete() throws {
|
|
let linkPreviewFetcher = LinkPreviewFetcher(
|
|
db: mockDB,
|
|
linkPreviewManager: mockLinkPreviewManager,
|
|
schedulers: TestSchedulers(scheduler: testScheduler)
|
|
)
|
|
|
|
mockLinkPreviewManager.fetchLinkPreviewBlock = { fetchedURL in
|
|
return .value(OWSLinkPreviewDraft(url: fetchedURL, title: "Website Title"))
|
|
}
|
|
|
|
testScheduler.stop()
|
|
|
|
let url1 = try XCTUnwrap(URL(string: "https://signal.org/one"))
|
|
linkPreviewFetcher.update(.init(text: "https://signal.org/one", ranges: .empty))
|
|
XCTAssert(linkPreviewFetcher.currentState.isLoading)
|
|
XCTAssertEqual(mockLinkPreviewManager.fetchedURLs, [url1])
|
|
|
|
// If there's a request in flight & we change the URL, drop the original request.
|
|
let url2 = try XCTUnwrap(URL(string: "https://signal.org/two"))
|
|
linkPreviewFetcher.update(.init(text: "https://signal.org/two", ranges: .empty))
|
|
XCTAssert(linkPreviewFetcher.currentState.isLoading)
|
|
XCTAssertEqual(mockLinkPreviewManager.fetchedURLs, [url1, url2])
|
|
|
|
testScheduler.start()
|
|
|
|
XCTAssert(linkPreviewFetcher.currentState.isLoaded)
|
|
XCTAssertEqual(linkPreviewFetcher.linkPreviewDraftIfLoaded?.url, url2)
|
|
XCTAssertEqual(linkPreviewFetcher.currentUrl, url2)
|
|
XCTAssertEqual(mockLinkPreviewManager.fetchedURLs, [url1, url2])
|
|
}
|
|
|
|
func testUpdatePrependScheme() {
|
|
let linkPreviewFetcher = LinkPreviewFetcher(
|
|
db: mockDB,
|
|
linkPreviewManager: mockLinkPreviewManager,
|
|
schedulers: TestSchedulers(scheduler: testScheduler)
|
|
)
|
|
|
|
mockLinkPreviewManager.fetchLinkPreviewBlock = { fetchedURL in
|
|
return .value(OWSLinkPreviewDraft(url: fetchedURL, title: "Signal"))
|
|
}
|
|
linkPreviewFetcher.update(.init(text: "signal.org", ranges: .empty), prependSchemeIfNeeded: false)
|
|
XCTAssert(linkPreviewFetcher.currentState.isNone)
|
|
XCTAssertEqual(mockLinkPreviewManager.fetchedURLs, [])
|
|
|
|
// If we should prepend a scheme, prepend "https://".
|
|
linkPreviewFetcher.update(.init(text: "signal.org", ranges: .empty), prependSchemeIfNeeded: true)
|
|
XCTAssert(linkPreviewFetcher.currentState.isLoaded)
|
|
XCTAssertEqual(mockLinkPreviewManager.fetchedURLs, [URL(string: "https://signal.org")!])
|
|
mockLinkPreviewManager.fetchedURLs.removeAll()
|
|
|
|
// If there's already a scheme, we don't add "https://". (We require
|
|
// "https://", so specify anything other scheme disables link previews.
|
|
linkPreviewFetcher.update(.init(text: "http://signal.org", ranges: .empty), prependSchemeIfNeeded: true)
|
|
XCTAssert(linkPreviewFetcher.currentState.isNone)
|
|
XCTAssertEqual(mockLinkPreviewManager.fetchedURLs, [])
|
|
}
|
|
|
|
func testOnStateChange() throws {
|
|
let linkPreviewFetcher = LinkPreviewFetcher(
|
|
db: mockDB,
|
|
linkPreviewManager: mockLinkPreviewManager,
|
|
schedulers: TestSchedulers(scheduler: testScheduler)
|
|
)
|
|
|
|
mockLinkPreviewManager.fetchLinkPreviewBlock = { fetchedURL in
|
|
return .value(OWSLinkPreviewDraft(url: fetchedURL, title: "Signal"))
|
|
}
|
|
|
|
var onStateChangeCount = 0
|
|
linkPreviewFetcher.onStateChange = { onStateChangeCount += 1 }
|
|
|
|
linkPreviewFetcher.update(.init(text: "", ranges: .empty))
|
|
XCTAssertEqual(onStateChangeCount, 0)
|
|
|
|
// Redundant updates generally don't result in state updates.
|
|
linkPreviewFetcher.update(.init(text: "a", ranges: .empty))
|
|
XCTAssertEqual(onStateChangeCount, 0)
|
|
|
|
// Fetching a URL should update the state twice: to loading & to loaded.
|
|
linkPreviewFetcher.update(.init(text: "https://signal.org", ranges: .empty))
|
|
XCTAssertEqual(onStateChangeCount, 2)
|
|
|
|
linkPreviewFetcher.update(.init(text: "https://signal.org", ranges: .empty))
|
|
XCTAssertEqual(onStateChangeCount, 2)
|
|
|
|
// Clearing the text should update the link preview.
|
|
linkPreviewFetcher.update(.init(text: "", ranges: .empty))
|
|
XCTAssertEqual(onStateChangeCount, 3)
|
|
|
|
// Assigning the URL again should fetch it again.
|
|
linkPreviewFetcher.update(.init(text: "https://signal.org", ranges: .empty))
|
|
XCTAssertEqual(onStateChangeCount, 5)
|
|
|
|
// Disabling the link preview should trigger an update.
|
|
linkPreviewFetcher.disable()
|
|
XCTAssertEqual(onStateChangeCount, 6)
|
|
}
|
|
|
|
func testDisable() throws {
|
|
let linkPreviewFetcher = LinkPreviewFetcher(
|
|
db: mockDB,
|
|
linkPreviewManager: mockLinkPreviewManager,
|
|
schedulers: TestSchedulers(scheduler: testScheduler)
|
|
)
|
|
|
|
mockLinkPreviewManager.fetchLinkPreviewBlock = { fetchedURL in
|
|
return .value(OWSLinkPreviewDraft(url: fetchedURL, title: "Signal"))
|
|
}
|
|
|
|
// Fetch the original preview.
|
|
let url = try XCTUnwrap(URL(string: "https://signal.org"))
|
|
linkPreviewFetcher.update(.init(text: "https://signal.org", ranges: .empty))
|
|
XCTAssertEqual(linkPreviewFetcher.linkPreviewDraftIfLoaded?.url, url)
|
|
XCTAssertEqual(mockLinkPreviewManager.fetchedURLs, [url])
|
|
mockLinkPreviewManager.fetchedURLs.removeAll()
|
|
|
|
// Dismiss the preview; make sure it goes away.
|
|
linkPreviewFetcher.disable()
|
|
XCTAssertNil(linkPreviewFetcher.linkPreviewDraftIfLoaded)
|
|
XCTAssertEqual(mockLinkPreviewManager.fetchedURLs, [])
|
|
|
|
// Assign the same URL again; make sure it doesn't come back.
|
|
linkPreviewFetcher.update(.init(text: "https://signal.org", ranges: .empty))
|
|
XCTAssertNil(linkPreviewFetcher.linkPreviewDraftIfLoaded)
|
|
XCTAssertEqual(mockLinkPreviewManager.fetchedURLs, [])
|
|
|
|
// Clear the URL; make sure it stays away.
|
|
linkPreviewFetcher.update(.init(text: "", ranges: .empty))
|
|
XCTAssertNil(linkPreviewFetcher.linkPreviewDraftIfLoaded)
|
|
XCTAssertEqual(mockLinkPreviewManager.fetchedURLs, [])
|
|
|
|
// Enter a different URL; make sure we don't fetch it.
|
|
linkPreviewFetcher.update(.init(text: "https://signal.org/one", ranges: .empty))
|
|
XCTAssertNil(linkPreviewFetcher.linkPreviewDraftIfLoaded)
|
|
XCTAssertEqual(mockLinkPreviewManager.fetchedURLs, [])
|
|
|
|
// Ensure "enableIfEmpty" doesn't enable when the text isn't empty.
|
|
linkPreviewFetcher.update(.init(text: "https://signal.org/one", ranges: .empty), enableIfEmpty: true)
|
|
XCTAssertNil(linkPreviewFetcher.linkPreviewDraftIfLoaded)
|
|
XCTAssertEqual(mockLinkPreviewManager.fetchedURLs, [])
|
|
|
|
// Clear the text with "enableIfEmpty" to re-enable link previews.
|
|
linkPreviewFetcher.update(.init(text: "", ranges: .empty), enableIfEmpty: true)
|
|
XCTAssertNil(linkPreviewFetcher.linkPreviewDraftIfLoaded)
|
|
XCTAssertEqual(mockLinkPreviewManager.fetchedURLs, [])
|
|
|
|
// Set a URL and make sure we fetch it.
|
|
let url2 = try XCTUnwrap(URL(string: "https://signal.org/two"))
|
|
linkPreviewFetcher.update(.init(text: "https://signal.org/two", ranges: .empty), enableIfEmpty: true)
|
|
XCTAssertEqual(linkPreviewFetcher.linkPreviewDraftIfLoaded?.url, url2)
|
|
XCTAssertEqual(mockLinkPreviewManager.fetchedURLs, [url2])
|
|
}
|
|
|
|
func testOnlyParseIfEnabled() throws {
|
|
mockLinkPreviewManager.areLinkPreviewsEnabledMock = false
|
|
|
|
do {
|
|
let linkPreviewFetcher = LinkPreviewFetcher(
|
|
db: mockDB,
|
|
linkPreviewManager: mockLinkPreviewManager,
|
|
schedulers: TestSchedulers(scheduler: testScheduler),
|
|
onlyParseIfEnabled: true
|
|
)
|
|
|
|
linkPreviewFetcher.update(.init(text: "https://signal.org", ranges: .empty))
|
|
XCTAssertNil(linkPreviewFetcher.currentUrl)
|
|
}
|
|
do {
|
|
let linkPreviewFetcher = LinkPreviewFetcher(
|
|
db: mockDB,
|
|
linkPreviewManager: mockLinkPreviewManager,
|
|
schedulers: TestSchedulers(scheduler: testScheduler),
|
|
onlyParseIfEnabled: false
|
|
)
|
|
|
|
// If link previews are disabled, we may still want to parse URLs in the
|
|
// text so that they can be attached (without a preview) to text stories.
|
|
mockLinkPreviewManager.fetchLinkPreviewBlock = { fetchedURL in
|
|
return Promise(error: OWSGenericError("Not found."))
|
|
}
|
|
linkPreviewFetcher.update(.init(text: "https://signal.org", ranges: .empty))
|
|
XCTAssertEqual(linkPreviewFetcher.currentUrl, try XCTUnwrap(URL(string: "https://signal.org")))
|
|
}
|
|
}
|
|
|
|
func testDontParseInSpoilers() throws {
|
|
mockLinkPreviewManager.fetchLinkPreviewBlock = { fetchedURL in
|
|
return .value(OWSLinkPreviewDraft(url: fetchedURL, title: "Signal"))
|
|
}
|
|
|
|
let linkPreviewFetcher = LinkPreviewFetcher(
|
|
db: mockDB,
|
|
linkPreviewManager: mockLinkPreviewManager,
|
|
schedulers: TestSchedulers(scheduler: testScheduler),
|
|
onlyParseIfEnabled: true
|
|
)
|
|
|
|
// Bold should have no effect
|
|
linkPreviewFetcher.update(.init(
|
|
text: "https://signal.org",
|
|
ranges: .init(
|
|
mentions: [:],
|
|
styles: [.init(.bold, range: NSRange(location: 0, length: 18))]
|
|
)
|
|
))
|
|
XCTAssertNotNil(linkPreviewFetcher.currentUrl)
|
|
|
|
// Spoiler should mean we don't match.
|
|
linkPreviewFetcher.update(.init(
|
|
text: "https://signal.org",
|
|
ranges: .init(
|
|
mentions: [:],
|
|
styles: [.init(.spoiler, range: NSRange(location: 0, length: 18))]
|
|
)
|
|
))
|
|
XCTAssertNil(linkPreviewFetcher.currentUrl)
|
|
|
|
// Even if only partially covering.
|
|
linkPreviewFetcher.update(.init(
|
|
text: "https://signal.org",
|
|
ranges: .init(
|
|
mentions: [:],
|
|
styles: [.init(.spoiler, range: NSRange(location: 3, length: 5))]
|
|
)
|
|
))
|
|
XCTAssertNil(linkPreviewFetcher.currentUrl)
|
|
|
|
// Including if we prepend a prefix.
|
|
linkPreviewFetcher.update(.init(
|
|
text: "signal.org",
|
|
ranges: .init(
|
|
mentions: [:],
|
|
styles: [.init(.spoiler, range: NSRange(location: 5, length: 5))]
|
|
)
|
|
))
|
|
XCTAssertNil(linkPreviewFetcher.currentUrl)
|
|
}
|
|
}
|