/ firmware / tests / http_api.rs
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  }