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;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum PatchOperation {
Add,
Remove,
Replace,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PathType {
Simple,
Complex,
MultiValued,
Filtered,
Invalid,
InvalidSyntax,
ReadOnly,
Immutable,
}
#[derive(Debug, Clone, PartialEq)]
pub enum ExpectedResult {
Success,
ScimError {
error_type: ScimErrorType,
status_code: u16,
},
ValidationError(ValidationErrorType),
NotImplemented,
}
#[derive(Debug, Clone, PartialEq)]
pub enum ScimErrorType {
InvalidPath,
InvalidValue,
NotFound,
Conflict,
PreconditionFailed,
Mutability,
Uniqueness,
TooMany,
InvalidFilter,
InvalidSyntax,
}
#[derive(Debug, Clone, PartialEq)]
pub enum ValidationErrorType {
RequiredAttribute,
TypeMismatch,
SchemaViolation,
UniquenessViolation,
}
#[derive(Debug, Clone)]
pub struct PatchTestCase {
pub name: String,
pub operation: PatchOperation,
pub path: String,
pub value: Option<Value>,
pub expected_result: ExpectedResult,
pub resource_type: String,
pub setup: TestSetup,
}
#[derive(Debug, Clone)]
pub struct TestSetup {
pub create_existing_resource: bool,
pub initial_resource: Option<Value>,
pub etag: Option<String>,
pub tenant_config: TenantConfig,
pub capabilities: TestCapabilities,
}
#[derive(Debug, Clone)]
pub struct TenantConfig {
pub tenant_id: String,
pub isolated: bool,
}
#[derive(Debug, Clone)]
pub struct TestCapabilities {
pub patch_supported: bool,
pub etag_supported: bool,
pub custom_capabilities: HashMap<String, bool>,
}
#[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,
}
#[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,
},
}
#[derive(Debug, Clone)]
pub struct CapabilityTestScenario {
pub name: String,
pub patch_supported: bool,
pub expected_behavior: ExpectedBehavior,
pub test_operation: TestOperation,
}
#[derive(Debug, Clone, PartialEq)]
pub enum ExpectedBehavior {
ProcessRequest,
Return501NotImplemented,
ReturnCapabilityInServiceConfig(bool),
ProviderSpecificBehavior,
}
#[derive(Debug, Clone)]
pub struct TestOperation {
pub resource_type: String,
pub resource_id: String,
pub patch_operations: Vec<PatchOperationSpec>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PatchOperationSpec {
pub op: String,
pub path: String,
pub value: Option<Value>,
}
#[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,
}
#[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 {
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(),
}
}
pub fn with_resource_type(mut self, resource_type: impl Into<String>) -> Self {
self.resource_type = resource_type.into();
self
}
pub fn with_setup(mut self, setup: TestSetup) -> Self {
self.setup = setup;
self
}
}
impl PatchOperationSpec {
pub fn add(path: impl Into<String>, value: Value) -> Self {
Self {
op: "add".to_string(),
path: path.into(),
value: Some(value),
}
}
pub fn remove(path: impl Into<String>) -> Self {
Self {
op: "remove".to_string(),
path: path.into(),
value: None,
}
}
pub fn replace(path: impl Into<String>, value: Value) -> Self {
Self {
op: "replace".to_string(),
path: path.into(),
value: Some(value),
}
}
}
#[derive(Debug, Clone)]
pub struct ETagTestCase {
pub name: String,
pub request_etag: Option<String>,
pub expected_result: ETagResult,
}
#[derive(Debug, Clone, PartialEq)]
pub enum ETagResult {
Success,
PreconditionFailed,
}
#[derive(Debug, Clone)]
pub struct AtomicTestCase {
pub name: String,
pub operations: Vec<PatchOperationSpec>,
pub expected_behavior: AtomicBehavior,
}
#[derive(Debug, Clone, PartialEq)]
pub enum AtomicBehavior {
AllSucceed,
AllFail,
ResolveConflicts,
}
#[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> {
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")
{
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> {
self.resource.as_ref().and_then(|r| {
r.get(path)
})
}
}