Files
runner_rust/tests/e2e_tests.rs
2025-11-04 13:09:13 +01:00

238 lines
7.4 KiB
Rust

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());
}