1use crate::{
2 InternalError, InternalErrorClass, InternalErrorOrigin,
3 dto::error::{Error, ErrorCode},
4};
5
6fn registry_policy_error_code(message: &str) -> Option<ErrorCode> {
7 if message.contains("already registered to") {
8 return Some(ErrorCode::PolicyRoleAlreadyRegistered);
9 }
10 if message.contains("already registered under parent") {
11 return Some(ErrorCode::PolicySingletonAlreadyRegisteredUnderParent);
12 }
13 if message.contains("must be created by a singleton parent with scaling config") {
14 return Some(ErrorCode::PolicyReplicaRequiresSingletonWithScaling);
15 }
16 if message.contains("must be created by a singleton parent with sharding config") {
17 return Some(ErrorCode::PolicyShardRequiresSingletonWithSharding);
18 }
19 if message.contains("must be created by a singleton parent") {
20 return Some(ErrorCode::PolicyTenantRequiresSingletonParent);
21 }
22
23 None
24}
25
26fn internal_error_to_public(err: &InternalError) -> Error {
27 if let Some(public) = err.public_error() {
28 return public.clone();
29 }
30
31 let message = err.to_string();
32
33 match err.class() {
34 InternalErrorClass::Access => Error::unauthorized(message),
35
36 InternalErrorClass::Domain => match err.origin() {
37 InternalErrorOrigin::Config => Error::invalid(message),
38 _ => {
39 if let Some(code) = registry_policy_error_code(&message) {
40 Error::policy(code, message)
41 } else {
42 Error::conflict(message)
43 }
44 }
45 },
46
47 InternalErrorClass::Invariant => Error::invariant(message),
48
49 InternalErrorClass::Infra | InternalErrorClass::Ops | InternalErrorClass::Workflow => {
50 Error::internal(message)
51 }
52 }
53}
54
55impl From<&InternalError> for Error {
56 fn from(err: &InternalError) -> Self {
57 internal_error_to_public(err)
58 }
59}
60
61impl From<InternalError> for Error {
62 fn from(err: InternalError) -> Self {
63 internal_error_to_public(&err)
64 }
65}
66
67#[cfg(test)]
72mod tests {
73 use super::*;
74 use crate::{
75 access::AccessError,
76 cdk::types::Principal,
77 domain::policy::topology::{TopologyPolicyError, registry::RegistryPolicyError},
78 ids::CanisterRole,
79 };
80
81 fn p(id: u8) -> Principal {
82 Principal::from_slice(&[id; 29])
83 }
84
85 #[test]
86 fn internal_error_mapping_matches_class_contract() {
87 let access: Error = InternalError::from(AccessError::Denied("denied".to_string())).into();
88 assert_eq!(access.code, ErrorCode::Unauthorized);
89
90 let domain_config: Error =
91 InternalError::domain(InternalErrorOrigin::Config, "bad config").into();
92 assert_eq!(domain_config.code, ErrorCode::InvalidInput);
93
94 let domain_other: Error =
95 InternalError::domain(InternalErrorOrigin::Domain, "conflict").into();
96 assert_eq!(domain_other.code, ErrorCode::Conflict);
97
98 let invariant: Error =
99 InternalError::invariant(InternalErrorOrigin::Ops, "broken invariant").into();
100 assert_eq!(invariant.code, ErrorCode::InvariantViolation);
101
102 let infra: Error = InternalError::infra(InternalErrorOrigin::Infra, "infra fail").into();
103 assert_eq!(infra.code, ErrorCode::Internal);
104
105 let ops: Error = InternalError::ops(InternalErrorOrigin::Ops, "ops fail").into();
106 assert_eq!(ops.code, ErrorCode::Internal);
107
108 let workflow: Error =
109 InternalError::workflow(InternalErrorOrigin::Workflow, "workflow fail").into();
110 assert_eq!(workflow.code, ErrorCode::Internal);
111 }
112
113 #[test]
114 fn public_error_is_preserved_without_remap() {
115 let public = Error::not_found("missing");
116 let remapped: Error = InternalError::public(public.clone()).into();
117 assert_eq!(remapped, public);
118 }
119
120 #[test]
121 fn registry_policy_errors_map_to_stable_public_policy_codes() {
122 let err = RegistryPolicyError::RoleAlreadyRegistered {
123 role: CanisterRole::new("app"),
124 pid: p(7),
125 };
126 let internal: InternalError = TopologyPolicyError::from(err).into();
127 let public: Error = internal.into();
128 assert_eq!(public.code, ErrorCode::PolicyRoleAlreadyRegistered);
129 }
130}