]> begriffs open source - pg_scribe/blob - tests/test_init.sh
Prevent double init, and add test suite
[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 SLOTS_TO_CLEANUP=()
36
37 #
38 # Logging functions
39 #
40
41 log_test() {
42     echo -e "${BLUE}TEST:${NC} $*"
43 }
44
45 log_pass() {
46     echo -e "${GREEN}PASS:${NC} $*"
47     ((TESTS_PASSED++))
48 }
49
50 log_fail() {
51     echo -e "${RED}FAIL:${NC} $*"
52     ((TESTS_FAILED++))
53 }
54
55 log_info() {
56     echo -e "${YELLOW}INFO:${NC} $*"
57 }
58
59 #
60 # Helper functions
61 #
62
63 run_psql() {
64     local dbname="$1"
65     shift
66     psql -U "$PGUSER" -d "$dbname" -tAq "$@"
67 }
68
69 query_db() {
70     local dbname="$1"
71     local query="$2"
72     run_psql "$dbname" -c "$query" 2>/dev/null || true
73 }
74
75 create_test_db() {
76     local dbname="$1"
77     log_info "Creating test database: $dbname"
78
79     # Drop if exists
80     psql -U "$PGUSER" -d postgres -c "DROP DATABASE IF EXISTS $dbname;" &>/dev/null || true
81
82     # Create database
83     psql -U "$PGUSER" -d postgres -c "CREATE DATABASE $dbname;" &>/dev/null
84
85     DATABASES_TO_CLEANUP+=("$dbname")
86 }
87
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 drop_replication_slot() {
104     local dbname="$1"
105     local slot="$2"
106     log_info "Dropping replication slot: $slot"
107
108     # Check if slot exists
109     local exists=$(query_db "$dbname" "
110         SELECT 1 FROM pg_replication_slots WHERE slot_name = '$slot';
111     ")
112
113     if [[ -n "$exists" ]]; then
114         # Drop slot
115         query_db "$dbname" "SELECT pg_drop_replication_slot('$slot');" || true
116     fi
117 }
118
119 check_slot_exists() {
120     local dbname="$1"
121     local slot="$2"
122     local exists=$(query_db "$dbname" "
123         SELECT 1 FROM pg_replication_slots WHERE slot_name = '$slot';
124     ")
125     [[ -n "$exists" ]]
126 }
127
128 check_extension_exists() {
129     local dbname="$1"
130     local extension="$2"
131     local exists=$(query_db "$dbname" "
132         SELECT 1 FROM pg_extension WHERE extname = '$extension';
133     ")
134     [[ -n "$exists" ]]
135 }
136
137 create_table_with_pk() {
138     local dbname="$1"
139     local table="$2"
140     query_db "$dbname" "
141         CREATE TABLE $table (
142             id SERIAL PRIMARY KEY,
143             name TEXT,
144             created_at TIMESTAMP DEFAULT now()
145         );
146     "
147 }
148
149 create_table_without_pk() {
150     local dbname="$1"
151     local table="$2"
152     query_db "$dbname" "
153         CREATE TABLE $table (
154             id INTEGER,
155             name TEXT
156         );
157     "
158 }
159
160 #
161 # Test cases
162 #
163
164 test_basic_init_success() {
165     ((TESTS_RUN++))
166     log_test "Basic --init success"
167
168     local dbname="${TEST_DB_PREFIX}_basic"
169     local slot="test_slot_basic"
170     local backup_dir="$TEST_DIR/basic"
171
172     # Setup
173     create_test_db "$dbname"
174     create_table_with_pk "$dbname" "users"
175     mkdir -p "$backup_dir"
176
177     # Run init
178     if "$PG_SCRIBE" --init -d "$dbname" -f "$backup_dir" -S "$slot" -U "$PGUSER" &>/dev/null; then
179         # Verify slot created
180         if ! check_slot_exists "$dbname" "$slot"; then
181             log_fail "Replication slot not created"
182             return 1
183         fi
184
185         # Verify extension installed
186         if ! check_extension_exists "$dbname" "wal2sql"; then
187             log_fail "wal2sql extension not installed"
188             return 1
189         fi
190
191         # Verify files created (use compgen to check for glob matches)
192         local base_files=("$backup_dir"/base-*.sql)
193         if [[ ! -f "${base_files[0]}" ]]; then
194             log_fail "Base backup file not created"
195             return 1
196         fi
197
198         local globals_files=("$backup_dir"/globals-*.sql)
199         if [[ ! -f "${globals_files[0]}" ]]; then
200             log_fail "Globals backup file not created"
201             return 1
202         fi
203
204         if [[ ! -f "$backup_dir/pg_scribe_metadata.txt" ]]; then
205             log_fail "Metadata file not created"
206             return 1
207         fi
208
209         # Verify metadata content
210         if ! grep -q "Database: $dbname" "$backup_dir/pg_scribe_metadata.txt"; then
211             log_fail "Metadata missing database name"
212             return 1
213         fi
214
215         if ! grep -q "Replication Slot: $slot" "$backup_dir/pg_scribe_metadata.txt"; then
216             log_fail "Metadata missing slot name"
217             return 1
218         fi
219
220         log_pass "Basic init successful"
221         return 0
222     else
223         log_fail "Init command failed"
224         return 1
225     fi
226 }
227
228 test_init_validation_failure() {
229     ((TESTS_RUN++))
230     log_test "Init validation failure (table without replica identity)"
231
232     local dbname="${TEST_DB_PREFIX}_nopk"
233     local slot="test_slot_nopk"
234     local backup_dir="$TEST_DIR/nopk"
235
236     # Setup - create table WITHOUT primary key
237     create_test_db "$dbname"
238     create_table_without_pk "$dbname" "bad_table"
239     mkdir -p "$backup_dir"
240
241     # Run init - should fail with exit code 5
242     local exit_code=0
243     "$PG_SCRIBE" --init -d "$dbname" -f "$backup_dir" -S "$slot" -U "$PGUSER" &>/dev/null || exit_code=$?
244
245     if [[ $exit_code -eq 5 ]]; then
246         # Verify slot was NOT created
247         if check_slot_exists "$dbname" "$slot"; then
248             log_fail "Replication slot should not be created on validation failure"
249             return 1
250         fi
251
252         log_pass "Validation failure detected correctly"
253         return 0
254     else
255         log_fail "Expected exit code 5, got $exit_code"
256         return 1
257     fi
258 }
259
260 test_init_force_flag() {
261     ((TESTS_RUN++))
262     log_test "Init with --force flag (bypass validation)"
263
264     local dbname="${TEST_DB_PREFIX}_force"
265     local slot="test_slot_force"
266     local backup_dir="$TEST_DIR/force"
267
268     # Setup - create table WITHOUT primary key
269     create_test_db "$dbname"
270     create_table_without_pk "$dbname" "bad_table"
271     mkdir -p "$backup_dir"
272
273     # Run init with --force - should succeed despite validation failure
274     if "$PG_SCRIBE" --init -d "$dbname" -f "$backup_dir" -S "$slot" -U "$PGUSER" --force &>/dev/null; then
275         # Verify slot was created
276         if ! check_slot_exists "$dbname" "$slot"; then
277             log_fail "Replication slot should be created with --force"
278             return 1
279         fi
280
281         log_pass "Force flag bypassed validation"
282         return 0
283     else
284         log_fail "Init with --force should succeed"
285         return 1
286     fi
287 }
288
289 test_init_non_idempotency() {
290     ((TESTS_RUN++))
291     log_test "Init refuses to run on already-initialized directory"
292
293     local dbname="${TEST_DB_PREFIX}_nonidempotent"
294     local slot="test_slot_nonidempotent"
295     local backup_dir="$TEST_DIR/nonidempotent"
296
297     # Setup
298     create_test_db "$dbname"
299     create_table_with_pk "$dbname" "users"
300     mkdir -p "$backup_dir"
301
302     # Run init first time
303     if ! "$PG_SCRIBE" --init -d "$dbname" -f "$backup_dir" -S "$slot" -U "$PGUSER" &>/dev/null; then
304         log_fail "First init failed"
305         return 1
306     fi
307
308     # Run init second time - should FAIL with validation error
309     local exit_code=0
310     "$PG_SCRIBE" --init -d "$dbname" -f "$backup_dir" -S "$slot" -U "$PGUSER" &>/dev/null || exit_code=$?
311
312     if [[ $exit_code -eq 5 ]]; then
313         # Verify slot still exists from first init
314         if ! check_slot_exists "$dbname" "$slot"; then
315             log_fail "Replication slot should still exist from first init"
316             return 1
317         fi
318
319         # Verify only 1 base backup file (from first init)
320         local base_count=$(ls "$backup_dir"/base-*.sql 2>/dev/null | wc -l)
321         if [[ $base_count -ne 1 ]]; then
322             log_fail "Expected 1 base backup file, got $base_count"
323             return 1
324         fi
325
326         log_pass "Init correctly refuses to reinitialize"
327         return 0
328     else
329         log_fail "Expected exit code 5 (validation error), got $exit_code"
330         return 1
331     fi
332 }
333
334 test_init_multiple_tables() {
335     ((TESTS_RUN++))
336     log_test "Init with multiple tables"
337
338     local dbname="${TEST_DB_PREFIX}_multi"
339     local slot="test_slot_multi"
340     local backup_dir="$TEST_DIR/multi"
341
342     # Setup - create multiple tables
343     create_test_db "$dbname"
344     create_table_with_pk "$dbname" "users"
345     create_table_with_pk "$dbname" "orders"
346     create_table_with_pk "$dbname" "products"
347
348     # Insert some data
349     query_db "$dbname" "INSERT INTO users (name) VALUES ('Alice'), ('Bob');"
350     query_db "$dbname" "INSERT INTO orders (name) VALUES ('Order1');"
351     query_db "$dbname" "INSERT INTO products (name) VALUES ('Widget');"
352
353     mkdir -p "$backup_dir"
354
355     # Run init
356     if "$PG_SCRIBE" --init -d "$dbname" -f "$backup_dir" -S "$slot" -U "$PGUSER" &>/dev/null; then
357         # Verify base backup contains all tables
358         local base_file=$(ls "$backup_dir"/base-*.sql | head -1)
359
360         if ! grep -q "CREATE TABLE public.users" "$base_file"; then
361             log_fail "Base backup missing users table"
362             return 1
363         fi
364
365         if ! grep -q "CREATE TABLE public.orders" "$base_file"; then
366             log_fail "Base backup missing orders table"
367             return 1
368         fi
369
370         if ! grep -q "CREATE TABLE public.products" "$base_file"; then
371             log_fail "Base backup missing products table"
372             return 1
373         fi
374
375         log_pass "Multiple tables backed up successfully"
376         return 0
377     else
378         log_fail "Init failed"
379         return 1
380     fi
381 }
382
383 test_init_with_unlogged_table() {
384     ((TESTS_RUN++))
385     log_test "Init with unlogged table (warning but success)"
386
387     local dbname="${TEST_DB_PREFIX}_unlogged"
388     local slot="test_slot_unlogged"
389     local backup_dir="$TEST_DIR/unlogged"
390
391     # Setup
392     create_test_db "$dbname"
393     create_table_with_pk "$dbname" "normal_table"
394     query_db "$dbname" "CREATE UNLOGGED TABLE unlogged_table (id SERIAL PRIMARY KEY, data TEXT);"
395     mkdir -p "$backup_dir"
396
397     # Run init - should succeed with warning (exit code 0 or 10)
398     local output_file="$TEST_DIR/unlogged_output.txt"
399     local exit_code=0
400     "$PG_SCRIBE" --init -d "$dbname" -f "$backup_dir" -S "$slot" -U "$PGUSER" &>"$output_file" || exit_code=$?
401
402     # Exit code 0 (success) or 10 (warning) are both acceptable
403     if [[ $exit_code -eq 0 || $exit_code -eq 10 ]]; then
404         # Check for warning message
405         if ! grep -i "unlogged" "$output_file"; then
406             log_fail "Expected warning about unlogged table"
407             return 1
408         fi
409
410         # Verify backup was created
411         local base_files=("$backup_dir"/base-*.sql)
412         if [[ ! -f "${base_files[0]}" ]]; then
413             log_fail "Base backup file not created"
414             return 1
415         fi
416
417         log_pass "Unlogged table warning shown correctly"
418         return 0
419     else
420         log_fail "Init failed with exit code $exit_code"
421         return 1
422     fi
423 }
424
425 test_backup_content_validity() {
426     ((TESTS_RUN++))
427     log_test "Verify backup SQL is valid"
428
429     local dbname="${TEST_DB_PREFIX}_valid"
430     local slot="test_slot_valid"
431     local backup_dir="$TEST_DIR/valid"
432     local restore_dbname="${TEST_DB_PREFIX}_restored"
433
434     # Setup
435     create_test_db "$dbname"
436     create_table_with_pk "$dbname" "test_data"
437     query_db "$dbname" "INSERT INTO test_data (name) VALUES ('Test Row 1'), ('Test Row 2');"
438     mkdir -p "$backup_dir"
439
440     # Run init
441     if ! "$PG_SCRIBE" --init -d "$dbname" -f "$backup_dir" -S "$slot" -U "$PGUSER" &>/dev/null; then
442         log_fail "Init failed"
443         return 1
444     fi
445
446     # Try to restore the backup to a new database
447     create_test_db "$restore_dbname"
448
449     local base_file=$(ls "$backup_dir"/base-*.sql | head -1)
450     local globals_file=$(ls "$backup_dir"/globals-*.sql | head -1)
451
452     # Apply globals (roles, etc.)
453     if ! psql -U "$PGUSER" -d postgres -f "$globals_file" &>/dev/null; then
454         log_fail "Failed to restore globals"
455         return 1
456     fi
457
458     # Apply base backup
459     if ! psql -U "$PGUSER" -d "$restore_dbname" -f "$base_file" &>/dev/null; then
460         log_fail "Failed to restore base backup"
461         return 1
462     fi
463
464     # Verify data
465     local count=$(query_db "$restore_dbname" "SELECT COUNT(*) FROM test_data;")
466     if [[ "$count" -ne 2 ]]; then
467         log_fail "Expected 2 rows, got $count"
468         return 1
469     fi
470
471     log_pass "Backup SQL is valid and restorable"
472     return 0
473 }
474
475 #
476 # Cleanup
477 #
478
479 cleanup() {
480     log_info "Cleaning up test resources..."
481
482     # Drop replication slots
483     for dbname in "${DATABASES_TO_CLEANUP[@]}"; do
484         # Try to drop any slots for this database
485         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
486             drop_replication_slot "$dbname" "$slot" 2>/dev/null || true
487         done
488     done
489
490     # Drop databases
491     for dbname in "${DATABASES_TO_CLEANUP[@]}"; do
492         drop_test_db "$dbname"
493     done
494
495     # Remove test directory
496     if [[ -d "$TEST_DIR" ]]; then
497         rm -rf "$TEST_DIR"
498     fi
499
500     log_info "Cleanup complete"
501 }
502
503 #
504 # Main test runner
505 #
506
507 main() {
508     echo "========================================"
509     echo "pg_scribe --init Test Suite"
510     echo "========================================"
511     echo ""
512
513     # Verify pg_scribe exists
514     if [[ ! -x "$PG_SCRIBE" ]]; then
515         echo "ERROR: pg_scribe not found or not executable: $PG_SCRIBE"
516         exit 1
517     fi
518
519     # Verify PostgreSQL is running
520     if ! psql -U "$PGUSER" -d postgres -c "SELECT 1;" &>/dev/null; then
521         echo "ERROR: Cannot connect to PostgreSQL"
522         exit 1
523     fi
524
525     # Verify wal_level is logical
526     local wal_level=$(psql -U "$PGUSER" -d postgres -tAq -c "SHOW wal_level;")
527     if [[ "$wal_level" != "logical" ]]; then
528         echo "ERROR: wal_level must be 'logical', currently: $wal_level"
529         echo "Update ~/.pgenv/pgsql/data/postgresql.conf and restart PostgreSQL"
530         exit 1
531     fi
532
533     # Create test directory
534     mkdir -p "$TEST_DIR"
535
536     # Set up cleanup trap
537     trap cleanup EXIT INT TERM
538
539     echo "Running tests..."
540     echo ""
541
542     # Run all tests (use || true to prevent set -e from exiting)
543     test_basic_init_success || true
544     test_init_validation_failure || true
545     test_init_force_flag || true
546     test_init_non_idempotency || true
547     test_init_multiple_tables || true
548     test_init_with_unlogged_table || true
549     test_backup_content_validity || true
550
551     # Summary
552     echo ""
553     echo "========================================"
554     echo "Test Results"
555     echo "========================================"
556     echo "Tests run:    $TESTS_RUN"
557     echo -e "Tests passed: ${GREEN}$TESTS_PASSED${NC}"
558     echo -e "Tests failed: ${RED}$TESTS_FAILED${NC}"
559     echo ""
560
561     if [[ $TESTS_FAILED -eq 0 ]]; then
562         echo -e "${GREEN}All tests passed!${NC}"
563         exit 0
564     else
565         echo -e "${RED}Some tests failed!${NC}"
566         exit 1
567     fi
568 }
569
570 main "$@"