Skip to content

Commit 44ea471

Browse files
committed
test: add edge-case tests for CRDT sync correctness and error handling
Add 7 new SQLite unit tests and 7 new PostgreSQL test files covering: - DWS/AWS algorithm rejection (unsupported CRDT algos return clean errors) - Corrupted payload handling (empty, garbage, truncated, bit-flipped) - Payload apply idempotency (3x apply produces identical results) - Causal-length tie-breaking determinism (3-way concurrent update convergence) - Delete/resurrect with out-of-order payload delivery - Large composite primary key (5-column mixed-type PK roundtrip) - PostgreSQL-specific type roundtrips (JSONB, TIMESTAMPTZ, NUMERIC, BYTEA) - Schema hash mismatch detection (ALTER TABLE without cloudsync workflow)
1 parent e143be3 commit 44ea471

9 files changed

+1417
-0
lines changed
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
-- Test unsupported CRDT algorithms (DWS, AWS)
2+
-- Verifies that cloudsync_init rejects DWS and AWS with clear errors
3+
-- and that no metadata tables are created.
4+
5+
\set testid '40'
6+
\ir helper_test_init.sql
7+
8+
\connect postgres
9+
\ir helper_psql_conn_setup.sql
10+
DROP DATABASE IF EXISTS cloudsync_test_40;
11+
CREATE DATABASE cloudsync_test_40;
12+
13+
\connect cloudsync_test_40
14+
\ir helper_psql_conn_setup.sql
15+
CREATE EXTENSION IF NOT EXISTS cloudsync;
16+
17+
CREATE TABLE test_dws (id TEXT PRIMARY KEY, val TEXT);
18+
CREATE TABLE test_aws (id TEXT PRIMARY KEY, val TEXT);
19+
20+
-- Test DWS rejection
21+
DO $$
22+
BEGIN
23+
PERFORM cloudsync_init('test_dws', 'dws', true);
24+
RAISE EXCEPTION 'cloudsync_init with dws should have failed';
25+
EXCEPTION WHEN OTHERS THEN
26+
IF SQLERRM NOT LIKE '%not yet supported%' THEN
27+
RAISE EXCEPTION 'Unexpected error for dws: %', SQLERRM;
28+
END IF;
29+
END $$;
30+
31+
-- Verify no companion table was created for DWS
32+
SELECT COUNT(*) = 0 AS no_dws_meta
33+
FROM information_schema.tables
34+
WHERE table_name = 'test_dws_cloudsync' \gset
35+
\if :no_dws_meta
36+
\echo [PASS] (:testid) DWS rejected - no metadata table created
37+
\else
38+
\echo [FAIL] (:testid) DWS metadata table should not exist
39+
SELECT (:fail::int + 1) AS fail \gset
40+
\endif
41+
42+
-- Test AWS rejection
43+
DO $$
44+
BEGIN
45+
PERFORM cloudsync_init('test_aws', 'aws', true);
46+
RAISE EXCEPTION 'cloudsync_init with aws should have failed';
47+
EXCEPTION WHEN OTHERS THEN
48+
IF SQLERRM NOT LIKE '%not yet supported%' THEN
49+
RAISE EXCEPTION 'Unexpected error for aws: %', SQLERRM;
50+
END IF;
51+
END $$;
52+
53+
-- Verify no companion table was created for AWS
54+
SELECT COUNT(*) = 0 AS no_aws_meta
55+
FROM information_schema.tables
56+
WHERE table_name = 'test_aws_cloudsync' \gset
57+
\if :no_aws_meta
58+
\echo [PASS] (:testid) AWS rejected - no metadata table created
59+
\else
60+
\echo [FAIL] (:testid) AWS metadata table should not exist
61+
SELECT (:fail::int + 1) AS fail \gset
62+
\endif
63+
64+
-- Verify CLS still works (sanity check)
65+
SELECT cloudsync_init('test_dws', 'cls', true) AS _init_cls \gset
66+
SELECT COUNT(*) = 1 AS cls_meta_ok
67+
FROM information_schema.tables
68+
WHERE table_name = 'test_dws_cloudsync' \gset
69+
\if :cls_meta_ok
70+
\echo [PASS] (:testid) CLS init works after DWS/AWS rejection
71+
\else
72+
\echo [FAIL] (:testid) CLS init should work
73+
SELECT (:fail::int + 1) AS fail \gset
74+
\endif
75+
76+
-- Cleanup
77+
\ir helper_test_cleanup.sql
78+
\if :should_cleanup
79+
DROP DATABASE IF EXISTS cloudsync_test_40;
80+
\endif
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
-- Test corrupted payload handling
2+
-- Verifies that cloudsync_payload_apply rejects corrupted payloads
3+
-- without crashing or corrupting state.
4+
5+
\set testid '41'
6+
\ir helper_test_init.sql
7+
8+
\connect postgres
9+
\ir helper_psql_conn_setup.sql
10+
DROP DATABASE IF EXISTS cloudsync_test_41_src;
11+
DROP DATABASE IF EXISTS cloudsync_test_41_dst;
12+
CREATE DATABASE cloudsync_test_41_src;
13+
CREATE DATABASE cloudsync_test_41_dst;
14+
15+
-- Setup source database with data
16+
\connect cloudsync_test_41_src
17+
\ir helper_psql_conn_setup.sql
18+
CREATE EXTENSION IF NOT EXISTS cloudsync;
19+
CREATE TABLE test_tbl (id TEXT PRIMARY KEY, val TEXT);
20+
SELECT cloudsync_init('test_tbl', 'CLS', true) AS _init_src \gset
21+
INSERT INTO test_tbl VALUES ('id1', 'value1');
22+
INSERT INTO test_tbl VALUES ('id2', 'value2');
23+
24+
-- Get a valid payload
25+
SELECT encode(cloudsync_payload_encode(tbl, pk, col_name, col_value, col_version, db_version, site_id, cl, seq), 'hex') AS valid_payload_hex
26+
FROM cloudsync_changes
27+
WHERE site_id = cloudsync_siteid() \gset
28+
29+
-- Setup destination database
30+
\connect cloudsync_test_41_dst
31+
\ir helper_psql_conn_setup.sql
32+
CREATE EXTENSION IF NOT EXISTS cloudsync;
33+
CREATE TABLE test_tbl (id TEXT PRIMARY KEY, val TEXT);
34+
SELECT cloudsync_init('test_tbl', 'CLS', true) AS _init_dst \gset
35+
36+
-- Record initial state
37+
SELECT COUNT(*) AS initial_count FROM test_tbl \gset
38+
39+
-- Test 1: Empty blob (zero bytes)
40+
DO $$
41+
BEGIN
42+
PERFORM cloudsync_payload_apply(''::bytea);
43+
-- If it returns without error with 0 rows, that's also acceptable
44+
EXCEPTION WHEN OTHERS THEN
45+
-- Expected: error on empty payload
46+
NULL;
47+
END $$;
48+
49+
SELECT COUNT(*) AS count_after_empty FROM test_tbl \gset
50+
SELECT (:count_after_empty::int = :initial_count::int) AS empty_blob_ok \gset
51+
\if :empty_blob_ok
52+
\echo [PASS] (:testid) Empty blob rejected - table unchanged
53+
\else
54+
\echo [FAIL] (:testid) Empty blob corrupted table state
55+
SELECT (:fail::int + 1) AS fail \gset
56+
\endif
57+
58+
-- Test 2: Random garbage bytes
59+
DO $$
60+
BEGIN
61+
PERFORM cloudsync_payload_apply(decode('deadbeefcafebabe0102030405060708', 'hex'));
62+
EXCEPTION WHEN OTHERS THEN
63+
-- Expected: error on garbage payload
64+
NULL;
65+
END $$;
66+
67+
SELECT COUNT(*) AS count_after_garbage FROM test_tbl \gset
68+
SELECT (:count_after_garbage::int = :initial_count::int) AS garbage_ok \gset
69+
\if :garbage_ok
70+
\echo [PASS] (:testid) Garbage bytes rejected - table unchanged
71+
\else
72+
\echo [FAIL] (:testid) Garbage bytes corrupted table state
73+
SELECT (:fail::int + 1) AS fail \gset
74+
\endif
75+
76+
-- Test 3: Truncated payload (first 10 bytes of valid payload)
77+
-- Build truncated hex at top level using psql variable interpolation
78+
SELECT substr(:'valid_payload_hex', 1, 20) AS truncated_hex \gset
79+
SELECT cloudsync_payload_apply(decode(:'truncated_hex', 'hex')) AS _apply_truncated \gset
80+
-- If the above errors, psql continues (ON_ERROR_STOP is off)
81+
82+
SELECT COUNT(*) AS count_after_truncated FROM test_tbl \gset
83+
SELECT (:count_after_truncated::int = :initial_count::int) AS truncated_ok \gset
84+
\if :truncated_ok
85+
\echo [PASS] (:testid) Truncated payload rejected - table unchanged
86+
\else
87+
\echo [FAIL] (:testid) Truncated payload corrupted table state
88+
SELECT (:fail::int + 1) AS fail \gset
89+
\endif
90+
91+
-- Test 4: Valid payload with flipped byte in the middle
92+
-- Compute corrupted payload at top level: flip one byte via XOR with FF
93+
SELECT
94+
substr(:'valid_payload_hex', 1, length(:'valid_payload_hex') / 2 - 1)
95+
|| lpad(to_hex(get_byte(decode(substr(:'valid_payload_hex', length(:'valid_payload_hex') / 2, 2), 'hex'), 0) # 255), 2, '0')
96+
|| substr(:'valid_payload_hex', length(:'valid_payload_hex') / 2 + 2)
97+
AS corrupted_hex \gset
98+
SELECT cloudsync_payload_apply(decode(:'corrupted_hex', 'hex')) AS _apply_corrupted \gset
99+
-- If the above errors, psql continues (ON_ERROR_STOP is off)
100+
101+
SELECT COUNT(*) AS count_after_flipped FROM test_tbl \gset
102+
SELECT (:count_after_flipped::int = :initial_count::int) AS flipped_ok \gset
103+
\if :flipped_ok
104+
\echo [PASS] (:testid) Flipped-byte payload rejected - table unchanged
105+
\else
106+
\echo [FAIL] (:testid) Flipped-byte payload corrupted table state
107+
SELECT (:fail::int + 1) AS fail \gset
108+
\endif
109+
110+
-- Test 5: Now apply the VALID payload to confirm it still works
111+
SELECT cloudsync_payload_apply(decode(:'valid_payload_hex', 'hex')) AS valid_apply \gset
112+
SELECT COUNT(*) AS count_after_valid FROM test_tbl \gset
113+
SELECT (:count_after_valid::int = 2) AS valid_ok \gset
114+
\if :valid_ok
115+
\echo [PASS] (:testid) Valid payload applied successfully after corrupted attempts
116+
\else
117+
\echo [FAIL] (:testid) Valid payload failed after corrupted attempts - count: :count_after_valid
118+
SELECT (:fail::int + 1) AS fail \gset
119+
\endif
120+
121+
-- Cleanup
122+
\ir helper_test_cleanup.sql
123+
\if :should_cleanup
124+
DROP DATABASE IF EXISTS cloudsync_test_41_src;
125+
DROP DATABASE IF EXISTS cloudsync_test_41_dst;
126+
\endif
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
-- Test payload apply idempotency
2+
-- Applying the same payload multiple times must produce identical results.
3+
4+
\set testid '42'
5+
\ir helper_test_init.sql
6+
7+
\connect postgres
8+
\ir helper_psql_conn_setup.sql
9+
DROP DATABASE IF EXISTS cloudsync_test_42_src;
10+
DROP DATABASE IF EXISTS cloudsync_test_42_dst;
11+
CREATE DATABASE cloudsync_test_42_src;
12+
CREATE DATABASE cloudsync_test_42_dst;
13+
14+
-- Setup source with data
15+
\connect cloudsync_test_42_src
16+
\ir helper_psql_conn_setup.sql
17+
CREATE EXTENSION IF NOT EXISTS cloudsync;
18+
CREATE TABLE test_tbl (id TEXT PRIMARY KEY, val TEXT, num INTEGER);
19+
SELECT cloudsync_init('test_tbl', 'CLS', true) AS _init_src \gset
20+
INSERT INTO test_tbl VALUES ('id1', 'hello', 10);
21+
INSERT INTO test_tbl VALUES ('id2', 'world', 20);
22+
UPDATE test_tbl SET val = 'hello_updated' WHERE id = 'id1';
23+
24+
-- Encode payload
25+
SELECT encode(cloudsync_payload_encode(tbl, pk, col_name, col_value, col_version, db_version, site_id, cl, seq), 'hex') AS payload_hex
26+
FROM cloudsync_changes
27+
WHERE site_id = cloudsync_siteid() \gset
28+
29+
-- Setup destination
30+
\connect cloudsync_test_42_dst
31+
\ir helper_psql_conn_setup.sql
32+
CREATE EXTENSION IF NOT EXISTS cloudsync;
33+
CREATE TABLE test_tbl (id TEXT PRIMARY KEY, val TEXT, num INTEGER);
34+
SELECT cloudsync_init('test_tbl', 'CLS', true) AS _init_dst \gset
35+
36+
-- Apply #1
37+
SELECT cloudsync_payload_apply(decode(:'payload_hex', 'hex')) AS apply_1 \gset
38+
SELECT md5(COALESCE(string_agg(id || ':' || COALESCE(val, '') || ':' || COALESCE(num::text, ''), ',' ORDER BY id), '')) AS hash_1
39+
FROM test_tbl \gset
40+
SELECT COUNT(*) AS count_1 FROM test_tbl \gset
41+
42+
-- Apply #2
43+
SELECT cloudsync_payload_apply(decode(:'payload_hex', 'hex')) AS apply_2 \gset
44+
SELECT md5(COALESCE(string_agg(id || ':' || COALESCE(val, '') || ':' || COALESCE(num::text, ''), ',' ORDER BY id), '')) AS hash_2
45+
FROM test_tbl \gset
46+
SELECT COUNT(*) AS count_2 FROM test_tbl \gset
47+
48+
-- Apply #3
49+
SELECT cloudsync_payload_apply(decode(:'payload_hex', 'hex')) AS apply_3 \gset
50+
SELECT md5(COALESCE(string_agg(id || ':' || COALESCE(val, '') || ':' || COALESCE(num::text, ''), ',' ORDER BY id), '')) AS hash_3
51+
FROM test_tbl \gset
52+
SELECT COUNT(*) AS count_3 FROM test_tbl \gset
53+
54+
-- Verify row count stays constant
55+
SELECT (:count_1::int = :count_2::int AND :count_2::int = :count_3::int) AS count_stable \gset
56+
\if :count_stable
57+
\echo [PASS] (:testid) Row count stable across 3 applies (:count_1 rows)
58+
\else
59+
\echo [FAIL] (:testid) Row count changed: :count_1 -> :count_2 -> :count_3
60+
SELECT (:fail::int + 1) AS fail \gset
61+
\endif
62+
63+
-- Verify data hash is identical after each apply
64+
SELECT (:'hash_1' = :'hash_2' AND :'hash_2' = :'hash_3') AS hash_stable \gset
65+
\if :hash_stable
66+
\echo [PASS] (:testid) Data hash identical across 3 applies
67+
\else
68+
\echo [FAIL] (:testid) Data hash changed: :hash_1 -> :hash_2 -> :hash_3
69+
SELECT (:fail::int + 1) AS fail \gset
70+
\endif
71+
72+
-- Verify data values are correct
73+
SELECT COUNT(*) = 1 AS data_ok
74+
FROM test_tbl
75+
WHERE id = 'id1' AND val = 'hello_updated' AND num = 10 \gset
76+
\if :data_ok
77+
\echo [PASS] (:testid) Data values correct after idempotent applies
78+
\else
79+
\echo [FAIL] (:testid) Data values incorrect
80+
SELECT (:fail::int + 1) AS fail \gset
81+
\endif
82+
83+
-- Cleanup
84+
\ir helper_test_cleanup.sql
85+
\if :should_cleanup
86+
DROP DATABASE IF EXISTS cloudsync_test_42_src;
87+
DROP DATABASE IF EXISTS cloudsync_test_42_dst;
88+
\endif

0 commit comments

Comments
 (0)