scim-server 0.5.3

A comprehensive SCIM 2.0 server library for Rust with multi-tenant support and type-safe operations
Documentation
//! SCIM PATCH Operation Integration Tests
//!
//! This module provides comprehensive testing of SCIM PATCH operations according to RFC 7644 Section 3.5.2.
//! The tests are designed to be economical and maintainable through parameterization and shared utilities.

pub mod assertions;
pub mod capabilities;
pub mod property_tests;
pub mod scenarios;
pub mod test_data;
pub mod test_helpers;

use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;

/// Core PATCH operation types for testing
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum PatchOperation {
    Add,
    Remove,
    Replace,
}

/// Path expression types for comprehensive testing
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PathType {
    /// Simple attribute path (e.g., "displayName")
    Simple,
    /// Complex attribute path (e.g., "name.givenName")
    Complex,
    /// Multi-valued attribute path (e.g., "emails")
    MultiValued,
    /// Filtered multi-valued path (e.g., "emails[type eq \"work\"]")
    Filtered,
    /// Invalid path for error testing
    Invalid,
    /// Invalid syntax for error testing (malformed filter expressions)
    InvalidSyntax,
    /// Read-only attribute path
    ReadOnly,
    /// Immutable attribute path
    Immutable,
}

/// Expected test result types
#[derive(Debug, Clone, PartialEq)]
pub enum ExpectedResult {
    /// Operation should succeed
    Success,
    /// Operation should fail with specific SCIM error
    ScimError {
        error_type: ScimErrorType,
        status_code: u16,
    },
    /// Operation should fail with validation error
    ValidationError(ValidationErrorType),
    /// Operation should be rejected due to capabilities
    NotImplemented,
}

/// SCIM error types for testing
#[derive(Debug, Clone, PartialEq)]
pub enum ScimErrorType {
    InvalidPath,
    InvalidValue,
    NotFound,
    Conflict,
    PreconditionFailed,
    Mutability,
    Uniqueness,
    TooMany,
    InvalidFilter,
    InvalidSyntax,
}

/// Validation error types
#[derive(Debug, Clone, PartialEq)]
pub enum ValidationErrorType {
    RequiredAttribute,
    TypeMismatch,
    SchemaViolation,
    UniquenessViolation,
}

/// Test case for parameterized PATCH operations
#[derive(Debug, Clone)]
pub struct PatchTestCase {
    /// Descriptive name for the test case
    pub name: String,
    /// The PATCH operation to perform
    pub operation: PatchOperation,
    /// The path to operate on
    pub path: String,
    /// The value for add/replace operations
    pub value: Option<Value>,
    /// Expected result of the operation
    pub expected_result: ExpectedResult,
    /// Resource type to test against
    pub resource_type: String,
    /// Optional setup requirements
    pub setup: TestSetup,
}

/// Test setup configuration
#[derive(Debug, Clone)]
pub struct TestSetup {
    /// Whether to create an existing resource
    pub create_existing_resource: bool,
    /// Initial resource data
    pub initial_resource: Option<Value>,
    /// ETag to use for conditional operations
    pub etag: Option<String>,
    /// Tenant configuration
    pub tenant_config: TenantConfig,
    /// Provider capabilities
    pub capabilities: TestCapabilities,
}

/// Tenant configuration for multi-tenant testing
#[derive(Debug, Clone)]
pub struct TenantConfig {
    pub tenant_id: String,
    pub isolated: bool,
}

/// Test capabilities configuration
#[derive(Debug, Clone)]
pub struct TestCapabilities {
    pub patch_supported: bool,
    pub etag_supported: bool,
    pub custom_capabilities: HashMap<String, bool>,
}

/// Multi-tenant test scenario
#[derive(Debug, Clone)]
pub struct MultiTenantTestCase {
    pub name: String,
    pub tenant_a_capabilities: TestCapabilities,
    pub tenant_b_capabilities: TestCapabilities,
    pub operation: TenantOperation,
    pub expected_isolation: bool,
}

/// Operations for multi-tenant testing
#[derive(Debug, Clone)]
pub enum TenantOperation {
    PatchInTenantA {
        resource_id: String,
        patch_request: Value,
    },
    PatchInTenantB {
        resource_id: String,
        patch_request: Value,
    },
    CrossTenantAccess {
        source_tenant: String,
        target_tenant: String,
        resource_id: String,
    },
}

/// Capability test scenario
#[derive(Debug, Clone)]
pub struct CapabilityTestScenario {
    pub name: String,
    pub patch_supported: bool,
    pub expected_behavior: ExpectedBehavior,
    pub test_operation: TestOperation,
}

/// Expected behavior for capability testing
#[derive(Debug, Clone, PartialEq)]
pub enum ExpectedBehavior {
    ProcessRequest,
    Return501NotImplemented,
    ReturnCapabilityInServiceConfig(bool),
    ProviderSpecificBehavior,
}

/// Test operation configuration
#[derive(Debug, Clone)]
pub struct TestOperation {
    pub resource_type: String,
    pub resource_id: String,
    pub patch_operations: Vec<PatchOperationSpec>,
}

/// Specification for a PATCH operation in tests
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PatchOperationSpec {
    pub op: String,
    pub path: String,
    pub value: Option<Value>,
}

