Skip to main content

pdk_token_introspection_lib/
error.rs

1// Copyright (c) 2026, Salesforce, Inc.,
2// All rights reserved.
3// For full license text, see the LICENSE.txt file
4
5//! Error types for token introspection operations.
6//!
7//! This module contains all error types used across the token introspection library.
8
9use thiserror::Error;
10
11/// Policy configuration errors.
12#[non_exhaustive]
13#[derive(Error, Debug, PartialEq, Eq)]
14pub enum ConfigError {
15    /// Invalid Introspection URL.
16    #[error("Config error: the introspection URL is not valid")]
17    InvalidIntrospectionURL,
18    /// Invalid authorization value.
19    #[error("Config error: invalid authorization value")]
20    InvalidAuthorizationValue,
21    /// Invalid consumer_by value.
22    #[error("Config error: invalid consumer_by value")]
23    InvalidConsumerBy,
24}
25
26/// Errors related to token validation.
27#[non_exhaustive]
28#[derive(Error, Debug, PartialEq, Eq)]
29pub enum ValidationError {
30    /// Token has expired.
31    #[error("Token has expired.")]
32    TokenExpired,
33    /// Token has been revoked.
34    #[error("Token has been revoked.")]
35    TokenRevoked,
36    /// The required scopes are not authorized.
37    #[error("The required scopes are not authorized")]
38    InvalidScopes,
39    /// Token is inactive.
40    #[error("Token is inactive.")]
41    TokenInactive,
42    /// The contract collector is not set.
43    #[error("The contract collector is not set")]
44    EmptyContractValidator,
45    /// Invalid client contracts.
46    #[error("Invalid Client")]
47    InvalidClientContracts {
48        name: Option<String>,
49        id: Option<String>,
50    },
51}
52
53/// Errors from the introspection flow.
54#[non_exhaustive]
55#[derive(Error, Debug)]
56pub enum IntrospectionError {
57    /// HTTP request to introspection endpoint failed.
58    #[error("Introspection request failed: {0}")]
59    RequestFailed(String),
60    /// HTTP response status was not 200.
61    #[error("Introspection HTTP error: status={status}, body={body}")]
62    HttpError { status: u32, body: String },
63    /// Failed to parse introspection response.
64    #[error("Failed to parse introspection response: {0}")]
65    ParseError(String),
66    /// Token validation failed.
67    #[error("Token validation failed: {0}")]
68    Validation(#[from] ValidationError),
69}
70
71/// Errors related to token parsing and caching.
72#[derive(Error, Debug, PartialEq, Eq)]
73pub(crate) enum TokenError {
74    /// Unexpected error when deserializing cached value.
75    #[error("Unexpected error when deserializing cached value: {msg:}")]
76    BinaryDeserializeError { msg: String },
77    /// Unexpected error when serializing cached value.
78    #[error("Unexpected error when serializing cached value: {msg:}")]
79    BinarySerializeError { msg: String },
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85
86    #[test]
87    fn config_error_messages_contain_context() {
88        let err = ConfigError::InvalidIntrospectionURL;
89        assert!(err.to_string().contains("introspection URL"));
90        assert!(!err.to_string().contains("authorization")); // proves distinction
91
92        let err = ConfigError::InvalidAuthorizationValue;
93        assert!(err.to_string().contains("authorization"));
94        assert!(!err.to_string().contains("introspection URL")); // proves distinction
95    }
96
97    #[test]
98    fn validation_errors_are_distinct() {
99        let expired = ValidationError::TokenExpired.to_string();
100        let revoked = ValidationError::TokenRevoked.to_string();
101        let scopes = ValidationError::InvalidScopes.to_string();
102
103        // Each has unique message
104        assert!(expired.contains("expired"));
105        assert!(revoked.contains("revoked"));
106        assert!(scopes.contains("scopes"));
107
108        // Messages are distinct
109        assert_ne!(expired, revoked);
110        assert_ne!(revoked, scopes);
111    }
112
113    #[test]
114    fn token_error_messages_include_dynamic_fields() {
115        let err1 = TokenError::BinaryDeserializeError {
116            msg: "invalid data".to_string(),
117        };
118        let err2 = TokenError::BinaryDeserializeError {
119            msg: "other error".to_string(),
120        };
121        assert!(err1.to_string().contains("invalid data"));
122        assert!(err2.to_string().contains("other error"));
123        assert_ne!(err1.to_string(), err2.to_string()); // proves dynamic
124
125        let err3 = TokenError::BinarySerializeError {
126            msg: "serialize error".to_string(),
127        };
128        assert!(err3.to_string().contains("serialize error"));
129        assert_ne!(err1.to_string(), err3.to_string()); // proves distinction
130    }
131}