summaryrefslogtreecommitdiff
path: root/tests/integration/cli_switch.rs
diff options
context:
space:
mode:
authorDawid Rycerz <dawid@rycerz.xyz>2026-02-15 21:27:00 +0100
committerDawid Rycerz <dawid@rycerz.xyz>2026-02-15 21:27:00 +0100
commitce0dbf6b249956700c6a1705bf4ad85a09d53e8c (patch)
treed7c3236807cfbf75d7f3a355eb5df5a5e2cc4ad7 /tests/integration/cli_switch.rs
parent064a1d01c5c14f5ecc032fa9b8346a4a88b893f6 (diff)
feat: witryna 0.2.0HEADv0.2.0main
Switch, cleanup, and status CLI commands. Persistent build state via state.json. Post-deploy hooks on success and failure with WITRYNA_BUILD_STATUS. Dependency diet (axum→tiny_http, clap→argh, tracing→log). Drop built-in rate limiting. Nix flake with NixOS module. Arch Linux PKGBUILD. Centralized version management. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'tests/integration/cli_switch.rs')
-rw-r--r--tests/integration/cli_switch.rs330
1 files changed, 330 insertions, 0 deletions
diff --git a/tests/integration/cli_switch.rs b/tests/integration/cli_switch.rs
new file mode 100644
index 0000000..fcbced6
--- /dev/null
+++ b/tests/integration/cli_switch.rs
@@ -0,0 +1,330 @@
+use std::fmt::Write as _;
+use std::process::Stdio;
+use tempfile::TempDir;
+use tokio::process::Command;
+
+fn witryna_bin() -> std::path::PathBuf {
+ let mut path = std::path::PathBuf::from(env!("CARGO_BIN_EXE_witryna"));
+ if !path.exists() {
+ path = std::path::PathBuf::from("target/debug/witryna");
+ }
+ path
+}
+
+async fn write_switch_config(
+ dir: &std::path::Path,
+ sites: &[&str],
+) -> (std::path::PathBuf, std::path::PathBuf) {
+ let base_dir = dir.join("data");
+ tokio::fs::create_dir_all(&base_dir).await.unwrap();
+
+ let mut sites_toml = String::new();
+ for name in sites {
+ write!(
+ sites_toml,
+ r#"
+[[sites]]
+name = "{name}"
+repo_url = "https://example.com/{name}.git"
+branch = "main"
+webhook_token = "unused"
+"#
+ )
+ .unwrap();
+ }
+
+ let config_path = dir.join("witryna.toml");
+ let config = format!(
+ r#"listen_address = "127.0.0.1:0"
+container_runtime = "podman"
+base_dir = "{base_dir}"
+log_dir = "{log_dir}"
+log_level = "info"
+{sites_toml}"#,
+ base_dir = base_dir.display(),
+ log_dir = dir.join("logs").display(),
+ );
+ tokio::fs::write(&config_path, config).await.unwrap();
+ (config_path, base_dir)
+}
+
+// ---------------------------------------------------------------------------
+// Tier 1: no container runtime / git needed
+// ---------------------------------------------------------------------------
+
+#[tokio::test]
+async fn cli_switch_unknown_site_exits_nonzero() {
+ let tempdir = TempDir::new().unwrap();
+ let (config_path, _) = write_switch_config(tempdir.path(), &["real-site"]).await;
+
+ let output = Command::new(witryna_bin())
+ .args([
+ "switch",
+ "--config",
+ config_path.to_str().unwrap(),
+ "nonexistent",
+ "20260126-143000-123456",
+ ])
+ .stdout(Stdio::piped())
+ .stderr(Stdio::piped())
+ .output()
+ .await
+ .unwrap();
+
+ assert!(!output.status.success(), "should exit non-zero");
+ let stderr = String::from_utf8_lossy(&output.stderr);
+ assert!(
+ stderr.contains("not found"),
+ "should mention 'not found', got: {stderr}"
+ );
+}
+
+#[tokio::test]
+async fn cli_switch_nonexistent_build_exits_nonzero() {
+ let tempdir = TempDir::new().unwrap();
+ let (config_path, base_dir) = write_switch_config(tempdir.path(), &["my-site"]).await;
+
+ // Create builds dir with one existing build
+ let builds_dir = base_dir.join("builds").join("my-site");
+ tokio::fs::create_dir_all(builds_dir.join("20260126-100000-000001"))
+ .await
+ .unwrap();
+
+ let output = Command::new(witryna_bin())
+ .args([
+ "switch",
+ "--config",
+ config_path.to_str().unwrap(),
+ "my-site",
+ "20260126-999999-999999",
+ ])
+ .stdout(Stdio::piped())
+ .stderr(Stdio::piped())
+ .output()
+ .await
+ .unwrap();
+
+ assert!(!output.status.success(), "should exit non-zero");
+ let stderr = String::from_utf8_lossy(&output.stderr);
+ assert!(
+ stderr.contains("not found"),
+ "should mention 'not found', got: {stderr}"
+ );
+ assert!(
+ stderr.contains("20260126-100000-000001"),
+ "should list available builds, got: {stderr}"
+ );
+}
+
+#[tokio::test]
+async fn cli_switch_invalid_timestamp_format_exits_nonzero() {
+ let tempdir = TempDir::new().unwrap();
+ let (config_path, base_dir) = write_switch_config(tempdir.path(), &["my-site"]).await;
+
+ // Create builds dir so the "no builds dir" check doesn't fire first
+ let builds_dir = base_dir.join("builds").join("my-site");
+ tokio::fs::create_dir_all(&builds_dir).await.unwrap();
+
+ let output = Command::new(witryna_bin())
+ .args([
+ "switch",
+ "--config",
+ config_path.to_str().unwrap(),
+ "my-site",
+ "not-a-timestamp",
+ ])
+ .stdout(Stdio::piped())
+ .stderr(Stdio::piped())
+ .output()
+ .await
+ .unwrap();
+
+ assert!(!output.status.success(), "should exit non-zero");
+ let stderr = String::from_utf8_lossy(&output.stderr);
+ assert!(
+ stderr.contains("not a valid build timestamp"),
+ "should mention invalid timestamp, got: {stderr}"
+ );
+}
+
+#[tokio::test]
+async fn cli_switch_updates_symlink() {
+ let tempdir = TempDir::new().unwrap();
+ let (config_path, base_dir) = write_switch_config(tempdir.path(), &["my-site"]).await;
+
+ let builds_dir = base_dir.join("builds").join("my-site");
+ let build1 = builds_dir.join("20260126-100000-000001");
+ let build2 = builds_dir.join("20260126-100000-000002");
+ tokio::fs::create_dir_all(&build1).await.unwrap();
+ tokio::fs::create_dir_all(&build2).await.unwrap();
+
+ // Point current at build1
+ let current = builds_dir.join("current");
+ tokio::fs::symlink(&build1, &current).await.unwrap();
+
+ // Switch to build2
+ let output = Command::new(witryna_bin())
+ .args([
+ "switch",
+ "--config",
+ config_path.to_str().unwrap(),
+ "my-site",
+ "20260126-100000-000002",
+ ])
+ .stdout(Stdio::piped())
+ .stderr(Stdio::piped())
+ .output()
+ .await
+ .unwrap();
+
+ assert!(
+ output.status.success(),
+ "should exit 0, stderr: {}",
+ String::from_utf8_lossy(&output.stderr)
+ );
+
+ // Verify symlink now points to build2
+ let target = tokio::fs::read_link(&current).await.unwrap();
+ assert_eq!(target, build2, "symlink should point to build2");
+
+ let stderr = String::from_utf8_lossy(&output.stderr);
+ assert!(
+ stderr.contains("switched my-site to build 20260126-100000-000002"),
+ "should confirm switch, got: {stderr}"
+ );
+}
+
+#[tokio::test]
+async fn cli_switch_preserves_builds() {
+ let tempdir = TempDir::new().unwrap();
+ let (config_path, base_dir) = write_switch_config(tempdir.path(), &["my-site"]).await;
+
+ let builds_dir = base_dir.join("builds").join("my-site");
+ let build1 = builds_dir.join("20260126-100000-000001");
+ let build2 = builds_dir.join("20260126-100000-000002");
+ tokio::fs::create_dir_all(&build1).await.unwrap();
+ tokio::fs::create_dir_all(&build2).await.unwrap();
+
+ // Point current at build1
+ let current = builds_dir.join("current");
+ tokio::fs::symlink(&build1, &current).await.unwrap();
+
+ // Switch to build2
+ let output = Command::new(witryna_bin())
+ .args([
+ "switch",
+ "--config",
+ config_path.to_str().unwrap(),
+ "my-site",
+ "20260126-100000-000002",
+ ])
+ .stdout(Stdio::piped())
+ .stderr(Stdio::piped())
+ .output()
+ .await
+ .unwrap();
+
+ assert!(output.status.success(), "should exit 0");
+
+ // Both builds should still exist
+ assert!(build1.exists(), "build1 should still exist after switch");
+ assert!(build2.exists(), "build2 should still exist after switch");
+}
+
+#[tokio::test]
+async fn cli_switch_updates_state_json() {
+ let tempdir = TempDir::new().unwrap();
+ let (config_path, base_dir) = write_switch_config(tempdir.path(), &["my-site"]).await;
+
+ let builds_dir = base_dir.join("builds").join("my-site");
+ let build1 = builds_dir.join("20260126-100000-000001");
+ let build2 = builds_dir.join("20260126-100000-000002");
+ tokio::fs::create_dir_all(&build1).await.unwrap();
+ tokio::fs::create_dir_all(&build2).await.unwrap();
+
+ // Write initial state.json with current pointing to build1
+ let state_json = serde_json::json!({
+ "current": "20260126-100000-000001",
+ "builds": [
+ {
+ "status": "success",
+ "timestamp": "20260126-100000-000002",
+ "started_at": "2026-01-26T10:00:00Z",
+ "git_commit": "bbb2222",
+ "duration": "20s",
+ "log": "/logs/2.log",
+ },
+ {
+ "status": "success",
+ "timestamp": "20260126-100000-000001",
+ "started_at": "2026-01-26T10:00:00Z",
+ "git_commit": "aaa1111",
+ "duration": "10s",
+ "log": "/logs/1.log",
+ }
+ ],
+ });
+ tokio::fs::write(builds_dir.join("state.json"), state_json.to_string())
+ .await
+ .unwrap();
+
+ // Create symlink
+ let current = builds_dir.join("current");
+ tokio::fs::symlink(&build1, &current).await.unwrap();
+
+ // Switch to build2
+ let output = Command::new(witryna_bin())
+ .args([
+ "switch",
+ "--config",
+ config_path.to_str().unwrap(),
+ "my-site",
+ "20260126-100000-000002",
+ ])
+ .stdout(Stdio::piped())
+ .stderr(Stdio::piped())
+ .output()
+ .await
+ .unwrap();
+
+ assert!(output.status.success(), "should exit 0");
+
+ // Verify state.json was updated
+ let state_content = tokio::fs::read_to_string(builds_dir.join("state.json"))
+ .await
+ .unwrap();
+ let state: serde_json::Value = serde_json::from_str(&state_content).unwrap();
+ assert_eq!(
+ state["current"], "20260126-100000-000002",
+ "state.json current should be updated after switch"
+ );
+}
+
+#[tokio::test]
+async fn cli_switch_no_builds_dir_exits_nonzero() {
+ let tempdir = TempDir::new().unwrap();
+ let (config_path, _) = write_switch_config(tempdir.path(), &["my-site"]).await;
+
+ // Don't create builds directory at all
+
+ let output = Command::new(witryna_bin())
+ .args([
+ "switch",
+ "--config",
+ config_path.to_str().unwrap(),
+ "my-site",
+ "20260126-100000-000001",
+ ])
+ .stdout(Stdio::piped())
+ .stderr(Stdio::piped())
+ .output()
+ .await
+ .unwrap();
+
+ assert!(!output.status.success(), "should exit non-zero");
+ let stderr = String::from_utf8_lossy(&output.stderr);
+ assert!(
+ stderr.contains("no builds found"),
+ "should mention no builds, got: {stderr}"
+ );
+}