171 lines
6.8 KiB
Python
171 lines
6.8 KiB
Python
"""Persistent SQLite configuration and migration integration tests."""
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import os
|
|
import sqlite3
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
import uuid
|
|
from contextlib import closing
|
|
from pathlib import Path
|
|
|
|
from kca.config import _database_url_from_parts
|
|
from kca.models import Vault
|
|
from kca.tortoise_mixin import TortoiseMixin
|
|
from tortoise.context import TortoiseContext
|
|
|
|
SERVER_ROOT = Path(__file__).resolve().parents[1]
|
|
MIGRATIONS_DIR = SERVER_ROOT / "migrations" / "models"
|
|
|
|
|
|
class SqliteConfigurationTests(unittest.TestCase):
|
|
def test_database_url_from_sqlite_parts(self) -> None:
|
|
self.assertEqual(
|
|
_database_url_from_parts(
|
|
"sqlite", "ignored", "ignored", "ignored", "5432", "/data/kca.db", "busy_timeout=5000"
|
|
),
|
|
"sqlite:///data/kca.db?busy_timeout=5000",
|
|
)
|
|
self.assertEqual(
|
|
_database_url_from_parts("sqlite", "", None, "", "", "data/kca.db", ""),
|
|
"sqlite://data/kca.db",
|
|
)
|
|
self.assertEqual(
|
|
_database_url_from_parts("sqlite", "", None, "", "", ":memory:", ""),
|
|
"sqlite://:memory:",
|
|
)
|
|
|
|
|
|
class SqliteMemoryTests(unittest.TestCase):
|
|
def test_memory_url_with_options_generates_schemas(self) -> None:
|
|
async def create_vault() -> None:
|
|
mixin = TortoiseMixin("sqlite://:memory:?busy_timeout=5000", ["kca.models"])
|
|
await mixin.ensure_context()
|
|
try:
|
|
await Vault.create(
|
|
id=uuid.uuid4(),
|
|
owner_issuer="https://issuer.example",
|
|
owner_sub="memory-user",
|
|
name="Memory vault",
|
|
settings={},
|
|
)
|
|
self.assertEqual(await Vault.all().count(), 1)
|
|
finally:
|
|
await mixin.aclose()
|
|
|
|
asyncio.run(create_vault())
|
|
|
|
|
|
class SqliteFileMigrationTests(unittest.TestCase):
|
|
def test_migrations_pragmas_and_persistence(self) -> None:
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
db_path = Path(tmp) / "kca.db"
|
|
database_url = f"sqlite://{db_path}?busy_timeout=5000"
|
|
env = os.environ.copy()
|
|
env["DATABASE_URL"] = database_url
|
|
aerich = Path(sys.executable).with_name("aerich")
|
|
|
|
for _ in range(2):
|
|
result = subprocess.run(
|
|
[str(aerich), "upgrade"],
|
|
cwd=SERVER_ROOT,
|
|
env=env,
|
|
text=True,
|
|
capture_output=True,
|
|
check=False,
|
|
)
|
|
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
|
|
|
expected_tables = {
|
|
"aerich",
|
|
"api_token",
|
|
"app_user",
|
|
"authority",
|
|
"certificate",
|
|
"certificate_revocation",
|
|
"crl",
|
|
"key",
|
|
"ocsp_snapshot",
|
|
"request",
|
|
"template",
|
|
"vault",
|
|
}
|
|
migration_count = len(list(MIGRATIONS_DIR.glob("[0-9]_*.py")))
|
|
with closing(sqlite3.connect(db_path)) as db:
|
|
tables = {
|
|
row[0]
|
|
for row in db.execute(
|
|
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'"
|
|
)
|
|
}
|
|
self.assertEqual(tables, expected_tables)
|
|
self.assertEqual(db.execute("SELECT COUNT(*) FROM aerich").fetchone()[0], migration_count)
|
|
certificate_columns = {row[1] for row in db.execute("PRAGMA table_info(certificate)")}
|
|
authority_columns = {row[1] for row in db.execute("PRAGMA table_info(authority)")}
|
|
user_columns = {row[1] for row in db.execute("PRAGMA table_info(app_user)")}
|
|
self.assertIn("no_expiration", certificate_columns)
|
|
self.assertIn("template_key", authority_columns)
|
|
self.assertIn("issuer", user_columns)
|
|
|
|
vault_id = uuid.uuid4()
|
|
|
|
async def write_vault() -> None:
|
|
ctx = TortoiseContext()
|
|
with ctx:
|
|
await ctx.init(db_url=database_url, modules={"models": ["kca.models"]})
|
|
try:
|
|
connection = ctx.connections.get("default")
|
|
foreign_keys = await connection.execute_query_dict("PRAGMA foreign_keys")
|
|
journal_mode = await connection.execute_query_dict("PRAGMA journal_mode")
|
|
busy_timeout = await connection.execute_query_dict("PRAGMA busy_timeout")
|
|
self.assertEqual(foreign_keys[0]["foreign_keys"], 1)
|
|
self.assertEqual(journal_mode[0]["journal_mode"].lower(), "wal")
|
|
self.assertEqual(busy_timeout[0]["timeout"], 5000)
|
|
await Vault.create(
|
|
id=vault_id,
|
|
owner_issuer="https://issuer.example",
|
|
owner_sub="sqlite-user",
|
|
name="Persistent vault",
|
|
settings={},
|
|
)
|
|
finally:
|
|
await ctx.close_connections()
|
|
|
|
asyncio.run(write_vault())
|
|
with closing(sqlite3.connect(db_path)) as db:
|
|
row = db.execute("SELECT name FROM vault WHERE id = ?", (str(vault_id),)).fetchone()
|
|
self.assertEqual(row, ("Persistent vault",))
|
|
|
|
# SQLite bootstraps at the current schema, so historical downgrades
|
|
# are executable no-ops and preserve both the schema and its data.
|
|
for _ in range(migration_count - 1):
|
|
result = subprocess.run(
|
|
[str(aerich), "downgrade", "--yes"],
|
|
cwd=SERVER_ROOT,
|
|
env=env,
|
|
text=True,
|
|
capture_output=True,
|
|
check=False,
|
|
)
|
|
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
|
result = subprocess.run(
|
|
[str(aerich), "upgrade"],
|
|
cwd=SERVER_ROOT,
|
|
env=env,
|
|
text=True,
|
|
capture_output=True,
|
|
check=False,
|
|
)
|
|
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
|
with closing(sqlite3.connect(db_path)) as db:
|
|
self.assertEqual(db.execute("SELECT COUNT(*) FROM aerich").fetchone()[0], migration_count)
|
|
row = db.execute("SELECT name FROM vault WHERE id = ?", (str(vault_id),)).fetchone()
|
|
self.assertEqual(row, ("Persistent vault",))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|