diff options
Diffstat (limited to 'warden/src/server/routes/config/schema/mod.rs')
| -rw-r--r-- | warden/src/server/routes/config/schema/mod.rs | 258 |
1 files changed, 258 insertions, 0 deletions
diff --git a/warden/src/server/routes/config/schema/mod.rs b/warden/src/server/routes/config/schema/mod.rs index 17db5ce..901d116 100644 --- a/warden/src/server/routes/config/schema/mod.rs +++ b/warden/src/server/routes/config/schema/mod.rs @@ -25,3 +25,261 @@ pub fn router(store: Arc<AppState>) -> OpenApiRouter { .routes(utoipa_axum::routes!(update::update_schema)) .with_state(store) } + +#[cfg(test)] +pub mod tests { + use std::collections::HashMap; + use std::sync::Arc; + + use api_config::schema::TransactionSchema; + use api_config::{ConfigurationError, schema::SchemaDriver}; + use async_trait::async_trait; + use time::OffsetDateTime; + use tokio::sync::RwLock; + + use serde_json::Value; + use warden_core::pagination::{Connection, Edge, PageInfo, PaginationArgs}; + + #[derive(Default)] + pub struct MockSchemaDriver { + store: Arc<RwLock<HashMap<(String, String), TransactionSchema>>>, + } + + impl MockSchemaDriver { + pub fn new() -> Self { + Self::default() + } + } + + use sqlx::error::DatabaseError; + use std::fmt; + + #[derive(Debug)] + struct UniqueViolationError { + msg: String, + } + + impl fmt::Display for UniqueViolationError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.msg) + } + } + + impl std::error::Error for UniqueViolationError {} + + impl DatabaseError for UniqueViolationError { + fn message(&self) -> &str { + &self.msg + } + + fn as_error(&self) -> &(dyn std::error::Error + Send + Sync + 'static) { + unimplemented!() + } + + fn as_error_mut(&mut self) -> &mut (dyn std::error::Error + Send + Sync + 'static) { + unimplemented!() + } + + fn into_error(self: Box<Self>) -> Box<dyn std::error::Error + Send + Sync + 'static> { + unimplemented!() + } + + fn kind(&self) -> sqlx::error::ErrorKind { + sqlx::error::ErrorKind::UniqueViolation + } + } + + #[derive(Debug)] + struct OtherDbError { + msg: String, + } + + impl fmt::Display for OtherDbError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.msg) + } + } + + impl std::error::Error for OtherDbError {} + + impl DatabaseError for OtherDbError { + fn message(&self) -> &str { + &self.msg + } + + fn as_error(&self) -> &(dyn std::error::Error + Send + Sync + 'static) { + unimplemented!() + } + + fn as_error_mut(&mut self) -> &mut (dyn std::error::Error + Send + Sync + 'static) { + unimplemented!() + } + + fn into_error(self: Box<Self>) -> Box<dyn std::error::Error + Send + Sync + 'static> { + unimplemented!() + } + + fn kind(&self) -> sqlx::error::ErrorKind { + sqlx::error::ErrorKind::Other + } + } + + #[async_trait] + impl SchemaDriver for MockSchemaDriver { + async fn create_schema( + &self, + kind: &str, + version: &str, + schema: &Value, + ) -> Result<TransactionSchema, ConfigurationError> { + let mut store = self.store.write().await; + let key = (kind.to_string(), version.to_string()); + + if store.contains_key(&key) { + let err = UniqueViolationError { + msg: "key".to_string(), + }; + dbg!(err.code()); + return Err(ConfigurationError::Database(sqlx::Error::Database( + Box::new(err), + ))); + }; + + if matches!(kind, "error") { + let err = OtherDbError { + msg: "key".to_string(), + }; + dbg!(err.code()); + return Err(ConfigurationError::Database(sqlx::Error::Database( + Box::new(err), + ))); + } + + let schema_obj = TransactionSchema { + id: 1, + schema_type: kind.to_string(), + schema_version: version.to_string(), + schema: schema.clone(), + created_at: OffsetDateTime::now_utc(), + updated_at: OffsetDateTime::now_utc(), + }; + + store.insert(key, schema_obj.clone()); + Ok(schema_obj) + } + + async fn delete_schema(&self, kind: &str, version: &str) -> Result<(), ConfigurationError> { + let mut store = self.store.write().await; + let key = (kind.to_string(), version.to_string()); + + store.remove(&key); + Ok(()) + } + + async fn get_schema( + &self, + kind: &str, + version: &str, + ) -> Result<Option<TransactionSchema>, ConfigurationError> { + let store = self.store.read().await; + let key = (kind.to_string(), version.to_string()); + + Ok(store.get(&key).cloned()) + } + + async fn update_schema( + &self, + kind: &str, + version: &str, + schema: &Value, + ) -> Result<Option<TransactionSchema>, ConfigurationError> { + let mut store = self.store.write().await; + let key = (kind.to_string(), version.to_string()); + + if let Some(existing) = store.get_mut(&key) { + existing.schema = schema.clone(); + return Ok(Some(existing.clone())); + } + + Ok(None) + } + + async fn list_schemas( + &self, + input: &PaginationArgs, + limit: i64, + ) -> Result<Connection<TransactionSchema>, ConfigurationError> { + let store = self.store.read().await; + + // 1. Collect + sort for stable pagination + let mut items: Vec<_> = store.values().cloned().collect(); + items.sort_by(|a, b| { + (a.schema_type.clone(), a.schema_version.clone()) + .cmp(&(b.schema_type.clone(), b.schema_version.clone())) + }); + + // 2. Convert to edges + let mut edges: Vec<Edge<TransactionSchema>> = items + .into_iter() + .map(|schema| Edge { + cursor: format!("{}:{}", schema.schema_type, schema.schema_version), + node: schema, + }) + .collect(); + + // 3. Apply cursors (after / before) + if let Some(after) = &input.after + && let Some(pos) = edges.iter().position(|e| &e.cursor == after) + { + edges = edges.into_iter().skip(pos + 1).collect(); + } + + if let Some(before) = &input.before + && let Some(pos) = edges.iter().position(|e| &e.cursor == before) + { + edges = edges.into_iter().take(pos).collect(); + } + + let total = edges.len(); + + // 4. Apply first / last + let mut sliced = edges; + + if let Some(first) = input.first { + let take = first.min(limit).max(0) as usize; + sliced = sliced.into_iter().take(take).collect(); + } else if let Some(last) = input.last { + let take = last.min(limit).max(0) as usize; + let len = sliced.len(); + sliced = sliced.into_iter().skip(len.saturating_sub(take)).collect(); + } else { + // default limit + sliced = sliced.into_iter().take(limit as usize).collect(); + } + + // 5. PageInfo + let start_cursor = sliced.first().map(|e| e.cursor.clone()); + let end_cursor = sliced.last().map(|e| e.cursor.clone()); + + let has_next_page = match input.first { + Some(first) => total > first as usize, + None => false, + }; + + let has_previous_page = match input.last { + Some(last) => total > last as usize, + None => false, + }; + + Ok(Connection { + edges: sliced, + page_info: PageInfo { + has_next_page, + has_previous_page, + start_cursor, + end_cursor, + }, + }) + } + } +} |
