3 # Test suite for pg_scribe --init command
6 # - Creates temporary test databases
7 # - Tests various --init scenarios
8 # - Verifies expected outcomes
9 # - Cleans up all resources
14 # Colors for test output
19 NC='\033[0m' # No Color
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}"
34 DATABASES_TO_CLEANUP=()
41 echo -e "${BLUE}TEST:${NC} $*"
45 echo -e "${GREEN}PASS:${NC} $*"
50 echo -e "${RED}FAIL:${NC} $*"
55 echo -e "${YELLOW}INFO:${NC} $*"
65 psql -U "$PGUSER" -d "$dbname" -tAq "$@"
71 run_psql "$dbname" -c "$query" 2>/dev/null || true
76 log_info "Creating test database: $dbname"
79 psql -U "$PGUSER" -d postgres -c "DROP DATABASE IF EXISTS $dbname;" &>/dev/null || true
82 psql -U "$PGUSER" -d postgres -c "CREATE DATABASE $dbname;" &>/dev/null
84 DATABASES_TO_CLEANUP+=("$dbname")
87 # shellcheck disable=SC2317 # Function called from cleanup trap handler
90 log_info "Dropping test database: $dbname"
92 # Terminate connections
93 psql -U "$PGUSER" -d postgres -c "
94 SELECT pg_terminate_backend(pid)
96 WHERE datname = '$dbname' AND pid <> pg_backend_pid();
100 psql -U "$PGUSER" -d postgres -c "DROP DATABASE IF EXISTS $dbname;" &>/dev/null || true
103 # shellcheck disable=SC2317 # Function called from cleanup trap handler
104 drop_replication_slot() {
107 log_info "Dropping replication slot: $slot"
109 # Check if slot exists
111 exists=$(query_db "$dbname" "
112 SELECT 1 FROM pg_replication_slots WHERE slot_name = '$slot';
115 if [[ -n "$exists" ]]; then
117 query_db "$dbname" "SELECT pg_drop_replication_slot('$slot');" || true
121 check_slot_exists() {
125 exists=$(query_db "$dbname" "
126 SELECT 1 FROM pg_replication_slots WHERE slot_name = '$slot';
131 check_extension_exists() {
135 exists=$(query_db "$dbname" "
136 SELECT 1 FROM pg_extension WHERE extname = '$extension';
141 create_table_with_pk() {
145 CREATE TABLE $table (
146 id SERIAL PRIMARY KEY,
148 created_at TIMESTAMP DEFAULT now()
153 create_table_without_pk() {
157 CREATE TABLE $table (
168 test_basic_init_success() {
170 log_test "Basic --init success"
172 local dbname="${TEST_DB_PREFIX}_basic"
173 local slot="test_slot_basic"
174 local backup_dir="$TEST_DIR/basic"
177 create_test_db "$dbname"
178 create_table_with_pk "$dbname" "users"
179 mkdir -p "$backup_dir"
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"
189 # Verify extension installed
190 if ! check_extension_exists "$dbname" "wal2sql"; then
191 log_fail "wal2sql extension not installed"
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"
202 local chain_dir="${chain_dirs[0]}"
204 # Verify files in chain directory
205 if [[ ! -f "$chain_dir/base.sql" ]]; then
206 log_fail "Base backup file not created in chain"
210 if [[ ! -f "$chain_dir/globals.sql" ]]; then
211 log_fail "Globals backup file not created in chain"
215 if [[ ! -f "$chain_dir/metadata.json" ]]; then
216 log_fail "Metadata file not created in chain"
220 # Verify pidfile placeholder created
221 if [[ ! -f "$backup_dir/.pg_scribe.pid" ]]; then
222 log_fail "Pidfile placeholder not created"
226 # Verify metadata content (JSON format)
227 if ! grep -q "\"database\": \"$dbname\"" "$chain_dir/metadata.json"; then
228 log_fail "Metadata missing database name"
232 if ! grep -q "\"replication_slot\": \"$slot\"" "$chain_dir/metadata.json"; then
233 log_fail "Metadata missing slot name"
237 log_pass "Basic init successful"
240 log_fail "Init command failed"
245 test_init_validation_failure() {
247 log_test "Init validation failure (table without replica identity)"
249 local dbname="${TEST_DB_PREFIX}_nopk"
250 local slot="test_slot_nopk"
251 local backup_dir="$TEST_DIR/nopk"
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"
258 # Run init - should fail with exit code 5
260 "$PG_SCRIBE" --init -d "$dbname" -f "$backup_dir" -S "$slot" -U "$PGUSER" &>/dev/null || exit_code=$?
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"
269 log_pass "Validation failure detected correctly"
272 log_fail "Expected exit code 5, got $exit_code"
277 test_init_force_flag() {
279 log_test "Init with --force flag (bypass validation)"
281 local dbname="${TEST_DB_PREFIX}_force"
282 local slot="test_slot_force"
283 local backup_dir="$TEST_DIR/force"
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"
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"
298 log_pass "Force flag bypassed validation"
301 log_fail "Init with --force should succeed"
306 test_init_non_idempotency() {
308 log_test "Init refuses to run on already-initialized directory"
310 local dbname="${TEST_DB_PREFIX}_nonidempotent"
311 local slot="test_slot_nonidempotent"
312 local backup_dir="$TEST_DIR/nonidempotent"
315 create_test_db "$dbname"
316 create_table_with_pk "$dbname" "users"
317 mkdir -p "$backup_dir"
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"
325 # Run init second time - should FAIL with validation error
327 "$PG_SCRIBE" --init -d "$dbname" -f "$backup_dir" -S "$slot" -U "$PGUSER" &>/dev/null || exit_code=$?
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"
336 # Verify only 1 chain directory (from first init)
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"
344 log_pass "Init correctly refuses to reinitialize"
347 log_fail "Expected exit code 5 (validation error), got $exit_code"
352 test_init_multiple_tables() {
354 log_test "Init with multiple tables"
356 local dbname="${TEST_DB_PREFIX}_multi"
357 local slot="test_slot_multi"
358 local backup_dir="$TEST_DIR/multi"
360 # Setup - create multiple tables
361 create_test_db "$dbname"
362 create_table_with_pk "$dbname" "users"
363 create_table_with_pk "$dbname" "orders"
364 create_table_with_pk "$dbname" "products"
367 query_db "$dbname" "INSERT INTO users (name) VALUES ('Alice'), ('Bob');"
368 query_db "$dbname" "INSERT INTO orders (name) VALUES ('Order1');"
369 query_db "$dbname" "INSERT INTO products (name) VALUES ('Widget');"
371 mkdir -p "$backup_dir"
374 if "$PG_SCRIBE" --init -d "$dbname" -f "$backup_dir" -S "$slot" -U "$PGUSER" &>/dev/null; then
375 # Verify base backup contains all tables
376 local chain_dirs=("$backup_dir"/chain-*)
377 local base_file="${chain_dirs[0]}/base.sql"
379 if ! grep -q "CREATE TABLE public.users" "$base_file"; then
380 log_fail "Base backup missing users table"
384 if ! grep -q "CREATE TABLE public.orders" "$base_file"; then
385 log_fail "Base backup missing orders table"
389 if ! grep -q "CREATE TABLE public.products" "$base_file"; then
390 log_fail "Base backup missing products table"
394 log_pass "Multiple tables backed up successfully"
397 log_fail "Init failed"
402 test_init_with_unlogged_table() {
404 log_test "Init with unlogged table (warning but success)"
406 local dbname="${TEST_DB_PREFIX}_unlogged"
407 local slot="test_slot_unlogged"
408 local backup_dir="$TEST_DIR/unlogged"
411 create_test_db "$dbname"
412 create_table_with_pk "$dbname" "normal_table"
413 query_db "$dbname" "CREATE UNLOGGED TABLE unlogged_table (id SERIAL PRIMARY KEY, data TEXT);"
414 mkdir -p "$backup_dir"
416 # Run init - should succeed with warning (exit code 0 or 10)
417 local output_file="$TEST_DIR/unlogged_output.txt"
419 "$PG_SCRIBE" --init -d "$dbname" -f "$backup_dir" -S "$slot" -U "$PGUSER" &>"$output_file" || exit_code=$?
421 # Exit code 0 (success) or 10 (warning) are both acceptable
422 if [[ $exit_code -eq 0 || $exit_code -eq 10 ]]; then
423 # Check for warning message
424 if ! grep -i "unlogged" "$output_file"; then
425 log_fail "Expected warning about unlogged table"
429 # Verify backup was created
430 local chain_dirs=("$backup_dir"/chain-*)
431 if [[ ! -f "${chain_dirs[0]}/base.sql" ]]; then
432 log_fail "Base backup file not created"
436 log_pass "Unlogged table warning shown correctly"
439 log_fail "Init failed with exit code $exit_code"
444 test_backup_content_validity() {
446 log_test "Verify backup SQL is valid"
448 local dbname="${TEST_DB_PREFIX}_valid"
449 local slot="test_slot_valid"
450 local backup_dir="$TEST_DIR/valid"
451 local restore_dbname="${TEST_DB_PREFIX}_restored"
454 create_test_db "$dbname"
455 create_table_with_pk "$dbname" "test_data"
456 query_db "$dbname" "INSERT INTO test_data (name) VALUES ('Test Row 1'), ('Test Row 2');"
457 mkdir -p "$backup_dir"
460 if ! "$PG_SCRIBE" --init -d "$dbname" -f "$backup_dir" -S "$slot" -U "$PGUSER" &>/dev/null; then
461 log_fail "Init failed"
465 # Try to restore the backup to a new database
466 create_test_db "$restore_dbname"
468 # Find chain directory
469 local chain_dirs=("$backup_dir"/chain-*)
470 local chain_dir="${chain_dirs[0]}"
472 local base_file="$chain_dir/base.sql"
473 local globals_file="$chain_dir/globals.sql"
475 # Apply globals (roles, etc.)
476 if ! psql -U "$PGUSER" -d postgres -f "$globals_file" &>/dev/null; then
477 log_fail "Failed to restore globals"
482 if ! psql -U "$PGUSER" -d "$restore_dbname" -f "$base_file" &>/dev/null; then
483 log_fail "Failed to restore base backup"
489 count=$(query_db "$restore_dbname" "SELECT COUNT(*) FROM test_data;")
490 if [[ "$count" -ne 2 ]]; then
491 log_fail "Expected 2 rows, got $count"
495 log_pass "Backup SQL is valid and restorable"
503 # shellcheck disable=SC2317 # Function called via trap handler
505 log_info "Cleaning up test resources..."
507 # Drop replication slots
508 for dbname in "${DATABASES_TO_CLEANUP[@]}"; do
509 # Try to drop any slots for this database
510 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
511 drop_replication_slot "$dbname" "$slot" 2>/dev/null || true
516 for dbname in "${DATABASES_TO_CLEANUP[@]}"; do
517 drop_test_db "$dbname"
520 # Remove test directory
521 if [[ -d "$TEST_DIR" ]]; then
525 log_info "Cleanup complete"
533 echo "========================================"
534 echo "pg_scribe --init Test Suite"
535 echo "========================================"
538 # Verify pg_scribe exists
539 if [[ ! -x "$PG_SCRIBE" ]]; then
540 echo "ERROR: pg_scribe not found or not executable: $PG_SCRIBE"
544 # Verify PostgreSQL is running
545 if ! psql -U "$PGUSER" -d postgres -c "SELECT 1;" &>/dev/null; then
546 echo "ERROR: Cannot connect to PostgreSQL"
550 # Verify wal_level is logical
552 wal_level=$(psql -U "$PGUSER" -d postgres -tAq -c "SHOW wal_level;")
553 if [[ "$wal_level" != "logical" ]]; then
554 echo "ERROR: wal_level must be 'logical', currently: $wal_level"
555 echo "Update ~/.pgenv/pgsql/data/postgresql.conf and restart PostgreSQL"
559 # Create test directory
562 # Set up cleanup trap
563 trap cleanup EXIT INT TERM
565 echo "Running tests..."
568 # Run all tests (use || true to prevent set -e from exiting)
569 test_basic_init_success || true
570 test_init_validation_failure || true
571 test_init_force_flag || true
572 test_init_non_idempotency || true
573 test_init_multiple_tables || true
574 test_init_with_unlogged_table || true
575 test_backup_content_validity || true
579 echo "========================================"
581 echo "========================================"
582 echo "Tests run: $TESTS_RUN"
583 echo -e "Tests passed: ${GREEN}$TESTS_PASSED${NC}"
584 echo -e "Tests failed: ${RED}$TESTS_FAILED${NC}"
587 if [[ $TESTS_FAILED -eq 0 ]]; then
588 echo -e "${GREEN}All tests passed!${NC}"
591 echo -e "${RED}Some tests failed!${NC}"