210 lines
9.0 KiB
Swift
210 lines
9.0 KiB
Swift
import Foundation
|
|||
|
|
import UniformTypeIdentifiers
|
||
|
|
import XCTest
|
||
|
|
@testable import QueueCubeShareCore
|
||
|
|
|
||
|
|
final class SharedInputTests: XCTestCase {
|
||
|
|
private let url = URL(string: "https://video.example/watch?v=shared")!
|
||
|
|
|
||
|
|
func testURLDataRepresentationLoadsWithTitleOnlyCompanion() async throws {
|
||
|
|
let result = try await SharedInput.load([extensionItem([
|
||
|
|
dataProvider(Data(url.absoluteString.utf8), type: .url),
|
||
|
|
textProvider("A video worth watching")
|
||
|
|
])])
|
||
|
|
XCTAssertEqual(result, url)
|
||
|
|
}
|
||
|
|
|
||
|
|
func testURLObjectRepresentationLoads() async throws {
|
||
|
|
let provider = NSItemProvider(item: url as NSURL, typeIdentifier: UTType.url.identifier)
|
||
|
|
let result = try await SharedInput.load([extensionItem([provider])])
|
||
|
|
XCTAssertEqual(result, url)
|
||
|
|
}
|
||
|
|
|
||
|
|
func testModernNSURLObjectProviderRoundTrips() async throws {
|
||
|
|
let provider = NSItemProvider(object: url as NSURL)
|
||
|
|
let result = try await SharedInput.load([extensionItem([
|
||
|
|
provider, textProvider("A video worth watching")
|
||
|
|
])])
|
||
|
|
XCTAssertEqual(result, url)
|
||
|
|
}
|
||
|
|
|
||
|
|
func testTypedFileAndCredentialURLsDoNotFallBackToCompanionText() async throws {
|
||
|
|
for invalid in [
|
||
|
|
URL(fileURLWithPath: "/tmp/video.mp4"),
|
||
|
|
URL(string: "https://user:synthetic@video.example/video")!
|
||
|
|
] {
|
||
|
|
await assertFailure(.noLink, items: [extensionItem([
|
||
|
|
NSItemProvider(object: invalid as NSURL),
|
||
|
|
textProvider(url.absoluteString)
|
||
|
|
])])
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func testLegacyURLStringProviderPreservesTitleAndLinkFallback() async throws {
|
||
|
|
let provider = NSItemProvider(item: "Watch this video: \(url.absoluteString)" as NSString, typeIdentifier: UTType.url.identifier)
|
||
|
|
let result = try await SharedInput.load([extensionItem([provider])])
|
||
|
|
XCTAssertEqual(result, url)
|
||
|
|
}
|
||
|
|
|
||
|
|
func testMultipleLinksInsideOneURLDataProviderAreRejected() async throws {
|
||
|
|
await assertFailure(.multipleLinks, items: [extensionItem([
|
||
|
|
dataProvider(Data("\(url.absoluteString) https://other.example/video".utf8), type: .url)
|
||
|
|
])])
|
||
|
|
}
|
||
|
|
|
||
|
|
func testUIKitURLPropertyListRepresentationLoadsWithTitleOnlyCompanion() async throws {
|
||
|
|
let data = try propertyList([url.absoluteString, "", [:]] as [Any])
|
||
|
|
let result = try await SharedInput.load([extensionItem([
|
||
|
|
dataProvider(data, type: .url), textProvider("A video worth watching")
|
||
|
|
])])
|
||
|
|
XCTAssertEqual(result, url)
|
||
|
|
}
|
||
|
|
|
||
|
|
func testUIKitURLPropertyListRejectsNonmatchingShapes() async throws {
|
||
|
|
let shapes: [Any] = [
|
||
|
|
url.absoluteString,
|
||
|
|
["URL": url.absoluteString],
|
||
|
|
[url.absoluteString],
|
||
|
|
[url.absoluteString, ""],
|
||
|
|
[url.absoluteString, "", [:], "extra"] as [Any],
|
||
|
|
[Data(url.absoluteString.utf8), "", [:]] as [Any],
|
||
|
|
[url.absoluteString, 0, [:]] as [Any],
|
||
|
|
[url.absoluteString, "", []] as [Any],
|
||
|
|
[url.absoluteString, "https://base.example/", [:]] as [Any],
|
||
|
|
[url.absoluteString, "", ["unexpected": "value"]] as [Any]
|
||
|
|
]
|
||
|
|
for (index, shape) in shapes.enumerated() {
|
||
|
|
await assertFailure(.noLink, items: [extensionItem([
|
||
|
|
dataProvider(try propertyList(shape), type: .url),
|
||
|
|
textProvider(url.absoluteString)
|
||
|
|
])], context: "plist shape \(index)")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func testUIKitURLPropertyListRejectsFilesAndCredentials() async throws {
|
||
|
|
for value in ["file:///tmp/video.mp4", "https://user:synthetic@video.example/video"] {
|
||
|
|
await assertFailure(.noLink, items: [extensionItem([
|
||
|
|
dataProvider(try propertyList([value, "", [:]] as [Any]), type: .url),
|
||
|
|
textProvider(url.absoluteString)
|
||
|
|
])])
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func testUIKitURLPropertyListPreservesExactOneLinkRequirement() async throws {
|
||
|
|
let another = "https://other.example/video"
|
||
|
|
let combined = try propertyList(["\(url.absoluteString) \(another)", "", [:]] as [Any])
|
||
|
|
await assertFailure(.multipleLinks, items: [extensionItem([dataProvider(combined, type: .url)])])
|
||
|
|
await assertFailure(.multipleLinks, items: [extensionItem([
|
||
|
|
dataProvider(try propertyList([url.absoluteString, "", [:]] as [Any]), type: .url),
|
||
|
|
dataProvider(try propertyList([another, "", [:]] as [Any]), type: .url)
|
||
|
|
])])
|
||
|
|
}
|
||
|
|
|
||
|
|
func testUIKitURLPropertyListIsNotDecodedForPlainTextProviders() async throws {
|
||
|
|
await assertFailure(.noLink, items: [extensionItem([
|
||
|
|
dataProvider(try propertyList([url.absoluteString, "", [:]] as [Any]), type: .plainText)
|
||
|
|
])])
|
||
|
|
}
|
||
|
|
|
||
|
|
func testArbitraryKeyedURLArchivesAreNotDecoded() async throws {
|
||
|
|
for value in [url as NSURL, url.absoluteString as NSString, Data(url.absoluteString.utf8) as NSData] as [Any] {
|
||
|
|
let archive = try NSKeyedArchiver.archivedData(withRootObject: value, requiringSecureCoding: true)
|
||
|
|
await assertFailure(.noLink, items: [extensionItem([
|
||
|
|
dataProvider(archive, type: .url), textProvider(url.absoluteString)
|
||
|
|
])])
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func testTruncatedUIKitURLPropertyListIsRejected() async throws {
|
||
|
|
let data = try propertyList([url.absoluteString, "", [:]] as [Any])
|
||
|
|
await assertFailure(.noLink, items: [extensionItem([
|
||
|
|
dataProvider(data.dropLast(5), type: .url), textProvider(url.absoluteString)
|
||
|
|
])])
|
||
|
|
}
|
||
|
|
|
||
|
|
func testExplicitURLTakesPrecedenceOverLinksInCompanionText() async throws {
|
||
|
|
let result = try await SharedInput.load([extensionItem([
|
||
|
|
dataProvider(Data(url.absoluteString.utf8), type: .url),
|
||
|
|
textProvider("Visit https://other.example/channel for more")
|
||
|
|
])])
|
||
|
|
XCTAssertEqual(result, url)
|
||
|
|
}
|
||
|
|
|
||
|
|
func testMultipleDistinctURLDataProvidersAreRejected() async throws {
|
||
|
|
await assertFailure(.multipleLinks, items: [extensionItem([
|
||
|
|
dataProvider(Data(url.absoluteString.utf8), type: .url),
|
||
|
|
dataProvider(Data("https://video.example/another".utf8), type: .url)
|
||
|
|
])])
|
||
|
|
}
|
||
|
|
|
||
|
|
func testRepeatedObjectAndDataURLRepresentationsAreOneLink() async throws {
|
||
|
|
let result = try await SharedInput.load([extensionItem([
|
||
|
|
NSItemProvider(item: url as NSURL, typeIdentifier: UTType.url.identifier),
|
||
|
|
dataProvider(Data(url.absoluteString.utf8), type: .url)
|
||
|
|
])])
|
||
|
|
XCTAssertEqual(result, url)
|
||
|
|
}
|
||
|
|
|
||
|
|
func testInvalidURLDataDoesNotFallBackToCompanionText() async throws {
|
||
|
|
for data in [Data([0xff, 0xfe, 0x80]), Data("not a URL".utf8), Data("file:///tmp/video.mp4".utf8)] {
|
||
|
|
await assertFailure(.noLink, items: [extensionItem([
|
||
|
|
dataProvider(data, type: .url),
|
||
|
|
textProvider(url.absoluteString)
|
||
|
|
])])
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func testPlainTextDataFallbackExtractsLinkAndIgnoresTitleOnlyProvider() async throws {
|
||
|
|
let result = try await SharedInput.load([extensionItem([
|
||
|
|
textProvider("A video worth watching"),
|
||
|
|
dataProvider(Data("Watch this: \(url.absoluteString)".utf8), type: .plainText)
|
||
|
|
])])
|
||
|
|
XCTAssertEqual(result, url)
|
||
|
|
}
|
||
|
|
|
||
|
|
func testAttributedTextFallbackAndEmptyInput() async throws {
|
||
|
|
let item = NSExtensionItem()
|
||
|
|
item.attributedContentText = NSAttributedString(string: "Watch this: \(url.absoluteString)")
|
||
|
|
let result = try await SharedInput.load([item])
|
||
|
|
XCTAssertEqual(result, url)
|
||
|
|
await assertFailure(.noLink, items: [])
|
||
|
|
}
|
||
|
|
|
||
|
|
private func assertFailure(_ expected: SharedVideo.Error, items: [Any], context: String = "attachment", file: StaticString = #filePath, line: UInt = #line) async {
|
||
|
|
do {
|
||
|
|
_ = try await SharedInput.load(items)
|
||
|
|
XCTFail("Expected \(context) loading to fail", file: file, line: line)
|
||
|
|
} catch let error as SharedVideo.Error {
|
||
|
|
switch (expected, error) {
|
||
|
|
case (.noLink, .noLink), (.multipleLinks, .multipleLinks): break
|
||
|
|
default: XCTFail("Unexpected error: \(error)", file: file, line: line)
|
||
|
|
}
|
||
|
|
} catch {
|
||
|
|
XCTFail("Unexpected provider error: \(error)", file: file, line: line)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private func extensionItem(_ providers: [NSItemProvider]) -> NSExtensionItem {
|
||
|
|
let item = NSExtensionItem()
|
||
|
|
item.attachments = providers
|
||
|
|
return item
|
||
|
|
}
|
||
|
|
|
||
|
|
private func textProvider(_ text: String) -> NSItemProvider {
|
||
|
|
NSItemProvider(item: text as NSString, typeIdentifier: UTType.plainText.identifier)
|
||
|
|
}
|
||
|
|
|
||
|
|
private func dataProvider(_ data: Data, type: UTType) -> NSItemProvider {
|
||
|
|
let provider = NSItemProvider()
|
||
|
|
provider.registerDataRepresentation(forTypeIdentifier: type.identifier, visibility: .all) { completion in
|
||
|
|
completion(data, nil)
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
return provider
|
||
|
|
}
|
||
|
|
|
||
|
|
private func propertyList(_ value: Any) throws -> Data {
|
||
|
|
try PropertyListSerialization.data(fromPropertyList: value, format: .binary, options: 0)
|
||
|
|
}
|
||
|
|
}
|