Skip to content

Commit 9938857

Browse files
committed
chore: Tested duplicated subscription
1 parent c16424b commit 9938857

14 files changed

+229
-290
lines changed

Cargo.lock

Lines changed: 90 additions & 198 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ actix-web = "4"
1717
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
1818
serde = {version = "1.0.163", features = ["derive"]}
1919
serde-aux = "4.2.0"
20-
config = "0.11"
20+
config = {version = "0.13", default-features = false, features = ["yaml"] }
2121
uuid = { version = "1.3.3", features = ["v4"] }
2222
chrono = "0.4.15"
2323
tracing = { version = "0.1", features = ["log"] }
@@ -26,7 +26,7 @@ tracing-bunyan-formatter = "0.3.3"
2626
tracing-log = "0.1"
2727
secrecy = { version = "0.8", features = ["serde"] }
2828
tracing-actix-web = "0.5"
29-
sqlx = { version = "0.5.7", default-features = false, features = ["offline", "runtime-actix-rustls", "macros", "postgres", "uuid", "chrono", "migrate"] }
29+
sqlx = { version = "0.6", default-features = false, features = ["runtime-actix-rustls", "macros", "postgres", "uuid", "chrono", "migrate", "offline"] }
3030
unicode-segmentation = "1"
3131
validator = "0.16.0"
3232
reqwest = { version = "0.11.18", default-features = false, features = ["json", "rustls-tls"] }

spec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,4 @@ databases:
6565
num_nodes: 1
6666
size: db-s-dev-database
6767
# Postgres version - using the latest here
68-
version: "12"
68+
version: "14"

sqlx-data.json

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,3 @@
11
{
2-
"db": "PostgreSQL",
3-
"bcfcfebc6f5e8ffbf97d97c5a209be78b46d703924482cf8b43842705fcb7714": {
4-
"describe": {
5-
"columns": [],
6-
"nullable": [],
7-
"parameters": {
8-
"Left": [
9-
"Uuid",
10-
"Text",
11-
"Text",
12-
"Timestamptz"
13-
]
14-
}
15-
},
16-
"query": "\n INSERT INTO subscriptions (id, email, name, subscribed_at)\n VALUES ($1, $2, $3, $4)\n "
17-
}
2+
"db": "PostgreSQL"
183
}

src/configuration.rs

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use secrecy::{ExposeSecret, Secret};
33
use serde_aux::field_attributes::deserialize_number_from_string;
44
use sqlx::postgres::{PgConnectOptions, PgSslMode};
55
use sqlx::ConnectOptions;
6+
use std::convert::{TryFrom, TryInto};
67
use tracing_log::log;
78

