Skip to content

Commit a07dd4c

Browse files
committed
test(ci): CI-25/26 unit and integration tests, language fixtures, insta snapshot
1 parent c844a44 commit a07dd4c

13 files changed

Lines changed: 714 additions & 2 deletions

File tree

‎Cargo.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ assert_cmd = "2"
125125
predicates = "3"
126126
tempfile = "3"
127127
proptest = "1"
128+
insta = { version = "1", features = ["yaml"] }
128129

129130
# Fast debug builds - prioritize compile speed over runtime
130131
[profile.dev]

‎src/generator/ci_generation/mod.rs‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,5 +35,4 @@ pub mod token_resolver;
3535
pub mod triggers;
3636
pub mod writer;
3737

38-
#[cfg(test)]
3938
pub mod test_helpers;

‎src/generator/ci_generation/test_helpers.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use crate::generator::ci_generation::context::{CiContext, PackageManager};
1212
/// Fields that matter for the test under hand should be overridden by the
1313
/// caller after construction. Using struct-update syntax is idiomatic:
1414
///
15-
/// ```rust
15+
/// ```ignore
1616
/// let ctx = make_base_ctx(dir.path(), "TypeScript");
1717
/// let ctx = CiContext { package_manager: PackageManager::Npm, ..ctx };
1818
/// ```

