from __future__ import annotations from pathlib import Path from unittest.mock import Mock from app import create_app from app import cli as cli_module from app.extensions import db from app.models import Account, Household, HouseholdMember, OptionItem from app.services.demo_seed import seed_stress_households def test_seed_admin_command_creates_admin_account() -> None: app = create_app("testing") with app.app_context(): db.create_all() runner = app.test_cli_runner() result = runner.invoke( args=[ "seed-admin", "--username", "owner", "--display-name", "主人", "--password", "OwnerPass123!", ], ) assert result.exit_code == 0 assert "created" in result.output with app.app_context(): account = db.session.execute( db.select(Account).where(Account.username == "owner"), ).scalar_one() assert account.role == "admin" assert account.display_name == "主人" assert account.password_hash != "OwnerPass123!" assert account.check_password("OwnerPass123!") is True def test_seed_admin_command_is_idempotent() -> None: app = create_app("testing") with app.app_context(): db.create_all() runner = app.test_cli_runner() first_result = runner.invoke(args=["seed-admin"]) second_result = runner.invoke(args=["seed-admin"]) assert first_result.exit_code == 0 assert second_result.exit_code == 0 assert "already exists" in second_result.output with app.app_context(): accounts = db.session.execute(db.select(Account)).scalars().all() assert len(accounts) == 1 def test_seed_options_command_creates_baseline_option_items() -> None: app = create_app("testing") with app.app_context(): db.create_all() runner = app.test_cli_runner() result = runner.invoke(args=["seed-options"]) assert result.exit_code == 0 assert "Seeded" in result.output with app.app_context(): option_groups = { option_item.option_group for option_item in db.session.execute(db.select(OptionItem)).scalars().all() } assert {"relation_category", "gift_method", "gift_scene"}.issubset(option_groups) assert "relation_detail" not in option_groups assert "tag" not in option_groups def test_seed_all_command_creates_admin_and_options() -> None: app = create_app("testing") with app.app_context(): db.create_all() runner = app.test_cli_runner() result = runner.invoke(args=["seed-all"]) assert result.exit_code == 0 assert "Admin account 'admin'" in result.output assert "Seeded" in result.output with app.app_context(): admin_account = db.session.execute( db.select(Account).where(Account.username == "admin"), ).scalar_one_or_none() option_count = db.session.execute(db.select(OptionItem)).scalars().all() assert admin_account is not None assert len(option_count) > 0 def test_seed_all_requires_initialized_tables(tmp_path: Path) -> None: db_path = tmp_path / "seed-missing-tables.db" app = create_app("testing") app.config["SQLALCHEMY_DATABASE_URI"] = f"sqlite:///{db_path}" with app.app_context(): db.session.remove() db.drop_all() runner = app.test_cli_runner() result = runner.invoke(args=["seed-all"]) assert result.exit_code != 0 assert "Run 'flask db upgrade' or 'flask init-db' first." in result.output def test_seed_demo_command_creates_demo_accounts_and_households() -> None: app = create_app("testing") with app.app_context(): db.create_all() runner = app.test_cli_runner() baseline_result = runner.invoke(args=["seed-all"]) assert baseline_result.exit_code == 0 runner = app.test_cli_runner() result = runner.invoke(args=["seed-demo"]) assert result.exit_code == 0 assert "Seeded 3 demo accounts." in result.output assert "Seeded 3 demo households." in result.output assert "editor-demo / EditorDemo123!" in result.output assert "quick-editor-demo / QuickEditor123!" in result.output with app.app_context(): demo_accounts = db.session.execute( db.select(Account).where(Account.username.in_(["editor-demo", "entry-demo", "quick-editor-demo"])), ).scalars().all() demo_households = db.session.execute( db.select(Household).where(Household.household_code.in_(["D001", "D002", "D003"])), ).scalars().all() member_count = db.session.execute(db.select(HouseholdMember)).scalars().all() assert len(demo_accounts) == 3 assert len(demo_households) == 3 assert len(member_count) >= 8 def test_seed_demo_command_is_idempotent() -> None: app = create_app("testing") with app.app_context(): db.create_all() runner = app.test_cli_runner() assert runner.invoke(args=["seed-all"]).exit_code == 0 runner = app.test_cli_runner() first_result = runner.invoke(args=["seed-demo"]) second_result = runner.invoke(args=["seed-demo"]) assert first_result.exit_code == 0 assert second_result.exit_code == 0 assert "Seeded 0 demo accounts." in second_result.output assert "Seeded 0 demo households." in second_result.output def test_seed_stress_command_creates_large_dataset() -> None: app = create_app("testing") with app.app_context(): db.create_all() runner = app.test_cli_runner() assert runner.invoke(args=["seed-all"]).exit_code == 0 runner = app.test_cli_runner() result = runner.invoke( args=[ "seed-stress", "--household-count", "12", "--members-per-household", "4", "--prefix", "STRESS-", ], ) assert result.exit_code == 0 assert "Seeded 12 stress households." in result.output assert "Seeded 48 stress household members." in result.output with app.app_context(): stress_households = db.session.execute( db.select(Household).where(Household.household_code.like("STRESS-%")), ).scalars().all() stress_members = db.session.execute( db.select(HouseholdMember) .join(Household, HouseholdMember.household_id == Household.id) .where(Household.household_code.like("STRESS-%")), ).scalars().all() assert len(stress_households) == 12 assert len(stress_members) == 48 assert all(household.head_name.startswith("压力户主") for household in stress_households) assert all(household.household_code.startswith("STRESS-") for household in stress_households) def test_seed_stress_command_is_idempotent_for_same_prefix() -> None: app = create_app("testing") with app.app_context(): db.create_all() runner = app.test_cli_runner() assert runner.invoke(args=["seed-all"]).exit_code == 0 runner = app.test_cli_runner() first_result = runner.invoke(args=["seed-stress", "--household-count", "5", "--members-per-household", "3"]) second_result = runner.invoke(args=["seed-stress", "--household-count", "5", "--members-per-household", "3"]) assert first_result.exit_code == 0 assert second_result.exit_code == 0 assert "Seeded 0 stress households." in second_result.output assert "Seeded 0 stress household members." in second_result.output with app.app_context(): households = db.session.execute( db.select(Household).where(Household.household_code.like("STRESS-%")), ).scalars().all() members = db.session.execute( db.select(HouseholdMember) .join(Household, HouseholdMember.household_id == Household.id) .where(Household.household_code.like("STRESS-%")), ).scalars().all() assert len(households) == 5 assert len(members) == 15 def test_seed_stress_command_is_prefix_safe() -> None: app = create_app("testing") with app.app_context(): db.create_all() runner = app.test_cli_runner() assert runner.invoke(args=["seed-all"]).exit_code == 0 runner = app.test_cli_runner() first_prefix_result = runner.invoke( args=["seed-stress", "--household-count", "3", "--members-per-household", "2", "--prefix", "A-"] ) second_prefix_result = runner.invoke( args=["seed-stress", "--household-count", "3", "--members-per-household", "2", "--prefix", "B-"] ) assert first_prefix_result.exit_code == 0 assert second_prefix_result.exit_code == 0 assert "Seeded 3 stress households." in first_prefix_result.output assert "Seeded 3 stress households." in second_prefix_result.output with app.app_context(): a_prefix_households = db.session.execute( db.select(Household).where(Household.household_code.like("A-%")), ).scalars().all() b_prefix_households = db.session.execute( db.select(Household).where(Household.household_code.like("B-%")), ).scalars().all() assert len(a_prefix_households) == 3 assert len(b_prefix_households) == 3 def test_seed_stress_households_normalizes_prefix_and_counts() -> None: app = create_app("testing") with app.app_context(): db.create_all() runner = app.test_cli_runner() assert runner.invoke(args=["seed-all"]).exit_code == 0 created_households, created_members = seed_stress_households( household_count=2, members_per_household=3, household_code_prefix=" stress ", ) db.session.commit() assert created_households == 2 assert created_members == 6 normalized_prefix_households = db.session.execute( db.select(Household).where(Household.household_code.like("STRESS%")), ).scalars().all() assert len(normalized_prefix_households) == 2 def test_seed_stress_command_rejects_invalid_arguments() -> None: app = create_app("testing") with app.app_context(): db.create_all() runner = app.test_cli_runner() assert runner.invoke(args=["seed-all"]).exit_code == 0 runner = app.test_cli_runner() zero_households = runner.invoke(args=["seed-stress", "--household-count", "0"]) blank_prefix = runner.invoke(args=["seed-stress", "--prefix", " "]) assert zero_households.exit_code != 0 assert "household_count must be greater than 0" in zero_households.output assert blank_prefix.exit_code != 0 assert "household_code_prefix cannot be empty" in blank_prefix.output def test_dev_bootstrap_command_runs_setup_steps_and_skips_server(monkeypatch) -> None: app = create_app("testing") with app.app_context(): db.create_all() recorded_calls: list[tuple[list[str], str]] = [] def fake_run_python_command( args: list[str], *, env: dict[str, str], cwd: str, label: str, ) -> None: _ = env _ = cwd recorded_calls.append((args, label)) run_mock = Mock() monkeypatch.setattr(cli_module, "_run_python_command", fake_run_python_command) monkeypatch.setattr(cli_module.Flask, "run", run_mock) runner = app.test_cli_runner() result = runner.invoke(args=["dev-bootstrap", "--skip-server", "--port", "5010"]) assert result.exit_code == 0 assert [label for _, label in recorded_calls] == [ "Applying database migrations", "Seeding baseline admin and options", "Seeding demo accounts and households", ] assert recorded_calls[0][0][-3:] == ["flask", "db", "upgrade"] assert recorded_calls[1][0][-2:] == ["flask", "seed-all"] assert recorded_calls[2][0][-2:] == ["flask", "seed-demo"] assert "Service URL: http://127.0.0.1:5010" in result.output assert "Skipped starting the Flask development server." in result.output run_mock.assert_not_called()