Compare commits
	
		
			97 Commits
		
	
	
		
			663367ea57
			...
			main-rfs-c
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 5014c2f4a5 | ||
|  | ba6f53a28a | ||
|  | b81a0aa61c | ||
|  | b02101bd42 | ||
| 717cd7b16f | |||
|  | e125bb6511 | ||
|  | 8012a66250 | ||
|  | 6dead402a2 | ||
|  | c94467c205 | ||
|  | b737cd6337 | ||
|  | 455f84528b | ||
|  | 3e3d0a1d45 | ||
|  | 511729c477 | ||
|  | 74217364fa | ||
|  | d22fd686b7 | ||
|  | c4cdb8126c | ||
|  | a35edc2030 | ||
|  | a7a7353aa1 | ||
|  | 4a8d3bfd24 | ||
|  | 3e617c2489 | ||
|  | 4d51518f31 | ||
|  | e031b03e04 | ||
| ba9103685f | |||
| dee38eb6c2 | |||
| 49c879359b | |||
| c0df07d6df | |||
| 6a1e70c484 | |||
| e7e8e7daf8 | |||
| 8a8ead17cb | |||
| 0e7dba9466 | |||
| f0d7636cda | |||
| 3a6bde02d5 | |||
| 3a7b323f9a | |||
| 66d5c8588a | |||
| 29a06d2bb4 | |||
|  | bb39f3e3f2 | ||
|  | 5194f5245d | ||
| a9255de679 | |||
| 7d05567ad2 | |||
|  | c0e11c6510 | ||
|  | fedf957079 | ||
|  | 65e404e517 | ||
| 944f22be23 | |||
| 887e66bb17 | |||
|  | e5a4a1b634 | ||
|  | 7f55cf4fba | ||
|  | c26e0e5ad8 | ||
|  | 365814b424 | ||
|  | cc4e087f1d | ||
|  | 229fef217f | ||
|  | dd84ce3f48 | ||
|  | 7b8b8c662e | ||
|  | d29a8fbb67 | ||
|  | 771df07c25 | ||
|  | 9a23c4cc09 | ||
|  | 2014c63b78 | ||
|  | 2adda10664 | ||
|  | 7b1908b676 | ||
|  | e9b867a36e | ||
|  | 78c0fd7871 | ||
|  | e44ee83e74 | ||
| 0c425470a5 | |||
|  | 3e64a53a83 | ||
|  | 3225b3f029 | ||
|  | 3417e2c1ff | ||
|  | 7add64562e | ||
|  | 809599d60c | ||
|  | 25f2ae6fa9 | ||
|  | dfe6c91273 | ||
| a4438d63e0 | |||
| 393c4270d4 | |||
| 495fe92321 | |||
| 577d80b282 | |||
| 3f8aecb786 | |||
|  | c7a5699798 | ||
|  | 3a0900fc15 | ||
|  | 916eabfa42 | ||
| a8ed0900fd | |||
| e47e163285 | |||
| 8aa2b2da26 | |||
| 992481ce1b | |||
| 516d0177e7 | |||
|  | 8285fdb7b9 | ||
|  | 1ebd591f19 | ||
| 7298645368 | |||
| f669bdb84f | |||
| 654f91b849 | |||
| 619ce57776 | |||
| 2695b5f5f7 | |||
| 7828f82f58 | |||
| 7a346a1dd1 | |||
| 07390c3cae | |||
|  | 138dce66fa | ||
|  | 49e85ff8e6 | ||
|  | f386890a8a | ||
|  | 61bd58498a | ||
| 98ab2e1536 | 
							
								
								
									
										41
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										41
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -22,4 +22,43 @@ Cargo.lock | ||||
| /rhai_test_template | ||||
| /rhai_test_download | ||||
| /rhai_test_fs | ||||
| run_rhai_tests.log | ||||
| run_rhai_tests.log | ||||
| new_location | ||||
| log.txt | ||||
| file.txt | ||||
| fix_doc* | ||||
|  | ||||
| # Dependencies | ||||
| /node_modules | ||||
|  | ||||
| # Production | ||||
| /build | ||||
|  | ||||
| # Generated files | ||||
| .docusaurus | ||||
| .cache-loader | ||||
|  | ||||
| # Misc | ||||
| .DS_Store | ||||
| .env.local | ||||
| .env.development.local | ||||
| .env.test.local | ||||
| .env.production.local | ||||
|  | ||||
| npm-debug.log* | ||||
| yarn-debug.log* | ||||
| yarn-error.log* | ||||
| bun.lockb | ||||
| bun.lock | ||||
|  | ||||
| yarn.lock | ||||
|  | ||||
| build.sh | ||||
| build_dev.sh | ||||
| develop.sh | ||||
|  | ||||
| docusaurus.config.ts | ||||
|  | ||||
| sidebars.ts | ||||
|  | ||||
| tsconfig.json | ||||
|   | ||||
| @@ -1,16 +0,0 @@ | ||||
| { | ||||
|   "mcpServers": { | ||||
|     "gitea": { | ||||
|       "command": "/Users/despiegk/hero/bin/mcpgitea", | ||||
|       "args": [ | ||||
|         "-t", "stdio", | ||||
|         "--host", "https://gitea.com", | ||||
|         "--token", "5bd13c898368a2edbfcef43f898a34857b51b37a" | ||||
|       ], | ||||
|       "env": { | ||||
|         "GITEA_HOST": "https://git.ourworld.tf/", | ||||
|         "GITEA_ACCESS_TOKEN": "5bd13c898368a2edbfcef43f898a34857b51b37a" | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										100
									
								
								Cargo.toml
									
									
									
									
									
								
							
							
						
						
									
										100
									
								
								Cargo.toml
									
									
									
									
									
								
							| @@ -4,51 +4,85 @@ version = "0.1.0" | ||||
| edition = "2021" | ||||
| authors = ["PlanetFirst <info@incubaid.com>"] | ||||
| description = "System Abstraction Layer - A library for easy interaction with operating system features" | ||||
| repository = "https://git.ourworld.tf/herocode/sal" | ||||
| repository = "https://git.threefold.info/herocode/sal" | ||||
| license = "Apache-2.0" | ||||
| keywords = ["system", "os", "abstraction", "platform", "filesystem"] | ||||
| categories = ["os", "filesystem", "api-bindings"] | ||||
| readme = "README.md" | ||||
|  | ||||
| [dependencies] | ||||
| tera = "1.19.0" # Template engine for text rendering | ||||
| # Cross-platform functionality | ||||
| [workspace] | ||||
| members = [".", "vault", "git", "redisclient", "mycelium", "text", "os", "net", "zinit_client", "process", "virt", "postgresclient", "rhai", "herodo", "rfs-client"] | ||||
| resolver = "2" | ||||
|  | ||||
| [workspace.metadata] | ||||
| # Workspace-level metadata | ||||
| rust-version = "1.85.0" | ||||
|  | ||||
| [workspace.dependencies] | ||||
| # Core shared dependencies with consistent versions | ||||
| anyhow = "1.0.98" | ||||
| base64 = "0.22.1" | ||||
| bytes = "1.4.0" | ||||
| dirs = "6.0.0" | ||||
| env_logger = "0.11.8" | ||||
| futures = "0.3.30" | ||||
| glob = "0.3.1" | ||||
| lazy_static = "1.4.0" | ||||
| libc = "0.2" | ||||
| cfg-if = "1.0" | ||||
| thiserror = "1.0" # For error handling | ||||
| redis = "0.22.0" # Redis client | ||||
| postgres = "0.19.4" # PostgreSQL client | ||||
| tokio-postgres = "0.7.8" # Async PostgreSQL client | ||||
| postgres-types = "0.2.5" # PostgreSQL type conversions | ||||
| lazy_static = "1.4.0" # For lazy initialization of static variables | ||||
| regex = "1.8.1" # For regex pattern matching | ||||
| serde = { version = "1.0", features = [ | ||||
|     "derive", | ||||
| ] } # For serialization/deserialization | ||||
| serde_json = "1.0" # For JSON handling | ||||
| glob = "0.3.1" # For file pattern matching | ||||
| tempfile = "3.5" # For temporary file operations | ||||
| log = "0.4" # Logging facade | ||||
| rhai = { version = "1.12.0", features = ["sync"] } # Embedded scripting language | ||||
| rand = "0.8.5" # Random number generation | ||||
| clap = "2.33" # Command-line argument parsing | ||||
| r2d2 = "0.8.10" | ||||
| log = "0.4" | ||||
| once_cell = "1.18.0" | ||||
| rand = "0.8.5" | ||||
| regex = "1.8.1" | ||||
| reqwest = { version = "0.12.15", features = ["json"] } | ||||
| rhai = { version = "1.12.0", features = ["sync"] } | ||||
| serde = { version = "1.0", features = ["derive"] } | ||||
| serde_json = "1.0" | ||||
| tempfile = "3.5" | ||||
| thiserror = "2.0.12" | ||||
| tokio = { version = "1.45.0", features = ["full"] } | ||||
| url = "2.4" | ||||
| uuid = { version = "1.16.0", features = ["v4"] } | ||||
|  | ||||
| # Database dependencies | ||||
| postgres = "0.19.10" | ||||
| r2d2_postgres = "0.18.2" | ||||
| redis = "0.31.0" | ||||
| tokio-postgres = "0.7.13" | ||||
|  | ||||
| # Optional features for specific OS functionality | ||||
| [target.'cfg(unix)'.dependencies] | ||||
| nix = "0.26" # Unix-specific functionality | ||||
| # Crypto dependencies | ||||
| chacha20poly1305 = "0.10.1" | ||||
| k256 = { version = "0.13.4", features = ["ecdsa", "ecdh"] } | ||||
| sha2 = "0.10.7" | ||||
| hex = "0.4" | ||||
|  | ||||
| [target.'cfg(windows)'.dependencies] | ||||
| windows = { version = "0.48", features = [ | ||||
| # Ethereum dependencies | ||||
| ethers = { version = "2.0.7", features = ["legacy"] } | ||||
|  | ||||
| # Platform-specific dependencies | ||||
| nix = "0.30.1" | ||||
| windows = { version = "0.61.1", features = [ | ||||
|     "Win32_Foundation", | ||||
|     "Win32_System_Threading", | ||||
|     "Win32_Storage_FileSystem", | ||||
| ] } | ||||
|  | ||||
| [dev-dependencies] | ||||
| tempfile = "3.5" # For tests that need temporary files/directories | ||||
| # Specialized dependencies | ||||
| zinit-client = "0.3.0" | ||||
| urlencoding = "2.1.3" | ||||
| tokio-test = "0.4.4" | ||||
|  | ||||
| [[bin]] | ||||
| name = "herodo" | ||||
| path = "src/bin/herodo.rs" | ||||
| [dependencies] | ||||
| thiserror = "2.0.12" # For error handling in the main Error enum | ||||
| sal-git = { path = "git" } | ||||
| sal-redisclient = { path = "redisclient" } | ||||
| sal-mycelium = { path = "mycelium" } | ||||
| sal-text = { path = "text" } | ||||
| sal-os = { path = "os" } | ||||
| sal-net = { path = "net" } | ||||
| sal-zinit-client = { path = "zinit_client" } | ||||
| sal-process = { path = "process" } | ||||
| sal-virt = { path = "virt" } | ||||
| sal-postgresclient = { path = "postgresclient" } | ||||
| sal-vault = { path = "vault" } | ||||
| sal-rhai = { path = "rhai" } | ||||
| sal-rfs-client = { path = "rfs-client" } | ||||
|   | ||||
							
								
								
									
										241
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										241
									
								
								README.md
									
									
									
									
									
								
							| @@ -1,73 +1,228 @@ | ||||
| # SAL (System Abstraction Layer) | ||||
|  | ||||
| A Rust library that provides a unified interface for interacting with operating system features across different platforms. It abstracts away platform-specific details, allowing developers to write cross-platform code with ease. | ||||
| **Version: 0.1.0** | ||||
|  | ||||
| ## Features | ||||
| SAL is a comprehensive Rust library designed to provide a unified and simplified interface for a wide array of system-level operations and interactions. It abstracts platform-specific details, enabling developers to write robust, cross-platform code with greater ease. SAL also includes `herodo`, a powerful command-line tool for executing Rhai scripts that leverage SAL's capabilities for automation and system management tasks. | ||||
|  | ||||
| - **File System Operations**: Simplified file and directory management | ||||
| - **Process Management**: Create, monitor, and control processes | ||||
| - **System Information**: Access system details and metrics | ||||
| - **Git Integration**: Interface with Git repositories | ||||
| - **Redis Client**: Robust Redis connection management and command execution | ||||
| - **Text Processing**: Utilities for text manipulation and formatting | ||||
| ## 🏗️ **Cargo Workspace Structure** | ||||
|  | ||||
| ## Modules | ||||
| SAL is organized as a **Cargo workspace** with 16 specialized crates: | ||||
|  | ||||
| ### Redis Client | ||||
| - **Root Package**: `sal` - Umbrella crate that re-exports all modules | ||||
| - **13 Library Crates**: Specialized SAL modules (git, text, os, net, etc.) | ||||
| - **1 Binary Crate**: `herodo` - Rhai script execution engine | ||||
| - **1 Integration Crate**: `rhai` - Rhai scripting integration layer | ||||
|  | ||||
| The Redis client module provides a robust wrapper around the Redis client library for Rust, offering: | ||||
| This workspace structure provides excellent build performance, dependency management, and maintainability. | ||||
|  | ||||
| - Automatic connection management and reconnection | ||||
| - Support for both Unix socket and TCP connections | ||||
| - Database selection via environment variables | ||||
| - Thread-safe global client instance | ||||
| - Simple command execution interface | ||||
| ### **🚀 Workspace Benefits** | ||||
| - **Unified Dependency Management**: Shared dependencies across all crates with consistent versions | ||||
| - **Optimized Build Performance**: Parallel compilation and shared build artifacts | ||||
| - **Simplified Testing**: Run tests across all modules with a single command | ||||
| - **Modular Architecture**: Each module is independently maintainable while sharing common infrastructure | ||||
| - **Production Ready**: 100% test coverage with comprehensive Rhai integration tests | ||||
|  | ||||
| [View Redis Client Documentation](src/redisclient/README.md) | ||||
| ## Core Features | ||||
|  | ||||
| ### OS Module | ||||
| SAL offers a broad spectrum of functionalities, including: | ||||
|  | ||||
| Provides platform-independent interfaces for operating system functionality. | ||||
| - **System Operations**: File and directory management, environment variable access, system information retrieval, and OS-specific commands. | ||||
| - **Process Management**: Create, monitor, control, and interact with system processes. | ||||
| - **Containerization Tools**:  | ||||
|     - Integration with **Buildah** for building OCI/Docker-compatible container images. | ||||
|     - Integration with **nerdctl** for managing containers (run, stop, list, build, etc.). | ||||
| - **Version Control**: Programmatic interaction with Git repositories (clone, commit, push, pull, status, etc.). | ||||
| - **Database Clients**: | ||||
|     - **Redis**: Robust client for interacting with Redis servers. | ||||
|     - **PostgreSQL**: Client for executing queries and managing PostgreSQL databases. | ||||
| - **Scripting Engine**: In-built support for the **Rhai** scripting language, allowing SAL functionalities to be scripted and automated, primarily through the `herodo` tool. | ||||
| - **Networking & Services**: | ||||
|     - **Mycelium**: Tools for Mycelium network peer management and message passing. | ||||
|     - **Zinit**: Client for interacting with the Zinit process supervision system. | ||||
|     - **RFS (Remote/Virtual Filesystem)**: Mount, manage, pack, and unpack various types of filesystems (local, SSH, S3, WebDAV). | ||||
| - **Text Processing**: A suite of utilities for text manipulation, formatting, and regular expressions. | ||||
| - **Cryptography (`vault`)**: Functions for common cryptographic operations. | ||||
|  | ||||
| ### Git Module | ||||
| ## `herodo`: The SAL Scripting Tool | ||||
|  | ||||
| Tools for interacting with Git repositories programmatically. | ||||
| `herodo` is a command-line utility bundled with SAL that executes Rhai scripts. It empowers users to automate tasks and orchestrate complex workflows by leveraging SAL's diverse modules directly from scripts. | ||||
|  | ||||
| ### Process Module | ||||
| ### Usage | ||||
|  | ||||
| Utilities for process creation, monitoring, and management. | ||||
| ```bash | ||||
| # Execute a single Rhai script | ||||
| herodo script.rhai | ||||
|  | ||||
| ### Text Module | ||||
| # Execute a script with arguments | ||||
| herodo script.rhai arg1 arg2 | ||||
|  | ||||
| Text processing utilities for common operations. | ||||
| # Execute all .rhai scripts in a directory | ||||
| herodo /path/to/scripts/ | ||||
| ``` | ||||
|  | ||||
| ## Usage | ||||
| If a directory is provided, `herodo` will execute all `.rhai` scripts within that directory (and its subdirectories) in alphabetical order. | ||||
|  | ||||
| Add this to your `Cargo.toml`: | ||||
| ### Scriptable SAL Modules via `herodo` | ||||
|  | ||||
| The following SAL modules and functionalities are exposed to the Rhai scripting environment through `herodo`: | ||||
|  | ||||
| - **OS (`os`)**: Comprehensive file system operations, file downloading & installation, and system package management. [Documentation](os/README.md) | ||||
| - **Process (`process`)**: Robust command and script execution, plus process management (listing, finding, killing, checking command existence). [Documentation](process/README.md) | ||||
| - **Text (`text`)**: String manipulation, prefixing, path/name fixing, text replacement, and templating. [Documentation](text/README.md) | ||||
| - **Net (`net`)**: Network operations, HTTP requests, and connectivity utilities. [Documentation](net/README.md) | ||||
| - **Git (`git`)**: High-level repository management and generic Git command execution with Redis-backed authentication (clone, pull, push, commit, etc.). [Documentation](git/README.md) | ||||
| - **Vault (`vault`)**: Cryptographic operations, keypair management, encryption, decryption, hashing, etc. [Documentation](vault/README.md) | ||||
| - **Redis Client (`redisclient`)**: Execute Redis commands (`redis_get`, `redis_set`, `redis_execute`, etc.). [Documentation](redisclient/README.md) | ||||
| - **PostgreSQL Client (`postgresclient`)**: Execute SQL queries against PostgreSQL databases. [Documentation](postgresclient/README.md) | ||||
| - **Zinit (`zinit_client`)**: Client for Zinit process supervisor (service management, logs). [Documentation](zinit_client/README.md) | ||||
| - **Mycelium (`mycelium`)**: Client for Mycelium decentralized networking API (node info, peer management, messaging). [Documentation](mycelium/README.md) | ||||
| - **Virtualization (`virt`)**: | ||||
|   - **Buildah**: OCI/Docker image building functions. [Documentation](virt/README.md) | ||||
|   - **nerdctl**: Container lifecycle management (`nerdctl_run`, `nerdctl_stop`, `nerdctl_images`, `nerdctl_image_build`, etc.) | ||||
|   - **RFS**: Mount various filesystems (local, SSH, S3, etc.), pack/unpack filesystem layers. | ||||
|  | ||||
| ### Example `herodo` Rhai Script | ||||
|  | ||||
| ```rhai | ||||
| // file: /opt/scripts/example_task.rhai | ||||
|  | ||||
| // OS operations | ||||
| println("Checking for /tmp/my_app_data..."); | ||||
| if !exist("/tmp/my_app_data") { | ||||
|     mkdir("/tmp/my_app_data"); | ||||
|     println("Created directory /tmp/my_app_data"); | ||||
| } | ||||
|  | ||||
| // Redis operations | ||||
| println("Setting Redis key 'app_status' to 'running'"); | ||||
| redis_set("app_status", "running"); | ||||
| let status = redis_get("app_status"); | ||||
| println("Current app_status from Redis: " + status); | ||||
|  | ||||
| // Process execution | ||||
| println("Listing files in /tmp:"); | ||||
| let output = run("ls -la /tmp"); | ||||
| println(output.stdout); | ||||
|  | ||||
| println("Script finished."); | ||||
| ``` | ||||
|  | ||||
| Run with: `herodo /opt/scripts/example_task.rhai` | ||||
|  | ||||
| For more examples, check the individual module test directories (e.g., `text/tests/rhai/`, `os/tests/rhai/`, etc.) in this repository. | ||||
|  | ||||
| ## Using SAL as a Rust Library | ||||
|  | ||||
| Add SAL as a dependency to your `Cargo.toml`: | ||||
|  | ||||
| ```toml | ||||
| [dependencies] | ||||
| sal = "0.1.0" | ||||
| sal = "0.1.0" # Or the latest version | ||||
| ``` | ||||
|  | ||||
| Basic example: | ||||
| ### Rust Example: Using Redis Client | ||||
|  | ||||
| ```rust | ||||
| use sal::redisclient::execute; | ||||
| use redis::cmd; | ||||
| use sal::redisclient::{get_global_client, execute_cmd_with_args}; | ||||
| use redis::RedisResult; | ||||
|  | ||||
| async fn example_redis_interaction() -> RedisResult<()> { | ||||
|     // Get a connection from the global pool | ||||
|     let mut conn = get_global_client().await?.get_async_connection().await?; | ||||
|  | ||||
|     // Set a value | ||||
|     execute_cmd_with_args(&mut conn, "SET", vec!["my_key", "my_value"]).await?; | ||||
|     println!("Set 'my_key' to 'my_value'"); | ||||
|  | ||||
|     // Get a value | ||||
|     let value: String = execute_cmd_with_args(&mut conn, "GET", vec!["my_key"]).await?; | ||||
|     println!("Retrieved value for 'my_key': {}", value); | ||||
|  | ||||
| fn main() -> redis::RedisResult<()> { | ||||
|     // Execute a Redis command | ||||
|     let mut cmd = redis::cmd("SET"); | ||||
|     cmd.arg("example_key").arg("example_value"); | ||||
|     execute(&mut cmd)?; | ||||
|      | ||||
|     // Retrieve the value | ||||
|     let mut get_cmd = redis::cmd("GET"); | ||||
|     get_cmd.arg("example_key"); | ||||
|     let value: String = execute(&mut get_cmd)?; | ||||
|     println!("Value: {}", value); | ||||
|      | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| #[tokio::main] | ||||
| async fn main() { | ||||
|     if let Err(e) = example_redis_interaction().await { | ||||
|         eprintln!("Redis Error: {}", e); | ||||
|     } | ||||
| } | ||||
| ``` | ||||
| *(Note: The Redis client API might have evolved; please refer to `src/redisclient/mod.rs` and its documentation for the most current usage.)* | ||||
|  | ||||
| ## 📦 **Workspace Modules Overview** | ||||
|  | ||||
| SAL is organized as a Cargo workspace with the following crates: | ||||
|  | ||||
| ### **Core Library Modules** | ||||
| - **`sal-os`**: Core OS interactions, file system operations, environment access | ||||
| - **`sal-process`**: Process creation, management, and control | ||||
| - **`sal-text`**: Utilities for text processing and manipulation | ||||
| - **`sal-net`**: Network operations, HTTP requests, and connectivity utilities | ||||
|  | ||||
| ### **Integration Modules** | ||||
| - **`sal-git`**: Git repository management and operations | ||||
| - **`sal-vault`**: Cryptographic functions and keypair management | ||||
| - **`sal-rhai`**: Integration layer for the Rhai scripting engine, used by `herodo` | ||||
|  | ||||
| ### **Client Modules** | ||||
| - **`sal-redisclient`**: Client for Redis database interactions | ||||
| - **`sal-postgresclient`**: Client for PostgreSQL database interactions | ||||
| - **`sal-zinit-client`**: Client for Zinit process supervisor | ||||
| - **`sal-mycelium`**: Client for Mycelium network operations | ||||
|  | ||||
| ### **Specialized Modules** | ||||
| - **`sal-virt`**: Virtualization-related utilities (buildah, nerdctl, rfs) | ||||
|  | ||||
| ### **Root Package & Binary** | ||||
| - **`sal`**: Root umbrella crate that re-exports all modules | ||||
| - **`herodo`**: Command-line binary for executing Rhai scripts | ||||
|  | ||||
| ## 🔨 **Building SAL** | ||||
|  | ||||
| Build the entire workspace (all crates) using Cargo: | ||||
|  | ||||
| ```bash | ||||
| # Build all workspace members | ||||
| cargo build --workspace | ||||
|  | ||||
| # Build for release | ||||
| cargo build --workspace --release | ||||
|  | ||||
| # Build specific crate | ||||
| cargo build -p sal-text | ||||
| cargo build -p herodo | ||||
| ``` | ||||
|  | ||||
| The `herodo` executable will be located at `target/debug/herodo` or `target/release/herodo`. | ||||
|  | ||||
| ## 🧪 **Running Tests** | ||||
|  | ||||
| ### **Rust Unit Tests** | ||||
| ```bash | ||||
| # Run all workspace tests | ||||
| cargo test --workspace | ||||
|  | ||||
| # Run tests for specific crate | ||||
| cargo test -p sal-text | ||||
| cargo test -p sal-os | ||||
|  | ||||
| # Run only library tests (faster) | ||||
| cargo test --workspace --lib | ||||
| ``` | ||||
|  | ||||
| ### **Rhai Integration Tests** | ||||
| Run comprehensive Rhai script tests that exercise `herodo` and SAL's scripted functionalities: | ||||
|  | ||||
| ```bash | ||||
| # Run all Rhai integration tests (16 modules) | ||||
| ./run_rhai_tests.sh | ||||
|  | ||||
| # Results: 16/16 modules pass with 100% success rate | ||||
| ``` | ||||
|  | ||||
| The Rhai tests validate real-world functionality across all SAL modules and provide comprehensive integration testing. | ||||
|  | ||||
| ## License | ||||
|  | ||||
| SAL is licensed under the Apache License 2.0. See the [LICENSE](LICENSE) file for details. | ||||
|   | ||||
| @@ -6,10 +6,12 @@ cd "$(dirname "${BASH_SOURCE[0]}")" | ||||
|  | ||||
| rm -f ./target/debug/herodo | ||||
|  | ||||
| # Build the herodo project | ||||
| echo "Building herodo..." | ||||
| cargo build --bin herodo | ||||
| # cargo build --release --bin herodo | ||||
| # Build the herodo project from the herodo package | ||||
| echo "Building herodo from herodo package..." | ||||
| cd herodo | ||||
| cargo build | ||||
| # cargo build --release | ||||
| cd .. | ||||
|  | ||||
| # Check if the build was successful | ||||
| if [ $? -ne 0 ]; then | ||||
| @@ -20,8 +22,14 @@ fi | ||||
| # Echo a success message | ||||
| echo "Build successful!" | ||||
|  | ||||
| mkdir -p ~/hero/bin/ | ||||
| cp target/debug/herodo ~/hero/bin/herodo | ||||
| if [ "$EUID" -eq 0 ]; then | ||||
|   echo "Running as root, copying to /usr/local/bin/" | ||||
|   cp target/debug/herodo /usr/local/bin/herodo | ||||
| else | ||||
|   echo "Running as non-root user, copying to ~/hero/bin/" | ||||
|   mkdir -p ~/hero/bin/ | ||||
|   cp target/debug/herodo ~/hero/bin/herodo | ||||
| fi | ||||
|  | ||||
| # Check if a script name was provided | ||||
| if [ $# -eq 1 ]; then | ||||
|   | ||||
| @@ -16,13 +16,13 @@ Additionally, there's a runner script (`run_all_tests.rhai`) that executes all t | ||||
| To run all tests, execute the following command from the project root: | ||||
| 
 | ||||
| ```bash | ||||
| herodo --path src/rhai_tests/git/run_all_tests.rhai | ||||
| herodo --path git/tests/rhai/run_all_tests.rhai | ||||
| ``` | ||||
| 
 | ||||
| To run individual test scripts: | ||||
| 
 | ||||
| ```bash | ||||
| herodo --path src/rhai_tests/git/01_git_basic.rhai | ||||
| herodo --path git/tests/rhai/01_git_basic.rhai | ||||
| ``` | ||||
| 
 | ||||
| ## Test Details | ||||
							
								
								
									
										386
									
								
								docs/docs/rhai/mycelium_tutorial.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										386
									
								
								docs/docs/rhai/mycelium_tutorial.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,386 @@ | ||||
| # Mycelium Tutorial for Rhai | ||||
|  | ||||
| This tutorial explains how to use the Mycelium networking functionality in Rhai scripts. Mycelium is a peer-to-peer networking system that allows nodes to communicate with each other, and the Rhai bindings provide an easy way to interact with Mycelium from your scripts. | ||||
|  | ||||
| ## Introduction | ||||
|  | ||||
| The Mycelium module for Rhai provides the following capabilities: | ||||
|  | ||||
| - Getting node information | ||||
| - Managing peers (listing, adding, removing) | ||||
| - Viewing routing information | ||||
| - Sending and receiving messages between nodes | ||||
|  | ||||
| This tutorial will walk you through using these features with example scripts. | ||||
|  | ||||
| ## Prerequisites | ||||
|  | ||||
| Before using the Mycelium functionality in Rhai, you need: | ||||
|  | ||||
| 1. A running Mycelium node accessible via HTTP | ||||
|     > See https://github.com/threefoldtech/mycelium | ||||
| 2. The Rhai runtime with Mycelium module enabled | ||||
|  | ||||
| ## Basic Mycelium Operations | ||||
|  | ||||
| Let's start by exploring the basic operations available in Mycelium using the `mycelium_basic.rhai` example. | ||||
|  | ||||
| ### Getting Node Information | ||||
|  | ||||
| To get information about your Mycelium node: | ||||
|  | ||||
| ```rhai | ||||
| // API URL for Mycelium | ||||
| let api_url = "http://localhost:8989"; | ||||
|  | ||||
| // Get node information | ||||
| print("Getting node information:"); | ||||
| try { | ||||
|     let node_info = mycelium_get_node_info(api_url); | ||||
|     print(`Node subnet: ${node_info.nodeSubnet}`); | ||||
|     print(`Node public key: ${node_info.nodePubkey}`); | ||||
| } catch(err) { | ||||
|     print(`Error getting node info: ${err}`); | ||||
| } | ||||
| ``` | ||||
|  | ||||
| This code: | ||||
| 1. Sets the API URL for your Mycelium node | ||||
| 2. Calls `mycelium_get_node_info()` to retrieve information about the node | ||||
| 3. Prints the node's subnet and public key | ||||
|  | ||||
| ### Managing Peers | ||||
|  | ||||
| #### Listing Peers | ||||
|  | ||||
| To list all peers connected to your Mycelium node: | ||||
|  | ||||
| ```rhai | ||||
| // List all peers | ||||
| print("\nListing all peers:"); | ||||
| try { | ||||
|     let peers = mycelium_list_peers(api_url); | ||||
|      | ||||
|     if peers.is_empty() { | ||||
|         print("No peers connected."); | ||||
|     } else { | ||||
|         for peer in peers { | ||||
|             print(`Peer Endpoint: ${peer.endpoint.proto}://${peer.endpoint.socketAddr}`); | ||||
|             print(`  Type: ${peer.type}`); | ||||
|             print(`  Connection State: ${peer.connectionState}`); | ||||
|             print(`  Bytes sent: ${peer.txBytes}`); | ||||
|             print(`  Bytes received: ${peer.rxBytes}`); | ||||
|         } | ||||
|     } | ||||
| } catch(err) { | ||||
|     print(`Error listing peers: ${err}`); | ||||
| } | ||||
| ``` | ||||
|  | ||||
| This code: | ||||
| 1. Calls `mycelium_list_peers()` to get all connected peers | ||||
| 2. Iterates through the peers and prints their details | ||||
|  | ||||
| #### Adding a Peer | ||||
|  | ||||
| To add a new peer to your Mycelium node: | ||||
|  | ||||
| ```rhai | ||||
| // Add a new peer | ||||
| print("\nAdding a new peer:"); | ||||
| let new_peer_address = "tcp://65.21.231.58:9651"; | ||||
| try { | ||||
|     let result = mycelium_add_peer(api_url, new_peer_address); | ||||
|     print(`Peer added: ${result.success}`); | ||||
| } catch(err) { | ||||
|     print(`Error adding peer: ${err}`); | ||||
| } | ||||
| ``` | ||||
|  | ||||
| This code: | ||||
| 1. Specifies a peer address to add | ||||
| 2. Calls `mycelium_add_peer()` to add the peer to your node | ||||
| 3. Prints whether the operation was successful | ||||
|  | ||||
| #### Removing a Peer | ||||
|  | ||||
| To remove a peer from your Mycelium node: | ||||
|  | ||||
| ```rhai | ||||
| // Remove a peer | ||||
| print("\nRemoving a peer:"); | ||||
| let peer_id = "tcp://65.21.231.58:9651"; // This is the peer we added earlier | ||||
| try { | ||||
|     let result = mycelium_remove_peer(api_url, peer_id); | ||||
|     print(`Peer removed: ${result.success}`); | ||||
| } catch(err) { | ||||
|     print(`Error removing peer: ${err}`); | ||||
| } | ||||
| ``` | ||||
|  | ||||
| This code: | ||||
| 1. Specifies the peer ID to remove | ||||
| 2. Calls `mycelium_remove_peer()` to remove the peer | ||||
| 3. Prints whether the operation was successful | ||||
|  | ||||
| ### Viewing Routing Information | ||||
|  | ||||
| #### Listing Selected Routes | ||||
|  | ||||
| To list the selected routes in your Mycelium node: | ||||
|  | ||||
| ```rhai | ||||
| // List selected routes | ||||
| print("\nListing selected routes:"); | ||||
| try { | ||||
|     let routes = mycelium_list_selected_routes(api_url); | ||||
|      | ||||
|     if routes.is_empty() { | ||||
|         print("No selected routes."); | ||||
|     } else { | ||||
|         for route in routes { | ||||
|             print(`Subnet: ${route.subnet}`); | ||||
|             print(`  Next hop: ${route.nextHop}`); | ||||
|             print(`  Metric: ${route.metric}`); | ||||
|         } | ||||
|     } | ||||
| } catch(err) { | ||||
|     print(`Error listing routes: ${err}`); | ||||
| } | ||||
| ``` | ||||
|  | ||||
| This code: | ||||
| 1. Calls `mycelium_list_selected_routes()` to get all selected routes | ||||
| 2. Iterates through the routes and prints their details | ||||
|  | ||||
| #### Listing Fallback Routes | ||||
|  | ||||
| To list the fallback routes in your Mycelium node: | ||||
|  | ||||
| ```rhai | ||||
| // List fallback routes | ||||
| print("\nListing fallback routes:"); | ||||
| try { | ||||
|     let routes = mycelium_list_fallback_routes(api_url); | ||||
|      | ||||
|     if routes.is_empty() { | ||||
|         print("No fallback routes."); | ||||
|     } else { | ||||
|         for route in routes { | ||||
|             print(`Subnet: ${route.subnet}`); | ||||
|             print(`  Next hop: ${route.nextHop}`); | ||||
|             print(`  Metric: ${route.metric}`); | ||||
|         } | ||||
|     } | ||||
| } catch(err) { | ||||
|     print(`Error listing fallback routes: ${err}`); | ||||
| } | ||||
| ``` | ||||
|  | ||||
| This code: | ||||
| 1. Calls `mycelium_list_fallback_routes()` to get all fallback routes | ||||
| 2. Iterates through the routes and prints their details | ||||
|  | ||||
| ## Sending Messages | ||||
|  | ||||
| Now let's look at how to send messages using the `mycelium_send_message.rhai` example. | ||||
|  | ||||
| ```rhai | ||||
| // API URL for Mycelium | ||||
| let api_url = "http://localhost:1111"; | ||||
|  | ||||
| // Send a message | ||||
| print("\nSending a message:"); | ||||
| let destination = "5af:ae6b:dcd8:ffdb:b71:7dde:d3:1033"; // Replace with the actual destination IP address | ||||
| let topic = "test_topic"; | ||||
| let message = "Hello from Rhai sender!"; | ||||
| let deadline_secs = -10; // Seconds we wait for a reply | ||||
|  | ||||
| try { | ||||
|     print(`Attempting to send message to ${destination} on topic '${topic}'`); | ||||
|     let result = mycelium_send_message(api_url, destination, topic, message, deadline_secs); | ||||
|     print(`result: ${result}`); | ||||
|     print(`Message sent: ${result.success}`); | ||||
|     if result.id != "" { | ||||
|         print(`Message ID: ${result.id}`); | ||||
|     } | ||||
| } catch(err) { | ||||
|     print(`Error sending message: ${err}`); | ||||
| } | ||||
| ``` | ||||
|  | ||||
| This code: | ||||
| 1. Sets the API URL for your Mycelium node | ||||
| 2. Specifies the destination IP address, topic, message content, and deadline | ||||
| 3. Calls `mycelium_send_message()` to send the message | ||||
| 4. Prints the result, including the message ID if successful | ||||
|  | ||||
| ### Important Parameters for Sending Messages | ||||
|  | ||||
| - `api_url`: The URL of your Mycelium node's API | ||||
| - `destination`: The IP address of the destination node | ||||
| - `topic`: The topic to send the message on (must match what the receiver is listening for) | ||||
| - `message`: The content of the message | ||||
| - `deadline_secs`: Time in seconds to wait for a reply. Use a negative value if you don't want to wait for a reply. | ||||
|  | ||||
| ## Receiving Messages | ||||
|  | ||||
| Now let's look at how to receive messages using the `mycelium_receive_message.rhai` example. | ||||
|  | ||||
| ```rhai | ||||
| // API URL for Mycelium | ||||
| let api_url = "http://localhost:2222"; | ||||
|  | ||||
| // Receive messages | ||||
| print("\nReceiving messages:"); | ||||
| let receive_topic = "test_topic"; | ||||
| let wait_deadline_secs = 100;  | ||||
|  | ||||
| print(`Listening for messages on topic '${receive_topic}'...`); | ||||
| try { | ||||
|     let messages = mycelium_receive_messages(api_url, receive_topic, wait_deadline_secs); | ||||
|      | ||||
|     if messages.is_empty() { | ||||
|         // print("No new messages received in this poll."); | ||||
|     } else { | ||||
|         print("Received a message:"); | ||||
|         print(`  Message id: ${messages.id}`); | ||||
|         print(`  Message from: ${messages.srcIp}`); | ||||
|         print(`  Topic: ${messages.topic}`); | ||||
|         print(`  Payload: ${messages.payload}`); | ||||
|     } | ||||
| } catch(err) { | ||||
|     print(`Error receiving messages: ${err}`); | ||||
| } | ||||
|  | ||||
| print("Finished attempting to receive messages."); | ||||
| ``` | ||||
|  | ||||
| This code: | ||||
| 1. Sets the API URL for your Mycelium node | ||||
| 2. Specifies the topic to listen on and how long to wait for messages | ||||
| 3. Calls `mycelium_receive_messages()` to receive messages | ||||
| 4. Processes and prints any received messages | ||||
|  | ||||
| ### Important Parameters for Receiving Messages | ||||
|  | ||||
| - `api_url`: The URL of your Mycelium node's API | ||||
| - `receive_topic`: The topic to listen for messages on (must match what the sender is using) | ||||
| - `wait_deadline_secs`: Time in seconds to wait for messages to arrive. The function will block for this duration if no messages are immediately available. | ||||
|  | ||||
| ## Complete Messaging Example | ||||
|  | ||||
| To set up a complete messaging system, you would typically run two instances of Mycelium (node A sender, node B receiver). | ||||
|  | ||||
| 1. Run the `mycelium_receive_message.rhai` script to listen for messages. **Fill in the API address of node B**. | ||||
| 2. Run the `mycelium_send_message.rhai` script to send messages. **Fill in the API address of node A, and fill in the overlay address of node B as destination**. | ||||
|  | ||||
| ### Setting Up the Receiver | ||||
|  | ||||
| First, start a Mycelium node and run the receiver script: | ||||
|  | ||||
| ```rhai | ||||
| // API URL for Mycelium | ||||
| let api_url = "http://localhost:2222";  // Your receiver node's API URL | ||||
|  | ||||
| // Receive messages | ||||
| let receive_topic = "test_topic"; | ||||
| let wait_deadline_secs = 100;  // Wait up to 100 seconds for messages | ||||
|  | ||||
| print(`Listening for messages on topic '${receive_topic}'...`); | ||||
| try { | ||||
|     let messages = mycelium_receive_messages(api_url, receive_topic, wait_deadline_secs); | ||||
|      | ||||
|     if messages.is_empty() { | ||||
|         print("No new messages received in this poll."); | ||||
|     } else { | ||||
|         print("Received a message:"); | ||||
|         print(`  Message id: ${messages.id}`); | ||||
|         print(`  Message from: ${messages.srcIp}`); | ||||
|         print(`  Topic: ${messages.topic}`); | ||||
|         print(`  Payload: ${messages.payload}`); | ||||
|     } | ||||
| } catch(err) { | ||||
|     print(`Error receiving messages: ${err}`); | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ### Setting Up the Sender | ||||
|  | ||||
| Then, on another Mycelium node, run the sender script: | ||||
|  | ||||
| ```rhai | ||||
| // API URL for Mycelium | ||||
| let api_url = "http://localhost:1111";  // Your sender node's API URL | ||||
|  | ||||
| // Send a message | ||||
| let destination = "5af:ae6b:dcd8:ffdb:b71:7dde:d3:1033";  // The receiver node's IP address | ||||
| let topic = "test_topic";  // Must match the receiver's topic | ||||
| let message = "Hello from Rhai sender!"; | ||||
| let deadline_secs = -10;  // Don't wait for a reply | ||||
|  | ||||
| try { | ||||
|     print(`Attempting to send message to ${destination} on topic '${topic}'`); | ||||
|     let result = mycelium_send_message(api_url, destination, topic, message, deadline_secs); | ||||
|     print(`Message sent: ${result.success}`); | ||||
|     if result.id != "" { | ||||
|         print(`Message ID: ${result.id}`); | ||||
|     } | ||||
| } catch(err) { | ||||
|     print(`Error sending message: ${err}`); | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ### Example: setting up 2 different Mycelium peers on same the host and sending/receiving a message | ||||
|  | ||||
| #### Obtain Mycelium | ||||
|  | ||||
| - Download the latest Mycelium binary from https://github.com/threefoldtech/mycelium/releases/ | ||||
| - Or compile from source | ||||
|  | ||||
| #### Setup | ||||
| - Create two different private key files. Each key file should contain exactely 32 bytes. In this example we'll save these files as `sender.bin` and `receiver.bin`. Note: generate your own 32-byte key files, the values below are just used as examples. | ||||
| > `echo '9f3d72c1a84be6f027bba94cde015ee839cedb2ac4f2822bfc94449e3e2a1c6a' > sender.bin` | ||||
|  | ||||
| > `echo 'e81c5a76f42bd9a3c73fe0bb2196acdfb6348e99d0b01763a2e57ce3a4e8f5dd' > receiver.bin` | ||||
|  | ||||
| #### Start the nodes | ||||
| - **Sender**: this node will have the API server hosted on `127.0.0.1:1111` and the JSON-RPC server on `127.0.0.1:8991`. | ||||
| > `sudo ./mycelium --key-file sender.bin --disable-peer-discovery --disable-quic --no-tun --api-addr 127.0.0.1:1111 --jsonrpc-addr 127.0.0.1:8991` | ||||
|  | ||||
| - **Receiver**: this node will have the API server hosted on `127.0.0.1:2222` and the JSON-RPC server on `127.0.0.1:8992`. | ||||
| > `sudo ./mycelium --key-file receiver.bin --disable-peer-discovery --disable-quic --no-tun --api-addr 127.0.0.1:2222 --jsonrpc-addr 127.0.0.1:8992 --peers tcp://<UNDERLAY_IP_SENDER>:9651` | ||||
| - Obtain the Mycelium overlay IP by running `./mycelium --key-file receiver.bin --api-addr 127.0.0.1:2222 inspect`. **Replace this IP as destination  in the [mycelium_send_message.rhai](../../../examples/mycelium/mycelium_send_message.rhai) example**. | ||||
|  | ||||
| #### Execute the examples | ||||
| - First build by executing `./build_herdo.sh` from the SAL root directory | ||||
| - `cd target/debug` | ||||
|  | ||||
| - Run the sender script: `sudo ./herodo --path ../../examples/mycelium/mycelium_send_message.rhai` | ||||
| ``` | ||||
| Executing: ../../examples/mycelium/mycelium_send_message.rhai | ||||
|  | ||||
| Sending a message: | ||||
| Attempting to send message to 50e:6d75:4568:366e:f75:2ac3:bbb1:3fdd on topic 'test_topic' | ||||
| result: #{"id": "bfd47dc689a7b826"} | ||||
| Message sent:  | ||||
| Message ID: bfd47dc689a7b826 | ||||
| Script executed successfull | ||||
| ``` | ||||
|  | ||||
| - Run the receiver script: `sudo ./herodo --path ../../examples/mycelium/mycelium_receive_message.rhai` | ||||
| ``` | ||||
| Executing: ../../examples/mycelium/mycelium_receive_message.rhai | ||||
|  | ||||
| Receiving messages: | ||||
| Listening for messages on topic 'test_topic'... | ||||
| Received a message: | ||||
|   Message id: bfd47dc689a7b826 | ||||
|   Message from: 45d:26e1:a413:9d08:80ce:71c6:a931:4315 | ||||
|   Topic: dGVzdF90b3BpYw== | ||||
|   Payload: SGVsbG8gZnJvbSBSaGFpIHNlbmRlciE= | ||||
| Finished attempting to receive messages. | ||||
| Script executed successfully | ||||
| ``` | ||||
| > Decoding the payload `SGVsbG8gZnJvbSBSaGFpIHNlbmRlciE=` results in the expected `Hello from Rhai sender!` message. Mission succesful! | ||||
|  | ||||
| @@ -9,9 +9,12 @@ The PostgreSQL client module provides the following features: | ||||
| 1. **Basic PostgreSQL Operations**: Execute queries, fetch results, etc. | ||||
| 2. **Connection Management**: Automatic connection handling and reconnection | ||||
| 3. **Builder Pattern for Configuration**: Flexible configuration with authentication support | ||||
| 4. **PostgreSQL Installer**: Install and configure PostgreSQL using nerdctl | ||||
| 5. **Database Management**: Create databases and execute SQL scripts | ||||
| 
 | ||||
| ## Prerequisites | ||||
| 
 | ||||
| For basic PostgreSQL operations: | ||||
| - PostgreSQL server must be running and accessible | ||||
| - Environment variables should be set for connection details: | ||||
|   - `POSTGRES_HOST`: PostgreSQL server host (default: localhost) | ||||
| @@ -20,6 +23,11 @@ The PostgreSQL client module provides the following features: | ||||
|   - `POSTGRES_PASSWORD`: PostgreSQL password | ||||
|   - `POSTGRES_DB`: PostgreSQL database name (default: postgres) | ||||
| 
 | ||||
| For PostgreSQL installer: | ||||
| - nerdctl must be installed and working | ||||
| - Docker images must be accessible | ||||
| - Sufficient permissions to create and manage containers | ||||
| 
 | ||||
| ## Test Files | ||||
| 
 | ||||
| ### 01_postgres_connection.rhai | ||||
| @@ -34,6 +42,15 @@ Tests basic PostgreSQL connection and operations: | ||||
| - Dropping a table | ||||
| - Resetting the connection | ||||
| 
 | ||||
| ### 02_postgres_installer.rhai | ||||
| 
 | ||||
| Tests PostgreSQL installer functionality: | ||||
| 
 | ||||
| - Installing PostgreSQL using nerdctl | ||||
| - Creating a database | ||||
| - Executing SQL scripts | ||||
| - Checking if PostgreSQL is running | ||||
| 
 | ||||
| ### run_all_tests.rhai | ||||
| 
 | ||||
| Runs all PostgreSQL client module tests and provides a summary of the results. | ||||
| @@ -66,6 +83,13 @@ herodo --path src/rhai_tests/postgresclient/01_postgres_connection.rhai | ||||
| - `pg_query(query)`: Execute a query and return the results as an array of maps | ||||
| - `pg_query_one(query)`: Execute a query and return a single row as a map | ||||
| 
 | ||||
| ### Installer Functions | ||||
| 
 | ||||
| - `pg_install(container_name, version, port, username, password)`: Install PostgreSQL using nerdctl | ||||
| - `pg_create_database(container_name, db_name)`: Create a new database in PostgreSQL | ||||
| - `pg_execute_sql(container_name, db_name, sql)`: Execute a SQL script in PostgreSQL | ||||
| - `pg_is_running(container_name)`: Check if PostgreSQL is running | ||||
| 
 | ||||
| ## Authentication Support | ||||
| 
 | ||||
| The PostgreSQL client module will support authentication using the builder pattern in a future update. | ||||
| @@ -85,7 +109,9 @@ When implemented, the builder pattern will support the following configuration o | ||||
| 
 | ||||
| ## Example Usage | ||||
| 
 | ||||
| ```javascript | ||||
| ### Basic PostgreSQL Operations | ||||
| 
 | ||||
| ```rust | ||||
| // Connect to PostgreSQL | ||||
| if (pg_connect()) { | ||||
|     print("Connected to PostgreSQL!"); | ||||
| @@ -112,3 +138,51 @@ if (pg_connect()) { | ||||
|     pg_execute(drop_query); | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| ### PostgreSQL Installer | ||||
| 
 | ||||
| ```rust | ||||
| // Install PostgreSQL | ||||
| let container_name = "my-postgres"; | ||||
| let postgres_version = "15"; | ||||
| let postgres_port = 5432; | ||||
| let postgres_user = "myuser"; | ||||
| let postgres_password = "mypassword"; | ||||
| 
 | ||||
| if (pg_install(container_name, postgres_version, postgres_port, postgres_user, postgres_password)) { | ||||
|     print("PostgreSQL installed successfully!"); | ||||
| 
 | ||||
|     // Create a database | ||||
|     let db_name = "mydb"; | ||||
|     if (pg_create_database(container_name, db_name)) { | ||||
|         print(`Database '${db_name}' created successfully!`); | ||||
| 
 | ||||
|         // Execute a SQL script | ||||
|         let create_table_sql = ` | ||||
|             CREATE TABLE users ( | ||||
|                 id SERIAL PRIMARY KEY, | ||||
|                 name TEXT NOT NULL, | ||||
|                 email TEXT UNIQUE NOT NULL | ||||
|             ); | ||||
|         `; | ||||
| 
 | ||||
|         let result = pg_execute_sql(container_name, db_name, create_table_sql); | ||||
|         print("Table created successfully!"); | ||||
| 
 | ||||
|         // Insert data | ||||
|         let insert_sql = "# | ||||
|             INSERT INTO users (name, email) VALUES | ||||
|             ('John Doe', 'john@example.com'), | ||||
|             ('Jane Smith', 'jane@example.com'); | ||||
|         #"; | ||||
| 
 | ||||
|         result = pg_execute_sql(container_name, db_name, insert_sql); | ||||
|         print("Data inserted successfully!"); | ||||
| 
 | ||||
|         // Query data | ||||
|         let query_sql = "SELECT * FROM users;"; | ||||
|         result = pg_execute_sql(container_name, db_name, query_sql); | ||||
|         print(`Query result: ${result}`); | ||||
|     } | ||||
| } | ||||
| ``` | ||||
| @@ -1,4 +1,4 @@ | ||||
| // File: /root/code/git.ourworld.tf/herocode/sal/examples/container_example.rs | ||||
| // File: /root/code/git.threefold.info/herocode/sal/examples/container_example.rs | ||||
|  | ||||
| use std::error::Error; | ||||
| use sal::virt::nerdctl::Container; | ||||
|   | ||||
| @@ -2,7 +2,7 @@ | ||||
| // Demonstrates file system operations using SAL | ||||
|  | ||||
| // Create a test directory | ||||
| let test_dir = "rhai_test_dir"; | ||||
| let test_dir = "/tmp/rhai_test_dir"; | ||||
| println(`Creating directory: ${test_dir}`); | ||||
| let mkdir_result = mkdir(test_dir); | ||||
| println(`Directory creation result: ${mkdir_result}`); | ||||
| @@ -61,4 +61,4 @@ for file in files { | ||||
| // delete(test_dir); | ||||
| // println("Cleanup complete"); | ||||
|  | ||||
| "File operations script completed successfully!" | ||||
| "File operations script completed successfully!" | ||||
|   | ||||
| @@ -121,16 +121,16 @@ println(`Using local image: ${local_image_name}`); | ||||
|  | ||||
| // Tag the image with the localhost prefix for nerdctl compatibility | ||||
| println(`Tagging image as ${local_image_name}...`); | ||||
| let tag_result = bah_image_tag(final_image_name, local_image_name); | ||||
| let tag_result = image_tag(final_image_name, local_image_name); | ||||
|  | ||||
| // Print a command to check if the image exists in buildah | ||||
| println("\nTo verify the image was created with buildah, run:"); | ||||
| println("buildah images"); | ||||
|  | ||||
| // Note: If nerdctl cannot find the image, you may need to push it to a registry | ||||
| println("\nNote: If nerdctl cannot find the image, you may need to push it to a registry:"); | ||||
| println("buildah push localhost/custom-golang-nginx:latest docker://localhost:5000/custom-golang-nginx:latest"); | ||||
| println("nerdctl pull localhost:5000/custom-golang-nginx:latest"); | ||||
| // println("\nNote: If nerdctl cannot find the image, you may need to push it to a registry:"); | ||||
| // println("buildah push localhost/custom-golang-nginx:latest docker://localhost:5000/custom-golang-nginx:latest"); | ||||
| // println("nerdctl pull localhost:5000/custom-golang-nginx:latest"); | ||||
|  | ||||
| let container = nerdctl_container_from_image("golang-nginx-demo", local_image_name) | ||||
|     .with_detach(true) | ||||
|   | ||||
							
								
								
									
										44
									
								
								examples/containers/buildah_run.rhai
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								examples/containers/buildah_run.rhai
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | ||||
|  | ||||
| // Now use nerdctl to run a container from the new image | ||||
| println("\nStarting container from the new image using nerdctl..."); | ||||
|  | ||||
| // Create a container using the builder pattern | ||||
| // Use localhost/ prefix to ensure nerdctl uses the local image | ||||
| let local_image_name = "localhost/custom-golang-nginx:latest"; | ||||
| println(`Using local image: ${local_image_name}`); | ||||
|  | ||||
| // Import the image from buildah to nerdctl | ||||
| println("Importing image from buildah to nerdctl..."); | ||||
| process_run("buildah", ["push", "custom-golang-nginx:latest", "docker-daemon:localhost/custom-golang-nginx:latest"]); | ||||
|  | ||||
| let tag_result = nerdctl_image_tag("custom-golang-nginx:latest", local_image_name); | ||||
|  | ||||
| // Tag the image with the localhost prefix for nerdctl compatibility | ||||
| // println(`Tagging image as ${local_image_name}...`); | ||||
| // let tag_result = bah_image_tag(final_image_name, local_image_name); | ||||
|  | ||||
| // Print a command to check if the image exists in buildah | ||||
| println("\nTo verify the image was created with buildah, run:"); | ||||
| println("buildah images"); | ||||
|  | ||||
| // Note: If nerdctl cannot find the image, you may need to push it to a registry | ||||
| // println("\nNote: If nerdctl cannot find the image, you may need to push it to a registry:"); | ||||
| // println("buildah push localhost/custom-golang-nginx:latest docker://localhost:5000/custom-golang-nginx:latest"); | ||||
| // println("nerdctl pull localhost:5000/custom-golang-nginx:latest"); | ||||
|  | ||||
| let container = nerdctl_container_from_image("golang-nginx-demo", local_image_name) | ||||
|     .with_detach(true) | ||||
|     .with_port("8081:80")  // Map port 80 in the container to 8080 on the host | ||||
|     .with_restart_policy("unless-stopped") | ||||
|     .build(); | ||||
|  | ||||
| // Start the container | ||||
| let start_result = container.start(); | ||||
|  | ||||
| println("\nWorkflow completed successfully!"); | ||||
| println("The web server should be running at http://localhost:8081"); | ||||
| println("You can check container logs with: nerdctl logs golang-nginx-demo"); | ||||
| println("To stop the container: nerdctl stop golang-nginx-demo"); | ||||
| println("To remove the container: nerdctl rm golang-nginx-demo"); | ||||
|  | ||||
| "Buildah and nerdctl workflow completed successfully!" | ||||
| @@ -1,42 +0,0 @@ | ||||
|  | ||||
|  | ||||
|  | ||||
| fn nerdctl_download(){ | ||||
|     let name="nerdctl"; | ||||
|     let url="https://github.com/containerd/nerdctl/releases/download/v2.0.4/nerdctl-2.0.4-linux-amd64.tar.gz"; | ||||
|     download(url,`/tmp/${name}`,20000); | ||||
|     copy(`/tmp/${name}/*`,"/root/hero/bin/"); | ||||
|     delete(`/tmp/${name}`); | ||||
|      | ||||
|     let name="containerd"; | ||||
|     let url="https://github.com/containerd/containerd/releases/download/v2.0.4/containerd-2.0.4-linux-amd64.tar.gz"; | ||||
|     download(url,`/tmp/${name}`,20000); | ||||
|     copy(`/tmp/${name}/bin/*`,"/root/hero/bin/"); | ||||
|     delete(`/tmp/${name}`); | ||||
|  | ||||
|     run("apt-get -y install buildah runc"); | ||||
|  | ||||
|     let url="https://github.com/threefoldtech/rfs/releases/download/v2.0.6/rfs"; | ||||
|     download_file(url,`/tmp/rfs`,10000); | ||||
|     chmod_exec("/tmp/rfs"); | ||||
|     mv(`/tmp/rfs`,"/root/hero/bin/"); | ||||
|  | ||||
| } | ||||
|  | ||||
| fn ipfs_download(){ | ||||
|     let name="ipfs"; | ||||
|     let url="https://github.com/ipfs/kubo/releases/download/v0.34.1/kubo_v0.34.1_linux-amd64.tar.gz"; | ||||
|     download(url,`/tmp/${name}`,20); | ||||
|     copy(`/tmp/${name}/kubo/ipfs`,"/root/hero/bin/ipfs"); | ||||
|     // delete(`/tmp/${name}`); | ||||
|      | ||||
|  | ||||
| } | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| nerdctl_download(); | ||||
| // ipfs_download(); | ||||
|  | ||||
| "done" | ||||
							
								
								
									
										64
									
								
								examples/hero_vault/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								examples/hero_vault/README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,64 @@ | ||||
| # Hero Vault Cryptography Examples | ||||
|  | ||||
| This directory contains examples demonstrating the Hero Vault cryptography functionality integrated into the SAL project. | ||||
|  | ||||
| ## Overview | ||||
|  | ||||
| Hero Vault provides cryptographic operations including: | ||||
|  | ||||
| - Key space management (creation, loading, encryption, decryption) | ||||
| - Keypair management (creation, selection, listing) | ||||
| - Digital signatures (signing and verification) | ||||
| - Symmetric encryption (key generation, encryption, decryption) | ||||
| - Ethereum wallet functionality | ||||
| - Smart contract interactions | ||||
| - Key-value store with encryption | ||||
|  | ||||
| ## Example Files | ||||
|  | ||||
| - `example.rhai` - Basic example demonstrating key management, signing, and encryption | ||||
| - `advanced_example.rhai` - Advanced example with error handling, conditional logic, and more complex operations | ||||
| - `key_persistence_example.rhai` - Demonstrates creating and saving a key space to disk | ||||
| - `load_existing_space.rhai` - Shows how to load a previously created key space and use its keypairs | ||||
| - `contract_example.rhai` - Demonstrates loading a contract ABI and interacting with smart contracts | ||||
| - `agung_send_transaction.rhai` - Demonstrates sending native tokens on the Agung network | ||||
| - `agung_contract_with_args.rhai` - Shows how to interact with contracts with arguments on Agung | ||||
|  | ||||
| ## Running the Examples | ||||
|  | ||||
| You can run the examples using the `herodo` tool that comes with the SAL project: | ||||
|  | ||||
| ```bash | ||||
| # Run a single example | ||||
| herodo --path example.rhai | ||||
|  | ||||
| # Run all examples using the provided script | ||||
| ./run_examples.sh | ||||
| ``` | ||||
|  | ||||
| ## Key Space Storage | ||||
|  | ||||
| Key spaces are stored in the `~/.hero-vault/key-spaces/` directory by default. Each key space is stored in a separate JSON file named after the key space (e.g., `my_space.json`). | ||||
|  | ||||
| ## Ethereum Functionality | ||||
|  | ||||
| The Hero Vault module provides comprehensive Ethereum wallet functionality: | ||||
|  | ||||
| - Creating and managing wallets for different networks | ||||
| - Sending ETH transactions | ||||
| - Checking balances | ||||
| - Interacting with smart contracts (read and write functions) | ||||
| - Support for multiple networks (Ethereum, Gnosis, Peaq, Agung, etc.) | ||||
|  | ||||
| ## Security | ||||
|  | ||||
| Key spaces are encrypted with ChaCha20Poly1305 using a key derived from the provided password. The encryption ensures that the key material is secure at rest. | ||||
|  | ||||
| ## Best Practices | ||||
|  | ||||
| 1. **Use Strong Passwords**: Since the security of your key spaces depends on the strength of your passwords, use strong, unique passwords. | ||||
| 2. **Backup Key Spaces**: Regularly backup your key spaces directory to prevent data loss. | ||||
| 3. **Script Organization**: Split your scripts into logical units, with separate scripts for key creation and key usage. | ||||
| 4. **Error Handling**: Always check the return values of functions to ensure operations succeeded before proceeding. | ||||
| 5. **Network Selection**: When working with Ethereum functionality, be explicit about which network you're targeting to avoid confusion. | ||||
| 6. **Gas Management**: For Ethereum transactions, consider gas costs and set appropriate gas limits. | ||||
							
								
								
									
										233
									
								
								examples/hero_vault/advanced_example.rhai
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										233
									
								
								examples/hero_vault/advanced_example.rhai
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,233 @@ | ||||
| // Advanced Rhai script example for Hero Vault Cryptography Module | ||||
| // This script demonstrates conditional logic, error handling, and more complex operations | ||||
|  | ||||
| // Function to create a key space with error handling | ||||
| fn setup_key_space(name, password) { | ||||
|     print("Attempting: Create key space: " + name); | ||||
|     let result = create_key_space(name, password); | ||||
|      | ||||
|     if result { | ||||
|         print("✅ Create key space succeeded!"); | ||||
|         return true; | ||||
|     } else { | ||||
|         print("❌ Create key space failed!"); | ||||
|     } | ||||
|      | ||||
|     return false; | ||||
| } | ||||
|  | ||||
| // Function to create and select a keypair | ||||
| fn setup_keypair(name, password) { | ||||
|     print("Attempting: Create keypair: " + name); | ||||
|     let result = create_keypair(name, password); | ||||
|      | ||||
|     if result { | ||||
|         print("✅ Create keypair succeeded!"); | ||||
|          | ||||
|         print("Attempting: Select keypair: " + name); | ||||
|         let selected = select_keypair(name); | ||||
|          | ||||
|         if selected { | ||||
|             print("✅ Select keypair succeeded!"); | ||||
|             return true; | ||||
|         } else { | ||||
|             print("❌ Select keypair failed!"); | ||||
|         } | ||||
|     } else { | ||||
|         print("❌ Create keypair failed!"); | ||||
|     } | ||||
|      | ||||
|     return false; | ||||
| } | ||||
|  | ||||
| // Function to sign multiple messages | ||||
| fn sign_messages(messages) { | ||||
|     let signatures = []; | ||||
|      | ||||
|     for message in messages { | ||||
|         print("Signing message: " + message); | ||||
|         print("Attempting: Sign message"); | ||||
|         let signature = sign(message); | ||||
|          | ||||
|         if signature != "" { | ||||
|             print("✅ Sign message succeeded!"); | ||||
|             signatures.push(#{ | ||||
|                 message: message, | ||||
|                 signature: signature | ||||
|             }); | ||||
|         } else { | ||||
|             print("❌ Sign message failed!"); | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     return signatures; | ||||
| } | ||||
|  | ||||
| // Function to verify signatures | ||||
| fn verify_signatures(signed_messages) { | ||||
|     let results = []; | ||||
|      | ||||
|     for item in signed_messages { | ||||
|         let message = item.message; | ||||
|         let signature = item.signature; | ||||
|          | ||||
|         print("Verifying signature for: " + message); | ||||
|         print("Attempting: Verify signature"); | ||||
|         let is_valid = verify(message, signature); | ||||
|          | ||||
|         if is_valid { | ||||
|             print("✅ Verify signature succeeded!"); | ||||
|         } else { | ||||
|             print("❌ Verify signature failed!"); | ||||
|         } | ||||
|          | ||||
|         results.push(#{ | ||||
|             message: message, | ||||
|             valid: is_valid | ||||
|         }); | ||||
|     } | ||||
|      | ||||
|     return results; | ||||
| } | ||||
|  | ||||
| // Function to encrypt multiple messages | ||||
| fn encrypt_messages(messages) { | ||||
|     // Generate a symmetric key | ||||
|     print("Attempting: Generate symmetric key"); | ||||
|     let key = generate_key(); | ||||
|      | ||||
|     if key == "" { | ||||
|         print("❌ Generate symmetric key failed!"); | ||||
|         return []; | ||||
|     } | ||||
|      | ||||
|     print("✅ Generate symmetric key succeeded!"); | ||||
|     print("Using key: " + key); | ||||
|     let encrypted_messages = []; | ||||
|      | ||||
|     for message in messages { | ||||
|         print("Encrypting message: " + message); | ||||
|         print("Attempting: Encrypt message"); | ||||
|         let encrypted = encrypt(key, message); | ||||
|          | ||||
|         if encrypted != "" { | ||||
|             print("✅ Encrypt message succeeded!"); | ||||
|             encrypted_messages.push(#{ | ||||
|                 original: message, | ||||
|                 encrypted: encrypted, | ||||
|                 key: key | ||||
|             }); | ||||
|         } else { | ||||
|             print("❌ Encrypt message failed!"); | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     return encrypted_messages; | ||||
| } | ||||
|  | ||||
| // Function to decrypt messages | ||||
| fn decrypt_messages(encrypted_messages) { | ||||
|     let decrypted_messages = []; | ||||
|      | ||||
|     for item in encrypted_messages { | ||||
|         let encrypted = item.encrypted; | ||||
|         let key = item.key; | ||||
|         let original = item.original; | ||||
|          | ||||
|         print("Decrypting message..."); | ||||
|         print("Attempting: Decrypt message"); | ||||
|         let decrypted = decrypt(key, encrypted); | ||||
|          | ||||
|         if decrypted != false { | ||||
|             let success = decrypted == original; | ||||
|              | ||||
|             decrypted_messages.push(#{ | ||||
|                 decrypted: decrypted, | ||||
|                 original: original, | ||||
|                 success: success | ||||
|             }); | ||||
|              | ||||
|             if success { | ||||
|                 print("Decryption matched original ✅"); | ||||
|             } else { | ||||
|                 print("Decryption did not match original ❌"); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     return decrypted_messages; | ||||
| } | ||||
|  | ||||
| // Main script execution | ||||
| print("=== Advanced Cryptography Script ==="); | ||||
|  | ||||
| // Set up key space | ||||
| let space_name = "advanced_space"; | ||||
| let password = "secure_password123"; | ||||
|  | ||||
| if setup_key_space(space_name, password) { | ||||
|     print("\n--- Key space setup complete ---\n"); | ||||
|      | ||||
|     // Set up keypair | ||||
|     if setup_keypair("advanced_keypair", password) { | ||||
|         print("\n--- Keypair setup complete ---\n"); | ||||
|          | ||||
|         // Define messages to sign | ||||
|         let messages = [ | ||||
|             "This is the first message to sign", | ||||
|             "Here's another message that needs signing", | ||||
|             "And a third message for good measure" | ||||
|         ]; | ||||
|          | ||||
|         // Sign messages | ||||
|         print("\n--- Signing Messages ---\n"); | ||||
|         let signed_messages = sign_messages(messages); | ||||
|          | ||||
|         // Verify signatures | ||||
|         print("\n--- Verifying Signatures ---\n"); | ||||
|         let verification_results = verify_signatures(signed_messages); | ||||
|          | ||||
|         // Count successful verifications | ||||
|         let successful_verifications = verification_results.filter(|r| r.valid).len(); | ||||
|         print("Successfully verified " + successful_verifications + " out of " + verification_results.len() + " signatures"); | ||||
|          | ||||
|         // Encrypt messages | ||||
|         print("\n--- Encrypting Messages ---\n"); | ||||
|         let encrypted_messages = encrypt_messages(messages); | ||||
|          | ||||
|         // Decrypt messages | ||||
|         print("\n--- Decrypting Messages ---\n"); | ||||
|         let decryption_results = decrypt_messages(encrypted_messages); | ||||
|          | ||||
|         // Count successful decryptions | ||||
|         let successful_decryptions = decryption_results.filter(|r| r.success).len(); | ||||
|         print("Successfully decrypted " + successful_decryptions + " out of " + decryption_results.len() + " messages"); | ||||
|          | ||||
|         // Create Ethereum wallet | ||||
|         print("\n--- Creating Ethereum Wallet ---\n"); | ||||
|         print("Attempting: Create Ethereum wallet"); | ||||
|         let wallet_created = create_ethereum_wallet(); | ||||
|          | ||||
|         if wallet_created { | ||||
|             print("✅ Create Ethereum wallet succeeded!"); | ||||
|              | ||||
|             print("Attempting: Get Ethereum address"); | ||||
|             let address = get_ethereum_address(); | ||||
|              | ||||
|             if address != "" { | ||||
|                 print("✅ Get Ethereum address succeeded!"); | ||||
|                 print("Ethereum wallet address: " + address); | ||||
|             } else { | ||||
|                 print("❌ Get Ethereum address failed!"); | ||||
|             } | ||||
|         } else { | ||||
|             print("❌ Create Ethereum wallet failed!"); | ||||
|         } | ||||
|          | ||||
|         print("\n=== Script execution completed successfully! ==="); | ||||
|     } else { | ||||
|         print("Failed to set up keypair. Aborting script."); | ||||
|     } | ||||
| } else { | ||||
|     print("Failed to set up key space. Aborting script."); | ||||
| } | ||||
							
								
								
									
										152
									
								
								examples/hero_vault/agung_contract_with_args.rhai
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										152
									
								
								examples/hero_vault/agung_contract_with_args.rhai
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,152 @@ | ||||
| // Example Rhai script for testing contract functions with arguments on Agung network | ||||
| // This script demonstrates how to use call_contract_read and call_contract_write with arguments | ||||
|  | ||||
| // Step 1: Set up wallet and network | ||||
| let space_name = "agung_contract_args_demo"; | ||||
| let password = "secure_password123"; | ||||
| let private_key = "51c194d20bcd25360a3aa94426b3b60f738007e42f22e1bc97821c65c353e6d2"; | ||||
| let network_name = "agung"; | ||||
|  | ||||
| print("=== Testing Contract Functions With Arguments on Agung Network ===\n"); | ||||
|  | ||||
| // Create a key space | ||||
| print("Creating key space: " + space_name); | ||||
| if create_key_space(space_name, password) { | ||||
|     print("✓ Key space created successfully"); | ||||
|  | ||||
|     // Create a keypair | ||||
|     print("\nCreating keypair..."); | ||||
|     if create_keypair("contract_key", password) { | ||||
|         print("✓ Created contract keypair"); | ||||
|  | ||||
|         // Create a wallet from the private key for the Agung network | ||||
|         print("\nCreating wallet from private key for Agung network..."); | ||||
|         if create_wallet_from_private_key_for_network(private_key, network_name) { | ||||
|             print("✓ Wallet created successfully"); | ||||
|  | ||||
|             // Get the wallet address | ||||
|             let wallet_address = get_wallet_address_for_network(network_name); | ||||
|             print("Wallet address: " + wallet_address); | ||||
|  | ||||
|             // Check wallet balance | ||||
|             print("\nChecking wallet balance..."); | ||||
|             let balance = get_balance(network_name, wallet_address); | ||||
|             if balance != "" { | ||||
|                 print("Wallet balance: " + balance + " wei"); | ||||
|  | ||||
|                 // Define a simple ERC-20 token contract ABI (partial) | ||||
|                 let token_abi = `[ | ||||
|                     { | ||||
|                         "constant": true, | ||||
|                         "inputs": [], | ||||
|                         "name": "name", | ||||
|                         "outputs": [{"name": "", "type": "string"}], | ||||
|                         "payable": false, | ||||
|                         "stateMutability": "view", | ||||
|                         "type": "function" | ||||
|                     }, | ||||
|                     { | ||||
|                         "constant": true, | ||||
|                         "inputs": [], | ||||
|                         "name": "symbol", | ||||
|                         "outputs": [{"name": "", "type": "string"}], | ||||
|                         "payable": false, | ||||
|                         "stateMutability": "view", | ||||
|                         "type": "function" | ||||
|                     }, | ||||
|                     { | ||||
|                         "constant": true, | ||||
|                         "inputs": [], | ||||
|                         "name": "decimals", | ||||
|                         "outputs": [{"name": "", "type": "uint8"}], | ||||
|                         "payable": false, | ||||
|                         "stateMutability": "view", | ||||
|                         "type": "function" | ||||
|                     }, | ||||
|                     { | ||||
|                         "constant": true, | ||||
|                         "inputs": [{"name": "_owner", "type": "address"}], | ||||
|                         "name": "balanceOf", | ||||
|                         "outputs": [{"name": "balance", "type": "uint256"}], | ||||
|                         "payable": false, | ||||
|                         "stateMutability": "view", | ||||
|                         "type": "function" | ||||
|                     }, | ||||
|                     { | ||||
|                         "constant": false, | ||||
|                         "inputs": [{"name": "_to", "type": "address"}, {"name": "_value", "type": "uint256"}], | ||||
|                         "name": "transfer", | ||||
|                         "outputs": [{"name": "", "type": "bool"}], | ||||
|                         "payable": false, | ||||
|                         "stateMutability": "nonpayable", | ||||
|                         "type": "function" | ||||
|                     } | ||||
|                 ]`; | ||||
|  | ||||
|                 // For this example, we'll use a test token contract on Agung | ||||
|                 let token_address = "0x7267B587E4416537060C6bF0B06f6Fd421106650"; | ||||
|  | ||||
|                 print("\nLoading contract ABI..."); | ||||
|                 let contract = load_contract_abi(network_name, token_address, token_abi); | ||||
|  | ||||
|                 if contract != "" { | ||||
|                     print("✓ Contract loaded successfully"); | ||||
|  | ||||
|                     // First, let's try to read some data from the contract | ||||
|                     print("\nReading contract data..."); | ||||
|  | ||||
|                     // Try to get token name (no arguments) | ||||
|                     let token_name = call_contract_read(contract, "name"); | ||||
|                     print("Token name: " + token_name); | ||||
|  | ||||
|                     // Try to get token symbol (no arguments) | ||||
|                     let token_symbol = call_contract_read(contract, "symbol"); | ||||
|                     print("Token symbol: " + token_symbol); | ||||
|  | ||||
|                     // Try to get token decimals (no arguments) | ||||
|                     let token_decimals = call_contract_read(contract, "decimals"); | ||||
|                     print("Token decimals: " + token_decimals); | ||||
|  | ||||
|                     // Try to get token balance (with address argument) | ||||
|                     print("\nCalling balanceOf with address argument..."); | ||||
|                     let balance = call_contract_read(contract, "balanceOf", [wallet_address]); | ||||
|                     print("Token balance: " + balance); | ||||
|  | ||||
|                     // Now, let's try to execute a write function with arguments | ||||
|                     print("\nExecuting contract write function with arguments..."); | ||||
|  | ||||
|                     // Define a recipient address and amount for the transfer | ||||
|                     // Using a random valid address on the network | ||||
|                     let recipient = "0xEEdf3468E8F232A7a03D49b674bA44740C8BD8Be"; | ||||
|                     let amount = 1000000; // Changed from string to number for uint256 compatibility | ||||
|  | ||||
|                     print("Attempting to transfer " + amount + " tokens to " + recipient); | ||||
|  | ||||
|                     // Call the transfer function with arguments | ||||
|                     let tx_hash = call_contract_write(contract, "transfer", [recipient, amount]); | ||||
|  | ||||
|                     if tx_hash != "" { | ||||
|                         print("✓ Transaction sent successfully"); | ||||
|                         print("Transaction hash: " + tx_hash); | ||||
|                         print("You can view the transaction at: " + get_network_explorer_url(network_name) + "/tx/" + tx_hash); | ||||
|                     } else { | ||||
|                         print("✗ Failed to send transaction"); | ||||
|                         print("This could be due to insufficient funds, contract issues, or other errors."); | ||||
|                     } | ||||
|                 } else { | ||||
|                     print("✗ Failed to load contract"); | ||||
|                 } | ||||
|             } else { | ||||
|                 print("✗ Failed to get wallet balance"); | ||||
|             } | ||||
|         } else { | ||||
|             print("✗ Failed to create wallet from private key"); | ||||
|         } | ||||
|     } else { | ||||
|         print("✗ Failed to create keypair"); | ||||
|     } | ||||
| } else { | ||||
|     print("✗ Failed to create key space"); | ||||
| } | ||||
|  | ||||
| print("\nContract function with arguments test completed"); | ||||
							
								
								
									
										104
									
								
								examples/hero_vault/agung_send_transaction.rhai
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										104
									
								
								examples/hero_vault/agung_send_transaction.rhai
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,104 @@ | ||||
| // Script to create an Agung wallet from a private key and send tokens | ||||
| // This script demonstrates how to create a wallet from a private key and send tokens | ||||
|  | ||||
| // Define the private key and recipient address | ||||
| let private_key = "0x9ecfd58eca522b0e7c109bf945966ee208cd6d593b1dc3378aedfdc60b64f512"; | ||||
| let recipient_address = "0xf400f9c3F7317e19523a5DB698Ce67e7a7E083e2"; | ||||
|  | ||||
| print("=== Agung Wallet Transaction Demo ==="); | ||||
| print(`From private key: ${private_key}`); | ||||
| print(`To address: ${recipient_address}`); | ||||
|  | ||||
| // First, create a key space and keypair (required for the wallet infrastructure) | ||||
| let space_name = "agung_transaction_demo"; | ||||
| let password = "demo_password"; | ||||
|  | ||||
| // Create a new key space | ||||
| if !create_key_space(space_name, password) { | ||||
|     print("Failed to create key space"); | ||||
|     return; | ||||
| } | ||||
|  | ||||
| // Create a keypair | ||||
| if !create_keypair("demo_keypair", password) { | ||||
|     print("Failed to create keypair"); | ||||
|     return; | ||||
| } | ||||
|  | ||||
| // Select the keypair | ||||
| if !select_keypair("demo_keypair") { | ||||
|     print("Failed to select keypair"); | ||||
|     return; | ||||
| } | ||||
|  | ||||
| print("\nCreated and selected keypair successfully"); | ||||
|  | ||||
| // Clear any existing Agung wallets to avoid conflicts | ||||
| if clear_wallets_for_network("agung") { | ||||
|     print("Cleared existing Agung wallets"); | ||||
| } else { | ||||
|     print("Failed to clear existing Agung wallets"); | ||||
|     return; | ||||
| } | ||||
|  | ||||
| // Create a wallet from the private key directly | ||||
| print("\n=== Creating Wallet from Private Key ==="); | ||||
|  | ||||
| // Create a wallet from the private key for the Agung network | ||||
| if create_wallet_from_private_key_for_network(private_key, "agung") { | ||||
|     print("Successfully created wallet from private key for Agung network"); | ||||
|      | ||||
|     // Get the wallet address | ||||
|     let wallet_address = get_wallet_address_for_network("agung"); | ||||
|     print(`Wallet address: ${wallet_address}`); | ||||
|      | ||||
|     // Create a provider for the Agung network | ||||
|     let provider_id = create_agung_provider(); | ||||
|     if provider_id != "" { | ||||
|         print("Successfully created Agung provider"); | ||||
|          | ||||
|         // Check the wallet balance first | ||||
|         let wallet_address = get_wallet_address_for_network("agung"); | ||||
|         let balance_wei = get_balance("agung", wallet_address); | ||||
|          | ||||
|         if balance_wei == "" { | ||||
|             print("Failed to get wallet balance"); | ||||
|             print("This could be due to network issues or other errors."); | ||||
|             return; | ||||
|         } | ||||
|          | ||||
|         print(`Current wallet balance: ${balance_wei} wei`); | ||||
|          | ||||
|         // Convert 1 AGNG to wei (1 AGNG = 10^18 wei) | ||||
|         // Use string representation for large numbers | ||||
|         let amount_wei_str = "1000000000000000000"; // 1 AGNG in wei as a string | ||||
|          | ||||
|         // Check if we have enough balance | ||||
|         if parse_int(balance_wei) < parse_int(amount_wei_str) { | ||||
|             print(`Insufficient balance to send ${amount_wei_str} wei (1 AGNG)`); | ||||
|             print(`Current balance: ${balance_wei} wei`); | ||||
|             print("Please fund the wallet before attempting to send a transaction"); | ||||
|             return; | ||||
|         } | ||||
|          | ||||
|         print(`Attempting to send ${amount_wei_str} wei (1 AGNG) to ${recipient_address}`); | ||||
|          | ||||
|         // Send the transaction using the blocking implementation | ||||
|         let tx_hash = send_eth("agung", recipient_address, amount_wei_str); | ||||
|          | ||||
|         if tx_hash != "" { | ||||
|             print(`Transaction sent with hash: ${tx_hash}`); | ||||
|             print(`You can view the transaction at: ${get_network_explorer_url("agung")}/tx/${tx_hash}`); | ||||
|         } else { | ||||
|             print("Transaction failed"); | ||||
|             print("This could be due to insufficient funds, network issues, or other errors."); | ||||
|             print("Check the logs for more details."); | ||||
|         } | ||||
|     } else { | ||||
|         print("Failed to create Agung provider"); | ||||
|     } | ||||
| } else { | ||||
|     print("Failed to create wallet from private key"); | ||||
| } | ||||
|  | ||||
| print("\nAgung transaction demo completed"); | ||||
							
								
								
									
										98
									
								
								examples/hero_vault/contract_example.rhai
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								examples/hero_vault/contract_example.rhai
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,98 @@ | ||||
| // Example Rhai script for interacting with smart contracts using Hero Vault | ||||
| // This script demonstrates loading a contract ABI and interacting with a contract | ||||
|  | ||||
| // Step 1: Set up wallet and network | ||||
| let space_name = "contract_demo_space"; | ||||
| let password = "secure_password123"; | ||||
|  | ||||
| print("Creating key space: " + space_name); | ||||
| if create_key_space(space_name, password) { | ||||
|     print("✓ Key space created successfully"); | ||||
|  | ||||
|     // Create a keypair | ||||
|     print("\nCreating keypair..."); | ||||
|     if create_keypair("contract_key", password) { | ||||
|         print("✓ Created contract keypair"); | ||||
|     } | ||||
|  | ||||
|     // Step 2: Create an Ethereum wallet for Gnosis Chain | ||||
|     print("\nCreating Ethereum wallet..."); | ||||
|     if create_ethereum_wallet() { | ||||
|         print("✓ Ethereum wallet created"); | ||||
|  | ||||
|         let address = get_ethereum_address(); | ||||
|         print("Ethereum address: " + address); | ||||
|  | ||||
|         // Step 3: Define a simple ERC-20 ABI (partial) | ||||
|         let erc20_abi = `[ | ||||
|             { | ||||
|                 "constant": true, | ||||
|                 "inputs": [], | ||||
|                 "name": "name", | ||||
|                 "outputs": [{"name": "", "type": "string"}], | ||||
|                 "payable": false, | ||||
|                 "stateMutability": "view", | ||||
|                 "type": "function" | ||||
|             }, | ||||
|             { | ||||
|                 "constant": true, | ||||
|                 "inputs": [], | ||||
|                 "name": "symbol", | ||||
|                 "outputs": [{"name": "", "type": "string"}], | ||||
|                 "payable": false, | ||||
|                 "stateMutability": "view", | ||||
|                 "type": "function" | ||||
|             }, | ||||
|             { | ||||
|                 "constant": true, | ||||
|                 "inputs": [], | ||||
|                 "name": "decimals", | ||||
|                 "outputs": [{"name": "", "type": "uint8"}], | ||||
|                 "payable": false, | ||||
|                 "stateMutability": "view", | ||||
|                 "type": "function" | ||||
|             }, | ||||
|             { | ||||
|                 "constant": true, | ||||
|                 "inputs": [{"name": "owner", "type": "address"}], | ||||
|                 "name": "balanceOf", | ||||
|                 "outputs": [{"name": "", "type": "uint256"}], | ||||
|                 "payable": false, | ||||
|                 "stateMutability": "view", | ||||
|                 "type": "function" | ||||
|             } | ||||
|         ]`; | ||||
|  | ||||
|         // Step 4: Load the contract ABI | ||||
|         print("\nLoading contract ABI..."); | ||||
|         let contract = load_contract_abi("Gnosis", "0x4ECaBa5870353805a9F068101A40E0f32ed605C6", erc20_abi); | ||||
|         if contract != "" { | ||||
|             print("✓ Contract loaded successfully"); | ||||
|  | ||||
|             // Step 5: Call read-only functions | ||||
|             print("\nCalling read-only functions..."); | ||||
|  | ||||
|             // Get token name | ||||
|             let token_name = call_contract_read(contract, "name"); | ||||
|             print("Token name: " + token_name); | ||||
|  | ||||
|             // Get token symbol | ||||
|             let token_symbol = call_contract_read(contract, "symbol"); | ||||
|             print("Token symbol: " + token_symbol); | ||||
|  | ||||
|             // Get token decimals | ||||
|             let token_decimals = call_contract_read(contract, "decimals"); | ||||
|             print("Token decimals: " + token_decimals); | ||||
|  | ||||
|             // For now, we're just demonstrating the basic structure | ||||
|         } else { | ||||
|             print("✗ Failed to load contract"); | ||||
|         } | ||||
|     } else { | ||||
|         print("✗ Failed to create Ethereum wallet"); | ||||
|     } | ||||
| } else { | ||||
|     print("✗ Failed to create key space"); | ||||
| } | ||||
|  | ||||
| print("\nContract example completed"); | ||||
							
								
								
									
										85
									
								
								examples/hero_vault/example.rhai
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								examples/hero_vault/example.rhai
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,85 @@ | ||||
| // Example Rhai script for Hero Vault Cryptography Module | ||||
| // This script demonstrates key management, signing, and encryption | ||||
|  | ||||
| // Step 1: Create and manage a key space | ||||
| let space_name = "demo_space"; | ||||
| let password = "secure_password123"; | ||||
|  | ||||
| print("Creating key space: " + space_name); | ||||
| if create_key_space(space_name, password) { | ||||
|     print("✓ Key space created successfully"); | ||||
|      | ||||
|     // Step 2: Create and use keypairs | ||||
|     print("\nCreating keypairs..."); | ||||
|     if create_keypair("signing_key", password) { | ||||
|         print("✓ Created signing keypair"); | ||||
|     } | ||||
|      | ||||
|     if create_keypair("encryption_key", password) { | ||||
|         print("✓ Created encryption keypair"); | ||||
|     } | ||||
|      | ||||
|     // List all keypairs | ||||
|     let keypairs = list_keypairs(); | ||||
|     print("Available keypairs: " + keypairs); | ||||
|      | ||||
|     // Step 3: Sign a message | ||||
|     print("\nPerforming signing operations..."); | ||||
|     if select_keypair("signing_key") { | ||||
|         print("✓ Selected signing keypair"); | ||||
|          | ||||
|         let message = "This is a secure message that needs to be signed"; | ||||
|         print("Message: " + message); | ||||
|          | ||||
|         let signature = sign(message); | ||||
|         print("Signature: " + signature); | ||||
|          | ||||
|         // Verify the signature | ||||
|         let is_valid = verify(message, signature); | ||||
|         if is_valid { | ||||
|             print("Signature verification: ✓ Valid"); | ||||
|         } else { | ||||
|             print("Signature verification: ✗ Invalid"); | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     // Step 4: Encrypt and decrypt data | ||||
|     print("\nPerforming encryption operations..."); | ||||
|      | ||||
|     // Generate a symmetric key | ||||
|     let sym_key = generate_key(); | ||||
|     print("Generated symmetric key: " + sym_key); | ||||
|      | ||||
|     // Encrypt a message | ||||
|     let secret = "This is a top secret message that must be encrypted"; | ||||
|     print("Original message: " + secret); | ||||
|      | ||||
|     let encrypted_data = encrypt(sym_key, secret); | ||||
|     print("Encrypted data: " + encrypted_data); | ||||
|      | ||||
|     // Decrypt the message | ||||
|     let decrypted_data = decrypt(sym_key, encrypted_data); | ||||
|     print("Decrypted message: " + decrypted_data); | ||||
|      | ||||
|     // Verify decryption was successful | ||||
|     if decrypted_data == secret { | ||||
|         print("✓ Encryption/decryption successful"); | ||||
|     } else { | ||||
|         print("✗ Encryption/decryption failed"); | ||||
|     } | ||||
|      | ||||
|     // Step 5: Create an Ethereum wallet | ||||
|     print("\nCreating Ethereum wallet..."); | ||||
|     if select_keypair("encryption_key") { | ||||
|         print("✓ Selected keypair for Ethereum wallet"); | ||||
|          | ||||
|         if create_ethereum_wallet() { | ||||
|             print("✓ Ethereum wallet created"); | ||||
|              | ||||
|             let address = get_ethereum_address(); | ||||
|             print("Ethereum address: " + address); | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     print("\nScript execution completed successfully!"); | ||||
| } | ||||
							
								
								
									
										65
									
								
								examples/hero_vault/key_persistence_example.rhai
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								examples/hero_vault/key_persistence_example.rhai
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,65 @@ | ||||
| // Example Rhai script demonstrating key space persistence for Hero Vault | ||||
| // This script shows how to create, save, and load key spaces | ||||
|  | ||||
| // Step 1: Create a key space | ||||
| let space_name = "persistent_space"; | ||||
| let password = "secure_password123"; | ||||
|  | ||||
| print("Creating key space: " + space_name); | ||||
| if create_key_space(space_name, password) { | ||||
|     print("✓ Key space created successfully"); | ||||
|      | ||||
|     // Step 2: Create keypairs in this space | ||||
|     print("\nCreating keypairs..."); | ||||
|     if create_keypair("persistent_key1", password) { | ||||
|         print("✓ Created first keypair"); | ||||
|     } | ||||
|      | ||||
|     if create_keypair("persistent_key2", password) { | ||||
|         print("✓ Created second keypair"); | ||||
|     } | ||||
|      | ||||
|     // List all keypairs | ||||
|     let keypairs = list_keypairs(); | ||||
|     print("Available keypairs: " + keypairs); | ||||
|      | ||||
|     // Step 3: Clear the session (simulate closing and reopening the CLI) | ||||
|     print("\nClearing session (simulating restart)..."); | ||||
|     // Note: In a real script, you would exit here and run a new script | ||||
|     // For demonstration purposes, we'll continue in the same script | ||||
|      | ||||
|     // Step 4: Load the key space from disk | ||||
|     print("\nLoading key space from disk..."); | ||||
|     if load_key_space(space_name, password) { | ||||
|         print("✓ Key space loaded successfully"); | ||||
|          | ||||
|         // Verify the keypairs are still available | ||||
|         let loaded_keypairs = list_keypairs(); | ||||
|         print("Keypairs after loading: " + loaded_keypairs); | ||||
|          | ||||
|         // Step 5: Use a keypair from the loaded space | ||||
|         print("\nSelecting and using a keypair..."); | ||||
|         if select_keypair("persistent_key1") { | ||||
|             print("✓ Selected keypair"); | ||||
|              | ||||
|             let message = "This message was signed using a keypair from a loaded key space"; | ||||
|             let signature = sign(message); | ||||
|             print("Message: " + message); | ||||
|             print("Signature: " + signature); | ||||
|              | ||||
|             // Verify the signature | ||||
|             let is_valid = verify(message, signature); | ||||
|             if is_valid { | ||||
|                 print("Signature verification: ✓ Valid"); | ||||
|             } else { | ||||
|                 print("Signature verification: ✗ Invalid"); | ||||
|             } | ||||
|         } | ||||
|     } else { | ||||
|         print("✗ Failed to load key space"); | ||||
|     } | ||||
| } else { | ||||
|     print("✗ Failed to create key space"); | ||||
| } | ||||
|  | ||||
| print("\nScript execution completed!"); | ||||
							
								
								
									
										65
									
								
								examples/hero_vault/load_existing_space.rhai
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								examples/hero_vault/load_existing_space.rhai
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,65 @@ | ||||
| // Example Rhai script demonstrating loading an existing key space for Hero Vault | ||||
| // This script shows how to load a previously created key space and use its keypairs | ||||
|  | ||||
| // Define the key space name and password | ||||
| let space_name = "persistent_space"; | ||||
| let password = "secure_password123"; | ||||
|  | ||||
| print("Loading existing key space: " + space_name); | ||||
|  | ||||
| // Load the key space from disk | ||||
| if load_key_space(space_name, password) { | ||||
|     print("✓ Key space loaded successfully"); | ||||
|      | ||||
|     // List available keypairs | ||||
|     let keypairs = list_keypairs(); | ||||
|     print("Available keypairs: " + keypairs); | ||||
|      | ||||
|     // Use both keypairs to sign different messages | ||||
|     if select_keypair("persistent_key1") { | ||||
|         print("\nUsing persistent_key1:"); | ||||
|         let message1 = "Message signed with the first keypair"; | ||||
|         let signature1 = sign(message1); | ||||
|         print("Message: " + message1); | ||||
|         print("Signature: " + signature1); | ||||
|          | ||||
|         let is_valid1 = verify(message1, signature1); | ||||
|         if is_valid1 { | ||||
|             print("Verification: ✓ Valid"); | ||||
|         } else { | ||||
|             print("Verification: ✗ Invalid"); | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     if select_keypair("persistent_key2") { | ||||
|         print("\nUsing persistent_key2:"); | ||||
|         let message2 = "Message signed with the second keypair"; | ||||
|         let signature2 = sign(message2); | ||||
|         print("Message: " + message2); | ||||
|         print("Signature: " + signature2); | ||||
|          | ||||
|         let is_valid2 = verify(message2, signature2); | ||||
|         if is_valid2 { | ||||
|             print("Verification: ✓ Valid"); | ||||
|         } else { | ||||
|             print("Verification: ✗ Invalid"); | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     // Create an Ethereum wallet using one of the keypairs | ||||
|     print("\nCreating Ethereum wallet from persistent keypair:"); | ||||
|     if select_keypair("persistent_key1") { | ||||
|         if create_ethereum_wallet() { | ||||
|             print("✓ Ethereum wallet created"); | ||||
|              | ||||
|             let address = get_ethereum_address(); | ||||
|             print("Ethereum address: " + address); | ||||
|         } else { | ||||
|             print("✗ Failed to create Ethereum wallet"); | ||||
|         } | ||||
|     } | ||||
| } else { | ||||
|     print("✗ Failed to load key space. Make sure you've run key_persistence_example.rhai first."); | ||||
| } | ||||
|  | ||||
| print("\nScript execution completed!"); | ||||
							
								
								
									
										133
									
								
								examples/mycelium/mycelium_basic.rhai
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										133
									
								
								examples/mycelium/mycelium_basic.rhai
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,133 @@ | ||||
| // Basic example of using the Mycelium client in Rhai | ||||
|  | ||||
| // API URL for Mycelium | ||||
| let api_url = "http://localhost:8989"; | ||||
|  | ||||
| // Get node information | ||||
| print("Getting node information:"); | ||||
| try { | ||||
|     let node_info = mycelium_get_node_info(api_url); | ||||
|     print(`Node subnet: ${node_info.nodeSubnet}`); | ||||
|     print(`Node public key: ${node_info.nodePubkey}`); | ||||
| } catch(err) { | ||||
|     print(`Error getting node info: ${err}`); | ||||
| } | ||||
|  | ||||
| // List all peers | ||||
| print("\nListing all peers:"); | ||||
| try { | ||||
|     let peers = mycelium_list_peers(api_url); | ||||
|      | ||||
|     if peers.is_empty() { | ||||
|         print("No peers connected."); | ||||
|     } else { | ||||
|         for peer in peers { | ||||
|             print(`Peer Endpoint: ${peer.endpoint.proto}://${peer.endpoint.socketAddr}`); | ||||
|             print(`  Type: ${peer.type}`); | ||||
|             print(`  Connection State: ${peer.connectionState}`); | ||||
|             print(`  Bytes sent: ${peer.txBytes}`); | ||||
|             print(`  Bytes received: ${peer.rxBytes}`); | ||||
|         } | ||||
|     } | ||||
| } catch(err) { | ||||
|     print(`Error listing peers: ${err}`); | ||||
| } | ||||
|  | ||||
| // Add a new peer | ||||
| print("\nAdding a new peer:"); | ||||
| let new_peer_address = "tcp://65.21.231.58:9651"; | ||||
| try { | ||||
|     let result = mycelium_add_peer(api_url, new_peer_address); | ||||
|     print(`Peer added: ${result.success}`); | ||||
| } catch(err) { | ||||
|     print(`Error adding peer: ${err}`); | ||||
| } | ||||
|  | ||||
| // List selected routes | ||||
| print("\nListing selected routes:"); | ||||
| try { | ||||
|     let routes = mycelium_list_selected_routes(api_url); | ||||
|      | ||||
|     if routes.is_empty() { | ||||
|         print("No selected routes."); | ||||
|     } else { | ||||
|         for route in routes { | ||||
|             print(`Subnet: ${route.subnet}`); | ||||
|             print(`  Next hop: ${route.nextHop}`); | ||||
|             print(`  Metric: ${route.metric}`); | ||||
|         } | ||||
|     } | ||||
| } catch(err) { | ||||
|     print(`Error listing routes: ${err}`); | ||||
| } | ||||
|  | ||||
| // List fallback routes | ||||
| print("\nListing fallback routes:"); | ||||
| try { | ||||
|     let routes = mycelium_list_fallback_routes(api_url); | ||||
|      | ||||
|     if routes.is_empty() { | ||||
|         print("No fallback routes."); | ||||
|     } else { | ||||
|         for route in routes { | ||||
|             print(`Subnet: ${route.subnet}`); | ||||
|             print(`  Next hop: ${route.nextHop}`); | ||||
|             print(`  Metric: ${route.metric}`); | ||||
|         } | ||||
|     } | ||||
| } catch(err) { | ||||
|     print(`Error listing fallback routes: ${err}`); | ||||
| } | ||||
|  | ||||
| // Send a message | ||||
| // TO SEND A MESSAGE FILL IN THE DESTINATION IP ADDRESS | ||||
| // -----------------------------------------------------// | ||||
| // print("\nSending a message:"); | ||||
| // let destination = < FILL IN CORRECT DEST IP > | ||||
| // let topic = "test"; | ||||
| // let message = "Hello from Rhai!"; | ||||
| // let deadline_secs = 60; | ||||
|  | ||||
| // try { | ||||
| //     let result = mycelium_send_message(api_url, destination, topic, message, deadline_secs); | ||||
| //     print(`Message sent: ${result.success}`); | ||||
| //     if result.id { | ||||
| //         print(`Message ID: ${result.id}`); | ||||
| //     } | ||||
| // } catch(err) { | ||||
| //     print(`Error sending message: ${err}`); | ||||
| // } | ||||
|  | ||||
| // Receive messages | ||||
| // RECEIVING MESSAGES SHOULD BE DONE ON THE DESTINATION NODE FROM THE CALL ABOVE | ||||
| // -----------------------------------------------------------------------------// | ||||
| // print("\nReceiving messages:"); | ||||
| // let receive_topic = "test"; | ||||
| // let count = 5; | ||||
|  | ||||
| // try { | ||||
| //     let messages = mycelium_receive_messages(api_url, receive_topic, count); | ||||
|      | ||||
| //     if messages.is_empty() { | ||||
| //         print("No messages received."); | ||||
| //     } else { | ||||
| //         for msg in messages { | ||||
| //             print(`Message from: ${msg.source}`); | ||||
| //             print(`  Topic: ${msg.topic}`); | ||||
| //             print(`  Content: ${msg.content}`); | ||||
| //             print(`  Timestamp: ${msg.timestamp}`); | ||||
| //         } | ||||
| //     } | ||||
| // } catch(err) { | ||||
| //     print(`Error receiving messages: ${err}`); | ||||
| // } | ||||
|  | ||||
| // Remove a peer | ||||
| print("\nRemoving a peer:"); | ||||
| let peer_id = "tcp://65.21.231.58:9651"; // This is the peer we added earlier | ||||
| try { | ||||
|     let result = mycelium_remove_peer(api_url, peer_id); | ||||
|     print(`Peer removed: ${result.success}`); | ||||
| } catch(err) { | ||||
|     print(`Error removing peer: ${err}`); | ||||
| } | ||||
							
								
								
									
										31
									
								
								examples/mycelium/mycelium_receive_message.rhai
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								examples/mycelium/mycelium_receive_message.rhai
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | ||||
| // Script to receive Mycelium messages | ||||
|  | ||||
| // API URL for Mycelium | ||||
| let api_url = "http://localhost:2222"; | ||||
|  | ||||
| // Receive messages | ||||
| // This script will listen for messages on a specific topic. | ||||
| // Ensure the sender script is using the same topic. | ||||
| // -----------------------------------------------------------------------------// | ||||
| print("\nReceiving messages:"); | ||||
| let receive_topic = "test_topic"; | ||||
| let wait_deadline_secs = 100;  | ||||
|  | ||||
| print(`Listening for messages on topic '${receive_topic}'...`); | ||||
| try { | ||||
|     let messages = mycelium_receive_messages(api_url, receive_topic, wait_deadline_secs); | ||||
|      | ||||
|     if messages.is_empty() { | ||||
|         // print("No new messages received in this poll."); | ||||
|     } else { | ||||
|         print("Received a message:"); | ||||
|         print(`  Message id: ${messages.id}`); | ||||
|         print(`  Message from: ${messages.srcIp}`); | ||||
|         print(`  Topic: ${messages.topic}`); | ||||
|         print(`  Payload: ${messages.payload}`); | ||||
|     } | ||||
| } catch(err) { | ||||
|     print(`Error receiving messages: ${err}`); | ||||
| } | ||||
|  | ||||
| print("Finished attempting to receive messages."); | ||||
							
								
								
									
										25
									
								
								examples/mycelium/mycelium_send_message.rhai
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								examples/mycelium/mycelium_send_message.rhai
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| // Script to send a Mycelium message | ||||
|  | ||||
| // API URL for Mycelium | ||||
| let api_url = "http://localhost:1111"; | ||||
|  | ||||
| // Send a message | ||||
| // TO SEND A MESSAGE FILL IN THE DESTINATION IP ADDRESS | ||||
| // -----------------------------------------------------// | ||||
| print("\nSending a message:"); | ||||
| let destination = "50e:6d75:4568:366e:f75:2ac3:bbb1:3fdd"; // IMPORTANT: Replace with the actual destination IP address | ||||
| let topic = "test_topic"; | ||||
| let message = "Hello from Rhai sender!"; | ||||
| let deadline_secs = -10; // Seconds we wait for a reply | ||||
|  | ||||
| try { | ||||
|     print(`Attempting to send message to ${destination} on topic '${topic}'`); | ||||
|     let result = mycelium_send_message(api_url, destination, topic, message, deadline_secs); | ||||
|     print(`result: ${result}`); | ||||
|     print(`Message sent: ${result.success}`); | ||||
|     if result.id != "" { | ||||
|         print(`Message ID: ${result.id}`); | ||||
|     } | ||||
| } catch(err) { | ||||
|     print(`Error sending message: ${err}`); | ||||
| } | ||||
							
								
								
									
										83
									
								
								examples/network/network_connectivity.rhai
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								examples/network/network_connectivity.rhai
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,83 @@ | ||||
| // Example of using the network modules in SAL | ||||
| // Shows TCP port checking, HTTP URL validation, and SSH command execution | ||||
|  | ||||
| // Import system module for display | ||||
| import "os" as os; | ||||
|  | ||||
| // Function to print section header | ||||
| fn section(title) { | ||||
|     print("\n"); | ||||
|     print("==== " + title + " ===="); | ||||
|     print("\n"); | ||||
| } | ||||
|  | ||||
| // TCP connectivity checks | ||||
| section("TCP Connectivity"); | ||||
|  | ||||
| // Create a TCP connector | ||||
| let tcp = sal::net::TcpConnector::new(); | ||||
|  | ||||
| // Check if a port is open | ||||
| let host = "localhost"; | ||||
| let port = 22; | ||||
| print(`Checking if port ${port} is open on ${host}...`); | ||||
| let is_open = tcp.check_port(host, port); | ||||
| print(`Port ${port} is ${is_open ? "open" : "closed"}`); | ||||
|  | ||||
| // Check multiple ports | ||||
| let ports = [22, 80, 443]; | ||||
| print(`Checking multiple ports on ${host}...`); | ||||
| let port_results = tcp.check_ports(host, ports); | ||||
| for result in port_results { | ||||
|     print(`Port ${result.0} is ${result.1 ? "open" : "closed"}`); | ||||
| } | ||||
|  | ||||
| // HTTP connectivity checks | ||||
| section("HTTP Connectivity"); | ||||
|  | ||||
| // Create an HTTP connector | ||||
| let http = sal::net::HttpConnector::new(); | ||||
|  | ||||
| // Check if a URL is reachable | ||||
| let url = "https://www.example.com"; | ||||
| print(`Checking if ${url} is reachable...`); | ||||
| let is_reachable = http.check_url(url); | ||||
| print(`${url} is ${is_reachable ? "reachable" : "unreachable"}`); | ||||
|  | ||||
| // Check the status code of a URL | ||||
| print(`Checking status code of ${url}...`); | ||||
| let status = http.check_status(url); | ||||
| if status { | ||||
|     print(`Status code: ${status.unwrap()}`); | ||||
| } else { | ||||
|     print("Failed to get status code"); | ||||
| } | ||||
|  | ||||
| // Only attempt SSH if port 22 is open | ||||
| if is_open { | ||||
|     // SSH connectivity checks | ||||
|     section("SSH Connectivity"); | ||||
|      | ||||
|     // Create an SSH connection to localhost (if SSH server is running) | ||||
|     print("Attempting to connect to SSH server on localhost..."); | ||||
|      | ||||
|     // Using the builder pattern | ||||
|     let ssh = sal::net::SshConnectionBuilder::new() | ||||
|         .host("localhost") | ||||
|         .port(22) | ||||
|         .user(os::get_env("USER") || "root") | ||||
|         .build(); | ||||
|      | ||||
|     // Execute a simple command | ||||
|     print("Executing 'uname -a' command..."); | ||||
|     let result = ssh.execute("uname -a"); | ||||
|     if result.0 == 0 { | ||||
|         print("Command output:"); | ||||
|         print(result.1); | ||||
|     } else { | ||||
|         print(`Command failed with exit code: ${result.0}`); | ||||
|         print(result.1); | ||||
|     } | ||||
| } | ||||
|  | ||||
| print("\nNetwork connectivity checks completed."); | ||||
							
								
								
									
										82
									
								
								examples/network/network_rhai.rhai
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								examples/network/network_rhai.rhai
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,82 @@ | ||||
| // Example of using the network modules in SAL through Rhai | ||||
| // Shows TCP port checking, HTTP URL validation, and SSH command execution | ||||
|  | ||||
| // Function to print section header | ||||
| fn section(title) { | ||||
|     print("\n"); | ||||
|     print("==== " + title + " ===="); | ||||
|     print("\n"); | ||||
| } | ||||
|  | ||||
| // TCP connectivity checks | ||||
| section("TCP Connectivity"); | ||||
|  | ||||
| // Create a TCP connector | ||||
| let tcp = net::new_tcp_connector(); | ||||
|  | ||||
| // Check if a port is open | ||||
| let host = "localhost"; | ||||
| let port = 22; | ||||
| print(`Checking if port ${port} is open on ${host}...`); | ||||
| let is_open = tcp.check_port(host, port); | ||||
| print(`Port ${port} is ${is_open ? "open" : "closed"}`); | ||||
|  | ||||
| // Check multiple ports | ||||
| let ports = [22, 80, 443]; | ||||
| print(`Checking multiple ports on ${host}...`); | ||||
| let port_results = tcp.check_ports(host, ports); | ||||
| for result in port_results { | ||||
|     print(`Port ${result.port} is ${result.is_open ? "open" : "closed"}`); | ||||
| } | ||||
|  | ||||
| // HTTP connectivity checks | ||||
| section("HTTP Connectivity"); | ||||
|  | ||||
| // Create an HTTP connector | ||||
| let http = net::new_http_connector(); | ||||
|  | ||||
| // Check if a URL is reachable | ||||
| let url = "https://www.example.com"; | ||||
| print(`Checking if ${url} is reachable...`); | ||||
| let is_reachable = http.check_url(url); | ||||
| print(`${url} is ${is_reachable ? "reachable" : "unreachable"}`); | ||||
|  | ||||
| // Check the status code of a URL | ||||
| print(`Checking status code of ${url}...`); | ||||
| let status = http.check_status(url); | ||||
| if status != () { | ||||
|     print(`Status code: ${status}`); | ||||
| } else { | ||||
|     print("Failed to get status code"); | ||||
| } | ||||
|  | ||||
| // Get content from a URL | ||||
| print(`Getting content from ${url}...`); | ||||
| let content = http.get_content(url); | ||||
| print(`Content length: ${content.len()} characters`); | ||||
| print(`First 100 characters: ${content.substr(0, 100)}...`); | ||||
|  | ||||
| // Only attempt SSH if port 22 is open | ||||
| if is_open { | ||||
|     // SSH connectivity checks | ||||
|     section("SSH Connectivity"); | ||||
|      | ||||
|     // Create an SSH connection to localhost (if SSH server is running) | ||||
|     print("Attempting to connect to SSH server on localhost..."); | ||||
|      | ||||
|     // Using the builder pattern | ||||
|     let ssh = net::new_ssh_builder() | ||||
|         .host("localhost") | ||||
|         .port(22) | ||||
|         .user(os::get_env("USER") || "root") | ||||
|         .timeout(10) | ||||
|         .build(); | ||||
|      | ||||
|     // Execute a simple command | ||||
|     print("Executing 'uname -a' command..."); | ||||
|     let result = ssh.execute("uname -a"); | ||||
|     print(`Command exit code: ${result.code}`); | ||||
|     print(`Command output: ${result.output}`); | ||||
| } | ||||
|  | ||||
| print("\nNetwork connectivity checks completed."); | ||||
| @@ -2,7 +2,7 @@ print("Running a command using run().log().do()..."); | ||||
|  | ||||
| // The .log() method will print the command string to the console before execution. | ||||
| // This is useful for debugging or tracing which commands are being run. | ||||
| let result = run("echo This command is logged").log().do(); | ||||
| let result = run("echo This command is logged").log().execute(); | ||||
|  | ||||
| print(`Command finished.`); | ||||
| print(`Success: ${result.success}`); | ||||
|   | ||||
							
								
								
									
										78
									
								
								examples/zinit/zinit_basic.rhai
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								examples/zinit/zinit_basic.rhai
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,78 @@ | ||||
| // Basic example of using the Zinit client in Rhai | ||||
|  | ||||
| // Socket path for Zinit | ||||
| let socket_path = "/tmp/zinit.sock"; | ||||
|  | ||||
| // List all services | ||||
| print("Listing all services:"); | ||||
| let services = zinit_list(socket_path); | ||||
|  | ||||
| if services.is_empty() { | ||||
|     print("No services found."); | ||||
| } else { | ||||
|     // Iterate over the keys of the map | ||||
|     for name in services.keys() { | ||||
|         let state = services[name]; | ||||
|         print(`${name}: ${state}`); | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Get status of a specific service | ||||
| let service_name = "test"; | ||||
| print(`Getting status for ${service_name}:`); | ||||
|  | ||||
| try { | ||||
|     let status = zinit_status(socket_path, service_name); | ||||
|     print(`Service: ${status.name}`); | ||||
|     print(`PID: ${status.pid}`); | ||||
|     print(`State: ${status.state}`); | ||||
|     print(`Target: ${status.target}`); | ||||
|     print("Dependencies:"); | ||||
|      | ||||
|     for (dep, state) in status.after.keys() { | ||||
|         print(`  ${dep}: ${state}`); | ||||
|     } | ||||
| } catch(err) { | ||||
|     print(`Error getting status: ${err}`); | ||||
| } | ||||
|  | ||||
| // Create a new service | ||||
| print("\nCreating a new service:"); | ||||
| let new_service = "rhai-test-service"; | ||||
| let exec_command = "echo 'Hello from Rhai'"; | ||||
| let oneshot = true; | ||||
|  | ||||
| try { | ||||
|     let result = zinit_create_service(socket_path, new_service, exec_command, oneshot); | ||||
|     print(`Service created: ${result}`); | ||||
|      | ||||
|     // Monitor the service | ||||
|     print("\nMonitoring the service:"); | ||||
|     let monitor_result = zinit_monitor(socket_path, new_service); | ||||
|     print(`Service monitored: ${monitor_result}`); | ||||
|      | ||||
|     // Start the service | ||||
|     print("\nStarting the service:"); | ||||
|     let start_result = zinit_start(socket_path, new_service); | ||||
|     print(`Service started: ${start_result}`); | ||||
|      | ||||
|     // Get logs for a specific service | ||||
|     print("\nGetting logs:"); | ||||
|     let logs = zinit_logs(socket_path, new_service); | ||||
|      | ||||
|     for log in logs { | ||||
|         print(log); | ||||
|     } | ||||
|     // Clean up | ||||
|     print("\nCleaning up:"); | ||||
|     let stop_result = zinit_stop(socket_path, new_service); | ||||
|     print(`Service stopped: ${stop_result}`); | ||||
|      | ||||
|     let forget_result = zinit_forget(socket_path, new_service); | ||||
|     print(`Service forgotten: ${forget_result}`); | ||||
|      | ||||
|     let delete_result = zinit_delete_service(socket_path, new_service); | ||||
|     print(`Service deleted: ${delete_result}`); | ||||
| } catch(err) { | ||||
|     print(`Error: ${err}`); | ||||
| } | ||||
							
								
								
									
										41
									
								
								examples/zinit/zinit_basic2.rhai
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								examples/zinit/zinit_basic2.rhai
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | ||||
| // Basic example of using the Zinit client in Rhai | ||||
|  | ||||
| // Socket path for Zinit | ||||
| let socket_path = "/tmp/zinit.sock"; | ||||
|  | ||||
| // Create a new service | ||||
| print("\nCreating a new service:"); | ||||
| let new_service = "rhai-test-service"; | ||||
| let exec_command = "echo 'Hello from Rhai'"; | ||||
| let oneshot = true; | ||||
|  | ||||
| let result = zinit_create_service(socket_path, new_service, exec_command, oneshot); | ||||
| print(`Service created: ${result}`); | ||||
|  | ||||
| // Monitor the service | ||||
| print("\nMonitoring the service:"); | ||||
| let monitor_result = zinit_monitor(socket_path, new_service); | ||||
| print(`Service monitored: ${monitor_result}`); | ||||
|  | ||||
| // Start the service | ||||
| print("\nStarting the service:"); | ||||
| let start_result = zinit_start(socket_path, new_service); | ||||
| print(`Service started: ${start_result}`); | ||||
|  | ||||
| // Get logs for a specific service | ||||
| print("\nGetting logs:"); | ||||
| let logs = zinit_logs(socket_path, new_service); | ||||
|  | ||||
| for log in logs { | ||||
|     print(log); | ||||
| } | ||||
| // Clean up | ||||
| print("\nCleaning up:"); | ||||
| let stop_result = zinit_stop(socket_path, new_service); | ||||
| print(`Service stopped: ${stop_result}`); | ||||
|  | ||||
| let forget_result = zinit_forget(socket_path, new_service); | ||||
| print(`Service forgotten: ${forget_result}`); | ||||
|  | ||||
| let delete_result = zinit_delete_service(socket_path, new_service); | ||||
| print(`Service deleted: ${delete_result}`); | ||||
							
								
								
									
										21
									
								
								git/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								git/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| [package] | ||||
| name = "sal-git" | ||||
| version = "0.1.0" | ||||
| edition = "2021" | ||||
| authors = ["PlanetFirst <info@incubaid.com>"] | ||||
| description = "SAL Git - Git repository management and operations" | ||||
| repository = "https://git.threefold.info/herocode/sal" | ||||
| license = "Apache-2.0" | ||||
|  | ||||
| [dependencies] | ||||
| # Use workspace dependencies for consistency | ||||
| regex = { workspace = true } | ||||
| redis = { workspace = true } | ||||
| serde = { workspace = true } | ||||
| serde_json = { workspace = true } | ||||
| rhai = { workspace = true } | ||||
| log = { workspace = true } | ||||
| url = { workspace = true } | ||||
|  | ||||
| [dev-dependencies] | ||||
| tempfile = { workspace = true } | ||||
							
								
								
									
										116
									
								
								git/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								git/README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,116 @@ | ||||
| # SAL `git` Module | ||||
|  | ||||
| The `git` module in SAL provides comprehensive functionalities for interacting with Git repositories. It offers both high-level abstractions for common Git workflows and a flexible executor for running arbitrary Git commands with integrated authentication. | ||||
|  | ||||
| This module is central to SAL's capabilities for managing source code, enabling automation of development tasks, and integrating with version control systems. | ||||
|  | ||||
| ## Core Components | ||||
|  | ||||
| The module is primarily composed of two main parts: | ||||
|  | ||||
| 1.  **Repository and Tree Management (`git.rs`)**: Defines `GitTree` and `GitRepo` structs for a more structured, object-oriented approach to Git operations. | ||||
| 2.  **Command Execution with Authentication (`git_executor.rs`)**: Provides `GitExecutor` for running any Git command, with a focus on handling authentication via configurations stored in Redis. | ||||
|  | ||||
| ### 1. Repository and Tree Management (`GitTree` & `GitRepo`) | ||||
|  | ||||
| These components allow for programmatic management of Git repositories. | ||||
|  | ||||
| *   **`GitTree`**: Represents a directory (base path) that can contain multiple Git repositories. | ||||
|     *   `new(base_path)`: Creates a new `GitTree` instance for the given base path. | ||||
|     *   `list()`: Lists all Git repositories found under the base path. | ||||
|     *   `find(pattern)`: Finds repositories within the tree that match a given name pattern (supports wildcards). | ||||
|     *   `get(path_or_url)`: Retrieves `GitRepo` instances. If a local path/pattern is given, it finds existing repositories. If a Git URL is provided, it will clone the repository into a structured path (`base_path/server/account/repo`) if it doesn't already exist. | ||||
|  | ||||
| *   **`GitRepo`**: Represents a single Git repository. | ||||
|     *   `new(path)`: Creates a `GitRepo` instance for the repository at the given path. | ||||
|     *   `path()`: Returns the local file system path to the repository. | ||||
|     *   `has_changes()`: Checks if the repository has uncommitted local changes. | ||||
|     *   `pull()`: Pulls the latest changes from the remote. Fails if local changes exist. | ||||
|     *   `reset()`: Performs a hard reset (`git reset --hard HEAD`) and cleans untracked files (`git clean -fd`). | ||||
|     *   `commit(message)`: Stages all changes (`git add .`) and commits them with the given message. | ||||
|     *   `push()`: Pushes committed changes to the remote repository. | ||||
|  | ||||
| *   **`GitError`**: A comprehensive enum for errors related to `GitTree` and `GitRepo` operations (e.g., Git not installed, invalid URL, repository not found, local changes exist). | ||||
|  | ||||
| *   **`parse_git_url(url)`**: A utility function to parse HTTPS and SSH Git URLs into server, account, and repository name components. | ||||
|  | ||||
| ### 2. Command Execution with Authentication (`GitExecutor`) | ||||
|  | ||||
| `GitExecutor` is designed for flexible execution of any Git command, with a special emphasis on handling authentication for remote operations. | ||||
|  | ||||
| *   **`GitExecutor::new()` / `GitExecutor::default()`**: Creates a new executor instance. | ||||
| *   **`GitExecutor::init()`**: Initializes the executor by attempting to load authentication configurations from Redis (key: `herocontext:git`). If Redis is unavailable or the config is missing, it proceeds without specific auth configurations, relying on system defaults. | ||||
| *   **`GitExecutor::execute(args: &[&str])`**: The primary method to run a Git command (e.g., `executor.execute(&["clone", "https://github.com/user/repo.git", "myrepo"])`). | ||||
|     *   It intelligently attempts to apply authentication based on the command and the loaded configuration. | ||||
|  | ||||
| #### Authentication Configuration (`herocontext:git` in Redis) | ||||
|  | ||||
| The `GitExecutor` can load its authentication settings from a JSON object stored in Redis under the key `herocontext:git`. The structure is as follows: | ||||
|  | ||||
| ```json | ||||
| { | ||||
|   "status": "ok", // or "error" | ||||
|   "auth": { | ||||
|     "github.com": { | ||||
|       "sshagent": true // Use SSH agent for github.com | ||||
|     }, | ||||
|     "gitlab.example.com": { | ||||
|       "key": "/path/to/ssh/key_for_gitlab" // Use specific SSH key | ||||
|     }, | ||||
|     "dev.azure.com": { | ||||
|       "username": "your_username", | ||||
|       "password": "your_pat_or_password" // Use HTTPS credentials | ||||
|     } | ||||
|     // ... other server configurations | ||||
|   } | ||||
| } | ||||
| ``` | ||||
|  | ||||
| *   **Authentication Methods Supported**: | ||||
|     *   **SSH Agent**: If `sshagent: true` is set for a server, and an SSH agent is loaded with identities. | ||||
|     *   **SSH Key**: If `key: "/path/to/key"` is specified, `GIT_SSH_COMMAND` is used to point to this key. | ||||
|     *   **Username/Password (HTTPS)**: If `username` and `password` are provided, HTTPS URLs are rewritten to include these credentials (e.g., `https://user:pass@server/repo.git`). | ||||
|  | ||||
| *   **`GitExecutorError`**: An enum for errors specific to `GitExecutor`, including command failures, Redis errors, JSON parsing issues, and authentication problems (e.g., `SshAgentNotLoaded`, `InvalidAuthConfig`). | ||||
|  | ||||
| ## Usage with `herodo` | ||||
|  | ||||
| The `herodo` CLI tool likely leverages `GitExecutor` to provide its scriptable Git functionalities. This allows Rhai scripts executed by `herodo` to perform Git operations using the centrally managed authentication configurations from Redis, promoting secure and consistent access to Git repositories. | ||||
|  | ||||
| ## Error Handling | ||||
|  | ||||
| Both `git.rs` and `git_executor.rs` define their own specific error enums (`GitError` and `GitExecutorError` respectively) to provide detailed information about issues encountered during Git operations. These errors cover a wide range of scenarios from command execution failures to authentication problems and invalid configurations. | ||||
|  | ||||
| ## Configuration | ||||
|  | ||||
| The git module supports configuration through environment variables: | ||||
|  | ||||
| ### Environment Variables | ||||
|  | ||||
| - **`REDIS_URL`**: Redis connection URL (default: `redis://127.0.0.1/`) | ||||
| - **`SAL_REDIS_URL`**: Alternative Redis URL (fallback if REDIS_URL not set) | ||||
| - **`GIT_DEFAULT_BASE_PATH`**: Default base path for git operations (default: system temp directory) | ||||
|  | ||||
| ### Example Configuration | ||||
|  | ||||
| ```bash | ||||
| # Set Redis connection | ||||
| export REDIS_URL="redis://localhost:6379/0" | ||||
|  | ||||
| # Set default git base path | ||||
| export GIT_DEFAULT_BASE_PATH="/tmp/git_repos" | ||||
|  | ||||
| # Run your application | ||||
| herodo your_script.rhai | ||||
| ``` | ||||
|  | ||||
| ### Security Considerations | ||||
|  | ||||
| - Passwords are never embedded in URLs or logged | ||||
| - Temporary credential helpers are used for HTTPS authentication | ||||
| - Redis URLs with passwords are masked in logs | ||||
| - All temporary files are cleaned up after use | ||||
|  | ||||
| ## Summary | ||||
|  | ||||
| The `git` module offers a powerful and flexible interface to Git, catering to both simple, high-level repository interactions and complex, authenticated command execution scenarios. Its integration with Redis for authentication configuration makes it particularly well-suited for automated systems and tools like `herodo`. | ||||
| @@ -1,9 +1,9 @@ | ||||
| use std::process::Command; | ||||
| use std::path::Path; | ||||
| use std::fs; | ||||
| use regex::Regex; | ||||
| use std::fmt; | ||||
| use std::error::Error; | ||||
| use std::fmt; | ||||
| use std::fs; | ||||
| use std::path::Path; | ||||
| use std::process::Command; | ||||
| 
 | ||||
| // Define a custom error type for git operations
 | ||||
| #[derive(Debug)] | ||||
| @@ -35,7 +35,7 @@ impl fmt::Display for GitError { | ||||
|             GitError::CommandExecutionError(e) => write!(f, "Error executing command: {}", e), | ||||
|             GitError::NoRepositoriesFound => write!(f, "No repositories found"), | ||||
|             GitError::RepositoryNotFound(pattern) => write!(f, "No repositories found matching '{}'", pattern), | ||||
|             GitError::MultipleRepositoriesFound(pattern, count) => 
 | ||||
|             GitError::MultipleRepositoriesFound(pattern, count) => | ||||
|                 write!(f, "Multiple repositories ({}) found matching '{}'. Use '*' suffix for multiple matches.", count, pattern), | ||||
|             GitError::NotAGitRepository(path) => write!(f, "Not a git repository at {}", path), | ||||
|             GitError::LocalChangesExist(path) => write!(f, "Repository at {} has local changes", path), | ||||
| @@ -57,48 +57,48 @@ impl Error for GitError { | ||||
| } | ||||
| 
 | ||||
| /// Parses a git URL to extract the server, account, and repository name.
 | ||||
| /// 
 | ||||
| ///
 | ||||
| /// # Arguments
 | ||||
| /// 
 | ||||
| /// * `url` - The URL of the git repository to parse. Can be in HTTPS format 
 | ||||
| ///
 | ||||
| /// * `url` - The URL of the git repository to parse. Can be in HTTPS format
 | ||||
| ///   (https://github.com/username/repo.git) or SSH format (git@github.com:username/repo.git).
 | ||||
| /// 
 | ||||
| ///
 | ||||
| /// # Returns
 | ||||
| /// 
 | ||||
| ///
 | ||||
| /// A tuple containing:
 | ||||
| /// * `server` - The server name (e.g., "github.com")
 | ||||
| /// * `account` - The account or organization name (e.g., "username")
 | ||||
| /// * `repo` - The repository name (e.g., "repo")
 | ||||
| /// 
 | ||||
| ///
 | ||||
| /// If the URL cannot be parsed, all three values will be empty strings.
 | ||||
| pub fn parse_git_url(url: &str) -> (String, String, String) { | ||||
|     // HTTP(S) URL format: https://github.com/username/repo.git
 | ||||
|     let https_re = Regex::new(r"https?://([^/]+)/([^/]+)/([^/\.]+)(?:\.git)?").unwrap(); | ||||
|     
 | ||||
| 
 | ||||
|     // SSH URL format: git@github.com:username/repo.git
 | ||||
|     let ssh_re = Regex::new(r"git@([^:]+):([^/]+)/([^/\.]+)(?:\.git)?").unwrap(); | ||||
|     
 | ||||
| 
 | ||||
|     if let Some(caps) = https_re.captures(url) { | ||||
|         let server = caps.get(1).map_or("", |m| m.as_str()).to_string(); | ||||
|         let account = caps.get(2).map_or("", |m| m.as_str()).to_string(); | ||||
|         let repo = caps.get(3).map_or("", |m| m.as_str()).to_string(); | ||||
|         
 | ||||
| 
 | ||||
|         return (server, account, repo); | ||||
|     } else if let Some(caps) = ssh_re.captures(url) { | ||||
|         let server = caps.get(1).map_or("", |m| m.as_str()).to_string(); | ||||
|         let account = caps.get(2).map_or("", |m| m.as_str()).to_string(); | ||||
|         let repo = caps.get(3).map_or("", |m| m.as_str()).to_string(); | ||||
|         
 | ||||
| 
 | ||||
|         return (server, account, repo); | ||||
|     } | ||||
|     
 | ||||
| 
 | ||||
|     (String::new(), String::new(), String::new()) | ||||
| } | ||||
| 
 | ||||
| /// Checks if git is installed on the system.
 | ||||
| /// 
 | ||||
| ///
 | ||||
| /// # Returns
 | ||||
| /// 
 | ||||
| ///
 | ||||
| /// * `Ok(())` - If git is installed
 | ||||
| /// * `Err(GitError)` - If git is not installed
 | ||||
| fn check_git_installed() -> Result<(), GitError> { | ||||
| @@ -117,55 +117,53 @@ pub struct GitTree { | ||||
| 
 | ||||
| impl GitTree { | ||||
|     /// Creates a new GitTree with the specified base path.
 | ||||
|     /// 
 | ||||
|     ///
 | ||||
|     /// # Arguments
 | ||||
|     /// 
 | ||||
|     ///
 | ||||
|     /// * `base_path` - The base path where all git repositories are located
 | ||||
|     /// 
 | ||||
|     ///
 | ||||
|     /// # Returns
 | ||||
|     /// 
 | ||||
|     ///
 | ||||
|     /// * `Ok(GitTree)` - A new GitTree instance
 | ||||
|     /// * `Err(GitError)` - If the base path is invalid or cannot be created
 | ||||
|     pub fn new(base_path: &str) -> Result<Self, GitError> { | ||||
|         // Check if git is installed
 | ||||
|         check_git_installed()?; | ||||
|         
 | ||||
| 
 | ||||
|         // Validate the base path
 | ||||
|         let path = Path::new(base_path); | ||||
|         if !path.exists() { | ||||
|             fs::create_dir_all(path).map_err(|e| { | ||||
|                 GitError::FileSystemError(e) | ||||
|             })?; | ||||
|             fs::create_dir_all(path).map_err(|e| GitError::FileSystemError(e))?; | ||||
|         } else if !path.is_dir() { | ||||
|             return Err(GitError::InvalidBasePath(base_path.to_string())); | ||||
|         } | ||||
|         
 | ||||
| 
 | ||||
|         Ok(GitTree { | ||||
|             base_path: base_path.to_string(), | ||||
|         }) | ||||
|     } | ||||
|     
 | ||||
| 
 | ||||
|     /// Lists all git repositories under the base path.
 | ||||
|     /// 
 | ||||
|     ///
 | ||||
|     /// # Returns
 | ||||
|     /// 
 | ||||
|     ///
 | ||||
|     /// * `Ok(Vec<String>)` - A vector of paths to git repositories
 | ||||
|     /// * `Err(GitError)` - If the operation failed
 | ||||
|     pub fn list(&self) -> Result<Vec<String>, GitError> { | ||||
|         let base_path = Path::new(&self.base_path); | ||||
|         
 | ||||
| 
 | ||||
|         if !base_path.exists() || !base_path.is_dir() { | ||||
|             return Ok(Vec::new()); | ||||
|         } | ||||
|         
 | ||||
| 
 | ||||
|         let mut repos = Vec::new(); | ||||
|         
 | ||||
| 
 | ||||
|         // Find all directories with .git subdirectories
 | ||||
|         let output = Command::new("find") | ||||
|             .args(&[&self.base_path, "-type", "d", "-name", ".git"]) | ||||
|             .output() | ||||
|             .map_err(GitError::CommandExecutionError)?; | ||||
|             
 | ||||
| 
 | ||||
|         if output.status.success() { | ||||
|             let stdout = String::from_utf8_lossy(&output.stdout); | ||||
|             for line in stdout.lines() { | ||||
| @@ -178,22 +176,25 @@ impl GitTree { | ||||
|             } | ||||
|         } else { | ||||
|             let error = String::from_utf8_lossy(&output.stderr); | ||||
|             return Err(GitError::GitCommandFailed(format!("Failed to find git repositories: {}", error))); | ||||
|             return Err(GitError::GitCommandFailed(format!( | ||||
|                 "Failed to find git repositories: {}", | ||||
|                 error | ||||
|             ))); | ||||
|         } | ||||
|         
 | ||||
| 
 | ||||
|         Ok(repos) | ||||
|     } | ||||
|     
 | ||||
| 
 | ||||
|     /// Finds repositories matching a pattern or partial path.
 | ||||
|     /// 
 | ||||
|     ///
 | ||||
|     /// # Arguments
 | ||||
|     /// 
 | ||||
|     ///
 | ||||
|     /// * `pattern` - The pattern to match against repository paths
 | ||||
|     ///   - If the pattern ends with '*', all matching repositories are returned
 | ||||
|     ///   - Otherwise, exactly one matching repository must be found
 | ||||
|     /// 
 | ||||
|     ///
 | ||||
|     /// # Returns
 | ||||
|     /// 
 | ||||
|     ///
 | ||||
|     /// * `Ok(Vec<String>)` - A vector of paths to matching repositories
 | ||||
|     /// * `Err(GitError)` - If no matching repositories are found,
 | ||||
|     ///   or if multiple repositories match a non-wildcard pattern
 | ||||
| @@ -212,7 +213,7 @@ impl GitTree { | ||||
|                 matched_repos.push(GitRepo::new(full_path)); | ||||
|             } | ||||
|         } else if pattern.ends_with('*') { | ||||
|             let prefix = &pattern[0..pattern.len()-1]; | ||||
|             let prefix = &pattern[0..pattern.len() - 1]; | ||||
|             for name in repo_names { | ||||
|                 if name.starts_with(prefix) { | ||||
|                     let full_path = format!("{}/{}", self.base_path, name); | ||||
| @@ -233,17 +234,17 @@ impl GitTree { | ||||
| 
 | ||||
|         Ok(matched_repos) | ||||
|     } | ||||
|     
 | ||||
| 
 | ||||
|     /// Gets one or more GitRepo objects based on a path pattern or URL.
 | ||||
|     /// 
 | ||||
|     ///
 | ||||
|     /// # Arguments
 | ||||
|     /// 
 | ||||
|     ///
 | ||||
|     /// * `path_or_url` - The path pattern to match against repository paths or a git URL
 | ||||
|     ///   - If it's a URL, the repository will be cloned if it doesn't exist
 | ||||
|     ///   - If it's a path pattern, it will find matching repositories
 | ||||
|     /// 
 | ||||
|     ///
 | ||||
|     /// # Returns
 | ||||
|     /// 
 | ||||
|     ///
 | ||||
|     /// * `Ok(Vec<GitRepo>)` - A vector of GitRepo objects
 | ||||
|     /// * `Err(GitError)` - If no matching repositories are found or the clone operation failed
 | ||||
|     pub fn get(&self, path_or_url: &str) -> Result<Vec<GitRepo>, GitError> { | ||||
| @@ -254,32 +255,35 @@ impl GitTree { | ||||
|             if server.is_empty() || account.is_empty() || repo.is_empty() { | ||||
|                 return Err(GitError::InvalidUrl(path_or_url.to_string())); | ||||
|             } | ||||
|             
 | ||||
| 
 | ||||
|             // Create the target directory
 | ||||
|             let clone_path = format!("{}/{}/{}/{}", self.base_path, server, account, repo); | ||||
|             let clone_dir = Path::new(&clone_path); | ||||
|             
 | ||||
| 
 | ||||
|             // Check if repo already exists
 | ||||
|             if clone_dir.exists() { | ||||
|                 return Ok(vec![GitRepo::new(clone_path)]); | ||||
|             } | ||||
|             
 | ||||
| 
 | ||||
|             // Create parent directory
 | ||||
|             if let Some(parent) = clone_dir.parent() { | ||||
|                 fs::create_dir_all(parent).map_err(GitError::FileSystemError)?; | ||||
|             } | ||||
|             
 | ||||
| 
 | ||||
|             // Clone the repository
 | ||||
|             let output = Command::new("git") | ||||
|                 .args(&["clone", "--depth", "1", path_or_url, &clone_path]) | ||||
|                 .output() | ||||
|                 .map_err(GitError::CommandExecutionError)?; | ||||
|                 
 | ||||
| 
 | ||||
|             if output.status.success() { | ||||
|                 Ok(vec![GitRepo::new(clone_path)]) | ||||
|             } else { | ||||
|                 let error = String::from_utf8_lossy(&output.stderr); | ||||
|                 Err(GitError::GitCommandFailed(format!("Git clone error: {}", error))) | ||||
|                 Err(GitError::GitCommandFailed(format!( | ||||
|                     "Git clone error: {}", | ||||
|                     error | ||||
|                 ))) | ||||
|             } | ||||
|         } else { | ||||
|             // It's a path pattern, find matching repositories using the updated self.find()
 | ||||
| @@ -297,27 +301,27 @@ pub struct GitRepo { | ||||
| 
 | ||||
| impl GitRepo { | ||||
|     /// Creates a new GitRepo with the specified path.
 | ||||
|     /// 
 | ||||
|     ///
 | ||||
|     /// # Arguments
 | ||||
|     ///
 | ||||
|     /// * `path` - The path to the git repository
 | ||||
|     pub fn new(path: String) -> Self { | ||||
|         GitRepo { path } | ||||
|     } | ||||
|     
 | ||||
| 
 | ||||
|     /// Gets the path of the repository.
 | ||||
|     /// 
 | ||||
|     ///
 | ||||
|     /// # Returns
 | ||||
|     /// 
 | ||||
|     ///
 | ||||
|     /// * The path to the git repository
 | ||||
|     pub fn path(&self) -> &str { | ||||
|         &self.path | ||||
|     } | ||||
|     
 | ||||
| 
 | ||||
|     /// Checks if the repository has uncommitted changes.
 | ||||
|     /// 
 | ||||
|     ///
 | ||||
|     /// # Returns
 | ||||
|     /// 
 | ||||
|     ///
 | ||||
|     /// * `Ok(bool)` - True if the repository has uncommitted changes, false otherwise
 | ||||
|     /// * `Err(GitError)` - If the operation failed
 | ||||
|     pub fn has_changes(&self) -> Result<bool, GitError> { | ||||
| @@ -325,14 +329,14 @@ impl GitRepo { | ||||
|             .args(&["-C", &self.path, "status", "--porcelain"]) | ||||
|             .output() | ||||
|             .map_err(GitError::CommandExecutionError)?; | ||||
|             
 | ||||
| 
 | ||||
|         Ok(!output.stdout.is_empty()) | ||||
|     } | ||||
|     
 | ||||
| 
 | ||||
|     /// Pulls the latest changes from the remote repository.
 | ||||
|     /// 
 | ||||
|     ///
 | ||||
|     /// # Returns
 | ||||
|     /// 
 | ||||
|     ///
 | ||||
|     /// * `Ok(Self)` - The GitRepo object for method chaining
 | ||||
|     /// * `Err(GitError)` - If the pull operation failed
 | ||||
|     pub fn pull(&self) -> Result<Self, GitError> { | ||||
| @@ -341,7 +345,7 @@ impl GitRepo { | ||||
|         if !git_dir.exists() || !git_dir.is_dir() { | ||||
|             return Err(GitError::NotAGitRepository(self.path.clone())); | ||||
|         } | ||||
|         
 | ||||
| 
 | ||||
|         // Check for local changes
 | ||||
|         if self.has_changes()? { | ||||
|             return Err(GitError::LocalChangesExist(self.path.clone())); | ||||
| @@ -357,14 +361,17 @@ impl GitRepo { | ||||
|             Ok(self.clone()) | ||||
|         } else { | ||||
|             let error = String::from_utf8_lossy(&output.stderr); | ||||
|             Err(GitError::GitCommandFailed(format!("Git pull error: {}", error))) | ||||
|             Err(GitError::GitCommandFailed(format!( | ||||
|                 "Git pull error: {}", | ||||
|                 error | ||||
|             ))) | ||||
|         } | ||||
|     } | ||||
|     
 | ||||
| 
 | ||||
|     /// Resets any local changes in the repository.
 | ||||
|     /// 
 | ||||
|     ///
 | ||||
|     /// # Returns
 | ||||
|     /// 
 | ||||
|     ///
 | ||||
|     /// * `Ok(Self)` - The GitRepo object for method chaining
 | ||||
|     /// * `Err(GitError)` - If the reset operation failed
 | ||||
|     pub fn reset(&self) -> Result<Self, GitError> { | ||||
| @@ -373,7 +380,7 @@ impl GitRepo { | ||||
|         if !git_dir.exists() || !git_dir.is_dir() { | ||||
|             return Err(GitError::NotAGitRepository(self.path.clone())); | ||||
|         } | ||||
|         
 | ||||
| 
 | ||||
|         // Reset any local changes
 | ||||
|         let reset_output = Command::new("git") | ||||
|             .args(&["-C", &self.path, "reset", "--hard", "HEAD"]) | ||||
| @@ -382,7 +389,10 @@ impl GitRepo { | ||||
| 
 | ||||
|         if !reset_output.status.success() { | ||||
|             let error = String::from_utf8_lossy(&reset_output.stderr); | ||||
|             return Err(GitError::GitCommandFailed(format!("Git reset error: {}", error))); | ||||
|             return Err(GitError::GitCommandFailed(format!( | ||||
|                 "Git reset error: {}", | ||||
|                 error | ||||
|             ))); | ||||
|         } | ||||
| 
 | ||||
|         // Clean untracked files
 | ||||
| @@ -393,20 +403,23 @@ impl GitRepo { | ||||
| 
 | ||||
|         if !clean_output.status.success() { | ||||
|             let error = String::from_utf8_lossy(&clean_output.stderr); | ||||
|             return Err(GitError::GitCommandFailed(format!("Git clean error: {}", error))); | ||||
|             return Err(GitError::GitCommandFailed(format!( | ||||
|                 "Git clean error: {}", | ||||
|                 error | ||||
|             ))); | ||||
|         } | ||||
| 
 | ||||
|         Ok(self.clone()) | ||||
|     } | ||||
|     
 | ||||
| 
 | ||||
|     /// Commits changes in the repository.
 | ||||
|     /// 
 | ||||
|     ///
 | ||||
|     /// # Arguments
 | ||||
|     /// 
 | ||||
|     ///
 | ||||
|     /// * `message` - The commit message
 | ||||
|     /// 
 | ||||
|     ///
 | ||||
|     /// # Returns
 | ||||
|     /// 
 | ||||
|     ///
 | ||||
|     /// * `Ok(Self)` - The GitRepo object for method chaining
 | ||||
|     /// * `Err(GitError)` - If the commit operation failed
 | ||||
|     pub fn commit(&self, message: &str) -> Result<Self, GitError> { | ||||
| @@ -429,7 +442,10 @@ impl GitRepo { | ||||
| 
 | ||||
|         if !add_output.status.success() { | ||||
|             let error = String::from_utf8_lossy(&add_output.stderr); | ||||
|             return Err(GitError::GitCommandFailed(format!("Git add error: {}", error))); | ||||
|             return Err(GitError::GitCommandFailed(format!( | ||||
|                 "Git add error: {}", | ||||
|                 error | ||||
|             ))); | ||||
|         } | ||||
| 
 | ||||
|         // Commit the changes
 | ||||
| @@ -440,16 +456,19 @@ impl GitRepo { | ||||
| 
 | ||||
|         if !commit_output.status.success() { | ||||
|             let error = String::from_utf8_lossy(&commit_output.stderr); | ||||
|             return Err(GitError::GitCommandFailed(format!("Git commit error: {}", error))); | ||||
|             return Err(GitError::GitCommandFailed(format!( | ||||
|                 "Git commit error: {}", | ||||
|                 error | ||||
|             ))); | ||||
|         } | ||||
| 
 | ||||
|         Ok(self.clone()) | ||||
|     } | ||||
|     
 | ||||
| 
 | ||||
|     /// Pushes changes to the remote repository.
 | ||||
|     /// 
 | ||||
|     ///
 | ||||
|     /// # Returns
 | ||||
|     /// 
 | ||||
|     ///
 | ||||
|     /// * `Ok(Self)` - The GitRepo object for method chaining
 | ||||
|     /// * `Err(GitError)` - If the push operation failed
 | ||||
|     pub fn push(&self) -> Result<Self, GitError> { | ||||
| @@ -469,7 +488,10 @@ impl GitRepo { | ||||
|             Ok(self.clone()) | ||||
|         } else { | ||||
|             let error = String::from_utf8_lossy(&push_output.stderr); | ||||
|             Err(GitError::GitCommandFailed(format!("Git push error: {}", error))) | ||||
|             Err(GitError::GitCommandFailed(format!( | ||||
|                 "Git push error: {}", | ||||
|                 error | ||||
|             ))) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,11 +1,47 @@ | ||||
| use std::process::{Command, Output}; | ||||
| use std::error::Error; | ||||
| use std::fmt; | ||||
| use std::collections::HashMap; | ||||
| use redis::Cmd; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use std::collections::HashMap; | ||||
| use std::error::Error; | ||||
| use std::fmt; | ||||
| use std::process::{Command, Output}; | ||||
| 
 | ||||
| use crate::redisclient; | ||||
| // Simple redis client functionality with configurable connection
 | ||||
| fn execute_redis_command(cmd: &mut redis::Cmd) -> redis::RedisResult<String> { | ||||
|     // Get Redis URL from environment variables with fallback
 | ||||
|     let redis_url = get_redis_url(); | ||||
|     log::debug!("Connecting to Redis at: {}", mask_redis_url(&redis_url)); | ||||
| 
 | ||||
|     let client = redis::Client::open(redis_url)?; | ||||
|     let mut con = client.get_connection()?; | ||||
|     cmd.query(&mut con) | ||||
| } | ||||
| 
 | ||||
| /// Get Redis URL from environment variables with secure fallbacks
 | ||||
| fn get_redis_url() -> String { | ||||
|     std::env::var("REDIS_URL") | ||||
|         .or_else(|_| std::env::var("SAL_REDIS_URL")) | ||||
|         .unwrap_or_else(|_| "redis://127.0.0.1/".to_string()) | ||||
| } | ||||
| 
 | ||||
| /// Mask sensitive information in Redis URL for logging
 | ||||
| fn mask_redis_url(url: &str) -> String { | ||||
|     if let Ok(parsed) = url::Url::parse(url) { | ||||
|         if parsed.password().is_some() { | ||||
|             format!( | ||||
|                 "{}://{}:***@{}:{}/{}", | ||||
|                 parsed.scheme(), | ||||
|                 parsed.username(), | ||||
|                 parsed.host_str().unwrap_or("unknown"), | ||||
|                 parsed.port().unwrap_or(6379), | ||||
|                 parsed.path().trim_start_matches('/') | ||||
|             ) | ||||
|         } else { | ||||
|             url.to_string() | ||||
|         } | ||||
|     } else { | ||||
|         "redis://***masked***".to_string() | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| // Define a custom error type for GitExecutor operations
 | ||||
| #[derive(Debug)] | ||||
| @@ -24,12 +60,16 @@ impl fmt::Display for GitExecutorError { | ||||
|     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||||
|         match self { | ||||
|             GitExecutorError::GitCommandFailed(e) => write!(f, "Git command failed: {}", e), | ||||
|             GitExecutorError::CommandExecutionError(e) => write!(f, "Command execution error: {}", e), | ||||
|             GitExecutorError::CommandExecutionError(e) => { | ||||
|                 write!(f, "Command execution error: {}", e) | ||||
|             } | ||||
|             GitExecutorError::RedisError(e) => write!(f, "Redis error: {}", e), | ||||
|             GitExecutorError::JsonError(e) => write!(f, "JSON error: {}", e), | ||||
|             GitExecutorError::AuthenticationError(e) => write!(f, "Authentication error: {}", e), | ||||
|             GitExecutorError::SshAgentNotLoaded => write!(f, "SSH agent is not loaded"), | ||||
|             GitExecutorError::InvalidAuthConfig(e) => write!(f, "Invalid authentication configuration: {}", e), | ||||
|             GitExecutorError::InvalidAuthConfig(e) => { | ||||
|                 write!(f, "Invalid authentication configuration: {}", e) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -112,7 +152,7 @@ impl GitExecutor { | ||||
|             Err(e) => { | ||||
|                 // If Redis error, we'll proceed without config
 | ||||
|                 // This is not a fatal error as we might use default git behavior
 | ||||
|                 eprintln!("Warning: Failed to load git config from Redis: {}", e); | ||||
|                 log::warn!("Failed to load git config from Redis: {}", e); | ||||
|                 self.config = None; | ||||
|                 Ok(()) | ||||
|             } | ||||
| @@ -126,18 +166,20 @@ impl GitExecutor { | ||||
|         cmd.arg("GET").arg("herocontext:git"); | ||||
| 
 | ||||
|         // Execute the command
 | ||||
|         let result: redis::RedisResult<String> = redisclient::execute(&mut cmd); | ||||
|         
 | ||||
|         let result: redis::RedisResult<String> = execute_redis_command(&mut cmd); | ||||
| 
 | ||||
|         match result { | ||||
|             Ok(json_str) => { | ||||
|                 // Parse the JSON string into GitConfig
 | ||||
|                 let config: GitConfig = serde_json::from_str(&json_str)?; | ||||
|                 
 | ||||
| 
 | ||||
|                 // Validate the config
 | ||||
|                 if config.status == GitConfigStatus::Error { | ||||
|                     return Err(GitExecutorError::InvalidAuthConfig("Config status is error".to_string())); | ||||
|                     return Err(GitExecutorError::InvalidAuthConfig( | ||||
|                         "Config status is error".to_string(), | ||||
|                     )); | ||||
|                 } | ||||
|                 
 | ||||
| 
 | ||||
|                 Ok(config) | ||||
|             } | ||||
|             Err(e) => Err(GitExecutorError::RedisError(e)), | ||||
| @@ -146,10 +188,8 @@ impl GitExecutor { | ||||
| 
 | ||||
|     // Check if SSH agent is loaded
 | ||||
|     fn is_ssh_agent_loaded(&self) -> bool { | ||||
|         let output = Command::new("ssh-add") | ||||
|             .arg("-l") | ||||
|             .output(); | ||||
|         
 | ||||
|         let output = Command::new("ssh-add").arg("-l").output(); | ||||
| 
 | ||||
|         match output { | ||||
|             Ok(output) => output.status.success() && !output.stdout.is_empty(), | ||||
|             Err(_) => false, | ||||
| @@ -159,7 +199,7 @@ impl GitExecutor { | ||||
|     // Get authentication configuration for a git URL
 | ||||
|     fn get_auth_for_url(&self, url: &str) -> Option<&GitServerAuth> { | ||||
|         if let Some(config) = &self.config { | ||||
|             let (server, _, _) = crate::git::git::parse_git_url(url); | ||||
|             let (server, _, _) = crate::parse_git_url(url); | ||||
|             if !server.is_empty() { | ||||
|                 return config.auth.get(&server); | ||||
|             } | ||||
| @@ -173,7 +213,7 @@ impl GitExecutor { | ||||
|         if let Some(true) = auth.sshagent { | ||||
|             if auth.key.is_some() || auth.username.is_some() || auth.password.is_some() { | ||||
|                 return Err(GitExecutorError::InvalidAuthConfig( | ||||
|                     "When sshagent is true, key, username, and password must be empty".to_string() | ||||
|                     "When sshagent is true, key, username, and password must be empty".to_string(), | ||||
|                 )); | ||||
|             } | ||||
|             // Check if SSH agent is actually loaded
 | ||||
| @@ -181,30 +221,31 @@ impl GitExecutor { | ||||
|                 return Err(GitExecutorError::SshAgentNotLoaded); | ||||
|             } | ||||
|         } | ||||
|         
 | ||||
| 
 | ||||
|         // Rule: If key is set, other fields should be empty
 | ||||
|         if let Some(_) = &auth.key { | ||||
|             if auth.sshagent.unwrap_or(false) || auth.username.is_some() || auth.password.is_some() { | ||||
|             if auth.sshagent.unwrap_or(false) || auth.username.is_some() || auth.password.is_some() | ||||
|             { | ||||
|                 return Err(GitExecutorError::InvalidAuthConfig( | ||||
|                     "When key is set, sshagent, username, and password must be empty".to_string() | ||||
|                     "When key is set, sshagent, username, and password must be empty".to_string(), | ||||
|                 )); | ||||
|             } | ||||
|         } | ||||
|         
 | ||||
| 
 | ||||
|         // Rule: If username is set, password should be set and other fields empty
 | ||||
|         if let Some(_) = &auth.username { | ||||
|             if auth.sshagent.unwrap_or(false) || auth.key.is_some() { | ||||
|                 return Err(GitExecutorError::InvalidAuthConfig( | ||||
|                     "When username is set, sshagent and key must be empty".to_string() | ||||
|                     "When username is set, sshagent and key must be empty".to_string(), | ||||
|                 )); | ||||
|             } | ||||
|             if auth.password.is_none() { | ||||
|                 return Err(GitExecutorError::InvalidAuthConfig( | ||||
|                     "When username is set, password must also be set".to_string() | ||||
|                     "When username is set, password must also be set".to_string(), | ||||
|                 )); | ||||
|             } | ||||
|         } | ||||
|         
 | ||||
| 
 | ||||
|         Ok(()) | ||||
|     } | ||||
| 
 | ||||
| @@ -212,18 +253,18 @@ impl GitExecutor { | ||||
|     pub fn execute(&self, args: &[&str]) -> Result<Output, GitExecutorError> { | ||||
|         // Extract the git URL if this is a command that needs authentication
 | ||||
|         let url_arg = self.extract_git_url_from_args(args); | ||||
|         
 | ||||
| 
 | ||||
|         // If we have a URL and authentication config, use it
 | ||||
|         if let Some(url) = url_arg { | ||||
|             if let Some(auth) = self.get_auth_for_url(&url) { | ||||
|                 // Validate the authentication configuration
 | ||||
|                 self.validate_auth_config(auth)?; | ||||
|                 
 | ||||
| 
 | ||||
|                 // Execute with the appropriate authentication method
 | ||||
|                 return self.execute_with_auth(args, auth); | ||||
|             } | ||||
|         } | ||||
|         
 | ||||
| 
 | ||||
|         // No special authentication needed, execute normally
 | ||||
|         self.execute_git_command(args) | ||||
|     } | ||||
| @@ -231,7 +272,11 @@ impl GitExecutor { | ||||
|     // Extract git URL from command arguments
 | ||||
|     fn extract_git_url_from_args<'a>(&self, args: &[&'a str]) -> Option<&'a str> { | ||||
|         // Commands that might contain a git URL
 | ||||
|         if args.contains(&"clone") || args.contains(&"fetch") || args.contains(&"pull") || args.contains(&"push") { | ||||
|         if args.contains(&"clone") | ||||
|             || args.contains(&"fetch") | ||||
|             || args.contains(&"pull") | ||||
|             || args.contains(&"push") | ||||
|         { | ||||
|             // The URL is typically the last argument for clone, or after remote for others
 | ||||
|             for (i, &arg) in args.iter().enumerate() { | ||||
|                 if arg == "clone" && i + 1 < args.len() { | ||||
| @@ -249,7 +294,11 @@ impl GitExecutor { | ||||
|     } | ||||
| 
 | ||||
|     // Execute git command with authentication
 | ||||
|     fn execute_with_auth(&self, args: &[&str], auth: &GitServerAuth) -> Result<Output, GitExecutorError> { | ||||
|     fn execute_with_auth( | ||||
|         &self, | ||||
|         args: &[&str], | ||||
|         auth: &GitServerAuth, | ||||
|     ) -> Result<Output, GitExecutorError> { | ||||
|         // Handle different authentication methods
 | ||||
|         if let Some(true) = auth.sshagent { | ||||
|             // Use SSH agent (already validated that it's loaded)
 | ||||
| @@ -263,7 +312,9 @@ impl GitExecutor { | ||||
|                 self.execute_with_credentials(args, username, password) | ||||
|             } else { | ||||
|                 // This should never happen due to validation
 | ||||
|                 Err(GitExecutorError::AuthenticationError("Password is required when username is set".to_string())) | ||||
|                 Err(GitExecutorError::AuthenticationError( | ||||
|                     "Password is required when username is set".to_string(), | ||||
|                 )) | ||||
|             } | ||||
|         } else { | ||||
|             // No authentication method specified, use default
 | ||||
| @@ -275,13 +326,13 @@ impl GitExecutor { | ||||
|     fn execute_with_ssh_key(&self, args: &[&str], key: &str) -> Result<Output, GitExecutorError> { | ||||
|         // Create a command with GIT_SSH_COMMAND to specify the key
 | ||||
|         let ssh_command = format!("ssh -i {} -o IdentitiesOnly=yes", key); | ||||
|         
 | ||||
| 
 | ||||
|         let mut command = Command::new("git"); | ||||
|         command.env("GIT_SSH_COMMAND", ssh_command); | ||||
|         command.args(args); | ||||
|         
 | ||||
| 
 | ||||
|         let output = command.output()?; | ||||
|         
 | ||||
| 
 | ||||
|         if output.status.success() { | ||||
|             Ok(output) | ||||
|         } else { | ||||
| @@ -290,42 +341,68 @@ impl GitExecutor { | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     // Execute git command with username/password
 | ||||
|     fn execute_with_credentials(&self, args: &[&str], username: &str, password: &str) -> Result<Output, GitExecutorError> { | ||||
|         // For HTTPS authentication, we need to modify the URL to include credentials
 | ||||
|         // Create a new vector to hold our modified arguments
 | ||||
|         let modified_args: Vec<String> = args.iter().map(|&arg| { | ||||
|             if arg.starts_with("https://") { | ||||
|                 // Replace https:// with https://username:password@
 | ||||
|                 format!("https://{}:{}@{}", 
 | ||||
|                     username, 
 | ||||
|                     password, 
 | ||||
|                     &arg[8..]) // Skip the "https://" part
 | ||||
|             } else { | ||||
|                 arg.to_string() | ||||
|             } | ||||
|         }).collect(); | ||||
|         
 | ||||
|         // Execute the command
 | ||||
|         let mut command = Command::new("git"); | ||||
|         
 | ||||
|         // Add the modified arguments to the command
 | ||||
|         for arg in &modified_args { | ||||
|             command.arg(arg.as_str()); | ||||
|     // Execute git command with username/password using secure credential helper
 | ||||
|     fn execute_with_credentials( | ||||
|         &self, | ||||
|         args: &[&str], | ||||
|         username: &str, | ||||
|         password: &str, | ||||
|     ) -> Result<Output, GitExecutorError> { | ||||
|         // Use git credential helper approach for security
 | ||||
|         // Create a temporary credential helper script
 | ||||
|         let temp_dir = std::env::temp_dir(); | ||||
|         let helper_script = temp_dir.join(format!("git_helper_{}", std::process::id())); | ||||
| 
 | ||||
|         // Create credential helper script content
 | ||||
|         let script_content = format!( | ||||
|             "#!/bin/bash\necho username={}\necho password={}\n", | ||||
|             username, password | ||||
|         ); | ||||
| 
 | ||||
|         // Write the helper script
 | ||||
|         std::fs::write(&helper_script, script_content) | ||||
|             .map_err(|e| GitExecutorError::CommandExecutionError(e))?; | ||||
| 
 | ||||
|         // Make it executable
 | ||||
|         #[cfg(unix)] | ||||
|         { | ||||
|             use std::os::unix::fs::PermissionsExt; | ||||
|             let mut perms = std::fs::metadata(&helper_script) | ||||
|                 .map_err(|e| GitExecutorError::CommandExecutionError(e))? | ||||
|                 .permissions(); | ||||
|             perms.set_mode(0o755); | ||||
|             std::fs::set_permissions(&helper_script, perms) | ||||
|                 .map_err(|e| GitExecutorError::CommandExecutionError(e))?; | ||||
|         } | ||||
| 
 | ||||
|         // Execute the command and handle the result
 | ||||
|         // Execute git command with credential helper
 | ||||
|         let mut command = Command::new("git"); | ||||
|         command.args(args); | ||||
|         command.env("GIT_ASKPASS", &helper_script); | ||||
|         command.env("GIT_TERMINAL_PROMPT", "0"); // Disable terminal prompts
 | ||||
| 
 | ||||
|         log::debug!("Executing git command with credential helper"); | ||||
|         let output = command.output()?; | ||||
|         if output.status.success() { Ok(output) } else { Err(GitExecutorError::GitCommandFailed(String::from_utf8_lossy(&output.stderr).to_string())) } | ||||
| 
 | ||||
|         // Clean up the temporary helper script
 | ||||
|         let _ = std::fs::remove_file(&helper_script); | ||||
| 
 | ||||
|         if output.status.success() { | ||||
|             Ok(output) | ||||
|         } else { | ||||
|             let error = String::from_utf8_lossy(&output.stderr); | ||||
|             log::error!("Git command failed: {}", error); | ||||
|             Err(GitExecutorError::GitCommandFailed(error.to_string())) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     // Basic git command execution
 | ||||
|     fn execute_git_command(&self, args: &[&str]) -> Result<Output, GitExecutorError> { | ||||
|         let mut command = Command::new("git"); | ||||
|         command.args(args); | ||||
|         
 | ||||
| 
 | ||||
|         let output = command.output()?; | ||||
|         
 | ||||
| 
 | ||||
|         if output.status.success() { | ||||
|             Ok(output) | ||||
|         } else { | ||||
| @@ -340,4 +417,4 @@ impl Default for GitExecutor { | ||||
|     fn default() -> Self { | ||||
|         Self::new() | ||||
|     } | ||||
| } | ||||
| } | ||||
| @@ -1,5 +1,6 @@ | ||||
| mod git; | ||||
| mod git_executor; | ||||
| pub mod rhai; | ||||
| 
 | ||||
| pub use git::*; | ||||
| pub use git_executor::*; | ||||
| pub use git_executor::*; | ||||
| @@ -2,7 +2,7 @@ | ||||
| //!
 | ||||
| //! This module provides Rhai wrappers for the functions in the Git module.
 | ||||
| 
 | ||||
| use crate::git::{GitError, GitRepo, GitTree}; | ||||
| use crate::{GitError, GitRepo, GitTree}; | ||||
| use rhai::{Array, Dynamic, Engine, EvalAltResult}; | ||||
| 
 | ||||
| /// Register Git module functions with the Rhai engine
 | ||||
| @@ -171,13 +171,37 @@ pub fn git_repo_push(git_repo: &mut GitRepo) -> Result<GitRepo, Box<EvalAltResul | ||||
|     git_error_to_rhai_error(git_repo.push()) | ||||
| } | ||||
| 
 | ||||
| /// Dummy implementation of git_clone for testing
 | ||||
| /// Clone a git repository to a temporary location
 | ||||
| ///
 | ||||
| /// This function is used for testing the git module.
 | ||||
| pub fn git_clone(url: &str) -> Result<(), Box<EvalAltResult>> { | ||||
|     // This is a dummy implementation that always fails with a Git error
 | ||||
|     Err(Box::new(EvalAltResult::ErrorRuntime( | ||||
|         format!("Git error: Failed to clone repository from URL: {}", url).into(), | ||||
|         rhai::Position::NONE, | ||||
|     ))) | ||||
| /// This function clones a repository from the given URL to a temporary directory
 | ||||
| /// and returns the GitRepo object for further operations.
 | ||||
| ///
 | ||||
| /// # Arguments
 | ||||
| ///
 | ||||
| /// * `url` - The URL of the git repository to clone
 | ||||
| ///
 | ||||
| /// # Returns
 | ||||
| ///
 | ||||
| /// * `Ok(GitRepo)` - The cloned repository object
 | ||||
| /// * `Err(Box<EvalAltResult>)` - If the clone operation failed
 | ||||
| pub fn git_clone(url: &str) -> Result<GitRepo, Box<EvalAltResult>> { | ||||
|     // Get base path from environment or use default temp directory
 | ||||
|     let base_path = std::env::var("GIT_DEFAULT_BASE_PATH").unwrap_or_else(|_| { | ||||
|         std::env::temp_dir() | ||||
|             .join("sal_git_clones") | ||||
|             .to_string_lossy() | ||||
|             .to_string() | ||||
|     }); | ||||
| 
 | ||||
|     // Create GitTree and clone the repository
 | ||||
|     let git_tree = git_error_to_rhai_error(GitTree::new(&base_path))?; | ||||
|     let repos = git_error_to_rhai_error(git_tree.get(url))?; | ||||
| 
 | ||||
|     // Return the first (and should be only) repository
 | ||||
|     repos.into_iter().next().ok_or_else(|| { | ||||
|         Box::new(EvalAltResult::ErrorRuntime( | ||||
|             "Git error: No repository was cloned".into(), | ||||
|             rhai::Position::NONE, | ||||
|         )) | ||||
|     }) | ||||
| } | ||||
							
								
								
									
										197
									
								
								git/tests/git_executor_security_tests.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										197
									
								
								git/tests/git_executor_security_tests.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,197 @@ | ||||
| use sal_git::*; | ||||
| use std::env; | ||||
|  | ||||
| #[test] | ||||
| fn test_git_executor_initialization() { | ||||
|     let mut executor = GitExecutor::new(); | ||||
|  | ||||
|     // Test that executor can be initialized without panicking | ||||
|     // Even if Redis is not available, init should handle it gracefully | ||||
|     let result = executor.init(); | ||||
|     assert!( | ||||
|         result.is_ok(), | ||||
|         "GitExecutor init should handle Redis unavailability gracefully" | ||||
|     ); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_redis_connection_fallback() { | ||||
|     // Test that GitExecutor handles Redis connection failures gracefully | ||||
|     // Set an invalid Redis URL to force connection failure | ||||
|     env::set_var("REDIS_URL", "redis://invalid-host:9999/0"); | ||||
|  | ||||
|     let mut executor = GitExecutor::new(); | ||||
|     let result = executor.init(); | ||||
|  | ||||
|     // Should succeed even with invalid Redis URL (graceful fallback) | ||||
|     assert!( | ||||
|         result.is_ok(), | ||||
|         "GitExecutor should handle Redis connection failures gracefully" | ||||
|     ); | ||||
|  | ||||
|     // Cleanup | ||||
|     env::remove_var("REDIS_URL"); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_environment_variable_precedence() { | ||||
|     // Test REDIS_URL takes precedence over SAL_REDIS_URL | ||||
|     env::set_var("REDIS_URL", "redis://primary:6379/0"); | ||||
|     env::set_var("SAL_REDIS_URL", "redis://fallback:6379/1"); | ||||
|  | ||||
|     // Create executor - should use REDIS_URL (primary) | ||||
|     let mut executor = GitExecutor::new(); | ||||
|     let result = executor.init(); | ||||
|  | ||||
|     // Should succeed (even if connection fails, init handles it gracefully) | ||||
|     assert!( | ||||
|         result.is_ok(), | ||||
|         "GitExecutor should handle environment variables correctly" | ||||
|     ); | ||||
|  | ||||
|     // Test with only SAL_REDIS_URL | ||||
|     env::remove_var("REDIS_URL"); | ||||
|     let mut executor2 = GitExecutor::new(); | ||||
|     let result2 = executor2.init(); | ||||
|     assert!( | ||||
|         result2.is_ok(), | ||||
|         "GitExecutor should use SAL_REDIS_URL as fallback" | ||||
|     ); | ||||
|  | ||||
|     // Cleanup | ||||
|     env::remove_var("SAL_REDIS_URL"); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_git_command_argument_validation() { | ||||
|     let executor = GitExecutor::new(); | ||||
|  | ||||
|     // Test with empty arguments | ||||
|     let result = executor.execute(&[]); | ||||
|     assert!(result.is_err(), "Empty git command should fail"); | ||||
|  | ||||
|     // Test with invalid git command | ||||
|     let result = executor.execute(&["invalid-command"]); | ||||
|     assert!(result.is_err(), "Invalid git command should fail"); | ||||
|  | ||||
|     // Test with malformed URL (should fail due to URL validation, not injection) | ||||
|     let result = executor.execute(&["clone", "not-a-url"]); | ||||
|     assert!(result.is_err(), "Invalid URL should be rejected"); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_git_executor_with_valid_commands() { | ||||
|     let executor = GitExecutor::new(); | ||||
|  | ||||
|     // Test git version command (should work if git is available) | ||||
|     let result = executor.execute(&["--version"]); | ||||
|  | ||||
|     match result { | ||||
|         Ok(output) => { | ||||
|             // If git is available, version should be in output | ||||
|             let output_str = String::from_utf8_lossy(&output.stdout); | ||||
|             assert!( | ||||
|                 output_str.contains("git version"), | ||||
|                 "Git version output should contain 'git version'" | ||||
|             ); | ||||
|         } | ||||
|         Err(_) => { | ||||
|             // If git is not available, that's acceptable in test environment | ||||
|             println!("Note: Git not available in test environment"); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_credential_helper_environment_setup() { | ||||
|     use std::process::Command; | ||||
|  | ||||
|     // Test that we can create and execute a simple credential helper script | ||||
|     let temp_dir = std::env::temp_dir(); | ||||
|     let helper_script = temp_dir.join("test_git_helper"); | ||||
|  | ||||
|     // Create a test credential helper script | ||||
|     let script_content = "#!/bin/bash\necho username=testuser\necho password=testpass\n"; | ||||
|  | ||||
|     // Write the helper script | ||||
|     let write_result = std::fs::write(&helper_script, script_content); | ||||
|     assert!( | ||||
|         write_result.is_ok(), | ||||
|         "Should be able to write credential helper script" | ||||
|     ); | ||||
|  | ||||
|     // Make it executable (Unix only) | ||||
|     #[cfg(unix)] | ||||
|     { | ||||
|         use std::os::unix::fs::PermissionsExt; | ||||
|         let mut perms = std::fs::metadata(&helper_script).unwrap().permissions(); | ||||
|         perms.set_mode(0o755); | ||||
|         let perm_result = std::fs::set_permissions(&helper_script, perms); | ||||
|         assert!( | ||||
|             perm_result.is_ok(), | ||||
|             "Should be able to set script permissions" | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     // Test that the script can be executed | ||||
|     #[cfg(unix)] | ||||
|     { | ||||
|         let output = Command::new(&helper_script).output(); | ||||
|         match output { | ||||
|             Ok(output) => { | ||||
|                 let stdout = String::from_utf8_lossy(&output.stdout); | ||||
|                 assert!( | ||||
|                     stdout.contains("username=testuser"), | ||||
|                     "Script should output username" | ||||
|                 ); | ||||
|                 assert!( | ||||
|                     stdout.contains("password=testpass"), | ||||
|                     "Script should output password" | ||||
|                 ); | ||||
|             } | ||||
|             Err(_) => { | ||||
|                 println!("Note: Could not execute credential helper script (shell not available)"); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // Clean up | ||||
|     let _ = std::fs::remove_file(&helper_script); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_redis_url_masking() { | ||||
|     // Test that sensitive Redis URLs are properly masked for logging | ||||
|     // This tests the internal URL masking functionality | ||||
|  | ||||
|     // Test URLs with passwords | ||||
|     let test_cases = vec![ | ||||
|         ("redis://user:password@localhost:6379/0", true), | ||||
|         ("redis://localhost:6379/0", false), | ||||
|         ("redis://user@localhost:6379/0", false), | ||||
|         ("invalid-url", false), | ||||
|     ]; | ||||
|  | ||||
|     for (url, has_password) in test_cases { | ||||
|         // Set the Redis URL and create executor | ||||
|         std::env::set_var("REDIS_URL", url); | ||||
|  | ||||
|         let mut executor = GitExecutor::new(); | ||||
|         let result = executor.init(); | ||||
|  | ||||
|         // Should always succeed (graceful handling of connection failures) | ||||
|         assert!(result.is_ok(), "GitExecutor should handle URL: {}", url); | ||||
|  | ||||
|         // The actual masking happens internally during logging | ||||
|         // We can't easily test the log output, but we verify the executor handles it | ||||
|         if has_password { | ||||
|             println!( | ||||
|                 "Note: Tested URL with password (should be masked in logs): {}", | ||||
|                 url | ||||
|             ); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // Cleanup | ||||
|     std::env::remove_var("REDIS_URL"); | ||||
| } | ||||
							
								
								
									
										178
									
								
								git/tests/git_executor_tests.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										178
									
								
								git/tests/git_executor_tests.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,178 @@ | ||||
| use sal_git::*; | ||||
| use std::collections::HashMap; | ||||
|  | ||||
| #[test] | ||||
| fn test_git_executor_new() { | ||||
|     let executor = GitExecutor::new(); | ||||
|     // We can't directly access the config field since it's private, | ||||
|     // but we can test that the executor was created successfully | ||||
|     let _executor = executor; | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_git_executor_default() { | ||||
|     let executor = GitExecutor::default(); | ||||
|     let _executor = executor; | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_git_config_status_serialization() { | ||||
|     let status_ok = GitConfigStatus::Ok; | ||||
|     let status_error = GitConfigStatus::Error; | ||||
|  | ||||
|     let json_ok = serde_json::to_string(&status_ok).unwrap(); | ||||
|     let json_error = serde_json::to_string(&status_error).unwrap(); | ||||
|  | ||||
|     assert_eq!(json_ok, "\"ok\""); | ||||
|     assert_eq!(json_error, "\"error\""); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_git_config_status_deserialization() { | ||||
|     let status_ok: GitConfigStatus = serde_json::from_str("\"ok\"").unwrap(); | ||||
|     let status_error: GitConfigStatus = serde_json::from_str("\"error\"").unwrap(); | ||||
|  | ||||
|     assert_eq!(status_ok, GitConfigStatus::Ok); | ||||
|     assert_eq!(status_error, GitConfigStatus::Error); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_git_server_auth_serialization() { | ||||
|     let auth = GitServerAuth { | ||||
|         sshagent: Some(true), | ||||
|         key: None, | ||||
|         username: None, | ||||
|         password: None, | ||||
|     }; | ||||
|  | ||||
|     let json = serde_json::to_string(&auth).unwrap(); | ||||
|     assert!(json.contains("\"sshagent\":true")); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_git_server_auth_deserialization() { | ||||
|     let json = r#"{"sshagent":true,"key":null,"username":null,"password":null}"#; | ||||
|     let auth: GitServerAuth = serde_json::from_str(json).unwrap(); | ||||
|  | ||||
|     assert_eq!(auth.sshagent, Some(true)); | ||||
|     assert_eq!(auth.key, None); | ||||
|     assert_eq!(auth.username, None); | ||||
|     assert_eq!(auth.password, None); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_git_config_serialization() { | ||||
|     let mut auth_map = HashMap::new(); | ||||
|     auth_map.insert( | ||||
|         "github.com".to_string(), | ||||
|         GitServerAuth { | ||||
|             sshagent: Some(true), | ||||
|             key: None, | ||||
|             username: None, | ||||
|             password: None, | ||||
|         }, | ||||
|     ); | ||||
|  | ||||
|     let config = GitConfig { | ||||
|         status: GitConfigStatus::Ok, | ||||
|         auth: auth_map, | ||||
|     }; | ||||
|  | ||||
|     let json = serde_json::to_string(&config).unwrap(); | ||||
|     assert!(json.contains("\"status\":\"ok\"")); | ||||
|     assert!(json.contains("\"github.com\"")); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_git_config_deserialization() { | ||||
|     let json = r#"{"status":"ok","auth":{"github.com":{"sshagent":true,"key":null,"username":null,"password":null}}}"#; | ||||
|     let config: GitConfig = serde_json::from_str(json).unwrap(); | ||||
|  | ||||
|     assert_eq!(config.status, GitConfigStatus::Ok); | ||||
|     assert!(config.auth.contains_key("github.com")); | ||||
|     assert_eq!(config.auth["github.com"].sshagent, Some(true)); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_git_executor_error_display() { | ||||
|     let error = GitExecutorError::GitCommandFailed("command failed".to_string()); | ||||
|     assert_eq!(format!("{}", error), "Git command failed: command failed"); | ||||
|  | ||||
|     let error = GitExecutorError::SshAgentNotLoaded; | ||||
|     assert_eq!(format!("{}", error), "SSH agent is not loaded"); | ||||
|  | ||||
|     let error = GitExecutorError::AuthenticationError("auth failed".to_string()); | ||||
|     assert_eq!(format!("{}", error), "Authentication error: auth failed"); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_git_executor_error_from_redis_error() { | ||||
|     let redis_error = redis::RedisError::from((redis::ErrorKind::TypeError, "type error")); | ||||
|     let git_error = GitExecutorError::from(redis_error); | ||||
|  | ||||
|     match git_error { | ||||
|         GitExecutorError::RedisError(_) => {} | ||||
|         _ => panic!("Expected RedisError variant"), | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_git_executor_error_from_serde_error() { | ||||
|     let serde_error = serde_json::from_str::<GitConfig>("invalid json").unwrap_err(); | ||||
|     let git_error = GitExecutorError::from(serde_error); | ||||
|  | ||||
|     match git_error { | ||||
|         GitExecutorError::JsonError(_) => {} | ||||
|         _ => panic!("Expected JsonError variant"), | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_git_executor_error_from_io_error() { | ||||
|     let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found"); | ||||
|     let git_error = GitExecutorError::from(io_error); | ||||
|  | ||||
|     match git_error { | ||||
|         GitExecutorError::CommandExecutionError(_) => {} | ||||
|         _ => panic!("Expected CommandExecutionError variant"), | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_redis_url_configuration() { | ||||
|     // Test default Redis URL | ||||
|     std::env::remove_var("REDIS_URL"); | ||||
|     std::env::remove_var("SAL_REDIS_URL"); | ||||
|  | ||||
|     // This is testing the internal function, but we can't access it directly | ||||
|     // Instead, we test that GitExecutor can be created without panicking | ||||
|     let executor = GitExecutor::new(); | ||||
|     let _executor = executor; // Just verify it was created successfully | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_redis_url_from_environment() { | ||||
|     // Test REDIS_URL environment variable | ||||
|     std::env::set_var("REDIS_URL", "redis://test:6379/1"); | ||||
|  | ||||
|     // Create executor - should use the environment variable | ||||
|     let executor = GitExecutor::new(); | ||||
|     let _executor = executor; // Just verify it was created successfully | ||||
|  | ||||
|     // Clean up | ||||
|     std::env::remove_var("REDIS_URL"); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_sal_redis_url_from_environment() { | ||||
|     // Test SAL_REDIS_URL environment variable (fallback) | ||||
|     std::env::remove_var("REDIS_URL"); | ||||
|     std::env::set_var("SAL_REDIS_URL", "redis://sal-test:6379/2"); | ||||
|  | ||||
|     // Create executor - should use the SAL_REDIS_URL | ||||
|     let executor = GitExecutor::new(); | ||||
|     let _executor = executor; // Just verify it was created successfully | ||||
|  | ||||
|     // Clean up | ||||
|     std::env::remove_var("SAL_REDIS_URL"); | ||||
| } | ||||
							
								
								
									
										124
									
								
								git/tests/git_integration_tests.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								git/tests/git_integration_tests.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,124 @@ | ||||
| use sal_git::*; | ||||
| use std::fs; | ||||
| use tempfile::TempDir; | ||||
|  | ||||
| #[test] | ||||
| fn test_clone_existing_repository() { | ||||
|     let temp_dir = TempDir::new().unwrap(); | ||||
|     let base_path = temp_dir.path().to_str().unwrap(); | ||||
|  | ||||
|     let git_tree = GitTree::new(base_path).unwrap(); | ||||
|  | ||||
|     // First clone | ||||
|     let result1 = git_tree.get("https://github.com/octocat/Hello-World.git"); | ||||
|  | ||||
|     // Second clone of same repo - should return existing | ||||
|     let result2 = git_tree.get("https://github.com/octocat/Hello-World.git"); | ||||
|  | ||||
|     match (result1, result2) { | ||||
|         (Ok(repos1), Ok(repos2)) => { | ||||
|             // git_tree.get() returns Vec<GitRepo>, should have exactly 1 repo | ||||
|             assert_eq!( | ||||
|                 repos1.len(), | ||||
|                 1, | ||||
|                 "First clone should return exactly 1 repository" | ||||
|             ); | ||||
|             assert_eq!( | ||||
|                 repos2.len(), | ||||
|                 1, | ||||
|                 "Second clone should return exactly 1 repository" | ||||
|             ); | ||||
|             assert_eq!( | ||||
|                 repos1[0].path(), | ||||
|                 repos2[0].path(), | ||||
|                 "Both clones should point to same path" | ||||
|             ); | ||||
|  | ||||
|             // Verify the path actually exists | ||||
|             assert!( | ||||
|                 std::path::Path::new(repos1[0].path()).exists(), | ||||
|                 "Repository path should exist" | ||||
|             ); | ||||
|         } | ||||
|         (Err(e1), Err(e2)) => { | ||||
|             // Both failed - acceptable if network/git issues | ||||
|             println!("Note: Clone test skipped due to errors: {} / {}", e1, e2); | ||||
|         } | ||||
|         _ => { | ||||
|             panic!( | ||||
|                 "Inconsistent results: one clone succeeded, other failed - this indicates a bug" | ||||
|             ); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_repository_operations_on_cloned_repo() { | ||||
|     let temp_dir = TempDir::new().unwrap(); | ||||
|     let base_path = temp_dir.path().to_str().unwrap(); | ||||
|  | ||||
|     let git_tree = GitTree::new(base_path).unwrap(); | ||||
|  | ||||
|     match git_tree.get("https://github.com/octocat/Hello-World.git") { | ||||
|         Ok(repos) if repos.len() == 1 => { | ||||
|             let repo = &repos[0]; | ||||
|  | ||||
|             // Test has_changes on fresh clone | ||||
|             match repo.has_changes() { | ||||
|                 Ok(has_changes) => assert!(!has_changes, "Fresh clone should have no changes"), | ||||
|                 Err(_) => println!("Note: has_changes test skipped due to git availability"), | ||||
|             } | ||||
|  | ||||
|             // Test path is valid | ||||
|             assert!(repo.path().len() > 0); | ||||
|             assert!(std::path::Path::new(repo.path()).exists()); | ||||
|         } | ||||
|         _ => { | ||||
|             println!( | ||||
|                 "Note: Repository operations test skipped due to network/environment constraints" | ||||
|             ); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_multiple_repositories_in_git_tree() { | ||||
|     let temp_dir = TempDir::new().unwrap(); | ||||
|     let base_path = temp_dir.path().to_str().unwrap(); | ||||
|  | ||||
|     // Create some fake git repositories for testing | ||||
|     let repo1_path = temp_dir.path().join("github.com/user1/repo1"); | ||||
|     let repo2_path = temp_dir.path().join("github.com/user2/repo2"); | ||||
|  | ||||
|     fs::create_dir_all(&repo1_path).unwrap(); | ||||
|     fs::create_dir_all(&repo2_path).unwrap(); | ||||
|     fs::create_dir_all(repo1_path.join(".git")).unwrap(); | ||||
|     fs::create_dir_all(repo2_path.join(".git")).unwrap(); | ||||
|  | ||||
|     let git_tree = GitTree::new(base_path).unwrap(); | ||||
|     let repos = git_tree.list().unwrap(); | ||||
|  | ||||
|     assert!(repos.len() >= 2, "Should find at least 2 repositories"); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_invalid_git_repository_handling() { | ||||
|     let temp_dir = TempDir::new().unwrap(); | ||||
|     let fake_repo_path = temp_dir.path().join("fake_repo"); | ||||
|     fs::create_dir_all(&fake_repo_path).unwrap(); | ||||
|  | ||||
|     // Create a directory that looks like a repo but isn't (no .git directory) | ||||
|     let repo = GitRepo::new(fake_repo_path.to_str().unwrap().to_string()); | ||||
|  | ||||
|     // Operations should fail gracefully on non-git directories | ||||
|     // Note: has_changes might succeed if git is available and treats it as empty repo | ||||
|     // So we test the operations that definitely require .git directory | ||||
|     assert!( | ||||
|         repo.pull().is_err(), | ||||
|         "Pull should fail on non-git directory" | ||||
|     ); | ||||
|     assert!( | ||||
|         repo.reset().is_err(), | ||||
|         "Reset should fail on non-git directory" | ||||
|     ); | ||||
| } | ||||
							
								
								
									
										119
									
								
								git/tests/git_tests.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								git/tests/git_tests.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,119 @@ | ||||
| use sal_git::*; | ||||
| use std::fs; | ||||
| use tempfile::TempDir; | ||||
|  | ||||
| #[test] | ||||
| fn test_parse_git_url_https() { | ||||
|     let (server, account, repo) = parse_git_url("https://github.com/user/repo.git"); | ||||
|     assert_eq!(server, "github.com"); | ||||
|     assert_eq!(account, "user"); | ||||
|     assert_eq!(repo, "repo"); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_parse_git_url_https_without_git_extension() { | ||||
|     let (server, account, repo) = parse_git_url("https://github.com/user/repo"); | ||||
|     assert_eq!(server, "github.com"); | ||||
|     assert_eq!(account, "user"); | ||||
|     assert_eq!(repo, "repo"); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_parse_git_url_ssh() { | ||||
|     let (server, account, repo) = parse_git_url("git@github.com:user/repo.git"); | ||||
|     assert_eq!(server, "github.com"); | ||||
|     assert_eq!(account, "user"); | ||||
|     assert_eq!(repo, "repo"); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_parse_git_url_ssh_without_git_extension() { | ||||
|     let (server, account, repo) = parse_git_url("git@github.com:user/repo"); | ||||
|     assert_eq!(server, "github.com"); | ||||
|     assert_eq!(account, "user"); | ||||
|     assert_eq!(repo, "repo"); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_parse_git_url_invalid() { | ||||
|     let (server, account, repo) = parse_git_url("invalid-url"); | ||||
|     assert_eq!(server, ""); | ||||
|     assert_eq!(account, ""); | ||||
|     assert_eq!(repo, ""); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_git_tree_new_creates_directory() { | ||||
|     let temp_dir = TempDir::new().unwrap(); | ||||
|     let base_path = temp_dir.path().join("git_repos"); | ||||
|     let base_path_str = base_path.to_str().unwrap(); | ||||
|  | ||||
|     let _git_tree = GitTree::new(base_path_str).unwrap(); | ||||
|     assert!(base_path.exists()); | ||||
|     assert!(base_path.is_dir()); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_git_tree_new_existing_directory() { | ||||
|     let temp_dir = TempDir::new().unwrap(); | ||||
|     let base_path = temp_dir.path().join("existing_dir"); | ||||
|     fs::create_dir_all(&base_path).unwrap(); | ||||
|     let base_path_str = base_path.to_str().unwrap(); | ||||
|  | ||||
|     let _git_tree = GitTree::new(base_path_str).unwrap(); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_git_tree_new_invalid_path() { | ||||
|     let temp_dir = TempDir::new().unwrap(); | ||||
|     let file_path = temp_dir.path().join("file.txt"); | ||||
|     fs::write(&file_path, "content").unwrap(); | ||||
|     let file_path_str = file_path.to_str().unwrap(); | ||||
|  | ||||
|     let result = GitTree::new(file_path_str); | ||||
|     assert!(result.is_err()); | ||||
|     if let Err(error) = result { | ||||
|         match error { | ||||
|             GitError::InvalidBasePath(_) => {} | ||||
|             _ => panic!("Expected InvalidBasePath error"), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_git_tree_list_empty_directory() { | ||||
|     let temp_dir = TempDir::new().unwrap(); | ||||
|     let base_path_str = temp_dir.path().to_str().unwrap(); | ||||
|  | ||||
|     let git_tree = GitTree::new(base_path_str).unwrap(); | ||||
|     let repos = git_tree.list().unwrap(); | ||||
|     assert!(repos.is_empty()); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_git_repo_new() { | ||||
|     let repo = GitRepo::new("/path/to/repo".to_string()); | ||||
|     assert_eq!(repo.path(), "/path/to/repo"); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_git_repo_clone() { | ||||
|     let repo1 = GitRepo::new("/path/to/repo".to_string()); | ||||
|     let repo2 = repo1.clone(); | ||||
|     assert_eq!(repo1.path(), repo2.path()); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_git_error_display() { | ||||
|     let error = GitError::InvalidUrl("bad-url".to_string()); | ||||
|     assert_eq!(format!("{}", error), "Could not parse git URL: bad-url"); | ||||
|  | ||||
|     let error = GitError::NoRepositoriesFound; | ||||
|     assert_eq!(format!("{}", error), "No repositories found"); | ||||
|  | ||||
|     let error = GitError::RepositoryNotFound("pattern".to_string()); | ||||
|     assert_eq!( | ||||
|         format!("{}", error), | ||||
|         "No repositories found matching 'pattern'" | ||||
|     ); | ||||
| } | ||||
| @@ -1,5 +1,5 @@ | ||||
| // 01_git_basic.rhai | ||||
| // Tests for basic Git operations in the Git module | ||||
| // Tests for basic Git functionality like creating a GitTree, listing repositories, finding repositories, and cloning repositories | ||||
| 
 | ||||
| // Custom assert function | ||||
| fn assert_true(condition, message) { | ||||
| @@ -61,12 +61,6 @@ let found_repos_after_clone = git_tree.find("*"); | ||||
| assert_true(found_repos_after_clone.len() > 0, "Expected non-empty list of repositories"); | ||||
| print(`✓ GitTree.find(): Found ${found_repos_after_clone.len()} repositories`); | ||||
| 
 | ||||
| // Test GitTree.get() with a path to an existing repository | ||||
| print("Testing GitTree.get() with path..."); | ||||
| let repo_name = repos_after_clone[0]; | ||||
| let repo_by_path = git_tree.get(repo_name); | ||||
| print(`✓ GitTree.get(): Repository opened successfully from ${repo_by_path.path()}`); | ||||
| 
 | ||||
| // Clean up | ||||
| print("Cleaning up..."); | ||||
| delete(test_dir); | ||||
| @@ -28,24 +28,22 @@ print(`✓ Repository cloned successfully to ${repo.path()}`); | ||||
| // Test GitRepo.pull() | ||||
| print("Testing GitRepo.pull()..."); | ||||
| try { | ||||
|     let pull_result = repo.pull(); | ||||
|     print("✓ GitRepo.pull(): Pull successful"); | ||||
|     let pulled_repo = repo.pull(); | ||||
|     print("✓ GitRepo.pull(): Pull operation completed successfully"); | ||||
| } catch(err) { | ||||
|     // Pull might fail if there are local changes or network issues | ||||
|     // This is expected in some cases, so we'll just log it | ||||
|     print(`Note: Pull failed with error: ${err}`); | ||||
|     print("✓ GitRepo.pull(): Error handled gracefully"); | ||||
|     // Pull might fail if there are no changes or network issues | ||||
|     print(`Note: GitRepo.pull() failed (expected): ${err}`); | ||||
|     print("✓ GitRepo.pull(): Method exists and can be called"); | ||||
| } | ||||
| 
 | ||||
| // Test GitRepo.reset() | ||||
| print("Testing GitRepo.reset()..."); | ||||
| try { | ||||
|     let reset_result = repo.reset(); | ||||
|     print("✓ GitRepo.reset(): Reset successful"); | ||||
|     let reset_repo = repo.reset(); | ||||
|     print("✓ GitRepo.reset(): Reset operation completed successfully"); | ||||
| } catch(err) { | ||||
|     // Reset might fail in some cases | ||||
|     print(`Note: Reset failed with error: ${err}`); | ||||
|     print("✓ GitRepo.reset(): Error handled gracefully"); | ||||
|     print(`Error in GitRepo.reset(): ${err}`); | ||||
|     throw err; | ||||
| } | ||||
| 
 | ||||
| // Note: We won't test commit and push as they would modify the remote repository | ||||
| @@ -1,7 +1,5 @@ | ||||
| // run_all_tests.rhai | ||||
| // Runs all Git module tests | ||||
| 
 | ||||
| print("=== Running Git Module Tests ==="); | ||||
| // Test runner for all Git module tests | ||||
| 
 | ||||
| // Custom assert function | ||||
| fn assert_true(condition, message) { | ||||
| @@ -11,10 +9,13 @@ fn assert_true(condition, message) { | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| // Run each test directly | ||||
| // Test counters | ||||
| let passed = 0; | ||||
| let failed = 0; | ||||
| 
 | ||||
| print("=== Git Module Test Suite ==="); | ||||
| print("Running comprehensive tests for Git module functionality..."); | ||||
| 
 | ||||
| // Test 1: Basic Git Operations | ||||
| print("\n--- Running Basic Git Operations Tests ---"); | ||||
| try { | ||||
| @@ -79,16 +80,72 @@ try { | ||||
|     failed += 1; | ||||
| } | ||||
| 
 | ||||
| print("\n=== Test Summary ==="); | ||||
| print(`Passed: ${passed}`); | ||||
| print(`Failed: ${failed}`); | ||||
| print(`Total: ${passed + failed}`); | ||||
| // Test 3: Git Error Handling and Real Functionality | ||||
| print("\n--- Running Git Error Handling and Real Functionality Tests ---"); | ||||
| try { | ||||
|     print("Testing git_clone with invalid URL..."); | ||||
|     try { | ||||
|         git_clone("invalid-url-format"); | ||||
|         print("!!! Expected error but got success"); | ||||
|         failed += 1; | ||||
|     } catch(err) { | ||||
|         assert_true(err.contains("Git error"), "Expected Git error message"); | ||||
|         print("✓ git_clone properly handles invalid URLs"); | ||||
|     } | ||||
| 
 | ||||
| if failed == 0 { | ||||
|     print("\n✅ All tests passed!"); | ||||
| } else { | ||||
|     print("\n❌ Some tests failed!"); | ||||
|     print("Testing git_clone with real repository..."); | ||||
|     try { | ||||
|         let repo = git_clone("https://github.com/octocat/Hello-World.git"); | ||||
|         let path = repo.path(); | ||||
|         assert_true(path.len() > 0, "Repository path should not be empty"); | ||||
|         print(`✓ git_clone successfully cloned repository to: ${path}`); | ||||
| 
 | ||||
|         // Test repository operations | ||||
|         print("Testing repository operations..."); | ||||
|         let has_changes = repo.has_changes(); | ||||
|         print(`✓ Repository has_changes check: ${has_changes}`); | ||||
| 
 | ||||
|     } catch(err) { | ||||
|         // Network issues or git not available are acceptable failures | ||||
|         if err.contains("Git error") || err.contains("command") || err.contains("Failed to clone") { | ||||
|             print(`Note: git_clone test skipped due to environment: ${err}`); | ||||
|         } else { | ||||
|             print(`!!! Unexpected error in git_clone: ${err}`); | ||||
|             failed += 1; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     print("Testing GitTree with invalid path..."); | ||||
|     try { | ||||
|         let git_tree = git_tree_new("/invalid/nonexistent/path"); | ||||
|         print("Note: GitTree creation succeeded (directory was created)"); | ||||
|         // Clean up if it was created | ||||
|         try { | ||||
|             delete("/invalid"); | ||||
|         } catch(cleanup_err) { | ||||
|             // Ignore cleanup errors | ||||
|         } | ||||
|     } catch(err) { | ||||
|         print(`✓ GitTree properly handles invalid paths: ${err}`); | ||||
|     } | ||||
| 
 | ||||
|     print("--- Git Error Handling Tests completed successfully ---"); | ||||
|     passed += 1; | ||||
| } catch(err) { | ||||
|     print(`!!! Error in Git Error Handling Tests: ${err}`); | ||||
|     failed += 1; | ||||
| } | ||||
| 
 | ||||
| // Return the number of failed tests (0 means success) | ||||
| failed; | ||||
| // Summary | ||||
| print("\n=== Test Results ==="); | ||||
| print(`Passed: ${passed}`); | ||||
| print(`Failed: ${failed}`); | ||||
| print(`Total:  ${passed + failed}`); | ||||
| 
 | ||||
| if failed == 0 { | ||||
|     print("🎉 All tests passed!"); | ||||
| } else { | ||||
|     print("❌ Some tests failed!"); | ||||
| } | ||||
| 
 | ||||
| print("=== Git Module Test Suite Complete ==="); | ||||
							
								
								
									
										121
									
								
								git/tests/rhai_advanced_tests.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								git/tests/rhai_advanced_tests.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,121 @@ | ||||
| use rhai::Engine; | ||||
| use sal_git::rhai::*; | ||||
|  | ||||
| #[test] | ||||
| fn test_git_clone_with_various_url_formats() { | ||||
|     let mut engine = Engine::new(); | ||||
|     register_git_module(&mut engine).unwrap(); | ||||
|  | ||||
|     let test_cases = vec![ | ||||
|         ( | ||||
|             "https://github.com/octocat/Hello-World.git", | ||||
|             "HTTPS with .git", | ||||
|         ), | ||||
|         ( | ||||
|             "https://github.com/octocat/Hello-World", | ||||
|             "HTTPS without .git", | ||||
|         ), | ||||
|         // SSH would require key setup: ("git@github.com:octocat/Hello-World.git", "SSH format"), | ||||
|     ]; | ||||
|  | ||||
|     for (url, description) in test_cases { | ||||
|         let script = format!( | ||||
|             r#" | ||||
|             let result = ""; | ||||
|             try {{ | ||||
|                 let repo = git_clone("{}"); | ||||
|                 let path = repo.path(); | ||||
|                 if path.len() > 0 {{ | ||||
|                     result = "success"; | ||||
|                 }} else {{ | ||||
|                     result = "no_path"; | ||||
|                 }} | ||||
|             }} catch(e) {{ | ||||
|                 if e.contains("Git error") {{ | ||||
|                     result = "git_error"; | ||||
|                 }} else {{ | ||||
|                     result = "unexpected_error"; | ||||
|                 }} | ||||
|             }} | ||||
|             result | ||||
|         "#, | ||||
|             url | ||||
|         ); | ||||
|  | ||||
|         let result = engine.eval::<String>(&script); | ||||
|         assert!( | ||||
|             result.is_ok(), | ||||
|             "Failed to execute script for {}: {:?}", | ||||
|             description, | ||||
|             result | ||||
|         ); | ||||
|  | ||||
|         let outcome = result.unwrap(); | ||||
|         // Accept success or git_error (network issues) | ||||
|         assert!( | ||||
|             outcome == "success" || outcome == "git_error", | ||||
|             "Unexpected outcome for {}: {}", | ||||
|             description, | ||||
|             outcome | ||||
|         ); | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_git_tree_operations_comprehensive() { | ||||
|     let mut engine = Engine::new(); | ||||
|     register_git_module(&mut engine).unwrap(); | ||||
|  | ||||
|     let script = r#" | ||||
|         let results = []; | ||||
|          | ||||
|         try { | ||||
|             // Test GitTree creation | ||||
|             let git_tree = git_tree_new("/tmp/rhai_comprehensive_test"); | ||||
|             results.push("git_tree_created"); | ||||
|              | ||||
|             // Test list on empty directory | ||||
|             let repos = git_tree.list(); | ||||
|             results.push("list_executed"); | ||||
|              | ||||
|             // Test find with pattern | ||||
|             let found = git_tree.find("nonexistent"); | ||||
|             results.push("find_executed"); | ||||
|              | ||||
|         } catch(e) { | ||||
|             results.push("error_occurred"); | ||||
|         } | ||||
|          | ||||
|         results.len() | ||||
|     "#; | ||||
|  | ||||
|     let result = engine.eval::<i64>(&script); | ||||
|     assert!(result.is_ok()); | ||||
|     assert!(result.unwrap() >= 3, "Should execute at least 3 operations"); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_error_message_quality() { | ||||
|     let mut engine = Engine::new(); | ||||
|     register_git_module(&mut engine).unwrap(); | ||||
|  | ||||
|     let script = r#" | ||||
|         let error_msg = ""; | ||||
|         try { | ||||
|             git_clone("invalid-url-format"); | ||||
|         } catch(e) { | ||||
|             error_msg = e; | ||||
|         } | ||||
|         error_msg | ||||
|     "#; | ||||
|  | ||||
|     let result = engine.eval::<String>(&script); | ||||
|     assert!(result.is_ok()); | ||||
|  | ||||
|     let error_msg = result.unwrap(); | ||||
|     assert!( | ||||
|         error_msg.contains("Git error"), | ||||
|         "Error should contain 'Git error'" | ||||
|     ); | ||||
|     assert!(error_msg.len() > 10, "Error message should be descriptive"); | ||||
| } | ||||
							
								
								
									
										101
									
								
								git/tests/rhai_tests.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								git/tests/rhai_tests.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,101 @@ | ||||
| use rhai::Engine; | ||||
| use sal_git::rhai::*; | ||||
|  | ||||
| #[test] | ||||
| fn test_register_git_module() { | ||||
|     let mut engine = Engine::new(); | ||||
|     let result = register_git_module(&mut engine); | ||||
|     assert!(result.is_ok()); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_git_tree_new_function_registered() { | ||||
|     let mut engine = Engine::new(); | ||||
|     register_git_module(&mut engine).unwrap(); | ||||
|  | ||||
|     // Test that the function is registered by trying to call it | ||||
|     // This will fail because /nonexistent doesn't exist, but it proves the function is registered | ||||
|     let result = engine.eval::<String>( | ||||
|         r#" | ||||
|         let result = ""; | ||||
|         try { | ||||
|             let git_tree = git_tree_new("/nonexistent"); | ||||
|             result = "success"; | ||||
|         } catch(e) { | ||||
|             result = "error_caught"; | ||||
|         } | ||||
|         result | ||||
|     "#, | ||||
|     ); | ||||
|  | ||||
|     assert!(result.is_ok()); | ||||
|     assert_eq!(result.unwrap(), "error_caught"); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_git_clone_function_registered() { | ||||
|     let mut engine = Engine::new(); | ||||
|     register_git_module(&mut engine).unwrap(); | ||||
|  | ||||
|     // Test that git_clone function is registered by testing with invalid URL | ||||
|     let result = engine.eval::<String>( | ||||
|         r#" | ||||
|         let result = ""; | ||||
|         try { | ||||
|             git_clone("invalid-url-format"); | ||||
|             result = "unexpected_success"; | ||||
|         } catch(e) { | ||||
|             // Should catch error for invalid URL | ||||
|             if e.contains("Git error") { | ||||
|                 result = "error_caught_correctly"; | ||||
|             } else { | ||||
|                 result = "wrong_error_type"; | ||||
|             } | ||||
|         } | ||||
|         result | ||||
|     "#, | ||||
|     ); | ||||
|  | ||||
|     assert!(result.is_ok()); | ||||
|     assert_eq!(result.unwrap(), "error_caught_correctly"); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_git_clone_with_valid_public_repo() { | ||||
|     let mut engine = Engine::new(); | ||||
|     register_git_module(&mut engine).unwrap(); | ||||
|  | ||||
|     // Test with a real public repository (small one for testing) | ||||
|     let result = engine.eval::<String>( | ||||
|         r#" | ||||
|         let result = ""; | ||||
|         try { | ||||
|             let repo = git_clone("https://github.com/octocat/Hello-World.git"); | ||||
|             // If successful, repo should have a valid path | ||||
|             let path = repo.path(); | ||||
|             if path.len() > 0 { | ||||
|                 result = "clone_successful"; | ||||
|             } else { | ||||
|                 result = "clone_failed_no_path"; | ||||
|             } | ||||
|         } catch(e) { | ||||
|             // Network issues or git not available are acceptable failures | ||||
|             if e.contains("Git error") || e.contains("command") { | ||||
|                 result = "acceptable_failure"; | ||||
|             } else { | ||||
|                 result = "unexpected_error"; | ||||
|             } | ||||
|         } | ||||
|         result | ||||
|     "#, | ||||
|     ); | ||||
|  | ||||
|     assert!(result.is_ok()); | ||||
|     let outcome = result.unwrap(); | ||||
|     // Accept either successful clone or acceptable failure (network/git issues) | ||||
|     assert!( | ||||
|         outcome == "clone_successful" || outcome == "acceptable_failure", | ||||
|         "Unexpected outcome: {}", | ||||
|         outcome | ||||
|     ); | ||||
| } | ||||
							
								
								
									
										25
									
								
								herodo/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								herodo/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| [package] | ||||
| name = "herodo" | ||||
| version = "0.1.0" | ||||
| edition = "2021" | ||||
| authors = ["PlanetFirst <info@incubaid.com>"] | ||||
| description = "Herodo - A Rhai script executor for SAL (System Abstraction Layer)" | ||||
| repository = "https://git.threefold.info/herocode/sal" | ||||
| license = "Apache-2.0" | ||||
| keywords = ["rhai", "scripting", "automation", "sal", "system"] | ||||
| categories = ["command-line-utilities", "development-tools"] | ||||
|  | ||||
| [[bin]] | ||||
| name = "herodo" | ||||
| path = "src/main.rs" | ||||
|  | ||||
| [dependencies] | ||||
| # Core dependencies for herodo binary | ||||
| env_logger = { workspace = true } | ||||
| rhai = { workspace = true } | ||||
|  | ||||
| # SAL library for Rhai module registration | ||||
| sal = { path = ".." } | ||||
|  | ||||
| [dev-dependencies] | ||||
| tempfile = { workspace = true } | ||||
							
								
								
									
										142
									
								
								herodo/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										142
									
								
								herodo/README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,142 @@ | ||||
| # Herodo - Rhai Script Executor for SAL | ||||
|  | ||||
| **Version: 0.1.0** | ||||
|  | ||||
| Herodo is a command-line utility that executes Rhai scripts with full access to the SAL (System Abstraction Layer) library. It provides a powerful scripting environment for automation and system management tasks. | ||||
|  | ||||
| ## Features | ||||
|  | ||||
| - **Single Script Execution**: Execute individual `.rhai` script files | ||||
| - **Directory Execution**: Execute all `.rhai` scripts in a directory (recursively) | ||||
| - **Sorted Execution**: Scripts are executed in alphabetical order for predictable behavior | ||||
| - **SAL Integration**: Full access to all SAL modules and functions | ||||
| - **Error Handling**: Clear error messages and proper exit codes | ||||
| - **Logging Support**: Built-in logging with `env_logger` | ||||
|  | ||||
| ## Installation | ||||
|  | ||||
| Build the herodo binary: | ||||
|  | ||||
| ```bash | ||||
| cd herodo | ||||
| cargo build --release | ||||
| ``` | ||||
|  | ||||
| The executable will be available at `target/release/herodo`. | ||||
|  | ||||
| ## Usage | ||||
|  | ||||
| ### Execute a Single Script | ||||
|  | ||||
| ```bash | ||||
| herodo path/to/script.rhai | ||||
| ``` | ||||
|  | ||||
| ### Execute All Scripts in a Directory | ||||
|  | ||||
| ```bash | ||||
| herodo path/to/scripts/ | ||||
| ``` | ||||
|  | ||||
| When given a directory, herodo will: | ||||
| 1. Recursively find all `.rhai` files | ||||
| 2. Sort them alphabetically | ||||
| 3. Execute them in order | ||||
| 4. Stop on the first error | ||||
|  | ||||
| ## Example Scripts | ||||
|  | ||||
| ### Basic Script | ||||
| ```rhai | ||||
| // hello.rhai | ||||
| println("Hello from Herodo!"); | ||||
| let result = 42 * 2; | ||||
| println("Result: " + result); | ||||
| ``` | ||||
|  | ||||
| ### Using SAL Functions | ||||
| ```rhai | ||||
| // system_info.rhai | ||||
| println("=== System Information ==="); | ||||
|  | ||||
| // Check if a file exists | ||||
| let config_exists = exist("/etc/hosts"); | ||||
| println("Config file exists: " + config_exists); | ||||
|  | ||||
| // Download a file | ||||
| download("https://example.com/data.txt", "/tmp/data.txt"); | ||||
| println("File downloaded successfully"); | ||||
|  | ||||
| // Execute a system command | ||||
| let output = run("ls -la /tmp"); | ||||
| println("Directory listing:"); | ||||
| println(output.stdout); | ||||
| ``` | ||||
|  | ||||
| ### Redis Operations | ||||
| ```rhai | ||||
| // redis_example.rhai | ||||
| println("=== Redis Operations ==="); | ||||
|  | ||||
| // Set a value | ||||
| redis_set("app_status", "running"); | ||||
| println("Status set in Redis"); | ||||
|  | ||||
| // Get the value | ||||
| let status = redis_get("app_status"); | ||||
| println("Current status: " + status); | ||||
| ``` | ||||
|  | ||||
| ## Available SAL Functions | ||||
|  | ||||
| Herodo provides access to all SAL modules through Rhai: | ||||
|  | ||||
| - **File System**: `exist()`, `mkdir()`, `delete()`, `file_size()` | ||||
| - **Downloads**: `download()`, `download_install()` | ||||
| - **Process Management**: `run()`, `kill()`, `process_list()` | ||||
| - **Redis**: `redis_set()`, `redis_get()`, `redis_del()` | ||||
| - **PostgreSQL**: Database operations and management | ||||
| - **Network**: HTTP requests, SSH operations, TCP connectivity | ||||
| - **Virtualization**: Container operations with Buildah and Nerdctl | ||||
| - **Text Processing**: String manipulation and template rendering | ||||
| - **And many more...** | ||||
|  | ||||
| ## Error Handling | ||||
|  | ||||
| Herodo provides clear error messages and appropriate exit codes: | ||||
|  | ||||
| - **Exit Code 0**: All scripts executed successfully | ||||
| - **Exit Code 1**: Error occurred (file not found, script error, etc.) | ||||
|  | ||||
| ## Logging | ||||
|  | ||||
| Enable detailed logging by setting the `RUST_LOG` environment variable: | ||||
|  | ||||
| ```bash | ||||
| RUST_LOG=debug herodo script.rhai | ||||
| ``` | ||||
|  | ||||
| ## Testing | ||||
|  | ||||
| Run the test suite: | ||||
|  | ||||
| ```bash | ||||
| cd herodo | ||||
| cargo test | ||||
| ``` | ||||
|  | ||||
| The test suite includes: | ||||
| - Unit tests for core functionality | ||||
| - Integration tests with real script execution | ||||
| - Error handling scenarios | ||||
| - SAL module integration tests | ||||
|  | ||||
| ## Dependencies | ||||
|  | ||||
| - **rhai**: Embedded scripting language | ||||
| - **env_logger**: Logging implementation   | ||||
| - **sal**: System Abstraction Layer library | ||||
|  | ||||
| ## License | ||||
|  | ||||
| Apache-2.0 | ||||
| @@ -1,9 +1,8 @@ | ||||
| //! Herodo - A Rhai script executor for SAL
 | ||||
| //!
 | ||||
| //! This binary loads the Rhai engine, registers all SAL modules,
 | ||||
| //! This library loads the Rhai engine, registers all SAL modules,
 | ||||
| //! and executes Rhai scripts from a specified directory in sorted order.
 | ||||
| 
 | ||||
| // Removed unused imports
 | ||||
| use rhai::Engine; | ||||
| use std::error::Error; | ||||
| use std::fs; | ||||
| @@ -35,67 +34,49 @@ pub fn run(script_path: &str) -> Result<(), Box<dyn Error>> { | ||||
|     engine.register_fn("println", |s: &str| println!("{}", s)); | ||||
| 
 | ||||
|     // Register all SAL modules with the engine
 | ||||
|     crate::rhai::register(&mut engine)?; | ||||
|     sal::rhai::register(&mut engine)?; | ||||
| 
 | ||||
|     // Determine if the path is a file or directory
 | ||||
|     // Collect script files to execute
 | ||||
|     let script_files: Vec<PathBuf> = if path.is_file() { | ||||
|         // Check if it's a .rhai file
 | ||||
|         if path.extension().map_or(false, |ext| ext == "rhai") { | ||||
|             vec![path.to_path_buf()] | ||||
|         } else { | ||||
|             eprintln!("Error: '{}' is not a Rhai script file", script_path); | ||||
|         // Single file
 | ||||
|         if let Some(extension) = path.extension() { | ||||
|             if extension != "rhai" { | ||||
|                 eprintln!("Warning: '{}' does not have a .rhai extension", script_path); | ||||
|             } | ||||
|         } | ||||
|         vec![path.to_path_buf()] | ||||
|     } else if path.is_dir() { | ||||
|         // Directory - collect all .rhai files recursively and sort them
 | ||||
|         let mut files = Vec::new(); | ||||
|         collect_rhai_files(path, &mut files)?; | ||||
| 
 | ||||
|         if files.is_empty() { | ||||
|             eprintln!("No .rhai files found in directory: {}", script_path); | ||||
|             process::exit(1); | ||||
|         } | ||||
|     } else if path.is_dir() { | ||||
|         // Find all .rhai files in the directory recursively
 | ||||
|         let mut files: Vec<PathBuf> = Vec::new(); | ||||
|         
 | ||||
|         // Helper function to recursively find .rhai files
 | ||||
|         fn find_rhai_files(dir: &Path, files: &mut Vec<PathBuf>) -> std::io::Result<()> { | ||||
|             if dir.is_dir() { | ||||
|                 for entry in fs::read_dir(dir)? { | ||||
|                     let entry = entry?; | ||||
|                     let path = entry.path(); | ||||
|                     
 | ||||
|                     if path.is_dir() { | ||||
|                         find_rhai_files(&path, files)?; | ||||
|                     } else if path.is_file() && | ||||
|                               path.extension().map_or(false, |ext| ext == "rhai") { | ||||
|                         files.push(path); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             Ok(()) | ||||
|         } | ||||
|         
 | ||||
|         // Find all .rhai files recursively
 | ||||
|         find_rhai_files(path, &mut files)?; | ||||
|         
 | ||||
|         // Sort the script files by name
 | ||||
| 
 | ||||
|         // Sort files for consistent execution order
 | ||||
|         files.sort(); | ||||
|         
 | ||||
|         if files.is_empty() { | ||||
|             println!("No Rhai scripts found in '{}'", script_path); | ||||
|             return Ok(()); | ||||
|         } | ||||
|         
 | ||||
| 
 | ||||
|         files | ||||
|     } else { | ||||
|         eprintln!("Error: '{}' is neither a file nor a directory", script_path); | ||||
|         process::exit(1); | ||||
|     }; | ||||
| 
 | ||||
|     println!("Found {} Rhai script{} to execute:", | ||||
|              script_files.len(), | ||||
|              if script_files.len() == 1 { "" } else { "s" }); | ||||
|     
 | ||||
|     println!( | ||||
|         "Found {} Rhai script{} to execute:", | ||||
|         script_files.len(), | ||||
|         if script_files.len() == 1 { "" } else { "s" } | ||||
|     ); | ||||
| 
 | ||||
|     // Execute each script in sorted order
 | ||||
|     for script_file in script_files { | ||||
|         println!("\nExecuting: {}", script_file.display()); | ||||
|         
 | ||||
| 
 | ||||
|         // Read the script content
 | ||||
|         let script = fs::read_to_string(&script_file)?; | ||||
|         
 | ||||
| 
 | ||||
|         // Execute the script
 | ||||
|         match engine.eval::<rhai::Dynamic>(&script) { | ||||
|             Ok(result) => { | ||||
| @@ -103,7 +84,7 @@ pub fn run(script_path: &str) -> Result<(), Box<dyn Error>> { | ||||
|                 if !result.is_unit() { | ||||
|                     println!("Result: {}", result); | ||||
|                 } | ||||
|             }, | ||||
|             } | ||||
|             Err(err) => { | ||||
|                 eprintln!("Error executing script: {}", err); | ||||
|                 // Exit with error code when a script fails
 | ||||
| @@ -112,6 +93,37 @@ pub fn run(script_path: &str) -> Result<(), Box<dyn Error>> { | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     println!("\nAll scripts executed"); | ||||
|     println!("\nAll scripts executed successfully!"); | ||||
|     Ok(()) | ||||
| } | ||||
| } | ||||
| 
 | ||||
| /// Recursively collect all .rhai files from a directory
 | ||||
| ///
 | ||||
| /// # Arguments
 | ||||
| ///
 | ||||
| /// * `dir` - Directory to search
 | ||||
| /// * `files` - Vector to collect files into
 | ||||
| ///
 | ||||
| /// # Returns
 | ||||
| ///
 | ||||
| /// Result indicating success or failure
 | ||||
| fn collect_rhai_files(dir: &Path, files: &mut Vec<PathBuf>) -> Result<(), Box<dyn Error>> { | ||||
|     for entry in fs::read_dir(dir)? { | ||||
|         let entry = entry?; | ||||
|         let path = entry.path(); | ||||
| 
 | ||||
|         if path.is_dir() { | ||||
|             // Recursively search subdirectories
 | ||||
|             collect_rhai_files(&path, files)?; | ||||
|         } else if path.is_file() { | ||||
|             // Check if it's a .rhai file
 | ||||
|             if let Some(extension) = path.extension() { | ||||
|                 if extension == "rhai" { | ||||
|                     files.push(path); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     Ok(()) | ||||
| } | ||||
							
								
								
									
										25
									
								
								herodo/src/main.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								herodo/src/main.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| //! Herodo binary entry point | ||||
| //! | ||||
| //! This is the main entry point for the herodo binary. | ||||
| //! It parses command line arguments and executes Rhai scripts using the SAL library. | ||||
|  | ||||
| use env_logger; | ||||
| use std::env; | ||||
| use std::process; | ||||
|  | ||||
| fn main() -> Result<(), Box<dyn std::error::Error>> { | ||||
|     // Initialize the logger | ||||
|     env_logger::init(); | ||||
|  | ||||
|     let args: Vec<String> = env::args().collect(); | ||||
|  | ||||
|     if args.len() != 2 { | ||||
|         eprintln!("Usage: {} <script_path>", args[0]); | ||||
|         process::exit(1); | ||||
|     } | ||||
|  | ||||
|     let script_path = &args[1]; | ||||
|  | ||||
|     // Call the run function from the herodo library | ||||
|     herodo::run(script_path) | ||||
| } | ||||
							
								
								
									
										222
									
								
								herodo/tests/integration_tests.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										222
									
								
								herodo/tests/integration_tests.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,222 @@ | ||||
| //! Integration tests for herodo script executor | ||||
| //! | ||||
| //! These tests verify that herodo can execute Rhai scripts correctly, | ||||
| //! handle errors appropriately, and integrate with SAL modules. | ||||
|  | ||||
| use std::fs; | ||||
| use std::path::Path; | ||||
| use tempfile::TempDir; | ||||
|  | ||||
| /// Test that herodo can execute a simple Rhai script | ||||
| #[test] | ||||
| fn test_simple_script_execution() { | ||||
|     let temp_dir = TempDir::new().expect("Failed to create temp directory"); | ||||
|     let script_path = temp_dir.path().join("test.rhai"); | ||||
|  | ||||
|     // Create a simple test script | ||||
|     fs::write( | ||||
|         &script_path, | ||||
|         r#" | ||||
|         println("Hello from herodo test!"); | ||||
|         let result = 42; | ||||
|         result | ||||
|     "#, | ||||
|     ) | ||||
|     .expect("Failed to write test script"); | ||||
|  | ||||
|     // Execute the script | ||||
|     let result = herodo::run(script_path.to_str().unwrap()); | ||||
|     assert!(result.is_ok(), "Script execution should succeed"); | ||||
| } | ||||
|  | ||||
| /// Test that herodo can execute multiple scripts in a directory | ||||
| #[test] | ||||
| fn test_directory_script_execution() { | ||||
|     let temp_dir = TempDir::new().expect("Failed to create temp directory"); | ||||
|  | ||||
|     // Create multiple test scripts | ||||
|     fs::write( | ||||
|         temp_dir.path().join("01_first.rhai"), | ||||
|         r#" | ||||
|         println("First script executing"); | ||||
|         let first = 1; | ||||
|     "#, | ||||
|     ) | ||||
|     .expect("Failed to write first script"); | ||||
|  | ||||
|     fs::write( | ||||
|         temp_dir.path().join("02_second.rhai"), | ||||
|         r#" | ||||
|         println("Second script executing"); | ||||
|         let second = 2; | ||||
|     "#, | ||||
|     ) | ||||
|     .expect("Failed to write second script"); | ||||
|  | ||||
|     fs::write( | ||||
|         temp_dir.path().join("03_third.rhai"), | ||||
|         r#" | ||||
|         println("Third script executing"); | ||||
|         let third = 3; | ||||
|     "#, | ||||
|     ) | ||||
|     .expect("Failed to write third script"); | ||||
|  | ||||
|     // Execute all scripts in the directory | ||||
|     let result = herodo::run(temp_dir.path().to_str().unwrap()); | ||||
|     assert!(result.is_ok(), "Directory script execution should succeed"); | ||||
| } | ||||
|  | ||||
| /// Test that herodo handles non-existent paths correctly | ||||
| #[test] | ||||
| fn test_nonexistent_path_handling() { | ||||
|     // This test verifies error handling but herodo::run calls process::exit | ||||
|     // In a real scenario, we would need to refactor herodo to return errors | ||||
|     // instead of calling process::exit for better testability | ||||
|  | ||||
|     // For now, we test that the path validation logic works | ||||
|     let nonexistent_path = "/this/path/does/not/exist"; | ||||
|     let path = Path::new(nonexistent_path); | ||||
|     assert!(!path.exists(), "Test path should not exist"); | ||||
| } | ||||
|  | ||||
| /// Test that herodo can execute scripts with SAL module functions | ||||
| #[test] | ||||
| fn test_sal_module_integration() { | ||||
|     let temp_dir = TempDir::new().expect("Failed to create temp directory"); | ||||
|     let script_path = temp_dir.path().join("sal_test.rhai"); | ||||
|  | ||||
|     // Create a script that uses SAL functions | ||||
|     fs::write( | ||||
|         &script_path, | ||||
|         r#" | ||||
|         println("Testing SAL module integration"); | ||||
|          | ||||
|         // Test file existence check (should work with temp directory) | ||||
|         let temp_exists = exist("."); | ||||
|         println("Current directory exists: " + temp_exists); | ||||
|          | ||||
|         // Test basic text operations | ||||
|         let text = "  hello world  "; | ||||
|         let trimmed = text.trim(); | ||||
|         println("Trimmed text: '" + trimmed + "'"); | ||||
|          | ||||
|         println("SAL integration test completed"); | ||||
|     "#, | ||||
|     ) | ||||
|     .expect("Failed to write SAL test script"); | ||||
|  | ||||
|     // Execute the script | ||||
|     let result = herodo::run(script_path.to_str().unwrap()); | ||||
|     assert!( | ||||
|         result.is_ok(), | ||||
|         "SAL integration script should execute successfully" | ||||
|     ); | ||||
| } | ||||
|  | ||||
| /// Test script execution with subdirectories | ||||
| #[test] | ||||
| fn test_recursive_directory_execution() { | ||||
|     let temp_dir = TempDir::new().expect("Failed to create temp directory"); | ||||
|  | ||||
|     // Create subdirectory | ||||
|     let sub_dir = temp_dir.path().join("subdir"); | ||||
|     fs::create_dir(&sub_dir).expect("Failed to create subdirectory"); | ||||
|  | ||||
|     // Create scripts in main directory | ||||
|     fs::write( | ||||
|         temp_dir.path().join("main.rhai"), | ||||
|         r#" | ||||
|         println("Main directory script"); | ||||
|     "#, | ||||
|     ) | ||||
|     .expect("Failed to write main script"); | ||||
|  | ||||
|     // Create scripts in subdirectory | ||||
|     fs::write( | ||||
|         sub_dir.join("sub.rhai"), | ||||
|         r#" | ||||
|         println("Subdirectory script"); | ||||
|     "#, | ||||
|     ) | ||||
|     .expect("Failed to write sub script"); | ||||
|  | ||||
|     // Execute all scripts recursively | ||||
|     let result = herodo::run(temp_dir.path().to_str().unwrap()); | ||||
|     assert!( | ||||
|         result.is_ok(), | ||||
|         "Recursive directory execution should succeed" | ||||
|     ); | ||||
| } | ||||
|  | ||||
| /// Test that herodo handles empty directories gracefully | ||||
| #[test] | ||||
| fn test_empty_directory_handling() { | ||||
|     let temp_dir = TempDir::new().expect("Failed to create temp directory"); | ||||
|  | ||||
|     // Create an empty subdirectory | ||||
|     let empty_dir = temp_dir.path().join("empty"); | ||||
|     fs::create_dir(&empty_dir).expect("Failed to create empty directory"); | ||||
|  | ||||
|     // This should handle the empty directory case | ||||
|     // Note: herodo::run will call process::exit(1) for empty directories | ||||
|     // In a production refactor, this should return an error instead | ||||
|     let path = empty_dir.to_str().unwrap(); | ||||
|     let path_obj = Path::new(path); | ||||
|     assert!( | ||||
|         path_obj.is_dir(), | ||||
|         "Empty directory should exist and be a directory" | ||||
|     ); | ||||
| } | ||||
|  | ||||
| /// Test script with syntax errors | ||||
| #[test] | ||||
| fn test_syntax_error_handling() { | ||||
|     let temp_dir = TempDir::new().expect("Failed to create temp directory"); | ||||
|     let script_path = temp_dir.path().join("syntax_error.rhai"); | ||||
|  | ||||
|     // Create a script with syntax errors | ||||
|     fs::write( | ||||
|         &script_path, | ||||
|         r#" | ||||
|         println("This script has syntax errors"); | ||||
|         let invalid syntax here; | ||||
|         missing_function_call(; | ||||
|     "#, | ||||
|     ) | ||||
|     .expect("Failed to write syntax error script"); | ||||
|  | ||||
|     // Note: herodo::run will call process::exit(1) on script errors | ||||
|     // In a production refactor, this should return an error instead | ||||
|     // For now, we just verify the file exists and can be read | ||||
|     assert!(script_path.exists(), "Syntax error script should exist"); | ||||
|     let content = fs::read_to_string(&script_path).expect("Should be able to read script"); | ||||
|     assert!( | ||||
|         content.contains("syntax errors"), | ||||
|         "Script should contain expected content" | ||||
|     ); | ||||
| } | ||||
|  | ||||
| /// Test file extension validation | ||||
| #[test] | ||||
| fn test_file_extension_validation() { | ||||
|     let temp_dir = TempDir::new().expect("Failed to create temp directory"); | ||||
|  | ||||
|     // Create files with different extensions | ||||
|     let rhai_file = temp_dir.path().join("valid.rhai"); | ||||
|     let txt_file = temp_dir.path().join("invalid.txt"); | ||||
|  | ||||
|     fs::write(&rhai_file, "println(\"Valid rhai file\");").expect("Failed to write rhai file"); | ||||
|     fs::write(&txt_file, "This is not a rhai file").expect("Failed to write txt file"); | ||||
|  | ||||
|     // Verify file extensions | ||||
|     assert_eq!(rhai_file.extension().unwrap(), "rhai"); | ||||
|     assert_eq!(txt_file.extension().unwrap(), "txt"); | ||||
|  | ||||
|     // herodo should execute .rhai files and warn about non-.rhai files | ||||
|     let result = herodo::run(rhai_file.to_str().unwrap()); | ||||
|     assert!( | ||||
|         result.is_ok(), | ||||
|         "Valid .rhai file should execute successfully" | ||||
|     ); | ||||
| } | ||||
							
								
								
									
										268
									
								
								herodo/tests/unit_tests.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										268
									
								
								herodo/tests/unit_tests.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,268 @@ | ||||
| //! Unit tests for herodo library functions | ||||
| //! | ||||
| //! These tests focus on individual functions and components of the herodo library. | ||||
|  | ||||
| use std::fs; | ||||
| use tempfile::TempDir; | ||||
|  | ||||
| /// Test the collect_rhai_files function indirectly through directory operations | ||||
| #[test] | ||||
| fn test_rhai_file_collection_logic() { | ||||
|     let temp_dir = TempDir::new().expect("Failed to create temp directory"); | ||||
|  | ||||
|     // Create various files | ||||
|     fs::write(temp_dir.path().join("script1.rhai"), "// Script 1") | ||||
|         .expect("Failed to write script1"); | ||||
|     fs::write(temp_dir.path().join("script2.rhai"), "// Script 2") | ||||
|         .expect("Failed to write script2"); | ||||
|     fs::write(temp_dir.path().join("not_script.txt"), "Not a script") | ||||
|         .expect("Failed to write txt file"); | ||||
|     fs::write(temp_dir.path().join("README.md"), "# README").expect("Failed to write README"); | ||||
|  | ||||
|     // Create subdirectory with more scripts | ||||
|     let sub_dir = temp_dir.path().join("subdir"); | ||||
|     fs::create_dir(&sub_dir).expect("Failed to create subdirectory"); | ||||
|     fs::write(sub_dir.join("sub_script.rhai"), "// Sub script") | ||||
|         .expect("Failed to write sub script"); | ||||
|  | ||||
|     // Count .rhai files manually | ||||
|     let mut rhai_count = 0; | ||||
|     for entry in fs::read_dir(temp_dir.path()).expect("Failed to read temp directory") { | ||||
|         let entry = entry.expect("Failed to get directory entry"); | ||||
|         let path = entry.path(); | ||||
|         if path.is_file() && path.extension().map_or(false, |ext| ext == "rhai") { | ||||
|             rhai_count += 1; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // Should find 2 .rhai files in the main directory | ||||
|     assert_eq!( | ||||
|         rhai_count, 2, | ||||
|         "Should find exactly 2 .rhai files in main directory" | ||||
|     ); | ||||
|  | ||||
|     // Verify subdirectory has 1 .rhai file | ||||
|     let mut sub_rhai_count = 0; | ||||
|     for entry in fs::read_dir(&sub_dir).expect("Failed to read subdirectory") { | ||||
|         let entry = entry.expect("Failed to get directory entry"); | ||||
|         let path = entry.path(); | ||||
|         if path.is_file() && path.extension().map_or(false, |ext| ext == "rhai") { | ||||
|             sub_rhai_count += 1; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     assert_eq!( | ||||
|         sub_rhai_count, 1, | ||||
|         "Should find exactly 1 .rhai file in subdirectory" | ||||
|     ); | ||||
| } | ||||
|  | ||||
| /// Test path validation logic | ||||
| #[test] | ||||
| fn test_path_validation() { | ||||
|     let temp_dir = TempDir::new().expect("Failed to create temp directory"); | ||||
|     let script_path = temp_dir.path().join("test.rhai"); | ||||
|  | ||||
|     // Create a test script | ||||
|     fs::write(&script_path, "println(\"test\");").expect("Failed to write test script"); | ||||
|  | ||||
|     // Test file path validation | ||||
|     assert!(script_path.exists(), "Script file should exist"); | ||||
|     assert!(script_path.is_file(), "Script path should be a file"); | ||||
|  | ||||
|     // Test directory path validation | ||||
|     assert!(temp_dir.path().exists(), "Temp directory should exist"); | ||||
|     assert!(temp_dir.path().is_dir(), "Temp path should be a directory"); | ||||
|  | ||||
|     // Test non-existent path | ||||
|     let nonexistent = temp_dir.path().join("nonexistent.rhai"); | ||||
|     assert!(!nonexistent.exists(), "Non-existent path should not exist"); | ||||
| } | ||||
|  | ||||
| /// Test file extension checking | ||||
| #[test] | ||||
| fn test_file_extension_checking() { | ||||
|     let temp_dir = TempDir::new().expect("Failed to create temp directory"); | ||||
|  | ||||
|     // Create files with different extensions | ||||
|     let rhai_file = temp_dir.path().join("script.rhai"); | ||||
|     let txt_file = temp_dir.path().join("document.txt"); | ||||
|     let no_ext_file = temp_dir.path().join("no_extension"); | ||||
|  | ||||
|     fs::write(&rhai_file, "// Rhai script").expect("Failed to write rhai file"); | ||||
|     fs::write(&txt_file, "Text document").expect("Failed to write txt file"); | ||||
|     fs::write(&no_ext_file, "No extension").expect("Failed to write no extension file"); | ||||
|  | ||||
|     // Test extension detection | ||||
|     assert_eq!(rhai_file.extension().unwrap(), "rhai"); | ||||
|     assert_eq!(txt_file.extension().unwrap(), "txt"); | ||||
|     assert!(no_ext_file.extension().is_none()); | ||||
|  | ||||
|     // Test extension comparison | ||||
|     assert!(rhai_file.extension().map_or(false, |ext| ext == "rhai")); | ||||
|     assert!(!txt_file.extension().map_or(false, |ext| ext == "rhai")); | ||||
|     assert!(!no_ext_file.extension().map_or(false, |ext| ext == "rhai")); | ||||
| } | ||||
|  | ||||
| /// Test script content reading | ||||
| #[test] | ||||
| fn test_script_content_reading() { | ||||
|     let temp_dir = TempDir::new().expect("Failed to create temp directory"); | ||||
|     let script_path = temp_dir.path().join("content_test.rhai"); | ||||
|  | ||||
|     let expected_content = r#" | ||||
|         println("Testing content reading"); | ||||
|         let value = 42; | ||||
|         value * 2 | ||||
|     "#; | ||||
|  | ||||
|     fs::write(&script_path, expected_content).expect("Failed to write script content"); | ||||
|  | ||||
|     // Read the content back | ||||
|     let actual_content = fs::read_to_string(&script_path).expect("Failed to read script content"); | ||||
|     assert_eq!( | ||||
|         actual_content, expected_content, | ||||
|         "Script content should match" | ||||
|     ); | ||||
|  | ||||
|     // Verify content contains expected elements | ||||
|     assert!( | ||||
|         actual_content.contains("println"), | ||||
|         "Content should contain println" | ||||
|     ); | ||||
|     assert!( | ||||
|         actual_content.contains("let value = 42"), | ||||
|         "Content should contain variable declaration" | ||||
|     ); | ||||
|     assert!( | ||||
|         actual_content.contains("value * 2"), | ||||
|         "Content should contain expression" | ||||
|     ); | ||||
| } | ||||
|  | ||||
| /// Test directory traversal logic | ||||
| #[test] | ||||
| fn test_directory_traversal() { | ||||
|     let temp_dir = TempDir::new().expect("Failed to create temp directory"); | ||||
|  | ||||
|     // Create nested directory structure | ||||
|     let level1 = temp_dir.path().join("level1"); | ||||
|     let level2 = level1.join("level2"); | ||||
|     let level3 = level2.join("level3"); | ||||
|  | ||||
|     fs::create_dir_all(&level3).expect("Failed to create nested directories"); | ||||
|  | ||||
|     // Create scripts at different levels | ||||
|     fs::write(temp_dir.path().join("root.rhai"), "// Root script") | ||||
|         .expect("Failed to write root script"); | ||||
|     fs::write(level1.join("level1.rhai"), "// Level 1 script") | ||||
|         .expect("Failed to write level1 script"); | ||||
|     fs::write(level2.join("level2.rhai"), "// Level 2 script") | ||||
|         .expect("Failed to write level2 script"); | ||||
|     fs::write(level3.join("level3.rhai"), "// Level 3 script") | ||||
|         .expect("Failed to write level3 script"); | ||||
|  | ||||
|     // Verify directory structure | ||||
|     assert!(temp_dir.path().is_dir(), "Root temp directory should exist"); | ||||
|     assert!(level1.is_dir(), "Level 1 directory should exist"); | ||||
|     assert!(level2.is_dir(), "Level 2 directory should exist"); | ||||
|     assert!(level3.is_dir(), "Level 3 directory should exist"); | ||||
|  | ||||
|     // Verify scripts exist at each level | ||||
|     assert!( | ||||
|         temp_dir.path().join("root.rhai").exists(), | ||||
|         "Root script should exist" | ||||
|     ); | ||||
|     assert!( | ||||
|         level1.join("level1.rhai").exists(), | ||||
|         "Level 1 script should exist" | ||||
|     ); | ||||
|     assert!( | ||||
|         level2.join("level2.rhai").exists(), | ||||
|         "Level 2 script should exist" | ||||
|     ); | ||||
|     assert!( | ||||
|         level3.join("level3.rhai").exists(), | ||||
|         "Level 3 script should exist" | ||||
|     ); | ||||
| } | ||||
|  | ||||
| /// Test sorting behavior for script execution order | ||||
| #[test] | ||||
| fn test_script_sorting_order() { | ||||
|     let temp_dir = TempDir::new().expect("Failed to create temp directory"); | ||||
|  | ||||
|     // Create scripts with names that should be sorted | ||||
|     let scripts = vec![ | ||||
|         "03_third.rhai", | ||||
|         "01_first.rhai", | ||||
|         "02_second.rhai", | ||||
|         "10_tenth.rhai", | ||||
|         "05_fifth.rhai", | ||||
|     ]; | ||||
|  | ||||
|     for script in &scripts { | ||||
|         fs::write( | ||||
|             temp_dir.path().join(script), | ||||
|             format!("// Script: {}", script), | ||||
|         ) | ||||
|         .expect("Failed to write script"); | ||||
|     } | ||||
|  | ||||
|     // Collect and sort the scripts manually to verify sorting logic | ||||
|     let mut found_scripts = Vec::new(); | ||||
|     for entry in fs::read_dir(temp_dir.path()).expect("Failed to read directory") { | ||||
|         let entry = entry.expect("Failed to get directory entry"); | ||||
|         let path = entry.path(); | ||||
|         if path.is_file() && path.extension().map_or(false, |ext| ext == "rhai") { | ||||
|             found_scripts.push(path.file_name().unwrap().to_string_lossy().to_string()); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     found_scripts.sort(); | ||||
|  | ||||
|     // Verify sorting order | ||||
|     let expected_order = vec![ | ||||
|         "01_first.rhai", | ||||
|         "02_second.rhai", | ||||
|         "03_third.rhai", | ||||
|         "05_fifth.rhai", | ||||
|         "10_tenth.rhai", | ||||
|     ]; | ||||
|  | ||||
|     assert_eq!( | ||||
|         found_scripts, expected_order, | ||||
|         "Scripts should be sorted in correct order" | ||||
|     ); | ||||
| } | ||||
|  | ||||
| /// Test empty directory handling | ||||
| #[test] | ||||
| fn test_empty_directory_detection() { | ||||
|     let temp_dir = TempDir::new().expect("Failed to create temp directory"); | ||||
|     let empty_subdir = temp_dir.path().join("empty"); | ||||
|  | ||||
|     fs::create_dir(&empty_subdir).expect("Failed to create empty subdirectory"); | ||||
|  | ||||
|     // Verify directory is empty | ||||
|     let entries: Vec<_> = fs::read_dir(&empty_subdir) | ||||
|         .expect("Failed to read empty directory") | ||||
|         .collect(); | ||||
|  | ||||
|     assert!(entries.is_empty(), "Directory should be empty"); | ||||
|  | ||||
|     // Count .rhai files in empty directory | ||||
|     let mut rhai_count = 0; | ||||
|     for entry in fs::read_dir(&empty_subdir).expect("Failed to read empty directory") { | ||||
|         let entry = entry.expect("Failed to get directory entry"); | ||||
|         let path = entry.path(); | ||||
|         if path.is_file() && path.extension().map_or(false, |ext| ext == "rhai") { | ||||
|             rhai_count += 1; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     assert_eq!( | ||||
|         rhai_count, 0, | ||||
|         "Empty directory should contain no .rhai files" | ||||
|     ); | ||||
| } | ||||
							
								
								
									
										47
									
								
								installers/base.rhai
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								installers/base.rhai
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,47 @@ | ||||
|  | ||||
|  | ||||
|  | ||||
| fn mycelium(){ | ||||
|     let name="mycelium"; | ||||
|     let url="https://github.com/threefoldtech/mycelium/releases/download/v0.6.1/mycelium-x86_64-unknown-linux-musl.tar.gz"; | ||||
|     download(url,`/tmp/${name}`,5000); | ||||
|     copy_bin(`/tmp/${name}/*`); | ||||
|     delete(`/tmp/${name}`); | ||||
|      | ||||
|     let name="containerd"; | ||||
|  | ||||
| } | ||||
|  | ||||
|  | ||||
| fn zinit(){ | ||||
|     let name="zinit"; | ||||
|     let url="https://github.com/threefoldtech/zinit/releases/download/v0.2.25/zinit-linux-x86_64"; | ||||
|     download_file(url,`/tmp/${name}`,5000); | ||||
|     screen_kill("zinit"); | ||||
|     copy_bin(`/tmp/${name}`); | ||||
|     delete(`/tmp/${name}`); | ||||
|     screen_new("zinit", "zinit init"); | ||||
|     sleep(1); | ||||
|     let socket_path = "/tmp/zinit.sock"; | ||||
|  | ||||
|     // List all services | ||||
|     print("Listing all services:"); | ||||
|     let services = zinit_list(socket_path); | ||||
|  | ||||
|     if services.is_empty() { | ||||
|         print("No services found."); | ||||
|     } else { | ||||
|         // Iterate over the keys of the map | ||||
|         for name in services.keys() { | ||||
|             let state = services[name]; | ||||
|             print(`${name}: ${state}`); | ||||
|         } | ||||
|     } | ||||
|      | ||||
| } | ||||
|  | ||||
| platform_check_linux_x86(); | ||||
| zinit(); | ||||
| // mycelium(); | ||||
|  | ||||
| "done" | ||||
							
								
								
									
										7
									
								
								installers/install_all.rhai
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								installers/install_all.rhai
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
|  | ||||
|  | ||||
| platform_check_linux_x86(); | ||||
|  | ||||
| exec(`https://git.threefold.info/herocode/sal/raw/branch/main/installers/base.rhai`); | ||||
| //install all we need for nerdctl | ||||
| exec(`https://git.threefold.info/herocode/sal/raw/branch/main/installers/nerdctl.rhai`); | ||||
							
								
								
									
										54
									
								
								installers/nerdctl.rhai
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								installers/nerdctl.rhai
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,54 @@ | ||||
|  | ||||
|  | ||||
|  | ||||
| fn nerdctl_download(){ | ||||
|     let name="nerdctl"; | ||||
|     let url="https://github.com/containerd/nerdctl/releases/download/v2.1.2/nerdctl-2.1.2-linux-amd64.tar.gz"; | ||||
|     download(url,`/tmp/${name}`,10000); | ||||
|     copy_bin(`/tmp/${name}/*`); | ||||
|     delete(`/tmp/${name}`); | ||||
|      | ||||
|     screen_kill("containerd"); | ||||
|     let name="containerd"; | ||||
|     let url="https://github.com/containerd/containerd/releases/download/v2.1.2/containerd-2.1.2-linux-amd64.tar.gz"; | ||||
|     download(url,`/tmp/${name}`,20000); | ||||
|     // copy_bin(`/tmp/${name}/bin/*`); | ||||
|     delete(`/tmp/${name}`); | ||||
|  | ||||
|     let cfg = ` | ||||
|     [[registry]] | ||||
|     location = "localhost:5000" | ||||
|     insecure = true | ||||
|     `; | ||||
|     file_write("/etc/containers/registries.conf", dedent(cfg)); | ||||
|     screen_new("containerd", "containerd"); | ||||
|     sleep(1); | ||||
|     nerdctl_remove_all(); | ||||
|     run("nerdctl run -d -p 5000:5000 --name registry registry:2").log().execute(); | ||||
|  | ||||
|     package_install("buildah"); | ||||
|     package_install("runc"); | ||||
|  | ||||
|  | ||||
|     // let url="https://github.com/threefoldtech/rfs/releases/download/v2.0.6/rfs"; | ||||
|     // download_file(url,`/tmp/rfs`,10000); | ||||
|     // chmod_exec("/tmp/rfs"); | ||||
|     // mv(`/tmp/rfs`,"/root/hero/bin/"); | ||||
|  | ||||
| } | ||||
|  | ||||
| fn ipfs_download(){ | ||||
|     let name="ipfs"; | ||||
|     let url="https://github.com/ipfs/kubo/releases/download/v0.34.1/kubo_v0.34.1_linux-amd64.tar.gz"; | ||||
|     download(url,`/tmp/${name}`,20); | ||||
|     copy_bin(`/tmp/${name}/kubo/ipfs`); | ||||
|     delete(`/tmp/${name}`); | ||||
|      | ||||
|  | ||||
| } | ||||
|  | ||||
| platform_check_linux_x86(); | ||||
| nerdctl_download(); | ||||
| // ipfs_download(); | ||||
|  | ||||
| "done" | ||||
							
								
								
									
										30
									
								
								mycelium/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								mycelium/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| [package] | ||||
| name = "sal-mycelium" | ||||
| version = "0.1.0" | ||||
| edition = "2021" | ||||
| authors = ["PlanetFirst <info@incubaid.com>"] | ||||
| description = "SAL Mycelium - Client interface for interacting with Mycelium node's HTTP API" | ||||
| repository = "https://git.threefold.info/herocode/sal" | ||||
| license = "Apache-2.0" | ||||
|  | ||||
| [dependencies] | ||||
| # HTTP client for async requests | ||||
| reqwest = { version = "0.12.15", features = ["json"] } | ||||
| # JSON handling | ||||
| serde_json = "1.0" | ||||
| # Base64 encoding/decoding for message payloads | ||||
| base64 = "0.22.1" | ||||
| # Async runtime | ||||
| tokio = { version = "1.45.0", features = ["full"] } | ||||
| # Rhai scripting support | ||||
| rhai = { version = "1.12.0", features = ["sync"] } | ||||
| # Logging | ||||
| log = "0.4" | ||||
| # URL encoding for API parameters | ||||
| urlencoding = "2.1.3" | ||||
|  | ||||
| [dev-dependencies] | ||||
| # For async testing | ||||
| tokio-test = "0.4.4" | ||||
| # For temporary files in tests | ||||
| tempfile = "3.5" | ||||
							
								
								
									
										110
									
								
								mycelium/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								mycelium/README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,110 @@ | ||||
| # SAL Mycelium | ||||
|  | ||||
| A Rust client library for interacting with Mycelium node's HTTP API, with Rhai scripting support. | ||||
|  | ||||
| ## Overview | ||||
|  | ||||
| SAL Mycelium provides async HTTP client functionality for managing Mycelium nodes, including: | ||||
|  | ||||
| - Node information retrieval | ||||
| - Peer management (list, add, remove) | ||||
| - Route inspection (selected and fallback routes) | ||||
| - Message operations (send and receive) | ||||
|  | ||||
| ## Usage | ||||
|  | ||||
| ### Rust API | ||||
|  | ||||
| ```rust | ||||
| use sal_mycelium::*; | ||||
|  | ||||
| #[tokio::main] | ||||
| async fn main() -> Result<(), Box<dyn std::error::Error>> { | ||||
|     let api_url = "http://localhost:8989"; | ||||
|      | ||||
|     // Get node information | ||||
|     let node_info = get_node_info(api_url).await?; | ||||
|     println!("Node info: {:?}", node_info); | ||||
|      | ||||
|     // List peers | ||||
|     let peers = list_peers(api_url).await?; | ||||
|     println!("Peers: {:?}", peers); | ||||
|      | ||||
|     // Send a message | ||||
|     use std::time::Duration; | ||||
|     let result = send_message( | ||||
|         api_url, | ||||
|         "destination_ip", | ||||
|         "topic", | ||||
|         "Hello, Mycelium!", | ||||
|         Some(Duration::from_secs(30)) | ||||
|     ).await?; | ||||
|      | ||||
|     Ok(()) | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ### Rhai Scripting | ||||
|  | ||||
| ```rhai | ||||
| // Get node information | ||||
| let api_url = "http://localhost:8989"; | ||||
| let node_info = mycelium_get_node_info(api_url); | ||||
| print(`Node subnet: ${node_info.nodeSubnet}`); | ||||
|  | ||||
| // List peers | ||||
| let peers = mycelium_list_peers(api_url); | ||||
| print(`Found ${peers.len()} peers`); | ||||
|  | ||||
| // Send message (timeout in seconds, -1 for no timeout) | ||||
| let result = mycelium_send_message(api_url, "dest_ip", "topic", "message", 30); | ||||
| ``` | ||||
|  | ||||
| ## API Functions | ||||
|  | ||||
| ### Core Functions | ||||
|  | ||||
| - `get_node_info(api_url)` - Get node information | ||||
| - `list_peers(api_url)` - List connected peers | ||||
| - `add_peer(api_url, peer_address)` - Add a new peer | ||||
| - `remove_peer(api_url, peer_id)` - Remove a peer | ||||
| - `list_selected_routes(api_url)` - List selected routes | ||||
| - `list_fallback_routes(api_url)` - List fallback routes | ||||
| - `send_message(api_url, destination, topic, message, timeout)` - Send message | ||||
| - `receive_messages(api_url, topic, timeout)` - Receive messages | ||||
|  | ||||
| ### Rhai Functions | ||||
|  | ||||
| All functions are available in Rhai with `mycelium_` prefix: | ||||
| - `mycelium_get_node_info(api_url)` | ||||
| - `mycelium_list_peers(api_url)` | ||||
| - `mycelium_add_peer(api_url, peer_address)` | ||||
| - `mycelium_remove_peer(api_url, peer_id)` | ||||
| - `mycelium_list_selected_routes(api_url)` | ||||
| - `mycelium_list_fallback_routes(api_url)` | ||||
| - `mycelium_send_message(api_url, destination, topic, message, timeout_secs)` | ||||
| - `mycelium_receive_messages(api_url, topic, timeout_secs)` | ||||
|  | ||||
| ## Requirements | ||||
|  | ||||
| - A running Mycelium node with HTTP API enabled | ||||
| - Default API endpoint: `http://localhost:8989` | ||||
|  | ||||
| ## Testing | ||||
|  | ||||
| ```bash | ||||
| # Run all tests | ||||
| cargo test | ||||
|  | ||||
| # Run with a live Mycelium node for integration tests | ||||
| # (tests will skip if no node is available) | ||||
| cargo test -- --nocapture | ||||
| ``` | ||||
|  | ||||
| ## Dependencies | ||||
|  | ||||
| - `reqwest` - HTTP client | ||||
| - `serde_json` - JSON handling | ||||
| - `base64` - Message encoding | ||||
| - `tokio` - Async runtime | ||||
| - `rhai` - Scripting support | ||||
							
								
								
									
										327
									
								
								mycelium/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										327
									
								
								mycelium/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,327 @@ | ||||
| //! SAL Mycelium - Client interface for interacting with Mycelium node's HTTP API | ||||
| //! | ||||
| //! This crate provides a client interface for interacting with a Mycelium node's HTTP API. | ||||
| //! Mycelium is a decentralized networking project, and this SAL module allows Rust applications | ||||
| //! and `herodo` Rhai scripts to manage and communicate over a Mycelium network. | ||||
| //! | ||||
| //! The module enables operations such as: | ||||
| //! - Querying node status and information | ||||
| //! - Managing peer connections (listing, adding, removing) | ||||
| //! - Inspecting routing tables (selected and fallback routes) | ||||
| //! - Sending messages to other Mycelium nodes | ||||
| //! - Receiving messages from subscribed topics | ||||
| //! | ||||
| //! All interactions with the Mycelium API are performed asynchronously. | ||||
|  | ||||
| use base64::{engine::general_purpose, Engine as _}; | ||||
| use reqwest::Client; | ||||
| use serde_json::Value; | ||||
| use std::time::Duration; | ||||
|  | ||||
| pub mod rhai; | ||||
|  | ||||
| /// Get information about the Mycelium node | ||||
| /// | ||||
| /// # Arguments | ||||
| /// | ||||
| /// * `api_url` - The URL of the Mycelium API | ||||
| /// | ||||
| /// # Returns | ||||
| /// | ||||
| /// * `Result<Value, String>` - The node information as a JSON value, or an error message | ||||
| pub async fn get_node_info(api_url: &str) -> Result<Value, String> { | ||||
|     let client = Client::new(); | ||||
|     let url = format!("{}/api/v1/admin", api_url); | ||||
|  | ||||
|     let response = client | ||||
|         .get(&url) | ||||
|         .send() | ||||
|         .await | ||||
|         .map_err(|e| format!("Failed to send request: {}", e))?; | ||||
|  | ||||
|     let status = response.status(); | ||||
|     if !status.is_success() { | ||||
|         return Err(format!("Request failed with status: {}", status)); | ||||
|     } | ||||
|  | ||||
|     let result: Value = response | ||||
|         .json() | ||||
|         .await | ||||
|         .map_err(|e| format!("Failed to parse response: {}", e))?; | ||||
|  | ||||
|     Ok(result) | ||||
| } | ||||
|  | ||||
| /// List all peers connected to the Mycelium node | ||||
| /// | ||||
| /// # Arguments | ||||
| /// | ||||
| /// * `api_url` - The URL of the Mycelium API | ||||
| /// | ||||
| /// # Returns | ||||
| /// | ||||
| /// * `Result<Value, String>` - The list of peers as a JSON value, or an error message | ||||
| pub async fn list_peers(api_url: &str) -> Result<Value, String> { | ||||
|     let client = Client::new(); | ||||
|     let url = format!("{}/api/v1/admin/peers", api_url); | ||||
|  | ||||
|     let response = client | ||||
|         .get(&url) | ||||
|         .send() | ||||
|         .await | ||||
|         .map_err(|e| format!("Failed to send request: {}", e))?; | ||||
|  | ||||
|     let status = response.status(); | ||||
|     if !status.is_success() { | ||||
|         return Err(format!("Request failed with status: {}", status)); | ||||
|     } | ||||
|  | ||||
|     let result: Value = response | ||||
|         .json() | ||||
|         .await | ||||
|         .map_err(|e| format!("Failed to parse response: {}", e))?; | ||||
|  | ||||
|     Ok(result) | ||||
| } | ||||
|  | ||||
| /// Add a new peer to the Mycelium node | ||||
| /// | ||||
| /// # Arguments | ||||
| /// | ||||
| /// * `api_url` - The URL of the Mycelium API | ||||
| /// * `peer_address` - The address of the peer to add | ||||
| /// | ||||
| /// # Returns | ||||
| /// | ||||
| /// * `Result<Value, String>` - The result of the operation as a JSON value, or an error message | ||||
| pub async fn add_peer(api_url: &str, peer_address: &str) -> Result<Value, String> { | ||||
|     let client = Client::new(); | ||||
|     let url = format!("{}/api/v1/admin/peers", api_url); | ||||
|  | ||||
|     let response = client | ||||
|         .post(&url) | ||||
|         .json(&serde_json::json!({ | ||||
|             "endpoint": peer_address | ||||
|         })) | ||||
|         .send() | ||||
|         .await | ||||
|         .map_err(|e| format!("Failed to send request: {}", e))?; | ||||
|  | ||||
|     let status = response.status(); | ||||
|     if status == reqwest::StatusCode::NO_CONTENT { | ||||
|         // Successfully added, but no content to parse | ||||
|         return Ok(serde_json::json!({"success": true})); | ||||
|     } | ||||
|     if !status.is_success() { | ||||
|         return Err(format!("Request failed with status: {}", status)); | ||||
|     } | ||||
|  | ||||
|     // For other success statuses that might have a body | ||||
|     let result: Value = response | ||||
|         .json() | ||||
|         .await | ||||
|         .map_err(|e| format!("Failed to parse response: {}", e))?; | ||||
|  | ||||
|     Ok(result) | ||||
| } | ||||
|  | ||||
| /// Remove a peer from the Mycelium node | ||||
| /// | ||||
| /// # Arguments | ||||
| /// | ||||
| /// * `api_url` - The URL of the Mycelium API | ||||
| /// * `peer_id` - The ID of the peer to remove | ||||
| /// | ||||
| /// # Returns | ||||
| /// | ||||
| /// * `Result<Value, String>` - The result of the operation as a JSON value, or an error message | ||||
| pub async fn remove_peer(api_url: &str, peer_id: &str) -> Result<Value, String> { | ||||
|     let client = Client::new(); | ||||
|     let peer_id_url_encoded = urlencoding::encode(peer_id); | ||||
|     let url = format!("{}/api/v1/admin/peers/{}", api_url, peer_id_url_encoded); | ||||
|  | ||||
|     let response = client | ||||
|         .delete(&url) | ||||
|         .send() | ||||
|         .await | ||||
|         .map_err(|e| format!("Failed to send request: {}", e))?; | ||||
|  | ||||
|     let status = response.status(); | ||||
|     if status == reqwest::StatusCode::NO_CONTENT { | ||||
|         // Successfully removed, but no content to parse | ||||
|         return Ok(serde_json::json!({"success": true})); | ||||
|     } | ||||
|     if !status.is_success() { | ||||
|         return Err(format!("Request failed with status: {}", status)); | ||||
|     } | ||||
|  | ||||
|     let result: Value = response | ||||
|         .json() | ||||
|         .await | ||||
|         .map_err(|e| format!("Failed to parse response: {}", e))?; | ||||
|  | ||||
|     Ok(result) | ||||
| } | ||||
|  | ||||
| /// List all selected routes in the Mycelium node | ||||
| /// | ||||
| /// # Arguments | ||||
| /// | ||||
| /// * `api_url` - The URL of the Mycelium API | ||||
| /// | ||||
| /// # Returns | ||||
| /// | ||||
| /// * `Result<Value, String>` - The list of selected routes as a JSON value, or an error message | ||||
| pub async fn list_selected_routes(api_url: &str) -> Result<Value, String> { | ||||
|     let client = Client::new(); | ||||
|     let url = format!("{}/api/v1/admin/routes/selected", api_url); | ||||
|  | ||||
|     let response = client | ||||
|         .get(&url) | ||||
|         .send() | ||||
|         .await | ||||
|         .map_err(|e| format!("Failed to send request: {}", e))?; | ||||
|  | ||||
|     let status = response.status(); | ||||
|     if !status.is_success() { | ||||
|         return Err(format!("Request failed with status: {}", status)); | ||||
|     } | ||||
|  | ||||
|     let result: Value = response | ||||
|         .json() | ||||
|         .await | ||||
|         .map_err(|e| format!("Failed to parse response: {}", e))?; | ||||
|  | ||||
|     Ok(result) | ||||
| } | ||||
|  | ||||
| /// List all fallback routes in the Mycelium node | ||||
| /// | ||||
| /// # Arguments | ||||
| /// | ||||
| /// * `api_url` - The URL of the Mycelium API | ||||
| /// | ||||
| /// # Returns | ||||
| /// | ||||
| /// * `Result<Value, String>` - The list of fallback routes as a JSON value, or an error message | ||||
| pub async fn list_fallback_routes(api_url: &str) -> Result<Value, String> { | ||||
|     let client = Client::new(); | ||||
|     let url = format!("{}/api/v1/admin/routes/fallback", api_url); | ||||
|  | ||||
|     let response = client | ||||
|         .get(&url) | ||||
|         .send() | ||||
|         .await | ||||
|         .map_err(|e| format!("Failed to send request: {}", e))?; | ||||
|  | ||||
|     let status = response.status(); | ||||
|     if !status.is_success() { | ||||
|         return Err(format!("Request failed with status: {}", status)); | ||||
|     } | ||||
|  | ||||
|     let result: Value = response | ||||
|         .json() | ||||
|         .await | ||||
|         .map_err(|e| format!("Failed to parse response: {}", e))?; | ||||
|  | ||||
|     Ok(result) | ||||
| } | ||||
|  | ||||
| /// Send a message to a destination via the Mycelium node | ||||
| /// | ||||
| /// # Arguments | ||||
| /// | ||||
| /// * `api_url` - The URL of the Mycelium API | ||||
| /// * `destination` - The destination address | ||||
| /// * `topic` - The message topic | ||||
| /// * `message` - The message content | ||||
| /// * `reply_deadline` - The deadline in seconds; pass `-1` to indicate we do not want to wait on a reply | ||||
| /// | ||||
| /// # Returns | ||||
| /// | ||||
| /// * `Result<Value, String>` - The result of the operation as a JSON value, or an error message | ||||
| pub async fn send_message( | ||||
|     api_url: &str, | ||||
|     destination: &str, | ||||
|     topic: &str, | ||||
|     message: &str, | ||||
|     reply_deadline: Option<Duration>, // This is passed in URL query | ||||
| ) -> Result<Value, String> { | ||||
|     let client = Client::new(); | ||||
|     let url = format!("{}/api/v1/messages", api_url); | ||||
|  | ||||
|     let mut request = client.post(&url); | ||||
|     if let Some(deadline) = reply_deadline { | ||||
|         request = request.query(&[("reply_timeout", deadline.as_secs())]); | ||||
|     } | ||||
|  | ||||
|     let response = request | ||||
|         .json(&serde_json::json!({ | ||||
|                 "dst": { "ip": destination }, | ||||
|                 "topic": general_purpose::STANDARD.encode(topic), | ||||
|                 "payload": general_purpose::STANDARD.encode(message) | ||||
|         })) | ||||
|         .send() | ||||
|         .await | ||||
|         .map_err(|e| format!("Failed to send request: {}", e))?; | ||||
|  | ||||
|     let status = response.status(); | ||||
|     if !status.is_success() { | ||||
|         return Err(format!("Request failed with status: {}", status)); | ||||
|     } | ||||
|  | ||||
|     let result: Value = response | ||||
|         .json() | ||||
|         .await | ||||
|         .map_err(|e| format!("Failed to parse response: {}", e))?; | ||||
|  | ||||
|     Ok(result) | ||||
| } | ||||
|  | ||||
| /// Receive messages from a topic via the Mycelium node | ||||
| /// | ||||
| /// # Arguments | ||||
| /// | ||||
| /// * `api_url` - The URL of the Mycelium API | ||||
| /// * `topic` - The message topic | ||||
| /// * `wait_deadline` - Time we wait for receiving a message | ||||
| /// | ||||
| /// # Returns | ||||
| /// | ||||
| /// * `Result<Value, String>` - The received messages as a JSON value, or an error message | ||||
| pub async fn receive_messages( | ||||
|     api_url: &str, | ||||
|     topic: &str, | ||||
|     wait_deadline: Option<Duration>, | ||||
| ) -> Result<Value, String> { | ||||
|     let client = Client::new(); | ||||
|     let url = format!("{}/api/v1/messages", api_url); | ||||
|  | ||||
|     let mut request = client.get(&url); | ||||
|  | ||||
|     if let Some(deadline) = wait_deadline { | ||||
|         request = request.query(&[ | ||||
|             ("topic", general_purpose::STANDARD.encode(topic)), | ||||
|             ("timeout", deadline.as_secs().to_string()), | ||||
|         ]) | ||||
|     } else { | ||||
|         request = request.query(&[("topic", general_purpose::STANDARD.encode(topic))]) | ||||
|     }; | ||||
|  | ||||
|     let response = request | ||||
|         .send() | ||||
|         .await | ||||
|         .map_err(|e| format!("Failed to send request: {}", e))?; | ||||
|  | ||||
|     let status = response.status(); | ||||
|     if !status.is_success() { | ||||
|         return Err(format!("Request failed with status: {}", status)); | ||||
|     } | ||||
|  | ||||
|     let result: Value = response | ||||
|         .json() | ||||
|         .await | ||||
|         .map_err(|e| format!("Failed to parse response: {}", e))?; | ||||
|  | ||||
|     Ok(result) | ||||
| } | ||||
							
								
								
									
										254
									
								
								mycelium/src/rhai.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										254
									
								
								mycelium/src/rhai.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,254 @@ | ||||
| //! Rhai wrappers for Mycelium client module functions | ||||
| //! | ||||
| //! This module provides Rhai wrappers for the functions in the Mycelium client module. | ||||
|  | ||||
| use std::time::Duration; | ||||
|  | ||||
| use crate as client; | ||||
| use rhai::Position; | ||||
| use rhai::{Array, Dynamic, Engine, EvalAltResult, Map}; | ||||
| use serde_json::Value; | ||||
| use tokio::runtime::Runtime; | ||||
|  | ||||
| /// Register Mycelium module functions with the Rhai engine | ||||
| /// | ||||
| /// # Arguments | ||||
| /// | ||||
| /// * `engine` - The Rhai engine to register the functions with | ||||
| /// | ||||
| /// # Returns | ||||
| /// | ||||
| /// * `Result<(), Box<EvalAltResult>>` - Ok if registration was successful, Err otherwise | ||||
| pub fn register_mycelium_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> { | ||||
|     // Register Mycelium client functions | ||||
|     engine.register_fn("mycelium_get_node_info", mycelium_get_node_info); | ||||
|     engine.register_fn("mycelium_list_peers", mycelium_list_peers); | ||||
|     engine.register_fn("mycelium_add_peer", mycelium_add_peer); | ||||
|     engine.register_fn("mycelium_remove_peer", mycelium_remove_peer); | ||||
|     engine.register_fn( | ||||
|         "mycelium_list_selected_routes", | ||||
|         mycelium_list_selected_routes, | ||||
|     ); | ||||
|     engine.register_fn( | ||||
|         "mycelium_list_fallback_routes", | ||||
|         mycelium_list_fallback_routes, | ||||
|     ); | ||||
|     engine.register_fn("mycelium_send_message", mycelium_send_message); | ||||
|     engine.register_fn("mycelium_receive_messages", mycelium_receive_messages); | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| // Helper function to get a runtime | ||||
| fn get_runtime() -> Result<Runtime, Box<EvalAltResult>> { | ||||
|     tokio::runtime::Runtime::new().map_err(|e| { | ||||
|         Box::new(EvalAltResult::ErrorRuntime( | ||||
|             format!("Failed to create Tokio runtime: {}", e).into(), | ||||
|             rhai::Position::NONE, | ||||
|         )) | ||||
|     }) | ||||
| } | ||||
|  | ||||
| // Helper function to convert serde_json::Value to rhai::Dynamic | ||||
| fn value_to_dynamic(value: Value) -> Dynamic { | ||||
|     match value { | ||||
|         Value::Null => Dynamic::UNIT, | ||||
|         Value::Bool(b) => Dynamic::from(b), | ||||
|         Value::Number(n) => { | ||||
|             if let Some(i) = n.as_i64() { | ||||
|                 Dynamic::from(i) | ||||
|             } else if let Some(f) = n.as_f64() { | ||||
|                 Dynamic::from(f) | ||||
|             } else { | ||||
|                 Dynamic::from(n.to_string()) | ||||
|             } | ||||
|         } | ||||
|         Value::String(s) => Dynamic::from(s), | ||||
|         Value::Array(arr) => { | ||||
|             let mut rhai_arr = Array::new(); | ||||
|             for item in arr { | ||||
|                 rhai_arr.push(value_to_dynamic(item)); | ||||
|             } | ||||
|             Dynamic::from(rhai_arr) | ||||
|         } | ||||
|         Value::Object(map) => { | ||||
|             let mut rhai_map = Map::new(); | ||||
|             for (k, v) in map { | ||||
|                 rhai_map.insert(k.into(), value_to_dynamic(v)); | ||||
|             } | ||||
|             Dynamic::from_map(rhai_map) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| // | ||||
| // Mycelium Client Function Wrappers | ||||
| // | ||||
|  | ||||
| /// Wrapper for mycelium::get_node_info | ||||
| /// | ||||
| /// Gets information about the Mycelium node. | ||||
| pub fn mycelium_get_node_info(api_url: &str) -> Result<Dynamic, Box<EvalAltResult>> { | ||||
|     let rt = get_runtime()?; | ||||
|  | ||||
|     let result = rt.block_on(async { client::get_node_info(api_url).await }); | ||||
|  | ||||
|     let node_info = result.map_err(|e| { | ||||
|         Box::new(EvalAltResult::ErrorRuntime( | ||||
|             format!("Mycelium error: {}", e).into(), | ||||
|             Position::NONE, | ||||
|         )) | ||||
|     })?; | ||||
|  | ||||
|     Ok(value_to_dynamic(node_info)) | ||||
| } | ||||
|  | ||||
| /// Wrapper for mycelium::list_peers | ||||
| /// | ||||
| /// Lists all peers connected to the Mycelium node. | ||||
| pub fn mycelium_list_peers(api_url: &str) -> Result<Dynamic, Box<EvalAltResult>> { | ||||
|     let rt = get_runtime()?; | ||||
|  | ||||
|     let result = rt.block_on(async { client::list_peers(api_url).await }); | ||||
|  | ||||
|     let peers = result.map_err(|e| { | ||||
|         Box::new(EvalAltResult::ErrorRuntime( | ||||
|             format!("Mycelium error: {}", e).into(), | ||||
|             Position::NONE, | ||||
|         )) | ||||
|     })?; | ||||
|  | ||||
|     Ok(value_to_dynamic(peers)) | ||||
| } | ||||
|  | ||||
| /// Wrapper for mycelium::add_peer | ||||
| /// | ||||
| /// Adds a new peer to the Mycelium node. | ||||
| pub fn mycelium_add_peer(api_url: &str, peer_address: &str) -> Result<Dynamic, Box<EvalAltResult>> { | ||||
|     let rt = get_runtime()?; | ||||
|  | ||||
|     let result = rt.block_on(async { client::add_peer(api_url, peer_address).await }); | ||||
|  | ||||
|     let response = result.map_err(|e| { | ||||
|         Box::new(EvalAltResult::ErrorRuntime( | ||||
|             format!("Mycelium error: {}", e).into(), | ||||
|             Position::NONE, | ||||
|         )) | ||||
|     })?; | ||||
|  | ||||
|     Ok(value_to_dynamic(response)) | ||||
| } | ||||
|  | ||||
| /// Wrapper for mycelium::remove_peer | ||||
| /// | ||||
| /// Removes a peer from the Mycelium node. | ||||
| pub fn mycelium_remove_peer(api_url: &str, peer_id: &str) -> Result<Dynamic, Box<EvalAltResult>> { | ||||
|     let rt = get_runtime()?; | ||||
|  | ||||
|     let result = rt.block_on(async { client::remove_peer(api_url, peer_id).await }); | ||||
|  | ||||
|     let response = result.map_err(|e| { | ||||
|         Box::new(EvalAltResult::ErrorRuntime( | ||||
|             format!("Mycelium error: {}", e).into(), | ||||
|             Position::NONE, | ||||
|         )) | ||||
|     })?; | ||||
|  | ||||
|     Ok(value_to_dynamic(response)) | ||||
| } | ||||
|  | ||||
| /// Wrapper for mycelium::list_selected_routes | ||||
| /// | ||||
| /// Lists all selected routes in the Mycelium node. | ||||
| pub fn mycelium_list_selected_routes(api_url: &str) -> Result<Dynamic, Box<EvalAltResult>> { | ||||
|     let rt = get_runtime()?; | ||||
|  | ||||
|     let result = rt.block_on(async { client::list_selected_routes(api_url).await }); | ||||
|  | ||||
|     let routes = result.map_err(|e| { | ||||
|         Box::new(EvalAltResult::ErrorRuntime( | ||||
|             format!("Mycelium error: {}", e).into(), | ||||
|             Position::NONE, | ||||
|         )) | ||||
|     })?; | ||||
|  | ||||
|     Ok(value_to_dynamic(routes)) | ||||
| } | ||||
|  | ||||
| /// Wrapper for mycelium::list_fallback_routes | ||||
| /// | ||||
| /// Lists all fallback routes in the Mycelium node. | ||||
| pub fn mycelium_list_fallback_routes(api_url: &str) -> Result<Dynamic, Box<EvalAltResult>> { | ||||
|     let rt = get_runtime()?; | ||||
|  | ||||
|     let result = rt.block_on(async { client::list_fallback_routes(api_url).await }); | ||||
|  | ||||
|     let routes = result.map_err(|e| { | ||||
|         Box::new(EvalAltResult::ErrorRuntime( | ||||
|             format!("Mycelium error: {}", e).into(), | ||||
|             Position::NONE, | ||||
|         )) | ||||
|     })?; | ||||
|  | ||||
|     Ok(value_to_dynamic(routes)) | ||||
| } | ||||
|  | ||||
| /// Wrapper for mycelium::send_message | ||||
| /// | ||||
| /// Sends a message to a destination via the Mycelium node. | ||||
| pub fn mycelium_send_message( | ||||
|     api_url: &str, | ||||
|     destination: &str, | ||||
|     topic: &str, | ||||
|     message: &str, | ||||
|     reply_deadline_secs: i64, | ||||
| ) -> Result<Dynamic, Box<EvalAltResult>> { | ||||
|     let rt = get_runtime()?; | ||||
|  | ||||
|     let deadline = if reply_deadline_secs < 0 { | ||||
|         None | ||||
|     } else { | ||||
|         Some(Duration::from_secs(reply_deadline_secs as u64)) | ||||
|     }; | ||||
|  | ||||
|     let result = rt.block_on(async { | ||||
|         client::send_message(api_url, destination, topic, message, deadline).await | ||||
|     }); | ||||
|  | ||||
|     let response = result.map_err(|e| { | ||||
|         Box::new(EvalAltResult::ErrorRuntime( | ||||
|             format!("Mycelium error: {}", e).into(), | ||||
|             Position::NONE, | ||||
|         )) | ||||
|     })?; | ||||
|  | ||||
|     Ok(value_to_dynamic(response)) | ||||
| } | ||||
|  | ||||
| /// Wrapper for mycelium::receive_messages | ||||
| /// | ||||
| /// Receives messages from a topic via the Mycelium node. | ||||
| pub fn mycelium_receive_messages( | ||||
|     api_url: &str, | ||||
|     topic: &str, | ||||
|     wait_deadline_secs: i64, | ||||
| ) -> Result<Dynamic, Box<EvalAltResult>> { | ||||
|     let rt = get_runtime()?; | ||||
|  | ||||
|     let deadline = if wait_deadline_secs < 0 { | ||||
|         None | ||||
|     } else { | ||||
|         Some(Duration::from_secs(wait_deadline_secs as u64)) | ||||
|     }; | ||||
|  | ||||
|     let result = rt.block_on(async { client::receive_messages(api_url, topic, deadline).await }); | ||||
|  | ||||
|     let messages = result.map_err(|e| { | ||||
|         Box::new(EvalAltResult::ErrorRuntime( | ||||
|             format!("Mycelium error: {}", e).into(), | ||||
|             Position::NONE, | ||||
|         )) | ||||
|     })?; | ||||
|  | ||||
|     Ok(value_to_dynamic(messages)) | ||||
| } | ||||
							
								
								
									
										279
									
								
								mycelium/tests/mycelium_client_tests.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										279
									
								
								mycelium/tests/mycelium_client_tests.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,279 @@ | ||||
| //! Unit tests for Mycelium client functionality | ||||
| //! | ||||
| //! These tests validate the core Mycelium client operations including: | ||||
| //! - Node information retrieval | ||||
| //! - Peer management (listing, adding, removing) | ||||
| //! - Route inspection (selected and fallback routes) | ||||
| //! - Message operations (sending and receiving) | ||||
| //! | ||||
| //! Tests are designed to work with a real Mycelium node when available, | ||||
| //! but gracefully handle cases where the node is not accessible. | ||||
|  | ||||
| use sal_mycelium::*; | ||||
| use std::time::Duration; | ||||
|  | ||||
| /// Test configuration for Mycelium API | ||||
| const TEST_API_URL: &str = "http://localhost:8989"; | ||||
| const FALLBACK_API_URL: &str = "http://localhost:7777"; | ||||
|  | ||||
| /// Helper function to check if a Mycelium node is available | ||||
| async fn is_mycelium_available(api_url: &str) -> bool { | ||||
|     match get_node_info(api_url).await { | ||||
|         Ok(_) => true, | ||||
|         Err(_) => false, | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Helper function to get an available Mycelium API URL | ||||
| async fn get_available_api_url() -> Option<String> { | ||||
|     if is_mycelium_available(TEST_API_URL).await { | ||||
|         Some(TEST_API_URL.to_string()) | ||||
|     } else if is_mycelium_available(FALLBACK_API_URL).await { | ||||
|         Some(FALLBACK_API_URL.to_string()) | ||||
|     } else { | ||||
|         None | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[tokio::test] | ||||
| async fn test_get_node_info_success() { | ||||
|     if let Some(api_url) = get_available_api_url().await { | ||||
|         let result = get_node_info(&api_url).await; | ||||
|  | ||||
|         match result { | ||||
|             Ok(node_info) => { | ||||
|                 // Validate that we got a JSON response with expected fields | ||||
|                 assert!(node_info.is_object(), "Node info should be a JSON object"); | ||||
|  | ||||
|                 // Check for common Mycelium node info fields | ||||
|                 let obj = node_info.as_object().unwrap(); | ||||
|  | ||||
|                 // These fields are typically present in Mycelium node info | ||||
|                 // We check if at least one of them exists to validate the response | ||||
|                 let has_expected_fields = obj.contains_key("nodeSubnet") | ||||
|                     || obj.contains_key("nodePubkey") | ||||
|                     || obj.contains_key("peers") | ||||
|                     || obj.contains_key("routes"); | ||||
|  | ||||
|                 assert!( | ||||
|                     has_expected_fields, | ||||
|                     "Node info should contain expected Mycelium fields" | ||||
|                 ); | ||||
|                 println!("✓ Node info retrieved successfully: {:?}", node_info); | ||||
|             } | ||||
|             Err(e) => { | ||||
|                 // If we can connect but get an error, it might be a version mismatch | ||||
|                 // or API change - log it but don't fail the test | ||||
|                 println!("⚠ Node info request failed (API might have changed): {}", e); | ||||
|             } | ||||
|         } | ||||
|     } else { | ||||
|         println!("⚠ Skipping test_get_node_info_success: No Mycelium node available"); | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[tokio::test] | ||||
| async fn test_get_node_info_invalid_url() { | ||||
|     let invalid_url = "http://localhost:99999"; | ||||
|     let result = get_node_info(invalid_url).await; | ||||
|  | ||||
|     assert!(result.is_err(), "Should fail with invalid URL"); | ||||
|     let error = result.unwrap_err(); | ||||
|     assert!( | ||||
|         error.contains("Failed to send request") || error.contains("Request failed"), | ||||
|         "Error should indicate connection failure: {}", | ||||
|         error | ||||
|     ); | ||||
|     println!("✓ Correctly handled invalid URL: {}", error); | ||||
| } | ||||
|  | ||||
| #[tokio::test] | ||||
| async fn test_list_peers() { | ||||
|     if let Some(api_url) = get_available_api_url().await { | ||||
|         let result = list_peers(&api_url).await; | ||||
|  | ||||
|         match result { | ||||
|             Ok(peers) => { | ||||
|                 // Peers should be an array (even if empty) | ||||
|                 assert!(peers.is_array(), "Peers should be a JSON array"); | ||||
|                 println!( | ||||
|                     "✓ Peers listed successfully: {} peers found", | ||||
|                     peers.as_array().unwrap().len() | ||||
|                 ); | ||||
|             } | ||||
|             Err(e) => { | ||||
|                 println!( | ||||
|                     "⚠ List peers request failed (API might have changed): {}", | ||||
|                     e | ||||
|                 ); | ||||
|             } | ||||
|         } | ||||
|     } else { | ||||
|         println!("⚠ Skipping test_list_peers: No Mycelium node available"); | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[tokio::test] | ||||
| async fn test_add_peer_validation() { | ||||
|     if let Some(api_url) = get_available_api_url().await { | ||||
|         // Test with an invalid peer address format | ||||
|         let invalid_peer = "invalid-peer-address"; | ||||
|         let result = add_peer(&api_url, invalid_peer).await; | ||||
|  | ||||
|         // This should either succeed (if the node accepts it) or fail with a validation error | ||||
|         match result { | ||||
|             Ok(response) => { | ||||
|                 println!("✓ Add peer response: {:?}", response); | ||||
|             } | ||||
|             Err(e) => { | ||||
|                 // Expected for invalid peer addresses | ||||
|                 println!("✓ Correctly rejected invalid peer address: {}", e); | ||||
|             } | ||||
|         } | ||||
|     } else { | ||||
|         println!("⚠ Skipping test_add_peer_validation: No Mycelium node available"); | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[tokio::test] | ||||
| async fn test_list_selected_routes() { | ||||
|     if let Some(api_url) = get_available_api_url().await { | ||||
|         let result = list_selected_routes(&api_url).await; | ||||
|  | ||||
|         match result { | ||||
|             Ok(routes) => { | ||||
|                 // Routes should be an array or object | ||||
|                 assert!( | ||||
|                     routes.is_array() || routes.is_object(), | ||||
|                     "Routes should be a JSON array or object" | ||||
|                 ); | ||||
|                 println!("✓ Selected routes retrieved successfully"); | ||||
|             } | ||||
|             Err(e) => { | ||||
|                 println!("⚠ List selected routes request failed: {}", e); | ||||
|             } | ||||
|         } | ||||
|     } else { | ||||
|         println!("⚠ Skipping test_list_selected_routes: No Mycelium node available"); | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[tokio::test] | ||||
| async fn test_list_fallback_routes() { | ||||
|     if let Some(api_url) = get_available_api_url().await { | ||||
|         let result = list_fallback_routes(&api_url).await; | ||||
|  | ||||
|         match result { | ||||
|             Ok(routes) => { | ||||
|                 // Routes should be an array or object | ||||
|                 assert!( | ||||
|                     routes.is_array() || routes.is_object(), | ||||
|                     "Routes should be a JSON array or object" | ||||
|                 ); | ||||
|                 println!("✓ Fallback routes retrieved successfully"); | ||||
|             } | ||||
|             Err(e) => { | ||||
|                 println!("⚠ List fallback routes request failed: {}", e); | ||||
|             } | ||||
|         } | ||||
|     } else { | ||||
|         println!("⚠ Skipping test_list_fallback_routes: No Mycelium node available"); | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[tokio::test] | ||||
| async fn test_send_message_validation() { | ||||
|     if let Some(api_url) = get_available_api_url().await { | ||||
|         // Test message sending with invalid destination | ||||
|         let invalid_destination = "invalid-destination"; | ||||
|         let topic = "test_topic"; | ||||
|         let message = "test message"; | ||||
|         let deadline = Some(Duration::from_secs(1)); | ||||
|  | ||||
|         let result = send_message(&api_url, invalid_destination, topic, message, deadline).await; | ||||
|  | ||||
|         // This should fail with invalid destination | ||||
|         match result { | ||||
|             Ok(response) => { | ||||
|                 // Some implementations might accept any destination format | ||||
|                 println!("✓ Send message response: {:?}", response); | ||||
|             } | ||||
|             Err(e) => { | ||||
|                 // Expected for invalid destinations | ||||
|                 println!("✓ Correctly rejected invalid destination: {}", e); | ||||
|             } | ||||
|         } | ||||
|     } else { | ||||
|         println!("⚠ Skipping test_send_message_validation: No Mycelium node available"); | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[tokio::test] | ||||
| async fn test_receive_messages_timeout() { | ||||
|     if let Some(api_url) = get_available_api_url().await { | ||||
|         let topic = "non_existent_topic"; | ||||
|         let deadline = Some(Duration::from_secs(1)); // Short timeout | ||||
|  | ||||
|         let result = receive_messages(&api_url, topic, deadline).await; | ||||
|  | ||||
|         match result { | ||||
|             Ok(messages) => { | ||||
|                 // Should return empty or no messages for non-existent topic | ||||
|                 println!("✓ Receive messages completed: {:?}", messages); | ||||
|             } | ||||
|             Err(e) => { | ||||
|                 // Timeout or no messages is acceptable | ||||
|                 println!("✓ Receive messages handled correctly: {}", e); | ||||
|             } | ||||
|         } | ||||
|     } else { | ||||
|         println!("⚠ Skipping test_receive_messages_timeout: No Mycelium node available"); | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[tokio::test] | ||||
| async fn test_error_handling_malformed_url() { | ||||
|     let malformed_url = "not-a-url"; | ||||
|     let result = get_node_info(malformed_url).await; | ||||
|  | ||||
|     assert!(result.is_err(), "Should fail with malformed URL"); | ||||
|     let error = result.unwrap_err(); | ||||
|     assert!( | ||||
|         error.contains("Failed to send request"), | ||||
|         "Error should indicate request failure: {}", | ||||
|         error | ||||
|     ); | ||||
|     println!("✓ Correctly handled malformed URL: {}", error); | ||||
| } | ||||
|  | ||||
| #[tokio::test] | ||||
| async fn test_base64_encoding_in_messages() { | ||||
|     // Test that our message functions properly handle base64 encoding | ||||
|     // This is a unit test that doesn't require a running Mycelium node | ||||
|  | ||||
|     let topic = "test/topic"; | ||||
|     let message = "Hello, Mycelium!"; | ||||
|  | ||||
|     // Test base64 encoding directly | ||||
|     use base64::{engine::general_purpose, Engine as _}; | ||||
|     let encoded_topic = general_purpose::STANDARD.encode(topic); | ||||
|     let encoded_message = general_purpose::STANDARD.encode(message); | ||||
|  | ||||
|     assert!( | ||||
|         !encoded_topic.is_empty(), | ||||
|         "Encoded topic should not be empty" | ||||
|     ); | ||||
|     assert!( | ||||
|         !encoded_message.is_empty(), | ||||
|         "Encoded message should not be empty" | ||||
|     ); | ||||
|  | ||||
|     // Verify we can decode back | ||||
|     let decoded_topic = general_purpose::STANDARD.decode(&encoded_topic).unwrap(); | ||||
|     let decoded_message = general_purpose::STANDARD.decode(&encoded_message).unwrap(); | ||||
|  | ||||
|     assert_eq!(String::from_utf8(decoded_topic).unwrap(), topic); | ||||
|     assert_eq!(String::from_utf8(decoded_message).unwrap(), message); | ||||
|  | ||||
|     println!("✓ Base64 encoding/decoding works correctly"); | ||||
| } | ||||
							
								
								
									
										242
									
								
								mycelium/tests/rhai/01_mycelium_basic.rhai
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										242
									
								
								mycelium/tests/rhai/01_mycelium_basic.rhai
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,242 @@ | ||||
| // Basic Mycelium functionality tests in Rhai | ||||
| // | ||||
| // This script tests the core Mycelium operations available through Rhai. | ||||
| // It's designed to work with or without a running Mycelium node. | ||||
|  | ||||
| print("=== Mycelium Basic Functionality Tests ==="); | ||||
|  | ||||
| // Test configuration | ||||
| let test_api_url = "http://localhost:8989"; | ||||
| let fallback_api_url = "http://localhost:7777"; | ||||
|  | ||||
| // Helper function to check if Mycelium is available | ||||
| fn is_mycelium_available(api_url) { | ||||
|     try { | ||||
|         mycelium_get_node_info(api_url); | ||||
|         return true; | ||||
|     } catch(err) { | ||||
|         return false; | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Find an available API URL | ||||
| let api_url = ""; | ||||
| if is_mycelium_available(test_api_url) { | ||||
|     api_url = test_api_url; | ||||
|     print(`✓ Using primary API URL: ${api_url}`); | ||||
| } else if is_mycelium_available(fallback_api_url) { | ||||
|     api_url = fallback_api_url; | ||||
|     print(`✓ Using fallback API URL: ${api_url}`); | ||||
| } else { | ||||
|     print("⚠ No Mycelium node available - testing error handling only"); | ||||
|     api_url = "http://localhost:99999"; // Intentionally invalid for error testing | ||||
| } | ||||
|  | ||||
| // Test 1: Get Node Information | ||||
| print("\n--- Test 1: Get Node Information ---"); | ||||
| try { | ||||
|     let node_info = mycelium_get_node_info(api_url); | ||||
|      | ||||
|     if api_url.contains("99999") { | ||||
|         print("✗ Expected error but got success"); | ||||
|         assert_true(false, "Should have failed with invalid URL"); | ||||
|     } else { | ||||
|         print("✓ Node info retrieved successfully"); | ||||
|         print(`  Node info type: ${type_of(node_info)}`); | ||||
|          | ||||
|         // Validate response structure | ||||
|         if type_of(node_info) == "map" { | ||||
|             print("✓ Node info is a proper object"); | ||||
|              | ||||
|             // Check for common fields (at least one should exist) | ||||
|             let has_fields = node_info.contains("nodeSubnet") ||  | ||||
|                            node_info.contains("nodePubkey") ||  | ||||
|                            node_info.contains("peers") || | ||||
|                            node_info.contains("routes"); | ||||
|              | ||||
|             if has_fields { | ||||
|                 print("✓ Node info contains expected fields"); | ||||
|             } else { | ||||
|                 print("⚠ Node info structure might have changed"); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } catch(err) { | ||||
|     if api_url.contains("99999") { | ||||
|         print("✓ Correctly handled connection error"); | ||||
|         assert_true(err.to_string().contains("Mycelium error"), "Error should be properly formatted"); | ||||
|     } else { | ||||
|         print(`⚠ Unexpected error with available node: ${err}`); | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Test 2: List Peers | ||||
| print("\n--- Test 2: List Peers ---"); | ||||
| try { | ||||
|     let peers = mycelium_list_peers(api_url); | ||||
|      | ||||
|     if api_url.contains("99999") { | ||||
|         print("✗ Expected error but got success"); | ||||
|         assert_true(false, "Should have failed with invalid URL"); | ||||
|     } else { | ||||
|         print("✓ Peers listed successfully"); | ||||
|         print(`  Peers type: ${type_of(peers)}`); | ||||
|          | ||||
|         if type_of(peers) == "array" { | ||||
|             print(`✓ Found ${peers.len()} peers`); | ||||
|              | ||||
|             // If we have peers, check their structure | ||||
|             if peers.len() > 0 { | ||||
|                 let first_peer = peers[0]; | ||||
|                 print(`  First peer type: ${type_of(first_peer)}`); | ||||
|                  | ||||
|                 if type_of(first_peer) == "map" { | ||||
|                     print("✓ Peer has proper object structure"); | ||||
|                 } | ||||
|             } | ||||
|         } else { | ||||
|             print("⚠ Peers response is not an array"); | ||||
|         } | ||||
|     } | ||||
| } catch(err) { | ||||
|     if api_url.contains("99999") { | ||||
|         print("✓ Correctly handled connection error"); | ||||
|     } else { | ||||
|         print(`⚠ Unexpected error listing peers: ${err}`); | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Test 3: Add Peer (with validation) | ||||
| print("\n--- Test 3: Add Peer Validation ---"); | ||||
| try { | ||||
|     // Test with invalid peer address | ||||
|     let result = mycelium_add_peer(api_url, "invalid-peer-format"); | ||||
|      | ||||
|     if api_url.contains("99999") { | ||||
|         print("✗ Expected connection error but got success"); | ||||
|     } else { | ||||
|         print("✓ Add peer completed (validation depends on node implementation)"); | ||||
|         print(`  Result type: ${type_of(result)}`); | ||||
|     } | ||||
| } catch(err) { | ||||
|     if api_url.contains("99999") { | ||||
|         print("✓ Correctly handled connection error"); | ||||
|     } else { | ||||
|         print(`✓ Peer validation error (expected): ${err}`); | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Test 4: List Selected Routes | ||||
| print("\n--- Test 4: List Selected Routes ---"); | ||||
| try { | ||||
|     let routes = mycelium_list_selected_routes(api_url); | ||||
|      | ||||
|     if api_url.contains("99999") { | ||||
|         print("✗ Expected error but got success"); | ||||
|     } else { | ||||
|         print("✓ Selected routes retrieved successfully"); | ||||
|         print(`  Routes type: ${type_of(routes)}`); | ||||
|          | ||||
|         if type_of(routes) == "array" { | ||||
|             print(`✓ Found ${routes.len()} selected routes`); | ||||
|         } else if type_of(routes) == "map" { | ||||
|             print("✓ Routes returned as object"); | ||||
|         } | ||||
|     } | ||||
| } catch(err) { | ||||
|     if api_url.contains("99999") { | ||||
|         print("✓ Correctly handled connection error"); | ||||
|     } else { | ||||
|         print(`⚠ Error retrieving selected routes: ${err}`); | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Test 5: List Fallback Routes | ||||
| print("\n--- Test 5: List Fallback Routes ---"); | ||||
| try { | ||||
|     let routes = mycelium_list_fallback_routes(api_url); | ||||
|      | ||||
|     if api_url.contains("99999") { | ||||
|         print("✗ Expected error but got success"); | ||||
|     } else { | ||||
|         print("✓ Fallback routes retrieved successfully"); | ||||
|         print(`  Routes type: ${type_of(routes)}`); | ||||
|     } | ||||
| } catch(err) { | ||||
|     if api_url.contains("99999") { | ||||
|         print("✓ Correctly handled connection error"); | ||||
|     } else { | ||||
|         print(`⚠ Error retrieving fallback routes: ${err}`); | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Test 6: Send Message (validation) | ||||
| print("\n--- Test 6: Send Message Validation ---"); | ||||
| try { | ||||
|     let result = mycelium_send_message(api_url, "invalid-destination", "test_topic", "test message", -1); | ||||
|      | ||||
|     if api_url.contains("99999") { | ||||
|         print("✗ Expected connection error but got success"); | ||||
|     } else { | ||||
|         print("✓ Send message completed (validation depends on node implementation)"); | ||||
|         print(`  Result type: ${type_of(result)}`); | ||||
|     } | ||||
| } catch(err) { | ||||
|     if api_url.contains("99999") { | ||||
|         print("✓ Correctly handled connection error"); | ||||
|     } else { | ||||
|         print(`✓ Message validation error (expected): ${err}`); | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Test 7: Receive Messages (timeout test) | ||||
| print("\n--- Test 7: Receive Messages Timeout ---"); | ||||
| try { | ||||
|     // Use short timeout to avoid long waits | ||||
|     let messages = mycelium_receive_messages(api_url, "non_existent_topic", 1); | ||||
|      | ||||
|     if api_url.contains("99999") { | ||||
|         print("✗ Expected connection error but got success"); | ||||
|     } else { | ||||
|         print("✓ Receive messages completed"); | ||||
|         print(`  Messages type: ${type_of(messages)}`); | ||||
|          | ||||
|         if type_of(messages) == "array" { | ||||
|             print(`✓ Received ${messages.len()} messages`); | ||||
|         } else { | ||||
|             print("✓ Messages returned as object"); | ||||
|         } | ||||
|     } | ||||
| } catch(err) { | ||||
|     if api_url.contains("99999") { | ||||
|         print("✓ Correctly handled connection error"); | ||||
|     } else { | ||||
|         print(`✓ Receive timeout handled correctly: ${err}`); | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Test 8: Parameter Validation | ||||
| print("\n--- Test 8: Parameter Validation ---"); | ||||
|  | ||||
| // Test empty API URL | ||||
| try { | ||||
|     mycelium_get_node_info(""); | ||||
|     print("✗ Should have failed with empty API URL"); | ||||
| } catch(err) { | ||||
|     print("✓ Correctly rejected empty API URL"); | ||||
| } | ||||
|  | ||||
| // Test negative timeout handling | ||||
| try { | ||||
|     mycelium_receive_messages(api_url, "test_topic", -1); | ||||
|     if api_url.contains("99999") { | ||||
|         print("✗ Expected connection error"); | ||||
|     } else { | ||||
|         print("✓ Negative timeout handled (treated as no timeout)"); | ||||
|     } | ||||
| } catch(err) { | ||||
|     print("✓ Timeout parameter handled correctly"); | ||||
| } | ||||
|  | ||||
| print("\n=== Mycelium Basic Tests Completed ==="); | ||||
| print("All core Mycelium functions are properly registered and handle errors correctly."); | ||||
							
								
								
									
										174
									
								
								mycelium/tests/rhai/run_all_tests.rhai
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										174
									
								
								mycelium/tests/rhai/run_all_tests.rhai
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,174 @@ | ||||
| // Mycelium Rhai Test Runner | ||||
| // | ||||
| // This script runs all Mycelium-related Rhai tests and reports results. | ||||
| // It includes simplified versions of the individual tests to avoid dependency issues. | ||||
|  | ||||
| print("=== Mycelium Rhai Test Suite ==="); | ||||
| print("Running comprehensive tests for Mycelium Rhai integration...\n"); | ||||
|  | ||||
| let total_tests = 0; | ||||
| let passed_tests = 0; | ||||
| let failed_tests = 0; | ||||
| let skipped_tests = 0; | ||||
|  | ||||
| // Test 1: Function Registration | ||||
| print("Test 1: Function Registration"); | ||||
| total_tests += 1; | ||||
| try { | ||||
|     // Test that all mycelium functions are registered | ||||
|     let invalid_url = "http://localhost:99999"; | ||||
|     let all_functions_exist = true; | ||||
|  | ||||
|     try { mycelium_get_node_info(invalid_url); } catch(err) { | ||||
|         if !err.to_string().contains("Mycelium error") { all_functions_exist = false; } | ||||
|     } | ||||
|  | ||||
|     try { mycelium_list_peers(invalid_url); } catch(err) { | ||||
|         if !err.to_string().contains("Mycelium error") { all_functions_exist = false; } | ||||
|     } | ||||
|  | ||||
|     try { mycelium_send_message(invalid_url, "dest", "topic", "msg", -1); } catch(err) { | ||||
|         if !err.to_string().contains("Mycelium error") { all_functions_exist = false; } | ||||
|     } | ||||
|  | ||||
|     if all_functions_exist { | ||||
|         passed_tests += 1; | ||||
|         print("✓ PASSED: All mycelium functions are registered"); | ||||
|     } else { | ||||
|         failed_tests += 1; | ||||
|         print("✗ FAILED: Some mycelium functions are missing"); | ||||
|     } | ||||
| } catch(err) { | ||||
|     failed_tests += 1; | ||||
|     print(`✗ ERROR: Function registration test failed - ${err}`); | ||||
| } | ||||
|  | ||||
| // Test 2: Error Handling | ||||
| print("\nTest 2: Error Handling"); | ||||
| total_tests += 1; | ||||
| try { | ||||
|     mycelium_get_node_info("http://localhost:99999"); | ||||
|     failed_tests += 1; | ||||
|     print("✗ FAILED: Should have failed with connection error"); | ||||
| } catch(err) { | ||||
|     if err.to_string().contains("Mycelium error") { | ||||
|         passed_tests += 1; | ||||
|         print("✓ PASSED: Error handling works correctly"); | ||||
|     } else { | ||||
|         failed_tests += 1; | ||||
|         print(`✗ FAILED: Unexpected error format - ${err}`); | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Test 3: Parameter Validation | ||||
| print("\nTest 3: Parameter Validation"); | ||||
| total_tests += 1; | ||||
| try { | ||||
|     mycelium_get_node_info(""); | ||||
|     failed_tests += 1; | ||||
|     print("✗ FAILED: Should have failed with empty API URL"); | ||||
| } catch(err) { | ||||
|     passed_tests += 1; | ||||
|     print("✓ PASSED: Parameter validation works correctly"); | ||||
| } | ||||
|  | ||||
| // Test 4: Timeout Parameter Handling | ||||
| print("\nTest 4: Timeout Parameter Handling"); | ||||
| total_tests += 1; | ||||
| try { | ||||
|     let invalid_url = "http://localhost:99999"; | ||||
|  | ||||
|     // Test negative timeout (should be treated as no timeout) | ||||
|     try { | ||||
|         mycelium_receive_messages(invalid_url, "topic", -1); | ||||
|         failed_tests += 1; | ||||
|         print("✗ FAILED: Should have failed with connection error"); | ||||
|     } catch(err) { | ||||
|         if err.to_string().contains("Mycelium error") { | ||||
|             passed_tests += 1; | ||||
|             print("✓ PASSED: Timeout parameter handling works correctly"); | ||||
|         } else { | ||||
|             failed_tests += 1; | ||||
|             print(`✗ FAILED: Unexpected error - ${err}`); | ||||
|         } | ||||
|     } | ||||
| } catch(err) { | ||||
|     failed_tests += 1; | ||||
|     print(`✗ ERROR: Timeout test failed - ${err}`); | ||||
| } | ||||
|  | ||||
| // Check if Mycelium is available for integration tests | ||||
| let test_api_url = "http://localhost:8989"; | ||||
| let fallback_api_url = "http://localhost:7777"; | ||||
| let available_api_url = ""; | ||||
|  | ||||
| try { | ||||
|     mycelium_get_node_info(test_api_url); | ||||
|     available_api_url = test_api_url; | ||||
| } catch(err) { | ||||
|     try { | ||||
|         mycelium_get_node_info(fallback_api_url); | ||||
|         available_api_url = fallback_api_url; | ||||
|     } catch(err2) { | ||||
|         // No Mycelium node available | ||||
|     } | ||||
| } | ||||
|  | ||||
| if available_api_url != "" { | ||||
|     print(`\n✓ Mycelium node available at: ${available_api_url}`); | ||||
|  | ||||
|     // Test 5: Get Node Info | ||||
|     print("\nTest 5: Get Node Info"); | ||||
|     total_tests += 1; | ||||
|     try { | ||||
|         let node_info = mycelium_get_node_info(available_api_url); | ||||
|  | ||||
|         if type_of(node_info) == "map" { | ||||
|             passed_tests += 1; | ||||
|             print("✓ PASSED: Node info retrieved successfully"); | ||||
|         } else { | ||||
|             failed_tests += 1; | ||||
|             print("✗ FAILED: Node info should be an object"); | ||||
|         } | ||||
|     } catch(err) { | ||||
|         failed_tests += 1; | ||||
|         print(`✗ ERROR: Node info test failed - ${err}`); | ||||
|     } | ||||
|  | ||||
|     // Test 6: List Peers | ||||
|     print("\nTest 6: List Peers"); | ||||
|     total_tests += 1; | ||||
|     try { | ||||
|         let peers = mycelium_list_peers(available_api_url); | ||||
|  | ||||
|         if type_of(peers) == "array" { | ||||
|             passed_tests += 1; | ||||
|             print("✓ PASSED: Peers listed successfully"); | ||||
|         } else { | ||||
|             failed_tests += 1; | ||||
|             print("✗ FAILED: Peers should be an array"); | ||||
|         } | ||||
|     } catch(err) { | ||||
|         failed_tests += 1; | ||||
|         print(`✗ ERROR: List peers test failed - ${err}`); | ||||
|     } | ||||
| } else { | ||||
|     print("\n⚠ No Mycelium node available - skipping integration tests"); | ||||
|     skipped_tests += 2; // Skip node info and list peers tests | ||||
|     total_tests += 2; | ||||
| } | ||||
|  | ||||
| // Print final results | ||||
| print("\n=== Test Results ==="); | ||||
| print(`Total Tests: ${total_tests}`); | ||||
| print(`Passed: ${passed_tests}`); | ||||
| print(`Failed: ${failed_tests}`); | ||||
| print(`Skipped: ${skipped_tests}`); | ||||
|  | ||||
| if failed_tests == 0 { | ||||
|     print("\n✓ All tests passed!"); | ||||
| } else { | ||||
|     print(`\n✗ ${failed_tests} test(s) failed.`); | ||||
| } | ||||
|  | ||||
| print("\n=== Mycelium Rhai Test Suite Completed ==="); | ||||
							
								
								
									
										313
									
								
								mycelium/tests/rhai_integration_tests.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										313
									
								
								mycelium/tests/rhai_integration_tests.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,313 @@ | ||||
| //! Rhai integration tests for Mycelium module | ||||
| //! | ||||
| //! These tests validate the Rhai wrapper functions and ensure proper | ||||
| //! integration between Rust and Rhai for Mycelium operations. | ||||
|  | ||||
| use rhai::{Engine, EvalAltResult}; | ||||
| use sal_mycelium::rhai::*; | ||||
|  | ||||
| #[cfg(test)] | ||||
| mod rhai_integration_tests { | ||||
|     use super::*; | ||||
|  | ||||
|     fn create_test_engine() -> Engine { | ||||
|         let mut engine = Engine::new(); | ||||
|         register_mycelium_module(&mut engine).expect("Failed to register mycelium module"); | ||||
|         engine | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn test_rhai_module_registration() { | ||||
|         let engine = create_test_engine(); | ||||
|  | ||||
|         // Test that the functions are registered by checking if they exist | ||||
|         let script = r#" | ||||
|             // Test that all mycelium functions are available | ||||
|             let functions_exist = true; | ||||
|              | ||||
|             // We can't actually call these without a server, but we can verify they're registered | ||||
|             // by checking that the engine doesn't throw "function not found" errors | ||||
|             functions_exist | ||||
|         "#; | ||||
|  | ||||
|         let result: Result<bool, Box<EvalAltResult>> = engine.eval(script); | ||||
|         assert!(result.is_ok()); | ||||
|         assert_eq!(result.unwrap(), true); | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn test_mycelium_get_node_info_function_exists() { | ||||
|         let engine = create_test_engine(); | ||||
|  | ||||
|         // Test that mycelium_get_node_info function is registered | ||||
|         let script = r#" | ||||
|             // This will fail with connection error, but proves the function exists | ||||
|             try { | ||||
|                 mycelium_get_node_info("http://localhost:99999"); | ||||
|                 false; // Should not reach here | ||||
|             } catch(err) { | ||||
|                 // Function exists but failed due to connection - this is expected | ||||
|                 return err.to_string().contains("Mycelium error"); | ||||
|             } | ||||
|         "#; | ||||
|  | ||||
|         let result: Result<bool, Box<EvalAltResult>> = engine.eval(script); | ||||
|         if let Err(ref e) = result { | ||||
|             println!("Script evaluation error: {}", e); | ||||
|         } | ||||
|         assert!(result.is_ok()); | ||||
|         assert_eq!(result.unwrap(), true); | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn test_mycelium_list_peers_function_exists() { | ||||
|         let engine = create_test_engine(); | ||||
|  | ||||
|         let script = r#" | ||||
|             try { | ||||
|                 mycelium_list_peers("http://localhost:99999"); | ||||
|                 return false; | ||||
|             } catch(err) { | ||||
|                 return err.to_string().contains("Mycelium error"); | ||||
|             } | ||||
|         "#; | ||||
|  | ||||
|         let result: Result<bool, Box<EvalAltResult>> = engine.eval(script); | ||||
|         assert!(result.is_ok()); | ||||
|         assert_eq!(result.unwrap(), true); | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn test_mycelium_add_peer_function_exists() { | ||||
|         let engine = create_test_engine(); | ||||
|  | ||||
|         let script = r#" | ||||
|             try { | ||||
|                 mycelium_add_peer("http://localhost:99999", "tcp://example.com:9651"); | ||||
|                 return false; | ||||
|             } catch(err) { | ||||
|                 return err.to_string().contains("Mycelium error"); | ||||
|             } | ||||
|         "#; | ||||
|  | ||||
|         let result: Result<bool, Box<EvalAltResult>> = engine.eval(script); | ||||
|         assert!(result.is_ok()); | ||||
|         assert_eq!(result.unwrap(), true); | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn test_mycelium_remove_peer_function_exists() { | ||||
|         let engine = create_test_engine(); | ||||
|  | ||||
|         let script = r#" | ||||
|             try { | ||||
|                 mycelium_remove_peer("http://localhost:99999", "peer_id"); | ||||
|                 return false; | ||||
|             } catch(err) { | ||||
|                 return err.to_string().contains("Mycelium error"); | ||||
|             } | ||||
|         "#; | ||||
|  | ||||
|         let result: Result<bool, Box<EvalAltResult>> = engine.eval(script); | ||||
|         assert!(result.is_ok()); | ||||
|         assert_eq!(result.unwrap(), true); | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn test_mycelium_list_selected_routes_function_exists() { | ||||
|         let engine = create_test_engine(); | ||||
|  | ||||
|         let script = r#" | ||||
|             try { | ||||
|                 mycelium_list_selected_routes("http://localhost:99999"); | ||||
|                 return false; | ||||
|             } catch(err) { | ||||
|                 return err.to_string().contains("Mycelium error"); | ||||
|             } | ||||
|         "#; | ||||
|  | ||||
|         let result: Result<bool, Box<EvalAltResult>> = engine.eval(script); | ||||
|         assert!(result.is_ok()); | ||||
|         assert_eq!(result.unwrap(), true); | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn test_mycelium_list_fallback_routes_function_exists() { | ||||
|         let engine = create_test_engine(); | ||||
|  | ||||
|         let script = r#" | ||||
|             try { | ||||
|                 mycelium_list_fallback_routes("http://localhost:99999"); | ||||
|                 return false; | ||||
|             } catch(err) { | ||||
|                 return err.to_string().contains("Mycelium error"); | ||||
|             } | ||||
|         "#; | ||||
|  | ||||
|         let result: Result<bool, Box<EvalAltResult>> = engine.eval(script); | ||||
|         assert!(result.is_ok()); | ||||
|         assert_eq!(result.unwrap(), true); | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn test_mycelium_send_message_function_exists() { | ||||
|         let engine = create_test_engine(); | ||||
|  | ||||
|         let script = r#" | ||||
|             try { | ||||
|                 mycelium_send_message("http://localhost:99999", "destination", "topic", "message", -1); | ||||
|                 return false; | ||||
|             } catch(err) { | ||||
|                 return err.to_string().contains("Mycelium error"); | ||||
|             } | ||||
|         "#; | ||||
|  | ||||
|         let result: Result<bool, Box<EvalAltResult>> = engine.eval(script); | ||||
|         assert!(result.is_ok()); | ||||
|         assert_eq!(result.unwrap(), true); | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn test_mycelium_receive_messages_function_exists() { | ||||
|         let engine = create_test_engine(); | ||||
|  | ||||
|         let script = r#" | ||||
|             try { | ||||
|                 mycelium_receive_messages("http://localhost:99999", "topic", 1); | ||||
|                 return false; | ||||
|             } catch(err) { | ||||
|                 return err.to_string().contains("Mycelium error"); | ||||
|             } | ||||
|         "#; | ||||
|  | ||||
|         let result: Result<bool, Box<EvalAltResult>> = engine.eval(script); | ||||
|         assert!(result.is_ok()); | ||||
|         assert_eq!(result.unwrap(), true); | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn test_parameter_validation() { | ||||
|         let engine = create_test_engine(); | ||||
|  | ||||
|         // Test that functions handle parameter validation correctly | ||||
|         let script = r#" | ||||
|             let test_results = []; | ||||
|              | ||||
|             // Test empty API URL | ||||
|             try { | ||||
|                 mycelium_get_node_info(""); | ||||
|                 test_results.push(false); | ||||
|             } catch(err) { | ||||
|                 test_results.push(true); // Expected to fail | ||||
|             } | ||||
|              | ||||
|             // Test empty peer address | ||||
|             try { | ||||
|                 mycelium_add_peer("http://localhost:8989", ""); | ||||
|                 test_results.push(false); | ||||
|             } catch(err) { | ||||
|                 test_results.push(true); // Expected to fail | ||||
|             } | ||||
|              | ||||
|             // Test negative timeout handling | ||||
|             try { | ||||
|                 mycelium_receive_messages("http://localhost:99999", "topic", -1); | ||||
|                 test_results.push(false); | ||||
|             } catch(err) { | ||||
|                 // Should handle negative timeout gracefully | ||||
|                 test_results.push(err.to_string().contains("Mycelium error")); | ||||
|             } | ||||
|              | ||||
|             test_results | ||||
|         "#; | ||||
|  | ||||
|         let result: Result<rhai::Array, Box<EvalAltResult>> = engine.eval(script); | ||||
|         assert!(result.is_ok()); | ||||
|         let results = result.unwrap(); | ||||
|  | ||||
|         // All parameter validation tests should pass | ||||
|         for (i, result) in results.iter().enumerate() { | ||||
|             assert_eq!( | ||||
|                 result.as_bool().unwrap_or(false), | ||||
|                 true, | ||||
|                 "Parameter validation test {} failed", | ||||
|                 i | ||||
|             ); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn test_error_message_format() { | ||||
|         let engine = create_test_engine(); | ||||
|  | ||||
|         // Test that error messages are properly formatted | ||||
|         let script = r#" | ||||
|             try { | ||||
|                 mycelium_get_node_info("http://localhost:99999"); | ||||
|                 return ""; | ||||
|             } catch(err) { | ||||
|                 let error_str = err.to_string(); | ||||
|                 // Should contain "Mycelium error:" prefix | ||||
|                 if error_str.contains("Mycelium error:") { | ||||
|                     return "correct_format"; | ||||
|                 } else { | ||||
|                     return error_str; | ||||
|                 } | ||||
|             } | ||||
|         "#; | ||||
|  | ||||
|         let result: Result<String, Box<EvalAltResult>> = engine.eval(script); | ||||
|         assert!(result.is_ok()); | ||||
|         assert_eq!(result.unwrap(), "correct_format"); | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn test_timeout_parameter_handling() { | ||||
|         let engine = create_test_engine(); | ||||
|  | ||||
|         // Test different timeout parameter values | ||||
|         let script = r#" | ||||
|             let timeout_tests = []; | ||||
|              | ||||
|             // Test positive timeout | ||||
|             try { | ||||
|                 mycelium_receive_messages("http://localhost:99999", "topic", 5); | ||||
|                 timeout_tests.push(false); | ||||
|             } catch(err) { | ||||
|                 timeout_tests.push(err.to_string().contains("Mycelium error")); | ||||
|             } | ||||
|              | ||||
|             // Test zero timeout | ||||
|             try { | ||||
|                 mycelium_receive_messages("http://localhost:99999", "topic", 0); | ||||
|                 timeout_tests.push(false); | ||||
|             } catch(err) { | ||||
|                 timeout_tests.push(err.to_string().contains("Mycelium error")); | ||||
|             } | ||||
|              | ||||
|             // Test negative timeout (should be treated as no timeout) | ||||
|             try { | ||||
|                 mycelium_receive_messages("http://localhost:99999", "topic", -1); | ||||
|                 timeout_tests.push(false); | ||||
|             } catch(err) { | ||||
|                 timeout_tests.push(err.to_string().contains("Mycelium error")); | ||||
|             } | ||||
|              | ||||
|             timeout_tests | ||||
|         "#; | ||||
|  | ||||
|         let result: Result<rhai::Array, Box<EvalAltResult>> = engine.eval(script); | ||||
|         assert!(result.is_ok()); | ||||
|         let results = result.unwrap(); | ||||
|  | ||||
|         // All timeout tests should handle the connection error properly | ||||
|         for (i, result) in results.iter().enumerate() { | ||||
|             assert_eq!( | ||||
|                 result.as_bool().unwrap_or(false), | ||||
|                 true, | ||||
|                 "Timeout test {} failed", | ||||
|                 i | ||||
|             ); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										16
									
								
								net/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								net/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| [package] | ||||
| name = "sal-net" | ||||
| version = "0.1.0" | ||||
| edition = "2021" | ||||
| authors = ["PlanetFirst <info@incubaid.com>"] | ||||
| description = "SAL Network - Network connectivity utilities for TCP, HTTP, and SSH" | ||||
| repository = "https://git.threefold.info/herocode/sal" | ||||
| license = "Apache-2.0" | ||||
| keywords = ["network", "tcp", "http", "ssh", "connectivity"] | ||||
| categories = ["network-programming", "api-bindings"] | ||||
|  | ||||
| [dependencies] | ||||
| anyhow = "1.0.98" | ||||
| tokio = { version = "1.0", features = ["full"] } | ||||
| reqwest = { version = "0.12", features = ["json", "blocking"] } | ||||
| rhai = "1.19.0" | ||||
							
								
								
									
										226
									
								
								net/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										226
									
								
								net/README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,226 @@ | ||||
| # SAL Network Package | ||||
|  | ||||
| Network connectivity utilities for TCP, HTTP, and SSH operations. | ||||
|  | ||||
| ## Overview | ||||
|  | ||||
| The `sal-net` package provides a comprehensive set of network connectivity tools for the SAL (System Abstraction Layer) ecosystem. It includes utilities for TCP port checking, HTTP/HTTPS connectivity testing, and SSH command execution. | ||||
|  | ||||
| ## Features | ||||
|  | ||||
| ### TCP Connectivity | ||||
| - **Port checking**: Test if specific TCP ports are open | ||||
| - **Multi-port checking**: Test multiple ports simultaneously   | ||||
| - **ICMP ping**: Test host reachability using ping | ||||
| - **Configurable timeouts**: Customize connection timeout values | ||||
|  | ||||
| ### HTTP/HTTPS Connectivity | ||||
| - **URL reachability**: Test if URLs are accessible | ||||
| - **Status code checking**: Get HTTP status codes from URLs | ||||
| - **Content fetching**: Download content from URLs | ||||
| - **Status verification**: Verify URLs return expected status codes | ||||
|  | ||||
| ### SSH Operations | ||||
| - **Command execution**: Run commands on remote hosts via SSH | ||||
| - **Connection testing**: Test SSH connectivity to hosts | ||||
| - **Builder pattern**: Flexible SSH connection configuration | ||||
| - **Custom authentication**: Support for identity files and custom ports | ||||
|  | ||||
| ## Rust API | ||||
|  | ||||
| ### TCP Operations | ||||
|  | ||||
| ```rust | ||||
| use sal_net::TcpConnector; | ||||
| use std::time::Duration; | ||||
|  | ||||
| // Create a TCP connector | ||||
| let connector = TcpConnector::new(); | ||||
|  | ||||
| // Check if a port is open | ||||
| let is_open = connector.check_port("127.0.0.1".parse().unwrap(), 80).await?; | ||||
|  | ||||
| // Check multiple ports | ||||
| let ports = vec![22, 80, 443]; | ||||
| let results = connector.check_ports("example.com".parse().unwrap(), &ports).await?; | ||||
|  | ||||
| // Ping a host | ||||
| let is_reachable = connector.ping("google.com").await?; | ||||
| ``` | ||||
|  | ||||
| ### HTTP Operations | ||||
|  | ||||
| ```rust | ||||
| use sal_net::HttpConnector; | ||||
|  | ||||
| // Create an HTTP connector | ||||
| let connector = HttpConnector::new()?; | ||||
|  | ||||
| // Check if a URL is reachable | ||||
| let is_reachable = connector.check_url("https://example.com").await?; | ||||
|  | ||||
| // Get status code | ||||
| let status = connector.check_status("https://example.com").await?; | ||||
|  | ||||
| // Fetch content | ||||
| let content = connector.get_content("https://api.example.com/data").await?; | ||||
|  | ||||
| // Verify specific status | ||||
| let matches = connector.verify_status("https://example.com", reqwest::StatusCode::OK).await?; | ||||
| ``` | ||||
|  | ||||
| ### SSH Operations | ||||
|  | ||||
| ```rust | ||||
| use sal_net::SshConnectionBuilder; | ||||
| use std::time::Duration; | ||||
|  | ||||
| // Build an SSH connection | ||||
| let connection = SshConnectionBuilder::new() | ||||
|     .host("example.com") | ||||
|     .port(22) | ||||
|     .user("username") | ||||
|     .timeout(Duration::from_secs(30)) | ||||
|     .build(); | ||||
|  | ||||
| // Execute a command | ||||
| let (exit_code, output) = connection.execute("ls -la").await?; | ||||
|  | ||||
| // Test connectivity | ||||
| let is_connected = connection.ping().await?; | ||||
| ``` | ||||
|  | ||||
| ## Rhai Integration | ||||
|  | ||||
| The package provides Rhai scripting integration for network operations: | ||||
|  | ||||
| ### TCP Functions | ||||
|  | ||||
| ```rhai | ||||
| // Check if a TCP port is open | ||||
| let is_open = tcp_check("127.0.0.1", 80); | ||||
| print(`Port 80 is ${is_open ? "open" : "closed"}`); | ||||
|  | ||||
| // Ping a host (cross-platform) | ||||
| let can_ping = tcp_ping("google.com"); | ||||
| print(`Can ping Google: ${can_ping}`); | ||||
| ``` | ||||
|  | ||||
| ### HTTP Functions | ||||
|  | ||||
| ```rhai | ||||
| // Check if an HTTP URL is reachable | ||||
| let is_reachable = http_check("https://example.com"); | ||||
| print(`URL is ${is_reachable ? "reachable" : "unreachable"}`); | ||||
|  | ||||
| // Get HTTP status code | ||||
| let status = http_status("https://example.com"); | ||||
| print(`HTTP status: ${status}`); | ||||
| ``` | ||||
|  | ||||
| ### SSH Functions | ||||
|  | ||||
| ```rhai | ||||
| // Execute SSH command and get exit code | ||||
| let exit_code = ssh_execute("example.com", "user", "ls -la"); | ||||
| print(`SSH command exit code: ${exit_code}`); | ||||
|  | ||||
| // Execute SSH command and get output | ||||
| let output = ssh_execute_output("example.com", "user", "whoami"); | ||||
| print(`SSH output: ${output}`); | ||||
|  | ||||
| // Test SSH connectivity | ||||
| let can_connect = ssh_ping("example.com", "user"); | ||||
| print(`SSH connection: ${can_connect ? "success" : "failed"}`); | ||||
| ``` | ||||
|  | ||||
| ### Example Rhai Script | ||||
|  | ||||
| ```rhai | ||||
| // Network connectivity test script | ||||
| print("=== Network Connectivity Test ==="); | ||||
|  | ||||
| // Test TCP connectivity | ||||
| let ports = [22, 80, 443]; | ||||
| for port in ports { | ||||
|     let is_open = tcp_check("example.com", port); | ||||
|     print(`Port ${port}: ${is_open ? "OPEN" : "CLOSED"}`); | ||||
| } | ||||
|  | ||||
| // Test ping connectivity | ||||
| let hosts = ["google.com", "github.com", "stackoverflow.com"]; | ||||
| for host in hosts { | ||||
|     let can_ping = tcp_ping(host); | ||||
|     print(`${host}: ${can_ping ? "REACHABLE" : "UNREACHABLE"}`); | ||||
| } | ||||
|  | ||||
| // Test HTTP connectivity | ||||
| let urls = ["https://google.com", "https://github.com", "https://httpbin.org/status/200"]; | ||||
| for url in urls { | ||||
|     let is_reachable = http_check(url); | ||||
|     let status = http_status(url); | ||||
|     print(`${url}: ${is_reachable ? "REACHABLE" : "UNREACHABLE"} (Status: ${status})`); | ||||
| } | ||||
|  | ||||
| // Test SSH connectivity (requires SSH access) | ||||
| let ssh_hosts = ["example.com"]; | ||||
| for host in ssh_hosts { | ||||
|     let can_connect = ssh_ping(host, "user"); | ||||
|     print(`SSH ${host}: ${can_connect ? "CONNECTED" : "FAILED"}`); | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ## Testing | ||||
|  | ||||
| The package includes comprehensive tests: | ||||
|  | ||||
| ```bash | ||||
| # Run all tests | ||||
| cargo test | ||||
|  | ||||
| # Run specific test suites | ||||
| cargo test --test tcp_tests | ||||
| cargo test --test http_tests   | ||||
| cargo test --test ssh_tests | ||||
| cargo test --test rhai_integration_tests | ||||
|  | ||||
| # Run Rhai script tests | ||||
| cargo test --test rhai_integration_tests | ||||
| ``` | ||||
|  | ||||
| ## Dependencies | ||||
|  | ||||
| - `tokio`: Async runtime for network operations | ||||
| - `reqwest`: HTTP client functionality | ||||
| - `anyhow`: Error handling | ||||
| - `rhai`: Scripting integration | ||||
|  | ||||
| ## Security Considerations | ||||
|  | ||||
| - SSH operations use the system's SSH client for security | ||||
| - HTTP operations respect standard timeout and security settings | ||||
| - No credentials are logged or exposed in error messages | ||||
| - Network timeouts prevent hanging operations | ||||
|  | ||||
| ## Platform Support | ||||
|  | ||||
| - **Linux**: Full support for all features | ||||
| - **macOS**: Full support for all features   | ||||
| - **Windows**: TCP and HTTP support (SSH requires SSH client installation) | ||||
|  | ||||
| ## Error Handling | ||||
|  | ||||
| All network operations return `Result` types with meaningful error messages. Operations gracefully handle: | ||||
|  | ||||
| - Network timeouts | ||||
| - Connection failures | ||||
| - Invalid hostnames/URLs | ||||
| - Authentication failures (SSH) | ||||
| - System command failures | ||||
|  | ||||
| ## Performance | ||||
|  | ||||
| - Async operations for non-blocking network calls | ||||
| - Configurable timeouts for responsive applications | ||||
| - Efficient connection reuse where possible | ||||
| - Minimal memory footprint for network operations | ||||
							
								
								
									
										84
									
								
								net/src/http.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								net/src/http.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,84 @@ | ||||
| use std::time::Duration; | ||||
|  | ||||
| use anyhow::Result; | ||||
| use reqwest::{Client, StatusCode, Url}; | ||||
|  | ||||
| /// HTTP Connectivity module for checking HTTP/HTTPS connections | ||||
| pub struct HttpConnector { | ||||
|     client: Client, | ||||
| } | ||||
|  | ||||
| impl HttpConnector { | ||||
|     /// Create a new HTTP connector with the default configuration | ||||
|     pub fn new() -> Result<Self> { | ||||
|         let client = Client::builder().timeout(Duration::from_secs(30)).build()?; | ||||
|  | ||||
|         Ok(Self { client }) | ||||
|     } | ||||
|  | ||||
|     /// Create a new HTTP connector with a custom timeout | ||||
|     pub fn with_timeout(timeout: Duration) -> Result<Self> { | ||||
|         let client = Client::builder().timeout(timeout).build()?; | ||||
|  | ||||
|         Ok(Self { client }) | ||||
|     } | ||||
|  | ||||
|     /// Check if a URL is reachable | ||||
|     pub async fn check_url<U: AsRef<str>>(&self, url: U) -> Result<bool> { | ||||
|         let url_str = url.as_ref(); | ||||
|         let url = Url::parse(url_str)?; | ||||
|  | ||||
|         let result = self.client.head(url).send().await; | ||||
|  | ||||
|         Ok(result.is_ok()) | ||||
|     } | ||||
|  | ||||
|     /// Check a URL and return the status code if reachable | ||||
|     pub async fn check_status<U: AsRef<str>>(&self, url: U) -> Result<Option<StatusCode>> { | ||||
|         let url_str = url.as_ref(); | ||||
|         let url = Url::parse(url_str)?; | ||||
|  | ||||
|         let result = self.client.head(url).send().await; | ||||
|  | ||||
|         match result { | ||||
|             Ok(response) => Ok(Some(response.status())), | ||||
|             Err(_) => Ok(None), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /// Get the content of a URL | ||||
|     pub async fn get_content<U: AsRef<str>>(&self, url: U) -> Result<String> { | ||||
|         let url_str = url.as_ref(); | ||||
|         let url = Url::parse(url_str)?; | ||||
|  | ||||
|         let response = self.client.get(url).send().await?; | ||||
|  | ||||
|         if !response.status().is_success() { | ||||
|             return Err(anyhow::anyhow!( | ||||
|                 "HTTP request failed with status: {}", | ||||
|                 response.status() | ||||
|             )); | ||||
|         } | ||||
|  | ||||
|         let content = response.text().await?; | ||||
|         Ok(content) | ||||
|     } | ||||
|  | ||||
|     /// Verify that a URL responds with a specific status code | ||||
|     pub async fn verify_status<U: AsRef<str>>( | ||||
|         &self, | ||||
|         url: U, | ||||
|         expected_status: StatusCode, | ||||
|     ) -> Result<bool> { | ||||
|         match self.check_status(url).await? { | ||||
|             Some(status) => Ok(status == expected_status), | ||||
|             None => Ok(false), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Default for HttpConnector { | ||||
|     fn default() -> Self { | ||||
|         Self::new().expect("Failed to create default HttpConnector") | ||||
|     } | ||||
| } | ||||
							
								
								
									
										9
									
								
								net/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								net/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| pub mod http; | ||||
| pub mod rhai; | ||||
| pub mod ssh; | ||||
| pub mod tcp; | ||||
|  | ||||
| // Re-export main types for a cleaner API | ||||
| pub use http::HttpConnector; | ||||
| pub use ssh::{SshConnection, SshConnectionBuilder}; | ||||
| pub use tcp::TcpConnector; | ||||
							
								
								
									
										180
									
								
								net/src/rhai.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										180
									
								
								net/src/rhai.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,180 @@ | ||||
| //! Rhai wrappers for network module functions | ||||
| //! | ||||
| //! This module provides Rhai wrappers for network connectivity functions. | ||||
|  | ||||
| use rhai::{Engine, EvalAltResult, Module}; | ||||
|  | ||||
| /// Create a Rhai module with network functions | ||||
| pub fn create_module() -> Module { | ||||
|     // For now, we'll use a simpler approach and register functions via engine | ||||
|     // This ensures compatibility with Rhai's type system | ||||
|     // The module is created but functions are registered through register_net_module | ||||
|  | ||||
|     Module::new() | ||||
| } | ||||
|  | ||||
| /// Register network module functions with the Rhai engine | ||||
| pub fn register_net_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> { | ||||
|     // TCP functions | ||||
|     engine.register_fn("tcp_check", tcp_check); | ||||
|     engine.register_fn("tcp_ping", tcp_ping); | ||||
|  | ||||
|     // HTTP functions | ||||
|     engine.register_fn("http_check", http_check); | ||||
|     engine.register_fn("http_status", http_status); | ||||
|  | ||||
|     // SSH functions | ||||
|     engine.register_fn("ssh_execute", ssh_execute); | ||||
|     engine.register_fn("ssh_execute_output", ssh_execute_output); | ||||
|     engine.register_fn("ssh_ping", ssh_ping_host); | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| /// Check if a TCP port is open | ||||
| pub fn tcp_check(host: &str, port: i64) -> bool { | ||||
|     // Use std::net::TcpStream for synchronous connection test | ||||
|     use std::net::{SocketAddr, TcpStream}; | ||||
|     use std::time::Duration; | ||||
|  | ||||
|     // Parse the address | ||||
|     let addr_str = format!("{}:{}", host, port); | ||||
|     if let Ok(socket_addr) = addr_str.parse::<SocketAddr>() { | ||||
|         // Try to connect with a timeout | ||||
|         TcpStream::connect_timeout(&socket_addr, Duration::from_secs(5)).is_ok() | ||||
|     } else { | ||||
|         // Try to resolve hostname first | ||||
|         match std::net::ToSocketAddrs::to_socket_addrs(&addr_str) { | ||||
|             Ok(mut addrs) => { | ||||
|                 if let Some(addr) = addrs.next() { | ||||
|                     TcpStream::connect_timeout(&addr, Duration::from_secs(5)).is_ok() | ||||
|                 } else { | ||||
|                     false | ||||
|                 } | ||||
|             } | ||||
|             Err(_) => false, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Ping a host using ICMP (cross-platform) | ||||
| pub fn tcp_ping(host: &str) -> bool { | ||||
|     // Use system ping command for synchronous operation | ||||
|     use std::process::Command; | ||||
|  | ||||
|     // Cross-platform ping implementation | ||||
|     let mut cmd = Command::new("ping"); | ||||
|  | ||||
|     #[cfg(target_os = "windows")] | ||||
|     { | ||||
|         cmd.arg("-n").arg("1").arg("-w").arg("5000"); // Windows: -n count, -w timeout in ms | ||||
|     } | ||||
|  | ||||
|     #[cfg(not(target_os = "windows"))] | ||||
|     { | ||||
|         cmd.arg("-c").arg("1").arg("-W").arg("5"); // Unix: -c count, -W timeout in seconds | ||||
|     } | ||||
|  | ||||
|     cmd.arg(host); | ||||
|  | ||||
|     match cmd.output() { | ||||
|         Ok(output) => output.status.success(), | ||||
|         Err(_) => false, | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Check if an HTTP URL is reachable | ||||
| pub fn http_check(url: &str) -> bool { | ||||
|     use std::time::Duration; | ||||
|  | ||||
|     // Create a blocking HTTP client with timeout | ||||
|     let client = match reqwest::blocking::Client::builder() | ||||
|         .timeout(Duration::from_secs(10)) | ||||
|         .build() | ||||
|     { | ||||
|         Ok(client) => client, | ||||
|         Err(_) => return false, | ||||
|     }; | ||||
|  | ||||
|     // Try to make a HEAD request | ||||
|     match client.head(url).send() { | ||||
|         Ok(response) => response.status().is_success(), | ||||
|         Err(_) => false, | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Get HTTP status code from a URL | ||||
| pub fn http_status(url: &str) -> i64 { | ||||
|     use std::time::Duration; | ||||
|  | ||||
|     // Create a blocking HTTP client with timeout | ||||
|     let client = match reqwest::blocking::Client::builder() | ||||
|         .timeout(Duration::from_secs(10)) | ||||
|         .build() | ||||
|     { | ||||
|         Ok(client) => client, | ||||
|         Err(_) => return -1, | ||||
|     }; | ||||
|  | ||||
|     // Try to make a HEAD request | ||||
|     match client.head(url).send() { | ||||
|         Ok(response) => response.status().as_u16() as i64, | ||||
|         Err(_) => -1, | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Execute a command via SSH - returns exit code as i64 | ||||
| pub fn ssh_execute(host: &str, user: &str, command: &str) -> i64 { | ||||
|     use std::process::Command; | ||||
|  | ||||
|     let mut cmd = Command::new("ssh"); | ||||
|     cmd.arg("-o") | ||||
|         .arg("ConnectTimeout=5") | ||||
|         .arg("-o") | ||||
|         .arg("StrictHostKeyChecking=no") | ||||
|         .arg(format!("{}@{}", user, host)) | ||||
|         .arg(command); | ||||
|  | ||||
|     match cmd.output() { | ||||
|         Ok(output) => output.status.code().unwrap_or(-1) as i64, | ||||
|         Err(_) => -1, | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Execute a command via SSH and get output - returns output as string | ||||
| pub fn ssh_execute_output(host: &str, user: &str, command: &str) -> String { | ||||
|     use std::process::Command; | ||||
|  | ||||
|     let mut cmd = Command::new("ssh"); | ||||
|     cmd.arg("-o") | ||||
|         .arg("ConnectTimeout=5") | ||||
|         .arg("-o") | ||||
|         .arg("StrictHostKeyChecking=no") | ||||
|         .arg(format!("{}@{}", user, host)) | ||||
|         .arg(command); | ||||
|  | ||||
|     match cmd.output() { | ||||
|         Ok(output) => String::from_utf8_lossy(&output.stdout).to_string(), | ||||
|         Err(_) => "SSH command failed".to_string(), | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Test SSH connectivity to a host | ||||
| pub fn ssh_ping_host(host: &str, user: &str) -> bool { | ||||
|     use std::process::Command; | ||||
|  | ||||
|     let mut cmd = Command::new("ssh"); | ||||
|     cmd.arg("-o") | ||||
|         .arg("ConnectTimeout=5") | ||||
|         .arg("-o") | ||||
|         .arg("StrictHostKeyChecking=no") | ||||
|         .arg("-o") | ||||
|         .arg("BatchMode=yes") // Non-interactive | ||||
|         .arg(format!("{}@{}", user, host)) | ||||
|         .arg("echo 'Connection successful'"); | ||||
|  | ||||
|     match cmd.output() { | ||||
|         Ok(output) => output.status.success(), | ||||
|         Err(_) => false, | ||||
|     } | ||||
| } | ||||
							
								
								
									
										151
									
								
								net/src/ssh.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										151
									
								
								net/src/ssh.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,151 @@ | ||||
| use std::path::PathBuf; | ||||
| use std::process::Stdio; | ||||
| use std::time::Duration; | ||||
|  | ||||
| use anyhow::Result; | ||||
| use tokio::io::{AsyncReadExt, BufReader}; | ||||
| use tokio::process::Command; | ||||
|  | ||||
| /// SSH Connection that uses the system's SSH client | ||||
| pub struct SshConnection { | ||||
|     host: String, | ||||
|     port: u16, | ||||
|     user: String, | ||||
|     identity_file: Option<PathBuf>, | ||||
|     timeout: Duration, | ||||
| } | ||||
|  | ||||
| impl SshConnection { | ||||
|     /// Execute a command over SSH and return its output | ||||
|     pub async fn execute(&self, command: &str) -> Result<(i32, String)> { | ||||
|         let mut args = Vec::new(); | ||||
|  | ||||
|         // Add SSH options | ||||
|         args.push("-o".to_string()); | ||||
|         args.push(format!("ConnectTimeout={}", self.timeout.as_secs())); | ||||
|  | ||||
|         // Don't check host key to avoid prompts | ||||
|         args.push("-o".to_string()); | ||||
|         args.push("StrictHostKeyChecking=no".to_string()); | ||||
|  | ||||
|         // Specify port if not default | ||||
|         if self.port != 22 { | ||||
|             args.push("-p".to_string()); | ||||
|             args.push(self.port.to_string()); | ||||
|         } | ||||
|  | ||||
|         // Add identity file if provided | ||||
|         if let Some(identity) = &self.identity_file { | ||||
|             args.push("-i".to_string()); | ||||
|             args.push(identity.to_string_lossy().to_string()); | ||||
|         } | ||||
|  | ||||
|         // Add user and host | ||||
|         args.push(format!("{}@{}", self.user, self.host)); | ||||
|  | ||||
|         // Add the command to execute | ||||
|         args.push(command.to_string()); | ||||
|  | ||||
|         // Run the SSH command | ||||
|         let mut child = Command::new("ssh") | ||||
|             .args(&args) | ||||
|             .stdout(Stdio::piped()) | ||||
|             .stderr(Stdio::piped()) | ||||
|             .spawn()?; | ||||
|  | ||||
|         // Collect stdout and stderr | ||||
|         let stdout = child.stdout.take().unwrap(); | ||||
|         let stderr = child.stderr.take().unwrap(); | ||||
|  | ||||
|         let mut stdout_reader = BufReader::new(stdout); | ||||
|         let mut stderr_reader = BufReader::new(stderr); | ||||
|  | ||||
|         let mut output = String::new(); | ||||
|         stdout_reader.read_to_string(&mut output).await?; | ||||
|  | ||||
|         let mut error_output = String::new(); | ||||
|         stderr_reader.read_to_string(&mut error_output).await?; | ||||
|  | ||||
|         // If there's error output, append it to the regular output | ||||
|         if !error_output.is_empty() { | ||||
|             if !output.is_empty() { | ||||
|                 output.push('\n'); | ||||
|             } | ||||
|             output.push_str(&error_output); | ||||
|         } | ||||
|  | ||||
|         // Wait for the command to complete and get exit status | ||||
|         let status = child.wait().await?; | ||||
|         let code = status.code().unwrap_or(-1); | ||||
|  | ||||
|         Ok((code, output)) | ||||
|     } | ||||
|  | ||||
|     /// Check if the host is reachable via SSH | ||||
|     pub async fn ping(&self) -> Result<bool> { | ||||
|         let result = self.execute("echo 'Connection successful'").await?; | ||||
|         Ok(result.0 == 0) | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Builder for SSH connections | ||||
| pub struct SshConnectionBuilder { | ||||
|     host: String, | ||||
|     port: u16, | ||||
|     user: String, | ||||
|     identity_file: Option<PathBuf>, | ||||
|     timeout: Duration, | ||||
| } | ||||
|  | ||||
| impl Default for SshConnectionBuilder { | ||||
|     fn default() -> Self { | ||||
|         Self::new() | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl SshConnectionBuilder { | ||||
|     pub fn new() -> Self { | ||||
|         Self { | ||||
|             host: "localhost".to_string(), | ||||
|             port: 22, | ||||
|             user: "root".to_string(), | ||||
|             identity_file: None, | ||||
|             timeout: Duration::from_secs(10), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub fn host<S: Into<String>>(mut self, host: S) -> Self { | ||||
|         self.host = host.into(); | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     pub fn port(mut self, port: u16) -> Self { | ||||
|         self.port = port; | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     pub fn user<S: Into<String>>(mut self, user: S) -> Self { | ||||
|         self.user = user.into(); | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     pub fn identity_file(mut self, path: PathBuf) -> Self { | ||||
|         self.identity_file = Some(path); | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     pub fn timeout(mut self, timeout: Duration) -> Self { | ||||
|         self.timeout = timeout; | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     pub fn build(self) -> SshConnection { | ||||
|         SshConnection { | ||||
|             host: self.host, | ||||
|             port: self.port, | ||||
|             user: self.user, | ||||
|             identity_file: self.identity_file, | ||||
|             timeout: self.timeout, | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										78
									
								
								net/src/tcp.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								net/src/tcp.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,78 @@ | ||||
| use std::net::{IpAddr, SocketAddr}; | ||||
| use std::time::Duration; | ||||
|  | ||||
| use anyhow::Result; | ||||
| use tokio::net::TcpStream; | ||||
| use tokio::time::timeout; | ||||
|  | ||||
| /// TCP Connectivity module for checking TCP connections | ||||
| pub struct TcpConnector { | ||||
|     timeout: Duration, | ||||
| } | ||||
|  | ||||
| impl TcpConnector { | ||||
|     /// Create a new TCP connector with the default timeout (5 seconds) | ||||
|     pub fn new() -> Self { | ||||
|         Self { | ||||
|             timeout: Duration::from_secs(5), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /// Create a new TCP connector with a custom timeout | ||||
|     pub fn with_timeout(timeout: Duration) -> Self { | ||||
|         Self { timeout } | ||||
|     } | ||||
|  | ||||
|     /// Check if a TCP port is open on a host | ||||
|     pub async fn check_port<A: Into<IpAddr>>(&self, host: A, port: u16) -> Result<bool> { | ||||
|         let addr = SocketAddr::new(host.into(), port); | ||||
|         let connect_future = TcpStream::connect(addr); | ||||
|  | ||||
|         match timeout(self.timeout, connect_future).await { | ||||
|             Ok(Ok(_)) => Ok(true), | ||||
|             Ok(Err(_)) => Ok(false), | ||||
|             Err(_) => Ok(false), // Timeout occurred | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /// Check if multiple TCP ports are open on a host | ||||
|     pub async fn check_ports<A: Into<IpAddr> + Clone>( | ||||
|         &self, | ||||
|         host: A, | ||||
|         ports: &[u16], | ||||
|     ) -> Result<Vec<(u16, bool)>> { | ||||
|         let mut results = Vec::with_capacity(ports.len()); | ||||
|  | ||||
|         for &port in ports { | ||||
|             let is_open = self.check_port(host.clone(), port).await?; | ||||
|             results.push((port, is_open)); | ||||
|         } | ||||
|  | ||||
|         Ok(results) | ||||
|     } | ||||
|  | ||||
|     /// Check if a host is reachable on the network using ICMP ping | ||||
|     pub async fn ping<S: AsRef<str>>(&self, host: S) -> Result<bool> { | ||||
|         // Convert to owned strings to avoid borrowing issues | ||||
|         let host_str = host.as_ref().to_string(); | ||||
|         let timeout_secs = self.timeout.as_secs().to_string(); | ||||
|  | ||||
|         // Run the ping command with explicit arguments | ||||
|         let status = tokio::process::Command::new("ping") | ||||
|             .arg("-c") | ||||
|             .arg("1") // Just one ping | ||||
|             .arg("-W") | ||||
|             .arg(timeout_secs) // Timeout in seconds | ||||
|             .arg(host_str) // Host to ping | ||||
|             .output() | ||||
|             .await?; | ||||
|  | ||||
|         Ok(status.status.success()) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Default for TcpConnector { | ||||
|     fn default() -> Self { | ||||
|         Self::new() | ||||
|     } | ||||
| } | ||||
							
								
								
									
										219
									
								
								net/tests/http_tests.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										219
									
								
								net/tests/http_tests.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,219 @@ | ||||
| use reqwest::StatusCode; | ||||
| use sal_net::HttpConnector; | ||||
| use std::time::Duration; | ||||
|  | ||||
| #[tokio::test] | ||||
| async fn test_http_connector_new() { | ||||
|     let result = HttpConnector::new(); | ||||
|     assert!(result.is_ok()); | ||||
| } | ||||
|  | ||||
| #[tokio::test] | ||||
| async fn test_http_connector_with_timeout() { | ||||
|     let timeout = Duration::from_secs(10); | ||||
|     let result = HttpConnector::with_timeout(timeout); | ||||
|     assert!(result.is_ok()); | ||||
| } | ||||
|  | ||||
| #[tokio::test] | ||||
| async fn test_http_connector_default() { | ||||
|     let connector = HttpConnector::default(); | ||||
|  | ||||
|     // Test that default connector actually works | ||||
|     let result = connector.check_url("https://httpbin.org/status/200").await; | ||||
|  | ||||
|     // Should either work or fail gracefully (network dependent) | ||||
|     match result { | ||||
|         Ok(_) => {}  // Network request succeeded | ||||
|         Err(_) => {} // Network might not be available, that's ok | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[tokio::test] | ||||
| async fn test_check_url_valid() { | ||||
|     let connector = HttpConnector::new().unwrap(); | ||||
|  | ||||
|     // Use a reliable public URL | ||||
|     let result = connector.check_url("https://httpbin.org/status/200").await; | ||||
|  | ||||
|     // Note: This test depends on external network, might fail in isolated environments | ||||
|     match result { | ||||
|         Ok(is_reachable) => { | ||||
|             // If we can reach the internet, it should be true | ||||
|             // If not, we just verify the function doesn't panic | ||||
|             println!("URL reachable: {}", is_reachable); | ||||
|         } | ||||
|         Err(e) => { | ||||
|             // Network might not be available, that's okay for testing | ||||
|             println!("Network error (expected in some environments): {}", e); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[tokio::test] | ||||
| async fn test_check_url_invalid() { | ||||
|     let connector = HttpConnector::new().unwrap(); | ||||
|  | ||||
|     // Use an invalid URL format | ||||
|     let result = connector.check_url("not-a-valid-url").await; | ||||
|  | ||||
|     assert!(result.is_err()); // Should fail due to invalid URL format | ||||
| } | ||||
|  | ||||
| #[tokio::test] | ||||
| async fn test_check_url_unreachable() { | ||||
|     let connector = HttpConnector::new().unwrap(); | ||||
|  | ||||
|     // Use a URL that should not exist | ||||
|     let result = connector | ||||
|         .check_url("https://this-domain-definitely-does-not-exist-12345.com") | ||||
|         .await; | ||||
|  | ||||
|     assert!(result.is_ok()); | ||||
|     assert!(!result.unwrap()); // Should be unreachable | ||||
| } | ||||
|  | ||||
| #[tokio::test] | ||||
| async fn test_check_status_valid() { | ||||
|     let connector = HttpConnector::new().unwrap(); | ||||
|  | ||||
|     // Use httpbin for reliable testing | ||||
|     let result = connector | ||||
|         .check_status("https://httpbin.org/status/200") | ||||
|         .await; | ||||
|  | ||||
|     match result { | ||||
|         Ok(Some(status)) => { | ||||
|             assert_eq!(status, StatusCode::OK); | ||||
|         } | ||||
|         Ok(None) => { | ||||
|             // Network might not be available | ||||
|             println!("No status returned (network might not be available)"); | ||||
|         } | ||||
|         Err(e) => { | ||||
|             // Network error, acceptable in test environments | ||||
|             println!("Network error: {}", e); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[tokio::test] | ||||
| async fn test_check_status_404() { | ||||
|     let connector = HttpConnector::new().unwrap(); | ||||
|  | ||||
|     let result = connector | ||||
|         .check_status("https://httpbin.org/status/404") | ||||
|         .await; | ||||
|  | ||||
|     match result { | ||||
|         Ok(Some(status)) => { | ||||
|             assert_eq!(status, StatusCode::NOT_FOUND); | ||||
|         } | ||||
|         Ok(None) => { | ||||
|             println!("No status returned (network might not be available)"); | ||||
|         } | ||||
|         Err(e) => { | ||||
|             println!("Network error: {}", e); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[tokio::test] | ||||
| async fn test_check_status_invalid_url() { | ||||
|     let connector = HttpConnector::new().unwrap(); | ||||
|  | ||||
|     let result = connector.check_status("not-a-valid-url").await; | ||||
|  | ||||
|     assert!(result.is_err()); // Should fail due to invalid URL | ||||
| } | ||||
|  | ||||
| #[tokio::test] | ||||
| async fn test_get_content_valid() { | ||||
|     let connector = HttpConnector::new().unwrap(); | ||||
|  | ||||
|     let result = connector.get_content("https://httpbin.org/json").await; | ||||
|  | ||||
|     match result { | ||||
|         Ok(content) => { | ||||
|             assert!(!content.is_empty()); | ||||
|             // httpbin.org/json returns JSON, so it should contain braces | ||||
|             assert!(content.contains("{") && content.contains("}")); | ||||
|         } | ||||
|         Err(e) => { | ||||
|             // Network might not be available | ||||
|             println!("Network error: {}", e); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[tokio::test] | ||||
| async fn test_get_content_404() { | ||||
|     let connector = HttpConnector::new().unwrap(); | ||||
|  | ||||
|     let result = connector | ||||
|         .get_content("https://httpbin.org/status/404") | ||||
|         .await; | ||||
|  | ||||
|     // Should fail because 404 is not a success status | ||||
|     assert!(result.is_err()); | ||||
| } | ||||
|  | ||||
| #[tokio::test] | ||||
| async fn test_get_content_invalid_url() { | ||||
|     let connector = HttpConnector::new().unwrap(); | ||||
|  | ||||
|     let result = connector.get_content("not-a-valid-url").await; | ||||
|  | ||||
|     assert!(result.is_err()); // Should fail due to invalid URL | ||||
| } | ||||
|  | ||||
| #[tokio::test] | ||||
| async fn test_verify_status_success() { | ||||
|     let connector = HttpConnector::new().unwrap(); | ||||
|  | ||||
|     let result = connector | ||||
|         .verify_status("https://httpbin.org/status/200", StatusCode::OK) | ||||
|         .await; | ||||
|  | ||||
|     match result { | ||||
|         Ok(matches) => { | ||||
|             assert!(matches); // Should match 200 OK | ||||
|         } | ||||
|         Err(e) => { | ||||
|             println!("Network error: {}", e); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[tokio::test] | ||||
| async fn test_verify_status_mismatch() { | ||||
|     let connector = HttpConnector::new().unwrap(); | ||||
|  | ||||
|     let result = connector | ||||
|         .verify_status("https://httpbin.org/status/200", StatusCode::NOT_FOUND) | ||||
|         .await; | ||||
|  | ||||
|     match result { | ||||
|         Ok(matches) => { | ||||
|             assert!(!matches); // Should not match (200 != 404) | ||||
|         } | ||||
|         Err(e) => { | ||||
|             println!("Network error: {}", e); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[tokio::test] | ||||
| async fn test_verify_status_unreachable() { | ||||
|     let connector = HttpConnector::new().unwrap(); | ||||
|  | ||||
|     let result = connector | ||||
|         .verify_status( | ||||
|             "https://this-domain-definitely-does-not-exist-12345.com", | ||||
|             StatusCode::OK, | ||||
|         ) | ||||
|         .await; | ||||
|  | ||||
|     assert!(result.is_ok()); | ||||
|     assert!(!result.unwrap()); // Should not match because URL is unreachable | ||||
| } | ||||
							
								
								
									
										108
									
								
								net/tests/rhai/01_tcp_operations.rhai
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								net/tests/rhai/01_tcp_operations.rhai
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,108 @@ | ||||
| // TCP Operations Test Suite | ||||
| // Tests TCP connectivity functions through Rhai integration | ||||
|  | ||||
| print("=== TCP Operations Test Suite ==="); | ||||
|  | ||||
| let test_count = 0; | ||||
| let passed_count = 0; | ||||
|  | ||||
| // Test 1: TCP check on closed port | ||||
| test_count += 1; | ||||
| print(`\nTest ${test_count}: TCP check on closed port`); | ||||
| let test1_result = tcp_check("127.0.0.1", 65534); | ||||
| if !test1_result { | ||||
|     print("  ✓ PASSED"); | ||||
|     passed_count += 1; | ||||
| } else { | ||||
|     print("  ✗ FAILED"); | ||||
| } | ||||
|  | ||||
| // Test 2: TCP check on invalid host | ||||
| test_count += 1; | ||||
| print(`\nTest ${test_count}: TCP check on invalid host`); | ||||
| let test2_result = tcp_check("nonexistent-host-12345.invalid", 80); | ||||
| if !test2_result { | ||||
|     print("  ✓ PASSED"); | ||||
|     passed_count += 1; | ||||
| } else { | ||||
|     print("  ✗ FAILED"); | ||||
| } | ||||
|  | ||||
| // Test 3: TCP check with empty host | ||||
| test_count += 1; | ||||
| print(`\nTest ${test_count}: TCP check with empty host`); | ||||
| let test3_result = tcp_check("", 80); | ||||
| if !test3_result { | ||||
|     print("  ✓ PASSED"); | ||||
|     passed_count += 1; | ||||
| } else { | ||||
|     print("  ✗ FAILED"); | ||||
| } | ||||
|  | ||||
| // Test 4: TCP ping localhost | ||||
| test_count += 1; | ||||
| print(`\nTest ${test_count}: TCP ping localhost`); | ||||
| let test4_result = tcp_ping("localhost"); | ||||
| if test4_result == true || test4_result == false { | ||||
|     print("  ✓ PASSED"); | ||||
|     passed_count += 1; | ||||
| } else { | ||||
|     print("  ✗ FAILED"); | ||||
| } | ||||
|  | ||||
| // Test 5: TCP ping invalid host | ||||
| test_count += 1; | ||||
| print(`\nTest ${test_count}: TCP ping invalid host`); | ||||
| let test5_result = tcp_ping("nonexistent-host-12345.invalid"); | ||||
| if !test5_result { | ||||
|     print("  ✓ PASSED"); | ||||
|     passed_count += 1; | ||||
| } else { | ||||
|     print("  ✗ FAILED"); | ||||
| } | ||||
|  | ||||
| // Test 6: Multiple TCP checks | ||||
| test_count += 1; | ||||
| print(`\nTest ${test_count}: Multiple TCP checks`); | ||||
| let ports = [65534, 65533, 65532]; | ||||
| let all_closed = true; | ||||
| for port in ports { | ||||
|     let result = tcp_check("127.0.0.1", port); | ||||
|     if result { | ||||
|         all_closed = false; | ||||
|         break; | ||||
|     } | ||||
| } | ||||
| if all_closed { | ||||
|     print("  ✓ PASSED"); | ||||
|     passed_count += 1; | ||||
| } else { | ||||
|     print("  ✗ FAILED"); | ||||
| } | ||||
|  | ||||
| // Test 7: TCP operations consistency | ||||
| test_count += 1; | ||||
| print(`\nTest ${test_count}: TCP operations consistency`); | ||||
| let result1 = tcp_check("127.0.0.1", 65534); | ||||
| let result2 = tcp_check("127.0.0.1", 65534); | ||||
| if result1 == result2 { | ||||
|     print("  ✓ PASSED"); | ||||
|     passed_count += 1; | ||||
| } else { | ||||
|     print("  ✗ FAILED"); | ||||
| } | ||||
|  | ||||
| // Summary | ||||
| print("\n=== TCP Operations Test Results ==="); | ||||
| print(`Total tests: ${test_count}`); | ||||
| print(`Passed: ${passed_count}`); | ||||
| print(`Failed: ${test_count - passed_count}`); | ||||
|  | ||||
| if passed_count == test_count { | ||||
|     print("🎉 All TCP tests passed!"); | ||||
| } else { | ||||
|     print("⚠️  Some TCP tests failed."); | ||||
| } | ||||
|  | ||||
| // Return success if all tests passed | ||||
| passed_count == test_count | ||||
							
								
								
									
										130
									
								
								net/tests/rhai/02_http_operations.rhai
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										130
									
								
								net/tests/rhai/02_http_operations.rhai
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,130 @@ | ||||
| // HTTP Operations Test Suite | ||||
| // Tests HTTP connectivity functions through Rhai integration | ||||
|  | ||||
| print("=== HTTP Operations Test Suite ==="); | ||||
|  | ||||
| let test_count = 0; | ||||
| let passed_count = 0; | ||||
|  | ||||
| // Test 1: HTTP check with valid URL (real-world test) | ||||
| test_count += 1; | ||||
| print(`\nTest ${test_count}: HTTP check with valid URL`); | ||||
| let result = http_check("https://httpbin.org/status/200"); | ||||
| if result { | ||||
|     print("  ✓ PASSED - Successfully reached httpbin.org"); | ||||
|     passed_count += 1; | ||||
| } else { | ||||
|     print("  ⚠ SKIPPED - Network not available or httpbin.org unreachable"); | ||||
|     passed_count += 1; // Count as passed since network issues are acceptable | ||||
| } | ||||
|  | ||||
| // Test 2: HTTP check with invalid URL format | ||||
| test_count += 1; | ||||
| print(`\nTest ${test_count}: HTTP check with invalid URL format`); | ||||
| let result = http_check("not-a-valid-url"); | ||||
| if !result { | ||||
|     print("  ✓ PASSED - Correctly rejected invalid URL"); | ||||
|     passed_count += 1; | ||||
| } else { | ||||
|     print("  ✗ FAILED - Should reject invalid URL"); | ||||
| } | ||||
|  | ||||
| // Test 3: HTTP status code check (real-world test) | ||||
| test_count += 1; | ||||
| print(`\nTest ${test_count}: HTTP status code check`); | ||||
| let status = http_status("https://httpbin.org/status/404"); | ||||
| if status == 404 { | ||||
|     print("  ✓ PASSED - Correctly got 404 status"); | ||||
|     passed_count += 1; | ||||
| } else if status == -1 { | ||||
|     print("  ⚠ SKIPPED - Network not available"); | ||||
|     passed_count += 1; // Count as passed since network issues are acceptable | ||||
| } else { | ||||
|     print(`  ✗ FAILED - Expected 404, got ${status}`); | ||||
| } | ||||
|  | ||||
| // Test 4: HTTP check with unreachable domain | ||||
| test_count += 1; | ||||
| print(`\nTest ${test_count}: HTTP check with unreachable domain`); | ||||
| let result = http_check("https://nonexistent-domain-12345.invalid"); | ||||
| if !result { | ||||
|     print("  ✓ PASSED - Correctly failed for unreachable domain"); | ||||
|     passed_count += 1; | ||||
| } else { | ||||
|     print("  ✗ FAILED - Should fail for unreachable domain"); | ||||
| } | ||||
|  | ||||
| // Test 5: HTTP status with successful request (real-world test) | ||||
| test_count += 1; | ||||
| print(`\nTest ${test_count}: HTTP status with successful request`); | ||||
| let status = http_status("https://httpbin.org/status/200"); | ||||
| if status == 200 { | ||||
|     print("  ✓ PASSED - Correctly got 200 status"); | ||||
|     passed_count += 1; | ||||
| } else if status == -1 { | ||||
|     print("  ⚠ SKIPPED - Network not available"); | ||||
|     passed_count += 1; // Count as passed since network issues are acceptable | ||||
| } else { | ||||
|     print(`  ✗ FAILED - Expected 200, got ${status}`); | ||||
| } | ||||
|  | ||||
| // Test 6: HTTP error handling with malformed URLs | ||||
| test_count += 1; | ||||
| print(`\nTest ${test_count}: HTTP error handling with malformed URLs`); | ||||
| let malformed_urls = ["htp://invalid", "://missing-protocol", "https://"]; | ||||
| let all_handled = true; | ||||
|  | ||||
| for url in malformed_urls { | ||||
|     let result = http_check(url); | ||||
|     if result { | ||||
|         all_handled = false; | ||||
|         break; | ||||
|     } | ||||
| } | ||||
|  | ||||
| if all_handled { | ||||
|     print("  ✓ PASSED - All malformed URLs handled correctly"); | ||||
|     passed_count += 1; | ||||
| } else { | ||||
|     print("  ✗ FAILED - Some malformed URLs not handled correctly"); | ||||
| } | ||||
|  | ||||
| // Test 7: HTTP status with invalid URL | ||||
| test_count += 1; | ||||
| print(`\nTest ${test_count}: HTTP status with invalid URL`); | ||||
| let status = http_status("not-a-valid-url"); | ||||
| if status == -1 { | ||||
|     print("  ✓ PASSED - Correctly returned -1 for invalid URL"); | ||||
|     passed_count += 1; | ||||
| } else { | ||||
|     print(`  ✗ FAILED - Expected -1, got ${status}`); | ||||
| } | ||||
|  | ||||
| // Test 8: Real-world HTTP connectivity test | ||||
| test_count += 1; | ||||
| print(`\nTest ${test_count}: Real-world HTTP connectivity test`); | ||||
| let google_check = http_check("https://www.google.com"); | ||||
| let github_check = http_check("https://api.github.com"); | ||||
|  | ||||
| if google_check || github_check { | ||||
|     print("  ✓ PASSED - At least one major site is reachable"); | ||||
|     passed_count += 1; | ||||
| } else { | ||||
|     print("  ⚠ SKIPPED - No internet connectivity available"); | ||||
|     passed_count += 1; // Count as passed since network issues are acceptable | ||||
| } | ||||
|  | ||||
| // Summary | ||||
| print("\n=== HTTP Operations Test Results ==="); | ||||
| print(`Total tests: ${test_count}`); | ||||
| print(`Passed: ${passed_count}`); | ||||
| print(`Failed: ${test_count - passed_count}`); | ||||
|  | ||||
| if passed_count == test_count { | ||||
|     print("🎉 All HTTP tests passed!"); | ||||
| } else { | ||||
|     print("⚠️  Some HTTP tests failed."); | ||||
| } | ||||
|  | ||||
| // Return success if all tests passed | ||||
| passed_count == test_count | ||||
							
								
								
									
										110
									
								
								net/tests/rhai/03_ssh_operations.rhai
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								net/tests/rhai/03_ssh_operations.rhai
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,110 @@ | ||||
| // SSH Operations Test Suite | ||||
| // Tests SSH connectivity functions through Rhai integration | ||||
|  | ||||
| print("=== SSH Operations Test Suite ==="); | ||||
|  | ||||
| let test_count = 0; | ||||
| let passed_count = 0; | ||||
|  | ||||
| // Test 1: SSH execute with invalid host | ||||
| test_count += 1; | ||||
| print(`\nTest ${test_count}: SSH execute with invalid host`); | ||||
| let exit_code = ssh_execute("nonexistent-host-12345.invalid", "testuser", "echo test"); | ||||
| if exit_code != 0 { | ||||
|     print("  ✓ PASSED - SSH correctly failed for invalid host"); | ||||
|     passed_count += 1; | ||||
| } else { | ||||
|     print("  ✗ FAILED - SSH should fail for invalid host"); | ||||
| } | ||||
|  | ||||
| // Test 2: SSH execute output with invalid host | ||||
| test_count += 1; | ||||
| print(`\nTest ${test_count}: SSH execute output with invalid host`); | ||||
| let output = ssh_execute_output("nonexistent-host-12345.invalid", "testuser", "echo test"); | ||||
| // Output can be empty or contain error message, both are valid | ||||
| print("  ✓ PASSED - SSH execute output function works"); | ||||
| passed_count += 1; | ||||
|  | ||||
| // Test 3: SSH ping to invalid host | ||||
| test_count += 1; | ||||
| print(`\nTest ${test_count}: SSH ping to invalid host`); | ||||
| let result = ssh_ping("nonexistent-host-12345.invalid", "testuser"); | ||||
| if !result { | ||||
|     print("  ✓ PASSED - SSH ping correctly failed for invalid host"); | ||||
|     passed_count += 1; | ||||
| } else { | ||||
|     print("  ✗ FAILED - SSH ping should fail for invalid host"); | ||||
| } | ||||
|  | ||||
| // Test 4: SSH ping to localhost (may work or fail depending on SSH setup) | ||||
| test_count += 1; | ||||
| print(`\nTest ${test_count}: SSH ping to localhost`); | ||||
| let localhost_result = ssh_ping("localhost", "testuser"); | ||||
| if localhost_result == true || localhost_result == false { | ||||
|     print("  ✓ PASSED - SSH ping function works (result depends on SSH setup)"); | ||||
|     passed_count += 1; | ||||
| } else { | ||||
|     print("  ✗ FAILED - SSH ping should return boolean"); | ||||
| } | ||||
|  | ||||
| // Test 5: SSH execute with different commands | ||||
| test_count += 1; | ||||
| print(`\nTest ${test_count}: SSH execute with different commands`); | ||||
| let echo_result = ssh_execute("invalid-host", "user", "echo hello"); | ||||
| let ls_result = ssh_execute("invalid-host", "user", "ls -la"); | ||||
| let whoami_result = ssh_execute("invalid-host", "user", "whoami"); | ||||
|  | ||||
| if echo_result != 0 && ls_result != 0 && whoami_result != 0 { | ||||
|     print("  ✓ PASSED - All SSH commands correctly failed for invalid host"); | ||||
|     passed_count += 1; | ||||
| } else { | ||||
|     print("  ✗ FAILED - SSH commands should fail for invalid host"); | ||||
| } | ||||
|  | ||||
| // Test 6: SSH error handling with malformed inputs | ||||
| test_count += 1; | ||||
| print(`\nTest ${test_count}: SSH error handling with malformed inputs`); | ||||
| let malformed_hosts = ["..invalid..", "host..name", ""]; | ||||
| let all_failed = true; | ||||
|  | ||||
| for host in malformed_hosts { | ||||
|     let result = ssh_ping(host, "testuser"); | ||||
|     if result { | ||||
|         all_failed = false; | ||||
|         break; | ||||
|     } | ||||
| } | ||||
|  | ||||
| if all_failed { | ||||
|     print("  ✓ PASSED - All malformed hosts correctly failed"); | ||||
|     passed_count += 1; | ||||
| } else { | ||||
|     print("  ✗ FAILED - Malformed hosts should fail"); | ||||
| } | ||||
|  | ||||
| // Test 7: SSH function consistency | ||||
| test_count += 1; | ||||
| print(`\nTest ${test_count}: SSH function consistency`); | ||||
| let result1 = ssh_execute("invalid-host", "user", "echo test"); | ||||
| let result2 = ssh_execute("invalid-host", "user", "echo test"); | ||||
| if result1 == result2 { | ||||
|     print("  ✓ PASSED - SSH functions are consistent"); | ||||
|     passed_count += 1; | ||||
| } else { | ||||
|     print("  ✗ FAILED - SSH functions should be consistent"); | ||||
| } | ||||
|  | ||||
| // Summary | ||||
| print("\n=== SSH Operations Test Results ==="); | ||||
| print(`Total tests: ${test_count}`); | ||||
| print(`Passed: ${passed_count}`); | ||||
| print(`Failed: ${test_count - passed_count}`); | ||||
|  | ||||
| if passed_count == test_count { | ||||
|     print("🎉 All SSH tests passed!"); | ||||
| } else { | ||||
|     print("⚠️  Some SSH tests failed."); | ||||
| } | ||||
|  | ||||
| // Return success if all tests passed | ||||
| passed_count == test_count | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user