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