feat(v2): complete v2 reimplementation
This commit is contained in:
@@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import os
|
||||
import sqlite3
|
||||
import stat
|
||||
from datetime import UTC, datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
@@ -24,8 +25,13 @@ EXPECTED_TABLES = {
|
||||
"idempotency_records",
|
||||
"jobs",
|
||||
"notification_deliveries",
|
||||
"notification_delivery_attempts",
|
||||
"notification_email_settings",
|
||||
"notification_events",
|
||||
"notification_signing_keys",
|
||||
"notification_subscriptions",
|
||||
"repositories",
|
||||
"repository_data_key_epochs",
|
||||
"restores",
|
||||
"schedules",
|
||||
"secrets",
|
||||
@@ -103,6 +109,199 @@ async def test_startup_rejects_unmigrated_database(tmp_path: Path) -> None:
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
def test_local_only_sources_migration_rejects_existing_remote_sources(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
settings = settings_for(tmp_path)
|
||||
migration = alembic_config(settings.database_url)
|
||||
command.upgrade(migration, "0005_restore_dry_run")
|
||||
with sqlite3.connect(settings.database_path) as connection:
|
||||
connection.execute(
|
||||
"""
|
||||
INSERT INTO sources (name, kind, public_config, secret_refs, state, last_probe, id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
("remote", "sftp", "{}", "[]", "active", None, "source-remote"),
|
||||
)
|
||||
|
||||
with pytest.raises(RuntimeError, match="found 1 non-local source row"):
|
||||
command.upgrade(migration, "head")
|
||||
|
||||
with sqlite3.connect(settings.database_path) as connection:
|
||||
connection.execute("UPDATE sources SET kind = ? WHERE id = ?", ("local", "source-remote"))
|
||||
|
||||
command.upgrade(migration, "head")
|
||||
with (
|
||||
sqlite3.connect(settings.database_path) as connection,
|
||||
pytest.raises(sqlite3.IntegrityError),
|
||||
):
|
||||
connection.execute(
|
||||
"""
|
||||
INSERT INTO sources (name, kind, public_config, secret_refs, state, last_probe, id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
("remote-two", "sftp", "{}", "[]", "active", None, "source-remote-two"),
|
||||
)
|
||||
|
||||
command.downgrade(migration, "0005_restore_dry_run")
|
||||
with sqlite3.connect(settings.database_path) as connection:
|
||||
connection.execute(
|
||||
"""
|
||||
INSERT INTO sources (name, kind, public_config, secret_refs, state, last_probe, id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
("remote-three", "sftp", "{}", "[]", "active", None, "source-remote-three"),
|
||||
)
|
||||
|
||||
|
||||
def test_0009_allows_ssh_sources_and_refuses_populated_downgrade(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
settings = settings_for(tmp_path)
|
||||
migration = alembic_config(settings.database_url)
|
||||
command.upgrade(migration, "0008_notification_outbox")
|
||||
with sqlite3.connect(settings.database_path) as connection:
|
||||
connection.execute(
|
||||
"""
|
||||
INSERT INTO sources (name, kind, public_config, secret_refs, state, last_probe, id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
("local", "local", '{"root":"/tmp"}', "[]", "active", None, "source-local"),
|
||||
)
|
||||
|
||||
command.upgrade(migration, "head")
|
||||
with sqlite3.connect(settings.database_path) as connection:
|
||||
local_source = connection.execute("SELECT kind FROM sources").fetchone()
|
||||
assert local_source is not None
|
||||
assert local_source[0] == "local"
|
||||
connection.execute(
|
||||
"""
|
||||
INSERT INTO sources (name, kind, public_config, secret_refs, state, last_probe, id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
"ssh",
|
||||
"ssh",
|
||||
'{"hostname":"backup.example.test"}',
|
||||
'["secret"]',
|
||||
"active",
|
||||
None,
|
||||
"source-ssh",
|
||||
),
|
||||
)
|
||||
with pytest.raises(sqlite3.IntegrityError):
|
||||
connection.execute(
|
||||
"""
|
||||
INSERT INTO sources (name, kind, public_config, secret_refs, state, last_probe, id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
("sftp", "sftp", "{}", "[]", "active", None, "source-sftp"),
|
||||
)
|
||||
|
||||
with pytest.raises(RuntimeError, match="found 1 SSH source row"):
|
||||
command.downgrade(migration, "0008_notification_outbox")
|
||||
|
||||
with sqlite3.connect(settings.database_path) as connection:
|
||||
connection.execute("DELETE FROM sources WHERE id = ?", ("source-ssh",))
|
||||
command.downgrade(migration, "0008_notification_outbox")
|
||||
with (
|
||||
sqlite3.connect(settings.database_path) as connection,
|
||||
pytest.raises(sqlite3.IntegrityError),
|
||||
):
|
||||
connection.execute(
|
||||
"""
|
||||
INSERT INTO sources (name, kind, public_config, secret_refs, state, last_probe, id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
"ssh-after-downgrade",
|
||||
"ssh",
|
||||
"{}",
|
||||
"[]",
|
||||
"active",
|
||||
None,
|
||||
"source-ssh-after",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def test_0008_migrates_unexpected_legacy_notification_delivery(tmp_path: Path) -> None:
|
||||
settings = settings_for(tmp_path)
|
||||
migration = alembic_config(settings.database_url)
|
||||
command.upgrade(migration, "0007_repository_data_key_epochs")
|
||||
with sqlite3.connect(settings.database_path) as connection:
|
||||
connection.execute(
|
||||
"""
|
||||
INSERT INTO notification_subscriptions
|
||||
(id, channel, event_filters, destination_config, state)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
"subscription",
|
||||
"email",
|
||||
'["execution.queued"]',
|
||||
'{"recipients":["a@example.test"]}',
|
||||
"active",
|
||||
),
|
||||
)
|
||||
connection.execute(
|
||||
"""
|
||||
INSERT INTO notification_deliveries
|
||||
(id, event_id, subscription_id, attempt, state, response_class)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
("delivery", "legacy-event", "subscription", 1, "delivered", "http_200"),
|
||||
)
|
||||
command.upgrade(migration, "head")
|
||||
with sqlite3.connect(settings.database_path) as connection:
|
||||
event = connection.execute("SELECT type, payload FROM notification_events").fetchone()
|
||||
attempt = connection.execute(
|
||||
"SELECT number, outcome, response_class FROM notification_delivery_attempts"
|
||||
).fetchone()
|
||||
assert event is not None and event[0] == "notification.legacy"
|
||||
assert "legacy-event" in event[1]
|
||||
assert attempt is not None
|
||||
assert attempt[0] == 1
|
||||
assert attempt[1] == "delivered"
|
||||
assert attempt[2] == "http_200"
|
||||
|
||||
|
||||
def test_0007_downgrade_refuses_populated_key_metadata(tmp_path: Path) -> None:
|
||||
settings = settings_for(tmp_path)
|
||||
migration = alembic_config(settings.database_url)
|
||||
command.upgrade(migration, "head")
|
||||
with sqlite3.connect(settings.database_path) as connection:
|
||||
connection.execute(
|
||||
"""
|
||||
INSERT INTO repositories
|
||||
(id, name, root, format_version, compression, encryption, active_data_key_id, state)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
"repository",
|
||||
"repository",
|
||||
str(settings.repository_roots[0]),
|
||||
1,
|
||||
"none",
|
||||
"none",
|
||||
"epoch",
|
||||
"active",
|
||||
),
|
||||
)
|
||||
connection.execute(
|
||||
"""
|
||||
INSERT INTO repository_data_key_epochs (id, repository_id, key_id, state)
|
||||
VALUES (?, ?, ?, ?)
|
||||
""",
|
||||
("epoch-row", "repository", "epoch", "active"),
|
||||
)
|
||||
|
||||
with pytest.raises(
|
||||
RuntimeError, match="cannot downgrade while repository data key metadata exists"
|
||||
):
|
||||
command.downgrade(migration, "0006_local_sources_only")
|
||||
|
||||
|
||||
def test_database_role_rejects_unmigrated_database(tmp_path: Path) -> None:
|
||||
cli = importlib.import_module("backup_tool.cli")
|
||||
db_module = importlib.import_module("backup_tool.db.engine")
|
||||
|
||||
Reference in New Issue
Block a user