diff --git a/tasks/test-pgtap.sh b/tasks/test-pgtap.sh new file mode 100755 index 0000000..3a5603d --- /dev/null +++ b/tasks/test-pgtap.sh @@ -0,0 +1,93 @@ +#!/usr/bin/env bash +#MISE description="Run pgTAP tests with pg_prove" +#USAGE flag "--postgres " help="Run tests for specified Postgres version" default="17" { +#USAGE choices "14" "15" "16" "17" +#USAGE } + +set -euo pipefail + +POSTGRES_VERSION=${usage_postgres} + +connection_url=postgresql://${POSTGRES_USER:-$USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB} +container_name=postgres-${POSTGRES_VERSION} + +fail_if_postgres_not_running () { + containers=$(docker ps --filter "name=^${container_name}$" --quiet) + if [ -z "${containers}" ]; then + echo "error: Docker container for PostgreSQL is not running" + echo "error: Try running 'mise run postgres:up ${container_name}' to start the container" + exit 65 + fi +} + +# setup +fail_if_postgres_not_running +mise run build --force +mise run reset --force --postgres ${POSTGRES_VERSION} + +echo +echo '###############################################' +echo '# Installing release/cipherstash-encrypt.sql' +echo '###############################################' +echo + +# Install EQL +cat release/cipherstash-encrypt.sql | docker exec -i ${container_name} psql ${connection_url} -f- + +# Install test helpers +cat tests/test_helpers.sql | docker exec -i ${container_name} psql ${connection_url} -f- +cat tests/ore.sql | docker exec -i ${container_name} psql ${connection_url} -f- +cat tests/ste_vec.sql | docker exec -i ${container_name} psql ${connection_url} -f- + +echo +echo '###############################################' +echo '# Installing pgTAP' +echo '###############################################' +echo + +# Install pgTAP +cat tests/install_pgtap.sql | docker exec -i ${container_name} psql ${connection_url} -f- + +echo +echo '###############################################' +echo '# Running pgTAP structure tests' +echo '###############################################' +echo + +# Run structure tests with pg_prove +if [ -d "tests/pgtap/structure" ]; then + docker exec -i ${container_name} pg_prove -v -d ${connection_url} /tests/pgtap/structure/*.sql 2>/dev/null || { + # Fallback: copy tests to container and run + for test_file in tests/pgtap/structure/*.sql; do + if [ -f "$test_file" ]; then + echo "Running: $test_file" + cat "$test_file" | docker exec -i ${container_name} psql ${connection_url} -f- + fi + done + } +fi + +echo +echo '###############################################' +echo '# Running pgTAP functionality tests' +echo '###############################################' +echo + +# Run functionality tests with pg_prove +if [ -d "tests/pgtap/functionality" ]; then + docker exec -i ${container_name} pg_prove -v -d ${connection_url} /tests/pgtap/functionality/*.sql 2>/dev/null || { + # Fallback: copy tests to container and run + for test_file in tests/pgtap/functionality/*.sql; do + if [ -f "$test_file" ]; then + echo "Running: $test_file" + cat "$test_file" | docker exec -i ${container_name} psql ${connection_url} -f- + fi + done + } +fi + +echo +echo '###############################################' +echo "# ✅ ALL PGTAP TESTS PASSED " +echo '###############################################' +echo diff --git a/tests/Dockerfile.pgtap b/tests/Dockerfile.pgtap new file mode 100644 index 0000000..83c421e --- /dev/null +++ b/tests/Dockerfile.pgtap @@ -0,0 +1,16 @@ +ARG POSTGRES_VERSION=17 +FROM postgres:${POSTGRES_VERSION} + +# Install build dependencies and pgTAP +RUN apt-get update && apt-get install -y \ + build-essential \ + postgresql-server-dev-${PG_MAJOR} \ + git \ + && rm -rf /var/lib/apt/lists/* + +# Install pgTAP +RUN git clone https://github.com/theory/pgtap.git /tmp/pgtap \ + && cd /tmp/pgtap \ + && make \ + && make install \ + && rm -rf /tmp/pgtap diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml index 13c28d4..c817902 100644 --- a/tests/docker-compose.yml +++ b/tests/docker-compose.yml @@ -1,7 +1,11 @@ services: postgres: &postgres container_name: postgres - image: postgres + build: + context: . + dockerfile: Dockerfile.pgtap + args: + POSTGRES_VERSION: "17" ports: - 7432:7432 environment: @@ -26,24 +30,40 @@ services: postgres-17: <<: *postgres - image: postgres:17 + build: + context: . + dockerfile: Dockerfile.pgtap + args: + POSTGRES_VERSION: "17" container_name: postgres-17 #volumes: # uncomment if you need to inspect the container contents #- ./pg/data-17:/var/lib/postgresql/data postgres-16: <<: *postgres - image: postgres:16 + build: + context: . + dockerfile: Dockerfile.pgtap + args: + POSTGRES_VERSION: "16" container_name: postgres-16 postgres-15: <<: *postgres - image: postgres:15 + build: + context: . + dockerfile: Dockerfile.pgtap + args: + POSTGRES_VERSION: "15" container_name: postgres-15 postgres-14: <<: *postgres - image: postgres:14 + build: + context: . + dockerfile: Dockerfile.pgtap + args: + POSTGRES_VERSION: "14" container_name: postgres-14 networks: diff --git a/tests/install_pgtap.sql b/tests/install_pgtap.sql new file mode 100644 index 0000000..02e6b6e --- /dev/null +++ b/tests/install_pgtap.sql @@ -0,0 +1,5 @@ +-- Install pgTAP extension for testing +CREATE EXTENSION IF NOT EXISTS pgtap; + +-- Verify pgTAP installation +SELECT * FROM pg_available_extensions WHERE name = 'pgtap'; diff --git a/tests/pgtap/functionality/comparison_test.sql b/tests/pgtap/functionality/comparison_test.sql new file mode 100644 index 0000000..7b3056b --- /dev/null +++ b/tests/pgtap/functionality/comparison_test.sql @@ -0,0 +1,167 @@ +-- Test EQL comparison operators +-- Tests <, <=, >, >= operators for encrypted data with ORE indexes + +BEGIN; + +-- Plan: count of tests to run +SELECT plan(12); + +-- Setup test data +SELECT lives_ok( + 'SELECT create_table_with_encrypted()', + 'Should create table with encrypted column' +); + +SELECT lives_ok( + 'SELECT seed_encrypted_json()', + 'Should seed encrypted data' +); + +-- Test 1: Less than operator with ORE index +DO $$ +DECLARE + e eql_v2_encrypted; +BEGIN + e := create_encrypted_ore_json(42); + + PERFORM ok( + (SELECT count(*) FROM encrypted WHERE e < e) >= 1, + 'Less than operator < works with ORE encrypted data' + ); +END; +$$ LANGUAGE plpgsql; + +-- Test 2: Less than or equal operator with ORE index +DO $$ +DECLARE + e eql_v2_encrypted; +BEGIN + e := create_encrypted_ore_json(42); + + PERFORM ok( + (SELECT count(*) FROM encrypted WHERE e <= e) >= 1, + 'Less than or equal operator <= works with ORE encrypted data' + ); +END; +$$ LANGUAGE plpgsql; + +-- Test 3: Greater than operator with ORE index +DO $$ +DECLARE + e eql_v2_encrypted; +BEGIN + e := create_encrypted_ore_json(1); + + PERFORM ok( + (SELECT count(*) FROM encrypted WHERE e > e) >= 1, + 'Greater than operator > works with ORE encrypted data' + ); +END; +$$ LANGUAGE plpgsql; + +-- Test 4: Greater than or equal operator with ORE index +DO $$ +DECLARE + e eql_v2_encrypted; +BEGIN + e := create_encrypted_ore_json(1); + + PERFORM ok( + (SELECT count(*) FROM encrypted WHERE e >= e) >= 1, + 'Greater than or equal operator >= works with ORE encrypted data' + ); +END; +$$ LANGUAGE plpgsql; + +-- Test 5: Not equal operator +DO $$ +DECLARE + e eql_v2_encrypted; +BEGIN + e := create_encrypted_json(1, 'hm'); + + PERFORM ok( + (SELECT count(*) FROM encrypted WHERE e <> e) >= 1, + 'Not equal operator <> works with encrypted data' + ); +END; +$$ LANGUAGE plpgsql; + +-- Test 6: eql_v2.lt() function +DO $$ +DECLARE + e eql_v2_encrypted; +BEGIN + e := create_encrypted_ore_json(42); + + PERFORM ok( + (SELECT count(*) FROM encrypted WHERE eql_v2.lt(e, e)) >= 1, + 'eql_v2.lt() function works with ORE encrypted data' + ); +END; +$$ LANGUAGE plpgsql; + +-- Test 7: eql_v2.lte() function +DO $$ +DECLARE + e eql_v2_encrypted; +BEGIN + e := create_encrypted_ore_json(42); + + PERFORM ok( + (SELECT count(*) FROM encrypted WHERE eql_v2.lte(e, e)) >= 1, + 'eql_v2.lte() function works with ORE encrypted data' + ); +END; +$$ LANGUAGE plpgsql; + +-- Test 8: eql_v2.gt() function +DO $$ +DECLARE + e eql_v2_encrypted; +BEGIN + e := create_encrypted_ore_json(1); + + PERFORM ok( + (SELECT count(*) FROM encrypted WHERE eql_v2.gt(e, e)) >= 1, + 'eql_v2.gt() function works with ORE encrypted data' + ); +END; +$$ LANGUAGE plpgsql; + +-- Test 9: eql_v2.gte() function +DO $$ +DECLARE + e eql_v2_encrypted; +BEGIN + e := create_encrypted_ore_json(1); + + PERFORM ok( + (SELECT count(*) FROM encrypted WHERE eql_v2.gte(e, e)) >= 1, + 'eql_v2.gte() function works with ORE encrypted data' + ); +END; +$$ LANGUAGE plpgsql; + +-- Test 10: eql_v2.neq() function +DO $$ +DECLARE + e eql_v2_encrypted; +BEGIN + e := create_encrypted_json(1, 'hm'); + + PERFORM ok( + (SELECT count(*) FROM encrypted WHERE eql_v2.neq(e, e)) >= 1, + 'eql_v2.neq() function works with encrypted data' + ); +END; +$$ LANGUAGE plpgsql; + +-- Cleanup +SELECT lives_ok( + 'SELECT drop_table_with_encrypted()', + 'Should drop test table' +); + +SELECT finish(); +ROLLBACK; diff --git a/tests/pgtap/functionality/config_test.sql b/tests/pgtap/functionality/config_test.sql new file mode 100644 index 0000000..eede8c5 --- /dev/null +++ b/tests/pgtap/functionality/config_test.sql @@ -0,0 +1,146 @@ +-- Test EQL configuration management +-- Tests add_search_config, remove_search_config, modify_search_config functions + +BEGIN; + +-- Plan: count of tests to run +SELECT plan(12); + +-- Helper function for checking if search config exists +CREATE OR REPLACE FUNCTION _search_config_exists(table_name text, column_name text, index_name text, state text DEFAULT 'pending') +RETURNS boolean +LANGUAGE sql STRICT PARALLEL SAFE +BEGIN ATOMIC + SELECT EXISTS (SELECT id FROM eql_v2_configuration c + WHERE c.state = _search_config_exists.state AND + c.data #> array['tables', table_name, column_name, 'indexes'] ? index_name); +END; + +-- Clean configuration table +SELECT lives_ok( + 'TRUNCATE TABLE eql_v2_configuration', + 'Should truncate configuration table' +); + +-- Test 1: Add search config creates pending configuration +DO $$ +BEGIN + PERFORM eql_v2.add_search_config('users', 'name', 'match', migrating => true); + + PERFORM ok( + _search_config_exists('users', 'name', 'match'), + 'add_search_config creates pending configuration with match index' + ); +END; +$$ LANGUAGE plpgsql; + +-- Test 2: Add search config with cast +DO $$ +BEGIN + PERFORM eql_v2.add_search_config('users', 'age', 'unique', 'int', migrating => true); + + PERFORM ok( + _search_config_exists('users', 'age', 'unique'), + 'add_search_config creates configuration with cast' + ); + + PERFORM ok( + EXISTS (SELECT id FROM eql_v2_configuration c + WHERE c.state = 'pending' AND + c.data #> array['tables', 'users', 'age'] ? 'cast_as'), + 'Configuration includes cast_as field' + ); +END; +$$ LANGUAGE plpgsql; + +-- Test 3: Remove search config +DO $$ +BEGIN + PERFORM eql_v2.remove_search_config('users', 'name', 'match', migrating => true); + + PERFORM ok( + NOT _search_config_exists('users', 'name', 'match'), + 'remove_search_config removes match index' + ); +END; +$$ LANGUAGE plpgsql; + +-- Test 4: Configuration preserved after removing all indexes +DO $$ +BEGIN + PERFORM eql_v2.remove_search_config('users', 'age', 'unique', migrating => true); + + PERFORM ok( + EXISTS (SELECT FROM eql_v2_configuration c WHERE c.state = 'pending'), + 'Pending configuration still exists after removing all indexes' + ); + + PERFORM ok( + (SELECT data #> array['tables', 'users', 'age', 'indexes'] = '{}' + FROM eql_v2_configuration c WHERE c.state = 'pending'), + 'Indexes object is empty after removal' + ); +END; +$$ LANGUAGE plpgsql; + +-- Test 5: Modify search config +SELECT lives_ok( + 'TRUNCATE TABLE eql_v2_configuration', + 'Should truncate configuration table for modify test' +); + +DO $$ +BEGIN + PERFORM eql_v2.add_search_config('users', 'email', 'match', migrating => true); + PERFORM eql_v2.modify_search_config('users', 'email', 'match', 'text', '{"option": "value"}'::jsonb, migrating => true); + + PERFORM ok( + EXISTS (SELECT id FROM eql_v2_configuration c + WHERE c.state = 'pending' AND + c.data #> array['tables', 'users', 'email', 'indexes', 'match'] ? 'option'), + 'modify_search_config adds options to index configuration' + ); +END; +$$ LANGUAGE plpgsql; + +-- Test 6: Add column to existing table +SELECT lives_ok( + 'TRUNCATE TABLE eql_v2_configuration', + 'Should truncate configuration table for column test' +); + +DO $$ +BEGIN + PERFORM create_table_with_encrypted(); + + PERFORM eql_v2.add_column('encrypted', 'e', migrating => true); + + PERFORM cmp_ok( + (SELECT count(*) FROM eql_v2_configuration c WHERE c.state = 'pending'), + '=', + 1::bigint, + 'add_column creates pending configuration' + ); +END; +$$ LANGUAGE plpgsql; + +-- Test 7: Remove column +DO $$ +BEGIN + PERFORM eql_v2.remove_column('encrypted', 'e', migrating => true); + + PERFORM ok( + (SELECT data #> array['tables'] = '{}' + FROM eql_v2_configuration c WHERE c.state = 'pending'), + 'remove_column empties tables configuration' + ); + + PERFORM drop_table_with_encrypted(); +END; +$$ LANGUAGE plpgsql; + +-- Cleanup helper function +DROP FUNCTION _search_config_exists(text, text, text, text); + +SELECT finish(); +ROLLBACK; diff --git a/tests/pgtap/functionality/equality_test.sql b/tests/pgtap/functionality/equality_test.sql new file mode 100644 index 0000000..6d4ea54 --- /dev/null +++ b/tests/pgtap/functionality/equality_test.sql @@ -0,0 +1,189 @@ +-- Test EQL equality operators +-- Tests the = operator and eq() function for encrypted data + +BEGIN; + +-- Plan: count of tests to run +SELECT plan(13); + +-- Setup test data +SELECT lives_ok( + 'SELECT create_table_with_encrypted()', + 'Should create table with encrypted column' +); + +SELECT lives_ok( + 'SELECT seed_encrypted_json()', + 'Should seed encrypted data' +); + +-- Test 1: eql_v2_encrypted = eql_v2_encrypted with unique index term (HMAC) +DO $$ +DECLARE + e eql_v2_encrypted; +BEGIN + e := create_encrypted_json(1, 'hm'); + + PERFORM results_eq( + format('SELECT e FROM encrypted WHERE e = %L', e), + format('SELECT e FROM encrypted WHERE id = 1'), + 'eql_v2_encrypted = eql_v2_encrypted finds matching record with HMAC index' + ); +END; +$$ LANGUAGE plpgsql; + +-- Test 2: eql_v2_encrypted = eql_v2_encrypted with no match +DO $$ +DECLARE + e eql_v2_encrypted; +BEGIN + e := create_encrypted_json(91347, 'hm'); + + PERFORM is_empty( + format('SELECT e FROM encrypted WHERE e = %L', e), + 'eql_v2_encrypted = eql_v2_encrypted returns no result for non-matching record' + ); +END; +$$ LANGUAGE plpgsql; + +-- Test 3: eql_v2.eq() function test +DO $$ +DECLARE + e eql_v2_encrypted; +BEGIN + e := create_encrypted_json(1)::jsonb-'ob'; + + PERFORM results_eq( + format('SELECT e FROM encrypted WHERE eql_v2.eq(e, %L)', e), + format('SELECT e FROM encrypted WHERE id = 1'), + 'eql_v2.eq() finds matching record' + ); +END; +$$ LANGUAGE plpgsql; + +-- Test 4: eql_v2.eq() with no match +DO $$ +DECLARE + e eql_v2_encrypted; +BEGIN + e := create_encrypted_json(91347)::jsonb-'ob'; + + PERFORM is_empty( + format('SELECT e FROM encrypted WHERE eql_v2.eq(e, %L)', e), + 'eql_v2.eq() returns no result for non-matching record' + ); +END; +$$ LANGUAGE plpgsql; + +-- Test 5: eql_v2_encrypted = jsonb +DO $$ +DECLARE + e jsonb; +BEGIN + e := create_encrypted_json(1)::jsonb-'ob'; + + PERFORM results_eq( + format('SELECT e FROM encrypted WHERE e = %L::jsonb', e), + format('SELECT e FROM encrypted WHERE id = 1'), + 'eql_v2_encrypted = jsonb finds matching record' + ); +END; +$$ LANGUAGE plpgsql; + +-- Test 6: jsonb = eql_v2_encrypted +DO $$ +DECLARE + e jsonb; +BEGIN + e := create_encrypted_json(1)::jsonb-'ob'; + + PERFORM results_eq( + format('SELECT e FROM encrypted WHERE %L::jsonb = e', e), + format('SELECT e FROM encrypted WHERE id = 1'), + 'jsonb = eql_v2_encrypted finds matching record' + ); +END; +$$ LANGUAGE plpgsql; + +-- Test 7: Blake3 equality - eql_v2_encrypted = eql_v2_encrypted +DO $$ +DECLARE + e eql_v2_encrypted; +BEGIN + e := create_encrypted_json(1, 'b3'); + + PERFORM results_eq( + format('SELECT e FROM encrypted WHERE e = %L', e), + format('SELECT e FROM encrypted WHERE id = 1'), + 'Blake3: eql_v2_encrypted = eql_v2_encrypted finds matching record' + ); +END; +$$ LANGUAGE plpgsql; + +-- Test 8: Blake3 equality with no match +DO $$ +DECLARE + e eql_v2_encrypted; +BEGIN + e := create_encrypted_json(91347, 'b3'); + + PERFORM is_empty( + format('SELECT e FROM encrypted WHERE e = %L', e), + 'Blake3: eql_v2_encrypted = eql_v2_encrypted returns no result for non-matching record' + ); +END; +$$ LANGUAGE plpgsql; + +-- Test 9: Blake3 eql_v2.eq() function +DO $$ +DECLARE + e eql_v2_encrypted; +BEGIN + e := create_encrypted_json(1, 'b3'); + + PERFORM results_eq( + format('SELECT e FROM encrypted WHERE eql_v2.eq(e, %L)', e), + format('SELECT e FROM encrypted WHERE id = 1'), + 'Blake3: eql_v2.eq() finds matching record' + ); +END; +$$ LANGUAGE plpgsql; + +-- Test 10: Blake3 eql_v2_encrypted = jsonb +DO $$ +DECLARE + e jsonb; +BEGIN + e := create_encrypted_json(1, 'b3'); + + PERFORM results_eq( + format('SELECT e FROM encrypted WHERE e = %L::jsonb', e), + format('SELECT e FROM encrypted WHERE id = 1'), + 'Blake3: eql_v2_encrypted = jsonb finds matching record' + ); +END; +$$ LANGUAGE plpgsql; + +-- Test 11: Blake3 jsonb = eql_v2_encrypted +DO $$ +DECLARE + e jsonb; +BEGIN + e := create_encrypted_json(1, 'b3'); + + PERFORM results_eq( + format('SELECT e FROM encrypted WHERE %L::jsonb = e', e), + format('SELECT e FROM encrypted WHERE id = 1'), + 'Blake3: jsonb = eql_v2_encrypted finds matching record' + ); +END; +$$ LANGUAGE plpgsql; + +-- Cleanup +SELECT lives_ok( + 'SELECT drop_table_with_encrypted()', + 'Should drop test table' +); + +SELECT finish(); +ROLLBACK; diff --git a/tests/pgtap/functionality/index_terms_test.sql b/tests/pgtap/functionality/index_terms_test.sql new file mode 100644 index 0000000..e74c490 --- /dev/null +++ b/tests/pgtap/functionality/index_terms_test.sql @@ -0,0 +1,220 @@ +-- Test EQL index term comparison functions +-- Tests blake3, hmac_256, and ORE comparison functions + +BEGIN; + +-- Plan: count of tests to run +SELECT plan(27); + +-- Test 1-9: Blake3 comparison function +DO $$ +DECLARE + a eql_v2_encrypted; + b eql_v2_encrypted; + c eql_v2_encrypted; +BEGIN + a := create_encrypted_json(1, 'b3'); + b := create_encrypted_json(2, 'b3'); + c := create_encrypted_json(3, 'b3'); + + -- Test equality + PERFORM is( + eql_v2.compare_blake3(a, a), + 0, + 'Blake3: compare_blake3(a, a) = 0 (equal)' + ); + + PERFORM is( + eql_v2.compare_blake3(b, b), + 0, + 'Blake3: compare_blake3(b, b) = 0 (equal)' + ); + + PERFORM is( + eql_v2.compare_blake3(c, c), + 0, + 'Blake3: compare_blake3(c, c) = 0 (equal)' + ); + + -- Test less than + PERFORM is( + eql_v2.compare_blake3(a, b), + -1, + 'Blake3: compare_blake3(a, b) = -1 (a < b)' + ); + + PERFORM is( + eql_v2.compare_blake3(a, c), + -1, + 'Blake3: compare_blake3(a, c) = -1 (a < c)' + ); + + PERFORM is( + eql_v2.compare_blake3(b, c), + -1, + 'Blake3: compare_blake3(b, c) = -1 (b < c)' + ); + + -- Test greater than + PERFORM is( + eql_v2.compare_blake3(b, a), + 1, + 'Blake3: compare_blake3(b, a) = 1 (b > a)' + ); + + PERFORM is( + eql_v2.compare_blake3(c, a), + 1, + 'Blake3: compare_blake3(c, a) = 1 (c > a)' + ); + + PERFORM is( + eql_v2.compare_blake3(c, b), + 1, + 'Blake3: compare_blake3(c, b) = 1 (c > b)' + ); +END; +$$ LANGUAGE plpgsql; + +-- Test 10-18: HMAC-256 comparison function +DO $$ +DECLARE + a eql_v2_encrypted; + b eql_v2_encrypted; + c eql_v2_encrypted; +BEGIN + a := create_encrypted_json(1, 'hm'); + b := create_encrypted_json(2, 'hm'); + c := create_encrypted_json(3, 'hm'); + + -- Test equality + PERFORM is( + eql_v2.compare_hmac_256(a, a), + 0, + 'HMAC-256: compare_hmac_256(a, a) = 0 (equal)' + ); + + PERFORM is( + eql_v2.compare_hmac_256(b, b), + 0, + 'HMAC-256: compare_hmac_256(b, b) = 0 (equal)' + ); + + PERFORM is( + eql_v2.compare_hmac_256(c, c), + 0, + 'HMAC-256: compare_hmac_256(c, c) = 0 (equal)' + ); + + -- Test less than + PERFORM is( + eql_v2.compare_hmac_256(a, b), + -1, + 'HMAC-256: compare_hmac_256(a, b) = -1 (a < b)' + ); + + PERFORM is( + eql_v2.compare_hmac_256(a, c), + -1, + 'HMAC-256: compare_hmac_256(a, c) = -1 (a < c)' + ); + + PERFORM is( + eql_v2.compare_hmac_256(b, c), + -1, + 'HMAC-256: compare_hmac_256(b, c) = -1 (b < c)' + ); + + -- Test greater than + PERFORM is( + eql_v2.compare_hmac_256(b, a), + 1, + 'HMAC-256: compare_hmac_256(b, a) = 1 (b > a)' + ); + + PERFORM is( + eql_v2.compare_hmac_256(c, a), + 1, + 'HMAC-256: compare_hmac_256(c, a) = 1 (c > a)' + ); + + PERFORM is( + eql_v2.compare_hmac_256(c, b), + 1, + 'HMAC-256: compare_hmac_256(c, b) = 1 (c > b)' + ); +END; +$$ LANGUAGE plpgsql; + +-- Test 19-27: ORE comparison functions +DO $$ +DECLARE + a eql_v2_encrypted; + b eql_v2_encrypted; + c eql_v2_encrypted; +BEGIN + a := create_encrypted_ore_json(1); + b := create_encrypted_ore_json(2); + c := create_encrypted_ore_json(3); + + -- Test equality with compare function + PERFORM is( + eql_v2.compare(a, a), + 0, + 'ORE: compare(a, a) = 0 (equal)' + ); + + PERFORM is( + eql_v2.compare(b, b), + 0, + 'ORE: compare(b, b) = 0 (equal)' + ); + + PERFORM is( + eql_v2.compare(c, c), + 0, + 'ORE: compare(c, c) = 0 (equal)' + ); + + -- Test less than + PERFORM is( + eql_v2.compare(a, b), + -1, + 'ORE: compare(a, b) = -1 (a < b)' + ); + + PERFORM is( + eql_v2.compare(a, c), + -1, + 'ORE: compare(a, c) = -1 (a < c)' + ); + + PERFORM is( + eql_v2.compare(b, c), + -1, + 'ORE: compare(b, c) = -1 (b < c)' + ); + + -- Test greater than + PERFORM is( + eql_v2.compare(b, a), + 1, + 'ORE: compare(b, a) = 1 (b > a)' + ); + + PERFORM is( + eql_v2.compare(c, a), + 1, + 'ORE: compare(c, a) = 1 (c > a)' + ); + + PERFORM is( + eql_v2.compare(c, b), + 1, + 'ORE: compare(c, b) = 1 (c > b)' + ); +END; +$$ LANGUAGE plpgsql; + +SELECT finish(); +ROLLBACK; diff --git a/tests/pgtap/functionality/jsonb_operators_test.sql b/tests/pgtap/functionality/jsonb_operators_test.sql new file mode 100644 index 0000000..6cd293e --- /dev/null +++ b/tests/pgtap/functionality/jsonb_operators_test.sql @@ -0,0 +1,186 @@ +-- Test EQL JSONB operators +-- Tests ->, ->>, @>, <@ operators for encrypted data + +BEGIN; + +-- Plan: count of tests to run +SELECT plan(13); + +-- Setup test data +SELECT lives_ok( + 'SELECT create_table_with_encrypted()', + 'Should create table with encrypted column' +); + +SELECT lives_ok( + 'SELECT seed_encrypted_json()', + 'Should seed encrypted data' +); + +-- Test 1: -> operator extracts encrypted term by selector +DO $$ +BEGIN + PERFORM isnt_empty( + $$SELECT e->'bca213de9ccce676fa849ff9c4807963'::text FROM encrypted$$, + 'Selector -> operator returns encrypted terms' + ); +END; +$$ LANGUAGE plpgsql; + +-- Test 2: -> operator returns correct count +DO $$ +BEGIN + PERFORM cmp_ok( + (SELECT count(*) FROM encrypted WHERE e->'bca213de9ccce676fa849ff9c4807963'::text IS NOT NULL), + '>=', + 1::bigint, + 'Selector -> operator returns expected number of results' + ); +END; +$$ LANGUAGE plpgsql; + +-- Test 3: -> operator returns NULL for unknown selector +DO $$ +BEGIN + PERFORM is( + (SELECT e->'blahvtha'::text FROM encrypted LIMIT 1), + NULL, + 'Unknown selector -> operator returns NULL' + ); +END; +$$ LANGUAGE plpgsql; + +-- Test 4: -> operator accepts eql_v2_encrypted as selector +DO $$ +DECLARE + term text; +BEGIN + term := '{"s": "bca213de9ccce676fa849ff9c4807963"}'; + + PERFORM isnt_empty( + format('SELECT e->%L::jsonb::eql_v2_encrypted FROM encrypted', term), + 'Selector -> operator works with eql_v2_encrypted selector' + ); +END; +$$ LANGUAGE plpgsql; + +-- Test 5: ->> operator extracts text value +DO $$ +BEGIN + PERFORM isnt_empty( + $$SELECT e->>'bca213de9ccce676fa849ff9c4807963'::text FROM encrypted$$, + 'Text extraction operator ->> returns values' + ); +END; +$$ LANGUAGE plpgsql; + +-- Test 6: @> operator - eql_v2_encrypted contains itself +DO $$ +DECLARE + a eql_v2_encrypted; + b eql_v2_encrypted; +BEGIN + a := get_numeric_ste_vec_10()::eql_v2_encrypted; + b := get_numeric_ste_vec_10()::eql_v2_encrypted; + + PERFORM ok( + a @> b, + '@> operator: eql_v2_encrypted contains itself' + ); +END; +$$ LANGUAGE plpgsql; + +-- Test 7: @> operator - reverse containment +DO $$ +DECLARE + a eql_v2_encrypted; + b eql_v2_encrypted; +BEGIN + a := get_numeric_ste_vec_10()::eql_v2_encrypted; + b := get_numeric_ste_vec_10()::eql_v2_encrypted; + + PERFORM ok( + b @> a, + '@> operator: reverse containment works' + ); +END; +$$ LANGUAGE plpgsql; + +-- Test 8: @> operator - contains term +DO $$ +DECLARE + a eql_v2_encrypted; + b eql_v2_encrypted; + term eql_v2_encrypted; +BEGIN + a := get_numeric_ste_vec_10()::eql_v2_encrypted; + b := get_numeric_ste_vec_10()::eql_v2_encrypted; + + -- Extract term at $.n + term := b->'2517068c0d1f9d4d41d2c666211f785e'::text; + + PERFORM ok( + a @> term, + '@> operator: eql_v2_encrypted contains term' + ); +END; +$$ LANGUAGE plpgsql; + +-- Test 9: @> operator - term does not contain parent +DO $$ +DECLARE + a eql_v2_encrypted; + b eql_v2_encrypted; + term eql_v2_encrypted; +BEGIN + a := get_numeric_ste_vec_10()::eql_v2_encrypted; + b := get_numeric_ste_vec_10()::eql_v2_encrypted; + + -- Extract term at $.n + term := b->'2517068c0d1f9d4d41d2c666211f785e'::text; + + PERFORM ok( + NOT (term @> a), + '@> operator: term does not contain parent' + ); +END; +$$ LANGUAGE plpgsql; + +-- Test 10: <@ operator - contained by relationship +DO $$ +DECLARE + a eql_v2_encrypted; + b eql_v2_encrypted; + term eql_v2_encrypted; +BEGIN + a := get_numeric_ste_vec_10()::eql_v2_encrypted; + b := get_numeric_ste_vec_10()::eql_v2_encrypted; + + -- Extract term at $.n + term := b->'2517068c0d1f9d4d41d2c666211f785e'::text; + + PERFORM ok( + term <@ a, + '<@ operator: term is contained by parent' + ); +END; +$$ LANGUAGE plpgsql; + +-- Test 11: Extract ciphertext via selector +DO $$ +BEGIN + PERFORM isnt_empty( + $$SELECT eql_v2.ciphertext(e->'2517068c0d1f9d4d41d2c666211f785e'::text) FROM encrypted$$, + 'Can extract ciphertext via selector' + ); +END; +$$ LANGUAGE plpgsql; + +-- Cleanup +SELECT lives_ok( + 'SELECT drop_table_with_encrypted()', + 'Should drop test table' +); + +SELECT finish(); +ROLLBACK; diff --git a/tests/pgtap/structure/functions_test.sql b/tests/pgtap/structure/functions_test.sql new file mode 100644 index 0000000..9a1dc2e --- /dev/null +++ b/tests/pgtap/structure/functions_test.sql @@ -0,0 +1,80 @@ +-- Test EQL function structure +-- Verifies that key EQL functions exist with correct signatures + +BEGIN; + +-- Plan: count of tests to run +SELECT plan(9); + +-- Test comparison functions +SELECT has_function( + 'eql_v2', + 'compare_blake3', + ARRAY['eql_v2_encrypted', 'eql_v2_encrypted'], + 'compare_blake3 function should exist with correct signature' +); + +SELECT function_returns( + 'eql_v2', + 'compare_blake3', + ARRAY['eql_v2_encrypted', 'eql_v2_encrypted'], + 'integer', + 'compare_blake3 should return integer' +); + +-- Test configuration management functions +SELECT has_function( + 'eql_v2', + 'diff_config', + ARRAY['jsonb', 'jsonb'], + 'diff_config function should exist' +); + +SELECT has_function( + 'eql_v2', + 'select_pending_columns', + ARRAY[]::text[], + 'select_pending_columns function should exist' +); + +SELECT has_function( + 'eql_v2', + 'select_target_columns', + ARRAY[]::text[], + 'select_target_columns function should exist' +); + +SELECT has_function( + 'eql_v2', + 'ready_for_encryption', + ARRAY[]::text[], + 'ready_for_encryption function should exist' +); + +SELECT function_returns( + 'eql_v2', + 'ready_for_encryption', + ARRAY[]::text[], + 'boolean', + 'ready_for_encryption should return boolean' +); + +-- Test table management functions +SELECT has_function( + 'eql_v2', + 'create_encrypted_columns', + ARRAY[]::text[], + 'create_encrypted_columns function should exist' +); + +-- Verify eql_v2 schema has functions +SELECT isnt_empty( + $$SELECT p.proname + FROM pg_proc p + JOIN pg_namespace n ON p.pronamespace = n.oid + WHERE n.nspname = 'eql_v2'$$, + 'eql_v2 schema should contain functions' +); + +SELECT finish(); +ROLLBACK; diff --git a/tests/pgtap/structure/operators_test.sql b/tests/pgtap/structure/operators_test.sql new file mode 100644 index 0000000..80a4e84 --- /dev/null +++ b/tests/pgtap/structure/operators_test.sql @@ -0,0 +1,19 @@ +-- Test EQL operator structure +-- Verifies that operators exist for eql_v2_encrypted type + +BEGIN; + +-- Plan: count of tests to run +SELECT plan(1); + +-- Test that operators exist for eql_v2_encrypted type +-- Operators are defined in the public schema +SELECT ok( + (SELECT count(*) FROM pg_operator o + JOIN pg_type t1 ON o.oprleft = t1.oid + WHERE t1.typname = 'eql_v2_encrypted') >= 10, + 'At least 10 operators should exist for eql_v2_encrypted type' +); + +SELECT finish(); +ROLLBACK; diff --git a/tests/pgtap/structure/schema_test.sql b/tests/pgtap/structure/schema_test.sql new file mode 100644 index 0000000..1b16723 --- /dev/null +++ b/tests/pgtap/structure/schema_test.sql @@ -0,0 +1,32 @@ +-- Test EQL schema structure +-- Verifies that the eql_v2 schema, types, and configuration table exist + +BEGIN; + +-- Plan: count of tests to run +SELECT plan(10); + +-- Test 1: Schema exists +SELECT has_schema('eql_v2', 'Schema eql_v2 should exist'); + +-- Test 2: Encrypted column type exists +SELECT has_type('public', 'eql_v2_encrypted', 'Encrypted column type should exist'); + +-- Test 3: Configuration table exists +SELECT has_table('public', 'eql_v2_configuration', 'Configuration table should exist'); + +-- Test 4-6: Configuration table columns exist +SELECT has_column('public', 'eql_v2_configuration', 'id', 'Configuration table has id column'); +SELECT has_column('public', 'eql_v2_configuration', 'state', 'Configuration table has state column'); +SELECT has_column('public', 'eql_v2_configuration', 'data', 'Configuration table has data column'); + +-- Test 7-9: Configuration table column types +SELECT col_type_is('public', 'eql_v2_configuration', 'id', 'bigint', 'id column is bigint'); +SELECT col_type_is('public', 'eql_v2_configuration', 'state', 'eql_v2_configuration_state', 'state column is eql_v2_configuration_state'); +SELECT col_type_is('public', 'eql_v2_configuration', 'data', 'jsonb', 'data column is jsonb'); + +-- Test 10: eql_v2_encrypted is a composite type +SELECT has_type('public', 'eql_v2_encrypted', 'eql_v2_encrypted type exists'); + +SELECT finish(); +ROLLBACK; diff --git a/tests/pgtap/structure/types_test.sql b/tests/pgtap/structure/types_test.sql new file mode 100644 index 0000000..d2eb0fb --- /dev/null +++ b/tests/pgtap/structure/types_test.sql @@ -0,0 +1,19 @@ +-- Test EQL type structure +-- Verifies that all index term types exist in the eql_v2 schema + +BEGIN; + +-- Plan: count of tests to run +SELECT plan(7); + +-- Test index term types exist +SELECT has_type('eql_v2', 'blake3', 'blake3 index term type should exist'); +SELECT has_type('eql_v2', 'hmac_256', 'hmac_256 index term type should exist'); +SELECT has_type('eql_v2', 'bloom_filter', 'bloom_filter index term type should exist'); +SELECT has_type('eql_v2', 'ore_cllw_u64_8', 'ore_cllw_u64_8 index term type should exist'); +SELECT has_type('eql_v2', 'ore_cllw_var_8', 'ore_cllw_var_8 index term type should exist'); +SELECT has_type('eql_v2', 'ore_block_u64_8_256', 'ore_block_u64_8_256 index term type should exist'); +SELECT has_type('eql_v2', 'ore_block_u64_8_256_term', 'ore_block_u64_8_256_term index term type should exist'); + +SELECT finish(); +ROLLBACK;