feat: add apps endpoint with list, get, and batch-get methods

Implement App model and three endpoint methods: list_apps (paginated),
get_app (by ID), and batch_get_apps (up to 100 by IDs). Add
comprehensive tests for success, pagination, not-found, and batch
scenarios.
This commit is contained in:
Stevan Freeborn
2026-03-25 13:37:55 -05:00
parent 6735bca09f
commit fe883a9dd2
5 changed files with 165 additions and 0 deletions
+32
View File
@@ -0,0 +1,32 @@
use reqwest::Method;
use crate::client::OnspringClient;
use crate::error::Result;
use crate::models::{App, CollectionResponse, PagedResponse, PagingRequest};
impl OnspringClient {
/// Gets all apps for the current client, with optional pagination.
pub async fn list_apps(&self, paging: Option<PagingRequest>) -> Result<PagedResponse<App>> {
let mut query = Vec::new();
if let Some(p) = paging {
query.push(("PageNumber", p.page_number.to_string()));
query.push(("PageSize", p.page_size.to_string()));
}
let query_refs: Vec<(&str, String)> = query.iter().map(|(k, v)| (*k, v.clone())).collect();
self.request(Method::GET, "/Apps", &query_refs, Option::<&()>::None)
.await
}
/// Gets an app by its identifier.
pub async fn get_app(&self, app_id: i32) -> Result<App> {
let path = format!("/Apps/id/{}", app_id);
self.request(Method::GET, &path, &[], Option::<&()>::None)
.await
}
/// Gets up to 100 apps by their identifiers.
pub async fn batch_get_apps(&self, ids: &[i32]) -> Result<CollectionResponse<App>> {
self.request(Method::POST, "/Apps/batch-get", &[], Some(&ids))
.await
}
}
+1
View File
@@ -1 +1,2 @@
mod apps;
mod ping;
+10
View File
@@ -0,0 +1,10 @@
use serde::Deserialize;
/// Represents an Onspring application.
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct App {
pub href: Option<String>,
pub id: i32,
pub name: Option<String>,
}
+2
View File
@@ -1,5 +1,7 @@
mod app;
mod enums;
mod paging;
pub use app::*;
pub use enums::*;
pub use paging::*;
+120
View File
@@ -0,0 +1,120 @@
mod common;
use wiremock::matchers::{body_json, header, method, path, query_param};
use wiremock::{Mock, ResponseTemplate};
#[tokio::test]
async fn test_list_apps_success() {
let (mock_server, client) = common::setup().await;
Mock::given(method("GET"))
.and(path("/Apps"))
.and(header("x-apikey", "test-api-key"))
.and(header("x-api-version", "2"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"pageNumber": 1,
"pageSize": 50,
"totalPages": 1,
"totalRecords": 2,
"items": [
{"href": "/Apps/id/1", "id": 1, "name": "App One"},
{"href": "/Apps/id/2", "id": 2, "name": "App Two"}
]
})))
.mount(&mock_server)
.await;
let result = client.list_apps(None).await.unwrap();
assert_eq!(result.total_records, Some(2));
let items = result.items.unwrap();
assert_eq!(items.len(), 2);
assert_eq!(items[0].id, 1);
assert_eq!(items[0].name.as_deref(), Some("App One"));
}
#[tokio::test]
async fn test_list_apps_with_paging() {
let (mock_server, client) = common::setup().await;
Mock::given(method("GET"))
.and(path("/Apps"))
.and(query_param("PageNumber", "2"))
.and(query_param("PageSize", "10"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"pageNumber": 2,
"pageSize": 10,
"totalPages": 3,
"totalRecords": 25,
"items": []
})))
.mount(&mock_server)
.await;
let paging = onspring::PagingRequest {
page_number: 2,
page_size: 10,
};
let result = client.list_apps(Some(paging)).await.unwrap();
assert_eq!(result.page_number, Some(2));
}
#[tokio::test]
async fn test_get_app_success() {
let (mock_server, client) = common::setup().await;
Mock::given(method("GET"))
.and(path("/Apps/id/42"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"href": "/Apps/id/42",
"id": 42,
"name": "Test App"
})))
.mount(&mock_server)
.await;
let app = client.get_app(42).await.unwrap();
assert_eq!(app.id, 42);
assert_eq!(app.name.as_deref(), Some("Test App"));
}
#[tokio::test]
async fn test_get_app_not_found() {
let (mock_server, client) = common::setup().await;
Mock::given(method("GET"))
.and(path("/Apps/id/999"))
.respond_with(ResponseTemplate::new(404))
.mount(&mock_server)
.await;
let result = client.get_app(999).await;
assert!(result.is_err());
if let Err(onspring::OnspringError::Api { status_code, .. }) = result {
assert_eq!(status_code, 404);
} else {
panic!("Expected Api error");
}
}
#[tokio::test]
async fn test_batch_get_apps() {
let (mock_server, client) = common::setup().await;
Mock::given(method("POST"))
.and(path("/Apps/batch-get"))
.and(body_json(serde_json::json!([1, 2, 3])))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"count": 3,
"items": [
{"href": "/Apps/id/1", "id": 1, "name": "App 1"},
{"href": "/Apps/id/2", "id": 2, "name": "App 2"},
{"href": "/Apps/id/3", "id": 3, "name": "App 3"}
]
})))
.mount(&mock_server)
.await;
let result = client.batch_get_apps(&[1, 2, 3]).await.unwrap();
assert_eq!(result.count, Some(3));
assert_eq!(result.items.unwrap().len(), 3);
}