ee6e255b2e
Email validation is still primitive but checks if the email could theoretically exist (name@domain.com). Separation of Subscriber type into a module as well as actions that cane be performed on it. Updates to the subscribe functionality to align with new test cases that have been made.
176 lines
5.6 KiB
Rust
176 lines
5.6 KiB
Rust
use std::net::TcpListener;
|
|
use sqlx::{Connection, Executor, PgConnection, PgPool};
|
|
use uuid::Uuid;
|
|
use once_cell::sync::Lazy;
|
|
|
|
use mail_app::startup::run;
|
|
use mail_app::configuration::{get_configuration, DatabaseSettings};
|
|
use mail_app::telemetry::{get_subscriber, init_subscriber};
|
|
|
|
pub struct TestApp {
|
|
pub address: String,
|
|
pub db_pool: PgPool,
|
|
}
|
|
|
|
static TRACING: Lazy<()> = Lazy::new(|| {
|
|
let default_filter_level = "info".to_string();
|
|
let subscriber_name = "test".to_string();
|
|
if std::env::var("TEST_LOG").is_ok() {
|
|
let subscriber = get_subscriber(subscriber_name, default_filter_level, std::io::stdout);
|
|
init_subscriber(subscriber);
|
|
} else {
|
|
let subscriber = get_subscriber(subscriber_name, default_filter_level, std::io::sink);
|
|
init_subscriber(subscriber);
|
|
};
|
|
});
|
|
|
|
// Create new instance of the application on a random port and return address [`http://localhost:XXXX`]
|
|
async fn spawn_app() -> TestApp {
|
|
Lazy::force(&TRACING);
|
|
|
|
let listener = TcpListener::bind("127.0.0.1:0")
|
|
.expect("Failed to bind to random 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.");
|
|
configuration.database.database_name = Uuid::new_v4().to_string(); // Adjust database string to be random!
|
|
let connection_pool = configure_database(&configuration.database).await;
|
|
|
|
let server = run(listener, connection_pool.clone())
|
|
.expect("Failed to bind address");
|
|
let _ = tokio::spawn(server);
|
|
TestApp {
|
|
address,
|
|
db_pool: connection_pool,
|
|
}
|
|
}
|
|
|
|
pub async fn configure_database(config: &DatabaseSettings) -> PgPool {
|
|
// Create database
|
|
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))
|
|
.await
|
|
.expect("Failed to create database.");
|
|
|
|
// Migrate database
|
|
let connection_pool = PgPool::connect_with(config.with_db())
|
|
.await
|
|
.expect("Failed to connect to Postgres.");
|
|
sqlx::migrate!("./migrations")
|
|
.run(&connection_pool)
|
|
.await
|
|
.expect("Failed to migrate the database");
|
|
|
|
connection_pool
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn health_check_works() {
|
|
// Arrange
|
|
let app = spawn_app().await;
|
|
let client = reqwest::Client::new();
|
|
|
|
// Act
|
|
let response = client
|
|
.get(&format!("{}/health_check", &app.address))
|
|
.send()
|
|
.await
|
|
.expect("Failed to execute request.");
|
|
|
|
// Assert our test
|
|
assert!(response.status().is_success());
|
|
assert_eq!(Some(0), response.content_length());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn subscribe_returns_200_for_valid_form_data() {
|
|
// Arrange
|
|
let app = spawn_app().await;
|
|
let client = reqwest::Client::new();
|
|
let body = "name=le%20guin&email=ursula_le_guin%40gmail.com";
|
|
|
|
// Act
|
|
let response = client
|
|
.post(&format!("{}/subscriptions", &app.address))
|
|
.header("Content-Type", "application/x-www-form-urlencoded")
|
|
.body(body)
|
|
.send()
|
|
.await
|
|
.expect("Failed to execute request.");
|
|
|
|
// Assert test
|
|
assert_eq!(200, response.status().as_u16());
|
|
|
|
let saved = sqlx::query!("SELECT email, name FROM subscriptions",)
|
|
.fetch_one(&app.db_pool)
|
|
.await
|
|
.expect("Failed to fetch saved subscription.");
|
|
|
|
assert_eq!(saved.email, "ursula_le_guin@gmail.com");
|
|
assert_eq!(saved.name, "le guin");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn subscribe_returns_400_for_missing_form_data() {
|
|
//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(),
|
|
// Customised error message on test failure
|
|
"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() {
|
|
// Arrange
|
|
let app = spawn_app().await;
|
|
let client = reqwest::Client::new();
|
|
let test_cases = vec![
|
|
("name=&email=ursula_le_guin%40gmail.com", "empty name"),
|
|
("name=Ursula&email=", "empty email"),
|
|
("name=Ursula&email=definitely-not-an-email", "invalid email"),
|
|
];
|
|
|
|
for (body, description) in test_cases {
|
|
// Act
|
|
let response = client
|
|
.post(&format!("{}/subscriptions", &app.address))
|
|
.header("Content-Type", "application/x-www-form-urlencoded")
|
|
.body(body)
|
|
.send()
|
|
.await
|
|
.expect("Failed to execute request.");
|
|
|
|
// Assert
|
|
assert_eq!(400,
|
|
response.status().as_u16(),
|
|
"The API did not return a 400 Bad Request when the payload was {}.", description
|
|
);
|
|
}
|
|
} |