]> begriffs open source - pg_scribe/blob - tests/test_init.sh
Better way to make a new chain and transfer streaming to it
[pg_scribe] / tests / test_init.sh
1 #!/usr/bin/env bash
2 #
3 # Test suite for pg_scribe --init command
4 #
5 # This test suite:
6 # - Creates temporary test databases
7 # - Tests various --init scenarios
8 # - Verifies expected outcomes
9 # - Cleans up all resources
10 #
11
12 set -euo pipefail
13
14 # Colors for test output
15 RED='\033[0;31m'
16 GREEN='\033[0;32m'
17 YELLOW='\033[0;33m'
18 BLUE='\033[0;34m'
19 NC='\033[0m' # No Color
20
21 # Test configuration
22 SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
23 PG_SCRIBE="$SCRIPT_DIR/scripts/pg_scribe"
24 TEST_DIR="/tmp/pg_scribe_test_$$"
25 TEST_DB_PREFIX="pg_scribe_test_$$"
26 PGUSER="${PGUSER:-postgres}"
27
28 # Test counters
29 TESTS_RUN=0
30 TESTS_PASSED=0
31 TESTS_FAILED=0
32
33 # Cleanup tracking
34 DATABASES_TO_CLEANUP=()
35
36 #
37 # Logging functions
38 #
39
40 log_test() {
41     echo -e "${BLUE}TEST:${NC} $*"
42 }
43
44 log_pass() {
45     echo -e "${GREEN}PASS:${NC} $*"
46     ((TESTS_PASSED++))
47 }
48
49 log_fail() {
50     echo -e "${RED}FAIL:${NC} $*"
51     ((TESTS_FAILED++))
52 }
53
54 log_info() {
55     echo -e "${YELLOW}INFO:${NC} $*"
56 }
57
58 #
59 # Helper functions
60 #
61
62 run_psql() {
63     local dbname="$1"
64     shift
65     psql -U "$PGUSER" -d "$dbname" -tAq "$@"
66 }
67
68 query_db() {
69     local dbname="$1"
70     local query="$2"
71     run_psql "$dbname" -c "$query" 2>/dev/null || true
72 }
73
74 create_test_db() {
75     local dbname="$1"
76     log_info "Creating test database: $dbname"
77
78     # Drop if exists
79     psql -U "$PGUSER" -d postgres -c "DROP DATABASE IF EXISTS $dbname;" &>/dev/null || true
80
81     # Create database
82     psql -U "$PGUSER" -d postgres -c "CREATE DATABASE $dbname;" &>/dev/null
83
84     DATABASES_TO_CLEANUP+=("$dbname")
85 }
86
87 # shellcheck disable=SC2317  # Function called from cleanup trap handler
88 drop_test_db() {
89     local dbname="$1"
90     log_info "Dropping test database: $dbname"
91
92     # Terminate connections
93     psql -U "$PGUSER" -d postgres -c "
94         SELECT pg_terminate_backend(pid)
95         FROM pg_stat_activity
96         WHERE datname = '$dbname' AND pid <> pg_backend_pid();
97     " &>/dev/null || true
98
99     # Drop database
100     psql -U "$PGUSER" -d postgres -c "DROP DATABASE IF EXISTS $dbname;" &>/dev/null || true
101 }
102
103 # shellcheck disable=SC2317  # Function called from cleanup trap handler
104 drop_replication_slot() {
105     local dbname="$1"
106     local slot="$2"
107     log_info "Dropping replication slot: $slot"
108
109     # Check if slot exists
110     local exists
111     exists=$(query_db "$dbname" "
112         SELECT 1 FROM pg_replication_slots WHERE slot_name = '$slot';
113     ")
114
115     if [[ -n "$exists" ]]; then
116         # Drop slot
117         query_db "$dbname" "SELECT pg_drop_replication_slot('$slot');" || true
118     fi
119 }
120
121 check_slot_exists() {
122     local dbname="$1"
123     local slot="$2"
124     local exists
125     exists=$(query_db "$dbname" "
126         SELECT 1 FROM pg_replication_slots WHERE slot_name = '$slot';
127     ")
128     [[ -n "$exists" ]]
129 }
130
131 check_extension_exists() {
132     local dbname="$1"
133     local extension="$2"
134     local exists
135     exists=$(query_db "$dbname" "
136         SELECT 1 FROM pg_extension WHERE extname = '$extension';
137     ")
138     [[ -n "$exists" ]]
139 }
140
141 create_table_with_pk() {
142     local dbname="$1"
143     local table="$2"
144     query_db "$dbname" "
145         CREATE TABLE $table (
146             id SERIAL PRIMARY KEY,
147             name TEXT,
148             created_at TIMESTAMP DEFAULT now()
149         );
150     "
151 }
152
153 create_table_without_pk() {
154     local dbname="$1"
155     local table="$2"
156     query_db "$dbname" "
157         CREATE TABLE $table (
158             id INTEGER,
159             name TEXT
160         );
161     "
162 }
163
164 #
165 # Test cases
166 #
167
168 test_basic_init_success() {
169     ((TESTS_RUN++))
170     log_test "Basic --init success"
171
172     local dbname="${TEST_DB_PREFIX}_basic"
173     local slot="test_slot_basic"
174     local backup_dir="$TEST_DIR/basic"
175
176     # Setup
177     create_test_db "$dbname"
178     create_table_with_pk "$dbname" "users"
179     mkdir -p "$backup_dir"
180
181     # Run init
182     if "$PG_SCRIBE" --init -d "$dbname" -f "$backup_dir" -S "$slot" -U "$PGUSER" &>/dev/null; then
183         # Verify slot created
184         if ! check_slot_exists "$dbname" "$slot"; then
185             log_fail "Replication slot not created"
186             return 1
187         fi
188
189         # Verify extension installed
190         if ! check_extension_exists "$dbname" "wal2sql"; then
191             log_fail "wal2sql extension not installed"
192             return 1
193         fi
194
195         # Verify chain directory created
196         local chain_dirs=("$backup_dir"/chain-*)
197         if [[ ! -d "${chain_dirs[0]}" ]]; then
198             log_fail "Chain directory not created"
199             return 1
200         fi
201
202         local chain_dir="${chain_dirs[0]}"
203
204         # Verify files in chain directory
205         if [[ ! -f "$chain_dir/base.sql" ]]; then
206             log_fail "Base backup file not created in chain"
207             return 1
208         fi
209
210         if [[ ! -f "$chain_dir/globals.sql" ]]; then
211             log_fail "Globals backup file not created in chain"
212             return 1
213         fi
214
215         if [[ ! -f "$chain_dir/metadata.json" ]]; then
216             log_fail "Metadata file not created in chain"
217             return 1
218         fi
219
220         # Verify pidfile is NOT created (only created during --start)
221         if [[ -f "$backup_dir/.pg_scribe.pid" ]]; then
222             log_fail "Pidfile should not be created by --init"
223             return 1
224         fi
225
226         # Verify metadata content (JSON format)
227         if ! grep -q "\"database\": \"$dbname\"" "$chain_dir/metadata.json"; then
228             log_fail "Metadata missing database name"
229             return 1
230         fi
231
232         if ! grep -q "\"replication_slot\": \"$slot\"" "$chain_dir/metadata.json"; then
233             log_fail "Metadata missing slot name"
234             return 1
235         fi
236
237         log_pass "Basic init successful"
238         return 0
239     else
240         log_fail "Init command failed"
241         return 1
242     fi
243 }
244
245 test_init_validation_failure() {
246     ((TESTS_RUN++))
247     log_test "Init validation failure (table without replica identity)"
248
249     local dbname="${TEST_DB_PREFIX}_nopk"
250     local slot="test_slot_nopk"
251     local backup_dir="$TEST_DIR/nopk"
252
253     # Setup - create table WITHOUT primary key
254     create_test_db "$dbname"
255     create_table_without_pk "$dbname" "bad_table"
256     mkdir -p "$backup_dir"
257
258     # Run init - should fail with exit code 5
259     local exit_code=0
260     "$PG_SCRIBE" --init -d "$dbname" -f "$backup_dir" -S "$slot" -U "$PGUSER" &>/dev/null || exit_code=$?
261
262     if [[ $exit_code -eq 5 ]]; then
263         # Verify slot was NOT created
264         if check_slot_exists "$dbname" "$slot"; then
265             log_fail "Replication slot should not be created on validation failure"
266             return 1
267         fi
268
269         log_pass "Validation failure detected correctly"
270         return 0
271     else
272         log_fail "Expected exit code 5, got $exit_code"
273         return 1
274     fi
275 }
276
277 test_init_force_flag() {
278     ((TESTS_RUN++))
279     log_test "Init with --force flag (bypass validation)"
280
281     local dbname="${TEST_DB_PREFIX}_force"
282     local slot="test_slot_force"
283     local backup_dir="$TEST_DIR/force"
284
285     # Setup - create table WITHOUT primary key
286     create_test_db "$dbname"
287     create_table_without_pk "$dbname" "bad_table"
288     mkdir -p "$backup_dir"
289
290     # Run init with --force - should succeed despite validation failure
291     if "$PG_SCRIBE" --init -d "$dbname" -f "$backup_dir" -S "$slot" -U "$PGUSER" --force &>/dev/null; then
292         # Verify slot was created
293         if ! check_slot_exists "$dbname" "$slot"; then
294             log_fail "Replication slot should be created with --force"
295             return 1
296         fi
297
298         log_pass "Force flag bypassed validation"
299         return 0
300     else
301         log_fail "Init with --force should succeed"
302         return 1
303     fi
304 }
305
306 test_init_non_idempotency() {
307     ((TESTS_RUN++))
308     log_test "Init refuses to run on already-initialized directory"
309
310     local dbname="${TEST_DB_PREFIX}_nonidempotent"
311     local slot="test_slot_nonidempotent"
312     local backup_dir="$TEST_DIR/nonidempotent"
313
314     # Setup
315     create_test_db "$dbname"
316     create_table_with_pk "$dbname" "users"
317     mkdir -p "$backup_dir"
318
319     # Run init first time
320     if ! "$PG_SCRIBE" --init -d "$dbname" -f "$backup_dir" -S "$slot" -U "$PGUSER" &>/dev/null; then
321         log_fail "First init failed"
322         return 1
323     fi
324
325     # Run init second time - should FAIL with validation error
326     local exit_code=0
327     "$PG_SCRIBE" --init -d "$dbname" -f "$backup_dir" -S "$slot" -U "$PGUSER" &>/dev/null || exit_code=$?
328
329     if [[ $exit_code -eq 5 ]]; then
330         # Verify slot still exists from first init
331         if ! check_slot_exists "$dbname" "$slot"; then
332             log_fail "Replication slot should still exist from first init"
333             return 1
334         fi
335
336         # Verify only 1 chain directory (from first init)
337         local chain_count
338         chain_count=$(find "$backup_dir" -maxdepth 1 -type d -name 'chain-*' 2>/dev/null | wc -l)
339         if [[ $chain_count -ne 1 ]]; then
340             log_fail "Expected 1 chain directory, got $chain_count"
341             return 1
342         fi
343
344         log_pass "Init correctly refuses to reinitialize"
345         return 0
346     else
347         log_fail "Expected exit code 5 (validation error), got $exit_code"
348         return 1
349     fi
350 }
351
352 test_if_not_exists_flag() {
353     ((TESTS_RUN++))
354     log_test "Init with --if-not-exists flag (idempotent)"
355
356     local dbname="${TEST_DB_PREFIX}_ifnotexists"
357     local slot="test_slot_ifnotexists"
358     local backup_dir="$TEST_DIR/ifnotexists"
359
360     # Setup
361     create_test_db "$dbname"
362     create_table_with_pk "$dbname" "users"
363     mkdir -p "$backup_dir"
364
365     # Run init first time
366     if ! "$PG_SCRIBE" --init -d "$dbname" -f "$backup_dir" -S "$slot" -U "$PGUSER" &>/dev/null; then
367         log_fail "First init failed"
368         return 1
369     fi
370
371     # Verify slot was created
372     if ! check_slot_exists "$dbname" "$slot"; then
373         log_fail "Replication slot not created on first init"
374         return 1
375     fi
376
377     # Run init second time with --if-not-exists - should SUCCEED with exit 0
378     local exit_code=0
379     "$PG_SCRIBE" --init -d "$dbname" -f "$backup_dir" -S "$slot" -U "$PGUSER" --if-not-exists &>/dev/null || exit_code=$?
380
381     if [[ $exit_code -eq 0 ]]; then
382         # Verify slot still exists from first init
383         if ! check_slot_exists "$dbname" "$slot"; then
384             log_fail "Replication slot should still exist"
385             return 1
386         fi
387
388         # Verify only 1 chain directory (from first init, second init should skip)
389         local chain_count
390         chain_count=$(find "$backup_dir" -maxdepth 1 -type d -name 'chain-*' 2>/dev/null | wc -l)
391         if [[ $chain_count -ne 1 ]]; then
392             log_fail "Expected 1 chain directory, got $chain_count"
393             return 1
394         fi
395
396         log_pass "--if-not-exists flag makes init idempotent"
397         return 0
398     else
399         log_fail "Expected exit code 0, got $exit_code"
400         return 1
401     fi
402 }
403
404 test_init_multiple_tables() {
405     ((TESTS_RUN++))
406     log_test "Init with multiple tables"
407
408     local dbname="${TEST_DB_PREFIX}_multi"
409     local slot="test_slot_multi"
410     local backup_dir="$TEST_DIR/multi"
411
412     # Setup - create multiple tables
413     create_test_db "$dbname"
414     create_table_with_pk "$dbname" "users"
415     create_table_with_pk "$dbname" "orders"
416     create_table_with_pk "$dbname" "products"
417
418     # Insert some data
419     query_db "$dbname" "INSERT INTO users (name) VALUES ('Alice'), ('Bob');"
420     query_db "$dbname" "INSERT INTO orders (name) VALUES ('Order1');"
421     query_db "$dbname" "INSERT INTO products (name) VALUES ('Widget');"
422
423     mkdir -p "$backup_dir"
424
425     # Run init
426     if "$PG_SCRIBE" --init -d "$dbname" -f "$backup_dir" -S "$slot" -U "$PGUSER" &>/dev/null; then
427         # Verify base backup contains all tables
428         local chain_dirs=("$backup_dir"/chain-*)
429         local base_file="${chain_dirs[0]}/base.sql"
430
431         if ! grep -q "CREATE TABLE public.users" "$base_file"; then
432             log_fail "Base backup missing users table"
433             return 1
434         fi
435
436         if ! grep -q "CREATE TABLE public.orders" "$base_file"; then
437             log_fail "Base backup missing orders table"
438             return 1
439         fi
440
441         if ! grep -q "CREATE TABLE public.products" "$base_file"; then
442             log_fail "Base backup missing products table"
443             return 1
444         fi
445
446         log_pass "Multiple tables backed up successfully"
447         return 0
448     else
449         log_fail "Init failed"
450         return 1
451     fi
452 }
453
454 test_init_with_unlogged_table() {
455     ((TESTS_RUN++))
456     log_test "Init with unlogged table (warning but success)"
457
458     local dbname="${TEST_DB_PREFIX}_unlogged"
459     local slot="test_slot_unlogged"
460     local backup_dir="$TEST_DIR/unlogged"
461
462     # Setup
463     create_test_db "$dbname"
464     create_table_with_pk "$dbname" "normal_table"
465     query_db "$dbname" "CREATE UNLOGGED TABLE unlogged_table (id SERIAL PRIMARY KEY, data TEXT);"
466     mkdir -p "$backup_dir"
467
468     # Run init - should succeed with warning (exit code 0 or 10)
469     local output_file="$TEST_DIR/unlogged_output.txt"
470     local exit_code=0
471     "$PG_SCRIBE" --init -d "$dbname" -f "$backup_dir" -S "$slot" -U "$PGUSER" &>"$output_file" || exit_code=$?
472
473     # Exit code 0 (success) or 10 (warning) are both acceptable
474     if [[ $exit_code -eq 0 || $exit_code -eq 10 ]]; then
475         # Check for warning message
476         if ! grep -i "unlogged" "$output_file"; then
477             log_fail "Expected warning about unlogged table"
478             return 1
479         fi
480
481         # Verify backup was created
482         local chain_dirs=("$backup_dir"/chain-*)
483         if [[ ! -f "${chain_dirs[0]}/base.sql" ]]; then
484             log_fail "Base backup file not created"
485             return 1
486         fi
487
488         log_pass "Unlogged table warning shown correctly"
489         return 0
490     else
491         log_fail "Init failed with exit code $exit_code"
492         return 1
493     fi
494 }
495
496 test_backup_content_validity() {
497     ((TESTS_RUN++))
498     log_test "Verify backup SQL is valid"
499
500     local dbname="${TEST_DB_PREFIX}_valid"
501     local slot="test_slot_valid"
502     local backup_dir="$TEST_DIR/valid"
503     local restore_dbname="${TEST_DB_PREFIX}_restored"
504
505     # Setup
506     create_test_db "$dbname"
507     create_table_with_pk "$dbname" "test_data"
508     query_db "$dbname" "INSERT INTO test_data (name) VALUES ('Test Row 1'), ('Test Row 2');"
509     mkdir -p "$backup_dir"
510
511     # Run init
512     if ! "$PG_SCRIBE" --init -d "$dbname" -f "$backup_dir" -S "$slot" -U "$PGUSER" &>/dev/null; then
513         log_fail "Init failed"
514         return 1
515     fi
516
517     # Try to restore the backup to a new database
518     create_test_db "$restore_dbname"
519
520     # Find chain directory
521     local chain_dirs=("$backup_dir"/chain-*)
522     local chain_dir="${chain_dirs[0]}"
523
524     local base_file="$chain_dir/base.sql"
525     local globals_file="$chain_dir/globals.sql"
526
527     # Apply globals (roles, etc.)
528     if ! psql -U "$PGUSER" -d postgres -f "$globals_file" &>/dev/null; then
529         log_fail "Failed to restore globals"
530         return 1
531     fi
532
533     # Apply base backup
534     if ! psql -U "$PGUSER" -d "$restore_dbname" -f "$base_file" &>/dev/null; then
535         log_fail "Failed to restore base backup"
536         return 1
537     fi
538
539     # Verify data
540     local count
541     count=$(query_db "$restore_dbname" "SELECT COUNT(*) FROM test_data;")
542     if [[ "$count" -ne 2 ]]; then
543         log_fail "Expected 2 rows, got $count"
544         return 1
545     fi
546
547     log_pass "Backup SQL is valid and restorable"
548     return 0
549 }
550
551 #
552 # Cleanup
553 #
554
555 # shellcheck disable=SC2317  # Function called via trap handler
556 cleanup() {
557     log_info "Cleaning up test resources..."
558
559     # Drop replication slots
560     for dbname in "${DATABASES_TO_CLEANUP[@]}"; do
561         # Try to drop any slots for this database
562         for slot in test_slot_basic test_slot_nopk test_slot_force test_slot_nonidempotent test_slot_ifnotexists test_slot_multi test_slot_unlogged test_slot_valid; do
563             drop_replication_slot "$dbname" "$slot" 2>/dev/null || true
564         done
565     done
566
567     # Drop databases
568     for dbname in "${DATABASES_TO_CLEANUP[@]}"; do
569         drop_test_db "$dbname"
570     done
571
572     # Remove test directory
573     if [[ -d "$TEST_DIR" ]]; then
574         rm -rf "$TEST_DIR"
575     fi
576
577     log_info "Cleanup complete"
578 }
579
580 #
581 # Main test runner
582 #
583
584 main() {
585     echo "========================================"
586     echo "pg_scribe --init Test Suite"
587     echo "========================================"
588     echo ""
589
590     # Verify pg_scribe exists
591     if [[ ! -x "$PG_SCRIBE" ]]; then
592         echo "ERROR: pg_scribe not found or not executable: $PG_SCRIBE"
593         exit 1
594     fi
595
596     # Verify PostgreSQL is running
597     if ! psql -U "$PGUSER" -d postgres -c "SELECT 1;" &>/dev/null; then
598         echo "ERROR: Cannot connect to PostgreSQL"
599         exit 1
600     fi
601
602     # Verify wal_level is logical
603     local wal_level
604     wal_level=$(psql -U "$PGUSER" -d postgres -tAq -c "SHOW wal_level;")
605     if [[ "$wal_level" != "logical" ]]; then
606         echo "ERROR: wal_level must be 'logical', currently: $wal_level"
607         echo "Update ~/.pgenv/pgsql/data/postgresql.conf and restart PostgreSQL"
608         exit 1
609     fi
610
611     # Create test directory
612     mkdir -p "$TEST_DIR"
613
614     # Set up cleanup trap
615     trap cleanup EXIT INT TERM
616
617     echo "Running tests..."
618     echo ""
619
620     # Run all tests (use || true to prevent set -e from exiting)
621     test_basic_init_success || true
622     test_init_validation_failure || true
623     test_init_force_flag || true
624     test_init_non_idempotency || true
625     test_if_not_exists_flag || true
626     test_init_multiple_tables || true
627     test_init_with_unlogged_table || true
628     test_backup_content_validity || true
629
630     # Summary
631     echo ""
632     echo "========================================"
633     echo "Test Results"
634     echo "========================================"
635     echo "Tests run:    $TESTS_RUN"
636     echo -e "Tests passed: ${GREEN}$TESTS_PASSED${NC}"
637     echo -e "Tests failed: ${RED}$TESTS_FAILED${NC}"
638     echo ""
639
640     if [[ $TESTS_FAILED -eq 0 ]]; then
641         echo -e "${GREEN}All tests passed!${NC}"
642         exit 0
643     else
644         echo -e "${RED}Some tests failed!${NC}"
645         exit 1
646     fi
647 }
648
649 main "$@"