Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 45 additions & 2 deletions rust/src/domain/agreements.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use serde_json::json;

use domains_client::types;

use super::common::{api_error, make_client, string_list};
use super::common::{api_error, comma_joined, make_client, string_list};
use crate::next_action::next_action;
use crate::scopes::DOMAINS_READ;

Expand Down Expand Up @@ -55,7 +55,13 @@ pub(super) fn command() -> RuntimeCommandSpec {
.unwrap_or(false);
let debug = !ctx.middleware.debug.is_empty();
let client = make_client(&ctx).await?;
let resp = match client.agreements().tlds(tlds).privacy(privacy).send().await {
let resp = match client
.agreements()
.tlds(comma_joined(tlds))
.privacy(privacy)
.send()
.await
{
Ok(r) => r,
Err(e) => return Err(api_error("retrieving legal agreements", debug, e).await),
};
Expand Down Expand Up @@ -88,3 +94,40 @@ pub(super) fn command() -> RuntimeCommandSpec {
},
)
}

#[cfg(test)]
mod tests {
use super::super::common::comma_joined;

#[tokio::test]
async fn tlds_are_sent_as_a_single_comma_joined_query_param() {
// Regression for DEVEX-882: the v1 `tlds` query param is OpenAPI
// `style: form, explode: false` — one comma-joined value. progenitor's
// generated `tlds()` setter always seq-serializes a `Vec` as repeated
// `tlds=` pairs, so callers must join multiple `--tld` occurrences into
// a single element before calling it, or the API rejects the request.
let server = httpmock::MockServer::start_async().await;
let mock = server
.mock_async(|when, then| {
when.method(httpmock::Method::GET)
.path("/v1/domains/agreements")
.query_param("tlds", "com,net,io");
then.status(200).json_body(serde_json::json!([]));
})
.await;
let client =
domains_client::client_with_auth(&server.base_url(), "Bearer tok", "test", "req-1")
.expect("build client");

let tlds = comma_joined(vec!["com".to_string(), "net".to_string(), "io".to_string()]);
client
.agreements()
.tlds(tlds)
.privacy(false)
.send()
.await
.expect("request succeeds");

mock.assert_async().await;
}
}
41 changes: 41 additions & 0 deletions rust/src/domain/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,23 @@ pub(crate) fn string_list(ctx: &CommandContext, key: &str) -> Vec<String> {
}
}

/// Collapse a repeatable CLI flag's values into the single query-string value
/// some domains-client endpoints require (e.g. `tlds`, whose OpenAPI param is
/// `style: form, explode: false` — one comma-joined value). progenitor's
/// generated setters always seq-serialize a `Vec` argument as repeated
/// `key=value` pairs regardless of the spec's `explode` setting, so passing
/// multiple `--tlds` occurrences straight through sends `tlds=com&tlds=net`
/// and the API rejects it (`400 MISMATCH_FORMAT`, DEVEX-882). Joining into a
/// single element before calling the setter produces the one pair the API
/// expects. `[]` stays `[]` so callers can still gate on "no filter given".
pub(crate) fn comma_joined(values: Vec<String>) -> Vec<String> {
if values.len() <= 1 {
values
} else {
vec![values.join(",")]
}
}

/// Turn a domains-client error into a `CliCoreError`, reading the response body
/// for unexpected (non-2xx) responses so the API's actual message isn't lost
/// (progenitor's `Display` prints only the status). Async because reading the
Expand Down Expand Up @@ -329,6 +346,30 @@ fn split_camel_case(s: &str) -> String {
mod tests {
use super::*;

#[test]
fn comma_joined_collapses_multiple_values_into_one_element() {
// Regression for DEVEX-882: repeated `--tlds`/`--tld` values must become
// one comma-joined query element, not stay as separate elements (which
// progenitor would send as repeated `tlds=` pairs).
assert_eq!(
comma_joined(vec!["com".to_string(), "net".to_string(), "io".to_string()]),
vec!["com,net,io".to_string()]
);
}

#[test]
fn comma_joined_single_value_is_unchanged() {
assert_eq!(
comma_joined(vec!["com".to_string()]),
vec!["com".to_string()]
);
}

#[test]
fn comma_joined_empty_stays_empty() {
assert_eq!(comma_joined(Vec::<String>::new()), Vec::<String>::new());
}

fn money(value: Option<i64>, currency: &str) -> types::SimpleMoney {
types::SimpleMoney {
value,
Expand Down
41 changes: 39 additions & 2 deletions rust/src/domain/suggest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ use serde_json::json;

use domains_client::types;

use super::common::{api_error, format_money, make_client, string_list, term_for_period};
use super::common::{
api_error, comma_joined, format_money, make_client, string_list, term_for_period,
};
use crate::next_action::next_action;
use crate::output_schema::output_schema;
use crate::scopes::DOMAINS_READ;
Expand Down Expand Up @@ -137,7 +139,7 @@ pub(super) fn command() -> RuntimeCommandSpec {
req = req.page_size(page_size);
}
if !tlds.is_empty() {
req = req.tlds(tlds);
req = req.tlds(comma_joined(tlds));
}
if let Some(n) = length_min.and_then(nonzero) {
req = req.length_min(n);
Expand All @@ -163,6 +165,7 @@ pub(super) fn command() -> RuntimeCommandSpec {

#[cfg(test)]
mod tests {
use super::super::common::comma_joined;
use super::{nonzero, suggestion_to_json};
use domains_client::types;

Expand All @@ -175,6 +178,40 @@ mod tests {
assert_eq!(nonzero(5).map(|n| n.get()), Some(5));
}

#[tokio::test]
async fn tlds_are_sent_as_a_single_comma_joined_query_param() {
// Regression for DEVEX-882: the `tlds` query param is OpenAPI `style:
// form, explode: false` — one comma-joined value. progenitor's generated
// `tlds()` setter always seq-serializes a `Vec` as repeated `tlds=`
// pairs, so callers must join multiple `--tlds` occurrences into a
// single element before calling it, or the API replies `400
// MISMATCH_FORMAT`.
let server = httpmock::MockServer::start_async().await;
let mock = server
.mock_async(|when, then| {
when.method(httpmock::Method::GET)
.path("/v3/domains/suggestions")
.query_param("tlds", "com,net,io");
then.status(200)
.json_body(serde_json::json!({ "items": [] }));
})
.await;
let client =
domains_client::client_with_auth(&server.base_url(), "Bearer tok", "test", "req-1")
.expect("build client");

let tlds = comma_joined(vec!["com".to_string(), "net".to_string(), "io".to_string()]);
client
.suggest_domains()
.query("pizza")
.tlds(tlds)
.send()
.await
.expect("request succeeds");

mock.assert_async().await;
}

fn money(value: i64, currency: &str) -> types::SimpleMoney {
types::SimpleMoney {
value: Some(value),
Expand Down