]> begriffs open source - pg_scribe/blob - tests/test_init.sh
More little refactors
[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 files created (use compgen to check for glob matches)
196         local base_files=("$backup_dir"/base-*.sql)
197         if [[ ! -f "${base_files[0]}" ]]; then
198             log_fail "Base backup file not created"
199             return 1
200         fi
201
202         local globals_files=("$backup_dir"/globals-*.sql)
203         if [[ ! -f "${globals_files[0]}" ]]; then
204             log_fail "Globals backup file not created"
205             return 1
206         fi
207
208         if [[ ! -f "$backup_dir/pg_scribe_metadata.txt" ]]; then
209             log_fail "Metadata file not created"
210             return 1
211         fi
212
213         # Verify metadata content
214         if ! grep -q "Database: $dbname" "$backup_dir/pg_scribe_metadata.txt"; then
215             log_fail "Metadata missing database name"
216             return 1
217         fi
218
219         if ! grep -q "Replication Slot: $slot" "$backup_dir/pg_scribe_metadata.txt"; then
220             log_fail "Metadata missing slot name"
221             return 1
222         fi
223
224         log_pass "Basic init successful"
225         return 0
226     else
227         log_fail "Init command failed"
228         return 1
229     fi
230 }
231
232 test_init_validation_failure() {
233     ((TESTS_RUN++))
234     log_test "Init validation failure (table without replica identity)"
235
236     local dbname="${TEST_DB_PREFIX}_nopk"
237     local slot="test_slot_nopk"
238     local backup_dir="$TEST_DIR/nopk"
239
240     # Setup - create table WITHOUT primary key
241     create_test_db "$dbname"
242     create_table_without_pk "$dbname" "bad_table"
243     mkdir -p "$backup_dir"
244
245     # Run init - should fail with exit code 5
246     local exit_code=0
247     "$PG_SCRIBE" --init -d "$dbname" -f "$backup_dir" -S "$slot" -U "$PGUSER" &>/dev/null || exit_code=$?
248
249     if [[ $exit_code -eq 5 ]]; then
250         # Verify slot was NOT created
251         if check_slot_exists "$dbname" "$slot"; then
252             log_fail "Replication slot should not be created on validation failure"
253             return 1
254         fi
255
256         log_pass "Validation failure detected correctly"
257         return 0
258     else
259         log_fail "Expected exit code 5, got $exit_code"
260         return 1
261     fi
262 }
263
264 test_init_force_flag() {
265     ((TESTS_RUN++))
266     log_test "Init with --force flag (bypass validation)"
267
268     local dbname="${TEST_DB_PREFIX}_force"
269     local slot="test_slot_force"
270     local backup_dir="$TEST_DIR/force"
271
272     # Setup - create table WITHOUT primary key
273     create_test_db "$dbname"
274     create_table_without_pk "$dbname" "bad_table"
275     mkdir -p "$backup_dir"
276
277     # Run init with --force - should succeed despite validation failure
278     if "$PG_SCRIBE" --init -d "$dbname" -f "$backup_dir" -S "$slot" -U "$PGUSER" --force &>/dev/null; then
279         # Verify slot was created
280         if ! check_slot_exists "$dbname" "$slot"; then
281             log_fail "Replication slot should be created with --force"
282             return 1
283         fi
284
285         log_pass "Force flag bypassed validation"
286         return 0
287     else
288         log_fail "Init with --force should succeed"
289         return 1
290     fi
291 }
292
293 test_init_non_idempotency() {
294     ((TESTS_RUN++))
295     log_test "Init refuses to run on already-initialized directory"
296
297     local dbname="${TEST_DB_PREFIX}_nonidempotent"
298     local slot="test_slot_nonidempotent"
299     local backup_dir="$TEST_DIR/nonidempotent"
300
301     # Setup
302     create_test_db "$dbname"
303     create_table_with_pk "$dbname" "users"
304     mkdir -p "$backup_dir"
305
306     # Run init first time
307     if ! "$PG_SCRIBE" --init -d "$dbname" -f "$backup_dir" -S "$slot" -U "$PGUSER" &>/dev/null; then
308         log_fail "First init failed"
309         return 1
310     fi
311
312     # Run init second time - should FAIL with validation error
313     local exit_code=0
314     "$PG_SCRIBE" --init -d "$dbname" -f "$backup_dir" -S "$slot" -U "$PGUSER" &>/dev/null || exit_code=$?
315
316     if [[ $exit_code -eq 5 ]]; then
317         # Verify slot still exists from first init
318         if ! check_slot_exists "$dbname" "$slot"; then
319             log_fail "Replication slot should still exist from first init"
320             return 1
321         fi
322
323         # Verify only 1 base backup file (from first init)
324         local base_count
325         base_count=$(find "$backup_dir" -maxdepth 1 -name 'base-*.sql' 2>/dev/null | wc -l)
326         if [[ $base_count -ne 1 ]]; then
327             log_fail "Expected 1 base backup file, got $base_count"
328             return 1
329         fi
330
331         log_pass "Init correctly refuses to reinitialize"
332         return 0
333     else
334         log_fail "Expected exit code 5 (validation error), got $exit_code"
335         return 1
336     fi
337 }
338
339 test_init_multiple_tables() {
340     ((TESTS_RUN++))
341     log_test "Init with multiple tables"
342
343     local dbname="${TEST_DB_PREFIX}_multi"
344     local slot="test_slot_multi"
345     local backup_dir="$TEST_DIR/multi"
346
347     # Setup - create multiple tables
348     create_test_db "$dbname"
349     create_table_with_pk "$dbname" "users"
350     create_table_with_pk "$dbname" "orders"
351     create_table_with_pk "$dbname" "products"
352
353     # Insert some data
354     query_db "$dbname" "INSERT INTO users (name) VALUES ('Alice'), ('Bob');"
355     query_db "$dbname" "INSERT INTO orders (name) VALUES ('Order1');"
356     query_db "$dbname" "INSERT INTO products (name) VALUES ('Widget');"
357
358     mkdir -p "$backup_dir"
359
360     # Run init
361     if "$PG_SCRIBE" --init -d "$dbname" -f "$backup_dir" -S "$slot" -U "$PGUSER" &>/dev/null; then
362         # Verify base backup contains all tables
363         local base_file
364         base_file=$(find "$backup_dir" -maxdepth 1 -name 'base-*.sql' -print -quit)
365
366         if ! grep -q "CREATE TABLE public.users" "$base_file"; then
367             log_fail "Base backup missing users table"
368             return 1
369         fi
370
371         if ! grep -q "CREATE TABLE public.orders" "$base_file"; then
372             log_fail "Base backup missing orders table"
373             return 1
374         fi
375
376         if ! grep -q "CREATE TABLE public.products" "$base_file"; then
377             log_fail "Base backup missing products table"
378             return 1
379         fi
380
381         log_pass "Multiple tables backed up successfully"
382         return 0
383     else
384         log_fail "Init failed"
385         return 1
386     fi
387 }
388
389 test_init_with_unlogged_table() {
390     ((TESTS_RUN++))
391     log_test "Init with unlogged table (warning but success)"
392
393     local dbname="${TEST_DB_PREFIX}_unlogged"
394     local slot="test_slot_unlogged"
395     local backup_dir="$TEST_DIR/unlogged"
396
397     # Setup
398     create_test_db "$dbname"
399     create_table_with_pk "$dbname" "normal_table"
400     query_db "$dbname" "CREATE UNLOGGED TABLE unlogged_table (id SERIAL PRIMARY KEY, data TEXT);"
401     mkdir -p "$backup_dir"
402
403     # Run init - should succeed with warning (exit code 0 or 10)
404     local output_file="$TEST_DIR/unlogged_output.txt"
405     local exit_code=0
406     "$PG_SCRIBE" --init -d "$dbname" -f "$backup_dir" -S "$slot" -U "$PGUSER" &>"$output_file" || exit_code=$?
407
408     # Exit code 0 (success) or 10 (warning) are both acceptable
409     if [[ $exit_code -eq 0 || $exit_code -eq 10 ]]; then
410         # Check for warning message
411         if ! grep -i "unlogged" "$output_file"; then
412             log_fail "Expected warning about unlogged table"
413             return 1
414         fi
415
416         # Verify backup was created
417         local base_files=("$backup_dir"/base-*.sql)
418         if [[ ! -f "${base_files[0]}" ]]; then
419             log_fail "Base backup file not created"
420             return 1
421         fi
422
423         log_pass "Unlogged table warning shown correctly"
424         return 0
425     else
426         log_fail "Init failed with exit code $exit_code"
427         return 1
428     fi
429 }
430
431 test_backup_content_validity() {
432     ((TESTS_RUN++))
433     log_test "Verify backup SQL is valid"
434
435     local dbname="${TEST_DB_PREFIX}_valid"
436     local slot="test_slot_valid"
437     local backup_dir="$TEST_DIR/valid"
438     local restore_dbname="${TEST_DB_PREFIX}_restored"
439
440     # Setup
441     create_test_db "$dbname"
442     create_table_with_pk "$dbname" "test_data"
443     query_db "$dbname" "INSERT INTO test_data (name) VALUES ('Test Row 1'), ('Test Row 2');"
444     mkdir -p "$backup_dir"
445
446     # Run init
447     if ! "$PG_SCRIBE" --init -d "$dbname" -f "$backup_dir" -S "$slot" -U "$PGUSER" &>/dev/null; then
448         log_fail "Init failed"
449         return 1
450     fi
451
452     # Try to restore the backup to a new database
453     create_test_db "$restore_dbname"
454
455     local base_file
456     base_file=$(find "$backup_dir" -maxdepth 1 -name 'base-*.sql' -print -quit)
457     local globals_file
458     globals_file=$(find "$backup_dir" -maxdepth 1 -name 'globals-*.sql' -print -quit)
459
460     # Apply globals (roles, etc.)
461     if ! psql -U "$PGUSER" -d postgres -f "$globals_file" &>/dev/null; then
462         log_fail "Failed to restore globals"
463         return 1
464     fi
465
466     # Apply base backup
467     if ! psql -U "$PGUSER" -d "$restore_dbname" -f "$base_file" &>/dev/null; then
468         log_fail "Failed to restore base backup"
469         return 1
470     fi
471
472     # Verify data
473     local count
474     count=$(query_db "$restore_dbname" "SELECT COUNT(*) FROM test_data;")
475     if [[ "$count" -ne 2 ]]; then
476         log_fail "Expected 2 rows, got $count"
477         return 1
478     fi
479
480     log_pass "Backup SQL is valid and restorable"
481     return 0
482 }
483
484 #
485 # Cleanup
486 #
487
488 # shellcheck disable=SC2317  # Function called via trap handler
489 cleanup() {
490     log_info "Cleaning up test resources..."
491
492     # Drop replication slots
493     for dbname in "${DATABASES_TO_CLEANUP[@]}"; do
494         # Try to drop any slots for this database
495         for slot in test_slot_basic test_slot_nopk test_slot_force test_slot_nonidempotent test_slot_multi test_slot_unlogged test_slot_valid; do
496             drop_replication_slot "$dbname" "$slot" 2>/dev/null || true
497         done
498     done
499
500     # Drop databases
501     for dbname in "${DATABASES_TO_CLEANUP[@]}"; do
502         drop_test_db "$dbname"
503     done
504
505     # Remove test directory
506     if [[ -d "$TEST_DIR" ]]; then
507         rm -rf "$TEST_DIR"
508     fi
509
510     log_info "Cleanup complete"
511 }
512
513 #
514 # Main test runner
515 #
516
517 main() {
518     echo "========================================"
519     echo "pg_scribe --init Test Suite"
520     echo "========================================"
521     echo ""
522
523     # Verify pg_scribe exists
524     if [[ ! -x "$PG_SCRIBE" ]]; then
525         echo "ERROR: pg_scribe not found or not executable: $PG_SCRIBE"
526         exit 1
527     fi
528
529     # Verify PostgreSQL is running
530     if ! psql -U "$PGUSER" -d postgres -c "SELECT 1;" &>/dev/null; then
531         echo "ERROR: Cannot connect to PostgreSQL"
532         exit 1
533     fi
534
535     # Verify wal_level is logical
536     local wal_level
537     wal_level=$(psql -U "$PGUSER" -d postgres -tAq -c "SHOW wal_level;")
538     if [[ "$wal_level" != "logical" ]]; then
539         echo "ERROR: wal_level must be 'logical', currently: $wal_level"
540         echo "Update ~/.pgenv/pgsql/data/postgresql.conf and restart PostgreSQL"
541         exit 1
542     fi
543
544     # Create test directory
545     mkdir -p "$TEST_DIR"
546
547     # Set up cleanup trap
548     trap cleanup EXIT INT TERM
549
550     echo "Running tests..."
551     echo ""
552
553     # Run all tests (use || true to prevent set -e from exiting)
554     test_basic_init_success || true
555     test_init_validation_failure || true
556     test_init_force_flag || true
557     test_init_non_idempotency || true
558     test_init_multiple_tables || true
559     test_init_with_unlogged_table || true
560     test_backup_content_validity || true
561
562     # Summary
563     echo ""
564     echo "========================================"
565     echo "Test Results"
566     echo "========================================"
567     echo "Tests run:    $TESTS_RUN"
568     echo -e "Tests passed: ${GREEN}$TESTS_PASSED${NC}"
569     echo -e "Tests failed: ${RED}$TESTS_FAILED${NC}"
570     echo ""
571
572     if [[ $TESTS_FAILED -eq 0 ]]; then
573         echo -e "${GREEN}All tests passed!${NC}"
574         exit 0
575     else
576         echo -e "${RED}Some tests failed!${NC}"
577         exit 1
578     fi
579 }
580
581 main "$@"