diff --git a/crates/smoketests/src/lib.rs b/crates/smoketests/src/lib.rs index aa54aea04ef..1ef17047d31 100644 --- a/crates/smoketests/src/lib.rs +++ b/crates/smoketests/src/lib.rs @@ -58,6 +58,7 @@ use regex::Regex; use spacetimedb_guard::{ensure_binaries_built, SpacetimeDbGuard}; use std::env; use std::fs; +use std::io::{BufRead, BufReader}; use std::path::{Path, PathBuf}; use std::process::{Command, Output, Stdio}; use std::sync::OnceLock; @@ -401,6 +402,29 @@ impl ApiResponse { } } +#[derive(Clone, Debug)] +pub struct PublishOptions { + pub clear: bool, + pub break_clients: bool, + pub num_replicas: Option, + pub organization: Option, + pub force: bool, + pub stdin_input: Option, +} + +impl Default for PublishOptions { + fn default() -> Self { + Self { + clear: false, + break_clients: false, + num_replicas: None, + organization: None, + force: true, + stdin_input: None, + } + } +} + /// Builder for creating `Smoketest` instances. pub struct SmoketestBuilder { module_code: Option, @@ -409,6 +433,7 @@ pub struct SmoketestBuilder { extra_deps: String, autopublish: bool, pg_port: Option, + server_url_override: Option, } impl Default for SmoketestBuilder { @@ -427,9 +452,15 @@ impl SmoketestBuilder { extra_deps: String::new(), autopublish: true, pg_port: None, + server_url_override: None, } } + pub fn server_url(mut self, url: &str) -> Self { + self.server_url_override = Some(url.to_string()); + self + } + /// Enables the PostgreSQL wire protocol on the specified port. pub fn pg_port(mut self, port: u16) -> Self { self.pg_port = Some(port); @@ -503,7 +534,10 @@ impl SmoketestBuilder { let build_start = Instant::now(); // Check if we're running against a remote server - let (guard, server_url) = if let Some(remote_url) = remote_server_url() { + let (guard, server_url) = if let Some(url) = self.server_url_override { + eprintln!("[REMOTE] Using explicit server URL: {}", url); + (None, url) + } else if let Some(remote_url) = remote_server_url() { eprintln!("[REMOTE] Using remote server: {}", remote_url); (None, remote_url) } else { @@ -629,6 +663,16 @@ impl Smoketest { .context("No spacetimedb_token found in config") } + pub fn login_with_token(&self, token: &str) -> Result<()> { + let host = self.server_host(); + let config_str = format!( + "default_server = \"localhost\"\n\nspacetimedb_token = \"{}\"\n\n[[server_configs]]\nnickname = \"localhost\"\nhost = \"{}\"\nprotocol = \"http\"\n", + token, host + ); + fs::write(&self.config_path, config_str).context("Failed to write config.toml")?; + Ok(()) + } + /// Runs psql command against the PostgreSQL wire protocol server. /// /// Returns the output on success, or an error with stderr on failure. @@ -998,7 +1042,7 @@ log = "0.4" /// Publishes the module and stores the database identity. pub fn publish_module(&mut self) -> Result { - self.publish_module_opts(None, false) + self.publish_module_internal_ext(None, PublishOptions::default()) } /// Publishes the module with a specific name and optional clear flag. @@ -1006,7 +1050,17 @@ log = "0.4" /// If `name` is provided, the database will be published with that name. /// If `clear` is true, the database will be cleared before publishing. pub fn publish_module_named(&mut self, name: &str, clear: bool) -> Result { - self.publish_module_opts(Some(name), clear) + self.publish_module_internal_ext( + Some(name), + PublishOptions { + clear, + ..PublishOptions::default() + }, + ) + } + + pub fn publish_module_named_ext(&mut self, name: &str, opts: PublishOptions) -> Result { + self.publish_module_internal_ext(Some(name), opts) } /// Re-publishes the module to the existing database identity with optional clear. @@ -1019,12 +1073,25 @@ log = "0.4" .as_ref() .context("No database published yet")? .clone(); - self.publish_module_opts(Some(&identity), clear) + self.publish_module_internal_ext( + Some(&identity), + PublishOptions { + clear, + ..PublishOptions::default() + }, + ) } /// Publishes the module with name, clear, and break_clients options. pub fn publish_module_with_options(&mut self, name: &str, clear: bool, break_clients: bool) -> Result { - self.publish_module_internal(Some(name), clear, break_clients, true, None) + self.publish_module_internal_ext( + Some(name), + PublishOptions { + clear, + break_clients, + ..PublishOptions::default() + }, + ) } /// Publishes the module and allows supplying stdin input to the CLI. @@ -1032,28 +1099,32 @@ log = "0.4" /// Useful for interactive publish prompts which require typed acknowledgements. /// Note: does NOT pass `--yes` so that interactive prompts are not suppressed. pub fn publish_module_with_stdin(&mut self, name: &str, stdin_input: &str) -> Result { - self.publish_module_internal(Some(name), false, false, false, Some(stdin_input)) + self.publish_module_internal_ext( + Some(name), + PublishOptions { + force: false, + stdin_input: Some(stdin_input.to_string()), + ..PublishOptions::default() + }, + ) } /// Publishes the module without passing `--yes`, so interactive prompts are not suppressed. pub fn publish_module_named_no_force(&mut self, name: &str) -> Result { - self.publish_module_internal(Some(name), false, false, false, None) + self.publish_module_internal_ext( + Some(name), + PublishOptions { + force: false, + ..PublishOptions::default() + }, + ) } - /// Internal helper for publishing with options. - fn publish_module_opts(&mut self, name: Option<&str>, clear: bool) -> Result { - self.publish_module_internal(name, clear, false, true, None) + pub fn publish_module_with_options_ext(&mut self, name: &str, opts: PublishOptions) -> Result { + self.publish_module_internal_ext(Some(name), opts) } - /// Internal helper for publishing with all options. - fn publish_module_internal( - &mut self, - name: Option<&str>, - clear: bool, - break_clients: bool, - force: bool, - stdin_input: Option<&str>, - ) -> Result { + fn publish_module_internal_ext(&mut self, name: Option<&str>, opts: PublishOptions) -> Result { let start = Instant::now(); // Determine the WASM path - either precompiled or build it @@ -1096,25 +1167,37 @@ log = "0.4" let publish_start = Instant::now(); let mut args = vec!["publish", "--server", &self.server_url, "--bin-path", &wasm_path_str]; - if force { + if opts.force { args.push("--yes"); } - if clear { + if opts.clear { args.push("--clear-database"); } - if break_clients { + if opts.break_clients { args.push("--break-clients"); } + let num_replicas_owned = opts.num_replicas.map(|n| n.to_string()); + if let Some(n) = num_replicas_owned.as_ref() { + args.push("--num-replicas"); + args.push(n); + } + + let org_owned = opts.organization.clone(); + if let Some(org) = org_owned.as_ref() { + args.push("--organization"); + args.push(org); + } + let name_owned; if let Some(n) = name { name_owned = n.to_string(); args.push(&name_owned); } - let output = match stdin_input { + let output = match opts.stdin_input.as_deref() { Some(stdin_input) => self.spacetime_with_stdin(&args, stdin_input)?, None => self.spacetime(&args)?, }; @@ -1396,15 +1479,45 @@ log = "0.4" self.subscribe_opts(queries, n, None) } + pub fn subscribe_on(&self, database: &str, queries: &[&str], n: usize) -> Result> { + self.subscribe_on_opts(database, queries, n, Some(false)) + } + /// Starts a subscription with --confirmed flag and waits for N updates. pub fn subscribe_confirmed(&self, queries: &[&str], n: usize) -> Result> { self.subscribe_opts(queries, n, Some(true)) } + pub fn subscribe_on_confirmed(&self, database: &str, queries: &[&str], n: usize) -> Result> { + self.subscribe_on_opts(database, queries, n, Some(true)) + } + /// Internal helper for subscribe with options. fn subscribe_opts(&self, queries: &[&str], n: usize, confirmed: Option) -> Result> { let start = Instant::now(); let identity = self.database_identity.as_ref().context("No database published")?; + self.subscribe_on_impl(identity, queries, n, confirmed, start) + } + + fn subscribe_on_opts( + &self, + database: &str, + queries: &[&str], + n: usize, + confirmed: Option, + ) -> Result> { + let start = Instant::now(); + self.subscribe_on_impl(database, queries, n, confirmed, start) + } + + fn subscribe_on_impl( + &self, + database: &str, + queries: &[&str], + n: usize, + confirmed: Option, + start: Instant, + ) -> Result> { let config_path_str = self.config_path.to_str().unwrap(); let cli_path = ensure_binaries_built(); @@ -1415,7 +1528,7 @@ log = "0.4" "subscribe".to_string(), "--server".to_string(), self.server_url.to_string(), - identity.to_string(), + database.to_string(), "-t".to_string(), "30".to_string(), "-n".to_string(), @@ -1456,6 +1569,10 @@ log = "0.4" self.subscribe_background_opts(queries, n, None) } + pub fn subscribe_background_on(&self, database: &str, queries: &[&str], n: usize) -> Result { + self.subscribe_background_on_opts(database, queries, n, Some(false)) + } + /// Starts a subscription in the background with --confirmed flag. pub fn subscribe_background_confirmed(&self, queries: &[&str], n: usize) -> Result { self.subscribe_background_opts(queries, n, Some(true)) @@ -1466,6 +1583,15 @@ log = "0.4" self.subscribe_background_opts(queries, n, Some(false)) } + pub fn subscribe_background_on_confirmed( + &self, + database: &str, + queries: &[&str], + n: usize, + ) -> Result { + self.subscribe_background_on_opts(database, queries, n, Some(true)) + } + /// Internal helper for background subscribe with options. fn subscribe_background_opts( &self, @@ -1473,14 +1599,32 @@ log = "0.4" n: usize, confirmed: Option, ) -> Result { - use std::io::{BufRead, BufReader}; - let identity = self .database_identity .as_ref() .context("No database published")? .clone(); + self.subscribe_background_on_impl(&identity, queries, n, confirmed) + } + + fn subscribe_background_on_opts( + &self, + database: &str, + queries: &[&str], + n: usize, + confirmed: Option, + ) -> Result { + self.subscribe_background_on_impl(database, queries, n, confirmed) + } + + fn subscribe_background_on_impl( + &self, + database: &str, + queries: &[&str], + n: usize, + confirmed: Option, + ) -> Result { let cli_path = ensure_binaries_built(); let mut cmd = Command::new(&cli_path); // Use --print-initial-update so we know when subscription is established @@ -1491,7 +1635,7 @@ log = "0.4" "subscribe".to_string(), "--server".to_string(), self.server_url.clone(), - identity, + database.to_string(), "-t".to_string(), "30".to_string(), "-n".to_string(), diff --git a/crates/smoketests/tests/smoketests/add_remove_index.rs b/crates/smoketests/tests/smoketests/add_remove_index.rs index 9b6eaa53478..a145374ae3f 100644 --- a/crates/smoketests/tests/smoketests/add_remove_index.rs +++ b/crates/smoketests/tests/smoketests/add_remove_index.rs @@ -15,7 +15,7 @@ fn test_add_then_remove_index() { .build(); // TODO: Does the name do anything? Other tests just let the DB assign. - let name = format!("test-db-{}", std::process::id()); + let name = format!("add-remove-index-{}", std::process::id()); // Publish and attempt a subscribing to a join query. // There are no indices, resulting in an unsupported unindexed join. diff --git a/crates/smoketests/tests/smoketests/change_host_type.rs b/crates/smoketests/tests/smoketests/change_host_type.rs index b72f86cdf04..b8038975ee6 100644 --- a/crates/smoketests/tests/smoketests/change_host_type.rs +++ b/crates/smoketests/tests/smoketests/change_host_type.rs @@ -99,7 +99,7 @@ fn test_repair_host_type() { let mut test = Smoketest::builder().autopublish(false).build(); - test.publish_typescript_module_source("modules-basic-ts", "basic-ts", TS_MODULE_BASIC) + test.publish_typescript_module_source("modules-basic-ts", "basic-ts-change-host-type", TS_MODULE_BASIC) .unwrap(); assert_host_type(&test, HostType::Js); // Set the program kind to the wrong value. diff --git a/crates/smoketests/tests/smoketests/delete_database.rs b/crates/smoketests/tests/smoketests/delete_database.rs index c101304563f..ba33c2d562b 100644 --- a/crates/smoketests/tests/smoketests/delete_database.rs +++ b/crates/smoketests/tests/smoketests/delete_database.rs @@ -12,7 +12,7 @@ fn test_delete_database() { .autopublish(false) .build(); - let name = format!("test-db-{}", std::process::id()); + let name = format!("delete-database-{}", std::process::id()); test.publish_module_named(&name, false).unwrap(); // Start subscription in background to collect updates diff --git a/crates/smoketests/tests/smoketests/domains.rs b/crates/smoketests/tests/smoketests/domains.rs index 5acf85e848f..db9f8c2044f 100644 --- a/crates/smoketests/tests/smoketests/domains.rs +++ b/crates/smoketests/tests/smoketests/domains.rs @@ -5,10 +5,10 @@ use spacetimedb_smoketests::Smoketest; fn test_set_name() { let mut test = Smoketest::builder().autopublish(false).build(); - let orig_name = format!("test-db-{}", std::process::id()); + let orig_name = format!("domains-set-name-{}", std::process::id()); test.publish_module_named(&orig_name, false).unwrap(); - let rand_name = format!("test-db-{}-renamed", std::process::id()); + let rand_name = format!("domains-set-name-{}-renamed", std::process::id()); // This should fail before there's a db with this name let result = test.spacetime(&["logs", "--server", &test.server_url, &rand_name]); @@ -33,7 +33,7 @@ fn test_set_name() { fn test_subdomain_behavior() { let mut test = Smoketest::builder().autopublish(false).build(); - let root_name = format!("test-db-{}", std::process::id()); + let root_name = format!("domains-subdomain-behavior-{}", std::process::id()); test.publish_module_named(&root_name, false).unwrap(); // Double slash should fail @@ -57,7 +57,7 @@ fn test_set_to_existing_name() { let id_to_rename = test.database_identity.clone().unwrap(); // Publish second database with a name - let rename_to = format!("test-db-{}-target", std::process::id()); + let rename_to = format!("domains-set-existing-target-{}", std::process::id()); test.publish_module_named(&rename_to, false).unwrap(); // Try to rename first db to the name of the second - should fail @@ -80,9 +80,9 @@ fn test_set_to_existing_name() { fn test_replace_names() { let mut test = Smoketest::builder().autopublish(false).build(); - let orig_name = format!("test-db-{}", std::process::id()); - let alt_name1 = format!("test-db-{}-alt1", std::process::id()); - let alt_name2 = format!("test-db-{}-alt2", std::process::id()); + let orig_name = format!("domains-replace-names-{}", std::process::id()); + let alt_name1 = format!("domains-replace-names-{}-alt1", std::process::id()); + let alt_name2 = format!("domains-replace-names-{}-alt2", std::process::id()); test.publish_module_named(&orig_name, false).unwrap(); // Use the API to replace names diff --git a/crates/smoketests/tests/smoketests/fail_initial_publish.rs b/crates/smoketests/tests/smoketests/fail_initial_publish.rs index ea901b4fad3..3d3308ab792 100644 --- a/crates/smoketests/tests/smoketests/fail_initial_publish.rs +++ b/crates/smoketests/tests/smoketests/fail_initial_publish.rs @@ -24,7 +24,7 @@ fn test_fail_initial_publish() { .autopublish(false) .build(); - let name = format!("test-db-{}", std::process::id()); + let name = format!("fail-initial-publish-{}", std::process::id()); // First publish should fail due to broken module let result = test.publish_module_named(&name, false); diff --git a/crates/smoketests/tests/smoketests/http_egress.rs b/crates/smoketests/tests/smoketests/http_egress.rs index d13c37b88ab..8fe24193d25 100644 --- a/crates/smoketests/tests/smoketests/http_egress.rs +++ b/crates/smoketests/tests/smoketests/http_egress.rs @@ -3,7 +3,7 @@ use std::net::TcpListener; use std::thread::JoinHandle; use std::time::{Duration, Instant}; -use spacetimedb_smoketests::Smoketest; +use spacetimedb_smoketests::{require_local_server, Smoketest}; fn module_code_http_disallowed_ip(addr: &str, port: u16) -> String { format!( @@ -82,6 +82,7 @@ fn test_http_disallowed_ip_is_blocked() { #[test] fn test_http_redirect_to_disallowed_ip_is_blocked() { + require_local_server!(); let (port, redirect_server) = spawn_redirect_server("http://10.0.0.1:80/"); let module_code = module_code_http_disallowed_ip("localhost", port); let test = Smoketest::builder().module_code(&module_code).build(); diff --git a/crates/smoketests/tests/smoketests/modules.rs b/crates/smoketests/tests/smoketests/modules.rs index 0cb9b1dc544..8d7c298ae60 100644 --- a/crates/smoketests/tests/smoketests/modules.rs +++ b/crates/smoketests/tests/smoketests/modules.rs @@ -8,7 +8,7 @@ fn test_module_update() { .autopublish(false) .build(); - let name = format!("test-db-{}", std::process::id()); + let name = format!("module-update-{}", std::process::id()); // Initial publish test.publish_module_named(&name, false).unwrap(); @@ -100,7 +100,7 @@ fn test_hotswap_module() { .autopublish(false) .build(); - let name = format!("test-db-{}", std::process::id()); + let name = format!("hotswap-module-{}", std::process::id()); // Publish initial module and subscribe to all test.publish_module_named(&name, false).unwrap(); diff --git a/crates/smoketests/tests/smoketests/permissions.rs b/crates/smoketests/tests/smoketests/permissions.rs index c2037cab812..f0fca600f55 100644 --- a/crates/smoketests/tests/smoketests/permissions.rs +++ b/crates/smoketests/tests/smoketests/permissions.rs @@ -102,7 +102,7 @@ fn test_replace_names() { .autopublish(false) .build(); - let name = format!("test-db-{}", std::process::id()); + let name = format!("permissions-replace-names-{}", std::process::id()); test.publish_module_named(&name, false).unwrap(); // Switch to a new identity diff --git a/crates/smoketests/tests/smoketests/pg_wire.rs b/crates/smoketests/tests/smoketests/pg_wire.rs index 376adbcc4be..94cbcf09e3c 100644 --- a/crates/smoketests/tests/smoketests/pg_wire.rs +++ b/crates/smoketests/tests/smoketests/pg_wire.rs @@ -14,11 +14,11 @@ fn test_sql_format() { .autopublish(false) .build(); - test.publish_module_named("quickstart", true).unwrap(); + test.publish_module_named("pgwire-sql-format", true).unwrap(); test.call("test", &[]).unwrap(); test.assert_psql( - "quickstart", + "pgwire-sql-format", "SELECT * FROM t_ints", r#"i_8 | i_16 | i_32 | i_64 | i_128 | i_256 -----+-------+--------+----------+---------------+--------------- @@ -27,7 +27,7 @@ fn test_sql_format() { ); test.assert_psql( - "quickstart", + "pgwire-sql-format", "SELECT * FROM t_ints_tuple", r#"tuple --------------------------------------------------------------------------------------------------------------- @@ -36,7 +36,7 @@ fn test_sql_format() { ); test.assert_psql( - "quickstart", + "pgwire-sql-format", "SELECT * FROM t_uints", r#"u_8 | u_16 | u_32 | u_64 | u_128 | u_256 -----+------+-------+----------+---------------+--------------- @@ -45,7 +45,7 @@ fn test_sql_format() { ); test.assert_psql( - "quickstart", + "pgwire-sql-format", "SELECT * FROM t_uints_tuple", r#"tuple ------------------------------------------------------------------------------------------------------------- @@ -54,7 +54,7 @@ fn test_sql_format() { ); test.assert_psql( - "quickstart", + "pgwire-sql-format", "SELECT * FROM t_simple_enum", r#"id | action ----+---------- @@ -64,7 +64,7 @@ fn test_sql_format() { ); test.assert_psql( - "quickstart", + "pgwire-sql-format", "SELECT * FROM t_enum", r#"id | color ----+--------------- @@ -86,7 +86,7 @@ fn test_sql_conn() { .autopublish(false) .build(); - test.publish_module_named("quickstart", true).unwrap(); + test.publish_module_named("pgwire-sql-conn", true).unwrap(); test.call("test", &[]).unwrap(); let token = test.read_token().unwrap(); @@ -98,7 +98,7 @@ fn test_sql_conn() { cfg.port(pg_port); cfg.user("postgres"); cfg.password(token); - cfg.dbname("quickstart"); + cfg.dbname("pgwire-sql-conn"); let rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(async move { @@ -158,17 +158,17 @@ fn test_failures() { .autopublish(false) .build(); - test.publish_module_named("quickstart", true).unwrap(); + test.publish_module_named("pgwire-failure", true).unwrap(); // Empty query returns empty result - let output = test.psql("quickstart", "").unwrap(); + let output = test.psql("pgwire-failure", "").unwrap(); assert!( output.is_empty(), "Expected empty output for empty query, got: {}", output ); - let result = test.psql_with_token("quickstart", "invalid_token", "SELECT * FROM t_uints"); + let result = test.psql_with_token("pgwire-failure", "invalid_token", "SELECT * FROM t_uints"); assert!(result.is_err(), "Expected error for invalid token"); let err = result.unwrap_err().to_string(); assert!( @@ -179,7 +179,7 @@ fn test_failures() { // Returns error for unsupported sql statements let result = test.psql( - "quickstart", + "pgwire-failure", "SELECT CASE a WHEN 1 THEN 'one' ELSE 'other' END FROM t_uints", ); assert!(result.is_err(), "Expected error for unsupported SQL"); @@ -191,7 +191,7 @@ fn test_failures() { ); // And prepared statements - let result = test.psql("quickstart", "SELECT * FROM t_uints where u8 = $1"); + let result = test.psql("pgwire-failure", "SELECT * FROM t_uints where u8 = $1"); assert!(result.is_err(), "Expected error for prepared statement"); let err = result.unwrap_err().to_string(); assert!( diff --git a/crates/smoketests/tests/smoketests/restart.rs b/crates/smoketests/tests/smoketests/restart.rs index f7af837cf2f..2372fd5e487 100644 --- a/crates/smoketests/tests/smoketests/restart.rs +++ b/crates/smoketests/tests/smoketests/restart.rs @@ -141,7 +141,7 @@ fn test_add_remove_index_after_restart() { .autopublish(false) .build(); - let name = format!("test-db-{}", std::process::id()); + let name = format!("restart-add-remove-index-{}", std::process::id()); // Publish and attempt subscribing to a join query. // There are no indices, resulting in an unsupported unindexed join. diff --git a/crates/smoketests/tests/smoketests/rls.rs b/crates/smoketests/tests/smoketests/rls.rs index af88bc42c14..5ed4ded348b 100644 --- a/crates/smoketests/tests/smoketests/rls.rs +++ b/crates/smoketests/tests/smoketests/rls.rs @@ -50,7 +50,7 @@ fn test_publish_fails_for_rls_on_private_table() { .autopublish(false) .build(); - let name = format!("test-db-{}", std::process::id()); + let name = format!("rls-rules-{}", std::process::id()); // Publishing should fail because RLS is on a private table let result = test.publish_module_named(&name, false); @@ -65,7 +65,7 @@ fn test_rls_disconnect_if_change() { .autopublish(false) .build(); - let name = format!("test-db-{}", std::process::id()); + let name = format!("rls-disconnect-{}", std::process::id()); // Initial publish without RLS test.publish_module_named(&name, false).unwrap(); @@ -100,7 +100,7 @@ fn test_rls_no_disconnect() { .autopublish(false) .build(); - let name = format!("test-db-{}", std::process::id()); + let name = format!("rls-no-disconnect-{}", std::process::id()); // Initial publish with RLS test.publish_module_named(&name, false).unwrap(); diff --git a/crates/smoketests/tests/smoketests/views.rs b/crates/smoketests/tests/smoketests/views.rs index a98635418b6..f44c6076159 100644 --- a/crates/smoketests/tests/smoketests/views.rs +++ b/crates/smoketests/tests/smoketests/views.rs @@ -782,8 +782,8 @@ fn test_typescript_query_builder_view_query() { require_pnpm!(); let mut test = Smoketest::builder().autopublish(false).build(); test.publish_typescript_module_source( - "views-subscribe-typescript", - "views-subscribe-typescript", + "views-query-builder-typescript", + "views-query-builder-typescript", TS_VIEWS_SUBSCRIBE_MODULE, ) .unwrap();