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 s3_presign_upload(
66 bucket_ptr: i32,
67 bucket_len: i32,
68 key_ptr: i32,
69 key_len: i32,
70 content_type_ptr: i32,
71 content_type_len: i32,
72 expires_secs: i32,
73 ) -> i32;
74 fn redis_get(key_ptr: i32, key_len: i32) -> i32;
75 fn redis_set(key_ptr: i32, key_len: i32, val_ptr: i32, val_len: i32, ttl_secs: i32) -> i32;
76 fn redis_del(key_ptr: i32, key_len: i32) -> i32;
77 fn generate_uuid() -> i32;
78}
79
80#[derive(Debug, Clone)]
101pub struct Auth {
102 pub sub: String,
104 pub preferred_username: Option<String>,
106 pub name: Option<String>,
108 pub email: Option<String>,
110 pub realm_roles: Vec<String>,
112 pub claims: HashMap<String, Value>,
114 pub permissions: Vec<String>,
116 pub role_names: Vec<String>,
118 pub is_service_account: bool,
121}
122
123impl Auth {
124 pub fn has_role(&self, role: &str) -> bool {
126 self.realm_roles.iter().any(|r| r == role)
127 }
128
129 pub fn can(&self, area: &str, operation: &str) -> bool {
140 let required = format!("{}:{}", area, operation);
141 let wildcard = format!("{}:*", area);
142 self.permissions
143 .iter()
144 .any(|p| p == &required || p == &wildcard || p == "*")
145 }
146
147 pub fn has_cufflink_role(&self, role: &str) -> bool {
149 self.role_names.iter().any(|r| r == role)
150 }
151
152 pub fn claim(&self, key: &str) -> Option<&Value> {
154 self.claims.get(key)
155 }
156}
157
158#[derive(Debug, Clone)]
165pub struct Request {
166 method: String,
167 handler: String,
168 headers: HashMap<String, String>,
169 body: Value,
170 tenant: String,
171 service: String,
172 auth: Option<Auth>,
173}
174
175impl Request {
176 pub fn from_json(json: &str) -> Option<Self> {
178 let v: Value = serde_json::from_str(json).ok()?;
179 let headers = v["headers"]
180 .as_object()
181 .map(|m| {
182 m.iter()
183 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
184 .collect()
185 })
186 .unwrap_or_default();
187
188 let auth = v["auth"].as_object().map(|auth_obj| {
189 let a = Value::Object(auth_obj.clone());
190 Auth {
191 sub: a["sub"].as_str().unwrap_or("").to_string(),
192 preferred_username: a["preferred_username"].as_str().map(|s| s.to_string()),
193 name: a["name"].as_str().map(|s| s.to_string()),
194 email: a["email"].as_str().map(|s| s.to_string()),
195 realm_roles: a["realm_roles"]
196 .as_array()
197 .map(|arr| {
198 arr.iter()
199 .filter_map(|v| v.as_str().map(|s| s.to_string()))
200 .collect()
201 })
202 .unwrap_or_default(),
203 claims: a["claims"]
204 .as_object()
205 .map(|m| m.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
206 .unwrap_or_default(),
207 permissions: a["permissions"]
208 .as_array()
209 .map(|arr| {
210 arr.iter()
211 .filter_map(|v| v.as_str().map(|s| s.to_string()))
212 .collect()
213 })
214 .unwrap_or_default(),
215 role_names: a["role_names"]
216 .as_array()
217 .map(|arr| {
218 arr.iter()
219 .filter_map(|v| v.as_str().map(|s| s.to_string()))
220 .collect()
221 })
222 .unwrap_or_default(),
223 is_service_account: a["is_service_account"].as_bool().unwrap_or(false),
224 }
225 });
226
227 Some(Self {
228 method: v["method"].as_str().unwrap_or("GET").to_string(),
229 handler: v["handler"].as_str().unwrap_or("").to_string(),
230 headers,
231 body: v["body"].clone(),
232 tenant: v["tenant"].as_str().unwrap_or("").to_string(),
233 service: v["service"].as_str().unwrap_or("").to_string(),
234 auth,
235 })
236 }
237
238 pub fn method(&self) -> &str {
240 &self.method
241 }
242
243 pub fn handler(&self) -> &str {
245 &self.handler
246 }
247
248 pub fn headers(&self) -> &HashMap<String, String> {
250 &self.headers
251 }
252
253 pub fn header(&self, name: &str) -> Option<&str> {
255 self.headers.get(name).map(|s| s.as_str())
256 }
257
258 pub fn body(&self) -> &Value {
260 &self.body
261 }
262
263 pub fn tenant(&self) -> &str {
265 &self.tenant
266 }
267
268 pub fn service(&self) -> &str {
270 &self.service
271 }
272
273 pub fn auth(&self) -> Option<&Auth> {
277 self.auth.as_ref()
278 }
279
280 pub fn require_auth(&self) -> Result<&Auth, Response> {
292 self.auth.as_ref().ok_or_else(|| {
293 Response::json(&serde_json::json!({
294 "error": "Authentication required",
295 "status": 401
296 }))
297 })
298 }
299}
300
301#[derive(Debug, Clone)]
305pub struct Response {
306 data: String,
307}
308
309impl Response {
310 pub fn json(value: &Value) -> Self {
312 Self {
313 data: serde_json::to_string(value).unwrap_or_else(|_| "{}".to_string()),
314 }
315 }
316
317 pub fn text(s: &str) -> Self {
319 Self::json(&Value::String(s.to_string()))
320 }
321
322 pub fn error(message: &str) -> Self {
324 Self::json(&serde_json::json!({"error": message}))
325 }
326
327 pub fn empty() -> Self {
329 Self::json(&serde_json::json!({"ok": true}))
330 }
331
332 pub fn into_data(self) -> String {
334 self.data
335 }
336}
337
338pub mod db {
345 use super::*;
346
347 pub fn query(sql: &str) -> Vec<Value> {
358 #[cfg(target_arch = "wasm32")]
359 {
360 let bytes = sql.as_bytes();
361 let result = unsafe { db_query(bytes.as_ptr() as i32, bytes.len() as i32) };
362 if result < 0 {
363 return vec![];
364 }
365 read_host_response()
366 }
367 #[cfg(not(target_arch = "wasm32"))]
368 {
369 let _ = sql;
370 vec![]
371 }
372 }
373
374 pub fn query_one(sql: &str) -> Option<Value> {
382 query(sql).into_iter().next()
383 }
384
385 pub fn execute(sql: &str) -> i32 {
394 #[cfg(target_arch = "wasm32")]
395 {
396 let bytes = sql.as_bytes();
397 unsafe { db_execute(bytes.as_ptr() as i32, bytes.len() as i32) }
398 }
399 #[cfg(not(target_arch = "wasm32"))]
400 {
401 let _ = sql;
402 0
403 }
404 }
405
406 #[cfg(target_arch = "wasm32")]
408 fn read_host_response() -> Vec<Value> {
409 let len = unsafe { get_host_response_len() };
410 if len <= 0 {
411 return vec![];
412 }
413 let mut buf = vec![0u8; len as usize];
414 let read = unsafe { get_host_response(buf.as_mut_ptr() as i32, len) };
415 if read <= 0 {
416 return vec![];
417 }
418 buf.truncate(read as usize);
419 let json_str = String::from_utf8_lossy(&buf);
420 serde_json::from_str(&json_str).unwrap_or_default()
421 }
422}
423
424pub mod nats {
431 #[allow(unused_imports)]
432 use super::*;
433
434 pub fn publish(subject: &str, payload: &str) -> bool {
445 #[cfg(target_arch = "wasm32")]
446 {
447 let subj_bytes = subject.as_bytes();
448 let payload_bytes = payload.as_bytes();
449 let result = unsafe {
450 nats_publish(
451 subj_bytes.as_ptr() as i32,
452 subj_bytes.len() as i32,
453 payload_bytes.as_ptr() as i32,
454 payload_bytes.len() as i32,
455 )
456 };
457 result == 0
458 }
459 #[cfg(not(target_arch = "wasm32"))]
460 {
461 let _ = (subject, payload);
462 true
463 }
464 }
465
466 pub fn request(subject: &str, payload: &str, timeout_ms: i32) -> Option<String> {
478 #[cfg(target_arch = "wasm32")]
479 {
480 let subj_bytes = subject.as_bytes();
481 let payload_bytes = payload.as_bytes();
482 let result = unsafe {
483 nats_request(
484 subj_bytes.as_ptr() as i32,
485 subj_bytes.len() as i32,
486 payload_bytes.as_ptr() as i32,
487 payload_bytes.len() as i32,
488 timeout_ms,
489 )
490 };
491 if result != 0 {
492 return None;
493 }
494 let len = unsafe { get_host_response_len() };
495 if len <= 0 {
496 return None;
497 }
498 let mut buf = vec![0u8; len as usize];
499 let read = unsafe { get_host_response(buf.as_mut_ptr() as i32, len) };
500 if read <= 0 {
501 return None;
502 }
503 String::from_utf8(buf[..read as usize].to_vec()).ok()
504 }
505 #[cfg(not(target_arch = "wasm32"))]
506 {
507 let _ = (subject, payload, timeout_ms);
508 None
509 }
510 }
511}
512
513pub mod log {
519 #[allow(unused_imports)]
520 use super::*;
521
522 pub fn error(msg: &str) {
524 write(0, msg);
525 }
526
527 pub fn warn(msg: &str) {
529 write(1, msg);
530 }
531
532 pub fn info(msg: &str) {
534 write(2, msg);
535 }
536
537 pub fn debug(msg: &str) {
539 write(3, msg);
540 }
541
542 fn write(level: i32, msg: &str) {
543 #[cfg(target_arch = "wasm32")]
544 {
545 let bytes = msg.as_bytes();
546 unsafe {
547 super::cufflink_log(level, bytes.as_ptr() as i32, bytes.len() as i32);
548 }
549 }
550 #[cfg(not(target_arch = "wasm32"))]
551 {
552 let _ = (level, msg);
553 }
554 }
555}
556
557pub mod http {
564 #[allow(unused_imports)]
565 use super::*;
566
567 #[derive(Debug, Clone)]
569 pub struct FetchResponse {
570 pub status: i32,
572 pub body: String,
574 pub body_encoding: String,
576 pub headers: HashMap<String, String>,
578 }
579
580 impl FetchResponse {
581 pub fn json(&self) -> Option<Value> {
583 serde_json::from_str(&self.body).ok()
584 }
585
586 pub fn is_success(&self) -> bool {
588 (200..300).contains(&self.status)
589 }
590
591 pub fn is_base64(&self) -> bool {
593 self.body_encoding == "base64"
594 }
595 }
596
597 pub fn fetch(
608 method: &str,
609 url: &str,
610 headers: &[(&str, &str)],
611 body: Option<&str>,
612 ) -> Option<FetchResponse> {
613 #[cfg(target_arch = "wasm32")]
614 {
615 let method_bytes = method.as_bytes();
616 let url_bytes = url.as_bytes();
617 let headers_map: HashMap<&str, &str> = headers.iter().copied().collect();
618 let headers_json = serde_json::to_string(&headers_map).unwrap_or_default();
619 let headers_bytes = headers_json.as_bytes();
620 let body_bytes = body.unwrap_or("").as_bytes();
621 let body_len = body.map(|b| b.len()).unwrap_or(0);
622
623 let result = unsafe {
624 http_fetch(
625 method_bytes.as_ptr() as i32,
626 method_bytes.len() as i32,
627 url_bytes.as_ptr() as i32,
628 url_bytes.len() as i32,
629 headers_bytes.as_ptr() as i32,
630 headers_bytes.len() as i32,
631 body_bytes.as_ptr() as i32,
632 body_len as i32,
633 )
634 };
635
636 if result < 0 {
637 return None;
638 }
639
640 read_fetch_response()
641 }
642 #[cfg(not(target_arch = "wasm32"))]
643 {
644 let _ = (method, url, headers, body);
645 None
646 }
647 }
648
649 pub fn get(url: &str, headers: &[(&str, &str)]) -> Option<FetchResponse> {
651 fetch("GET", url, headers, None)
652 }
653
654 pub fn post(url: &str, headers: &[(&str, &str)], body: &str) -> Option<FetchResponse> {
656 fetch("POST", url, headers, Some(body))
657 }
658
659 pub fn put(url: &str, headers: &[(&str, &str)], body: &str) -> Option<FetchResponse> {
661 fetch("PUT", url, headers, Some(body))
662 }
663
664 pub fn delete(url: &str, headers: &[(&str, &str)]) -> Option<FetchResponse> {
666 fetch("DELETE", url, headers, None)
667 }
668
669 pub fn patch(url: &str, headers: &[(&str, &str)], body: &str) -> Option<FetchResponse> {
671 fetch("PATCH", url, headers, Some(body))
672 }
673
674 #[cfg(target_arch = "wasm32")]
676 fn read_fetch_response() -> Option<FetchResponse> {
677 let len = unsafe { get_host_response_len() };
678 if len <= 0 {
679 return None;
680 }
681 let mut buf = vec![0u8; len as usize];
682 let read = unsafe { get_host_response(buf.as_mut_ptr() as i32, len) };
683 if read <= 0 {
684 return None;
685 }
686 buf.truncate(read as usize);
687 let json_str = String::from_utf8_lossy(&buf);
688 let v: Value = serde_json::from_str(&json_str).ok()?;
689 Some(FetchResponse {
690 status: v["status"].as_i64().unwrap_or(0) as i32,
691 body: v["body"].as_str().unwrap_or("").to_string(),
692 body_encoding: v["body_encoding"].as_str().unwrap_or("utf8").to_string(),
693 headers: v["headers"]
694 .as_object()
695 .map(|m| {
696 m.iter()
697 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
698 .collect()
699 })
700 .unwrap_or_default(),
701 })
702 }
703}
704
705pub mod config {
713 #[allow(unused_imports)]
714 use super::*;
715
716 pub fn get(key: &str) -> Option<String> {
725 #[cfg(target_arch = "wasm32")]
726 {
727 let bytes = key.as_bytes();
728 let result = unsafe { get_config(bytes.as_ptr() as i32, bytes.len() as i32) };
729 if result < 0 {
730 return None;
731 }
732 read_config_response()
733 }
734 #[cfg(not(target_arch = "wasm32"))]
735 {
736 let _ = key;
737 None
738 }
739 }
740
741 #[cfg(target_arch = "wasm32")]
743 fn read_config_response() -> Option<String> {
744 let len = unsafe { get_host_response_len() };
745 if len <= 0 {
746 return None;
747 }
748 let mut buf = vec![0u8; len as usize];
749 let read = unsafe { get_host_response(buf.as_mut_ptr() as i32, len) };
750 if read <= 0 {
751 return None;
752 }
753 buf.truncate(read as usize);
754 String::from_utf8(buf).ok()
755 }
756}
757
758pub mod storage {
766 #[allow(unused_imports)]
767 use super::*;
768
769 pub fn download(bucket: &str, key: &str) -> Option<String> {
780 #[cfg(target_arch = "wasm32")]
781 {
782 let bucket_bytes = bucket.as_bytes();
783 let key_bytes = key.as_bytes();
784 let result = unsafe {
785 s3_download(
786 bucket_bytes.as_ptr() as i32,
787 bucket_bytes.len() as i32,
788 key_bytes.as_ptr() as i32,
789 key_bytes.len() as i32,
790 )
791 };
792 if result < 0 {
793 return None;
794 }
795 read_storage_response()
796 }
797 #[cfg(not(target_arch = "wasm32"))]
798 {
799 let _ = (bucket, key);
800 None
801 }
802 }
803
804 pub fn presign_upload(
817 bucket: &str,
818 key: &str,
819 content_type: &str,
820 expires_secs: u64,
821 ) -> Option<String> {
822 #[cfg(target_arch = "wasm32")]
823 {
824 let bucket_bytes = bucket.as_bytes();
825 let key_bytes = key.as_bytes();
826 let ct_bytes = content_type.as_bytes();
827 let result = unsafe {
828 s3_presign_upload(
829 bucket_bytes.as_ptr() as i32,
830 bucket_bytes.len() as i32,
831 key_bytes.as_ptr() as i32,
832 key_bytes.len() as i32,
833 ct_bytes.as_ptr() as i32,
834 ct_bytes.len() as i32,
835 expires_secs as i32,
836 )
837 };
838 if result < 0 {
839 return None;
840 }
841 read_storage_response()
842 }
843 #[cfg(not(target_arch = "wasm32"))]
844 {
845 let _ = (bucket, key, content_type, expires_secs);
846 None
847 }
848 }
849
850 #[cfg(target_arch = "wasm32")]
852 fn read_storage_response() -> Option<String> {
853 let len = unsafe { get_host_response_len() };
854 if len <= 0 {
855 return None;
856 }
857 let mut buf = vec![0u8; len as usize];
858 let read = unsafe { get_host_response(buf.as_mut_ptr() as i32, len) };
859 if read <= 0 {
860 return None;
861 }
862 buf.truncate(read as usize);
863 String::from_utf8(buf).ok()
864 }
865}
866
867pub mod redis {
874 #[allow(unused_imports)]
875 use super::*;
876
877 pub fn get(key: &str) -> Option<String> {
886 #[cfg(target_arch = "wasm32")]
887 {
888 let bytes = key.as_bytes();
889 let result = unsafe { redis_get(bytes.as_ptr() as i32, bytes.len() as i32) };
890 if result < 0 {
891 return None;
892 }
893 read_redis_response()
894 }
895 #[cfg(not(target_arch = "wasm32"))]
896 {
897 let _ = key;
898 None
899 }
900 }
901
902 pub fn set(key: &str, value: &str, ttl_secs: i32) -> bool {
910 #[cfg(target_arch = "wasm32")]
911 {
912 let key_bytes = key.as_bytes();
913 let val_bytes = value.as_bytes();
914 let result = unsafe {
915 redis_set(
916 key_bytes.as_ptr() as i32,
917 key_bytes.len() as i32,
918 val_bytes.as_ptr() as i32,
919 val_bytes.len() as i32,
920 ttl_secs,
921 )
922 };
923 result == 0
924 }
925 #[cfg(not(target_arch = "wasm32"))]
926 {
927 let _ = (key, value, ttl_secs);
928 true
929 }
930 }
931
932 pub fn del(key: &str) -> bool {
940 #[cfg(target_arch = "wasm32")]
941 {
942 let bytes = key.as_bytes();
943 let result = unsafe { redis_del(bytes.as_ptr() as i32, bytes.len() as i32) };
944 result == 0
945 }
946 #[cfg(not(target_arch = "wasm32"))]
947 {
948 let _ = key;
949 true
950 }
951 }
952
953 #[cfg(target_arch = "wasm32")]
955 fn read_redis_response() -> Option<String> {
956 let len = unsafe { get_host_response_len() };
957 if len <= 0 {
958 return None;
959 }
960 let mut buf = vec![0u8; len as usize];
961 let read = unsafe { get_host_response(buf.as_mut_ptr() as i32, len) };
962 if read <= 0 {
963 return None;
964 }
965 buf.truncate(read as usize);
966 String::from_utf8(buf).ok()
967 }
968}
969
970pub mod util {
974 #[allow(unused_imports)]
975 use super::*;
976
977 pub fn generate_uuid() -> String {
984 #[cfg(target_arch = "wasm32")]
985 {
986 let result = unsafe { super::generate_uuid() };
987 if result < 0 {
988 return String::new();
989 }
990 let len = unsafe { get_host_response_len() };
991 if len <= 0 {
992 return String::new();
993 }
994 let mut buf = vec![0u8; len as usize];
995 let read = unsafe { get_host_response(buf.as_mut_ptr() as i32, len) };
996 if read <= 0 {
997 return String::new();
998 }
999 buf.truncate(read as usize);
1000 String::from_utf8(buf).unwrap_or_default()
1001 }
1002
1003 #[cfg(not(target_arch = "wasm32"))]
1004 {
1005 format!(
1006 "{:08x}-{:04x}-4{:03x}-{:04x}-{:012x}",
1007 std::time::SystemTime::now()
1008 .duration_since(std::time::UNIX_EPOCH)
1009 .map(|d| d.as_nanos() as u32)
1010 .unwrap_or(0),
1011 std::process::id() as u16,
1012 0u16,
1013 0x8000u16,
1014 0u64,
1015 )
1016 }
1017 }
1018}
1019
1020#[doc(hidden)]
1024pub fn __run_handler<F>(ptr: i32, len: i32, f: F) -> i32
1025where
1026 F: FnOnce(Request) -> Response,
1027{
1028 let request_json = unsafe {
1030 let slice = std::slice::from_raw_parts(ptr as *const u8, len as usize);
1031 String::from_utf8_lossy(slice).into_owned()
1032 };
1033
1034 let request = Request::from_json(&request_json).unwrap_or_else(|| Request {
1036 method: "GET".to_string(),
1037 handler: String::new(),
1038 headers: HashMap::new(),
1039 body: Value::Null,
1040 tenant: String::new(),
1041 service: String::new(),
1042 auth: None,
1043 });
1044
1045 let response = f(request);
1047 let response_bytes = response.into_data().into_bytes();
1048
1049 let total = 4 + response_bytes.len();
1051 let layout = std::alloc::Layout::from_size_align(total, 1).expect("invalid layout");
1052 let out_ptr = unsafe { std::alloc::alloc(layout) };
1053
1054 unsafe {
1055 let len_bytes = (response_bytes.len() as u32).to_le_bytes();
1056 std::ptr::copy_nonoverlapping(len_bytes.as_ptr(), out_ptr, 4);
1057 std::ptr::copy_nonoverlapping(
1058 response_bytes.as_ptr(),
1059 out_ptr.add(4),
1060 response_bytes.len(),
1061 );
1062 }
1063
1064 out_ptr as i32
1065}
1066
1067#[macro_export]
1080macro_rules! init {
1081 () => {
1082 #[no_mangle]
1083 pub extern "C" fn alloc(size: i32) -> i32 {
1084 let layout = std::alloc::Layout::from_size_align(size as usize, 1).unwrap();
1085 unsafe { std::alloc::alloc(layout) as i32 }
1086 }
1087 };
1088}
1089
1090#[macro_export]
1117macro_rules! handler {
1118 ($name:ident, |$req:ident : Request| $body:expr) => {
1119 #[no_mangle]
1120 pub extern "C" fn $name(ptr: i32, len: i32) -> i32 {
1121 $crate::__run_handler(ptr, len, |$req: $crate::Request| $body)
1122 }
1123 };
1124}
1125
1126pub mod prelude {
1134 pub use crate::config;
1135 pub use crate::db;
1136 pub use crate::http;
1137 pub use crate::log;
1138 pub use crate::nats;
1139 pub use crate::redis;
1140 pub use crate::storage;
1141 pub use crate::util;
1142 pub use crate::Auth;
1143 pub use crate::Request;
1144 pub use crate::Response;
1145 pub use serde_json::{json, Value};
1146}
1147
1148#[cfg(test)]
1151mod tests {
1152 use super::*;
1153 use serde_json::json;
1154
1155 #[test]
1156 fn test_request_parsing() {
1157 let json = serde_json::to_string(&json!({
1158 "method": "POST",
1159 "handler": "checkout",
1160 "headers": {"content-type": "application/json"},
1161 "body": {"item": "widget", "qty": 3},
1162 "tenant": "acme",
1163 "service": "shop"
1164 }))
1165 .unwrap();
1166
1167 let req = Request::from_json(&json).unwrap();
1168 assert_eq!(req.method(), "POST");
1169 assert_eq!(req.handler(), "checkout");
1170 assert_eq!(req.tenant(), "acme");
1171 assert_eq!(req.service(), "shop");
1172 assert_eq!(req.body()["item"], "widget");
1173 assert_eq!(req.body()["qty"], 3);
1174 assert_eq!(req.header("content-type"), Some("application/json"));
1175 }
1176
1177 #[test]
1178 fn test_request_missing_fields() {
1179 let json = r#"{"method": "GET"}"#;
1180 let req = Request::from_json(json).unwrap();
1181 assert_eq!(req.method(), "GET");
1182 assert_eq!(req.handler(), "");
1183 assert_eq!(req.tenant(), "");
1184 assert_eq!(req.body(), &Value::Null);
1185 }
1186
1187 #[test]
1188 fn test_response_json() {
1189 let resp = Response::json(&json!({"status": "ok", "count": 42}));
1190 let data = resp.into_data();
1191 let parsed: Value = serde_json::from_str(&data).unwrap();
1192 assert_eq!(parsed["status"], "ok");
1193 assert_eq!(parsed["count"], 42);
1194 }
1195
1196 #[test]
1197 fn test_response_error() {
1198 let resp = Response::error("something went wrong");
1199 let data = resp.into_data();
1200 let parsed: Value = serde_json::from_str(&data).unwrap();
1201 assert_eq!(parsed["error"], "something went wrong");
1202 }
1203
1204 #[test]
1205 fn test_response_empty() {
1206 let resp = Response::empty();
1207 let data = resp.into_data();
1208 let parsed: Value = serde_json::from_str(&data).unwrap();
1209 assert_eq!(parsed["ok"], true);
1210 }
1211
1212 #[test]
1213 fn test_response_text() {
1214 let resp = Response::text("hello world");
1215 let data = resp.into_data();
1216 let parsed: Value = serde_json::from_str(&data).unwrap();
1217 assert_eq!(parsed, "hello world");
1218 }
1219
1220 #[test]
1221 fn test_db_query_noop_on_native() {
1222 let rows = db::query("SELECT 1");
1224 assert!(rows.is_empty());
1225 }
1226
1227 #[test]
1228 fn test_db_query_one_noop_on_native() {
1229 let row = db::query_one("SELECT 1");
1230 assert!(row.is_none());
1231 }
1232
1233 #[test]
1234 fn test_db_execute_noop_on_native() {
1235 let affected = db::execute("INSERT INTO x VALUES (1)");
1236 assert_eq!(affected, 0);
1237 }
1238
1239 #[test]
1240 fn test_nats_publish_noop_on_native() {
1241 let ok = nats::publish("test.subject", "payload");
1242 assert!(ok);
1243 }
1244
1245 #[test]
1246 fn test_request_with_auth() {
1247 let json = serde_json::to_string(&json!({
1248 "method": "POST",
1249 "handler": "checkout",
1250 "headers": {},
1251 "body": {},
1252 "tenant": "acme",
1253 "service": "shop",
1254 "auth": {
1255 "sub": "user-123",
1256 "preferred_username": "john",
1257 "name": "John Doe",
1258 "email": "john@example.com",
1259 "realm_roles": ["admin", "manager"],
1260 "claims": {"department": "engineering"}
1261 }
1262 }))
1263 .unwrap();
1264
1265 let req = Request::from_json(&json).unwrap();
1266 let auth = req.auth().unwrap();
1267 assert_eq!(auth.sub, "user-123");
1268 assert_eq!(auth.preferred_username.as_deref(), Some("john"));
1269 assert_eq!(auth.name.as_deref(), Some("John Doe"));
1270 assert_eq!(auth.email.as_deref(), Some("john@example.com"));
1271 assert!(auth.has_role("admin"));
1272 assert!(auth.has_role("manager"));
1273 assert!(!auth.has_role("viewer"));
1274 assert_eq!(
1275 auth.claim("department").and_then(|v| v.as_str()),
1276 Some("engineering")
1277 );
1278 }
1279
1280 #[test]
1281 fn test_request_without_auth() {
1282 let json = r#"{"method": "GET"}"#;
1283 let req = Request::from_json(json).unwrap();
1284 assert!(req.auth().is_none());
1285 }
1286
1287 #[test]
1288 fn test_request_null_auth() {
1289 let json = serde_json::to_string(&json!({
1290 "method": "GET",
1291 "auth": null
1292 }))
1293 .unwrap();
1294 let req = Request::from_json(&json).unwrap();
1295 assert!(req.auth().is_none());
1296 }
1297
1298 #[test]
1299 fn test_require_auth_success() {
1300 let json = serde_json::to_string(&json!({
1301 "method": "GET",
1302 "auth": {"sub": "user-1", "realm_roles": [], "claims": {}}
1303 }))
1304 .unwrap();
1305 let req = Request::from_json(&json).unwrap();
1306 assert!(req.require_auth().is_ok());
1307 assert_eq!(req.require_auth().unwrap().sub, "user-1");
1308 }
1309
1310 #[test]
1311 fn test_require_auth_fails_when_unauthenticated() {
1312 let json = r#"{"method": "GET"}"#;
1313 let req = Request::from_json(json).unwrap();
1314 assert!(req.require_auth().is_err());
1315 }
1316
1317 #[test]
1318 fn test_http_fetch_noop_on_native() {
1319 let resp = http::fetch("GET", "https://example.com", &[], None);
1320 assert!(resp.is_none());
1321 }
1322
1323 #[test]
1324 fn test_http_get_noop_on_native() {
1325 let resp = http::get("https://example.com", &[]);
1326 assert!(resp.is_none());
1327 }
1328
1329 #[test]
1330 fn test_http_post_noop_on_native() {
1331 let resp = http::post("https://example.com", &[], "{}");
1332 assert!(resp.is_none());
1333 }
1334
1335 #[test]
1336 fn test_storage_download_noop_on_native() {
1337 let data = storage::download("my-bucket", "images/photo.jpg");
1338 assert!(data.is_none());
1339 }
1340
1341 #[test]
1342 fn test_auth_permissions() {
1343 let json = serde_json::to_string(&json!({
1344 "method": "POST",
1345 "handler": "test",
1346 "headers": {},
1347 "body": {},
1348 "tenant": "acme",
1349 "service": "shop",
1350 "auth": {
1351 "sub": "user-1",
1352 "realm_roles": ["admin"],
1353 "claims": {},
1354 "permissions": ["staff:create", "staff:view", "items:*"],
1355 "role_names": ["admin", "manager"]
1356 }
1357 }))
1358 .unwrap();
1359
1360 let req = Request::from_json(&json).unwrap();
1361 let auth = req.auth().unwrap();
1362
1363 assert!(auth.can("staff", "create"));
1365 assert!(auth.can("staff", "view"));
1366 assert!(!auth.can("staff", "delete"));
1367
1368 assert!(auth.can("items", "create"));
1370 assert!(auth.can("items", "view"));
1371 assert!(auth.can("items", "delete"));
1372
1373 assert!(!auth.can("batches", "view"));
1375
1376 assert!(auth.has_cufflink_role("admin"));
1378 assert!(auth.has_cufflink_role("manager"));
1379 assert!(!auth.has_cufflink_role("viewer"));
1380 }
1381
1382 #[test]
1383 fn test_auth_super_wildcard() {
1384 let auth = Auth {
1385 sub: "user-1".to_string(),
1386 preferred_username: None,
1387 name: None,
1388 email: None,
1389 realm_roles: vec![],
1390 claims: HashMap::new(),
1391 permissions: vec!["*".to_string()],
1392 role_names: vec!["superadmin".to_string()],
1393 is_service_account: false,
1394 };
1395
1396 assert!(auth.can("anything", "everything"));
1397 assert!(auth.can("staff", "create"));
1398 }
1399
1400 #[test]
1401 fn test_auth_empty_permissions() {
1402 let auth = Auth {
1403 sub: "user-1".to_string(),
1404 preferred_username: None,
1405 name: None,
1406 email: None,
1407 realm_roles: vec![],
1408 claims: HashMap::new(),
1409 permissions: vec![],
1410 role_names: vec![],
1411 is_service_account: false,
1412 };
1413
1414 assert!(!auth.can("staff", "create"));
1415 assert!(!auth.has_cufflink_role("admin"));
1416 }
1417
1418 #[test]
1419 fn test_redis_get_noop_on_native() {
1420 let val = redis::get("some-key");
1421 assert!(val.is_none());
1422 }
1423
1424 #[test]
1425 fn test_redis_set_noop_on_native() {
1426 let ok = redis::set("key", "value", 3600);
1427 assert!(ok);
1428 }
1429
1430 #[test]
1431 fn test_redis_del_noop_on_native() {
1432 let ok = redis::del("key");
1433 assert!(ok);
1434 }
1435
1436 #[test]
1437 fn test_http_fetch_response_helpers() {
1438 let resp = http::FetchResponse {
1439 status: 200,
1440 body: r#"{"key": "value"}"#.to_string(),
1441 body_encoding: "utf8".to_string(),
1442 headers: HashMap::new(),
1443 };
1444 assert!(resp.is_success());
1445 assert!(!resp.is_base64());
1446 let json = resp.json().unwrap();
1447 assert_eq!(json["key"], "value");
1448
1449 let err_resp = http::FetchResponse {
1450 status: 404,
1451 body: "not found".to_string(),
1452 body_encoding: "utf8".to_string(),
1453 headers: HashMap::new(),
1454 };
1455 assert!(!err_resp.is_success());
1456
1457 let binary_resp = http::FetchResponse {
1458 status: 200,
1459 body: "aW1hZ2VkYXRh".to_string(),
1460 body_encoding: "base64".to_string(),
1461 headers: HashMap::new(),
1462 };
1463 assert!(binary_resp.is_base64());
1464 }
1465}