From dca7facd75c7dc56b2e0ca86b68cc2595a23e306 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 23 Aug 2025 02:26:21 +0000 Subject: [PATCH 1/2] Initial plan From 67964ef7c7f06b3305cf02308e337e9f71eb07bc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 23 Aug 2025 02:39:02 +0000 Subject: [PATCH 2/2] Implement JWT authentication system with login and protected test endpoints Co-authored-by: LuigimonSoft <7293391+LuigimonSoft@users.noreply.github.com> --- Cargo.toml | 8 +- src/controllers/auth_controller.rs | 86 +++++++++++++++++ src/controllers/mod.rs | 3 +- src/errors/mod.rs | 102 +++++++++++--------- src/middleware/auth_middleware.rs | 59 ++++++++++++ src/middleware/mod.rs | 3 +- src/models/auth_model.rs | 22 +++++ src/models/error_response.rs | 13 +-- src/models/mod.rs | 5 +- src/router.rs | 95 +++++++++++-------- src/swagger.rs | 51 +++++----- src/test/auth_integration_test.rs | 146 +++++++++++++++++++++++++++++ src/test/mod.rs | 1 + 13 files changed, 478 insertions(+), 116 deletions(-) create mode 100644 src/controllers/auth_controller.rs create mode 100644 src/middleware/auth_middleware.rs create mode 100644 src/models/auth_model.rs create mode 100644 src/test/auth_integration_test.rs diff --git a/Cargo.toml b/Cargo.toml index 806b3de..06c7e9f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,9 +14,11 @@ lazy_static = "1.4" dotenv = "0.15" utoipa = "4" utoipa-swagger-ui = "7" -utoipauto = "0.1.12" -mockall = "0.13" -once_cell = "1.8" +utoipauto = "0.1.12" +mockall = "0.13" +once_cell = "1.8" +jsonwebtoken = "9.3" +chrono = { version = "0.4", features = ["serde"] } [dev-dependencies] reqwest = { version = "0.12", features = ["json"] } diff --git a/src/controllers/auth_controller.rs b/src/controllers/auth_controller.rs new file mode 100644 index 0000000..5d4affa --- /dev/null +++ b/src/controllers/auth_controller.rs @@ -0,0 +1,86 @@ +use warp::reply::with_status; +use crate::models::auth_model::{LoginRequest, TokenResponse, Claims}; +use crate::middleware::auth_middleware::create_jwt; +use crate::models::error_response::ErrorResponse; + +const VALID_USERNAME: &str = "admin"; +const VALID_PASSWORD: &str = "password123"; + +#[utoipa::path( + post, + path = "/api/v1/auth/login", + tag = "Authentication", + responses( + (status = 200, body = TokenResponse), + (status = 401, description="Unauthorized", body = ErrorResponse), + (status = 400, description="Bad request", body = ErrorResponse), + (status = 500, body = ErrorResponse) + ), + request_body(content = LoginRequest, description = "Login credentials", content_type = "application/json") +)] +pub async fn handle_login( + login_request: LoginRequest, +) -> Result, warp::Rejection> { + // Simple credential validation (in production, use proper authentication) + if login_request.username == VALID_USERNAME && login_request.password == VALID_PASSWORD { + match create_jwt(&login_request.username) { + Ok(token) => { + let response = TokenResponse { + access_token: token, + token_type: "Bearer".to_string(), + expires_in: 86400, // 24 hours in seconds + }; + Ok(Box::new(warp::reply::json(&response))) + } + Err(_) => { + let error_response = ErrorResponse { + instance: Some("/api/v1/auth/login".to_string()), + title: "Internal Server Error".to_string(), + detail: "Failed to generate token".to_string(), + status: 500, + details: None, + }; + Ok(Box::new(with_status( + warp::reply::json(&error_response), + warp::http::StatusCode::INTERNAL_SERVER_ERROR, + ))) + } + } + } else { + let error_response = ErrorResponse { + instance: Some("/api/v1/auth/login".to_string()), + title: "Unauthorized".to_string(), + detail: "Invalid username or password".to_string(), + status: 401, + details: None, + }; + Ok(Box::new(with_status( + warp::reply::json(&error_response), + warp::http::StatusCode::UNAUTHORIZED, + ))) + } +} + +#[utoipa::path( + get, + path = "/api/v1/test/protected", + tag = "Test", + responses( + (status = 200, description = "Success", body = String), + (status = 401, description="Unauthorized", body = ErrorResponse), + (status = 500, body = ErrorResponse) + ), + security( + ("Bearer" = []) + ) +)] +pub async fn handle_protected_test( + claims: Claims, +) -> Result, warp::Rejection> { + let response = serde_json::json!({ + "message": "This is a protected endpoint", + "user": claims.sub, + "timestamp": chrono::Utc::now().to_rfc3339() + }); + Ok(Box::new(warp::reply::json(&response))) +} \ No newline at end of file diff --git a/src/controllers/mod.rs b/src/controllers/mod.rs index 237c3f3..6807b82 100644 --- a/src/controllers/mod.rs +++ b/src/controllers/mod.rs @@ -1,4 +1,5 @@ -pub mod base_controller; +pub mod base_controller; +pub mod auth_controller; use warp::Rejection; use std::sync::Arc; diff --git a/src/errors/mod.rs b/src/errors/mod.rs index ce1cf1f..e02743d 100644 --- a/src/errors/mod.rs +++ b/src/errors/mod.rs @@ -27,48 +27,62 @@ impl Reject for ApiError {} -pub async fn handle_rejection(err: Rejection) -> Result { - let dict = ERROR_CODES.read().unwrap(); - let errors: ErrorResponse = if err.is_not_found() { - ErrorResponse { title: "Not Found".to_string(), status: StatusCode::NOT_FOUND.as_u16(), instance: None, details: None} - } else if let Some(e) = err.find::() { - match e { - ApiError::NotFound => ErrorResponse { title: e.to_string(), status: StatusCode::NOT_FOUND.as_u16(), instance: None, details: None}, - ApiError::BadRequest(details, code) => ErrorResponse { title: "Bad request".to_string(), status: StatusCode::BAD_REQUEST.as_u16(), instance: None, details: Some(vec![ValidationProblem {field: None, message: details.clone(), error_code: code.clone()}])}, - ApiError::InternalServerError => ErrorResponse { title: "Internal server error".to_string(), status: StatusCode::INTERNAL_SERVER_ERROR.as_u16(), instance: None, details: Some(vec![ValidationProblem {field: None, message: e.to_string(), error_code: 0}])}, - ApiError::ErrorCode(code) => { - if let Some(errorcode) = dict.get(code){ - ErrorResponse { title: errorcode.message.clone(), status: errorcode.status_code.as_u16(), instance: None, details: Some(vec![ValidationProblem {field: None, message: errorcode.message.clone(), error_code: errorcode.code}])}//(errorcode.status_code, errorcode.code, errorcode.message.to_string()) - } else { - ErrorResponse { title: "Internal server error".to_string(), status: StatusCode::INTERNAL_SERVER_ERROR.as_u16(), instance: None, details: Some(vec![ValidationProblem {field: None, message: e.to_string(), error_code: 0}])} - } - }, - ApiError::MultipleErrors(errors, field, instance) => { - let mut validation_problems: Option> = Some(vec![]); - let mut status_code: u16 = 0; - if let Some(error_list) = errors { - for code in error_list { - if let Some(errorcode) = dict.get(code){ - status_code = errorcode.status_code.as_u16(); - if let Some(ref mut problems) = validation_problems { - problems.push(ValidationProblem {field: field.clone(), message: errorcode.message.clone(), error_code: errorcode.code}); - } - } - } - } - ErrorResponse { title: e.to_string(), status: status_code, instance: instance.clone(), details: validation_problems} - } - } - } else { - ErrorResponse { title: "Internal server error".to_string(), status: StatusCode::INTERNAL_SERVER_ERROR.as_u16(), instance: None, details: None} - }; - - let json = warp::reply::json(&errors); - let res_status_code: StatusCode; - match StatusCode::from_u16(errors.status) { - Ok(status_code)=> {res_status_code = status_code}, - Err(_)=> {res_status_code = StatusCode::INTERNAL_SERVER_ERROR} - } - - Ok(warp::reply::with_status(json, res_status_code)) +pub async fn handle_rejection(err: Rejection) -> Result { + let dict = ERROR_CODES.read().unwrap(); + + // Handle authentication errors + if let Some(_) = err.find::() { + let error_response = ErrorResponse { + title: "Unauthorized".to_string(), + status: StatusCode::UNAUTHORIZED.as_u16(), + instance: None, + detail: "Authentication failed".to_string(), + details: None, + }; + let json = warp::reply::json(&error_response); + return Ok(warp::reply::with_status(json, StatusCode::UNAUTHORIZED)); + } + + let errors: ErrorResponse = if err.is_not_found() { + ErrorResponse { title: "Not Found".to_string(), status: StatusCode::NOT_FOUND.as_u16(), instance: None, detail: "Resource not found".to_string(), details: None} + } else if let Some(e) = err.find::() { + match e { + ApiError::NotFound => ErrorResponse { title: e.to_string(), status: StatusCode::NOT_FOUND.as_u16(), instance: None, detail: e.to_string(), details: None}, + ApiError::BadRequest(details, code) => ErrorResponse { title: "Bad request".to_string(), status: StatusCode::BAD_REQUEST.as_u16(), instance: None, detail: details.clone(), details: Some(vec![ValidationProblem {field: None, message: details.clone(), error_code: code.clone()}])}, + ApiError::InternalServerError => ErrorResponse { title: "Internal server error".to_string(), status: StatusCode::INTERNAL_SERVER_ERROR.as_u16(), instance: None, detail: e.to_string(), details: Some(vec![ValidationProblem {field: None, message: e.to_string(), error_code: 0}])}, + ApiError::ErrorCode(code) => { + if let Some(errorcode) = dict.get(code){ + ErrorResponse { title: errorcode.message.clone(), status: errorcode.status_code.as_u16(), instance: None, detail: errorcode.message.clone(), details: Some(vec![ValidationProblem {field: None, message: errorcode.message.clone(), error_code: errorcode.code}])}//(errorcode.status_code, errorcode.code, errorcode.message.to_string()) + } else { + ErrorResponse { title: "Internal server error".to_string(), status: StatusCode::INTERNAL_SERVER_ERROR.as_u16(), instance: None, detail: e.to_string(), details: Some(vec![ValidationProblem {field: None, message: e.to_string(), error_code: 0}])} + } + }, + ApiError::MultipleErrors(errors, field, instance) => { + let mut validation_problems: Option> = Some(vec![]); + let mut status_code: u16 = 0; + if let Some(error_list) = errors { + for code in error_list { + if let Some(errorcode) = dict.get(code){ + status_code = errorcode.status_code.as_u16(); + if let Some(ref mut problems) = validation_problems { + problems.push(ValidationProblem {field: field.clone(), message: errorcode.message.clone(), error_code: errorcode.code}); + } + } + } + } + ErrorResponse { title: e.to_string(), status: status_code, instance: instance.clone(), detail: e.to_string(), details: validation_problems} + } + } + } else { + ErrorResponse { title: "Internal server error".to_string(), status: StatusCode::INTERNAL_SERVER_ERROR.as_u16(), instance: None, detail: "Internal server error".to_string(), details: None} + }; + + let json = warp::reply::json(&errors); + let res_status_code: StatusCode; + match StatusCode::from_u16(errors.status) { + Ok(status_code)=> {res_status_code = status_code}, + Err(_)=> {res_status_code = StatusCode::INTERNAL_SERVER_ERROR} + } + + Ok(warp::reply::with_status(json, res_status_code)) } \ No newline at end of file diff --git a/src/middleware/auth_middleware.rs b/src/middleware/auth_middleware.rs new file mode 100644 index 0000000..48e3f40 --- /dev/null +++ b/src/middleware/auth_middleware.rs @@ -0,0 +1,59 @@ +use jsonwebtoken::{decode, DecodingKey, Validation, Algorithm}; +use warp::{Filter, Rejection}; +use crate::models::auth_model::Claims; +use std::convert::Infallible; + +const JWT_SECRET: &[u8] = b"your-secret-key"; // In production, use environment variable + +pub fn with_auth() -> impl Filter + Clone { + warp::header::optional::("authorization") + .and_then(|auth_header: Option| async move { + match auth_header { + Some(header) => { + if let Some(token) = header.strip_prefix("Bearer ") { + match decode_jwt(token) { + Ok(claims) => Ok(claims), + Err(_) => Err(warp::reject::custom(AuthError::InvalidToken)), + } + } else { + Err(warp::reject::custom(AuthError::MissingToken)) + } + } + None => Err(warp::reject::custom(AuthError::MissingToken)), + } + }) +} + +fn decode_jwt(token: &str) -> Result { + let decoding_key = DecodingKey::from_secret(JWT_SECRET); + let validation = Validation::new(Algorithm::HS256); + + decode::(token, &decoding_key, &validation) + .map(|data| data.claims) +} + +pub fn create_jwt(username: &str) -> Result { + use jsonwebtoken::{encode, EncodingKey, Header}; + use chrono::{Utc, Duration}; + + let now = Utc::now(); + let expires_in = Duration::hours(24); + let exp = (now + expires_in).timestamp(); + + let claims = Claims { + sub: username.to_owned(), + exp, + iat: now.timestamp(), + }; + + let encoding_key = EncodingKey::from_secret(JWT_SECRET); + encode(&Header::default(), &claims, &encoding_key) +} + +#[derive(Debug)] +pub enum AuthError { + InvalidToken, + MissingToken, +} + +impl warp::reject::Reject for AuthError {} \ No newline at end of file diff --git a/src/middleware/mod.rs b/src/middleware/mod.rs index bcc8508..ba82252 100644 --- a/src/middleware/mod.rs +++ b/src/middleware/mod.rs @@ -1 +1,2 @@ -pub mod validator; \ No newline at end of file +pub mod validator; +pub mod auth_middleware; \ No newline at end of file diff --git a/src/models/auth_model.rs b/src/models/auth_model.rs new file mode 100644 index 0000000..cba82ec --- /dev/null +++ b/src/models/auth_model.rs @@ -0,0 +1,22 @@ +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +#[derive(Debug, Serialize, Deserialize, ToSchema, Clone)] +pub struct LoginRequest { + pub username: String, + pub password: String, +} + +#[derive(Debug, Serialize, Deserialize, ToSchema)] +pub struct TokenResponse { + pub access_token: String, + pub token_type: String, + pub expires_in: i64, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Claims { + pub sub: String, + pub exp: i64, + pub iat: i64, +} \ No newline at end of file diff --git a/src/models/error_response.rs b/src/models/error_response.rs index ec01924..7823a02 100644 --- a/src/models/error_response.rs +++ b/src/models/error_response.rs @@ -1,11 +1,12 @@ use serde::Serialize; -#[derive(Serialize, utoipa::ToResponse,utoipa::ToSchema)] -pub struct ErrorResponse { - pub title:String, - pub status:u16, - pub instance: Option, - pub details: Option> +#[derive(Serialize, utoipa::ToResponse,utoipa::ToSchema)] +pub struct ErrorResponse { + pub title:String, + pub status:u16, + pub instance: Option, + pub detail: String, + pub details: Option> } #[derive(Debug, Serialize, utoipa::ToResponse, utoipa::ToSchema)] diff --git a/src/models/mod.rs b/src/models/mod.rs index eb2df33..0dffe51 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,2 +1,3 @@ -pub mod messageModel; -pub mod error_response; \ No newline at end of file +pub mod messageModel; +pub mod error_response; +pub mod auth_model; \ No newline at end of file diff --git a/src/router.rs b/src/router.rs index 6a8f241..fe80d71 100644 --- a/src/router.rs +++ b/src/router.rs @@ -1,10 +1,12 @@ -use warp::Filter; -use crate::services::base_service::BaseService; -use crate::config::Config; -use std::sync::Arc; +use warp::Filter; +use crate::services::base_service::BaseService; +use crate::config::Config; +use std::sync::Arc; use std::convert::Infallible; use warp::Rejection; -use crate::controllers::base_controller::{handle_get_messages, handle_create_message, handle_search_messages}; +use crate::controllers::base_controller::{handle_get_messages, handle_create_message, handle_search_messages}; +use crate::controllers::auth_controller::{handle_login, handle_protected_test}; +use crate::middleware::auth_middleware::with_auth; pub struct Router { service: Arc, @@ -21,41 +23,60 @@ impl Router { pub fn routes(&self) -> impl Filter + Clone { - let service = self.service.clone(); - let api_base = self.config.api_base.trim_matches('/').to_string(); - let api_segments: Vec = api_base.split('/').map(|s| s.to_string()).collect(); - let api_path_complete: String = api_base.clone() + ("/messages"); - - let mut api_path = warp::path(api_segments[0].clone()).boxed(); - for segment in &api_segments[1..] { - api_path = api_path.and(warp::path(segment.clone())).boxed(); - } - - let get_messages = warp::get() - .and(api_path.clone()) - .and(warp::path("messages")) - .and(warp::path::end()) - .and(with_service(Arc::clone(&service))) - .and_then(handle_get_messages); - - let add_message = warp::post() - .and(api_path.clone()) - .and(warp::path("messages")) - .and(warp::path::end()) - .and(crate::validators::base_validator::validate_create_message(Some(api_path_complete.clone()))) - .and(with_service(Arc::clone(&service))) - .and_then(handle_create_message); - - let search_messages = warp::get() - .and(api_path.clone()) - .and(warp::path("messages")) - .and(warp::path::param::()) - .and(with_service(Arc::clone(&service))) - .and_then(handle_search_messages); - + let service = self.service.clone(); + let api_base = self.config.api_base.trim_matches('/').to_string(); + let api_segments: Vec = api_base.split('/').map(|s| s.to_string()).collect(); + let api_path_complete: String = api_base.clone() + ("/messages"); + + let mut api_path = warp::path(api_segments[0].clone()).boxed(); + for segment in &api_segments[1..] { + api_path = api_path.and(warp::path(segment.clone())).boxed(); + } + + let get_messages = warp::get() + .and(api_path.clone()) + .and(warp::path("messages")) + .and(warp::path::end()) + .and(with_service(Arc::clone(&service))) + .and_then(handle_get_messages); + + let add_message = warp::post() + .and(api_path.clone()) + .and(warp::path("messages")) + .and(warp::path::end()) + .and(crate::validators::base_validator::validate_create_message(Some(api_path_complete.clone()))) + .and(with_service(Arc::clone(&service))) + .and_then(handle_create_message); + + let search_messages = warp::get() + .and(api_path.clone()) + .and(warp::path("messages")) + .and(warp::path::param::()) + .and(with_service(Arc::clone(&service))) + .and_then(handle_search_messages); + + // Authentication routes + let login = warp::post() + .and(api_path.clone()) + .and(warp::path("auth")) + .and(warp::path("login")) + .and(warp::path::end()) + .and(warp::body::json()) + .and_then(handle_login); + + let protected_test = warp::get() + .and(api_path.clone()) + .and(warp::path("test")) + .and(warp::path("protected")) + .and(warp::path::end()) + .and(with_auth()) + .and_then(handle_protected_test); + get_messages .or(add_message) .or(search_messages) + .or(login) + .or(protected_test) } } diff --git a/src/swagger.rs b/src/swagger.rs index 4218418..956039a 100644 --- a/src/swagger.rs +++ b/src/swagger.rs @@ -1,7 +1,8 @@ -use utoipa::OpenApi; -use crate::models::messageModel::{CreateMessageModelDto, MessageResponseDto}; -use crate::models::error_response::{ErrorResponse, ValidationProblem}; -use utoipauto::utoipauto; +use utoipa::OpenApi; +use utoipa::openapi::security::{ApiKey, ApiKeyValue, SecurityScheme}; +use crate::models::messageModel::{CreateMessageModelDto, MessageResponseDto}; +use crate::models::error_response::{ErrorResponse, ValidationProblem}; +use crate::models::auth_model::{LoginRequest, TokenResponse}; use warp::{ http::Uri, @@ -12,24 +13,30 @@ use std::str::FromStr; use std::sync::Arc; use utoipa_swagger_ui::Config; -#[utoipauto( - paths = "./src/controllers" -)] -#[derive(OpenApi)] -#[openapi( - info( - title = "Rust Base Backend API ", - version = "1.0.0", - description = "This is a simple Rust Base Backend API", - ), - components( - schemas( - CreateMessageModelDto, - MessageResponseDto, - ErrorResponse, - ValidationProblem - ) - ) +#[derive(OpenApi)] +#[openapi( + paths( + crate::controllers::base_controller::handle_get_messages, + crate::controllers::base_controller::handle_create_message, + crate::controllers::base_controller::handle_search_messages, + crate::controllers::auth_controller::handle_login, + crate::controllers::auth_controller::handle_protected_test + ), + info( + title = "Rust Base Backend API ", + version = "1.0.0", + description = "This is a simple Rust Base Backend API", + ), + components( + schemas( + CreateMessageModelDto, + MessageResponseDto, + ErrorResponse, + ValidationProblem, + LoginRequest, + TokenResponse + ) + ) )] pub struct ApiDoc; diff --git a/src/test/auth_integration_test.rs b/src/test/auth_integration_test.rs new file mode 100644 index 0000000..53b3874 --- /dev/null +++ b/src/test/auth_integration_test.rs @@ -0,0 +1,146 @@ +use crate::config; +use crate::server::run_server; +use dotenv::dotenv; +use serde_json::Value; +use std::sync::Arc; +use tokio::sync::oneshot; +use tokio::time::{sleep, Duration}; + +async fn spawn_server() -> (oneshot::Sender<()>, String) { + std::env::set_var("PORT", "0"); + let (shutdown, base) = run_server().await; + // give the server time to start + sleep(Duration::from_millis(100)).await; + (shutdown, base) +} + +fn build_address(base: &str, endpoint: &str) -> String { + dotenv().ok(); + let config = Arc::new(config::Config::from_env()); + format!("{}/{}/{}", base, config.api_base, endpoint) +} + +#[tokio::test] +async fn test_login_valid_credentials() { + let (shutdown, base) = spawn_server().await; + let address = build_address(&base, "auth/login"); + let client = reqwest::Client::new(); + + let response = client + .post(address) + .json(&serde_json::json!({ + "username": "admin", + "password": "password123" + })) + .send() + .await + .unwrap(); + + assert_eq!(response.status(), 200); + + let body: Value = response.json().await.unwrap(); + assert!(body["access_token"].is_string()); + assert_eq!(body["token_type"], "Bearer"); + assert_eq!(body["expires_in"], 86400); + + let _ = shutdown.send(()); +} + +#[tokio::test] +async fn test_login_invalid_credentials() { + let (shutdown, base) = spawn_server().await; + let address = build_address(&base, "auth/login"); + let client = reqwest::Client::new(); + + let response = client + .post(address) + .json(&serde_json::json!({ + "username": "admin", + "password": "wrongpassword" + })) + .send() + .await + .unwrap(); + + assert_eq!(response.status(), 401); + + let body: Value = response.json().await.unwrap(); + assert_eq!(body["title"], "Unauthorized"); + assert_eq!(body["detail"], "Invalid username or password"); + + let _ = shutdown.send(()); +} + +#[tokio::test] +async fn test_protected_endpoint_with_valid_token() { + let (shutdown, base) = spawn_server().await; + let client = reqwest::Client::new(); + + // First, get a token + let login_address = build_address(&base, "auth/login"); + let login_response = client + .post(login_address) + .json(&serde_json::json!({ + "username": "admin", + "password": "password123" + })) + .send() + .await + .unwrap(); + + let login_body: Value = login_response.json().await.unwrap(); + let token = login_body["access_token"].as_str().unwrap(); + + // Then, access the protected endpoint + let protected_address = build_address(&base, "test/protected"); + let protected_response = client + .get(protected_address) + .header("Authorization", format!("Bearer {}", token)) + .send() + .await + .unwrap(); + + assert_eq!(protected_response.status(), 200); + + let body: Value = protected_response.json().await.unwrap(); + assert_eq!(body["message"], "This is a protected endpoint"); + assert_eq!(body["user"], "admin"); + assert!(body["timestamp"].is_string()); + + let _ = shutdown.send(()); +} + +#[tokio::test] +async fn test_protected_endpoint_without_token() { + let (shutdown, base) = spawn_server().await; + let client = reqwest::Client::new(); + + let protected_address = build_address(&base, "test/protected"); + let response = client + .get(protected_address) + .send() + .await + .unwrap(); + + assert_eq!(response.status(), 401); + + let _ = shutdown.send(()); +} + +#[tokio::test] +async fn test_protected_endpoint_with_invalid_token() { + let (shutdown, base) = spawn_server().await; + let client = reqwest::Client::new(); + + let protected_address = build_address(&base, "test/protected"); + let response = client + .get(protected_address) + .header("Authorization", "Bearer invalid-token") + .send() + .await + .unwrap(); + + assert_eq!(response.status(), 401); + + let _ = shutdown.send(()); +} \ No newline at end of file diff --git a/src/test/mod.rs b/src/test/mod.rs index d6db76e..2e9d191 100644 --- a/src/test/mod.rs +++ b/src/test/mod.rs @@ -3,3 +3,4 @@ pub mod base_service_test; pub mod base_controller_test; pub mod integration_test; pub mod static_files_test; +pub mod auth_integration_test;