89
#[derive(serde::Deserialize, Clone)]
@@ -73,28 +74,27 @@ impl DatabaseSettings {
7374

7475
pub fn get_configuration() -> Result<Settings, config::ConfigError> {
7576
// Initialize our configuration reader
76-
let mut settings = config::Config::default();
7777
let base_path = std::env::current_dir().expect("Failed to determine the current directory");
7878
let configuration_directory = base_path.join("configuration");
7979

80-
// Read the "default" configuration file
81-
settings.merge(config::File::from(configuration_directory.join("base")).required(true))?;
82-
8380
// Detect the running environment
8481
// Default to 'local' if unspecified
8582
let environment: Environment = std::env::var("APP_ENVIRONMENT")
8683
.unwrap_or_else(|_| "local".into())
8784
.try_into()
8885
.expect("APP_ENVIRONMENT must be 'local' or 'production'");
8986

90-
// Layer on environment specific values
91-
settings.merge(
92-
config::File::from(configuration_directory.join(environment.as_str())).required(true),
93-
)?;
87+
let environment_filename = format!("{}.yml", environment.as_str());
9488

95-
settings.merge(config::Environment::with_prefix("app").separator("__"))?;
89+
let settings = config::Config::builder().add_source(
90+
config::File::from(configuration_directory.join("base"),
91+
)).add_source(
92+
config::File::from(configuration_directory.join(environment_filename)),
93+
).add_source(
94+
config::Environment::with_prefix("app").prefix_separator("_").separator("__"),
95+
).build()?;
9696

97-
settings.try_into()
97+
settings.try_deserialize::<Settings>()
9898
}
9999

100100
// The possible runtime environment for our application
@@ -105,7 +105,7 @@ pub enum Environment {
105105
}
106106

107107
impl Environment {
108-
pub fn as_str(&self) -> &str {
108+
pub fn as_str(&self) -> &'static str {
109109
match self {
110110
Environment::Local => "local",
111111
Environment::Production => "production",

src/domain/subscriber_email.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use validator::validate_email;
22

3-
#[derive(Debug, PartialEq, Clone)]
3+
#[derive(Debug, Clone)]
44
pub struct SubscriberEmail(String);
55

66
impl SubscriberEmail {

src/email_client.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,9 @@ impl EmailClient {
4444
text_body: text_content,
4545
};
4646

47-
let _builder = self
47+
self
4848
.http_client
49-
.post(url)
49+
.post(&url)
5050
.header(
5151
"X-Postmark-Server-Token",
5252
self.authorization_token.expose_secret(),
@@ -62,11 +62,11 @@ impl EmailClient {
6262
#[derive(serde::Serialize)]
6363
#[serde(rename_all = "PascalCase")]
6464
pub struct SendEmailRequest<'a> {
65-
pub from: &'a str,
66-
pub to: &'a str,
67-
pub subject: &'a str,
68-
pub html_body: &'a str,
69-
pub text_body: &'a str,
65+
from: &'a str,
66+
to: &'a str,
67+
subject: &'a str,
68+
html_body: &'a str,
69+
text_body: &'a str,
7070
}
7171

7272
#[cfg(test)]

src/routes/health_check.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use actix_web::HttpResponse;
22

33
pub async fn health_check() -> HttpResponse {
4-
HttpResponse::Ok().into()
4+
HttpResponse::Ok().finish()
55
}

src/routes/subscriptions.rs

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ use crate::{
66
use actix_web::{web, HttpResponse};
77
use chrono::Utc;
88
use rand::{distributions::Alphanumeric, thread_rng, Rng};
9-
10-
use sqlx::types::Uuid;
119
use sqlx::{PgPool, Postgres, Transaction};
10+
use std::convert::{TryInto, TryFrom};
11+
use uuid::Uuid;
1212

1313
#[allow(dead_code)]
1414
#[derive(serde::Deserialize)]
@@ -27,12 +27,13 @@ impl TryFrom<FormData> for NewSubscriber {
2727
}
2828
}
2929

30+
#[allow(clippy::async_yields_async)]
3031
#[tracing::instrument(
3132
name = "Adding a new subscriber",
3233
skip(form, pool, email_client, base_url),
3334
fields(
34-
email = %form.email,
35-
name = %form.name
35+
subscriber_email = %form.email,
36+
subscriber_name = %form.name
3637
)
3738
)]
3839
pub async fn subscribe(
@@ -43,22 +44,30 @@ pub async fn subscribe(
4344
) -> HttpResponse {
4445
let new_subscriber = match form.0.try_into() {
4546
Ok(subscriber) => subscriber,
46-
Err(e) => return HttpResponse::BadRequest().body(e),
47+
Err(_) => return HttpResponse::BadRequest().finish(),
4748
};
4849

4950
let mut transaction = match pool.begin().await {
5051
Ok(t) => t,
5152
Err(_) => return HttpResponse::InternalServerError().finish(),
5253
};
5354

55+
if search_for_existing_subscription(&new_subscriber, &mut transaction).await.is_err() {
56+
return HttpResponse::InternalServerError().finish();
57+
}
58+
5459
let subscriber_id = match insert_subscriber(&new_subscriber, &mut transaction).await {
5560
Ok(id) => id,
5661
Err(_) => return HttpResponse::InternalServerError().finish(),
5762
};
5863

5964
let subscription_token = generate_subscription_token();
6065

61-
if store_token(subscriber_id, &subscription_token, &pool).await.is_err() {
66+
if store_token(subscriber_id, &subscription_token, &mut transaction).await.is_err() {
67+
return HttpResponse::InternalServerError().finish();
68+
}
69+
70+
if transaction.commit().await.is_err() {
6271
return HttpResponse::InternalServerError().finish();
6372
}
6473

@@ -69,10 +78,6 @@ pub async fn subscribe(
6978
return HttpResponse::InternalServerError().finish();
7079
}
7180

72-
if transaction.commit().await.is_err() {
73-
return HttpResponse::InternalServerError().finish();
74-
}
75-
7681
HttpResponse::Ok().finish()
7782
}
7883

@@ -142,12 +147,12 @@ async fn insert_subscriber(
142147
/// Store subscription token in the database
143148
#[tracing::instrument(
144149
name = "Saving subscription token in the database",
145-
skip(subscriber_id, subscription_token, pool)
150+
skip(subscriber_id, subscription_token, transaction)
146151
)]
147152
async fn store_token(
148153
subscriber_id: Uuid,
149154
subscription_token: &str,
150-
pool: &PgPool,
155+
transaction: &mut Transaction <'_ , Postgres>
151156
) -> Result<(), sqlx::Error> {
152157
sqlx::query!(
153158
r#"
@@ -157,7 +162,7 @@ async fn store_token(
157162
subscription_token,
158163
subscriber_id,
159164
)
160-
.execute(pool)
165+
.execute(transaction)
161166
.await
162167
.map_err(|e| {
163168
tracing::error!("Failed to execute query: {:?}", e);
@@ -173,4 +178,26 @@ fn generate_subscription_token() -> String {
173178
.map(char::from)
174179
.take(25)
175180
.collect()
181+
}
182+
183+
async fn search_for_existing_subscription(
184+
new_subscriber: &NewSubscriber,
185+
transaction: &mut Transaction<'_, Postgres>
186+
) -> Result<(), sqlx::Error> {
187+
sqlx::query!(
188+
r#"
189+
SELECT email, name
190+
FROM subscriptions
191+
WHERE email = $1 AND name = $2
192+
"#,
193+
new_subscriber.email.as_ref(),
194+
new_subscriber.name.as_ref(),
195+
)
196+
.fetch_optional(transaction)
197+
.await
198+
.map_err(|e| {
199+
tracing::error!("Failed to execute query: {:?}", e);
200+
e
201+
})?;
202+
Ok(())
176203
}

src/routes/subscriptions_confirm.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use actix_web::{web, HttpResponse};
2-
use sqlx::{PgPool, types::Uuid};
2+
use sqlx::PgPool;
3+
use uuid::Uuid;
34

45
#[derive(serde::Deserialize)]
56
pub struct Parameters {
@@ -10,7 +11,7 @@ pub struct Parameters {
1011
pub async fn confirm(parameters: web::Query<Parameters>, pool: web::Data<PgPool>) -> HttpResponse {
1112
let id = match get_subscriber_id_from_token(&parameters.subscription_token, &pool).await {
1213
Ok(id) => id,
13-
Err(_) => return HttpResponse::BadRequest().finish(),
14+
Err(_) => return HttpResponse::InternalServerError().finish(),
1415
};
1516

1617
match id {

0 commit comments

Comments
 (0)