Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion src/http/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pub type Result<T, E = Error> = std::result::Result<T, E>;

mod escrow;
mod health_check;
pub mod newsletter;
mod project;
mod support_ticket;
mod transaction;
Expand Down Expand Up @@ -49,10 +50,11 @@ pub fn api_router(app_state: AppState) -> Router {

Router::new()
.merge(health_check::router())
.merge(transaction::router())
.merge(project::router())
.merge(transaction::router())
.merge(support_ticket::router())
.merge(escrow::router())
.merge(newsletter::router())
.layer(trace_layer)
.layer(request_id_layer)
.layer(propagate_request_id_layer)
Expand Down
10 changes: 10 additions & 0 deletions src/http/newsletter/domain.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
use garde::Validate;
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
pub struct NewsletterSubscriber {
#[garde(email)]
pub email: String,
#[garde(length(min = 2, max = 255))]
pub name: String,
}
9 changes: 9 additions & 0 deletions src/http/newsletter/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
pub mod domain;
pub mod subscribe;

use crate::AppState;
use axum::Router;

pub fn router() -> Router<AppState> {
Router::new().nest("/newsletter", subscribe::router())
}
43 changes: 43 additions & 0 deletions src/http/newsletter/subscribe.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
use axum::{
Json,
extract::State,
http::StatusCode,
response::IntoResponse,
routing::{Router, post},
};
use garde::Validate;

use crate::{AppState, Result, http::newsletter::domain::NewsletterSubscriber};

pub fn router() -> Router<AppState> {
Router::new().route("/subscribe", post(subscribe_handler))
}

#[tracing::instrument(
name = "Subscribe to newsletter",
skip(state, req),
fields(
subscriber_email = %req.email,
subscriber_name = %req.name
)
)]
pub async fn subscribe_handler(
state: State<AppState>,
Json(req): Json<NewsletterSubscriber>,
) -> Result<impl IntoResponse> {
req.validate()?;

sqlx::query!(
r#"
INSERT INTO newsletter_subscribers (email, name)
VALUES ($1, $2)
RETURNING id
"#,
req.email,
req.name
)
.fetch_one(&state.db.pool)
.await?;

Ok(StatusCode::CREATED)
}
1 change: 1 addition & 0 deletions tests/api/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ mod create_project;
mod escrow;
mod health_check;
mod helpers;
mod newsletter;
mod projects;
mod support_tickets;
mod transaction;
67 changes: 67 additions & 0 deletions tests/api/newsletter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
use crate::helpers::TestApp;
use axum::{
body::Body,
http::{Request, StatusCode},
};
use fortichain_server::http::newsletter::domain::NewsletterSubscriber;
use serde_json::json;

#[tokio::test]
async fn subscribe_returns_a_200_for_valid_form_data() {
// Arrange
let app = TestApp::new().await;

let body = json!({
"email": "test@example.com",
"name": "Test"
});
// Act
let req = Request::post("/newsletter/subscribe")
.header("Content-Type", "application/json")
.body(Body::from(body.to_string()))
.unwrap();

let response = app.request(req).await;

// Assert
assert_eq!(response.status(), StatusCode::CREATED);

let saved = sqlx::query_as!(
NewsletterSubscriber,
"SELECT email, name FROM newsletter_subscribers",
)
.fetch_one(&app.db.pool)
.await
.expect("Failed to fetch saved subscriber.");

assert_eq!(saved.email, "test@example.com");
assert_eq!(saved.name, "Test");
}

#[tokio::test]
async fn subscribe_returns_a_400_when_data_is_missing() {
// Arrange
let app = TestApp::new().await;
let test_cases = vec![
(json!({"name": "Test"}), "missing the email"),
(json!({"email": "test@example.com"}), "missing the name"),
(json!({}), "missing both name and email"),
];

for (invalid_body, error_message) in test_cases {
// Act
let req = Request::post("/newsletter/subscribe")
.header("Content-Type", "application/json")
.body(Body::from(invalid_body.to_string()))
.unwrap();
let response = app.request(req).await;

// Assert
assert_eq!(
response.status(),
StatusCode::UNPROCESSABLE_ENTITY,
"The API did not fail with 422 Unprocessable Entity when the payload was {}.",
error_message
);
}
}