""" 数据库 URL 构建单元测试(D1 多库支持) ============================================================= 验证 _build_database_url() 对 mysql / postgresql / mssql / sqlite / oracle 五类驱动的 URL 构建正确,且 sqlite 无 host/port/账号分支。 """ import config.database as db_mod from src.common.constants.common import DEFAULT_PASSWORD def _url(monkeypatch, driver, **overrides): defaults = { 'DB_DRIVER': driver, 'DB_HOST': '127.0.0.1', 'DB_PORT': 1, 'DB_DATABASE': 'appdb', 'DB_USERNAME': 'user', 'DB_PASSWORD': 'pass', 'DB_SSL': False, } defaults.update(overrides) for key, value in defaults.items(): monkeypatch.setattr(db_mod, key, value) return db_mod._build_database_url() class TestBuildDatabaseUrl: def test_mysql_default(self, monkeypatch): assert _url(monkeypatch, 'mysql') == 'mysql+pymysql://user:pass@127.0.0.1:1/appdb?charset=utf8mb4' def test_postgresql(self, monkeypatch): assert _url(monkeypatch, 'postgresql') == 'postgresql+psycopg2://user:pass@127.0.0.1:1/appdb' def test_pgsql_alias(self, monkeypatch): assert _url(monkeypatch, 'pgsql') == 'postgresql+psycopg2://user:pass@127.0.0.1:1/appdb' def test_mssql(self, monkeypatch): assert _url(monkeypatch, 'mssql') == 'mssql+pymssql://user:pass@127.0.0.1:1/appdb' def test_sqlite_file_path(self, monkeypatch): # Windows 反斜杠路径应规范化为正斜杠,且无 host/账号 assert _url(monkeypatch, 'sqlite', DB_DATABASE=r'D:\data\app.db') == 'sqlite:///D:/data/app.db' def test_sqlite_memory(self, monkeypatch): assert _url(monkeypatch, 'sqlite', DB_DATABASE=':memory:') == 'sqlite://' def test_oracle_service_name(self, monkeypatch): # Oracle 无"数据库名",DB_DATABASE 即 SERVICE_NAME,走 ?service_name= 查询参数 assert _url(monkeypatch, 'oracle') == 'oracle+oracledb://user:pass@127.0.0.1:1/?service_name=appdb' def test_unknown_driver_falls_back_mysql(self, monkeypatch): assert _url(monkeypatch, 'db2') == 'mysql+pymysql://user:pass@127.0.0.1:1/appdb?charset=utf8mb4' def test_driver_default_ports(self): # DB_PORT 未显式配置时按驱动回退(_DEFAULT_PORT 表) assert db_mod._DEFAULT_PORT['oracle'] == 1521 assert db_mod._DEFAULT_PORT['mysql'] == 3306 assert db_mod._DEFAULT_PORT['postgresql'] == 5432 assert db_mod._DEFAULT_PORT['pgsql'] == 5432 assert db_mod._DEFAULT_PORT['mssql'] == 1433 assert db_mod._DEFAULT_PORT['sqlite'] == 0 class TestSeedData: """种子数据(D3):幂等写入 admin/角色/菜单""" def test_seed_creates_admin_role_menu(self, app): from scripts.seed_data import seed_all from src.modules.system.user.repository import UserRepository from src.modules.system.role.repository import RoleRepository from src.modules.system.menu.repository import MenuRepository with app.app_context(): seed_all() admin = UserRepository.first_by({'username': 'admin'}) assert admin is not None assert admin.password != DEFAULT_PASSWORD # 已加密 assert admin.salt role = RoleRepository.first_by({'code': 'admin'}) assert role is not None assert MenuRepository.first_by({'name': '系统管理', 'parent_id': 0}) is not None def test_seed_is_idempotent(self, app): from scripts.seed_data import seed_all from src.modules.system.user.repository import UserRepository with app.app_context(): seed_all() seed_all() admins = UserRepository.find_list(filters={'username': 'admin'}) assert len(admins) == 1 class TestNormalizeDriver: """驱动别名归一化(_normalize_driver,与 migrate_db.py 共用)""" def test_canonical(self): from config.database import _normalize_driver assert _normalize_driver('mysql') == 'mysql' assert _normalize_driver('postgresql') == 'postgresql' assert _normalize_driver('mssql') == 'mssql' assert _normalize_driver('sqlite') == 'sqlite' assert _normalize_driver('oracle') == 'oracle' def test_aliases(self): from config.database import _normalize_driver assert _normalize_driver('postgres') == 'postgresql' assert _normalize_driver('pgsql') == 'postgresql' assert _normalize_driver('sqlserver') == 'mssql' assert _normalize_driver('sqlite3') == 'sqlite' def test_upper_case(self): from config.database import _normalize_driver assert _normalize_driver('PostgreSQL') == 'postgresql' class TestSqliteUrl: """SQLite 连接 URL 构建(_sqlite_url,与 migrate_db.py 共用)""" def test_memory(self): from config.database import _sqlite_url assert _sqlite_url(':memory:') == 'sqlite://' def test_windows_path(self): from config.database import _sqlite_url assert _sqlite_url(r'D:\data\app.db') == 'sqlite:///D:/data/app.db' class TestMigrateDbUrl: """跨库迁移脚本 migrate_db.py 的 URL 构建(显式参数版,M1)""" def test_make_url_mysql(self): from scripts.migrate_db import make_url assert make_url('mysql', '10.0.0.1', 3306, 'src_db', 'root', 'pwd') \ == 'mysql+pymysql://root:pwd@10.0.0.1:3306/src_db?charset=utf8mb4' def test_make_url_postgresql(self): from scripts.migrate_db import make_url assert make_url('postgresql', '10.0.0.1', 5432, 'dst_db', 'pg', 'pwd') \ == 'postgresql+psycopg2://pg:pwd@10.0.0.1:5432/dst_db' def test_make_url_oracle_service_name(self): from scripts.migrate_db import make_url assert make_url('oracle', '10.0.0.1', 1521, 'XEPDB1', 'system', 'pwd') \ == 'oracle+oracledb://system:pwd@10.0.0.1:1521/?service_name=XEPDB1' def test_make_url_mssql(self): from scripts.migrate_db import make_url assert make_url('mssql', '10.0.0.1', 1433, 'dst_db', 'sa', 'pwd') \ == 'mssql+pymssql://sa:pwd@10.0.0.1:1433/dst_db?charset=utf8' def test_make_url_sqlite_path(self): from scripts.migrate_db import make_url assert make_url('sqlite', '', None, r'D:\data\app.db', '', '') == 'sqlite:///D:/data/app.db' def test_make_url_driver_alias(self): from scripts.migrate_db import make_url # 别名:postgres→postgresql、sqlserver→mssql assert make_url('postgres', '10.0.0.1', 5432, 'dst_db', 'pg', 'pwd') \ == 'postgresql+psycopg2://pg:pwd@10.0.0.1:5432/dst_db' assert make_url('sqlserver', '10.0.0.1', 1433, 'dst_db', 'sa', 'pwd') \ == 'mssql+pymssql://sa:pwd@10.0.0.1:1433/dst_db?charset=utf8' class TestFriendlyDbError: """数据库连接异常友好提示(翻译认证失败/库不存在/拒连)""" def test_auth_failed_gbk(self): from config import friendly_db_error gbk = '用户 "postgres" Password 认证失败'.encode('gbk') e = UnicodeDecodeError('utf-8', gbk, 0, len(gbk), 'invalid continuation byte') assert '认证失败' in friendly_db_error(e) assert 'DB_PASSWORD' in friendly_db_error(e) def test_db_not_exist(self): from config import friendly_db_error assert '数据库不存在' in friendly_db_error(Exception('database "x" does not exist')) def test_connection_refused(self): from config import friendly_db_error assert '连接被拒绝' in friendly_db_error(Exception('Connection refused')) def test_oracle_auth_failed(self): from config import friendly_db_error assert '认证失败' in friendly_db_error(Exception('ORA-01017: invalid username/password; logon denied')) def test_oracle_service_name_error(self): from config import friendly_db_error msg = friendly_db_error(Exception('ORA-12514: TNS:listener does not currently know of service requested')) assert '服务名' in msg assert 'SERVICE_NAME' in msg def test_oracle_connection_refused(self): from config import friendly_db_error assert '连接被拒绝' in friendly_db_error(Exception('ORA-12170: TNS:Connect timeout occurred')) def test_unknown_error(self): from config import friendly_db_error assert 'boom' in friendly_db_error(Exception('boom'))