You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
359 lines
12 KiB
359 lines
12 KiB
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()
|
|
|