http_api.rs
1 //! `describe("HTTP API Contracts")` 2 //! 3 //! Compile-time contract tests for API route paths, JSON response 4 //! envelope shapes, and CloudEvent format. No HTTP server, no WiFi, 5 //! no network — just string assertions that catch drift. 6 7 #![no_std] 8 #![no_main] 9 10 use defmt::info; 11 12 esp_bootloader_esp_idf::esp_app_desc!(); 13 14 const FILESYSTEM_LIST_ENDPOINT_PATH: &str = "/api/filesystem/list"; 15 const FILESYSTEM_FILE_ENDPOINT_PREFIX: &str = "/api/filesystem/file/"; 16 const FILESYSTEM_UPLOAD_ENDPOINT_PREFIX: &str = "/api/filesystem/file/"; 17 const SYSTEM_DEVICE_STATUS_ENDPOINT_PATH: &str = "/api/system/device/status"; 18 const API_ROUTE_PREFIX: &str = "/api"; 19 const FILESYSTEM_ROUTE_PREFIX: &str = "/filesystem"; 20 const SYSTEM_ROUTE_PREFIX: &str = "/system"; 21 const DEVICE_ROUTE_PREFIX: &str = "/device"; 22 23 const SYSTEM_DEVICE_STATUS_CLOUDEVENT_EXAMPLE: &str = "{\"specversion\":\"1.0\",\"id\":\"system-device-status-45591\",\"source\":\"urn:apidae-systems:tenant:p-uot-ins:site:university-of-ottawa\",\"type\":\"com.apidae.system.device.status.v1\",\"datacontenttype\":\"application/json\",\"time\":\"2026-04-03T17:18:43Z\",\"data\":{\"device\":{\"chip_id\":966764,\"chip_model\":\"ESP32-S3\",\"chip_cores\":2,\"chip_revision\":2,\"efuse_mac\":\"119572138669276\"},\"network\":{\"ipv4_address\":\"10.0.0.95\",\"wifi_rssi\":-61},\"runtime\":{\"uptime\":\"45s\",\"uptime_seconds\":45,\"memory_heap_bytes\":46016},\"storage\":{\"location\":\"sd\",\"total_bytes\":1876951040,\"used_bytes\":557056,\"free_bytes\":1876393984}}}"; 24 const FILESYSTEM_LIST_JSON_RESPONSE_EXAMPLE: &str = 25 "{\"ok\":true,\"data\":{\"entries\":[{\"name\":\"DATA.CSV\",\"size\":270,\"last_write_unix\":0}]}}"; 26 const FILESYSTEM_ERROR_JSON_RESPONSE_EXAMPLE: &str = 27 "{\"ok\":false,\"error\":{\"code\":\"INVALID_PATH\",\"message\":\"invalid file path\"}}"; 28 29 #[cfg(test)] 30 #[embedded_test::setup] 31 fn setup() { 32 rtt_target::rtt_init_defmt!(); 33 } 34 35 #[cfg(test)] 36 #[embedded_test::tests(executor = esp_rtos::embassy::Executor::new())] 37 mod tests { 38 use super::*; 39 use esp_hal::{clock::CpuClock, interrupt::software::SoftwareInterruptControl, timer::timg::TimerGroup}; 40 41 #[init] 42 fn init() { 43 let peripherals = esp_hal::init(esp_hal::Config::default().with_cpu_clock(CpuClock::max())); 44 let timer_group0 = TimerGroup::new(peripherals.TIMG0); 45 let software_interrupts = SoftwareInterruptControl::new(peripherals.SW_INTERRUPT); 46 esp_rtos::start(timer_group0.timer0, software_interrupts.software_interrupt0); 47 info!("=== HTTP API Contracts — describe block ==="); 48 } 49 50 /// `it("user verifies HTTP filesystem endpoint paths are stable")` 51 #[test] 52 async fn http_filesystem_endpoint_contracts_are_stable() { 53 defmt::assert_eq!(API_ROUTE_PREFIX, "/api"); 54 defmt::assert_eq!(FILESYSTEM_ROUTE_PREFIX, "/filesystem"); 55 defmt::assert_eq!(SYSTEM_ROUTE_PREFIX, "/system"); 56 defmt::assert_eq!(DEVICE_ROUTE_PREFIX, "/device"); 57 58 defmt::assert_eq!(FILESYSTEM_LIST_ENDPOINT_PATH, "/api/filesystem/list"); 59 defmt::assert_eq!(FILESYSTEM_FILE_ENDPOINT_PREFIX, "/api/filesystem/file/"); 60 defmt::assert_eq!(FILESYSTEM_UPLOAD_ENDPOINT_PREFIX, "/api/filesystem/file/"); 61 defmt::assert_eq!(SYSTEM_DEVICE_STATUS_ENDPOINT_PATH, "/api/system/device/status"); 62 } 63 64 /// `it("user verifies filesystem JSON envelope shape is stable")` 65 #[test] 66 async fn scalable_filesystem_json_envelope_shape_is_stable() { 67 defmt::assert!(FILESYSTEM_LIST_JSON_RESPONSE_EXAMPLE.contains("\"ok\":true")); 68 defmt::assert!(FILESYSTEM_LIST_JSON_RESPONSE_EXAMPLE.contains("\"data\":{\"entries\":")); 69 defmt::assert!(FILESYSTEM_LIST_JSON_RESPONSE_EXAMPLE.contains("\"name\":")); 70 defmt::assert!(FILESYSTEM_LIST_JSON_RESPONSE_EXAMPLE.contains("\"size\":")); 71 defmt::assert!(FILESYSTEM_LIST_JSON_RESPONSE_EXAMPLE.contains("\"last_write_unix\":")); 72 73 defmt::assert!(FILESYSTEM_ERROR_JSON_RESPONSE_EXAMPLE.contains("\"ok\":false")); 74 defmt::assert!(FILESYSTEM_ERROR_JSON_RESPONSE_EXAMPLE.contains("\"error\":{\"code\":")); 75 defmt::assert!(FILESYSTEM_ERROR_JSON_RESPONSE_EXAMPLE.contains("\"message\":")); 76 } 77 78 /// `it("user verifies upload limits match runtime expectation")` 79 #[test] 80 async fn upload_limits_match_runtime_expectation() { 81 defmt::assert_eq!(firmware::config::app::sd_card::FILE_UPLOAD_MAX_BYTES, 4096); 82 } 83 84 /// `it("user verifies CloudEvent device status shape is stable")` 85 #[test] 86 async fn system_device_status_cloud_event_shape_is_stable() { 87 defmt::assert!( 88 SYSTEM_DEVICE_STATUS_CLOUDEVENT_EXAMPLE.contains("\"specversion\":\"1.0\"") 89 ); 90 defmt::assert!(SYSTEM_DEVICE_STATUS_CLOUDEVENT_EXAMPLE.contains( 91 "\"type\":\"com.apidae.system.device.status.v1\"" 92 )); 93 defmt::assert!(SYSTEM_DEVICE_STATUS_CLOUDEVENT_EXAMPLE.contains("\"data\":{\"device\":")); 94 defmt::assert!(SYSTEM_DEVICE_STATUS_CLOUDEVENT_EXAMPLE.contains("\"network\":")); 95 defmt::assert!(SYSTEM_DEVICE_STATUS_CLOUDEVENT_EXAMPLE.contains("\"runtime\":")); 96 defmt::assert!(SYSTEM_DEVICE_STATUS_CLOUDEVENT_EXAMPLE.contains("\"storage\":")); 97 } 98 }