From 920f991cfd376aa7b61baef961fffabf1ef480c7 Mon Sep 17 00:00:00 2001 From: Toby Hede Date: Wed, 4 Mar 2026 14:56:03 +1100 Subject: [PATCH 1/6] test(integration): expand numeric ORE ordering tests with edge cases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace duplicated numeric order tests with a generic helper that inserts values in interleaved (zigzag) order and verifies ORDER BY. Increases per-test coverage from 3 to 10 values including negatives, zero, and boundary values. Adds missing int4 DESC, int8 DESC, and float8 ASC/DESC tests (16 → 20 total tests). --- .../src/map_ore_index_order.rs | 211 +++++++----------- 1 file changed, 79 insertions(+), 132 deletions(-) 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..cc2c42b4 100644 --- a/packages/cipherstash-proxy-integration/src/map_ore_index_order.rs +++ b/packages/cipherstash-proxy-integration/src/map_ore_index_order.rs @@ -1,5 +1,7 @@ #[cfg(test)] mod tests { + use std::fmt::Debug; + use tokio_postgres::types::{FromSql, ToSql}; use tokio_postgres::SimpleQueryMessage; use crate::common::{clear, connect_with_tls, random_id, trace, PROXY}; @@ -417,164 +419,109 @@ mod tests { #[tokio::test] 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]; + map_ore_order_generic("encrypted_int2", values, "ASC").await; } #[tokio::test] 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]; + map_ore_order_generic("encrypted_int2", values, "DESC").await; } #[tokio::test] 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]; + map_ore_order_generic("encrypted_int4", values, "ASC").await; + } - let n_one = 10i32; - let n_two = 20i32; - let n_three = 30i32; + #[tokio::test] + async fn map_ore_order_int4_desc() { + let values: Vec = vec![-50_000, -1_000, -1, 0, 1, 42, 1_000, 10_000, 50_000, 100_000]; + map_ore_order_generic("encrypted_int4", values, "DESC").await; + } - let sql = " - INSERT INTO encrypted (id, encrypted_int4) - VALUES ($1, $2), ($3, $4), ($5, $6) - "; + #[tokio::test] + async fn map_ore_order_int8() { + let values: Vec = vec![-1_000_000, -10_000, -1, 0, 1, 42, 10_000, 100_000, 1_000_000, 9_999_999]; + map_ore_order_generic("encrypted_int8", values, "ASC").await; + } - client - .query( - sql, - &[ - &random_id(), - &n_two, - &random_id(), - &n_one, - &random_id(), - &n_three, - ], - ) - .await - .unwrap(); + #[tokio::test] + async fn map_ore_order_int8_desc() { + let values: Vec = vec![-1_000_000, -10_000, -1, 0, 1, 42, 10_000, 100_000, 1_000_000, 9_999_999]; + map_ore_order_generic("encrypted_int8", values, "DESC").await; + } - let sql = "SELECT encrypted_int4 FROM encrypted ORDER BY encrypted_int4"; - let rows = client.query(sql, &[]).await.unwrap(); + #[tokio::test] + async fn map_ore_order_float8() { + let values: Vec = vec![-99.9, -1.5, -0.001, 0.0, 0.001, 1.5, 3.14, 42.0, 99.9, 1000.5]; + map_ore_order_generic("encrypted_float8", values, "ASC").await; + } - let actual = rows.iter().map(|row| row.get(0)).collect::>(); - let expected = vec![n_one, n_two, n_three]; + #[tokio::test] + async fn map_ore_order_float8_desc() { + let values: Vec = vec![-99.9, -1.5, -0.001, 0.0, 0.001, 1.5, 3.14, 42.0, 99.9, 1000.5]; + map_ore_order_generic("encrypted_float8", values, "DESC").await; + } - assert_eq!(actual, expected); + /// Returns indices in zigzag order so insertion is never accidentally sorted. + /// For len=5: [4, 0, 3, 1, 2] + 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 } - #[tokio::test] - async fn map_ore_order_int8() { + /// Generic ORE ordering test. + /// + /// `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. + async fn map_ore_order_generic(col_name: &str, values: Vec, direction: &str) + where + for<'a> T: Clone + PartialEq + ToSql + Sync + FromSql<'a> + PartialOrd + Debug, + { trace(); clear().await; let client = connect_with_tls(PROXY).await; - let n_one = 10i64; - let n_two = 20i64; - let n_three = 30i64; + let insert_sql = format!("INSERT INTO encrypted (id, {col_name}) VALUES ($1, $2)"); - let sql = " - INSERT INTO encrypted (id, encrypted_int8) - VALUES ($1, $2), ($3, $4), ($5, $6) - "; + // Insert in interleaved order to avoid accidentally-sorted insertion + for idx in interleaved_indices(values.len()) { + client + .query(&insert_sql, &[&random_id(), &values[idx]]) + .await + .unwrap(); + } - client - .query( - sql, - &[ - &random_id(), - &n_two, - &random_id(), - &n_one, - &random_id(), - &n_three, - ], - ) - .await - .unwrap(); + let select_sql = format!( + "SELECT {col_name} FROM encrypted ORDER BY {col_name} {direction}" + ); + let rows = client.query(&select_sql, &[]).await.unwrap(); - let sql = "SELECT encrypted_int8 FROM encrypted ORDER BY encrypted_int8"; - let rows = client.query(sql, &[]).await.unwrap(); + let actual: Vec = rows.iter().map(|row| row.get(0)).collect(); - let actual = rows.iter().map(|row| row.get(0)).collect::>(); - let expected = vec![n_one, n_two, n_three]; + let expected: Vec = if direction == "DESC" { + values.into_iter().rev().collect() + } else { + values + }; assert_eq!(actual, expected); } From cddf2c730266c471863183fd4eb32a4fa56146c7 Mon Sep 17 00:00:00 2001 From: Toby Hede Date: Wed, 4 Mar 2026 15:01:54 +1100 Subject: [PATCH 2/6] style: apply cargo fmt to ORE ordering tests --- .../src/map_ore_index_order.rs | 29 +++++++++++++------ 1 file changed, 20 insertions(+), 9 deletions(-) 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 cc2c42b4..40051f75 100644 --- a/packages/cipherstash-proxy-integration/src/map_ore_index_order.rs +++ b/packages/cipherstash-proxy-integration/src/map_ore_index_order.rs @@ -431,37 +431,49 @@ mod tests { #[tokio::test] async fn map_ore_order_int4() { - let values: Vec = vec![-50_000, -1_000, -1, 0, 1, 42, 1_000, 10_000, 50_000, 100_000]; + let values: Vec = vec![ + -50_000, -1_000, -1, 0, 1, 42, 1_000, 10_000, 50_000, 100_000, + ]; map_ore_order_generic("encrypted_int4", values, "ASC").await; } #[tokio::test] async fn map_ore_order_int4_desc() { - let values: Vec = vec![-50_000, -1_000, -1, 0, 1, 42, 1_000, 10_000, 50_000, 100_000]; + let values: Vec = vec![ + -50_000, -1_000, -1, 0, 1, 42, 1_000, 10_000, 50_000, 100_000, + ]; map_ore_order_generic("encrypted_int4", values, "DESC").await; } #[tokio::test] async fn map_ore_order_int8() { - let values: Vec = vec![-1_000_000, -10_000, -1, 0, 1, 42, 10_000, 100_000, 1_000_000, 9_999_999]; + let values: Vec = vec![ + -1_000_000, -10_000, -1, 0, 1, 42, 10_000, 100_000, 1_000_000, 9_999_999, + ]; map_ore_order_generic("encrypted_int8", values, "ASC").await; } #[tokio::test] async fn map_ore_order_int8_desc() { - let values: Vec = vec![-1_000_000, -10_000, -1, 0, 1, 42, 10_000, 100_000, 1_000_000, 9_999_999]; + let values: Vec = vec![ + -1_000_000, -10_000, -1, 0, 1, 42, 10_000, 100_000, 1_000_000, 9_999_999, + ]; map_ore_order_generic("encrypted_int8", values, "DESC").await; } #[tokio::test] async fn map_ore_order_float8() { - let values: Vec = vec![-99.9, -1.5, -0.001, 0.0, 0.001, 1.5, 3.14, 42.0, 99.9, 1000.5]; + let values: Vec = vec![ + -99.9, -1.5, -0.001, 0.0, 0.001, 1.5, 3.14, 42.0, 99.9, 1000.5, + ]; map_ore_order_generic("encrypted_float8", values, "ASC").await; } #[tokio::test] async fn map_ore_order_float8_desc() { - let values: Vec = vec![-99.9, -1.5, -0.001, 0.0, 0.001, 1.5, 3.14, 42.0, 99.9, 1000.5]; + let values: Vec = vec![ + -99.9, -1.5, -0.001, 0.0, 0.001, 1.5, 3.14, 42.0, 99.9, 1000.5, + ]; map_ore_order_generic("encrypted_float8", values, "DESC").await; } @@ -510,9 +522,8 @@ mod tests { .unwrap(); } - let select_sql = format!( - "SELECT {col_name} FROM encrypted ORDER BY {col_name} {direction}" - ); + let select_sql = + format!("SELECT {col_name} FROM encrypted ORDER BY {col_name} {direction}"); let rows = client.query(&select_sql, &[]).await.unwrap(); let actual: Vec = rows.iter().map(|row| row.get(0)).collect(); From e19d40acf68636a8d1c08b9f3776e1e81c228ec8 Mon Sep 17 00:00:00 2001 From: Toby Hede Date: Wed, 4 Mar 2026 15:04:54 +1100 Subject: [PATCH 3/6] fix(integration): replace 3.14 with 3.25 to avoid clippy approx_constant lint --- .../cipherstash-proxy-integration/src/map_ore_index_order.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 40051f75..5f703b01 100644 --- a/packages/cipherstash-proxy-integration/src/map_ore_index_order.rs +++ b/packages/cipherstash-proxy-integration/src/map_ore_index_order.rs @@ -464,7 +464,7 @@ mod tests { #[tokio::test] async fn map_ore_order_float8() { let values: Vec = vec![ - -99.9, -1.5, -0.001, 0.0, 0.001, 1.5, 3.14, 42.0, 99.9, 1000.5, + -99.9, -1.5, -0.001, 0.0, 0.001, 1.5, 3.25, 42.0, 99.9, 1000.5, ]; map_ore_order_generic("encrypted_float8", values, "ASC").await; } @@ -472,7 +472,7 @@ mod tests { #[tokio::test] async fn map_ore_order_float8_desc() { let values: Vec = vec![ - -99.9, -1.5, -0.001, 0.0, 0.001, 1.5, 3.14, 42.0, 99.9, 1000.5, + -99.9, -1.5, -0.001, 0.0, 0.001, 1.5, 3.25, 42.0, 99.9, 1000.5, ]; map_ore_order_generic("encrypted_float8", values, "DESC").await; } From bdd5d63d8981b783de447a8a6d349d8366b3cee1 Mon Sep 17 00:00:00 2001 From: Toby Hede Date: Wed, 4 Mar 2026 16:28:45 +1100 Subject: [PATCH 4/6] test(integration): add multitenant ORE ordering tests for all tenant keysets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Generate 54 tests (18 per tenant × 3 keysets) covering text ASC/DESC, NULLs, qualified columns, aliases, projection, plaintext, mixed columns, simple protocol, and numeric types (int2/int4/int8/float8 ASC/DESC). Uses a macro to generate per-tenant submodules without extra dependencies. --- .../src/map_ore_index_order.rs | 6 +- .../src/multitenant/mod.rs | 1 + .../src/multitenant/ore_order.rs | 614 ++++++++++++++++++ 3 files changed, 618 insertions(+), 3 deletions(-) create mode 100644 packages/cipherstash-proxy-integration/src/multitenant/ore_order.rs 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 5f703b01..370dac79 100644 --- a/packages/cipherstash-proxy-integration/src/map_ore_index_order.rs +++ b/packages/cipherstash-proxy-integration/src/map_ore_index_order.rs @@ -336,7 +336,7 @@ mod tests { let s_plaintext_two = "a"; let s_plaintext_three = "b"; - let s_enctrypted_one = "a"; + let s_encrypted_one = "a"; let s_encrypted_two = "b"; let s_encrypted_three = "c"; @@ -354,7 +354,7 @@ mod tests { &s_encrypted_two, &random_id(), &s_plaintext_one, - &s_enctrypted_one, + &s_encrypted_one, &random_id(), &s_plaintext_three, &s_encrypted_three, @@ -373,7 +373,7 @@ mod tests { .collect::>(); let expected = vec![ - (s_plaintext_one, s_enctrypted_one), + (s_plaintext_one, s_encrypted_one), (s_plaintext_two, s_encrypted_two), (s_plaintext_three, s_encrypted_three), ]; 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..2cdab73e --- /dev/null +++ b/packages/cipherstash-proxy-integration/src/multitenant/ore_order.rs @@ -0,0 +1,614 @@ +/// 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 std::fmt::Debug; + use tokio_postgres::types::{FromSql, ToSql}; + use tokio_postgres::SimpleQueryMessage; + + use crate::common::{clear, connect_with_tls, random_id, trace, PROXY}; + + /// Connect to the proxy and set the tenant keyset. + async fn connect_as_tenant(keyset_id: &str) -> tokio_postgres::Client { + 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")) + } + + /// Returns indices in zigzag order so insertion is never accidentally sorted. + /// For len=5: [4, 0, 3, 1, 2] + 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 + } + + /// Text ASC ordering test for a tenant connection. + 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 test for a tenant connection. + 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. + 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. + 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`. + 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`. + 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. + 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 with tenant keyset active). + 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. + 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_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); + } + + /// Simple query protocol ordering. + 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. + async fn ore_order_generic( + client: &tokio_postgres::Client, + col_name: &str, + values: Vec, + direction: &str, + ) 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 select_sql = + format!("SELECT {col_name} FROM encrypted ORDER BY {col_name} {direction}"); + let rows = client.query(&select_sql, &[]).await.unwrap(); + + let actual: Vec = rows.iter().map(|row| row.get(0)).collect(); + + let expected: Vec = if direction == "DESC" { + values.into_iter().rev().collect() + } else { + values + }; + + assert_eq!(actual, expected); + } + + /// 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::*; + + #[tokio::test] + async fn multitenant_ore_order_text() { + trace(); + clear().await; + let client = connect_as_tenant(&keyset_id($env_var)).await; + ore_order_text(&client).await; + } + + #[tokio::test] + async fn multitenant_ore_order_text_desc() { + trace(); + clear().await; + let client = connect_as_tenant(&keyset_id($env_var)).await; + ore_order_text_desc(&client).await; + } + + #[tokio::test] + async fn multitenant_ore_order_nulls_last_by_default() { + trace(); + clear().await; + let client = connect_as_tenant(&keyset_id($env_var)).await; + ore_order_nulls_last_by_default(&client).await; + } + + #[tokio::test] + async fn multitenant_ore_order_nulls_first() { + trace(); + clear().await; + let client = connect_as_tenant(&keyset_id($env_var)).await; + ore_order_nulls_first(&client).await; + } + + #[tokio::test] + async fn multitenant_ore_order_qualified_column() { + trace(); + clear().await; + let client = connect_as_tenant(&keyset_id($env_var)).await; + ore_order_qualified_column(&client).await; + } + + #[tokio::test] + async fn multitenant_ore_order_qualified_column_with_alias() { + trace(); + clear().await; + let client = connect_as_tenant(&keyset_id($env_var)).await; + ore_order_qualified_column_with_alias(&client).await; + } + + #[tokio::test] + 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_no_eql_column_in_select_projection(&client).await; + } + + #[tokio::test] + async fn multitenant_can_order_by_plaintext_column() { + trace(); + clear().await; + let client = connect_as_tenant(&keyset_id($env_var)).await; + ore_order_plaintext_column(&client).await; + } + + #[tokio::test] + 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_plaintext_and_eql_columns(&client).await; + } + + #[tokio::test] + async fn multitenant_ore_order_simple_protocol() { + trace(); + clear().await; + let client = connect_as_tenant(&keyset_id($env_var)).await; + ore_order_simple_protocol(&client).await; + } + + #[tokio::test] + 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_generic(&client, "encrypted_int2", values, "ASC").await; + } + + #[tokio::test] + 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_generic(&client, "encrypted_int2", values, "DESC").await; + } + + #[tokio::test] + 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_generic(&client, "encrypted_int4", values, "ASC").await; + } + + #[tokio::test] + 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_generic(&client, "encrypted_int4", values, "DESC").await; + } + + #[tokio::test] + 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_generic(&client, "encrypted_int8", values, "ASC").await; + } + + #[tokio::test] + 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_generic(&client, "encrypted_int8", values, "DESC").await; + } + + #[tokio::test] + 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_generic(&client, "encrypted_float8", values, "ASC").await; + } + + #[tokio::test] + 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_generic(&client, "encrypted_float8", values, "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"); +} From 76d611d85f350dd2d6145fef168556cf1ffd8203 Mon Sep 17 00:00:00 2001 From: Toby Hede Date: Wed, 4 Mar 2026 17:03:07 +1100 Subject: [PATCH 5/6] refactor(integration): deduplicate ORE ordering test helpers Extract shared test logic from map_ore_index_order.rs and multitenant/ore_order.rs into ore_order_helpers.rs to eliminate code duplication. Move interleaved_indices into crate::common. Add UUID validation to connect_as_tenant to prevent SQL injection. --- .../src/common.rs | 20 + .../cipherstash-proxy-integration/src/lib.rs | 1 + .../src/map_ore_index_order.rs | 458 ++--------------- .../src/multitenant/ore_order.rs | 473 ++---------------- .../src/ore_order_helpers.rs | 397 +++++++++++++++ 5 files changed, 504 insertions(+), 845 deletions(-) create mode 100644 packages/cipherstash-proxy-integration/src/ore_order_helpers.rs 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 370dac79..6f2456bd 100644 --- a/packages/cipherstash-proxy-integration/src/map_ore_index_order.rs +++ b/packages/cipherstash-proxy-integration/src/map_ore_index_order.rs @@ -1,539 +1,169 @@ #[cfg(test)] mod tests { - use std::fmt::Debug; - use tokio_postgres::types::{FromSql, ToSql}; - 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; #[tokio::test] 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] 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] 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] 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] 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] 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] 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] 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] 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_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); + ore_order_helpers::ore_order_plaintext_and_eql_columns(&client).await; } #[tokio::test] 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] async fn map_ore_order_int2() { + trace(); + clear().await; + let client = connect_with_tls(PROXY).await; let values: Vec = vec![-100, -10, -1, 0, 1, 5, 10, 20, 100, 200]; - map_ore_order_generic("encrypted_int2", values, "ASC").await; + ore_order_helpers::ore_order_generic(&client, "encrypted_int2", values, "ASC").await; } #[tokio::test] async fn map_ore_order_int2_desc() { + trace(); + clear().await; + let client = connect_with_tls(PROXY).await; let values: Vec = vec![-100, -10, -1, 0, 1, 5, 10, 20, 100, 200]; - map_ore_order_generic("encrypted_int2", values, "DESC").await; + ore_order_helpers::ore_order_generic(&client, "encrypted_int2", values, "DESC").await; } #[tokio::test] 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, ]; - map_ore_order_generic("encrypted_int4", values, "ASC").await; + ore_order_helpers::ore_order_generic(&client, "encrypted_int4", values, "ASC").await; } #[tokio::test] 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, ]; - map_ore_order_generic("encrypted_int4", values, "DESC").await; + ore_order_helpers::ore_order_generic(&client, "encrypted_int4", values, "DESC").await; } #[tokio::test] 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, ]; - map_ore_order_generic("encrypted_int8", values, "ASC").await; + ore_order_helpers::ore_order_generic(&client, "encrypted_int8", values, "ASC").await; } #[tokio::test] 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, ]; - map_ore_order_generic("encrypted_int8", values, "DESC").await; + ore_order_helpers::ore_order_generic(&client, "encrypted_int8", values, "DESC").await; } #[tokio::test] 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, ]; - map_ore_order_generic("encrypted_float8", values, "ASC").await; + ore_order_helpers::ore_order_generic(&client, "encrypted_float8", values, "ASC").await; } #[tokio::test] async fn map_ore_order_float8_desc() { - let values: Vec = vec![ - -99.9, -1.5, -0.001, 0.0, 0.001, 1.5, 3.25, 42.0, 99.9, 1000.5, - ]; - map_ore_order_generic("encrypted_float8", values, "DESC").await; - } - - /// Returns indices in zigzag order so insertion is never accidentally sorted. - /// For len=5: [4, 0, 3, 1, 2] - 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 - } - - /// Generic ORE ordering test. - /// - /// `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. - async fn map_ore_order_generic(col_name: &str, values: Vec, direction: &str) - where - for<'a> T: Clone + PartialEq + ToSql + Sync + FromSql<'a> + PartialOrd + Debug, - { trace(); - clear().await; - let client = connect_with_tls(PROXY).await; - - let insert_sql = format!("INSERT INTO encrypted (id, {col_name}) VALUES ($1, $2)"); - - // Insert in interleaved order to avoid accidentally-sorted insertion - for idx in interleaved_indices(values.len()) { - client - .query(&insert_sql, &[&random_id(), &values[idx]]) - .await - .unwrap(); - } - - let select_sql = - format!("SELECT {col_name} FROM encrypted ORDER BY {col_name} {direction}"); - let rows = client.query(&select_sql, &[]).await.unwrap(); - - let actual: Vec = rows.iter().map(|row| row.get(0)).collect(); - - let expected: Vec = if direction == "DESC" { - values.into_iter().rev().collect() - } else { - values - }; - - assert_eq!(actual, expected); + 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, "DESC").await; } } diff --git a/packages/cipherstash-proxy-integration/src/multitenant/ore_order.rs b/packages/cipherstash-proxy-integration/src/multitenant/ore_order.rs index 2cdab73e..c79436fd 100644 --- a/packages/cipherstash-proxy-integration/src/multitenant/ore_order.rs +++ b/packages/cipherstash-proxy-integration/src/multitenant/ore_order.rs @@ -7,14 +7,15 @@ /// Uses a macro to generate all 18 ORE ordering tests for each of 3 tenant keysets (54 total). #[cfg(test)] mod tests { - use std::fmt::Debug; - use tokio_postgres::types::{FromSql, ToSql}; - 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; /// 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(); @@ -27,414 +28,6 @@ mod tests { .unwrap_or_else(|_| panic!("{env_var} must be set for multitenant ORE tests")) } - /// Returns indices in zigzag order so insertion is never accidentally sorted. - /// For len=5: [4, 0, 3, 1, 2] - 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 - } - - /// Text ASC ordering test for a tenant connection. - 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 test for a tenant connection. - 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. - 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. - 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`. - 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`. - 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. - 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 with tenant keyset active). - 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. - 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_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); - } - - /// Simple query protocol ordering. - 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. - async fn ore_order_generic( - client: &tokio_postgres::Client, - col_name: &str, - values: Vec, - direction: &str, - ) 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 select_sql = - format!("SELECT {col_name} FROM encrypted ORDER BY {col_name} {direction}"); - let rows = client.query(&select_sql, &[]).await.unwrap(); - - let actual: Vec = rows.iter().map(|row| row.get(0)).collect(); - - let expected: Vec = if direction == "DESC" { - values.into_iter().rev().collect() - } else { - values - }; - - assert_eq!(actual, expected); - } - /// 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) => { @@ -446,7 +39,7 @@ mod tests { trace(); clear().await; let client = connect_as_tenant(&keyset_id($env_var)).await; - ore_order_text(&client).await; + ore_order_helpers::ore_order_text(&client).await; } #[tokio::test] @@ -454,7 +47,7 @@ mod tests { trace(); clear().await; let client = connect_as_tenant(&keyset_id($env_var)).await; - ore_order_text_desc(&client).await; + ore_order_helpers::ore_order_text_desc(&client).await; } #[tokio::test] @@ -462,7 +55,7 @@ mod tests { trace(); clear().await; let client = connect_as_tenant(&keyset_id($env_var)).await; - ore_order_nulls_last_by_default(&client).await; + ore_order_helpers::ore_order_nulls_last_by_default(&client).await; } #[tokio::test] @@ -470,7 +63,7 @@ mod tests { trace(); clear().await; let client = connect_as_tenant(&keyset_id($env_var)).await; - ore_order_nulls_first(&client).await; + ore_order_helpers::ore_order_nulls_first(&client).await; } #[tokio::test] @@ -478,7 +71,7 @@ mod tests { trace(); clear().await; let client = connect_as_tenant(&keyset_id($env_var)).await; - ore_order_qualified_column(&client).await; + ore_order_helpers::ore_order_qualified_column(&client).await; } #[tokio::test] @@ -486,7 +79,7 @@ mod tests { trace(); clear().await; let client = connect_as_tenant(&keyset_id($env_var)).await; - ore_order_qualified_column_with_alias(&client).await; + ore_order_helpers::ore_order_qualified_column_with_alias(&client).await; } #[tokio::test] @@ -494,7 +87,7 @@ mod tests { trace(); clear().await; let client = connect_as_tenant(&keyset_id($env_var)).await; - ore_order_no_eql_column_in_select_projection(&client).await; + ore_order_helpers::ore_order_no_eql_column_in_select_projection(&client).await; } #[tokio::test] @@ -502,7 +95,7 @@ mod tests { trace(); clear().await; let client = connect_as_tenant(&keyset_id($env_var)).await; - ore_order_plaintext_column(&client).await; + ore_order_helpers::ore_order_plaintext_column(&client).await; } #[tokio::test] @@ -510,7 +103,7 @@ mod tests { trace(); clear().await; let client = connect_as_tenant(&keyset_id($env_var)).await; - ore_order_plaintext_and_eql_columns(&client).await; + ore_order_helpers::ore_order_plaintext_and_eql_columns(&client).await; } #[tokio::test] @@ -518,7 +111,7 @@ mod tests { trace(); clear().await; let client = connect_as_tenant(&keyset_id($env_var)).await; - ore_order_simple_protocol(&client).await; + ore_order_helpers::ore_order_simple_protocol(&client).await; } #[tokio::test] @@ -527,7 +120,8 @@ mod tests { 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_generic(&client, "encrypted_int2", values, "ASC").await; + ore_order_helpers::ore_order_generic(&client, "encrypted_int2", values, "ASC") + .await; } #[tokio::test] @@ -536,7 +130,8 @@ mod tests { 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_generic(&client, "encrypted_int2", values, "DESC").await; + ore_order_helpers::ore_order_generic(&client, "encrypted_int2", values, "DESC") + .await; } #[tokio::test] @@ -547,7 +142,8 @@ mod tests { let values: Vec = vec![ -50_000, -1_000, -1, 0, 1, 42, 1_000, 10_000, 50_000, 100_000, ]; - ore_order_generic(&client, "encrypted_int4", values, "ASC").await; + ore_order_helpers::ore_order_generic(&client, "encrypted_int4", values, "ASC") + .await; } #[tokio::test] @@ -558,7 +154,8 @@ mod tests { let values: Vec = vec![ -50_000, -1_000, -1, 0, 1, 42, 1_000, 10_000, 50_000, 100_000, ]; - ore_order_generic(&client, "encrypted_int4", values, "DESC").await; + ore_order_helpers::ore_order_generic(&client, "encrypted_int4", values, "DESC") + .await; } #[tokio::test] @@ -569,7 +166,8 @@ mod tests { 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_generic(&client, "encrypted_int8", values, "ASC").await; + ore_order_helpers::ore_order_generic(&client, "encrypted_int8", values, "ASC") + .await; } #[tokio::test] @@ -580,7 +178,8 @@ mod tests { 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_generic(&client, "encrypted_int8", values, "DESC").await; + ore_order_helpers::ore_order_generic(&client, "encrypted_int8", values, "DESC") + .await; } #[tokio::test] @@ -591,7 +190,13 @@ mod tests { 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_generic(&client, "encrypted_float8", values, "ASC").await; + ore_order_helpers::ore_order_generic( + &client, + "encrypted_float8", + values, + "ASC", + ) + .await; } #[tokio::test] @@ -602,7 +207,13 @@ mod tests { 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_generic(&client, "encrypted_float8", values, "DESC").await; + ore_order_helpers::ore_order_generic( + &client, + "encrypted_float8", + values, + "DESC", + ) + .await; } } }; 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..c9a05bde --- /dev/null +++ b/packages/cipherstash-proxy-integration/src/ore_order_helpers.rs @@ -0,0 +1,397 @@ +#![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}; + +/// 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: &str, +) 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 select_sql = format!("SELECT {col_name} FROM encrypted ORDER BY {col_name} {direction}"); + let rows = client.query(&select_sql, &[]).await.unwrap(); + + let actual: Vec = rows.iter().map(|row| row.get(0)).collect(); + + let expected: Vec = if direction == "DESC" { + values.into_iter().rev().collect() + } else { + values + }; + + assert_eq!(actual, expected); +} From f207300148700b2da273b96fa594ebc320639d77 Mon Sep 17 00:00:00 2001 From: Toby Hede Date: Thu, 5 Mar 2026 10:17:45 +1100 Subject: [PATCH 6/6] fix(integration): serialize ORE ordering tests and add SortDirection enum Add serial_test dependency and #[serial] attribute to all ORE ordering tests to prevent race conditions from concurrent clear() calls on shared tables. Replace direction string literals with a type-safe SortDirection enum in ore_order_generic. --- Cargo.lock | 42 ++++++++++ .../cipherstash-proxy-integration/Cargo.toml | 1 + .../src/map_ore_index_order.rs | 69 ++++++++++++++-- .../src/multitenant/ore_order.rs | 78 +++++++++++++++---- .../src/ore_order_helpers.rs | 23 +++++- 5 files changed, 188 insertions(+), 25 deletions(-) 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/map_ore_index_order.rs b/packages/cipherstash-proxy-integration/src/map_ore_index_order.rs index 6f2456bd..08118b65 100644 --- a/packages/cipherstash-proxy-integration/src/map_ore_index_order.rs +++ b/packages/cipherstash-proxy-integration/src/map_ore_index_order.rs @@ -2,8 +2,11 @@ mod tests { 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; @@ -12,6 +15,7 @@ mod tests { } #[tokio::test] + #[serial] async fn map_ore_order_text_desc() { trace(); clear().await; @@ -20,6 +24,7 @@ mod tests { } #[tokio::test] + #[serial] async fn map_ore_order_nulls_last_by_default() { trace(); clear().await; @@ -28,6 +33,7 @@ mod tests { } #[tokio::test] + #[serial] async fn map_ore_order_nulls_first() { trace(); clear().await; @@ -36,6 +42,7 @@ mod tests { } #[tokio::test] + #[serial] async fn map_ore_order_qualified_column() { trace(); clear().await; @@ -44,6 +51,7 @@ mod tests { } #[tokio::test] + #[serial] async fn map_ore_order_qualified_column_with_alias() { trace(); clear().await; @@ -52,6 +60,7 @@ mod tests { } #[tokio::test] + #[serial] async fn map_ore_order_no_eql_column_in_select_projection() { trace(); clear().await; @@ -60,6 +69,7 @@ mod tests { } #[tokio::test] + #[serial] async fn can_order_by_plaintext_column() { trace(); clear().await; @@ -68,6 +78,7 @@ mod tests { } #[tokio::test] + #[serial] async fn can_order_by_plaintext_and_eql_columns() { trace(); clear().await; @@ -76,6 +87,7 @@ mod tests { } #[tokio::test] + #[serial] async fn map_ore_order_simple_protocol() { trace(); clear().await; @@ -84,24 +96,34 @@ mod tests { } #[tokio::test] + #[serial] async fn map_ore_order_int2() { trace(); clear().await; let client = connect_with_tls(PROXY).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, "ASC").await; + 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 values: Vec = vec![-100, -10, -1, 0, 1, 5, 10, 20, 100, 200]; - ore_order_helpers::ore_order_generic(&client, "encrypted_int2", values, "DESC").await; + 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; @@ -109,10 +131,12 @@ mod tests { 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, "ASC").await; + ore_order_helpers::ore_order_generic(&client, "encrypted_int4", values, SortDirection::Asc) + .await; } #[tokio::test] + #[serial] async fn map_ore_order_int4_desc() { trace(); clear().await; @@ -120,10 +144,17 @@ mod tests { 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, "DESC").await; + 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; @@ -131,10 +162,12 @@ mod tests { 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, "ASC").await; + ore_order_helpers::ore_order_generic(&client, "encrypted_int8", values, SortDirection::Asc) + .await; } #[tokio::test] + #[serial] async fn map_ore_order_int8_desc() { trace(); clear().await; @@ -142,10 +175,17 @@ mod tests { 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, "DESC").await; + ore_order_helpers::ore_order_generic( + &client, + "encrypted_int8", + values, + SortDirection::Desc, + ) + .await; } #[tokio::test] + #[serial] async fn map_ore_order_float8() { trace(); clear().await; @@ -153,10 +193,17 @@ mod tests { 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, "ASC").await; + ore_order_helpers::ore_order_generic( + &client, + "encrypted_float8", + values, + SortDirection::Asc, + ) + .await; } #[tokio::test] + #[serial] async fn map_ore_order_float8_desc() { trace(); clear().await; @@ -164,6 +211,12 @@ mod tests { 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, "DESC").await; + ore_order_helpers::ore_order_generic( + &client, + "encrypted_float8", + values, + SortDirection::Desc, + ) + .await; } } diff --git a/packages/cipherstash-proxy-integration/src/multitenant/ore_order.rs b/packages/cipherstash-proxy-integration/src/multitenant/ore_order.rs index c79436fd..4e71d8ee 100644 --- a/packages/cipherstash-proxy-integration/src/multitenant/ore_order.rs +++ b/packages/cipherstash-proxy-integration/src/multitenant/ore_order.rs @@ -9,6 +9,7 @@ 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. /// @@ -33,8 +34,10 @@ mod tests { ($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; @@ -43,6 +46,7 @@ mod tests { } #[tokio::test] + #[serial] async fn multitenant_ore_order_text_desc() { trace(); clear().await; @@ -51,6 +55,7 @@ mod tests { } #[tokio::test] + #[serial] async fn multitenant_ore_order_nulls_last_by_default() { trace(); clear().await; @@ -59,6 +64,7 @@ mod tests { } #[tokio::test] + #[serial] async fn multitenant_ore_order_nulls_first() { trace(); clear().await; @@ -67,6 +73,7 @@ mod tests { } #[tokio::test] + #[serial] async fn multitenant_ore_order_qualified_column() { trace(); clear().await; @@ -75,6 +82,7 @@ mod tests { } #[tokio::test] + #[serial] async fn multitenant_ore_order_qualified_column_with_alias() { trace(); clear().await; @@ -83,6 +91,7 @@ mod tests { } #[tokio::test] + #[serial] async fn multitenant_ore_order_no_eql_column_in_select_projection() { trace(); clear().await; @@ -91,6 +100,7 @@ mod tests { } #[tokio::test] + #[serial] async fn multitenant_can_order_by_plaintext_column() { trace(); clear().await; @@ -99,6 +109,7 @@ mod tests { } #[tokio::test] + #[serial] async fn multitenant_can_order_by_plaintext_and_eql_columns() { trace(); clear().await; @@ -107,6 +118,7 @@ mod tests { } #[tokio::test] + #[serial] async fn multitenant_ore_order_simple_protocol() { trace(); clear().await; @@ -115,26 +127,39 @@ mod tests { } #[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, "ASC") - .await; + 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, "DESC") - .await; + 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; @@ -142,11 +167,17 @@ mod tests { 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, "ASC") - .await; + 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; @@ -154,11 +185,17 @@ mod tests { 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, "DESC") - .await; + 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; @@ -166,11 +203,17 @@ mod tests { 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, "ASC") - .await; + 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; @@ -178,11 +221,17 @@ mod tests { 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, "DESC") - .await; + 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; @@ -194,12 +243,13 @@ mod tests { &client, "encrypted_float8", values, - "ASC", + SortDirection::Asc, ) .await; } #[tokio::test] + #[serial] async fn multitenant_ore_order_float8_desc() { trace(); clear().await; @@ -211,7 +261,7 @@ mod tests { &client, "encrypted_float8", values, - "DESC", + SortDirection::Desc, ) .await; } diff --git a/packages/cipherstash-proxy-integration/src/ore_order_helpers.rs b/packages/cipherstash-proxy-integration/src/ore_order_helpers.rs index c9a05bde..0fa80fbd 100644 --- a/packages/cipherstash-proxy-integration/src/ore_order_helpers.rs +++ b/packages/cipherstash-proxy-integration/src/ore_order_helpers.rs @@ -10,6 +10,22 @@ 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"; @@ -369,7 +385,7 @@ pub async fn ore_order_generic( client: &tokio_postgres::Client, col_name: &str, values: Vec, - direction: &str, + direction: SortDirection, ) where for<'a> T: Clone + PartialEq + ToSql + Sync + FromSql<'a> + PartialOrd + Debug, { @@ -382,12 +398,13 @@ pub async fn ore_order_generic( .unwrap(); } - let select_sql = format!("SELECT {col_name} FROM encrypted ORDER BY {col_name} {direction}"); + 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 direction == "DESC" { + let expected: Vec = if matches!(direction, SortDirection::Desc) { values.into_iter().rev().collect() } else { values