Skip to content

Commit

Permalink
chore: e2e tests
Browse files Browse the repository at this point in the history
  • Loading branch information
EstebanBorai committed Jan 20, 2024
1 parent bcb1df1 commit 9c54609
Show file tree
Hide file tree
Showing 8 changed files with 193 additions and 23 deletions.
1 change: 0 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ reqwest = "0.11.22"
serde = "1.0.192"
serde_json = "1.0.108"
tokio = "1.34.0"
tower = "0.4.13"
tracing = "0.1.40"
tracing-subscriber = "0.3.18"
uuid = { version = "1.6.1", features = ["v4"] }
Expand Down
1 change: 0 additions & 1 deletion crates/server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ dotenv = { workspace = true }
http = { workspace = true }
serde = { workspace = true, features = ["derive"] }
tokio = { workspace = true, features = ["rt", "rt-multi-thread", "macros"] }
tower = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true, features = ["env-filter"] }
url = { workspace = true, features= ["serde"] }
Expand Down
34 changes: 19 additions & 15 deletions crates/server/src/router/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use axum::response::IntoResponse;
use axum::Json;
use axum::Router;
use http::StatusCode;
use serde::Deserialize;
use serde::Serialize;

use commune::error::HttpStatusCode;
Expand All @@ -16,16 +17,16 @@ impl Api {
}
}

#[derive(Debug, Serialize)]
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ApiError {
message: String,
code: &'static str,
pub message: String,
pub code: String,
#[serde(skip)]
status: StatusCode,
pub status: StatusCode,
}

