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 })
601 .await
602 .unwrap();
603
604 (user.id, raw_key.to_string())
605 }
606
607 #[tokio::test]
608 async fn api_key_extractor_valid_key() {
609 let state = test_state();
610 let (user_id, raw_key) = setup_api_key(&state.store).await;
611
612 let app = Router::new()
613 .route(
614 "/check",
615 get(|key: ApiKeyAuth| async move {
616 Json(json!({
617 "user_id": key.user_id,
618 "key_name": key.key_name,
619 "scopes": key.scopes,
620 "owner_is_admin": key.owner_is_admin
621 }))
622 }),
623 )
624 .with_state(state);
625
626 let req = Request::builder()
627 .uri("/check")
628 .header("authorization", format!("Bearer {raw_key}"))
629 .body(Body::empty())
630 .unwrap();
631
632 let resp = app.oneshot(req).await.unwrap();
633 assert_eq!(resp.status(), StatusCode::OK);
634
635 let json = response_json(resp).await;
636 assert_eq!(json["user_id"], user_id.to_string());
637 assert_eq!(json["key_name"], "test-key");
638 assert_eq!(json["owner_is_admin"], true);
639 }
640
641 #[tokio::test]
642 async fn api_key_extractor_rejects_missing_header() {
643 let app = Router::new()
644 .route("/check", get(|_key: ApiKeyAuth| async { "ok" }))
645 .with_state(test_state());
646
647 let req = Request::builder()
648 .uri("/check")
649 .body(Body::empty())
650 .unwrap();
651
652 let resp = app.oneshot(req).await.unwrap();
653 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
654
655 let json = response_json(resp).await;
656 assert_eq!(json["error"]["code"], "MISSING_TOKEN");
657 }
658
659 #[tokio::test]
660 async fn api_key_extractor_rejects_non_irfl_token() {
661 let app = Router::new()
662 .route("/check", get(|_key: ApiKeyAuth| async { "ok" }))
663 .with_state(test_state());
664
665 let req = Request::builder()
666 .uri("/check")
667 .header("authorization", "Bearer not-an-api-key")
668 .body(Body::empty())
669 .unwrap();
670
671 let resp = app.oneshot(req).await.unwrap();
672 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
673
674 let json = response_json(resp).await;
675 assert_eq!(json["error"]["code"], "INVALID_TOKEN");
676 }
677
678 #[tokio::test]
679 async fn api_key_extractor_rejects_unknown_key() {
680 let app = Router::new()
681 .route("/check", get(|_key: ApiKeyAuth| async { "ok" }))
682 .with_state(test_state());
683
684 let req = Request::builder()
685 .uri("/check")
686 .header("authorization", "Bearer irfl_unknown1rest-of-key")
687 .body(Body::empty())
688 .unwrap();
689
690 let resp = app.oneshot(req).await.unwrap();
691 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
692
693 let json = response_json(resp).await;
694 assert_eq!(json["error"]["code"], "INVALID_TOKEN");
695 }
696
697 #[test]
702 fn has_scope_returns_true_for_granted_scope() {
703 let auth = ApiKeyAuth {
704 key_id: Uuid::now_v7(),
705 user_id: Uuid::now_v7(),
706 key_name: "k".to_string(),
707 scopes: vec![ApiKeyScope::RunsRead, ApiKeyScope::WorkflowsRead],
708 owner_is_admin: false,
709 };
710 assert!(auth.has_scope(&ApiKeyScope::RunsRead));
711 assert!(auth.has_scope(&ApiKeyScope::WorkflowsRead));
712 }
713
714 #[test]
715 fn has_scope_returns_false_for_missing_scope() {
716 let auth = ApiKeyAuth {
717 key_id: Uuid::now_v7(),
718 user_id: Uuid::now_v7(),
719 key_name: "k".to_string(),
720 scopes: vec![ApiKeyScope::RunsRead],
721 owner_is_admin: false,
722 };
723 assert!(!auth.has_scope(&ApiKeyScope::RunsWrite));
724 assert!(!auth.has_scope(&ApiKeyScope::Admin));
725 }
726
727 #[test]
728 fn has_scope_admin_grants_everything() {
729 let auth = ApiKeyAuth {
730 key_id: Uuid::now_v7(),
731 user_id: Uuid::now_v7(),
732 key_name: "k".to_string(),
733 scopes: vec![ApiKeyScope::Admin],
734 owner_is_admin: true,
735 };
736 assert!(auth.has_scope(&ApiKeyScope::RunsRead));
737 assert!(auth.has_scope(&ApiKeyScope::RunsWrite));
738 assert!(auth.has_scope(&ApiKeyScope::StatsRead));
739 }
740
741 #[tokio::test]
746 async fn authenticated_via_jwt() {
747 let state = test_state();
748 let user_id = Uuid::now_v7();
749 let token = AccessToken::for_user(user_id, "alice", true, &state.jwt_config).unwrap();
750
751 let app = Router::new()
752 .route(
753 "/auth",
754 get(|auth: Authenticated| async move {
755 Json(json!({
756 "user_id": auth.user_id,
757 "is_admin": auth.is_admin(),
758 "method": match auth.method {
759 AuthMethod::Jwt { .. } => "jwt",
760 AuthMethod::ApiKey { .. } => "api_key",
761 }
762 }))
763 }),
764 )
765 .with_state(state);
766
767 let req = Request::builder()
768 .uri("/auth")
769 .header("authorization", format!("Bearer {}", token.0))
770 .body(Body::empty())
771 .unwrap();
772
773 let resp = app.oneshot(req).await.unwrap();
774 assert_eq!(resp.status(), StatusCode::OK);
775
776 let json = response_json(resp).await;
777 assert_eq!(json["user_id"], user_id.to_string());
778 assert_eq!(json["is_admin"], true);
779 assert_eq!(json["method"], "jwt");
780 }
781
782 #[tokio::test]
783 async fn authenticated_via_api_key() {
784 let state = test_state();
785 let (user_id, raw_key) = setup_api_key(&state.store).await;
786
787 let app = Router::new()
788 .route(
789 "/auth",
790 get(|auth: Authenticated| async move {
791 Json(json!({
792 "user_id": auth.user_id,
793 "is_admin": auth.is_admin(),
794 "method": match auth.method {
795 AuthMethod::Jwt { .. } => "jwt",
796 AuthMethod::ApiKey { .. } => "api_key",
797 }
798 }))
799 }),
800 )
801 .with_state(state);
802
803 let req = Request::builder()
804 .uri("/auth")
805 .header("authorization", format!("Bearer {raw_key}"))
806 .body(Body::empty())
807 .unwrap();
808
809 let resp = app.oneshot(req).await.unwrap();
810 assert_eq!(resp.status(), StatusCode::OK);
811
812 let json = response_json(resp).await;
813 assert_eq!(json["user_id"], user_id.to_string());
814 assert_eq!(json["is_admin"], true);
815 assert_eq!(json["method"], "api_key");
816 }
817
818 #[tokio::test]
819 async fn authenticated_rejects_missing_token() {
820 let app = Router::new()
821 .route("/auth", get(|_auth: Authenticated| async { "ok" }))
822 .with_state(test_state());
823
824 let req = Request::builder().uri("/auth").body(Body::empty()).unwrap();
825
826 let resp = app.oneshot(req).await.unwrap();
827 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
828
829 let json = response_json(resp).await;
830 assert_eq!(json["error"]["code"], "MISSING_TOKEN");
831 }
832
833 #[test]
838 fn is_admin_jwt_true() {
839 let auth = Authenticated {
840 user_id: Uuid::now_v7(),
841 method: AuthMethod::Jwt {
842 username: "admin".to_string(),
843 is_admin: true,
844 },
845 };
846 assert!(auth.is_admin());
847 }
848
849 #[test]
850 fn is_admin_jwt_false() {
851 let auth = Authenticated {
852 user_id: Uuid::now_v7(),
853 method: AuthMethod::Jwt {
854 username: "user".to_string(),
855 is_admin: false,
856 },
857 };
858 assert!(!auth.is_admin());
859 }
860
861 #[test]
862 fn is_admin_api_key_true() {
863 let auth = Authenticated {
864 user_id: Uuid::now_v7(),
865 method: AuthMethod::ApiKey {
866 key_id: Uuid::now_v7(),
867 key_name: "k".to_string(),
868 scopes: vec![],
869 owner_is_admin: true,
870 },
871 };
872 assert!(auth.is_admin());
873 }
874
875 #[test]
876 fn is_admin_api_key_false() {
877 let auth = Authenticated {
878 user_id: Uuid::now_v7(),
879 method: AuthMethod::ApiKey {
880 key_id: Uuid::now_v7(),
881 key_name: "k".to_string(),
882 scopes: vec![],
883 owner_is_admin: false,
884 },
885 };
886 assert!(!auth.is_admin());
887 }
888
889 #[tokio::test]
894 async fn auth_rejection_into_response() {
895 let rejection = AuthRejection {
896 status: StatusCode::UNAUTHORIZED,
897 code: "TEST_CODE",
898 message: "test message",
899 };
900 let resp = rejection.into_response();
901 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
902
903 let body = resp.into_body().collect().await.unwrap().to_bytes();
904 let json: Value = serde_json::from_slice(&body).unwrap();
905 assert_eq!(json["error"]["code"], "TEST_CODE");
906 assert_eq!(json["error"]["message"], "test message");
907 }
908
909 #[tokio::test]
910 async fn api_key_rejection_into_response() {
911 let rejection = ApiKeyRejection {
912 status: StatusCode::FORBIDDEN,
913 code: "KEY_DISABLED",
914 message: "API key is disabled",
915 };
916 let resp = rejection.into_response();
917 assert_eq!(resp.status(), StatusCode::FORBIDDEN);
918
919 let body = resp.into_body().collect().await.unwrap().to_bytes();
920 let json: Value = serde_json::from_slice(&body).unwrap();
921 assert_eq!(json["error"]["code"], "KEY_DISABLED");
922 assert_eq!(json["error"]["message"], "API key is disabled");
923 }
924}