diff --git a/Cargo.toml b/Cargo.toml index a7cb856..0029aca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ members = [ "crates/core", "crates/public", "crates/worker", + "e2e-tests", ] [workspace.dependencies] @@ -14,7 +15,6 @@ irelia_adapter = { path = "crates/adapter" } irelia_common = { path = "crates/common" } irelia_core = { path = "crates/core" } - anyhow = { version = "1.0.87" } aptos-sdk = { git = "https://github.com/aptos-labs/aptos-core", branch = "mainnet" } aptos-testcontainer = { version = "0.1.2", features = ["testing"] } @@ -37,11 +37,12 @@ opentelemetry = { version = "0.26.0" } opentelemetry-otlp = { version = "0.26.0" } opentelemetry-semantic-conventions = { version = "0.26.0" } opentelemetry_sdk = { version = "0.26.0" } +rand = { version = "0.8.5" } rand_core = { version = "0.5.1" } readonly = { version = "0.2.12" } redis-async = { version = "0.17.2" } regex = { version = "1.11.0" } -reqwest = { version = "0.12.9" } +reqwest = { version = "0.12.9", features = ["json"] } scopeguard = { version = "1.2.0" } serde = { version = "1.0.210", features = ["derive"] } serde_json = { version = "1.0.128" } @@ -49,10 +50,10 @@ sqlx = { version = "*" } stone-cli = { git = "https://github.com/zksecurity/stone-cli.git", branch = "main" } tempfile = { version = "3.13.0" } test-log = { version = "0.2.16" } +testcontainers-modules = { version = "0.11.4" } thiserror = { version = "1.0.64" } -tokio = { version = "1.39.3", features = ["macros", "rt-multi-thread"] } +tokio = { version = "1.39.3", features = ["full"] } tokio-postgres = { version = "0.7.12" } -toml = { version = "0.8.19" } tower-http = { version = "0.6.1" } tracing = { version = "0.1.40" } tracing-bunyan-formatter = { version = "0.3.9" } diff --git a/crates/public/Cargo.toml b/crates/public/Cargo.toml index 3a56027..69c7e3c 100644 --- a/crates/public/Cargo.toml +++ b/crates/public/Cargo.toml @@ -17,14 +17,11 @@ deadpool-diesel = { workspace = true, features = ["postgres", "serde"] } diesel_migrations = { workspace = true } opentelemetry = { workspace = true } readonly = { workspace = true } -reqwest = { workspace = true, features = ["json"] } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } stone-cli = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true, features = ["full"] } -tokio-postgres = { workspace = true } -toml = { workspace = true } tower-http = { workspace = true, features = ["timeout", "trace"] } tracing = { workspace = true } uuid = { workspace = true } diff --git a/crates/public/src/lib.rs b/crates/public/src/lib.rs index 8fb8dd1..e1d0287 100644 --- a/crates/public/src/lib.rs +++ b/crates/public/src/lib.rs @@ -5,5 +5,4 @@ pub mod json_response; pub mod options; pub mod router; pub mod services; -pub mod tests; pub mod utils; diff --git a/crates/public/src/tests/mod.rs b/crates/public/src/tests/mod.rs deleted file mode 100644 index 8c8905e..0000000 --- a/crates/public/src/tests/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -#[cfg(test)] -pub mod test_add_job; -#[cfg(test)] -pub mod test_get_status; diff --git a/crates/public/src/tests/test_add_job.rs b/crates/public/src/tests/test_add_job.rs deleted file mode 100644 index 67a2ffa..0000000 --- a/crates/public/src/tests/test_add_job.rs +++ /dev/null @@ -1,135 +0,0 @@ -use std::fs; - -use reqwest::Client; -use serde_json::{json, Value}; -use tokio; -use uuid::Uuid; - -use crate::options::Options; - -#[tokio::test] -async fn test_add_job() { - let client = Client::new(); - - let config_content = - fs::read_to_string("./config/00-default.toml").expect("Failed to read config file"); - - let options: Options = toml::from_str(&config_content).expect("Failed to parse config file"); - - let base_url = format!( - "http://{}:{}", - options.server.url.as_str(), - options.server.port - ); - - let cairo_pie = fs::read_to_string("./src/assets/test_data/encoded_cairo_pie.txt").unwrap(); - - test_incorrect_layout(client.clone(), base_url.clone(), cairo_pie.clone()).await; - println!("✅ test_incorrect_layout completed"); - - test_additional_bad_flag(client.clone(), base_url.clone(), cairo_pie.clone()).await; - println!("✅ test_additional_bad_flag completed"); - - test_no_cairo_job_id(client.clone(), base_url.clone(), cairo_pie.clone()).await; - println!("✅ test_no_cairo_job_id completed"); - - test_incorrect_offchain_proof(client.clone(), base_url.clone(), cairo_pie.clone()).await; - println!("✅ test_incorrect_offchain_proof completed"); - - test_successfully(client.clone(), base_url.clone(), cairo_pie.clone()).await; - println!("✅ test_successfully completed"); -} - -async fn test_incorrect_layout(client: Client, base_url: String, cairo_pie: String) { - let url = - format!( - "{}/v1/gateway/add_job?customer_id={}&cairo_job_key={}&offchain_proof={}&proof_layout={}", - base_url, Uuid::new_v4(), Uuid::new_v4(), true, "smal" - ); - let correct_body = cairo_pie.to_string(); - let expected = json!( - { - "code": "500", - "message": "Internal server error" - } - ); - let res = post_request(client, url, correct_body).await; - assert_eq!(res, expected, "Response did not match expected value"); -} - -async fn test_additional_bad_flag(client: Client, base_url: String, cairo_pie: String) { - let url = format!( - "{}/v1/gateway/add_job?customer_id={}&cairo_job_key={}&offchain_proof={}&proof_layout={}&bla={}", - base_url, Uuid::new_v4(), Uuid::new_v4(), true, "small", true - ); - let correct_body = cairo_pie.to_string(); - let expected = json!( - {"code" : "JOB_RECEIVED_SUCCESSFULLY"} - ); - let res = post_request(client, url, correct_body).await; - assert_eq!(res, expected, "Response did not match expected value"); -} - -async fn test_no_cairo_job_id(client: Client, base_url: String, cairo_pie: String) { - let url = format!( - "{}/v1/gateway/add_job?customer_id={}&offchain_proof={}&proof_layout={}", - base_url, - Uuid::new_v4(), - true, - "small" - ); - let correct_body = cairo_pie.to_string(); - let expected = json!( - { - "code": "500", - "message": "Internal server error" - } - ); - let res = post_request(client, url, correct_body).await; - assert_eq!(res, expected, "Response did not match expected value"); -} - -async fn test_incorrect_offchain_proof(client: Client, base_url: String, cairo_pie: String) { - let url = - format!( - "{}/v1/gateway/add_job?customer_id={}&cairo_job_key={}&offchain_proof={}&proof_layout={}", - base_url, Uuid::new_v4(), Uuid::new_v4(), false, "small" - ); - let correct_body = cairo_pie.to_string(); - let expected = json!( - { - "code": "500", - "message": "Internal server error" - } - ); - let res = post_request(client, url, correct_body).await; - assert_eq!(res, expected, "Response did not match expected value"); -} - -async fn test_successfully(client: Client, base_url: String, cairo_pie: String) { - let url = - format!( - "{}/v1/gateway/add_job?customer_id={}&cairo_job_key={}&offchain_proof={}&proof_layout={}", - base_url, Uuid::new_v4(), Uuid::new_v4(), true, "starknet" - ); - - let correct_body = cairo_pie.to_string(); - - let expected = json!( - {"code" : "JOB_RECEIVED_SUCCESSFULLY"} - ); - let res = post_request(client, url, correct_body).await; - assert_eq!(res, expected, "Response did not match expected value"); -} - -async fn post_request(client: Client, url: String, body: String) -> Value { - client - .post(&url) - .body(body) - .send() - .await - .expect("Failed to send POST request") - .json::() - .await - .expect("Failed to parse response body as JSON") -} diff --git a/crates/public/src/tests/test_get_status.rs b/crates/public/src/tests/test_get_status.rs deleted file mode 100644 index a0e232f..0000000 --- a/crates/public/src/tests/test_get_status.rs +++ /dev/null @@ -1,241 +0,0 @@ -use std::fs; - -use reqwest::Client; -use serde_json::{json, Value}; -use tokio; -use tokio_postgres::NoTls; - -use crate::options::Options; - -#[tokio::test] -async fn test_get_status() { - let client = Client::new(); - - let config_content = - fs::read_to_string("./config/00-default.toml").expect("Failed to read config file"); - - let options: Options = toml::from_str(&config_content).expect("Failed to parse config file"); - - let base_url = format!( - "http://{}:{}", - options.server.url.as_str(), - options.server.port - ); - - // Set up the database - setup_database(&*options.pg.url).await; - println!("✅ Database setup completed"); - - test_failed(client.clone(), base_url.clone()).await; - println!("✅ test_failed completed"); - - test_invalid(client.clone(), base_url.clone()).await; - println!("✅ test_invalid completed"); - - test_unknown(client.clone(), base_url.clone()).await; - println!("✅ test_unknown completed"); - - test_in_progress(client.clone(), base_url.clone()).await; - println!("✅ test_in_progress completed"); - - test_additional_bad_flag(client.clone(), base_url.clone()).await; - println!("✅ test_additional_bad_flag completed"); - - test_not_created(client.clone(), base_url.clone()).await; - println!("✅ test_not_created completed"); - - test_processed(client.clone(), base_url.clone()).await; - println!("✅ test_processed completed"); - - test_onchain(client.clone(), base_url.clone()).await; - println!("✅ test_onchain completed"); -} - -async fn test_failed(client: Client, base_url: String) { - let customer_id = "93bc3373-5115-4f99-aecc-1bc57997bfd3".to_string(); - let cairo_job_key = "11395dd2-b874-4c11-8744-ba6482da997d".to_string(); - - let expected = json!( - { - "status" : "FAILED", - "invalid_reason" : "", - "error_log": "Sharp task failed", - "validation_done": false - } - ); - let res = get_response(client, base_url, customer_id, cairo_job_key).await; - assert_eq!(res, expected, "Response did not match expected value"); -} - -async fn test_invalid(client: Client, base_url: String) { - let customer_id = "18dc4b30-8b46-42d1-8b51-aba8c8abc7b0".to_string(); - let cairo_job_key = "09a10775-7294-4e5d-abbc-7659caa1a330".to_string(); - - let expected = json!( - { - "status" : "INVALID", - "invalid_reason": "INVALID_CAIRO_PIE_FILE_FORMAT", - "error_log": "The Cairo PIE file has a wrong format. \ - Deserialization ended with \ - exception: Invalid prefix for zip file..", - "validation_done": false - } - ); - let res = get_response(client, base_url, customer_id, cairo_job_key).await; - assert_eq!(res, expected, "Response did not match expected value"); -} - -async fn test_unknown(client: Client, base_url: String) { - let customer_id = "2dd71442-58ca-4c35-a6de-8e637ff3c24b".to_string(); - let cairo_job_key = "f946ec7d-c3bf-42df-8bf0-9bcc751a8b3e".to_string(); - - let expected = json!( - { - "status" : "UNKNOWN", - "invalid_reason" : "", - "error_log": "", - "validation_done": false - } - ); - let res = get_response(client, base_url, customer_id, cairo_job_key).await; - assert_eq!(res, expected, "Response did not match expected value"); -} - -async fn test_in_progress(client: Client, base_url: String) { - let customer_id = "e703be2c-9ffe-4992-b968-da75da75d0b8".to_string(); - let cairo_job_key = "37e9d193-8e94-4df3-893a-cafa62a418c0".to_string(); - - let expected = json!( - { - "status" : "IN_PROGRESS", - "invalid_reason" : "", - "error_log": "", - "validation_done": false - } - ); - let res = get_response(client, base_url, customer_id, cairo_job_key).await; - assert_eq!(res, expected, "Response did not match expected value"); -} - -async fn test_additional_bad_flag(client: Client, base_url: String) { - let customer_id = "0581368e-2a32-4e93-b211-3f0ac9bae790".to_string(); - let cairo_job_key = "b01d3ad5-10db-4fcd-8746-fdc886de50bc".to_string(); - - let expected = json!( - { - "status" : "IN_PROGRESS", - "invalid_reason" : "", - "error_log": "", - "validation_done": true - } - ); - let res = get_response(client, base_url, customer_id, cairo_job_key).await; - assert_eq!(res, expected, "Response did not match expected value"); -} - -async fn test_not_created(client: Client, base_url: String) { - let customer_id = "040832f8-245f-4f05-a165-e2810e30047f".to_string(); - let cairo_job_key = "803eac13-3dbb-4ad2-a1df-311cfc2829cf".to_string(); - - let expected = json!( - { - "status" : "NOT_CREATED", - "invalid_reason" : "", - "error_log": "", - "validation_done": false - } - ); - let res = get_response(client, base_url, customer_id, cairo_job_key).await; - assert_eq!(res, expected, "Response did not match expected value"); -} - -async fn test_processed(client: Client, base_url: String) { - let customer_id = "8758d917-bbdc-4573-97ae-817e94fa31fb".to_string(); - let cairo_job_key = "59732e57-5722-4eb7-98db-8b90b89276f8".to_string(); - - let expected = json!( - { - "status" : "PROCESSED", - "invalid_reason" : "", - "error_log": "", - "validation_done": false - } - ); - let res = get_response(client, base_url, customer_id, cairo_job_key).await; - assert_eq!(res, expected, "Response did not match expected value"); -} - -async fn test_onchain(client: Client, base_url: String) { - let customer_id = "e3133ecb-e6e9-493a-ad64-ab9a4495af57".to_string(); - let cairo_job_key = "39af2c49-0c81-450e-91a9-aeff8dba2318".to_string(); - - let expected = json!( - { - "status" : "ONCHAIN", - "invalid_reason" : "", - "error_log": "", - "validation_done": true - } - ); - let res = get_response(client, base_url, customer_id, cairo_job_key).await; - assert_eq!(res, expected, "Response did not match expected value"); -} - -async fn get_response( - client: Client, - base_url: String, - customer_id: String, - cairo_job_key: String, -) -> Value { - let url = format!( - "{}/v1/gateway/get_status?customer_id={}&cairo_job_key={}", - base_url, customer_id, cairo_job_key - ); - client - .get(&url) - .send() - .await - .expect("Failed to send GET request") - .json::() - .await - .expect("Failed to parse response body as JSON") -} - -async fn setup_database(url: &str) { - let (client, connection) = tokio_postgres::connect(url, NoTls) - .await - .expect("Failed to connect to database"); - - // Spawn the connection in the background - tokio::spawn(async move { - if let Err(e) = connection.await { - eprintln!("Connection error: {}", e); - } - }); - - // SQL to drop and recreate the table - let reset_queries = r#" - INSERT INTO jobs (id, customer_id, cairo_job_key, status, invalid_reason, error_log, validation_done) - VALUES - ('2a3ee88d-e19d-43ed-a79e-da9a28dc9525', '93bc3373-5115-4f99-aecc-1bc57997bfd3', '11395dd2-b874-4c11-8744-ba6482da997d','FAILED', '', 'Sharp task failed', false), - - ('58f667ea-67b3-4b32-b4f8-ef24ea1c8f12', '18dc4b30-8b46-42d1-8b51-aba8c8abc7b0', '09a10775-7294-4e5d-abbc-7659caa1a330', 'INVALID', 'INVALID_CAIRO_PIE_FILE_FORMAT', 'The Cairo PIE file has a wrong format. Deserialization ended with exception: Invalid prefix for zip file..', false), - - ('f2c604b7-52c5-4b69-9a67-de1276f9b8f8', '2dd71442-58ca-4c35-a6de-8e637ff3c24b', 'f946ec7d-c3bf-42df-8bf0-9bcc751a8b3e', 'UNKNOWN', '', '', false), - - ('d7045419-2b0f-4210-9e3d-7fb002839202', 'e703be2c-9ffe-4992-b968-da75da75d0b8', '37e9d193-8e94-4df3-893a-cafa62a418c0', 'IN_PROGRESS', '', '', false), - - ('18ef16cd-4511-4f29-a1d8-cd117d801f77', '0581368e-2a32-4e93-b211-3f0ac9bae790', 'b01d3ad5-10db-4fcd-8746-fdc886de50bc', 'IN_PROGRESS', '', '', true), - - ('549139a0-b288-401c-afb4-0f1018fd99f8', '040832f8-245f-4f05-a165-e2810e30047f', '803eac13-3dbb-4ad2-a1df-311cfc2829cf', 'NOT_CREATED', '', '', false), - - ('2283042d-f102-4ee6-a92f-73f3a86850e8', '8758d917-bbdc-4573-97ae-817e94fa31fb', '59732e57-5722-4eb7-98db-8b90b89276f8', 'PROCESSED', '', '', false), - - ('69f7ae7a-e981-44d2-9eb2-dfa551474870', 'e3133ecb-e6e9-493a-ad64-ab9a4495af57', '39af2c49-0c81-450e-91a9-aeff8dba2318', 'ONCHAIN', '', '', true); - "#; - - client - .batch_execute(reset_queries) - .await - .expect("Failed to reset database"); -} diff --git a/e2e-tests/Cargo.toml b/e2e-tests/Cargo.toml new file mode 100644 index 0000000..8cf3b08 --- /dev/null +++ b/e2e-tests/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "e2e-tests" +version = "0.1.0" +edition = "2021" + +[dependencies] +log = { workspace = true } +rand = { workspace = true } +reqwest = { workspace = true } +serde_json = { workspace = true } +testcontainers-modules = { workspace = true } +tokio = { workspace = true } +tokio-postgres = { workspace = true } +uuid = { workspace = true } + +[dev-dependencies] +test-log = { workspace = true } + +[[test]] +name = "flow_test" +path = "tests.rs" diff --git a/crates/public/src/assets/test_data/encoded_cairo_pie.txt b/e2e-tests/assets/test_data/encoded_cairo_pie.txt similarity index 100% rename from crates/public/src/assets/test_data/encoded_cairo_pie.txt rename to e2e-tests/assets/test_data/encoded_cairo_pie.txt diff --git a/crates/public/src/assets/test_data/fibonacci_with_output.zip b/e2e-tests/assets/test_data/fibonacci_with_output.zip similarity index 100% rename from crates/public/src/assets/test_data/fibonacci_with_output.zip rename to e2e-tests/assets/test_data/fibonacci_with_output.zip diff --git a/e2e-tests/src/lib.rs b/e2e-tests/src/lib.rs new file mode 100644 index 0000000..ed9ecdc --- /dev/null +++ b/e2e-tests/src/lib.rs @@ -0,0 +1,3 @@ +pub mod postgres; +pub mod program; +pub mod utils; diff --git a/e2e-tests/src/postgres.rs b/e2e-tests/src/postgres.rs new file mode 100644 index 0000000..67f85ed --- /dev/null +++ b/e2e-tests/src/postgres.rs @@ -0,0 +1,53 @@ +use std::borrow::Cow; +use std::collections::HashMap; + +use testcontainers_modules::testcontainers::core::WaitFor; +use testcontainers_modules::testcontainers::{CopyToContainer, Image}; + +const NAME: &str = "postgres"; +const TAG: &str = "latest"; + +pub struct Postgres { + env_vars: HashMap, + copy_to_sources: Vec, +} + +impl Default for Postgres { + fn default() -> Self { + let mut env_vars = HashMap::new(); + env_vars.insert("POSTGRES_DB".to_owned(), "postgres".to_owned()); + env_vars.insert("POSTGRES_USER".to_owned(), "postgres".to_owned()); + env_vars.insert("POSTGRES_PASSWORD".to_owned(), "postgres".to_owned()); + Self { + env_vars, + copy_to_sources: Vec::new(), + } + } +} + +impl Image for Postgres { + fn name(&self) -> &str { + NAME + } + + fn tag(&self) -> &str { + TAG + } + + fn ready_conditions(&self) -> Vec { + vec![ + WaitFor::message_on_stderr("database system is ready to accept connections"), + WaitFor::message_on_stdout("database system is ready to accept connections"), + ] + } + + fn env_vars( + &self, + ) -> impl IntoIterator>, impl Into>)> { + &self.env_vars + } + + fn copy_to_sources(&self) -> impl IntoIterator { + &self.copy_to_sources + } +} diff --git a/e2e-tests/src/program.rs b/e2e-tests/src/program.rs new file mode 100644 index 0000000..2bbe76d --- /dev/null +++ b/e2e-tests/src/program.rs @@ -0,0 +1,125 @@ +use std::io::{BufRead, BufReader}; +use std::process::{Child, Command, ExitStatus, Stdio}; +use std::thread; +use std::time::Duration; + +use log::info; +use tokio::net::TcpStream; + +use crate::utils::{get_free_port, get_repository_root}; + +const CONNECTION_ATTEMPTS: usize = 2000; +const CONNECTION_ATTEMPT_DELAY_MS: u64 = 1000; + +#[derive(Debug)] +pub struct Program { + pub url: String, + pub port: String, + + process: Child, +} + +impl Drop for Program { + fn drop(&mut self) { + let mut kill = Command::new("kill") + .args(["-s", "TERM", &self.process.id().to_string()]) + .spawn() + .expect("Failed to kill"); + kill.wait().expect("Failed to kill the process"); + } +} + +impl Program { + pub fn run(log_name: String, bin_name: &str, envs: Vec<(String, String)>) -> Self { + let port = get_free_port(); + Self::run_with_port(log_name, bin_name, envs, port) + } + + pub fn run_with_port( + log_name: String, + bin_name: &str, + envs: Vec<(String, String)>, + port: u16, + ) -> Self { + let repository_root = &get_repository_root(); + std::env::set_current_dir(repository_root).expect("Failed to set current directory"); + let url = "0.0.0.0".to_string(); + let port_str = format!("{}", port); + + let envs = [ + envs, + vec![ + ("SERVER__URL".to_string(), url.clone()), + ("SERVER__PORT".to_string(), port_str.clone()), + ], + ] + .concat(); + + let mut command = Command::new("cargo"); + command + .args(["run"]) + .args(["--bin"]) + .args([bin_name]) + .current_dir(repository_root) + .envs(envs) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + + let mut process = command.spawn().expect("Failed to start process"); + + // Capture and print stdout + let stdout = process.stdout.take().expect("Failed to capture stdout"); + + let log_name_1 = log_name.clone(); + thread::spawn(move || { + let reader = BufReader::new(stdout); + reader.lines().for_each(|line| { + if let Ok(line) = line { + info!("{} STDOUT: {}", &log_name_1, line); + } + }); + }); + + // Capture and print stderr + let stderr = process.stderr.take().expect("Failed to capture stderr"); + thread::spawn(move || { + let reader = BufReader::new(stderr); + reader.lines().for_each(|line| { + if let Ok(line) = line { + info!("{} STDERR: {}", &log_name, line); + } + }); + }); + + Self { + url, + port: port_str, + process, + } + } + + pub fn has_exited(&mut self) -> Option { + self.process.try_wait().expect("Failed to get exit status") + } + + pub async fn wait_till_started(&mut self) { + let mut attempts = CONNECTION_ATTEMPTS; + loop { + let addr = format!("{}:{}", &self.url, &self.port); + match TcpStream::connect(&addr).await { + Ok(_) => return, + Err(err) => { + if let Some(status) = self.has_exited() { + panic!("Program exited early with {}", status); + } + if attempts == 0 { + panic!("Failed to connect to {}:{}: {}", &self.url, &self.port, err); + } + } + }; + + attempts -= 1; + tokio::time::sleep(Duration::from_millis(CONNECTION_ATTEMPT_DELAY_MS)).await; + } + } +} diff --git a/e2e-tests/src/utils.rs b/e2e-tests/src/utils.rs new file mode 100644 index 0000000..9da3026 --- /dev/null +++ b/e2e-tests/src/utils.rs @@ -0,0 +1,27 @@ +use std::net::TcpListener; +use std::path::{Path, PathBuf}; + +use rand::Rng; + +const MIN_PORT: u16 = 49_152; +const MAX_PORT: u16 = 65_535; +const MAX_TRIES: usize = 1000; +pub fn get_free_port() -> u16 { + let mut rng = rand::thread_rng(); + for _ in 0..MAX_TRIES { + let port = rng.gen_range(MIN_PORT..=MAX_PORT); + if let Ok(listener) = TcpListener::bind(("127.0.0.1", port)) { + return listener.local_addr().expect("No local addr").port(); + } + // otherwise port is occupied + } + panic!("No free ports available"); +} + +pub fn get_repository_root() -> PathBuf { + let manifest_path = Path::new(&env!("CARGO_MANIFEST_DIR")); + let repository_root = manifest_path + .parent() + .expect("Failed to get parent directory of CARGO_MANIFEST_DIR"); + repository_root.to_path_buf() +} diff --git a/e2e-tests/tests.rs b/e2e-tests/tests.rs new file mode 100644 index 0000000..bf4e681 --- /dev/null +++ b/e2e-tests/tests.rs @@ -0,0 +1,449 @@ +use e2e_tests::postgres::Postgres; +use e2e_tests::program::Program; +use log::info; +use testcontainers_modules::testcontainers::runners::AsyncRunner; + +#[allow(dead_code)] +/// Initial setup for e2e tests +struct Setup { + pub postgres_instance: ContainerAsync, + pub envs: Vec<(String, String)>, +} + +use testcontainers_modules::testcontainers::ContainerAsync; + +impl Setup { + /// Initialise a new setup + pub async fn new() -> Self { + // Set up a postgres database port for testing + let postgres_instance = Postgres::default().start().await.unwrap(); + + let dataserver_endpoint = format!( + "postgres://postgres:postgres@{}:{}/postgres", + postgres_instance.get_host().await.unwrap(), + postgres_instance.get_host_port_ipv4(5432).await.unwrap() + ); + info!( + "✅ PostgresDB setup completed with URL: {}", + &dataserver_endpoint + ); + + Self { + postgres_instance, + envs: vec![ + ("WORKER__SCHEMA".to_string(), "worker_schema".to_string()), + ("PG__URL".to_string(), dataserver_endpoint), + ("PG__MAX_SIZE".to_string(), "10".to_string()), + ], + } + } +} + +#[cfg(test)] +mod tests { + use std::fs; + + use reqwest::Client; + use serde_json::{json, Value}; + use test_log::test; + use tokio_postgres::NoTls; + use uuid::Uuid; + + use super::*; + + #[test(tokio::test)] + async fn test_full_flow() { + let setup_config = Setup::new().await; + + // setup irelia server + let mut server_envs = setup_config.envs.clone(); + server_envs.append(&mut vec![ + ("SERVICE_NAME".to_string(), "irelia-server".to_string()), + ( + "EXPORTER_ENDPOINT".to_string(), + "127.0.0.1:7281".to_string(), + ), + ]); + let (_, pg_url) = server_envs + .iter() + .find(|(key, _)| key == "PG__URL") + .unwrap(); + let mut server = Program::run("SERVER".to_string(), "irelia", server_envs.clone()); + server.wait_till_started().await; + let server_endpoint = format!("http://{}:{}", server.url, server.port); + + // setup irelia worker + let mut worker_envs = setup_config.envs; + worker_envs.append(&mut vec![ + ("SERVICE_NAME".to_string(), "irelia-worker".to_string()), + ("WORKER__CONCURRENT".to_string(), "4".to_string()), + ( + "EXPORTER_ENDPOINT".to_string(), + "127.0.0.1:7281".to_string(), + ), + ]); + let mut worker = Program::run("WORKER".to_string(), "irelia_worker", worker_envs); + worker.wait_till_started().await; + + let client = Client::new(); + // test add job + let cairo_pie = + fs::read_to_string("./e2e-tests/assets/test_data/encoded_cairo_pie.txt").unwrap(); + test_add_job_incorrect_layout(client.clone(), server_endpoint.clone(), cairo_pie.clone()) + .await; + println!("✅ test_add_job_incorrect_layout completed"); + + test_add_job_additional_bad_flag( + client.clone(), + server_endpoint.clone(), + cairo_pie.clone(), + ) + .await; + println!("✅ test_add_job_additional_bad_flag completed"); + + test_add_job_no_cairo_job_id(client.clone(), server_endpoint.clone(), cairo_pie.clone()) + .await; + println!("✅ test_add_job_no_cairo_job_id completed"); + + test_add_job_incorrect_offchain_proof( + client.clone(), + server_endpoint.clone(), + cairo_pie.clone(), + ) + .await; + println!("✅ test_add_job_incorrect_offchain_proof completed"); + + test_add_job_successfully(client.clone(), server_endpoint.clone(), cairo_pie).await; + println!("✅ test_add_job_successfully completed"); + + // test get status + // Set up the database + setup_database(pg_url).await; + println!("✅ Database setup completed"); + + test_get_status_failed(client.clone(), server_endpoint.clone()).await; + println!("✅ test_get_status_failed completed"); + + test_get_status_invalid(client.clone(), server_endpoint.clone()).await; + println!("✅ test_get_status_invalid completed"); + + test_get_status_unknown(client.clone(), server_endpoint.clone()).await; + println!("✅ test_get_status_unknown completed"); + + test_get_status_in_progress(client.clone(), server_endpoint.clone()).await; + println!("✅ test_get_status_in_progress completed"); + + test_get_status_additional_bad_flag(client.clone(), server_endpoint.clone()).await; + println!("✅ test_get_status_additional_bad_flag completed"); + + test_get_status_not_created(client.clone(), server_endpoint.clone()).await; + println!("✅ test_get_status_not_created completed"); + + test_get_status_processed(client.clone(), server_endpoint.clone()).await; + println!("✅ test_get_status_processed completed"); + + test_get_status_onchain(client, server_endpoint).await; + println!("✅ test_get_status_onchain completed"); + } + + //test add job function + async fn test_add_job_incorrect_layout( + client: Client, + server_endpoint: String, + cairo_pie: String, + ) { + let url = + format!( + "{}/v1/gateway/add_job?customer_id={}&cairo_job_key={}&offchain_proof={}&proof_layout={}", + server_endpoint, Uuid::new_v4(), Uuid::new_v4(), true, "stark" + ); + let correct_body = cairo_pie.to_string(); + let expected = json!( + { + "code": "500", + "message": "Internal server error" + } + ); + let res = post_request(client, url, correct_body).await; + assert_eq!(res, expected, "Response did not match expected value"); + } + + async fn test_add_job_additional_bad_flag( + client: Client, + server_endpoint: String, + cairo_pie: String, + ) { + let url = format!( + "{}/v1/gateway/add_job?customer_id={}&cairo_job_key={}&offchain_proof={}&proof_layout={}&bla={}", + server_endpoint, Uuid::new_v4(), Uuid::new_v4(), true, "starknet", true + ); + let correct_body = cairo_pie.to_string(); + let expected = json!( + {"code" : "JOB_RECEIVED_SUCCESSFULLY"} + ); + let res = post_request(client, url, correct_body).await; + assert_eq!(res, expected, "Response did not match expected value"); + } + + async fn test_add_job_no_cairo_job_id( + client: Client, + server_endpoint: String, + cairo_pie: String, + ) { + let url = format!( + "{}/v1/gateway/add_job?customer_id={}&offchain_proof={}&proof_layout={}", + server_endpoint, + Uuid::new_v4(), + true, + "starknet" + ); + let correct_body = cairo_pie.to_string(); + let expected = json!( + { + "code": "500", + "message": "Internal server error" + } + ); + let res = post_request(client, url, correct_body).await; + assert_eq!(res, expected, "Response did not match expected value"); + } + + async fn test_add_job_incorrect_offchain_proof( + client: Client, + server_endpoint: String, + cairo_pie: String, + ) { + let url = + format!( + "{}/v1/gateway/add_job?customer_id={}&cairo_job_key={}&offchain_proof={}&proof_layout={}", + server_endpoint, Uuid::new_v4(), Uuid::new_v4(), false, "starknet" + ); + let correct_body = cairo_pie.to_string(); + let expected = json!( + { + "code": "500", + "message": "Internal server error" + } + ); + let res = post_request(client, url, correct_body).await; + assert_eq!(res, expected, "Response did not match expected value"); + } + + async fn test_add_job_successfully(client: Client, server_endpoint: String, cairo_pie: String) { + let url = + format!( + "{}/v1/gateway/add_job?customer_id={}&cairo_job_key={}&offchain_proof={}&proof_layout={}", + server_endpoint, Uuid::new_v4(), Uuid::new_v4(), true, "starknet" + ); + + let correct_body = cairo_pie.to_string(); + + let expected = json!( + {"code" : "JOB_RECEIVED_SUCCESSFULLY"} + ); + let res = post_request(client, url, correct_body).await; + assert_eq!(res, expected, "Response did not match expected value"); + } + + async fn post_request(client: Client, url: String, body: String) -> Value { + client + .post(&url) + .body(body) + .send() + .await + .expect("Failed to send POST request") + .json::() + .await + .expect("Failed to parse response body as JSON") + } + + // test get status function + async fn test_get_status_failed(client: Client, server_endpoint: String) { + let customer_id = "93bc3373-5115-4f99-aecc-1bc57997bfd3".to_string(); + let cairo_job_key = "11395dd2-b874-4c11-8744-ba6482da997d".to_string(); + + let expected = json!( + { + "status" : "FAILED", + "invalid_reason" : "", + "error_log": "Sharp task failed", + "validation_done": false + } + ); + let res = get_response(client, server_endpoint, customer_id, cairo_job_key).await; + assert_eq!(res, expected, "Response did not match expected value"); + } + + async fn test_get_status_invalid(client: Client, server_endpoint: String) { + let customer_id = "18dc4b30-8b46-42d1-8b51-aba8c8abc7b0".to_string(); + let cairo_job_key = "09a10775-7294-4e5d-abbc-7659caa1a330".to_string(); + + let expected = json!( + { + "status" : "INVALID", + "invalid_reason": "INVALID_CAIRO_PIE_FILE_FORMAT", + "error_log": "The Cairo PIE file has a wrong format. \ + Deserialization ended with \ + exception: Invalid prefix for zip file..", + "validation_done": false + } + ); + let res = get_response(client, server_endpoint, customer_id, cairo_job_key).await; + assert_eq!(res, expected, "Response did not match expected value"); + } + + async fn test_get_status_unknown(client: Client, server_endpoint: String) { + let customer_id = "2dd71442-58ca-4c35-a6de-8e637ff3c24b".to_string(); + let cairo_job_key = "f946ec7d-c3bf-42df-8bf0-9bcc751a8b3e".to_string(); + + let expected = json!( + { + "status" : "UNKNOWN", + "invalid_reason" : "", + "error_log": "", + "validation_done": false + } + ); + let res = get_response(client, server_endpoint, customer_id, cairo_job_key).await; + assert_eq!(res, expected, "Response did not match expected value"); + } + + async fn test_get_status_in_progress(client: Client, server_endpoint: String) { + let customer_id = "e703be2c-9ffe-4992-b968-da75da75d0b8".to_string(); + let cairo_job_key = "37e9d193-8e94-4df3-893a-cafa62a418c0".to_string(); + + let expected = json!( + { + "status" : "IN_PROGRESS", + "invalid_reason" : "", + "error_log": "", + "validation_done": false + } + ); + let res = get_response(client, server_endpoint, customer_id, cairo_job_key).await; + assert_eq!(res, expected, "Response did not match expected value"); + } + + async fn test_get_status_additional_bad_flag(client: Client, server_endpoint: String) { + let customer_id = "0581368e-2a32-4e93-b211-3f0ac9bae790".to_string(); + let cairo_job_key = "b01d3ad5-10db-4fcd-8746-fdc886de50bc".to_string(); + + let expected = json!( + { + "status" : "IN_PROGRESS", + "invalid_reason" : "", + "error_log": "", + "validation_done": true + } + ); + let res = get_response(client, server_endpoint, customer_id, cairo_job_key).await; + assert_eq!(res, expected, "Response did not match expected value"); + } + + async fn test_get_status_not_created(client: Client, server_endpoint: String) { + let customer_id = "040832f8-245f-4f05-a165-e2810e30047f".to_string(); + let cairo_job_key = "803eac13-3dbb-4ad2-a1df-311cfc2829cf".to_string(); + + let expected = json!( + { + "status" : "NOT_CREATED", + "invalid_reason" : "", + "error_log": "", + "validation_done": false + } + ); + let res = get_response(client, server_endpoint, customer_id, cairo_job_key).await; + assert_eq!(res, expected, "Response did not match expected value"); + } + + async fn test_get_status_processed(client: Client, server_endpoint: String) { + let customer_id = "8758d917-bbdc-4573-97ae-817e94fa31fb".to_string(); + let cairo_job_key = "59732e57-5722-4eb7-98db-8b90b89276f8".to_string(); + + let expected = json!( + { + "status" : "PROCESSED", + "invalid_reason" : "", + "error_log": "", + "validation_done": false + } + ); + let res = get_response(client, server_endpoint, customer_id, cairo_job_key).await; + assert_eq!(res, expected, "Response did not match expected value"); + } + + async fn test_get_status_onchain(client: Client, server_endpoint: String) { + let customer_id = "e3133ecb-e6e9-493a-ad64-ab9a4495af57".to_string(); + let cairo_job_key = "39af2c49-0c81-450e-91a9-aeff8dba2318".to_string(); + + let expected = json!( + { + "status" : "ONCHAIN", + "invalid_reason" : "", + "error_log": "", + "validation_done": true + } + ); + let res = get_response(client, server_endpoint, customer_id, cairo_job_key).await; + assert_eq!(res, expected, "Response did not match expected value"); + } + + async fn get_response( + client: Client, + server_endpoint: String, + customer_id: String, + cairo_job_key: String, + ) -> Value { + let url = format!( + "{}/v1/gateway/get_status?customer_id={}&cairo_job_key={}", + server_endpoint, customer_id, cairo_job_key + ); + client + .get(&url) + .send() + .await + .expect("Failed to send GET request") + .json::() + .await + .expect("Failed to parse response body as JSON") + } + + async fn setup_database(url: &str) { + let (client, connection) = tokio_postgres::connect(url, NoTls) + .await + .expect("Failed to connect to database"); + + // Spawn the connection in the background + tokio::spawn(async move { + if let Err(e) = connection.await { + eprintln!("Connection error: {}", e); + } + }); + + // SQL to drop and recreate the table + let reset_queries = r#" + INSERT INTO jobs (id, customer_id, cairo_job_key, status, invalid_reason, error_log, validation_done) + VALUES + ('2a3ee88d-e19d-43ed-a79e-da9a28dc9525', '93bc3373-5115-4f99-aecc-1bc57997bfd3', '11395dd2-b874-4c11-8744-ba6482da997d','FAILED', '', 'Sharp task failed', false), + + ('58f667ea-67b3-4b32-b4f8-ef24ea1c8f12', '18dc4b30-8b46-42d1-8b51-aba8c8abc7b0', '09a10775-7294-4e5d-abbc-7659caa1a330', 'INVALID', 'INVALID_CAIRO_PIE_FILE_FORMAT', 'The Cairo PIE file has a wrong format. Deserialization ended with exception: Invalid prefix for zip file..', false), + + ('f2c604b7-52c5-4b69-9a67-de1276f9b8f8', '2dd71442-58ca-4c35-a6de-8e637ff3c24b', 'f946ec7d-c3bf-42df-8bf0-9bcc751a8b3e', 'UNKNOWN', '', '', false), + + ('d7045419-2b0f-4210-9e3d-7fb002839202', 'e703be2c-9ffe-4992-b968-da75da75d0b8', '37e9d193-8e94-4df3-893a-cafa62a418c0', 'IN_PROGRESS', '', '', false), + + ('18ef16cd-4511-4f29-a1d8-cd117d801f77', '0581368e-2a32-4e93-b211-3f0ac9bae790', 'b01d3ad5-10db-4fcd-8746-fdc886de50bc', 'IN_PROGRESS', '', '', true), + + ('549139a0-b288-401c-afb4-0f1018fd99f8', '040832f8-245f-4f05-a165-e2810e30047f', '803eac13-3dbb-4ad2-a1df-311cfc2829cf', 'NOT_CREATED', '', '', false), + + ('2283042d-f102-4ee6-a92f-73f3a86850e8', '8758d917-bbdc-4573-97ae-817e94fa31fb', '59732e57-5722-4eb7-98db-8b90b89276f8', 'PROCESSED', '', '', false), + + ('69f7ae7a-e981-44d2-9eb2-dfa551474870', 'e3133ecb-e6e9-493a-ad64-ab9a4495af57', '39af2c49-0c81-450e-91a9-aeff8dba2318', 'ONCHAIN', '', '', true); + "#; + + client + .batch_execute(reset_queries) + .await + .expect("Failed to reset database"); + } +}