luminate/Tests/LuminateServicesTests/MockJellyfinAPI.swift

285 lines
12 KiB
Swift

//
// MockJellyfinAPI.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 LuminateAPI
import LuminateServices
import Synchronization
import Testing
/// Records the last input each operation received, so a test can assert parameter mapping.
///
/// A reference type with a `Mutex` rather than a mutable struct field: `MockJellyfinAPI` is
/// copied into the actor-isolated `JellyfinClient`, so a value-typed log would record into a
/// copy the test cannot see.
final class OperationInputLog: Sendable {
private let entries = Mutex<[String: any Sendable]>([:])
/// Stores `input` as the most recent call to `operation`.
func record(_ input: any Sendable, for operation: String) {
entries.withLock { $0[operation] = input }
}
/// The most recent input recorded for `operation`, or `nil` when it was never called.
func last<Input: Sendable>(_ operation: String, as type: Input.Type) -> Input? {
entries.withLock { $0[operation] as? Input }
}
}
/// A ``JellyfinAPI`` double that answers with pre-baked generated outputs.
///
/// Only the operations a test actually stubs are usable; every other call fails the test with a
/// clear message rather than returning a fabricated success. Outputs are plain `Sendable` values,
/// so the whole double is a `Sendable` struct with no isolation escape hatches.
struct MockJellyfinAPI: JellyfinAPI {
/// Records the input each operation method received, keyed by operation identifier.
let inputs = OperationInputLog()
/// The output returned by ``authenticateUserByName(_:)``.
var authenticateUserByNameOutput: Operations.AuthenticateUserByName.Output?
/// The output returned by ``authenticateWithQuickConnect(_:)``.
var authenticateWithQuickConnectOutput: Operations.AuthenticateWithQuickConnect.Output?
/// The output returned by ``getPublicUsers(_:)``.
var getPublicUsersOutput: Operations.GetPublicUsers.Output?
/// The output returned by ``getCurrentUser(_:)``.
var getCurrentUserOutput: Operations.GetCurrentUser.Output?
/// The output returned by ``getPublicSystemInfo(_:)``.
var getPublicSystemInfoOutput: Operations.GetPublicSystemInfo.Output?
/// The output returned by ``getSystemInfo(_:)``.
var getSystemInfoOutput: Operations.GetSystemInfo.Output?
/// The output returned by ``getQuickConnectEnabled(_:)``.
var getQuickConnectEnabledOutput: Operations.GetQuickConnectEnabled.Output?
/// The output returned by ``initiateQuickConnect(_:)``.
var initiateQuickConnectOutput: Operations.InitiateQuickConnect.Output?
/// The output returned by ``getQuickConnectState(_:)``.
var getQuickConnectStateOutput: Operations.GetQuickConnectState.Output?
/// The output returned by ``updateUserPassword(_:)``.
var updateUserPasswordOutput: Operations.UpdateUserPassword.Output?
/// The output returned by ``getUserViews(_:)``.
var getUserViewsOutput: Operations.GetUserViews.Output?
/// The output returned by ``getItems(_:)``.
var getItemsOutput: Operations.GetItems.Output?
/// The output returned by ``getItem(_:)``.
var getItemOutput: Operations.GetItem.Output?
/// The output returned by ``getResumeItems(_:)``.
var getResumeItemsOutput: Operations.GetResumeItems.Output?
/// The output returned by ``getNextUp(_:)``.
var getNextUpOutput: Operations.GetNextUp.Output?
/// The output returned by ``getLatestMedia(_:)``.
var getLatestMediaOutput: Operations.GetLatestMedia.Output?
/// The output returned by ``getSeasons(_:)``.
var getSeasonsOutput: Operations.GetSeasons.Output?
/// The output returned by ``getEpisodes(_:)``.
var getEpisodesOutput: Operations.GetEpisodes.Output?
/// The output returned by ``getSearchHints(_:)``.
var getSearchHintsOutput: Operations.GetSearchHints.Output?
/// The output returned by ``getItemImage(_:)``.
var getItemImageOutput: Operations.GetItemImage.Output?
/// The output returned by ``getItemImageByIndex(_:)``.
var getItemImageByIndexOutput: Operations.GetItemImageByIndex.Output?
/// The output returned by ``markPlayedItem(_:)``.
var markPlayedItemOutput: Operations.MarkPlayedItem.Output?
/// The output returned by ``markUnplayedItem(_:)``.
var markUnplayedItemOutput: Operations.MarkUnplayedItem.Output?
/// The output returned by ``markFavoriteItem(_:)``.
var markFavoriteItemOutput: Operations.MarkFavoriteItem.Output?
/// The output returned by ``unmarkFavoriteItem(_:)``.
var unmarkFavoriteItemOutput: Operations.UnmarkFavoriteItem.Output?
/// Returns a stubbed output, or fails the test naming the unstubbed operation.
///
/// - Parameters:
/// - output: The stubbed output for the operation, if the test supplied one.
/// - operation: The operation identifier, used in the failure message.
/// - Returns: The stubbed output.
/// - Throws: An error that fails the test when the operation was not stubbed.
private func unwrap<Output>(_ output: Output?, _ operation: String) throws -> Output {
try #require(output, "MockJellyfinAPI received an unstubbed call to \(operation)")
}
func authenticateUserByName(_ input: Operations.AuthenticateUserByName.Input) async throws
-> Operations.AuthenticateUserByName.Output
{
inputs.record(input, for: "AuthenticateUserByName")
return try unwrap(authenticateUserByNameOutput, "AuthenticateUserByName")
}
func authenticateWithQuickConnect(_ input: Operations.AuthenticateWithQuickConnect.Input) async throws
-> Operations.AuthenticateWithQuickConnect.Output
{
inputs.record(input, for: "AuthenticateWithQuickConnect")
return try unwrap(authenticateWithQuickConnectOutput, "AuthenticateWithQuickConnect")
}
func getPublicUsers(_ input: Operations.GetPublicUsers.Input) async throws -> Operations.GetPublicUsers.Output {
inputs.record(input, for: "GetPublicUsers")
return try unwrap(getPublicUsersOutput, "GetPublicUsers")
}
func getCurrentUser(_ input: Operations.GetCurrentUser.Input) async throws -> Operations.GetCurrentUser.Output {
inputs.record(input, for: "GetCurrentUser")
return try unwrap(getCurrentUserOutput, "GetCurrentUser")
}
func getPublicSystemInfo(_ input: Operations.GetPublicSystemInfo.Input) async throws
-> Operations.GetPublicSystemInfo.Output
{
inputs.record(input, for: "GetPublicSystemInfo")
return try unwrap(getPublicSystemInfoOutput, "GetPublicSystemInfo")
}
func getSystemInfo(_ input: Operations.GetSystemInfo.Input) async throws -> Operations.GetSystemInfo.Output {
inputs.record(input, for: "GetSystemInfo")
return try unwrap(getSystemInfoOutput, "GetSystemInfo")
}
func getQuickConnectEnabled(_ input: Operations.GetQuickConnectEnabled.Input) async throws
-> Operations.GetQuickConnectEnabled.Output
{
inputs.record(input, for: "GetQuickConnectEnabled")
return try unwrap(getQuickConnectEnabledOutput, "GetQuickConnectEnabled")
}
func initiateQuickConnect(_ input: Operations.InitiateQuickConnect.Input) async throws
-> Operations.InitiateQuickConnect.Output
{
inputs.record(input, for: "InitiateQuickConnect")
return try unwrap(initiateQuickConnectOutput, "InitiateQuickConnect")
}
func getQuickConnectState(_ input: Operations.GetQuickConnectState.Input) async throws
-> Operations.GetQuickConnectState.Output
{
inputs.record(input, for: "GetQuickConnectState")
return try unwrap(getQuickConnectStateOutput, "GetQuickConnectState")
}
func updateUserPassword(_ input: Operations.UpdateUserPassword.Input) async throws
-> Operations.UpdateUserPassword.Output
{
inputs.record(input, for: "UpdateUserPassword")
return try unwrap(updateUserPasswordOutput, "UpdateUserPassword")
}
func getUserViews(_ input: Operations.GetUserViews.Input) async throws -> Operations.GetUserViews.Output {
inputs.record(input, for: "GetUserViews")
return try unwrap(getUserViewsOutput, "GetUserViews")
}
func getItems(_ input: Operations.GetItems.Input) async throws -> Operations.GetItems.Output {
inputs.record(input, for: "GetItems")
return try unwrap(getItemsOutput, "GetItems")
}
func getItem(_ input: Operations.GetItem.Input) async throws -> Operations.GetItem.Output {
inputs.record(input, for: "GetItem")
return try unwrap(getItemOutput, "GetItem")
}
func getResumeItems(_ input: Operations.GetResumeItems.Input) async throws -> Operations.GetResumeItems.Output {
inputs.record(input, for: "GetResumeItems")
return try unwrap(getResumeItemsOutput, "GetResumeItems")
}
func getNextUp(_ input: Operations.GetNextUp.Input) async throws -> Operations.GetNextUp.Output {
inputs.record(input, for: "GetNextUp")
return try unwrap(getNextUpOutput, "GetNextUp")
}
func getLatestMedia(_ input: Operations.GetLatestMedia.Input) async throws -> Operations.GetLatestMedia.Output {
inputs.record(input, for: "GetLatestMedia")
return try unwrap(getLatestMediaOutput, "GetLatestMedia")
}
func getSeasons(_ input: Operations.GetSeasons.Input) async throws -> Operations.GetSeasons.Output {
inputs.record(input, for: "GetSeasons")
return try unwrap(getSeasonsOutput, "GetSeasons")
}
func getEpisodes(_ input: Operations.GetEpisodes.Input) async throws -> Operations.GetEpisodes.Output {
inputs.record(input, for: "GetEpisodes")
return try unwrap(getEpisodesOutput, "GetEpisodes")
}
func getSearchHints(_ input: Operations.GetSearchHints.Input) async throws -> Operations.GetSearchHints.Output {
inputs.record(input, for: "GetSearchHints")
return try unwrap(getSearchHintsOutput, "GetSearchHints")
}
func getItemImage(_ input: Operations.GetItemImage.Input) async throws -> Operations.GetItemImage.Output {
inputs.record(input, for: "GetItemImage")
return try unwrap(getItemImageOutput, "GetItemImage")
}
func getItemImageByIndex(_ input: Operations.GetItemImageByIndex.Input) async throws
-> Operations.GetItemImageByIndex.Output
{
inputs.record(input, for: "GetItemImageByIndex")
return try unwrap(getItemImageByIndexOutput, "GetItemImageByIndex")
}
func markPlayedItem(_ input: Operations.MarkPlayedItem.Input) async throws -> Operations.MarkPlayedItem.Output {
inputs.record(input, for: "MarkPlayedItem")
return try unwrap(markPlayedItemOutput, "MarkPlayedItem")
}
func markUnplayedItem(_ input: Operations.MarkUnplayedItem.Input) async throws -> Operations.MarkUnplayedItem.Output
{
inputs.record(input, for: "MarkUnplayedItem")
return try unwrap(markUnplayedItemOutput, "MarkUnplayedItem")
}
func markFavoriteItem(_ input: Operations.MarkFavoriteItem.Input) async throws -> Operations.MarkFavoriteItem.Output
{
inputs.record(input, for: "MarkFavoriteItem")
return try unwrap(markFavoriteItemOutput, "MarkFavoriteItem")
}
func unmarkFavoriteItem(_ input: Operations.UnmarkFavoriteItem.Input) async throws
-> Operations.UnmarkFavoriteItem.Output
{
inputs.record(input, for: "UnmarkFavoriteItem")
return try unwrap(unmarkFavoriteItemOutput, "UnmarkFavoriteItem")
}
}