/// Error test case configuration
#[derive(Debug, Clone)]
pub struct ErrorTestCase {
    pub name: String,
    pub setup: fn() -> TestSetup,
    pub patch_request: Value,
    pub expected_error: ScimErrorType,
    pub expected_status: u16,
}

/// Property test configuration
#[derive(Debug, Clone)]
pub struct PropertyTestConfig {
    pub max_operations: usize,
    pub resource_types: Vec<String>,
    pub include_etag_tests: bool,
    pub include_multi_tenant: bool,
}

impl Default for TestSetup {
    fn default() -> Self {
        Self {
            create_existing_resource: true,
            initial_resource: None,
            etag: None,
            tenant_config: TenantConfig {
                tenant_id: "default".to_string(),
                isolated: false,
            },
            capabilities: TestCapabilities {
                patch_supported: true,
                etag_supported: true,
                custom_capabilities: HashMap::new(),
            },
        }
    }
}

impl Default for TestCapabilities {
    fn default() -> Self {
        Self {
            patch_supported: true,
            etag_supported: true,
            custom_capabilities: HashMap::new(),
        }
    }
}

impl PatchTestCase {
    /// Create a new test case with default setup
    pub fn new(
        name: impl Into<String>,
        operation: PatchOperation,
        path: impl Into<String>,
        value: Option<Value>,
        expected_result: ExpectedResult,
    ) -> Self {
        Self {
            name: name.into(),
            operation,
            path: path.into(),
            value,
            expected_result,
            resource_type: "User".to_string(),
            setup: TestSetup::default(),
        }
    }

    /// Set the resource type for this test case
    pub fn with_resource_type(mut self, resource_type: impl Into<String>) -> Self {
        self.resource_type = resource_type.into();
        self
    }

    /// Set custom setup for this test case
    pub fn with_setup(mut self, setup: TestSetup) -> Self {
        self.setup = setup;
        self
    }
}

impl PatchOperationSpec {
    /// Create a new add operation
    pub fn add(path: impl Into<String>, value: Value) -> Self {
        Self {
            op: "add".to_string(),
            path: path.into(),
            value: Some(value),
        }
    }

    /// Create a new remove operation
    pub fn remove(path: impl Into<String>) -> Self {
        Self {
            op: "remove".to_string(),
            path: path.into(),
            value: None,
        }
    }

    /// Create a new replace operation
    pub fn replace(path: impl Into<String>, value: Value) -> Self {
        Self {
            op: "replace".to_string(),
            path: path.into(),
            value: Some(value),
        }
    }
}

/// ETag test case for concurrency control testing
#[derive(Debug, Clone)]
pub struct ETagTestCase {
    pub name: String,
    pub request_etag: Option<String>,
    pub expected_result: ETagResult,
}

/// Expected result for ETag operations
#[derive(Debug, Clone, PartialEq)]
pub enum ETagResult {
    Success,
    PreconditionFailed,
}

/// Atomic test case for multi-operation testing
#[derive(Debug, Clone)]
pub struct AtomicTestCase {
    pub name: String,
    pub operations: Vec<PatchOperationSpec>,
    pub expected_behavior: AtomicBehavior,
}

/// Expected behavior for atomic operations
#[derive(Debug, Clone, PartialEq)]
pub enum AtomicBehavior {
    AllSucceed,
    AllFail,
    ResolveConflicts,
}

/// Test result wrapper for assertions
#[derive(Debug)]
pub struct PatchTestResult {
    pub success: bool,
    pub resource: Option<Value>,
    pub error: Option<String>,
    pub status_code: Option<u16>,
    pub etag: Option<String>,
}

impl PatchTestResult {
    pub fn is_ok(&self) -> bool {
        self.success
    }

    pub fn is_err(&self) -> bool {
        !self.success
    }

    pub fn error_type(&self) -> Option<ScimErrorType> {
        // Parse error message to determine SCIM error type
        if let Some(error_msg) = &self.error {
            if error_msg.contains("readonly attribute")
                || error_msg.contains("Cannot modify readonly")
            {
                return Some(ScimErrorType::Mutability);
            }
            if error_msg.contains("not found") || error_msg.contains("NotFound") {
                return Some(ScimErrorType::NotFound);
            }
            if error_msg.contains("ETag mismatch") || error_msg.contains("Precondition failed") {
                return Some(ScimErrorType::PreconditionFailed);
            }
            if error_msg.contains("syntax")
                || error_msg.contains("Syntax")
                || error_msg.contains("JSON")
                || error_msg.contains("PATCH request must contain Operations array")
            {
                // Check if this is a malformed filter syntax error which should be InvalidPath
                if error_msg.contains("malformed filter syntax") {
                    return Some(ScimErrorType::InvalidPath);
                }
                return Some(ScimErrorType::InvalidSyntax);
            }
            if error_msg.contains("invalid") || error_msg.contains("Invalid") {
                if error_msg.contains("path") {
                    return Some(ScimErrorType::InvalidPath);
                }
                return Some(ScimErrorType::InvalidValue);
            }
            if error_msg.contains("duplicate") || error_msg.contains("uniqueness") {
                return Some(ScimErrorType::Uniqueness);
            }
        }
        None
    }

    pub fn get_attribute_value(&self, path: &str) -> Option<&Value> {
        // This would implement path-based value extraction
        self.resource.as_ref().and_then(|r| {
            // Simplified - real implementation would handle complex paths
            r.get(path)
        })
    }
}