luminate/Tests/LuminateCoreTests/JellyfinServiceMock.swift

233 lines
12 KiB
Swift

//
// JellyfinServiceMock.swift
//
// Copyright 2026 Brendan Szymanski <hello@bscubed.dev>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//
// SPDX-License-Identifier: GPL-3.0-or-later
//
import Foundation
import Testing
@testable import LuminateCore
/// A ``JellyfinService`` double that answers each requirement with a test-supplied handler.
///
/// Mirrors `MockJellyfinAPI` in `LuminateServicesTests`: every requirement is implemented exactly
/// once here, so a test configures only the handlers it needs through the initializer and every
/// other call fails the test with a clear message rather than a fabricated success. Handlers are
/// `let`, so the whole instance is provably `Sendable` with no isolation escape hatches -- a
/// handler that must vary its answer across calls closes over its own actor or queue instead of
/// mutating this type.
final class JellyfinServiceMock: JellyfinService, Sendable {
private let isAuthenticatedValue: Bool
private let publicServerInfoHandler: (@Sendable () async throws -> JellyfinServerInfo)?
private let serverInfoHandler: (@Sendable () async throws -> JellyfinServerInfo)?
private let publicUsersHandler: (@Sendable () async throws -> [JellyfinUser])?
private let authenticateHandler: (@Sendable (String, String) async throws -> JellyfinAuthentication)?
private let quickConnectEnabledHandler: (@Sendable () async throws -> Bool)?
private let initiateQuickConnectHandler: (@Sendable () async throws -> JellyfinQuickConnectState)?
private let quickConnectStateHandler: (@Sendable (String) async throws -> JellyfinQuickConnectState)?
private let authenticateWithQuickConnectHandler: (@Sendable (String) async throws -> JellyfinAuthentication)?
private let updateUserPasswordHandler: (@Sendable (String?, String?, String?, String?, Bool?) async throws -> Void)?
private let librariesHandler: (@Sendable (JellyfinListOptions) async throws -> [JellyfinLibrary])?
private let itemsHandler: (@Sendable (JellyfinMediaQuery) async throws -> JellyfinMediaPage)?
private let itemHandler: (@Sendable (String, String?) async throws -> JellyfinMediaItem)?
private let resumeItemsHandler: (@Sendable (JellyfinListOptions) async throws -> JellyfinMediaPage)?
private let nextUpHandler: (@Sendable (String?, JellyfinListOptions) async throws -> JellyfinMediaPage)?
private let latestMediaHandler: (@Sendable (JellyfinListOptions) async throws -> [JellyfinMediaItem])?
private let seasonsHandler: (@Sendable (String, JellyfinListOptions) async throws -> JellyfinMediaPage)?
private let episodesHandler: (@Sendable (String, JellyfinListOptions) async throws -> JellyfinMediaPage)?
private let searchHintsHandler: (@Sendable (String, JellyfinListOptions) async throws -> [JellyfinSearchHint])?
private let imageHandler: (
@Sendable (String, JellyfinImageType, Int32?, JellyfinImageRequest) async throws -> Data
)?
private let markPlayedHandler: (@Sendable (String, String?, Date?) async throws -> JellyfinUserData)?
private let markUnplayedHandler: (@Sendable (String, String?) async throws -> JellyfinUserData)?
private let markFavoriteHandler: (@Sendable (String, String?) async throws -> JellyfinUserData)?
private let unmarkFavoriteHandler: (@Sendable (String, String?) async throws -> JellyfinUserData)?
/// Creates a mock; every parameter defaults to `nil`, so a call the test never stubbed fails
/// with a clear message instead of returning a fabricated value.
init(
isAuthenticated: Bool = false,
publicServerInfo: (@Sendable () async throws -> JellyfinServerInfo)? = nil,
serverInfo: (@Sendable () async throws -> JellyfinServerInfo)? = nil,
publicUsers: (@Sendable () async throws -> [JellyfinUser])? = nil,
authenticate: (@Sendable (String, String) async throws -> JellyfinAuthentication)? = nil,
quickConnectEnabled: (@Sendable () async throws -> Bool)? = nil,
initiateQuickConnect: (@Sendable () async throws -> JellyfinQuickConnectState)? = nil,
quickConnectState: (@Sendable (String) async throws -> JellyfinQuickConnectState)? = nil,
authenticateWithQuickConnect: (@Sendable (String) async throws -> JellyfinAuthentication)? = nil,
updateUserPassword: (@Sendable (String?, String?, String?, String?, Bool?) async throws -> Void)? = nil,
libraries: (@Sendable (JellyfinListOptions) async throws -> [JellyfinLibrary])? = nil,
items: (@Sendable (JellyfinMediaQuery) async throws -> JellyfinMediaPage)? = nil,
item: (@Sendable (String, String?) async throws -> JellyfinMediaItem)? = nil,
resumeItems: (@Sendable (JellyfinListOptions) async throws -> JellyfinMediaPage)? = nil,
nextUp: (@Sendable (String?, JellyfinListOptions) async throws -> JellyfinMediaPage)? = nil,
latestMedia: (@Sendable (JellyfinListOptions) async throws -> [JellyfinMediaItem])? = nil,
seasons: (@Sendable (String, JellyfinListOptions) async throws -> JellyfinMediaPage)? = nil,
episodes: (@Sendable (String, JellyfinListOptions) async throws -> JellyfinMediaPage)? = nil,
searchHints: (@Sendable (String, JellyfinListOptions) async throws -> [JellyfinSearchHint])? = nil,
image: (@Sendable (String, JellyfinImageType, Int32?, JellyfinImageRequest) async throws -> Data)? = nil,
markPlayed: (@Sendable (String, String?, Date?) async throws -> JellyfinUserData)? = nil,
markUnplayed: (@Sendable (String, String?) async throws -> JellyfinUserData)? = nil,
markFavorite: (@Sendable (String, String?) async throws -> JellyfinUserData)? = nil,
unmarkFavorite: (@Sendable (String, String?) async throws -> JellyfinUserData)? = nil
) {
self.isAuthenticatedValue = isAuthenticated
self.publicServerInfoHandler = publicServerInfo
self.serverInfoHandler = serverInfo
self.publicUsersHandler = publicUsers
self.authenticateHandler = authenticate
self.quickConnectEnabledHandler = quickConnectEnabled
self.initiateQuickConnectHandler = initiateQuickConnect
self.quickConnectStateHandler = quickConnectState
self.authenticateWithQuickConnectHandler = authenticateWithQuickConnect
self.updateUserPasswordHandler = updateUserPassword
self.librariesHandler = libraries
self.itemsHandler = items
self.itemHandler = item
self.resumeItemsHandler = resumeItems
self.nextUpHandler = nextUp
self.latestMediaHandler = latestMedia
self.seasonsHandler = seasons
self.episodesHandler = episodes
self.searchHintsHandler = searchHints
self.imageHandler = image
self.markPlayedHandler = markPlayed
self.markUnplayedHandler = markUnplayed
self.markFavoriteHandler = markFavorite
self.unmarkFavoriteHandler = unmarkFavorite
}
var isAuthenticated: Bool { isAuthenticatedValue }
/// Returns a stubbed handler, or fails the test naming the unstubbed operation.
private func unwrap<Handler>(_ handler: Handler?, _ operation: String) throws -> Handler {
try #require(handler, "JellyfinServiceMock received an unstubbed call to \(operation)")
}
func publicServerInfo() async throws -> JellyfinServerInfo {
try await unwrap(publicServerInfoHandler, "publicServerInfo")()
}
func serverInfo() async throws -> JellyfinServerInfo {
try await unwrap(serverInfoHandler, "serverInfo")()
}
func publicUsers() async throws -> [JellyfinUser] {
try await unwrap(publicUsersHandler, "publicUsers")()
}
func authenticate(username: String, password: String) async throws -> JellyfinAuthentication {
try await unwrap(authenticateHandler, "authenticate")(username, password)
}
func quickConnectEnabled() async throws -> Bool {
try await unwrap(quickConnectEnabledHandler, "quickConnectEnabled")()
}
func initiateQuickConnect() async throws -> JellyfinQuickConnectState {
try await unwrap(initiateQuickConnectHandler, "initiateQuickConnect")()
}
func quickConnectState(secret: String) async throws -> JellyfinQuickConnectState {
try await unwrap(quickConnectStateHandler, "quickConnectState")(secret)
}
func authenticateWithQuickConnect(secret: String) async throws -> JellyfinAuthentication {
try await unwrap(authenticateWithQuickConnectHandler, "authenticateWithQuickConnect")(secret)
}
func updateUserPassword(
userID: String?,
currentPassword: String?,
currentPIN: String?,
newPassword: String?,
resetPassword: Bool?
) async throws {
try await unwrap(updateUserPasswordHandler, "updateUserPassword")(
userID,
currentPassword,
currentPIN,
newPassword,
resetPassword
)
}
func libraries(_ options: JellyfinListOptions) async throws -> [JellyfinLibrary] {
try await unwrap(librariesHandler, "libraries")(options)
}
func items(_ query: JellyfinMediaQuery) async throws -> JellyfinMediaPage {
try await unwrap(itemsHandler, "items")(query)
}
func item(id: String, userID: String?) async throws -> JellyfinMediaItem {
try await unwrap(itemHandler, "item")(id, userID)
}
func resumeItems(_ options: JellyfinListOptions) async throws -> JellyfinMediaPage {
try await unwrap(resumeItemsHandler, "resumeItems")(options)
}
func nextUp(seriesID: String?, options: JellyfinListOptions) async throws -> JellyfinMediaPage {
try await unwrap(nextUpHandler, "nextUp")(seriesID, options)
}
func latestMedia(_ options: JellyfinListOptions) async throws -> [JellyfinMediaItem] {
try await unwrap(latestMediaHandler, "latestMedia")(options)
}
func seasons(seriesID: String, options: JellyfinListOptions) async throws -> JellyfinMediaPage {
try await unwrap(seasonsHandler, "seasons")(seriesID, options)
}
func episodes(seriesID: String, options: JellyfinListOptions) async throws -> JellyfinMediaPage {
try await unwrap(episodesHandler, "episodes")(seriesID, options)
}
func searchHints(term: String, options: JellyfinListOptions) async throws -> [JellyfinSearchHint] {
try await unwrap(searchHintsHandler, "searchHints")(term, options)
}
func image(
itemID: String,
type: JellyfinImageType,
index: Int32?,
request: JellyfinImageRequest
) async throws -> Data {
try await unwrap(imageHandler, "image")(itemID, type, index, request)
}
func markPlayed(itemID: String, userID: String?, datePlayed: Date?) async throws -> JellyfinUserData {
try await unwrap(markPlayedHandler, "markPlayed")(itemID, userID, datePlayed)
}
func markUnplayed(itemID: String, userID: String?) async throws -> JellyfinUserData {
try await unwrap(markUnplayedHandler, "markUnplayed")(itemID, userID)
}
func markFavorite(itemID: String, userID: String?) async throws -> JellyfinUserData {
try await unwrap(markFavoriteHandler, "markFavorite")(itemID, userID)
}
func unmarkFavorite(itemID: String, userID: String?) async throws -> JellyfinUserData {
try await unwrap(unmarkFavoriteHandler, "unmarkFavorite")(itemID, userID)
}
}