Test for user system and comments
This commit is contained in:
@@ -1,5 +1,3 @@
|
|||||||
use std::fmt::Display;
|
|
||||||
|
|
||||||
use crate::telemetry::spawn_blocking_with_tracing;
|
use crate::telemetry::spawn_blocking_with_tracing;
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use argon2::{
|
use argon2::{
|
||||||
@@ -8,6 +6,7 @@ use argon2::{
|
|||||||
};
|
};
|
||||||
use secrecy::{ExposeSecret, SecretString};
|
use secrecy::{ExposeSecret, SecretString};
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
|
use std::fmt::Display;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
pub struct Credentials {
|
pub struct Credentials {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ mod posts;
|
|||||||
mod subscribers;
|
mod subscribers;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
authentication::{AuthenticatedUser, Role},
|
authentication::AuthenticatedUser,
|
||||||
routes::{AppError, error_chain_fmt},
|
routes::{AppError, error_chain_fmt},
|
||||||
session_state::TypedSession,
|
session_state::TypedSession,
|
||||||
templates::{HtmlTemplate, MessageTemplate},
|
templates::{HtmlTemplate, MessageTemplate},
|
||||||
@@ -81,11 +81,10 @@ pub async fn require_admin(
|
|||||||
request: Request,
|
request: Request,
|
||||||
next: Next,
|
next: Next,
|
||||||
) -> Result<Response, AppError> {
|
) -> Result<Response, AppError> {
|
||||||
if let Role::Admin = session
|
if session
|
||||||
.get_role()
|
.has_admin_permissions()
|
||||||
.await
|
.await
|
||||||
.context("Error retrieving user role in session.")?
|
.context("Error retrieving user role in session.")?
|
||||||
.ok_or(anyhow::anyhow!("Could not find user role in session."))?
|
|
||||||
{
|
{
|
||||||
Ok(next.run(request).await)
|
Ok(next.run(request).await)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -62,7 +62,10 @@ impl TryFrom<CreateUserForm> for NewUser {
|
|||||||
anyhow::bail!("Password mismatch.");
|
anyhow::bail!("Password mismatch.");
|
||||||
}
|
}
|
||||||
|
|
||||||
let role = value.admin.map(|_| Role::Admin).unwrap_or(Role::Writer);
|
let role = match value.admin {
|
||||||
|
Some(true) => Role::Admin,
|
||||||
|
_ => Role::Writer,
|
||||||
|
};
|
||||||
let password_hash = crate::authentication::compute_pasword_hash(value.password)
|
let password_hash = crate::authentication::compute_pasword_hash(value.password)
|
||||||
.context("Failed to hash password.")?;
|
.context("Failed to hash password.")?;
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
|
|||||||
@@ -42,6 +42,15 @@ impl TypedSession {
|
|||||||
self.0.get(Self::ROLE_KEY).await
|
self.0.get(Self::ROLE_KEY).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn has_admin_permissions(&self) -> Result<bool> {
|
||||||
|
let role = self.0.get(Self::ROLE_KEY).await?;
|
||||||
|
if let Some(Role::Admin) = role {
|
||||||
|
Ok(true)
|
||||||
|
} else {
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn clear(&self) {
|
pub async fn clear(&self) {
|
||||||
self.0.clear().await;
|
self.0.clear().await;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="comments-list" class="space-y-6">
|
<div id="comments-list">
|
||||||
{% block comments %}
|
{% block comments %}
|
||||||
{% if comments.is_empty() %}
|
{% if comments.is_empty() %}
|
||||||
<div class="p-8 text-center">
|
<div class="p-8 text-center">
|
||||||
@@ -29,11 +29,11 @@
|
|||||||
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"/>
|
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="text-lg font-medium text-gray-900 mb-2">No data to display</h3>
|
<h3 class="text-lg font-medium text-gray-900 mb-2">No comments to display</h3>
|
||||||
<p class="text-gray-600">The request did not return any data.</p>
|
<p class="text-gray-600">Content may have shifted due to recent updates or list is empty.</p>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="divide-y divide-gray-200">
|
<div class="divide-y divide-gray-200 mb-6">
|
||||||
{% for comment in comments %}
|
{% for comment in comments %}
|
||||||
{% include "dashboard/comments/card.html" %}
|
{% include "dashboard/comments/card.html" %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="posts-list" class="space-y-6">
|
<div id="posts-list">
|
||||||
{% block posts %}
|
{% block posts %}
|
||||||
{% if posts.is_empty() %}
|
{% if posts.is_empty() %}
|
||||||
<div class="p-8 text-center">
|
<div class="p-8 text-center">
|
||||||
@@ -29,11 +29,11 @@
|
|||||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="text-lg font-medium text-gray-900 mb-2">No data to display</h3>
|
<h3 class="text-lg font-medium text-gray-900 mb-2">No posts to display</h3>
|
||||||
<p class="text-gray-600">The request did not return any data.</p>
|
<p class="text-gray-600">Content may have shifted due to recent updates or list is empty.</p>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="divide-y divide-gray-200">
|
<div class="divide-y divide-gray-200 mb-6">
|
||||||
{% for post in posts %}
|
{% for post in posts %}
|
||||||
{% include "dashboard/posts/card.html" %}
|
{% include "dashboard/posts/card.html" %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -19,10 +19,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="subscribers-list" class="space-y-6">
|
<div id="subscribers-list">
|
||||||
{% block subs %}
|
{% block subs %}
|
||||||
{% if subscribers.is_empty() %}
|
{% if subscribers.is_empty() %}
|
||||||
<div class="g p-8 text-center">
|
<div class="p-8 text-center">
|
||||||
<div class="w-16 h-16 bg-gray-200 rounded-full flex items-center justify-center mx-auto mb-4">
|
<div class="w-16 h-16 bg-gray-200 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
<svg class="w-8 h-8 text-gray-500"
|
<svg class="w-8 h-8 text-gray-500"
|
||||||
fill="none"
|
fill="none"
|
||||||
@@ -32,11 +32,11 @@
|
|||||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="text-lg font-medium text-gray-900 mb-2">No data available</h3>
|
<h3 class="text-lg font-medium text-gray-900 mb-2">No subscribers to display</h3>
|
||||||
<p class="text-gray-600">Content may have shifted due to recent updates or list is empty.</p>
|
<p class="text-gray-600">Content may have shifted due to recent updates or list is empty.</p>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="divide-y divide-gray-200">
|
<div class="divide-y divide-gray-200 mb-6">
|
||||||
{% for subscriber in subscribers %}
|
{% for subscriber in subscribers %}
|
||||||
{% include "dashboard/subscribers/card.html" %}
|
{% include "dashboard/subscribers/card.html" %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -28,8 +28,8 @@
|
|||||||
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
|
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="text-lg font-medium text-gray-900 mb-2">No users found</h3>
|
<h3 class="text-lg font-medium text-gray-900 mb-2">No users to display</h3>
|
||||||
<p class="text-gray-600">No users in the system.</p>
|
<p class="text-gray-600">Content may have shifted due to recent updates or list is empty.</p>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="divide-y divide-gray-200">
|
<div class="divide-y divide-gray-200">
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ async fn subscribers_are_visible_on_the_dashboard(connection_pool: PgPool) {
|
|||||||
app.admin_login().await;
|
app.admin_login().await;
|
||||||
|
|
||||||
let response = app.get_admin_dashboard_html().await;
|
let response = app.get_admin_dashboard_html().await;
|
||||||
assert!(response.contains("No data available"));
|
assert!(response.contains("No subscribers to display"));
|
||||||
|
|
||||||
app.create_confirmed_subscriber().await;
|
app.create_confirmed_subscriber().await;
|
||||||
let subscriber = sqlx::query!("SELECT id, email FROM subscriptions")
|
let subscriber = sqlx::query!("SELECT id, email FROM subscriptions")
|
||||||
@@ -53,10 +53,90 @@ async fn subscribers_are_visible_on_the_dashboard(connection_pool: PgPool) {
|
|||||||
|
|
||||||
app.delete_subscriber(subscriber.id).await;
|
app.delete_subscriber(subscriber.id).await;
|
||||||
let response = app.get_admin_dashboard_html().await;
|
let response = app.get_admin_dashboard_html().await;
|
||||||
assert!(response.contains("No data available"));
|
assert!(response.contains("No subscribers to display"));
|
||||||
assert!(!response.contains(&subscriber.email));
|
assert!(!response.contains(&subscriber.email));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[sqlx::test]
|
||||||
|
async fn posts_are_visible_on_the_dashboard(connection_pool: PgPool) {
|
||||||
|
let app = TestApp::spawn(connection_pool).await;
|
||||||
|
app.admin_login().await;
|
||||||
|
|
||||||
|
let response = app.get_admin_dashboard_html().await;
|
||||||
|
assert!(response.contains("No posts to display"));
|
||||||
|
|
||||||
|
let response = app.post_create_post(&fake_post_body()).await;
|
||||||
|
assert!(
|
||||||
|
response
|
||||||
|
.text()
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.contains("Your new post has been published")
|
||||||
|
);
|
||||||
|
|
||||||
|
let (post_id, post_title) = {
|
||||||
|
let record = sqlx::query!("SELECT post_id, title FROM posts")
|
||||||
|
.fetch_one(&app.connection_pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
(record.post_id, record.title)
|
||||||
|
};
|
||||||
|
|
||||||
|
let html = app.get_admin_dashboard_html().await;
|
||||||
|
assert!(html.contains(&post_title));
|
||||||
|
|
||||||
|
app.delete_post(post_id).await;
|
||||||
|
let response = app.get_admin_dashboard_html().await;
|
||||||
|
assert!(response.contains("No posts to display"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[sqlx::test]
|
||||||
|
async fn comments_are_visible_on_the_dashboard(connection_pool: PgPool) {
|
||||||
|
let app = TestApp::spawn(connection_pool).await;
|
||||||
|
app.admin_login().await;
|
||||||
|
|
||||||
|
let response = app.get_admin_dashboard_html().await;
|
||||||
|
assert!(response.contains("No comments to display"));
|
||||||
|
|
||||||
|
app.post_create_post(&fake_post_body()).await;
|
||||||
|
|
||||||
|
let (post_id, post_title) = {
|
||||||
|
let record = sqlx::query!("SELECT post_id, title FROM posts")
|
||||||
|
.fetch_one(&app.connection_pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
(record.post_id, record.title)
|
||||||
|
};
|
||||||
|
|
||||||
|
let author = "author";
|
||||||
|
let content = "comment";
|
||||||
|
let comment_body = serde_json::json!({
|
||||||
|
"author": author,
|
||||||
|
"content": content,
|
||||||
|
"idempotency_key": "key"
|
||||||
|
});
|
||||||
|
app.post_comment(&post_id, &comment_body).await;
|
||||||
|
|
||||||
|
let response = app.get_admin_dashboard_html().await;
|
||||||
|
assert!(response.contains(author));
|
||||||
|
assert!(response.contains(content));
|
||||||
|
|
||||||
|
let html = app.get_admin_dashboard_html().await;
|
||||||
|
assert!(html.contains(&post_title));
|
||||||
|
|
||||||
|
let comment_id = {
|
||||||
|
let record = sqlx::query!("SELECT comment_id FROM comments")
|
||||||
|
.fetch_one(&app.connection_pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
record.comment_id
|
||||||
|
};
|
||||||
|
|
||||||
|
app.delete_comment(comment_id).await;
|
||||||
|
let response = app.get_admin_dashboard_html().await;
|
||||||
|
assert!(response.contains("No comments to display"));
|
||||||
|
}
|
||||||
|
|
||||||
#[sqlx::test]
|
#[sqlx::test]
|
||||||
async fn dashboard_shows_correct_stats(connection_pool: PgPool) {
|
async fn dashboard_shows_correct_stats(connection_pool: PgPool) {
|
||||||
let app = TestApp::spawn(connection_pool).await;
|
let app = TestApp::spawn(connection_pool).await;
|
||||||
|
|||||||
@@ -130,6 +130,26 @@ impl TestApp {
|
|||||||
app
|
app
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn create_user(
|
||||||
|
&self,
|
||||||
|
username: &str,
|
||||||
|
password: &str,
|
||||||
|
admin: bool,
|
||||||
|
) -> reqwest::Response {
|
||||||
|
let body = serde_json::json!({
|
||||||
|
"username": username,
|
||||||
|
"password": password,
|
||||||
|
"password_check": password,
|
||||||
|
"admin": admin,
|
||||||
|
});
|
||||||
|
self.api_client
|
||||||
|
.post(format!("{}/admin/users", self.address))
|
||||||
|
.form(&body)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn create_unconfirmed_subscriber(&self) -> ConfirmationLinks {
|
pub async fn create_unconfirmed_subscriber(&self) -> ConfirmationLinks {
|
||||||
let email: String = SafeEmail().fake();
|
let email: String = SafeEmail().fake();
|
||||||
let body = format!("email={email}");
|
let body = format!("email={email}");
|
||||||
@@ -166,7 +186,7 @@ impl TestApp {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn delete_subscriber(&self, subscriber_id: Uuid) {
|
pub async fn delete_subscriber(&self, subscriber_id: Uuid) -> reqwest::Response {
|
||||||
self.api_client
|
self.api_client
|
||||||
.delete(format!(
|
.delete(format!(
|
||||||
"{}/admin/subscribers/{}",
|
"{}/admin/subscribers/{}",
|
||||||
@@ -174,7 +194,15 @@ impl TestApp {
|
|||||||
))
|
))
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.expect("Could not delete subscriber");
|
.expect("Could not delete subscriber")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete_user(&self, user_id: Uuid) -> reqwest::Response {
|
||||||
|
self.api_client
|
||||||
|
.delete(format!("{}/admin/users/{}", self.address, user_id))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Could not delete user")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn dispatch_all_pending_emails(&self) {
|
pub async fn dispatch_all_pending_emails(&self) {
|
||||||
@@ -371,12 +399,20 @@ impl TestApp {
|
|||||||
.expect("Failed to execute request")
|
.expect("Failed to execute request")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn delete_post(&self, post_id: Uuid) {
|
pub async fn delete_post(&self, post_id: Uuid) -> reqwest::Response {
|
||||||
self.api_client
|
self.api_client
|
||||||
.delete(format!("{}/admin/posts/{}", self.address, post_id))
|
.delete(format!("{}/admin/posts/{}", self.address, post_id))
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.expect("Could not delete post");
|
.expect("Could not delete post")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete_comment(&self, comment_id: Uuid) -> reqwest::Response {
|
||||||
|
self.api_client
|
||||||
|
.delete(format!("{}/admin/comments/{}", self.address, comment_id))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Could not delete comment")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn post_unsubscribe<Body>(&self, body: &Body) -> reqwest::Response
|
pub async fn post_unsubscribe<Body>(&self, body: &Body) -> reqwest::Response
|
||||||
@@ -418,8 +454,7 @@ pub fn fake_post_body() -> serde_json::Value {
|
|||||||
serde_json::json!({
|
serde_json::json!({
|
||||||
"title": "Post title",
|
"title": "Post title",
|
||||||
"content": "Post content",
|
"content": "Post content",
|
||||||
"idempotency_key": Uuid::new_v4().to_string(),
|
"idempotency_key": Uuid::new_v4().to_string()
|
||||||
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,3 +10,4 @@ mod subscriptions;
|
|||||||
mod subscriptions_confirm;
|
mod subscriptions_confirm;
|
||||||
mod unsubscribe;
|
mod unsubscribe;
|
||||||
mod unsubscribe_confirm;
|
mod unsubscribe_confirm;
|
||||||
|
mod users;
|
||||||
|
|||||||
346
tests/api/users.rs
Normal file
346
tests/api/users.rs
Normal file
@@ -0,0 +1,346 @@
|
|||||||
|
use crate::helpers::{TestApp, fake_newsletter_body, fake_post_body, when_sending_an_email};
|
||||||
|
use sqlx::PgPool;
|
||||||
|
use wiremock::ResponseTemplate;
|
||||||
|
use zero2prod::authentication::Role;
|
||||||
|
|
||||||
|
#[sqlx::test]
|
||||||
|
async fn admin_can_create_user(connection_pool: PgPool) {
|
||||||
|
let app = TestApp::spawn(connection_pool).await;
|
||||||
|
app.admin_login().await;
|
||||||
|
let username = "alphonse";
|
||||||
|
let password = "123456789abc";
|
||||||
|
app.create_user(username, password, false).await;
|
||||||
|
|
||||||
|
let record = sqlx::query!("SELECT user_id FROM users WHERE username = $1", username)
|
||||||
|
.fetch_optional(&app.connection_pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(record.is_some());
|
||||||
|
|
||||||
|
let html = app.get_admin_dashboard_html().await;
|
||||||
|
assert!(html.contains(username));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[sqlx::test]
|
||||||
|
async fn admin_can_create_admin_user(connection_pool: PgPool) {
|
||||||
|
let app = TestApp::spawn(connection_pool).await;
|
||||||
|
app.admin_login().await;
|
||||||
|
let username = "alphonse";
|
||||||
|
let password = "123456789abc";
|
||||||
|
app.create_user(username, password, true).await;
|
||||||
|
|
||||||
|
let record = sqlx::query!(
|
||||||
|
r#"
|
||||||
|
SELECT role as "role: Role"
|
||||||
|
FROM users WHERE username = $1
|
||||||
|
"#,
|
||||||
|
username
|
||||||
|
)
|
||||||
|
.fetch_one(&app.connection_pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
matches!(record.role, Role::Admin);
|
||||||
|
|
||||||
|
app.logout().await;
|
||||||
|
let login_body = serde_json::json!({
|
||||||
|
"username": username,
|
||||||
|
"password": password
|
||||||
|
});
|
||||||
|
app.post_login(&login_body).await;
|
||||||
|
|
||||||
|
let html = app.get_admin_dashboard_html().await;
|
||||||
|
assert!(html.contains("Administration"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[sqlx::test]
|
||||||
|
async fn admin_users_can_create_posts(connection_pool: PgPool) {
|
||||||
|
let app = TestApp::spawn(connection_pool).await;
|
||||||
|
app.admin_login().await;
|
||||||
|
let username = "alphonse";
|
||||||
|
let password = "123456789abc";
|
||||||
|
app.create_user(username, password, true).await;
|
||||||
|
app.logout().await;
|
||||||
|
let login_body = serde_json::json!({
|
||||||
|
"username": username,
|
||||||
|
"password": password
|
||||||
|
});
|
||||||
|
app.post_login(&login_body).await;
|
||||||
|
app.create_confirmed_subscriber().await;
|
||||||
|
when_sending_an_email()
|
||||||
|
.respond_with(ResponseTemplate::new(200))
|
||||||
|
.expect(1)
|
||||||
|
.mount(&app.email_server)
|
||||||
|
.await;
|
||||||
|
app.post_create_post(&fake_post_body()).await;
|
||||||
|
app.dispatch_all_pending_emails().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[sqlx::test]
|
||||||
|
async fn admin_users_can_send_emails(connection_pool: PgPool) {
|
||||||
|
let app = TestApp::spawn(connection_pool).await;
|
||||||
|
app.admin_login().await;
|
||||||
|
let username = "alphonse";
|
||||||
|
let password = "123456789abc";
|
||||||
|
app.create_user(username, password, true).await;
|
||||||
|
app.logout().await;
|
||||||
|
let login_body = serde_json::json!({
|
||||||
|
"username": username,
|
||||||
|
"password": password
|
||||||
|
});
|
||||||
|
app.post_login(&login_body).await;
|
||||||
|
app.create_confirmed_subscriber().await;
|
||||||
|
when_sending_an_email()
|
||||||
|
.respond_with(ResponseTemplate::new(200))
|
||||||
|
.expect(1)
|
||||||
|
.mount(&app.email_server)
|
||||||
|
.await;
|
||||||
|
app.post_newsletters(&fake_newsletter_body()).await;
|
||||||
|
app.dispatch_all_pending_emails().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[sqlx::test]
|
||||||
|
async fn admin_users_can_create_users(connection_pool: PgPool) {
|
||||||
|
let app = TestApp::spawn(connection_pool).await;
|
||||||
|
app.admin_login().await;
|
||||||
|
let username = "alphonse";
|
||||||
|
let password = "123456789abc";
|
||||||
|
app.create_user(username, password, true).await;
|
||||||
|
app.logout().await;
|
||||||
|
let login_body = serde_json::json!({
|
||||||
|
"username": username,
|
||||||
|
"password": password
|
||||||
|
});
|
||||||
|
app.post_login(&login_body).await;
|
||||||
|
|
||||||
|
let username = "other_user";
|
||||||
|
app.create_user(username, password, true).await;
|
||||||
|
let html = app.get_admin_dashboard_html().await;
|
||||||
|
assert!(html.contains(username));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[sqlx::test]
|
||||||
|
async fn admin_users_can_delete_contents(connection_pool: PgPool) {
|
||||||
|
let app = TestApp::spawn(connection_pool).await;
|
||||||
|
app.admin_login().await;
|
||||||
|
let username = "alphonse";
|
||||||
|
let password = "123456789abc";
|
||||||
|
app.create_user(username, password, true).await;
|
||||||
|
app.logout().await;
|
||||||
|
let login_body = serde_json::json!({
|
||||||
|
"username": username,
|
||||||
|
"password": password
|
||||||
|
});
|
||||||
|
app.post_login(&login_body).await;
|
||||||
|
|
||||||
|
app.create_confirmed_subscriber().await;
|
||||||
|
let (subscriber_id, email) = {
|
||||||
|
let record = sqlx::query!("SELECT id, email FROM subscriptions")
|
||||||
|
.fetch_one(&app.connection_pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
(record.id, record.email)
|
||||||
|
};
|
||||||
|
let response = app.delete_subscriber(subscriber_id).await;
|
||||||
|
let text = response.text().await.unwrap();
|
||||||
|
assert!(text.contains(&email));
|
||||||
|
assert!(text.contains("has been deleted"));
|
||||||
|
|
||||||
|
app.create_user("other_user", password, true).await;
|
||||||
|
let user_id = {
|
||||||
|
let record = sqlx::query!("SELECT user_id FROM users")
|
||||||
|
.fetch_one(&app.connection_pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
record.user_id
|
||||||
|
};
|
||||||
|
let response = app.delete_user(user_id).await;
|
||||||
|
let text = response.text().await.unwrap();
|
||||||
|
assert!(text.contains("The user has been deleted"));
|
||||||
|
|
||||||
|
app.post_create_post(&fake_post_body()).await;
|
||||||
|
let post_id = {
|
||||||
|
let record = sqlx::query!("SELECT post_id FROM posts")
|
||||||
|
.fetch_one(&app.connection_pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
record.post_id
|
||||||
|
};
|
||||||
|
|
||||||
|
let comment_body = serde_json::json!({
|
||||||
|
"author": "author",
|
||||||
|
"content": "comment",
|
||||||
|
"idempotency_key": "key",
|
||||||
|
});
|
||||||
|
app.post_comment(&post_id, &comment_body).await;
|
||||||
|
let comment_id = {
|
||||||
|
let record = sqlx::query!("SELECT comment_id FROM comments")
|
||||||
|
.fetch_one(&app.connection_pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
record.comment_id
|
||||||
|
};
|
||||||
|
let response = app.delete_comment(comment_id).await;
|
||||||
|
assert!(
|
||||||
|
response
|
||||||
|
.text()
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.contains("The comment has been deleted")
|
||||||
|
);
|
||||||
|
|
||||||
|
let response = app.delete_post(post_id).await;
|
||||||
|
let text = response.text().await.unwrap();
|
||||||
|
assert!(text.contains("The post has been deleted"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[sqlx::test]
|
||||||
|
async fn admin_functions_are_hidden_for_non_admin_users(connection_pool: PgPool) {
|
||||||
|
let app = TestApp::spawn(connection_pool).await;
|
||||||
|
app.admin_login().await;
|
||||||
|
let username = "alphonse";
|
||||||
|
let password = "123456789abc";
|
||||||
|
app.create_user(username, password, false).await;
|
||||||
|
let record = sqlx::query!(
|
||||||
|
r#"
|
||||||
|
SELECT role as "role: Role"
|
||||||
|
FROM users WHERE username = $1
|
||||||
|
"#,
|
||||||
|
username
|
||||||
|
)
|
||||||
|
.fetch_one(&app.connection_pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
matches!(record.role, Role::Writer);
|
||||||
|
app.logout().await;
|
||||||
|
let login_body = serde_json::json!({
|
||||||
|
"username": username,
|
||||||
|
"password": password
|
||||||
|
});
|
||||||
|
let response = app.post_login(&login_body).await;
|
||||||
|
assert!(!response.text().await.unwrap().contains("Administration"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[sqlx::test]
|
||||||
|
async fn writers_can_publish_posts_and_send_emails(connection_pool: PgPool) {
|
||||||
|
let app = TestApp::spawn(connection_pool).await;
|
||||||
|
app.admin_login().await;
|
||||||
|
let username = "alphonse";
|
||||||
|
let password = "123456789abc";
|
||||||
|
app.create_user(username, password, false).await;
|
||||||
|
app.logout().await;
|
||||||
|
let login_body = serde_json::json!({
|
||||||
|
"username": username,
|
||||||
|
"password": password
|
||||||
|
});
|
||||||
|
app.post_login(&login_body).await;
|
||||||
|
|
||||||
|
app.create_confirmed_subscriber().await;
|
||||||
|
when_sending_an_email()
|
||||||
|
.respond_with(ResponseTemplate::new(200))
|
||||||
|
.expect(2)
|
||||||
|
.mount(&app.email_server)
|
||||||
|
.await;
|
||||||
|
app.post_create_post(&fake_post_body()).await;
|
||||||
|
app.post_newsletters(&fake_newsletter_body()).await;
|
||||||
|
app.dispatch_all_pending_emails().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[sqlx::test]
|
||||||
|
async fn writers_cannot_perform_admin_functions(connection_pool: PgPool) {
|
||||||
|
let app = TestApp::spawn(connection_pool).await;
|
||||||
|
app.admin_login().await;
|
||||||
|
let username = "alphonse";
|
||||||
|
let password = "123456789abc";
|
||||||
|
app.create_user(username, password, false).await;
|
||||||
|
app.post_create_post(&fake_post_body()).await;
|
||||||
|
let post_id = {
|
||||||
|
let record = sqlx::query!("SELECT post_id FROM posts")
|
||||||
|
.fetch_one(&app.connection_pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
record.post_id
|
||||||
|
};
|
||||||
|
app.create_confirmed_subscriber().await;
|
||||||
|
let subscriber_id = {
|
||||||
|
let record = sqlx::query!("SELECT id FROM subscriptions")
|
||||||
|
.fetch_one(&app.connection_pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
record.id
|
||||||
|
};
|
||||||
|
let comment_body = serde_json::json!({
|
||||||
|
"author": "author",
|
||||||
|
"content": "comment",
|
||||||
|
"idempotency_key": "key",
|
||||||
|
});
|
||||||
|
app.post_comment(&post_id, &comment_body).await;
|
||||||
|
let comment_id = {
|
||||||
|
let record = sqlx::query!("SELECT comment_id FROM comments")
|
||||||
|
.fetch_one(&app.connection_pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
record.comment_id
|
||||||
|
};
|
||||||
|
|
||||||
|
app.logout().await;
|
||||||
|
let login_body = serde_json::json!({
|
||||||
|
"username": username,
|
||||||
|
"password": password
|
||||||
|
});
|
||||||
|
app.post_login(&login_body).await;
|
||||||
|
|
||||||
|
let response = app.delete_subscriber(subscriber_id).await;
|
||||||
|
let html = response.text().await.unwrap();
|
||||||
|
assert!(html.contains("requires administrator privileges"));
|
||||||
|
let record = sqlx::query!("SELECT id FROM subscriptions")
|
||||||
|
.fetch_optional(&app.connection_pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(record.is_some());
|
||||||
|
|
||||||
|
let response = app.delete_comment(comment_id).await;
|
||||||
|
let html = response.text().await.unwrap();
|
||||||
|
assert!(html.contains("requires administrator privileges"));
|
||||||
|
let record = sqlx::query!("SELECT comment_id FROM comments")
|
||||||
|
.fetch_optional(&app.connection_pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(record.is_some());
|
||||||
|
|
||||||
|
let response = app.delete_post(post_id).await;
|
||||||
|
let html = response.text().await.unwrap();
|
||||||
|
assert!(html.contains("requires administrator privileges"));
|
||||||
|
let record = sqlx::query!("SELECT post_id FROM posts")
|
||||||
|
.fetch_optional(&app.connection_pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(record.is_some());
|
||||||
|
|
||||||
|
let user_id = {
|
||||||
|
let record = sqlx::query!("SELECT user_id FROM users")
|
||||||
|
.fetch_one(&app.connection_pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
record.user_id
|
||||||
|
};
|
||||||
|
let response = app.delete_user(user_id).await;
|
||||||
|
let html = response.text().await.unwrap();
|
||||||
|
assert!(html.contains("requires administrator privileges"));
|
||||||
|
|
||||||
|
let record = sqlx::query_scalar!("SELECT username FROM users WHERE user_id = $1", user_id)
|
||||||
|
.fetch_optional(&app.connection_pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(record.is_some());
|
||||||
|
|
||||||
|
let username = "friend";
|
||||||
|
let password = "123456789abc";
|
||||||
|
let response = app.create_user(username, password, false).await;
|
||||||
|
let html = response.text().await.unwrap();
|
||||||
|
assert!(html.contains("requires administrator privileges"));
|
||||||
|
let record = sqlx::query!("SELECT user_id FROM users WHERE username = $1", username)
|
||||||
|
.fetch_optional(&app.connection_pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(record.is_none());
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user