Skip to content

Commit

Permalink
added hard and soft delete for servers
Browse files Browse the repository at this point in the history
  • Loading branch information
alexpitsikoulis committed Apr 25, 2024
1 parent b2dc653 commit e7eb3d7
Show file tree
Hide file tree
Showing 10 changed files with 304 additions and 6 deletions.
84 changes: 84 additions & 0 deletions src/handlers/server/delete.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
use actix_web::{
web::{Data, Path},
HttpResponse,
};
use chrono::Utc;
use sqlx::PgPool;
use uuid::Uuid;

use crate::storage::{get_server_by_id, hard_delete_server, soft_delete_server};

#[tracing::instrument(
name = "Soft Deleting Server",
skip(server_id, db_pool),
fields(
id = %server_id,
)
)]
pub async fn soft_delete(server_id: Path<Uuid>, db_pool: Data<PgPool>) -> HttpResponse {
let now = Utc::now();
let id = server_id.into_inner();

match get_server_by_id(&db_pool, id).await {
Ok(server) => {
if server.deleted_at().is_some() {
let err = format!("server {} has already been soft deleted", id);
tracing::error!(err);
HttpResponse::BadRequest().body(err)
} else {
match soft_delete_server(&db_pool, id, now).await {
Ok(_) => {
tracing::info!("server {} successfully soft deleted", id);
HttpResponse::Ok().finish()
}
Err(e) => {
let err = format!("failed to soft delete server {}: {}", id, e);
tracing::error!(err);
HttpResponse::InternalServerError().finish()
}
}
}
}
Err(e) => match e {
sqlx::Error::RowNotFound => {
let err = format!("server {} not found", id);
tracing::error!(err);
HttpResponse::NotFound().body(err)
}
e => {
let err = format!("failed to soft delete server {}: {}", id, e);
tracing::error!(err);
HttpResponse::InternalServerError().body(err)
}
},
}
}

#[tracing::instrument(
name = "Hard Deleting Server",
skip(server_id, db_pool),
fields(
id = %server_id,
)
)]
pub async fn hard_delete(server_id: Path<Uuid>, db_pool: Data<PgPool>) -> HttpResponse {
let id = server_id.into_inner();
match hard_delete_server(&db_pool, id).await {
Ok(_) => {
tracing::info!("server {} successfully hard deleted", id);
HttpResponse::Ok().finish()
}
Err(e) => match e {
sqlx::Error::RowNotFound => {
let err = format!("server {} not found", id);
tracing::error!(err);
HttpResponse::NotFound().body(err)
}
e => {
let err = format!("failed to hard delete server {}: {}", id, e);
tracing::error!(err);
HttpResponse::InternalServerError().body(err)
}
},
}
}
2 changes: 2 additions & 0 deletions src/handlers/server/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
mod create;
mod delete;
mod update;

pub use create::*;
pub use delete::*;
pub use update::*;

pub const BASE_PATH: &str = "/servers";
7 changes: 6 additions & 1 deletion src/startup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,12 @@ impl App {
.service(
scope(server::BASE_PATH)
.route("", post().to(server::create))
.service(scope("/{server_id}").route("", put().to(server::update))),
.service(
scope("/{server_id}")
.route("", put().to(server::update))
.route("", delete().to(server::soft_delete))
.route("/hard", delete().to(server::hard_delete)),
),
)
.service(
scope("/ws")
Expand Down
43 changes: 43 additions & 0 deletions src/storage/server.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::domain::server::Server;
use chrono::{DateTime, Utc};
use sqlx::{postgres::PgQueryResult, query, query_as, Error, PgPool};
use uuid::Uuid;

Expand Down Expand Up @@ -86,3 +87,45 @@ pub async fn get_many_servers_by_owner_id(
.fetch_all(db_pool)
.await
}

#[tracing::instrument(
name = "Soft Deleting Server in Database",
skip(server_id, deleted_at, db_pool),
fields(
server_id = %server_id,
deleted_at = %deleted_at,
)
)]
pub async fn soft_delete_server(
db_pool: &PgPool,
server_id: Uuid,
deleted_at: DateTime<Utc>,
) -> Result<PgQueryResult, Error> {
query(
r#"
UPDATE servers SET deleted_at = $1 WHERE id = $2;
"#,
)
.bind(deleted_at)
.bind(server_id)
.execute(db_pool)
.await
}

#[tracing::instrument(
name = "Hard Deleting Server in Database",
skip(server_id, db_pool),
fields(
server_id = %server_id,
)
)]
pub async fn hard_delete_server(db_pool: &PgPool, server_id: Uuid) -> Result<PgQueryResult, Error> {
query(
r#"
DELETE FROM servers WHERE id = $1;
"#,
)
.bind(server_id)
.execute(db_pool)
.await
}
5 changes: 4 additions & 1 deletion tests/api/server/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,10 @@ async fn test_create_server_success() {
let id = Uuid::parse_str(&response.text().await.expect("response body was empty"))
.expect("response was not a valid UUID");

let server = app.database.get_server_by_id(id).await;
let server = match app.database.get_server_by_id(id).await {
Ok(server) => server,
Err(e) => panic!("failed to retrieve server {} from database: {}", id, e),
};

assert_eq!(body, server)
}
Expand Down
153 changes: 153 additions & 0 deletions tests/api/server/delete.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
use crate::utils::{
app::TestApp,
http_client::{ContentType, Header, Path},
};
use chrono::{Days, Utc};
use claim::{assert_err, assert_none, assert_some};
use muttr_server::handlers::server::BASE_PATH;
use uuid::Uuid;

