test(db): add schema migration tests

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-11 09:53:49 +08:00
parent 00e3116339
commit 06aed39661

View File

@@ -0,0 +1,376 @@
# -*- coding: utf-8 -*-
"""Tests for v2 schema DDL and migrate_v1_to_v2 / ensure_schema."""
import os
import sqlite3
import sys
import tempfile
import time
import pytest
_scripts = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "scripts")
if _scripts not in sys.path:
sys.path.insert(0, _scripts)
from util.constants import SKILL_SLUG # noqa: E402
# Minimal v1-like DDL (no auth_strategy / session_persistent / device_fingerprint /
# platforms.default_auth_strategy; credentials without account_id).
_V1_DDL = """
CREATE TABLE platforms (
platform_key TEXT PRIMARY KEY,
display_name TEXT NOT NULL,
domain TEXT NOT NULL,
provider_code TEXT,
default_url TEXT,
aliases_json TEXT NOT NULL DEFAULT '[]',
capabilities_json TEXT NOT NULL DEFAULT '{}',
enabled INTEGER NOT NULL DEFAULT 1,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE TABLE accounts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
platform_key TEXT NOT NULL,
account_label TEXT NOT NULL,
login_id TEXT,
login_id_type TEXT NOT NULL DEFAULT 'unknown',
tenant_id TEXT,
environment TEXT NOT NULL DEFAULT 'production',
role TEXT NOT NULL DEFAULT 'default',
provider_code TEXT,
profile_dir TEXT,
url TEXT,
status TEXT NOT NULL DEFAULT 'active',
session_status TEXT NOT NULL DEFAULT 'unknown',
last_used_at INTEGER,
last_login_check_at INTEGER,
last_error_code TEXT,
last_error_message TEXT,
extra_json TEXT NOT NULL DEFAULT '{}',
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE TABLE credentials (
id INTEGER PRIMARY KEY AUTOINCREMENT,
platform_key TEXT NOT NULL,
credential_label TEXT NOT NULL,
credential_type TEXT NOT NULL,
secret_storage TEXT NOT NULL,
secret_ref TEXT,
secret_mask TEXT,
expires_at INTEGER,
status TEXT NOT NULL DEFAULT 'active',
last_used_at INTEGER,
extra_json TEXT NOT NULL DEFAULT '{}',
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE TABLE account_leases (
id INTEGER PRIMARY KEY AUTOINCREMENT,
account_id INTEGER NOT NULL,
lease_token TEXT NOT NULL UNIQUE,
holder TEXT NOT NULL,
purpose TEXT,
expires_at INTEGER NOT NULL,
heartbeat_at INTEGER,
released_at INTEGER,
status TEXT NOT NULL DEFAULT 'active',
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
"""
def _db_path_for_env(root: str, uid: str) -> str:
return os.path.join(root, uid, SKILL_SLUG, "account-manager.db")
def _prep_logging():
from util.logging_config import setup_skill_logging
setup_skill_logging(SKILL_SLUG, "openclaw.skill.account_manager_migration_test")
def _column_names(conn: sqlite3.Connection, table: str) -> list[str]:
cur = conn.cursor()
cur.execute(f"PRAGMA table_info({table})")
return [row[1] for row in cur.fetchall()]
@pytest.fixture()
def iso_env(monkeypatch):
with tempfile.TemporaryDirectory(prefix="acc_mgr_schema_", ignore_cleanup_errors=True) as tmp:
data_root = os.path.join(tmp, "data-root")
os.makedirs(data_root)
uid = "migration_uid"
monkeypatch.setenv("JIANGCHANG_DATA_ROOT", data_root)
monkeypatch.setenv("JIANGCHANG_USER_ID", uid)
db_path = _db_path_for_env(data_root, uid)
os.makedirs(os.path.dirname(db_path), exist_ok=True)
_prep_logging()
yield {"tmp": tmp, "data_root": data_root, "uid": uid, "db_path": db_path}
class TestFreshSchema:
def test_fresh_schema_has_v2_columns_and_meta(self, iso_env):
from db.connection import init_db, get_conn
if os.path.isfile(iso_env["db_path"]):
os.remove(iso_env["db_path"])
init_db()
conn = get_conn()
try:
cols_accounts = _column_names(conn, "accounts")
assert "auth_strategy" in cols_accounts
assert "session_persistent" in cols_accounts
assert "device_fingerprint" in cols_accounts
cols_plat = _column_names(conn, "platforms")
assert "default_auth_strategy" in cols_plat
cols_cred = _column_names(conn, "credentials")
assert "account_id" in cols_cred
cur = conn.cursor()
cur.execute(
"SELECT value FROM _schema_meta WHERE key = ?",
("schema_version",),
)
row = cur.fetchone()
assert row is not None and row[0] == "2"
finally:
conn.close()
class TestMigrateFromV1:
def test_v1_to_v2_migrates_and_preserves_rows(self, iso_env, capsys):
db_path = iso_env["db_path"]
if os.path.isfile(db_path):
os.remove(db_path)
os.makedirs(os.path.dirname(db_path), exist_ok=True)
raw = sqlite3.connect(db_path)
try:
raw.executescript(_V1_DDL)
now = int(time.time())
raw.execute(
"""
INSERT INTO platforms (platform_key, display_name, domain, provider_code,
default_url, aliases_json, capabilities_json, enabled, created_at, updated_at)
VALUES ('keep_me', 'Keep', 'generic', NULL, '', '[]', '{}', 1, ?, ?)
""",
(now, now),
)
raw.execute(
"""
INSERT INTO accounts (platform_key, account_label, login_id, login_id_type,
tenant_id, environment, role, provider_code, profile_dir, url,
status, session_status, extra_json, created_at, updated_at)
VALUES ('keep_me', 'acc', 'u@test.com', 'email', NULL, 'production',
'booking', NULL, NULL, '', 'active', 'unknown', '{}', ?, ?)
""",
(now, now),
)
raw.commit()
finally:
raw.close()
from db.connection import init_db, get_conn
init_db()
err = capsys.readouterr().err
assert "[migration] account-manager v1→v2:" in err
conn = get_conn()
try:
cur = conn.cursor()
cur.execute("SELECT COUNT(*) FROM accounts WHERE login_id = ?", ("u@test.com",))
assert int(cur.fetchone()[0]) == 1
cur.execute(
"SELECT auth_strategy, session_persistent FROM accounts WHERE login_id = ?",
("u@test.com",),
)
strat, sp = cur.fetchone()
assert strat == "password_auto"
assert int(sp) == 1
finally:
conn.close()
def test_migration_idempotent(self, iso_env, capsys):
db_path = iso_env["db_path"]
if os.path.isfile(db_path):
os.remove(db_path)
os.makedirs(os.path.dirname(db_path), exist_ok=True)
raw = sqlite3.connect(db_path)
try:
raw.executescript(_V1_DDL)
raw.commit()
finally:
raw.close()
from db.connection import init_db
init_db()
capsys.readouterr()
init_db()
err = capsys.readouterr().err
assert "v1→v2" not in err
conn = sqlite3.connect(db_path)
try:
cols = _column_names(conn, "accounts")
assert cols.count("auth_strategy") == 1
finally:
conn.close()
def test_platform_default_mapping_douyin(self, iso_env):
db_path = iso_env["db_path"]
if os.path.isfile(db_path):
os.remove(db_path)
os.makedirs(os.path.dirname(db_path), exist_ok=True)
raw = sqlite3.connect(db_path)
now = int(time.time())
try:
raw.executescript(_V1_DDL)
raw.execute(
"""
INSERT INTO platforms (platform_key, display_name, domain, provider_code,
default_url, aliases_json, capabilities_json, enabled, created_at, updated_at)
VALUES ('douyin', 'Douyin', 'content', NULL, '', '[]', '{}', 1, ?, ?)
""",
(now, now),
)
raw.commit()
finally:
raw.close()
from db.connection import init_db, get_conn
init_db()
conn = get_conn()
try:
cur = conn.cursor()
cur.execute(
"SELECT default_auth_strategy FROM platforms WHERE platform_key = ?",
("douyin",),
)
assert cur.fetchone()[0] == "qr_code_manual"
finally:
conn.close()
def test_accounts_auth_strategy_priority(self, iso_env):
db_path = iso_env["db_path"]
if os.path.isfile(db_path):
os.remove(db_path)
os.makedirs(os.path.dirname(db_path), exist_ok=True)
raw = sqlite3.connect(db_path)
now = int(time.time())
try:
raw.executescript(_V1_DDL)
for pk, dn in (
("maersk", "Maersk"),
("unknown_platform", "Unknown"),
):
raw.execute(
"""
INSERT INTO platforms (platform_key, display_name, domain, provider_code,
default_url, aliases_json, capabilities_json, enabled, created_at, updated_at)
VALUES (?, ?, 'generic', NULL, '', '[]', '{}', 1, ?, ?)
""",
(pk, dn, now, now),
)
raw.execute(
"""
INSERT INTO accounts (platform_key, account_label, login_id, login_id_type,
tenant_id, environment, role, provider_code, profile_dir, url,
status, session_status, extra_json, created_at, updated_at)
VALUES ('maersk', 'm', NULL, 'unknown', NULL, 'production',
'booking', NULL, NULL, '', 'active', 'unknown', '{}', ?, ?)
""",
(now, now),
)
raw.execute(
"""
INSERT INTO accounts (platform_key, account_label, login_id, login_id_type,
tenant_id, environment, role, provider_code, profile_dir, url,
status, session_status, extra_json, created_at, updated_at)
VALUES ('unknown_platform', 'u', NULL, 'unknown', NULL, 'production',
'booking', NULL, NULL, '', 'active', 'unknown', '{}', ?, ?)
""",
(now, now),
)
raw.commit()
finally:
raw.close()
from db.connection import init_db, get_conn
init_db()
conn = get_conn()
try:
cur = conn.cursor()
cur.execute(
"SELECT auth_strategy FROM accounts WHERE platform_key = ? ORDER BY id",
("maersk",),
)
assert cur.fetchone()[0] == "qr_code_manual"
cur.execute(
"SELECT auth_strategy FROM accounts WHERE platform_key = ? ORDER BY id",
("unknown_platform",),
)
assert cur.fetchone()[0] == "password_auto"
finally:
conn.close()
class TestInsertAccountValidation:
def test_insert_account_rejects_invalid_strategy(self, iso_env):
from db.connection import init_db, get_conn
from db.accounts_repo import insert_account
from db.auth_strategy import AUTH_STRATEGY_QR_CODE_MANUAL
if os.path.isfile(iso_env["db_path"]):
os.remove(iso_env["db_path"])
init_db()
conn = get_conn()
try:
now = int(time.time())
with pytest.raises(ValueError, match="invalid auth_strategy"):
insert_account(
conn,
platform_key="x",
account_label="l",
login_id=None,
login_id_type="unknown",
tenant_id=None,
environment="production",
role="booking",
provider_code=None,
profile_dir=None,
url=None,
extra_json="{}",
now=now,
auth_strategy="not_a_strategy",
)
aid = insert_account(
conn,
platform_key="x",
account_label="l",
login_id=None,
login_id_type="unknown",
tenant_id=None,
environment="production",
role="booking",
provider_code=None,
profile_dir=None,
url=None,
extra_json="{}",
now=now,
auth_strategy=AUTH_STRATEGY_QR_CODE_MANUAL,
)
assert isinstance(aid, int) and aid > 0
conn.commit()
finally:
conn.close()