| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862 |
- """B9 机构与用户维护写操作测试。
- 覆盖本期新增/重构的逻辑:
- - 自我保护(admin 不能编辑自己角色/状态、删除、重置密码)
- - 最后一个活跃管理员守卫(统一覆盖删除/降级/停用)
- - row_version 乐观锁冲突
- - 机构循环检测(parentId 指向后代)
- - 机构占用守卫(存在子机构或挂载用户)
- - 级联路径更新 + 路径长度预校验
- - 软删除后同名重建(active_marker 复合唯一索引)
- - auth_version 在角色变更/停用/重置密码场景下的递增
- """
- from __future__ import annotations
- import pytest
- from dms.extensions import db
- from dms.models import AuditLog, Organization, Role, User
- def _auth(token: str) -> dict[str, str]:
- return {"Authorization": f"Bearer {token}"}
- def _login(client, password: str, *, username: str = "admin") -> str:
- response = client.post(
- "/api/v1/auth/login",
- json={
- "username": username,
- "password": password,
- "keepSignedIn": False,
- },
- )
- assert response.status_code == 200, response.get_json()
- return response.get_json()["data"]["accessToken"]
- def _org_id(app, code: str) -> int:
- with app.app_context():
- org = db.session.scalar(
- db.select(Organization).where(Organization.org_code == code)
- )
- assert org is not None, f"organization {code} not found"
- return org.id
- def _user_id(app, username: str) -> int:
- with app.app_context():
- user = db.session.scalar(db.select(User).where(User.username == username))
- assert user is not None, f"user {username} not found"
- return user.id
- def _create_org(
- client,
- token: str,
- *,
- code: str,
- name: str,
- parent_id: str | None = None,
- sort_no: int = 100,
- status: str = "ENABLED",
- ):
- return client.post(
- "/api/v1/organizations",
- json={
- "orgCode": code,
- "orgName": name,
- "parentId": parent_id,
- "sortNo": sort_no,
- "status": status,
- },
- headers=_auth(token),
- )
- def _update_org(
- client,
- token: str,
- org_id: str,
- *,
- code: str,
- name: str,
- parent_id: str | None,
- sort_no: int,
- status: str,
- row_version: int,
- ):
- return client.put(
- f"/api/v1/organizations/{org_id}",
- json={
- "orgCode": code,
- "orgName": name,
- "parentId": parent_id,
- "sortNo": sort_no,
- "status": status,
- "rowVersion": row_version,
- },
- headers=_auth(token),
- )
- def _delete_org(client, token: str, org_id: str, row_version: int):
- return client.delete(
- f"/api/v1/organizations/{org_id}?rowVersion={row_version}",
- headers=_auth(token),
- )
- def _create_user(
- client,
- token: str,
- *,
- username: str,
- real_name: str,
- password: str = "Initial@2026",
- organization_id: str | None = None,
- role_code: str = "USER",
- role_id: str | None = None,
- security_level: str = "INTERNAL",
- status: str = "ENABLED",
- ):
- return client.post(
- "/api/v1/users",
- json={
- "username": username,
- "realName": real_name,
- "initialPassword": password,
- "organizationId": organization_id,
- "roleCode": role_code,
- "roleId": role_id,
- "securityLevel": security_level,
- "status": status,
- },
- headers=_auth(token),
- )
- def _update_user(
- client,
- token: str,
- user_id: str,
- *,
- real_name: str,
- organization_id: str | None,
- role_code: str,
- security_level: str,
- status: str,
- row_version: int,
- ):
- return client.put(
- f"/api/v1/users/{user_id}",
- json={
- "realName": real_name,
- "organizationId": organization_id,
- "roleCode": role_code,
- "roleId": None,
- "securityLevel": security_level,
- "status": status,
- "rowVersion": row_version,
- },
- headers=_auth(token),
- )
- def _reset_password(
- client, token: str, user_id: str, *, new_password: str, row_version: int
- ):
- return client.post(
- f"/api/v1/users/{user_id}/reset-password",
- json={"newPassword": new_password, "rowVersion": row_version},
- headers=_auth(token),
- )
- # =====================================================================
- # 机构生命周期 + 软删除 + 复合唯一索引
- # =====================================================================
- @pytest.mark.integration
- def test_create_organization_success_writes_audit(b2_app, b2_client, login_token):
- response = _create_org(
- b2_client,
- login_token,
- code="ORG_B9_NEW",
- name="B9新增机构",
- parent_id=str(_org_id(b2_app, "ORG_ROOT")),
- sort_no=99,
- )
- assert response.status_code == 201, response.get_json()
- payload = response.get_json()["data"]
- assert payload["orgCode"] == "ORG_B9_NEW"
- assert payload["orgPath"] == "/机关/B9新增机构"
- assert payload["parentId"] == str(_org_id(b2_app, "ORG_ROOT"))
- with b2_app.app_context():
- audit = db.session.scalar(
- db.select(AuditLog).where(AuditLog.action_type == "CREATE_ORGANIZATION")
- )
- assert audit is not None
- assert audit.target_id == int(payload["id"])
- @pytest.mark.integration
- def test_create_organization_rejects_duplicate_code(b2_client, login_token):
- response = _create_org(
- b2_client,
- login_token,
- code="ORG_ROOT",
- name="重名机构",
- )
- assert response.status_code == 409
- assert response.get_json()["code"] == "DATA_VERSION_CONFLICT"
- @pytest.mark.integration
- def test_create_organization_rejects_disabled_parent(b2_client, login_token, b2_app):
- disabled_id = str(_org_id(b2_app, "ORG_DISABLED"))
- response = _create_org(
- b2_client,
- login_token,
- code="ORG_B9_UNDER_DISABLED",
- name="挂在停用机构下",
- parent_id=disabled_id,
- )
- assert response.status_code == 400
- assert response.get_json()["code"] == "INVALID_ARGUMENT"
- @pytest.mark.integration
- def test_soft_deleted_org_code_can_be_reused(b2_client, login_token, b2_app):
- """active_marker 复合唯一索引:软删除后允许重建同名条目。"""
- create_resp = _create_org(
- b2_client, login_token, code="ORG_B9_REVIVE", name="原机构"
- )
- assert create_resp.status_code == 201
- org_id = create_resp.get_json()["data"]["id"]
- row_version = create_resp.get_json()["data"]["rowVersion"]
- delete_resp = _delete_org(b2_client, login_token, org_id, row_version)
- assert delete_resp.status_code == 200
- rebuild_resp = _create_org(
- b2_client, login_token, code="ORG_B9_REVIVE", name="重建机构"
- )
- assert rebuild_resp.status_code == 201
- assert rebuild_resp.get_json()["data"]["orgName"] == "重建机构"
- # =====================================================================
- # 机构更新:row_version / 循环 / 级联路径 / 长度上限
- # =====================================================================
- @pytest.mark.integration
- def test_update_organization_row_version_conflict(b2_client, login_token, b2_app):
- create_resp = _create_org(
- b2_client, login_token, code="ORG_B9_VC", name="版本冲突"
- )
- org_id = create_resp.get_json()["data"]["id"]
- response = _update_org(
- b2_client,
- login_token,
- org_id,
- code="ORG_B9_VC",
- name="版本冲突",
- parent_id=None,
- sort_no=10,
- status="ENABLED",
- row_version=99,
- )
- assert response.status_code == 409
- body = response.get_json()
- assert body["code"] == "DATA_VERSION_CONFLICT"
- assert body["details"]["currentRowVersion"] == 0
- @pytest.mark.integration
- def test_update_organization_rejects_cycle_to_descendant(
- b2_client, login_token, b2_app
- ):
- """parentId 不能指向自己的后代(否则会形成环)。"""
- grandparent_resp = _create_org(
- b2_client, login_token, code="ORG_B9_GP", name="祖父"
- )
- gp_id = grandparent_resp.get_json()["data"]["id"]
- parent_resp = _create_org(
- b2_client,
- login_token,
- code="ORG_B9_P",
- name="父",
- parent_id=gp_id,
- )
- p_id = parent_resp.get_json()["data"]["id"]
- child_resp = _create_org(
- b2_client,
- login_token,
- code="ORG_B9_C",
- name="子",
- parent_id=p_id,
- )
- c_id = child_resp.get_json()["data"]["id"]
- # 尝试把"父"挂到"子"下面 -> 应当拒绝
- response = _update_org(
- b2_client,
- login_token,
- p_id,
- code="ORG_B9_P",
- name="父",
- parent_id=c_id,
- sort_no=10,
- status="ENABLED",
- row_version=0,
- )
- assert response.status_code == 400
- assert response.get_json()["code"] == "INVALID_ARGUMENT"
- @pytest.mark.integration
- def test_update_organization_cascades_descendant_paths(
- b2_client, login_token, b2_app
- ):
- """重命名中间节点时,所有后代的 org_path 必须同步刷新。"""
- a_resp = _create_org(b2_client, login_token, code="ORG_B9_CASCADE_A", name="原名A")
- a_id = a_resp.get_json()["data"]["id"]
- b_resp = _create_org(
- b2_client,
- login_token,
- code="ORG_B9_CASCADE_B",
- name="原名B",
- parent_id=a_id,
- )
- b_id = b_resp.get_json()["data"]["id"]
- c_resp = _create_org(
- b2_client,
- login_token,
- code="ORG_B9_CASCADE_C",
- name="原名C",
- parent_id=b_id,
- )
- c_id = c_resp.get_json()["data"]["id"]
- rename_resp = _update_org(
- b2_client,
- login_token,
- a_id,
- code="ORG_B9_CASCADE_A",
- name="新名A",
- parent_id=None,
- sort_no=10,
- status="ENABLED",
- row_version=0,
- )
- assert rename_resp.status_code == 200
- with b2_app.app_context():
- a = db.session.get(Organization, int(a_id))
- b = db.session.get(Organization, int(b_id))
- c = db.session.get(Organization, int(c_id))
- assert a.org_path == "/新名A"
- assert b.org_path == "/新名A/原名B"
- assert c.org_path == "/新名A/原名B/原名C"
- # row_version 都被递增
- assert a.row_version == 1
- assert b.row_version == 1
- assert c.row_version == 1
- @pytest.mark.integration
- def test_update_organization_path_length_pre_validation(
- b2_client, login_token, b2_app
- ):
- """M-4:路径长度溢出时整个事务回滚,不留半成品状态。"""
- a_resp = _create_org(
- b2_client, login_token, code="ORG_B9_LEN_A", name="A"
- )
- a_id = a_resp.get_json()["data"]["id"]
- b_resp = _create_org(
- b2_client, login_token, code="ORG_B9_LEN_B", name="B", parent_id=a_id
- )
- b_id = b_resp.get_json()["data"]["id"]
- # 制造一个超长名(>1000 字符),更新 B 应当整体回滚
- long_name = "X" * 1100
- response = _update_org(
- b2_client,
- login_token,
- b_id,
- code="ORG_B9_LEN_B",
- name=long_name,
- parent_id=a_id,
- sort_no=10,
- status="ENABLED",
- row_version=0,
- )
- assert response.status_code == 400
- assert response.get_json()["code"] == "INVALID_ARGUMENT"
- # B 的原状态保持不变
- with b2_app.app_context():
- b = db.session.get(Organization, int(b_id))
- assert b.org_name == "B"
- assert b.org_path == "/A/B"
- assert b.row_version == 0
- # =====================================================================
- # 机构删除:占用守卫
- # =====================================================================
- @pytest.mark.integration
- def test_delete_organization_blocks_when_has_children(b2_client, login_token):
- parent_resp = _create_org(
- b2_client, login_token, code="ORG_B9_DEL_PARENT", name="父"
- )
- p_id = parent_resp.get_json()["data"]["id"]
- _create_org(
- b2_client,
- login_token,
- code="ORG_B9_DEL_CHILD",
- name="子",
- parent_id=p_id,
- )
- response = _delete_org(b2_client, login_token, p_id, row_version=0)
- assert response.status_code == 409
- body = response.get_json()
- assert body["code"] == "ORGANIZATION_IN_USE"
- assert body["details"]["childOrganizationCount"] == 1
- assert body["details"]["userCount"] == 0
- @pytest.mark.integration
- def test_delete_organization_blocks_when_has_users(
- b2_client, login_token, b2_app
- ):
- org_resp = _create_org(
- b2_client, login_token, code="ORG_B9_DEL_WITH_USER", name="占用机构"
- )
- org_id = org_resp.get_json()["data"]["id"]
- user_resp = _create_user(
- b2_client,
- login_token,
- username="b9_user_on_org",
- real_name="占用用户",
- organization_id=org_id,
- )
- assert user_resp.status_code == 201
- response = _delete_org(b2_client, login_token, org_id, row_version=0)
- assert response.status_code == 409
- body = response.get_json()
- assert body["code"] == "ORGANIZATION_IN_USE"
- assert body["details"]["childOrganizationCount"] == 0
- assert body["details"]["userCount"] == 1
- @pytest.mark.integration
- def test_delete_organization_row_version_conflict(b2_client, login_token):
- org_resp = _create_org(
- b2_client, login_token, code="ORG_B9_DEL_VC", name="版本"
- )
- org_id = org_resp.get_json()["data"]["id"]
- response = _delete_org(b2_client, login_token, org_id, row_version=99)
- assert response.status_code == 409
- assert response.get_json()["code"] == "DATA_VERSION_CONFLICT"
- # =====================================================================
- # 用户生命周期 + 自我保护 + 最后管理员守卫
- # =====================================================================
- @pytest.mark.integration
- def test_create_user_success_and_duplicate(b2_client, login_token, b2_app):
- org_id = str(_org_id(b2_app, "ORG_ROOT"))
- first = _create_user(
- b2_client,
- login_token,
- username="b9_dup",
- real_name="重名测试",
- organization_id=org_id,
- )
- assert first.status_code == 201
- second = _create_user(
- b2_client,
- login_token,
- username="b9_dup",
- real_name="重名测试2",
- organization_id=org_id,
- )
- assert second.status_code == 409
- assert second.get_json()["code"] == "DATA_VERSION_CONFLICT"
- @pytest.mark.integration
- def test_update_user_row_version_conflict(b2_client, login_token, b2_app):
- user_id = str(_user_id(b2_app, "user"))
- response = _update_user(
- b2_client,
- login_token,
- user_id,
- real_name="普通用户",
- organization_id=str(_org_id(b2_app, "ORG_OPS")),
- role_code="USER",
- security_level="SECRET",
- status="ENABLED",
- row_version=99,
- )
- assert response.status_code == 409
- body = response.get_json()
- assert body["code"] == "DATA_VERSION_CONFLICT"
- assert body["details"]["currentRowVersion"] == 0
- @pytest.mark.integration
- def test_self_protection_admin_cannot_edit_own_role_or_status(
- b2_app, b2_client, b2_password
- ):
- """admin 不能编辑自己的角色/状态(避免误锁自己)。"""
- token = _login(b2_client, b2_password, username="admin")
- admin_id = str(_user_id(b2_app, "admin"))
- # 试图改自己的角色
- role_resp = _update_user(
- b2_client,
- token,
- admin_id,
- real_name="系统管理员",
- organization_id=str(_org_id(b2_app, "ORG_ROOT")),
- role_code="USER",
- security_level="TOP_SECRET",
- status="ENABLED",
- row_version=0,
- )
- assert role_resp.status_code == 403
- assert role_resp.get_json()["code"] == "USER_SELF_PROTECTION"
- # 试图停用自己
- disable_resp = _update_user(
- b2_client,
- token,
- admin_id,
- real_name="系统管理员",
- organization_id=str(_org_id(b2_app, "ORG_ROOT")),
- role_code="ADMIN",
- security_level="TOP_SECRET",
- status="DISABLED",
- row_version=0,
- )
- assert disable_resp.status_code == 403
- assert disable_resp.get_json()["code"] == "USER_SELF_PROTECTION"
- @pytest.mark.integration
- def test_self_protection_admin_cannot_delete_self(b2_app, b2_client, b2_password):
- token = _login(b2_client, b2_password, username="admin")
- admin_id = str(_user_id(b2_app, "admin"))
- response = b2_client.delete(
- f"/api/v1/users/{admin_id}?rowVersion=0",
- headers=_auth(token),
- )
- assert response.status_code == 403
- assert response.get_json()["code"] == "USER_SELF_PROTECTION"
- @pytest.mark.integration
- def test_self_protection_admin_cannot_reset_own_password(
- b2_app, b2_client, b2_password
- ):
- token = _login(b2_client, b2_password, username="admin")
- admin_id = str(_user_id(b2_app, "admin"))
- response = _reset_password(
- b2_client, token, admin_id, new_password="NewPassword@2026", row_version=0
- )
- assert response.status_code == 403
- assert response.get_json()["code"] == "USER_SELF_PROTECTION"
- def _guard_admin_token(b2_app, b2_client, login_token, b2_password) -> str:
- """创建具备用户编辑权限的全局管理员并登录,用于验证内置 admin 守卫。
- 内置 admin 的保护语义是"任何其他用户都不可操作",因此验证必须由
- 第二个有权限的管理员发起操作,而非 admin 操作自己(那会先触发自我保护)。
- """
- created = _create_user(
- b2_client,
- login_token,
- username="b9_guard_admin",
- real_name="守卫验证管理员",
- organization_id=str(_org_id(b2_app, "ORG_ROOT")),
- role_code="ADMIN",
- security_level="TOP_SECRET",
- status="ENABLED",
- perm_user_edit=True,
- )
- assert created.status_code == 201, created.get_json()
- return _login(b2_client, "Initial@2026", username="b9_guard_admin")
- @pytest.mark.integration
- def test_builtin_admin_cannot_be_deleted(b2_app, b2_client, login_token, b2_password):
- """内置 admin 账号不允许被任何人删除(系统守护账号)。"""
- token = _guard_admin_token(b2_app, b2_client, login_token, b2_password)
- admin_id = str(_user_id(b2_app, "admin"))
- response = b2_client.delete(
- f"/api/v1/users/{admin_id}?rowVersion=0",
- headers=_auth(token),
- )
- assert response.status_code == 403
- assert response.get_json()["code"] == "FORBIDDEN"
- @pytest.mark.integration
- def test_builtin_admin_cannot_be_downgraded(b2_app, b2_client, login_token, b2_password):
- """内置 admin 账号不允许被编辑(降级/停用一律拒绝)。"""
- token = _guard_admin_token(b2_app, b2_client, login_token, b2_password)
- admin_id = str(_user_id(b2_app, "admin"))
- response = _update_user(
- b2_client,
- token,
- admin_id,
- real_name="系统管理员",
- organization_id=str(_org_id(b2_app, "ORG_ROOT")),
- role_code="USER",
- security_level="TOP_SECRET",
- status="ENABLED",
- row_version=0,
- )
- assert response.status_code == 403
- assert response.get_json()["code"] == "FORBIDDEN"
- @pytest.mark.integration
- def test_builtin_admin_cannot_be_disabled(b2_app, b2_client, login_token, b2_password):
- """内置 admin 账号不允许被停用。"""
- token = _guard_admin_token(b2_app, b2_client, login_token, b2_password)
- admin_id = str(_user_id(b2_app, "admin"))
- response = _update_user(
- b2_client,
- token,
- admin_id,
- real_name="系统管理员",
- organization_id=str(_org_id(b2_app, "ORG_ROOT")),
- role_code="ADMIN",
- security_level="TOP_SECRET",
- status="DISABLED",
- row_version=0,
- )
- assert response.status_code == 403
- assert response.get_json()["code"] == "FORBIDDEN"
- @pytest.mark.integration
- def test_builtin_admin_cannot_be_reset_password(b2_app, b2_client, login_token, b2_password):
- """内置 admin 账号的密码不允许被他人重置。"""
- token = _guard_admin_token(b2_app, b2_client, login_token, b2_password)
- admin_id = str(_user_id(b2_app, "admin"))
- response = _reset_password(
- b2_client, token, admin_id, new_password="NewPassword@2026", row_version=0
- )
- assert response.status_code == 403
- assert response.get_json()["code"] == "FORBIDDEN"
- @pytest.mark.integration
- def test_second_admin_full_lifecycle_after_builtin_guard(
- b2_client, login_token, b2_app
- ):
- """非内置管理员可走完完整生命周期:创建→降级→停用→删除。
- 内置 admin 恒为活跃守护账号,系统永不失去管理员,
- 非内置管理员的降级/停用/删除不再受"最后一个活跃管理员"守卫阻塞。
- """
- # 1) 新建第二个 admin
- second_resp = _create_user(
- b2_client,
- login_token,
- username="b9_second_admin",
- real_name="第二管理员",
- organization_id=str(_org_id(b2_app, "ORG_ROOT")),
- role_code="ADMIN",
- security_level="TOP_SECRET",
- status="ENABLED",
- )
- assert second_resp.status_code == 201
- second_id = second_resp.get_json()["data"]["id"]
- # 2) 降级为 USER(内置 admin 守护系统,应当放行)
- downgrade_resp = _update_user(
- b2_client,
- login_token,
- second_id,
- real_name="第二管理员",
- organization_id=str(_org_id(b2_app, "ORG_ROOT")),
- role_code="USER",
- security_level="TOP_SECRET",
- status="ENABLED",
- row_version=0,
- )
- assert downgrade_resp.status_code == 200, downgrade_resp.get_json()
- new_version = downgrade_resp.get_json()["data"]["rowVersion"]
- # 3) 停用(此时已不是活跃管理员,应当放行)
- disable_resp = _update_user(
- b2_client,
- login_token,
- second_id,
- real_name="第二管理员",
- organization_id=str(_org_id(b2_app, "ORG_ROOT")),
- role_code="USER",
- security_level="TOP_SECRET",
- status="DISABLED",
- row_version=new_version,
- )
- assert disable_resp.status_code == 200, disable_resp.get_json()
- new_version = disable_resp.get_json()["data"]["rowVersion"]
- # 4) 删除——应当放行
- delete_resp = b2_client.delete(
- f"/api/v1/users/{second_id}?rowVersion={new_version}",
- headers=_auth(login_token),
- )
- assert delete_resp.status_code == 200, delete_resp.get_json()
- # =====================================================================
- # auth_version 在用户写操作中的递增策略
- # =====================================================================
- @pytest.mark.integration
- def test_auth_version_bumped_on_role_change(b2_app, b2_client, login_token):
- user_id = str(_user_id(b2_app, "user"))
- with b2_app.app_context():
- before_version = db.session.scalar(
- db.select(User.auth_version).where(User.username == "user")
- )
- response = _update_user(
- b2_client,
- login_token,
- user_id,
- real_name="普通用户",
- organization_id=str(_org_id(b2_app, "ORG_OPS")),
- role_code="ADMIN",
- security_level="SECRET",
- status="ENABLED",
- row_version=0,
- )
- assert response.status_code == 200
- with b2_app.app_context():
- after_version = db.session.scalar(
- db.select(User.auth_version).where(User.username == "user")
- )
- assert after_version == before_version + 1
- @pytest.mark.integration
- def test_auth_version_bumped_on_disable(b2_app, b2_client, login_token):
- user_id = str(_user_id(b2_app, "user"))
- with b2_app.app_context():
- before_version = db.session.scalar(
- db.select(User.auth_version).where(User.username == "user")
- )
- response = _update_user(
- b2_client,
- login_token,
- user_id,
- real_name="普通用户",
- organization_id=str(_org_id(b2_app, "ORG_OPS")),
- role_code="USER",
- security_level="SECRET",
- status="DISABLED",
- row_version=0,
- )
- assert response.status_code == 200
- with b2_app.app_context():
- after_version = db.session.scalar(
- db.select(User.auth_version).where(User.username == "user")
- )
- assert after_version == before_version + 1
- @pytest.mark.integration
- def test_auth_version_not_bumped_on_realname_only(b2_app, b2_client, login_token):
- """只改 realName 不应让旧 token 失效。"""
- user_id = str(_user_id(b2_app, "user"))
- with b2_app.app_context():
- before_version = db.session.scalar(
- db.select(User.auth_version).where(User.username == "user")
- )
- response = _update_user(
- b2_client,
- login_token,
- user_id,
- real_name="普通用户改名",
- organization_id=str(_org_id(b2_app, "ORG_OPS")),
- role_code="USER",
- security_level="SECRET",
- status="ENABLED",
- row_version=0,
- )
- assert response.status_code == 200
- with b2_app.app_context():
- after_version = db.session.scalar(
- db.select(User.auth_version).where(User.username == "user")
- )
- assert after_version == before_version
- @pytest.mark.integration
- def test_auth_version_bumped_on_password_reset(b2_app, b2_client, login_token):
- user_id = str(_user_id(b2_app, "user"))
- with b2_app.app_context():
- before_version = db.session.scalar(
- db.select(User.auth_version).where(User.username == "user")
- )
- response = _reset_password(
- b2_client,
- login_token,
- user_id,
- new_password="BrandNew@2026",
- row_version=0,
- )
- assert response.status_code == 200
- assert response.get_json()["data"]["authVersion"] == before_version + 1
- # =====================================================================
- # 管理员关联角色(角色仅约束其文档操作权限矩阵,管理位不受影响)
- # =====================================================================
- @pytest.mark.integration
- def test_admin_can_be_created_with_role(b2_app, b2_client, login_token):
- """ADMIN 创建/编辑可携带 roleId,响应回带角色名。"""
- with b2_app.app_context():
- role = Role(role_name="管理员挂载角色", sort_no=10, status="ENABLED")
- db.session.add(role)
- db.session.commit()
- role_id = str(role.id)
- created = _create_user(
- b2_client,
- login_token,
- username="b9_role_admin",
- real_name="挂角色管理员",
- organization_id=str(_org_id(b2_app, "ORG_ROOT")),
- role_code="ADMIN",
- role_id=role_id,
- security_level="TOP_SECRET",
- status="ENABLED",
- )
- assert created.status_code == 201, created.get_json()
- data = created.get_json()["data"]
- assert data["roleId"] == role_id
- assert data["roleName"] == "管理员挂载角色"
|