‎tests/ci_generator_integration.rs‎

Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
//! CI-26 — End-to-end integration tests for the CI generation subsystem.
2+
//!
3+
//! Tests template rendering at the full-pipeline level — valid YAML output,
4+
//! required structural fields, and absence of hardcoded secrets.
5+
//!
6+
//! Also exercises `collect_ci_context` against the language fixture projects
7+
//! in `tests/fixtures/ci/` to verify that context collection succeeds and
8+
//! produces the expected primary language for each ecosystem.
9+
//!
10+
//! # Note on CI-01 wiring
11+
//!
12+
//! The CLI handler `handle_generate_ci` currently returns a static skeleton
13+
//! rather than invoking the full pipeline. These tests exercise the template
14+
//! layer directly. A companion test asserting the full CLI binary output will
15+
//! be added once CI-01 (final wiring) replaces the stub.
16+
17+
use std::path::PathBuf;
18+
19+
use syncable_cli::cli::{CiFormat, CiPlatform};
20+
use syncable_cli::generator::ci_generation::{
21+
context::collect_ci_context,
22+
schema::{
23+
CiPipeline, InstallStep, RuntimeStep, SecretScanStep, TestStep, TriggerConfig,
24+
},
25+
templates,
26+
};
27+
28+
// ── Shared helpers ────────────────────────────────────────────────────────────
29+
30+
fn minimal_pipeline(platform: CiPlatform, format: CiFormat) -> CiPipeline {
31+
CiPipeline {
32+
project_name: "integration-test-app".to_string(),
33+
platform,
34+
format,
35+
triggers: TriggerConfig {
36+
push_branches: vec!["main".to_string()],
37+
pr_branches: vec!["main".to_string()],
38+
tag_pattern: None,
39+
scheduled: None,
40+
},
41+
runtime: RuntimeStep {
42+
action: "actions/setup-node@v4".to_string(),
43+
version: "20".to_string(),
44+
},
45+
cache: None,
46+
install: InstallStep { command: "npm ci".to_string() },
47+
lint: None,
48+
test: TestStep {
49+
command: "npm test".to_string(),
50+
coverage_flag: None,
51+
coverage_report_path: None,
52+
},
53+
build: None,
54+
docker_build: None,
55+
image_scan: None,
56+
secret_scan: syncable_cli::generator::ci_generation::schema::SecretScanStep {
57+
github_token_expr: "${{ secrets.GITHUB_TOKEN }}".to_string(),
58+
gitleaks_license_secret: None,
59+
},
60+
upload_artifact: None,
61+
unresolved_tokens: vec![],
62+
}
63+
}
64+
65+
/// Returns the absolute path to a CI language fixture directory.
66+
fn fixture(lang: &str) -> PathBuf {
67+
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
68+
.join("tests")
69+
.join("fixtures")
70+
.join("ci")
71+
.join(lang)
72+
}
73+
74+
/// Asserts that `yaml` contains no string patterns that look like real
75+
/// credential values (GitHub tokens, AWS keys, etc.).
76+
fn assert_no_hardcoded_secrets(yaml: &str) {
77+
// Real GitHub personal-access tokens start with "ghp_" followed by 36+ alphanum chars.
78+
assert!(
79+
!yaml.split_whitespace().any(|w| w.starts_with("ghp_") && w.len() > 10),
80+
"output contains a GitHub token pattern: {yaml}"
81+
);
82+
// Real AWS access key IDs start with "AKIA" followed by exactly 16 uppercase chars.
83+
assert!(
84+
!yaml.split_whitespace().any(|w| {
85+
w.starts_with("AKIA")
86+
&& w.len() == 20
87+
&& w[4..].chars().all(|c| c.is_ascii_uppercase() || c.is_ascii_digit())
88+
}),
89+
"output contains an AWS access key pattern: {yaml}"
90+
);
91+
}
92+
93+
// ── GitHub Actions end-to-end ─────────────────────────────────────────────────
94+
95+
#[test]
96+
fn github_actions_output_is_valid_yaml() {
97+
let yaml = templates::github_actions::render(&minimal_pipeline(
98+
CiPlatform::Hetzner,
99+
CiFormat::GithubActions,
100+
));
101+
serde_yaml::from_str::<serde_yaml::Value>(&yaml)
102+
.expect("GitHub Actions output must be valid YAML");
103+
}
104+
105+
#[test]
106+
fn github_actions_output_contains_checkout_step() {
107+
let yaml = templates::github_actions::render(&minimal_pipeline(
108+
CiPlatform::Hetzner,
109+
CiFormat::GithubActions,
110+
));
111+
assert!(
112+
yaml.contains("actions/checkout"),
113+
"GitHub Actions pipeline must include a checkout step"
114+
);
115+
}
116+
117+
#[test]
118+
fn github_actions_output_contains_runtime_setup_step() {
119+
let yaml = templates::github_actions::render(&minimal_pipeline(
120+
CiPlatform::Hetzner,
121+
CiFormat::GithubActions,
122+
));
123+
// Runtime setup action was injected into the pipeline.
124+
assert!(
125+
yaml.contains("setup-node"),
126+
"pipeline must contain a runtime setup step"
127+
);
128+
}
129+
130+
#[test]
131+
fn github_actions_output_contains_test_step() {
132+
let yaml = templates::github_actions::render(&minimal_pipeline(
133+
CiPlatform::Hetzner,
134+
CiFormat::GithubActions,
135+
));
136+
assert!(yaml.contains("npm test"), "pipeline must contain the test command");
137+
}
138+
139+
#[test]
140+
fn github_actions_output_has_no_hardcoded_secrets() {
141+
let yaml = templates::github_actions::render(&minimal_pipeline(
142+
CiPlatform::Hetzner,
143+
CiFormat::GithubActions,
144+
));
145+
assert_no_hardcoded_secrets(&yaml);
146+
}
147+
148+
// ── Azure Pipelines end-to-end ────────────────────────────────────────────────
149+
150+
#[test]
151+
fn azure_pipelines_output_is_valid_yaml() {
152+
let yaml = templates::azure_pipelines::render(&minimal_pipeline(
153+
CiPlatform::Azure,
154+
CiFormat::AzurePipelines,
155+
));
156+
serde_yaml::from_str::<serde_yaml::Value>(&yaml)
157+
.expect("Azure Pipelines output must be valid YAML");
158+
}
159+
160+
#[test]
161+
fn azure_pipelines_output_contains_required_fields() {
162+
let yaml = templates::azure_pipelines::render(&minimal_pipeline(
163+
CiPlatform::Azure,
164+
CiFormat::AzurePipelines,
165+
));
166+
// Azure auto-checkouts; runtime setup and test step are required.
167+
assert!(yaml.contains("npm test"), "Azure pipeline must contain the test command");
168+
assert!(
169+
yaml.contains("ubuntu") || yaml.contains("ubuntu-latest"),
170+
"Azure pipeline must specify an agent VM image"
171+
);
172+
}
173+
174+
#[test]
175+
fn azure_pipelines_output_has_no_hardcoded_secrets() {
176+
let yaml = templates::azure_pipelines::render(&minimal_pipeline(
177+
CiPlatform::Azure,
178+
CiFormat::AzurePipelines,
179+
));
180+
assert_no_hardcoded_secrets(&yaml);
181+
}
182+
183+
// ── Cloud Build end-to-end ────────────────────────────────────────────────────
184+
185+
#[test]
186+
fn cloud_build_output_is_valid_yaml() {
187+
let yaml = templates::cloud_build::render(&minimal_pipeline(
188+
CiPlatform::Gcp,
189+
CiFormat::CloudBuild,
190+
));
191+
serde_yaml::from_str::<serde_yaml::Value>(&yaml)
192+
.expect("Cloud Build output must be valid YAML");
193+
}
194+
195+
#[test]
196+
fn cloud_build_output_contains_test_step() {
197+
let yaml = templates::cloud_build::render(&minimal_pipeline(
198+
CiPlatform::Gcp,
199+
CiFormat::CloudBuild,
200+
));
201+
assert!(yaml.contains("npm test"), "Cloud Build pipeline must contain the test command");
202+
}
203+
204+
#[test]
205+
fn cloud_build_output_has_no_hardcoded_secrets() {
206+
let yaml = templates::cloud_build::render(&minimal_pipeline(
207+
CiPlatform::Gcp,
208+
CiFormat::CloudBuild,
209+
));
210+
assert_no_hardcoded_secrets(&yaml);
211+
}
212+
213+
// ── CiContext collection from language fixtures ───────────────────────────────
214+
215+
#[test]
216+
fn collect_ci_context_succeeds_for_node_fixture() {
217+
let ctx = collect_ci_context(&fixture("node"), CiPlatform::Hetzner, CiFormat::GithubActions)
218+
.expect("should collect context from Node.js fixture");
219+
assert_ne!(
220+
ctx.primary_language.to_lowercase(),
221+
"unknown",
222+
"should detect a real language for Node.js fixture"
223+
);
224+
}
225+
226+
#[test]
227+
fn collect_ci_context_succeeds_for_python_fixture() {
228+
let ctx =
229+
collect_ci_context(&fixture("python"), CiPlatform::Gcp, CiFormat::GithubActions)
230+
.expect("should collect context from Python fixture");
231+
assert_ne!(ctx.primary_language.to_lowercase(), "unknown");
232+
}
233+
234+
#[test]
235+
fn collect_ci_context_succeeds_for_rust_fixture() {
236+
let ctx =
237+
collect_ci_context(&fixture("rust"), CiPlatform::Hetzner, CiFormat::GithubActions)
238+
.expect("should collect context from Rust fixture");
239+
assert!(
240+
ctx.primary_language.to_lowercase().contains("rust"),
241+
"expected Rust primary language, got: {}",
242+
ctx.primary_language
243+
);
244+
}
245+
246+
#[test]
247+
fn collect_ci_context_succeeds_for_go_fixture() {
248+
let ctx = collect_ci_context(&fixture("go"), CiPlatform::Gcp, CiFormat::GithubActions)
249+
.expect("should collect context from Go fixture");
250+
assert_ne!(ctx.primary_language.to_lowercase(), "unknown");
251+
}
252+
253+
#[test]
254+
fn collect_ci_context_succeeds_for_java_fixture() {
255+
let ctx = collect_ci_context(&fixture("java"), CiPlatform::Azure, CiFormat::AzurePipelines)
256+
.expect("should collect context from Java fixture");
257+
assert_ne!(ctx.primary_language.to_lowercase(), "unknown");
258+
}

0 commit comments

Comments
 (0)