Skip to content

Commit 9393e1b

Browse files
authored
Merge pull request #9888 from Turbo87/resend-tests
Add tests for `PUT /user/:user_id/resend` route
2 parents 18b2883 + 7bf0db5 commit 9393e1b

File tree

6 files changed

+121
-44
lines changed

6 files changed

+121
-44
lines changed

src/controllers/user.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
pub mod me;
22
pub mod other;
3+
mod resend;
34
pub mod session;
45
pub mod update;
56

7+
pub use resend::regenerate_token_and_send;
68
pub use update::update_user;

src/controllers/user/resend.rs

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
use super::update::UserConfirmEmail;
2+
use crate::app::AppState;
3+
use crate::auth::AuthCheck;
4+
use crate::controllers::helpers::ok_true;
5+
use crate::models::Email;
6+
use crate::tasks::spawn_blocking;
7+
use crate::util::errors::bad_request;
8+
use crate::util::errors::AppResult;
9+
use axum::extract::Path;
10+
use axum::response::Response;
11+
use crates_io_database::schema::emails;
12+
use diesel::dsl::sql;
13+
use diesel::prelude::*;
14+
use diesel_async::async_connection_wrapper::AsyncConnectionWrapper;
15+
use http::request::Parts;
16+
17+
/// Handles `PUT /user/:user_id/resend` route
18+
pub async fn regenerate_token_and_send(
19+
state: AppState,
20+
Path(param_user_id): Path<i32>,
21+
req: Parts,
22+
) -> AppResult<Response> {
23+
let mut conn = state.db_write().await?;
24+
let auth = AuthCheck::default().check(&req, &mut conn).await?;
25+
spawn_blocking(move || {
26+
let conn: &mut AsyncConnectionWrapper<_> = &mut conn.into();
27+
28+
let user = auth.user();
29+
30+
// need to check if current user matches user to be updated
31+
if user.id != param_user_id {
32+
return Err(bad_request("current user does not match requested user"));
33+
}
34+
35+
conn.transaction(|conn| -> AppResult<_> {
36+
let email: Email = diesel::update(Email::belonging_to(user))
37+
.set(emails::token.eq(sql("DEFAULT")))
38+
.get_result(conn)
39+
.optional()?
40+
.ok_or_else(|| bad_request("Email could not be found"))?;
41+
42+
let email1 = UserConfirmEmail {
43+
user_name: &user.gh_login,
44+
domain: &state.emails.domain,
45+
token: email.token,
46+
};
47+
48+
state.emails.send(&email.email, email1).map_err(Into::into)
49+
})?;
50+
51+
ok_true()
52+
})
53+
.await
54+
}
55+
56+
#[cfg(test)]
57+
mod tests {
58+
use crate::tests::util::{RequestHelper, TestApp};
59+
use http::StatusCode;
60+
use insta::assert_snapshot;
61+
62+
#[tokio::test(flavor = "multi_thread")]
63+
async fn test_no_auth() {
64+
let (app, anon, user) = TestApp::init().with_user();
65+
66+
let url = format!("/api/v1/users/{}/resend", user.as_model().id);
67+
let response = anon.put::<()>(&url, "").await;
68+
assert_eq!(response.status(), StatusCode::FORBIDDEN);
69+
assert_snapshot!(response.text(), @r#"{"errors":[{"detail":"this action requires authentication"}]}"#);
70+
71+
assert_eq!(app.emails().len(), 0);
72+
}
73+
74+
#[tokio::test(flavor = "multi_thread")]
75+
async fn test_wrong_user() {
76+
let (app, _anon, user) = TestApp::init().with_user();
77+
let user2 = app.db_new_user("bar");
78+
79+
let url = format!("/api/v1/users/{}/resend", user2.as_model().id);
80+
let response = user.put::<()>(&url, "").await;
81+
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
82+
assert_snapshot!(response.text(), @r#"{"errors":[{"detail":"current user does not match requested user"}]}"#);
83+
84+
assert_eq!(app.emails().len(), 0);
85+
}
86+
87+
#[tokio::test(flavor = "multi_thread")]
88+
async fn test_happy_path() {
89+
let (app, _anon, user) = TestApp::init().with_user();
90+
91+
let url = format!("/api/v1/users/{}/resend", user.as_model().id);
92+
let response = user.put::<()>(&url, "").await;
93+
assert_eq!(response.status(), StatusCode::OK);
94+
assert_snapshot!(response.text(), @r###"{"ok":true}"###);
95+
96+
assert_snapshot!(app.emails_snapshot());
97+
}
98+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
source: src/controllers/user/resend.rs
3+
expression: app.emails_snapshot()
4+
snapshot_kind: text
5+
---
6+
To: foo@example.com
7+
From: crates.io <noreply@crates.io>
8+
Subject: crates.io: Please confirm your email address
9+
Content-Type: text/plain; charset=utf-8
10+
Content-Transfer-Encoding: 7bit
11+
12+
Hello foo! Welcome to crates.io. Please click the
13+
link below to verify your email address. Thank you!
14+
15+
https://crates.io/confirm/[confirm-token]

src/controllers/user/update.rs

Lines changed: 1 addition & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
use crate::app::AppState;
22
use crate::auth::AuthCheck;
33
use crate::controllers::helpers::ok_true;
4-
use crate::models::{Email, NewEmail};
4+
use crate::models::NewEmail;
55
use crate::schema::{emails, users};
66
use crate::tasks::spawn_blocking;
77
use crate::util::diesel::prelude::*;
88
use crate::util::errors::{bad_request, server_error, AppResult};
99
use axum::extract::Path;
1010
use axum::response::Response;
1111
use axum::Json;
12-
use diesel::dsl::sql;
1312
use diesel_async::async_connection_wrapper::AsyncConnectionWrapper;
1413
use http::request::Parts;
1514
use lettre::Address;
@@ -114,47 +113,6 @@ pub async fn update_user(
114113
.await
115114
}
116115

117-
/// Handles `PUT /user/:user_id/resend` route
118-
pub async fn regenerate_token_and_send(
119-
state: AppState,
120-
Path(param_user_id): Path<i32>,
121-
req: Parts,
122-
) -> AppResult<Response> {
123-
let mut conn = state.db_write().await?;
124-
let auth = AuthCheck::default().check(&req, &mut conn).await?;
125-
spawn_blocking(move || {
126-
use diesel::RunQueryDsl;
127-
128-
let conn: &mut AsyncConnectionWrapper<_> = &mut conn.into();
129-
130-
let user = auth.user();
131-
132-
// need to check if current user matches user to be updated
133-
if user.id != param_user_id {
134-
return Err(bad_request("current user does not match requested user"));
135-
}
136-
137-
conn.transaction(|conn| -> AppResult<_> {
138-
let email: Email = diesel::update(Email::belonging_to(user))
139-
.set(emails::token.eq(sql("DEFAULT")))
140-
.get_result(conn)
141-
.optional()?
142-
.ok_or_else(|| bad_request("Email could not be found"))?;
143-
144-
let email1 = UserConfirmEmail {
145-
user_name: &user.gh_login,
146-
domain: &state.emails.domain,
147-
token: email.token,
148-
};
149-
150-
state.emails.send(&email.email, email1).map_err(Into::into)
151-
})?;
152-
153-
ok_true()
154-
})
155-
.await
156-
}
157-
158116
pub struct UserConfirmEmail<'a> {
159117
pub user_name: &'a str,
160118
pub domain: &'a str,

src/router.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ pub fn build_axum_router(state: AppState) -> Router<()> {
133133
)
134134
.route(
135135
"/api/v1/users/:user_id/resend",
136-
put(user::update::regenerate_token_and_send),
136+
put(user::regenerate_token_and_send),
137137
)
138138
.route(
139139
"/api/v1/site_metadata",

src/tests/util/test_app.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,9 @@ impl TestApp {
189189
static DATE_TIME_REGEX: LazyLock<Regex> =
190190
LazyLock::new(|| Regex::new(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z").unwrap());
191191

192+
static EMAIL_CONFIRM_REGEX: LazyLock<Regex> =
193+
LazyLock::new(|| Regex::new(r"/confirm/\w+").unwrap());
194+
192195
static INVITE_TOKEN_REGEX: LazyLock<Regex> =
193196
LazyLock::new(|| Regex::new(r"/accept-invite/\w+").unwrap());
194197

@@ -199,6 +202,7 @@ impl TestApp {
199202
.map(|email| {
200203
let email = EMAIL_HEADER_REGEX.replace_all(&email, "");
201204
let email = DATE_TIME_REGEX.replace_all(&email, "[0000-00-00T00:00:00Z]");
205+
let email = EMAIL_CONFIRM_REGEX.replace_all(&email, "/confirm/[confirm-token]");
202206
let email = INVITE_TOKEN_REGEX.replace_all(&email, "/accept-invite/[invite-token]");
203207
email.to_string()
204208
})

0 commit comments

Comments
 (0)