mod common;
use common::{fixture, pinoc, stdout};
use serde_json::Value;
fn findings(fixture_name: &str, args: &[&str]) -> Vec<Value> {
let mut all = vec!["check", "--json"];
all.extend(args);
let out = pinoc(&fixture(fixture_name), &all);
sorted(serde_json::from_str(&stdout(&out)).expect("--json stdout is a JSON array"))
}
fn sorted(mut findings: Vec<Value>) -> Vec<Value> {
findings.sort_by_key(|f| {
(
f["span"]["file"].as_str().unwrap().to_string(),
f["span"]["line"].as_u64().unwrap(),
f["code"].as_str().unwrap().to_string(),
)
});
findings
}
fn codes(findings: &[Value]) -> Vec<&str> {
findings
.iter()
.map(|f| f["code"].as_str().unwrap())
.collect()
}
fn in_file<'a>(findings: &'a [Value], file: &str) -> Vec<(&'a str, &'a str)> {
findings
.iter()
.filter(|f| f["span"]["file"] == file)
.map(|f| (f["code"].as_str().unwrap(), f["evidence"].as_str().unwrap()))
.collect()
}
#[test]
fn free_function_output_matches_0_2_5() {
let dir = fixture("check_free");
let expected: Vec<Value> =
serde_json::from_str(&std::fs::read_to_string(dir.join("expected/check.json")).unwrap())
.unwrap();
assert_eq!(findings("check_free", &[]), sorted(expected));
let lines = |s: &str| {
let mut v: Vec<String> = s.lines().map(str::to_string).collect();
v.sort();
v
};
let expected = std::fs::read_to_string(dir.join("expected/check.txt")).unwrap();
let actual = stdout(&pinoc(&dir, &["check"]));
assert_eq!(lines(&actual), lines(&expected));
}
#[test]
fn impl_methods_are_analysed_like_free_functions() {
let args = ["--deny", "all"];
let free = findings("check_free", &args);
let methods = findings("check_impl", &args);
let expected = in_file(&free, "src/handlers.rs");
assert_eq!(
expected.iter().map(|f| f.0).collect::<Vec<_>>(),
["ACC001-P", "ZC002-P", "ACC002-P", "ACC003-P", "CPI001-P"]
);
assert_eq!(in_file(&methods, "src/handlers.rs"), expected);
assert_eq!(codes(&methods), codes(&free));
}
#[test]
fn zero_handlers_is_reported_not_passed_as_clean() {
let dir = fixture("check_no_handlers");
let out = pinoc(&dir, &["check"]);
let text = stdout(&out);
assert!(text.contains("NO-HANDLERS"), "{text}");
assert!(text.contains("not a clean result"), "{text}");
assert!(!text.contains("No issues found"), "{text}");
assert!(text.contains("ZC001-P"), "{text}");
assert_eq!(out.status.code(), Some(0));
assert_eq!(
codes(&findings("check_no_handlers", &[])),
["NO-HANDLERS", "ZC001-P"]
);
for deny in ["all", "NO-HANDLERS"] {
let out = pinoc(&dir, &["check", "--deny", deny]);
assert_eq!(out.status.code(), Some(1), "--deny {deny}");
}
let out = pinoc(&dir, &["check", "--allow", "NO-HANDLERS", "ZC001-P"]);
assert!(stdout(&out).contains("No issues found"));
}
#[test]
fn missing_src_is_reported() {
let dir = fixture("check_no_handlers").join("src");
let text = stdout(&pinoc(&dir, &["check"]));
assert!(text.contains("NO-HANDLERS"), "{text}");
assert!(text.contains("no `src/` directory"), "{text}");
}
fn located(findings: &[Value]) -> Vec<(&str, &str, u64)> {
findings
.iter()
.map(|f| {
(
f["code"].as_str().unwrap(),
f["span"]["file"].as_str().unwrap(),
f["span"]["line"].as_u64().unwrap(),
)
})
.collect()
}
#[test]
fn typed_context_and_pinocchio_0_11_names() {
let found = findings("check_typed_context", &[]);
assert_eq!(
located(&found),
[
("ACC001-P", "src/lib.rs", 40),
("ZC002-P", "src/lib.rs", 40),
("ZC002-P", "src/lib.rs", 53),
("ACC001-P", "src/lib.rs", 64),
]
);
let evidence = found[0]["evidence"].as_str().unwrap();
assert!(
evidence.contains("(in `Deposit::process`, bound in `DepositAccounts::try_from`)"),
"{evidence}"
);
}
#[test]
fn accounts_are_followed_from_try_from_into_process_and_helpers() {
let found = findings("check_context", &[]);
assert_eq!(
located(&found),
[
("UNTRACKED-ACCOUNTS", "src", 0),
("ZC002-P", "src/instructions/set_fee.rs", 20),
("ACC001-P", "src/instructions/withdraw.rs", 27),
("ACC002-P", "src/instructions/withdraw.rs", 28),
("ACC001-P", "src/processor.rs", 5),
]
);
let evidence = |index: usize| found[index]["evidence"].as_str().unwrap();
assert!(
evidence(0).contains("`ArchiveAccounts::try_from`"),
"{}",
evidence(0)
);
assert!(!evidence(0).contains("Deposit"), "{}", evidence(0));
assert!(
evidence(2).contains("account `vault`")
&& evidence(2)
.contains("(in `Withdraw::process`, bound in `WithdrawAccounts::try_from`)"),
"{}",
evidence(2)
);
assert!(evidence(3).contains("account `admin`"), "{}", evidence(3));
assert!(
evidence(4).contains("(in `process_sweep`, bound in `SweepAccounts::try_from`)"),
"{}",
evidence(4)
);
}
#[test]
fn instructions_sharing_an_account_list_are_checked_separately() {
let found = findings("check_shared_accounts", &[]);
assert_eq!(
located(&found),
[
("UNTRACKED-ACCOUNTS", "src", 0),
("ACC001-P", "src/batch.rs", 6),
("ACC001-P", "src/batch.rs", 27),
("ACC001-P", "src/sell.rs", 63),
("ACC001-P", "src/tail.rs", 24),
("ACC001-P", "src/tail.rs", 33),
("ACC002-P", "src/unwind.rs", 25),
]
);
let evidence = |index: usize| found[index]["evidence"].as_str().unwrap();
assert!(
evidence(0).contains("`process_pairs` (binds no account from its slice)"),
"{}",
evidence(0)
);
assert!(evidence(2).contains("account `oracle`"), "{}", evidence(2));
assert!(
evidence(3).contains(
"(in `Sell::pool_reserve`, as part of `Unwind`, bound in `SellAccounts::load`)"
),
"{}",
evidence(3)
);
assert!(evidence(6).contains("account `admin`"), "{}", evidence(6));
}
#[test]
fn a_project_can_name_its_own_authorities() {
let dir = common::temp_copy("check_shared_accounts");
std::fs::write(
dir.join("Pinoc.toml"),
"[provider]\ncluster = \"localhost\"\nwallet = \"~/.config/solana/id.json\"\n\n[check]\nauthority_names = [\"keeper\"]\n",
)
.unwrap();
let out = pinoc(&dir, &["check", "--json"]);
let found = sorted(serde_json::from_str(&stdout(&out)).unwrap());
let signer: Vec<(&str, u64)> = found
.iter()
.filter(|f| f["code"] == "ACC002-P")
.map(|f| {
(
f["span"]["file"].as_str().unwrap(),
f["span"]["line"].as_u64().unwrap(),
)
})
.collect();
assert_eq!(signer, [("src/unwind.rs", 25), ("src/unwind.rs", 26)]);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn a_misspelt_check_key_or_section_is_refused() {
let provider = "[provider]\ncluster = \"localhost\"\nwallet = \"~/.config/solana/id.json\"\n\n";
for (config, named) in [
(
"[check]\nautority_names = [\"keeper\"]\n",
"unknown field `autority_names`",
),
(
"[chek]\nauthority_names = [\"keeper\"]\n",
"unknown field `chek`",
),
] {
let dir = common::temp_copy("check_shared_accounts");
std::fs::write(dir.join("Pinoc.toml"), format!("{provider}{config}")).unwrap();
let out = pinoc(&dir, &["check"]);
assert!(!out.status.success(), "{config}");
let text = common::stderr(&out);
assert!(text.contains(named), "{text}");
let _ = std::fs::remove_dir_all(&dir);
}
}
#[test]
fn a_check_value_that_cannot_be_applied_is_refused() {
let provider = "[provider]\ncluster = \"localhost\"\nwallet = \"~/.config/solana/id.json\"\n\n";
for (config, named) in [
(
"[check]\nconfidence_threshold = \"possible\"\n",
"`confidence_threshold = \"possible\"` under `[check]` in Pinoc.toml is not a confidence level",
),
(
"[check]\nallow = [\"ACC001\"]\n",
"`ACC001`, given to `allow` under `[check]` in Pinoc.toml, is not a lint code",
),
] {
let dir = common::temp_copy("check_shared_accounts");
std::fs::write(dir.join("Pinoc.toml"), format!("{provider}{config}")).unwrap();
let out = pinoc(&dir, &["check"]);
assert!(!out.status.success(), "{config}");
let text = common::stderr(&out);
assert!(text.contains(named), "{text}");
let _ = std::fs::remove_dir_all(&dir);
}
let out = pinoc(
&fixture("check_shared_accounts"),
&["check", "--deny", "ACC9"],
);
assert!(!out.status.success());
let text = common::stderr(&out);
assert!(
text.contains("`ACC9`, given to `--deny`, is not a lint code"),
"{text}"
);
}
#[test]
fn the_hidden_findings_line_names_their_codes() {
let dir = common::temp_copy("check_shared_accounts");
std::fs::write(
dir.join("Pinoc.toml"),
"[provider]\ncluster = \"localhost\"\nwallet = \"~/.config/solana/id.json\"\n\n[check]\nconfidence_threshold = \"definite\"\n",
)
.unwrap();
let text = stdout(&pinoc(&dir, &["check"]));
assert!(
text.contains(
"lower-confidence findings (ACC001-P, ACC002-P, ACC003-P) below the `definite` threshold hidden"
),
"{text}"
);
let _ = std::fs::remove_dir_all(&dir);
}