1use serde_json::Value;
31use std::collections::HashMap;
32
33#[cfg(target_arch = "wasm32")]
38extern "C" {
39 #[link_name = "cufflink_log"]
40 fn cufflink_log(level: i32, msg_ptr: i32, msg_len: i32);
41 fn db_query(sql_ptr: i32, sql_len: i32) -> i32;
42 fn db_execute(sql_ptr: i32, sql_len: i32) -> i32;
43 fn get_host_response_len() -> i32;
44 fn get_host_response(buf_ptr: i32, buf_len: i32) -> i32;
45 fn nats_publish(subj_ptr: i32, subj_len: i32, payload_ptr: i32, payload_len: i32) -> i32;
46 fn nats_request(
47 subj_ptr: i32,
48 subj_len: i32,
49 payload_ptr: i32,
50 payload_len: i32,
51 timeout_ms: i32,
52 ) -> i32;
53 fn http_fetch(
54 method_ptr: i32,
55 method_len: i32,
56 url_ptr: i32,
57 url_len: i32,
58 headers_ptr: i32,
59 headers_len: i32,
60 body_ptr: i32,
61 body_len: i32,
62 ) -> i32;
63 fn get_config(key_ptr: i32, key_len: i32) -> i32;
64 fn s3_download(bucket_ptr: i32, bucket_len: i32, key_ptr: i32, key_len: i32) -> i32;
65 fn redis_get(key_ptr: i32, key_len: i32) -> i32;
66 fn redis_set(key_ptr: i32, key_len: i32, val_ptr: i32, val_len: i32, ttl_secs: i32) -> i32;
67 fn redis_del(key_ptr: i32, key_len: i32) -> i32;
68 fn generate_uuid() -> i32;
69}
70
71#[derive(Debug, Clone)]
92pub struct Auth {
93 pub sub: String,
95 pub preferred_username: Option<String>,
97 pub name: Option<String>,
99 pub email: Option<String>,
101 pub realm_roles: Vec<String>,
103 pub claims: HashMap<String, Value>,
105 pub permissions: Vec<String>,
107 pub role_names: Vec<String>,
109 pub is_service_account: bool,
112}
113
114impl Auth {
115 pub fn has_role(&self, role: &str) -> bool {
117 self.realm_roles.iter().any(|r| r == role)
118 }
119
120 pub fn can(&self, area: &str, operation: &str) -> bool {
131 let required = format!("{}:{}", area, operation);
132 let wildcard = format!("{}:*", area);
133 self.permissions
134 .iter()
135 .any(|p| p == &required || p == &wildcard || p == "*")
136 }
137
138 pub fn has_cufflink_role(&self, role: &str) -> bool {
140 self.role_names.iter().any(|r| r == role)
141 }
142
143 pub fn claim(&self, key: &str) -> Option<&Value> {
145 self.claims.get(key)
146 }
147}
148
149#[derive(Debug, Clone)]
156pub struct Request {
157 method: String,
158 handler: String,
159 headers: HashMap<String, String>,
160 body: Value,
161 tenant: String,
162 service: String,
163 auth: Option<Auth>,
164}
165
166impl Request {
167 pub fn from_json(json: &str) -> Option<Self> {
169 let v: Value = serde_json::from_str(json).ok()?;
170 let headers = v["headers"]
171 .as_object()
172 .map(|m| {
173 m.iter()
174 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
175 .collect()
176 })
177 .unwrap_or_default();
178
179 let auth = v["auth"].as_object().map(|auth_obj| {
180 let a = Value::Object(auth_obj.clone());
181 Auth {
182 sub: a["sub"].as_str().unwrap_or("").to_string(),
183 preferred_username: a["preferred_username"].as_str().map(|s| s.to_string()),
184 name: a["name"].as_str().map(|s| s.to_string()),
185 email: a["email"].as_str().map(|s| s.to_string()),
186 realm_roles: a["realm_roles"]
187 .as_array()
188 .map(|arr| {
189 arr.iter()
190 .filter_map(|v| v.as_str().map(|s| s.to_string()))
191 .collect()
192 })
193 .unwrap_or_default(),
194 claims: a["claims"]
195 .as_object()
196 .map(|m| m.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
197 .unwrap_or_default(),
198 permissions: a["permissions"]
199 .as_array()
200 .map(|arr| {
201 arr.iter()
202 .filter_map(|v| v.as_str().map(|s| s.to_string()))
203 .collect()
204 })
205 .unwrap_or_default(),
206 role_names: a["role_names"]
207 .as_array()
208 .map(|arr| {
209 arr.iter()
210 .filter_map(|v| v.as_str().map(|s| s.to_string()))
211 .collect()
212 })
213 .unwrap_or_default(),
214 is_service_account: a["is_service_account"].as_bool().unwrap_or(false),
215 }
216 });
217
218 Some(Self {
219 method: v["method"].as_str().unwrap_or("GET").to_string(),
220 handler: v["handler"].as_str().unwrap_or("").to_string(),
221 headers,
222 body: v["body"].clone(),
223 tenant: v["tenant"].as_str().unwrap_or("").to_string(),
224 service: v["service"].as_str().unwrap_or("").to_string(),
225 auth,
226 })
227 }
228
229 pub fn method(&self) -> &str {
231 &self.method
232 }
233
234 pub fn handler(&self) -> &str {
236 &self.handler
237 }
238
239 pub fn headers(&self) -> &HashMap<String, String> {
241 &self.headers
242 }
243
244 pub fn header(&self, name: &str) -> Option<&str> {
246 self.headers.get(name).map(|s| s.as_str())
247 }
248
249 pub fn body(&self) -> &Value {
251 &self.body
252 }
253
254 pub fn tenant(&self) -> &str {
256 &self.tenant
257 }
258
259 pub fn service(&self) -> &str {
261 &self.service
262 }
263
264 pub fn auth(&self) -> Option<&Auth> {
268 self.auth.as_ref()
269 }
270
271 pub fn require_auth(&self) -> Result<&Auth, Response> {
283 self.auth.as_ref().ok_or_else(|| {
284 Response::json(&serde_json::json!({
285 "error": "Authentication required",
286 "status": 401
287 }))
288 })
289 }
290}
291
292#[derive(Debug, Clone)]
296pub struct Response {
297 data: String,
298}
299
300impl Response {
301 pub fn json(value: &Value) -> Self {
303 Self {
304 data: serde_json::to_string(value).unwrap_or_else(|_| "{}".to_string()),
305 }
306 }
307
308 pub fn text(s: &str) -> Self {
310 Self::json(&Value::String(s.to_string()))
311 }
312
313 pub fn error(message: &str) -> Self {
315 Self::json(&serde_json::json!({"error": message}))
316 }
317
318 pub fn empty() -> Self {
320 Self::json(&serde_json::json!({"ok": true}))
321 }
322
323 pub fn into_data(self) -> String {
325 self.data
326 }
327}
328
329pub mod db {
336 use super::*;
337
338 pub fn query(sql: &str) -> Vec<Value> {
349 #[cfg(target_arch = "wasm32")]
350 {
351 let bytes = sql.as_bytes();
352 let result = unsafe { db_query(bytes.as_ptr() as i32, bytes.len() as i32) };
353 if result < 0 {
354 return vec![];
355 }
356 read_host_response()
357 }
358 #[cfg(not(target_arch = "wasm32"))]
359 {
360 let _ = sql;
361 vec![]
362 }
363 }
364
365 pub fn query_one(sql: &str) -> Option<Value> {
373 query(sql).into_iter().next()
374 }
375
376 pub fn execute(sql: &str) -> i32 {
385 #[cfg(target_arch = "wasm32")]
386 {
387 let bytes = sql.as_bytes();
388 unsafe { db_execute(bytes.as_ptr() as i32, bytes.len() as i32) }
389 }
390 #[cfg(not(target_arch = "wasm32"))]
391 {
392 let _ = sql;
393 0
394 }
395 }
396
397 #[cfg(target_arch = "wasm32")]
399 fn read_host_response() -> Vec<Value> {
400 let len = unsafe { get_host_response_len() };
401 if len <= 0 {
402 return vec![];
403 }
404 let mut buf = vec![0u8; len as usize];
405 let read = unsafe { get_host_response(buf.as_mut_ptr() as i32, len) };
406 if read <= 0 {
407 return vec![];
408 }
409 buf.truncate(read as usize);
410 let json_str = String::from_utf8_lossy(&buf);
411 serde_json::from_str(&json_str).unwrap_or_default()
412 }
413}
414
415pub mod nats {
422 #[allow(unused_imports)]
423 use super::*;
424
425 pub fn publish(subject: &str, payload: &str) -> bool {
436 #[cfg(target_arch = "wasm32")]
437 {
438 let subj_bytes = subject.as_bytes();
439 let payload_bytes = payload.as_bytes();
440 let result = unsafe {
441 nats_publish(
442 subj_bytes.as_ptr() as i32,
443 subj_bytes.len() as i32,
444 payload_bytes.as_ptr() as i32,
445 payload_bytes.len() as i32,
446 )
447 };
448 result == 0
449 }
450 #[cfg(not(target_arch = "wasm32"))]
451 {
452 let _ = (subject, payload);
453 true
454 }
455 }
456
457 pub fn request(subject: &str, payload: &str, timeout_ms: i32) -> Option<String> {
469 #[cfg(target_arch = "wasm32")]
470 {
471 let subj_bytes = subject.as_bytes();
472 let payload_bytes = payload.as_bytes();
473 let result = unsafe {
474 nats_request(
475 subj_bytes.as_ptr() as i32,
476 subj_bytes.len() as i32,
477 payload_bytes.as_ptr() as i32,
478 payload_bytes.len() as i32,
479 timeout_ms,
480 )
481 };
482 if result != 0 {
483 return None;
484 }
485 let len = unsafe { get_host_response_len() };
486 if len <= 0 {
487 return None;
488 }
489 let mut buf = vec![0u8; len as usize];
490 let read = unsafe { get_host_response(buf.as_mut_ptr() as i32, len) };
491 if read <= 0 {
492 return None;
493 }
494 String::from_utf8(buf[..read as usize].to_vec()).ok()
495 }
496 #[cfg(not(target_arch = "wasm32"))]
497 {
498 let _ = (subject, payload, timeout_ms);
499 None
500 }
501 }
502}
503
504pub mod log {
510 #[allow(unused_imports)]
511 use super::*;
512
513 pub fn error(msg: &str) {
515 write(0, msg);
516 }
517
518 pub fn warn(msg: &str) {
520 write(1, msg);
521 }
522
523 pub fn info(msg: &str) {
525 write(2, msg);
526 }
527
528 pub fn debug(msg: &str) {
530 write(3, msg);
531 }
532
533 fn write(level: i32, msg: &str) {
534 #[cfg(target_arch = "wasm32")]
535 {
536 let bytes = msg.as_bytes();
537 unsafe {
538 super::cufflink_log(level, bytes.as_ptr() as i32, bytes.len() as i32);
539 }
540 }
541 #[cfg(not(target_arch = "wasm32"))]
542 {
543 let _ = (level, msg);
544 }
545 }
546}
547
548pub mod http {
555 #[allow(unused_imports)]
556 use super::*;
557
558 #[derive(Debug, Clone)]
560 pub struct FetchResponse {
561 pub status: i32,
563 pub body: String,
565 pub body_encoding: String,
567 pub headers: HashMap<String, String>,
569 }
570
571 impl FetchResponse {
572 pub fn json(&self) -> Option<Value> {
574 serde_json::from_str(&self.body).ok()
575 }
576
577 pub fn is_success(&self) -> bool {
579 (200..300).contains(&self.status)
580 }
581
582 pub fn is_base64(&self) -> bool {
584 self.body_encoding == "base64"
585 }
586 }
587
588 pub fn fetch(
599 method: &str,
600 url: &str,
601 headers: &[(&str, &str)],
602 body: Option<&str>,
603 ) -> Option<FetchResponse> {
604 #[cfg(target_arch = "wasm32")]
605 {
606 let method_bytes = method.as_bytes();
607 let url_bytes = url.as_bytes();
608 let headers_map: HashMap<&str, &str> = headers.iter().copied().collect();
609 let headers_json = serde_json::to_string(&headers_map).unwrap_or_default();
610 let headers_bytes = headers_json.as_bytes();
611 let body_bytes = body.unwrap_or("").as_bytes();
612 let body_len = body.map(|b| b.len()).unwrap_or(0);
613
614 let result = unsafe {
615 http_fetch(
616 method_bytes.as_ptr() as i32,
617 method_bytes.len() as i32,
618 url_bytes.as_ptr() as i32,
619 url_bytes.len() as i32,
620 headers_bytes.as_ptr() as i32,
621 headers_bytes.len() as i32,
622 body_bytes.as_ptr() as i32,
623 body_len as i32,
624 )
625 };
626
627 if result < 0 {
628 return None;
629 }
630
631 read_fetch_response()
632 }
633 #[cfg(not(target_arch = "wasm32"))]
634 {
635 let _ = (method, url, headers, body);
636 None
637 }
638 }
639
640 pub fn get(url: &str, headers: &[(&str, &str)]) -> Option<FetchResponse> {
642 fetch("GET", url, headers, None)
643 }
644
645 pub fn post(url: &str, headers: &[(&str, &str)], body: &str) -> Option<FetchResponse> {
647 fetch("POST", url, headers, Some(body))
648 }
649
650 pub fn put(url: &str, headers: &[(&str, &str)], body: &str) -> Option<FetchResponse> {
652 fetch("PUT", url, headers, Some(body))
653 }
654
655 pub fn delete(url: &str, headers: &[(&str, &str)]) -> Option<FetchResponse> {
657 fetch("DELETE", url, headers, None)
658 }
659
660 pub fn patch(url: &str, headers: &[(&str, &str)], body: &str) -> Option<FetchResponse> {
662 fetch("PATCH", url, headers, Some(body))
663 }
664
665 #[cfg(target_arch = "wasm32")]
667 fn read_fetch_response() -> Option<FetchResponse> {
668 let len = unsafe { get_host_response_len() };
669 if len <= 0 {
670 return None;
671 }
672 let mut buf = vec![0u8; len as usize];
673 let read = unsafe { get_host_response(buf.as_mut_ptr() as i32, len) };
674 if read <= 0 {
675 return None;
676 }
677 buf.truncate(read as usize);
678 let json_str = String::from_utf8_lossy(&buf);
679 let v: Value = serde_json::from_str(&json_str).ok()?;
680 Some(FetchResponse {
681 status: v["status"].as_i64().unwrap_or(0) as i32,
682 body: v["body"].as_str().unwrap_or("").to_string(),
683 body_encoding: v["body_encoding"].as_str().unwrap_or("utf8").to_string(),
684 headers: v["headers"]
685 .as_object()
686 .map(|m| {
687 m.iter()
688 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
689 .collect()
690 })
691 .unwrap_or_default(),
692 })
693 }
694}
695
696pub mod config {
704 #[allow(unused_imports)]
705 use super::*;
706
707 pub fn get(key: &str) -> Option<String> {
716 #[cfg(target_arch = "wasm32")]
717 {
718 let bytes = key.as_bytes();
719 let result = unsafe { get_config(bytes.as_ptr() as i32, bytes.len() as i32) };
720 if result < 0 {
721 return None;
722 }
723 read_config_response()
724 }
725 #[cfg(not(target_arch = "wasm32"))]
726 {
727 let _ = key;
728 None
729 }
730 }
731
732 #[cfg(target_arch = "wasm32")]
734 fn read_config_response() -> Option<String> {
735 let len = unsafe { get_host_response_len() };
736 if len <= 0 {
737 return None;
738 }
739 let mut buf = vec![0u8; len as usize];
740 let read = unsafe { get_host_response(buf.as_mut_ptr() as i32, len) };
741 if read <= 0 {
742 return None;
743 }
744 buf.truncate(read as usize);
745 String::from_utf8(buf).ok()
746 }
747}
748
749pub mod storage {
757 #[allow(unused_imports)]
758 use super::*;
759
760 pub fn download(bucket: &str, key: &str) -> Option<String> {
771 #[cfg(target_arch = "wasm32")]
772 {
773 let bucket_bytes = bucket.as_bytes();
774 let key_bytes = key.as_bytes();
775 let result = unsafe {
776 s3_download(
777 bucket_bytes.as_ptr() as i32,
778 bucket_bytes.len() as i32,
779 key_bytes.as_ptr() as i32,
780 key_bytes.len() as i32,
781 )
782 };
783 if result < 0 {
784 return None;
785 }
786 read_storage_response()
787 }
788 #[cfg(not(target_arch = "wasm32"))]
789 {
790 let _ = (bucket, key);
791 None
792 }
793 }
794
795 #[cfg(target_arch = "wasm32")]
797 fn read_storage_response() -> Option<String> {
798 let len = unsafe { get_host_response_len() };
799 if len <= 0 {
800 return None;
801 }
802 let mut buf = vec![0u8; len as usize];
803 let read = unsafe { get_host_response(buf.as_mut_ptr() as i32, len) };
804 if read <= 0 {
805 return None;
806 }
807 buf.truncate(read as usize);
808 String::from_utf8(buf).ok()
809 }
810}
811
812pub mod redis {
819 #[allow(unused_imports)]
820 use super::*;
821
822 pub fn get(key: &str) -> Option<String> {
831 #[cfg(target_arch = "wasm32")]
832 {
833 let bytes = key.as_bytes();
834 let result = unsafe { redis_get(bytes.as_ptr() as i32, bytes.len() as i32) };
835 if result < 0 {
836 return None;
837 }
838 read_redis_response()
839 }
840 #[cfg(not(target_arch = "wasm32"))]
841 {
842 let _ = key;
843 None
844 }
845 }
846
847 pub fn set(key: &str, value: &str, ttl_secs: i32) -> bool {
855 #[cfg(target_arch = "wasm32")]
856 {
857 let key_bytes = key.as_bytes();
858 let val_bytes = value.as_bytes();
859 let result = unsafe {
860 redis_set(
861 key_bytes.as_ptr() as i32,
862 key_bytes.len() as i32,
863 val_bytes.as_ptr() as i32,
864 val_bytes.len() as i32,
865 ttl_secs,
866 )
867 };
868 result == 0
869 }
870 #[cfg(not(target_arch = "wasm32"))]
871 {
872 let _ = (key, value, ttl_secs);
873 true
874 }
875 }
876
877 pub fn del(key: &str) -> bool {
885 #[cfg(target_arch = "wasm32")]
886 {
887 let bytes = key.as_bytes();
888 let result = unsafe { redis_del(bytes.as_ptr() as i32, bytes.len() as i32) };
889 result == 0
890 }
891 #[cfg(not(target_arch = "wasm32"))]
892 {
893 let _ = key;
894 true
895 }
896 }
897
898 #[cfg(target_arch = "wasm32")]
900 fn read_redis_response() -> Option<String> {
901 let len = unsafe { get_host_response_len() };
902 if len <= 0 {
903 return None;
904 }
905 let mut buf = vec![0u8; len as usize];
906 let read = unsafe { get_host_response(buf.as_mut_ptr() as i32, len) };
907 if read <= 0 {
908 return None;
909 }
910 buf.truncate(read as usize);
911 String::from_utf8(buf).ok()
912 }
913}
914
915pub mod util {
919 #[allow(unused_imports)]
920 use super::*;
921
922 pub fn generate_uuid() -> String {
929 #[cfg(target_arch = "wasm32")]
930 {
931 let result = unsafe { super::generate_uuid() };
932 if result < 0 {
933 return String::new();
934 }
935 let len = unsafe { get_host_response_len() };
936 if len <= 0 {
937 return String::new();
938 }
939 let mut buf = vec![0u8; len as usize];
940 let read = unsafe { get_host_response(buf.as_mut_ptr() as i32, len) };
941 if read <= 0 {
942 return String::new();
943 }
944 buf.truncate(read as usize);
945 String::from_utf8(buf).unwrap_or_default()
946 }
947
948 #[cfg(not(target_arch = "wasm32"))]
949 {
950 format!(
951 "{:08x}-{:04x}-4{:03x}-{:04x}-{:012x}",
952 std::time::SystemTime::now()
953 .duration_since(std::time::UNIX_EPOCH)
954 .map(|d| d.as_nanos() as u32)
955 .unwrap_or(0),
956 std::process::id() as u16,
957 0u16,
958 0x8000u16,
959 0u64,
960 )
961 }
962 }
963}
964
965#[doc(hidden)]
969pub fn __run_handler<F>(ptr: i32, len: i32, f: F) -> i32
970where
971 F: FnOnce(Request) -> Response,
972{
973 let request_json = unsafe {
975 let slice = std::slice::from_raw_parts(ptr as *const u8, len as usize);
976 String::from_utf8_lossy(slice).into_owned()
977 };
978
979 let request = Request::from_json(&request_json).unwrap_or_else(|| Request {
981 method: "GET".to_string(),
982 handler: String::new(),
983 headers: HashMap::new(),
984 body: Value::Null,
985 tenant: String::new(),
986 service: String::new(),
987 auth: None,
988 });
989
990 let response = f(request);
992 let response_bytes = response.into_data().into_bytes();
993
994 let total = 4 + response_bytes.len();
996 let layout = std::alloc::Layout::from_size_align(total, 1).expect("invalid layout");
997 let out_ptr = unsafe { std::alloc::alloc(layout) };
998
999 unsafe {
1000 let len_bytes = (response_bytes.len() as u32).to_le_bytes();
1001 std::ptr::copy_nonoverlapping(len_bytes.as_ptr(), out_ptr, 4);
1002 std::ptr::copy_nonoverlapping(
1003 response_bytes.as_ptr(),
1004 out_ptr.add(4),
1005 response_bytes.len(),
1006 );
1007 }
1008
1009 out_ptr as i32
1010}
1011
1012#[macro_export]
1025macro_rules! init {
1026 () => {
1027 #[no_mangle]
1028 pub extern "C" fn alloc(size: i32) -> i32 {
1029 let layout = std::alloc::Layout::from_size_align(size as usize, 1).unwrap();
1030 unsafe { std::alloc::alloc(layout) as i32 }
1031 }
1032 };
1033}
1034
1035#[macro_export]
1062macro_rules! handler {
1063 ($name:ident, |$req:ident : Request| $body:expr) => {
1064 #[no_mangle]
1065 pub extern "C" fn $name(ptr: i32, len: i32) -> i32 {
1066 $crate::__run_handler(ptr, len, |$req: $crate::Request| $body)
1067 }
1068 };
1069}
1070
1071pub mod prelude {
1079 pub use crate::config;
1080 pub use crate::db;
1081 pub use crate::http;
1082 pub use crate::log;
1083 pub use crate::nats;
1084 pub use crate::redis;
1085 pub use crate::storage;
1086 pub use crate::util;
1087 pub use crate::Auth;
1088 pub use crate::Request;
1089 pub use crate::Response;
1090 pub use serde_json::{json, Value};
1091}
1092
1093#[cfg(test)]
1096mod tests {
1097 use super::*;
1098 use serde_json::json;
1099
1100 #[test]
1101 fn test_request_parsing() {
1102 let json = serde_json::to_string(&json!({
1103 "method": "POST",
1104 "handler": "checkout",
1105 "headers": {"content-type": "application/json"},
1106 "body": {"item": "widget", "qty": 3},
1107 "tenant": "acme",
1108 "service": "shop"
1109 }))
1110 .unwrap();
1111
1112 let req = Request::from_json(&json).unwrap();
1113 assert_eq!(req.method(), "POST");
1114 assert_eq!(req.handler(), "checkout");
1115 assert_eq!(req.tenant(), "acme");
1116 assert_eq!(req.service(), "shop");
1117 assert_eq!(req.body()["item"], "widget");
1118 assert_eq!(req.body()["qty"], 3);
1119 assert_eq!(req.header("content-type"), Some("application/json"));
1120 }
1121
1122 #[test]
1123 fn test_request_missing_fields() {
1124 let json = r#"{"method": "GET"}"#;
1125 let req = Request::from_json(json).unwrap();
1126 assert_eq!(req.method(), "GET");
1127 assert_eq!(req.handler(), "");
1128 assert_eq!(req.tenant(), "");
1129 assert_eq!(req.body(), &Value::Null);
1130 }
1131
1132 #[test]
1133 fn test_response_json() {
1134 let resp = Response::json(&json!({"status": "ok", "count": 42}));
1135 let data = resp.into_data();
1136 let parsed: Value = serde_json::from_str(&data).unwrap();
1137 assert_eq!(parsed["status"], "ok");
1138 assert_eq!(parsed["count"], 42);
1139 }
1140
1141 #[test]
1142 fn test_response_error() {
1143 let resp = Response::error("something went wrong");
1144 let data = resp.into_data();
1145 let parsed: Value = serde_json::from_str(&data).unwrap();
1146 assert_eq!(parsed["error"], "something went wrong");
1147 }
1148
1149 #[test]
1150 fn test_response_empty() {
1151 let resp = Response::empty();
1152 let data = resp.into_data();
1153 let parsed: Value = serde_json::from_str(&data).unwrap();
1154 assert_eq!(parsed["ok"], true);
1155 }
1156
1157 #[test]
1158 fn test_response_text() {
1159 let resp = Response::text("hello world");
1160 let data = resp.into_data();
1161 let parsed: Value = serde_json::from_str(&data).unwrap();
1162 assert_eq!(parsed, "hello world");
1163 }
1164
1165 #[test]
1166 fn test_db_query_noop_on_native() {
1167 let rows = db::query("SELECT 1");
1169 assert!(rows.is_empty());
1170 }
1171
1172 #[test]
1173 fn test_db_query_one_noop_on_native() {
1174 let row = db::query_one("SELECT 1");
1175 assert!(row.is_none());
1176 }
1177
1178 #[test]
1179 fn test_db_execute_noop_on_native() {
1180 let affected = db::execute("INSERT INTO x VALUES (1)");
1181 assert_eq!(affected, 0);
1182 }
1183
1184 #[test]
1185 fn test_nats_publish_noop_on_native() {
1186 let ok = nats::publish("test.subject", "payload");
1187 assert!(ok);
1188 }
1189
1190 #[test]
1191 fn test_request_with_auth() {
1192 let json = serde_json::to_string(&json!({
1193 "method": "POST",
1194 "handler": "checkout",
1195 "headers": {},
1196 "body": {},
1197 "tenant": "acme",
1198 "service": "shop",
1199 "auth": {
1200 "sub": "user-123",
1201 "preferred_username": "john",
1202 "name": "John Doe",
1203 "email": "john@example.com",
1204 "realm_roles": ["admin", "manager"],
1205 "claims": {"department": "engineering"}
1206 }
1207 }))
1208 .unwrap();
1209
1210 let req = Request::from_json(&json).unwrap();
1211 let auth = req.auth().unwrap();
1212 assert_eq!(auth.sub, "user-123");
1213 assert_eq!(auth.preferred_username.as_deref(), Some("john"));
1214 assert_eq!(auth.name.as_deref(), Some("John Doe"));
1215 assert_eq!(auth.email.as_deref(), Some("john@example.com"));
1216 assert!(auth.has_role("admin"));
1217 assert!(auth.has_role("manager"));
1218 assert!(!auth.has_role("viewer"));
1219 assert_eq!(
1220 auth.claim("department").and_then(|v| v.as_str()),
1221 Some("engineering")
1222 );
1223 }
1224
1225 #[test]
1226 fn test_request_without_auth() {
1227 let json = r#"{"method": "GET"}"#;
1228 let req = Request::from_json(json).unwrap();
1229 assert!(req.auth().is_none());
1230 }
1231
1232 #[test]
1233 fn test_request_null_auth() {
1234 let json = serde_json::to_string(&json!({
1235 "method": "GET",
1236 "auth": null
1237 }))
1238 .unwrap();
1239 let req = Request::from_json(&json).unwrap();
1240 assert!(req.auth().is_none());
1241 }
1242
1243 #[test]
1244 fn test_require_auth_success() {
1245 let json = serde_json::to_string(&json!({
1246 "method": "GET",
1247 "auth": {"sub": "user-1", "realm_roles": [], "claims": {}}
1248 }))
1249 .unwrap();
1250 let req = Request::from_json(&json).unwrap();
1251 assert!(req.require_auth().is_ok());
1252 assert_eq!(req.require_auth().unwrap().sub, "user-1");
1253 }
1254
1255 #[test]
1256 fn test_require_auth_fails_when_unauthenticated() {
1257 let json = r#"{"method": "GET"}"#;
1258 let req = Request::from_json(json).unwrap();
1259 assert!(req.require_auth().is_err());
1260 }
1261
1262 #[test]
1263 fn test_http_fetch_noop_on_native() {
1264 let resp = http::fetch("GET", "https://example.com", &[], None);
1265 assert!(resp.is_none());
1266 }
1267
1268 #[test]
1269 fn test_http_get_noop_on_native() {
1270 let resp = http::get("https://example.com", &[]);
1271 assert!(resp.is_none());
1272 }
1273
1274 #[test]
1275 fn test_http_post_noop_on_native() {
1276 let resp = http::post("https://example.com", &[], "{}");
1277 assert!(resp.is_none());
1278 }
1279
1280 #[test]
1281 fn test_storage_download_noop_on_native() {
1282 let data = storage::download("my-bucket", "images/photo.jpg");
1283 assert!(data.is_none());
1284 }
1285
1286 #[test]
1287 fn test_auth_permissions() {
1288 let json = serde_json::to_string(&json!({
1289 "method": "POST",
1290 "handler": "test",
1291 "headers": {},
1292 "body": {},
1293 "tenant": "acme",
1294 "service": "shop",
1295 "auth": {
1296 "sub": "user-1",
1297 "realm_roles": ["admin"],
1298 "claims": {},
1299 "permissions": ["staff:create", "staff:view", "items:*"],
1300 "role_names": ["admin", "manager"]
1301 }
1302 }))
1303 .unwrap();
1304
1305 let req = Request::from_json(&json).unwrap();
1306 let auth = req.auth().unwrap();
1307
1308 assert!(auth.can("staff", "create"));
1310 assert!(auth.can("staff", "view"));
1311 assert!(!auth.can("staff", "delete"));
1312
1313 assert!(auth.can("items", "create"));
1315 assert!(auth.can("items", "view"));
1316 assert!(auth.can("items", "delete"));
1317
1318 assert!(!auth.can("batches", "view"));
1320
1321 assert!(auth.has_cufflink_role("admin"));
1323 assert!(auth.has_cufflink_role("manager"));
1324 assert!(!auth.has_cufflink_role("viewer"));
1325 }
1326
1327 #[test]
1328 fn test_auth_super_wildcard() {
1329 let auth = Auth {
1330 sub: "user-1".to_string(),
1331 preferred_username: None,
1332 name: None,
1333 email: None,
1334 realm_roles: vec![],
1335 claims: HashMap::new(),
1336 permissions: vec!["*".to_string()],
1337 role_names: vec!["superadmin".to_string()],
1338 is_service_account: false,
1339 };
1340
1341 assert!(auth.can("anything", "everything"));
1342 assert!(auth.can("staff", "create"));
1343 }
1344
1345 #[test]
1346 fn test_auth_empty_permissions() {
1347 let auth = Auth {
1348 sub: "user-1".to_string(),
1349 preferred_username: None,
1350 name: None,
1351 email: None,
1352 realm_roles: vec![],
1353 claims: HashMap::new(),
1354 permissions: vec![],
1355 role_names: vec![],
1356 is_service_account: false,
1357 };
1358
1359 assert!(!auth.can("staff", "create"));
1360 assert!(!auth.has_cufflink_role("admin"));
1361 }
1362
1363 #[test]
1364 fn test_redis_get_noop_on_native() {
1365 let val = redis::get("some-key");
1366 assert!(val.is_none());
1367 }
1368
1369 #[test]
1370 fn test_redis_set_noop_on_native() {
1371 let ok = redis::set("key", "value", 3600);
1372 assert!(ok);
1373 }
1374
1375 #[test]
1376 fn test_redis_del_noop_on_native() {
1377 let ok = redis::del("key");
1378 assert!(ok);
1379 }
1380
1381 #[test]
1382 fn test_http_fetch_response_helpers() {
1383 let resp = http::FetchResponse {
1384 status: 200,
1385 body: r#"{"key": "value"}"#.to_string(),
1386 body_encoding: "utf8".to_string(),
1387 headers: HashMap::new(),
1388 };
1389 assert!(resp.is_success());
1390 assert!(!resp.is_base64());
1391 let json = resp.json().unwrap();
1392 assert_eq!(json["key"], "value");
1393
1394 let err_resp = http::FetchResponse {
1395 status: 404,
1396 body: "not found".to_string(),
1397 body_encoding: "utf8".to_string(),
1398 headers: HashMap::new(),
1399 };
1400 assert!(!err_resp.is_success());
1401
1402 let binary_resp = http::FetchResponse {
1403 status: 200,
1404 body: "aW1hZ2VkYXRh".to_string(),
1405 body_encoding: "base64".to_string(),
1406 headers: HashMap::new(),
1407 };
1408 assert!(binary_resp.is_base64());
1409 }
1410}