impl ApiError {
pub fn new(message: String, code: &'static str, status: StatusCode) -> Self {
pub fn new(message: String, code: String, status: StatusCode) -> Self {
Self {
message,
code,
Expand All @@ -35,16 +36,16 @@ impl ApiError {

pub fn unauthorized() -> Self {
Self::new(
"You must be authenticated to access this request".to_string(),
"UNAUTHORIZED",
"You must be authenticated to access this resource".to_string(),
"UNAUTHORIZED".to_string(),
StatusCode::UNAUTHORIZED,
)
}

pub fn internal_server_error() -> Self {
Self::new(
"Internal server error".to_string(),
"INTERNAL_SERVER_ERROR",
"INTERNAL_SERVER_ERROR".to_string(),
StatusCode::INTERNAL_SERVER_ERROR,
)
}
Expand All @@ -54,7 +55,7 @@ impl From<commune::error::Error> for ApiError {
fn from(err: commune::error::Error) -> Self {
Self {
message: err.to_string(),
code: err.error_code(),
code: err.error_code().to_string(),
status: err.status_code(),
}
}
Expand All @@ -71,19 +72,22 @@ impl From<anyhow::Error> for ApiError {
fn from(err: anyhow::Error) -> Self {
Self {
message: err.to_string(),
code: "UNKNOWN_ERROR",
code: "UNKNOWN_ERROR".to_string(),
status: StatusCode::INTERNAL_SERVER_ERROR,
}
}
}

impl IntoResponse for ApiError {
fn into_response(self) -> axum::response::Response {
let status = self.status.as_u16();
let mut response = Json(self).into_response();
if let Ok(status) = axum::http::StatusCode::from_u16(self.status.as_u16()) {
let mut response = Json(self).into_response();

*response.status_mut() =
axum::http::StatusCode::from_u16(status).expect("Invalid status code");
response
*response.status_mut() = status;
return response;
}

tracing::error!(status=%self.status, "Failed to convert status code to http::StatusCode");
ApiError::internal_server_error().into_response()
}
}
13 changes: 8 additions & 5 deletions crates/server/src/router/middleware/auth.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use axum::body::Body;
use axum::http::{header::AUTHORIZATION, Request};
use axum::middleware::Next;
use axum::response::Response;
use axum::response::{IntoResponse, Response};

use commune::util::secret::Secret;

Expand All @@ -17,21 +17,24 @@ impl ToString for AccessToken {
}
}

pub async fn auth(mut request: Request<Body>, next: Next) -> Result<Response, ApiError> {
pub async fn auth(mut request: Request<Body>, next: Next) -> Result<Response, Response> {
let access_token = request
.headers()
.get(AUTHORIZATION)
.and_then(|value| value.to_str().ok())
.and_then(|value| value.strip_prefix("Bearer "))
.ok_or(ApiError::unauthorized())?
.ok_or_else(|| {
tracing::warn!("No access token provided");
ApiError::unauthorized().into_response()
})?
.to_owned();

let services = request
.extensions()
.get::<SharedServices>()
.ok_or_else(|| {
tracing::error!("SharedServices not found in request extensions");
ApiError::internal_server_error()
ApiError::internal_server_error().into_response()
})?;

let access_token = Secret::new(access_token);
Expand All @@ -42,7 +45,7 @@ pub async fn auth(mut request: Request<Body>, next: Next) -> Result<Response, Ap
.await
.map_err(|err| {
tracing::error!("Failed to validate token: {}", err);
ApiError::internal_server_error()
ApiError::internal_server_error().into_response()
})?;

request.extensions_mut().insert(user);
Expand Down
1 change: 1 addition & 0 deletions crates/test/src/server/api/v1/account/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
mod login;
mod root;
mod session;
148 changes: 148 additions & 0 deletions crates/test/src/server/api/v1/account/session.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
use commune_server::router::api::v1::account::root::AccountRegisterPayload;
use commune_server::router::api::v1::account::session::AccountSessionResponse;
use commune_server::router::api::ApiError;
use fake::faker::internet::en::{FreeEmail, Password};
use fake::Fake;
use reqwest::StatusCode;
use scraper::Selector;
use uuid::Uuid;

use commune::util::secret::Secret;
use commune_server::router::api::v1::account::login::{AccountLoginPayload, AccountLoginResponse};
use commune_server::router::api::v1::account::verify_code::{
AccountVerifyCodePayload, VerifyCodeResponse,
};
use commune_server::router::api::v1::account::verify_code_email::{
AccountVerifyCodeEmailPayload, VerifyCodeEmailResponse,
};

use crate::tools::http::HttpClient;
use crate::tools::maildev::MailDevClient;

#[tokio::test]
async fn retrieves_session_user_from_token() {
let http_client = HttpClient::new().await;
let session = Uuid::new_v4();
let email: String = FreeEmail().fake();
let verify_code_pld = AccountVerifyCodePayload {
email: email.clone(),
session,
};
let verify_code_res = http_client
.post("/api/v1/account/verify/code")
.json(&verify_code_pld)
.send()
.await;
let verify_code = verify_code_res.json::<VerifyCodeResponse>().await;

assert!(verify_code.sent, "should return true for sent");

let maildev = MailDevClient::new();
let mail = maildev.latest().await.unwrap().unwrap();
let html = mail.html();
let code_sel = Selector::parse("#code").unwrap();
let mut code_el = html.select(&code_sel);
let code = code_el.next().unwrap().inner_html();
let verify_code_email_pld = AccountVerifyCodeEmailPayload {
email: email.clone(),
code: Secret::new(code.clone()),
session,
};

let verify_code_res = http_client
.post("/api/v1/account/verify/code/email")
.json(&verify_code_email_pld)
.send()
.await;
let verify_code_email = verify_code_res.json::<VerifyCodeEmailResponse>().await;

assert!(verify_code_email.valid, "should return true for valid");

let username: String = (10..12).fake();
let username = username.to_ascii_lowercase();
let password: String = Password(14..20).fake();
let request_payload = AccountRegisterPayload {
username: username.clone(),
password: password.clone(),
email: email.clone(),
code,
session,
};
let response = http_client
.post("/api/v1/account")
.json(&request_payload)
.send()
.await;

assert_eq!(
response.status(),
StatusCode::CREATED,
"should return 201 for successful registration"
);

let response = http_client
.post("/api/v1/account/login")
.json(&AccountLoginPayload {
username: username.clone(),
password,
})
.send()
.await;
let response_status = response.status();
let response_payload = response.json::<AccountLoginResponse>().await;

assert_eq!(
response_status,
StatusCode::OK,
"should return 200 for successful login"
);
assert!(!response_payload.access_token.is_empty());

let session_res = http_client
.get("/api/v1/account/session")
.token(response_payload.access_token)
.send()
.await;
let session_res_status = session_res.status();
let session_res_payload = session_res.json::<AccountSessionResponse>().await;

assert_eq!(
session_res_status,
StatusCode::OK,
"should return 200 for successful session"
);
assert!(session_res_payload
.credentials
.username
.starts_with(&format!("@{}", username)));
assert_eq!(
session_res_payload.credentials.email, email,
"should return email"
);
assert!(
session_res_payload.credentials.verified,
"should return verified"
);
assert!(
!session_res_payload.credentials.admin,
"should return admin"
);
}

#[tokio::test]
async fn kicks_users_with_no_token_specified() {
let http_client = HttpClient::new().await;
let session_res = http_client.get("/api/v1/account/session").send().await;
let session_res_status = session_res.status();
let session_res_payload = session_res.json::<ApiError>().await;

assert_eq!(session_res_status, StatusCode::UNAUTHORIZED.as_u16(),);
assert_eq!(
session_res_payload.code, "UNAUTHORIZED",
"should return UNAUTHORIZED"
);
assert_eq!(
session_res_payload.message,
"You must be authenticated to access this resource",
);
}
17 changes: 16 additions & 1 deletion crates/test/src/tools/http.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::net::SocketAddr;

use dotenv::dotenv;
use reqwest::{Client, StatusCode};
use reqwest::{header::AUTHORIZATION, Client, StatusCode};
use tokio::net::TcpListener;

use commune_server::serve;
Expand Down Expand Up @@ -30,6 +30,12 @@ impl HttpClient {
HttpClient { client, addr }
}

pub(crate) fn get(&self, url: &str) -> RequestBuilder {
RequestBuilder {
builder: self.client.get(self.path(url)),
}
}

pub(crate) fn post(&self, url: &str) -> RequestBuilder {
RequestBuilder {
builder: self.client.post(self.path(url)),
Expand All @@ -52,6 +58,15 @@ impl RequestBuilder {
}
}

pub(crate) fn token(mut self, token: impl AsRef<str>) -> Self {
let next = self
.builder
.header(AUTHORIZATION, format!("Bearer {}", token.as_ref()));

self.builder = next;
self
}

pub(crate) fn json<T>(mut self, json: &T) -> Self
where
T: serde::Serialize,
Expand Down
1 change: 1 addition & 0 deletions rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
[toolchain]
channel = "1.75.0"
components = [ "rustfmt", "clippy" ]

0 comments on commit 9c54609

Please sign in to comment.