From 4621ed3afa1310bda937ed1d4715327d8d78dffe Mon Sep 17 00:00:00 2001 From: lovasoa Date: Sun, 4 Oct 2026 23:38:55 +0200 Subject: [PATCH] test(oracle): close prepared statements and pools before runtime shutdown --- .github/workflows/ci.yml | 19 +++++----- CONTRIBUTING.md | 6 +++ scripts/README.md | 3 +- scripts/install-oracle-odbc.sh | 50 ++++++++++++++++++++++++ scripts/run-test-binaries.sh | 2 +- tests/basic/mod.rs | 8 ++-- tests/common/mod.rs | 69 +++++++++++++++++++++++++++++++++- tests/cookies/mod.rs | 14 +++---- tests/core/mod.rs | 36 +++++++++++------- tests/core/path_aliases.rs | 10 ++--- tests/data_formats/mod.rs | 26 ++++++------- tests/errors/basic_auth.rs | 4 +- tests/errors/invalid_header.rs | 2 +- tests/errors/mod.rs | 14 +++---- tests/exec/mod.rs | 2 +- tests/oidc/mod.rs | 68 ++++++++++++++++----------------- tests/parameter_binding/mod.rs | 2 +- tests/requests/mod.rs | 14 +++---- tests/requests/webhook_hmac.rs | 6 +-- tests/server_timing/mod.rs | 8 ++-- tests/sql_test_files/mod.rs | 4 +- tests/transactions/mod.rs | 4 +- tests/uploads/mod.rs | 16 ++++---- 23 files changed, 259 insertions(+), 128 deletions(-) create mode 100755 scripts/install-oracle-odbc.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3e6eb4eba..dbb4b6fd3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -98,6 +98,7 @@ jobs: container: mssql db_url: "mssql://root:Password123!@127.0.0.1/sqlpage" - database: oracle + test_args: --test-threads=2 container: oracle db_url: "Driver=Oracle 21 ODBC driver;Dbq=//127.0.0.1:1521/FREEPDB1;Uid=root;Pwd=Password123!" - database: duckdb @@ -122,17 +123,17 @@ jobs: run: | sudo apt-get update && sudo apt-get install -y unixodbc unzip sudo scripts/install-duckdb-odbc.sh + - name: Cache Oracle Instant Client archives + if: matrix.database == 'oracle' + uses: actions/cache@v6 + with: + path: ${{ runner.temp }}/sqlpage-oracle-instantclient-archives + key: oracle-instantclient-linux-x64-21.21.0.0.0-v1 - name: Install Oracle ODBC driver if: matrix.database == 'oracle' run: | - sudo apt-get install -y alien libaio1t64 libodbcinst2 unixodbc - sudo rpm --import https://yum.oracle.com/RPM-GPG-KEY-oracle-ol8 - wget https://yum.oracle.com/repo/OracleLinux/OL8/oracle/instantclient21/x86_64/getPackage/oracle-instantclient-{basic,odbc}-21.21.0.0.0-1.el8.x86_64.rpm - sudo alien -i oracle-instantclient-basic-21.21.0.0.0-1.el8.x86_64.rpm - sudo alien -i oracle-instantclient-odbc-21.21.0.0.0-1.el8.x86_64.rpm - sudo ln -s /usr/lib/x86_64-linux-gnu/libaio.so.1t64 /usr/lib/libaio.so.1 - sudo /usr/lib/oracle/21/client64/bin/odbc_update_ini.sh / /usr/lib/oracle/21/client64/lib - echo "LD_LIBRARY_PATH=/usr/lib/oracle/21/client64/lib:$LD_LIBRARY_PATH" >> "$GITHUB_ENV" + sudo apt-get install -y libaio1t64 libodbcinst2 unixodbc unzip + scripts/install-oracle-odbc.sh - name: Start database container if: matrix.container != '' run: docker compose up --wait ${{ matrix.container }} @@ -141,7 +142,7 @@ jobs: run: docker compose logs ${{ matrix.container }} - name: Run tests against ${{ matrix.database }} timeout-minutes: 5 - run: scripts/run-test-binaries.sh + run: scripts/run-test-binaries.sh ${{ matrix.test_args }} env: DATABASE_URL: ${{ matrix.db_url }} MALLOC_CHECK_: 3 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 43487637c..00f6394b1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -96,6 +96,12 @@ export DATABASE_URL=mssql://root:Password123!@localhost/sqlpage cargo test ``` +Integration tests that create application state should use `common::make_app_state_from_config` or +`common::make_app_data_from_config` and `#[actix_web::rt::test(system = "crate::common::TestSystem")]`. +The shared test runtime clears prepared statements and closes database pools before shutdown, +including after a panic. This prevents Oracle's ODBC driver from hanging at process exit. +When testing Oracle locally, use `cargo test -- --test-threads=2` to avoid overwhelming the listener. + ### End-to-End Tests We use Playwright for end-to-end testing of dynamic frontend features. diff --git a/scripts/README.md b/scripts/README.md index 750695a6a..9c6929da6 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -14,5 +14,6 @@ These scripts pass configuration between build stages through temporary files in ## CI runs these - **`package-test-binaries.sh`**: Compiles the Rust test harnesses once and tars them, so the database matrix runs the same executables instead of recompiling SQLPage six times. -- **`run-test-binaries.sh`**: Runs SQLPage compiled executables against whatever `DATABASE_URL` names. +- **`install-oracle-odbc.sh`**: Verifies and extracts the pinned Oracle Instant Client ZIPs, registers a private ODBC driver, and writes its environment to `GITHUB_ENV`. CI caches the archives under `RUNNER_TEMP`. Requires curl, unzip, unixODBC, and libaio. +- **`run-test-binaries.sh`**: Runs SQLPage compiled executables against whatever `DATABASE_URL` names. Additional arguments are forwarded to each Rust harness; Oracle CI uses `--test-threads=2` to avoid exhausting its listener/process capacity. - **`test-examples-hurl.sh`**: Starts an example's containers and runs its `test.hurl` suite. Takes an example path to filter on. diff --git a/scripts/install-oracle-odbc.sh b/scripts/install-oracle-odbc.sh new file mode 100755 index 000000000..2399d0be3 --- /dev/null +++ b/scripts/install-oracle-odbc.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +set -euo pipefail + +: "${GITHUB_ENV:?Set GITHUB_ENV to the file that should receive the driver environment}" + +cd "$(dirname "${BASH_SOURCE[0]}")/.." + +# Keep the same client version as the RPM installation, without converting RPMs +# to Debian packages. CI caches the archives; verify them even on cache hits. +archive_dir="${RUNNER_TEMP:-/tmp}/sqlpage-oracle-instantclient-archives" +install_dir="${RUNNER_TEMP:-/tmp}/sqlpage-oracle-instantclient" +mkdir -p "$archive_dir" "$install_dir" + +for package in basic odbc; do + archive="instantclient-${package}-linux.x64-21.21.0.0.0dbru.zip" + if [[ ! -f "$archive_dir/$archive" ]]; then + curl --fail --location --remove-on-error --retry 3 --connect-timeout 15 \ + --max-time 180 --output "$archive_dir/$archive" \ + "https://download.oracle.com/otn_software/linux/instantclient/2121000/$archive" + fi +done + +( + cd "$archive_dir" + sha256sum --check <<'CHECKSUMS' +9cd0d5d5619ddaac43aa2214bab48e84155ca7e057d937634d1909b298125e8a instantclient-basic-linux.x64-21.21.0.0.0dbru.zip +37e4326ac14b08d9130d499fe5c1ba58f8ad72e8196f5618c0dc5880a24d6a23 instantclient-odbc-linux.x64-21.21.0.0.0dbru.zip +CHECKSUMS +) + +for package in basic odbc; do + unzip -oq "$archive_dir/instantclient-${package}-linux.x64-21.21.0.0.0dbru.zip" -d "$install_dir" +done +client_dir="$install_dir/instantclient_21_21" + +# Ubuntu 24.04's libaio package uses a different SONAME from Oracle's client. +libaio_path="$(ldconfig -p | awk '$1 ~ /^libaio\.so\.1(t64)?$/ { path = $NF } END { print path }')" +test -n "$libaio_path" +ln -sf "$libaio_path" "$client_dir/libaio.so.1" + +cat > "$install_dir/odbcinst.ini" <> "$GITHUB_ENV" diff --git a/scripts/run-test-binaries.sh b/scripts/run-test-binaries.sh index 6a02b2a77..4d0038a4d 100755 --- a/scripts/run-test-binaries.sh +++ b/scripts/run-test-binaries.sh @@ -13,6 +13,6 @@ fi for test_binary in "${test_binaries[@]}"; do echo "::group::$(basename "$test_binary")" - "$test_binary" --quiet + "$test_binary" --quiet "$@" echo "::endgroup::" done diff --git a/tests/basic/mod.rs b/tests/basic/mod.rs index 1db6cce64..b24dadadd 100644 --- a/tests/basic/mod.rs +++ b/tests/basic/mod.rs @@ -6,7 +6,7 @@ use actix_web::{ use crate::common::req_path; -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn test_index_ok() { let resp = req_path("/").await.unwrap(); assert_eq!(resp.status(), http::StatusCode::OK); @@ -17,7 +17,7 @@ async fn test_index_ok() { assert!(!body.contains("error")); } -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn test_access_config_forbidden() { let resp_result = req_path("/sqlpage/sqlpage.json").await; assert!( @@ -33,7 +33,7 @@ async fn test_access_config_forbidden() { ); } -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn test_static_files() { let resp = req_path("/tests/it_works.txt").await.unwrap(); assert_eq!(resp.status(), http::StatusCode::OK); @@ -41,7 +41,7 @@ async fn test_static_files() { assert_eq!(&body, &b"It works !"[..]); } -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn test_spaces_in_file_names() { let resp = req_path("/tests/core/spaces%20in%20file%20name.sql") .await diff --git a/tests/common/mod.rs b/tests/common/mod.rs index da2b689f0..9929a06ab 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -37,8 +37,75 @@ pub(crate) async fn get_request_to(path: &str) -> actix_web::Result get_request_to_with_data(path, data).await } +// Pools must close while the test runtime is still running. Cancelling their +// background work at runtime teardown can leave Oracle statements/connections +// open and deadlock the ODBC driver's process destructor. +thread_local! { + static TEST_POOLS: std::cell::RefCell> = const { std::cell::RefCell::new(Vec::new()) }; +} + +pub(crate) struct TestSystem(actix_web::rt::SystemRunner); + +impl TestSystem { + pub(crate) fn new() -> Self { + Self(actix_web::rt::System::new()) + } + + pub(crate) fn block_on(&self, future: F) -> F::Output { + use futures_util::FutureExt as _; + + self.0.block_on(async { + let result = std::panic::AssertUnwindSafe(future).catch_unwind().await; + let pools = TEST_POOLS.with(std::cell::RefCell::take); + for pool in pools { + use sqlx::connection::Connection as _; + + // Release prepared statements before disconnecting. Oracle can + // retain native resources if cached statements outlive their connection. + let mut connections = Vec::new(); + for _ in 0..pool.size() { + let mut connection = pool.acquire().await.unwrap(); + connection.clear_cached_statements().await.unwrap(); + connections.push(connection); + } + drop(connections); + pool.close().await; + } + match result { + Ok(output) => output, + Err(panic) => std::panic::resume_unwind(panic), + } + }) + } +} + +pub(crate) async fn make_app_state_from_config(config: &AppConfig) -> anyhow::Result { + let state = AppState::init(config).await?; + TEST_POOLS.with(|pools| pools.borrow_mut().push(state.db.connection.clone())); + Ok(state) +} + +#[test] +fn test_system_closes_pools_on_success_and_panic() { + for panic_in_test in [false, true] { + let system = TestSystem::new(); + let mut pool = None; + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + system.block_on(async { + let mut config = test_config(); + config.database_url = "sqlite::memory:".to_owned(); + let state = make_app_state_from_config(&config).await.unwrap(); + pool = Some(state.db.connection.clone()); + assert!(!panic_in_test, "intentional test panic"); + }); + })); + assert_eq!(result.is_err(), panic_in_test); + assert!(pool.unwrap().is_closed()); + } +} + pub(crate) async fn make_app_data_from_config(config: AppConfig) -> Data { - let state = AppState::init(&config).await.unwrap(); + let state = make_app_state_from_config(&config).await.unwrap(); Data::new(state) } diff --git a/tests/cookies/mod.rs b/tests/cookies/mod.rs index 1fa2b1db8..b9d5f1b36 100644 --- a/tests/cookies/mod.rs +++ b/tests/cookies/mod.rs @@ -12,7 +12,7 @@ async fn set_cookie_header(path: &str) -> String { .to_owned() } -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn cookies_are_http_only_secure_and_same_site_strict_by_default() { let header = set_cookie_header("/tests/cookies/set_cookie_defaults.sql").await; assert!(header.starts_with("session=abc123"), "{header}"); @@ -22,7 +22,7 @@ async fn cookies_are_http_only_secure_and_same_site_strict_by_default() { assert!(header.contains("Path=/"), "{header}"); } -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn zero_turns_off_a_cookie_protection() { let header = set_cookie_header("/tests/cookies/set_cookie_opt_out.sql").await; assert!(!header.contains("HttpOnly"), "{header}"); @@ -31,7 +31,7 @@ async fn zero_turns_off_a_cookie_protection() { assert!(header.contains("Path=/admin"), "{header}"); } -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn a_page_made_only_of_header_components_still_sends_them() { let resp = req_path("/tests/cookies/header_only_response.sql") .await @@ -40,20 +40,20 @@ async fn a_page_made_only_of_header_components_still_sends_them() { assert!(resp.headers().contains_key(SET_COOKIE)); } -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn a_log_row_does_not_end_the_header_phase() { let header = set_cookie_header("/tests/cookies/log_before_cookie.sql").await; assert!(header.starts_with("session=abc123"), "{header}"); } -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn removing_a_cookie_expires_it() { let header = set_cookie_header("/tests/cookies/remove_cookie.sql").await; assert!(header.starts_with("session=;"), "{header}"); assert!(header.contains("Max-Age=0"), "{header}"); } -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn an_rfc_3339_expires_date_becomes_an_http_date() { let header = set_cookie_header("/tests/cookies/cookie_expires_rfc3339.sql").await; assert!( @@ -62,7 +62,7 @@ async fn an_rfc_3339_expires_date_becomes_an_http_date() { ); } -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn a_unix_timestamp_expires_date_becomes_an_http_date() { let header = set_cookie_header("/tests/cookies/cookie_expires_timestamp.sql").await; assert!( diff --git a/tests/core/mod.rs b/tests/core/mod.rs index 6062d877c..06f68c388 100644 --- a/tests/core/mod.rs +++ b/tests/core/mod.rs @@ -9,7 +9,7 @@ use crate::common::{make_app_data_from_config, req_path, req_path_with_app_data, mod path_aliases; -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn test_concurrent_requests() { let components = [ "table", "form", "card", "datagrid", "hero", "list", "timeline", @@ -42,7 +42,7 @@ async fn test_concurrent_requests() { } } -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn test_datagrid_description_presence_controls_placeholder() { let resp = req_path("/tests/components/datagrid_icon_only.sql") .await @@ -56,7 +56,7 @@ async fn test_datagrid_description_presence_controls_placeholder() { assert_eq!(body.matches('–').count(), 1, "{body}"); } -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn test_routing_with_db_fs() { let mut config = test_config(); if config.database_url.contains("memory") { @@ -64,7 +64,9 @@ async fn test_routing_with_db_fs() { } config.site_prefix = "/prefix/".to_string(); - let state = AppState::init(&config).await.unwrap(); + let state = crate::common::make_app_state_from_config(&config) + .await + .unwrap(); if matches!( state.db.info.database_type, @@ -101,7 +103,9 @@ async fn test_routing_with_db_fs() { .await .unwrap(); - let state = AppState::init(&config).await.unwrap(); + let state = crate::common::make_app_state_from_config(&config) + .await + .unwrap(); let app_data = actix_web::web::Data::new(state); let resp = req_path_with_app_data("/prefix/on_db.sql", app_data.clone()) @@ -117,7 +121,7 @@ async fn test_routing_with_db_fs() { } #[cfg(unix)] -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn test_non_unicode_static_path_returns_bad_request_with_db_fs() { let mut config = test_config(); if !config.database_url.starts_with("sqlite") { @@ -126,7 +130,9 @@ async fn test_non_unicode_static_path_returns_bad_request_with_db_fs() { config.database_url = "sqlite://file:test_non_unicode_static_path?mode=memory&cache=shared".to_string(); - let state = AppState::init(&config).await.unwrap(); + let state = crate::common::make_app_state_from_config(&config) + .await + .unwrap(); let expected_db_path = "\u{FFFD}.txt"; let mut conn = state.db.connection.acquire().await.unwrap(); @@ -149,7 +155,9 @@ async fn test_non_unicode_static_path_returns_bad_request_with_db_fs() { .unwrap(); drop(conn); - let state = AppState::init(&config).await.unwrap(); + let state = crate::common::make_app_state_from_config(&config) + .await + .unwrap(); let app_data = actix_web::web::Data::new(state); let req = test::TestRequest::get() .uri("/%FF.txt") @@ -165,11 +173,13 @@ async fn test_non_unicode_static_path_returns_bad_request_with_db_fs() { ); } -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn test_routing_with_prefix() { let mut config = test_config(); config.site_prefix = "/prefix/".to_string(); - let state = AppState::init(&config).await.unwrap(); + let state = crate::common::make_app_state_from_config(&config) + .await + .unwrap(); let app_data = actix_web::web::Data::new(state); let resp = req_path_with_app_data( @@ -220,7 +230,7 @@ async fn test_routing_with_prefix() { assert_eq!(location.to_str().unwrap(), "/prefix/"); } -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn test_hidden_files() { let resp_result = req_path("/tests/core/.hidden.sql").await; assert!( @@ -238,7 +248,7 @@ async fn test_hidden_files() { ); } -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn test_official_website_documentation() { let app_data = make_app_data_for_official_website().await; let resp = req_path_with_app_data("/component.sql?component=button", app_data) @@ -255,7 +265,7 @@ async fn test_official_website_documentation() { ); } -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn test_official_website_basic_auth_example() { let resp = req_path_with_app_data( "/examples/authentication/basic_auth.sql", diff --git a/tests/core/path_aliases.rs b/tests/core/path_aliases.rs index a83b3f68a..36bb6ad13 100644 --- a/tests/core/path_aliases.rs +++ b/tests/core/path_aliases.rs @@ -19,12 +19,12 @@ async fn assert_sql_response(path: &str, expected_status: StatusCode) { } } -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn mixed_case_sql_file_is_executed() { assert_sql_response("/tests/core/mixed_case.%53ql", StatusCode::OK).await; } -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn trailing_space_sql_path_is_forbidden() { assert_sql_response("/tests/core/sql_source.sql%20", StatusCode::FORBIDDEN).await; } @@ -38,19 +38,19 @@ async fn assert_windows_alias_response(path: &str, expected_status: StatusCode) } #[cfg(windows)] -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn windows_mixed_case_sql_alias_is_executed() { assert_windows_alias_response("/tests/core/sql_source.SQL", StatusCode::OK).await; } #[cfg(windows)] -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn windows_trailing_dot_sql_alias_is_forbidden() { assert_windows_alias_response("/tests/core/sql_source.sql.", StatusCode::FORBIDDEN).await; } #[cfg(windows)] -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn windows_ntfs_stream_sql_alias_is_forbidden() { assert_windows_alias_response("/tests/core/sql_source.sql::$DATA", StatusCode::FORBIDDEN).await; } diff --git a/tests/data_formats/mod.rs b/tests/data_formats/mod.rs index 7c6c3d2f1..94021f7ad 100644 --- a/tests/data_formats/mod.rs +++ b/tests/data_formats/mod.rs @@ -19,7 +19,7 @@ async fn req_with_accept( main_handler(req).await } -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn test_json_body() -> actix_web::Result<()> { let req = get_request_to("/tests/data_formats/json_data.sql") .await? @@ -39,7 +39,7 @@ async fn test_json_body() -> actix_web::Result<()> { Ok(()) } -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn test_csv_body() -> actix_web::Result<()> { let app_data = make_app_data().await; if matches!( @@ -68,7 +68,7 @@ async fn test_csv_body() -> actix_web::Result<()> { Ok(()) } -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn test_csv_filename_header_injection() -> actix_web::Result<()> { use actix_web::http::header::ContentDisposition; @@ -107,7 +107,7 @@ async fn test_csv_filename_header_injection() -> actix_web::Result<()> { Ok(()) } -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn test_json_columns() { let app_data = make_app_data().await; if !matches!( @@ -140,7 +140,7 @@ async fn test_json_columns() { ); } -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn test_accept_json_returns_json_array() -> actix_web::Result<()> { let resp = req_with_accept( "/tests/sql_test_files/component_rendering/simple.sql", @@ -161,7 +161,7 @@ async fn test_accept_json_returns_json_array() -> actix_web::Result<()> { Ok(()) } -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn test_accept_ndjson_returns_jsonlines() -> actix_web::Result<()> { let resp = req_with_accept( "/tests/sql_test_files/component_rendering/simple.sql", @@ -188,7 +188,7 @@ async fn test_accept_ndjson_returns_jsonlines() -> actix_web::Result<()> { Ok(()) } -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn test_accept_html_returns_html() -> actix_web::Result<()> { let resp = req_with_accept( "/tests/sql_test_files/component_rendering/simple.sql", @@ -205,7 +205,7 @@ async fn test_accept_html_returns_html() -> actix_web::Result<()> { Ok(()) } -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn test_accept_wildcard_returns_html() -> actix_web::Result<()> { let resp = req_with_accept( "/tests/sql_test_files/component_rendering/simple.sql", @@ -220,7 +220,7 @@ async fn test_accept_wildcard_returns_html() -> actix_web::Result<()> { Ok(()) } -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn test_accept_json_redirect_still_works() -> actix_web::Result<()> { let resp = req_with_accept("/tests/server_timing/redirect_test.sql", "application/json").await?; @@ -274,7 +274,7 @@ fn assert_no_sql_leak(body: &str, context: &str) { ); } -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn test_prod_json_error_does_not_leak_sql() { let body = req_prod_with_accept( "/tests/data_formats/json_error_leak.sql", @@ -288,7 +288,7 @@ async fn test_prod_json_error_does_not_leak_sql() { assert_no_sql_leak(&body, "json error"); } -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn test_prod_csv_error_does_not_leak_sql() { let app_data = make_prod_app_data().await; if matches!( @@ -315,7 +315,7 @@ async fn test_prod_csv_error_does_not_leak_sql() { /// A CSV page can hit an error before its first data row (so no header has been /// written and `columns` is empty). The generic error message must still be /// emitted instead of an empty record. -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn test_prod_csv_error_before_any_row_still_reports() { let app_data = make_prod_app_data().await; if matches!( @@ -342,7 +342,7 @@ async fn test_prod_csv_error_before_any_row_still_reports() { /// An author may only intend a page to be served as HTML, but a client can /// request it with `Accept: application/json` and pick the JSON renderer. /// In production that path must not leak SQL text either. -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn test_prod_html_page_requested_as_json_does_not_leak_sql() { let body = req_prod_with_accept( "/tests/data_formats/text_error_leak.sql", diff --git a/tests/errors/basic_auth.rs b/tests/errors/basic_auth.rs index 69330691a..6451bcc35 100644 --- a/tests/errors/basic_auth.rs +++ b/tests/errors/basic_auth.rs @@ -2,7 +2,7 @@ use crate::common::{get_request_to, req_path}; use actix_web::{http::StatusCode, test}; use sqlpage::webserver::http::main_handler; -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn test_basic_auth_not_provided() { let resp_result = req_path("/tests/errors/basic_auth.sql").await; let resp = resp_result.unwrap(); @@ -23,7 +23,7 @@ async fn test_basic_auth_not_provided() { ); } -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn test_basic_auth_with_credentials() { let req = get_request_to("/tests/errors/basic_auth.sql") .await diff --git a/tests/errors/invalid_header.rs b/tests/errors/invalid_header.rs index 73bf8dc46..c1200cca0 100644 --- a/tests/errors/invalid_header.rs +++ b/tests/errors/invalid_header.rs @@ -36,7 +36,7 @@ async fn assert_invalid_header_response(case: &InvalidHeaderCase) { ); } -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn test_invalid_header_components_return_an_error_response() { let cases = vec![ InvalidHeaderCase { diff --git a/tests/errors/mod.rs b/tests/errors/mod.rs index a6bfe55a6..eaefe81c0 100644 --- a/tests/errors/mod.rs +++ b/tests/errors/mod.rs @@ -30,7 +30,7 @@ async fn direct_request_status(path: &str, app_data: actix_web::web::Data, FakeOidcProvider, ) { - use sqlpage::{ - AppState, - app_config::{AppConfig, test_database_url}, - }; + use sqlpage::app_config::{AppConfig, test_database_url}; crate::common::init_log(); let provider = FakeOidcProvider::new(); provider.with_state_mut(provider_mutator); @@ -368,12 +365,14 @@ async fn setup_oidc_test_with_paths( ); let config: AppConfig = serde_json::from_str(&config_json).unwrap(); - let app_state = AppState::init(&config).await.unwrap(); + let app_state = crate::common::make_app_state_from_config(&config) + .await + .unwrap(); let app = test::init_service(create_app(Data::new(app_state))).await; (app, provider) } -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn test_public_clean_url_cannot_execute_a_protected_sql_file() { let file = "/tests/sql_test_files/data/regex_match_routing.sql"; let protected_paths = [file]; @@ -390,7 +389,7 @@ async fn test_public_clean_url_cannot_execute_a_protected_sql_file() { assert_ne!(public.status(), StatusCode::SEE_OTHER); } -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn test_top_level_builtin_assets_are_accessible_without_login_when_protected() { let protected_paths = ["/sqlpage."]; let (app, _provider) = setup_oidc_test_with_paths(|_| {}, &protected_paths, &[]).await; @@ -413,7 +412,7 @@ async fn test_top_level_builtin_assets_are_accessible_without_login_when_protect ); } -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn test_oidc_cached_authorization_redirect_cannot_replay_consumed_state() { let (app, provider) = setup_oidc_test(|_| {}).await; let mut cookies: Vec> = Vec::new(); @@ -475,7 +474,7 @@ async fn test_oidc_cached_authorization_redirect_cannot_replay_consumed_state() assert_eq!(final_response.status(), StatusCode::OK); } -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn test_oidc_happy_path() { let (app, provider) = setup_oidc_test(|_| {}).await; let mut cookies: Vec> = Vec::new(); @@ -588,12 +587,12 @@ async fn assert_oidc_callback_fails_with_bad_jwt( .await; } -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn test_oidc_csrf_state_mismatch_is_rejected() { assert_oidc_login_fails(|_| {}, Some("wrong_state".to_string())).await; } -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn test_oidc_nonce_mismatch_is_rejected() { assert_oidc_callback_fails_with_bad_jwt(|claims| { claims["nonce"] = json!("wrong_nonce"); @@ -601,7 +600,7 @@ async fn test_oidc_nonce_mismatch_is_rejected() { .await; } -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn test_oidc_bad_signature_is_rejected() { assert_oidc_login_fails( |state| { @@ -612,7 +611,7 @@ async fn test_oidc_bad_signature_is_rejected() { .await; } -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn test_oidc_wrong_audience_is_rejected() { assert_oidc_callback_fails_with_bad_jwt(|claims| { claims["aud"] = json!("wrong_client"); @@ -620,7 +619,7 @@ async fn test_oidc_wrong_audience_is_rejected() { .await; } -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn test_oidc_wrong_issuer_is_rejected() { assert_oidc_callback_fails_with_bad_jwt(|claims| { claims["iss"] = json!("https://wrong-issuer.com"); @@ -628,7 +627,7 @@ async fn test_oidc_wrong_issuer_is_rejected() { .await; } -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn test_oidc_expired_token_is_rejected() { assert_oidc_callback_fails_with_bad_jwt(|claims| { let current_exp = claims["exp"].as_i64().unwrap(); @@ -637,7 +636,7 @@ async fn test_oidc_expired_token_is_rejected() { .await; } -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn test_repeatedly_failing_callback_stops_redirecting_to_the_provider() { let (app, provider) = setup_oidc_test(|_| {}).await; let mut cookies: Vec> = Vec::new(); @@ -700,10 +699,7 @@ async fn setup_oidc_test_with_prefix( >, FakeOidcProvider, ) { - use sqlpage::{ - AppState, - app_config::{AppConfig, test_database_url}, - }; + use sqlpage::app_config::{AppConfig, test_database_url}; crate::common::init_log(); let provider = FakeOidcProvider::new(); provider.with_state_mut(provider_mutator); @@ -722,12 +718,14 @@ async fn setup_oidc_test_with_prefix( ); let config: AppConfig = serde_json::from_str(&config_json).unwrap(); - let app_state = AppState::init(&config).await.unwrap(); + let app_state = crate::common::make_app_state_from_config(&config) + .await + .unwrap(); let app = test::init_service(create_app(Data::new(app_state))).await; (app, provider) } -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn test_oidc_with_site_prefix() { let (app, _provider) = setup_oidc_test_with_prefix(|_| {}, "/my-app/").await; let mut cookies: Vec> = Vec::new(); @@ -745,12 +743,9 @@ async fn test_oidc_with_site_prefix() { ); } -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn test_oidc_logout_uses_correct_scheme() { - use sqlpage::{ - AppState, - app_config::{AppConfig, test_database_url}, - }; + use sqlpage::app_config::{AppConfig, test_database_url}; crate::common::init_log(); let provider = FakeOidcProvider::new(); @@ -768,7 +763,9 @@ async fn test_oidc_logout_uses_correct_scheme() { ); let config: AppConfig = serde_json::from_str(&config_json).unwrap(); - let app_state = AppState::init(&config).await.unwrap(); + let app_state = crate::common::make_app_state_from_config(&config) + .await + .unwrap(); let logout_path = app_state .oidc_state .as_ref() @@ -794,7 +791,7 @@ async fn test_oidc_logout_uses_correct_scheme() { /// An OIDC provider metadata refresh must not block authenticated requests. /// The refresh should happen in the background while existing requests are /// served using the current (possibly stale) OIDC client. -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn test_slow_discovery_does_not_block_authenticated_requests() { let (app, provider) = setup_oidc_test(|_| {}).await; let mut cookies: Vec> = Vec::new(); @@ -851,7 +848,7 @@ async fn test_slow_discovery_does_not_block_authenticated_requests() { /// A slow OIDC token endpoint must not freeze the server. /// The body-read timeout fires and the request completes with a redirect. -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn test_slow_token_endpoint_does_not_freeze_server() { let (app, provider) = setup_oidc_test(|_| {}).await; let mut cookies: Vec> = Vec::new(); @@ -897,12 +894,9 @@ async fn test_slow_token_endpoint_does_not_freeze_server() { /// generated for one session must NOT clear a different browser's auth cookie /// (forced-logout CSRF), while the legitimate logout of the issuing session /// must keep working. -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn test_oidc_logout_is_session_bound() { - use sqlpage::{ - AppState, - app_config::{AppConfig, test_database_url}, - }; + use sqlpage::app_config::{AppConfig, test_database_url}; crate::common::init_log(); let provider = FakeOidcProvider::new(); @@ -921,7 +915,9 @@ async fn test_oidc_logout_is_session_bound() { ); let config: AppConfig = serde_json::from_str(&config_json).unwrap(); - let app_state = AppState::init(&config).await.unwrap(); + let app_state = crate::common::make_app_state_from_config(&config) + .await + .unwrap(); let oidc_state = app_state.oidc_state.clone().unwrap(); let app = test::init_service(create_app(Data::new(app_state))).await; diff --git a/tests/parameter_binding/mod.rs b/tests/parameter_binding/mod.rs index 5fe31a4d6..bbf553f4d 100644 --- a/tests/parameter_binding/mod.rs +++ b/tests/parameter_binding/mod.rs @@ -6,7 +6,7 @@ use sqlpage::webserver::http::main_handler; use crate::common::{get_request_to_with_data, make_app_data}; -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn test_parameterized_pages_leave_a_prepared_statement_in_the_cache() -> actix_web::Result<()> { let data = make_app_data().await; diff --git a/tests/requests/mod.rs b/tests/requests/mod.rs index bd308f96a..df1d650a0 100644 --- a/tests/requests/mod.rs +++ b/tests/requests/mod.rs @@ -10,7 +10,7 @@ async fn rendered_page(req: actix_web::dev::ServiceRequest) -> actix_web::Result Ok(String::from_utf8(test::read_body(resp).await.to_vec()).unwrap()) } -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn test_request_body() -> actix_web::Result<()> { let page = rendered_page( get_request_to("/tests/requests/request_body_test.sql") @@ -48,7 +48,7 @@ async fn test_request_body() -> actix_web::Result<()> { Ok(()) } -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn test_request_body_base64() -> actix_web::Result<()> { let binary_data = (0u8..=255u8).collect::>(); let expected_base64 = @@ -90,7 +90,7 @@ async fn test_request_body_base64() -> actix_web::Result<()> { Ok(()) } -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn test_download_data_url() -> actix_web::Result<()> { let req = get_request_to("/tests/requests/request_download_test.sql") .await? @@ -110,7 +110,7 @@ async fn test_download_data_url() -> actix_web::Result<()> { Ok(()) } -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn test_large_form_field_roundtrip() -> actix_web::Result<()> { let long_string = "a".repeat(123_454); let req = get_request_to("/tests/components/display_form_field.sql") @@ -133,7 +133,7 @@ async fn test_large_form_field_roundtrip() -> actix_web::Result<()> { Ok(()) } -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn test_variables_function() -> actix_web::Result<()> { let url = "/tests/requests/variables.sql?common=get_value&get_only=get_val"; let req_body = "common=post_value&post_only=post_val"; @@ -200,7 +200,7 @@ async fn test_variables_function() -> actix_web::Result<()> { Ok(()) } -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn test_invalid_utf8_multipart_text_field_returns_bad_request() -> actix_web::Result<()> { let req = get_request_to("/tests/requests/variables.sql") .await? @@ -229,7 +229,7 @@ async fn test_invalid_utf8_multipart_text_field_returns_bad_request() -> actix_w Ok(()) } -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn test_missing_multipart_content_disposition_returns_bad_request() -> actix_web::Result<()> { let req = get_request_to("/tests/requests/variables.sql") .await? diff --git a/tests/requests/webhook_hmac.rs b/tests/requests/webhook_hmac.rs index a5af9dd87..9376900eb 100644 --- a/tests/requests/webhook_hmac.rs +++ b/tests/requests/webhook_hmac.rs @@ -3,7 +3,7 @@ use sqlpage::webserver::http::main_handler; use crate::common::get_request_to; -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn test_webhook_hmac_invalid_signature() -> actix_web::Result<()> { // Set up environment variable for webhook secret unsafe { @@ -39,7 +39,7 @@ async fn test_webhook_hmac_invalid_signature() -> actix_web::Result<()> { Ok(()) } -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn test_webhook_hmac_valid_signature() -> actix_web::Result<()> { // Set up environment variable for webhook secret unsafe { @@ -69,7 +69,7 @@ async fn test_webhook_hmac_valid_signature() -> actix_web::Result<()> { Ok(()) } -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn test_webhook_hmac_missing_signature() -> actix_web::Result<()> { // Set up environment variable for webhook secret unsafe { diff --git a/tests/server_timing/mod.rs b/tests/server_timing/mod.rs index 6d0a42faa..aa21f3ce7 100644 --- a/tests/server_timing/mod.rs +++ b/tests/server_timing/mod.rs @@ -3,7 +3,7 @@ use sqlpage::webserver::http::main_handler; use crate::common::{get_request_to, make_app_data_from_config, test_config}; -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn test_server_timing_disabled_in_production() -> actix_web::Result<()> { let mut config = test_config(); config.environment = sqlpage::app_config::DevOrProd::Production; @@ -25,7 +25,7 @@ async fn test_server_timing_disabled_in_production() -> actix_web::Result<()> { Ok(()) } -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn test_server_timing_enabled_in_development() -> actix_web::Result<()> { let mut config = test_config(); config.environment = sqlpage::app_config::DevOrProd::Development; @@ -70,7 +70,7 @@ async fn test_server_timing_enabled_in_development() -> actix_web::Result<()> { Ok(()) } -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn test_server_timing_format() -> actix_web::Result<()> { let req = get_request_to("/tests/sql_test_files/data/postgres_cast_syntax.sql") .await? @@ -103,7 +103,7 @@ async fn test_server_timing_format() -> actix_web::Result<()> { Ok(()) } -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn test_server_timing_in_redirect() -> actix_web::Result<()> { let mut config = test_config(); config.environment = sqlpage::app_config::DevOrProd::Development; diff --git a/tests/sql_test_files/mod.rs b/tests/sql_test_files/mod.rs index c8cf53f32..2a66cbeb6 100644 --- a/tests/sql_test_files/mod.rs +++ b/tests/sql_test_files/mod.rs @@ -5,7 +5,7 @@ use std::time::Duration; use tokio::sync::oneshot; use tokio::task::JoinHandle; -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn run_all_sql_test_files() { let app_data = crate::common::make_app_data().await; run_sql_test_cases(&app_data, get_sql_test_cases()).await; @@ -14,7 +14,7 @@ async fn run_all_sql_test_files() { /// Runs the SQL test files in `database-specific//`. /// These files use syntax that only works on a single database engine, so they /// cannot be part of the generic `run_all_sql_test_files` test. -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn run_database_specific_sql_test_files() { let app_data = crate::common::make_app_data().await; let db_type = database_type_name(&app_data); diff --git a/tests/transactions/mod.rs b/tests/transactions/mod.rs index 3718f09bf..27798994d 100644 --- a/tests/transactions/mod.rs +++ b/tests/transactions/mod.rs @@ -3,7 +3,7 @@ use sqlpage::webserver::{database::SupportedDatabase, http::main_handler}; use crate::common::{get_request_to_with_data, make_app_data}; -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn test_transaction_error() -> actix_web::Result<()> { let data = make_app_data().await; let path = match data.db.info.database_type { @@ -41,7 +41,7 @@ async fn test_transaction_error() -> actix_web::Result<()> { Ok(()) } -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn test_failed_copy_followed_by_query() -> actix_web::Result<()> { let app_data = make_app_data().await; let big_csv = "col1,col2\nval1,val2\n".repeat(1000); diff --git a/tests/uploads/mod.rs b/tests/uploads/mod.rs index fe30cd54e..42ea4c39b 100644 --- a/tests/uploads/mod.rs +++ b/tests/uploads/mod.rs @@ -28,7 +28,7 @@ async fn test_file_upload(target: &str) -> actix_web::Result<()> { Ok(()) } -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn test_persist_uploaded_file_mode() -> actix_web::Result<()> { let app_data = crate::common::make_app_data().await; let req = test::TestRequest::get() @@ -84,17 +84,17 @@ async fn test_persist_uploaded_file_mode() -> actix_web::Result<()> { Ok(()) } -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn test_file_upload_direct() -> actix_web::Result<()> { test_file_upload("/tests/uploads/upload_file_test.sql").await } -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn test_file_upload_through_runsql() -> actix_web::Result<()> { test_file_upload("/tests/uploads/upload_file_runsql_test.sql").await } -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn test_blank_file_upload_field() -> actix_web::Result<()> { let req = get_request_to("/tests/uploads/upload_file_test.sql") .await? @@ -120,7 +120,7 @@ async fn test_blank_file_upload_field() -> actix_web::Result<()> { Ok(()) } -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn test_file_upload_too_large() -> actix_web::Result<()> { let req = get_request_to("/tests/uploads/upload_file_test.sql") .await? @@ -149,7 +149,7 @@ async fn test_file_upload_too_large() -> actix_web::Result<()> { Ok(()) } -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn test_upload_file_data_url() -> actix_web::Result<()> { let req = get_request_to("/tests/uploads/upload_file_data_url_test.sql") .await? @@ -171,7 +171,7 @@ async fn test_upload_file_data_url() -> actix_web::Result<()> { Ok(()) } -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn test_uploaded_file_name() -> actix_web::Result<()> { let req = get_request_to("/tests/uploads/uploaded_file_name_test.sql") .await? @@ -193,7 +193,7 @@ async fn test_uploaded_file_name() -> actix_web::Result<()> { Ok(()) } -#[actix_web::test] +#[actix_web::rt::test(system = "crate::common::TestSystem")] async fn test_csv_upload() -> actix_web::Result<()> { let req = get_request_to("/tests/uploads/upload_csv_test.sql") .await?