diff --git a/Cargo.lock b/Cargo.lock index bc7fe6f9..06a32297 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -919,6 +919,7 @@ dependencies = [ "rustls", "serde", "serde_json", + "serial_test", "tap", "tokio", "tokio-postgres", @@ -3833,6 +3834,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scc" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46e6f046b7fef48e2660c57ed794263155d713de679057f2d0c169bfc6e756cc" +dependencies = [ + "sdd", +] + [[package]] name = "schannel" version = "0.1.27" @@ -3854,6 +3864,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sdd" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca" + [[package]] name = "seahash" version = "4.1.0" @@ -4005,6 +4021,32 @@ dependencies = [ "zeroize", ] +[[package]] +name = "serial_test" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "911bd979bf1070a3f3aa7b691a3b3e9968f339ceeec89e08c280a8a22207a32f" +dependencies = [ + "futures-executor", + "futures-util", + "log", + "once_cell", + "parking_lot", + "scc", + "serial_test_derive", +] + +[[package]] +name = "serial_test_derive" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a7d91949b85b0d2fb687445e448b40d322b6b3e4af6b44a29b21d9a5f33e6d9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + [[package]] name = "sha1_smol" version = "1.0.1" diff --git a/packages/cipherstash-proxy-integration/Cargo.toml b/packages/cipherstash-proxy-integration/Cargo.toml index 5a1f45ce..376ac6ca 100644 --- a/packages/cipherstash-proxy-integration/Cargo.toml +++ b/packages/cipherstash-proxy-integration/Cargo.toml @@ -28,3 +28,4 @@ tracing = { workspace = true } tracing-subscriber = { workspace = true } uuid = { version = "1.11.0", features = ["serde", "v4"] } reqwest = { version = "0.13", features = ["rustls"] } +serial_test = "3" diff --git a/packages/cipherstash-proxy-integration/src/common.rs b/packages/cipherstash-proxy-integration/src/common.rs index 31e274bf..20a9aacb 100644 --- a/packages/cipherstash-proxy-integration/src/common.rs +++ b/packages/cipherstash-proxy-integration/src/common.rs @@ -467,6 +467,26 @@ where ); } +/// Returns indices in zigzag order so insertion is never accidentally sorted. +/// For len=5: [4, 0, 3, 1, 2] +pub fn interleaved_indices(len: usize) -> Vec { + let mut indices = Vec::with_capacity(len); + let mut lo = 0; + let mut hi = len; + let mut take_hi = true; + while lo < hi { + if take_hi { + hi -= 1; + indices.push(hi); + } else { + indices.push(lo); + lo += 1; + } + take_hi = !take_hi; + } + indices +} + /// /// Configure the client TLS settings. /// These are the settings for connecting to the database with TLS. diff --git a/packages/cipherstash-proxy-integration/src/lib.rs b/packages/cipherstash-proxy-integration/src/lib.rs index e88d7cb8..e3b873f8 100644 --- a/packages/cipherstash-proxy-integration/src/lib.rs +++ b/packages/cipherstash-proxy-integration/src/lib.rs @@ -17,6 +17,7 @@ mod map_params; mod map_unique_index; mod migrate; mod multitenant; +mod ore_order_helpers; mod passthrough; mod pipeline; mod schema_change; diff --git a/packages/cipherstash-proxy-integration/src/map_ore_index_order.rs b/packages/cipherstash-proxy-integration/src/map_ore_index_order.rs index c72e9c06..08118b65 100644 --- a/packages/cipherstash-proxy-integration/src/map_ore_index_order.rs +++ b/packages/cipherstash-proxy-integration/src/map_ore_index_order.rs @@ -1,581 +1,222 @@ #[cfg(test)] mod tests { - use tokio_postgres::SimpleQueryMessage; - - use crate::common::{clear, connect_with_tls, random_id, trace, PROXY}; + use crate::common::{clear, connect_with_tls, trace, PROXY}; + use crate::ore_order_helpers; + use crate::ore_order_helpers::SortDirection; + use serial_test::serial; #[tokio::test] + #[serial] async fn map_ore_order_text() { trace(); - clear().await; - let client = connect_with_tls(PROXY).await; - - let s_one = "a"; - let s_two = "b"; - let s_three = "c"; - - let sql = " - INSERT INTO encrypted (id, encrypted_text) - VALUES ($1, $2), ($3, $4), ($5, $6) - "; - - client - .query( - sql, - &[ - &random_id(), - &s_two, - &random_id(), - &s_one, - &random_id(), - &s_three, - ], - ) - .await - .unwrap(); - - let sql = "SELECT encrypted_text FROM encrypted ORDER BY encrypted_text"; - let rows = client.query(sql, &[]).await.unwrap(); - - let actual = rows.iter().map(|row| row.get(0)).collect::>(); - let expected = vec![s_one, s_two, s_three]; - - assert_eq!(actual, expected); + ore_order_helpers::ore_order_text(&client).await; } #[tokio::test] + #[serial] async fn map_ore_order_text_desc() { trace(); - clear().await; - let client = connect_with_tls(PROXY).await; - - let s_one = "a"; - let s_two = "b"; - let s_three = "c"; - - let sql = " - INSERT INTO encrypted (id, encrypted_text) - VALUES ($1, $2), ($3, $4), ($5, $6) - "; - - client - .query( - sql, - &[ - &random_id(), - &s_two, - &random_id(), - &s_one, - &random_id(), - &s_three, - ], - ) - .await - .unwrap(); - - let sql = "SELECT encrypted_text FROM encrypted ORDER BY encrypted_text DESC"; - let rows = client.query(sql, &[]).await.unwrap(); - - let actual = rows.iter().map(|row| row.get(0)).collect::>(); - let expected = vec![s_three, s_two, s_one]; - - assert_eq!(actual, expected); + ore_order_helpers::ore_order_text_desc(&client).await; } #[tokio::test] + #[serial] async fn map_ore_order_nulls_last_by_default() { trace(); - clear().await; - let client = connect_with_tls(PROXY).await; - - let s_one = "a"; - let s_two = "b"; - - client - .query("INSERT INTO encrypted (id) values ($1)", &[&random_id()]) - .await - .unwrap(); - - let sql = " - INSERT INTO encrypted (id, encrypted_text) - VALUES ($1, $2), ($3, $4) - "; - - client - .query(sql, &[&random_id(), &s_one, &random_id(), &s_two]) - .await - .unwrap(); - - let sql = "SELECT encrypted_text FROM encrypted ORDER BY encrypted_text"; - let rows = client.query(sql, &[]).await.unwrap(); - - let actual = rows - .iter() - .map(|row| row.get(0)) - .collect::>>(); - let expected = vec![Some(s_one.to_string()), Some(s_two.to_string()), None]; - - assert_eq!(actual, expected); + ore_order_helpers::ore_order_nulls_last_by_default(&client).await; } #[tokio::test] + #[serial] async fn map_ore_order_nulls_first() { trace(); - clear().await; - let client = connect_with_tls(PROXY).await; - - let s_one = "a"; - let s_two = "b"; - - let sql = " - INSERT INTO encrypted (id, encrypted_text) - VALUES ($1, $2), ($3, $4) - "; - - client - .query(sql, &[&random_id(), &s_one, &random_id(), &s_two]) - .await - .unwrap(); - - client - .query("INSERT INTO encrypted (id) values ($1)", &[&random_id()]) - .await - .unwrap(); - - let sql = "SELECT encrypted_text FROM encrypted ORDER BY encrypted_text NULLS FIRST"; - let rows = client.query(sql, &[]).await.unwrap(); - - let actual = rows - .iter() - .map(|row| row.get(0)) - .collect::>>(); - let expected = vec![None, Some(s_one.to_string()), Some(s_two.to_string())]; - - assert_eq!(actual, expected); + ore_order_helpers::ore_order_nulls_first(&client).await; } #[tokio::test] + #[serial] async fn map_ore_order_qualified_column() { trace(); - clear().await; - let client = connect_with_tls(PROXY).await; - - let s_one = "a"; - let s_two = "b"; - let s_three = "c"; - - let sql = " - INSERT INTO encrypted (id, encrypted_text) - VALUES ($1, $2), ($3, $4), ($5, $6) - "; - - client - .query( - sql, - &[ - &random_id(), - &s_two, - &random_id(), - &s_one, - &random_id(), - &s_three, - ], - ) - .await - .unwrap(); - - let sql = "SELECT encrypted_text FROM encrypted ORDER BY encrypted.encrypted_text"; - let rows = client.query(sql, &[]).await.unwrap(); - - let actual = rows.iter().map(|row| row.get(0)).collect::>(); - let expected = vec![s_one, s_two, s_three]; - - assert_eq!(actual, expected); + ore_order_helpers::ore_order_qualified_column(&client).await; } #[tokio::test] + #[serial] async fn map_ore_order_qualified_column_with_alias() { trace(); - clear().await; - let client = connect_with_tls(PROXY).await; - - let s_one = "a"; - let s_two = "b"; - let s_three = "c"; - - let sql = " - INSERT INTO encrypted (id, encrypted_text) - VALUES ($1, $2), ($3, $4), ($5, $6) - "; - - client - .query( - sql, - &[ - &random_id(), - &s_two, - &random_id(), - &s_one, - &random_id(), - &s_three, - ], - ) - .await - .unwrap(); - - let sql = "SELECT encrypted_text FROM encrypted e ORDER BY e.encrypted_text"; - let rows = client.query(sql, &[]).await.unwrap(); - - let actual = rows.iter().map(|row| row.get(0)).collect::>(); - let expected = vec![s_one, s_two, s_three]; - - assert_eq!(actual, expected); + ore_order_helpers::ore_order_qualified_column_with_alias(&client).await; } #[tokio::test] + #[serial] async fn map_ore_order_no_eql_column_in_select_projection() { trace(); - clear().await; - let client = connect_with_tls(PROXY).await; - - let id_one = random_id(); - let s_one = "a"; - let id_two = random_id(); - let s_two = "b"; - let id_three = random_id(); - let s_three = "c"; - - let sql = " - INSERT INTO encrypted (id, encrypted_text) - VALUES ($1, $2), ($3, $4), ($5, $6) - "; - - client - .query( - sql, - &[&id_two, &s_two, &id_one, &s_one, &id_three, &s_three], - ) - .await - .unwrap(); - - let sql = "SELECT id FROM encrypted ORDER BY encrypted_text"; - let rows = client.query(sql, &[]).await.unwrap(); - - let actual = rows.iter().map(|row| row.get(0)).collect::>(); - let expected = vec![id_one, id_two, id_three]; - - assert_eq!(actual, expected); + ore_order_helpers::ore_order_no_eql_column_in_select_projection(&client).await; } #[tokio::test] + #[serial] async fn can_order_by_plaintext_column() { trace(); - clear().await; - let client = connect_with_tls(PROXY).await; - - let s_one = "a"; - let s_two = "b"; - let s_three = "c"; - - let sql = " - INSERT INTO encrypted (id, plaintext) - VALUES ($1, $2), ($3, $4), ($5, $6) - "; - - client - .query( - sql, - &[ - &random_id(), - &s_two, - &random_id(), - &s_one, - &random_id(), - &s_three, - ], - ) - .await - .unwrap(); - - let sql = "SELECT plaintext FROM encrypted ORDER BY plaintext"; - let rows = client.query(sql, &[]).await.unwrap(); - - let actual = rows.iter().map(|row| row.get(0)).collect::>(); - let expected = vec![s_one, s_two, s_three]; - - assert_eq!(actual, expected); + ore_order_helpers::ore_order_plaintext_column(&client).await; } #[tokio::test] + #[serial] async fn can_order_by_plaintext_and_eql_columns() { trace(); - clear().await; - let client = connect_with_tls(PROXY).await; - - let s_plaintext_one = "a"; - let s_plaintext_two = "a"; - let s_plaintext_three = "b"; - - let s_enctrypted_one = "a"; - let s_encrypted_two = "b"; - let s_encrypted_three = "c"; - - let sql = " - INSERT INTO encrypted (id, plaintext, encrypted_text) - VALUES ($1, $2, $3), ($4, $5, $6), ($7, $8, $9) - "; - - client - .query( - sql, - &[ - &random_id(), - &s_plaintext_two, - &s_encrypted_two, - &random_id(), - &s_plaintext_one, - &s_enctrypted_one, - &random_id(), - &s_plaintext_three, - &s_encrypted_three, - ], - ) - .await - .unwrap(); - - let sql = - "SELECT plaintext, encrypted_text FROM encrypted ORDER BY plaintext, encrypted_text"; - let rows = client.query(sql, &[]).await.unwrap(); - - let actual = rows - .iter() - .map(|row| (row.get(0), row.get(1))) - .collect::>(); - - let expected = vec![ - (s_plaintext_one, s_enctrypted_one), - (s_plaintext_two, s_encrypted_two), - (s_plaintext_three, s_encrypted_three), - ]; - - assert_eq!(actual, expected); + ore_order_helpers::ore_order_plaintext_and_eql_columns(&client).await; } #[tokio::test] + #[serial] async fn map_ore_order_simple_protocol() { trace(); - clear().await; - let client = connect_with_tls(PROXY).await; - - let sql = format!( - "INSERT INTO encrypted (id, encrypted_text) VALUES ({}, 'y'), ({}, 'x'), ({}, 'z')", - random_id(), - random_id(), - random_id() - ); - - client.simple_query(&sql).await.unwrap(); - - let sql = "SELECT encrypted_text FROM encrypted ORDER BY encrypted_text"; - let rows = client.simple_query(sql).await.unwrap(); - - let actual = rows - .iter() - .filter_map(|row| { - if let SimpleQueryMessage::Row(row) = row { - row.get(0) - } else { - None - } - }) - .collect::>(); - - let expected = vec!["x", "y", "z"]; - - assert_eq!(actual, expected); + ore_order_helpers::ore_order_simple_protocol(&client).await; } #[tokio::test] + #[serial] async fn map_ore_order_int2() { trace(); - clear().await; - let client = connect_with_tls(PROXY).await; - - let n_one = 10i16; - let n_two = 20i16; - let n_three = 30i16; - - let sql = " - INSERT INTO encrypted (id, encrypted_int2) - VALUES ($1, $2), ($3, $4), ($5, $6) - "; - - client - .query( - sql, - &[ - &random_id(), - &n_two, - &random_id(), - &n_one, - &random_id(), - &n_three, - ], - ) - .await - .unwrap(); - - let sql = "SELECT encrypted_int2 FROM encrypted ORDER BY encrypted_int2"; - let rows = client.query(sql, &[]).await.unwrap(); - - let actual = rows.iter().map(|row| row.get(0)).collect::>(); - let expected = vec![n_one, n_two, n_three]; - - assert_eq!(actual, expected); + let values: Vec = vec![-100, -10, -1, 0, 1, 5, 10, 20, 100, 200]; + ore_order_helpers::ore_order_generic(&client, "encrypted_int2", values, SortDirection::Asc) + .await; } #[tokio::test] + #[serial] async fn map_ore_order_int2_desc() { trace(); - clear().await; - let client = connect_with_tls(PROXY).await; - - let n_one = 10i16; - let n_two = 20i16; - let n_three = 30i16; - - let sql = " - INSERT INTO encrypted (id, encrypted_int2) - VALUES ($1, $2), ($3, $4), ($5, $6) - "; - - client - .query( - sql, - &[ - &random_id(), - &n_two, - &random_id(), - &n_one, - &random_id(), - &n_three, - ], - ) - .await - .unwrap(); - - let sql = "SELECT encrypted_int2 FROM encrypted ORDER BY encrypted_int2 DESC"; - let rows = client.query(sql, &[]).await.unwrap(); - - let actual = rows.iter().map(|row| row.get(0)).collect::>(); - let expected = vec![n_three, n_two, n_one]; - - assert_eq!(actual, expected); + let values: Vec = vec![-100, -10, -1, 0, 1, 5, 10, 20, 100, 200]; + ore_order_helpers::ore_order_generic( + &client, + "encrypted_int2", + values, + SortDirection::Desc, + ) + .await; } #[tokio::test] + #[serial] async fn map_ore_order_int4() { trace(); - clear().await; - let client = connect_with_tls(PROXY).await; + let values: Vec = vec![ + -50_000, -1_000, -1, 0, 1, 42, 1_000, 10_000, 50_000, 100_000, + ]; + ore_order_helpers::ore_order_generic(&client, "encrypted_int4", values, SortDirection::Asc) + .await; + } - let n_one = 10i32; - let n_two = 20i32; - let n_three = 30i32; - - let sql = " - INSERT INTO encrypted (id, encrypted_int4) - VALUES ($1, $2), ($3, $4), ($5, $6) - "; - - client - .query( - sql, - &[ - &random_id(), - &n_two, - &random_id(), - &n_one, - &random_id(), - &n_three, - ], - ) - .await - .unwrap(); - - let sql = "SELECT encrypted_int4 FROM encrypted ORDER BY encrypted_int4"; - let rows = client.query(sql, &[]).await.unwrap(); - - let actual = rows.iter().map(|row| row.get(0)).collect::>(); - let expected = vec![n_one, n_two, n_three]; - - assert_eq!(actual, expected); + #[tokio::test] + #[serial] + async fn map_ore_order_int4_desc() { + trace(); + clear().await; + let client = connect_with_tls(PROXY).await; + let values: Vec = vec![ + -50_000, -1_000, -1, 0, 1, 42, 1_000, 10_000, 50_000, 100_000, + ]; + ore_order_helpers::ore_order_generic( + &client, + "encrypted_int4", + values, + SortDirection::Desc, + ) + .await; } #[tokio::test] + #[serial] async fn map_ore_order_int8() { trace(); - clear().await; - let client = connect_with_tls(PROXY).await; + let values: Vec = vec![ + -1_000_000, -10_000, -1, 0, 1, 42, 10_000, 100_000, 1_000_000, 9_999_999, + ]; + ore_order_helpers::ore_order_generic(&client, "encrypted_int8", values, SortDirection::Asc) + .await; + } - let n_one = 10i64; - let n_two = 20i64; - let n_three = 30i64; - - let sql = " - INSERT INTO encrypted (id, encrypted_int8) - VALUES ($1, $2), ($3, $4), ($5, $6) - "; - - client - .query( - sql, - &[ - &random_id(), - &n_two, - &random_id(), - &n_one, - &random_id(), - &n_three, - ], - ) - .await - .unwrap(); - - let sql = "SELECT encrypted_int8 FROM encrypted ORDER BY encrypted_int8"; - let rows = client.query(sql, &[]).await.unwrap(); + #[tokio::test] + #[serial] + async fn map_ore_order_int8_desc() { + trace(); + clear().await; + let client = connect_with_tls(PROXY).await; + let values: Vec = vec![ + -1_000_000, -10_000, -1, 0, 1, 42, 10_000, 100_000, 1_000_000, 9_999_999, + ]; + ore_order_helpers::ore_order_generic( + &client, + "encrypted_int8", + values, + SortDirection::Desc, + ) + .await; + } - let actual = rows.iter().map(|row| row.get(0)).collect::>(); - let expected = vec![n_one, n_two, n_three]; + #[tokio::test] + #[serial] + async fn map_ore_order_float8() { + trace(); + clear().await; + let client = connect_with_tls(PROXY).await; + let values: Vec = vec![ + -99.9, -1.5, -0.001, 0.0, 0.001, 1.5, 3.25, 42.0, 99.9, 1000.5, + ]; + ore_order_helpers::ore_order_generic( + &client, + "encrypted_float8", + values, + SortDirection::Asc, + ) + .await; + } - assert_eq!(actual, expected); + #[tokio::test] + #[serial] + async fn map_ore_order_float8_desc() { + trace(); + clear().await; + let client = connect_with_tls(PROXY).await; + let values: Vec = vec![ + -99.9, -1.5, -0.001, 0.0, 0.001, 1.5, 3.25, 42.0, 99.9, 1000.5, + ]; + ore_order_helpers::ore_order_generic( + &client, + "encrypted_float8", + values, + SortDirection::Desc, + ) + .await; } } diff --git a/packages/cipherstash-proxy-integration/src/multitenant/mod.rs b/packages/cipherstash-proxy-integration/src/multitenant/mod.rs index 8eb25ddf..d77190c0 100644 --- a/packages/cipherstash-proxy-integration/src/multitenant/mod.rs +++ b/packages/cipherstash-proxy-integration/src/multitenant/mod.rs @@ -1,3 +1,4 @@ mod contention; +mod ore_order; mod set_keyset_id; mod set_keyset_name; diff --git a/packages/cipherstash-proxy-integration/src/multitenant/ore_order.rs b/packages/cipherstash-proxy-integration/src/multitenant/ore_order.rs new file mode 100644 index 00000000..4e71d8ee --- /dev/null +++ b/packages/cipherstash-proxy-integration/src/multitenant/ore_order.rs @@ -0,0 +1,275 @@ +/// Multitenant ORE ordering tests. +/// +/// Verifies that ORDER BY works correctly on encrypted columns for each tenant keyset. +/// The default keyset (`CS_DEFAULT_KEYSET_ID`) is already covered by `map_ore_index_order.rs` +/// and is unset during multitenant test execution. +/// +/// Uses a macro to generate all 18 ORE ordering tests for each of 3 tenant keysets (54 total). +#[cfg(test)] +mod tests { + use crate::common::{clear, connect_with_tls, trace, PROXY}; + use crate::ore_order_helpers; + use crate::ore_order_helpers::SortDirection; + + /// Connect to the proxy and set the tenant keyset. + /// + /// Validates `keyset_id` as a UUID before issuing the SET command. + async fn connect_as_tenant(keyset_id: &str) -> tokio_postgres::Client { + uuid::Uuid::parse_str(keyset_id) + .unwrap_or_else(|_| panic!("invalid UUID for keyset_id: {keyset_id}")); + let client = connect_with_tls(PROXY).await; + let sql = format!("SET CIPHERSTASH.KEYSET_ID = '{keyset_id}'"); + client.execute(&sql, &[]).await.unwrap(); + client + } + + /// Read a keyset ID from the environment, panicking with a descriptive message. + fn keyset_id(env_var: &str) -> String { + std::env::var(env_var) + .unwrap_or_else(|_| panic!("{env_var} must be set for multitenant ORE tests")) + } + + /// Generates a submodule with all 18 ORE ordering tests for a given tenant keyset. + macro_rules! ore_order_tests_for_tenant { + ($mod_name:ident, $env_var:expr) => { + mod $mod_name { + use super::*; + use serial_test::serial; + + #[tokio::test] + #[serial] + async fn multitenant_ore_order_text() { + trace(); + clear().await; + let client = connect_as_tenant(&keyset_id($env_var)).await; + ore_order_helpers::ore_order_text(&client).await; + } + + #[tokio::test] + #[serial] + async fn multitenant_ore_order_text_desc() { + trace(); + clear().await; + let client = connect_as_tenant(&keyset_id($env_var)).await; + ore_order_helpers::ore_order_text_desc(&client).await; + } + + #[tokio::test] + #[serial] + async fn multitenant_ore_order_nulls_last_by_default() { + trace(); + clear().await; + let client = connect_as_tenant(&keyset_id($env_var)).await; + ore_order_helpers::ore_order_nulls_last_by_default(&client).await; + } + + #[tokio::test] + #[serial] + async fn multitenant_ore_order_nulls_first() { + trace(); + clear().await; + let client = connect_as_tenant(&keyset_id($env_var)).await; + ore_order_helpers::ore_order_nulls_first(&client).await; + } + + #[tokio::test] + #[serial] + async fn multitenant_ore_order_qualified_column() { + trace(); + clear().await; + let client = connect_as_tenant(&keyset_id($env_var)).await; + ore_order_helpers::ore_order_qualified_column(&client).await; + } + + #[tokio::test] + #[serial] + async fn multitenant_ore_order_qualified_column_with_alias() { + trace(); + clear().await; + let client = connect_as_tenant(&keyset_id($env_var)).await; + ore_order_helpers::ore_order_qualified_column_with_alias(&client).await; + } + + #[tokio::test] + #[serial] + async fn multitenant_ore_order_no_eql_column_in_select_projection() { + trace(); + clear().await; + let client = connect_as_tenant(&keyset_id($env_var)).await; + ore_order_helpers::ore_order_no_eql_column_in_select_projection(&client).await; + } + + #[tokio::test] + #[serial] + async fn multitenant_can_order_by_plaintext_column() { + trace(); + clear().await; + let client = connect_as_tenant(&keyset_id($env_var)).await; + ore_order_helpers::ore_order_plaintext_column(&client).await; + } + + #[tokio::test] + #[serial] + async fn multitenant_can_order_by_plaintext_and_eql_columns() { + trace(); + clear().await; + let client = connect_as_tenant(&keyset_id($env_var)).await; + ore_order_helpers::ore_order_plaintext_and_eql_columns(&client).await; + } + + #[tokio::test] + #[serial] + async fn multitenant_ore_order_simple_protocol() { + trace(); + clear().await; + let client = connect_as_tenant(&keyset_id($env_var)).await; + ore_order_helpers::ore_order_simple_protocol(&client).await; + } + + #[tokio::test] + #[serial] + async fn multitenant_ore_order_int2() { + trace(); + clear().await; + let client = connect_as_tenant(&keyset_id($env_var)).await; + let values: Vec = vec![-100, -10, -1, 0, 1, 5, 10, 20, 100, 200]; + ore_order_helpers::ore_order_generic( + &client, + "encrypted_int2", + values, + SortDirection::Asc, + ) + .await; + } + + #[tokio::test] + #[serial] + async fn multitenant_ore_order_int2_desc() { + trace(); + clear().await; + let client = connect_as_tenant(&keyset_id($env_var)).await; + let values: Vec = vec![-100, -10, -1, 0, 1, 5, 10, 20, 100, 200]; + ore_order_helpers::ore_order_generic( + &client, + "encrypted_int2", + values, + SortDirection::Desc, + ) + .await; + } + + #[tokio::test] + #[serial] + async fn multitenant_ore_order_int4() { + trace(); + clear().await; + let client = connect_as_tenant(&keyset_id($env_var)).await; + let values: Vec = vec![ + -50_000, -1_000, -1, 0, 1, 42, 1_000, 10_000, 50_000, 100_000, + ]; + ore_order_helpers::ore_order_generic( + &client, + "encrypted_int4", + values, + SortDirection::Asc, + ) + .await; + } + + #[tokio::test] + #[serial] + async fn multitenant_ore_order_int4_desc() { + trace(); + clear().await; + let client = connect_as_tenant(&keyset_id($env_var)).await; + let values: Vec = vec![ + -50_000, -1_000, -1, 0, 1, 42, 1_000, 10_000, 50_000, 100_000, + ]; + ore_order_helpers::ore_order_generic( + &client, + "encrypted_int4", + values, + SortDirection::Desc, + ) + .await; + } + + #[tokio::test] + #[serial] + async fn multitenant_ore_order_int8() { + trace(); + clear().await; + let client = connect_as_tenant(&keyset_id($env_var)).await; + let values: Vec = vec![ + -1_000_000, -10_000, -1, 0, 1, 42, 10_000, 100_000, 1_000_000, 9_999_999, + ]; + ore_order_helpers::ore_order_generic( + &client, + "encrypted_int8", + values, + SortDirection::Asc, + ) + .await; + } + + #[tokio::test] + #[serial] + async fn multitenant_ore_order_int8_desc() { + trace(); + clear().await; + let client = connect_as_tenant(&keyset_id($env_var)).await; + let values: Vec = vec![ + -1_000_000, -10_000, -1, 0, 1, 42, 10_000, 100_000, 1_000_000, 9_999_999, + ]; + ore_order_helpers::ore_order_generic( + &client, + "encrypted_int8", + values, + SortDirection::Desc, + ) + .await; + } + + #[tokio::test] + #[serial] + async fn multitenant_ore_order_float8() { + trace(); + clear().await; + let client = connect_as_tenant(&keyset_id($env_var)).await; + let values: Vec = vec![ + -99.9, -1.5, -0.001, 0.0, 0.001, 1.5, 3.25, 42.0, 99.9, 1000.5, + ]; + ore_order_helpers::ore_order_generic( + &client, + "encrypted_float8", + values, + SortDirection::Asc, + ) + .await; + } + + #[tokio::test] + #[serial] + async fn multitenant_ore_order_float8_desc() { + trace(); + clear().await; + let client = connect_as_tenant(&keyset_id($env_var)).await; + let values: Vec = vec![ + -99.9, -1.5, -0.001, 0.0, 0.001, 1.5, 3.25, 42.0, 99.9, 1000.5, + ]; + ore_order_helpers::ore_order_generic( + &client, + "encrypted_float8", + values, + SortDirection::Desc, + ) + .await; + } + } + }; + } + + ore_order_tests_for_tenant!(tenant1, "CS_TENANT_KEYSET_ID_1"); + ore_order_tests_for_tenant!(tenant2, "CS_TENANT_KEYSET_ID_2"); + ore_order_tests_for_tenant!(tenant3, "CS_TENANT_KEYSET_ID_3"); +} diff --git a/packages/cipherstash-proxy-integration/src/ore_order_helpers.rs b/packages/cipherstash-proxy-integration/src/ore_order_helpers.rs new file mode 100644 index 00000000..0fa80fbd --- /dev/null +++ b/packages/cipherstash-proxy-integration/src/ore_order_helpers.rs @@ -0,0 +1,414 @@ +#![allow(dead_code)] +//! Shared ORE ordering test helpers. +//! +//! Used by both `map_ore_index_order` (default keyset) and `multitenant::ore_order` +//! (per-tenant keysets) to avoid duplicating test logic. + +use std::fmt::Debug; +use tokio_postgres::types::{FromSql, ToSql}; +use tokio_postgres::SimpleQueryMessage; + +use crate::common::{interleaved_indices, random_id}; + +/// Sort direction for ORE ordering tests. +#[derive(Clone, Copy)] +pub enum SortDirection { + Asc, + Desc, +} + +impl SortDirection { + pub fn as_sql(&self) -> &'static str { + match self { + SortDirection::Asc => "ASC", + SortDirection::Desc => "DESC", + } + } +} + +/// Text ASC ordering. +pub async fn ore_order_text(client: &tokio_postgres::Client) { + let s_one = "a"; + let s_two = "b"; + let s_three = "c"; + + let sql = " + INSERT INTO encrypted (id, encrypted_text) + VALUES ($1, $2), ($3, $4), ($5, $6) + "; + + client + .query( + sql, + &[ + &random_id(), + &s_two, + &random_id(), + &s_one, + &random_id(), + &s_three, + ], + ) + .await + .unwrap(); + + let sql = "SELECT encrypted_text FROM encrypted ORDER BY encrypted_text"; + let rows = client.query(sql, &[]).await.unwrap(); + + let actual = rows.iter().map(|row| row.get(0)).collect::>(); + let expected = vec![s_one, s_two, s_three]; + + assert_eq!(actual, expected); +} + +/// Text DESC ordering. +pub async fn ore_order_text_desc(client: &tokio_postgres::Client) { + let s_one = "a"; + let s_two = "b"; + let s_three = "c"; + + let sql = " + INSERT INTO encrypted (id, encrypted_text) + VALUES ($1, $2), ($3, $4), ($5, $6) + "; + + client + .query( + sql, + &[ + &random_id(), + &s_two, + &random_id(), + &s_one, + &random_id(), + &s_three, + ], + ) + .await + .unwrap(); + + let sql = "SELECT encrypted_text FROM encrypted ORDER BY encrypted_text DESC"; + let rows = client.query(sql, &[]).await.unwrap(); + + let actual = rows.iter().map(|row| row.get(0)).collect::>(); + let expected = vec![s_three, s_two, s_one]; + + assert_eq!(actual, expected); +} + +/// NULLs sort last in ASC by default. +pub async fn ore_order_nulls_last_by_default(client: &tokio_postgres::Client) { + let s_one = "a"; + let s_two = "b"; + + client + .query("INSERT INTO encrypted (id) values ($1)", &[&random_id()]) + .await + .unwrap(); + + let sql = " + INSERT INTO encrypted (id, encrypted_text) + VALUES ($1, $2), ($3, $4) + "; + + client + .query(sql, &[&random_id(), &s_one, &random_id(), &s_two]) + .await + .unwrap(); + + let sql = "SELECT encrypted_text FROM encrypted ORDER BY encrypted_text"; + let rows = client.query(sql, &[]).await.unwrap(); + + let actual = rows + .iter() + .map(|row| row.get(0)) + .collect::>>(); + let expected = vec![Some(s_one.to_string()), Some(s_two.to_string()), None]; + + assert_eq!(actual, expected); +} + +/// NULLS FIRST clause. +pub async fn ore_order_nulls_first(client: &tokio_postgres::Client) { + let s_one = "a"; + let s_two = "b"; + + let sql = " + INSERT INTO encrypted (id, encrypted_text) + VALUES ($1, $2), ($3, $4) + "; + + client + .query(sql, &[&random_id(), &s_one, &random_id(), &s_two]) + .await + .unwrap(); + + client + .query("INSERT INTO encrypted (id) values ($1)", &[&random_id()]) + .await + .unwrap(); + + let sql = "SELECT encrypted_text FROM encrypted ORDER BY encrypted_text NULLS FIRST"; + let rows = client.query(sql, &[]).await.unwrap(); + + let actual = rows + .iter() + .map(|row| row.get(0)) + .collect::>>(); + let expected = vec![None, Some(s_one.to_string()), Some(s_two.to_string())]; + + assert_eq!(actual, expected); +} + +/// Fully qualified column name: `encrypted.encrypted_text`. +pub async fn ore_order_qualified_column(client: &tokio_postgres::Client) { + let s_one = "a"; + let s_two = "b"; + let s_three = "c"; + + let sql = " + INSERT INTO encrypted (id, encrypted_text) + VALUES ($1, $2), ($3, $4), ($5, $6) + "; + + client + .query( + sql, + &[ + &random_id(), + &s_two, + &random_id(), + &s_one, + &random_id(), + &s_three, + ], + ) + .await + .unwrap(); + + let sql = "SELECT encrypted_text FROM encrypted ORDER BY encrypted.encrypted_text"; + let rows = client.query(sql, &[]).await.unwrap(); + + let actual = rows.iter().map(|row| row.get(0)).collect::>(); + let expected = vec![s_one, s_two, s_three]; + + assert_eq!(actual, expected); +} + +/// Table alias: `e.encrypted_text`. +pub async fn ore_order_qualified_column_with_alias(client: &tokio_postgres::Client) { + let s_one = "a"; + let s_two = "b"; + let s_three = "c"; + + let sql = " + INSERT INTO encrypted (id, encrypted_text) + VALUES ($1, $2), ($3, $4), ($5, $6) + "; + + client + .query( + sql, + &[ + &random_id(), + &s_two, + &random_id(), + &s_one, + &random_id(), + &s_three, + ], + ) + .await + .unwrap(); + + let sql = "SELECT encrypted_text FROM encrypted e ORDER BY e.encrypted_text"; + let rows = client.query(sql, &[]).await.unwrap(); + + let actual = rows.iter().map(|row| row.get(0)).collect::>(); + let expected = vec![s_one, s_two, s_three]; + + assert_eq!(actual, expected); +} + +/// ORDER BY column not in SELECT projection. +pub async fn ore_order_no_eql_column_in_select_projection(client: &tokio_postgres::Client) { + let id_one = random_id(); + let s_one = "a"; + let id_two = random_id(); + let s_two = "b"; + let id_three = random_id(); + let s_three = "c"; + + let sql = " + INSERT INTO encrypted (id, encrypted_text) + VALUES ($1, $2), ($3, $4), ($5, $6) + "; + + client + .query( + sql, + &[&id_two, &s_two, &id_one, &s_one, &id_three, &s_three], + ) + .await + .unwrap(); + + let sql = "SELECT id FROM encrypted ORDER BY encrypted_text"; + let rows = client.query(sql, &[]).await.unwrap(); + + let actual = rows.iter().map(|row| row.get(0)).collect::>(); + let expected = vec![id_one, id_two, id_three]; + + assert_eq!(actual, expected); +} + +/// Plaintext column ordering (sanity check). +pub async fn ore_order_plaintext_column(client: &tokio_postgres::Client) { + let s_one = "a"; + let s_two = "b"; + let s_three = "c"; + + let sql = " + INSERT INTO encrypted (id, plaintext) + VALUES ($1, $2), ($3, $4), ($5, $6) + "; + + client + .query( + sql, + &[ + &random_id(), + &s_two, + &random_id(), + &s_one, + &random_id(), + &s_three, + ], + ) + .await + .unwrap(); + + let sql = "SELECT plaintext FROM encrypted ORDER BY plaintext"; + let rows = client.query(sql, &[]).await.unwrap(); + + let actual = rows.iter().map(|row| row.get(0)).collect::>(); + let expected = vec![s_one, s_two, s_three]; + + assert_eq!(actual, expected); +} + +/// Mixed plaintext + encrypted column ordering. +pub async fn ore_order_plaintext_and_eql_columns(client: &tokio_postgres::Client) { + let s_plaintext_one = "a"; + let s_plaintext_two = "a"; + let s_plaintext_three = "b"; + + let s_encrypted_one = "a"; + let s_encrypted_two = "b"; + let s_encrypted_three = "c"; + + let sql = " + INSERT INTO encrypted (id, plaintext, encrypted_text) + VALUES ($1, $2, $3), ($4, $5, $6), ($7, $8, $9) + "; + + client + .query( + sql, + &[ + &random_id(), + &s_plaintext_two, + &s_encrypted_two, + &random_id(), + &s_plaintext_one, + &s_encrypted_one, + &random_id(), + &s_plaintext_three, + &s_encrypted_three, + ], + ) + .await + .unwrap(); + + let sql = "SELECT plaintext, encrypted_text FROM encrypted ORDER BY plaintext, encrypted_text"; + let rows = client.query(sql, &[]).await.unwrap(); + + let actual = rows + .iter() + .map(|row| (row.get(0), row.get(1))) + .collect::>(); + + let expected = vec![ + (s_plaintext_one, s_encrypted_one), + (s_plaintext_two, s_encrypted_two), + (s_plaintext_three, s_encrypted_three), + ]; + + assert_eq!(actual, expected); +} + +/// Simple query protocol ordering. +pub async fn ore_order_simple_protocol(client: &tokio_postgres::Client) { + let sql = format!( + "INSERT INTO encrypted (id, encrypted_text) VALUES ({}, 'y'), ({}, 'x'), ({}, 'z')", + random_id(), + random_id(), + random_id() + ); + + client.simple_query(&sql).await.unwrap(); + + let sql = "SELECT encrypted_text FROM encrypted ORDER BY encrypted_text"; + let rows = client.simple_query(sql).await.unwrap(); + + let actual = rows + .iter() + .filter_map(|row| { + if let SimpleQueryMessage::Row(row) = row { + row.get(0) + } else { + None + } + }) + .collect::>(); + + let expected = vec!["x", "y", "z"]; + + assert_eq!(actual, expected); +} + +/// Generic ORE ordering test for numeric types. +/// +/// `values` must be provided in ascending sorted order. +/// Values are inserted in interleaved (non-sorted) order, then verified +/// via ORDER BY in the given direction. +pub async fn ore_order_generic( + client: &tokio_postgres::Client, + col_name: &str, + values: Vec, + direction: SortDirection, +) where + for<'a> T: Clone + PartialEq + ToSql + Sync + FromSql<'a> + PartialOrd + Debug, +{ + let insert_sql = format!("INSERT INTO encrypted (id, {col_name}) VALUES ($1, $2)"); + + for idx in interleaved_indices(values.len()) { + client + .query(&insert_sql, &[&random_id(), &values[idx]]) + .await + .unwrap(); + } + + let dir = direction.as_sql(); + let select_sql = format!("SELECT {col_name} FROM encrypted ORDER BY {col_name} {dir}"); + let rows = client.query(&select_sql, &[]).await.unwrap(); + + let actual: Vec = rows.iter().map(|row| row.get(0)).collect(); + + let expected: Vec = if matches!(direction, SortDirection::Desc) { + values.into_iter().rev().collect() + } else { + values + }; + + assert_eq!(actual, expected); +}