Newer
Older
gnexus-creds / tests / test_backup.py
"""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)