#[actix::test]
async fn test_soft_delete_success() {
let mut app = TestApp::spawn().await;

let user = app
.database
.insert_user("[email protected]", "test.user", true)
.await;
let mut server = app.database.insert_server(user.id()).await;

assert_none!(
server.deleted_at(),
"Server not initialized with None value for deleted_at",
);

let now = Utc::now();
let response = app
.client
.request(
Path::DELETE(format!("{}/{}", BASE_PATH, server.id())),
&[Header::ContentType(ContentType::Json)],
None::<String>,
)
.await;

assert_eq!(
200,
response.status(),
"The API did not return 200 on valid server soft delete",
);

server = match app.database.get_server_by_id(server.id()).await {
Ok(server) => server,
Err(e) => panic!(
"failed to retrieve server {} from database: {}",
server.id(),
e
),
};
let deleted_at = assert_some!(server.deleted_at(), "Server deleted_at is None");
let day = Days::new(1);
assert!(
deleted_at > now.checked_sub_days(day).unwrap()
&& deleted_at < now.checked_add_days(day).unwrap(),
"Server's deleted_at time was incorrect",
);
}

#[actix::test]
async fn test_soft_delete_failure() {
let mut app = TestApp::spawn().await;

let id = Uuid::new_v4();
let mut response = app
.client
.request(
Path::DELETE(format!("{}/{}", BASE_PATH, id)),
&[Header::ContentType(ContentType::Json)],
None::<String>,
)
.await;

assert_eq!(
404,
response.status(),
"The APII did not return 404 when trying to soft delete a non-existant server",
);

let user = app
.database
.insert_user("[email protected]", "test.user", true)
.await;
let server = app.database.insert_server(user.id()).await;

response = app
.client
.request(
Path::DELETE(format!("{}/{}", BASE_PATH, server.id())),
&[Header::ContentType(ContentType::Json)],
None::<String>,
)
.await;

assert_eq!(
200,
response.status(),
"Unexpectedly failed to soft delete test server: {}",
response.text().await.unwrap_or_default(),
);

response = app
.client
.request(
Path::DELETE(format!("{}/{}", BASE_PATH, server.id())),
&[Header::ContentType(ContentType::Json)],
None::<String>,
)
.await;

assert_eq!(
400,
response.status(),
"The API did not return 400 when trying to soft delete a server that has already been soft deleted",
);

assert_eq!(
format!("server {} has already been soft deleted", server.id()),
response.text().await.unwrap_or_default(),
"The response body did not match expected text"
);
}

#[actix::test]
async fn test_hard_delete_success() {
let mut app = TestApp::spawn().await;

let user = app
.database
.insert_user("[email protected]", "test.user", true)
.await;
let server = app.database.insert_server(user.id()).await;

let response = app
.client
.request(
Path::DELETE(format!("{}/{}/hard", BASE_PATH, server.id())),
&[Header::ContentType(ContentType::Json)],
None::<String>,
)
.await;

assert_eq!(
200,
response.status(),
"The API did not return 200 on valid server hard delete: {}",
response.text().await.unwrap_or_default(),
);

assert_err!(
app.database.get_server_by_id(server.id()).await,
"Server {} still exists in the database",
server.id(),
);
}
1 change: 1 addition & 0 deletions tests/api/server/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
mod create;
mod delete;
mod update;
9 changes: 8 additions & 1 deletion tests/api/server/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,14 @@ async fn test_update_server_success() {
error_case,
);

server = app.database.get_server_by_id(server.id()).await;
server = match app.database.get_server_by_id(server.id()).await {
Ok(server) => server,
Err(e) => panic!(
"failed to retrieve server {} from database: {}",
server.id(),
e
),
};

assert_eq!(
body.name(),
Expand Down
2 changes: 1 addition & 1 deletion tests/api/user/delete.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use crate::utils::{
app::TestApp,
http_client::{ContentType, Header, Path},
};
use chrono::{DateTime, Days, Utc};
use chrono::{Days, Utc};
use claim::{assert_err, assert_none, assert_ok, assert_some};
use fake::{faker::internet::en::SafeEmail, Fake};
use muttr_server::{domain::user, handlers::user::BASE_PATH};
Expand Down
4 changes: 2 additions & 2 deletions tests/utils/db/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ impl TestDB {
}
}

pub async fn get_server_by_id(&mut self, id: Uuid) -> Server {
get_server_by_id(&self.db_pool, id).await.unwrap()
pub async fn get_server_by_id(&mut self, id: Uuid) -> Result<Server, sqlx::Error> {
get_server_by_id(&self.db_pool, id).await
}
}

0 comments on commit e7eb3d7

Please sign in to comment.