Skip to content
This repository was archived by the owner on Feb 21, 2026. It is now read-only.

Commit 76aca2d

Browse files
committed
Refactor
1 parent f5b5798 commit 76aca2d

7 files changed

Lines changed: 229 additions & 220 deletions

File tree

src/main.rs

Lines changed: 5 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,16 @@
1-
use sqlx::postgres::PgPoolOptions;
2-
use std::net::TcpListener;
31
use zero2prod::config::get_config;
4-
use zero2prod::email_client::EmailClient;
5-
use zero2prod::startup::run;
2+
use zero2prod::startup::App;
63
use zero2prod::telemetry::{get_subscriber, init_subscriber};
74

85
#[tokio::main]
9-
async fn main() -> std::io::Result<()> {
6+
async fn main() -> Result<(), std::io::Error> {
107
let subscriber = get_subscriber("zero2prod".into(), "info".into(), std::io::stdout);
118
init_subscriber(subscriber);
129

1310
let cfg = get_config().expect("failed to read config");
14-
let sender = cfg
15-
.email_client
16-
.sender()
17-
.expect("invalid sender email address");
18-
let timeout = cfg.email_client.timeout();
1911

20-
let listener = TcpListener::bind(cfg.service.host).expect("failed to bind random port");
12+
let app = App::build(cfg).await?;
13+
app.run_until_stopped().await?;
2114

22-
let database = PgPoolOptions::new()
23-
.max_connections(5)
24-
.connect_with(cfg.database.with_db())
25-
.await
26-
.expect("failed to connect to Postgres");
27-
28-
let email_client = EmailClient::new(
29-
cfg.email_client.base_url,
30-
sender,
31-
cfg.email_client.auth_token,
32-
timeout,
33-
);
34-
35-
run(listener, database, email_client)?.await
15+
Ok(())
3616
}

src/startup.rs

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,63 @@
11
use crate::{
2+
config::Settings,
23
email_client::EmailClient,
34
routes::{healthz, subscribe},
45
};
5-
use actix_web::{App, HttpServer, dev::Server, web};
6-
use sqlx::PgPool;
6+
use actix_web::{HttpServer, dev::Server, web};
7+
use sqlx::{PgPool, postgres::PgPoolOptions};
78
use std::{net::TcpListener, time::Duration};
89
use tracing_actix_web::TracingLogger;
910

10-
pub fn run(
11+
pub struct App {
12+
port: u16,
13+
server: Server,
14+
}
15+
16+
impl App {
17+
pub async fn build(cfg: Settings) -> Result<Self, std::io::Error> {
18+
let pg_pool = PgPoolOptions::new()
19+
.max_connections(5)
20+
.connect_with(cfg.database.with_db())
21+
.await
22+
.expect("failed to connect to Postgres");
23+
24+
let sender = cfg
25+
.email_client
26+
.sender()
27+
.expect("invalid sender email address");
28+
let timeout = cfg.email_client.timeout();
29+
let email_client = EmailClient::new(
30+
cfg.email_client.base_url,
31+
sender,
32+
cfg.email_client.auth_token,
33+
timeout,
34+
);
35+
36+
let listener = TcpListener::bind(cfg.service.host).expect("failed to bind port");
37+
let port = listener.local_addr().unwrap().port();
38+
let server = run(listener, pg_pool, email_client)?;
39+
40+
Ok(Self { port, server })
41+
}
42+
43+
pub fn port(&self) -> u16 {
44+
self.port
45+
}
46+
47+
pub async fn run_until_stopped(self) -> Result<(), std::io::Error> {
48+
self.server.await
49+
}
50+
}
51+
52+
fn run(
1153
listener: TcpListener,
12-
database: PgPool,
54+
pg_pool: PgPool,
1355
email_client: EmailClient,
1456
) -> std::io::Result<Server> {
15-
let database = web::Data::new(database);
57+
let database = web::Data::new(pg_pool);
1658

1759
let srv = HttpServer::new(move || {
18-
App::new()
60+
actix_web::App::new()
1961
.wrap(TracingLogger::default())
2062
.route("/healthz", web::get().to(healthz))
2163
.route("/subscriptions", web::post().to(subscribe))

tests/api/healthz.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
use crate::helpers::spawn_app;
2+
3+
#[tokio::test]
4+
async fn healthz_works() {
5+
let app = spawn_app().await;
6+
let client = reqwest::Client::new();
7+
8+
let resp = client
9+
.get(&format!("{}/healthz", &app.host))
10+
.send()
11+
.await
12+
.expect("failed to execute request");
13+
14+
assert!(resp.status().is_success());
15+
assert_eq!(Some(0), resp.content_length());
16+
}

tests/api/helpers.rs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
use once_cell::sync::Lazy;
2+
use sqlx::{Connection, Executor, PgConnection, PgPool};
3+
use uuid::Uuid;
4+
use zero2prod::{
5+
config::{Database, get_config},
6+
startup::App,
7+
telemetry::{get_subscriber, init_subscriber},
8+
};
9+
10+
static TRACING: Lazy<()> = Lazy::new(|| {
11+
if std::env::var("TEST_LOG").is_ok() {
12+
let subscriber = get_subscriber("test".into(), "debug".into(), std::io::stdout);
13+
init_subscriber(subscriber);
14+
} else {
15+
let subscriber = get_subscriber("test".into(), "debug".into(), std::io::sink);
16+
init_subscriber(subscriber);
17+
};
18+
});
19+
20+
pub struct TestApp {
21+
pub host: String,
22+
pub pg_pool: PgPool,
23+
}
24+
25+
pub async fn spawn_app() -> TestApp {
26+
Lazy::force(&TRACING);
27+
28+
let mut cfg = get_config().expect("failed to read config");
29+
30+
cfg.database.database_name = Uuid::now_v7().to_string();
31+
let pg_pool = configure_database(&cfg.database).await;
32+
33+
cfg.service.host = "127.0.0.1:0".to_string();
34+
let app = App::build(cfg).await.expect("failed to build app");
35+
let host = format!("http://127.0.0.1:{}", app.port());
36+
let _ = tokio::spawn(app.run_until_stopped());
37+
38+
TestApp { host, pg_pool }
39+
}
40+
41+
async fn configure_database(cfg: &Database) -> PgPool {
42+
let mut conn = PgConnection::connect_with(&cfg.without_db())
43+
.await
44+
.expect("failed to connect to Postgres");
45+
conn.execute(format!(r#"create database "{}""#, cfg.database_name).as_str())
46+
.await
47+
.expect("failed to create database");
48+
49+
let pool = PgPool::connect_with(cfg.with_db())
50+
.await
51+
.expect("failed to connect to Postgres");
52+
sqlx::migrate!()
53+
.run(&pool)
54+
.await
55+
.expect("failed to migrate the database");
56+
57+
pool
58+
}

tests/api/main.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
mod healthz;
2+
mod helpers;
3+
mod subscriptions;

tests/api/subscriptions.rs

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
use reqwest::StatusCode;
2+
use serde::Serialize;
3+
4+
use crate::helpers::spawn_app;
5+
6+
#[derive(Serialize)]
7+
struct Subscription {
8+
name: Option<String>,
9+
email: Option<String>,
10+
}
11+
12+
#[tokio::test]
13+
async fn subscribe_returns_a_200_for_valid_form_data() {
14+
let app = spawn_app().await;
15+
let client = reqwest::Client::new();
16+
17+
let sub = Subscription {
18+
name: Some("vla".to_string()),
19+
email: Some("vla@example.com".to_string()),
20+
};
21+
22+
let resp = client
23+
.post(&format!("{}/subscriptions", &app.host))
24+
.form(&sub)
25+
.send()
26+
.await
27+
.expect("failed to execute request");
28+
29+
assert_eq!(StatusCode::OK, resp.status());
30+
31+
let saved = sqlx::query!("select email, name from subscriptions")
32+
.fetch_one(&app.pg_pool)
33+
.await
34+
.expect("failed to fetch saved subscription");
35+
36+
assert_eq!(saved.name, sub.name);
37+
assert_eq!(saved.email, sub.email);
38+
}
39+
40+
#[tokio::test]
41+
async fn subscribe_returns_a_400() {
42+
let app = spawn_app().await;
43+
let client = reqwest::Client::new();
44+
let test_cases = vec![
45+
(
46+
"Missing email",
47+
Subscription {
48+
name: Some("vla".to_string()),
49+
email: None,
50+
},
51+
),
52+
(
53+
"Missing name",
54+
Subscription {
55+
name: None,
56+
email: Some("vla@example.com".to_string()),
57+
},
58+
),
59+
(
60+
"Missing whole data",
61+
Subscription {
62+
name: None,
63+
email: None,
64+
},
65+
),
66+
(
67+
"Empty email",
68+
Subscription {
69+
name: Some("vla".to_string()),
70+
email: Some("".to_string()),
71+
},
72+
),
73+
(
74+
"Empty name",
75+
Subscription {
76+
name: Some("".to_string()),
77+
email: Some("vla@example.com".to_string()),
78+
},
79+
),
80+
(
81+
"Invalid email",
82+
Subscription {
83+
name: Some("vla".to_string()),
84+
email: Some("vla-example.com".to_string()),
85+
},
86+
),
87+
];
88+
89+
for (name, input) in test_cases {
90+
let resp = client
91+
.post(&format!("{}/subscriptions", &app.host))
92+
.form(&input)
93+
.send()
94+
.await
95+
.expect("failed to execute request");
96+
97+
assert_eq!(StatusCode::BAD_REQUEST, resp.status(), "case name {}", name);
98+
}
99+
}

0 commit comments

Comments
 (0)