1use std::sync::{Arc, LazyLock};
49
50use regex::Regex;
51use thiserror::Error;
52
53static API_KEY_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
56 Regex::new(r"\b[a-zA-Z0-9_-]{3,}\.[a-zA-Z0-9_-]{10,}\b").expect("invalid regex")
57});
58
59static SENSITIVE_PATTERNS: LazyLock<Vec<(Regex, &'static str)>> = LazyLock::new(|| {
60 vec![
61 (
62 Regex::new(r"(?i)(api[_-]?key\s*[=:]\s*)[^\s,]+").expect("invalid regex"),
63 "$1[FILTERED]",
64 ),
65 (
66 Regex::new(r"(?i)(password\s*[=:]\s*)[^\s,]+").expect("invalid regex"),
67 "$1[FILTERED]",
68 ),
69 (
70 Regex::new(r"(?i)(token\s*[=:]\s*)[^\s,]+").expect("invalid regex"),
71 "$1[FILTERED]",
72 ),
73 (
74 Regex::new(r"(?i)(secret\s*[=:]\s*)[^\s,]+").expect("invalid regex"),
75 "$1[FILTERED]",
76 ),
77 (
78 Regex::new(r"(?i)(bearer\s+[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+)").expect("invalid regex"),
79 "bearer [FILTERED]",
80 ),
81 (
82 Regex::new(r"(?i)(authorization\s*:\s*Bearer\s+)[^\s,]+").expect("invalid regex"),
83 "$1[FILTERED]",
84 ),
85 ]
86});
87
88static CONTAINS_SENSITIVE_PATTERNS: LazyLock<Vec<Regex>> = LazyLock::new(|| {
89 vec![
90 Regex::new(r"(?i)api[_-]?key\s*[=:]").expect("invalid regex"),
91 Regex::new(r"(?i)password\s*[=:]").expect("invalid regex"),
92 Regex::new(r"(?i)token\s*[=:]").expect("invalid regex"),
93 Regex::new(r"(?i)secret\s*[=:]").expect("invalid regex"),
94 Regex::new(r"(?i)authorization\s*:\s*Bearer").expect("invalid regex"),
95 ]
96});
97
98pub fn mask_sensitive_info(text: &str) -> String {
132 let mut result = API_KEY_PATTERN.replace_all(text, "[FILTERED]").to_string();
133
134 for (re, replacement) in SENSITIVE_PATTERNS.iter() {
135 result = re.replace_all(&result, *replacement).to_string();
136 }
137
138 result
139}
140
141pub fn mask_api_key(text: &str) -> String {
146 API_KEY_PATTERN.replace_all(text, "[FILTERED]").to_string()
147}
148
149pub fn contains_sensitive_info(text: &str) -> bool {
151 if API_KEY_PATTERN.is_match(text) {
152 return true;
153 }
154
155 CONTAINS_SENSITIVE_PATTERNS
156 .iter()
157 .any(|re| re.is_match(text))
158}
159
160pub fn validate_api_key(api_key: &str) -> ZaiResult<()> {
185 if api_key.is_empty() {
186 return Err(ZaiError::ApiError {
187 code: 1200,
188 message: "API key cannot be empty".to_string(),
189 });
190 }
191
192 let parts: Vec<&str> = api_key.split('.').collect();
193 if parts.len() != 2 {
194 return Err(ZaiError::ApiError {
195 code: 1001,
196 message: "API key must be in format '<id>.<secret>'".to_string(),
197 });
198 }
199
200 let (id, secret) = (parts[0], parts[1]);
201
202 if id.is_empty() || secret.is_empty() {
203 return Err(ZaiError::ApiError {
204 code: 1200,
205 message: "API key id and secret must not be empty".to_string(),
206 });
207 }
208
209 let valid_chars = |s: &str| -> bool {
212 s.chars()
213 .all(|c| c.is_alphanumeric() || c == '_' || c == '-')
214 };
215
216 if !valid_chars(id) || !valid_chars(secret) {
217 return Err(ZaiError::ApiError {
218 code: 1200,
219 message: "API key contains invalid characters".to_string(),
220 });
221 }
222
223 if id.len() < 3 {
226 return Err(ZaiError::ApiError {
227 code: 1200,
228 message: "API key id is too short".to_string(),
229 });
230 }
231
232 if secret.len() < 10 {
233 return Err(ZaiError::ApiError {
234 code: 1200,
235 message: "API key secret is too short".to_string(),
236 });
237 }
238
239 Ok(())
240}
241
242pub mod codes {
251 pub const SDK_VALIDATION: u16 = 9001;
253
254 pub const SDK_CONFIG: u16 = 9600;
256
257 pub const SDK_FILE_NOT_FOUND: u16 = 9100;
259
260 pub const SDK_FILE_TOO_LARGE: u16 = 9101;
262
263 pub const SDK_FILE_TYPE_UNSUPPORTED: u16 = 9102;
265
266 pub const SDK_IO: u16 = 9400;
268
269 pub const SDK_TIMEOUT: u16 = 9300;
271
272 pub const SDK_EXTERNAL_TOOL: u16 = 9500;
274}
275
276#[derive(Error, Debug)]
278pub enum ZaiError {
279 #[error("HTTP error [{status}]: {message}")]
281 HttpError { status: u16, message: String },
282
283 #[error("Authentication error [{code}]: {message}")]
285 AuthError { code: u16, message: String },
286
287 #[error("Account error [{code}]: {message}")]
289 AccountError { code: u16, message: String },
290
291 #[error("API error [{code}]: {message}")]
293 ApiError { code: u16, message: String },
294
295 #[error("Rate limit error [{code}]: {message}")]
297 RateLimitError { code: u16, message: String },
298
299 #[error("Content policy error [{code}]: {message}")]
301 ContentPolicyError { code: u16, message: String },
302
303 #[error("File error [{code}]: {message}")]
305 FileError { code: u16, message: String },
306
307 #[error("Network error: {0}")]
309 NetworkError(Arc<reqwest::Error>),
310
311 #[error("JSON error: {0}")]
313 JsonError(Arc<serde_json::Error>),
314
315 #[error("Realtime error: {0}")]
318 RealtimeError(Arc<RealtimeErrorKind>),
319
320 #[error("Realtime auth error: {0}")]
323 RealtimeAuthError(String),
324
325 #[error("Unknown error [{code}]: {message}")]
327 Unknown { code: u16, message: String },
328}
329
330#[derive(Debug, thiserror::Error)]
336pub enum RealtimeErrorKind {
337 #[error("websocket: {source}")]
341 WebSocket {
342 #[source]
344 source: tokio_tungstenite::tungstenite::Error,
345 },
346
347 #[error("serialize: {source}")]
349 Serialize {
350 #[source]
351 source: serde_json::Error,
352 },
353
354 #[error("protocol: {0}")]
356 Protocol(String),
357
358 #[error("server error event [code={code:?}]: {message}")]
360 ServerEvent {
361 code: String,
363 message: String,
365 },
366
367 #[error("session closed")]
369 Closed,
370}
371
372impl ZaiError {
373 pub fn from_api_response(status: u16, api_code: u16, api_message: String) -> Self {
375 if api_code != 0 {
376 return match api_code {
377 1000..=1004 | 1100 => ZaiError::AuthError {
379 code: api_code,
380 message: api_message,
381 },
382 1110..=1121 => ZaiError::AccountError {
384 code: api_code,
385 message: api_message,
386 },
387 1200..=1234 => ZaiError::ApiError {
389 code: api_code,
390 message: api_message,
391 },
392 1300..=1313 => ZaiError::RateLimitError {
394 code: api_code,
395 message: api_message,
396 },
397 1400..=1499 => ZaiError::FileError {
399 code: api_code,
400 message: api_message,
401 },
402 _ => ZaiError::Unknown {
403 code: api_code,
404 message: if api_message.is_empty() {
405 "Unknown error".to_string()
406 } else {
407 api_message
408 },
409 },
410 };
411 }
412
413 match status {
415 400 => ZaiError::HttpError {
416 status,
417 message: if api_message.is_empty() {
418 "Bad request - check your parameters".to_string()
419 } else {
420 api_message
421 },
422 },
423 401 => ZaiError::HttpError {
424 status,
425 message: "Unauthorized - check your API key".to_string(),
426 },
427 404 => ZaiError::HttpError {
428 status,
429 message: "Not found - requested resource doesn't exist".to_string(),
430 },
431 429 => ZaiError::HttpError {
432 status,
433 message: if api_message.is_empty() {
434 "Too many requests - rate limit exceeded".to_string()
435 } else {
436 api_message
437 },
438 },
439 434 => ZaiError::HttpError {
440 status,
441 message: "No API permission - feature not available".to_string(),
442 },
443 435 => ZaiError::HttpError {
444 status,
445 message: "File size exceeds 100MB limit".to_string(),
446 },
447 500 => ZaiError::HttpError {
448 status,
449 message: "Internal server error - try again later".to_string(),
450 },
451 _ => ZaiError::Unknown {
452 code: status,
453 message: if api_message.is_empty() {
454 "Unknown error".to_string()
455 } else {
456 api_message
457 },
458 },
459 }
460 }
461
462 pub fn is_rate_limit(&self) -> bool {
464 matches!(self, ZaiError::RateLimitError { .. })
465 }
466
467 pub fn is_auth_error(&self) -> bool {
469 matches!(self, ZaiError::AuthError { .. })
470 }
471
472 pub fn is_client_error(&self) -> bool {
474 match self {
475 ZaiError::HttpError { status, .. } => *status >= 400 && *status < 500,
476 ZaiError::AuthError { .. }
477 | ZaiError::AccountError { .. }
478 | ZaiError::ApiError { .. }
479 | ZaiError::RateLimitError { .. }
480 | ZaiError::ContentPolicyError { .. }
481 | ZaiError::FileError { .. }
482 | ZaiError::RealtimeAuthError(_) => true,
483 ZaiError::RealtimeError(kind) => match kind.as_ref() {
484 RealtimeErrorKind::Protocol(_)
487 | RealtimeErrorKind::Serialize { .. }
488 | RealtimeErrorKind::ServerEvent { .. } => true,
489 RealtimeErrorKind::WebSocket { .. } | RealtimeErrorKind::Closed => false,
490 },
491 _ => false,
492 }
493 }
494
495 pub fn is_server_error(&self) -> bool {
497 match self {
498 ZaiError::HttpError { status, .. } => *status >= 500,
499 ZaiError::Unknown { code, .. } => *code >= 500,
500 _ => false,
501 }
502 }
503
504 pub fn is_sdk_error(&self) -> bool {
511 self.code().is_some_and(|c| (9000..=9999).contains(&c))
512 }
513
514 pub fn compact(&self) -> String {
516 match self {
517 ZaiError::HttpError { status, message } => {
518 format!("HTTP[{}]: {}", status, message)
519 },
520 ZaiError::AuthError { code, message } => {
521 format!("AUTH[{}]: {}", code, message)
522 },
523 ZaiError::AccountError { code, message } => {
524 format!("ACCOUNT[{}]: {}", code, message)
525 },
526 ZaiError::ApiError { code, message } => {
527 format!("API[{}]: {}", code, message)
528 },
529 ZaiError::RateLimitError { code, message } => {
530 format!("RATE_LIMIT[{}]: {}", code, message)
531 },
532 ZaiError::ContentPolicyError { code, message } => {
533 format!("POLICY[{}]: {}", code, message)
534 },
535 ZaiError::FileError { code, message } => {
536 format!("FILE[{}]: {}", code, message)
537 },
538 ZaiError::NetworkError(err) => {
539 format!("NETWORK: {}", err)
540 },
541 ZaiError::JsonError(err) => {
542 format!("JSON: {}", err)
543 },
544 ZaiError::RealtimeError(kind) => {
545 format!("REALTIME: {}", kind)
546 },
547 ZaiError::RealtimeAuthError(msg) => {
548 format!("REALTIME_AUTH: {}", msg)
549 },
550 ZaiError::Unknown { code, message } => {
551 format!("UNKNOWN[{}]: {}", code, message)
552 },
553 }
554 }
555
556 pub fn code(&self) -> Option<u16> {
558 match self {
559 ZaiError::HttpError { status, .. } => Some(*status),
560 ZaiError::AuthError { code, .. } => Some(*code),
561 ZaiError::AccountError { code, .. } => Some(*code),
562 ZaiError::ApiError { code, .. } => Some(*code),
563 ZaiError::RateLimitError { code, .. } => Some(*code),
564 ZaiError::ContentPolicyError { code, .. } => Some(*code),
565 ZaiError::FileError { code, .. } => Some(*code),
566 ZaiError::NetworkError(_) => None,
567 ZaiError::JsonError(_) => None,
568 ZaiError::RealtimeError(_) | ZaiError::RealtimeAuthError(_) => None,
569 ZaiError::Unknown { code, .. } => Some(*code),
570 }
571 }
572
573 pub fn message(&self) -> String {
575 match self {
576 ZaiError::HttpError { message, .. } => message.clone(),
577 ZaiError::AuthError { message, .. } => message.clone(),
578 ZaiError::AccountError { message, .. } => message.clone(),
579 ZaiError::ApiError { message, .. } => message.clone(),
580 ZaiError::RateLimitError { message, .. } => message.clone(),
581 ZaiError::ContentPolicyError { message, .. } => message.clone(),
582 ZaiError::FileError { message, .. } => message.clone(),
583 ZaiError::NetworkError(err) => err.to_string(),
584 ZaiError::JsonError(err) => err.to_string(),
585 ZaiError::RealtimeError(kind) => kind.to_string(),
586 ZaiError::RealtimeAuthError(msg) => msg.clone(),
587 ZaiError::Unknown { message, .. } => message.clone(),
588 }
589 }
590
591 pub fn context(self, context: &str) -> Self {
615 let with_context = |message: String| format!("{context}: {message}");
616 match self {
617 Self::HttpError { status, message } => Self::HttpError {
618 status,
619 message: with_context(message),
620 },
621 Self::AuthError { code, message } => Self::AuthError {
622 code,
623 message: with_context(message),
624 },
625 Self::AccountError { code, message } => Self::AccountError {
626 code,
627 message: with_context(message),
628 },
629 Self::ApiError { code, message } => Self::ApiError {
630 code,
631 message: with_context(message),
632 },
633 Self::RateLimitError { code, message } => Self::RateLimitError {
634 code,
635 message: with_context(message),
636 },
637 Self::ContentPolicyError { code, message } => Self::ContentPolicyError {
638 code,
639 message: with_context(message),
640 },
641 Self::FileError { code, message } => Self::FileError {
642 code,
643 message: with_context(message),
644 },
645 Self::NetworkError(err) => Self::NetworkError(err),
648 Self::JsonError(err) => Self::JsonError(err),
649 Self::RealtimeError(kind) => Self::RealtimeError(kind),
650 Self::RealtimeAuthError(message) => Self::RealtimeAuthError(with_context(message)),
651 Self::Unknown { code, message } => Self::Unknown {
652 code,
653 message: with_context(message),
654 },
655 }
656 }
657}
658
659impl Clone for ZaiError {
660 fn clone(&self) -> Self {
661 match self {
662 ZaiError::HttpError { status, message } => ZaiError::HttpError {
663 status: *status,
664 message: message.clone(),
665 },
666 ZaiError::AuthError { code, message } => ZaiError::AuthError {
667 code: *code,
668 message: message.clone(),
669 },
670 ZaiError::AccountError { code, message } => ZaiError::AccountError {
671 code: *code,
672 message: message.clone(),
673 },
674 ZaiError::ApiError { code, message } => ZaiError::ApiError {
675 code: *code,
676 message: message.clone(),
677 },
678 ZaiError::RateLimitError { code, message } => ZaiError::RateLimitError {
679 code: *code,
680 message: message.clone(),
681 },
682 ZaiError::ContentPolicyError { code, message } => ZaiError::ContentPolicyError {
683 code: *code,
684 message: message.clone(),
685 },
686 ZaiError::FileError { code, message } => ZaiError::FileError {
687 code: *code,
688 message: message.clone(),
689 },
690 ZaiError::NetworkError(err) => ZaiError::NetworkError(Arc::clone(err)),
692 ZaiError::JsonError(err) => ZaiError::JsonError(Arc::clone(err)),
693 ZaiError::RealtimeError(kind) => ZaiError::RealtimeError(Arc::clone(kind)),
694 ZaiError::RealtimeAuthError(msg) => ZaiError::RealtimeAuthError(msg.clone()),
695 ZaiError::Unknown { code, message } => ZaiError::Unknown {
696 code: *code,
697 message: message.clone(),
698 },
699 }
700 }
701}
702
703pub type ZaiResult<T> = Result<T, ZaiError>;
705
706impl From<reqwest::Error> for ZaiError {
708 fn from(err: reqwest::Error) -> Self {
709 if let Some(status) = err.status() {
710 ZaiError::from_api_response(status.as_u16(), 0, err.to_string())
711 } else {
712 ZaiError::NetworkError(Arc::new(err))
713 }
714 }
715}
716
717impl From<serde_json::Error> for ZaiError {
719 fn from(err: serde_json::Error) -> Self {
720 ZaiError::JsonError(Arc::new(err))
721 }
722}
723
724impl From<validator::ValidationErrors> for ZaiError {
726 fn from(err: validator::ValidationErrors) -> Self {
727 ZaiError::ApiError {
728 code: 1200,
729 message: format!("Validation error: {:?}", err),
730 }
731 }
732}
733
734impl From<std::io::Error> for ZaiError {
742 fn from(err: std::io::Error) -> Self {
743 use std::io::ErrorKind;
744 match err.kind() {
745 ErrorKind::NotFound => ZaiError::FileError {
746 code: codes::SDK_FILE_NOT_FOUND,
747 message: err.to_string(),
748 },
749 ErrorKind::PermissionDenied => ZaiError::FileError {
750 code: codes::SDK_IO,
751 message: err.to_string(),
752 },
753 ErrorKind::TimedOut => ZaiError::ApiError {
754 code: codes::SDK_TIMEOUT,
755 message: err.to_string(),
756 },
757 _ => ZaiError::Unknown {
758 code: codes::SDK_IO,
759 message: err.to_string(),
760 },
761 }
762 }
763}
764
765impl From<RealtimeErrorKind> for ZaiError {
767 fn from(kind: RealtimeErrorKind) -> Self {
768 ZaiError::RealtimeError(Arc::new(kind))
769 }
770}
771
772impl From<tokio_tungstenite::tungstenite::Error> for ZaiError {
776 fn from(err: tokio_tungstenite::tungstenite::Error) -> Self {
777 ZaiError::RealtimeError(Arc::new(RealtimeErrorKind::WebSocket { source: err }))
778 }
779}
780
781#[cfg(test)]
782mod tests {
783 use super::*;
784
785 #[test]
786 fn test_from_api_response_bad_request() {
787 let err = ZaiError::from_api_response(400, 0, "Invalid input".to_string());
788 assert!(err.is_client_error());
789 assert!(!err.is_server_error());
790 assert_eq!(err.code(), Some(400));
791 }
792
793 #[test]
794 fn test_from_api_response_unauthorized() {
795 let err = ZaiError::from_api_response(401, 0, "".to_string());
796 assert!(err.is_client_error());
797 assert_eq!(err.message(), "Unauthorized - check your API key");
798 }
799
800 #[test]
801 fn test_from_api_response_rate_limit() {
802 let err = ZaiError::from_api_response(429, 1301, "Too many requests".to_string());
804 assert!(err.is_client_error());
805 assert!(err.is_rate_limit());
806 assert_eq!(err.code(), Some(1301));
807
808 let err = ZaiError::from_api_response(200, 1301, "Too many requests".to_string());
811 assert!(err.is_client_error());
812 assert!(err.is_rate_limit());
813 assert_eq!(err.code(), Some(1301));
814 }
815
816 #[test]
817 fn test_from_api_response_package_limit_codes() {
818 for code in [1300, 1312, 1313] {
819 let err = ZaiError::from_api_response(429, code, "Limited".to_string());
820 assert!(err.is_rate_limit());
821 assert_eq!(err.code(), Some(code));
822 }
823 }
824
825 #[test]
826 fn test_from_api_response_server_error() {
827 let err = ZaiError::from_api_response(500, 0, "".to_string());
828 assert!(!err.is_client_error());
829 assert!(err.is_server_error());
830 }
831
832 #[test]
833 fn test_from_api_response_auth_error_code() {
834 let err = ZaiError::from_api_response(200, 1001, "Invalid API key".to_string());
835 assert!(err.is_auth_error());
836 assert_eq!(err.code(), Some(1001));
837 assert_eq!(err.message(), "Invalid API key");
838 }
839
840 #[test]
841 fn test_from_api_response_account_error() {
842 let err = ZaiError::from_api_response(200, 1110, "Account expired".to_string());
843 assert!(err.is_client_error());
844 assert_eq!(err.code(), Some(1110));
845 }
846
847 #[test]
848 fn test_from_api_response_api_error() {
849 let err = ZaiError::from_api_response(200, 1200, "Invalid parameters".to_string());
850 assert!(err.is_client_error());
851 assert_eq!(err.code(), Some(1200));
852 }
853
854 #[test]
855 fn test_from_api_response_unknown_code() {
856 let err = ZaiError::from_api_response(200, 9999, "Unknown error".to_string());
857 assert!(!err.is_client_error()); assert_eq!(err.code(), Some(9999));
859 }
860
861 #[test]
862 fn test_compact() {
863 let err = ZaiError::HttpError {
864 status: 404,
865 message: "Not found".to_string(),
866 };
867 assert_eq!(err.compact(), "HTTP[404]: Not found");
868
869 let err = ZaiError::AuthError {
870 code: 1001,
871 message: "Invalid key".to_string(),
872 };
873 assert_eq!(err.compact(), "AUTH[1001]: Invalid key");
874 }
875
876 #[test]
877 fn test_code() {
878 let io_err =
882 std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "connection refused");
883 let err = ZaiError::from(io_err);
884 assert_eq!(err.code(), Some(codes::SDK_IO));
885
886 let err = ZaiError::JsonError(std::sync::Arc::new(serde_json::Error::io(
888 std::io::Error::new(std::io::ErrorKind::InvalidData, "invalid JSON"),
889 )));
890 assert!(err.code().is_none());
891
892 let err = ZaiError::HttpError {
894 status: 500,
895 message: "Server error".to_string(),
896 };
897 assert_eq!(err.code(), Some(500));
898 }
899
900 #[test]
901 fn test_message() {
902 let err = ZaiError::RateLimitError {
903 code: 1300,
904 message: "Too many requests".to_string(),
905 };
906 assert_eq!(err.message(), "Too many requests");
907 }
908
909 #[test]
910 fn test_from_reqwest_error_with_status() {
911 let io_err = std::io::Error::other("test error");
912 let zai_err = ZaiError::from(io_err);
913 match zai_err {
914 ZaiError::Unknown { .. } => {},
915 _ => panic!("Expected Unknown error for io::Error"),
916 }
917 }
918
919 #[test]
920 fn test_sdk_code_constants_in_reserved_range() {
921 for code in [
922 codes::SDK_VALIDATION,
923 codes::SDK_CONFIG,
924 codes::SDK_FILE_NOT_FOUND,
925 codes::SDK_FILE_TOO_LARGE,
926 codes::SDK_FILE_TYPE_UNSUPPORTED,
927 codes::SDK_IO,
928 codes::SDK_TIMEOUT,
929 codes::SDK_EXTERNAL_TOOL,
930 ] {
931 assert!((9000..=9999).contains(&code), "code {code} outside 9000-9999");
932 }
933 }
934
935 #[test]
936 fn test_is_sdk_error_classification() {
937 assert!(ZaiError::FileError {
939 code: codes::SDK_FILE_NOT_FOUND,
940 message: "x".into(),
941 }
942 .is_sdk_error());
943 assert!(ZaiError::ApiError {
944 code: codes::SDK_TIMEOUT,
945 message: "x".into(),
946 }
947 .is_sdk_error());
948
949 assert!(!ZaiError::AuthError {
951 code: 1001,
952 message: "x".into(),
953 }
954 .is_sdk_error());
955 assert!(!ZaiError::RateLimitError {
956 code: 1301,
957 message: "x".into(),
958 }
959 .is_sdk_error());
960 assert!(!ZaiError::HttpError {
961 status: 500,
962 message: "x".into(),
963 }
964 .is_sdk_error());
965
966 assert!(!ZaiError::RealtimeAuthError("x".into()).is_sdk_error());
968 }
969
970 #[test]
971 fn test_from_io_maps_by_kind() {
972 use std::io::{Error, ErrorKind};
973
974 let err = ZaiError::from(Error::from(ErrorKind::NotFound));
975 assert!(matches!(
976 err,
977 ZaiError::FileError { code, .. } if code == codes::SDK_FILE_NOT_FOUND
978 ));
979
980 let err = ZaiError::from(Error::from(ErrorKind::TimedOut));
981 assert!(matches!(
982 err,
983 ZaiError::ApiError { code, .. } if code == codes::SDK_TIMEOUT
984 ));
985
986 let err = ZaiError::from(Error::from(ErrorKind::PermissionDenied));
987 assert!(matches!(
988 err,
989 ZaiError::FileError { code, .. } if code == codes::SDK_IO
990 ));
991
992 let err = ZaiError::from(Error::other("boom"));
994 assert!(matches!(
995 err,
996 ZaiError::Unknown { code, .. } if code == codes::SDK_IO
997 ));
998 }
999
1000 #[test]
1001 fn test_context_preserves_code_and_variant() {
1002 let err = ZaiError::ApiError {
1003 code: 1200,
1004 message: "bad model".into(),
1005 }
1006 .context("file parser create");
1007 assert!(matches!(
1008 err,
1009 ZaiError::ApiError { code, .. } if code == 1200
1010 ));
1011 assert_eq!(err.message(), "file parser create: bad model");
1012
1013 let err = ZaiError::Unknown {
1014 code: codes::SDK_IO,
1015 message: "boom".into(),
1016 }
1017 .context("read");
1018 assert_eq!(err.code(), Some(codes::SDK_IO));
1019 assert_eq!(err.message(), "read: boom");
1020 }
1021
1022 #[test]
1023 fn test_sdk_timeout_is_not_rate_limit() {
1024 let err = ZaiError::ApiError {
1028 code: codes::SDK_TIMEOUT,
1029 message: "Timeout waiting for parsing result".into(),
1030 };
1031 assert!(!err.is_rate_limit());
1032 assert!(err.is_sdk_error());
1033 }
1034
1035 #[test]
1036 fn test_validate_api_key_valid() {
1037 assert!(validate_api_key("abc123.abcdefghijklmnopqrstuvwxyz").is_ok());
1038 }
1043
1044 #[test]
1045 fn test_validate_api_key_empty() {
1046 let result = validate_api_key("");
1047 assert!(result.is_err());
1048 match result {
1049 Err(ZaiError::ApiError { code, .. }) => {
1050 assert_eq!(code, 1200);
1051 },
1052 _ => panic!("Expected ApiError"),
1053 }
1054 }
1055
1056 #[test]
1057 fn test_validate_api_key_no_dot() {
1058 let result = validate_api_key("invalid");
1059 assert!(result.is_err());
1060 match result {
1061 Err(ZaiError::ApiError { code, message }) => {
1062 assert_eq!(code, 1001);
1063 assert!(message.contains("format"));
1064 },
1065 _ => panic!("Expected ApiError"),
1066 }
1067 }
1068
1069 #[test]
1070 fn test_validate_api_key_multiple_dots() {
1071 let result = validate_api_key("id.secret.extra");
1072 assert!(result.is_err());
1073 assert_eq!(result.unwrap_err().code(), Some(1001));
1074 }
1075
1076 #[test]
1077 fn test_validate_api_key_empty_id() {
1078 let result = validate_api_key(".secret123456789");
1079 assert!(result.is_err());
1080 assert_eq!(result.unwrap_err().code(), Some(1200));
1081 }
1082
1083 #[test]
1084 fn test_validate_api_key_empty_secret() {
1085 let result = validate_api_key("id123.");
1086 assert!(result.is_err());
1087 assert_eq!(result.unwrap_err().code(), Some(1200));
1088 }
1089
1090 #[test]
1091 fn test_validate_api_key_invalid_chars() {
1092 let result = validate_api_key("id$123.secret@456");
1093 assert!(result.is_err());
1094 assert_eq!(result.unwrap_err().code(), Some(1200));
1095 }
1096
1097 #[test]
1098 fn test_validate_api_key_id_too_short() {
1099 let result = validate_api_key("ab.abcdefghijklmn");
1100 assert!(result.is_err());
1101 assert!(result.unwrap_err().message().contains("id is too short"));
1102 }
1103
1104 #[test]
1105 fn test_validate_api_key_secret_too_short() {
1106 let result = validate_api_key("id123.short");
1107 assert!(result.is_err());
1108 assert!(
1109 result
1110 .unwrap_err()
1111 .message()
1112 .contains("secret is too short")
1113 );
1114 }
1115
1116 #[test]
1117 fn test_mask_sensitive_info_api_key() {
1118 let text = "API key: abc123.abcdefghijklmnopqrstuvwxyz12345";
1119 let filtered = mask_sensitive_info(text);
1120 assert!(filtered.contains("[FILTERED]"));
1121 assert!(!filtered.contains("abc123"));
1122 assert!(!filtered.contains("abcdefghijklmnopqrstuvwxyz"));
1123 }
1124
1125 #[test]
1126 fn test_mask_sensitive_info_password() {
1127 let text = "password: secret123, other text";
1128 let filtered = mask_sensitive_info(text);
1129 assert!(filtered.contains("[FILTERED]"));
1130 assert!(!filtered.contains("secret123"));
1131 }
1132
1133 #[test]
1134 fn test_mask_sensitive_info_token() {
1135 let text = "token=abc123xyz, other content";
1136 let filtered = mask_sensitive_info(text);
1137 assert!(filtered.contains("[FILTERED]"));
1138 assert!(!filtered.contains("abc123xyz"));
1139 }
1140
1141 #[test]
1142 fn test_mask_sensitive_info_bearer() {
1143 let text = "Authorization: Bearer abc123.abc1234567890";
1144 let filtered = mask_sensitive_info(text);
1145 assert!(filtered.contains("[FILTERED]"));
1146 assert!(!filtered.contains("abc123"));
1147 }
1148
1149 #[test]
1150 fn test_mask_sensitive_info_multiple() {
1151 let text = "api_key=abc123.xyz456, password=secret123";
1152 let filtered = mask_sensitive_info(text);
1153 let filtered_count = filtered.matches("[FILTERED]").count();
1154 assert_eq!(filtered_count, 2);
1155 }
1156
1157 #[test]
1158 fn test_mask_sensitive_info_no_sensitive() {
1159 let text = "Regular text without sensitive information";
1160 let filtered = mask_sensitive_info(text);
1161 assert_eq!(filtered, text);
1162 }
1163
1164 #[test]
1165 fn test_mask_api_key() {
1166 let text = "API key: abc123.abcdefghijklmnopqrstuvwxyz12345";
1167 let filtered = mask_api_key(text);
1168 assert!(filtered.contains("[FILTERED]"));
1169 assert!(!filtered.contains("abc123"));
1170 }
1171
1172 #[test]
1173 fn test_contains_sensitive_info_api_key() {
1174 assert!(contains_sensitive_info("api_key: abc123.abc1234567890"));
1175 assert!(!contains_sensitive_info("regular text"));
1176 }
1177
1178 #[test]
1179 fn test_contains_sensitive_info_password() {
1180 assert!(contains_sensitive_info("password: secret"));
1181 assert!(contains_sensitive_info("password=123"));
1182 assert!(!contains_sensitive_info("password"));
1183 assert!(!contains_sensitive_info("word:password"));
1184 }
1185
1186 #[test]
1187 fn test_contains_sensitive_info_token() {
1188 assert!(contains_sensitive_info("token=abc123"));
1189 assert!(contains_sensitive_info("token: xyz123"));
1190 assert!(!contains_sensitive_info("token"));
1191 assert!(!contains_sensitive_info("tokenize this"));
1192 }
1193}