"""Tests for backup module."""
import json
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
from gnexus_creds import backup as backup_module
from gnexus_creds.backup import _backup_dir, create_backup, list_backups, restore_backup
@pytest.fixture
def sqlite_settings():
s = MagicMock()
s.database_url = "sqlite:///./test_data.db"
s.backup_dir = "./test_backups"
return s
@pytest.fixture
def pg_settings():
s = MagicMock()
s.database_url = "postgresql+psycopg://user:pass@localhost/db"
s.backup_dir = "./test_backups"
return s
@pytest.fixture(autouse=True)
def clean_test_backups(tmp_path):
backup_dir = tmp_path / "test_backups"
backup_dir.mkdir(exist_ok=True)
with patch.object(backup_module, "get_settings") as mock:
s = MagicMock()
s.backup_dir = str(backup_dir)
mock.return_value = s
yield
class TestNormalizePgUrl:
def test_strips_psycopg_suffix(self):
result = backup_module._normalize_pg_url("postgresql+psycopg://user:pass@localhost/db")
assert result == "postgresql://user:pass@localhost/db"
def test_leaves_plain_postgresql(self):
result = backup_module._normalize_pg_url("postgresql://user:pass@localhost/db")
assert result == "postgresql://user:pass@localhost/db"
class TestBackupDir:
def test_creates_directory_if_missing(self, tmp_path):
settings = MagicMock()
settings.backup_dir = str(tmp_path / "new_backups")
result = _backup_dir(settings)
assert result.exists()
assert result.is_dir()
class TestCreateBackupSqlite:
def test_copies_db_file(self, tmp_path):
db_path = tmp_path / "test.db"
db_path.write_text("sqlite content")
settings = MagicMock()
settings.database_url = f"sqlite:///{db_path}"
settings.backup_dir = str(tmp_path / "backups")
result = create_backup(settings)
assert result["engine"] == "sqlite"
assert result["filename"].startswith("backup_")
assert result["filename"].endswith(".db")
assert Path(result["path"]).exists()
assert Path(result["path"]).read_text() == "sqlite content"
class TestCreateBackupPostgresql:
def test_runs_pg_dump(self, tmp_path):
settings = MagicMock()
settings.database_url = "postgresql+psycopg://user:pass@localhost/db"
settings.backup_dir = str(tmp_path / "backups")
with patch("gnexus_creds.backup.subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0, stdout="-- pg_dump output")
result = create_backup(settings)
assert result["engine"] == "postgresql"
assert result["filename"].startswith("backup_")
assert result["filename"].endswith(".sql")
assert Path(result["path"]).exists()
assert Path(result["path"]).read_text() == "-- pg_dump output"
mock_run.assert_called_once()
args = mock_run.call_args[0][0]
assert args[0] == "pg_dump"
assert "postgresql://user:pass@localhost/db" in args
def test_raises_on_pg_dump_failure(self, tmp_path):
settings = MagicMock()
settings.database_url = "postgresql+psycopg://user:pass@localhost/db"
settings.backup_dir = str(tmp_path / "backups")
with patch("gnexus_creds.backup.subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=1, stderr="connection failed")
with pytest.raises(RuntimeError, match="pg_dump failed"):
create_backup(settings)
class TestListBackups:
def test_lists_backup_files(self, tmp_path):
backup_dir = tmp_path / "backups"
backup_dir.mkdir()
(backup_dir / "backup_20260101_120000.sql").write_text("content")
(backup_dir / "other_file.txt").write_text("ignore me")
settings = MagicMock()
settings.backup_dir = str(backup_dir)
result = list_backups(settings)
assert len(result) == 1
assert result[0]["filename"] == "backup_20260101_120000.sql"
assert "path" in result[0]
assert "size" in result[0]
assert "created_at" in result[0]
def test_returns_empty_list_when_no_backups(self, tmp_path):
backup_dir = tmp_path / "backups"
backup_dir.mkdir()
settings = MagicMock()
settings.backup_dir = str(backup_dir)
result = list_backups(settings)
assert result == []
class TestRestoreBackupSqlite:
def test_copies_backup_to_db_path(self, tmp_path):
backup_file = tmp_path / "backup_20260101_120000.db"
backup_file.write_text("restored content")
db_path = tmp_path / "target.db"
settings = MagicMock()
settings.database_url = f"sqlite:///{db_path}"
result = restore_backup(str(backup_file), settings)
assert result["engine"] == "sqlite"
assert db_path.read_text() == "restored content"
def test_raises_on_missing_file(self, tmp_path):
settings = MagicMock()
settings.database_url = "sqlite:///./test.db"
with pytest.raises(FileNotFoundError):
restore_backup(str(tmp_path / "missing.db"), settings)
class TestRestoreBackupPostgresql:
def test_runs_psql(self, tmp_path):
backup_file = tmp_path / "backup.sql"
backup_file.write_text("-- restore script")
settings = MagicMock()
settings.database_url = "postgresql+psycopg://user:pass@localhost/db"
with patch("gnexus_creds.backup.subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0)
result = restore_backup(str(backup_file), settings)
assert result["engine"] == "postgresql"
mock_run.assert_called_once()
args = mock_run.call_args[0][0]
assert args[0] == "psql"
assert "postgresql://user:pass@localhost/db" in args
assert str(backup_file) in args
def test_raises_on_psql_failure(self, tmp_path):
backup_file = tmp_path / "backup.sql"
backup_file.write_text("-- restore script")
settings = MagicMock()
settings.database_url = "postgresql+psycopg://user:pass@localhost/db"
with patch("gnexus_creds.backup.subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=1, stderr="restore failed")
with pytest.raises(RuntimeError, match="psql failed"):
restore_backup(str(backup_file), settings)
class TestUnsupportedDatabase:
def test_create_backup_raises(self):
settings = MagicMock()
settings.database_url = "mysql://localhost/db"
settings.backup_dir = "./backups"
with pytest.raises(RuntimeError, match="Unsupported database URL"):
create_backup(settings)
def test_restore_backup_raises(self, tmp_path):
backup_file = tmp_path / "backup.sql"
backup_file.write_text("-- script")
settings = MagicMock()
settings.database_url = "mysql://localhost/db"
with pytest.raises(RuntimeError, match="Unsupported database URL"):
restore_backup(str(backup_file), settings)