179 lines
6.5 KiB
Rust
179 lines
6.5 KiB
Rust
//! The state file: a first start, surviving a restart, its limits, and refusing a damaged file
|
|
//! instead of guessing (M4a spec, section 9). Do not edit.
|
|
|
|
#[path = "support/tmp.rs"]
|
|
mod tmp;
|
|
|
|
use std::os::unix::fs::PermissionsExt;
|
|
|
|
use gatewayd::state::{InFlight, RECENT_KEPT, State, StateError, THREADS_KEPT};
|
|
use tmp::TempDir;
|
|
|
|
const CHAN: &str = "c0000000000000000000000000";
|
|
const DM: &str = "d0000000000000000000000000";
|
|
|
|
fn id(n: usize) -> String {
|
|
format!("p{n:025}")
|
|
}
|
|
|
|
fn turn(n: usize) -> InFlight {
|
|
InFlight {
|
|
session: format!("mm-{}", id(n)),
|
|
channel: DM.to_string(),
|
|
root: id(n),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn a_first_start_then_a_restart() {
|
|
let dir = TempDir::new("state-restart");
|
|
let path = dir.path().join("gateway/state.json");
|
|
let mut s = State::load(&path).unwrap();
|
|
assert!(!path.exists(), "loading writes nothing");
|
|
assert_eq!(
|
|
(s.since(CHAN), s.channels().len(), s.seen(&id(1))),
|
|
(None, 0, false)
|
|
);
|
|
s.handled(&id(1), CHAN, 2000).unwrap();
|
|
s.handled(&id(2), CHAN, 1500).unwrap();
|
|
s.mark(DM, 3000).unwrap();
|
|
s.mark(DM, 9000).unwrap();
|
|
s.join_thread(&id(1)).unwrap();
|
|
s.start_turn(turn(1)).unwrap();
|
|
s.start_turn(turn(2)).unwrap();
|
|
s.end_turn(&format!("mm-{}", id(2))).unwrap();
|
|
|
|
let mode = std::fs::metadata(&path).unwrap().permissions().mode() & 0o777;
|
|
let dir_mode = std::fs::metadata(path.parent().unwrap())
|
|
.unwrap()
|
|
.permissions()
|
|
.mode()
|
|
& 0o777;
|
|
assert_eq!((mode, dir_mode), (0o600, 0o700));
|
|
assert!(!dir.path().join("gateway/state.json.tmp").exists());
|
|
|
|
let mut again = State::load(&path).unwrap();
|
|
assert_eq!(again.since(CHAN), Some(2000), "the mark never moves back");
|
|
assert_eq!(
|
|
again.since(DM),
|
|
Some(3000),
|
|
"mark only sets a channel without one"
|
|
);
|
|
assert_eq!(again.channels(), [CHAN, DM]);
|
|
assert!(again.seen(&id(1)) && again.seen(&id(2)));
|
|
assert!(again.knows_thread(&id(1)) && !again.knows_thread(&id(2)));
|
|
assert_eq!(again.take_in_flight().unwrap(), [turn(1)]);
|
|
assert!(
|
|
State::load(&path)
|
|
.unwrap()
|
|
.take_in_flight()
|
|
.unwrap()
|
|
.is_empty(),
|
|
"taking is saved"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn only_the_newest_posts_and_threads_are_kept() {
|
|
let dir = TempDir::new("state-limits");
|
|
// A full file to start from: posts 0.. and threads 0.., at their limits.
|
|
let recent: Vec<String> = (0..RECENT_KEPT).map(id).collect();
|
|
let threads: Vec<String> = (0..THREADS_KEPT).map(id).collect();
|
|
let full =
|
|
serde_json::json!({"channels": {}, "recent": recent, "threads": threads, "in_flight": []});
|
|
let path = dir.write("state.json", &full.to_string());
|
|
let mut s = State::load(&path).unwrap();
|
|
s.handled(&id(RECENT_KEPT), CHAN, 1).unwrap();
|
|
s.handled(&id(RECENT_KEPT + 1), CHAN, 1).unwrap();
|
|
s.handled(&id(RECENT_KEPT + 1), CHAN, 1).unwrap();
|
|
s.join_thread(&id(THREADS_KEPT)).unwrap();
|
|
s.join_thread(&id(3)).unwrap();
|
|
let s = State::load(&path).unwrap();
|
|
assert!(!s.seen(&id(0)) && !s.seen(&id(1)) && s.seen(&id(2)) && s.seen(&id(RECENT_KEPT + 1)));
|
|
assert!(!s.knows_thread(&id(0)) && s.knows_thread(&id(1)) && s.knows_thread(&id(THREADS_KEPT)));
|
|
let text = std::fs::read_to_string(&path).unwrap();
|
|
let v: serde_json::Value = serde_json::from_str(&text).unwrap();
|
|
assert_eq!(
|
|
v["recent"].as_array().unwrap().len(),
|
|
RECENT_KEPT,
|
|
"a repeat is not stored twice"
|
|
);
|
|
assert_eq!(v["threads"].as_array().unwrap().len(), THREADS_KEPT);
|
|
}
|
|
|
|
#[test]
|
|
fn a_damaged_file_stops_with_its_pointer() {
|
|
let dir = TempDir::new("state-damaged");
|
|
let good_turn = r#"{"session":"mm-p0000000000000000000000001","channel":"d0000000000000000000000000","root":"p0000000000000000000000001"}"#;
|
|
let cases = [
|
|
"".to_string(),
|
|
"{".to_string(),
|
|
"[]".to_string(),
|
|
r#"{"channels":{},"recent":[],"threads":[]}"#.to_string(),
|
|
r#"{"channels":{},"recent":[],"threads":[],"in_flight":[],"extra":1}"#.to_string(),
|
|
r#"{"channels":{"../x":1},"recent":[],"threads":[],"in_flight":[]}"#.to_string(),
|
|
r#"{"channels":{},"recent":["short"],"threads":[],"in_flight":[]}"#.to_string(),
|
|
r#"{"channels":{},"recent":[],"threads":["P0000000000000000000000000"],"in_flight":[]}"#
|
|
.to_string(),
|
|
format!(
|
|
r#"{{"channels":{{}},"recent":[],"threads":[],"in_flight":[{}]}}"#,
|
|
good_turn.replace("mm-", "xx-")
|
|
),
|
|
format!(
|
|
r#"{{"channels":{{}},"recent":[],"threads":[],"in_flight":[{}]}}"#,
|
|
good_turn.replace("\"d0", "\"D0")
|
|
),
|
|
];
|
|
for (n, text) in cases.iter().enumerate() {
|
|
let path = dir.write(&format!("s{n}.json"), text);
|
|
match State::load(&path) {
|
|
Err(e @ StateError::Read(..)) => {
|
|
let message = e.to_string();
|
|
assert!(
|
|
message.starts_with(&path.display().to_string()),
|
|
"{message}"
|
|
);
|
|
assert!(
|
|
message.ends_with("\nsee docs/runbook.md#gateway-state-damaged"),
|
|
"{message}"
|
|
);
|
|
}
|
|
Err(e) => panic!("{text}: {e}"),
|
|
Ok(_) => panic!("accepted: {text}"),
|
|
}
|
|
}
|
|
let ok = dir.write(
|
|
"ok.json",
|
|
&format!(
|
|
r#"{{"channels":{{"{CHAN}":5}},"recent":[],"threads":[],"in_flight":[{good_turn}]}}"#
|
|
),
|
|
);
|
|
assert_eq!(State::load(&ok).unwrap().since(CHAN), Some(5));
|
|
std::fs::create_dir(dir.path().join("adir")).unwrap();
|
|
assert!(matches!(
|
|
State::load(&dir.path().join("adir")),
|
|
Err(StateError::Read(..))
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn a_failed_write_is_an_error_and_keeps_the_old_file() {
|
|
let dir = TempDir::new("state-readonly");
|
|
let sub = dir.path().join("gateway");
|
|
let path = sub.join("state.json");
|
|
let mut s = State::load(&path).unwrap();
|
|
s.handled(&id(1), CHAN, 5).unwrap();
|
|
let before = std::fs::read(&path).unwrap();
|
|
std::fs::set_permissions(&sub, std::fs::Permissions::from_mode(0o500)).unwrap();
|
|
let got = s.handled(&id(2), CHAN, 6);
|
|
std::fs::set_permissions(&sub, std::fs::Permissions::from_mode(0o700)).unwrap();
|
|
let e = got.unwrap_err();
|
|
assert!(matches!(e, StateError::Write(..)), "{e}");
|
|
assert!(
|
|
e.to_string()
|
|
.ends_with("see docs/runbook.md#gateway-state-damaged"),
|
|
"{e}"
|
|
);
|
|
assert_eq!(std::fs::read(&path).unwrap(), before);
|
|
}
|