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