Posts editing tests
This commit is contained in:
@@ -28,6 +28,7 @@ pub use subscriptions::*;
|
||||
pub use subscriptions_confirm::*;
|
||||
pub use unsubscribe::*;
|
||||
pub use users::*;
|
||||
use validator::ValidationErrors;
|
||||
|
||||
use crate::{
|
||||
authentication::AuthError,
|
||||
@@ -209,3 +210,20 @@ where
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn join_error_messages(e: ValidationErrors) -> String {
|
||||
let error_messages: Vec<_> = e
|
||||
.field_errors()
|
||||
.iter()
|
||||
.flat_map(|(field, errors)| {
|
||||
errors.iter().map(move |error| {
|
||||
error
|
||||
.message
|
||||
.as_ref()
|
||||
.map(|msg| msg.to_string())
|
||||
.unwrap_or(format!("Invalid field: {}", field))
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
error_messages.join("\n")
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::authentication::AuthenticatedUser;
|
||||
use crate::routes::{COMMENTS_PER_PAGE, Query, get_max_page};
|
||||
use crate::routes::{COMMENTS_PER_PAGE, Query, get_max_page, join_error_messages};
|
||||
use crate::session_state::TypedSession;
|
||||
use crate::templates::{ErrorTemplate, MessageTemplate, PostsPageDashboardTemplate};
|
||||
use crate::{
|
||||
@@ -19,6 +19,7 @@ use axum::{
|
||||
};
|
||||
use sqlx::PgPool;
|
||||
use uuid::Uuid;
|
||||
use validator::Validate;
|
||||
|
||||
pub const POSTS_PER_PAGE: i64 = 3;
|
||||
|
||||
@@ -119,9 +120,11 @@ pub async fn get_posts_count(connection_pool: &PgPool) -> Result<i64, sqlx::Erro
|
||||
.map(|r| r.count.unwrap())
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[derive(Validate, serde::Deserialize)]
|
||||
pub struct EditPostForm {
|
||||
#[validate(length(min = 1, message = "Title must be at least one character."))]
|
||||
pub title: String,
|
||||
#[validate(length(min = 1, message = "Content must be at least one character."))]
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
@@ -141,6 +144,10 @@ pub async fn update_post(
|
||||
match record {
|
||||
None => Ok(HtmlTemplate(ErrorTemplate::NotFound).into_response()),
|
||||
Some(record) if record.author_id == user_id => {
|
||||
if let Err(e) = form.validate().map_err(join_error_messages) {
|
||||
let template = HtmlTemplate(MessageTemplate::error(e));
|
||||
return Ok(template.into_response());
|
||||
}
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE posts
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::authentication::AuthenticatedUser;
|
||||
use crate::routes::verify_password;
|
||||
use crate::routes::{join_error_messages, verify_password};
|
||||
use crate::session_state::TypedSession;
|
||||
use crate::templates::{MessageTemplate, UserEditTemplate};
|
||||
use crate::{
|
||||
@@ -64,21 +64,8 @@ pub async fn update_user(
|
||||
}): Extension<AuthenticatedUser>,
|
||||
Form(form): Form<EditProfileForm>,
|
||||
) -> Result<Response, AppError> {
|
||||
if let Err(e) = form.validate() {
|
||||
let error_messages: Vec<_> = e
|
||||
.field_errors()
|
||||
.iter()
|
||||
.flat_map(|(field, errors)| {
|
||||
errors.iter().map(move |error| {
|
||||
error
|
||||
.message
|
||||
.as_ref()
|
||||
.map(|msg| msg.to_string())
|
||||
.unwrap_or(format!("Invalid field: {}", field))
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
let template = HtmlTemplate(MessageTemplate::error(error_messages.join("\n")));
|
||||
if let Err(e) = form.validate().map_err(join_error_messages) {
|
||||
let template = HtmlTemplate(MessageTemplate::error(e));
|
||||
return Ok(template.into_response());
|
||||
}
|
||||
if form.user_id != session_user_id {
|
||||
|
||||
@@ -1,111 +1,107 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Edit: {{ post.title }}{% endblock %}
|
||||
{% block title %}{{ post.title }}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="max-w-3xl mx-auto">
|
||||
<article>
|
||||
<header class="mb-4">
|
||||
<h1 class="text-4xl font-bold text-gray-900 mb-4 leading-tight">{{ post.title }}</h1>
|
||||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between text-sm text-gray-600">
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="flex items-center">
|
||||
<div class="w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center mr-2">
|
||||
<svg class="w-4 h-4 text-blue-600"
|
||||
<div class="max-w-3xl mx-auto">
|
||||
<article>
|
||||
<header class="mb-4">
|
||||
<h1 class="text-4xl font-bold text-gray-900 mb-4 leading-tight">{{ post.title }}</h1>
|
||||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between text-sm text-gray-600">
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="flex items-center">
|
||||
<div class="w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center mr-2">
|
||||
<svg class="w-4 h-4 text-blue-600"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 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>
|
||||
</div>
|
||||
<a href="/users/{{ post.author }}"
|
||||
class="hover:text-blue-600 hover:underline font-medium">{{ post.author }}</a>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<svg class="w-4 h-4 mr-1 text-gray-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<time datetime="{{ post.published_at }}">
|
||||
{{ post.formatted_date() }}
|
||||
</time>
|
||||
</div>
|
||||
<a href="/users/{{ post.author }}"
|
||||
class="hover:text-blue-600 hover:underline font-medium">{{ post.author }}</a>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<svg class="w-4 h-4 mr-1 text-gray-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
<time datetime="{{ post.published_at }}">
|
||||
{{ post.formatted_date() }}
|
||||
</time>
|
||||
</div>
|
||||
{% if session_username.as_deref() == Some(post.author) %}
|
||||
<div class="mt-4 sm:mt-0">
|
||||
<button onclick="document.getElementById('edit-form').classList.toggle('hidden')"
|
||||
class="inline-flex items-center px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-md transition-colors">
|
||||
<svg class="w-4 h-4 mr-1"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if session_username.as_deref() == Some(post.author) %}
|
||||
<div class="mt-4 sm:mt-0">
|
||||
<button onclick="document.getElementById('edit-form').classList.toggle('hidden')"
|
||||
class="inline-flex items-center px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-md transition-colors">
|
||||
<svg class="w-4 h-4 mr-1"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</header>
|
||||
{% if session_username.as_deref() == Some(post.author) %}
|
||||
<div id="edit-form"
|
||||
class="hidden bg-gray-50 border border-gray-200 rounded-lg p-6">
|
||||
<h2 class="text-xl font-bold text-gray-900 mb-4">Edit post</h2>
|
||||
<form hx-put="/posts/{{ post.post_id }}"
|
||||
hx-target="#edit-messages"
|
||||
hx-swap="innerHTML">
|
||||
<div class="mb-4">
|
||||
<label for="title" class="block text-sm font-medium text-gray-700 mb-2">Title</label>
|
||||
<input type="text"
|
||||
id="title"
|
||||
name="title"
|
||||
value="{{ post.title }}"
|
||||
required
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label for="content" class="block text-sm font-medium text-gray-700 mb-2">Content (markdown)</label>
|
||||
<textarea id="content"
|
||||
name="content"
|
||||
rows="12"
|
||||
required
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 font-mono text-sm">{{ post.content }}</textarea>
|
||||
</div>
|
||||
<div class="flex items-center space-x-3">
|
||||
<button type="submit"
|
||||
class="inline-flex items-center px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-md transition-colors">
|
||||
<svg class="w-4 h-4 mr-2"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
Save changes
|
||||
</button>
|
||||
<button type="button"
|
||||
onclick="document.getElementById('edit-form').classList.add('hidden')"
|
||||
class="inline-flex items-center px-4 py-2 bg-gray-200 hover:bg-gray-300 text-gray-700 font-medium rounded-md transition-colors">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<div id="edit-messages" class="mt-6"></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{% if session_username.as_deref() == Some(post.author) %}
|
||||
<div id="edit-form" class="hidden bg-gray-50 border border-gray-200 rounded-lg p-6">
|
||||
<h2 class="text-xl font-bold text-gray-900 mb-4">Edit post</h2>
|
||||
<form hx-put="/posts/{{ post.post_id }}"
|
||||
hx-target="#edit-messages"
|
||||
hx-swap="innerHTML">
|
||||
<div class="mb-4">
|
||||
<label for="title" class="block text-sm font-medium text-gray-700 mb-2">Title</label>
|
||||
<input type="text"
|
||||
id="title"
|
||||
name="title"
|
||||
value="{{ post.title }}"
|
||||
required
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label for="content" class="block text-sm font-medium text-gray-700 mb-2">Content (markdown)</label>
|
||||
<textarea id="content"
|
||||
name="content"
|
||||
rows="12"
|
||||
required
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 font-mono text-sm">{{ post.content }}</textarea>
|
||||
</div>
|
||||
<div class="flex items-center space-x-3">
|
||||
<button type="submit"
|
||||
class="inline-flex items-center px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-md transition-colors">
|
||||
<svg class="w-4 h-4 mr-2"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M5 13l4 4L19 7"/>
|
||||
</svg>
|
||||
Save changes
|
||||
</button>
|
||||
<button type="button"
|
||||
onclick="document.getElementById('edit-form').classList.add('hidden')"
|
||||
class="inline-flex items-center px-4 py-2 bg-gray-200 hover:bg-gray-300 text-gray-700 font-medium rounded-md transition-colors">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<div id="edit-messages" class="mt-6"></div>
|
||||
{% endif %}
|
||||
<div id="content-display" class="prose-compact">{{ post_html | safe }}</div>
|
||||
</article>
|
||||
<div class="mt-8">{% include "posts/comments/list.html" %}</div>
|
||||
<div class="mt-8 bg-gradient-to-r from-blue-600 to-indigo-700 rounded-lg shadow-lg text-white p-8 text-center">
|
||||
<h3 class="text-2xl font-bold mb-2">Enjoyed this post?</h3>
|
||||
<p class="text-blue-100 mb-4">Subscribe to my newsletter for more insights on Rust backend development.</p>
|
||||
<a href="/#newsletter-signup"
|
||||
class="inline-block bg-white text-blue-600 hover:bg-gray-100 font-semibold py-3 px-6 rounded-md transition-colors">
|
||||
Subscribe
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div id="content-display" class="prose-compact">{{ post_html | safe }}</div>
|
||||
</article>
|
||||
<div class="mt-8">{% include "posts/comments/list.html" %}</div>
|
||||
<div class="mt-8 bg-gradient-to-r from-blue-600 to-indigo-700 rounded-lg shadow-lg text-white p-8 text-center">
|
||||
<h3 class="text-2xl font-bold mb-2">Enjoyed this post?</h3>
|
||||
<p class="text-blue-100 mb-4">Subscribe to my newsletter for more insights on Rust backend development.</p>
|
||||
<a href="/#newsletter-signup"
|
||||
class="inline-block bg-white text-blue-600 hover:bg-gray-100 font-semibold py-3 px-6 rounded-md transition-colors">
|
||||
Subscribe
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -21,7 +21,7 @@ async fn visitor_can_leave_a_comment(connection_pool: PgPool) {
|
||||
"idempotency_key": "key",
|
||||
});
|
||||
app.post_comment(&post_id, &comment_body).await;
|
||||
let post = app.get_post_html(post_id).await;
|
||||
let post = app.get_post_html(&post_id).await;
|
||||
assert!(post.contains(comment_author));
|
||||
assert!(post.contains(comment_content));
|
||||
}
|
||||
@@ -44,7 +44,7 @@ async fn visitor_can_comment_anonymously(connection_pool: PgPool) {
|
||||
"idempotency_key": "key",
|
||||
});
|
||||
app.post_comment(&post_id, &comment_body).await;
|
||||
let post = app.get_post_html(post_id).await;
|
||||
let post = app.get_post_html(&post_id).await;
|
||||
assert!(post.contains("Anonymous"));
|
||||
assert!(post.contains(comment_content));
|
||||
}
|
||||
|
||||
@@ -289,6 +289,18 @@ impl TestApp {
|
||||
self.get_admin_dashboard().await.text().await.unwrap()
|
||||
}
|
||||
|
||||
pub async fn edit_post<Body>(&self, body: &Body, post_id: &Uuid) -> reqwest::Response
|
||||
where
|
||||
Body: serde::Serialize,
|
||||
{
|
||||
self.api_client
|
||||
.put(format!("{}/posts/{}", self.address, post_id))
|
||||
.form(body)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to execute request")
|
||||
}
|
||||
|
||||
pub async fn get_posts(&self) -> reqwest::Response {
|
||||
self.api_client
|
||||
.get(format!("{}/posts", &self.address))
|
||||
@@ -301,7 +313,7 @@ impl TestApp {
|
||||
self.get_posts().await.text().await.unwrap()
|
||||
}
|
||||
|
||||
pub async fn get_post(&self, post_id: Uuid) -> reqwest::Response {
|
||||
pub async fn get_post(&self, post_id: &Uuid) -> reqwest::Response {
|
||||
self.api_client
|
||||
.get(format!("{}/posts/{}", &self.address, post_id))
|
||||
.send()
|
||||
@@ -309,7 +321,7 @@ impl TestApp {
|
||||
.expect("Failed to execute request")
|
||||
}
|
||||
|
||||
pub async fn get_post_html(&self, post_id: Uuid) -> String {
|
||||
pub async fn get_post_html(&self, post_id: &Uuid) -> String {
|
||||
self.get_post(post_id).await.text().await.unwrap()
|
||||
}
|
||||
|
||||
|
||||
@@ -144,7 +144,7 @@ async fn new_posts_are_visible_on_the_website(connection_pool: PgPool) {
|
||||
.fetch_one(&app.connection_pool)
|
||||
.await
|
||||
.unwrap();
|
||||
let html = app.get_post_html(post.post_id).await;
|
||||
let html = app.get_post_html(&post.post_id).await;
|
||||
assert!(html.contains(&title));
|
||||
}
|
||||
|
||||
@@ -171,7 +171,7 @@ async fn visitor_can_read_a_blog_post(connection_pool: PgPool) {
|
||||
.fetch_one(&app.connection_pool)
|
||||
.await
|
||||
.unwrap();
|
||||
let html = app.get_post_html(post.post_id).await;
|
||||
let html = app.get_post_html(&post.post_id).await;
|
||||
assert!(html.contains(&title));
|
||||
}
|
||||
|
||||
@@ -197,7 +197,7 @@ async fn a_deleted_blog_post_returns_404(connection_pool: PgPool) {
|
||||
|
||||
app.delete_post(post.post_id).await;
|
||||
|
||||
let html = app.get_post_html(post.post_id).await;
|
||||
let html = app.get_post_html(&post.post_id).await;
|
||||
assert!(html.contains("Not Found"));
|
||||
}
|
||||
|
||||
@@ -234,3 +234,109 @@ async fn clicking_the_notification_link_marks_the_email_as_opened(connection_poo
|
||||
.opened
|
||||
);
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
async fn only_post_author_can_access_the_edit_form(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 login_body = serde_json::json!({
|
||||
"username": username,
|
||||
"password": password
|
||||
});
|
||||
app.post_login(&login_body).await;
|
||||
app.post_create_post(&fake_post_body()).await;
|
||||
let post_id = sqlx::query!("SELECT post_id FROM posts")
|
||||
.fetch_one(&app.connection_pool)
|
||||
.await
|
||||
.unwrap()
|
||||
.post_id;
|
||||
let html = app.get_post_html(&post_id).await;
|
||||
assert!(html.contains("Edit"));
|
||||
|
||||
app.logout().await;
|
||||
app.admin_login().await;
|
||||
let html = app.get_post_html(&post_id).await;
|
||||
assert!(!html.contains("Edit"));
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
async fn only_post_author_can_edit_post(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 login_body = serde_json::json!({
|
||||
"username": username,
|
||||
"password": password
|
||||
});
|
||||
app.post_login(&login_body).await;
|
||||
app.post_create_post(&fake_post_body()).await;
|
||||
let post_id = sqlx::query!("SELECT post_id FROM posts")
|
||||
.fetch_one(&app.connection_pool)
|
||||
.await
|
||||
.unwrap()
|
||||
.post_id;
|
||||
|
||||
let new_title = "Stunning new title";
|
||||
let new_content = "Astonishing content";
|
||||
let edit_body = serde_json::json!({
|
||||
"title": new_title,
|
||||
"content": new_content,
|
||||
});
|
||||
let response = app.edit_post(&edit_body, &post_id).await;
|
||||
let text = response.text().await.unwrap();
|
||||
assert!(text.contains("Your changes have been saved"));
|
||||
let text = app.get_post_html(&post_id).await;
|
||||
assert!(text.contains(new_title));
|
||||
assert!(text.contains(new_content));
|
||||
|
||||
app.logout().await;
|
||||
app.admin_login().await;
|
||||
let response = app.edit_post(&edit_body, &post_id).await;
|
||||
let text = response.text().await.unwrap();
|
||||
assert!(text.contains("You are not authorized."));
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
async fn invalid_fields_are_rejected(connection_pool: PgPool) {
|
||||
let app = TestApp::spawn(connection_pool).await;
|
||||
app.admin_login().await;
|
||||
app.post_create_post(&fake_post_body()).await;
|
||||
let post_id = sqlx::query!("SELECT post_id FROM posts")
|
||||
.fetch_one(&app.connection_pool)
|
||||
.await
|
||||
.unwrap()
|
||||
.post_id;
|
||||
|
||||
let test_cases = [
|
||||
(
|
||||
serde_json::json!({
|
||||
"title": "",
|
||||
"content": "content"
|
||||
}),
|
||||
"Title must be at least one character",
|
||||
"title was empty",
|
||||
),
|
||||
(
|
||||
serde_json::json!({
|
||||
"title": "Title",
|
||||
"content": ""
|
||||
}),
|
||||
"Content must be at least one character",
|
||||
"content was empty",
|
||||
),
|
||||
];
|
||||
for (invalid_body, expected_error_message, explaination) in test_cases {
|
||||
let response = app.edit_post(&invalid_body, &post_id).await;
|
||||
let text = response.text().await.unwrap();
|
||||
assert!(
|
||||
text.contains(expected_error_message),
|
||||
"The API did not reject the changes when the {}",
|
||||
explaination
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user