1#![allow(
22 clippy::missing_errors_doc,
23 clippy::must_use_candidate,
24 clippy::missing_panics_doc
25)]
26use std::{ffi::OsString, fmt::Display, path::Path};
27
28use assert_cmd::{assert::Assert, Command};
29use assert_fs::{fixture::FixtureError, prelude::PathChild, TempDir};
30use fs_extra::dir::CopyOptions;
31
32use soroban_cli::{
33 commands::{contract::invoke, global, keys, NetworkRunnable},
34 config::{self, network},
35 CommandParser,
36};
37
38mod wasm;
39
40pub use wasm::Wasm;
41
42pub const TEST_ACCOUNT: &str = "test";
43
44pub const LOCAL_NETWORK_PASSPHRASE: &str = "Standalone Network ; February 2017";
45
46#[derive(thiserror::Error, Debug)]
47pub enum Error {
48 #[error(transparent)]
49 TempDir(#[from] FixtureError),
50
51 #[error(transparent)]
52 FsError(#[from] fs_extra::error::Error),
53
54 #[error(transparent)]
55 Invoke(#[from] invoke::Error),
56}
57
58pub struct TestEnv {
61 pub temp_dir: TempDir,
62 pub network: network::Network,
63}
64
65impl Default for TestEnv {
66 fn default() -> Self {
67 let temp_dir = TempDir::new().unwrap();
68 Self {
69 temp_dir,
70 network: network::Network {
71 rpc_url: "http://localhost:8889/soroban/rpc".to_string(),
72 network_passphrase: LOCAL_NETWORK_PASSPHRASE.to_string(),
73 rpc_headers: [].to_vec(),
74 },
75 }
76 }
77}
78
79impl TestEnv {
80 pub fn with_default<F: FnOnce(&TestEnv)>(f: F) {
94 let test_env = TestEnv::default();
95 f(&test_env);
96 }
97
98 pub fn with_default_network<F: FnOnce(&TestEnv)>(f: F) {
99 let test_env = TestEnv::new();
100 f(&test_env);
101 }
102
103 pub fn with_port(host_port: u16) -> TestEnv {
104 Self::with_rpc_url(&format!("http://localhost:{host_port}/soroban/rpc"))
105 }
106
107 pub fn with_rpc_url(rpc_url: &str) -> TestEnv {
108 let mut env = TestEnv::default();
109 env.network.rpc_url = rpc_url.to_string();
110 if let Ok(network_passphrase) = std::env::var("STELLAR_NETWORK_PASSPHRASE") {
111 env.network.network_passphrase = network_passphrase;
112 };
113 env.generate_account("test", None).assert().success();
114 env
115 }
116
117 pub fn with_rpc_provider(rpc_url: &str, rpc_headers: Vec<(String, String)>) -> TestEnv {
118 let mut env = TestEnv::default();
119 env.network.rpc_url = rpc_url.to_string();
120 env.network.rpc_headers = rpc_headers;
121 if let Ok(network_passphrase) = std::env::var("STELLAR_NETWORK_PASSPHRASE") {
122 env.network.network_passphrase = network_passphrase;
123 };
124 env.generate_account("test", None).assert().success();
125 env
126 }
127
128 pub fn new() -> TestEnv {
129 if let Ok(rpc_url) = std::env::var("SOROBAN_RPC_URL") {
130 return Self::with_rpc_url(&rpc_url);
131 }
132 if let Ok(rpc_url) = std::env::var("STELLAR_RPC_URL") {
133 return Self::with_rpc_url(&rpc_url);
134 }
135 let host_port = std::env::var("SOROBAN_PORT")
136 .as_deref()
137 .ok()
138 .and_then(|n| n.parse().ok())
139 .unwrap_or(8000);
140 Self::with_port(host_port)
141 }
142 pub fn new_assert_cmd(&self, subcommand: &str) -> Command {
145 let mut cmd: Command = self.bin();
146
147 cmd.arg(subcommand)
148 .env("SOROBAN_ACCOUNT", TEST_ACCOUNT)
149 .env("SOROBAN_RPC_URL", &self.network.rpc_url)
150 .env("SOROBAN_NETWORK_PASSPHRASE", LOCAL_NETWORK_PASSPHRASE)
151 .env("XDG_CONFIG_HOME", self.temp_dir.join("config").as_os_str())
152 .env("XDG_DATA_HOME", self.temp_dir.join("data").as_os_str())
153 .current_dir(&self.temp_dir);
154
155 if !self.network.rpc_headers.is_empty() {
156 cmd.env(
157 "STELLAR_RPC_HEADERS",
158 format!(
159 "{}:{}",
160 &self.network.rpc_headers[0].0, &self.network.rpc_headers[0].1
161 ),
162 );
163 }
164
165 cmd
166 }
167
168 pub fn bin(&self) -> Command {
169 Command::cargo_bin("soroban").unwrap_or_else(|_| Command::new("soroban"))
170 }
171
172 pub fn generate_account(&self, account: &str, seed: Option<String>) -> Command {
173 let mut cmd = self.new_assert_cmd("keys");
174 cmd.arg("generate").arg(account);
175 if let Some(seed) = seed {
176 cmd.arg(format!("--seed={seed}"));
177 }
178 cmd
179 }
180
181 pub fn fund_account(&self, account: &str) -> Assert {
182 self.new_assert_cmd("keys")
183 .arg("fund")
184 .arg(account)
185 .assert()
186 }
187
188 pub fn cmd<T: CommandParser<T>>(&self, args: &str) -> T {
192 Self::cmd_with_pwd(args, self.dir())
193 }
194
195 pub fn cmd_with_pwd<T: CommandParser<T>>(args: &str, pwd: &Path) -> T {
197 let args = format!("--config-dir={pwd:?} {args}");
198 T::parse(&args).unwrap()
199 }
200
201 pub fn cmd_arr_with_pwd<T: CommandParser<T>>(args: &[&str], pwd: &Path) -> T {
203 let mut cmds = vec!["--config-dir", pwd.to_str().unwrap()];
204 cmds.extend_from_slice(args);
205 T::parse_arg_vec(&cmds).unwrap()
206 }
207
208 pub fn cmd_arr<T: CommandParser<T>>(&self, args: &[&str]) -> T {
211 Self::cmd_arr_with_pwd(args, self.dir())
212 }
213
214 pub async fn invoke_with_test<I: AsRef<str>>(
216 &self,
217 command_str: &[I],
218 ) -> Result<String, invoke::Error> {
219 self.invoke_with(command_str, "test").await
220 }
221
222 pub async fn invoke_with<I: AsRef<str>>(
224 &self,
225 command_str: &[I],
226 source: &str,
227 ) -> Result<String, invoke::Error> {
228 let cmd = self.cmd_with_config::<I, invoke::Cmd>(command_str, None);
229 self.run_cmd_with(cmd, source)
230 .await
231 .map(|tx| tx.into_result().unwrap())
232 }
233
234 pub fn cmd_with_config<I: AsRef<str>, T: CommandParser<T> + NetworkRunnable>(
236 &self,
237 command_str: &[I],
238 source_account: Option<&str>,
239 ) -> T {
240 let source = source_account.unwrap_or("test");
241 let source_str = format!("--source-account={source}");
242 let mut arg = vec![
243 "--network=local",
244 "--rpc-url=http",
245 "--network-passphrase=AA",
246 source_str.as_str(),
247 ];
248 let input = command_str
249 .iter()
250 .map(AsRef::as_ref)
251 .filter(|s| !s.is_empty())
252 .collect::<Vec<_>>();
253 arg.extend(input);
254 T::parse_arg_vec(&arg).unwrap()
255 }
256
257 pub fn clone_config(&self, account: &str) -> config::Args {
258 let config_dir = Some(self.dir().to_path_buf());
259 config::Args {
260 network: network::Args {
261 rpc_url: Some(self.network.rpc_url.clone()),
262 rpc_headers: [].to_vec(),
263 network_passphrase: Some(LOCAL_NETWORK_PASSPHRASE.to_string()),
264 network: None,
265 },
266 source_account: account.parse().unwrap(),
267 locator: config::locator::Args {
268 global: false,
269 config_dir,
270 },
271 hd_path: None,
272 }
273 }
274
275 pub async fn run_cmd_with<T: NetworkRunnable>(
277 &self,
278 cmd: T,
279 account: &str,
280 ) -> Result<T::Result, T::Error> {
281 let config = self.clone_config(account);
282 cmd.run_against_rpc_server(
283 Some(&global::Args {
284 locator: config.locator.clone(),
285 filter_logs: Vec::default(),
286 quiet: false,
287 verbose: false,
288 very_verbose: false,
289 list: false,
290 no_cache: false,
291 }),
292 Some(&config),
293 )
294 .await
295 }
296
297 pub fn dir(&self) -> &TempDir {
299 &self.temp_dir
300 }
301
302 pub async fn test_address(&self, hd_path: usize) -> String {
304 self.cmd::<keys::public_key::Cmd>(&format!("--hd-path={hd_path}"))
305 .public_key()
306 .await
307 .unwrap()
308 .to_string()
309 }
310
311 pub fn test_show(&self, hd_path: usize) -> String {
313 self.cmd::<keys::secret::Cmd>(&format!("--hd-path={hd_path}"))
314 .private_key()
315 .unwrap()
316 .to_string()
317 }
318
319 pub fn fork(&self) -> Result<TestEnv, Error> {
321 let this = TestEnv::new();
322 self.save(&this.temp_dir)?;
323 Ok(this)
324 }
325
326 pub fn save(&self, dst: &Path) -> Result<(), Error> {
328 fs_extra::dir::copy(&self.temp_dir, dst, &CopyOptions::new())?;
329 Ok(())
330 }
331
332 pub fn client(&self) -> soroban_rpc::Client {
333 self.network.rpc_client().unwrap()
334 }
335
336 #[cfg(feature = "emulator-tests")]
337 pub async fn speculos_container(
338 ledger_device_model: &str,
339 ) -> testcontainers::ContainerAsync<stellar_ledger::emulator_test_support::speculos::Speculos>
340 {
341 use stellar_ledger::emulator_test_support::{
342 enable_hash_signing, get_container, wait_for_emulator_start_text,
343 };
344 let container = get_container(ledger_device_model).await;
345 let ui_host_port: u16 = container.get_host_port_ipv4(5000).await.unwrap();
346 wait_for_emulator_start_text(ui_host_port).await;
347 enable_hash_signing(ui_host_port).await;
348 container
349 }
350}
351
352pub fn temp_ledger_file() -> OsString {
353 TempDir::new()
354 .unwrap()
355 .child("ledger.json")
356 .as_os_str()
357 .into()
358}
359
360pub trait AssertExt {
361 fn stdout_as_str(&self) -> String;
362 fn stderr_as_str(&self) -> String;
363}
364
365impl AssertExt for Assert {
366 fn stdout_as_str(&self) -> String {
367 String::from_utf8(self.get_output().stdout.clone())
368 .expect("failed to make str")
369 .trim()
370 .to_owned()
371 }
372 fn stderr_as_str(&self) -> String {
373 String::from_utf8(self.get_output().stderr.clone())
374 .expect("failed to make str")
375 .trim()
376 .to_owned()
377 }
378}
379pub trait CommandExt {
380 fn json_arg<A>(&mut self, j: A) -> &mut Self
381 where
382 A: Display;
383}
384
385impl CommandExt for Command {
386 fn json_arg<A>(&mut self, j: A) -> &mut Self
387 where
388 A: Display,
389 {
390 self.arg(OsString::from(j.to_string()))
391 }
392}