#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod support;
use std::path::{Path, PathBuf};
use ggen_engine::sync::{sync, SyncOptions};
use support::copy_tree;
use tempfile::TempDir;
fn packs_dir() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../packs")
}
fn scaffold(extra_ttl: &str) -> (TempDir, PathBuf) {
let dir = TempDir::new().expect("tempdir");
let project = dir.path().join("consumer");
copy_tree(
&packs_dir().join("github-actions-pack"),
&project.join("packs/github-actions-pack"),
);
std::fs::create_dir_all(project.join("templates")).expect("mkdir templates");
let ontology = format!(
"@prefix gha: <http://seanchatmangpt.github.io/packs/github-actions#> .\n\
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .\n\n\
{extra_ttl}\n"
);
std::fs::write(project.join("ontology.ttl"), ontology).expect("write ontology.ttl");
std::fs::write(
project.join("ggen.toml"),
"[project]\nname = \"gha-consumer\"\n\n\
[ontology]\nsource = \"ontology.ttl\"\n\n\
[packs]\ngithub-actions-pack = { path = \"packs/github-actions-pack\" }\n\n\
[templates]\ndir = \"templates\"\n",
)
.expect("write ggen.toml");
(dir, project)
}
const PRODUCTS: [&str; 4] = [
".github/workflows/reusable-rust-inspection.yml",
".github/actions/setup-ggen/action.yml",
".github/actions/emit-evidence/action.yml",
"docs/github-actions/inspection-caller-example.yml",
];
#[test]
fn github_actions_pack_syncs_schema_only_clean_and_idempotent() {
let (_dir, project) = scaffold("");
let first = sync(
&project,
SyncOptions {
consumer_mode: Default::default(),
dry_run: false,
..Default::default()
},
)
.expect("schema-only pack must sync cleanly with no consumer individuals");
for rel in PRODUCTS {
assert!(
!project.join(rel).exists(),
"schema-only pack must not fabricate retired product {rel}"
);
}
assert!(
!project.join(".github/workflows").exists(),
"no consumer individuals means no generated workflows"
);
let lock = std::fs::read_to_string(project.join("ggen.lock")).expect("ggen.lock");
assert!(
lock.contains("github-actions-pack"),
"lock must record the pack: {lock}"
);
let _ = sync(
&project,
SyncOptions {
consumer_mode: Default::default(),
dry_run: false,
..Default::default()
},
)
.expect("second sync");
assert!(
!project.join(".github/workflows").exists(),
"second sync must not start emitting products"
);
assert!(first.written.is_empty() || !project.join(".github/workflows").exists());
}
#[test]
fn github_actions_pack_refuses_write_all_ceiling() {
let (_dir, project) = scaffold(
"gha:SabotageWriteAll a gha:Workflow ;\n\
\u{20} gha:purpose \"sabotage: over-broad token\" ;\n\
\u{20} gha:trigger \"push\" ;\n\
\u{20} gha:permissionCeiling \"write-all\" .\n",
);
let err = sync(
&project,
SyncOptions {
consumer_mode: Default::default(),
dry_run: false,
..Default::default()
},
)
.expect_err("write-all ceiling must refuse the sync");
let msg = format!("{err}");
assert!(
msg.contains("020_security"),
"refusal must name the security gate: {msg}"
);
assert!(
msg.to_lowercase().contains("write-all"),
"refusal must name the write-all violation: {msg}"
);
for rel in PRODUCTS {
assert!(
!project.join(rel).exists(),
"refused sync must not emit {rel}"
);
}
}
#[test]
fn github_actions_pack_refuses_mutable_action_ref() {
let (_dir, project) = scaffold("gha:SabotageStep gha:usesAction \"actions/checkout@v4\" .\n");
let err = sync(
&project,
SyncOptions {
consumer_mode: Default::default(),
dry_run: false,
..Default::default()
},
)
.expect_err("mutable action ref must refuse the sync");
let msg = format!("{err}");
assert!(
msg.contains("020_security"),
"refusal must name the security gate: {msg}"
);
assert!(
msg.contains("actions/checkout@v4") && msg.contains("40-hex"),
"refusal must name the offending ref and the SHA-pin remedy: {msg}"
);
}
#[test]
fn github_actions_pack_refuses_workflow_missing_required_facts() {
let (_dir, project) = scaffold(
"gha:SabotageBare a gha:Workflow ;\n\
\u{20} rdfs:label \"sabotage: workflow with nothing declared\" .\n",
);
let err = sync(
&project,
SyncOptions {
consumer_mode: Default::default(),
dry_run: false,
..Default::default()
},
)
.expect_err("workflow without required facts must refuse the sync");
let msg = format!("{err}");
assert!(
msg.contains("010_required"),
"refusal must name the required-facts gate: {msg}"
);
assert!(
msg.contains("SabotageBare"),
"refusal must name the offending subject: {msg}"
);
}