mockforge_vbr/
integration.rs

1//! HTTP integration layer
2//!
3//! This module provides integration with the existing HTTP server (mockforge-http)
4//! for route registration and middleware integration.
5
6use crate::Result;
7use axum::{
8    extract::Extension,
9    routing::{delete, get, patch, post, put},
10    Router,
11};
12use tower::ServiceBuilder;
13use tower_http::cors::CorsLayer;
14
15/// Create router for VBR endpoints
16///
17/// This creates a router with generic entity routes that can be merged into
18/// the main mockforge-http router. The HandlerContext must be provided via
19/// Extension when the router is used.
20///
21/// # Example
22/// ```no_run
23/// use mockforge_vbr::integration::create_vbr_router;
24/// use axum::Router;
25///
26/// let vbr_router = create_vbr_router("/vbr-api").unwrap();
27/// let app = Router::new().merge(vbr_router);
28/// ```
29pub fn create_vbr_router(api_prefix: &str) -> Result<Router> {
30    let router = Router::new()
31        // Health check endpoint
32        .route(
33            &format!("{}/health", api_prefix),
34            get(|| async { "OK" }),
35        )
36        // Entity list endpoint (will be registered per entity)
37        // GET /api/{entity}
38        .route(
39            &format!("{}/{{entity}}", api_prefix),
40            get(crate::handlers::list_handler),
41        )
42        // Entity create endpoint
43        // POST /api/{entity}
44        .route(
45            &format!("{}/{{entity}}", api_prefix),
46            post(crate::handlers::create_handler),
47        )
48        // Entity get by ID endpoint
49        // GET /api/{entity}/{id}
50        .route(
51            &format!("{}/{{entity}}/{{id}}", api_prefix),
52            get(crate::handlers::get_handler),
53        )
54        // Entity update endpoint (PUT)
55        // PUT /api/{entity}/{id}
56        .route(
57            &format!("{}/{{entity}}/{{id}}", api_prefix),
58            put(crate::handlers::update_handler),
59        )
60        // Entity partial update endpoint (PATCH)
61        // PATCH /api/{entity}/{id}
62        .route(
63            &format!("{}/{{entity}}/{{id}}", api_prefix),
64            patch(crate::handlers::patch_handler),
65        )
66        // Entity delete endpoint
67        // DELETE /api/{entity}/{id}
68        .route(
69            &format!("{}/{{entity}}/{{id}}", api_prefix),
70            delete(crate::handlers::delete_handler),
71        )
72        // Relationship endpoint
73        // GET /api/{entity}/{id}/{relationship}
74        .route(
75            &format!("{}/{{entity}}/{{id}}/{{relationship}}", api_prefix),
76            get(crate::handlers::get_relationship_handler),
77        )
78        // Snapshot endpoints
79        // POST /vbr-api/snapshots - Create snapshot
80        .route(
81            &format!("{}/snapshots", api_prefix),
82            post(crate::handlers::create_snapshot_handler),
83        )
84        // GET /vbr-api/snapshots - List snapshots
85        .route(
86            &format!("{}/snapshots", api_prefix),
87            get(crate::handlers::list_snapshots_handler),
88        )
89        // POST /vbr-api/snapshots/{name}/restore - Restore snapshot
90        .route(
91            &format!("{}/snapshots/{{name}}/restore", api_prefix),
92            post(crate::handlers::restore_snapshot_handler),
93        )
94        // DELETE /vbr-api/snapshots/{name} - Delete snapshot
95        .route(
96            &format!("{}/snapshots/{{name}}", api_prefix),
97            delete(crate::handlers::delete_snapshot_handler),
98        )
99        // POST /vbr-api/reset - Reset database
100        .route(
101            &format!("{}/reset", api_prefix),
102            post(crate::handlers::reset_handler),
103        )
104        .layer(CorsLayer::permissive());
105
106    Ok(router)
107}
108
109/// Create a VBR router with handler context
110///
111/// This is a convenience function that creates a router with the HandlerContext
112/// already provided via Extension. Use this when you have a VbrEngine ready.
113pub fn create_vbr_router_with_context(
114    api_prefix: &str,
115    context: crate::handlers::HandlerContext,
116) -> Result<Router> {
117    let router = create_vbr_router(api_prefix)?;
118    Ok(router.layer(ServiceBuilder::new().layer(Extension(context)).into_inner()))
119}
120
121/// Register VBR routes dynamically for each entity
122///
123/// Adds entity-specific routes to an existing router. This allows you to
124/// register routes for individual entities as they are added to the registry.
125pub fn register_entity_routes(router: Router, entity_name: &str, api_prefix: &str) -> Router {
126    router
127        // List all entities
128        .route(
129            &format!("{}/{}", api_prefix, entity_name.to_lowercase()),
130            get(crate::handlers::list_handler),
131        )
132        // Create entity
133        .route(
134            &format!("{}/{}", api_prefix, entity_name.to_lowercase()),
135            post(crate::handlers::create_handler),
136        )
137        // Get entity by ID
138        .route(
139            &format!("{}/{}/{{id}}", api_prefix, entity_name.to_lowercase()),
140            get(crate::handlers::get_handler),
141        )
142        // Update entity (PUT)
143        .route(
144            &format!("{}/{}/{{id}}", api_prefix, entity_name.to_lowercase()),
145            put(crate::handlers::update_handler),
146        )
147        // Partial update entity (PATCH)
148        .route(
149            &format!("{}/{}/{{id}}", api_prefix, entity_name.to_lowercase()),
150            patch(crate::handlers::patch_handler),
151        )
152        // Delete entity
153        .route(
154            &format!("{}/{}/{{id}}", api_prefix, entity_name.to_lowercase()),
155            delete(crate::handlers::delete_handler),
156        )
157        // Get relationship endpoint
158        // GET /api/{entity}/{id}/{relationship}
159        .route(
160            &format!("{}/{}/{{id}}/{{relationship}}", api_prefix, entity_name.to_lowercase()),
161            get(crate::handlers::get_relationship_handler),
162        )
163}
164
165/// Integration helper for mockforge-http
166///
167/// This function can be called from mockforge-http to integrate VBR routes
168/// into the main application router. It takes an existing router and merges
169/// VBR routes into it.
170pub fn integrate_vbr_routes(
171    app: Router,
172    api_prefix: &str,
173    context: crate::handlers::HandlerContext,
174) -> Result<Router> {
175    let vbr_router = create_vbr_router_with_context(api_prefix, context)?;
176    Ok(app.merge(vbr_router))
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182    use crate::database::{InMemoryDatabase, VirtualDatabase};
183    use crate::entities::{Entity, EntityRegistry};
184    use crate::handlers::HandlerContext;
185    use crate::schema::VbrSchemaDefinition;
186    use mockforge_data::{FieldDefinition, SchemaDefinition};
187    use std::sync::Arc;
188
189    async fn setup_test_context() -> HandlerContext {
190        let mut db = InMemoryDatabase::new().await.unwrap();
191        db.initialize().await.unwrap();
192        let registry = EntityRegistry::new();
193
194        HandlerContext {
195            database: Arc::new(db),
196            registry,
197            session_manager: None,
198            snapshots_dir: None,
199        }
200    }
201
202    fn create_test_entity(name: &str) -> Entity {
203        let base_schema = SchemaDefinition::new(name.to_string())
204            .with_field(FieldDefinition::new("id".to_string(), "string".to_string()))
205            .with_field(FieldDefinition::new("name".to_string(), "string".to_string()));
206
207        let vbr_schema = VbrSchemaDefinition::new(base_schema);
208        Entity::new(name.to_string(), vbr_schema)
209    }
210
211    #[tokio::test]
212    async fn test_create_vbr_router() {
213        let result = create_vbr_router("/api");
214        assert!(result.is_ok());
215    }
216
217    #[tokio::test]
218    async fn test_create_vbr_router_with_context() {
219        let context = setup_test_context().await;
220        let result = create_vbr_router_with_context("/api", context);
221        assert!(result.is_ok());
222    }
223
224    #[tokio::test]
225    async fn test_create_vbr_router_custom_prefix() {
226        let result = create_vbr_router("/custom-api");
227        assert!(result.is_ok());
228    }
229
230    #[tokio::test]
231    async fn test_register_entity_routes() {
232        let base_router = Router::new();
233        let entity = create_test_entity("User");
234
235        let router = register_entity_routes(base_router, &entity.name, "/api");
236
237        // Verify the router compiles (we can't easily test route registration without a full server)
238        assert!(true);
239    }
240
241    #[tokio::test]
242    async fn test_register_entity_routes_multiple_entities() {
243        let mut router = Router::new();
244
245        let user_entity = create_test_entity("User");
246        let product_entity = create_test_entity("Product");
247
248        router = register_entity_routes(router, &user_entity.name, "/api");
249        router = register_entity_routes(router, &product_entity.name, "/api");
250
251        // Verify the router compiles with multiple entities
252        assert!(true);
253    }
254
255    #[tokio::test]
256    async fn test_integrate_vbr_routes() {
257        let app = Router::new();
258        let context = setup_test_context().await;
259
260        let result = integrate_vbr_routes(app, "/vbr-api", context);
261        assert!(result.is_ok());
262    }
263
264    #[tokio::test]
265    async fn test_integrate_vbr_routes_custom_prefix() {
266        let app = Router::new();
267        let context = setup_test_context().await;
268
269        let result = integrate_vbr_routes(app, "/custom", context);
270        assert!(result.is_ok());
271    }
272
273    #[tokio::test]
274    async fn test_create_vbr_router_with_health_endpoint() {
275        let router = create_vbr_router("/api").unwrap();
276        // Health endpoint is registered at /api/health
277        // We can't easily test the actual route without spinning up a server
278        // but we can verify the router was created successfully
279        assert!(true);
280    }
281
282    #[tokio::test]
283    async fn test_create_vbr_router_with_all_crud_routes() {
284        let router = create_vbr_router("/api").unwrap();
285        // Verifies all CRUD routes are registered:
286        // - GET /api/{entity} (list)
287        // - POST /api/{entity} (create)
288        // - GET /api/{entity}/{id} (get)
289        // - PUT /api/{entity}/{id} (update)
290        // - PATCH /api/{entity}/{id} (patch)
291        // - DELETE /api/{entity}/{id} (delete)
292        // - GET /api/{entity}/{id}/{relationship} (relationships)
293        assert!(true);
294    }
295
296    #[tokio::test]
297    async fn test_create_vbr_router_with_snapshot_routes() {
298        let router = create_vbr_router("/vbr-api").unwrap();
299        // Verifies snapshot routes are registered:
300        // - POST /vbr-api/snapshots (create)
301        // - GET /vbr-api/snapshots (list)
302        // - POST /vbr-api/snapshots/{name}/restore (restore)
303        // - DELETE /vbr-api/snapshots/{name} (delete)
304        // - POST /vbr-api/reset (reset)
305        assert!(true);
306    }
307
308    #[tokio::test]
309    async fn test_create_vbr_router_with_cors() {
310        let router = create_vbr_router("/api").unwrap();
311        // Verify CORS layer is applied (permissive)
312        // The router should have CorsLayer::permissive() applied
313        assert!(true);
314    }
315
316    #[tokio::test]
317    async fn test_register_entity_routes_with_lowercase() {
318        let base_router = Router::new();
319
320        // Entity name "User" should create routes for "user"
321        let router = register_entity_routes(base_router, "User", "/api");
322
323        // Routes should be:
324        // - /api/user
325        // - /api/user/{id}
326        // - /api/user/{id}/{relationship}
327        assert!(true);
328    }
329
330    #[tokio::test]
331    async fn test_context_with_session_manager() {
332        let mut db = InMemoryDatabase::new().await.unwrap();
333        db.initialize().await.unwrap();
334        let registry = EntityRegistry::new();
335
336        let context = HandlerContext {
337            database: Arc::new(db),
338            registry,
339            session_manager: None, // Could be Some(...) in real usage
340            snapshots_dir: None,
341        };
342
343        let result = create_vbr_router_with_context("/api", context);
344        assert!(result.is_ok());
345    }
346
347    #[tokio::test]
348    async fn test_context_with_snapshots_dir() {
349        let mut db = InMemoryDatabase::new().await.unwrap();
350        db.initialize().await.unwrap();
351        let registry = EntityRegistry::new();
352        let temp_dir = tempfile::tempdir().unwrap();
353
354        let context = HandlerContext {
355            database: Arc::new(db),
356            registry,
357            session_manager: None,
358            snapshots_dir: Some(temp_dir.path().to_path_buf()),
359        };
360
361        let result = create_vbr_router_with_context("/api", context);
362        assert!(result.is_ok());
363    }
364
365    #[tokio::test]
366    async fn test_empty_api_prefix() {
367        let result = create_vbr_router("");
368        assert!(result.is_ok());
369    }
370
371    #[tokio::test]
372    async fn test_api_prefix_with_trailing_slash() {
373        let result = create_vbr_router("/api/");
374        assert!(result.is_ok());
375    }
376
377    #[tokio::test]
378    async fn test_nested_api_prefix() {
379        let result = create_vbr_router("/v1/api");
380        assert!(result.is_ok());
381    }
382
383    #[tokio::test]
384    async fn test_router_can_be_merged_multiple_times() {
385        let app1 = Router::new();
386        let context1 = setup_test_context().await;
387        let app1 = integrate_vbr_routes(app1, "/api1", context1).unwrap();
388
389        let context2 = setup_test_context().await;
390        let result = integrate_vbr_routes(app1, "/api2", context2);
391        assert!(result.is_ok());
392    }
393}