//! A fake Jellyfin server the online repository can actually talk to. //! //! # Why this exists //! //! Before it, `src-tauri/` contained no HTTP mocking of any kind. Every test of //! the ~4,800-line online adapter asserted on a *constructed URL string*, and //! not one exercised a response. That has a specific, recorded cost: a deleted //! test file re-implemented the URL builders inside its own mock and then //! asserted against itself, and `online.rs` still carries the comment recording //! that the production builder meanwhile shipped a `/Videos/{id}/download` //! endpoint which 404s on real servers — silently breaking every download while //! the "test" stayed green. //! //! So the rule here is: **assert against a response from a mock *server*, never //! against a mock that re-derives the thing under test.** Nothing in this module //! may reimplement anything from `endpoints.rs` or `online.rs`. //! //! # Two generations //! //! [`FakeJellyfin::start`] takes the version string the fake server reports, and //! the repository it hands back resolves its capabilities from exactly that — the //! same path production takes. A test that runs against both generations is //! therefore running the real resolution, not a stubbed one. //! //! TRACES: UR-085 | DR-281 use std::sync::Arc; use serde_json::json; use wiremock::matchers::{method, path_regex}; use wiremock::{Mock, MockServer, Request, ResponseTemplate}; use super::capabilities::ServerCapabilities; use super::online::OnlineRepository; use crate::jellyfin::{HttpClient, HttpConfig}; /// Jellyfin's current stable line, and the line this client was built against. pub const V12: &str = "12.0.0"; pub const V10_11: &str = "10.11.5"; /// Both live generations. `#[test]`s that care about compatibility iterate this. /// /// There is no 11 in the middle: Jellyfin dropped the leading `10` from its /// scheme with 12.0, so what would have been 10.12.0 shipped as `12.0`. pub const BOTH_GENERATIONS: [&str; 2] = [V10_11, V12]; pub struct FakeJellyfin { server: MockServer, version: String, } impl FakeJellyfin { /// Stand up a server reporting `version`, answering any `/Items`-shaped /// query with one item and any `/Users/.../Views` with one library. /// /// The response bodies are deliberately minimal: this module's job is to let /// tests observe what the *client* sent, not to re-specify Jellyfin. pub async fn start(version: &str) -> Self { let server = MockServer::start().await; let item = json!({ "Id": "item-1", "Name": "A Film", "Type": "Movie", "IsFolder": false, "ServerId": "srv-1", }); Mock::given(method("GET")) .and(path_regex(r".*/Views$")) .respond_with(ResponseTemplate::new(200).set_body_json(json!({ "Items": [{ "Id": "lib-1", "Name": "Movies", "Type": "CollectionFolder", "CollectionType": "movies", "IsFolder": true, "ServerId": "srv-1", }], "TotalRecordCount": 1, }))) .mount(&server) .await; // Everything else that returns a listing. Mock::given(method("GET")) .respond_with(ResponseTemplate::new(200).set_body_json(json!({ "Items": [item], "TotalRecordCount": 1, }))) .mount(&server) .await; Mock::given(method("POST")) .respond_with(ResponseTemplate::new(204)) .mount(&server) .await; Self { server, version: version.to_string(), } } /// A repository pointed at this server, with capabilities resolved from the /// version it reports — the same resolution production performs. pub fn repository(&self) -> OnlineRepository { let http = HttpClient::new_allowing_plaintext_for_tests(HttpConfig::default()) .expect("test http client"); OnlineRepository::new( Arc::new(http), self.server.uri(), "user-1".to_string(), "token-abc".to_string(), ) .with_capabilities(ServerCapabilities::from_reported(&self.version)) } /// Every request the server received, in order. pub async fn requests(&self) -> Vec { self.server .received_requests() .await .expect("request recording is enabled") } /// The single request received, failing loudly if there was not exactly one. pub async fn only_request(&self) -> Request { let mut received = self.requests().await; assert_eq!( received.len(), 1, "expected exactly one request, got {}", received.len() ); received.remove(0) } } /// The request target as the server saw it — path plus query. pub fn target(request: &Request) -> String { match request.url.query() { Some(q) => format!("{}?{}", request.url.path(), q), None => request.url.path().to_string(), } }