Body 结构

为了发送新闻通讯,我们需要了解哪些信息?

如果我们力求使其尽可能简洁:

  • 标题,用作电子邮件主题
  • 内容,以 HTML 和纯文本形式呈现,以满足所有电子邮件客户端的需求。

我们可以使用派生自 serde::Deserialize 的结构体来编码我们的需求,就像我们在 POST /subscriptions 中使用 FormData 进行编码一样。

//! src/routes/newsletters.rs
// [...]
#[derive(serde::Deserialize)]
pub struct BodyData {
    title: String,
    content: Content,
}

#[derive(serde::Deserialize)]
pub struct Content {
    html: String,
    text: String,
}

由于 BodyData 中的所有字段类型都实现了 serde::Deserialize, 因此 serde 对我们的嵌套布局没有任何问题。然后,我们可以使用 actix-web 提取器从传入的请求正文中解析出 BodyData。只有一个问题需要回答:我们使用什么序列化格式?

对于 POST /subscriptions, 由于我们处理的是 HTML 表单,我们使用 application/x-www-form-urlencode 作为 Content-Type。 对于 POST /newsletters, 我们不受网页中嵌入表单的约束: 我们将使用 JSON,这是构建 REST API 时的常见选择。

相应的提取器是 actix_web::web::Json:

//! src/routes/newsletters.rs
// [...]
use actix_web::web;

// We are prefixing `body` with a `_` to avoid
// a compiler warning about unused arguments
pub async fn publish_newsletter(_body: web::Json<BodyData>) -> HttpResponse {
    HttpResponse::Ok().finish()
}

测试不合法输入

信任但要验证: 让我们添加一个新的测试用例, 在 POST /newsletters 端点抛出无效数据。

//! tests/api/newsletter.rs
// [...]

#[tokio::test]
async fn newsletters_returns_400_for_invalid_data() {
    // Arrange
    let app = spawn_app().await;
    let test_cases = vec![
        (
            serde_json::json!({
            "content": {
            "text": "Newsletter body as plain text",
            "html": "<p>Newsletter body as HTML</p>",
            }
            }),
            "missing title",
        ),
        (
            serde_json::json!({"title": "Newsletter!"}),
            "missing content",
        ),
    ];

    for (invalid_body, error_message) in test_cases {
        let response = reqwest::Client::new()
            .post(&format!("{}/newsletters", &app.address))
            .json(&invalid_body)
            .send()
            .await
            .expect("Failed to execute request.");

        // Assert
        assert_eq!(
            400,
            response.status().as_u16(),
            "The API did not fail with 400 Bad Request when the payload was {}.",
            error_message
        )
    }
}

新的测试通过了——如果你愿意,还可以添加一些用例。

让我们抓住机会稍微重构一下,删除一些重复的代码——我们可以将触发 POST /newsletters 请求的逻辑提取到 TestApp 上的一个共享辅助方法中,就像我们对 POST /subscriptions 所做的那样:

//! tests/api/helpers.rs
// [...]

impl TestApp {
    // [...]
    pub async fn post_newsletters(&self, body: serde_json::Value) -> reqwest::Response {
        reqwest::Client::new()
            .post(&format!("{}/newsletters", &self.address))
            .json(&body)
            .send()
            .await
            .expect("Failed to execute request.")
    }
}
//! tests/api/newsletter.rs
// [...]

async fn newsletters_are_delivered_to_confirmed_subscribers() {
    // [...]
    let response = app.post_newsletters(newsletter_request_body).await;

    // [...]
}

async fn newsletters_returns_400_for_invalid_data() {
    // Arrange
    // [...]

    for (invalid_body, error_message) in test_cases {
        let response = app.post_newsletters(invalid_body).await;

        // Assert
        // [...]
    }
}

async fn newsletters_are_not_delivered_to_unconfirmed_subscribers() {
    // Arrange
    // [...]
    
    let response = app.post_newsletters(newsletter_request_body).await;

    // Assert
    // [...]
}