238 lines
7.4 KiB
Rust
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());
|
|
}
|