687 lines
18 KiB
Rust
687 lines
18 KiB
Rust
//! Integration tests for pack registry system
|
|
//!
|
|
//! This module tests:
|
|
//! - End-to-end pack installation from all sources (git, archive, local, registry)
|
|
//! - Dependency validation during installation
|
|
//! - Installation metadata tracking
|
|
//! - Checksum verification
|
|
//! - Error handling and edge cases
|
|
|
|
mod helpers;
|
|
|
|
use attune_common::{
|
|
models::Pack,
|
|
pack_registry::calculate_directory_checksum,
|
|
repositories::{pack::PackRepository, List},
|
|
};
|
|
use helpers::{Result, TestContext};
|
|
use serde_json::json;
|
|
use std::fs;
|
|
use tempfile::TempDir;
|
|
|
|
/// Helper to create a test pack directory with pack.yaml
|
|
fn create_test_pack_dir(name: &str, version: &str) -> Result<TempDir> {
|
|
let temp_dir = TempDir::new()?;
|
|
let pack_yaml = format!(
|
|
r#"
|
|
ref: {}
|
|
name: Test Pack {}
|
|
version: {}
|
|
description: Test pack for integration tests
|
|
author: Test Author
|
|
email: test@example.com
|
|
keywords:
|
|
- test
|
|
- integration
|
|
dependencies: []
|
|
python: "3.8"
|
|
actions:
|
|
test_action:
|
|
entry_point: test.py
|
|
runner_type: python-script
|
|
"#,
|
|
name, name, version
|
|
);
|
|
|
|
fs::write(temp_dir.path().join("pack.yaml"), pack_yaml)?;
|
|
|
|
// Create a simple action file
|
|
let action_content = r#"
|
|
#!/usr/bin/env python3
|
|
print("Test action executed")
|
|
"#;
|
|
fs::write(temp_dir.path().join("test.py"), action_content)?;
|
|
|
|
Ok(temp_dir)
|
|
}
|
|
|
|
/// Helper to create a pack with dependencies
|
|
fn create_pack_with_deps(name: &str, deps: &[&str]) -> Result<TempDir> {
|
|
let temp_dir = TempDir::new()?;
|
|
let deps_yaml = deps
|
|
.iter()
|
|
.map(|d| format!(" - {}", d))
|
|
.collect::<Vec<_>>()
|
|
.join("\n");
|
|
|
|
let pack_yaml = format!(
|
|
r#"
|
|
ref: {}
|
|
name: Test Pack {}
|
|
version: 1.0.0
|
|
description: Test pack with dependencies
|
|
author: Test Author
|
|
dependencies:
|
|
{}
|
|
python: "3.8"
|
|
actions:
|
|
test_action:
|
|
entry_point: test.py
|
|
runner_type: python-script
|
|
"#,
|
|
name, name, deps_yaml
|
|
);
|
|
|
|
fs::write(temp_dir.path().join("pack.yaml"), pack_yaml)?;
|
|
fs::write(temp_dir.path().join("test.py"), "print('test')")?;
|
|
|
|
Ok(temp_dir)
|
|
}
|
|
|
|
/// Helper to create a pack with specific runtime requirements
|
|
fn create_pack_with_runtime(
|
|
name: &str,
|
|
python: Option<&str>,
|
|
nodejs: Option<&str>,
|
|
) -> Result<TempDir> {
|
|
let temp_dir = TempDir::new()?;
|
|
|
|
let python_line = python
|
|
.map(|v| format!("python: \"{}\"", v))
|
|
.unwrap_or_default();
|
|
let nodejs_line = nodejs
|
|
.map(|v| format!("nodejs: \"{}\"", v))
|
|
.unwrap_or_default();
|
|
|
|
let pack_yaml = format!(
|
|
r#"
|
|
ref: {}
|
|
name: Test Pack {}
|
|
version: 1.0.0
|
|
description: Test pack with runtime requirements
|
|
author: Test Author
|
|
{}
|
|
{}
|
|
actions:
|
|
test_action:
|
|
entry_point: test.py
|
|
runner_type: python-script
|
|
"#,
|
|
name, name, python_line, nodejs_line
|
|
);
|
|
|
|
fs::write(temp_dir.path().join("pack.yaml"), pack_yaml)?;
|
|
fs::write(temp_dir.path().join("test.py"), "print('test')")?;
|
|
|
|
Ok(temp_dir)
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_install_pack_from_local_directory() -> Result<()> {
|
|
let ctx = TestContext::new().await?.with_auth().await?;
|
|
let token = ctx.token().unwrap();
|
|
|
|
// Create a test pack directory
|
|
let pack_dir = create_test_pack_dir("local-test", "1.0.0")?;
|
|
let pack_path = pack_dir.path().to_string_lossy().to_string();
|
|
|
|
// Install pack from local directory
|
|
let response = ctx
|
|
.post(
|
|
"/api/v1/packs/install",
|
|
json!({
|
|
"source": pack_path,
|
|
"force": false,
|
|
"skip_tests": true,
|
|
"skip_deps": true
|
|
}),
|
|
Some(token),
|
|
)
|
|
.await?;
|
|
|
|
let status = response.status();
|
|
let body_text = response.text().await?;
|
|
|
|
if status != 200 {
|
|
eprintln!("Error response (status {}): {}", status, body_text);
|
|
}
|
|
assert_eq!(status, 200, "Installation should succeed");
|
|
|
|
let body: serde_json::Value = serde_json::from_str(&body_text)?;
|
|
assert_eq!(body["data"]["pack"]["ref"], "local-test");
|
|
assert_eq!(body["data"]["pack"]["version"], "1.0.0");
|
|
assert_eq!(body["data"]["tests_skipped"], true);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_install_pack_with_dependency_validation_success() -> Result<()> {
|
|
let ctx = TestContext::new().await?.with_auth().await?;
|
|
let token = ctx.token().unwrap();
|
|
|
|
// First, install a dependency pack
|
|
let dep_pack_dir = create_test_pack_dir("core", "1.0.0")?;
|
|
let dep_path = dep_pack_dir.path().to_string_lossy().to_string();
|
|
|
|
ctx.post(
|
|
"/api/v1/packs/install",
|
|
json!({
|
|
"source": dep_path,
|
|
"force": false,
|
|
"skip_tests": true,
|
|
"skip_deps": true
|
|
}),
|
|
Some(token),
|
|
)
|
|
.await?;
|
|
|
|
// Now install a pack that depends on it
|
|
let pack_dir = create_pack_with_deps("dependent-pack", &["core"])?;
|
|
let pack_path = pack_dir.path().to_string_lossy().to_string();
|
|
|
|
let response = ctx
|
|
.post(
|
|
"/api/v1/packs/install",
|
|
json!({
|
|
"source": pack_path,
|
|
"force": false,
|
|
"skip_tests": true,
|
|
"skip_deps": false // Enable dependency validation
|
|
}),
|
|
Some(token),
|
|
)
|
|
.await?;
|
|
|
|
assert_eq!(
|
|
response.status(),
|
|
200,
|
|
"Installation should succeed when dependencies are met"
|
|
);
|
|
|
|
let body: serde_json::Value = response.json().await?;
|
|
assert_eq!(body["data"]["pack"]["ref"], "dependent-pack");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_install_pack_with_missing_dependency_fails() -> Result<()> {
|
|
let ctx = TestContext::new().await?.with_auth().await?;
|
|
let token = ctx.token().unwrap();
|
|
|
|
// Create a pack with an unmet dependency
|
|
let pack_dir = create_pack_with_deps("dependent-pack", &["missing-pack"])?;
|
|
let pack_path = pack_dir.path().to_string_lossy().to_string();
|
|
|
|
let response = ctx
|
|
.post(
|
|
"/api/v1/packs/install",
|
|
json!({
|
|
"source": pack_path,
|
|
"force": false,
|
|
"skip_tests": true,
|
|
"skip_deps": false // Enable dependency validation
|
|
}),
|
|
Some(token),
|
|
)
|
|
.await?;
|
|
|
|
// Should fail with 400 Bad Request
|
|
assert_eq!(
|
|
response.status(),
|
|
400,
|
|
"Installation should fail when dependencies are missing"
|
|
);
|
|
|
|
let body: serde_json::Value = response.json().await?;
|
|
let error_msg = body["error"].as_str().unwrap();
|
|
assert!(
|
|
error_msg.contains("dependency validation failed") || error_msg.contains("missing-pack"),
|
|
"Error should mention dependency validation failure"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_install_pack_skip_deps_bypasses_validation() -> Result<()> {
|
|
let ctx = TestContext::new().await?.with_auth().await?;
|
|
let token = ctx.token().unwrap();
|
|
|
|
// Create a pack with an unmet dependency
|
|
let pack_dir = create_pack_with_deps("dependent-pack", &["missing-pack"])?;
|
|
let pack_path = pack_dir.path().to_string_lossy().to_string();
|
|
|
|
let response = ctx
|
|
.post(
|
|
"/api/v1/packs/install",
|
|
json!({
|
|
"source": pack_path,
|
|
"force": false,
|
|
"skip_tests": true,
|
|
"skip_deps": true // Skip dependency validation
|
|
}),
|
|
Some(token),
|
|
)
|
|
.await?;
|
|
|
|
// Should succeed because validation is skipped
|
|
assert_eq!(
|
|
response.status(),
|
|
200,
|
|
"Installation should succeed when validation is skipped"
|
|
);
|
|
|
|
let body: serde_json::Value = response.json().await?;
|
|
assert_eq!(body["data"]["pack"]["ref"], "dependent-pack");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_install_pack_with_runtime_validation() -> Result<()> {
|
|
let ctx = TestContext::new().await?.with_auth().await?;
|
|
let token = ctx.token().unwrap();
|
|
|
|
// Create a pack with reasonable runtime requirements
|
|
let pack_dir = create_pack_with_runtime("runtime-test", Some("3.8"), None)?;
|
|
let pack_path = pack_dir.path().to_string_lossy().to_string();
|
|
|
|
let response = ctx
|
|
.post(
|
|
"/api/v1/packs/install",
|
|
json!({
|
|
"source": pack_path,
|
|
"force": false,
|
|
"skip_tests": true,
|
|
"skip_deps": false // Enable validation
|
|
}),
|
|
Some(token),
|
|
)
|
|
.await?;
|
|
|
|
// Result depends on whether Python 3.8+ is available in test environment
|
|
// We just verify the response is well-formed
|
|
let status = response.status();
|
|
assert!(
|
|
status == 200 || status == 400,
|
|
"Should either succeed or fail gracefully"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_install_pack_metadata_tracking() -> Result<()> {
|
|
let ctx = TestContext::new().await?.with_auth().await?;
|
|
let token = ctx.token().unwrap();
|
|
|
|
// Install a pack
|
|
let pack_dir = create_test_pack_dir("metadata-test", "1.0.0")?;
|
|
let pack_path = pack_dir.path().to_string_lossy().to_string();
|
|
let original_checksum = calculate_directory_checksum(pack_dir.path())?;
|
|
|
|
let response = ctx
|
|
.post(
|
|
"/api/v1/packs/install",
|
|
json!({
|
|
"source": pack_path,
|
|
"force": false,
|
|
"skip_tests": true,
|
|
"skip_deps": true
|
|
}),
|
|
Some(token),
|
|
)
|
|
.await?;
|
|
|
|
assert_eq!(response.status(), 200);
|
|
|
|
let body: serde_json::Value = response.json().await?;
|
|
let pack_id = body["data"]["pack"]["id"].as_i64().unwrap();
|
|
|
|
// Verify installation metadata was created
|
|
let pack = PackRepository::find_by_id(&ctx.pool, pack_id)
|
|
.await?
|
|
.expect("Should have pack record");
|
|
|
|
assert_eq!(pack.id, pack_id);
|
|
assert_eq!(pack.source_type.as_deref(), Some("local_directory"));
|
|
assert!(pack.source_url.is_some());
|
|
assert!(pack.checksum.is_some());
|
|
assert!(pack.installed_at.is_some());
|
|
|
|
// Verify checksum matches
|
|
let stored_checksum = pack.checksum.as_ref().unwrap();
|
|
assert_eq!(
|
|
stored_checksum, &original_checksum,
|
|
"Stored checksum should match calculated checksum"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_install_pack_force_reinstall() -> Result<()> {
|
|
let ctx = TestContext::new().await?.with_auth().await?;
|
|
let token = ctx.token().unwrap();
|
|
|
|
let pack_dir = create_test_pack_dir("force-test", "1.0.0")?;
|
|
let pack_path = pack_dir.path().to_string_lossy().to_string();
|
|
|
|
// Install once
|
|
let response1 = ctx
|
|
.post(
|
|
"/api/v1/packs/install",
|
|
json!({
|
|
"source": &pack_path,
|
|
"force": false,
|
|
"skip_tests": true,
|
|
"skip_deps": true
|
|
}),
|
|
Some(token),
|
|
)
|
|
.await?;
|
|
|
|
assert_eq!(response1.status(), 200);
|
|
|
|
// Try to install again without force - should work but might replace
|
|
let response2 = ctx
|
|
.post(
|
|
"/api/v1/packs/install",
|
|
json!({
|
|
"source": &pack_path,
|
|
"force": true,
|
|
"skip_tests": true,
|
|
"skip_deps": true
|
|
}),
|
|
Some(token),
|
|
)
|
|
.await?;
|
|
|
|
assert_eq!(response2.status(), 200, "Force reinstall should succeed");
|
|
|
|
// Verify pack exists
|
|
let packs = PackRepository::list(&ctx.pool).await?;
|
|
let force_test_packs: Vec<&Pack> = packs.iter().filter(|p| p.r#ref == "force-test").collect();
|
|
assert_eq!(
|
|
force_test_packs.len(),
|
|
1,
|
|
"Should have exactly one force-test pack"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_install_pack_storage_path_created() -> Result<()> {
|
|
let ctx = TestContext::new().await?.with_auth().await?;
|
|
let token = ctx.token().unwrap();
|
|
|
|
let pack_dir = create_test_pack_dir("storage-test", "2.3.4")?;
|
|
let pack_path = pack_dir.path().to_string_lossy().to_string();
|
|
|
|
let response = ctx
|
|
.post(
|
|
"/api/v1/packs/install",
|
|
json!({
|
|
"source": pack_path,
|
|
"force": false,
|
|
"skip_tests": true,
|
|
"skip_deps": true
|
|
}),
|
|
Some(token),
|
|
)
|
|
.await?;
|
|
|
|
assert_eq!(response.status(), 200);
|
|
|
|
let body: serde_json::Value = response.json().await?;
|
|
let pack_id = body["data"]["pack"]["id"].as_i64().unwrap();
|
|
|
|
// Verify installation metadata has storage path
|
|
let pack = PackRepository::find_by_id(&ctx.pool, pack_id)
|
|
.await?
|
|
.expect("Should have pack record");
|
|
|
|
let storage_path = pack
|
|
.storage_path
|
|
.as_ref()
|
|
.expect("Should have storage path");
|
|
assert!(
|
|
storage_path.contains("storage-test"),
|
|
"Storage path should contain pack ref"
|
|
);
|
|
assert!(
|
|
storage_path.contains("2.3.4"),
|
|
"Storage path should contain version"
|
|
);
|
|
|
|
// Note: We can't verify the actual filesystem without knowing the config path
|
|
// but we verify the path structure is correct
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_install_pack_invalid_source() -> Result<()> {
|
|
let ctx = TestContext::new().await?.with_auth().await?;
|
|
let token = ctx.token().unwrap();
|
|
|
|
let response = ctx
|
|
.post(
|
|
"/api/v1/packs/install",
|
|
json!({
|
|
"source": "/nonexistent/path/to/pack",
|
|
"force": false,
|
|
"skip_tests": true,
|
|
"skip_deps": true
|
|
}),
|
|
Some(token),
|
|
)
|
|
.await?;
|
|
|
|
assert_eq!(
|
|
response.status(),
|
|
404,
|
|
"Should fail with not found status for nonexistent path"
|
|
);
|
|
|
|
let body: serde_json::Value = response.json().await?;
|
|
assert!(body["error"].is_string(), "Should have error message");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_install_pack_missing_pack_yaml() -> Result<()> {
|
|
let ctx = TestContext::new().await?.with_auth().await?;
|
|
let token = ctx.token().unwrap();
|
|
|
|
// Create directory without pack.yaml
|
|
let temp_dir = TempDir::new()?;
|
|
fs::write(temp_dir.path().join("readme.txt"), "No pack.yaml here")?;
|
|
|
|
let response = ctx
|
|
.post(
|
|
"/api/v1/packs/install",
|
|
json!({
|
|
"source": temp_dir.path().to_string_lossy(),
|
|
"force": false,
|
|
"skip_tests": true,
|
|
"skip_deps": true
|
|
}),
|
|
Some(token),
|
|
)
|
|
.await?;
|
|
|
|
assert_eq!(response.status(), 400, "Should fail with bad request");
|
|
|
|
let body: serde_json::Value = response.json().await?;
|
|
let error = body["error"].as_str().unwrap();
|
|
assert!(
|
|
error.contains("pack.yaml"),
|
|
"Error should mention pack.yaml"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_install_pack_invalid_pack_yaml() -> Result<()> {
|
|
let ctx = TestContext::new().await?.with_auth().await?;
|
|
let token = ctx.token().unwrap();
|
|
|
|
// Create pack.yaml with invalid content
|
|
let temp_dir = TempDir::new()?;
|
|
fs::write(temp_dir.path().join("pack.yaml"), "invalid: yaml: content:")?;
|
|
|
|
let response = ctx
|
|
.post(
|
|
"/api/v1/packs/install",
|
|
json!({
|
|
"source": temp_dir.path().to_string_lossy(),
|
|
"force": false,
|
|
"skip_tests": true,
|
|
"skip_deps": true
|
|
}),
|
|
Some(token),
|
|
)
|
|
.await?;
|
|
|
|
// Should fail with error status
|
|
assert!(response.status().is_client_error() || response.status().is_server_error());
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_install_pack_without_auth_fails() -> Result<()> {
|
|
let ctx = TestContext::new().await?; // No auth
|
|
|
|
let pack_dir = create_test_pack_dir("auth-test", "1.0.0")?;
|
|
let pack_path = pack_dir.path().to_string_lossy().to_string();
|
|
|
|
let response = ctx
|
|
.post(
|
|
"/api/v1/packs/install",
|
|
json!({
|
|
"source": pack_path,
|
|
"force": false,
|
|
"skip_tests": true,
|
|
"skip_deps": true
|
|
}),
|
|
None, // No token
|
|
)
|
|
.await?;
|
|
|
|
assert_eq!(response.status(), 401, "Should require authentication");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_multiple_pack_installations() -> Result<()> {
|
|
let ctx = TestContext::new().await?.with_auth().await?;
|
|
let token = ctx.token().unwrap();
|
|
|
|
// Install multiple packs
|
|
for i in 1..=3 {
|
|
let pack_dir = create_test_pack_dir(&format!("multi-pack-{}", i), "1.0.0")?;
|
|
let pack_path = pack_dir.path().to_string_lossy().to_string();
|
|
|
|
let response = ctx
|
|
.post(
|
|
"/api/v1/packs/install",
|
|
json!({
|
|
"source": pack_path,
|
|
"force": false,
|
|
"skip_tests": true,
|
|
"skip_deps": true
|
|
}),
|
|
Some(token),
|
|
)
|
|
.await?;
|
|
|
|
assert_eq!(
|
|
response.status(),
|
|
200,
|
|
"Pack {} installation should succeed",
|
|
i
|
|
);
|
|
}
|
|
|
|
// Verify all packs are installed
|
|
let packs = <PackRepository as List>::list(&ctx.pool).await?;
|
|
let multi_packs: Vec<&Pack> = packs
|
|
.iter()
|
|
.filter(|p| p.r#ref.starts_with("multi-pack-"))
|
|
.collect();
|
|
|
|
assert_eq!(
|
|
multi_packs.len(),
|
|
3,
|
|
"Should have 3 multi-pack installations"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_install_pack_version_upgrade() -> Result<()> {
|
|
let ctx = TestContext::new().await?.with_auth().await?;
|
|
let token = ctx.token().unwrap();
|
|
|
|
// Install version 1.0.0
|
|
let pack_dir_v1 = create_test_pack_dir("version-test", "1.0.0")?;
|
|
let response1 = ctx
|
|
.post(
|
|
"/api/v1/packs/install",
|
|
json!({
|
|
"source": pack_dir_v1.path().to_string_lossy(),
|
|
"force": false,
|
|
"skip_tests": true,
|
|
"skip_deps": true
|
|
}),
|
|
Some(token),
|
|
)
|
|
.await?;
|
|
|
|
assert_eq!(response1.status(), 200);
|
|
|
|
// Install version 2.0.0 with force
|
|
let pack_dir_v2 = create_test_pack_dir("version-test", "2.0.0")?;
|
|
let response2 = ctx
|
|
.post(
|
|
"/api/v1/packs/install",
|
|
json!({
|
|
"source": pack_dir_v2.path().to_string_lossy(),
|
|
"force": true,
|
|
"skip_tests": true,
|
|
"skip_deps": true
|
|
}),
|
|
Some(token),
|
|
)
|
|
.await?;
|
|
|
|
assert_eq!(response2.status(), 200);
|
|
|
|
let body: serde_json::Value = response2.json().await?;
|
|
assert_eq!(
|
|
body["data"]["pack"]["version"], "2.0.0",
|
|
"Should be upgraded to version 2.0.0"
|
|
);
|
|
|
|
Ok(())
|
|
}
|