1#[cfg(test)]
17mod tests {
18 use std::fs;
19 use std::os::unix::fs::PermissionsExt;
20 use std::path::{Path, PathBuf};
21 use std::process::Command;
22
23 fn repo_root() -> PathBuf {
24 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
25 .parent()
26 .expect("xtask has a parent")
27 .to_path_buf()
28 }
29
30 fn write_shim(dir: &Path, name: &str, body: &str) {
31 let path = dir.join(name);
32 fs::write(&path, body).expect("write shim");
33 let mut perms = fs::metadata(&path).expect("stat shim").permissions();
34 perms.set_mode(0o755);
35 fs::set_permissions(&path, perms).expect("chmod shim");
36 }
37
38 fn render_job() -> String {
44 use std::sync::atomic::{AtomicUsize, Ordering};
45 static SEQ: AtomicUsize = AtomicUsize::new(0);
46 let unique = format!(
47 "{}-{}",
48 std::process::id(),
49 SEQ.fetch_add(1, Ordering::Relaxed)
50 );
51 let root = repo_root();
52 let script = root.join(".forgejo/scripts/deploy.sh");
53 assert!(
54 script.exists(),
55 "deploy.sh not found at {}",
56 script.display()
57 );
58
59 let tmp = std::env::temp_dir().join(format!("nomisync-deploy-render-{unique}"));
60 let _ = fs::remove_dir_all(&tmp);
61 fs::create_dir_all(&tmp).expect("temp dir");
62 let captured = tmp.join("job.yaml");
63
64 write_shim(
66 &tmp,
67 "kubectl",
68 &format!(
69 "#!/usr/bin/env bash\nfor a in \"$@\"; do [ \"$a\" = \"-f\" ] && exec cat > {}; done\nexit 0\n",
70 captured.display()
71 ),
72 );
73 write_shim(
75 &tmp,
76 "git",
77 "#!/usr/bin/env bash\ncase \"$1\" in\n ls-remote) echo \"$SHA refs/heads/master\";;\nesac\nexit 0\n",
78 );
79
80 let path = format!(
81 "{}:{}",
82 tmp.display(),
83 std::env::var("PATH").unwrap_or_default()
84 );
85 let _ = Command::new("timeout")
88 .args(["25", "bash", &script.to_string_lossy()])
89 .env("PATH", path)
90 .env("REPO", "rayslava/nomisync")
91 .env("SHA", "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef")
92 .env("GIT_TOKEN", "token")
93 .env("GITHUB_RUN_NUMBER", "1")
94 .env("GITHUB_RUN_ID", "1")
95 .current_dir(&root)
96 .output()
97 .expect("run deploy.sh");
98
99 let rendered = fs::read_to_string(&captured).unwrap_or_else(|_| {
100 panic!(
101 "deploy.sh applied nothing — it died before rendering the Job. \
102 That is what an unescaped $ or a backtick in the heredoc does."
103 )
104 });
105 let _ = fs::remove_dir_all(&tmp);
106 rendered
107 }
108
109 fn prepare_script(job: &str) -> String {
111 let marker = " - |\n";
112 let start = job.rfind(marker).expect("a block scalar in the Job") + marker.len();
113 job[start..]
114 .lines()
115 .map(|line| line.strip_prefix(" ").unwrap_or(line))
116 .collect::<Vec<_>>()
117 .join("\n")
118 }
119
120 #[test]
121 fn deploy_job_renders_at_all() {
122 let job = render_job();
123 assert!(job.contains("kind: Job"), "not a Job manifest:\n{job}");
124 assert!(
125 job.contains("name: kaniko"),
126 "the kaniko container is missing, so the heredoc was truncated"
127 );
128 }
129
130 #[test]
131 fn the_outer_heredoc_does_not_eat_inner_dollars() {
132 let job = render_job();
135 assert!(
136 job.contains("print(m$1)"),
137 "$1 was expanded away by the outer heredoc; escape it as \\$1"
138 );
139 }
140
141 #[test]
142 fn the_heredoc_source_contains_no_backticks() {
143 let source = fs::read_to_string(repo_root().join(".forgejo/scripts/deploy.sh"))
149 .expect("read deploy.sh");
150 let start = source
151 .find("kubectl apply -f - <<EOF")
152 .expect("the Job heredoc");
153 let region = &source[start..];
154 let end = region.find("\nEOF\n").map_or(region.len(), |i| i + 1);
155
156 let offenders: Vec<&str> = region[..end]
157 .lines()
158 .filter(|line| line.contains('`'))
159 .collect();
160 assert!(
161 offenders.is_empty(),
162 "backticks inside the unquoted <<EOF are command substitution, even in comments:\n{}",
163 offenders.join("\n")
164 );
165 }
166
167 #[test]
168 fn the_prepare_script_is_valid_bash() {
169 let job = render_job();
170 let script = prepare_script(&job);
171 let dir = std::env::temp_dir().join(format!(
172 "nomisync-prepare-{}-{:?}",
173 std::process::id(),
174 std::thread::current().id()
175 ));
176 std::fs::create_dir_all(&dir).expect("temp dir");
177 let path = dir.join("prepare.sh");
178 std::fs::write(&path, &script).expect("write prepare");
179
180 let out = Command::new("bash")
181 .args(["-n", &path.to_string_lossy()])
182 .output()
183 .expect("bash -n");
184 let _ = std::fs::remove_dir_all(&dir);
185
186 assert!(
187 out.status.success(),
188 "the rendered prepare script is not valid bash:\n{}",
189 String::from_utf8_lossy(&out.stderr)
190 );
191 }
192}