Skip to content

Commit ba4847a

Browse files
fix(catalog): reject unsupported Spotify shelf filters
Use existing cross-field admission rules for library and recommendation shelves, matching the pinned native AppWorld provider. Invalid combinations fail before HTTP dispatch rather than being silently ignored. Validation: spotify_shelves_reject_unsupported_filters_before_dispatch packaged integration test; three valid requests and four rejected combinations with no rejected-request dispatch.
1 parent b511d96 commit ba4847a

2 files changed

Lines changed: 122 additions & 1 deletion

File tree

‎apis/appworld/spotify/domain.yaml‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
version: 39
1+
version: 40
22
entry_id: spotify
33
auth:
44
scheme: none
@@ -2232,6 +2232,16 @@ capabilities:
22322232
value_ref: nv_song_shelf
22332233
required: true
22342234
description: catalog | library | recommendations
2235+
input_validation:
2236+
cross_field_rules:
2237+
- when: {field: shelf, equals: library}
2238+
rule_type: forbidden
2239+
fields: [artist_id, album_id, genre, min_release_date, max_release_date, min_duration, max_duration, min_rating, max_rating, min_like_count, max_like_count, min_play_count, max_play_count]
2240+
error_message: Library songs do not support catalog filters; apply predicates to the returned saved songs.
2241+
- when: {field: shelf, equals: recommendations}
2242+
rule_type: forbidden
2243+
fields: [artist_id, album_id, genre, min_release_date, max_release_date, min_duration, max_duration, min_rating, max_rating, min_like_count, max_like_count, min_play_count, max_play_count, query, sort_by]
2244+
error_message: Recommended songs support pagination only; apply content predicates to the returned songs.
22352245
# Common list fields across catalog, library, and recommendations shelves.
22362246
# Genre and play statistics are supplied by song_get hydration.
22372247
provides:

‎crates/plasm-e2e/tests/appworld_catalog_contracts.rs‎

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,117 @@ use std::sync::{
88
Arc,
99
};
1010

11+
#[tokio::test]
12+
async fn spotify_shelves_reject_unsupported_filters_before_dispatch() {
13+
let count = Arc::new(AtomicUsize::new(0));
14+
let seen = count.clone();
15+
let app = axum::Router::new().route(
16+
"/spotify/{*path}",
17+
axum::routing::get(
18+
move |axum::extract::Path(path): axum::extract::Path<String>,
19+
axum::extract::Query(q): axum::extract::Query<
20+
std::collections::HashMap<String, String>,
21+
>| {
22+
let seen = seen.clone();
23+
async move {
24+
match path.as_str() {
25+
"songs" => assert!(q.contains_key("min_release_date")),
26+
"library/songs" => {
27+
assert_eq!(q.get("query").map(String::as_str), Some("probe"));
28+
assert!(q.keys().all(|key| [
29+
"query",
30+
"sort_by",
31+
"page_index",
32+
"page_limit"
33+
]
34+
.contains(&key.as_str())));
35+
}
36+
"recommendations" => {
37+
assert!(q
38+
.keys()
39+
.all(|key| ["page_index", "page_limit"].contains(&key.as_str())))
40+
}
41+
_ => panic!("unexpected shelf route"),
42+
}
43+
seen.fetch_add(1, Ordering::SeqCst);
44+
axum::Json(serde_json::json!([]))
45+
}
46+
},
47+
),
48+
);
49+
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
50+
let url = format!("http://{}", listener.local_addr().unwrap());
51+
let server = tokio::spawn(async move { axum::serve(listener, app).await.unwrap() });
52+
let cgs = plasm_core::load_schema_dir(
53+
&std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../apis/appworld/spotify"),
54+
)
55+
.unwrap();
56+
let compiled = Arc::new(plasm_compile::compile_cgs_capability_templates(&cgs).unwrap());
57+
let engine = ExecutionEngine::new(ExecutionConfig {
58+
base_url: Some(url),
59+
..Default::default()
60+
})
61+
.unwrap();
62+
for source in [
63+
r#"Song{access_token="fixture",shelf="catalog",min_release_date="2030-01-02T00:00:00"}"#,
64+
r#"Song{access_token="fixture",shelf="library",query="probe"}"#,
65+
r#"Song{access_token="fixture",shelf="recommendations"}"#,
66+
] {
67+
let expr = plasm_core::expr_parser::parse_session_line(source, &cgs, None)
68+
.unwrap()
69+
.expr;
70+
engine
71+
.execute(
72+
&expr,
73+
&cgs,
74+
&mut SessionMaterialization::new(),
75+
None,
76+
StreamConsumeOpts::default(),
77+
ExecuteOptions {
78+
compiled_catalog: Some(compiled.clone()),
79+
..Default::default()
80+
},
81+
)
82+
.await
83+
.unwrap();
84+
}
85+
for (shelf, input) in [
86+
("library", r#"min_release_date="2030-01-02T00:00:00""#),
87+
("library", r#"genre="rock""#),
88+
("recommendations", r#"query="probe""#),
89+
("recommendations", r#"min_duration=1"#),
90+
] {
91+
let source = format!(r#"Song{{access_token="fixture",shelf="{shelf}",{input}}}"#);
92+
let expr = plasm_core::expr_parser::parse_session_line(&source, &cgs, None)
93+
.unwrap()
94+
.expr;
95+
let error = engine
96+
.execute(
97+
&expr,
98+
&cgs,
99+
&mut SessionMaterialization::new(),
100+
None,
101+
StreamConsumeOpts::default(),
102+
ExecuteOptions {
103+
compiled_catalog: Some(compiled.clone()),
104+
..Default::default()
105+
},
106+
)
107+
.await
108+
.unwrap_err();
109+
assert!(
110+
error.to_string().contains(if shelf == "library" {
111+
"Library songs do not support catalog filters"
112+
} else {
113+
"Recommended songs support pagination only"
114+
}),
115+
"{error}"
116+
);
117+
}
118+
assert_eq!(count.load(Ordering::SeqCst), 3);
119+
server.abort();
120+
}
121+
11122
#[tokio::test]
12123
async fn phone_message_variant_contracts_dispatch_only_supported_parameters() {
13124
let count = Arc::new(AtomicUsize::new(0));

0 commit comments

Comments
 (0)