1use crate::errors::{AuthError, Result};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::sync::Arc;
10use tokio::sync::RwLock;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct UmaDiscoveryMetadata {
19 pub issuer: String,
21 pub token_endpoint: String,
23 pub resource_registration_endpoint: String,
25 pub permission_endpoint: String,
27 pub rpt_endpoint: String,
29 pub introspection_endpoint: String,
31 pub claims_interaction_endpoint: String,
33 pub grant_types_supported: Vec<String>,
35 pub token_endpoint_auth_methods_supported: Vec<String>,
37 pub uma_profiles_supported: Vec<String>,
39}
40
41impl UmaDiscoveryMetadata {
42 pub fn new(issuer: impl Into<String>) -> Self {
44 let base = issuer.into();
45 Self {
46 token_endpoint: format!("{base}/oauth/token"),
47 resource_registration_endpoint: format!("{base}/uma/resource_set"),
48 permission_endpoint: format!("{base}/uma/permission"),
49 rpt_endpoint: format!("{base}/oauth/token"),
50 introspection_endpoint: format!("{base}/oauth/introspect"),
51 claims_interaction_endpoint: format!("{base}/uma/claims"),
52 grant_types_supported: vec!["urn:ietf:params:oauth:grant-type:uma-ticket".to_string()],
53 token_endpoint_auth_methods_supported: vec![
54 "client_secret_basic".to_string(),
55 "client_secret_post".to_string(),
56 ],
57 uma_profiles_supported: vec![],
58 issuer: base,
59 }
60 }
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct UmaConfig {
65 pub enabled: bool,
66 pub pat_lifetime: u64, pub rpt_lifetime: u64, pub claims_interaction_endpoint: String,
69 pub ticket_lifetime: u64,
71}
72
73impl Default for UmaConfig {
74 fn default() -> Self {
75 Self {
76 enabled: false,
77 pat_lifetime: 3600,
78 rpt_lifetime: 3600,
79 claims_interaction_endpoint: "/api/uma/claims".to_string(),
80 ticket_lifetime: 300,
81 }
82 }
83}
84
85pub struct UmaService {
87 config: UmaConfig,
88 resource_sets: Arc<RwLock<HashMap<String, UmaResourceSet>>>,
89 permission_tickets: Arc<RwLock<HashMap<String, PermissionTicket>>>,
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct UmaResourceSet {
94 pub id: String,
95 pub name: String,
96 pub scopes: Vec<String>,
97 pub owner_id: String,
98 pub required_claims: HashMap<String, String>,
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize)]
104struct PermissionTicket {
105 pub ticket: String,
106 pub resource_id: String,
107 pub requested_scopes: Vec<String>,
108 pub created_at: u64,
109}
110
111impl UmaService {
112 pub fn new(config: UmaConfig) -> Self {
113 Self {
114 config,
115 resource_sets: Arc::new(RwLock::new(HashMap::new())),
116 permission_tickets: Arc::new(RwLock::new(HashMap::new())),
117 }
118 }
119
120 pub async fn register_resource_set(&self, mut resource_set: UmaResourceSet) -> Result<String> {
122 if !self.config.enabled {
123 return Err(AuthError::config("UMA 2.0 protocol is currently disabled"));
124 }
125
126 if resource_set.id.is_empty() {
127 resource_set.id = uuid::Uuid::new_v4().to_string();
128 }
129
130 let id = resource_set.id.clone();
131 self.resource_sets
132 .write()
133 .await
134 .insert(id.clone(), resource_set);
135 Ok(id)
136 }
137
138 pub async fn create_permission_ticket(
140 &self,
141 resource_id: &str,
142 requested_scopes: Vec<String>,
143 ) -> Result<String> {
144 if !self.config.enabled {
145 return Err(AuthError::config("UMA 2.0 protocol is currently disabled"));
146 }
147
148 let resources = self.resource_sets.read().await;
150 let resource = resources
151 .get(resource_id)
152 .ok_or_else(|| AuthError::validation("Resource set not found"))?;
153
154 for scope in &requested_scopes {
156 if !resource.scopes.contains(scope) {
157 return Err(AuthError::validation(&format!(
158 "Scope '{}' is not valid for resource '{}'",
159 scope, resource_id
160 )));
161 }
162 }
163
164 let ticket = format!("urn:uma:ticket:{}", uuid::Uuid::new_v4());
165 let now = std::time::SystemTime::now()
166 .duration_since(std::time::UNIX_EPOCH)
167 .unwrap_or_default()
168 .as_secs();
169
170 let permission = PermissionTicket {
171 ticket: ticket.clone(),
172 resource_id: resource_id.to_string(),
173 requested_scopes,
174 created_at: now,
175 };
176
177 self.permission_tickets
178 .write()
179 .await
180 .insert(ticket.clone(), permission);
181
182 Ok(ticket)
183 }
184
185 pub async fn request_rpt(
187 &self,
188 ticket: &str,
189 claims: Option<HashMap<String, String>>,
190 ) -> Result<String> {
191 if !self.config.enabled {
192 return Err(AuthError::config("UMA 2.0 protocol is currently disabled"));
193 }
194
195 if ticket.is_empty() {
196 return Err(AuthError::validation("Missing permission ticket"));
197 }
198
199 let tickets = self.permission_tickets.read().await;
201 let permission = tickets
202 .get(ticket)
203 .ok_or_else(|| AuthError::validation("Permission ticket not found or expired"))?;
204
205 let now = std::time::SystemTime::now()
207 .duration_since(std::time::UNIX_EPOCH)
208 .unwrap_or_default()
209 .as_secs();
210 if now.saturating_sub(permission.created_at) > self.config.ticket_lifetime {
211 drop(tickets);
212 self.permission_tickets.write().await.remove(ticket);
213 return Err(AuthError::validation("Permission ticket has expired"));
214 };
215
216 let resources = self.resource_sets.read().await;
218 let resource = resources.get(&permission.resource_id).ok_or_else(|| {
219 AuthError::internal("Resource for permission ticket no longer exists")
220 })?;
221
222 if !resource.required_claims.is_empty() {
224 let provided_claims = claims.as_ref().ok_or_else(|| {
225 AuthError::validation(&format!(
226 "UMA need_info: Redirect to {} with ticket {}",
227 self.config.claims_interaction_endpoint, ticket
228 ))
229 })?;
230
231 for (required_claim, expected_value) in &resource.required_claims {
232 match provided_claims.get(required_claim) {
233 Some(actual_value) if actual_value == expected_value => {}
234 Some(_) => {
235 return Err(AuthError::validation(&format!(
236 "Claim '{}' does not match required policy",
237 required_claim
238 )));
239 }
240 None => {
241 return Err(AuthError::validation(&format!(
242 "UMA need_info: Missing required claim '{}'",
243 required_claim
244 )));
245 }
246 }
247 }
248 }
249
250 let rpt = format!("urn:uma:rpt:{}", uuid::Uuid::new_v4());
252
253 drop(tickets);
255 drop(resources);
256 self.permission_tickets.write().await.remove(ticket);
257
258 Ok(rpt)
259 }
260
261 pub async fn list_resource_sets(&self, owner_id: &str) -> Result<Vec<UmaResourceSet>> {
263 if !self.config.enabled {
264 return Err(AuthError::config("UMA 2.0 protocol is currently disabled"));
265 }
266
267 let resources = self.resource_sets.read().await;
268 Ok(resources
269 .values()
270 .filter(|r| r.owner_id == owner_id)
271 .cloned()
272 .collect())
273 }
274
275 pub async fn delete_resource_set(&self, resource_id: &str, owner_id: &str) -> Result<()> {
277 if !self.config.enabled {
278 return Err(AuthError::config("UMA 2.0 protocol is currently disabled"));
279 }
280
281 let mut resources = self.resource_sets.write().await;
282 if let Some(resource) = resources.get(resource_id) {
283 if resource.owner_id != owner_id {
284 return Err(AuthError::validation(
285 "Only the resource owner can delete a resource set",
286 ));
287 }
288 resources.remove(resource_id);
289 Ok(())
290 } else {
291 Err(AuthError::validation("Resource set not found"))
292 }
293 }
294
295 pub async fn cleanup_expired_tickets(&self) {
297 let now = std::time::SystemTime::now()
298 .duration_since(std::time::UNIX_EPOCH)
299 .unwrap_or_default()
300 .as_secs();
301 let lifetime = self.config.ticket_lifetime;
302 self.permission_tickets
303 .write()
304 .await
305 .retain(|_, t| now.saturating_sub(t.created_at) <= lifetime);
306 }
307
308 pub fn discovery_metadata(&self, issuer: &str) -> UmaDiscoveryMetadata {
310 let mut meta = UmaDiscoveryMetadata::new(issuer);
311 meta.claims_interaction_endpoint = self.config.claims_interaction_endpoint.clone();
312 meta
313 }
314
315 pub async fn ticket_count(&self) -> usize {
317 self.permission_tickets.read().await.len()
318 }
319
320 pub async fn resource_count(&self) -> usize {
322 self.resource_sets.read().await.len()
323 }
324
325 pub async fn get_resource_set(&self, resource_id: &str) -> Option<UmaResourceSet> {
327 self.resource_sets.read().await.get(resource_id).cloned()
328 }
329
330 pub async fn update_resource_set(
332 &self,
333 resource_id: &str,
334 owner_id: &str,
335 name: Option<String>,
336 scopes: Option<Vec<String>>,
337 required_claims: Option<HashMap<String, String>>,
338 ) -> Result<()> {
339 if !self.config.enabled {
340 return Err(AuthError::config("UMA 2.0 protocol is currently disabled"));
341 }
342
343 let mut resources = self.resource_sets.write().await;
344 let resource = resources
345 .get_mut(resource_id)
346 .ok_or_else(|| AuthError::validation("Resource set not found"))?;
347
348 if resource.owner_id != owner_id {
349 return Err(AuthError::validation(
350 "Only the resource owner can update a resource set",
351 ));
352 }
353
354 if let Some(n) = name {
355 resource.name = n;
356 }
357 if let Some(s) = scopes {
358 resource.scopes = s;
359 }
360 if let Some(rc) = required_claims {
361 resource.required_claims = rc;
362 }
363 Ok(())
364 }
365}
366
367#[derive(Debug, Clone, Serialize, Deserialize)]
374pub struct Pat {
375 pub access_token: String,
377 pub client_id: String,
379 pub issued_at: u64,
381 pub expires_at: u64,
383}
384
385impl Pat {
386 pub fn is_expired(&self) -> bool {
388 let now = std::time::SystemTime::now()
389 .duration_since(std::time::UNIX_EPOCH)
390 .unwrap_or_default()
391 .as_secs();
392 now >= self.expires_at
393 }
394}
395
396pub struct PatStore {
398 tokens: Arc<RwLock<HashMap<String, Pat>>>,
399}
400
401impl PatStore {
402 pub fn new() -> Self {
404 Self {
405 tokens: Arc::new(RwLock::new(HashMap::new())),
406 }
407 }
408
409 pub async fn issue(&self, client_id: &str, lifetime_secs: u64) -> Pat {
411 let now = std::time::SystemTime::now()
412 .duration_since(std::time::UNIX_EPOCH)
413 .unwrap_or_default()
414 .as_secs();
415 let pat = Pat {
416 access_token: format!("pat_{}", uuid::Uuid::new_v4()),
417 client_id: client_id.to_string(),
418 issued_at: now,
419 expires_at: now + lifetime_secs,
420 };
421 self.tokens
422 .write()
423 .await
424 .insert(pat.access_token.clone(), pat.clone());
425 pat
426 }
427
428 pub async fn validate(&self, token: &str) -> Result<Pat> {
430 let tokens = self.tokens.read().await;
431 let pat = tokens
432 .get(token)
433 .ok_or_else(|| AuthError::invalid_credential("PAT", "Invalid or unknown PAT"))?;
434 if pat.is_expired() {
435 return Err(AuthError::invalid_credential("PAT", "PAT has expired"));
436 }
437 Ok(pat.clone())
438 }
439
440 pub async fn revoke(&self, token: &str) -> bool {
442 self.tokens.write().await.remove(token).is_some()
443 }
444
445 pub async fn cleanup_expired(&self) {
447 self.tokens.write().await.retain(|_, p| !p.is_expired());
448 }
449
450 pub async fn count(&self) -> usize {
452 self.tokens.read().await.len()
453 }
454}
455
456impl Default for PatStore {
457 fn default() -> Self {
458 Self::new()
459 }
460}
461
462#[derive(Debug, Clone, Serialize, Deserialize)]
466pub struct RptIntrospectionResponse {
467 pub active: bool,
469 #[serde(default)]
471 pub permissions: Vec<RptPermission>,
472 #[serde(default)]
474 pub exp: Option<u64>,
475 #[serde(default)]
477 pub iat: Option<u64>,
478}
479
480#[derive(Debug, Clone, Serialize, Deserialize)]
482pub struct RptPermission {
483 pub resource_id: String,
485 pub scopes: Vec<String>,
487}
488
489pub struct RptStore {
491 tokens: Arc<RwLock<HashMap<String, RptIntrospectionResponse>>>,
492}
493
494impl RptStore {
495 pub fn new() -> Self {
497 Self {
498 tokens: Arc::new(RwLock::new(HashMap::new())),
499 }
500 }
501
502 pub async fn register(
504 &self,
505 rpt: &str,
506 resource_id: &str,
507 scopes: Vec<String>,
508 lifetime_secs: u64,
509 ) {
510 let now = std::time::SystemTime::now()
511 .duration_since(std::time::UNIX_EPOCH)
512 .unwrap_or_default()
513 .as_secs();
514 let resp = RptIntrospectionResponse {
515 active: true,
516 permissions: vec![RptPermission {
517 resource_id: resource_id.to_string(),
518 scopes,
519 }],
520 exp: Some(now + lifetime_secs),
521 iat: Some(now),
522 };
523 self.tokens.write().await.insert(rpt.to_string(), resp);
524 }
525
526 pub async fn introspect(&self, rpt: &str) -> RptIntrospectionResponse {
528 let tokens = self.tokens.read().await;
529 match tokens.get(rpt) {
530 Some(resp) => {
531 let now = std::time::SystemTime::now()
532 .duration_since(std::time::UNIX_EPOCH)
533 .unwrap_or_default()
534 .as_secs();
535 let expired = resp.exp.is_some_and(|exp| now >= exp);
536 if expired {
537 RptIntrospectionResponse {
538 active: false,
539 permissions: vec![],
540 exp: resp.exp,
541 iat: resp.iat,
542 }
543 } else {
544 resp.clone()
545 }
546 }
547 None => RptIntrospectionResponse {
548 active: false,
549 permissions: vec![],
550 exp: None,
551 iat: None,
552 },
553 }
554 }
555
556 pub async fn revoke(&self, rpt: &str) -> bool {
558 self.tokens.write().await.remove(rpt).is_some()
559 }
560
561 pub async fn cleanup_expired(&self) {
563 let now = std::time::SystemTime::now()
564 .duration_since(std::time::UNIX_EPOCH)
565 .unwrap_or_default()
566 .as_secs();
567 self.tokens
568 .write()
569 .await
570 .retain(|_, r| r.exp.map_or(true, |exp| now < exp));
571 }
572
573 pub async fn count(&self) -> usize {
575 self.tokens.read().await.len()
576 }
577}
578
579impl Default for RptStore {
580 fn default() -> Self {
581 Self::new()
582 }
583}
584
585#[cfg(test)]
586mod tests {
587 use super::*;
588
589 fn enabled_config() -> UmaConfig {
590 UmaConfig {
591 enabled: true,
592 ..UmaConfig::default()
593 }
594 }
595
596 fn sample_resource(owner: &str) -> UmaResourceSet {
597 UmaResourceSet {
598 id: String::new(),
599 name: "Photos".to_string(),
600 scopes: vec!["read".to_string(), "write".to_string()],
601 owner_id: owner.to_string(),
602 required_claims: HashMap::new(),
603 }
604 }
605
606 #[tokio::test]
607 async fn test_register_resource_set() {
608 let svc = UmaService::new(enabled_config());
609 let id = svc
610 .register_resource_set(sample_resource("alice"))
611 .await
612 .unwrap();
613 assert!(!id.is_empty());
614 }
615
616 #[tokio::test]
617 async fn test_list_resource_sets_filters_by_owner() {
618 let svc = UmaService::new(enabled_config());
619 svc.register_resource_set(sample_resource("alice"))
620 .await
621 .unwrap();
622 svc.register_resource_set(sample_resource("bob"))
623 .await
624 .unwrap();
625 let alice_rs = svc.list_resource_sets("alice").await.unwrap();
626 assert_eq!(alice_rs.len(), 1);
627 assert_eq!(alice_rs[0].owner_id, "alice");
628 }
629
630 #[tokio::test]
631 async fn test_delete_resource_set() {
632 let svc = UmaService::new(enabled_config());
633 let id = svc
634 .register_resource_set(sample_resource("alice"))
635 .await
636 .unwrap();
637 svc.delete_resource_set(&id, "alice").await.unwrap();
638 let resources = svc.list_resource_sets("alice").await.unwrap();
639 assert!(resources.is_empty());
640 }
641
642 #[tokio::test]
643 async fn test_delete_resource_wrong_owner_rejected() {
644 let svc = UmaService::new(enabled_config());
645 let id = svc
646 .register_resource_set(sample_resource("alice"))
647 .await
648 .unwrap();
649 let result = svc.delete_resource_set(&id, "eve").await;
650 assert!(result.is_err());
651 }
652
653 #[tokio::test]
654 async fn test_create_permission_ticket() {
655 let svc = UmaService::new(enabled_config());
656 let id = svc
657 .register_resource_set(sample_resource("alice"))
658 .await
659 .unwrap();
660 let ticket = svc
661 .create_permission_ticket(&id, vec!["read".to_string()])
662 .await
663 .unwrap();
664 assert!(!ticket.is_empty());
665 }
666
667 #[tokio::test]
668 async fn test_permission_ticket_invalid_scope_rejected() {
669 let svc = UmaService::new(enabled_config());
670 let id = svc
671 .register_resource_set(sample_resource("alice"))
672 .await
673 .unwrap();
674 let result = svc
675 .create_permission_ticket(&id, vec!["delete".to_string()])
676 .await;
677 assert!(result.is_err());
678 }
679
680 #[tokio::test]
681 async fn test_request_rpt_with_valid_ticket() {
682 let svc = UmaService::new(enabled_config());
683 let id = svc
684 .register_resource_set(sample_resource("alice"))
685 .await
686 .unwrap();
687 let ticket = svc
688 .create_permission_ticket(&id, vec!["read".to_string()])
689 .await
690 .unwrap();
691 let rpt = svc.request_rpt(&ticket, None).await.unwrap();
692 assert!(!rpt.is_empty());
693 }
694
695 #[tokio::test]
696 async fn test_request_rpt_invalid_ticket_rejected() {
697 let svc = UmaService::new(enabled_config());
698 let result = svc.request_rpt("bogus-ticket", None).await;
699 assert!(result.is_err());
700 }
701
702 #[tokio::test]
703 async fn test_disabled_service_rejects() {
704 let svc = UmaService::new(UmaConfig::default()); let result = svc.register_resource_set(sample_resource("alice")).await;
706 assert!(result.is_err());
707 }
708
709 #[tokio::test]
710 async fn test_required_claims_enforced() {
711 let svc = UmaService::new(enabled_config());
712 let mut rs = sample_resource("alice");
713 rs.required_claims
714 .insert("country".to_string(), "US".to_string());
715 let id = svc.register_resource_set(rs).await.unwrap();
716 let ticket = svc
717 .create_permission_ticket(&id, vec!["read".to_string()])
718 .await
719 .unwrap();
720
721 let result = svc.request_rpt(&ticket, None).await;
723 assert!(result.is_err());
724
725 let ticket2 = svc
727 .create_permission_ticket(&id, vec!["read".to_string()])
728 .await
729 .unwrap();
730 let mut claims = HashMap::new();
732 claims.insert("country".to_string(), "US".to_string());
733 let rpt = svc.request_rpt(&ticket2, Some(claims)).await.unwrap();
734 assert!(!rpt.is_empty());
735 }
736
737 #[test]
740 fn test_uma_discovery_metadata() {
741 let meta = UmaDiscoveryMetadata::new("https://auth.example.com");
742 assert_eq!(meta.issuer, "https://auth.example.com");
743 assert_eq!(meta.token_endpoint, "https://auth.example.com/oauth/token");
744 assert_eq!(
745 meta.resource_registration_endpoint,
746 "https://auth.example.com/uma/resource_set"
747 );
748 assert_eq!(
749 meta.permission_endpoint,
750 "https://auth.example.com/uma/permission"
751 );
752 assert_eq!(
753 meta.introspection_endpoint,
754 "https://auth.example.com/oauth/introspect"
755 );
756 assert!(
757 meta.grant_types_supported
758 .contains(&"urn:ietf:params:oauth:grant-type:uma-ticket".to_string())
759 );
760 }
761
762 #[test]
763 fn test_uma_discovery_serialization() {
764 let meta = UmaDiscoveryMetadata::new("https://auth.example.com");
765 let json = serde_json::to_value(&meta).unwrap();
766 assert_eq!(json["issuer"], "https://auth.example.com");
767 assert!(json["grant_types_supported"].as_array().unwrap().len() > 0);
768 }
769
770 #[tokio::test]
771 async fn test_uma_service_discovery() {
772 let svc = UmaService::new(enabled_config());
773 let meta = svc.discovery_metadata("https://auth.example.com");
774 assert_eq!(meta.claims_interaction_endpoint, "/api/uma/claims");
775 }
776
777 #[tokio::test]
780 async fn test_update_resource_set() {
781 let svc = UmaService::new(enabled_config());
782 let id = svc
783 .register_resource_set(sample_resource("alice"))
784 .await
785 .unwrap();
786 svc.update_resource_set(
787 &id,
788 "alice",
789 Some("Updated Photos".to_string()),
790 Some(vec![
791 "read".to_string(),
792 "write".to_string(),
793 "delete".to_string(),
794 ]),
795 None,
796 )
797 .await
798 .unwrap();
799 let rs = svc.get_resource_set(&id).await.unwrap();
800 assert_eq!(rs.name, "Updated Photos");
801 assert_eq!(rs.scopes.len(), 3);
802 }
803
804 #[tokio::test]
805 async fn test_update_resource_set_wrong_owner() {
806 let svc = UmaService::new(enabled_config());
807 let id = svc
808 .register_resource_set(sample_resource("alice"))
809 .await
810 .unwrap();
811 assert!(
812 svc.update_resource_set(&id, "eve", Some("Hacked".to_string()), None, None)
813 .await
814 .is_err()
815 );
816 }
817
818 #[tokio::test]
819 async fn test_resource_count() {
820 let svc = UmaService::new(enabled_config());
821 assert_eq!(svc.resource_count().await, 0);
822 svc.register_resource_set(sample_resource("alice"))
823 .await
824 .unwrap();
825 assert_eq!(svc.resource_count().await, 1);
826 }
827
828 #[tokio::test]
831 async fn test_pat_issue_and_validate() {
832 let store = PatStore::new();
833 let pat = store.issue("client1", 3600).await;
834 assert!(pat.access_token.starts_with("pat_"));
835 assert_eq!(pat.client_id, "client1");
836 assert!(!pat.is_expired());
837
838 let validated = store.validate(&pat.access_token).await.unwrap();
839 assert_eq!(validated.client_id, "client1");
840 }
841
842 #[tokio::test]
843 async fn test_pat_validate_unknown() {
844 let store = PatStore::new();
845 assert!(store.validate("bogus").await.is_err());
846 }
847
848 #[tokio::test]
849 async fn test_pat_revoke() {
850 let store = PatStore::new();
851 let pat = store.issue("client1", 3600).await;
852 assert!(store.revoke(&pat.access_token).await);
853 assert!(!store.revoke(&pat.access_token).await);
854 assert!(store.validate(&pat.access_token).await.is_err());
855 }
856
857 #[tokio::test]
858 async fn test_pat_count() {
859 let store = PatStore::new();
860 store.issue("c1", 3600).await;
861 store.issue("c2", 3600).await;
862 assert_eq!(store.count().await, 2);
863 }
864
865 #[tokio::test]
868 async fn test_rpt_register_and_introspect() {
869 let store = RptStore::new();
870 store
871 .register("rpt-123", "resource-1", vec!["read".to_string()], 3600)
872 .await;
873 let resp = store.introspect("rpt-123").await;
874 assert!(resp.active);
875 assert_eq!(resp.permissions.len(), 1);
876 assert_eq!(resp.permissions[0].resource_id, "resource-1");
877 assert_eq!(resp.permissions[0].scopes, vec!["read"]);
878 }
879
880 #[tokio::test]
881 async fn test_rpt_introspect_unknown() {
882 let store = RptStore::new();
883 let resp = store.introspect("unknown").await;
884 assert!(!resp.active);
885 assert!(resp.permissions.is_empty());
886 }
887
888 #[tokio::test]
889 async fn test_rpt_revoke() {
890 let store = RptStore::new();
891 store
892 .register("rpt-456", "res-1", vec!["write".to_string()], 3600)
893 .await;
894 assert!(store.revoke("rpt-456").await);
895 let resp = store.introspect("rpt-456").await;
896 assert!(!resp.active);
897 }
898
899 #[tokio::test]
900 async fn test_rpt_count() {
901 let store = RptStore::new();
902 store
903 .register("rpt-1", "r1", vec!["read".to_string()], 3600)
904 .await;
905 store
906 .register("rpt-2", "r2", vec!["write".to_string()], 3600)
907 .await;
908 assert_eq!(store.count().await, 2);
909 }
910
911 #[tokio::test]
912 async fn test_rpt_introspection_serialization() {
913 let resp = RptIntrospectionResponse {
914 active: true,
915 permissions: vec![RptPermission {
916 resource_id: "res-1".to_string(),
917 scopes: vec!["read".to_string()],
918 }],
919 exp: Some(9999999999),
920 iat: Some(1000000000),
921 };
922 let json = serde_json::to_value(&resp).unwrap();
923 assert_eq!(json["active"], true);
924 assert_eq!(json["permissions"][0]["resource_id"], "res-1");
925 }
926}