pdk-token-introspection-lib 1.7.0

PDK Token Introspection Library
Documentation
// Copyright (c) 2026, Salesforce, Inc.,
// All rights reserved.
// For full license text, see the LICENSE.txt file

//! Error types for token introspection operations.
//!
//! This module contains all error types used across the token introspection library.

use thiserror::Error;

/// Policy configuration errors.
#[non_exhaustive]
#[derive(Error, Debug, PartialEq, Eq)]
pub enum ConfigError {
    /// Invalid Introspection URL.
    #[error("Config error: the introspection URL is not valid")]
    InvalidIntrospectionURL,
    /// Invalid authorization value.
    #[error("Config error: invalid authorization value")]
    InvalidAuthorizationValue,
    /// Invalid consumer_by value.
    #[error("Config error: invalid consumer_by value")]
    InvalidConsumerBy,
}

/// Errors related to token validation.
#[non_exhaustive]
#[derive(Error, Debug, PartialEq, Eq)]
pub enum ValidationError {
    /// Token has expired.
    #[error("Token has expired.")]
    TokenExpired,
    /// Token has been revoked.
    #[error("Token has been revoked.")]
    TokenRevoked,
    /// The required scopes are not authorized.
    #[error("The required scopes are not authorized")]
    InvalidScopes,
    /// Token is inactive.
    #[error("Token is inactive.")]
    TokenInactive,
    /// The contract collector is not set.
    #[error("The contract collector is not set")]
    EmptyContractValidator,
    /// Invalid client contracts.
    #[error("Invalid Client")]
    InvalidClientContracts {
        name: Option<String>,
        id: Option<String>,
    },
}

/// Errors from the introspection flow.
#[non_exhaustive]
#[derive(Error, Debug)]
pub enum IntrospectionError {
    /// HTTP request to introspection endpoint failed.
    #[error("Introspection request failed: {0}")]
    RequestFailed(String),
    /// HTTP response status was not 200.
    #[error("Introspection HTTP error: status={status}, body={body}")]
    HttpError { status: u32, body: String },
    /// Failed to parse introspection response.
    #[error("Failed to parse introspection response: {0}")]
    ParseError(String),
    /// Token validation failed.
    #[error("Token validation failed: {0}")]
    Validation(#[from] ValidationError),
}

/// Errors related to token parsing and caching.
#[derive(Error, Debug, PartialEq, Eq)]
pub(crate) enum TokenError {
    /// Unexpected error when deserializing cached value.
    #[error("Unexpected error when deserializing cached value: {msg:}")]
    BinaryDeserializeError { msg: String },
    /// Unexpected error when serializing cached value.
    #[error("Unexpected error when serializing cached value: {msg:}")]
    BinarySerializeError { msg: String },
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn config_error_messages_contain_context() {
        let err = ConfigError::InvalidIntrospectionURL;
        assert!(err.to_string().contains("introspection URL"));
        assert!(!err.to_string().contains("authorization")); // proves distinction

        let err = ConfigError::InvalidAuthorizationValue;
        assert!(err.to_string().contains("authorization"));
        assert!(!err.to_string().contains("introspection URL")); // proves distinction
    }

    #[test]
    fn validation_errors_are_distinct() {
        let expired = ValidationError::TokenExpired.to_string();
        let revoked = ValidationError::TokenRevoked.to_string();
        let scopes = ValidationError::InvalidScopes.to_string();

        // Each has unique message
        assert!(expired.contains("expired"));
        assert!(revoked.contains("revoked"));
        assert!(scopes.contains("scopes"));

        // Messages are distinct
        assert_ne!(expired, revoked);
        assert_ne!(revoked, scopes);
    }

    #[test]
    fn token_error_messages_include_dynamic_fields() {
        let err1 = TokenError::BinaryDeserializeError {
            msg: "invalid data".to_string(),
        };
        let err2 = TokenError::BinaryDeserializeError {
            msg: "other error".to_string(),
        };
        assert!(err1.to_string().contains("invalid data"));
        assert!(err2.to_string().contains("other error"));
        assert_ne!(err1.to_string(), err2.to_string()); // proves dynamic

        let err3 = TokenError::BinarySerializeError {
            msg: "serialize error".to_string(),
        };
        assert!(err3.to_string().contains("serialize error"));
        assert_ne!(err1.to_string(), err3.to_string()); // proves distinction
    }
}