3 # Test suite for pg_scribe --new-chain command
6 # - Creates temporary test databases
7 # - Tests various --new-chain 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 create_table_with_pk() {
107 CREATE TABLE $table (
108 id SERIAL PRIMARY KEY,
110 created_at TIMESTAMP DEFAULT now()
115 # Initialize a backup directory (creates replication slot and initial backups)
116 init_backup_system() {
118 local backup_dir="$2"
121 mkdir -p "$backup_dir"
122 "$PG_SCRIBE" --init -d "$dbname" -f "$backup_dir" -S "$slot" -U "$PGUSER" &>/dev/null
129 test_new_chain_requires_args() {
131 log_test "New chain requires database and directory"
136 "$PG_SCRIBE" --new-chain -f /tmp/test &>/dev/null || exit_code=$?
137 if [[ $exit_code -ne 5 ]]; then
138 log_fail "Should fail with exit code 5 when missing database"
144 "$PG_SCRIBE" --new-chain -d testdb &>/dev/null || exit_code=$?
145 if [[ $exit_code -ne 5 ]]; then
146 log_fail "Should fail with exit code 5 when missing directory"
150 log_pass "Argument validation works"
154 test_new_chain_directory_must_exist() {
156 log_test "New chain requires existing directory"
158 local dbname="${TEST_DB_PREFIX}_dircheck"
159 local backup_dir="$TEST_DIR/nonexistent_dir"
161 create_test_db "$dbname"
162 create_table_with_pk "$dbname" "users"
164 # Try to create chain in non-existent directory
166 "$PG_SCRIBE" --new-chain -d "$dbname" -f "$backup_dir" -U "$PGUSER" &>/dev/null || exit_code=$?
168 if [[ $exit_code -eq 4 ]]; then
169 log_pass "Correctly rejects non-existent directory"
172 log_fail "Expected exit code 4, got $exit_code"
177 test_new_chain_basic_success() {
179 log_test "Basic new chain success (no compression)"
181 local dbname="${TEST_DB_PREFIX}_basic"
182 local backup_dir="$TEST_DIR/basic"
183 local slot="test_slot_basic"
185 # Setup - initialize backup system first
186 create_test_db "$dbname"
187 create_table_with_pk "$dbname" "users"
188 query_db "$dbname" "INSERT INTO users (name) VALUES ('Alice'), ('Bob'), ('Charlie');"
189 init_backup_system "$dbname" "$backup_dir" "$slot"
191 # Sleep to ensure different timestamp
194 # Create a new chain without compression
195 if "$PG_SCRIBE" --new-chain -d "$dbname" -f "$backup_dir" -Z none -U "$PGUSER" &>/dev/null; then
196 # Count chain directories (should have 2: 1 from init, 1 from new-chain)
198 chain_count=$(find "$backup_dir" -maxdepth 1 -type d -name 'chain-*' 2>/dev/null | wc -l)
200 if [[ $chain_count -ne 2 ]]; then
201 log_fail "Expected 2 chain directories, got $chain_count"
205 # Get latest chain directory
207 latest_chain=$(find "$backup_dir" -maxdepth 1 -type d -name 'chain-*' 2>/dev/null | sort | tail -1)
209 # Verify chain structure
210 if [[ ! -f "$latest_chain/base.sql" ]]; then
211 log_fail "base.sql not found in latest chain"
215 if [[ ! -f "$latest_chain/globals.sql" ]]; then
216 log_fail "globals.sql not found in latest chain"
220 if [[ ! -f "$latest_chain/metadata.json" ]]; then
221 log_fail "metadata.json not found in latest chain"
225 # Verify backup content
226 if ! grep -q "CREATE TABLE public.users" "$latest_chain/base.sql"; then
227 log_fail "Base backup missing table definition"
231 if ! grep -q "Alice" "$latest_chain/base.sql"; then
232 log_fail "Base backup missing data"
236 # Verify metadata content (JSON format)
237 if ! grep -q "\"database\": \"$dbname\"" "$latest_chain/metadata.json"; then
238 log_fail "Metadata missing database name"
242 log_pass "Basic new chain successful"
245 log_fail "New chain command failed"
250 test_new_chain_with_gzip_compression() {
252 log_test "New chain with gzip compression"
254 local dbname="${TEST_DB_PREFIX}_gzip"
255 local backup_dir="$TEST_DIR/gzip"
256 local slot="test_slot_gzip"
259 create_test_db "$dbname"
260 create_table_with_pk "$dbname" "data_table"
261 # Add enough data to see compression benefit
262 query_db "$dbname" "INSERT INTO data_table (name) SELECT 'Row ' || generate_series(1, 1000);"
263 init_backup_system "$dbname" "$backup_dir" "$slot"
265 # Sleep to ensure different timestamp
268 # Create new chain with gzip compression
269 if "$PG_SCRIBE" --new-chain -d "$dbname" -f "$backup_dir" -Z gzip -U "$PGUSER" &>/dev/null; then
270 # Get latest chain directory
272 latest_chain=$(find "$backup_dir" -maxdepth 1 -type d -name 'chain-*' 2>/dev/null | sort | tail -1)
274 # Check for compressed base.sql in chain directory
275 if [[ ! -f "$latest_chain/base.sql.gz" ]]; then
276 log_fail "No gzip-compressed base backup found in chain"
280 # Globals are not compressed (too small to benefit)
281 if [[ ! -f "$latest_chain/globals.sql" ]]; then
282 log_fail "No globals backup found in chain"
286 # Verify we can decompress and read the backup
287 if ! gunzip -t "$latest_chain/base.sql.gz" &>/dev/null; then
288 log_fail "Compressed backup file is invalid"
293 if ! gunzip -c "$latest_chain/base.sql.gz" | grep -q "CREATE TABLE public.data_table"; then
294 log_fail "Compressed backup missing table definition"
298 log_pass "Gzip compression successful"
301 log_fail "New chain with gzip failed"
306 test_new_chain_no_compression() {
308 log_test "New chain with no compression (default)"
310 local dbname="${TEST_DB_PREFIX}_nocomp"
311 local backup_dir="$TEST_DIR/nocomp"
312 local slot="test_slot_nocomp"
315 create_test_db "$dbname"
316 create_table_with_pk "$dbname" "data_table"
317 query_db "$dbname" "INSERT INTO data_table (name) SELECT 'Row ' || generate_series(1, 500);"
318 init_backup_system "$dbname" "$backup_dir" "$slot"
320 # Sleep to ensure different timestamp
323 # Create new chain with default compression (none)
324 if "$PG_SCRIBE" --new-chain -d "$dbname" -f "$backup_dir" -U "$PGUSER" &>/dev/null; then
325 # Get latest chain directory
327 latest_chain=$(find "$backup_dir" -maxdepth 1 -type d -name 'chain-*' 2>/dev/null | sort | tail -1)
329 # Check for uncompressed base.sql in chain directory
330 if [[ ! -f "$latest_chain/base.sql" ]]; then
331 log_fail "No uncompressed base backup found in chain"
335 # Globals are not compressed (too small to benefit)
336 if [[ ! -f "$latest_chain/globals.sql" ]]; then
337 log_fail "No globals backup found in chain"
342 if ! grep -q "CREATE TABLE public.data_table" "$latest_chain/base.sql"; then
343 log_fail "Base backup missing table definition"
347 log_pass "No compression (default) successful"
350 log_fail "New chain with default compression failed"
355 test_new_chain_multiple_times() {
357 log_test "Multiple new chains (retention simulation)"
359 local dbname="${TEST_DB_PREFIX}_multi"
360 local backup_dir="$TEST_DIR/multi"
361 local slot="test_slot_multi"
364 create_test_db "$dbname"
365 create_table_with_pk "$dbname" "counter"
366 init_backup_system "$dbname" "$backup_dir" "$slot"
368 # Create multiple new chains with data changes
370 query_db "$dbname" "INSERT INTO counter (name) VALUES ('Iteration $i');"
371 sleep 1 # Ensure different timestamps
373 if ! "$PG_SCRIBE" --new-chain -d "$dbname" -f "$backup_dir" -Z none -U "$PGUSER" &>/dev/null; then
374 log_fail "New chain $i failed"
379 # Count total chains (1 from init + 3 from new-chain = 4)
381 chain_count=$(find "$backup_dir" -maxdepth 1 -type d -name 'chain-*' 2>/dev/null | wc -l)
383 if [[ $chain_count -ne 4 ]]; then
384 log_fail "Expected 4 chain directories, got $chain_count"
388 # Verify latest chain has all data
390 latest_chain=$(find "$backup_dir" -maxdepth 1 -type d -name 'chain-*' 2>/dev/null | sort | tail -1)
393 if ! grep -q "Iteration $i" "$latest_chain/base.sql"; then
394 log_fail "Latest chain missing data from iteration $i"
399 log_pass "Multiple new chains successful"
403 test_new_chain_restorability() {
405 log_test "New chain is restorable"
407 local dbname="${TEST_DB_PREFIX}_restore"
408 local backup_dir="$TEST_DIR/restore"
409 local slot="test_slot_restore"
410 local restore_dbname="${TEST_DB_PREFIX}_restored"
413 create_test_db "$dbname"
414 create_table_with_pk "$dbname" "products"
415 query_db "$dbname" "INSERT INTO products (name) VALUES ('Widget'), ('Gadget'), ('Doohickey');"
416 init_backup_system "$dbname" "$backup_dir" "$slot"
419 if ! "$PG_SCRIBE" --new-chain -d "$dbname" -f "$backup_dir" -Z none -U "$PGUSER" &>/dev/null; then
420 log_fail "New chain failed"
424 # Use pg_scribe --restore to restore the chain
425 if ! "$PG_SCRIBE" --restore -d "$restore_dbname" -f "$backup_dir" -C -U "$PGUSER" &>/dev/null; then
426 log_fail "Restore failed"
430 DATABASES_TO_CLEANUP+=("$restore_dbname")
432 # Verify restored data
434 count=$(query_db "$restore_dbname" "SELECT COUNT(*) FROM products;")
435 if [[ "$count" -ne 3 ]]; then
436 log_fail "Expected 3 rows, got $count"
441 widget_exists=$(query_db "$restore_dbname" "SELECT COUNT(*) FROM products WHERE name = 'Widget';")
442 if [[ "$widget_exists" -ne 1 ]]; then
443 log_fail "Expected to find Widget in restored data"
447 log_pass "New chain is restorable"
451 test_new_chain_with_complex_schema() {
453 log_test "New chain with complex schema (indexes, constraints)"
455 local dbname="${TEST_DB_PREFIX}_complex"
456 local backup_dir="$TEST_DIR/complex"
457 local slot="test_slot_complex"
459 # Setup with complex schema
460 create_test_db "$dbname"
463 CREATE TABLE authors (
464 id SERIAL PRIMARY KEY,
465 name TEXT NOT NULL UNIQUE,
470 id SERIAL PRIMARY KEY,
472 author_id INTEGER REFERENCES authors(id) ON DELETE CASCADE,
477 CREATE INDEX idx_books_title ON books(title);
478 CREATE INDEX idx_books_published ON books(published_date);
482 INSERT INTO authors (name, email) VALUES ('John Doe', 'john@example.com');
483 INSERT INTO books (title, author_id, published_date, isbn)
484 VALUES ('Test Book', 1, '2024-01-01', '1234567890');
487 init_backup_system "$dbname" "$backup_dir" "$slot"
489 # Sleep to ensure different timestamp
493 if ! "$PG_SCRIBE" --new-chain -d "$dbname" -f "$backup_dir" -Z none -U "$PGUSER" &>/dev/null; then
494 log_fail "New chain failed"
498 # Get latest chain and verify backup contains schema elements
500 latest_chain=$(find "$backup_dir" -maxdepth 1 -type d -name 'chain-*' 2>/dev/null | sort | tail -1)
502 if ! grep -q "CREATE TABLE public.authors" "$latest_chain/base.sql"; then
503 log_fail "Backup missing authors table"
507 if ! grep -q "CREATE TABLE public.books" "$latest_chain/base.sql"; then
508 log_fail "Backup missing books table"
512 if ! grep -q "UNIQUE" "$latest_chain/base.sql"; then
513 log_fail "Backup missing unique constraints"
517 if ! grep -q "REFERENCES" "$latest_chain/base.sql"; then
518 log_fail "Backup missing foreign key"
522 if ! grep -q "CREATE INDEX" "$latest_chain/base.sql"; then
523 log_fail "Backup missing indexes"
527 log_pass "Complex schema backed up successfully"
531 test_new_chain_metadata_tracking() {
533 log_test "Metadata file tracks chain information"
535 local dbname="${TEST_DB_PREFIX}_metadata"
536 local backup_dir="$TEST_DIR/metadata"
537 local slot="test_slot_metadata"
540 create_test_db "$dbname"
541 create_table_with_pk "$dbname" "test_table"
542 init_backup_system "$dbname" "$backup_dir" "$slot"
544 # Sleep to ensure different timestamp
548 if ! "$PG_SCRIBE" --new-chain -d "$dbname" -f "$backup_dir" -Z none -U "$PGUSER" &>/dev/null; then
549 log_fail "New chain failed"
553 # Get latest chain and verify metadata content
555 latest_chain=$(find "$backup_dir" -maxdepth 1 -type d -name 'chain-*' 2>/dev/null | sort | tail -1)
556 local metadata_file="$latest_chain/metadata.json"
558 if [[ ! -f "$metadata_file" ]]; then
559 log_fail "Metadata file not found in chain"
563 if ! grep -q "\"database\": \"$dbname\"" "$metadata_file"; then
564 log_fail "Metadata missing database name"
568 if ! grep -q "\"created\":" "$metadata_file"; then
569 log_fail "Metadata missing created timestamp"
573 log_pass "Metadata tracking works correctly"
577 test_new_chain_with_start_flag() {
579 log_test "New chain with --start flag validates arguments"
581 local dbname="${TEST_DB_PREFIX}_start_flag"
582 local backup_dir="$TEST_DIR/start_flag"
583 local slot="test_slot_start_flag"
586 create_test_db "$dbname"
587 create_table_with_pk "$dbname" "test_table"
588 init_backup_system "$dbname" "$backup_dir" "$slot"
590 # Test 1: --new-chain --start should require database name
592 "$PG_SCRIBE" --new-chain --start -f "$backup_dir" -U "$PGUSER" &>/dev/null || exit_code=$?
594 if [[ $exit_code -ne 5 ]]; then
595 log_fail "Should require database name with --start, got exit code $exit_code"
599 # Test 2: Verify --start flag is accepted with proper arguments
600 # Note: We can't fully test the background behavior in a simple test,
601 # but we can verify the command parses correctly and would start
602 # (We won't actually let it start streaming to avoid complexity)
604 # Sleep to ensure different timestamp for chain
607 # Create new chain WITHOUT --start flag (to verify it works)
608 if ! "$PG_SCRIBE" --new-chain -d "$dbname" -f "$backup_dir" -Z none -U "$PGUSER" &>/dev/null; then
609 log_fail "New chain creation failed"
613 # Verify 2 chains exist now
615 chain_count=$(find "$backup_dir" -maxdepth 1 -type d -name 'chain-*' 2>/dev/null | wc -l)
617 if [[ $chain_count -ne 2 ]]; then
618 log_fail "Expected 2 chains, got $chain_count"
622 log_pass "--start flag argument parsing works correctly"
630 # shellcheck disable=SC2317 # Function called via trap handler
632 log_info "Cleaning up test resources..."
634 # Drop replication slots (before dropping databases)
635 for dbname in "${DATABASES_TO_CLEANUP[@]}"; do
636 for slot in test_slot_basic test_slot_gzip test_slot_nocomp test_slot_multi test_slot_restore test_slot_complex test_slot_metadata test_slot_start_flag; do
638 psql -U "$PGUSER" -d "$dbname" -c "
639 SELECT pg_drop_replication_slot('$slot')
640 FROM pg_replication_slots
641 WHERE slot_name = '$slot';
642 " &>/dev/null || true
647 for dbname in "${DATABASES_TO_CLEANUP[@]}"; do
648 drop_test_db "$dbname"
651 # Remove test directory
652 if [[ -d "$TEST_DIR" ]]; then
656 log_info "Cleanup complete"
664 echo "========================================"
665 echo "pg_scribe --new-chain Test Suite"
666 echo "========================================"
669 # Verify pg_scribe exists
670 if [[ ! -x "$PG_SCRIBE" ]]; then
671 echo "ERROR: pg_scribe not found or not executable: $PG_SCRIBE"
675 # Verify PostgreSQL is running
676 if ! psql -U "$PGUSER" -d postgres -c "SELECT 1;" &>/dev/null; then
677 echo "ERROR: Cannot connect to PostgreSQL"
681 # Verify wal_level is logical
683 wal_level=$(psql -U "$PGUSER" -d postgres -tAq -c "SHOW wal_level;")
684 if [[ "$wal_level" != "logical" ]]; then
685 echo "ERROR: wal_level must be 'logical', currently: $wal_level"
686 echo "Update ~/.pgenv/pgsql/data/postgresql.conf and restart PostgreSQL"
690 # Create test directory
693 # Set up cleanup trap
694 trap cleanup EXIT INT TERM
696 echo "Running tests..."
699 # Run all tests (use || true to prevent set -e from exiting)
700 test_new_chain_requires_args || true
701 test_new_chain_directory_must_exist || true
702 test_new_chain_basic_success || true
703 test_new_chain_with_gzip_compression || true
704 test_new_chain_no_compression || true
705 test_new_chain_multiple_times || true
706 test_new_chain_restorability || true
707 test_new_chain_with_complex_schema || true
708 test_new_chain_metadata_tracking || true
709 test_new_chain_with_start_flag || true
713 echo "========================================"
715 echo "========================================"
716 echo "Tests run: $TESTS_RUN"
717 echo -e "Tests passed: ${GREEN}$TESTS_PASSED${NC}"
718 echo -e "Tests failed: ${RED}$TESTS_FAILED${NC}"
721 if [[ $TESTS_FAILED -eq 0 ]]; then
722 echo -e "${GREEN}All tests passed!${NC}"
725 echo -e "${RED}Some tests failed!${NC}"