diff --git a/blahd/src/feed.rs b/blahd/src/feed.rs
index 76678e0..d359ef8 100644
--- a/blahd/src/feed.rs
+++ b/blahd/src/feed.rs
@@ -195,7 +195,7 @@ impl fmt::Display for AtomFeed {
{author}
{esc_content}
- "#
+"#
)?;
}
diff --git a/blahd/tests/webapi.rs b/blahd/tests/webapi.rs
index c693582..da2f066 100644
--- a/blahd/tests/webapi.rs
+++ b/blahd/tests/webapi.rs
@@ -763,11 +763,17 @@ async fn room_chat_post_read(server: Server) {
assert_eq!(msgs, RoomMsgs::default());
}
+#[cfg(feature = "unsafe_use_mock_instant_for_testing")]
#[rstest]
#[case::json("json")]
#[case::atom("atom")]
#[tokio::test]
async fn room_feed(server: Server, #[case] typ: &'static str) {
+ use mock_instant::thread_local::MockClock;
+
+ MockClock::set_time(Duration::ZERO);
+ MockClock::set_system_time(Duration::ZERO);
+
// Only public readable rooms provides feed. Not even for public joinable ones.
let rid_need_join = server
.create_room(&ALICE, RoomAttrs::PUBLIC_JOINABLE, "not so public")
@@ -827,7 +833,7 @@ async fn room_feed(server: Server, #[case] typ: &'static str) {
}
// Post more chats.
- let cid2 = server.post_chat(rid, &BOB, "b1").await.unwrap().cid;
+ server.post_chat(rid, &BOB, "b1").await.unwrap();
let cid3 = server.post_chat(rid, &BOB, "b2").await.unwrap().cid;
let etag_last = format!("\"{cid3}\"");
@@ -844,29 +850,102 @@ async fn room_feed(server: Server, #[case] typ: &'static str) {
if typ == "json" {
let feed = serde_json::from_str::(&resp).unwrap();
- // TODO: Ideally we should assert on the result, but it contains time and random id currently.
- assert_eq!(feed["title"].as_str().unwrap(), "public");
- assert_eq!(feed["items"].as_array().unwrap().len(), 2);
- let feed_url = format!("{BASE_URL}/_blah/room/{rid}/feed.json");
- assert_eq!(feed["feed_url"].as_str().unwrap(), feed_url,);
- assert_eq!(
- feed["next_url"].as_str().unwrap(),
- format!("{feed_url}?skipToken={cid2}&top=2"),
- );
+ let got_pretty = serde_json::to_string_pretty(&feed).unwrap();
+ let expect = expect![[r#"
+ {
+ "feed_url": "http://base.example.com/_blah/room/2/feed.json",
+ "items": [
+ {
+ "authors": [
+ {
+ "name": "2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12"
+ }
+ ],
+ "content_html": "b2",
+ "date_published": "1970-01-01T00:00:00Z",
+ "id": "tag:base.example.com,1970:blah/msg/5"
+ },
+ {
+ "authors": [
+ {
+ "name": "2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12"
+ }
+ ],
+ "content_html": "b1",
+ "date_published": "1970-01-01T00:00:00Z",
+ "id": "tag:base.example.com,1970:blah/msg/4"
+ }
+ ],
+ "next_url": "http://base.example.com/_blah/room/2/feed.json?skipToken=4&top=2",
+ "title": "public",
+ "version": "https://jsonfeed.org/version/1.1"
+ }"#]];
+ expect.assert_eq(&got_pretty);
+ let next_url = feed["next_url"]
+ .as_str()
+ .unwrap()
+ .strip_prefix(BASE_URL)
+ .unwrap()
+ .strip_prefix("/_blah")
+ .unwrap();
let feed2 = server
- .get::(
- &format!("/room/{rid}/feed.json?skipToken={cid2}&top=2"),
- None,
- )
+ .get::(next_url, None)
.await
.unwrap();
- let items = feed2["items"].as_array().unwrap();
- assert_eq!(items.len(), 1);
- assert_eq!(items[0]["content_html"].as_str().unwrap(), "a");
+ let got2_pretty = serde_json::to_string_pretty(&feed2).unwrap();
+
+ expect![[r#"
+ {
+ "feed_url": "http://base.example.com/_blah/room/2/feed.json",
+ "items": [
+ {
+ "authors": [
+ {
+ "name": "db995fe25169d141cab9bbba92baa01f9f2e1ece7df4cb2ac05190f37fcc1f9d"
+ }
+ ],
+ "content_html": "a",
+ "date_published": "1970-01-01T00:00:00Z",
+ "id": "tag:base.example.com,1970:blah/msg/3"
+ }
+ ],
+ "title": "public",
+ "version": "https://jsonfeed.org/version/1.1"
+ }"#]]
+ .assert_eq(&got2_pretty);
} else {
assert!(resp.starts_with(r#""#));
- assert_eq!(resp.matches("").count(), 2);
+
+ expect![[r#"
+
+
+ tag:base.example.com,1970:blah/room/2
+ public
+ 1970-01-01T00:00:00Z
+
+
+
+
+ tag:base.example.com,1970:blah/msg/5
+ b2
+ 1970-01-01T00:00:00Z
+ 1970-01-01T00:00:00Z
+ 2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12
+ b2
+
+
+
+ tag:base.example.com,1970:blah/msg/4
+ b1
+ 1970-01-01T00:00:00Z
+ 1970-01-01T00:00:00Z
+ 2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12
+ b1
+
+
+
+ "#]].assert_eq(&resp);
}
}
@@ -1339,6 +1418,84 @@ unsafe_allow_id_url_single_label = {allow_single_label}
ret.expect_api_err(StatusCode::FORBIDDEN, "disabled");
}
+#[cfg(feature = "unsafe_use_mock_instant_for_testing")]
+#[rstest]
+#[tokio::test]
+async fn register_nonce() {
+ use mock_instant::thread_local::MockClock;
+
+ // Matches the config below.
+ const NONCE_PERIOD: Duration = Duration::from_secs(10);
+
+ let config = |_port| {
+ format!(
+ r#"
+base_url="{BASE_URL}"
+[register]
+enable_public = true
+difficulty = 64 # Should fail the challenge if nonce matches.
+nonce_rotate_secs = 10
+unsafe_allow_id_url_http = true
+ "#
+ )
+ };
+ let db_config = blahd::DatabaseConfig {
+ in_memory: true,
+ ..Default::default()
+ };
+ MockClock::set_time(Duration::ZERO);
+ let server = server_with(Database::open(&db_config).unwrap(), &config);
+ // Avoid hitting the period boundary.
+ MockClock::advance(Duration::from_secs(1));
+
+ let (nonce0, _diff) = server.get_me(Some(&CAROL)).await.unwrap_err().unwrap();
+
+ let register = |nonce: u32, expect_ok| {
+ let req = UserRegisterPayload {
+ id_key: CAROL.pubkeys.id_key.clone(),
+ server_url: BASE_URL.parse().unwrap(),
+ id_url: BASE_URL.parse().unwrap(),
+ challenge_nonce: nonce,
+ }
+ .sign_msg(&CAROL.pubkeys.id_key, &CAROL.act_priv)
+ .unwrap();
+ let expect_err = if expect_ok {
+ "hash challenge failed"
+ } else {
+ "invalid challenge nonce"
+ };
+ async {
+ server
+ .request::<_, ()>(Method::POST, "/user/me", None, Some(req))
+ .await
+ .expect_invalid_request(expect_err);
+ }
+ };
+
+ // Valid nonce.
+ register(nonce0, true).await;
+
+ // After one nonce period, a new nonce is generated.
+ MockClock::advance(NONCE_PERIOD);
+ let (nonce1, _diff) = server.get_me(Some(&CAROL)).await.unwrap_err().unwrap();
+ assert_ne!(nonce0, nonce1);
+
+ // Both nonce are valid yet, because it's in the grace period.
+ register(nonce0, true).await;
+ register(nonce1, true).await;
+
+ // After one period, another new nonce is generated.
+ MockClock::advance(NONCE_PERIOD);
+ let (nonce2, _diff) = server.get_me(Some(&CAROL)).await.unwrap_err().unwrap();
+ assert_ne!(nonce0, nonce2);
+ assert_ne!(nonce1, nonce2);
+
+ // The oldest nonce expired. The last two noncesa re valid.
+ register(nonce0, false).await;
+ register(nonce1, true).await;
+ register(nonce2, true).await;
+}
+
#[rstest]
#[tokio::test]
async fn event(server: Server) {