From 92346301c15329c7e8795bfbfa223e9012f952ec Mon Sep 17 00:00:00 2001 From: euzu <33094714+euzu@users.noreply.github.com> Date: Wed, 30 Sep 2026 09:02:32 +0200 Subject: [PATCH] Fixed: tmdb error stops pipeline processing (#886) * **Bug Fixes** * Playlist publication now continues when optional content curation is unavailable or encounters service, credential, request-limit, or certificate issues. * In these cases, regular catalog content can still be published to supported outputs and saved to the cache. Curation-specific views are omitted when they cannot be prepared. * Empty playlists continue to be rejected by the regular publication process. --- .../src/processor/playlist/target.rs | 42 +++++---- .../src/processor/playlist/tests.rs | 35 ++++---- .../playlist/tests/curation_publication.rs | 63 +++++++++++--- .../client_diagnostics.rs | 13 ++- .../tests/curation_publication/item_limit.rs | 85 +++++++------------ .../tests/curation_publication/tls_profile.rs | 11 ++- 6 files changed, 128 insertions(+), 121 deletions(-) diff --git a/backend/processing/src/processor/playlist/target.rs b/backend/processing/src/processor/playlist/target.rs index 87921e56f..2fd5596a2 100644 --- a/backend/processing/src/processor/playlist/target.rs +++ b/backend/processing/src/processor/playlist/target.rs @@ -491,7 +491,7 @@ pub(crate) async fn finalize_prepared_target>, prepared: PreparedTarget, ) -> (Result<(), Vec>, Vec) { - // No generic-client fallback: unavailable transport participates in the same admission gate. + // Keep TMDB transport settings independent of the generic client. let tmdb_client = build_target_tmdb_client( &prepared.target, || tuliprox_core::utils::network::request::create_tmdb_client(&ctx.config), @@ -528,13 +528,7 @@ pub(super) async fn finalize_prepared_target_with_tmdb views, - Err(error) => { - step.stop("Curation failed; skipping persist to preserve finalized artifacts"); - return (Err(vec![error]), errors); - } - }; + let views = prepare_target_playlist_views(&ctx.client, tmdb_client, target, eligible_catalog).await; if views.base.is_empty() && views.xtream.is_none() && views.publication_plan == PlaylistPublicationPlan::Ordinary @@ -932,13 +926,13 @@ pub(crate) async fn prepare_target_playlist_views( tmdb_client: Option<&reqwest::Client>, target: &ConfigTarget, playlist: Vec, -) -> Result { +) -> TargetPlaylistViews { let Some(config) = target.effective_curation() else { - return Ok(TargetPlaylistViews { + return TargetPlaylistViews { base: playlist, xtream: None, publication_plan: PlaylistPublicationPlan::Ordinary, - }); + }; }; let outcome = evaluate_curation(client, tmdb_client, &playlist, &target.name, &config).await; @@ -950,14 +944,16 @@ pub(super) fn curation_playlist_views( playlist: Vec, config: &CurationConfig, outcome: CurationRunOutcome, -) -> Result { +) -> TargetPlaylistViews { match outcome { - CurationRunOutcome::NotConfigured => Ok(TargetPlaylistViews { - base: playlist, - xtream: None, - publication_plan: PlaylistPublicationPlan::Ordinary, - }), - CurationRunOutcome::Failed(failure) => Err(curation_failure_error(&target.name, &failure)), + CurationRunOutcome::NotConfigured => { + TargetPlaylistViews { base: playlist, xtream: None, publication_plan: PlaylistPublicationPlan::Ordinary } + } + CurationRunOutcome::Failed(failure) => { + warn_curation_failure(&target.name, &failure); + // Discovery is optional; unavailable selections must not suppress the source catalog or EPG. + TargetPlaylistViews { base: playlist, xtream: None, publication_plan: PlaylistPublicationPlan::Ordinary } + } CurationRunOutcome::Complete(evaluation) => { let views = build_curated_playlist_views( playlist, @@ -972,11 +968,11 @@ pub(super) fn curation_playlist_views( evaluation.memberships.len(), views.xtream.as_deref().map_or(0, <[PlaylistGroup]>::len) ); - Ok(views) + views } } } -fn curation_failure_error(target_name: &str, failure: &CurationFailure) -> TuliproxError { +fn warn_curation_failure(target_name: &str, failure: &CurationFailure) { let mut complete = 0usize; let mut incomplete = 0usize; let mut unavailable = 0usize; @@ -987,9 +983,9 @@ fn curation_failure_error(target_name: &str, failure: &CurationFailure) -> Tulip SelectorOutcome::Unavailable { .. } => unavailable += 1, } } - TuliproxError::RepositoryPlaylist(format!( - "Target '{target_name}' curation refresh failed (complete selectors: {complete}, incomplete: {incomplete}, unavailable: {unavailable}); existing finalized artifacts were retained" - )) + warn!( + "Target '{target_name}' curation refresh failed (complete selectors: {complete}, incomplete: {incomplete}, unavailable: {unavailable}); continuing with the regular catalog" + ); } fn item_subject_uuid(item: &PlaylistItem) -> UUIDType { diff --git a/backend/processing/src/processor/playlist/tests.rs b/backend/processing/src/processor/playlist/tests.rs index 80f856584..a177324aa 100644 --- a/backend/processing/src/processor/playlist/tests.rs +++ b/backend/processing/src/processor/playlist/tests.rs @@ -2751,9 +2751,7 @@ fn persist_filter_can_select_a_base_group() { async fn trakt_target_curation_is_a_noop_without_xtream_configuration() { let target = ConfigTarget::from(&ConfigTargetDto::default()); - let views = prepare_target_playlist_views(&reqwest::Client::new(), None, &target, Vec::new()) - .await - .expect("unconfigured curation should not fail"); + let views = prepare_target_playlist_views(&reqwest::Client::new(), None, &target, Vec::new()).await; assert!(views.base.is_empty()); assert!(views.xtream.is_none()); @@ -2761,7 +2759,7 @@ async fn trakt_target_curation_is_a_noop_without_xtream_configuration() { } #[tokio::test] -async fn unavailable_required_selector_returns_target_failure_instead_of_base_fallback() { +async fn unavailable_required_selector_continues_with_the_base_catalog() { let target = ConfigTarget::from(&ConfigTargetDto { name: "curation-failure".to_string(), output: vec![TargetOutputDto::Xtream(XtreamTargetOutputDto { @@ -2794,11 +2792,11 @@ async fn unavailable_required_selector_returns_target_failure_instead_of_base_fa xtream_cluster: XtreamCluster::Video, }]; - let error = prepare_target_playlist_views(&reqwest::Client::new(), None, &target, base) - .await - .expect_err("missing credentials must stop target publication"); - - assert!(error.message().contains("existing finalized artifacts were retained")); + let views = prepare_target_playlist_views(&reqwest::Client::new(), None, &target, base).await; + assert_eq!(views.base.len(), 1); + assert_eq!(views.base[0].channels[0].header.title.as_ref(), "Base movie"); + assert!(views.xtream.is_none()); + assert_eq!(views.publication_plan, PlaylistPublicationPlan::Ordinary); } mod curation_effect_gate { @@ -2933,12 +2931,12 @@ mod curation_effect_gate { } #[tokio::test] - async fn unavailable_curation_retains_seeded_artifacts_cache_and_watch_state() { - assert_curation_retains_seeded_state(None).await; + async fn unavailable_curation_preserves_the_empty_mixed_output_guard() { + assert_curation_preserves_empty_mixed_output_guard(None).await; } #[tokio::test] - async fn tmdb_and_mixed_failures_preserve_files_ids_cache_and_watches_under_full_and_curated() { + async fn tmdb_and_mixed_failures_preserve_the_empty_mixed_output_guard() { for policy in ["full", "curated"] { for mixed in [false, true] { let (url, server) = empty_trakt_server().await; @@ -2948,7 +2946,7 @@ mod curation_effect_gate { } let mut dto: shared::model::CurationConfigDto = serde_json::from_value(value).unwrap(); dto.prepare(&[TargetOutputDto::Xtream(XtreamTargetOutputDto::default())]).unwrap(); - assert_curation_retains_seeded_state(Some(dto)).await; + assert_curation_preserves_empty_mixed_output_guard(Some(dto)).await; if mixed { server.await.unwrap(); } else { @@ -2958,7 +2956,7 @@ mod curation_effect_gate { } } - async fn assert_curation_retains_seeded_state(canonical: Option) { + async fn assert_curation_preserves_empty_mixed_output_guard(canonical: Option) { let directory = tempdir().expect("tempdir"); let app_config = app_config(directory.path()); let playlist_state = Arc::new(PlaylistStorageState::new()); @@ -3040,7 +3038,9 @@ mod curation_effect_gate { let (result, errors) = finalize_prepared_target(Arc::new(context), prepared).await; - assert!(result.is_err()); + assert!(result.is_err_and(|errors| errors.iter().any(|error| { + error.message().contains("non-Xtream outputs that cannot safely publish a fully empty forced result") + }))); assert!(errors.is_empty()); assert_eq!(file_snapshot(directory.path()), before_files); let after_cache_len = playlist_state @@ -3050,6 +3050,7 @@ mod curation_effect_gate { .get("curation-effect-gate") .and_then(|storage| storage.xtream.as_ref()) .map_or(0, |storage| storage.vod.len()); + assert!(before_cache_len > 0); assert_eq!(after_cache_len, before_cache_len); } @@ -3179,7 +3180,7 @@ fn tmdb_complete_target_selection_preserves_live_and_keeps_xtream_aliases_out_of CurationMediaKind::Movie, 0, )])); - let views = target::curation_playlist_views(&target, playlist.clone(), &config, outcome).unwrap(); + let views = target::curation_playlist_views(&target, playlist.clone(), &config, outcome); assert_eq!( views.base.iter().flat_map(|g| &g.channels).map(|i| i.header.uuid).collect::>(), [live_id, id] @@ -3192,7 +3193,7 @@ fn tmdb_complete_target_selection_preserves_live_and_keeps_xtream_aliases_out_of assert!(views.xtream.is_none()); } let empty = CurationRunOutcome::Complete(complete_catalog_evaluation(Vec::new())); - let empty_views = target::curation_playlist_views(&target, playlist, &config, empty).unwrap(); + let empty_views = target::curation_playlist_views(&target, playlist, &config, empty); assert_eq!(empty_views.base.len(), 1); assert_eq!(empty_views.base[0].xtream_cluster, XtreamCluster::Live); assert_ne!(empty_views.publication_plan, PlaylistPublicationPlan::Ordinary); diff --git a/backend/processing/src/processor/playlist/tests/curation_publication.rs b/backend/processing/src/processor/playlist/tests/curation_publication.rs index 89f8d004c..ad84eac27 100644 --- a/backend/processing/src/processor/playlist/tests/curation_publication.rs +++ b/backend/processing/src/processor/playlist/tests/curation_publication.rs @@ -326,6 +326,35 @@ impl Publication { values } + async fn assert_regular_publication(&self, expected: &[PlaylistGroup]) { + if self.target.get_xtream_output().is_some() { + let rows = self.xtream_rows().await; + for item in expected.iter().flat_map(|group| &group.channels) { + assert!(rows.iter().any(|row| row.title == item.header.title && row.url == item.header.url)); + } + assert!(rows.iter().all(|row| !row.group.starts_with("TMDB") && !row.group.starts_with("Trakt"))); + } + assert!(std::fs::read_to_string(self.directory.path().join("published.m3u")).is_ok_and(|text| { + expected + .iter() + .flat_map(|group| &group.channels) + .filter(|item| item.header.item_type != PlaylistItemType::SeriesInfo) + .all(|item| text.contains(item.header.title.as_ref()) && text.contains(item.header.url.as_ref())) + })); + let strm = self.strm_contents(); + assert!(!strm.is_empty()); + let files = file_snapshot(self.directory.path()); + let live_title = expected[0].channels[0].header.title.as_bytes(); + assert!( + files.iter().any(|(path, contents)| { + path.to_string_lossy().contains("epg") + && contents.windows(live_title.len()).any(|part| part == live_title) + }), + "new EPG programme must be published" + ); + assert!(!self.cache_signature().await.is_empty()); + } + async fn identity_signature(&self) -> BTreeMap { let cache = self.context.playlist_state.as_ref().unwrap().data.read().await; cache[&self.target.name] @@ -464,7 +493,7 @@ async fn tmdb_https_empty_and_no_match_clear_published_vod_series_but_keep_live( } #[tokio::test] -async fn either_https_failure_retains_every_published_file_id_cache_and_watch_in_full_and_curated() { +async fn either_https_failure_continues_regular_publication_in_full_and_curated() { for policy in ["full", "curated"] { for (route, status, body) in [ (MOVIES, 503, "{}"), @@ -473,20 +502,30 @@ async fn either_https_failure_retains_every_published_file_id_cache_and_watch_in ] { let server = DiscoveryServer::start().await; let run = Publication::new(&server, &target(policy, true, true)); - let initial = file_snapshot(run.directory.path()); server.reply(route, status, body); - assert!(run.publish().await.is_err(), "first run must not publish a successful sibling"); - assert_eq!(file_snapshot(run.directory.path()), initial); - assert!(run.cache_signature().await.is_empty()); + assert!(run.publish().await.is_ok()); + run.assert_regular_publication(&catalog()).await; server.reply(route, 200, if route == TRAKT { TRAKT_PAGE } else { MOVIE_PAGE }); - run.publish().await.unwrap(); - let files = file_snapshot(run.directory.path()); - let cache = run.cache_signature().await; - assert!(!files.is_empty() && !cache.is_empty()); + assert!(run.publish().await.is_ok()); server.reply(route, status, body); - assert!(run.publish().await.is_err()); - assert_eq!(file_snapshot(run.directory.path()), files); - assert_eq!(run.cache_signature().await, cache); + assert!(run.publish_catalog(item_limit::changed_catalog()).await.is_ok()); + run.assert_regular_publication(&item_limit::changed_catalog()).await; + } + } +} + +#[tokio::test] +async fn missing_tmdb_credentials_publish_playlists_and_epg_with_and_without_xtream() { + for policy in ["full", "curated"] { + for xtream in [false, true] { + let server = DiscoveryServer::start().await; + let mut run = Publication::new(&server, &target(policy, false, xtream)); + if let Some(source) = run.target.curation.as_mut().and_then(|config| config.tmdb.as_mut()) { + source.api.access_token.clear(); + } + assert!(run.publish().await.is_ok()); + run.assert_regular_publication(&catalog()).await; + assert!(server.requests.lock().is_ok_and(|requests| requests.is_empty())); } } } diff --git a/backend/processing/src/processor/playlist/tests/curation_publication/client_diagnostics.rs b/backend/processing/src/processor/playlist/tests/curation_publication/client_diagnostics.rs index 86745363f..fd9ce73e1 100644 --- a/backend/processing/src/processor/playlist/tests/curation_publication/client_diagnostics.rs +++ b/backend/processing/src/processor/playlist/tests/curation_publication/client_diagnostics.rs @@ -3,7 +3,7 @@ use target::build_target_tmdb_client; use tuliprox_core::{model::ProxyConfig, utils::network::request::create_tmdb_client}; #[tokio::test] -async fn curation_tmdb_client_construction_reports_only_safe_phases_and_keeps_publication_closed() { +async fn curation_tmdb_client_construction_reports_safe_phases_and_continues_publication() { for policy in ["full", "curated"] { for phase in ["phase=profile_configuration", "phase=client_build"] { let server = DiscoveryServer::start().await; @@ -12,7 +12,6 @@ async fn curation_tmdb_client_construction_reports_only_safe_phases_and_keeps_pu if previously_published { run.publish().await.unwrap(); } - let files = file_snapshot(run.directory.path()); let cache = run.cache_signature().await; assert_eq!(!cache.is_empty(), previously_published); let before = server.requests.lock().unwrap().len(); @@ -43,7 +42,7 @@ async fn curation_tmdb_client_construction_reports_only_safe_phases_and_keeps_pu assert_eq!(diagnostics, [phase]); assert!(!diagnostics[0].contains("synthetic-")); let mut changed = catalog(); - changed[0].channels[0].header.title = "Must not publish".intern(); + changed[0].channels[0].header.title = "Updated Live".intern(); let prepared = PreparedTarget { target: run.target.clone(), playlist: changed, @@ -62,12 +61,12 @@ async fn curation_tmdb_client_construction_reports_only_safe_phases_and_keeps_pu ) .await .unwrap(); - assert!(result.is_err()); + assert!(result.is_ok()); assert!(errors.is_empty()); - assert_eq!(file_snapshot(run.directory.path()), files); - assert_eq!(run.cache_signature().await, cache); + assert!(run.xtream_rows().await.iter().any(|row| row.title.as_ref() == "Updated Live")); + assert_ne!(run.cache_signature().await, cache); let requests = server.requests.lock().unwrap(); - assert_eq!(requests.len(), before + 1, "complete Trakt sibling cannot authorize publication"); + assert_eq!(requests.len(), before + 1, "failed TMDB transport must not use the generic client"); assert!(requests[before].starts_with(&format!("GET {TRAKT} "))); } } diff --git a/backend/processing/src/processor/playlist/tests/curation_publication/item_limit.rs b/backend/processing/src/processor/playlist/tests/curation_publication/item_limit.rs index f49654598..52cff6edd 100644 --- a/backend/processing/src/processor/playlist/tests/curation_publication/item_limit.rs +++ b/backend/processing/src/processor/playlist/tests/curation_publication/item_limit.rs @@ -1,6 +1,6 @@ use super::*; -fn changed_catalog() -> Vec { +pub(super) fn changed_catalog() -> Vec { let mut playlist = catalog(); for group in &mut playlist { for item in &mut group.channels { @@ -63,7 +63,7 @@ fn fail_late(server: &DiscoveryServer, failure: &str) { } #[tokio::test] -async fn curation_late_failure_keeps_all_files_epg_cache_mapping_and_watches_in_both_policies() { +async fn curation_late_failure_continues_playlists_epg_cache_and_watches_in_both_policies() { for policy in ["full", "curated"] { for failure in [ "status", @@ -79,52 +79,30 @@ async fn curation_late_failure_keeps_all_files_epg_cache_mapping_and_watches_in_ ] { let server = DiscoveryServer::start().await; let mut dto = target(policy, true, true); - // A selection-only selector is just as required as its successful projecting siblings. - dto.curation.as_mut().unwrap().tmdb.as_mut().unwrap().trending[0].create_xtream_category = false; + if let Some(source) = dto.curation.as_mut().and_then(|config| config.tmdb.as_mut()) { + source.trending[0].create_xtream_category = false; + } let run = Publication::new(&server, &dto); let successful_replies = server.replies.lock().unwrap().clone(); - let initial = file_snapshot(run.directory.path()); fail_late(&server, failure); - assert!(run.publish_catalog(changed_catalog()).await.is_err(), "{policy}/{failure}: first refresh"); - assert_eq!(file_snapshot(run.directory.path()), initial); - assert!(run.context.playlist_state.as_ref().unwrap().data.read().await.is_empty()); - assert!(run.cache_signature().await.is_empty()); - let count = server.requests.lock().unwrap().len(); - assert_eq!( - count, - if failure == "requests" { 34 } else { 4 }, - "Trakt and TV are evaluated too, but not published" - ); + assert!(run.publish_catalog(changed_catalog()).await.is_ok(), "{policy}/{failure}"); + run.assert_regular_publication(&changed_catalog()).await; + assert_eq!(server.requests.lock().unwrap().len(), if failure == "requests" { 34 } else { 4 }); *server.replies.lock().unwrap() = successful_replies; - run.publish().await.unwrap(); - let files = file_snapshot(run.directory.path()); - assert!( - files.keys().any(|path| path.to_string_lossy().contains("epg")), - "EPG must be in the retention inventory: {:?}", - files.keys().collect::>() - ); - let cache = run.cache_signature().await; - assert!(cache.iter().any(|entry| entry.starts_with("mapping:"))); - let watch_path = run.directory.path().join("publication.groups.bin"); - let watch_before: std::collections::BTreeSet> = - tuliprox_core::utils::binary_deserialize(&std::fs::read(&watch_path).unwrap()).unwrap(); - assert!(!watch_before.is_empty()); + assert!(run.publish().await.is_ok()); fail_late(&server, failure); - assert!( - run.publish_catalog(changed_catalog()).await.is_err(), - "{policy}/{failure}: refresh with prior state" - ); - assert_eq!(file_snapshot(run.directory.path()), files, "inventory and bytes, not just file sizes"); - assert_eq!(run.cache_signature().await, cache, "all cached rows and mapping records, including timestamps"); - let watch_after: std::collections::BTreeSet> = - tuliprox_core::utils::binary_deserialize(&std::fs::read(&watch_path).unwrap()).unwrap(); - assert_eq!(watch_before, watch_after); + assert!(run.publish_catalog(changed_catalog()).await.is_ok()); + run.assert_regular_publication(&changed_catalog()).await; + assert!(std::fs::read(run.directory.path().join("publication.groups.bin")).is_ok_and(|bytes| { + tuliprox_core::utils::binary_deserialize::>>(&bytes) + .is_ok_and(|groups| groups.iter().any(|group| group.as_ref() == "Movies")) + })); } } } #[tokio::test] -async fn curation_shared_batch_exhaustion_marks_pending_selectors_required_without_129th_get() { +async fn curation_shared_batch_exhaustion_continues_publication_without_129th_get() { for policy in ["full", "curated"] { let server = DiscoveryServer::start().await; let dto = target(policy, false, true); @@ -136,21 +114,16 @@ async fn curation_shared_batch_exhaustion_marks_pending_selectors_required_witho selector.create_xtream_category = false; source.trending = vec![selector; 129]; let failing = run.target.clone(); - let initial = file_snapshot(run.directory.path()); - assert!(run.publish().await.is_err()); + assert!(run.publish().await.is_ok()); assert_eq!(server.requests.lock().unwrap().len(), 128); - assert_eq!(file_snapshot(run.directory.path()), initial); - assert!(run.cache_signature().await.is_empty()); + run.assert_regular_publication(&catalog()).await; run.target = ordinary; - run.publish().await.unwrap(); - let before = file_snapshot(run.directory.path()); - let cache = run.cache_signature().await; + assert!(run.publish().await.is_ok()); let requests = server.requests.lock().unwrap().len(); run.target = failing; - assert!(run.publish_catalog(changed_catalog()).await.is_err()); + assert!(run.publish_catalog(changed_catalog()).await.is_ok()); assert_eq!(server.requests.lock().unwrap().len() - requests, 128); - assert_eq!(file_snapshot(run.directory.path()), before); - assert_eq!(run.cache_signature().await, cache); + run.assert_regular_publication(&changed_catalog()).await; } } @@ -235,7 +208,7 @@ async fn curation_full_empty_or_no_match_removes_projections_not_ordinary_catalo } #[tokio::test] -async fn curation_profile_requires_the_fixture_ca_and_has_no_unconfigured_fallback() { +async fn curation_profile_requires_the_fixture_ca_and_continues_regular_publication() { let server = DiscoveryServer::start().await; let mut run = Publication::new(&server, &target("full", false, true)); run.tmdb_client = tuliprox_core::utils::network::request::create_tmdb_client(&run.context.config) @@ -244,9 +217,9 @@ async fn curation_profile_requires_the_fixture_ca_and_has_no_unconfigured_fallba .resolve("api.themoviedb.org", server.address) .build() .unwrap(); - assert!(run.publish().await.is_err(), "untrusted CA fails rather than disabling TLS verification"); + assert!(run.publish().await.is_ok()); + run.assert_regular_publication(&catalog()).await; assert!(server.requests.lock().unwrap().is_empty()); - assert!(file_snapshot(run.directory.path()).is_empty()); } #[tokio::test] @@ -266,17 +239,17 @@ async fn curation_production_wiring_does_not_fall_back_to_the_generic_client() { library_empty: tuliprox_repository::LibraryEmptyPublication::None, }; let (result, _) = finalize_prepared_target(Arc::clone(&run.context), prepared).await; - assert!(result.is_err()); + assert!(result.is_ok()); assert!( server.requests.lock().unwrap().is_empty(), "generic client has the fixture CA and could succeed, but is not used" ); - assert!(run.cache_signature().await.is_empty()); - assert!(file_snapshot(run.directory.path()).is_empty()); + assert!(!run.cache_signature().await.is_empty()); + assert!(!file_snapshot(run.directory.path()).is_empty()); } #[tokio::test] -async fn curation_does_not_authorize_empty_inputs_and_authorized_empty_still_waits_for_all_selectors() { +async fn curation_failure_respects_empty_input_authorization() { for authorized in [false, true] { let server = DiscoveryServer::start().await; let run = Publication::new(&server, &target("curated", false, true)); @@ -292,7 +265,7 @@ async fn curation_does_not_authorize_empty_inputs_and_authorized_empty_still_wai let (result, _) = target::finalize_prepared_target_with_tmdb(Arc::clone(&run.context), prepared, Some(&run.tmdb_client)) .await; - assert_eq!(result.is_err(), authorized); + assert_eq!(result.is_err(), authorized, "ordinary M3U persistence still rejects an empty playlist"); assert_eq!(server.requests.lock().unwrap().len(), if authorized { 3 } else { 0 }); assert!(file_snapshot(run.directory.path()).is_empty()); assert!(run.cache_signature().await.is_empty()); diff --git a/backend/processing/src/processor/playlist/tests/curation_publication/tls_profile.rs b/backend/processing/src/processor/playlist/tests/curation_publication/tls_profile.rs index 821342f0b..a512077b1 100644 --- a/backend/processing/src/processor/playlist/tests/curation_publication/tls_profile.rs +++ b/backend/processing/src/processor/playlist/tests/curation_publication/tls_profile.rs @@ -72,7 +72,7 @@ async fn tmdb_profile_tls_does_not_change_generic_clients_global_insecure_policy } #[tokio::test] -async fn tmdb_profile_tls_failure_blocks_publication_and_retains_artifacts_even_with_complete_trakt() { +async fn tmdb_profile_tls_failure_continues_publication_without_bypassing_verification() { for policy in ["full", "curated"] { let server = DiscoveryServer::start().await; let mut run = Publication::new(&server, &target(policy, true, true)); @@ -88,16 +88,15 @@ async fn tmdb_profile_tls_failure_blocks_publication_and_retains_artifacts_even_ run.tmdb_client = trusted.clone(); run.publish().await.unwrap(); } - let files = file_snapshot(run.directory.path()); let cache = run.cache_signature().await; assert_eq!(!cache.is_empty(), previously_published); let before = server.requests.lock().unwrap().len(); run.tmdb_client = rejected.clone(); let mut changed = catalog(); - changed[0].channels[0].header.title = "Must not publish".intern(); - assert!(run.publish_catalog(changed).await.is_err()); - assert_eq!(file_snapshot(run.directory.path()), files, "all files, IDs and watches retained"); - assert_eq!(run.cache_signature().await, cache, "no partial cache publication"); + changed[0].channels[0].header.title = "Updated Live".intern(); + assert!(run.publish_catalog(changed.clone()).await.is_ok()); + run.assert_regular_publication(&changed).await; + assert_ne!(run.cache_signature().await, cache); let requests = server.requests.lock().unwrap(); assert_eq!(requests.len(), before + 1, "only the required Trakt sibling reached HTTP; no fallback"); assert!(requests[before].starts_with(&format!("GET {TRAKT} ")));