simple-module-users 0.0.35__tar.gz → 0.0.37__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (211) hide show
  1. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/.gitignore +1 -0
  2. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/PKG-INFO +6 -6
  3. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/pyproject.toml +6 -6
  4. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/tests/_middleware_support.py +0 -2
  5. simple_module_users-0.0.37/tests/_users_oauth_support.py +84 -0
  6. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/tests/test_auth_screens_props.py +7 -0
  7. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/tests/test_external_users.py +6 -45
  8. simple_module_users-0.0.37/tests/test_legacy_cached_tenant.py +54 -0
  9. simple_module_users-0.0.37/tests/test_login_redirect_root_mount.py +39 -0
  10. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/tests/test_negative_authz.py +4 -1
  11. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/tests/test_token_api.py +22 -1
  12. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/tests/test_user_manager.py +42 -4
  13. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/tests/test_user_model.py +0 -1
  14. simple_module_users-0.0.37/tests/test_users_bearer_scheme.py +54 -0
  15. simple_module_users-0.0.37/tests/test_users_default_role.py +82 -0
  16. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/tests/test_users_middleware.py +0 -1
  17. simple_module_users-0.0.37/tests/test_users_oauth_signup_gate.py +63 -0
  18. simple_module_users-0.0.37/tests/test_users_oauth_unverified_link.py +85 -0
  19. simple_module_users-0.0.37/tests/test_users_register_role_durable.py +54 -0
  20. simple_module_users-0.0.37/tests/test_users_require_verification.py +104 -0
  21. simple_module_users-0.0.37/tests/test_users_seed_role_migration.py +77 -0
  22. simple_module_users-0.0.37/tests/test_users_setup_lock.py +63 -0
  23. simple_module_users-0.0.37/tests/test_users_setup_wizard.py +187 -0
  24. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/tests/test_users_shared_props.py +15 -0
  25. simple_module_users-0.0.37/tests-js/LoginForm.test.tsx +37 -0
  26. simple_module_users-0.0.37/tests-js/Register.test.tsx +77 -0
  27. simple_module_users-0.0.37/tests-js/registerErrors.test.ts +72 -0
  28. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/admin/queries.py +9 -9
  29. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/auth_local/api.py +5 -3
  30. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/auth_local/components/LoginForm.tsx +7 -3
  31. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/auth_local/components/PasswordFields.tsx +5 -1
  32. simple_module_users-0.0.37/users/auth_local/registerErrors.ts +79 -0
  33. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/auth_local/token_api.py +13 -3
  34. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/auth_local/views.py +19 -3
  35. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/bootstrap.py +22 -12
  36. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/contracts/schemas.py +0 -1
  37. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/deps.py +2 -2
  38. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/locales/en.json +15 -5
  39. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/manager.py +30 -4
  40. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/models/role.py +16 -0
  41. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/models/user.py +5 -5
  42. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/models/user_role.py +3 -2
  43. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/oauth/api.py +53 -0
  44. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/pages/AcceptInvite.tsx +3 -3
  45. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/pages/Login.tsx +7 -1
  46. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/pages/Register.tsx +86 -34
  47. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/pages/ResetPassword.tsx +3 -2
  48. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/pages/Users/AddPeople.tsx +103 -96
  49. simple_module_users-0.0.37/users/pages/Users/Edit.tsx +129 -0
  50. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/pages/Users/Index.tsx +69 -66
  51. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/provider.py +14 -4
  52. simple_module_users-0.0.37/users/registration.py +41 -0
  53. simple_module_users-0.0.37/users/setup.py +101 -0
  54. simple_module_users-0.0.37/users/setup_action.py +129 -0
  55. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/shared_props.py +8 -2
  56. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/startup.py +2 -6
  57. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/token_strategy.py +4 -1
  58. simple_module_users-0.0.35/users/pages/Users/Edit.tsx +0 -126
  59. simple_module_users-0.0.35/users/setup.py +0 -51
  60. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/LICENSE +0 -0
  61. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/README.md +0 -0
  62. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/package.json +0 -0
  63. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/tests/.gitkeep +0 -0
  64. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/tests/_demo_support.py +0 -0
  65. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/tests/_users_app_builders.py +0 -0
  66. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/tests/conftest.py +0 -0
  67. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/tests/test_access_token_model.py +0 -0
  68. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/tests/test_activity_summary.py +0 -0
  69. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/tests/test_admin_resend_invite.py +0 -0
  70. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/tests/test_api_admin.py +0 -0
  71. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/tests/test_api_admin_crud.py +0 -0
  72. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/tests/test_api_admin_filters.py +0 -0
  73. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/tests/test_api_auth.py +0 -0
  74. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/tests/test_backend.py +0 -0
  75. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/tests/test_bearer_token_age.py +0 -0
  76. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/tests/test_bearer_token_expiry.py +0 -0
  77. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/tests/test_bootstrap.py +0 -0
  78. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/tests/test_bootstrap_resolution.py +0 -0
  79. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/tests/test_bulk_invite_message.py +0 -0
  80. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/tests/test_cli.py +0 -0
  81. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/tests/test_constants.py +0 -0
  82. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/tests/test_db_adapter.py +0 -0
  83. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/tests/test_demo_account.py +0 -0
  84. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/tests/test_demo_guard.py +0 -0
  85. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/tests/test_demo_login.py +0 -0
  86. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/tests/test_edit_recent_activity.py +0 -0
  87. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/tests/test_invite_flow.py +0 -0
  88. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/tests/test_invite_reuse.py +0 -0
  89. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/tests/test_legacy_session_expiry.py +0 -0
  90. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/tests/test_mailer.py +0 -0
  91. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/tests/test_oauth.py +0 -0
  92. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/tests/test_oauth_routes.py +0 -0
  93. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/tests/test_profile_password.py +0 -0
  94. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/tests/test_rate_limit.py +0 -0
  95. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/tests/test_remember_me.py +0 -0
  96. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/tests/test_revocation_broadcast.py +0 -0
  97. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/tests/test_role_model.py +0 -0
  98. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/tests/test_service_admin.py +0 -0
  99. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/tests/test_service_admin_crud.py +0 -0
  100. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/tests/test_session_expiry.py +0 -0
  101. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/tests/test_session_revocation.py +0 -0
  102. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/tests/test_session_version_cache.py +0 -0
  103. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/tests/test_session_version_failopen.py +0 -0
  104. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/tests/test_session_version_hardening.py +0 -0
  105. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/tests/test_sessions_revoke.py +0 -0
  106. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/tests/test_settings.py +0 -0
  107. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/tests/test_user_role_model.py +0 -0
  108. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/tests/test_user_service.py +0 -0
  109. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/tests/test_user_state.py +0 -0
  110. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/tests/test_users_bulk_invite.py +0 -0
  111. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/tests/test_users_deps.py +0 -0
  112. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/tests/test_users_list_invites.py +0 -0
  113. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/tests/test_users_login_deep_link.py +0 -0
  114. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/tests/test_users_middleware_public_paths.py +0 -0
  115. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/tests/test_users_middleware_resolvers.py +0 -0
  116. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/tests/test_users_provider.py +0 -0
  117. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/tests/test_views.py +0 -0
  118. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/tests/test_views_admin.py +0 -0
  119. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/tests-js/EmailChipInput.test.tsx +0 -0
  120. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/tests-js/deriveState.test.ts +0 -0
  121. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/tests-js/invite-emails.test.ts +0 -0
  122. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/tsconfig.json +0 -0
  123. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/__init__.py +0 -0
  124. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/admin/__init__.py +0 -0
  125. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/admin/api.py +0 -0
  126. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/admin/bulk_invite.py +0 -0
  127. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/admin/components/IndexFilters.tsx +0 -0
  128. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/admin/components/RolesTab.tsx +0 -0
  129. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/admin/components/StatusPill.tsx +0 -0
  130. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/admin/components/UserCards.tsx +0 -0
  131. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/admin/components/UserRow.tsx +0 -0
  132. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/admin/components/UserRowMenu.tsx +0 -0
  133. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/admin/components/UsersEmpty.tsx +0 -0
  134. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/admin/components/UsersTable.tsx +0 -0
  135. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/admin/components/useUserRowActions.ts +0 -0
  136. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/admin/components/user-list-item.ts +0 -0
  137. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/admin/recent_activity.py +0 -0
  138. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/admin/resend_invite.py +0 -0
  139. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/admin/service.py +0 -0
  140. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/admin/user_state.py +0 -0
  141. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/admin/views.py +0 -0
  142. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/audit.py +0 -0
  143. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/auth_local/__init__.py +0 -0
  144. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/auth_local/components/AuthField.tsx +0 -0
  145. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/auth_local/components/AuthIntro.tsx +0 -0
  146. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/auth_local/components/AuthStateCard.tsx +0 -0
  147. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/auth_local/components/DemoSignIn.tsx +0 -0
  148. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/auth_local/components/InviteSummary.tsx +0 -0
  149. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/auth_local/components/PasswordCard.tsx +0 -0
  150. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/auth_local/components/PreferencesCard.tsx +0 -0
  151. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/auth_local/components/ProfileDetailsCard.tsx +0 -0
  152. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/auth_local/components/ResendVerification.tsx +0 -0
  153. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/auth_local/components/ResetLinkSent.tsx +0 -0
  154. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/auth_local/components/SessionsCard.tsx +0 -0
  155. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/auth_local/components/WaitingOnYou.tsx +0 -0
  156. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/auth_local/components/profile-user.ts +0 -0
  157. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/auth_local/demo_api.py +0 -0
  158. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/auth_local/invite_preview.py +0 -0
  159. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/auth_local/rate_limit.py +0 -0
  160. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/auth_local/self_account.py +0 -0
  161. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/auth_local/token_preview.py +0 -0
  162. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/backend.py +0 -0
  163. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/cli.py +0 -0
  164. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/constants.py +0 -0
  165. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/contracts/__init__.py +0 -0
  166. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/contracts/events.py +0 -0
  167. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/db_adapter.py +0 -0
  168. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/demo.py +0 -0
  169. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/demo_guard.py +0 -0
  170. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/exceptions.py +0 -0
  171. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/health.py +0 -0
  172. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/mailer/__init__.py +0 -0
  173. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/mailer/console.py +0 -0
  174. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/mailer/smtp.py +0 -0
  175. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/mailer/templates/.gitkeep +0 -0
  176. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/mailer/templates/invite.txt +0 -0
  177. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/mailer/templates/reset_password.txt +0 -0
  178. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/mailer/templates/verify_email.txt +0 -0
  179. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/middleware.py +0 -0
  180. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/models/__init__.py +0 -0
  181. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/models/_base.py +0 -0
  182. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/models/access_token.py +0 -0
  183. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/models/oauth_account.py +0 -0
  184. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/models/refresh_token.py +0 -0
  185. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/module.py +0 -0
  186. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/oauth/__init__.py +0 -0
  187. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/oauth/providers.py +0 -0
  188. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/pages/.gitkeep +0 -0
  189. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/pages/ForgotPassword.tsx +0 -0
  190. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/pages/Profile.tsx +0 -0
  191. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/pages/Users/components/AccountCard.tsx +0 -0
  192. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/pages/Users/components/CreateUserFields.tsx +0 -0
  193. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/pages/Users/components/DangerZone.tsx +0 -0
  194. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/pages/Users/components/DetailsCard.tsx +0 -0
  195. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/pages/Users/components/EditHeaderActions.tsx +0 -0
  196. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/pages/Users/components/EmailChipInput.tsx +0 -0
  197. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/pages/Users/components/InviteFields.tsx +0 -0
  198. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/pages/Users/components/InviteResults.tsx +0 -0
  199. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/pages/Users/components/RecentActivityCard.tsx +0 -0
  200. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/pages/Users/components/RolePicker.tsx +0 -0
  201. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/pages/Users/components/UserStats.tsx +0 -0
  202. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/pages/Users/components/useEditUserForm.ts +0 -0
  203. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/pages/Users/components/useUserActions.ts +0 -0
  204. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/pages/Users/invite-emails.ts +0 -0
  205. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/pages/VerifyEmail.tsx +0 -0
  206. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/py.typed +0 -0
  207. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/roles_cache.py +0 -0
  208. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/session_revocation.py +0 -0
  209. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/session_version_cache.py +0 -0
  210. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/settings.py +0 -0
  211. {simple_module_users-0.0.35 → simple_module_users-0.0.37}/users/state.py +0 -0
