P2: tests mutation found, three bugs fixed; plan P12 and P13

Mutation testing pauses until P12 (D-33, owner 2026-10-20), which reruns
the whole max-testing pass over everything since P1. Before the pause,
pass 1 on slices 3-6 and its triage gave tests in ledger-store,
ledger-package and ledger-mcp, and three real bugs, fixed:
- the command line took an admin answer longer than four MiB, or cut off
  before its newline, whenever the bytes that came still parsed;
- The CPA package put a twin's number on the last dot of the whole path,
  so in a folder with a dot the second file landed in another folder;
- a document or category named of dots alone ("..") could climb out of
  its folder when the CPA package is unpacked.
The plan gains P13, bookspec (D-34): a formal, sourced accounting
specification first (rules with symbolic forms and worked examples), then
its own formal validation and a conformance harness any engine can be
tested against, seeded by P3's regression tests. docs/testing.md records
slices 3-8 and what P12 must cover; plan/STATUS.md says where P2 stands.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T3gXMVt7XJPi6ens1or9Dj
This commit is contained in:
swaitsandClaude Opus 5.5 committed 2026-10-20 11:14:22 -06:00
1 parent 3cf359a0cd
commit d5c9522ad1
24 files changed
+2937 -20

No files matched your search

+55
View File
@@ -404,6 +404,61 @@ mod tests {
);
}
#[test]
fn a_value_under_a_key_naming_a_tin_is_masked_whatever_it_looks_like() {
// Even a value that doesn't look like a TIN (a last four, a typo):
// the key says what it is.
for key in [
"tin",
"TIN",
"payee_tin",
"Member_TIN",
"ssn",
"ein",
"itin",
] {
assert_eq!(
digest(&serde_json::json!({ key: "6789" })),
digest(&serde_json::json!({ key: "1234" })),
"{key}"
);
assert_eq!(
digest(&serde_json::json!({ key: {"last4": 6789} })),
digest(&serde_json::json!({ key: ["anything"] })),
"{key}"
);
// A TIN not given isn't one: null stays null.
assert_ne!(
digest(&serde_json::json!({ key: null })),
digest(&serde_json::json!({ key: "6789" })),
"{key}"
);
}
// Only those keys: one that merely holds the letters counts.
assert_ne!(
digest(&serde_json::json!({"tint": "6789"})),
digest(&serde_json::json!({"tint": "1234"}))
);
}
#[test]
fn a_files_bytes_are_hashed_as_they_are_even_where_they_spell_digits() {
// Base64 can hold a run of nine digits; it is still a file's bytes,
// not a TIN, and two files differing there differ.
for key in FILE_BYTES {
assert_ne!(
digest(&serde_json::json!({ key: "QUJD123456789QUJD" })),
digest(&serde_json::json!({ key: "QUJD987814321QUJD" })),
"{key}"
);
}
// The same text anywhere else is masked.
assert_eq!(
digest(&serde_json::json!({"memo": "QUJD123456789QUJD"})),
digest(&serde_json::json!({"memo": "QUJD987814321QUJD"}))
);
}
#[test]
fn a_tin_at_the_cut_is_masked_whole() {
// The TIN straddles the 64-character limit.
+34
View File
@@ -579,6 +579,40 @@ mod tests {
assert_eq!(e, AuthError::Newer(1));
}
#[test]
fn a_replacement_names_the_live_token_of_its_own_book_and_name() {
let replacing = |old: &str, book: &str, name: &str| {
let mut r = created("tok_2", name);
r["book"] = book.into();
r["replaces"] = old.into();
r
};
// The right one: it takes the old one's place.
let p = open_with(&[created("tok_1", "a"), replacing("tok_1", "acme", "a")]).unwrap();
let live: Vec<&str> = p.tokens().iter().map(|t| t.id.as_str()).collect();
assert_eq!(live, ["tok_2"]);
// Any one of id, book or name wrong: a log that contradicts itself.
for wrong in [
replacing("tok_9", "acme", "a"),
replacing("tok_1", "other", "a"),
replacing("tok_1", "acme", "b"),
] {
let e = open_with(&[created("tok_1", "a"), wrong.clone()]).unwrap_err();
assert!(matches!(e, AuthError::Inconsistent(2, _)), "{wrong}: {e}");
}
}
#[test]
fn a_revocation_ends_the_token_it_names_and_no_other() {
let revoked = |id: &str| serde_json::json!({"event": "token.revoked", "id": id});
for (gone, left) in [("tok_1", "tok_2"), ("tok_2", "tok_1")] {
let p =
open_with(&[created("tok_1", "a"), created("tok_2", "b"), revoked(gone)]).unwrap();
let live: Vec<&str> = p.tokens().iter().map(|t| t.id.as_str()).collect();
assert_eq!(live, [left], "{gone} revoked");
}
}
#[test]
fn a_changed_record_is_found() {
let dir = tempfile::tempdir().unwrap();
+75
View File
@@ -441,6 +441,64 @@ mod tests {
}
}
#[test]
fn a_log_the_disk_wont_read_is_refused_not_taken_for_empty() {
// A folder where the log should be: reading it fails, and an empty
// log is never made of that.
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("log.jsonl");
std::fs::create_dir(&path).unwrap();
assert!(matches!(read(&path, G), Err(ChainError::Io(_))));
assert!(matches!(ChainLog::open(&path, G), Err(ChainError::Io(_))));
}
#[test]
fn a_log_shows_its_head_but_never_its_path() {
let dir = tempfile::tempdir().unwrap();
let path = three(dir.path());
let (log, _) = ChainLog::open(&path, G).unwrap();
let shown = format!("{log:?}");
assert!(shown.starts_with("ChainLog"), "{shown}");
assert!(shown.contains("seq: 3"), "{shown}");
assert!(shown.contains(&log.head().hash), "{shown}");
assert!(shown.contains("broken: false"), "{shown}");
assert!(
!shown.contains(&dir.path().display().to_string()),
"{shown}"
);
}
#[cfg(unix)]
#[test]
fn only_a_new_log_syncs_its_folder_and_only_others_bits_are_taken_away() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
let path = three(dir.path());
// The owner's own bits are the owner's: a log 0700 stays 0700.
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o700)).unwrap();
drop(ChainLog::open(&path, G).unwrap());
let mode = std::fs::metadata(&path).unwrap().permissions().mode();
assert_eq!(mode & 0o777, 0o700);
// A folder that can be entered but not listed: a log already in it
// opens (its folder's entry for it was synced when it was made);
// a new one can't be made there, as its entry can't be synced.
let shut = dir.path().join("shut");
std::fs::create_dir(&shut).unwrap();
let (mut log, _) = ChainLog::open(&shut.join("old.jsonl"), G).unwrap();
log.append(fields(1)).unwrap();
drop(log);
std::fs::set_permissions(&shut, std::fs::Permissions::from_mode(0o300)).unwrap();
if File::open(&shut).is_err() {
let (_, records) = ChainLog::open(&shut.join("old.jsonl"), G).unwrap();
assert_eq!(records.len(), 1);
assert_eq!(
ChainLog::open(&shut.join("new.jsonl"), G).unwrap_err(),
ChainError::Io(std::io::ErrorKind::PermissionDenied)
);
}
std::fs::set_permissions(&shut, std::fs::Permissions::from_mode(0o700)).unwrap();
}
#[test]
fn an_unfinished_last_line_is_cut_away_on_open() {
let dir = tempfile::tempdir().unwrap();
@@ -484,6 +542,23 @@ mod tests {
] {
assert!(!e.to_string().contains('/'), "{e}");
}
// Each says what went wrong, and where in the log.
assert_eq!(
ChainError::Damaged {
line: 3,
why: "its number is out of order".into(),
}
.to_string(),
"record 3 is damaged: its number is out of order"
);
assert_eq!(
ChainError::Io(std::io::ErrorKind::PermissionDenied).to_string(),
"the disk refused: permission denied"
);
assert_eq!(
ChainError::Broken.to_string(),
"an earlier record couldn't be written"
);
}
#[cfg(target_os = "linux")]
+79
View File
@@ -256,6 +256,85 @@ mod tests {
const NOW: i64 = 1_800_000_000;
#[test]
fn an_export_is_kept_fifteen_minutes() {
// What its refusal tells the human: kept 15 minutes, then gone.
let dir = tempfile::tempdir().unwrap();
let x = Exports::open(dir.path(), NOW).unwrap();
let e = x.put("t", "a.csv", "text/csv", b"x", NOW).unwrap();
assert_eq!(e.expires, NOW + 15 * 60);
assert!(x.get(&e.id, "t", NOW + 15 * 60 - 1).is_ok());
let lapsed = x.get(&e.id, "t", NOW + 15 * 60).unwrap_err();
assert!(lapsed.to_string().contains("kept 15 minutes"), "{lapsed}");
}
#[test]
fn an_id_is_ex_and_thirty_two_letters_a_to_p_and_nothing_else() {
assert!(is_id(&format!("ex_{}", "a".repeat(32))));
assert!(is_id(&format!("ex_{}", "p".repeat(32))));
for bad in [
String::new(),
"ex_".into(),
format!("ex_{}", "a".repeat(31)),
format!("ex_{}", "a".repeat(33)),
format!("up_{}", "a".repeat(32)),
format!("exx{}", "a".repeat(32)),
format!("ex_{}", "q".repeat(32)),
format!("ex_{}0", "a".repeat(31)),
format!("ex_{}.", "a".repeat(31)),
] {
assert!(!is_id(&bad), "{bad}");
}
}
#[test]
fn a_fetch_never_reaches_past_the_exports_folder() {
// A whole export kept beside the books, not among their exports:
// no id names it.
let dir = tempfile::tempdir().unwrap();
let x = Exports::open(dir.path(), NOW).unwrap();
let bytes = b"not an export".to_vec();
for id in ["../outside", &format!("up_{}", "a".repeat(32))] {
let at = dir.path().join(EXPORTS_DIR).join(id);
let record = Export {
id: id.into(),
token: "t".into(),
file_name: "a.csv".into(),
mime: "text/csv".into(),
size: bytes.len() as u64,
sha256: sha256_hex(&bytes),
expires: NOW + EXPORT_TTL,
};
std::fs::write(
at.with_extension("json"),
serde_json::to_vec(&record).unwrap(),
)
.unwrap();
std::fs::write(at.with_extension("bin"), &bytes).unwrap();
assert!(
matches!(x.get(id, "t", NOW).unwrap_err(), ExportError::Unknown(_)),
"{id}"
);
}
// And a refusal quotes no more than 40 characters of what it got.
let long = format!("../{}", "x".repeat(100));
let ExportError::Unknown(quoted) = x.get(&long, "t", NOW).unwrap_err() else {
panic!("an unknown export")
};
assert_eq!(quoted.chars().count(), 40);
}
#[test]
fn an_export_counts_against_its_token_until_the_moment_it_lapses() {
let dir = tempfile::tempdir().unwrap();
let x = Exports::open(dir.path(), NOW).unwrap();
let e = x.put("t", "a.csv", "text/csv", b"x", NOW).unwrap();
assert_eq!(x.live_for("t", e.expires - 1), 1);
// Lapsed at its expiry, as a fetch then finds (not yet swept).
assert_eq!(x.live_for("t", e.expires), 0);
assert_eq!(x.live_for("u", NOW), 0);
}
#[test]
fn an_export_is_fetched_by_its_token_until_it_lapses() {
let dir = tempfile::tempdir().unwrap();
+2
View File
@@ -31,6 +31,8 @@ pub mod meta;
pub mod ops;
pub mod passphrase;
pub mod reconcilable;
#[cfg(test)]
mod refuse;
pub mod root;
pub mod saving;
pub mod schema;
+66
View File
@@ -288,6 +288,72 @@ mod tests {
assert!(verify(dir.path(), FIRST).unwrap());
}
/// A record of `FIRST` hashed with the plan's parameters but this salt
/// and output length, as a hand could write it.
fn record_with(dir: &Path, salt: &[u8], out: usize) {
let hasher = Argon2::new(
Algorithm::Argon2id,
Version::V0x13,
Params::new(M_COST, T_COST, P_COST, Some(out)).unwrap(),
);
let owner = Owner {
argon2id: hasher
.hash_password_with_salt(FIRST.as_bytes(), salt)
.unwrap()
.to_string(),
set: "2026-10-19T00:00:00Z".into(),
};
std::fs::write(dir.join(OWNER_FILE), serde_json::to_string(&owner).unwrap()).unwrap();
}
#[test]
fn a_salt_shorter_than_sixteen_bytes_or_an_output_not_thirty_two_is_refused() {
let dir = tempfile::tempdir().unwrap();
// The plan's own: a salt of 16 bytes or more, an output of 32.
for salt in [16, 17, 32, 48] {
record_with(dir.path(), &vec![7u8; salt], 32);
assert_eq!(verify(dir.path(), FIRST), Ok(true), "a {salt}-byte salt");
}
// Each right but for the salt's length, or the output's.
for (salt, out) in [(8, 32), (15, 32), (16, 16), (16, 64)] {
record_with(dir.path(), &vec![7u8; salt], out);
assert!(
matches!(verify(dir.path(), FIRST), Err(PassError::Record(_))),
"a {salt}-byte salt, a {out}-byte output"
);
}
}
#[test]
fn a_record_the_disk_wont_read_is_never_taken_for_none() {
// A folder where the record should be: no passphrase can be set
// as if none were, and the refusal is the disk's own.
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir(dir.path().join(OWNER_FILE)).unwrap();
assert!(matches!(is_set(dir.path()), Err(PassError::Io(_))));
assert!(matches!(verify(dir.path(), FIRST), Err(PassError::Io(_))));
assert!(matches!(
set(dir.path(), FIRST, None),
Err(PassError::Io(_))
));
}
#[cfg(target_os = "linux")]
#[test]
fn a_leftover_that_cant_be_cleared_stops_a_change_saying_why() {
// A folder (with something in it) left where the record is written
// aside: removing it fails, and that failure is what is reported.
let dir = tempfile::tempdir().unwrap();
let aside = dir.path().join(".owner.json.new");
std::fs::create_dir(&aside).unwrap();
std::fs::write(aside.join("x"), "x").unwrap();
assert_eq!(
set(dir.path(), FIRST, None),
Err(PassError::Io(std::io::ErrorKind::IsADirectory))
);
assert!(!is_set(dir.path()).unwrap());
}
#[test]
fn errors_name_no_path_and_repeat_no_passphrase() {
for e in [
+93
View File
@@ -0,0 +1,93 @@
//! Tests only: system calls refused, as a disk or a file system can
//! refuse them, in one thread of the test's own. A seccomp filter makes
//! each named call fail at once with the error chosen, before the kernel
//! looks at any path, so a refusal no real folder can be made to give
//! (a lookup that fails where making the folder would not; a link
//! refused where the name is already taken) is given every time. The
//! filter holds the calling thread only and ends with it: the rest of
//! the test process is untouched.
#![cfg(target_os = "linux")]
/// Run `f` in a thread of its own in which each call in `calls` fails
/// with `errno`, and give back what it returns; None where the system
/// won't put the filter on (the test then has nothing to show).
pub(crate) fn refusing<T: Send>(
calls: &[libc::c_long],
errno: i32,
f: impl FnOnce() -> T + Send,
) -> Option<T> {
std::thread::scope(|s| {
s.spawn(|| {
// Looked up once before the filter, so std knows statx is
// there and never mistakes a refused one for a missing one.
let _ = std::fs::symlink_metadata("/");
filter(calls, errno).then(f)
})
.join()
.unwrap()
})
}
/// Put the filter on this thread: the call's number checked against
/// each of `calls`, a match refused with `errno`, anything else let be.
fn filter(calls: &[libc::c_long], errno: i32) -> bool {
use libc::{
BPF_ABS, BPF_JEQ, BPF_JMP, BPF_K, BPF_LD, BPF_RET, BPF_W, SECCOMP_RET_ALLOW,
SECCOMP_RET_ERRNO, sock_filter,
};
let stmt = |code: u32, k: u32| sock_filter {
code: code as u16,
jt: 0,
jf: 0,
k,
};
let n = calls.len();
let mut prog = vec![stmt(
BPF_LD | BPF_W | BPF_ABS,
std::mem::offset_of!(libc::seccomp_data, nr) as u32,
)];
for (i, call) in calls.iter().enumerate() {
prog.push(sock_filter {
code: (BPF_JMP | BPF_JEQ | BPF_K) as u16,
// To the refusal, past the other checks and the allow.
jt: u8::try_from(n - i).unwrap(),
jf: 0,
k: u32::try_from(*call).unwrap(),
});
}
prog.push(stmt(BPF_RET | BPF_K, SECCOMP_RET_ALLOW));
prog.push(stmt(
BPF_RET | BPF_K,
SECCOMP_RET_ERRNO | u32::try_from(errno).unwrap(),
));
let fprog = libc::sock_fprog {
len: u16::try_from(prog.len()).unwrap(),
filter: prog.as_mut_ptr(),
};
// SAFETY: prctl with a filter program that outlives the call (the
// kernel copies it); without TSYNC it binds this thread alone.
unsafe {
libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) == 0
&& libc::prctl(
libc::PR_SET_SECCOMP,
libc::SECCOMP_MODE_FILTER,
&fprog as *const libc::sock_fprog,
) == 0
}
}
/// The calls that look a name up without following it (std's
/// `symlink_metadata`): statx, and what std and libc fall back to.
pub(crate) const LOOKUPS: &[libc::c_long] = &[
libc::SYS_statx,
libc::SYS_newfstatat,
#[cfg(target_arch = "x86_64")]
libc::SYS_lstat,
];
/// The calls that make a hard link (std's `hard_link`).
pub(crate) const LINKS: &[libc::c_long] = &[
libc::SYS_linkat,
#[cfg(target_arch = "x86_64")]
libc::SYS_link,
];
+25
View File
@@ -1711,4 +1711,29 @@ mod unit {
let (_, login) = unix_account().unwrap();
assert!(!login.is_empty());
}
#[cfg(target_os = "linux")]
#[test]
fn a_folder_the_disk_wont_look_up_is_never_made_over() {
// The disk can't say what is under the name (an I/O error, where
// making the folder would go through): refused with the disk's
// own error, and no folder made over whatever is there.
let dir = tempfile::tempdir().unwrap();
let book = dir.path();
let Some(got) = crate::refuse::refusing(crate::refuse::LOOKUPS, libc::EIO, || {
private_book_folder(book, "keys")
}) else {
eprintln!("skipped: no seccomp here");
return;
};
let Err(RootError::Io(why, e)) = got else {
panic!("refused as an I/O error: {got:?}")
};
assert_eq!(e.raw_os_error(), Some(libc::EIO));
assert!(!why.contains(&book.display().to_string()), "{why}");
assert!(!book.join("keys").exists());
// Looked up as it should be: made, private.
let keys = private_book_folder(book, "keys").unwrap();
assert_eq!(keys, book.join("keys"));
}
}
+164
View File
@@ -124,6 +124,170 @@ pub fn save_new(
mod tests {
use super::*;
#[test]
fn a_hidden_name_keeps_its_dot_when_numbered() {
// `.env` is a stem, not an extension: `.env (1)`, never ` (1).env`.
let dir = tempfile::tempdir().unwrap();
let d = dir.path();
assert_eq!(
save_new(d, ".env", "attachment-7", b"a").unwrap(),
d.join(".env")
);
assert_eq!(
save_new(d, ".env", "attachment-7", b"b").unwrap(),
d.join(".env (1)")
);
assert_eq!(
save_new(d, "r.tar.gz", "attachment-7", b"c").unwrap(),
d.join("r.tar.gz")
);
assert_eq!(
save_new(d, "r.tar.gz", "attachment-7", b"d").unwrap(),
d.join("r.tar (1).gz")
);
}
#[test]
fn when_no_name_is_taken_the_folders_own_refusal_is_said() {
// Both names too long for the folder: its refusal, not "no free
// name", and nothing left behind.
let dir = tempfile::tempdir().unwrap();
let d = dir.path();
let e = save_new(d, &"x".repeat(300), &"y".repeat(300), b"ours").unwrap_err();
assert!(!e.contains("no free name"), "{e}");
assert!(
e.ends_with(&std::io::ErrorKind::InvalidFilename.to_string()),
"{e}"
);
assert_eq!(std::fs::read_dir(d).unwrap().count(), 0);
}
#[cfg(unix)]
#[test]
fn when_a_folder_refuses_the_copy_written_in_place_its_refusal_is_said() {
// Both names in a folder that can't be written: the link is
// refused (permission denied, so the copy is tried in place), and
// so is the file in place. That refusal is said, not "no free
// name", and the next name is never tried for it.
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
let d = dir.path();
let ro = d.join("ro");
std::fs::create_dir(&ro).unwrap();
std::fs::set_permissions(&ro, std::fs::Permissions::from_mode(0o500)).unwrap();
// (Skipped where nothing is refused, as for root.)
if std::fs::write(ro.join("probe"), b"").is_err() {
let e = save_new(d, "ro/r.pdf", "ro/attachment-7", b"ours").unwrap_err();
assert!(!e.contains("no free name"), "{e}");
assert!(
e.ends_with(&std::io::ErrorKind::PermissionDenied.to_string()),
"{e}"
);
// A name of its own that the folder takes: that one.
assert_eq!(
save_new(d, "ro/r.pdf", "attachment-7", b"ours").unwrap(),
d.join("attachment-7")
);
}
std::fs::set_permissions(&ro, std::fs::Permissions::from_mode(0o700)).unwrap();
}
#[cfg(target_os = "linux")]
#[test]
fn a_name_refused_for_another_reason_than_links_moves_on_never_written_in_place() {
// A name reaching onto another file system: the link is refused
// (it crosses devices), and the copy is never written there in
// place as if the folder took no links; the fallback is used.
use std::os::unix::fs::MetadataExt;
let dir = tempfile::tempdir().unwrap();
let d = dir.path();
let Ok(other) = tempfile::tempdir_in("/dev/shm") else {
return;
};
let dev = |p: &std::path::Path| std::fs::metadata(p).unwrap().dev();
if dev(d) == dev(other.path()) {
return;
}
std::os::unix::fs::symlink(other.path(), d.join("elsewhere")).unwrap();
assert_eq!(
save_new(d, "elsewhere/r.pdf", "attachment-7", b"ours").unwrap(),
d.join("attachment-7")
);
assert_eq!(std::fs::read_dir(other.path()).unwrap().count(), 0);
}
#[cfg(target_os = "linux")]
#[test]
fn a_folder_without_links_gets_the_copy_written_in_place_never_over_a_file() {
// Links refused as FAT refuses them (not supported, or not
// permitted), and before the folder looks at the name: a name
// already taken is then found only when the copy is written in
// place, and the next name is tried, never the fallback, never
// over the file.
for errno in [libc::EOPNOTSUPP, libc::EPERM] {
let dir = tempfile::tempdir().unwrap();
let d = dir.path();
std::fs::write(d.join("theirs.pdf"), b"theirs").unwrap();
let Some(saved) = crate::refuse::refusing(crate::refuse::LINKS, errno, || {
[
save_new(d, "r.pdf", "attachment-7", b"ours"),
save_new(d, "r.pdf", "attachment-7", b"again"),
save_new(d, "theirs.pdf", "attachment-7", b"mine"),
]
}) else {
eprintln!("skipped: no seccomp here");
return;
};
assert_eq!(
saved,
[
Ok(d.join("r.pdf")),
Ok(d.join("r (1).pdf")),
Ok(d.join("theirs (1).pdf")),
],
"errno {errno}"
);
assert_eq!(std::fs::read(d.join("r.pdf")).unwrap(), b"ours");
assert_eq!(std::fs::read(d.join("r (1).pdf")).unwrap(), b"again");
assert_eq!(std::fs::read(d.join("theirs.pdf")).unwrap(), b"theirs");
assert_eq!(std::fs::read(d.join("theirs (1).pdf")).unwrap(), b"mine");
// Nothing aside is left.
let mut names: Vec<String> = std::fs::read_dir(d)
.unwrap()
.map(|e| e.unwrap().file_name().to_string_lossy().into_owned())
.collect();
names.sort();
assert_eq!(
names,
["r (1).pdf", "r.pdf", "theirs (1).pdf", "theirs.pdf"],
"errno {errno}"
);
}
}
#[cfg(target_os = "linux")]
#[test]
fn a_link_refused_for_any_other_reason_is_never_written_in_place() {
// Every link refused as crossing devices: no copy is written in
// place as if the folder took no links; the folder's refusal is
// said, and nothing is left behind.
let dir = tempfile::tempdir().unwrap();
let d = dir.path();
let Some(saved) = crate::refuse::refusing(crate::refuse::LINKS, libc::EXDEV, || {
save_new(d, "r.pdf", "attachment-7", b"ours")
}) else {
eprintln!("skipped: no seccomp here");
return;
};
let e = saved.unwrap_err();
assert!(!e.contains("no free name"), "{e}");
assert!(
e.ends_with(&std::io::ErrorKind::CrossesDevices.to_string()),
"{e}"
);
assert_eq!(std::fs::read_dir(d).unwrap().count(), 0);
}
#[test]
fn a_saved_copy_is_new_and_whole_beside_what_is_there() {
let dir = tempfile::tempdir().unwrap();
+129
View File
@@ -907,6 +907,135 @@ mod tests {
assert!(!folder.join(format!("{}.part", g.id)).exists());
}
#[test]
fn bytes_that_finish_after_anothers_were_kept_or_held_are_not_kept() {
// A send is under way when the books are opened again (its `.part`
// dropped as a stopped server's half send) and another send of the
// same bytes finishes first: those are kept, or held by a call that
// took them. The first send, finishing, finds that and keeps
// nothing.
for hold in [false, true] {
let dir = tempfile::tempdir().unwrap();
let u = Uploads::open(dir.path(), NOW).unwrap();
let bytes = b"%PDF-1.4 twice".to_vec();
let g = grant_for(&u, "tok_a", &bytes);
let held = std::cell::RefCell::new(None);
let mut body = Forgetting {
bytes: &bytes,
then: Some(Box::new(|| {
let again = Uploads::open(dir.path(), NOW).unwrap();
again.receive(&g.id, "tok_a", &mut &bytes[..], NOW).unwrap();
if hold {
*held.borrow_mut() = Some(again.take(&g.id, "tok_a", NOW).unwrap());
}
})),
};
assert_eq!(
u.receive(&g.id, "tok_a", &mut body, NOW).unwrap_err(),
UploadError::AlreadySent,
"held: {hold}"
);
drop(body);
// The other send's bytes are what the grant has.
let r = held
.into_inner()
.unwrap_or_else(|| u.take(&g.id, "tok_a", NOW).unwrap());
assert_eq!(r.bytes, bytes);
assert!(
!dir.path()
.join(UPLOADS_DIR)
.join(format!("{}.part", g.id))
.exists()
);
}
}
#[test]
fn a_grant_lasts_fifteen_minutes() {
// Plan 4.7: "valid 15 minutes".
let dir = tempfile::tempdir().unwrap();
let u = Uploads::open(dir.path(), NOW).unwrap();
let bytes = b"%PDF-1.4 in time".to_vec();
let g = grant_for(&u, "tok_a", &bytes);
assert_eq!(g.expires, NOW + 15 * 60);
u.receive(&g.id, "tok_a", &mut &bytes[..], NOW + 15 * 60 - 1)
.unwrap();
assert!(matches!(
u.take(&g.id, "tok_a", NOW + 15 * 60).unwrap_err(),
UploadError::Unknown(_)
));
}
#[test]
fn a_grant_counts_against_its_token_until_the_moment_it_lapses() {
let dir = tempfile::tempdir().unwrap();
let u = Uploads::open(dir.path(), NOW).unwrap();
let g = grant_for(&u, "tok_a", b"x");
assert_eq!(u.live_for("tok_a", g.expires - 1), 1);
// Lapsed at its expiry, as a send then finds (not yet swept).
assert_eq!(u.live_for("tok_a", g.expires), 0);
assert_eq!(u.live_for("tok_b", NOW), 0);
}
/// A body that must never be read.
struct Unread;
impl std::io::Read for Unread {
fn read(&mut self, _: &mut [u8]) -> std::io::Result<usize> {
panic!("bytes already sent are never read again");
}
}
#[test]
fn bytes_sent_or_held_are_refused_before_anything_more_is_read() {
let dir = tempfile::tempdir().unwrap();
let u = Uploads::open(dir.path(), NOW).unwrap();
let bytes = b"%PDF-1.4 once".to_vec();
let g = grant_for(&u, "tok_a", &bytes);
u.receive(&g.id, "tok_a", &mut &bytes[..], NOW).unwrap();
// Kept: refused at once, whatever comes.
assert_eq!(
u.receive(&g.id, "tok_a", &mut Unread, NOW).unwrap_err(),
UploadError::AlreadySent
);
// Held by a call: the same.
let held = u.take(&g.id, "tok_a", NOW).unwrap();
assert_eq!(
u.receive(&g.id, "tok_a", &mut Unread, NOW).unwrap_err(),
UploadError::AlreadySent
);
assert!(
!dir.path()
.join(UPLOADS_DIR)
.join(format!("{}.part", g.id))
.exists()
);
drop(held);
}
#[cfg(unix)]
#[test]
fn a_send_the_disk_refuses_says_so_not_that_another_is_under_way() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
let u = Uploads::open(dir.path(), NOW).unwrap();
let bytes = b"%PDF-1.4 refused".to_vec();
let g = grant_for(&u, "tok_a", &bytes);
let folder = dir.path().join(UPLOADS_DIR);
std::fs::set_permissions(&folder, std::fs::Permissions::from_mode(0o500)).unwrap();
// (Skipped where nothing is refused, as for root.)
if std::fs::write(folder.join("probe"), b"").is_err() {
assert_eq!(
u.receive(&g.id, "tok_a", &mut &bytes[..], NOW).unwrap_err(),
UploadError::Io(std::io::ErrorKind::PermissionDenied)
);
}
std::fs::set_permissions(&folder, std::fs::Permissions::from_mode(0o700)).unwrap();
// The grant still stands.
let _ = std::fs::remove_file(folder.join("probe"));
u.receive(&g.id, "tok_a", &mut &bytes[..], NOW).unwrap();
}
#[test]
fn a_name_is_plain_and_visible() {
for ok in ["r.pdf", "Résumé 2026 (final).pdf", "收据.png", "a"] {