Some checks failed
CI / Rustfmt (push) Successful in 23s
CI / Cargo Audit & Deny (push) Successful in 30s
CI / Web Blocking Checks (push) Successful in 48s
CI / Security Blocking Checks (push) Successful in 8s
CI / Clippy (push) Failing after 1m55s
CI / Web Advisory Checks (push) Successful in 35s
CI / Security Advisory Checks (push) Successful in 37s
CI / Tests (push) Successful in 8m5s
523 lines
16 KiB
Rust
523 lines
16 KiB
Rust
//! CLI integration tests for pack registry commands
|
|
#![allow(deprecated)]
|
|
|
|
//!
|
|
//! This module tests:
|
|
//! - `attune pack install` command with all sources
|
|
//! - `attune pack checksum` command
|
|
//! - `attune pack index-entry` command
|
|
//! - `attune pack index-update` command
|
|
//! - `attune pack index-merge` command
|
|
//! - Error handling and output formatting
|
|
|
|
use assert_cmd::Command;
|
|
use predicates::prelude::*;
|
|
use serde_json::Value;
|
|
use std::fs;
|
|
|
|
use tempfile::TempDir;
|
|
|
|
/// Helper to create a test pack directory with pack.yaml
|
|
fn create_test_pack(name: &str, version: &str, deps: &[&str]) -> TempDir {
|
|
let temp_dir = TempDir::new().unwrap();
|
|
|
|
let deps_yaml = if deps.is_empty() {
|
|
"dependencies: []".to_string()
|
|
} else {
|
|
let dep_list = deps
|
|
.iter()
|
|
.map(|d| format!(" - {}", d))
|
|
.collect::<Vec<_>>()
|
|
.join("\n");
|
|
format!("dependencies:\n{}", dep_list)
|
|
};
|
|
|
|
let pack_yaml = format!(
|
|
r#"
|
|
ref: {}
|
|
name: Test Pack {}
|
|
version: {}
|
|
description: Test pack for CLI integration tests
|
|
author: Test Author
|
|
email: test@example.com
|
|
license: Apache-2.0
|
|
homepage: https://example.com
|
|
repository: https://github.com/example/pack
|
|
keywords:
|
|
- test
|
|
- cli
|
|
{}
|
|
python: "3.8"
|
|
actions:
|
|
test_action:
|
|
entry_point: test.py
|
|
runner_type: python-script
|
|
description: Test action
|
|
sensors:
|
|
test_sensor:
|
|
entry_point: sensor.py
|
|
runner_type: python-script
|
|
triggers:
|
|
test_trigger:
|
|
description: Test trigger
|
|
"#,
|
|
name, name, version, deps_yaml
|
|
);
|
|
|
|
fs::write(temp_dir.path().join("pack.yaml"), pack_yaml).unwrap();
|
|
fs::write(temp_dir.path().join("test.py"), "print('test action')").unwrap();
|
|
fs::write(temp_dir.path().join("sensor.py"), "print('test sensor')").unwrap();
|
|
|
|
temp_dir
|
|
}
|
|
|
|
/// Helper to create a registry index file
|
|
fn create_test_index(packs: &[(&str, &str)]) -> TempDir {
|
|
let temp_dir = TempDir::new().unwrap();
|
|
|
|
let pack_entries: Vec<String> = packs
|
|
.iter()
|
|
.map(|(name, version)| {
|
|
format!(
|
|
r#"{{
|
|
"ref": "{}",
|
|
"label": "Test Pack {}",
|
|
"version": "{}",
|
|
"author": "Test",
|
|
"license": "Apache-2.0",
|
|
"keywords": ["test"],
|
|
"install_sources": [
|
|
{{
|
|
"type": "git",
|
|
"url": "https://github.com/test/{}.git",
|
|
"ref": "v{}",
|
|
"checksum": "sha256:abc123"
|
|
}}
|
|
]
|
|
}}"#,
|
|
name, name, version, name, version
|
|
)
|
|
})
|
|
.collect();
|
|
|
|
let index = format!(
|
|
r#"{{
|
|
"version": "1.0",
|
|
"packs": [
|
|
{}
|
|
]
|
|
}}"#,
|
|
pack_entries.join(",\n")
|
|
);
|
|
|
|
fs::write(temp_dir.path().join("index.json"), index).unwrap();
|
|
|
|
temp_dir
|
|
}
|
|
|
|
/// Create an isolated CLI command that never touches the user's real config.
|
|
///
|
|
/// Returns `(Command, TempDir)` — the `TempDir` must be kept alive for the
|
|
/// duration of the test so the config directory isn't deleted prematurely.
|
|
fn isolated_cmd() -> (Command, TempDir) {
|
|
let config_dir = TempDir::new().expect("Failed to create temp config dir");
|
|
|
|
// Write a minimal default config so the CLI doesn't try to create one
|
|
let attune_dir = config_dir.path().join("attune");
|
|
fs::create_dir_all(&attune_dir).expect("Failed to create attune config dir");
|
|
fs::write(
|
|
attune_dir.join("config.yaml"),
|
|
"profile: default\nformat: table\nprofiles:\n default:\n api_url: http://localhost:8080\n",
|
|
)
|
|
.expect("Failed to write test config");
|
|
|
|
let mut cmd = Command::cargo_bin("attune").unwrap();
|
|
cmd.env("XDG_CONFIG_HOME", config_dir.path())
|
|
.env("HOME", config_dir.path());
|
|
(cmd, config_dir)
|
|
}
|
|
|
|
#[test]
|
|
fn test_pack_checksum_directory() {
|
|
let pack_dir = create_test_pack("checksum-test", "1.0.0", &[]);
|
|
|
|
let (mut cmd, _config_dir) = isolated_cmd();
|
|
cmd.arg("--output")
|
|
.arg("table")
|
|
.arg("pack")
|
|
.arg("checksum")
|
|
.arg(pack_dir.path().to_str().unwrap());
|
|
|
|
cmd.assert()
|
|
.success()
|
|
.stdout(predicate::str::contains("sha256:"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_pack_checksum_json_output() {
|
|
let pack_dir = create_test_pack("checksum-json", "1.0.0", &[]);
|
|
|
|
let (mut cmd, _config_dir) = isolated_cmd();
|
|
cmd.arg("--output")
|
|
.arg("json")
|
|
.arg("pack")
|
|
.arg("checksum")
|
|
.arg(pack_dir.path().to_str().unwrap());
|
|
|
|
let output = cmd.assert().success();
|
|
let stdout = String::from_utf8(output.get_output().stdout.clone()).unwrap();
|
|
|
|
// Verify it's valid JSON
|
|
let json: Value = serde_json::from_str(&stdout).unwrap();
|
|
assert!(json["checksum"].is_string());
|
|
assert!(json["checksum"].as_str().unwrap().starts_with("sha256:"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_pack_checksum_nonexistent_path() {
|
|
let (mut cmd, _config_dir) = isolated_cmd();
|
|
cmd.arg("pack").arg("checksum").arg("/nonexistent/path");
|
|
|
|
cmd.assert().failure().stderr(
|
|
predicate::str::contains("not found").or(predicate::str::contains("does not exist")),
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_pack_index_entry_generates_valid_json() {
|
|
let pack_dir = create_test_pack("index-entry-test", "1.2.3", &[]);
|
|
|
|
let (mut cmd, _config_dir) = isolated_cmd();
|
|
cmd.arg("--output")
|
|
.arg("json")
|
|
.arg("pack")
|
|
.arg("index-entry")
|
|
.arg(pack_dir.path().to_str().unwrap())
|
|
.arg("--git-url")
|
|
.arg("https://github.com/test/pack.git")
|
|
.arg("--git-ref")
|
|
.arg("v1.2.3");
|
|
|
|
let output = cmd.assert().success();
|
|
let stdout = String::from_utf8(output.get_output().stdout.clone()).unwrap();
|
|
|
|
// Verify it's valid JSON
|
|
let json: Value = serde_json::from_str(&stdout).unwrap();
|
|
assert_eq!(json["ref"], "index-entry-test");
|
|
assert_eq!(json["version"], "1.2.3");
|
|
assert!(json["install_sources"].is_array());
|
|
assert!(json["install_sources"][0]["checksum"]
|
|
.as_str()
|
|
.unwrap()
|
|
.starts_with("sha256:"));
|
|
|
|
// Verify metadata
|
|
assert_eq!(json["author"], "Test Author");
|
|
assert_eq!(json["license"], "Apache-2.0");
|
|
assert!(!json["keywords"].as_array().unwrap().is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_pack_index_entry_with_archive_url() {
|
|
let pack_dir = create_test_pack("archive-test", "2.0.0", &[]);
|
|
|
|
let (mut cmd, _config_dir) = isolated_cmd();
|
|
cmd.arg("--output")
|
|
.arg("json")
|
|
.arg("pack")
|
|
.arg("index-entry")
|
|
.arg(pack_dir.path().to_str().unwrap())
|
|
.arg("--archive-url")
|
|
.arg("https://releases.example.com/pack-2.0.0.tar.gz");
|
|
|
|
let output = cmd.assert().success();
|
|
let stdout = String::from_utf8(output.get_output().stdout.clone()).unwrap();
|
|
|
|
let json: Value = serde_json::from_str(&stdout).unwrap();
|
|
assert!(!json["install_sources"].as_array().unwrap().is_empty());
|
|
|
|
let archive_source = &json["install_sources"][0];
|
|
assert_eq!(archive_source["type"], "archive");
|
|
assert_eq!(
|
|
archive_source["url"],
|
|
"https://releases.example.com/pack-2.0.0.tar.gz"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_pack_index_entry_missing_pack_yaml() {
|
|
let temp_dir = TempDir::new().unwrap();
|
|
fs::write(temp_dir.path().join("readme.txt"), "No pack.yaml here").unwrap();
|
|
|
|
let (mut cmd, _config_dir) = isolated_cmd();
|
|
cmd.arg("pack")
|
|
.arg("index-entry")
|
|
.arg(temp_dir.path().to_str().unwrap());
|
|
|
|
cmd.assert()
|
|
.failure()
|
|
.stderr(predicate::str::contains("pack.yaml"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_pack_index_update_adds_new_entry() {
|
|
let index_dir = create_test_index(&[("existing-pack", "1.0.0")]);
|
|
let index_path = index_dir.path().join("index.json");
|
|
|
|
let pack_dir = create_test_pack("new-pack", "1.0.0", &[]);
|
|
|
|
let (mut cmd, _config_dir) = isolated_cmd();
|
|
cmd.arg("pack")
|
|
.arg("index-update")
|
|
.arg("--index")
|
|
.arg(index_path.to_str().unwrap())
|
|
.arg(pack_dir.path().to_str().unwrap())
|
|
.arg("--git-url")
|
|
.arg("https://github.com/test/new-pack.git")
|
|
.arg("--git-ref")
|
|
.arg("v1.0.0");
|
|
|
|
cmd.assert()
|
|
.success()
|
|
.stdout(predicate::str::contains("new-pack"))
|
|
.stdout(predicate::str::contains("1.0.0"));
|
|
|
|
// Verify index was updated
|
|
let updated_index = fs::read_to_string(&index_path).unwrap();
|
|
let json: Value = serde_json::from_str(&updated_index).unwrap();
|
|
assert_eq!(json["packs"].as_array().unwrap().len(), 2);
|
|
}
|
|
|
|
#[test]
|
|
fn test_pack_index_update_prevents_duplicate_without_flag() {
|
|
let index_dir = create_test_index(&[("existing-pack", "1.0.0")]);
|
|
let index_path = index_dir.path().join("index.json");
|
|
|
|
let pack_dir = create_test_pack("existing-pack", "1.0.0", &[]);
|
|
|
|
let (mut cmd, _config_dir) = isolated_cmd();
|
|
cmd.arg("pack")
|
|
.arg("index-update")
|
|
.arg("--index")
|
|
.arg(index_path.to_str().unwrap())
|
|
.arg(pack_dir.path().to_str().unwrap())
|
|
.arg("--git-url")
|
|
.arg("https://github.com/test/existing-pack.git");
|
|
|
|
cmd.assert()
|
|
.failure()
|
|
.stderr(predicate::str::contains("already exists"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_pack_index_update_with_update_flag() {
|
|
let index_dir = create_test_index(&[("existing-pack", "1.0.0")]);
|
|
let index_path = index_dir.path().join("index.json");
|
|
|
|
let pack_dir = create_test_pack("existing-pack", "2.0.0", &[]);
|
|
|
|
let (mut cmd, _config_dir) = isolated_cmd();
|
|
cmd.arg("pack")
|
|
.arg("index-update")
|
|
.arg("--index")
|
|
.arg(index_path.to_str().unwrap())
|
|
.arg(pack_dir.path().to_str().unwrap())
|
|
.arg("--git-url")
|
|
.arg("https://github.com/test/existing-pack.git")
|
|
.arg("--git-ref")
|
|
.arg("v2.0.0")
|
|
.arg("--update");
|
|
|
|
cmd.assert()
|
|
.success()
|
|
.stdout(predicate::str::contains("existing-pack"))
|
|
.stdout(predicate::str::contains("2.0.0"));
|
|
|
|
// Verify version was updated
|
|
let updated_index = fs::read_to_string(&index_path).unwrap();
|
|
let json: Value = serde_json::from_str(&updated_index).unwrap();
|
|
let packs = json["packs"].as_array().unwrap();
|
|
assert_eq!(packs.len(), 1);
|
|
assert_eq!(packs[0]["version"], "2.0.0");
|
|
}
|
|
|
|
#[test]
|
|
fn test_pack_index_update_invalid_index_file() {
|
|
let temp_dir = TempDir::new().unwrap();
|
|
let bad_index = temp_dir.path().join("bad-index.json");
|
|
fs::write(&bad_index, "not valid json {").unwrap();
|
|
|
|
let pack_dir = create_test_pack("test-pack", "1.0.0", &[]);
|
|
|
|
let (mut cmd, _config_dir) = isolated_cmd();
|
|
cmd.arg("pack")
|
|
.arg("index-update")
|
|
.arg("--index")
|
|
.arg(bad_index.to_str().unwrap())
|
|
.arg(pack_dir.path().to_str().unwrap());
|
|
|
|
cmd.assert().failure();
|
|
}
|
|
|
|
#[test]
|
|
fn test_pack_index_merge_combines_indexes() {
|
|
let index1 = create_test_index(&[("pack-a", "1.0.0"), ("pack-b", "1.0.0")]);
|
|
let index2 = create_test_index(&[("pack-c", "1.0.0"), ("pack-d", "1.0.0")]);
|
|
|
|
let output_dir = TempDir::new().unwrap();
|
|
let output_path = output_dir.path().join("merged.json");
|
|
|
|
let (mut cmd, _config_dir) = isolated_cmd();
|
|
cmd.arg("--output")
|
|
.arg("table")
|
|
.arg("pack")
|
|
.arg("index-merge")
|
|
.arg("--file")
|
|
.arg(output_path.to_str().unwrap())
|
|
.arg(index1.path().join("index.json").to_str().unwrap())
|
|
.arg(index2.path().join("index.json").to_str().unwrap());
|
|
|
|
cmd.assert()
|
|
.success()
|
|
.stdout(predicate::str::contains("Merged"))
|
|
.stdout(predicate::str::contains("2"));
|
|
|
|
// Verify merged file
|
|
let merged_content = fs::read_to_string(&output_path).unwrap();
|
|
let json: Value = serde_json::from_str(&merged_content).unwrap();
|
|
assert_eq!(json["packs"].as_array().unwrap().len(), 4);
|
|
}
|
|
|
|
#[test]
|
|
fn test_pack_index_merge_deduplicates() {
|
|
let index1 = create_test_index(&[("pack-a", "1.0.0"), ("pack-b", "1.0.0")]);
|
|
let index2 = create_test_index(&[("pack-a", "2.0.0"), ("pack-c", "1.0.0")]);
|
|
|
|
let output_dir = TempDir::new().unwrap();
|
|
let output_path = output_dir.path().join("merged.json");
|
|
|
|
let (mut cmd, _config_dir) = isolated_cmd();
|
|
cmd.arg("--output")
|
|
.arg("table")
|
|
.arg("pack")
|
|
.arg("index-merge")
|
|
.arg("--file")
|
|
.arg(output_path.to_str().unwrap())
|
|
.arg(index1.path().join("index.json").to_str().unwrap())
|
|
.arg(index2.path().join("index.json").to_str().unwrap());
|
|
|
|
cmd.assert()
|
|
.success()
|
|
.stdout(predicate::str::contains("Duplicates resolved"));
|
|
|
|
// Verify deduplication (should have 3 unique packs: pack-a, pack-b, pack-c)
|
|
let merged_content = fs::read_to_string(&output_path).unwrap();
|
|
let json: Value = serde_json::from_str(&merged_content).unwrap();
|
|
let packs = json["packs"].as_array().unwrap();
|
|
assert_eq!(packs.len(), 3);
|
|
|
|
// Verify pack-a has the newer version
|
|
let pack_a = packs.iter().find(|p| p["ref"] == "pack-a").unwrap();
|
|
assert_eq!(pack_a["version"], "2.0.0");
|
|
}
|
|
|
|
#[test]
|
|
fn test_pack_index_merge_output_exists_without_force() {
|
|
let index1 = create_test_index(&[("pack-a", "1.0.0")]);
|
|
|
|
let output_dir = TempDir::new().unwrap();
|
|
let output_path = output_dir.path().join("merged.json");
|
|
fs::write(&output_path, "existing content").unwrap();
|
|
|
|
let (mut cmd, _config_dir) = isolated_cmd();
|
|
cmd.arg("pack")
|
|
.arg("index-merge")
|
|
.arg("--file")
|
|
.arg(output_path.to_str().unwrap())
|
|
.arg(index1.path().join("index.json").to_str().unwrap());
|
|
|
|
cmd.assert()
|
|
.failure()
|
|
.stderr(predicate::str::contains("already exists").or(predicate::str::contains("force")));
|
|
}
|
|
|
|
#[test]
|
|
fn test_pack_index_merge_with_force_flag() {
|
|
let index1 = create_test_index(&[("pack-a", "1.0.0")]);
|
|
|
|
let output_dir = TempDir::new().unwrap();
|
|
let output_path = output_dir.path().join("merged.json");
|
|
fs::write(&output_path, "existing content").unwrap();
|
|
|
|
let (mut cmd, _config_dir) = isolated_cmd();
|
|
cmd.arg("pack")
|
|
.arg("index-merge")
|
|
.arg("--file")
|
|
.arg(output_path.to_str().unwrap())
|
|
.arg(index1.path().join("index.json").to_str().unwrap())
|
|
.arg("--force");
|
|
|
|
cmd.assert().success();
|
|
|
|
// Verify file was overwritten
|
|
let merged_content = fs::read_to_string(&output_path).unwrap();
|
|
assert_ne!(merged_content, "existing content");
|
|
}
|
|
|
|
#[test]
|
|
fn test_pack_index_merge_empty_input_list() {
|
|
let output_dir = TempDir::new().unwrap();
|
|
let output_path = output_dir.path().join("merged.json");
|
|
|
|
let (mut cmd, _config_dir) = isolated_cmd();
|
|
cmd.arg("pack")
|
|
.arg("index-merge")
|
|
.arg("--file")
|
|
.arg(output_path.to_str().unwrap());
|
|
|
|
// Should fail due to missing required inputs
|
|
cmd.assert().failure();
|
|
}
|
|
|
|
#[test]
|
|
fn test_pack_index_merge_missing_input_file() {
|
|
let index1 = create_test_index(&[("pack-a", "1.0.0")]);
|
|
let output_dir = TempDir::new().unwrap();
|
|
let output_path = output_dir.path().join("merged.json");
|
|
|
|
let (mut cmd, _config_dir) = isolated_cmd();
|
|
cmd.arg("--output")
|
|
.arg("table")
|
|
.arg("pack")
|
|
.arg("index-merge")
|
|
.arg("--file")
|
|
.arg(output_path.to_str().unwrap())
|
|
.arg(index1.path().join("index.json").to_str().unwrap())
|
|
.arg("/nonexistent/index.json");
|
|
|
|
// Should succeed but skip missing file (with warning in stderr)
|
|
cmd.assert()
|
|
.success()
|
|
.stderr(predicate::str::contains("Skipping").or(predicate::str::contains("missing")));
|
|
}
|
|
|
|
#[test]
|
|
fn test_pack_commands_help() {
|
|
let commands = vec![
|
|
vec!["pack", "checksum", "--help"],
|
|
vec!["pack", "index-entry", "--help"],
|
|
vec!["pack", "index-update", "--help"],
|
|
vec!["pack", "index-merge", "--help"],
|
|
];
|
|
|
|
for args in commands {
|
|
let (mut cmd, _config_dir) = isolated_cmd();
|
|
for arg in &args {
|
|
cmd.arg(arg);
|
|
}
|
|
cmd.assert()
|
|
.success()
|
|
.stdout(predicate::str::contains("Usage:"));
|
|
}
|
|
}
|