use std::time::Duration; use tokio::time::sleep; use runner_rust::{JobBuilder, Client}; use runner_rust::job::JobSignature; use uuid::Uuid; use rhai::Engine; /// Helper function to create a SAL engine for testing fn create_test_sal_engine() -> Engine { // Create a basic Rhai engine for testing Engine::new() } /// Test job execution with empty signatures array /// This verifies that jobs without signatures can execute successfully #[tokio::test] async fn test_job_execution_without_signatures() { let redis_url = "redis://localhost:6379"; let runner_id = format!("test-runner-{}", Uuid::new_v4()); // Create client let mut client = Client::builder() .redis_url(redis_url) .build() .await .expect("Failed to create client"); // Create job with empty signatures array let job = JobBuilder::new() .caller_id("test_caller") .context_id("test_context") .payload("print(\"Hello from unsigned job\");") .runner(&runner_id) .executor("rhai") .timeout(30) .build() .expect("Job creation should succeed"); let job_id = job.id.clone(); // Verify signatures array is empty assert!(job.signatures.is_empty(), "Job should have no signatures"); // Save job to Redis client.store_job_in_redis(&job).await .expect("Failed to save job to Redis"); // Queue the job client.dispatch_job(&job_id, &runner_id).await .expect("Failed to queue job"); // Spawn runner in background let runner_id_clone = runner_id.clone(); let redis_url_clone = redis_url.to_string(); let runner_handle = tokio::spawn(async move { let (shutdown_tx, shutdown_rx) = tokio::sync::mpsc::channel::<()>(1); // Run for 5 seconds then shutdown tokio::spawn(async move { sleep(Duration::from_secs(5)).await; let _ = shutdown_tx.send(()).await; }); runner_rust::spawn_sync_runner( runner_id_clone, redis_url_clone, shutdown_rx, create_test_sal_engine, ).await }); // Wait for job to be processed sleep(Duration::from_secs(3)).await; // Check job result let result = client.get_result(&job_id).await .expect("Failed to get job result"); assert!(result.is_some(), "Job should have produced a result"); // Cleanup let _ = runner_handle.await; client.delete_job(&job_id).await.ok(); } /// Test job signature verification with valid signatures /// This verifies that jobs with valid signatures are accepted #[tokio::test] async fn test_job_signature_verification() { use secp256k1::{Secp256k1, SecretKey, Message}; use sha2::{Sha256, Digest}; let redis_url = "redis://localhost:6379"; let runner_id = format!("test-runner-{}", Uuid::new_v4()); // Create client let mut client = Client::builder() .redis_url(redis_url) .build() .await .expect("Failed to create client"); // Generate a keypair for signing let secp = Secp256k1::new(); let secret_key = SecretKey::from_slice(&[0xcd; 32]) .expect("32 bytes, within curve order"); let public_key = secp256k1::PublicKey::from_secret_key(&secp, &secret_key); // Create job let mut job = JobBuilder::new() .caller_id("test_caller") .context_id("test_context") .payload("print(\"Hello from signed job\");") .runner(&runner_id) .executor("rhai") .timeout(30) .build() .expect("Job creation should succeed"); let job_id = job.id.clone(); // Sign the job let canonical = job.canonical_representation(); let mut hasher = Sha256::new(); hasher.update(canonical.as_bytes()); let hash = hasher.finalize(); let message = Message::from_digest_slice(&hash) .expect("32 bytes"); let signature = secp.sign_ecdsa(&message, &secret_key); // Add signature to job job.signatures.push(JobSignature { public_key: hex::encode(public_key.serialize()), signature: hex::encode(signature.serialize_compact()), }); // Verify the job has a signature assert_eq!(job.signatures.len(), 1, "Job should have one signature"); // Verify signatures are valid job.verify_signatures() .expect("Signature verification should succeed"); // Save and queue job client.store_job_in_redis(&job).await .expect("Failed to save job to Redis"); client.dispatch_job(&job_id, &runner_id).await .expect("Failed to queue job"); // Spawn runner let runner_id_clone = runner_id.clone(); let redis_url_clone = redis_url.to_string(); let runner_handle = tokio::spawn(async move { let (shutdown_tx, shutdown_rx) = tokio::sync::mpsc::channel::<()>(1); tokio::spawn(async move { sleep(Duration::from_secs(5)).await; let _ = shutdown_tx.send(()).await; }); runner_rust::spawn_sync_runner( runner_id_clone, redis_url_clone, shutdown_rx, create_test_sal_engine, ).await }); // Wait for processing sleep(Duration::from_secs(3)).await; // Check result let result = client.get_result(&job_id).await .expect("Failed to get job result"); assert!(result.is_some(), "Signed job should have produced a result"); // Cleanup let _ = runner_handle.await; client.delete_job(&job_id).await.ok(); } /// Test job with invalid signature is rejected #[tokio::test] async fn test_job_invalid_signature_rejected() { // Create job with invalid signature let mut job = JobBuilder::new() .caller_id("test_caller") .context_id("test_context") .payload("print(\"This should fail\");") .runner("test-runner") .executor("rhai") .build() .expect("Job creation should succeed"); // Add invalid signature job.signatures.push(JobSignature { public_key: "04invalid_public_key".to_string(), signature: "invalid_signature".to_string(), }); // Verify signatures should fail let result = job.verify_signatures(); assert!(result.is_err(), "Invalid signature should be rejected"); } /// Test job creation and serialization #[tokio::test] async fn test_job_creation_and_serialization() { let job = JobBuilder::new() .caller_id("test_caller") .context_id("test_context") .payload("ping") .runner("default") .executor("rhai") .build() .expect("Job creation should succeed"); assert_eq!(job.caller_id, "test_caller"); assert_eq!(job.context_id, "test_context"); assert_eq!(job.payload, "ping"); assert_eq!(job.runner, "default"); assert_eq!(job.executor, "rhai"); assert!(job.signatures.is_empty(), "Default job should have no signatures"); // Test serialization let json = serde_json::to_string(&job) .expect("Job should serialize to JSON"); // Test deserialization let deserialized: runner_rust::Job = serde_json::from_str(&json) .expect("Job should deserialize from JSON"); assert_eq!(job.id, deserialized.id); assert_eq!(job.caller_id, deserialized.caller_id); assert_eq!(job.signatures.len(), deserialized.signatures.len()); }