feat: serve gopher with dot-stuffing and a synthetic root menu
This commit is contained in:
parent
1de4674b09
commit
dc2114a4d2
8 changed files with 334 additions and 11 deletions
|
|
@ -27,6 +27,8 @@ impl Server {
|
|||
write(dir.path(), "one/about.md", "# About\n\nAbout the first.\n");
|
||||
write(dir.path(), "one/img.png", "not really a png");
|
||||
write(dir.path(), "one/.itsybitsy.toml", "[defaults]\nmax_age = 120\n");
|
||||
// A line starting with a dot, which Gopher has to double.
|
||||
write(dir.path(), "one/dotted.md", "# Dotted\n\n .hidden\n");
|
||||
write(dir.path(), "two/index.md", "# Two\n\nSecond site.\n");
|
||||
write(
|
||||
dir.path(),
|
||||
|
|
@ -69,6 +71,12 @@ protocol = "nex"
|
|||
bind = "127.0.0.1:0"
|
||||
formats = ["gemtext"]
|
||||
site = "two"
|
||||
|
||||
[listener.gopher]
|
||||
protocol = "gopher"
|
||||
bind = "127.0.0.1:0"
|
||||
formats = ["gemtext"]
|
||||
site = "one"
|
||||
"#,
|
||||
one = dir.path().join("one").to_str().unwrap(),
|
||||
two = dir.path().join("two").to_str().unwrap(),
|
||||
|
|
@ -408,6 +416,95 @@ fn nothing_outside_the_root_is_reachable_over_any_protocol() {
|
|||
assert_eq!(server.send("nex", b"/.itsybitsy.toml\r\n"), "Not found\n");
|
||||
}
|
||||
|
||||
// -- Gopher --------------------------------------------------------------
|
||||
//
|
||||
// Ported from smolweb's tests/test_gopher.py, where the exact wire bytes are the
|
||||
// assertion: Gopher has no status line, so framing is all a client has.
|
||||
|
||||
#[test]
|
||||
fn an_empty_selector_gets_a_one_item_menu() {
|
||||
// RFC 4266: a prefix-less `gopher://host/` means item type 1, so a client
|
||||
// asking for the root expects a menu rather than a text file.
|
||||
let server = Server::start();
|
||||
let port = server.addr("gopher").port();
|
||||
let response = server.send("gopher", b"\r\n");
|
||||
assert_eq!(response, format!("0Home\t/\tone.test\t{port}\r\n.\r\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_root_selector_serves_the_document_as_text() {
|
||||
let server = Server::start();
|
||||
let response = server.send("gopher", b"/\r\n");
|
||||
assert!(response.starts_with("# One"), "{response:?}");
|
||||
assert!(response.ends_with(".\r\n"), "{response:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_text_item_ends_with_the_lone_dot_terminator() {
|
||||
let server = Server::start();
|
||||
let response = server.send("gopher", b"/about\r\n");
|
||||
assert!(response.contains("About the first."), "{response:?}");
|
||||
assert!(response.ends_with("\n.\r\n"), "{response:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_missing_selector_is_a_well_formed_text_item() {
|
||||
// No status to report with, so the error is the item's content. smolweb says
|
||||
// "Not found." here and "Not found" over HTTP and Nex; one wording is used
|
||||
// across every protocol instead.
|
||||
let server = Server::start();
|
||||
assert_eq!(server.send("gopher", b"/nope\r\n"), "Not found\n.\r\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_selector_without_a_leading_slash_still_resolves() {
|
||||
// Clients built from a menu line send the selector verbatim, and a menu may
|
||||
// carry it either way.
|
||||
let server = Server::start();
|
||||
assert!(server.send("gopher", b"about\r\n").contains("About the first."));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_tab_separated_search_term_is_not_part_of_the_selector() {
|
||||
let server = Server::start();
|
||||
let response = server.send("gopher", b"/about\tignored words\r\n");
|
||||
assert!(response.contains("About the first."), "{response:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gopher_resolves_a_redirect_server_side() {
|
||||
// There is no redirect item type to bounce with.
|
||||
let server = Server::start();
|
||||
let response = server.send("gopher", b"/about.md\r\n");
|
||||
assert!(response.contains("About the first."), "{response:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_binary_item_is_sent_raw_with_no_terminator() {
|
||||
// Dot-stuffing it would corrupt it, and a terminator would become part of
|
||||
// the file.
|
||||
let server = Server::start();
|
||||
let response = server.send("gopher", b"/img.png\r\n");
|
||||
assert_eq!(response, "not really a png");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_leading_dot_in_the_content_is_doubled() {
|
||||
let server = Server::start();
|
||||
let response = server.send("gopher", b"/dotted\r\n");
|
||||
// The source line is `.hidden`; undoubled it would end the item early.
|
||||
assert!(response.contains("\n..hidden"), "{response:?}");
|
||||
assert!(response.ends_with(".\r\n"), "{response:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_oversized_selector_is_refused() {
|
||||
let server = Server::start();
|
||||
let mut request = vec![b'a'; 600];
|
||||
request.extend_from_slice(b"\r\n");
|
||||
assert_eq!(server.send("gopher", &request), "Bad request\n.\r\n");
|
||||
}
|
||||
|
||||
// -- Sub-documents -------------------------------------------------------
|
||||
|
||||
/// A page that paginates addresses sub-documents under its own URL. Only a
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue