From fe883a9dd21094aa0e61abb52bced1cd63d7082e Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Wed, 25 Mar 2026 13:37:55 -0500 Subject: [PATCH] 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. --- src/endpoints/apps.rs | 32 +++++++++++ src/endpoints/mod.rs | 1 + src/models/app.rs | 10 ++++ src/models/mod.rs | 2 + tests/test_apps.rs | 120 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 165 insertions(+) create mode 100644 src/endpoints/apps.rs create mode 100644 src/models/app.rs create mode 100644 tests/test_apps.rs diff --git a/src/endpoints/apps.rs b/src/endpoints/apps.rs new file mode 100644 index 0000000..0b59a9a --- /dev/null +++ b/src/endpoints/apps.rs @@ -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) -> Result> { + 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 { + 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> { + self.request(Method::POST, "/Apps/batch-get", &[], Some(&ids)) + .await + } +} diff --git a/src/endpoints/mod.rs b/src/endpoints/mod.rs index b32533b..5d163cf 100644 --- a/src/endpoints/mod.rs +++ b/src/endpoints/mod.rs @@ -1 +1,2 @@ +mod apps; mod ping; diff --git a/src/models/app.rs b/src/models/app.rs new file mode 100644 index 0000000..8a02a23 --- /dev/null +++ b/src/models/app.rs @@ -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, + pub id: i32, + pub name: Option, +} diff --git a/src/models/mod.rs b/src/models/mod.rs index b6e42b1..e2ff6ed 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,5 +1,7 @@ +mod app; mod enums; mod paging; +pub use app::*; pub use enums::*; pub use paging::*; diff --git a/tests/test_apps.rs b/tests/test_apps.rs new file mode 100644 index 0000000..0ea6993 --- /dev/null +++ b/tests/test_apps.rs @@ -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); +}