use crate::Cirrus;
use crate::error::{CirrusError, CirrusResult};
use crate::response::ApiVersion;
impl Cirrus {
pub async fn versions(&self) -> CirrusResult<Vec<ApiVersion>> {
self.get("/services/data").await
}
pub async fn latest_api_version(&self) -> CirrusResult<String> {
let list = self.versions().await?;
let latest = ApiVersion::latest(&list).ok_or_else(|| {
CirrusError::InvalidResponse("/services/data returned no parseable API versions".into())
})?;
Ok(format!("v{}", latest.version))
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use crate::Cirrus;
use crate::auth::StaticTokenAuth;
use std::sync::Arc;
use wiremock::matchers::{header, method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
#[tokio::test]
async fn versions_returns_parsed_array() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/services/data"))
.and(header("authorization", "Bearer tok"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([
{"label": "Winter '24", "url": "/services/data/v66.0", "version": "60.0"},
{"label": "Spring '24", "url": "/services/data/v61.0", "version": "61.0"}
])))
.mount(&server)
.await;
let auth = Arc::new(StaticTokenAuth::new("tok", server.uri()));
let sf = Cirrus::builder().auth(auth).build().unwrap();
let versions = sf.versions().await.unwrap();
assert_eq!(versions.len(), 2);
assert_eq!(versions[0].version, "60.0");
assert_eq!(versions[1].label, "Spring '24");
}
#[tokio::test]
async fn versions_surfaces_api_error_array() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/services/data"))
.respond_with(ResponseTemplate::new(401).set_body_json(serde_json::json!([
{"message": "Session expired or invalid", "errorCode": "INVALID_SESSION_ID"}
])))
.mount(&server)
.await;
let auth = Arc::new(StaticTokenAuth::new("tok", server.uri()));
let sf = Cirrus::builder().auth(auth).build().unwrap();
let err = sf.versions().await.unwrap_err();
match err {
crate::CirrusError::Api {
status,
errors,
raw,
} => {
assert_eq!(status, 401);
assert_eq!(errors.len(), 1);
assert_eq!(errors[0].error_code, "INVALID_SESSION_ID");
assert!(raw.is_none());
}
other => panic!("expected Api error, got {other:?}"),
}
}
#[test]
fn api_version_number_parses_major_minor() {
let v = crate::ApiVersion {
label: "Spring '26".into(),
url: "/services/data/v66.0".into(),
version: "66.0".into(),
};
assert_eq!(v.version_number(), Some((66, 0)));
}
#[test]
fn api_version_number_returns_none_for_malformed() {
let v = crate::ApiVersion {
label: "x".into(),
url: "/services/data/vNaN".into(),
version: "not-a-version".into(),
};
assert_eq!(v.version_number(), None);
}
#[test]
fn latest_picks_highest_by_numeric_not_lexical_ordering() {
let versions = vec![
crate::ApiVersion {
label: "x".into(),
url: "/x".into(),
version: "9.0".into(),
},
crate::ApiVersion {
label: "x".into(),
url: "/x".into(),
version: "60.0".into(),
},
crate::ApiVersion {
label: "x".into(),
url: "/x".into(),
version: "10.0".into(),
},
];
let latest = crate::ApiVersion::latest(&versions).unwrap();
assert_eq!(latest.version, "60.0");
}
#[test]
fn latest_returns_none_for_empty_slice() {
assert!(crate::ApiVersion::latest(&[]).is_none());
}
#[tokio::test]
async fn latest_api_version_returns_v_prefixed_highest() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/services/data"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([
{"label": "Winter '24", "url": "/services/data/v66.0", "version": "60.0"},
{"label": "Spring '26", "url": "/services/data/v66.0", "version": "66.0"},
{"label": "Summer '25", "url": "/services/data/v64.0", "version": "64.0"}
])))
.mount(&server)
.await;
let auth = Arc::new(StaticTokenAuth::new("tok", server.uri()));
let sf = Cirrus::builder().auth(auth).build().unwrap();
let latest = sf.latest_api_version().await.unwrap();
assert_eq!(latest, "v66.0");
}
#[tokio::test]
async fn build_with_latest_version_reconfigures_client() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/services/data"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([
{"label": "Spring '26", "url": "/services/data/v66.0", "version": "66.0"},
{"label": "Winter '24", "url": "/services/data/v66.0", "version": "60.0"}
])))
.mount(&server)
.await;
let auth = Arc::new(StaticTokenAuth::new("tok", server.uri()));
let sf = Cirrus::builder()
.auth(auth)
.build_with_latest_version()
.await
.unwrap();
assert_eq!(sf.api_version(), "v66.0");
let url = sf.resolve_url("limits");
assert!(url.contains("/v66.0/limits"), "expected v66.0 in {url}");
}
}