233 lines
12 KiB
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)
|
|
}
|
|
}
|