Files
buzzert 8723955705 ios: add share extension to queue and play video links
Embed an iOS and Catalyst share extension using existing discovery and player APIs. Share saved players through an App Group snapshot, preserve the queue, and verify advancing playback before reporting success.

Cover URL provider transport, settings migration, discovery addresses, duplicate handling, cancellation, and partial enqueue/play outcomes with focused tests. Document authentication and signing limits.
2026-10-20 22:08:12 -07:00

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