use assert_cmd::Command;
use predicates::prelude::*;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command as StdCommand;
use tempfile::TempDir;
fn sdkt() -> Command {
Command::cargo_bin("sdkt").unwrap()
}
const DOCUMENTED_FILES: &[&str] = &[
"Cargo.toml",
"src/lib.rs",
"src/plugin_abi.rs",
"src/plugin_abi_wasm.rs",
"plugin/plugin.toml",
"plugin-wasm/plugin.toml",
"README.md",
"tests/rule_test.rs",
".gitignore",
];
fn init_project(dir: &Path, name: &str) -> PathBuf {
let project = dir.join(name);
sdkt()
.args(["plugin", "init", project.to_str().unwrap()])
.assert()
.success()
.stdout(predicate::str::contains("Created plugin rule project '"));
project
}
#[test]
fn plugin_init_creates_documented_layout() {
let tmp = TempDir::new().unwrap();
let project = init_project(tmp.path(), "tmp-rule");
for f in DOCUMENTED_FILES {
assert!(project.join(f).exists(), "missing scaffolded file: {f}");
}
let lib = fs::read_to_string(project.join("src/lib.rs")).unwrap();
assert!(
lib.contains("TMP-RULE-001"),
"rule id derived from project name"
);
assert!(
lib.contains("TODO"),
"check() skeleton must carry TODO markers"
);
assert!(lib.contains("impl AuditRule"), "must implement AuditRule");
let readme = fs::read_to_string(project.join("README.md")).unwrap();
assert!(readme.contains("cargo build --release --features plugins"));
assert!(readme.contains("sdkt plugin pack"));
assert!(readme.contains("sdkt plugin install"));
assert!(readme.contains("sdkt audit path/to/contract.rs --rules"));
let cargo = fs::read_to_string(project.join("Cargo.toml")).unwrap();
assert!(
cargo.contains("[workspace]"),
"standalone crate must declare [workspace]"
);
assert!(cargo.contains("sdkt-audit"));
assert!(cargo.contains("plugins = [\"sdkt-audit/plugins\"]"));
assert!(cargo.contains("crate-type = [\"rlib\", \"cdylib\"]"));
let toml = fs::read_to_string(project.join("plugin/plugin.toml")).unwrap();
assert!(toml.contains("id = \"tmp_rule\""));
assert!(toml.contains("kind = \"native\""));
assert!(toml.contains("abi_major = 1"));
let artifact = format!(
"{}{}{}",
std::env::consts::DLL_PREFIX,
"tmp_rule",
std::env::consts::DLL_SUFFIX
);
assert!(
toml.contains(&format!("artifact = \"{artifact}\"")),
"plugin.toml must name the platform-native artifact"
);
assert!(
readme.contains(&format!("cp target/release/{artifact} plugin/")),
"README build-to-audit instructions must reference the same artifact"
);
let wasm = fs::read_to_string(project.join("plugin-wasm/plugin.toml")).unwrap();
assert!(wasm.contains("kind = \"wasm\""));
assert!(wasm.contains("artifact = \"tmp_rule.wasm\""));
assert!(
readme.contains("--features wasm-plugins"),
"README must document the WASM build path"
);
}
#[test]
fn plugin_init_rejects_existing_directory() {
let tmp = TempDir::new().unwrap();
let project = tmp.path().join("existing");
fs::create_dir_all(&project).unwrap();
sdkt()
.args(["plugin", "init", project.to_str().unwrap()])
.assert()
.failure()
.stderr(predicate::str::contains("already exists"));
}
#[test]
fn plugin_init_json_output() {
let tmp = TempDir::new().unwrap();
let project = tmp.path().join("json_proj");
sdkt()
.args([
"plugin",
"init",
project.to_str().unwrap(),
"--format",
"json",
])
.assert()
.success()
.stdout(predicate::str::contains("\"status\": \"created\""));
assert!(project.join("src/lib.rs").exists());
}
fn find_cdylib_artifact(release_dir: &Path) -> Option<PathBuf> {
let entries = fs::read_dir(release_dir).ok()?;
for e in entries.flatten() {
let name = e.file_name().to_string_lossy().into_owned();
if name.contains("tmp_rule")
&& (name.ends_with(".so") || name.ends_with(".dylib") || name.ends_with(".dll"))
{
return Some(e.path());
}
}
None
}
fn sdkt_audit_patch_config() -> String {
let path = concat!(env!("CARGO_MANIFEST_DIR"), "/../sdkt-audit");
format!(
"patch.crates-io.sdkt-audit.path=\"{}\"",
path.replace('\\', "/")
)
}
#[test]
fn plugin_init_scaffold_builds_and_rule_produces_finding() {
let tmp = TempDir::new().unwrap();
let project = init_project(tmp.path(), "tmp-rule");
let status = StdCommand::new("cargo")
.arg("build")
.arg("--release")
.args([
"--features",
"plugins",
"--config",
&sdkt_audit_patch_config(),
])
.current_dir(&project)
.status()
.expect("cargo build failed to execute");
assert!(
status.success(),
"Scaffolded plugin failed to build with --features plugins"
);
let artifact = find_cdylib_artifact(&project.join("target/release"));
assert!(
artifact.is_some(),
"cdylib artifact was not produced by the build"
);
let status = StdCommand::new("cargo")
.arg("test")
.args([
"--features",
"plugins",
"--config",
&sdkt_audit_patch_config(),
])
.current_dir(&project)
.status()
.expect("cargo test failed to execute");
assert!(
status.success(),
"Scaffolded plugin unit tests (finding production) failed"
);
}
#[test]
#[ignore = "requires the workspace sdkt-audit version to be published on crates.io"]
fn plugin_init_scaffold_resolves_published_audit() {
let tmp = TempDir::new().unwrap();
let project = init_project(tmp.path(), "tmp-rule");
let status = StdCommand::new("cargo")
.args(["build", "--release", "--features", "plugins"])
.current_dir(&project)
.status()
.expect("cargo build failed to execute");
assert!(
status.success(),
"Scaffolded plugin failed to build against the published sdkt-audit"
);
}