1use std::sync::Arc;
10
11use axum::Json;
12use axum::extract::{FromRef, FromRequestParts};
13use axum::http::StatusCode;
14use axum::http::request::Parts;
15use axum::response::{IntoResponse, Response};
16use axum_extra::extract::cookie::CookieJar;
17use chrono::Utc;
18use ironflow_store::entities::ApiKeyScope;
19use ironflow_store::store::Store;
20use serde_json::json;
21use uuid::Uuid;
22
23use crate::cookies::AUTH_COOKIE_NAME;
24use crate::jwt::{AccessToken, JwtConfig};
25use crate::password;
26
27#[derive(Debug, Clone)]
46pub struct AuthenticatedUser {
47 pub user_id: Uuid,
49 pub username: String,
51 pub is_admin: bool,
53}
54
55impl<S> FromRequestParts<S> for AuthenticatedUser
56where
57 S: Send + Sync,
58 Arc<JwtConfig>: FromRef<S>,
59{
60 type Rejection = AuthRejection;
61
62 async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
63 let jwt_config = Arc::<JwtConfig>::from_ref(state);
64
65 let jar = CookieJar::from_headers(&parts.headers);
66 let token = jar
67 .get(AUTH_COOKIE_NAME)
68 .map(|c| c.value().to_string())
69 .or_else(|| {
70 parts
71 .headers
72 .get("authorization")
73 .and_then(|v| v.to_str().ok())
74 .and_then(|v| v.strip_prefix("Bearer "))
75 .map(|t| t.to_string())
76 });
77
78 let token = token.ok_or(AuthRejection {
79 status: StatusCode::UNAUTHORIZED,
80 code: "MISSING_TOKEN",
81 message: "No authentication token provided",
82 })?;
83
84 let claims = AccessToken::decode(&token, &jwt_config).map_err(|_| AuthRejection {
85 status: StatusCode::UNAUTHORIZED,
86 code: "INVALID_TOKEN",
87 message: "Invalid or expired authentication token",
88 })?;
89
90 Ok(AuthenticatedUser {
91 user_id: claims.user_id,
92 username: claims.username,
93 is_admin: claims.is_admin,
94 })
95 }
96}
97
98pub struct AuthRejection {
100 status: StatusCode,
101 code: &'static str,
102 message: &'static str,
103}
104
105impl IntoResponse for AuthRejection {
106 fn into_response(self) -> Response {
107 let body = json!({
108 "error": {
109 "code": self.code,
110 "message": self.message,
111 }
112 });
113 (self.status, Json(body)).into_response()
114 }
115}
116
117pub const API_KEY_PREFIX: &str = "irfl_";
123
124pub const API_KEY_SUFFIX_LEN: usize = 8;
126
127#[derive(Debug, Clone)]
141pub struct ApiKeyAuth {
142 pub key_id: Uuid,
144 pub user_id: Uuid,
146 pub key_name: String,
148 pub scopes: Vec<ApiKeyScope>,
150 pub owner_is_admin: bool,
152}
153
154impl ApiKeyAuth {
155 pub fn has_scope(&self, required: &ApiKeyScope) -> bool {
157 ApiKeyScope::has_permission(&self.scopes, required)
158 }
159}
160
161pub struct ApiKeyRejection {
163 status: StatusCode,
164 code: &'static str,
165 message: &'static str,
166}
167
168impl IntoResponse for ApiKeyRejection {
169 fn into_response(self) -> Response {
170 let body = json!({
171 "error": {
172 "code": self.code,
173 "message": self.message,
174 }
175 });
176 (self.status, Json(body)).into_response()
177 }
178}
179
180impl<S> FromRequestParts<S> for ApiKeyAuth
181where
182 S: Send + Sync,
183 Arc<dyn Store>: FromRef<S>,
184{
185 type Rejection = ApiKeyRejection;
186
187 async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
188 let store = Arc::<dyn Store>::from_ref(state);
189
190 let token = parts
191 .headers
192 .get("authorization")
193 .and_then(|v| v.to_str().ok())
194 .and_then(|v| v.strip_prefix("Bearer "))
195 .ok_or(ApiKeyRejection {
196 status: StatusCode::UNAUTHORIZED,
197 code: "MISSING_TOKEN",
198 message: "No authentication token provided",
199 })?;
200
201 if !token.starts_with(API_KEY_PREFIX) {
202 return Err(ApiKeyRejection {
203 status: StatusCode::UNAUTHORIZED,
204 code: "INVALID_TOKEN",
205 message: "Expected API key (irfl_...) in Authorization header",
206 });
207 }
208
209 let suffix_len = (token.len() - API_KEY_PREFIX.len()).min(API_KEY_SUFFIX_LEN);
210 let prefix = &token[..API_KEY_PREFIX.len() + suffix_len];
211
212 let api_key = store
213 .find_api_key_by_prefix(prefix)
214 .await
215 .map_err(|_| ApiKeyRejection {
216 status: StatusCode::INTERNAL_SERVER_ERROR,
217 code: "INTERNAL_ERROR",
218 message: "Failed to look up API key",
219 })?
220 .ok_or(ApiKeyRejection {
221 status: StatusCode::UNAUTHORIZED,
222 code: "INVALID_TOKEN",
223 message: "Invalid API key",
224 })?;
225
226 if !api_key.is_active {
227 return Err(ApiKeyRejection {
228 status: StatusCode::UNAUTHORIZED,
229 code: "KEY_DISABLED",
230 message: "API key is disabled",
231 });
232 }
233
234 if let Some(expires_at) = api_key.expires_at
235 && expires_at < Utc::now()
236 {
237 return Err(ApiKeyRejection {
238 status: StatusCode::UNAUTHORIZED,
239 code: "KEY_EXPIRED",
240 message: "API key has expired",
241 });
242 }
243
244 let valid = password::verify(token, &api_key.key_hash).map_err(|_| ApiKeyRejection {
245 status: StatusCode::INTERNAL_SERVER_ERROR,
246 code: "INTERNAL_ERROR",
247 message: "Failed to verify API key",
248 })?;
249
250 if !valid {
251 return Err(ApiKeyRejection {
252 status: StatusCode::UNAUTHORIZED,
253 code: "INVALID_TOKEN",
254 message: "Invalid API key",
255 });
256 }
257
258 let _ = store.touch_api_key(api_key.id).await;
259
260 let owner = store
261 .find_user_by_id(api_key.user_id)
262 .await
263 .map_err(|_| ApiKeyRejection {
264 status: StatusCode::INTERNAL_SERVER_ERROR,
265 code: "INTERNAL_ERROR",
266 message: "Failed to look up API key owner",
267 })?;
268 let owner_is_admin = owner.map(|u| u.is_admin).unwrap_or(false);
269
270 Ok(ApiKeyAuth {
271 key_id: api_key.id,
272 user_id: api_key.user_id,
273 key_name: api_key.name,
274 scopes: api_key.scopes,
275 owner_is_admin,
276 })
277 }
278}
279
280#[derive(Debug, Clone)]
299pub struct Authenticated {
300 pub user_id: Uuid,
302 pub method: AuthMethod,
304}
305
306#[derive(Debug, Clone)]
308pub enum AuthMethod {
309 Jwt {
311 username: String,
313 is_admin: bool,
315 },
316 ApiKey {
318 key_id: Uuid,
320 key_name: String,
322 scopes: Vec<ApiKeyScope>,
324 owner_is_admin: bool,
326 },
327}
328
329impl Authenticated {
330 pub fn is_admin(&self) -> bool {
336 match &self.method {
337 AuthMethod::Jwt { is_admin, .. } => *is_admin,
338 AuthMethod::ApiKey { owner_is_admin, .. } => *owner_is_admin,
339 }
340 }
341}
342
343impl<S> FromRequestParts<S> for Authenticated
344where
345 S: Send + Sync,
346 Arc<JwtConfig>: FromRef<S>,
347 Arc<dyn Store>: FromRef<S>,
348{
349 type Rejection = AuthRejection;
350
351 async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
352 let jar = CookieJar::from_headers(&parts.headers);
353 let cookie_token = jar.get(AUTH_COOKIE_NAME).map(|c| c.value().to_string());
354
355 let header_token = parts
356 .headers
357 .get("authorization")
358 .and_then(|v| v.to_str().ok())
359 .and_then(|v| v.strip_prefix("Bearer "))
360 .map(|t| t.to_string());
361
362 if let Some(ref token) = header_token
364 && token.starts_with(API_KEY_PREFIX)
365 {
366 let api_key_auth =
367 ApiKeyAuth::from_request_parts(parts, state)
368 .await
369 .map_err(|_| AuthRejection {
370 status: StatusCode::UNAUTHORIZED,
371 code: "INVALID_TOKEN",
372 message: "Invalid or expired authentication token",
373 })?;
374 return Ok(Authenticated {
375 user_id: api_key_auth.user_id,
376 method: AuthMethod::ApiKey {
377 key_id: api_key_auth.key_id,
378 key_name: api_key_auth.key_name,
379 scopes: api_key_auth.scopes,
380 owner_is_admin: api_key_auth.owner_is_admin,
381 },
382 });
383 }
384
385 let token = cookie_token.or(header_token).ok_or(AuthRejection {
387 status: StatusCode::UNAUTHORIZED,
388 code: "MISSING_TOKEN",
389 message: "No authentication token provided",
390 })?;
391
392 let jwt_config = Arc::<JwtConfig>::from_ref(state);
393 let claims = AccessToken::decode(&token, &jwt_config).map_err(|_| AuthRejection {
394 status: StatusCode::UNAUTHORIZED,
395 code: "INVALID_TOKEN",
396 message: "Invalid or expired authentication token",
397 })?;
398
399 Ok(Authenticated {
400 user_id: claims.user_id,
401 method: AuthMethod::Jwt {
402 username: claims.username,
403 is_admin: claims.is_admin,
404 },
405 })
406 }
407}
408
409#[cfg(test)]
410mod tests {
411 use std::sync::Arc;
412
413 use axum::body::Body;
414 use axum::extract::FromRef;
415 use axum::http::{Request, StatusCode};
416 use axum::routing::get;
417 use axum::{Json, Router};
418 use http_body_util::BodyExt;
419 use ironflow_store::entities::NewUser;
420 use ironflow_store::entities::{ApiKeyScope, NewApiKey};
421 use ironflow_store::memory::InMemoryStore;
422 use ironflow_store::store::Store;
423 use serde_json::Value;
424 use tower::ServiceExt;
425 use uuid::Uuid;
426
427 use crate::jwt::{AccessToken, JwtConfig};
428 use crate::password;
429
430 use super::*;
431
432 #[derive(Clone)]
433 struct TestState {
434 jwt_config: Arc<JwtConfig>,
435 store: Arc<dyn Store>,
436 }
437
438 impl FromRef<TestState> for Arc<JwtConfig> {
439 fn from_ref(state: &TestState) -> Self {
440 state.jwt_config.clone()
441 }
442 }
443
444 impl FromRef<TestState> for Arc<dyn Store> {
445 fn from_ref(state: &TestState) -> Self {
446 state.store.clone()
447 }
448 }
449
450 fn test_jwt_config() -> Arc<JwtConfig> {
451 Arc::new(JwtConfig {
452 secret: "test-secret-key-for-extractor-tests".to_string(),
453 access_token_ttl_secs: 900,
454 refresh_token_ttl_secs: 604800,
455 cookie_domain: None,
456 cookie_secure: false,
457 })
458 }
459
460 fn test_state() -> TestState {
461 TestState {
462 jwt_config: test_jwt_config(),
463 store: Arc::new(InMemoryStore::new()),
464 }
465 }
466
467 async fn response_json(resp: axum::http::Response<Body>) -> Value {
468 let body = resp.into_body().collect().await.unwrap().to_bytes();
469 serde_json::from_slice(&body).unwrap()
470 }
471
472 #[tokio::test]
477 async fn jwt_extractor_from_bearer_header() {
478 let state = test_state();
479 let user_id = Uuid::now_v7();
480 let token = AccessToken::for_user(user_id, "alice", false, &state.jwt_config).unwrap();
481
482 let app = Router::new()
483 .route(
484 "/me",
485 get(|user: AuthenticatedUser| async move {
486 Json(json!({
487 "user_id": user.user_id,
488 "username": user.username,
489 "is_admin": user.is_admin
490 }))
491 }),
492 )
493 .with_state(state);
494
495 let req = Request::builder()
496 .uri("/me")
497 .header("authorization", format!("Bearer {}", token.0))
498 .body(Body::empty())
499 .unwrap();
500
501 let resp = app.oneshot(req).await.unwrap();
502 assert_eq!(resp.status(), StatusCode::OK);
503
504 let json = response_json(resp).await;
505 assert_eq!(json["user_id"], user_id.to_string());
506 assert_eq!(json["username"], "alice");
507 assert_eq!(json["is_admin"], false);
508 }
509
510 #[tokio::test]
511 async fn jwt_extractor_from_cookie() {
512 let state = test_state();
513 let user_id = Uuid::now_v7();
514 let token = AccessToken::for_user(user_id, "bob", true, &state.jwt_config).unwrap();
515
516 let app = Router::new()
517 .route(
518 "/me",
519 get(|user: AuthenticatedUser| async move {
520 Json(json!({ "username": user.username, "is_admin": user.is_admin }))
521 }),
522 )
523 .with_state(state);
524
525 let req = Request::builder()
526 .uri("/me")
527 .header("cookie", format!("{}={}", AUTH_COOKIE_NAME, token.0))
528 .body(Body::empty())
529 .unwrap();
530
531 let resp = app.oneshot(req).await.unwrap();
532 assert_eq!(resp.status(), StatusCode::OK);
533
534 let json = response_json(resp).await;
535 assert_eq!(json["username"], "bob");
536 assert_eq!(json["is_admin"], true);
537 }
538
539 #[tokio::test]
540 async fn jwt_extractor_rejects_missing_token() {
541 let app = Router::new()
542 .route("/me", get(|_user: AuthenticatedUser| async { "ok" }))
543 .with_state(test_state());
544
545 let req = Request::builder().uri("/me").body(Body::empty()).unwrap();
546
547 let resp = app.oneshot(req).await.unwrap();
548 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
549
550 let json = response_json(resp).await;
551 assert_eq!(json["error"]["code"], "MISSING_TOKEN");
552 }
553
554 #[tokio::test]
555 async fn jwt_extractor_rejects_invalid_token() {
556 let app = Router::new()
557 .route("/me", get(|_user: AuthenticatedUser| async { "ok" }))
558 .with_state(test_state());
559
560 let req = Request::builder()
561 .uri("/me")
562 .header("authorization", "Bearer invalid.token.here")
563 .body(Body::empty())
564 .unwrap();
565
566 let resp = app.oneshot(req).await.unwrap();
567 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
568
569 let json = response_json(resp).await;
570 assert_eq!(json["error"]["code"], "INVALID_TOKEN");
571 }
572
573 async fn setup_api_key(store: &Arc<dyn Store>) -> (Uuid, String) {
578 let user = store
579 .create_user(NewUser {
580 email: "key-owner@test.com".to_string(),
581 username: "keyowner".to_string(),
582 password_hash: password::hash("pass123").unwrap(),
583 is_admin: Some(true),
584 })
585 .await
586 .unwrap();
587
588 let raw_key = "irfl_abcdef12rest-of-secret-key";
589 let key_hash = password::hash(raw_key).unwrap();
590 let prefix = &raw_key[..API_KEY_PREFIX.len() + API_KEY_SUFFIX_LEN];
591
592 store
593 .create_api_key(NewApiKey {
594 user_id: user.id,
595 name: "test-key".to_string(),
596 key_hash,
597 key_prefix: prefix.to_string(),
598 scopes: vec![ApiKeyScope::RunsRead, ApiKeyScope::WorkflowsRead],
599 expires_at: None,
600 rate_limit_override: None,
601 })
602 .await
603 .unwrap();
604
605 (user.id, raw_key.to_string())
606 }
607
608 #[tokio::test]
609 async fn api_key_extractor_valid_key() {
610 let state = test_state();
611 let (user_id, raw_key) = setup_api_key(&state.store).await;
612
613 let app = Router::new()
614 .route(
615 "/check",
616 get(|key: ApiKeyAuth| async move {
617 Json(json!({
618 "user_id": key.user_id,
619 "key_name": key.key_name,
620 "scopes": key.scopes,
621 "owner_is_admin": key.owner_is_admin
622 }))
623 }),
624 )
625 .with_state(state);
626
627 let req = Request::builder()
628 .uri("/check")
629 .header("authorization", format!("Bearer {raw_key}"))
630 .body(Body::empty())
631 .unwrap();
632
633 let resp = app.oneshot(req).await.unwrap();
634 assert_eq!(resp.status(), StatusCode::OK);
635
636 let json = response_json(resp).await;
637 assert_eq!(json["user_id"], user_id.to_string());
638 assert_eq!(json["key_name"], "test-key");
639 assert_eq!(json["owner_is_admin"], true);
640 }
641
642 #[tokio::test]
643 async fn api_key_extractor_rejects_missing_header() {
644 let app = Router::new()
645 .route("/check", get(|_key: ApiKeyAuth| async { "ok" }))
646 .with_state(test_state());
647
648 let req = Request::builder()
649 .uri("/check")
650 .body(Body::empty())
651 .unwrap();
652
653 let resp = app.oneshot(req).await.unwrap();
654 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
655
656 let json = response_json(resp).await;
657 assert_eq!(json["error"]["code"], "MISSING_TOKEN");
658 }
659
660 #[tokio::test]
661 async fn api_key_extractor_rejects_non_irfl_token() {
662 let app = Router::new()
663 .route("/check", get(|_key: ApiKeyAuth| async { "ok" }))
664 .with_state(test_state());
665
666 let req = Request::builder()
667 .uri("/check")
668 .header("authorization", "Bearer not-an-api-key")
669 .body(Body::empty())
670 .unwrap();
671
672 let resp = app.oneshot(req).await.unwrap();
673 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
674
675 let json = response_json(resp).await;
676 assert_eq!(json["error"]["code"], "INVALID_TOKEN");
677 }
678
679 #[tokio::test]
680 async fn api_key_extractor_rejects_unknown_key() {
681 let app = Router::new()
682 .route("/check", get(|_key: ApiKeyAuth| async { "ok" }))
683 .with_state(test_state());
684
685 let req = Request::builder()
686 .uri("/check")
687 .header("authorization", "Bearer irfl_unknown1rest-of-key")
688 .body(Body::empty())
689 .unwrap();
690
691 let resp = app.oneshot(req).await.unwrap();
692 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
693
694 let json = response_json(resp).await;
695 assert_eq!(json["error"]["code"], "INVALID_TOKEN");
696 }
697
698 #[test]
703 fn has_scope_returns_true_for_granted_scope() {
704 let auth = ApiKeyAuth {
705 key_id: Uuid::now_v7(),
706 user_id: Uuid::now_v7(),
707 key_name: "k".to_string(),
708 scopes: vec![ApiKeyScope::RunsRead, ApiKeyScope::WorkflowsRead],
709 owner_is_admin: false,
710 };
711 assert!(auth.has_scope(&ApiKeyScope::RunsRead));
712 assert!(auth.has_scope(&ApiKeyScope::WorkflowsRead));
713 }
714
715 #[test]
716 fn has_scope_returns_false_for_missing_scope() {
717 let auth = ApiKeyAuth {
718 key_id: Uuid::now_v7(),
719 user_id: Uuid::now_v7(),
720 key_name: "k".to_string(),
721 scopes: vec![ApiKeyScope::RunsRead],
722 owner_is_admin: false,
723 };
724 assert!(!auth.has_scope(&ApiKeyScope::RunsWrite));
725 assert!(!auth.has_scope(&ApiKeyScope::Admin));
726 }
727
728 #[test]
729 fn has_scope_admin_grants_everything() {
730 let auth = ApiKeyAuth {
731 key_id: Uuid::now_v7(),
732 user_id: Uuid::now_v7(),
733 key_name: "k".to_string(),
734 scopes: vec![ApiKeyScope::Admin],
735 owner_is_admin: true,
736 };
737 assert!(auth.has_scope(&ApiKeyScope::RunsRead));
738 assert!(auth.has_scope(&ApiKeyScope::RunsWrite));
739 assert!(auth.has_scope(&ApiKeyScope::StatsRead));
740 }
741
742 #[tokio::test]
747 async fn authenticated_via_jwt() {
748 let state = test_state();
749 let user_id = Uuid::now_v7();
750 let token = AccessToken::for_user(user_id, "alice", true, &state.jwt_config).unwrap();
751
752 let app = Router::new()
753 .route(
754 "/auth",
755 get(|auth: Authenticated| async move {
756 Json(json!({
757 "user_id": auth.user_id,
758 "is_admin": auth.is_admin(),
759 "method": match auth.method {
760 AuthMethod::Jwt { .. } => "jwt",
761 AuthMethod::ApiKey { .. } => "api_key",
762 }
763 }))
764 }),
765 )
766 .with_state(state);
767
768 let req = Request::builder()
769 .uri("/auth")
770 .header("authorization", format!("Bearer {}", token.0))
771 .body(Body::empty())
772 .unwrap();
773
774 let resp = app.oneshot(req).await.unwrap();
775 assert_eq!(resp.status(), StatusCode::OK);
776
777 let json = response_json(resp).await;
778 assert_eq!(json["user_id"], user_id.to_string());
779 assert_eq!(json["is_admin"], true);
780 assert_eq!(json["method"], "jwt");
781 }
782
783 #[tokio::test]
784 async fn authenticated_via_api_key() {
785 let state = test_state();
786 let (user_id, raw_key) = setup_api_key(&state.store).await;
787
788 let app = Router::new()
789 .route(
790 "/auth",
791 get(|auth: Authenticated| async move {
792 Json(json!({
793 "user_id": auth.user_id,
794 "is_admin": auth.is_admin(),
795 "method": match auth.method {
796 AuthMethod::Jwt { .. } => "jwt",
797 AuthMethod::ApiKey { .. } => "api_key",
798 }
799 }))
800 }),
801 )
802 .with_state(state);
803
804 let req = Request::builder()
805 .uri("/auth")
806 .header("authorization", format!("Bearer {raw_key}"))
807 .body(Body::empty())
808 .unwrap();
809
810 let resp = app.oneshot(req).await.unwrap();
811 assert_eq!(resp.status(), StatusCode::OK);
812
813 let json = response_json(resp).await;
814 assert_eq!(json["user_id"], user_id.to_string());
815 assert_eq!(json["is_admin"], true);
816 assert_eq!(json["method"], "api_key");
817 }
818
819 #[tokio::test]
820 async fn authenticated_rejects_missing_token() {
821 let app = Router::new()
822 .route("/auth", get(|_auth: Authenticated| async { "ok" }))
823 .with_state(test_state());
824
825 let req = Request::builder().uri("/auth").body(Body::empty()).unwrap();
826
827 let resp = app.oneshot(req).await.unwrap();
828 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
829
830 let json = response_json(resp).await;
831 assert_eq!(json["error"]["code"], "MISSING_TOKEN");
832 }
833
834 #[test]
839 fn is_admin_jwt_true() {
840 let auth = Authenticated {
841 user_id: Uuid::now_v7(),
842 method: AuthMethod::Jwt {
843 username: "admin".to_string(),
844 is_admin: true,
845 },
846 };
847 assert!(auth.is_admin());
848 }
849
850 #[test]
851 fn is_admin_jwt_false() {
852 let auth = Authenticated {
853 user_id: Uuid::now_v7(),
854 method: AuthMethod::Jwt {
855 username: "user".to_string(),
856 is_admin: false,
857 },
858 };
859 assert!(!auth.is_admin());
860 }
861
862 #[test]
863 fn is_admin_api_key_true() {
864 let auth = Authenticated {
865 user_id: Uuid::now_v7(),
866 method: AuthMethod::ApiKey {
867 key_id: Uuid::now_v7(),
868 key_name: "k".to_string(),
869 scopes: vec![],
870 owner_is_admin: true,
871 },
872 };
873 assert!(auth.is_admin());
874 }
875
876 #[test]
877 fn is_admin_api_key_false() {
878 let auth = Authenticated {
879 user_id: Uuid::now_v7(),
880 method: AuthMethod::ApiKey {
881 key_id: Uuid::now_v7(),
882 key_name: "k".to_string(),
883 scopes: vec![],
884 owner_is_admin: false,
885 },
886 };
887 assert!(!auth.is_admin());
888 }
889
890 #[tokio::test]
895 async fn auth_rejection_into_response() {
896 let rejection = AuthRejection {
897 status: StatusCode::UNAUTHORIZED,
898 code: "TEST_CODE",
899 message: "test message",
900 };
901 let resp = rejection.into_response();
902 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
903
904 let body = resp.into_body().collect().await.unwrap().to_bytes();
905 let json: Value = serde_json::from_slice(&body).unwrap();
906 assert_eq!(json["error"]["code"], "TEST_CODE");
907 assert_eq!(json["error"]["message"], "test message");
908 }
909
910 #[tokio::test]
911 async fn api_key_rejection_into_response() {
912 let rejection = ApiKeyRejection {
913 status: StatusCode::FORBIDDEN,
914 code: "KEY_DISABLED",
915 message: "API key is disabled",
916 };
917 let resp = rejection.into_response();
918 assert_eq!(resp.status(), StatusCode::FORBIDDEN);
919
920 let body = resp.into_body().collect().await.unwrap().to_bytes();
921 let json: Value = serde_json::from_slice(&body).unwrap();
922 assert_eq!(json["error"]["code"], "KEY_DISABLED");
923 assert_eq!(json["error"]["message"], "API key is disabled");
924 }
925}