The bulk (#11)
Rust Build / Rustfmt (push) Successful in 1m26s
Rust Build / Test Suite (push) Successful in 1m41s
Rust Build / Check (push) Successful in 2m19s
Rust Build / Clippy (push) Successful in 1m36s
textsender_api PR / Rustfmt (pull_request) Successful in 31s
Rust Build / build (push) Successful in 2m11s
textsender_api PR / Clippy (pull_request) Successful in 1m39s
textsender_api PR / Check (pull_request) Successful in 2m30s

Reviewed-on: phoenix/textsender_api#11
This commit was merged in pull request #11.
This commit is contained in:
2026-06-17 13:40:15 -04:00
parent a226adcd0f
commit 98e93b175e
8 changed files with 452 additions and 183 deletions
Generated
+1 -1
View File
@@ -2293,7 +2293,7 @@ dependencies = [
[[package]]
name = "textsender_api"
version = "0.1.6"
version = "0.1.7"
dependencies = [
"axum",
"axum-extra",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "textsender_api"
version = "0.1.6"
version = "0.1.7"
edition = "2024"
rust-version = "1.96"
+114
View File
@@ -22,6 +22,24 @@ pub mod request {
pub id: Option<uuid::Uuid>,
pub user_id: Option<uuid::Uuid>,
}
#[derive(Debug, Deserialize, Serialize, ToSchema)]
pub struct UpdateContactNamesRequest {
pub id: uuid::Uuid,
pub firstname: Option<String>,
pub lastname: Option<String>,
pub nickname: Option<String>,
}
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<UpdateContactNameChange>,
}
#[derive(Debug, Default, Deserialize, Serialize, ToSchema)]
pub struct UpdateContactNameChange {
pub old_firstname: Option<String>,
pub old_lastname: Option<String>,
pub old_nickname: Option<String>,
pub new_firstname: Option<String>,
pub new_lastname: Option<String>,
pub new_nickname: Option<String>,
}
/*
#[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<sqlx::PgPool>,
axum::Json(payload): axum::Json<super::request::UpdateContactNamesRequest>,
) -> (
axum::http::StatusCode,
axum::Json<super::response::UpdateContactNamesResponse>,
) {
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<String> = contact.firstname.clone();
let old_lastname: Option<String> = contact.lastname.clone();
let old_nickname: Option<String> = 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))
}
}
}
+2
View File
@@ -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 {
+7 -1
View File
@@ -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::<axum::body::Body>,
)),
)
.route(
crate::caller::endpoints::UPDATE_CONTACT_NAME,
patch(contact_endpoints::update_contact_names).route_layer(
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
),
)
.layer(cors::configure_cors().await)
}
+200
View File
@@ -0,0 +1,200 @@
use sqlx::Row;
pub async fn insert(
pool: &sqlx::PgPool,
contact: &textsender_models::contact::Contact,
) -> Result<uuid::Uuid, sqlx::Error> {
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<textsender_models::contact::Contact, sqlx::Error> {
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<textsender_models::contact::Contact, sqlx::Error> {
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<Vec<textsender_models::contact::Contact>, 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<textsender_models::contact::Contact> = 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<String>,
lastname: &Option<String>,
nickname: &Option<String>,
) -> 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),
}
}
+1 -180
View File
@@ -1,180 +1 @@
pub mod contact {
use sqlx::Row;
pub async fn insert(
pool: &sqlx::PgPool,
contact: &textsender_models::contact::Contact,
) -> Result<uuid::Uuid, sqlx::Error> {
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<textsender_models::contact::Contact, sqlx::Error> {
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<textsender_models::contact::Contact, sqlx::Error> {
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<Vec<textsender_models::contact::Contact>, 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<textsender_models::contact::Contact> = 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;
+126
View File
@@ -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<axum::response::Response, std::convert::Infallible> {
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<textsender_models::contact::Contact> = 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;
}