@@ -64,3 +64,4 @@ host/client_app/.playwright-cli/*
64
64
  .superpowers/
65
65
  .qa/
66
66
  .verify/
67
+ /openapi.json
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: simple_module_users
3
- Version: 0.0.35
3
+ Version: 0.0.37
4
4
  Summary: Email + password user management, admin invites, RBAC-ready — replaces Keycloak for simple_module apps
5
5
  Project-URL: Homepage, https://github.com/antosubash/simple_module_python
6
6
  Project-URL: Repository, https://github.com/antosubash/simple_module_python
@@ -24,11 +24,11 @@ Requires-Python: >=3.12
24
24
  Requires-Dist: aiosmtplib>=3.0
25
25
  Requires-Dist: cachetools>=5.3
26
26
  Requires-Dist: fastapi-users[oauth,sqlalchemy]<16,>=15
27
- Requires-Dist: simple-module-auth==0.0.35
28
- Requires-Dist: simple-module-core==0.0.35
29
- Requires-Dist: simple-module-db==0.0.35
30
- Requires-Dist: simple-module-hosting==0.0.35
31
- Requires-Dist: simple-module-settings==0.0.35
27
+ Requires-Dist: simple-module-auth==0.0.37
28
+ Requires-Dist: simple-module-core==0.0.37
29
+ Requires-Dist: simple-module-db==0.0.37
30
+ Requires-Dist: simple-module-hosting==0.0.37
31
+ Requires-Dist: simple-module-settings==0.0.37
32
32
  Requires-Dist: typer>=0.12
33
33
  Description-Content-Type: text/markdown
34
34
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "simple_module_users"
3
- version = "0.0.35"
3
+ version = "0.0.37"
4
4
  description = "Email + password user management, admin invites, RBAC-ready — replaces Keycloak for simple_module apps"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -21,11 +21,11 @@ classifiers = [
21
21
  "Typing :: Typed",
22
22
  ]
23
23
  dependencies = [
24
- "simple_module_core==0.0.35",
25
- "simple_module_db==0.0.35",
26
- "simple_module_hosting==0.0.35",
27
- "simple_module_settings==0.0.35",
28
- "simple_module_auth==0.0.35",
24
+ "simple_module_core==0.0.37",
25
+ "simple_module_db==0.0.37",
26
+ "simple_module_hosting==0.0.37",
27
+ "simple_module_settings==0.0.37",
28
+ "simple_module_auth==0.0.37",
29
29
  # Pinned to a narrow range: `deps.py` relies on mutating CookieTransport
30
30
  # fields after construction (see reconfigure_cookie_transport in backend.py).
31
31
  # Bumping the major version requires re-checking those field names.
@@ -49,7 +49,6 @@ async def _build_app(db_state, inner_handler=None, *, principal_resolvers=None):
49
49
  "email": user.email,
50
50
  "name": user.name,
51
51
  "roles": user.roles,
52
- "tenant_id": user.tenant_id,
53
52
  }
54
53
  if user is not None
55
54
  else None
@@ -106,7 +105,6 @@ async def mw_active_user(db_session, _mw_seed_roles):
106
105
  is_superuser=False,
107
106
  is_verified=True,
108
107
  full_name="Middleware Tester",
109
- tenant_id="acme",
110
108
  )
111
109
  link = UserRole(user_id=user_id, role_id=ADMIN_ROLE_ID)
112
110
  db_session.add_all([user, link])
@@ -0,0 +1,84 @@
1
+ """A fake OAuth provider (no network) to drive the ``/login`` + ``/callback`` flow."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import uuid
6
+ from urllib.parse import parse_qs, urlparse
7
+
8
+ import httpx
9
+ from fastapi_users.password import PasswordHelper
10
+ from sqlalchemy import select
11
+ from users.models import User
12
+
13
+
14
+ class _FakeOAuthClient:
15
+ def __init__(self, account_id: str, account_email: str) -> None:
16
+ self._account_id = account_id
17
+ self._account_email = account_email
18
+
19
+ async def get_authorization_url(self, redirect_uri: str, state: str) -> str:
20
+ return f"https://idp.example/authorize?state={state}"
21
+
22
+ async def get_access_token(self, code: str, redirect_uri: str) -> dict:
23
+ return {"access_token": "fake-token", "expires_at": None, "refresh_token": None}
24
+
25
+ async def get_id_email(self, access_token: str) -> tuple[str, str]:
26
+ return self._account_id, self._account_email
27
+
28
+
29
+ def install_fake_provider(app, account_id: str, account_email: str) -> None:
30
+ from users.oauth import OAuthProvider
31
+
32
+ app.state.users.oauth_clients["microsoft"] = OAuthProvider(
33
+ "microsoft", "Microsoft", _FakeOAuthClient(account_id, account_email)
34
+ )
35
+
36
+
37
+ def set_users_settings(app, **update) -> None:
38
+ """Change fields on the live users settings, as an admin edit would."""
39
+ settings = app.state.users.settings
40
+ app.state.users.settings = settings.model_copy(update=update)
41
+
42
+
43
+ def set_allow_signup(app, allowed: bool) -> None:
44
+ """Flip ``allow_signup`` on the live settings, as an admin edit would."""
45
+ set_users_settings(app, allow_signup=allowed)
46
+
47
+
48
+ async def find_user(app, email: str) -> User | None:
49
+ async with app.state.sm.db.session_factory() as session:
50
+ stmt = select(User).where(User.email == email)
51
+ return (await session.execute(stmt)).scalar_one_or_none()
52
+
53
+
54
+ async def seed_password_user(app, email: str, **fields) -> uuid.UUID:
55
+ """Insert an active password account for ``email``; return its id."""
56
+ user_id = uuid.uuid4()
57
+ async with app.state.sm.db.session_factory() as session:
58
+ session.add(
59
+ User(
60
+ id=user_id,
61
+ email=email,
62
+ hashed_password=PasswordHelper().hash("SecurePass1!"),
63
+ is_active=True,
64
+ **fields,
65
+ )
66
+ )
67
+ await session.commit()
68
+ return user_id
69
+
70
+
71
+ async def oauth_callback(client: httpx.AsyncClient) -> httpx.Response:
72
+ """Drive /login then /callback; return the callback response."""
73
+ login = await client.get("/api/users/auth/microsoft/login", follow_redirects=False)
74
+ assert login.status_code == 302
75
+ state = parse_qs(urlparse(login.headers["location"]).query)["state"][0]
76
+ return await client.get(
77
+ f"/api/users/auth/microsoft/callback?code=abc&state={state}",
78
+ follow_redirects=False,
79
+ )
80
+
81
+
82
+ async def run_oauth_login(client: httpx.AsyncClient) -> int:
83
+ """Drive /login then /callback; return the callback status code."""
84
+ return (await oauth_callback(client)).status_code
@@ -257,3 +257,10 @@ class TestAcceptInviteFullName:
257
257
  await session.execute(select(User).where(User.email == "joins@example.com"))
258
258
  ).scalar_one()
259
259
  assert stored.full_name == "Rob Meyer"
260
+
261
+
262
+ class TestLoginAsideProps:
263
+ @pytest.mark.anyio
264
+ async def test_keycloak_flag_is_false_without_the_module(self, anon_client):
265
+ props = _props(await anon_client.get("/users/login", headers=_INERTIA_HEADERS))
266
+ assert props["keycloak_installed"] is False
@@ -11,9 +11,9 @@ Covers the feature added on top of the OAuth/OIDC login flow:
11
11
  from __future__ import annotations
12
12
 
13
13
  import uuid
14
- from urllib.parse import parse_qs, urlparse
15
14
 
16
15
  import pytest
16
+ from _users_oauth_support import install_fake_provider, run_oauth_login, set_allow_signup
17
17
  from fastapi_users.password import PasswordHelper
18
18
  from sqlalchemy import select
19
19
  from users.models import Role, User
@@ -21,46 +21,6 @@ from users.models import Role, User
21
21
  _pw = PasswordHelper()
22
22
 
23
23
 
24
- # ---------------------------------------------------------------------------
25
- # Fake OAuth provider (no network) to drive the /login + /callback flow
26
- # ---------------------------------------------------------------------------
27
-
28
-
29
- class _FakeOAuthClient:
30
- def __init__(self, account_id: str, account_email: str) -> None:
31
- self._account_id = account_id
32
- self._account_email = account_email
33
-
34
- async def get_authorization_url(self, redirect_uri: str, state: str) -> str:
35
- return f"https://idp.example/authorize?state={state}"
36
-
37
- async def get_access_token(self, code: str, redirect_uri: str) -> dict:
38
- return {"access_token": "fake-token", "expires_at": None, "refresh_token": None}
39
-
40
- async def get_id_email(self, access_token: str) -> tuple[str, str]:
41
- return self._account_id, self._account_email
42
-
43
-
44
- def _install_fake_provider(app, account_id: str, account_email: str) -> None:
45
- from users.oauth import OAuthProvider
46
-
47
- app.state.users.oauth_clients["microsoft"] = OAuthProvider(
48
- "microsoft", "Microsoft", _FakeOAuthClient(account_id, account_email)
49
- )
50
-
51
-
52
- async def _run_oauth_login(client) -> int:
53
- """Drive /login then /callback; return the callback status code."""
54
- login = await client.get("/api/users/auth/microsoft/login", follow_redirects=False)
55
- assert login.status_code == 302
56
- state = parse_qs(urlparse(login.headers["location"]).query)["state"][0]
57
- cb = await client.get(
58
- f"/api/users/auth/microsoft/callback?code=abc&state={state}",
59
- follow_redirects=False,
60
- )
61
- return cb.status_code
62
-
63
-
64
24
  async def _get_user_by_email(db, email: str) -> User:
65
25
  return (await db.execute(select(User).where(User.email == email))).scalar_one()
66
26
 
@@ -72,9 +32,10 @@ async def _get_user_by_email(db, email: str) -> User:
72
32
 
73
33
  @pytest.mark.anyio
74
34
  async def test_new_oauth_login_creates_external_user(users_app, anon_client, users_db):
75
- _install_fake_provider(users_app, "ms-oid-1", "sso-new@example.com")
35
+ set_allow_signup(users_app, True) # creating an account via OAuth is a signup (#543)
36
+ install_fake_provider(users_app, "ms-oid-1", "sso-new@example.com")
76
37
 
77
- assert await _run_oauth_login(anon_client) == 303
38
+ assert await run_oauth_login(anon_client) == 303
78
39
 
79
40
  user = await _get_user_by_email(users_db, "sso-new@example.com")
80
41
  assert user.is_external is True
@@ -96,8 +57,8 @@ async def test_oauth_login_links_existing_password_user_stays_local(
96
57
  users_db.add(existing)
97
58
  await users_db.commit()
98
59
 
99
- _install_fake_provider(users_app, "ms-oid-2", "local@example.com")
100
- assert await _run_oauth_login(anon_client) == 303
60
+ install_fake_provider(users_app, "ms-oid-2", "local@example.com")
61
+ assert await run_oauth_login(anon_client) == 303
101
62
 
102
63
  user = await _get_user_by_email(users_db, "local@example.com")
103
64
  assert user.is_external is False
@@ -0,0 +1,54 @@
1
+ """A pre-#381 session cookie's cached ``tenant_id`` is ignored.
2
+
3
+ ``users_user.tenant_id`` is gone and the active tenant is resolved per request
4
+ from memberships, but cookies signed before the upgrade still carry the old
5
+ value inside ``user_ctx``. Honouring it would keep acting for a tenant the
6
+ user may have left.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import time
12
+
13
+ import pytest
14
+ from auth.contracts.schemas import UserContext
15
+ from simple_module_hosting.session import SESSION_EXPIRES_AT_KEY
16
+ from sqlalchemy import select
17
+ from starlette.requests import Request
18
+ from users.models import User
19
+ from users.provider import UsersAuthProvider
20
+
21
+
22
+ async def _admin_id(users_app) -> str:
23
+ async with users_app.state.sm.db.session_factory() as session:
24
+ stmt = select(User.id).where(User.email == "admin@example.com")
25
+ return str((await session.execute(stmt)).scalar_one())
26
+
27
+
28
+ @pytest.mark.anyio
29
+ async def test_old_cookie_tenant_id_is_dropped_on_the_cached_path(users_app):
30
+ user_id = await _admin_id(users_app)
31
+ legacy_ctx = UserContext(
32
+ id=user_id, email="admin@example.com", name="Admin", roles=["admin"], tenant_id="acme"
33
+ ).to_session_dict()
34
+ session = {
35
+ "user_id": user_id,
36
+ "user_ctx": legacy_ctx,
37
+ "session_version": 0,
38
+ SESSION_EXPIRES_AT_KEY: int(time.time()) + 3600,
39
+ }
40
+ scope = {
41
+ "type": "http",
42
+ "method": "GET",
43
+ "path": "/",
44
+ "headers": [],
45
+ "app": users_app,
46
+ "session": session,
47
+ }
48
+
49
+ ctx = await UsersAuthProvider().resolve_user(Request(scope))
50
+
51
+ assert ctx is not None
52
+ assert ctx.id == user_id
53
+ assert ctx.name == "Admin" # the cached context was used, not a DB reload
54
+ assert ctx.tenant_id is None
@@ -0,0 +1,39 @@
1
+ """Login fallback with a module mounted at the root (#435)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import pytest
6
+
7
+
8
+ def _fallback_url(app, prefixes: dict[str, str]) -> str:
9
+ """Run the login fallback against a Dashboard-less module set (#435)."""
10
+ from users.startup import apply_login_redirect_fallback
11
+
12
+ sm = app.state.sm
13
+ saved_mods, saved_prefixes = sm.modules, dict(sm.view_prefixes)
14
+ settings = app.state.users.settings
15
+ saved_url = settings.login_redirect_url
16
+ object.__setattr__(sm, "modules", tuple(m for m in saved_mods if m.meta.name != "Dashboard"))
17
+ sm.view_prefixes.clear()
18
+ sm.view_prefixes.update(prefixes)
19
+ settings.login_redirect_url = "/dashboard/"
20
+ try:
21
+ apply_login_redirect_fallback(app, settings, "Users", "/dashboard/")
22
+ return settings.login_redirect_url
23
+ finally:
24
+ object.__setattr__(sm, "modules", saved_mods)
25
+ sm.view_prefixes.clear()
26
+ sm.view_prefixes.update(saved_prefixes)
27
+ settings.login_redirect_url = saved_url
28
+
29
+
30
+ @pytest.mark.anyio
31
+ async def test_login_redirect_fallback_prefers_root_mounted_module(users_app):
32
+ """#435: a module that serves views at ``/`` (view_prefix == "") wins."""
33
+ assert _fallback_url(users_app, {"Users": "/users", "Home": "", "Other": "/o"}) == "/"
34
+
35
+
36
+ @pytest.mark.anyio
37
+ async def test_login_redirect_fallback_skips_api_only_module(users_app):
38
+ """An API-only module also has view_prefix == "" but is not in the map."""
39
+ assert _fallback_url(users_app, {"Users": "/users", "Other": "/o"}) == "/o/"
@@ -17,7 +17,10 @@ import uuid
17
17
 
18
18
  import pytest
19
19
 
20
- _FAKE_ID = uuid.uuid4()
20
+ # Fixed, not ``uuid4()``: the id lands in the parametrize ids, and pytest-xdist
21
+ # refuses to run when its workers collect differently-named tests. The request
22
+ # must be refused before any lookup, so which id it names does not matter.
23
+ _FAKE_ID = uuid.UUID("00000000-0000-4000-8000-000000000000")
21
24
  _PROTECTED_ENDPOINTS: tuple[tuple[str, str, dict | None], ...] = (
22
25
  # users — admin sub-router
23
26
  ("GET", "/api/users/admin", None),
@@ -71,6 +71,27 @@ class TestTokenLogin:
71
71
  )
72
72
  assert resp.status_code == 401
73
73
 
74
+ @pytest.mark.anyio
75
+ async def test_legacy_hash_is_upgraded_on_token_login(self, anon_client, users_db):
76
+ from pwdlib.hashers.bcrypt import BcryptHasher
77
+
78
+ legacy = BcryptHasher().hash("SecurePass1!")
79
+ assert legacy.startswith("$2")
80
+ user = await _seed_user(users_db, email="legacy@example.com")
81
+ user.hashed_password = legacy
82
+ await users_db.commit()
83
+
84
+ for _ in range(2): # second login proves the upgraded hash still verifies
85
+ resp = await anon_client.post(
86
+ "/api/users/auth/token",
87
+ json={"email": "legacy@example.com", "password": "SecurePass1!"},
88
+ )
89
+ assert resp.status_code == 200
90
+
91
+ await users_db.refresh(user)
92
+ assert user.hashed_password != legacy
93
+ assert user.hashed_password.startswith("$argon2")
94
+
74
95
  @pytest.mark.anyio
75
96
  async def test_valid_credentials_returns_token_pair(self, anon_client, users_db):
76
97
  await _seed_user(users_db)
@@ -80,7 +101,7 @@ class TestTokenLogin:
80
101
  )
81
102
  assert resp.status_code == 200
82
103
  body = resp.json()
83
- assert body["token_type"] == "bearer"
104
+ assert body["token_type"] == "Bearer"
84
105
  assert body["access_token"]
85
106
  assert body["refresh_token"]
86
107
  assert body["expires_in"] > 0
@@ -4,7 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import uuid
6
6
  from datetime import UTC, datetime
7
- from unittest.mock import AsyncMock, MagicMock
7
+ from unittest.mock import AsyncMock, MagicMock, patch
8
8
 
9
9
  import pytest
10
10
  from fastapi_users import exceptions
@@ -51,6 +51,8 @@ def fake_user():
51
51
  def fake_user_db():
52
52
  db = MagicMock()
53
53
  db.update = AsyncMock(return_value=None)
54
+ db.session.flush = AsyncMock()
55
+ db.session.commit = AsyncMock()
54
56
  return db
55
57
 
56
58
 
@@ -125,17 +127,53 @@ class TestOnAfterRegister:
125
127
  # Pre-verified so the manager doesn't try to send a verify email.
126
128
  fake_user.is_verified = True
127
129
 
128
- await manager.on_after_register(fake_user, request)
130
+ with patch("users.manager.attach_default_role", AsyncMock()):
131
+ await manager.on_after_register(fake_user, request)
129
132
 
130
133
  (event,) = bus.assert_published(UserRegistered)
131
134
  assert event.user_id == fake_user.id
132
135
  assert event.email == fake_user.email
133
136
 
134
137
  @pytest.mark.anyio
135
- async def test_without_request_skips_publish(self, manager, fake_user):
136
- """CLI/bootstrap flows pass request=None — must not crash."""
138
+ async def test_request_bound_registration_gets_the_user_role(
139
+ self, manager, fake_user, fake_user_db
140
+ ):
141
+ """GH #488: a signup with no role holds no permissions at all."""
142
+ from users.models import UserRole
143
+
144
+ request = MagicMock()
145
+ request.state = MagicMock(spec=[]) # a local signup, not OAuth
146
+ request.app.state.sm.event_bus = MagicMock(publish=AsyncMock())
147
+ fake_user.is_verified = True
148
+ role = MagicMock(id=uuid.uuid4())
149
+ fake_user_db.session.execute = AsyncMock(
150
+ return_value=MagicMock(scalar_one_or_none=MagicMock(return_value=role))
151
+ )
152
+ await manager.on_after_register(fake_user, request)
153
+
154
+ (added,) = fake_user_db.session.add.call_args.args
155
+ assert isinstance(added, UserRole)
156
+ assert (added.user_id, added.role_id) == (fake_user.id, role.id)
157
+
158
+ @pytest.mark.anyio
159
+ async def test_a_deleted_user_role_is_not_recreated(self, manager, fake_user, fake_user_db):
160
+ """An admin who removed the role meant it; a public signup must not undo that."""
161
+ request = MagicMock()
162
+ request.app.state.sm.event_bus = MagicMock(publish=AsyncMock())
163
+ fake_user.is_verified = True
164
+ fake_user_db.session.execute = AsyncMock(
165
+ return_value=MagicMock(scalar_one_or_none=MagicMock(return_value=None))
166
+ )
167
+ await manager.on_after_register(fake_user, request)
168
+ fake_user_db.session.add.assert_not_called()
169
+
170
+ @pytest.mark.anyio
171
+ async def test_without_request_skips_publish(self, manager, fake_user, fake_user_db):
172
+ """CLI/bootstrap and admin flows pass request=None — no crash, no role:
173
+ the admin picks the roles there, and may pick none."""
137
174
  fake_user.is_verified = True
138
175
  await manager.on_after_register(fake_user, None)
176
+ fake_user_db.session.add.assert_not_called()
139
177
 
140
178
 
141
179
  class TestOnAfterForgotPassword:
@@ -35,7 +35,6 @@ class TestUserTableShape:
35
35
  "is_superuser",
36
36
  "is_verified",
37
37
  "full_name",
38
- "tenant_id",
39
38
  "disabled_at",
40
39
  "last_login_at",
41
40
  # AuditMixin columns
@@ -0,0 +1,54 @@
1
+ """The ``Authorization`` scheme is case-insensitive (RFC 9110 §11.1, GH #533).
2
+
3
+ ``/auth/token`` used to answer ``token_type="bearer"``, and a client that built
4
+ its header from that value sent ``bearer <token>`` — which the provider refused.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import pytest
10
+ from starlette.requests import Request
11
+
12
+ _TOKEN = "/api/users/auth/token"
13
+
14
+
15
+ def _request(app, header: str) -> Request:
16
+ return Request(
17
+ {
18
+ "type": "http",
19
+ "method": "GET",
20
+ "path": "/api/anything",
21
+ "headers": [(b"authorization", header.encode())],
22
+ "session": {},
23
+ "app": app,
24
+ }
25
+ )
26
+
27
+
28
+ @pytest.mark.anyio
29
+ @pytest.mark.parametrize("scheme", ["Bearer", "bearer", "BEARER", " bearer "])
30
+ async def test_any_scheme_casing_authenticates(users_app, anon_client, scheme):
31
+ from users.provider import UsersAuthProvider
32
+
33
+ resp = await anon_client.post(
34
+ _TOKEN, json={"email": "admin@example.com", "password": "AdminPass1!"}
35
+ )
36
+ assert resp.status_code == 200, resp.text
37
+ body = resp.json()
38
+ assert body["token_type"] == "Bearer"
39
+
40
+ provider = UsersAuthProvider()
41
+ request = _request(users_app, f"{scheme} {body['access_token']}")
42
+ assert provider.is_bearer_request(request)
43
+ ctx = await provider.resolve_user(request)
44
+ assert ctx is not None
45
+ assert ctx.email == "admin@example.com"
46
+
47
+
48
+ @pytest.mark.anyio
49
+ async def test_another_scheme_is_not_a_bearer_request(users_app):
50
+ from users.provider import UsersAuthProvider
51
+
52
+ request = _request(users_app, "Basic dXNlcjpwYXNz")
53
+ assert not UsersAuthProvider().is_bearer_request(request)
54
+ assert await UsersAuthProvider().resolve_user(request) is None
@@ -0,0 +1,82 @@
1
+ """Which registrations get the standard ``user`` role (GH #488 review).
2
+
3
+ OAuth provisioning is not gated by ``allow_signup``, so the role it attaches
4
+ must be: with signups closed, an account any provider user can create holds no
5
+ role until an admin grants one.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import uuid
11
+ from types import SimpleNamespace
12
+ from unittest.mock import AsyncMock, MagicMock
13
+
14
+ import pytest
15
+ from users.constants import OAUTH_REGISTRATION_REQUEST_FLAG
16
+ from users.manager import UserManager
17
+ from users.models import UserRole
18
+ from users.settings import UsersSettings
19
+
20
+ _ROLE_ID = uuid.uuid4()
21
+
22
+
23
+ def _manager(*, allow_signup: bool) -> UserManager:
24
+ settings = UsersSettings(
25
+ verification_token_secret="test-verify-secret-at-least-32-bytes-long",
26
+ reset_password_token_secret="test-reset-secret-at-least-32-bytes-long",
27
+ allow_signup=allow_signup,
28
+ )
29
+ user_db = MagicMock()
30
+ user_db.update = AsyncMock()
31
+ user_db.session.flush = AsyncMock()
32
+ user_db.session.commit = AsyncMock()
33
+ user_db.session.execute = AsyncMock(
34
+ return_value=MagicMock(scalar_one_or_none=MagicMock(return_value=MagicMock(id=_ROLE_ID)))
35
+ )
36
+ return UserManager(user_db, MagicMock(send_verification=AsyncMock()), settings)
37
+
38
+
39
+ def _request(*, oauth: bool) -> MagicMock:
40
+ request = MagicMock()
41
+ request.app.state.sm.event_bus = MagicMock(publish=AsyncMock())
42
+ request.state = SimpleNamespace(**({OAUTH_REGISTRATION_REQUEST_FLAG: True} if oauth else {}))
43
+ return request
44
+
45
+
46
+ def _user() -> MagicMock:
47
+ return MagicMock(id=uuid.uuid4(), email="new@example.com", is_verified=True)
48
+
49
+
50
+ def _added_roles(manager: UserManager) -> list[UserRole]:
51
+ return [
52
+ c.args[0]
53
+ for c in manager.user_db.session.add.call_args_list
54
+ if isinstance(c.args[0], UserRole)
55
+ ]
56
+
57
+
58
+ @pytest.mark.anyio
59
+ async def test_oauth_provisioning_with_signup_closed_gets_no_role():
60
+ manager = _manager(allow_signup=False)
61
+ await manager.on_after_register(_user(), _request(oauth=True))
62
+ assert _added_roles(manager) == []
63
+
64
+
65
+ @pytest.mark.anyio
66
+ async def test_oauth_provisioning_with_signup_open_gets_the_user_role():
67
+ manager = _manager(allow_signup=True)
68
+ user = _user()
69
+ await manager.on_after_register(user, _request(oauth=True))
70
+ (link,) = _added_roles(manager)
71
+ assert (link.user_id, link.role_id) == (user.id, _ROLE_ID)
72
+
73
+
74
+ @pytest.mark.anyio
75
+ async def test_local_registration_gets_the_user_role():
76
+ # /register itself 404s while signup is closed, so reaching the hook
77
+ # means the operator opened it.
78
+ manager = _manager(allow_signup=True)
79
+ user = _user()
80
+ await manager.on_after_register(user, _request(oauth=False))
81
+ (link,) = _added_roles(manager)
82
+ assert (link.user_id, link.role_id) == (user.id, _ROLE_ID)
@@ -81,7 +81,6 @@ async def test_authenticated_request_sets_user_context(db_state, mw_active_user)
81
81
  assert data["user"]["email"] == "middleware-test@example.com"
82
82
  assert data["user"]["name"] == "Middleware Tester"
83
83
  assert data["user"]["roles"] == ["admin"]
84
- assert data["user"]["tenant_id"] == "acme"
85
84
 
86
85
 
87
86
  # ---------------------------------------------------------------------------