use just_shield::github_facts::GithubFacts;
use std::collections::HashMap;
use std::io;
use std::path::PathBuf;
struct FakeGithub(HashMap<(String, String), String>);
impl FakeGithub {
fn new(entries: &[(&str, &str, &str)]) -> Self {
Self(
entries
.iter()
.map(|(repo, r, sha)| ((repo.to_string(), r.to_string()), sha.to_string()))
.collect(),
)
}
}
impl GithubFacts for FakeGithub {
fn resolve_ref(&self, owner_repo: &str, git_ref: &str) -> io::Result<Option<String>> {
Ok(self
.0
.get(&(owner_repo.to_string(), git_ref.to_string()))
.cloned())
}
}
fn make_repo(name: &str, workflow: &str) -> PathBuf {
let root = std::env::temp_dir().join(format!("just-shield-fix-{}-{name}", std::process::id()));
let _ = std::fs::remove_dir_all(&root);
std::fs::create_dir_all(root.join(".github").join("workflows")).unwrap();
std::fs::write(
root.join(".github").join("workflows").join("ci.yml"),
workflow,
)
.unwrap();
root
}
const SHA_A: &str = "aaaa000000000000000000000000000000000000";
const SHA_B: &str = "bbbb000000000000000000000000000000000000";
const SHA_C: &str = "cccc000000000000000000000000000000000000";
fn fake() -> FakeGithub {
FakeGithub::new(&[
("aquasecurity/trivy-action", "v0.28.0", SHA_A),
("quoted/action", "v1", SHA_B),
("commented/action", "v2", SHA_C),
])
}
const WORKFLOW: &str = "name: CI\non: push # 트리거 주석\npermissions:\n contents: read\njobs:\n b:\n steps:\n - uses: aquasecurity/trivy-action@v0.28.0\n - uses: \"quoted/action@v1\"\n - uses: commented/action@v2 # 기존 주석\n - uses: ./local/action\n - uses: docker://alpine:3.19\n - uses: pinned/action@0123456789abcdef0123456789abcdef01234567 # v9\n - uses: unknown/action@v3\n";
#[test]
fn replaces_mutable_refs_and_preserves_everything_else() {
let root = make_repo("replace", WORKFLOW);
let outcome = just_shield::fix::fix(&root, &fake(), false).unwrap();
let content =
std::fs::read_to_string(root.join(".github").join("workflows").join("ci.yml")).unwrap();
let _ = std::fs::remove_dir_all(&root);
assert!(outcome.applied);
assert_eq!(outcome.changes.len(), 3);
assert!(content.contains(&format!(
"- uses: aquasecurity/trivy-action@{SHA_A} # v0.28.0"
)));
assert!(content.contains(&format!("- uses: \"quoted/action@{SHA_B}\" # v1")));
assert!(content.contains(&format!("- uses: commented/action@{SHA_C} # 기존 주석")));
assert!(!content.contains("# 기존 주석 # v2"));
assert!(content.contains("- uses: ./local/action"));
assert!(content.contains("- uses: docker://alpine:3.19"));
assert!(
content.contains("- uses: pinned/action@0123456789abcdef0123456789abcdef01234567 # v9")
);
assert!(content.contains("on: push # 트리거 주석"));
assert!(content.contains("- uses: unknown/action@v3"));
assert_eq!(outcome.skipped.len(), 1);
assert!(outcome.skipped[0].0.contains("unknown/action@v3"));
}
#[test]
fn fix_is_idempotent() {
let root = make_repo("idem", WORKFLOW);
just_shield::fix::fix(&root, &fake(), false).unwrap();
let first =
std::fs::read_to_string(root.join(".github").join("workflows").join("ci.yml")).unwrap();
let second_outcome = just_shield::fix::fix(&root, &fake(), false).unwrap();
let second =
std::fs::read_to_string(root.join(".github").join("workflows").join("ci.yml")).unwrap();
let _ = std::fs::remove_dir_all(&root);
assert_eq!(second_outcome.changes.len(), 0);
assert_eq!(first, second);
}
#[test]
fn dry_run_reports_but_does_not_write() {
let root = make_repo("dry", WORKFLOW);
let outcome = just_shield::fix::fix(&root, &fake(), true).unwrap();
let content =
std::fs::read_to_string(root.join(".github").join("workflows").join("ci.yml")).unwrap();
let _ = std::fs::remove_dir_all(&root);
assert!(!outcome.applied);
assert_eq!(outcome.changes.len(), 3, "미리보기에도 변경 목록은 나온다");
assert_eq!(content, WORKFLOW, "파일은 바이트 단위로 그대로여야 한다");
}
#[test]
fn crlf_line_endings_are_preserved() {
let crlf_workflow = WORKFLOW.replace('\n', "\r\n");
let root = make_repo("crlf", &crlf_workflow);
just_shield::fix::fix(&root, &fake(), false).unwrap();
let content =
std::fs::read_to_string(root.join(".github").join("workflows").join("ci.yml")).unwrap();
let _ = std::fs::remove_dir_all(&root);
assert!(!content.contains("\n\n") || crlf_workflow.contains("\r\n\r\n"));
assert_eq!(
content.matches('\n').count(),
content.matches("\r\n").count()
);
assert!(content.contains(&format!(
"- uses: aquasecurity/trivy-action@{SHA_A} # v0.28.0\r\n"
)));
}
#[test]
fn first_party_refs_are_left_alone() {
let root = make_repo(
"firstparty",
"on: push\njobs:\n b:\n steps:\n - uses: myorg/tool@v1\n - uses: other/tool@v1\n",
);
std::fs::create_dir_all(root.join(".git")).unwrap();
std::fs::write(
root.join(".git").join("config"),
"[remote \"origin\"]\n\turl = https://github.com/myorg/myrepo.git\n",
)
.unwrap();
let facts = FakeGithub::new(&[("myorg/tool", "v1", SHA_A), ("other/tool", "v1", SHA_B)]);
let outcome = just_shield::fix::fix(&root, &facts, false).unwrap();
let content =
std::fs::read_to_string(root.join(".github").join("workflows").join("ci.yml")).unwrap();
let _ = std::fs::remove_dir_all(&root);
assert_eq!(outcome.changes.len(), 1);
assert!(content.contains("- uses: myorg/tool@v1"));
assert!(content.contains(&format!("- uses: other/tool@{SHA_B} # v1")));
}