1pub mod builder;
16pub mod conditional_provider;
17pub mod context;
18pub mod core;
19pub mod handlers;
20pub mod mapper;
21pub mod provider;
22pub mod resource;
23pub mod serialization;
24pub mod tenant;
25pub mod types;
26pub mod value_objects;
27pub mod version;
28
29pub use builder::ResourceBuilder;
31pub use context::{ListQuery, RequestContext};
32pub use resource::Resource;
33pub use tenant::{IsolationLevel, TenantContext, TenantPermissions};
34pub use crate::multi_tenant::ScimOperation;
36pub use handlers::{AttributeHandler, DynamicResource, ResourceHandler, SchemaResourceBuilder};
37pub use mapper::{DatabaseMapper, SchemaMapper};
38pub use provider::{ResourceProvider, ResourceProviderExt};
39pub use types::EmailAddress;
40pub use value_objects::{
41 Address, EmailAddress as EmailAddressValue, ExternalId, Meta, Name, PhoneNumber, ResourceId,
42 SchemaUri, UserName,
43};
44
45#[cfg(test)]
46mod tests {
47 use super::*;
48 use serde_json::json;
49
50 #[test]
51 fn test_resource_creation() {
52 let data = json!({
53 "userName": "testuser",
54 "displayName": "Test User"
55 });
56 let resource = Resource::from_json("User".to_string(), data).unwrap();
57
58 assert_eq!(resource.resource_type, "User");
59 assert_eq!(resource.get_username(), Some("testuser"));
60 }
61
62 #[test]
63 fn test_resource_id_extraction() {
64 let data = json!({
65 "id": "12345",
66 "userName": "testuser"
67 });
68 let resource = Resource::from_json("User".to_string(), data).unwrap();
69
70 assert_eq!(resource.get_id(), Some("12345"));
71 }
72
73 #[test]
74 fn test_resource_schemas() {
75 let data = json!({
76 "userName": "testuser"
77 });
78 let resource = Resource::from_json("User".to_string(), data).unwrap();
79
80 let schemas = resource.get_schemas();
81 assert_eq!(schemas.len(), 1);
82 assert_eq!(schemas[0], "urn:ietf:params:scim:schemas:core:2.0:User");
83 }
84
85 #[test]
86 fn test_email_extraction() {
87 let data = json!({
88 "userName": "testuser",
89 "emails": [
90 {
91 "value": "test@example.com",
92 "type": "work",
93 "primary": true
94 }
95 ]
96 });
97 let resource = Resource::from_json("User".to_string(), data).unwrap();
98
99 let emails = resource.get_emails().expect("Should have emails");
100 assert_eq!(emails.len(), 1);
101 let email = emails.get(0).expect("Should have first email");
102 assert_eq!(email.value(), "test@example.com");
103 }
104
105 #[test]
106 fn test_request_context_creation() {
107 let context = RequestContext::new("test-request".to_string());
108 assert!(!context.request_id.is_empty());
109
110 let context_with_id = RequestContext::new("test-123".to_string());
111 assert_eq!(context_with_id.request_id, "test-123");
112 }
113
114 #[test]
115 fn test_resource_active_status() {
116 let active_data = json!({
117 "userName": "testuser",
118 "active": true
119 });
120 let active_resource = Resource::from_json("User".to_string(), active_data).unwrap();
121 assert!(active_resource.is_active());
122
123 let inactive_data = json!({
124 "userName": "testuser",
125 "active": false
126 });
127 let inactive_resource = Resource::from_json("User".to_string(), inactive_data).unwrap();
128 assert!(!inactive_resource.is_active());
129
130 let no_active_data = json!({
131 "userName": "testuser"
132 });
133 let default_resource = Resource::from_json("User".to_string(), no_active_data).unwrap();
134 assert!(default_resource.is_active()); }
136
137 #[test]
138 fn test_meta_extraction_from_json() {
139 use chrono::{TimeZone, Utc};
140
141 let data_with_meta = json!({
143 "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
144 "id": "12345",
145 "userName": "testuser",
146 "meta": {
147 "resourceType": "User",
148 "created": "2023-01-01T12:00:00Z",
149 "lastModified": "2023-01-02T12:00:00Z",
150 "location": "https://example.com/Users/12345",
151 "version": "W/\"12345-1672574400000\""
152 }
153 });
154
155 let resource = Resource::from_json("User".to_string(), data_with_meta).unwrap();
156 let meta = resource.get_meta().unwrap();
157
158 assert_eq!(meta.resource_type(), "User");
159 assert_eq!(
160 meta.created(),
161 Utc.with_ymd_and_hms(2023, 1, 1, 12, 0, 0).unwrap()
162 );
163 assert_eq!(
164 meta.last_modified(),
165 Utc.with_ymd_and_hms(2023, 1, 2, 12, 0, 0).unwrap()
166 );
167 assert_eq!(meta.location(), Some("https://example.com/Users/12345"));
168 assert_eq!(meta.version(), Some("W/\"12345-1672574400000\""));
169 }
170
171 #[test]
172 fn test_meta_extraction_minimal() {
173 let data_minimal_meta = json!({
175 "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
176 "userName": "testuser",
177 "meta": {
178 "resourceType": "User",
179 "created": "2023-01-01T12:00:00Z",
180 "lastModified": "2023-01-01T12:00:00Z"
181 }
182 });
183
184 let resource = Resource::from_json("User".to_string(), data_minimal_meta).unwrap();
185 let meta = resource.get_meta().unwrap();
186
187 assert_eq!(meta.resource_type(), "User");
188 assert_eq!(meta.location(), None);
189 assert_eq!(meta.version(), None);
190 }
191
192 #[test]
193 fn test_meta_extraction_invalid_datetime_returns_error() {
194 let data_invalid_meta = json!({
196 "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
197 "userName": "testuser",
198 "meta": {
199 "resourceType": "User",
200 "created": "invalid-date",
201 "lastModified": "2023-01-01T12:00:00Z"
202 }
203 });
204
205 let result = Resource::from_json("User".to_string(), data_invalid_meta);
206 assert!(result.is_err());
207 match result.unwrap_err() {
208 crate::error::ValidationError::InvalidCreatedDateTime => {
209 }
211 other => panic!("Expected InvalidCreatedDateTime, got {:?}", other),
212 }
213 }
214
215 #[test]
216 fn test_meta_extraction_incomplete_is_ignored() {
217 let data_incomplete_meta = json!({
219 "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
220 "userName": "testuser",
221 "meta": {
222 "resourceType": "User"
223 }
225 });
226
227 let resource = Resource::from_json("User".to_string(), data_incomplete_meta).unwrap();
228 assert!(resource.get_meta().is_none());
229 }
230
231 #[test]
232 fn test_meta_extraction_missing() {
233 let data_no_meta = json!({
235 "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
236 "userName": "testuser"
237 });
238
239 let resource = Resource::from_json("User".to_string(), data_no_meta).unwrap();
240 assert!(resource.get_meta().is_none());
241 }
242
243 #[test]
244 fn test_set_meta() {
245 use crate::resource::value_objects::Meta;
246 use chrono::Utc;
247
248 let mut resource = Resource::from_json(
249 "User".to_string(),
250 json!({
251 "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
252 "userName": "testuser"
253 }),
254 )
255 .unwrap();
256
257 let now = Utc::now();
258 let meta = Meta::new_simple("User".to_string(), now, now).unwrap();
259 resource.set_meta(meta.clone());
260
261 assert!(resource.get_meta().is_some());
262 assert_eq!(resource.get_meta().unwrap().resource_type(), "User");
263
264 let json_output = resource.to_json().unwrap();
266 assert!(json_output.get("meta").is_some());
267 }
268
269 #[test]
270 fn test_create_meta() {
271 let mut resource = Resource::from_json(
272 "User".to_string(),
273 json!({
274 "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
275 "id": "12345",
276 "userName": "testuser"
277 }),
278 )
279 .unwrap();
280
281 resource.create_meta("https://example.com").unwrap();
282
283 let meta = resource.get_meta().unwrap();
284 assert_eq!(meta.resource_type(), "User");
285 assert_eq!(meta.created(), meta.last_modified());
286 assert_eq!(meta.location(), Some("https://example.com/Users/12345"));
287 }
288
289 #[test]
290 fn test_update_meta() {
291 use crate::resource::value_objects::Meta;
292 use chrono::Utc;
293 use std::thread;
294 use std::time::Duration;
295
296 let mut resource = Resource::from_json(
297 "User".to_string(),
298 json!({
299 "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
300 "userName": "testuser"
301 }),
302 )
303 .unwrap();
304
305 let now = Utc::now();
306 let meta = Meta::new_simple("User".to_string(), now, now).unwrap();
307 resource.set_meta(meta);
308
309 let original_modified = resource.get_meta().unwrap().last_modified();
310
311 thread::sleep(Duration::from_millis(10));
313
314 resource.update_meta();
315
316 let updated_modified = resource.get_meta().unwrap().last_modified();
317 assert!(updated_modified > original_modified);
318 assert_eq!(resource.get_meta().unwrap().created(), now);
319 }
320
321 #[test]
322 fn test_add_metadata_legacy_compatibility() {
323 let mut resource = Resource::from_json(
324 "User".to_string(),
325 json!({
326 "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
327 "id": "12345",
328 "userName": "testuser"
329 }),
330 )
331 .unwrap();
332
333 resource.add_metadata(
334 "https://example.com",
335 "2023-01-01T12:00:00Z",
336 "2023-01-02T12:00:00Z",
337 );
338
339 let meta = resource.get_meta().unwrap();
341 assert_eq!(meta.resource_type(), "User");
342 assert_eq!(meta.location(), Some("https://example.com/Users/12345"));
343
344 let json_output = resource.to_json().unwrap();
346 assert!(json_output.get("meta").is_some());
347 }
348
349 #[test]
350 fn test_meta_serialization_in_to_json() {
351 use crate::resource::value_objects::Meta;
352 use chrono::{TimeZone, Utc};
353
354 let mut resource = Resource::from_json(
355 "User".to_string(),
356 json!({
357 "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
358 "userName": "testuser"
359 }),
360 )
361 .unwrap();
362
363 let created = Utc.with_ymd_and_hms(2023, 1, 1, 12, 0, 0).unwrap();
364 let modified = Utc.with_ymd_and_hms(2023, 1, 2, 12, 0, 0).unwrap();
365 let meta = Meta::new(
366 "User".to_string(),
367 created,
368 modified,
369 Some("https://example.com/Users/123".to_string()),
370 Some("W/\"123-456\"".to_string()),
371 )
372 .unwrap();
373
374 resource.set_meta(meta);
375
376 let json_output = resource.to_json().unwrap();
377 let meta_json = json_output.get("meta").unwrap();
378
379 assert_eq!(
380 meta_json.get("resourceType").unwrap().as_str().unwrap(),
381 "User"
382 );
383 assert!(
384 meta_json
385 .get("created")
386 .unwrap()
387 .as_str()
388 .unwrap()
389 .starts_with("2023-01-01T12:00:00")
390 );
391 assert!(
392 meta_json
393 .get("lastModified")
394 .unwrap()
395 .as_str()
396 .unwrap()
397 .starts_with("2023-01-02T12:00:00")
398 );
399 assert_eq!(
400 meta_json.get("location").unwrap().as_str().unwrap(),
401 "https://example.com/Users/123"
402 );
403 assert_eq!(
404 meta_json.get("version").unwrap().as_str().unwrap(),
405 "W/\"123-456\""
406 );
407 }
408
409 #[test]
410 fn test_resource_with_name_extraction() {
411 let data = json!({
412 "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
413 "userName": "testuser",
414 "name": {
415 "formatted": "John Doe",
416 "familyName": "Doe",
417 "givenName": "John"
418 }
419 });
420
421 let resource = Resource::from_json("User".to_string(), data).unwrap();
422
423 assert!(resource.get_name().is_some());
424 let name = resource.get_name().unwrap();
425 assert_eq!(name.formatted(), Some("John Doe"));
426 assert_eq!(name.family_name(), Some("Doe"));
427 assert_eq!(name.given_name(), Some("John"));
428 }
429
430 #[test]
431 fn test_resource_with_addresses_extraction() {
432 let data = json!({
433 "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
434 "userName": "testuser",
435 "addresses": [
436 {
437 "type": "work",
438 "streetAddress": "123 Main St",
439 "locality": "Anytown",
440 "region": "CA",
441 "postalCode": "12345",
442 "country": "US",
443 "primary": true
444 }
445 ]
446 });
447
448 let resource = Resource::from_json("User".to_string(), data).unwrap();
449
450 let addresses = resource.get_addresses().expect("Should have addresses");
451 assert_eq!(addresses.len(), 1);
452 let address = addresses.get(0).expect("Should have first address");
453 assert_eq!(address.address_type(), Some("work"));
454 assert_eq!(address.street_address(), Some("123 Main St"));
455 assert_eq!(address.locality(), Some("Anytown"));
456 assert_eq!(address.region(), Some("CA"));
457 assert_eq!(address.postal_code(), Some("12345"));
458 assert_eq!(address.country(), Some("US"));
459 assert_eq!(address.is_primary(), true);
460 }
461
462 #[test]
463 fn test_resource_with_phone_numbers_extraction() {
464 let data = json!({
465 "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
466 "userName": "testuser",
467 "phoneNumbers": [
468 {
469 "value": "tel:+1-555-555-5555",
470 "type": "work",
471 "primary": true
472 }
473 ]
474 });
475
476 let resource = Resource::from_json("User".to_string(), data).unwrap();
477
478 let phones = resource
479 .get_phone_numbers()
480 .expect("Should have phone numbers");
481 assert_eq!(phones.len(), 1);
482 let phone = phones.get(0).expect("Should have first phone");
483 assert_eq!(phone.value(), "tel:+1-555-555-5555");
484 assert_eq!(phone.phone_type(), Some("work"));
485 assert_eq!(phone.is_primary(), true);
486 }
487
488 #[test]
489 fn test_resource_builder_basic() {
490 use crate::resource::value_objects::{ResourceId, UserName};
491
492 let resource = ResourceBuilder::new("User".to_string())
493 .with_id(ResourceId::new("123".to_string()).unwrap())
494 .with_username(UserName::new("jdoe".to_string()).unwrap())
495 .with_attribute("displayName", json!("John Doe"))
496 .build()
497 .unwrap();
498
499 assert_eq!(resource.resource_type, "User");
500 assert_eq!(resource.get_id(), Some("123"));
501 assert_eq!(resource.get_username(), Some("jdoe"));
502 assert_eq!(
503 resource.get_attribute("displayName"),
504 Some(&json!("John Doe"))
505 );
506 assert_eq!(resource.schemas.len(), 1);
507 assert_eq!(
508 resource.schemas[0].as_str(),
509 "urn:ietf:params:scim:schemas:core:2.0:User"
510 );
511 }
512
513 #[test]
514 fn test_resource_builder_with_complex_attributes() {
515 use crate::resource::value_objects::{Address, Name, PhoneNumber};
516
517 let name = Name::new_simple("John".to_string(), "Doe".to_string()).unwrap();
518 let address = Address::new_work(
519 "123 Main St".to_string(),
520 "Anytown".to_string(),
521 "CA".to_string(),
522 "12345".to_string(),
523 "US".to_string(),
524 )
525 .unwrap();
526 let phone = PhoneNumber::new_work("tel:+1-555-555-5555".to_string()).unwrap();
527
528 let resource = ResourceBuilder::new("User".to_string())
529 .with_name(name)
530 .add_address(address)
531 .add_phone_number(phone)
532 .build()
533 .unwrap();
534
535 assert!(resource.get_name().is_some());
536 assert_eq!(resource.get_addresses().unwrap().len(), 1);
537 assert_eq!(resource.get_phone_numbers().unwrap().len(), 1);
538
539 let json_output = resource.to_json().unwrap();
541 assert!(json_output.get("name").is_some());
542 assert!(json_output.get("addresses").is_some());
543 assert!(json_output.get("phoneNumbers").is_some());
544 }
545
546 #[test]
547 fn test_resource_builder_with_meta() {
548 use crate::resource::value_objects::ResourceId;
549
550 let resource = ResourceBuilder::new("User".to_string())
551 .with_id(ResourceId::new("123".to_string()).unwrap())
552 .build_with_meta("https://example.com")
553 .unwrap();
554
555 assert!(resource.get_meta().is_some());
556 let meta = resource.get_meta().unwrap();
557 assert_eq!(meta.resource_type(), "User");
558 assert_eq!(meta.location(), Some("https://example.com/Users/123"));
559 }
560
561 #[test]
562 fn test_resource_builder_validation() {
563 let builder = ResourceBuilder::new("User".to_string());
565 let builder_no_schema = builder.with_schemas(vec![]);
567 let result = builder_no_schema.build();
568 assert!(result.is_err());
569 }
570
571 #[test]
572 fn test_resource_setter_methods() {
573 use crate::resource::value_objects::{Address, Name, PhoneNumber};
574
575 let mut resource = Resource::from_json(
576 "User".to_string(),
577 json!({
578 "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
579 "userName": "testuser"
580 }),
581 )
582 .unwrap();
583
584 let name = Name::new_simple("Jane".to_string(), "Smith".to_string()).unwrap();
586 resource.set_name(name);
587 assert!(resource.get_name().is_some());
588 assert_eq!(resource.get_name().unwrap().given_name(), Some("Jane"));
589
590 let address = Address::new(
592 None,
593 Some("456 Oak Ave".to_string()),
594 Some("Hometown".to_string()),
595 Some("NY".to_string()),
596 Some("67890".to_string()),
597 Some("US".to_string()),
598 Some("home".to_string()),
599 Some(false),
600 )
601 .unwrap();
602 resource.add_address(address).unwrap();
603 let addresses = resource.get_addresses().expect("Should have addresses");
604 assert_eq!(addresses.len(), 1);
605 let address = addresses.get(0).expect("Should have first address");
606 assert_eq!(address.address_type(), Some("home"));
607
608 let phone = PhoneNumber::new_mobile("tel:+1-555-123-4567".to_string()).unwrap();
610 resource.add_phone_number(phone).unwrap();
611 let phones = resource
612 .get_phone_numbers()
613 .expect("Should have phone numbers");
614 assert_eq!(phones.len(), 1);
615 let phone = phones.get(0).expect("Should have first phone");
616 assert_eq!(phone.phone_type(), Some("mobile"));
617 }
618
619 #[test]
620 fn test_resource_json_round_trip_with_complex_attributes() {
621 let data = json!({
622 "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
623 "id": "123",
624 "userName": "jdoe",
625 "name": {
626 "formatted": "John Doe",
627 "familyName": "Doe",
628 "givenName": "John"
629 },
630 "addresses": [
631 {
632 "type": "work",
633 "streetAddress": "123 Main St",
634 "locality": "Anytown",
635 "region": "CA",
636 "postalCode": "12345",
637 "country": "US",
638 "primary": true
639 }
640 ],
641 "phoneNumbers": [
642 {
643 "value": "tel:+1-555-555-5555",
644 "type": "work",
645 "primary": true
646 }
647 ]
648 });
649
650 let resource = Resource::from_json("User".to_string(), data).unwrap();
651 let json_output = resource.to_json().unwrap();
652
653 assert!(json_output.get("name").is_some());
655 assert!(json_output.get("addresses").is_some());
656 assert!(json_output.get("phoneNumbers").is_some());
657
658 let name_json = json_output.get("name").unwrap();
660 assert_eq!(
661 name_json.get("formatted").unwrap().as_str().unwrap(),
662 "John Doe"
663 );
664
665 let addresses_json = json_output.get("addresses").unwrap().as_array().unwrap();
666 assert_eq!(addresses_json.len(), 1);
667 assert_eq!(
668 addresses_json[0].get("type").unwrap().as_str().unwrap(),
669 "work"
670 );
671
672 let phones_json = json_output.get("phoneNumbers").unwrap().as_array().unwrap();
673 assert_eq!(phones_json.len(), 1);
674 assert_eq!(
675 phones_json[0].get("value").unwrap().as_str().unwrap(),
676 "tel:+1-555-555-5555"
677 );
678 }
679
680 #[test]
681 fn test_resource_invalid_complex_attributes() {
682 let invalid_name_data = json!({
684 "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
685 "userName": "testuser",
686 "name": "should be object not string"
687 });
688 let result = Resource::from_json("User".to_string(), invalid_name_data);
689 assert!(result.is_err());
690
691 let invalid_addresses_data = json!({
693 "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
694 "userName": "testuser",
695 "addresses": "should be array not string"
696 });
697 let result = Resource::from_json("User".to_string(), invalid_addresses_data);
698 assert!(result.is_err());
699
700 let invalid_phones_data = json!({
702 "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
703 "userName": "testuser",
704 "phoneNumbers": "should be array not string"
705 });
706 let result = Resource::from_json("User".to_string(), invalid_phones_data);
707 assert!(result.is_err());
708 }
709}