Skip to content

Commit

Permalink
feat: 第七章代码(二)
Browse files Browse the repository at this point in the history
  • Loading branch information
fan-tastic-z committed Nov 2, 2024
1 parent a844ae4 commit 6bbf022
Show file tree
Hide file tree
Showing 9 changed files with 188 additions and 219 deletions.
6 changes: 6 additions & 0 deletions src/configuration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,12 @@ pub struct ApplicationSettings {
pub port: u16,
}

impl ApplicationSettings {
pub fn address(&self) -> String {
format!("{}:{}", self.host, self.port)
}
}

#[derive(Deserialize)]
pub struct DatabaseSettings {
pub username: String,
Expand Down
34 changes: 4 additions & 30 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,39 +1,13 @@
use std::sync::Arc;

use sqlx::postgres::PgPoolOptions;
use zero2prod::{
configuration::get_configuration, email_client::EmailClient, startup::run,
configuration::get_configuration,
startup::{run_until_stopped, AppState},
telemetry::init_tracing,
};

#[tokio::main]
async fn main() -> std::io::Result<()> {
init_tracing();

let configuration = get_configuration().expect("Failed to read configuration.");

let connection_pool = Arc::new(
PgPoolOptions::new()
.acquire_timeout(std::time::Duration::from_secs(2))
.connect_lazy_with(configuration.database.with_db()),
);
let address = format!(
"{}:{}",
configuration.application.host, configuration.application.port
);

let sender_email = configuration
.email_client
.sender()
.expect("Invalid sender email address");
let timeout = configuration.email_client.timeout();
let email_client = Arc::new(EmailClient::new(
configuration.email_client.base_url,
sender_email,
configuration.email_client.authorization_token,
timeout,
));

let listener = tokio::net::TcpListener::bind(address).await?;
run(listener, connection_pool, email_client).await
let app_state = AppState::build(&configuration).await;
run_until_stopped(app_state, configuration).await
}
46 changes: 32 additions & 14 deletions src/startup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,12 @@ use axum::{
routing::{get, post},
Router,
};
use sqlx::Connection;
use sqlx::{postgres::PgPoolOptions, Connection};
use sqlx::{Executor, PgConnection, PgPool, Pool, Postgres};
use tokio::net::TcpListener;
use tower_http::trace::TraceLayer;

use crate::{
configuration::DatabaseSettings,
configuration::{DatabaseSettings, Settings},
email_client::EmailClient,
routes::{health, subscribe},
};
Expand All @@ -21,6 +20,32 @@ pub struct AppState {
pub email_client: Arc<EmailClient>,
}

impl AppState {
pub async fn build(configuration: &Settings) -> Self {
let db_pool = Arc::new(
PgPoolOptions::new()
.acquire_timeout(std::time::Duration::from_secs(2))
.connect_lazy_with(configuration.database.with_db()),
);

let sender_email = configuration
.email_client
.sender()
.expect("Invalid sender email address");
let timeout = configuration.email_client.timeout();
let email_client = Arc::new(EmailClient::new(
configuration.email_client.base_url.clone(),
sender_email,
configuration.email_client.authorization_token.clone(),
timeout,
));
Self {
db_pool,
email_client,
}
}
}

pub fn app(state: AppState) -> Router {
Router::new()
.route("/health", get(health))
Expand All @@ -29,23 +54,17 @@ pub fn app(state: AppState) -> Router {
.layer(TraceLayer::new_for_http())
}

pub async fn run(
listener: TcpListener,
db_pool: Arc<Pool<Postgres>>,
email_client: Arc<EmailClient>,
) -> std::io::Result<()> {
let state = AppState {
db_pool,
email_client,
};
pub async fn run_until_stopped(state: AppState, configuration: Settings) -> std::io::Result<()> {
let app = app(state);
let listener = tokio::net::TcpListener::bind(configuration.application.address()).await?;
axum::serve(listener, app).await
}

pub async fn configuration_database(config: &DatabaseSettings) -> PgPool {
pub async fn configuration_database(config: &DatabaseSettings) {
let mut connection = PgConnection::connect_with(&config.without_db())
.await
.expect("Failed to connect to Postgres");

connection
.execute(format!(r#"CREATE DATABASE "{}";"#, config.database_name).as_str())
.await
Expand All @@ -59,5 +78,4 @@ pub async fn configuration_database(config: &DatabaseSettings) -> PgPool {
.run(&db_pool)
.await
.expect("Failed to migrate the database");
db_pool
}
31 changes: 31 additions & 0 deletions tests/api/health_check.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
use axum::{
body::Body,
http::{HeaderValue, Request},
};
use reqwest::header::CONTENT_LENGTH;
use tower::ServiceExt;
use zero2prod::startup::app;

use crate::helpers::spawn_app;

#[tokio::test]
async fn health_check_works() {
let state = spawn_app().await;
let app = app(state);

let response = app
.oneshot(
Request::builder()
.uri("/health")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();

assert!(response.status().is_success());
assert_eq!(
response.headers().get(CONTENT_LENGTH),
Some(&HeaderValue::from_str("0").unwrap())
);
}
19 changes: 19 additions & 0 deletions tests/api/helpers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
use once_cell::sync::Lazy;
use uuid::Uuid;
use zero2prod::{
configuration::get_configuration,
startup::{configuration_database, AppState},
telemetry::init_tracing,
};

static TRACING: Lazy<()> = Lazy::new(|| {
init_tracing();
});

pub async fn spawn_app() -> AppState {
Lazy::force(&TRACING);
let mut configuration = get_configuration().expect("Failed to read configuration.");
configuration.database.database_name = Uuid::new_v4().to_string();
configuration_database(&configuration.database).await;
AppState::build(&configuration).await
}
3 changes: 3 additions & 0 deletions tests/api/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
mod health_check;
mod helpers;
mod subscriptions;
93 changes: 93 additions & 0 deletions tests/api/subscriptions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
use axum::{
body::Body,
http::{self, Request},
Router,
};
use tower::ServiceExt;
use zero2prod::startup::app;

use crate::helpers::spawn_app;

pub async fn post_subscriptions(app: Router, body: &str) -> http::Response<Body> {
app.oneshot(
Request::builder()
.method(http::Method::POST)
.uri("/subscriptions")
.header(
http::header::CONTENT_TYPE,
mime::APPLICATION_WWW_FORM_URLENCODED.as_ref(),
)
.body(Body::new(body.to_string()))
.unwrap(),
)
.await
.unwrap()
}

#[tokio::test]
async fn subscribe_returns_a_200_for_valid_form_data() {
let state = spawn_app().await;
let app = app(state.clone());
let body = "name=fan-tastic.z&email=fantastic.fun.zf@gmail.com";
let response = post_subscriptions(app, body).await;

assert!(response.status().is_success());

#[derive(sqlx::FromRow, Debug, PartialEq, Eq)]
struct Subscription {
name: String,
email: String,
}
let saved: Subscription = sqlx::query_as("SELECT email, name FROM subscriptions")
.fetch_one(state.db_pool.as_ref())
.await
.expect("Failed to fetch saved subscription.");

assert_eq!(saved.email, "fantastic.fun.zf@gmail.com");
assert_eq!(saved.name, "fan-tastic.z");
}

#[tokio::test]
async fn subscribe_returns_a_422_when_data_is_missing() {
let state = spawn_app().await;
let app = app(state);
let test_cases = vec![
("name=fan-tastic.z", "missing the email"),
("email=fantastic.fun.zf@gmail.com", "missing the name"),
("", "missing both name and email"),
];

for (invalid_body, error_message) in test_cases {
let response = post_subscriptions(app.clone(), invalid_body).await;
assert_eq!(
422,
response.status().as_u16(),
"The API did not fail with 400 Bad Request when the payload was {}.",
error_message
);
}
}

#[tokio::test]
async fn subscribe_returns_a_400_when_fields_are_present_but_empty() {
let state = spawn_app().await;
let app = app(state);

let test_cases = vec![
("name=&email=fantastic.fun.zf@gmail.com", "empty name"),
("name=fan-tastic.z&email=", "empty email"),
(
"name=fan-tastic.z&email=definitely-not-an-email",
"invalid email",
),
];
for (body, description) in test_cases {
let response = post_subscriptions(app.clone(), body).await;
assert_eq!(
400,
response.status().as_u16(),
"The API did not return a 200 OK when the payload was {}",
description
);
}
}
Loading

0 comments on commit 6bbf022

Please sign in to comment.