use assert_cmd::Command;
use std::fs;
use std::path::Path;
const MANIFEST: &str = r#"
version = 1
canonical = "json-schema-2020-12"
authoring = "zod"
[[contracts]]
name = "user"
source = "contracts/user.zod.ts"
emit = ["zod", "sql"]
[[mappings]]
glob = "app/api/**/*.ts"
contracts = ["user"]
require = "boundary-validation"
[gates]
suppression_comments = "deny"
protected_paths = ["pushkin.toml"]
"#;
const CONFORMING: &str = "import { UserSchema } from \"../../generated/user.zod.gen\";\n\
export async function POST(req: Request) {\n \
const body = UserSchema.parse(await req.json());\n \
return Response.json({ name: body.name });\n}\n";
const NONCONFORMING: &str = "export async function POST(req: Request) {\n \
const body = await req.json();\n return Response.json({ name: body.name });\n}\n";
fn git(dir: &Path, args: &[&str]) -> Option<()> {
let status = std::process::Command::new("git")
.args(args)
.current_dir(dir)
.env("GIT_AUTHOR_NAME", "Affected Tester")
.env("GIT_AUTHOR_EMAIL", "affected@example.com")
.env("GIT_COMMITTER_NAME", "Affected Tester")
.env("GIT_COMMITTER_EMAIL", "affected@example.com")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.ok()?;
assert!(status.success(), "git {args:?} must succeed");
Some(())
}
fn repo() -> Option<tempfile::TempDir> {
let dir = tempfile::tempdir().ok()?;
git(dir.path(), &["init", "-q"])?;
fs::write(dir.path().join("pushkin.toml"), MANIFEST).ok()?;
fs::write(dir.path().join("README.md"), "readme\n").ok()?;
git(dir.path(), &["add", "."])?;
git(dir.path(), &["commit", "-q", "-m", "seed"])?;
Some(dir)
}
fn stage(dir: &Path, rel: &str, content: &str) -> Option<()> {
let path = dir.join(rel);
fs::create_dir_all(path.parent()?).ok()?;
fs::write(&path, content).ok()?;
git(dir, &["add", rel])
}
fn affected_output(dir: &Path) -> Option<(String, i32)> {
let output = Command::cargo_bin("pushkin")
.ok()?
.arg("affected")
.current_dir(dir)
.output()
.ok()?;
Some((
String::from_utf8_lossy(&output.stdout).into_owned(),
output.status.code().unwrap_or(-1),
))
}
#[test]
fn staged_diff_maps_to_touched_contracts() {
let dir = repo().unwrap();
stage(dir.path(), "app/api/users/route.ts", CONFORMING).unwrap();
let (stdout, _) = affected_output(dir.path()).unwrap();
assert!(
stdout.contains("user"),
"touched mapped file must name its contract: {stdout}"
);
assert!(
stdout.contains("app/api/users/route.ts"),
"the touched file itself must be listed: {stdout}"
);
}
#[test]
fn unmapped_change_yields_empty_affected_set() {
let dir = repo().unwrap();
stage(dir.path(), "docs/notes.md", "notes\n").unwrap();
let (stdout, code) = affected_output(dir.path()).unwrap();
assert_eq!(code, 0, "unmapped-only diff must exit 0");
assert!(
!stdout.contains("contract: user"),
"no contract may be reported for an unmapped diff: {stdout}"
);
}
#[test]
fn affected_lists_blast_radius_bindings_and_globs() {
let dir = repo().unwrap();
stage(dir.path(), "app/api/users/route.ts", CONFORMING).unwrap();
let (stdout, _) = affected_output(dir.path()).unwrap();
for needle in [
"generated/user.zod.gen.ts",
"generated/user.gen.sql",
"app/api/**/*.ts",
] {
assert!(
stdout.contains(needle),
"blast radius must list {needle}: {stdout}"
);
}
}
#[test]
fn affected_runs_targeted_checks_only_on_touched_files() {
let dir = repo().unwrap();
fs::create_dir_all(dir.path().join("app/api/legacy")).unwrap();
fs::write(dir.path().join("app/api/legacy/route.ts"), NONCONFORMING).unwrap();
git(dir.path(), &["add", "app/api/legacy/route.ts"]).unwrap();
git(dir.path(), &["commit", "-q", "-m", "legacy violation"]).unwrap();
stage(dir.path(), "app/api/users/route.ts", CONFORMING).unwrap();
let (stdout, code) = affected_output(dir.path()).unwrap();
assert_eq!(
code, 0,
"conforming touched file must pass even though an untouched \
violation exists elsewhere: {stdout}"
);
assert!(
!stdout.contains("legacy/route.ts"),
"untouched files must not be checked: {stdout}"
);
}
#[test]
fn touched_violation_blocks_with_exit_2() {
let dir = repo().unwrap();
stage(dir.path(), "app/api/users/route.ts", NONCONFORMING).unwrap();
let (stdout, code) = affected_output(dir.path()).unwrap();
assert_eq!(
code, 2,
"a violating touched file must block (exit 2): {stdout}"
);
}
#[test]
fn base_ref_range_maps_committed_changes() {
let dir = repo().unwrap();
stage(dir.path(), "app/api/users/route.ts", CONFORMING).unwrap();
git(dir.path(), &["commit", "-q", "-m", "add users route"]).unwrap();
let output = Command::cargo_bin("pushkin")
.unwrap()
.args(["affected", "--base", "HEAD~1"])
.current_dir(dir.path())
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("user") && stdout.contains("app/api/users/route.ts"),
"--base <ref> must map the committed range: {stdout}"
);
}