mirror of
https://github.com/LukeMathWalker/zero-to-production.git
synced 2024-12-18 14:06:37 +00:00
Chapter 3 Part 1 - Patches (#4)
* Adjust content encoding. * Refactor assertion. * Add one more test case * Fix assertions. * Fix route. * Stricter assertions * Few fixes. * Formatting
This commit is contained in:
parent
656ee46c09
commit
24dccda00c
9 changed files with 49 additions and 23 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -444,7 +444,6 @@ dependencies = [
|
||||||
"config 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
"config 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"reqwest 0.10.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
"reqwest 0.10.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"serde 1.0.115 (registry+https://github.com/rust-lang/crates.io-index)",
|
"serde 1.0.115 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"serde_json 1.0.57 (registry+https://github.com/rust-lang/crates.io-index)",
|
|
||||||
"sqlx 0.4.0-beta.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
"sqlx 0.4.0-beta.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"tokio 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)",
|
"tokio 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"uuid 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
"uuid 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
|
|
@ -25,4 +25,3 @@ chrono = "0.4.15"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
reqwest = { version = "0.10.7", features = ["json"] }
|
reqwest = { version = "0.10.7", features = ["json"] }
|
||||||
serde_json = "1.0.57"
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
application_port: 8000
|
||||||
database:
|
database:
|
||||||
host: "localhost"
|
host: "localhost"
|
||||||
port: 5432
|
port: 5432
|
||||||
|
|
|
@ -10,8 +10,6 @@ DB_PASSWORD="${POSTGRES_PASSWORD:=password}"
|
||||||
DB_NAME="${POSTGRES_DB:=newsletter}"
|
DB_NAME="${POSTGRES_DB:=newsletter}"
|
||||||
# Check if a custom port has been set, otherwise default to '5432'
|
# Check if a custom port has been set, otherwise default to '5432'
|
||||||
DB_PORT="${POSTGRES_PORT:=5432}"
|
DB_PORT="${POSTGRES_PORT:=5432}"
|
||||||
# Check if a custom host has been set, otherwise default to 'localhost'
|
|
||||||
DB_HOST="${POSTGRES_HOST:=localhost}"
|
|
||||||
|
|
||||||
# Allow to skip Docker if a dockerized Postgres database is already running
|
# Allow to skip Docker if a dockerized Postgres database is already running
|
||||||
if [[ -z "${SKIP_DOCKER}" ]]
|
if [[ -z "${SKIP_DOCKER}" ]]
|
||||||
|
@ -28,14 +26,14 @@ then
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Keep pinging Postgres until it's ready to accept commands
|
# Keep pinging Postgres until it's ready to accept commands
|
||||||
until PGPASSWORD="${DB_PASSWORD}" psql -h "${DB_HOST}" -U "${DB_USER}" -p "${DB_PORT}" -d "postgres" -c '\q'; do
|
until PGPASSWORD="${DB_PASSWORD}" psql -h "localhost" -U "${DB_USER}" -p "${DB_PORT}" -d "postgres" -c '\q'; do
|
||||||
>&2 echo "Postgres is still unavailable - sleeping"
|
>&2 echo "Postgres is still unavailable - sleeping"
|
||||||
sleep 1
|
sleep 1
|
||||||
done
|
done
|
||||||
|
|
||||||
>&2 echo "Postgres is up and running on port ${DB_PORT} - running migrations now!"
|
>&2 echo "Postgres is up and running on port ${DB_PORT} - running migrations now!"
|
||||||
|
|
||||||
export DATABASE_URL=postgres://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}
|
export DATABASE_URL=postgres://${DB_USER}:${DB_PASSWORD}@localhost:${DB_PORT}/${DB_NAME}
|
||||||
sqlx database create
|
sqlx database create
|
||||||
sqlx migrate run
|
sqlx migrate run
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
pub struct Settings {
|
pub struct Settings {
|
||||||
pub database: DatabaseSettings,
|
pub database: DatabaseSettings,
|
||||||
|
pub application_port: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
|
|
|
@ -11,7 +11,8 @@ async fn main() -> Result<(), anyhow::Error> {
|
||||||
.await
|
.await
|
||||||
.map_err(anyhow::Error::from)
|
.map_err(anyhow::Error::from)
|
||||||
.with_context(|| "Failed to connect to Postgres.")?;
|
.with_context(|| "Failed to connect to Postgres.")?;
|
||||||
let address = TcpListener::bind("127.0.0.1:8000")?;
|
let address = format!("127.0.0.1:{}", configuration.application_port);
|
||||||
run(address, connection_pool)?.await?;
|
let listener = TcpListener::bind(address)?;
|
||||||
|
run(listener, connection_pool)?.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ pub struct SubscribeRequest {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn subscribe(
|
pub async fn subscribe(
|
||||||
payload: web::Json<SubscribeRequest>,
|
payload: web::Form<SubscribeRequest>,
|
||||||
pool: web::Data<PgPool>,
|
pool: web::Data<PgPool>,
|
||||||
) -> Result<HttpResponse, HttpResponse> {
|
) -> Result<HttpResponse, HttpResponse> {
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
|
|
|
@ -14,6 +14,7 @@ pub async fn spawn_app() -> TestApp {
|
||||||
let listener = TcpListener::bind("127.0.0.1:0").expect("Failed to bind random port");
|
let listener = TcpListener::bind("127.0.0.1:0").expect("Failed to bind random port");
|
||||||
// We retrieve the port assigned to us by the OS
|
// We retrieve the port assigned to us by the OS
|
||||||
let port = listener.local_addr().unwrap().port();
|
let port = listener.local_addr().unwrap().port();
|
||||||
|
let address = format!("http://127.0.0.1:{}", port);
|
||||||
|
|
||||||
let mut configuration = get_configuration().expect("Failed to read configuration.");
|
let mut configuration = get_configuration().expect("Failed to read configuration.");
|
||||||
configuration.database.database_name = Uuid::new_v4().to_string();
|
configuration.database.database_name = Uuid::new_v4().to_string();
|
||||||
|
@ -22,9 +23,8 @@ pub async fn spawn_app() -> TestApp {
|
||||||
|
|
||||||
let server = run(listener, connection_pool.clone()).expect("Failed to bind address");
|
let server = run(listener, connection_pool.clone()).expect("Failed to bind address");
|
||||||
let _ = tokio::spawn(server);
|
let _ = tokio::spawn(server);
|
||||||
// We return the application address to the caller!
|
|
||||||
TestApp {
|
TestApp {
|
||||||
address: format!("http://127.0.0.1:{}", port),
|
address,
|
||||||
db_pool: connection_pool,
|
db_pool: connection_pool,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,34 +1,61 @@
|
||||||
use crate::helpers::spawn_app;
|
use crate::helpers::spawn_app;
|
||||||
use serde_json::json;
|
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn subscribe_works() {
|
async fn subscribe_accepts_valid_form_data() {
|
||||||
// Arrange
|
// Arrange
|
||||||
let app = spawn_app().await;
|
let app = spawn_app().await;
|
||||||
let client = reqwest::Client::new();
|
let client = reqwest::Client::new();
|
||||||
let email = "myemail@mydomain.com";
|
let body = "name=le%20guin&email=ursula_le_guin%40gmail.com";
|
||||||
let name = "my name";
|
|
||||||
let payload = json!({
|
|
||||||
"email": email,
|
|
||||||
"name": name
|
|
||||||
});
|
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
let response = client
|
let response = client
|
||||||
.post(&format!("{}/subscriptions", &app.address))
|
.post(&format!("{}/subscriptions", &app.address))
|
||||||
.json(&payload)
|
.header("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
.body(body)
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.expect("Failed to execute request.");
|
.expect("Failed to execute request.");
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
assert!(response.status().is_success());
|
assert_eq!(200, response.status().as_u16());
|
||||||
|
|
||||||
let saved = sqlx::query!("SELECT email, name FROM subscriptions",)
|
let saved = sqlx::query!("SELECT email, name FROM subscriptions",)
|
||||||
.fetch_one(&app.db_pool)
|
.fetch_one(&app.db_pool)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to fetch saved subscription.");
|
.expect("Failed to fetch saved subscription.");
|
||||||
|
|
||||||
assert_eq!(saved.email, email);
|
assert_eq!(saved.email, "ursula_le_guin@gmail.com");
|
||||||
assert_eq!(saved.name, name);
|
assert_eq!(saved.name, "le guin");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn subscribe_returns_a_400_when_data_is_missing() {
|
||||||
|
// Arrange
|
||||||
|
let app = spawn_app().await;
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let test_cases = vec![
|
||||||
|
("name=le%20guin", "missing the email"),
|
||||||
|
("email=ursula_le_guin%40gmail.com", "missing the name"),
|
||||||
|
("", "missing both name and email"),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (invalid_body, error_message) in test_cases {
|
||||||
|
// Act
|
||||||
|
let response = client
|
||||||
|
.post(&format!("{}/subscriptions", &app.address))
|
||||||
|
.header("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
.body(invalid_body)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to execute request.");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assert_eq!(
|
||||||
|
400,
|
||||||
|
response.status().as_u16(),
|
||||||
|
// Additional customised error message on test failure
|
||||||
|
"The API did not fail with 400 Bad Request when the payload was {}.",
|
||||||
|
error_message
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue