Files
QueueCube/ios/Tests/QueueCubeShareCoreTests/SharePlaybackTests.swift
T
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

370 lines
15 KiB
Swift

import Foundation
import XCTest
@testable import QueueCubeShareCore
final class SharePlaybackTests: XCTestCase {
private let url = URL(string: "https://video.example/watch?v=shared")!
func testSelectsNewDuplicateByIDAtItsLatestQueueIndex() async throws {
let existing = item(10)
let other = item(11, filename: "https://video.example/other")
let appended = item(12)
let api = FakePlayer(
playlists: [.success([existing, other]), .success([existing, other, appended]), .success([other, appended, existing])],
nowPlaying: [
.success(nowPlaying(appended, paused: true)),
.success(nowPlaying(appended, position: 0)),
.success(nowPlaying(appended, position: 0.25))
]
)
try await enqueue(using: api)
let calls = await api.calls
XCTAssertEqual(calls.addedURLs, [url.absoluteString])
XCTAssertEqual(calls.skippedIndices, [1])
XCTAssertEqual(calls.playCount, 1)
XCTAssertEqual(calls.nowPlayingReads, 3, "A paused player or a single position sample cannot prove playback")
}
func testEmptyQueueWaitsForAsynchronousAppendThenStartsPlayer() async throws {
let appended = item(42)
let api = FakePlayer(
playlists: [.success([]), .success([]), .success([]), .success([appended]), .success([appended])],
nowPlaying: [
.success(nowPlaying(nil)),
.success(nowPlaying(appended, position: 0)),
.success(nowPlaying(appended, position: 0.25))
]
)
try await enqueue(using: api)
let calls = await api.calls
XCTAssertEqual(calls.addedURLs.count, 1)
XCTAssertEqual(calls.skippedIndices, [0])
XCTAssertEqual(calls.playCount, 1)
XCTAssertEqual(calls.playlistReads, 5)
}
func testPreflightFailureAllowsRetryAndDoesNotMutatePlayer() async throws {
let api = FakePlayer(playlists: [.failure(.connection)], nowPlaying: [])
let failure = await failure(using: api)
XCTAssertTrue(failure.canRetry)
XCTAssertTrue(failure.message.contains("No link was added"))
let calls = await api.calls
XCTAssertTrue(calls.addedURLs.isEmpty)
XCTAssertTrue(calls.skippedIndices.isEmpty)
XCTAssertEqual(calls.playCount, 0)
}
func testUncertainAppendResponseDoesNotRepeatMutationOrAllowRetry() async throws {
let api = FakePlayer(playlists: [.success([])], nowPlaying: [], addError: .connection)
let failure = await failure(using: api)
XCTAssertFalse(failure.canRetry)
XCTAssertTrue(failure.message.contains("may already be queued"))
let calls = await api.calls
XCTAssertEqual(calls.addedURLs, [url.absoluteString])
XCTAssertTrue(calls.skippedIndices.isEmpty)
XCTAssertEqual(calls.playCount, 0)
}
func testAppendNeverAppearsDoesNotRepeatMutation() async throws {
let api = FakePlayer(playlists: [.success([])], nowPlaying: [])
let failure = await failure(using: api)
XCTAssertFalse(failure.canRetry)
let calls = await api.calls
XCTAssertEqual(calls.addedURLs.count, 1)
XCTAssertTrue(calls.skippedIndices.isEmpty)
XCTAssertEqual(calls.playCount, 0)
}
func testConcurrentIdenticalAppendsFailWithoutSelectingAnArbitraryItem() async throws {
let api = FakePlayer(playlists: [.success([]), .success([item(1), item(2)])], nowPlaying: [])
let failure = await failure(using: api)
XCTAssertFalse(failure.canRetry)
let calls = await api.calls
XCTAssertEqual(calls.addedURLs.count, 1)
XCTAssertTrue(calls.skippedIndices.isEmpty)
XCTAssertEqual(calls.playCount, 0)
}
func testDuplicateAppearingInFinalSnapshotDoesNotSelectAnArbitraryItem() async throws {
let api = FakePlayer(
playlists: [.success([]), .success([item(1)]), .success([item(1), item(2)])],
nowPlaying: []
)
let failure = await failure(using: api)
XCTAssertFalse(failure.canRetry)
let calls = await api.calls
XCTAssertEqual(calls.addedURLs.count, 1)
XCTAssertTrue(calls.skippedIndices.isEmpty)
XCTAssertEqual(calls.playCount, 0)
}
func testDifferentCandidateInFinalSnapshotDoesNotSelectAnotherClientsAppend() async throws {
let api = FakePlayer(playlists: [.success([]), .success([item(1)]), .success([item(2)])], nowPlaying: [])
let failure = await failure(using: api)
XCTAssertFalse(failure.canRetry)
let calls = await api.calls
XCTAssertTrue(calls.skippedIndices.isEmpty)
XCTAssertEqual(calls.playCount, 0)
}
func testAppendedItemRemovedBeforeSelectionDoesNotSkipAnotherItem() async throws {
let api = FakePlayer(playlists: [.success([]), .success([item(1)]), .success([])], nowPlaying: [])
let failure = await failure(using: api)
XCTAssertFalse(failure.canRetry)
XCTAssertTrue(failure.message.contains("was queued"))
let calls = await api.calls
XCTAssertTrue(calls.skippedIndices.isEmpty)
XCTAssertEqual(calls.playCount, 0)
}
func testPlayRequestFailureReportsPartialSuccessWithoutRetry() async throws {
let appended = item(1)
let api = FakePlayer(playlists: [.success([]), .success([appended])], nowPlaying: [], playError: .connection)
let failure = await failure(using: api)
XCTAssertFalse(failure.canRetry)
XCTAssertTrue(failure.message.contains("was queued, but playback could not be confirmed"))
let calls = await api.calls
XCTAssertEqual(calls.addedURLs.count, 1)
XCTAssertEqual(calls.skippedIndices, [0])
XCTAssertEqual(calls.playCount, 1)
}
func testPlaybackErrorIsNotReportedAsSuccess() async throws {
let appended = item(1)
let errored = item(1, playbackError: "Unsupported video")
let api = FakePlayer(
playlists: [.success([]), .success([appended])],
nowPlaying: [.success(nowPlaying(errored))]
)
let failure = await failure(using: api)
XCTAssertFalse(failure.canRetry)
let calls = await api.calls
XCTAssertEqual(calls.nowPlayingReads, 1)
XCTAssertEqual(calls.addedURLs.count, 1)
}
func testWrongPlayingItemAndPausedPlaybackDoNotReportSuccess() async throws {
for info in [nowPlaying(item(8)), nowPlaying(item(1), paused: true)] {
let api = FakePlayer(playlists: [.success([]), .success([item(1)])], nowPlaying: [.success(info)])
let failure = await failure(using: api)
XCTAssertFalse(failure.canRetry)
let calls = await api.calls
XCTAssertEqual(calls.nowPlayingReads, 4)
XCTAssertEqual(calls.playCount, 1)
}
}
func testLoadingItemFollowedByLoadErrorDoesNotReportSuccess() async throws {
let appended = item(1)
let errored = item(1, playbackError: "Failed to load video")
let api = FakePlayer(
playlists: [.success([]), .success([appended])],
nowPlaying: [.success(nowPlaying(appended)), .success(nowPlaying(errored))]
)
let failure = await failure(using: api)
XCTAssertFalse(failure.canRetry)
let calls = await api.calls
XCTAssertEqual(calls.nowPlayingReads, 2)
XCTAssertEqual(calls.playCount, 1)
}
func testLoadingItemFollowedByDifferentCurrentItemDoesNotReportSuccess() async throws {
let appended = item(1)
let api = FakePlayer(
playlists: [.success([]), .success([appended])],
nowPlaying: [
.success(nowPlaying(appended)),
.success(nowPlaying(item(2), position: 0)),
.success(nowPlaying(item(2), position: 0.25))
]
)
let failure = await failure(using: api)
XCTAssertFalse(failure.canRetry)
let calls = await api.calls
XCTAssertEqual(calls.nowPlayingReads, 4)
}
func testLoadingItemFollowedByMatchingPlaybackProgressSucceeds() async throws {
let appended = item(1)
let api = FakePlayer(
playlists: [.success([]), .success([appended])],
nowPlaying: [
.success(nowPlaying(appended)),
.success(nowPlaying(appended, position: 0)),
.success(nowPlaying(appended, position: 0.25))
]
)
try await enqueue(using: api)
let calls = await api.calls
XCTAssertEqual(calls.nowPlayingReads, 3)
XCTAssertEqual(calls.addedURLs.count, 1)
XCTAssertEqual(calls.playCount, 1)
}
func testFrozenPlaybackPositionDoesNotReportSuccess() async throws {
let appended = item(1)
let api = FakePlayer(
playlists: [.success([]), .success([appended])],
nowPlaying: [.success(nowPlaying(appended, position: 0))]
)
let failure = await failure(using: api)
XCTAssertFalse(failure.canRetry)
let calls = await api.calls
XCTAssertEqual(calls.nowPlayingReads, 4)
}
func testInvalidPositionCannotEstablishPlaybackProgress() async throws {
let appended = item(1)
for invalidPosition in [-1, Double.infinity, -Double.infinity, Double.nan] {
let api = FakePlayer(
playlists: [.success([]), .success([appended])],
nowPlaying: [
.success(nowPlaying(appended, position: invalidPosition)),
.success(nowPlaying(appended, position: 1))
]
)
let failure = await failure(using: api)
XCTAssertFalse(failure.canRetry)
let calls = await api.calls
XCTAssertEqual(calls.nowPlayingReads, 4)
}
}
func testInterruptedPlaybackObservationsResetProgressBaseline() async throws {
let appended = item(1)
let interruptions = [
nowPlaying(appended),
nowPlaying(appended, paused: true, position: 0.25),
nowPlaying(item(2), position: 0.25)
]
for interruption in interruptions {
let api = FakePlayer(
playlists: [.success([]), .success([appended])],
nowPlaying: [
.success(nowPlaying(appended, position: 0)),
.success(interruption),
.success(nowPlaying(appended, position: 1))
]
)
let failure = await failure(using: api)
XCTAssertFalse(failure.canRetry)
let calls = await api.calls
XCTAssertEqual(calls.nowPlayingReads, 4)
}
}
func testCancellationBeforeAppendDoesNotMutateAndAllowsRetry() async throws {
let api = FakePlayer(playlists: [.success([])], nowPlaying: [], cancellation: .preflight)
let failure = await cancelledFailure(using: api)
XCTAssertTrue(failure.canRetry)
XCTAssertTrue(failure.message.contains("No link was added"))
let calls = await api.calls
XCTAssertTrue(calls.addedURLs.isEmpty)
XCTAssertTrue(calls.skippedIndices.isEmpty)
XCTAssertEqual(calls.playCount, 0)
}
func testCancellationAfterAppendDoesNotRepeatMutationOrAllowRetry() async throws {
let api = FakePlayer(playlists: [.success([])], nowPlaying: [], cancellation: .afterAppend)
let failure = await cancelledFailure(using: api)
XCTAssertFalse(failure.canRetry)
XCTAssertTrue(failure.message.contains("may already be queued"))
let calls = await api.calls
XCTAssertEqual(calls.addedURLs, [url.absoluteString])
XCTAssertTrue(calls.skippedIndices.isEmpty)
XCTAssertEqual(calls.playCount, 0)
}
private func cancelledFailure(using api: FakePlayer) async -> SharePlayback.Failure {
// Cancel only this child operation, leaving XCTest's task unaffected.
await Task { await failure(using: api) }.value
}
private func enqueue(using api: FakePlayer) async throws {
try await SharePlayback.enqueueAndPlay(url, using: api, attempts: 4, pollInterval: .zero)
}
private func failure(using api: FakePlayer, file: StaticString = #filePath, line: UInt = #line) async -> SharePlayback.Failure {
do {
try await enqueue(using: api)
XCTFail("Expected playback to fail", file: file, line: line)
} catch let failure as SharePlayback.Failure {
return failure
} catch {
XCTFail("Unexpected error: \(error)", file: file, line: line)
}
return .init(message: "Test did not receive the expected failure", canRetry: true)
}
private func item(_ id: Int, filename: String? = nil, playbackError: String? = nil) -> MediaItem {
MediaItem(filename: filename ?? url.absoluteString, title: nil, id: id, current: nil, playing: nil, metadata: nil, playbackError: playbackError)
}
private func nowPlaying(_ item: MediaItem?, paused: Bool = false, position: Double? = nil) -> NowPlayingInfo {
NowPlayingInfo(playingItem: item, isPaused: paused, volume: 50, timePosition: position, duration: nil, seekable: nil)
}
}
private enum TestError: Error { case connection }
private actor FakePlayer: SharePlayerAPI {
enum Cancellation { case preflight, afterAppend }
struct Calls {
var addedURLs: [String] = []
var skippedIndices: [Int?] = []
var playCount = 0
var playlistReads = 0
var nowPlayingReads = 0
}
private var playlists: [Result<[MediaItem], TestError>]
private var nowPlaying: [Result<NowPlayingInfo, TestError>]
private let addError: TestError?
private let playError: TestError?
private let cancellation: Cancellation?
private(set) var calls = Calls()
init(playlists: [Result<[MediaItem], TestError>], nowPlaying: [Result<NowPlayingInfo, TestError>], addError: TestError? = nil, playError: TestError? = nil, cancellation: Cancellation? = nil) {
self.playlists = playlists
self.nowPlaying = nowPlaying
self.addError = addError
self.playError = playError
self.cancellation = cancellation
}
func fetchPlaylist() async throws -> [MediaItem] {
calls.playlistReads += 1
if cancellation == .preflight, calls.playlistReads == 1 {
withUnsafeCurrentTask { $0?.cancel() }
}
guard let next = playlists.first else { throw TestError.connection }
if playlists.count > 1 { playlists.removeFirst() }
return try next.get()
}
func add(mediaURL: String) async throws {
calls.addedURLs.append(mediaURL)
if cancellation == .afterAppend { withUnsafeCurrentTask { $0?.cancel() } }
if let addError { throw addError }
}
func skip(_ to: Int?) async throws { calls.skippedIndices.append(to) }
func play() async throws {
calls.playCount += 1
if let playError { throw playError }
}
func fetchNowPlayingInfo() async throws -> NowPlayingInfo {
calls.nowPlayingReads += 1
guard let next = nowPlaying.first else { throw TestError.connection }
if nowPlaying.count > 1 { nowPlaying.removeFirst() }
return try next.get()
}
}