diff --git a/Cargo.lock b/Cargo.lock index d9a4ff5..dc3eaa9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2293,7 +2293,7 @@ dependencies = [ [[package]] name = "textsender_api" -version = "0.1.6" +version = "0.1.7" dependencies = [ "axum", "axum-extra", diff --git a/Cargo.toml b/Cargo.toml index 9d3c478..ccf8ec5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "textsender_api" -version = "0.1.6" +version = "0.1.7" edition = "2024" rust-version = "1.96" diff --git a/src/caller/contact.rs b/src/caller/contact.rs index d1d296d..3b07568 100644 --- a/src/caller/contact.rs +++ b/src/caller/contact.rs @@ -22,6 +22,24 @@ pub mod request { pub id: Option, pub user_id: Option, } + + #[derive(Debug, Deserialize, Serialize, ToSchema)] + pub struct UpdateContactNamesRequest { + pub id: uuid::Uuid, + pub firstname: Option, + pub lastname: Option, + pub nickname: Option, + } + + impl UpdateContactNamesRequest { + pub fn is_valid(&self) -> bool { + if self.id.is_nil() { + false + } else { + self.firstname.is_some() || self.lastname.is_some() || self.nickname.is_some() + } + } + } } pub mod response { @@ -36,6 +54,22 @@ pub mod response { pub use AddContactResponse as GetContactResponse; + #[derive(Debug, Default, Deserialize, Serialize, ToSchema)] + pub struct UpdateContactNamesResponse { + pub message: String, + pub data: Vec, + } + + #[derive(Debug, Default, Deserialize, Serialize, ToSchema)] + pub struct UpdateContactNameChange { + pub old_firstname: Option, + pub old_lastname: Option, + pub old_nickname: Option, + pub new_firstname: Option, + pub new_lastname: Option, + pub new_nickname: Option, + } + /* #[derive(Debug, Default, Deserialize, Serialize, ToSchema)] pub struct GetContactResponse { @@ -195,4 +229,84 @@ pub mod endpoint { (axum::http::StatusCode::OK, axum::Json(response)) } + + /// Endpoint to update Contact + #[utoipa::path( + patch, + path = super::super::endpoints::UPDATE_CONTACT_NAME, + request_body( + content = super::request::UpdateContactNamesRequest, + description = "Data needed to update Contact names", + content_type = "application/json" + ), + responses( + (status = 201, description = "Contact created", body = super::response::UpdateContactNamesResponse), + (status = 400, description = "Error", body = super::response::UpdateContactNamesResponse), + (status = 500, description = "Error creating Contact", body = super::response::UpdateContactNamesResponse) + ) +)] + pub async fn update_contact_names( + axum::Extension(pool): axum::Extension, + axum::Json(payload): axum::Json, + ) -> ( + axum::http::StatusCode, + axum::Json, + ) { + let mut response = super::response::UpdateContactNamesResponse::default(); + + if payload.is_valid() { + match contact_repo::get(&pool, &payload.id).await { + Ok(contact) => { + let old_firstname: Option = contact.firstname.clone(); + let old_lastname: Option = contact.lastname.clone(); + let old_nickname: Option = contact.nickname.clone(); + let new_firstname = payload.firstname; + let new_lastname = payload.lastname; + let new_nickname = payload.nickname; + match contact_repo::update_names( + &pool, + &payload.id, + &new_firstname, + &new_lastname, + &new_nickname, + ) + .await + { + Ok(()) => { + response.message = String::from(super::super::response::SUCCESSFUL); + let data = super::response::UpdateContactNameChange { + old_firstname, + old_lastname, + old_nickname, + new_firstname, + new_lastname, + new_nickname, + }; + response.data = vec![data]; + (axum::http::StatusCode::OK, axum::Json(response)) + } + Err(err) => { + eprintln!("Error: {err:?}"); + response.message = String::from("Something went wrong"); + ( + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + axum::Json(response), + ) + } + } + } + Err(err) => { + eprintln!("Error: {err:?}"); + response.message = String::from("Something went wrong"); + ( + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + axum::Json(response), + ) + } + } + } else { + response.message = String::from("Request body is not valid"); + (axum::http::StatusCode::BAD_REQUEST, axum::Json(response)) + } + } } diff --git a/src/caller/mod.rs b/src/caller/mod.rs index d0911be..17bc20a 100644 --- a/src/caller/mod.rs +++ b/src/caller/mod.rs @@ -7,6 +7,8 @@ pub mod endpoints { pub const ADD_CONTACT: &str = "/api/v1/contact/new"; /// Constant for getting Contact endpoint pub const GET_CONTACT: &str = "/api/v1/contact"; + /// Constant for updating names of a Contact endpoint + pub const UPDATE_CONTACT_NAME: &str = "/api/v1/contact/update"; } pub mod response { diff --git a/src/config/mod.rs b/src/config/mod.rs index ebded9a..105fb1c 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -10,7 +10,7 @@ pub mod host { pub mod init { use std::time::Duration; - use axum::routing::{get, post}; + use axum::routing::{get, patch, post}; use tower_http::timeout::TimeoutLayer; use utoipa::OpenApi; @@ -95,6 +95,12 @@ pub mod init { crate::auth::auth::, )), ) + .route( + crate::caller::endpoints::UPDATE_CONTACT_NAME, + patch(contact_endpoints::update_contact_names).route_layer( + axum::middleware::from_fn(crate::auth::auth::), + ), + ) .layer(cors::configure_cors().await) } diff --git a/src/repo/contact.rs b/src/repo/contact.rs new file mode 100644 index 0000000..5902aed --- /dev/null +++ b/src/repo/contact.rs @@ -0,0 +1,200 @@ +use sqlx::Row; + +pub async fn insert( + pool: &sqlx::PgPool, + contact: &textsender_models::contact::Contact, +) -> Result { + match sqlx::query( + r#" + INSERT INTO "contacts" (firstname, lastname, nickname, phone_number, user_id) + VALUES($1, $2, $3, $4, $5) RETURNING id; + "#, + ) + .bind(&contact.firstname) + .bind(&contact.lastname) + .bind(&contact.nickname) + .bind(&contact.phone_number) + .bind(contact.user_id) + .fetch_one(pool) + .await + { + Ok(row) => { + let id: uuid::Uuid = row.try_get("id")?; + Ok(id) + } + Err(_) => Err(sqlx::Error::RowNotFound), + } +} + +pub async fn get( + pool: &sqlx::PgPool, + id: &uuid::Uuid, +) -> Result { + match sqlx::query( + r#" + SELECT id, firstname, lastname, nickname, phone_number, user_id FROM "contacts" + WHERE + id = $1; + "#, + ) + .bind(id) + .fetch_one(pool) + .await + { + Ok(row) => { + let id: uuid::Uuid = row.try_get("id")?; + + let phone_number: String = row.try_get("phone_number")?; + let user_id: uuid::Uuid = row.try_get("user_id")?; + Ok(textsender_models::contact::Contact { + id: Some(id), + firstname: match row.try_get("firstname") { + Ok(val) => val, + Err(err) => { + eprintln!("Error: {err:?}"); + None + } + }, + lastname: match row.try_get("lastname") { + Ok(val) => val, + Err(err) => { + eprintln!("Error: {err:?}"); + None + } + }, + nickname: match row.try_get("nickname") { + Ok(val) => val, + Err(err) => { + eprintln!("Error: nickname has no value for contacts record {err:?}"); + None + } + }, + phone_number, + user_id: Some(user_id), + }) + } + Err(_) => Err(sqlx::Error::RowNotFound), + } +} + +pub async fn get_with_user_id_and_phone( + pool: &sqlx::PgPool, + user_id: &uuid::Uuid, + phone_number: &str, +) -> Result { + match sqlx::query( + r#" + SELECT id, firstname, lastname, nickname, phone_number, user_id FROM "contacts" + WHERE + phone_number = $1 AND user_id = $2; + "#, + ) + .bind(phone_number) + .bind(user_id) + .fetch_one(pool) + .await + { + Ok(row) => { + println!("In here"); + let id: uuid::Uuid = row.try_get("id")?; + let firstname: String = row.try_get("firstname")?; + let lastname: String = row.try_get("lastname")?; + let nickname: String = row.try_get("nickname")?; + let phone_number: String = row.try_get("phone_number")?; + let user_id: uuid::Uuid = row.try_get("user_id")?; + println!("Found"); + Ok(textsender_models::contact::Contact { + id: Some(id), + firstname: Some(firstname), + lastname: Some(lastname), + nickname: Some(nickname), + phone_number, + user_id: Some(user_id), + }) + } + Err(err) => match err { + sqlx::Error::RowNotFound => Err(err), + _ => Err(sqlx::Error::WorkerCrashed), + }, + } +} + +pub async fn get_with_user_id( + pool: &sqlx::PgPool, + user_id: &uuid::Uuid, +) -> Result, sqlx::Error> { + match sqlx::query( + r#" + SELECT id, firstname, lastname, nickname, phone_number, user_id FROM "contacts" + WHERE + user_id = $1; + "#, + ) + .bind(user_id) + .fetch_all(pool) + .await + { + Ok(rows) => { + let mut contacts: Vec = Vec::new(); + for row in rows { + let id: uuid::Uuid = row.try_get("id")?; + + let phone_number: String = row.try_get("phone_number")?; + let user_id: uuid::Uuid = row.try_get("user_id")?; + let contact = textsender_models::contact::Contact { + id: Some(id), + firstname: match row.try_get("firstname") { + Ok(val) => val, + Err(err) => { + eprintln!("Error: {err:?}"); + None + } + }, + lastname: match row.try_get("lastname") { + Ok(val) => val, + Err(err) => { + eprintln!("Error: {err:?}"); + None + } + }, + nickname: match row.try_get("nickname") { + Ok(val) => val, + Err(err) => { + eprintln!("Error: nickname has no value for contacts record {err:?}"); + None + } + }, + phone_number, + user_id: Some(user_id), + }; + contacts.push(contact); + } + Ok(contacts) + } + Err(_) => Err(sqlx::Error::RowNotFound), + } +} + +pub async fn update_names( + pool: &sqlx::PgPool, + id: &uuid::Uuid, + firstname: &Option, + lastname: &Option, + nickname: &Option, +) -> Result<(), sqlx::Error> { + match sqlx::query( + r#" + UPDATE "contacts" SET firstname = $1, lastname = $2, nickname = $3 WHERE id = $4; + "#, + ) + .bind(firstname) + .bind(lastname) + .bind(nickname) + .bind(id) + .execute(pool) + .await + { + Ok(_row) => Ok(()), + Err(_) => Err(sqlx::Error::RowNotFound), + } +} diff --git a/src/repo/mod.rs b/src/repo/mod.rs index 6bda712..40d6353 100644 --- a/src/repo/mod.rs +++ b/src/repo/mod.rs @@ -1,180 +1 @@ -pub mod contact { - use sqlx::Row; - - pub async fn insert( - pool: &sqlx::PgPool, - contact: &textsender_models::contact::Contact, - ) -> Result { - match sqlx::query( - r#" - INSERT INTO "contacts" (firstname, lastname, nickname, phone_number, user_id) - VALUES($1, $2, $3, $4, $5) RETURNING id; - "#, - ) - .bind(&contact.firstname) - .bind(&contact.lastname) - .bind(&contact.nickname) - .bind(&contact.phone_number) - .bind(contact.user_id) - .fetch_one(pool) - .await - { - Ok(row) => { - let id: uuid::Uuid = row.try_get("id")?; - Ok(id) - } - Err(_) => Err(sqlx::Error::RowNotFound), - } - } - - pub async fn get( - pool: &sqlx::PgPool, - id: &uuid::Uuid, - ) -> Result { - match sqlx::query( - r#" - SELECT id, firstname, lastname, nickname, phone_number, user_id FROM "contacts" - WHERE - id = $1; - "#, - ) - .bind(id) - .fetch_one(pool) - .await - { - Ok(row) => { - let id: uuid::Uuid = row.try_get("id")?; - - let phone_number: String = row.try_get("phone_number")?; - let user_id: uuid::Uuid = row.try_get("user_id")?; - Ok(textsender_models::contact::Contact { - id: Some(id), - firstname: match row.try_get("firstname") { - Ok(val) => val, - Err(err) => { - eprintln!("Error: {err:?}"); - None - } - }, - lastname: match row.try_get("lastname") { - Ok(val) => val, - Err(err) => { - eprintln!("Error: {err:?}"); - None - } - }, - nickname: match row.try_get("nickname") { - Ok(val) => val, - Err(err) => { - eprintln!("Error: nickname has no value for contacts record {err:?}"); - None - } - }, - phone_number, - user_id: Some(user_id), - }) - } - Err(_) => Err(sqlx::Error::RowNotFound), - } - } - - pub async fn get_with_user_id_and_phone( - pool: &sqlx::PgPool, - user_id: &uuid::Uuid, - phone_number: &str, - ) -> Result { - match sqlx::query( - r#" - SELECT id, firstname, lastname, nickname, phone_number, user_id FROM "contacts" - WHERE - phone_number = $1 AND user_id = $2; - "#, - ) - .bind(phone_number) - .bind(user_id) - .fetch_one(pool) - .await - { - Ok(row) => { - println!("In here"); - let id: uuid::Uuid = row.try_get("id")?; - let firstname: String = row.try_get("firstname")?; - let lastname: String = row.try_get("lastname")?; - let nickname: String = row.try_get("nickname")?; - let phone_number: String = row.try_get("phone_number")?; - let user_id: uuid::Uuid = row.try_get("user_id")?; - println!("Found"); - Ok(textsender_models::contact::Contact { - id: Some(id), - firstname: Some(firstname), - lastname: Some(lastname), - nickname: Some(nickname), - phone_number, - user_id: Some(user_id), - }) - } - Err(err) => match err { - sqlx::Error::RowNotFound => Err(err), - _ => Err(sqlx::Error::WorkerCrashed), - }, - } - } - - pub async fn get_with_user_id( - pool: &sqlx::PgPool, - user_id: &uuid::Uuid, - ) -> Result, sqlx::Error> { - match sqlx::query( - r#" - SELECT id, firstname, lastname, nickname, phone_number, user_id FROM "contacts" - WHERE - user_id = $1; - "#, - ) - .bind(user_id) - .fetch_all(pool) - .await - { - Ok(rows) => { - let mut contacts: Vec = Vec::new(); - for row in rows { - let id: uuid::Uuid = row.try_get("id")?; - - let phone_number: String = row.try_get("phone_number")?; - let user_id: uuid::Uuid = row.try_get("user_id")?; - let contact = textsender_models::contact::Contact { - id: Some(id), - firstname: match row.try_get("firstname") { - Ok(val) => val, - Err(err) => { - eprintln!("Error: {err:?}"); - None - } - }, - lastname: match row.try_get("lastname") { - Ok(val) => val, - Err(err) => { - eprintln!("Error: {err:?}"); - None - } - }, - nickname: match row.try_get("nickname") { - Ok(val) => val, - Err(err) => { - eprintln!( - "Error: nickname has no value for contacts record {err:?}" - ); - None - } - }, - phone_number, - user_id: Some(user_id), - }; - contacts.push(contact); - } - Ok(contacts) - } - Err(_) => Err(sqlx::Error::RowNotFound), - } - } -} +pub mod contact; diff --git a/tests/test.rs b/tests/test.rs index 23592f9..801fc62 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -212,11 +212,46 @@ mod request { app.clone().oneshot(req).await } + + pub async fn update_contact_names( + app: &axum::Router, + id: &uuid::Uuid, + firstname: Option<&str>, + lastname: Option<&str>, + nickname: Option<&str>, + ) -> Result { + let payload = serde_json::json!({ + "firstname": firstname, + "lastname": lastname, + "nickname": nickname, + "phone_number": super::TEST_CONTACT_PHONE_NUMBER, + "id": id + }); + + let req = axum::http::Request::builder() + .method(axum::http::Method::PATCH) + .uri(textsender_api::caller::endpoints::UPDATE_CONTACT_NAME) + .header(axum::http::header::CONTENT_TYPE, "application/json") + .header( + axum::http::header::AUTHORIZATION, + super::bearer_auth().await, + ) + .body(axum::body::Body::from(payload.to_string())) + .unwrap(); + app.clone().oneshot(req).await + } } /// Test contact phone number pub const TEST_CONTACT_PHONE_NUMBER: &str = "+10123456789"; +/// Test contact updated firstname +pub const TEST_CONTACT_UPDATED_FIRSTNAME: &str = "Johnny"; +/// Test contact updated lastname +pub const TEST_CONTACT_UPDATED_LASTNAME: &str = "CASH"; +/// Test contact updated nickname +pub const TEST_CONTACT_UPDATED_NICKNAME: &str = "The Man in Black"; + #[tokio::test] async fn test_create_contact() { let tm_pool = db_mgr::get_pool().await.unwrap(); @@ -307,3 +342,94 @@ async fn test_get_contact() { let _ = db_mgr::drop_database(&tm_pool, &db_name).await; } + +#[tokio::test] +async fn test_update_contact_names() { + let tm_pool = db_mgr::get_pool().await.unwrap(); + let db_name = db_mgr::generate_db_name().await; + + match db_mgr::create_database(&tm_pool, &db_name).await { + Ok(_) => { + println!("Success"); + } + Err(err) => { + assert!(false, "Error: {:?}", err); + } + } + + let pool = db_mgr::connect_to_db(&db_name).await.unwrap(); + db::migrations(&pool).await; + + let app = init::app(pool).await; + + let mut opt_contact: Option = None; + + // Send request + match request::create_contact(&app).await { + Ok(response) => { + let resp = util::get_resp_data::< + textsender_api::caller::contact::response::AddContactResponse, + >(response) + .await; + assert_eq!(false, resp.data.is_empty(), "Should not be empty"); + let contact = &resp.data[0]; + assert_eq!(true, contact.id.is_some(), "Missing Contact Id"); + let id = contact.id.unwrap(); + match request::get_contact(&app, &id).await { + Ok(response) => { + let resp = util::get_resp_data::< + textsender_api::caller::contact::response::GetContactResponse, + >(response) + .await; + assert_eq!( + false, + resp.data.is_empty(), + "At least Contact record should have been returned" + ); + opt_contact = Some(resp.data[0].clone()) + } + Err(err) => { + assert!(false, "Error: {err:?}"); + } + } + } + Err(err) => { + assert!(false, "Error: {:?}", err); + } + }; + + match opt_contact { + Some(contact) => { + let new_firstname = Some(TEST_CONTACT_UPDATED_FIRSTNAME); + let new_lastname = Some(TEST_CONTACT_UPDATED_LASTNAME); + let new_nickname = Some(TEST_CONTACT_UPDATED_NICKNAME); + + let contact_id = contact.id.unwrap(); + match request::update_contact_names( + &app, + &contact_id, + new_firstname, + new_lastname, + new_nickname, + ) + .await + { + Ok(response) => { + let resp = util::get_resp_data::< + textsender_api::caller::contact::response::UpdateContactNamesResponse, + >(response) + .await; + assert_eq!(false, resp.data.is_empty(), "Error updating Contact names"); + } + Err(err) => { + assert!(false, "Error: {err:?}"); + } + } + } + None => { + assert!(false, "Contact not found"); + } + } + + let _ = db_mgr::drop_database(&tm_pool, &db_name).await; +}