mirror of
https://github.com/euzu/tuliprox.git
synced 2026-10-18 13:52:20 +02:00
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.
This commit is contained in:
@@ -491,7 +491,7 @@ pub(crate) async fn finalize_prepared_target<E: EventSink + Clone + 'static, M:
|
||||
ctx: Arc<PlaylistProcessingContext<E, M>>,
|
||||
prepared: PreparedTarget,
|
||||
) -> (Result<(), Vec<TuliproxError>>, Vec<TuliproxError>) {
|
||||
// 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<E: EventSink + Clone + 's
|
||||
}
|
||||
|
||||
let eligible_catalog = prepare_eligible_catalog(target, new_playlist, &mut step);
|
||||
let views = match prepare_target_playlist_views(&ctx.client, tmdb_client, target, eligible_catalog).await {
|
||||
Ok(views) => 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<PlaylistGroup>,
|
||||
) -> Result<TargetPlaylistViews, TuliproxError> {
|
||||
) -> 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<PlaylistGroup>,
|
||||
config: &CurationConfig,
|
||||
outcome: CurationRunOutcome,
|
||||
) -> Result<TargetPlaylistViews, TuliproxError> {
|
||||
) -> 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 {
|
||||
|
||||
@@ -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<shared::model::CurationConfigDto>) {
|
||||
async fn assert_curation_preserves_empty_mixed_output_guard(canonical: Option<shared::model::CurationConfigDto>) {
|
||||
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::<Vec<_>>(),
|
||||
[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);
|
||||
|
||||
@@ -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<String, (u32, u32)> {
|
||||
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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+6
-7
@@ -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} ")));
|
||||
}
|
||||
}
|
||||
|
||||
+29
-56
@@ -1,6 +1,6 @@
|
||||
use super::*;
|
||||
|
||||
fn changed_catalog() -> Vec<PlaylistGroup> {
|
||||
pub(super) fn changed_catalog() -> Vec<PlaylistGroup> {
|
||||
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::<Vec<_>>()
|
||||
);
|
||||
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<Arc<str>> =
|
||||
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<Arc<str>> =
|
||||
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::<std::collections::BTreeSet<Arc<str>>>(&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());
|
||||
|
||||
@@ -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} ")));
|
||||
|
||||
Reference in New Issue
Block a user