use serde_json::{Value, json};
pub mod builders;
pub mod fixtures;
pub mod multi_tenant;
pub mod providers;
pub mod test_utils;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum ValidationErrorCode {
MissingSchemas, EmptySchemas, InvalidSchemaUri, UnknownSchemaUri, DuplicateSchemaUri, MissingBaseSchema, ExtensionWithoutBase, MissingRequiredExtension,
MissingId, EmptyId, InvalidIdFormat, ClientProvidedId, InvalidExternalId, InvalidMetaStructure, MissingResourceType, InvalidResourceType, ClientProvidedMeta, InvalidCreatedDateTime, InvalidModifiedDateTime, InvalidLocationUri, InvalidVersionFormat,
MissingRequiredAttribute, InvalidDataType, InvalidStringFormat, InvalidBooleanValue, InvalidDecimalFormat, InvalidIntegerValue, InvalidDateTimeFormat, InvalidBinaryData, InvalidReferenceUri, InvalidReferenceType, BrokenReference,
SingleValueForMultiValued, ArrayForSingleValued, MultiplePrimaryValues, InvalidMultiValuedStructure, MissingRequiredSubAttribute, InvalidCanonicalValue,
MissingRequiredSubAttributes, InvalidSubAttributeType, UnknownSubAttribute, NestedComplexAttributes, MalformedComplexStructure,
CaseSensitivityViolation, ReadOnlyMutabilityViolation, ImmutableMutabilityViolation, WriteOnlyAttributeReturned, ServerUniquenessViolation, GlobalUniquenessViolation, InvalidCanonicalValueChoice, UnknownAttributeForSchema, RequiredCharacteristicViolation, }
#[macro_export]
macro_rules! assert_validation_error {
($result:expr, $expected_error:expr) => {
match $result {
Err(ScimError::Validation(_)) => {
}
Ok(_) => panic!(
"Expected validation error {:?}, but validation passed",
$expected_error
),
Err(other) => panic!(
"Expected validation error {:?}, got {:?}",
$expected_error, other
),
}
};
}
#[macro_export]
macro_rules! assert_error_message_contains {
($result:expr, $substring:expr) => {
match $result {
Err(err) => assert!(
err.to_string().contains($substring),
"Error message '{}' does not contain '{}'",
err.to_string(),
$substring
),
Ok(_) => panic!(
"Expected error containing '{}', but validation passed",
$substring
),
}
};
}
#[macro_export]
macro_rules! assert_validation_success {
($result:expr) => {
match $result {
Ok(_) => {
}
Err(err) => panic!("Expected validation to succeed, but got error: {}", err),
}
};
}
#[macro_export]
macro_rules! assert_specific_validation_error {
($result:expr, $error_variant:pat) => {
match $result {
Err(ScimError::Validation($error_variant)) => {
}
Ok(_) => panic!("Expected validation error, but validation passed"),
Err(other) => panic!("Expected specific validation error, got {:?}", other),
}
};
}
#[derive(Debug, Default)]
pub struct TestCoverage {
covered_errors: std::collections::HashSet<ValidationErrorCode>,
}
impl TestCoverage {
pub fn new() -> Self {
Self::default()
}
pub fn mark_tested(&mut self, error: ValidationErrorCode) {
self.covered_errors.insert(error);
}
pub fn is_tested(&self, error: &ValidationErrorCode) -> bool {
self.covered_errors.contains(error)
}
pub fn coverage_percentage(&self) -> f64 {
let total_errors = Self::total_validation_errors();
if total_errors == 0 {
100.0
} else {
(self.covered_errors.len() as f64 / total_errors as f64) * 100.0
}
}
pub fn covered_errors(&self) -> &std::collections::HashSet<ValidationErrorCode> {
&self.covered_errors
}
pub fn untested_errors(&self) -> Vec<ValidationErrorCode> {
Self::all_validation_errors()
.into_iter()
.filter(|error| !self.covered_errors.contains(error))
.collect()
}
pub fn total_validation_errors() -> usize {
52 }
fn all_validation_errors() -> Vec<ValidationErrorCode> {
use ValidationErrorCode::*;
vec![
MissingSchemas,
EmptySchemas,
InvalidSchemaUri,
UnknownSchemaUri,
DuplicateSchemaUri,
MissingBaseSchema,
ExtensionWithoutBase,
MissingRequiredExtension,
MissingId,
EmptyId,
InvalidIdFormat,
ClientProvidedId,
InvalidExternalId,
InvalidMetaStructure,
MissingResourceType,
InvalidResourceType,
ClientProvidedMeta,
InvalidCreatedDateTime,
InvalidModifiedDateTime,
InvalidLocationUri,
InvalidVersionFormat,
MissingRequiredAttribute,
InvalidDataType,
InvalidStringFormat,
InvalidBooleanValue,
InvalidDecimalFormat,
InvalidIntegerValue,
InvalidDateTimeFormat,
InvalidBinaryData,
InvalidReferenceUri,
InvalidReferenceType,
BrokenReference,
SingleValueForMultiValued,
ArrayForSingleValued,
MultiplePrimaryValues,
InvalidMultiValuedStructure,
MissingRequiredSubAttribute,
InvalidCanonicalValue,
MissingRequiredSubAttributes,
InvalidSubAttributeType,
UnknownSubAttribute,
NestedComplexAttributes,
MalformedComplexStructure,
CaseSensitivityViolation,
ReadOnlyMutabilityViolation,
ImmutableMutabilityViolation,
WriteOnlyAttributeReturned,
ServerUniquenessViolation,
GlobalUniquenessViolation,
InvalidCanonicalValueChoice,
UnknownAttributeForSchema,
RequiredCharacteristicViolation,
]
}
}
pub fn load_fixture(path: &str) -> Value {
let fixture_path = format!("tests/fixtures/{}", path);
let content = std::fs::read_to_string(&fixture_path)
.unwrap_or_else(|_| panic!("Failed to load fixture: {}", fixture_path));
serde_json::from_str(&content)
.unwrap_or_else(|_| panic!("Failed to parse JSON fixture: {}", fixture_path))
}
pub fn valid_user_minimal() -> Value {
json!({
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"id": "2819c223-7f76-453a-919d-413861904646",
"userName": "bjensen@example.com",
"meta": {
"resourceType": "User",
"created": "2010-01-23T04:56:22Z",
"lastModified": "2011-05-13T04:42:34Z",
"version": "W/\"3694e05e9dff590\"",
"location": "https://example.com/v2/Users/2819c223-7f76-453a-919d-413861904646"
}
})
}
pub fn valid_group_minimal() -> Value {
json!({
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"],
"id": "e9e30dba-f08f-4109-8486-d5c6a331660a",
"displayName": "Tour Guides",
"meta": {
"resourceType": "Group",
"created": "2010-01-23T04:56:22Z",
"lastModified": "2011-05-13T04:42:34Z",
"version": "W/\"3694e05e9dff592\"",
"location": "https://example.com/v2/Groups/e9e30dba-f08f-4109-8486-d5c6a331660a"
}
})
}
pub fn modify_json(mut base: Value, path: &str, new_value: Option<Value>) -> Value {
let path_parts: Vec<&str> = path.split('.').collect();
if path_parts.len() == 1 {
match new_value {
Some(value) => {
base[path_parts[0]] = value;
}
None => {
if let Some(obj) = base.as_object_mut() {
obj.remove(path_parts[0]);
}
}
}
} else {
if path_parts.len() == 2 {
match new_value {
Some(value) => {
base[path_parts[0]][path_parts[1]] = value;
}
None => {
if let Some(obj) = base[path_parts[0]].as_object_mut() {
obj.remove(path_parts[1]);
}
}
}
}
}
base
}
#[allow(ambiguous_glob_reexports)]
pub use multi_tenant::*;
pub use providers::*;
#[allow(ambiguous_glob_reexports)]
pub use test_utils::*;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_coverage_tracker() {
let mut coverage = TestCoverage::new();
assert_eq!(coverage.coverage_percentage(), 0.0);
coverage.mark_tested(ValidationErrorCode::MissingSchemas);
assert!(coverage.is_tested(&ValidationErrorCode::MissingSchemas));
assert!(!coverage.is_tested(&ValidationErrorCode::EmptySchemas));
let untested = coverage.untested_errors();
assert!(!untested.contains(&ValidationErrorCode::MissingSchemas));
assert!(untested.contains(&ValidationErrorCode::EmptySchemas));
}
#[test]
fn test_valid_user_minimal() {
let user = valid_user_minimal();
assert_eq!(
user["schemas"][0],
"urn:ietf:params:scim:schemas:core:2.0:User"
);
assert_eq!(user["userName"], "bjensen@example.com");
}
#[test]
fn test_modify_json() {
let base = json!({"name": "test", "nested": {"value": 42}});
let modified = modify_json(base.clone(), "name", None);
assert!(!modified.as_object().unwrap().contains_key("name"));
let modified = modify_json(base.clone(), "name", Some(json!("new_value")));
assert_eq!(modified["name"], "new_value");
let modified = modify_json(base.clone(), "nested.value", Some(json!(100)));
assert_eq!(modified["nested"]["value"], 100);
}
}