Skip to main content

canic_core/api/
error.rs

1use crate::{InternalError, InternalErrorClass, InternalErrorOrigin, dto::error::Error};
2
3fn internal_error_to_public(err: &InternalError) -> Error {
4    if let Some(public) = err.public_error() {
5        return public.clone();
6    }
7
8    let message = err.to_string();
9
10    match err.class() {
11        InternalErrorClass::Access => Error::unauthorized(message),
12
13        InternalErrorClass::Domain => match err.origin() {
14            InternalErrorOrigin::Config => Error::invalid(message),
15            _ => Error::conflict(message),
16        },
17
18        InternalErrorClass::Invariant => Error::invariant(message),
19
20        InternalErrorClass::Infra | InternalErrorClass::Ops | InternalErrorClass::Workflow => {
21            Error::internal(message)
22        }
23    }
24}
25
26impl From<&InternalError> for Error {
27    fn from(err: &InternalError) -> Self {
28        internal_error_to_public(err)
29    }
30}
31
32impl From<InternalError> for Error {
33    fn from(err: InternalError) -> Self {
34        internal_error_to_public(&err)
35    }
36}
37
38// -----------------------------------------------------------------------------
39// Tests
40// -----------------------------------------------------------------------------
41
42#[cfg(test)]
43mod tests {
44    use super::*;
45    use crate::{
46        access::AccessError,
47        cdk::types::Principal,
48        domain::policy::pure::{
49            component_allocation::ComponentAllocationPolicyError,
50            topology::{TopologyPolicyError, registry::RegistryPolicyError},
51        },
52        dto::error::ErrorCode,
53        ids::CanisterRole,
54    };
55
56    fn p(id: u8) -> Principal {
57        Principal::from_slice(&[id; 29])
58    }
59
60    #[test]
61    fn internal_error_mapping_matches_class_contract() {
62        let access: Error = InternalError::from(AccessError::Denied("denied".to_string())).into();
63        assert_eq!(access.code, ErrorCode::Unauthorized);
64
65        let domain_config: Error =
66            InternalError::domain(InternalErrorOrigin::Config, "bad config").into();
67        assert_eq!(domain_config.code, ErrorCode::InvalidInput);
68
69        let domain_other: Error =
70            InternalError::domain(InternalErrorOrigin::Domain, "conflict").into();
71        assert_eq!(domain_other.code, ErrorCode::Conflict);
72
73        let invariant: Error =
74            InternalError::invariant(InternalErrorOrigin::Ops, "broken invariant").into();
75        assert_eq!(invariant.code, ErrorCode::InvariantViolation);
76
77        let infra: Error = InternalError::infra(InternalErrorOrigin::Infra, "infra fail").into();
78        assert_eq!(infra.code, ErrorCode::Internal);
79
80        let ops: Error = InternalError::ops(InternalErrorOrigin::Ops, "ops fail").into();
81        assert_eq!(ops.code, ErrorCode::Internal);
82
83        let workflow: Error =
84            InternalError::workflow(InternalErrorOrigin::Workflow, "workflow fail").into();
85        assert_eq!(workflow.code, ErrorCode::Internal);
86
87        let invalid_allocation: Error =
88            InternalError::from(ComponentAllocationPolicyError::EmptyOperationId).into();
89        assert_eq!(invalid_allocation.code, ErrorCode::InvalidInput);
90
91        let exhausted_allocation: Error =
92            InternalError::from(ComponentAllocationPolicyError::ComponentCapacityExhausted).into();
93        assert_eq!(exhausted_allocation.code, ErrorCode::ResourceExhausted);
94
95        let invalid_authority: Error =
96            InternalError::from(ComponentAllocationPolicyError::RootTopologyDigestMismatch).into();
97        assert_eq!(invalid_authority.code, ErrorCode::InvariantViolation);
98
99        let token_expired: Error = AccessError::DelegatedAuthTokenExpired.into();
100        assert_eq!(token_expired.code, ErrorCode::AuthTokenExpired);
101
102        let cert_expired: Error = AccessError::DelegatedAuthCertExpired.into();
103        assert_eq!(cert_expired.code, ErrorCode::AuthProofExpired);
104    }
105
106    #[test]
107    fn public_error_is_preserved_without_remap() {
108        let public = Error::not_found("missing");
109        let remapped: Error = InternalError::public(public.clone()).into();
110        assert_eq!(remapped, public);
111    }
112
113    #[test]
114    fn registry_policy_errors_map_to_stable_public_policy_codes() {
115        let err = RegistryPolicyError::RoleAlreadyRegistered {
116            role: CanisterRole::new("app"),
117            pid: p(7),
118        };
119        let internal: InternalError = TopologyPolicyError::from(err).into();
120        let public: Error = internal.into();
121        assert_eq!(public.code, ErrorCode::PolicyRoleAlreadyRegistered);
122    }
123
124    #[test]
125    fn registry_policy_service_parent_errors_use_service_owned_public_codes() {
126        let cases = [
127            (
128                RegistryPolicyError::ReplicaRequiresServiceWithScaling {
129                    role: CanisterRole::new("replica"),
130                    parent_role: CanisterRole::new("plain_parent"),
131                },
132                ErrorCode::PolicyReplicaRequiresServiceWithScaling,
133            ),
134            (
135                RegistryPolicyError::ShardRequiresServiceWithSharding {
136                    role: CanisterRole::new("shard"),
137                    parent_role: CanisterRole::new("plain_parent"),
138                },
139                ErrorCode::PolicyShardRequiresServiceWithSharding,
140            ),
141            (
142                RegistryPolicyError::InstanceRequiresServiceWithIndex {
143                    role: CanisterRole::new("instance"),
144                    parent_role: CanisterRole::new("plain_parent"),
145                },
146                ErrorCode::PolicyInstanceRequiresServiceWithIndex,
147            ),
148        ];
149
150        for (err, expected) in cases {
151            let internal: InternalError = TopologyPolicyError::from(err).into();
152            let public: Error = internal.into();
153
154            assert_eq!(public.code, expected);
155        }
156    }
157}