test_post_users_omission.py 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. """Real position-user PUT route against SQLite; never connect to business databases."""
  2. from datetime import datetime
  3. import pymysql
  4. import pytest
  5. from fastapi import FastAPI
  6. from fastapi.testclient import TestClient
  7. from sqlalchemy import create_engine, event
  8. from sqlalchemy.orm import Session
  9. from sqlalchemy.pool import StaticPool
  10. from app.api import admin
  11. from app.models.models import SysPost, SysUser, SysUserPost
  12. @pytest.fixture
  13. def context(monkeypatch):
  14. def forbidden(*args, **kwargs):
  15. raise AssertionError('Business database access is forbidden')
  16. monkeypatch.setattr(pymysql, 'connect', forbidden)
  17. monkeypatch.setattr(pymysql.connections.Connection, 'connect', forbidden)
  18. engine = create_engine('sqlite://', connect_args={'check_same_thread': False}, poolclass=StaticPool)
  19. for model in (SysPost, SysUser, SysUserPost):
  20. model.__table__.create(engine)
  21. db = Session(engine, expire_on_commit=False)
  22. for uid, tenant, deleted in [('actor', 't', 0), ('u1', 't', 0), ('u2', 't', 0), ('foreign', 'other', 0), ('deleted', 't', 1)]:
  23. db.add(SysUser(id=uid, user_no=uid, nick_name=uid, tenant_no=tenant, del_flag=deleted))
  24. for pid, tenant, deleted in [('p', 't', 0), ('p2', 't', 0), ('foreign-p', 'other', 0), ('deleted-p', 't', 1)]:
  25. db.add(SysPost(id=pid, post_name=pid, post_code=pid, tenant_no=tenant, del_flag=deleted))
  26. for rid, uid, pid, tenant in [('kept', 'u1', 'p', 't'), ('other-post', 'u1', 'p2', 't'), ('other-tenant', 'foreign', 'foreign-p', 'other')]:
  27. db.add(SysUserPost(id=rid, user_id=uid, post_id=pid, tenant_no=tenant, del_flag=0,
  28. create_by='creator', create_time=datetime(2020, 1, 1)))
  29. db.commit()
  30. writes, commits = [], []
  31. def capture(_conn, _cursor, statement, *_args):
  32. if statement.lstrip().upper().startswith(('INSERT', 'UPDATE', 'DELETE')):
  33. writes.append(statement)
  34. event.listen(engine, 'before_cursor_execute', capture)
  35. event.listen(db, 'after_commit', lambda _db: commits.append(True))
  36. app = FastAPI()
  37. app.add_api_route('/posts/{post_id}/users', admin.update_post_bound_users, methods=['PUT'])
  38. app.dependency_overrides[admin.get_db] = lambda: db
  39. app.dependency_overrides[admin.require_admin_or_super_admin] = lambda: {'user_id': 'actor'}
  40. with TestClient(app) as client:
  41. yield client, db, writes, commits
  42. db.close()
  43. engine.dispose()
  44. def put(context, payload, post_id='p'):
  45. return context[0].put(f'/posts/{post_id}/users', json=payload)
  46. def test_omitted_user_ids_does_not_write_or_commit(context):
  47. response = put(context, {})
  48. assert response.status_code == 200
  49. _, db, writes, commits = context
  50. assert db.get(SysUserPost, 'kept').del_flag == 0
  51. assert not writes and not commits
  52. assert not db.dirty and not db.new
  53. @pytest.mark.parametrize('key', ['userIds', 'user_ids'])
  54. def test_explicit_empty_array_clears_only_current_post(context, key):
  55. assert put(context, {key: []}).status_code == 200
  56. db = context[1]
  57. assert db.get(SysUserPost, 'kept').del_flag == 1
  58. assert db.get(SysUserPost, 'other-post').del_flag == 0
  59. assert db.get(SysUserPost, 'other-tenant').del_flag == 0
  60. assert db.query(SysUserPost).count() == 3
  61. @pytest.mark.parametrize('key', ['userIds', 'user_ids'])
  62. def test_explicit_null_is_rejected_before_any_write(context, key):
  63. assert put(context, {key: None}).status_code == 422
  64. assert context[1].get(SysUserPost, 'kept').del_flag == 0
  65. assert not context[2] and not context[3]
  66. def test_nonempty_array_preserves_old_identity_and_adds_only_new_user(context):
  67. for _ in range(2):
  68. assert put(context, {'userIds': ['u1', 'u2']}).status_code == 200
  69. db = context[1]
  70. kept = db.get(SysUserPost, 'kept')
  71. assert kept.del_flag == 0 and kept.create_by == 'creator'
  72. assert kept.create_time == datetime(2020, 1, 1)
  73. assert db.query(SysUserPost).filter_by(post_id='p', del_flag=0).count() == 2
  74. assert db.query(SysUserPost).count() == 4
  75. assert db.get(SysUserPost, 'other-post').del_flag == 0
  76. assert db.get(SysUserPost, 'other-tenant').del_flag == 0
  77. @pytest.mark.parametrize('uid', ['foreign', 'missing', 'deleted'])
  78. def test_invalid_user_rejected_before_mutating_existing_bindings(context, uid):
  79. response = put(context, {'userIds': ['u2', uid]})
  80. assert response.json()['code'] == 400
  81. assert context[1].get(SysUserPost, 'kept').del_flag == 0
  82. assert not context[2] and not context[3]
  83. @pytest.mark.parametrize('post_id', ['foreign-p', 'missing', 'deleted-p'])
  84. @pytest.mark.parametrize('payload', [{}, {'userIds': []}])
  85. def test_post_scope_is_validated_even_when_user_ids_are_omitted(context, post_id, payload):
  86. response = put(context, payload, post_id)
  87. assert response.json()['code'] == 404
  88. assert not context[2] and not context[3]