Files
QueueCube/ios/Tests/QueueCubeShareCoreTests/SharedInputTests.swift
T

210 lines
9.0 KiB
Swift
Raw Normal View History

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)
}
}