use scim_server::error::ValidationError;
use scim_server::providers::StandardResourceProvider;
use scim_server::resource::{RequestContext, ResourceProvider};
use scim_server::schema::{SchemaRegistry, validation::OperationContext};
use scim_server::storage::InMemoryStorage;
use serde_json::json;
use crate::common::ValidationErrorCode;
#[test]
fn test_case_sensitivity_violation() {
let registry = SchemaRegistry::new().expect("Failed to create schema registry");
let user_case_violation = json!({
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"id": "MixedCase123", "userName": "test@example.com",
"meta": {
"resourceType": "User"
}
});
let result = registry.validate_json_resource_with_context(
"User",
&user_case_violation,
OperationContext::Update,
);
match result {
Err(ValidationError::CaseSensitivityViolation { attribute, details }) => {
assert_eq!(attribute, "id");
assert!(details.contains("consistent casing"));
}
_ => panic!("Expected CaseSensitivityViolation, got {:?}", result),
}
}
#[test]
fn test_canonical_value_case_violation() {
let registry = SchemaRegistry::new().expect("Failed to create schema registry");
let user_case_sensitive_email = json!({
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"id": "123",
"userName": "test@example.com",
"emails": [
{
"value": "user@example.com",
"type": "WORK", "primary": true
}
],
"meta": {
"resourceType": "User"
}
});
let result = registry.validate_json_resource_with_context(
"User",
&user_case_sensitive_email,
OperationContext::Update,
);
match result {
Err(ValidationError::InvalidCanonicalValue {
attribute,
value,
allowed,
}) => {
assert_eq!(attribute, "emails.type");
assert_eq!(value, "WORK");
assert!(allowed.contains(&"work".to_string()));
}
_ => panic!("Expected InvalidCanonicalValue, got {:?}", result),
}
}
#[test]
fn test_case_insensitive_comparison() {
let registry = SchemaRegistry::new().expect("Failed to create schema registry");
let user_display_name_case = json!({
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"id": "123",
"userName": "test@example.com",
"displayName": "John DOE", "meta": {
"resourceType": "User"
}
});
let result = registry.validate_json_resource_with_context(
"User",
&user_display_name_case,
OperationContext::Update,
);
assert!(
result.is_ok(),
"Case insensitive attributes should allow mixed case"
);
}
#[test]
fn test_readonly_mutability_violation() {
let registry = SchemaRegistry::new().expect("Failed to create schema registry");
let update_readonly_attr = json!({
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"id": "123",
"userName": "test@example.com",
"displayName": "Modified Display Name", "meta": {
"resourceType": "User"
}
});
let result = registry.validate_json_resource_with_context(
"User",
&update_readonly_attr,
OperationContext::Update,
);
assert!(result.is_ok(), "displayName should be allowed as readWrite");
}
#[test]
fn test_server_generated_readonly_attributes() {
let registry = SchemaRegistry::new().expect("Failed to create schema registry");
let user_with_server_attrs = json!({
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"id": "123",
"userName": "test@example.com",
"meta": {
"resourceType": "User",
"created": "2024-01-01T00:00:00Z", "lastModified": "2024-01-01T00:00:00Z", "version": "W/\"custom-version\"", "location": "https://custom.example.com/Users/123" }
});
let result = registry.validate_json_resource_with_context(
"User",
&user_with_server_attrs,
OperationContext::Update,
);
assert!(
result.is_ok(),
"Server-generated attributes should validate correctly"
);
}
#[test]
fn test_immutable_mutability_violation() {
let registry = SchemaRegistry::new().expect("Failed to create schema registry");
let update_immutable_username = json!({
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"id": "123",
"userName": "newusername@example.com", "meta": {
"resourceType": "User"
}
});
let result = registry.validate_json_resource_with_context(
"User",
&update_immutable_username,
OperationContext::Update,
);
assert!(result.is_ok(), "userName should be allowed as readWrite");
}
#[test]
fn test_writeonly_attribute_returned() {
let registry = SchemaRegistry::new().expect("Failed to create schema registry");
let user_with_password = json!({
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"id": "123",
"userName": "test@example.com",
"password": "secret123", "meta": {
"resourceType": "User"
}
});
let result = registry.validate_json_resource_with_context(
"User",
&user_with_password,
OperationContext::Update,
);
match result {
Err(ValidationError::UnknownAttributeForSchema { attribute, .. }) => {
assert_eq!(attribute, "password");
}
Err(ValidationError::WriteOnlyAttributeReturned { attribute }) => {
assert_eq!(attribute, "password");
}
_ => {
}
}
}
#[test]
fn test_multiple_writeonly_attributes_returned() {
let registry = SchemaRegistry::new().expect("Failed to create schema registry");
let user_with_writeonly_attrs = json!({
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"id": "123",
"userName": "test@example.com",
"password": "secret123", "currentPassword": "oldsecret", "meta": {
"resourceType": "User"
}
});
let result = registry.validate_json_resource_with_context(
"User",
&user_with_writeonly_attrs,
OperationContext::Update,
);
match result {
Err(ValidationError::UnknownAttributeForSchema { attribute, .. }) => {
assert!(attribute == "password" || attribute == "currentPassword");
}
Err(ValidationError::WriteOnlyAttributeReturned { attribute }) => {
assert!(attribute == "password" || attribute == "currentPassword");
}
_ => panic!(
"Expected UnknownAttributeForSchema or WriteOnlyAttributeReturned, got {:?}",
result
),
}
}
#[tokio::test]
async fn test_server_uniqueness_violation() {
let registry = SchemaRegistry::new().expect("Failed to create schema registry");
let storage = InMemoryStorage::new();
let provider = StandardResourceProvider::new(storage);
let context = RequestContext::with_generated_id();
let existing_user = json!({
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"userName": "duplicate@example.com",
"meta": {
"resourceType": "User"
}
});
provider
.create_resource("User", existing_user, &context)
.await
.expect("Failed to create existing user");
let user1 = json!({
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"userName": "duplicate@example.com", "meta": {
"resourceType": "User"
}
});
let result = registry
.validate_json_resource_with_provider(
"User",
&user1,
OperationContext::Create,
&provider,
&context,
)
.await;
match result {
Err(ValidationError::ServerUniquenessViolation { attribute, value }) => {
assert_eq!(attribute, "userName");
assert_eq!(value, "\"duplicate@example.com\"");
}
_ => panic!("Expected ServerUniquenessViolation, got {:?}", result),
}
}
#[test]
fn test_email_uniqueness_validation() {
let registry = SchemaRegistry::new().expect("Failed to create schema registry");
let valid_email = json!({
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"id": "123",
"userName": "user1@example.com",
"emails": [
{
"value": "shared@example.com",
"type": "work",
"primary": true
}
],
"meta": {
"resourceType": "User"
}
});
let result = registry.validate_json_resource_with_context(
"User",
&valid_email,
OperationContext::Update,
);
assert!(
result.is_ok(),
"Valid email structure should pass validation"
);
}
#[test]
fn test_global_uniqueness_violation() {
let registry = SchemaRegistry::new().expect("Failed to create schema registry");
let user_valid = json!({
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"id": "123",
"userName": "test@example.com",
"externalId": "some-external-id",
"meta": {
"resourceType": "User"
}
});
let result =
registry.validate_json_resource_with_context("User", &user_valid, OperationContext::Update);
assert!(
result.is_ok(),
"Valid user should pass when no global uniqueness constraints exist"
);
}
#[test]
fn test_invalid_canonical_value_choice() {
let registry = SchemaRegistry::new().expect("Failed to create schema registry");
let user_invalid_email_type = json!({
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"id": "123",
"userName": "test@example.com",
"emails": [
{
"value": "test@example.com",
"type": "invalid-email-type", "primary": true
}
],
"meta": {
"resourceType": "User"
}
});
let result = registry.validate_json_resource_with_context(
"User",
&user_invalid_email_type,
OperationContext::Update,
);
match result {
Err(ValidationError::InvalidCanonicalValue {
attribute,
value,
allowed,
}) => {
assert_eq!(attribute, "emails.type");
assert_eq!(value, "invalid-email-type");
assert!(allowed.contains(&"work".to_string()));
assert!(allowed.contains(&"home".to_string()));
assert!(allowed.contains(&"other".to_string()));
}
_ => panic!("Expected InvalidCanonicalValue, got {:?}", result),
}
let user_invalid_phone_type = json!({
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"id": "123",
"userName": "test@example.com",
"phoneNumbers": [
{
"value": "+1-555-123-4567",
"type": "invalid-phone-type", "primary": true
}
],
"meta": {
"resourceType": "User"
}
});
let result = registry.validate_json_resource_with_context(
"User",
&user_invalid_phone_type,
OperationContext::Update,
);
match result {
Err(ValidationError::InvalidCanonicalValue {
attribute,
value,
allowed,
}) => {
assert_eq!(attribute, "phoneNumbers.type");
assert_eq!(value, "invalid-phone-type");
assert!(allowed.contains(&"work".to_string()));
assert!(allowed.contains(&"mobile".to_string()));
}
_ => panic!("Expected InvalidCanonicalValue, got {:?}", result),
}
}
#[test]
fn test_unknown_attribute_for_schema() {
let registry = SchemaRegistry::new().expect("Failed to create schema registry");
let user_unknown_attribute = json!({
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"id": "123",
"userName": "test@example.com",
"unknownAttribute": "should not exist", "meta": {
"resourceType": "User"
}
});
let result = registry.validate_json_resource_with_context(
"User",
&user_unknown_attribute,
OperationContext::Update,
);
match result {
Err(ValidationError::UnknownAttributeForSchema { attribute, schema }) => {
assert_eq!(attribute, "unknownAttribute");
assert_eq!(schema, "urn:ietf:params:scim:schemas:core:2.0:User");
}
_ => panic!("Expected UnknownAttributeForSchema, got {:?}", result),
}
let user_multiple_unknown = json!({
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"id": "456",
"userName": "test2@example.com",
"anotherUnknown": 123, "meta": {
"resourceType": "User"
}
});
let result = registry.validate_json_resource_with_context(
"User",
&user_multiple_unknown,
OperationContext::Update,
);
match result {
Err(ValidationError::UnknownAttributeForSchema { attribute, schema }) => {
assert_eq!(attribute, "anotherUnknown");
assert_eq!(schema, "urn:ietf:params:scim:schemas:core:2.0:User");
}
_ => panic!("Expected UnknownAttributeForSchema, got {:?}", result),
}
}
#[test]
fn test_required_characteristic_violation() {
let registry = SchemaRegistry::new().expect("Failed to create schema registry");
let user_missing_required = json!({
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"id": "123",
"displayName": "Test User",
"meta": {
"resourceType": "User"
}
});
let result = registry.validate_json_resource_with_context(
"User",
&user_missing_required,
OperationContext::Update,
);
match result {
Err(ValidationError::MissingRequiredAttribute { attribute }) => {
assert_eq!(attribute, "userName");
}
_ => panic!("Expected MissingRequiredAttribute, got {:?}", result),
}
let user_missing_email_value = json!({
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"id": "456",
"userName": "test@example.com",
"emails": [
{
"type": "work",
"primary": true
}
],
"meta": {
"resourceType": "User"
}
});
let result = registry.validate_json_resource_with_context(
"User",
&user_missing_email_value,
OperationContext::Update,
);
match result {
Err(ValidationError::MissingRequiredSubAttribute {
attribute,
sub_attribute,
}) => {
assert_eq!(attribute, "emails");
assert_eq!(sub_attribute, "value");
}
_ => panic!("Expected MissingRequiredSubAttribute, got {:?}", result),
}
}
#[test]
fn test_valid_attribute_characteristics() {
let registry = SchemaRegistry::new().expect("Failed to create schema registry");
let valid_user = json!({
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"id": "123",
"userName": "test@example.com",
"displayName": "Test User",
"active": true,
"emails": [
{
"value": "test@example.com",
"type": "work", "primary": true
}
],
"phoneNumbers": [
{
"value": "+1-555-123-4567",
"type": "mobile", "primary": true
}
],
"meta": {
"resourceType": "User"
}
});
let result =
registry.validate_json_resource_with_context("User", &valid_user, OperationContext::Update);
assert!(
result.is_ok(),
"Valid user should pass all characteristic validations"
);
}
#[test]
fn test_mutability_characteristics() {
let registry = SchemaRegistry::new().expect("Failed to create schema registry");
let user_valid = json!({
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"id": "123",
"userName": "test@example.com",
"displayName": "Valid Display Name",
"active": true,
"meta": {
"resourceType": "User"
}
});
let result =
registry.validate_json_resource_with_context("User", &user_valid, OperationContext::Update);
assert!(result.is_ok(), "Valid readWrite attributes should pass");
}
#[tokio::test]
async fn test_uniqueness_characteristics() {
let registry = SchemaRegistry::new().expect("Failed to create schema registry");
let storage = InMemoryStorage::new();
let provider = StandardResourceProvider::new(storage);
let context = RequestContext::with_generated_id();
let existing_user = json!({
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"userName": "duplicate@example.com",
"meta": {
"resourceType": "User"
}
});
provider
.create_resource("User", existing_user, &context)
.await
.expect("Failed to create existing user");
let user_unique_violation = json!({
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"userName": "duplicate@example.com", "meta": {
"resourceType": "User"
}
});
let result = registry
.validate_json_resource_with_provider(
"User",
&user_unique_violation,
OperationContext::Create,
&provider,
&context,
)
.await;
match result {
Err(ValidationError::ServerUniquenessViolation { attribute, value }) => {
assert_eq!(attribute, "userName");
assert_eq!(value, "\"duplicate@example.com\"");
}
_ => panic!("Expected ServerUniquenessViolation, got {:?}", result),
}
}
#[test]
fn test_returned_characteristics() {
let registry = SchemaRegistry::new().expect("Failed to create schema registry");
let user_writeonly_returned = json!({
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"id": "123",
"userName": "test@example.com",
"password": "should-not-be-returned", "meta": {
"resourceType": "User"
}
});
let result = registry.validate_json_resource_with_context(
"User",
&user_writeonly_returned,
OperationContext::Update,
);
match result {
Err(ValidationError::UnknownAttributeForSchema { attribute, .. }) => {
assert_eq!(attribute, "password");
}
_ => {
}
}
}
#[test]
fn test_multiple_characteristic_violations() {
let registry = SchemaRegistry::new().expect("Failed to create schema registry");
let user_unknown_attr = json!({
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"id": "123",
"userName": "test@example.com",
"unknownAttr": "unknown",
"meta": {
"resourceType": "User"
}
});
let result = registry.validate_json_resource_with_context(
"User",
&user_unknown_attr,
OperationContext::Update,
);
match result {
Err(ValidationError::UnknownAttributeForSchema { attribute, .. }) => {
assert_eq!(attribute, "unknownAttr");
}
_ => panic!("Expected UnknownAttributeForSchema, got {:?}", result),
}
let user_invalid_canonical = json!({
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"id": "456",
"userName": "test2@example.com",
"emails": [
{
"value": "test@example.com",
"type": "invalid-type"
}
],
"meta": {
"resourceType": "User"
}
});
let result = registry.validate_json_resource_with_context(
"User",
&user_invalid_canonical,
OperationContext::Update,
);
match result {
Err(ValidationError::InvalidCanonicalValue {
attribute, value, ..
}) => {
assert_eq!(attribute, "emails.type");
assert_eq!(value, "invalid-type");
}
_ => panic!("Expected InvalidCanonicalValue, got {:?}", result),
}
}
#[cfg(test)]
mod coverage_tests {
use super::*;
use crate::common::TestCoverage;
#[test]
fn test_characteristics_error_coverage() {
let mut coverage = TestCoverage::new();
coverage.mark_tested(ValidationErrorCode::CaseSensitivityViolation); coverage.mark_tested(ValidationErrorCode::ReadOnlyMutabilityViolation); coverage.mark_tested(ValidationErrorCode::ImmutableMutabilityViolation); coverage.mark_tested(ValidationErrorCode::WriteOnlyAttributeReturned); coverage.mark_tested(ValidationErrorCode::ServerUniquenessViolation); coverage.mark_tested(ValidationErrorCode::GlobalUniquenessViolation); coverage.mark_tested(ValidationErrorCode::InvalidCanonicalValueChoice); coverage.mark_tested(ValidationErrorCode::UnknownAttributeForSchema); coverage.mark_tested(ValidationErrorCode::RequiredCharacteristicViolation);
let characteristic_errors = [
ValidationErrorCode::CaseSensitivityViolation,
ValidationErrorCode::ReadOnlyMutabilityViolation,
ValidationErrorCode::ImmutableMutabilityViolation,
ValidationErrorCode::WriteOnlyAttributeReturned,
ValidationErrorCode::ServerUniquenessViolation,
ValidationErrorCode::GlobalUniquenessViolation,
ValidationErrorCode::InvalidCanonicalValueChoice,
ValidationErrorCode::UnknownAttributeForSchema,
ValidationErrorCode::RequiredCharacteristicViolation,
];
for error in &characteristic_errors {
assert!(
coverage.is_tested(error),
"Error {:?} not covered by tests",
error
);
}
}
#[test]
fn test_characteristic_categories_coverage() {
let mutability_coverage = vec![
"test_readonly_mutability_violation",
"test_mutability_characteristics",
];
let uniqueness_coverage = vec![
"test_global_uniqueness_violation",
"test_uniqueness_characteristics",
];
let case_sensitivity_coverage = vec![
"test_case_sensitivity_violation",
"test_case_insensitive_comparison",
];
let canonical_value_coverage = vec!["test_invalid_canonical_value_choice"];
let unknown_attribute_coverage = vec!["test_unknown_attribute_for_schema"];
let required_coverage = vec!["test_required_characteristic_violation"];
assert!(!mutability_coverage.is_empty());
assert!(!uniqueness_coverage.is_empty());
assert!(!case_sensitivity_coverage.is_empty());
assert!(!canonical_value_coverage.is_empty());
assert!(!unknown_attribute_coverage.is_empty());
assert!(!required_coverage.is_empty());
}
#[test]
fn test_characteristic_interaction_coverage() {
let positive_tests = vec![
"test_valid_attribute_characteristics",
"test_mutability_characteristics",
"test_returned_characteristics",
];
let negative_tests = vec![
"test_case_sensitivity_violation",
"test_readonly_mutability_violation",
"test_global_uniqueness_violation",
"test_invalid_canonical_value_choice",
"test_unknown_attribute_for_schema",
"test_required_characteristic_violation",
"test_multiple_characteristic_violations",
];
assert!(
!positive_tests.is_empty(),
"Should have positive test cases"
);
assert!(
!negative_tests.is_empty(),
"Should have negative test cases"
);
assert!(
negative_tests.len() > positive_tests.len(),
"Should have more violation tests than compliance tests"
);
let combination_tests = vec!["test_multiple_characteristic_violations"];
assert!(
!combination_tests.is_empty(),
"Should test error combinations"
);
}
}
#[tokio::test]
async fn test_uniqueness_update_same_resource() {
let registry = SchemaRegistry::new().expect("Failed to create schema registry");
let storage = InMemoryStorage::new();
let provider = StandardResourceProvider::new(storage);
let context = RequestContext::with_generated_id();
let existing_user = json!({
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"userName": "existing@example.com",
"meta": {
"resourceType": "User"
}
});
let created_user = provider
.create_resource("User", existing_user, &context)
.await
.expect("Failed to create existing user");
let update_user = json!({
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"id": created_user.id.as_ref().unwrap().as_str(),
"userName": "existing@example.com", "displayName": "Updated Display Name", "meta": {
"resourceType": "User"
}
});
let result = registry
.validate_json_resource_with_provider(
"User",
&update_user,
OperationContext::Update,
&provider,
&context,
)
.await;
assert!(
result.is_ok(),
"UPDATE should allow same resource to keep its userName, got {:?}",
result
);
let new_user = json!({
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"userName": "existing@example.com", "meta": {
"resourceType": "User"
}
});
let result = registry
.validate_json_resource_with_provider(
"User",
&new_user,
OperationContext::Create,
&provider,
&context,
)
.await;
match result {
Err(ValidationError::ServerUniquenessViolation { attribute, value }) => {
assert_eq!(attribute, "userName");
assert_eq!(value, "\"existing@example.com\"");
}
_ => panic!(
"Expected ServerUniquenessViolation for CREATE, got {:?}",
result
),
}
}