fastapi-fullstack 0.1.7__py3-none-any.whl

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 (241) hide show
  1. fastapi_fullstack-0.1.7.dist-info/METADATA +739 -0
  2. fastapi_fullstack-0.1.7.dist-info/RECORD +241 -0
  3. fastapi_fullstack-0.1.7.dist-info/WHEEL +4 -0
  4. fastapi_fullstack-0.1.7.dist-info/entry_points.txt +2 -0
  5. fastapi_fullstack-0.1.7.dist-info/licenses/LICENSE +21 -0
  6. fastapi_gen/__init__.py +3 -0
  7. fastapi_gen/cli.py +442 -0
  8. fastapi_gen/config.py +356 -0
  9. fastapi_gen/generator.py +207 -0
  10. fastapi_gen/prompts.py +874 -0
  11. fastapi_gen/template/VARIABLES.md +276 -0
  12. fastapi_gen/template/cookiecutter.json +93 -0
  13. fastapi_gen/template/hooks/post_gen_project.py +355 -0
  14. fastapi_gen/template/{{cookiecutter.project_slug}}/.env.prod.example +56 -0
  15. fastapi_gen/template/{{cookiecutter.project_slug}}/.github/workflows/ci.yml +150 -0
  16. fastapi_gen/template/{{cookiecutter.project_slug}}/.gitignore +109 -0
  17. fastapi_gen/template/{{cookiecutter.project_slug}}/AGENTS.md +55 -0
  18. fastapi_gen/template/{{cookiecutter.project_slug}}/CLAUDE.md +99 -0
  19. fastapi_gen/template/{{cookiecutter.project_slug}}/Makefile +315 -0
  20. fastapi_gen/template/{{cookiecutter.project_slug}}/README.md +768 -0
  21. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/.dockerignore +60 -0
  22. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/.env.example +155 -0
  23. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/.pre-commit-config.yaml +32 -0
  24. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/Dockerfile +56 -0
  25. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/alembic/env.py +76 -0
  26. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/alembic/script.py.mako +30 -0
  27. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/alembic/versions/.gitkeep +0 -0
  28. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/alembic.ini +48 -0
  29. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/__init__.py +3 -0
  30. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/admin.py +447 -0
  31. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/__init__.py +23 -0
  32. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/assistant.py +226 -0
  33. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/langchain_assistant.py +226 -0
  34. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/prompts.py +10 -0
  35. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/tools/__init__.py +13 -0
  36. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/tools/datetime_tool.py +17 -0
  37. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/__init__.py +1 -0
  38. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/deps.py +541 -0
  39. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/exception_handlers.py +98 -0
  40. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/router.py +10 -0
  41. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/__init__.py +9 -0
  42. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/__init__.py +87 -0
  43. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/agent.py +902 -0
  44. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/auth.py +395 -0
  45. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/conversations.py +498 -0
  46. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/health.py +227 -0
  47. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/items.py +275 -0
  48. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/oauth.py +205 -0
  49. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/sessions.py +168 -0
  50. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/users.py +333 -0
  51. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/webhooks.py +477 -0
  52. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/ws.py +46 -0
  53. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/versioning.py +221 -0
  54. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/clients/__init__.py +14 -0
  55. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/clients/redis.py +88 -0
  56. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/commands/__init__.py +117 -0
  57. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/commands/cleanup.py +75 -0
  58. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/commands/example.py +28 -0
  59. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/commands/seed.py +266 -0
  60. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/__init__.py +5 -0
  61. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/cache.py +23 -0
  62. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/config.py +267 -0
  63. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/csrf.py +153 -0
  64. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/exceptions.py +122 -0
  65. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/logfire_setup.py +101 -0
  66. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/middleware.py +99 -0
  67. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/oauth.py +23 -0
  68. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/rate_limit.py +58 -0
  69. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/sanitize.py +271 -0
  70. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/security.py +102 -0
  71. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/__init__.py +7 -0
  72. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/base.py +41 -0
  73. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/__init__.py +31 -0
  74. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/conversation.py +319 -0
  75. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/item.py +96 -0
  76. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/session.py +126 -0
  77. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/user.py +218 -0
  78. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/webhook.py +244 -0
  79. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/session.py +130 -0
  80. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/main.py +334 -0
  81. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/pipelines/__init__.py +9 -0
  82. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/pipelines/base.py +73 -0
  83. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/__init__.py +49 -0
  84. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/base.py +154 -0
  85. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/conversation.py +838 -0
  86. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/item.py +222 -0
  87. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/session.py +318 -0
  88. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/user.py +322 -0
  89. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/webhook.py +358 -0
  90. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/schemas/__init__.py +50 -0
  91. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/schemas/base.py +57 -0
  92. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/schemas/conversation.py +192 -0
  93. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/schemas/item.py +52 -0
  94. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/schemas/session.py +42 -0
  95. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/schemas/token.py +31 -0
  96. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/schemas/user.py +64 -0
  97. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/schemas/webhook.py +89 -0
  98. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/__init__.py +38 -0
  99. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/conversation.py +850 -0
  100. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/item.py +246 -0
  101. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/session.py +333 -0
  102. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/user.py +432 -0
  103. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/webhook.py +561 -0
  104. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/__init__.py +5 -0
  105. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/celery_app.py +64 -0
  106. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/taskiq_app.py +38 -0
  107. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/tasks/__init__.py +25 -0
  108. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/tasks/examples.py +106 -0
  109. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/tasks/schedules.py +29 -0
  110. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/tasks/taskiq_examples.py +92 -0
  111. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/cli/__init__.py +1 -0
  112. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/cli/commands.py +438 -0
  113. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/pyproject.toml +180 -0
  114. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/scripts/.gitkeep +0 -0
  115. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/__init__.py +1 -0
  116. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/api/__init__.py +1 -0
  117. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/api/test_auth.py +242 -0
  118. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/api/test_exceptions.py +151 -0
  119. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/api/test_health.py +113 -0
  120. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/api/test_items.py +310 -0
  121. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/api/test_users.py +253 -0
  122. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/conftest.py +151 -0
  123. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_admin.py +890 -0
  124. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_agents.py +261 -0
  125. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_clients.py +183 -0
  126. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_commands.py +173 -0
  127. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_core.py +143 -0
  128. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_pipelines.py +118 -0
  129. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_repositories.py +181 -0
  130. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_security.py +124 -0
  131. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_services.py +363 -0
  132. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_worker.py +85 -0
  133. fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.dev.yml +242 -0
  134. fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.frontend.yml +31 -0
  135. fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.prod.yml +435 -0
  136. fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.yml +241 -0
  137. fastapi_gen/template/{{cookiecutter.project_slug}}/docs/adding_features.md +132 -0
  138. fastapi_gen/template/{{cookiecutter.project_slug}}/docs/architecture.md +63 -0
  139. fastapi_gen/template/{{cookiecutter.project_slug}}/docs/patterns.md +161 -0
  140. fastapi_gen/template/{{cookiecutter.project_slug}}/docs/testing.md +120 -0
  141. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/.dockerignore +40 -0
  142. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/.env.example +12 -0
  143. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/.gitignore +45 -0
  144. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/.prettierignore +19 -0
  145. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/.prettierrc +11 -0
  146. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/Dockerfile +44 -0
  147. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/README.md +648 -0
  148. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/e2e/auth.setup.ts +49 -0
  149. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/e2e/auth.spec.ts +134 -0
  150. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/e2e/chat.spec.ts +207 -0
  151. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/e2e/home.spec.ts +73 -0
  152. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/instrumentation.ts +14 -0
  153. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/messages/en.json +84 -0
  154. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/messages/pl.json +84 -0
  155. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/next.config.ts +76 -0
  156. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/package.json +69 -0
  157. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/playwright.config.ts +101 -0
  158. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/postcss.config.mjs +7 -0
  159. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/(auth)/layout.tsx +11 -0
  160. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/(auth)/login/page.tsx +5 -0
  161. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/(auth)/register/page.tsx +5 -0
  162. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/(dashboard)/chat/page.tsx +48 -0
  163. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/(dashboard)/dashboard/page.tsx +99 -0
  164. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/(dashboard)/layout.tsx +17 -0
  165. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/(dashboard)/profile/page.tsx +152 -0
  166. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/auth/callback/page.tsx +113 -0
  167. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/layout.tsx +46 -0
  168. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/page.tsx +73 -0
  169. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/login/route.ts +58 -0
  170. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/logout/route.ts +24 -0
  171. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/me/route.ts +39 -0
  172. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/oauth-callback/route.ts +50 -0
  173. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/refresh/route.ts +54 -0
  174. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/register/route.ts +26 -0
  175. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/conversations/[id]/messages/route.ts +41 -0
  176. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/conversations/[id]/route.ts +108 -0
  177. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/conversations/route.ts +73 -0
  178. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/health/route.ts +21 -0
  179. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/globals.css +323 -0
  180. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/layout.tsx +22 -0
  181. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/providers.tsx +29 -0
  182. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/auth/index.ts +2 -0
  183. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/auth/login-form.tsx +120 -0
  184. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/auth/register-form.tsx +153 -0
  185. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/chat-container.tsx +234 -0
  186. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/chat-input.tsx +72 -0
  187. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/conversation-sidebar.tsx +328 -0
  188. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/copy-button.tsx +46 -0
  189. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/index.ts +11 -0
  190. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/local-conversation-sidebar.tsx +295 -0
  191. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/markdown-content.tsx +167 -0
  192. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/message-item.tsx +79 -0
  193. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/message-list.tsx +18 -0
  194. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/tool-call-card.tsx +79 -0
  195. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/icons/google-icon.tsx +32 -0
  196. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/icons/index.ts +3 -0
  197. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/language-switcher.tsx +97 -0
  198. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/layout/header.tsx +65 -0
  199. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/layout/index.ts +2 -0
  200. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/layout/sidebar.tsx +82 -0
  201. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/theme/index.ts +7 -0
  202. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/theme/theme-provider.tsx +53 -0
  203. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/theme/theme-toggle.tsx +105 -0
  204. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/badge.tsx +35 -0
  205. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/button.test.tsx +75 -0
  206. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/button.tsx +56 -0
  207. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/card.tsx +82 -0
  208. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/index.ts +13 -0
  209. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/input.tsx +21 -0
  210. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/label.tsx +21 -0
  211. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/sheet.tsx +109 -0
  212. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/hooks/index.ts +7 -0
  213. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/hooks/use-auth.ts +97 -0
  214. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/hooks/use-chat.ts +203 -0
  215. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/hooks/use-conversations.ts +181 -0
  216. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/hooks/use-local-chat.ts +165 -0
  217. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/hooks/use-websocket.ts +105 -0
  218. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/i18n.ts +37 -0
  219. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/lib/api-client.ts +90 -0
  220. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/lib/constants.ts +39 -0
  221. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/lib/server-api.ts +78 -0
  222. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/lib/utils.test.ts +44 -0
  223. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/lib/utils.ts +44 -0
  224. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/middleware.ts +31 -0
  225. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/stores/auth-store.test.ts +72 -0
  226. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/stores/auth-store.ts +64 -0
  227. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/stores/chat-sidebar-store.ts +17 -0
  228. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/stores/chat-store.ts +65 -0
  229. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/stores/conversation-store.ts +76 -0
  230. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/stores/index.ts +9 -0
  231. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/stores/local-chat-store.ts +255 -0
  232. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/stores/sidebar-store.ts +17 -0
  233. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/stores/theme-store.ts +44 -0
  234. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/types/api.ts +27 -0
  235. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/types/auth.ts +52 -0
  236. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/types/chat.ts +83 -0
  237. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/types/conversation.ts +49 -0
  238. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/types/index.ts +10 -0
  239. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/tsconfig.json +28 -0
  240. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/vitest.config.ts +36 -0
  241. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/vitest.setup.ts +56 -0
@@ -0,0 +1,890 @@
1
+ {%- if cookiecutter.enable_admin_panel and cookiecutter.use_postgresql %}
2
+ """Tests for admin panel with automatic model discovery."""
3
+
4
+ from typing import ClassVar
5
+ from unittest.mock import MagicMock, patch, AsyncMock
6
+
7
+ import pytest
8
+ from sqlalchemy import Boolean, Integer, String, DateTime, Text
9
+ from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
10
+
11
+ from app.admin import (
12
+ SENSITIVE_COLUMN_PATTERNS,
13
+ AUTO_GENERATED_COLUMNS,
14
+ MODEL_ICONS,
15
+ discover_models,
16
+ get_model_columns,
17
+ get_searchable_columns,
18
+ get_sortable_columns,
19
+ get_form_excluded_columns,
20
+ pluralize,
21
+ create_model_admin,
22
+ register_models_auto,
23
+ get_sync_engine,
24
+ setup_admin,
25
+ )
26
+ {%- if cookiecutter.admin_require_auth %}
27
+ from app.admin import AdminAuth
28
+ {%- endif %}
29
+
30
+
31
+ class MockBase(DeclarativeBase):
32
+ """Mock base class for testing."""
33
+
34
+ pass
35
+
36
+
37
+ class MockUser(MockBase):
38
+ """Mock user model for testing."""
39
+
40
+ __tablename__ = "mock_users"
41
+
42
+ id: Mapped[int] = mapped_column(Integer, primary_key=True)
43
+ email: Mapped[str] = mapped_column(String(255), nullable=False)
44
+ full_name: Mapped[str] = mapped_column(String(255), nullable=True)
45
+ hashed_password: Mapped[str] = mapped_column(String(255), nullable=True)
46
+ is_active: Mapped[bool] = mapped_column(Boolean, default=True)
47
+ created_at: Mapped[str] = mapped_column(DateTime, nullable=True)
48
+ updated_at: Mapped[str] = mapped_column(DateTime, nullable=True)
49
+
50
+
51
+ class MockItem(MockBase):
52
+ """Mock item model for testing."""
53
+
54
+ __tablename__ = "mock_items"
55
+
56
+ id: Mapped[int] = mapped_column(Integer, primary_key=True)
57
+ title: Mapped[str] = mapped_column(String(100), nullable=False)
58
+ description: Mapped[str] = mapped_column(Text, nullable=True)
59
+ price: Mapped[int] = mapped_column(Integer, nullable=True)
60
+
61
+
62
+ class MockSession(MockBase):
63
+ """Mock session model with sensitive columns."""
64
+
65
+ __tablename__ = "mock_sessions"
66
+
67
+ id: Mapped[int] = mapped_column(Integer, primary_key=True)
68
+ user_id: Mapped[int] = mapped_column(Integer, nullable=False)
69
+ refresh_token_hash: Mapped[str] = mapped_column(String(255), nullable=True)
70
+ api_key: Mapped[str] = mapped_column(String(255), nullable=True)
71
+ secret: Mapped[str] = mapped_column(String(255), nullable=True)
72
+
73
+
74
+ class TestConstants:
75
+ """Tests for module constants."""
76
+
77
+ def test_sensitive_column_patterns_exist(self):
78
+ """Test sensitive column patterns are defined."""
79
+ assert isinstance(SENSITIVE_COLUMN_PATTERNS, list)
80
+ assert "password" in SENSITIVE_COLUMN_PATTERNS
81
+ assert "hashed_password" in SENSITIVE_COLUMN_PATTERNS
82
+ assert "secret" in SENSITIVE_COLUMN_PATTERNS
83
+ assert "token" in SENSITIVE_COLUMN_PATTERNS
84
+ assert "api_key" in SENSITIVE_COLUMN_PATTERNS
85
+
86
+ def test_auto_generated_columns_exist(self):
87
+ """Test auto-generated columns are defined."""
88
+ assert isinstance(AUTO_GENERATED_COLUMNS, list)
89
+ assert "created_at" in AUTO_GENERATED_COLUMNS
90
+ assert "updated_at" in AUTO_GENERATED_COLUMNS
91
+
92
+ def test_model_icons_exist(self):
93
+ """Test model icons mapping is defined."""
94
+ assert isinstance(MODEL_ICONS, dict)
95
+ assert "User" in MODEL_ICONS
96
+ assert MODEL_ICONS["User"] == "fa-solid fa-user"
97
+
98
+
99
+ class TestDiscoverModels:
100
+ """Tests for discover_models function."""
101
+
102
+ def test_discovers_all_registered_models(self):
103
+ """Test that discover_models finds all models in the registry."""
104
+ models = discover_models(MockBase)
105
+ model_names = [m.__name__ for m in models]
106
+
107
+ assert "MockUser" in model_names
108
+ assert "MockItem" in model_names
109
+ assert "MockSession" in model_names
110
+
111
+ def test_returns_list(self):
112
+ """Test that discover_models returns a list."""
113
+ models = discover_models(MockBase)
114
+ assert isinstance(models, list)
115
+
116
+ def test_returns_model_classes(self):
117
+ """Test that discovered items are model classes."""
118
+ models = discover_models(MockBase)
119
+ for model in models:
120
+ assert hasattr(model, "__tablename__")
121
+
122
+
123
+ class TestGetModelColumns:
124
+ """Tests for get_model_columns function."""
125
+
126
+ def test_returns_all_columns(self):
127
+ """Test that all columns are returned."""
128
+ columns = get_model_columns(MockUser)
129
+
130
+ assert "id" in columns
131
+ assert "email" in columns
132
+ assert "full_name" in columns
133
+ assert "hashed_password" in columns
134
+ assert "is_active" in columns
135
+ assert "created_at" in columns
136
+ assert "updated_at" in columns
137
+
138
+ def test_returns_list_of_strings(self):
139
+ """Test that column names are strings."""
140
+ columns = get_model_columns(MockItem)
141
+
142
+ assert isinstance(columns, list)
143
+ for col in columns:
144
+ assert isinstance(col, str)
145
+
146
+
147
+ class TestGetSearchableColumns:
148
+ """Tests for get_searchable_columns function."""
149
+
150
+ def test_returns_string_columns(self):
151
+ """Test that string columns are included."""
152
+ columns = get_searchable_columns(MockUser)
153
+
154
+ assert "email" in columns
155
+ assert "full_name" in columns
156
+
157
+ def test_excludes_sensitive_columns(self):
158
+ """Test that sensitive columns are excluded."""
159
+ columns = get_searchable_columns(MockUser)
160
+
161
+ assert "hashed_password" not in columns
162
+
163
+ def test_excludes_non_string_columns(self):
164
+ """Test that non-string columns are excluded."""
165
+ columns = get_searchable_columns(MockUser)
166
+
167
+ assert "id" not in columns
168
+ assert "is_active" not in columns
169
+
170
+ def test_excludes_multiple_sensitive_patterns(self):
171
+ """Test that all sensitive patterns are excluded."""
172
+ columns = get_searchable_columns(MockSession)
173
+
174
+ assert "refresh_token_hash" not in columns
175
+ assert "api_key" not in columns
176
+ assert "secret" not in columns
177
+
178
+
179
+ class TestGetSortableColumns:
180
+ """Tests for get_sortable_columns function."""
181
+
182
+ def test_returns_all_columns(self):
183
+ """Test that all columns are returned as sortable."""
184
+ columns = get_sortable_columns(MockItem)
185
+
186
+ assert "id" in columns
187
+ assert "title" in columns
188
+ assert "description" in columns
189
+ assert "price" in columns
190
+
191
+ def test_returns_list(self):
192
+ """Test that a list is returned."""
193
+ columns = get_sortable_columns(MockUser)
194
+
195
+ assert isinstance(columns, list)
196
+
197
+
198
+ # =============================================================================
199
+ # Tests for get_form_excluded_columns
200
+ # =============================================================================
201
+
202
+
203
+ class TestGetFormExcludedColumns:
204
+ """Tests for get_form_excluded_columns function."""
205
+
206
+ def test_excludes_sensitive_columns(self):
207
+ """Test that sensitive columns are excluded from forms."""
208
+ columns = get_form_excluded_columns(MockUser)
209
+
210
+ assert "hashed_password" in columns
211
+
212
+ def test_excludes_auto_generated_columns(self):
213
+ """Test that auto-generated columns are excluded from forms."""
214
+ columns = get_form_excluded_columns(MockUser)
215
+
216
+ assert "created_at" in columns
217
+ assert "updated_at" in columns
218
+
219
+ def test_does_not_exclude_regular_columns(self):
220
+ """Test that regular columns are not excluded."""
221
+ columns = get_form_excluded_columns(MockUser)
222
+
223
+ assert "email" not in columns
224
+ assert "full_name" not in columns
225
+ assert "is_active" not in columns
226
+
227
+ def test_excludes_all_sensitive_patterns(self):
228
+ """Test that all sensitive patterns are matched."""
229
+ columns = get_form_excluded_columns(MockSession)
230
+
231
+ assert "refresh_token_hash" in columns
232
+ assert "api_key" in columns
233
+ assert "secret" in columns
234
+
235
+
236
+ # =============================================================================
237
+ # Tests for pluralize
238
+ # =============================================================================
239
+
240
+
241
+ class TestPluralize:
242
+ """Tests for pluralize function."""
243
+
244
+ def test_regular_pluralization(self):
245
+ """Test regular word pluralization (add 's')."""
246
+ assert pluralize("User") == "Users"
247
+ assert pluralize("Item") == "Items"
248
+ assert pluralize("Model") == "Models"
249
+
250
+ def test_words_ending_in_y(self):
251
+ """Test words ending in 'y' (change to 'ies')."""
252
+ assert pluralize("Category") == "Categories"
253
+ assert pluralize("Delivery") == "Deliveries"
254
+ assert pluralize("Entry") == "Entries"
255
+
256
+ def test_words_ending_in_s(self):
257
+ """Test words ending in 's' (add 'es')."""
258
+ assert pluralize("Address") == "Addresses"
259
+ assert pluralize("Class") == "Classes"
260
+
261
+ def test_words_ending_in_x(self):
262
+ """Test words ending in 'x' (add 'es')."""
263
+ assert pluralize("Box") == "Boxes"
264
+ assert pluralize("Tax") == "Taxes"
265
+
266
+ def test_words_ending_in_ch(self):
267
+ """Test words ending in 'ch' (add 'es')."""
268
+ assert pluralize("Match") == "Matches"
269
+ assert pluralize("Batch") == "Batches"
270
+
271
+ def test_words_ending_in_sh(self):
272
+ """Test words ending in 'sh' (add 'es')."""
273
+ assert pluralize("Dish") == "Dishes"
274
+ assert pluralize("Wish") == "Wishes"
275
+
276
+
277
+ # =============================================================================
278
+ # Tests for create_model_admin
279
+ # =============================================================================
280
+
281
+
282
+ class TestCreateModelAdmin:
283
+ """Tests for create_model_admin function."""
284
+
285
+ def test_creates_model_view_class(self):
286
+ """Test that a ModelView subclass is created."""
287
+ from sqladmin import ModelView
288
+
289
+ admin_class = create_model_admin(MockItem)
290
+
291
+ assert admin_class is not None
292
+ assert issubclass(admin_class, ModelView)
293
+
294
+ def test_binds_model_via_metaclass(self):
295
+ """Test that the model is properly bound via the metaclass."""
296
+ admin_class = create_model_admin(MockItem)
297
+
298
+ # The model should be accessible after metaclass processing
299
+ assert hasattr(admin_class, "model")
300
+ assert admin_class.model == MockItem
301
+
302
+ def test_generates_class_name(self):
303
+ """Test that the class name is generated correctly."""
304
+ admin_class = create_model_admin(MockItem)
305
+
306
+ assert admin_class.__name__ == "MockItemAdmin"
307
+
308
+ def test_sets_display_name(self):
309
+ """Test that the display name is set."""
310
+ admin_class = create_model_admin(MockItem)
311
+
312
+ assert admin_class.name == "MockItem"
313
+
314
+ def test_sets_plural_name(self):
315
+ """Test that the plural name is set."""
316
+ admin_class = create_model_admin(MockItem)
317
+
318
+ assert admin_class.name_plural == "MockItems"
319
+
320
+ def test_custom_name_override(self):
321
+ """Test that custom name can be provided."""
322
+ admin_class = create_model_admin(MockItem, name="Product")
323
+
324
+ assert admin_class.name == "Product"
325
+
326
+ def test_custom_name_plural_override(self):
327
+ """Test that custom plural name can be provided."""
328
+ admin_class = create_model_admin(MockItem, name_plural="Products")
329
+
330
+ assert admin_class.name_plural == "Products"
331
+
332
+ def test_sets_icon_from_mapping(self):
333
+ """Test that icon is set from MODEL_ICONS mapping."""
334
+ admin_class = create_model_admin(MockUser)
335
+
336
+ # MockUser won't be in MODEL_ICONS, so it should get default
337
+ assert admin_class.icon == "fa-solid fa-database"
338
+
339
+ def test_custom_icon_override(self):
340
+ """Test that custom icon can be provided."""
341
+ admin_class = create_model_admin(MockItem, icon="fa-solid fa-star")
342
+
343
+ assert admin_class.icon == "fa-solid fa-star"
344
+
345
+ def test_sets_column_list(self):
346
+ """Test that column_list is populated."""
347
+ admin_class = create_model_admin(MockItem)
348
+
349
+ assert admin_class.column_list is not None
350
+ assert len(admin_class.column_list) > 0
351
+
352
+ def test_custom_column_list(self):
353
+ """Test that custom column_list can be provided."""
354
+ admin_class = create_model_admin(
355
+ MockItem, column_list=[MockItem.id, MockItem.title]
356
+ )
357
+
358
+ assert len(admin_class.column_list) == 2
359
+
360
+ def test_sets_searchable_columns(self):
361
+ """Test that searchable columns are set."""
362
+ admin_class = create_model_admin(MockItem)
363
+
364
+ assert admin_class.column_searchable_list is not None
365
+
366
+ def test_sets_sortable_columns(self):
367
+ """Test that sortable columns are set."""
368
+ admin_class = create_model_admin(MockItem)
369
+
370
+ assert admin_class.column_sortable_list is not None
371
+
372
+ def test_sets_form_excluded_columns(self):
373
+ """Test that form excluded columns are set."""
374
+ admin_class = create_model_admin(MockUser)
375
+
376
+ assert admin_class.form_excluded_columns is not None
377
+
378
+ def test_crud_permissions_default_true(self):
379
+ """Test that CRUD permissions default to True."""
380
+ admin_class = create_model_admin(MockItem)
381
+
382
+ assert admin_class.can_create is True
383
+ assert admin_class.can_edit is True
384
+ assert admin_class.can_delete is True
385
+ assert admin_class.can_view_details is True
386
+
387
+ def test_crud_permissions_can_be_disabled(self):
388
+ """Test that CRUD permissions can be disabled."""
389
+ admin_class = create_model_admin(
390
+ MockItem,
391
+ can_create=False,
392
+ can_edit=False,
393
+ can_delete=False,
394
+ can_view_details=False,
395
+ )
396
+
397
+ assert admin_class.can_create is False
398
+ assert admin_class.can_edit is False
399
+ assert admin_class.can_delete is False
400
+ assert admin_class.can_view_details is False
401
+
402
+
403
+ # =============================================================================
404
+ # Tests for register_models_auto
405
+ # =============================================================================
406
+
407
+
408
+ class TestRegisterModelsAuto:
409
+ """Tests for register_models_auto function."""
410
+
411
+ def test_registers_all_models(self):
412
+ """Test that all models are registered."""
413
+ mock_admin = MagicMock()
414
+
415
+ registered = register_models_auto(mock_admin, MockBase)
416
+
417
+ assert len(registered) >= 3 # MockUser, MockItem, MockSession
418
+ assert mock_admin.add_view.call_count >= 3
419
+
420
+ def test_excludes_specified_models(self):
421
+ """Test that excluded models are not registered."""
422
+ mock_admin = MagicMock()
423
+
424
+ registered = register_models_auto(
425
+ mock_admin, MockBase, exclude_models=[MockSession]
426
+ )
427
+
428
+ registered_names = [r.__name__ for r in registered]
429
+ assert "MockSessionAdmin" not in registered_names
430
+
431
+ def test_applies_custom_configs(self):
432
+ """Test that custom configs are applied."""
433
+ mock_admin = MagicMock()
434
+ custom_configs = {
435
+ MockItem: {
436
+ "can_create": False,
437
+ "icon": "fa-solid fa-custom",
438
+ }
439
+ }
440
+
441
+ registered = register_models_auto(
442
+ mock_admin, MockBase, custom_configs=custom_configs
443
+ )
444
+
445
+ # Find the MockItem admin class
446
+ item_admin = next(r for r in registered if r.model == MockItem)
447
+ assert item_admin.can_create is False
448
+ assert item_admin.icon == "fa-solid fa-custom"
449
+
450
+ def test_returns_list_of_model_views(self):
451
+ """Test that a list of ModelView classes is returned."""
452
+ from sqladmin import ModelView
453
+
454
+ mock_admin = MagicMock()
455
+
456
+ registered = register_models_auto(mock_admin, MockBase)
457
+
458
+ assert isinstance(registered, list)
459
+ for admin_class in registered:
460
+ assert issubclass(admin_class, ModelView)
461
+
462
+
463
+ # =============================================================================
464
+ # Tests for get_sync_engine
465
+ # =============================================================================
466
+
467
+
468
+ class TestGetSyncEngine:
469
+ """Tests for get_sync_engine function."""
470
+
471
+ @patch("app.admin.create_engine")
472
+ @patch("app.admin.settings")
473
+ def test_creates_engine_with_settings(self, mock_settings, mock_create_engine):
474
+ """Test that engine is created with correct settings."""
475
+ import app.admin as admin_module
476
+
477
+ # Reset the cached engine
478
+ admin_module._sync_engine = None
479
+
480
+ mock_settings.DATABASE_URL_SYNC = "postgresql://test"
481
+ mock_settings.DEBUG = False
482
+ mock_engine = MagicMock()
483
+ mock_create_engine.return_value = mock_engine
484
+
485
+ engine = get_sync_engine()
486
+
487
+ mock_create_engine.assert_called_once_with(
488
+ "postgresql://test", echo=False
489
+ )
490
+ assert engine == mock_engine
491
+
492
+ # Reset for other tests
493
+ admin_module._sync_engine = None
494
+
495
+ @patch("app.admin.create_engine")
496
+ @patch("app.admin.settings")
497
+ def test_returns_cached_engine(self, mock_settings, mock_create_engine):
498
+ """Test that engine is cached and reused."""
499
+ import app.admin as admin_module
500
+
501
+ # Reset the cached engine
502
+ admin_module._sync_engine = None
503
+
504
+ mock_settings.DATABASE_URL_SYNC = "postgresql://test"
505
+ mock_settings.DEBUG = False
506
+ mock_engine = MagicMock()
507
+ mock_create_engine.return_value = mock_engine
508
+
509
+ engine1 = get_sync_engine()
510
+ engine2 = get_sync_engine()
511
+
512
+ # Should only create once
513
+ mock_create_engine.assert_called_once()
514
+ assert engine1 is engine2
515
+
516
+ # Reset for other tests
517
+ admin_module._sync_engine = None
518
+
519
+
520
+ # =============================================================================
521
+ # Tests for setup_admin
522
+ # =============================================================================
523
+
524
+
525
+ class TestSetupAdmin:
526
+ """Tests for setup_admin function."""
527
+
528
+ @patch("app.admin.register_models_auto")
529
+ @patch("app.admin.get_sync_engine")
530
+ @patch("app.admin.Admin")
531
+ def test_creates_admin_instance(
532
+ self, mock_admin_class, mock_get_engine, mock_register
533
+ ):
534
+ """Test that Admin instance is created."""
535
+ mock_engine = MagicMock()
536
+ mock_get_engine.return_value = mock_engine
537
+ mock_admin_instance = MagicMock()
538
+ mock_admin_class.return_value = mock_admin_instance
539
+ mock_app = MagicMock()
540
+
541
+ result = setup_admin(mock_app)
542
+
543
+ mock_admin_class.assert_called_once()
544
+ assert result == mock_admin_instance
545
+
546
+ @patch("app.admin.register_models_auto")
547
+ @patch("app.admin.get_sync_engine")
548
+ @patch("app.admin.Admin")
549
+ def test_calls_register_models_auto(
550
+ self, mock_admin_class, mock_get_engine, mock_register
551
+ ):
552
+ """Test that register_models_auto is called."""
553
+ mock_engine = MagicMock()
554
+ mock_get_engine.return_value = mock_engine
555
+ mock_admin_instance = MagicMock()
556
+ mock_admin_class.return_value = mock_admin_instance
557
+ mock_app = MagicMock()
558
+
559
+ setup_admin(mock_app)
560
+
561
+ mock_register.assert_called_once()
562
+
563
+ @patch("app.admin.register_models_auto")
564
+ @patch("app.admin.get_sync_engine")
565
+ @patch("app.admin.Admin")
566
+ def test_uses_correct_engine(
567
+ self, mock_admin_class, mock_get_engine, mock_register
568
+ ):
569
+ """Test that the sync engine is used."""
570
+ mock_engine = MagicMock()
571
+ mock_get_engine.return_value = mock_engine
572
+ mock_admin_instance = MagicMock()
573
+ mock_admin_class.return_value = mock_admin_instance
574
+ mock_app = MagicMock()
575
+
576
+ setup_admin(mock_app)
577
+
578
+ # Check that Admin was called with the engine
579
+ call_args = mock_admin_class.call_args
580
+ assert call_args[0][1] == mock_engine
581
+
582
+
583
+ {%- if cookiecutter.admin_require_auth %}
584
+
585
+
586
+ # =============================================================================
587
+ # Tests for AdminAuth
588
+ # =============================================================================
589
+
590
+
591
+ class TestAdminAuth:
592
+ """Tests for AdminAuth authentication backend."""
593
+
594
+ @pytest.fixture
595
+ def auth_backend(self):
596
+ """Create an AdminAuth instance for testing."""
597
+ return AdminAuth(secret_key="test-secret-key")
598
+
599
+ @pytest.fixture
600
+ def mock_request(self):
601
+ """Create a mock request object."""
602
+ request = MagicMock()
603
+ request.session = {}
604
+ return request
605
+
606
+ @pytest.mark.anyio
607
+ async def test_login_returns_false_for_empty_credentials(
608
+ self, auth_backend, mock_request
609
+ ):
610
+ """Test that login fails with empty credentials."""
611
+ mock_request.form = AsyncMock(return_value={"username": "", "password": ""})
612
+
613
+ result = await auth_backend.login(mock_request)
614
+
615
+ assert result is False
616
+
617
+ @pytest.mark.anyio
618
+ async def test_login_returns_false_for_missing_email(
619
+ self, auth_backend, mock_request
620
+ ):
621
+ """Test that login fails with missing email."""
622
+ mock_request.form = AsyncMock(return_value={"username": None, "password": "pass"})
623
+
624
+ result = await auth_backend.login(mock_request)
625
+
626
+ assert result is False
627
+
628
+ @pytest.mark.anyio
629
+ async def test_login_returns_false_for_missing_password(
630
+ self, auth_backend, mock_request
631
+ ):
632
+ """Test that login fails with missing password."""
633
+ mock_request.form = AsyncMock(return_value={"username": "test@test.com", "password": None})
634
+
635
+ result = await auth_backend.login(mock_request)
636
+
637
+ assert result is False
638
+
639
+ @pytest.mark.anyio
640
+ @patch("app.admin.get_sync_engine")
641
+ @patch("app.admin.verify_password")
642
+ async def test_login_returns_false_for_nonexistent_user(
643
+ self, mock_verify, mock_get_engine, auth_backend, mock_request
644
+ ):
645
+ """Test that login fails for non-existent user."""
646
+ mock_request.form = AsyncMock(
647
+ return_value={"username": "nonexistent@test.com", "password": "password"}
648
+ )
649
+
650
+ mock_session = MagicMock()
651
+ mock_session.query.return_value.filter.return_value.first.return_value = None
652
+ mock_engine = MagicMock()
653
+ mock_get_engine.return_value = mock_engine
654
+
655
+ with patch("sqlalchemy.orm.Session") as mock_session_class:
656
+ mock_session_class.return_value.__enter__ = MagicMock(
657
+ return_value=mock_session
658
+ )
659
+ mock_session_class.return_value.__exit__ = MagicMock(return_value=False)
660
+
661
+ result = await auth_backend.login(mock_request)
662
+
663
+ assert result is False
664
+
665
+ @pytest.mark.anyio
666
+ @patch("app.admin.get_sync_engine")
667
+ @patch("app.admin.verify_password")
668
+ async def test_login_returns_false_for_wrong_password(
669
+ self, mock_verify, mock_get_engine, auth_backend, mock_request
670
+ ):
671
+ """Test that login fails for wrong password."""
672
+ mock_request.form = AsyncMock(
673
+ return_value={"username": "test@test.com", "password": "wrongpassword"}
674
+ )
675
+ mock_verify.return_value = False
676
+
677
+ mock_user = MagicMock()
678
+ mock_user.is_superuser = True
679
+ mock_user.hashed_password = "hashed"
680
+
681
+ mock_session = MagicMock()
682
+ mock_session.query.return_value.filter.return_value.first.return_value = mock_user
683
+ mock_engine = MagicMock()
684
+ mock_get_engine.return_value = mock_engine
685
+
686
+ with patch("sqlalchemy.orm.Session") as mock_session_class:
687
+ mock_session_class.return_value.__enter__ = MagicMock(
688
+ return_value=mock_session
689
+ )
690
+ mock_session_class.return_value.__exit__ = MagicMock(return_value=False)
691
+
692
+ result = await auth_backend.login(mock_request)
693
+
694
+ assert result is False
695
+
696
+ @pytest.mark.anyio
697
+ @patch("app.admin.get_sync_engine")
698
+ @patch("app.admin.verify_password")
699
+ async def test_login_returns_false_for_non_superuser(
700
+ self, mock_verify, mock_get_engine, auth_backend, mock_request
701
+ ):
702
+ """Test that login fails for non-superuser."""
703
+ mock_request.form = AsyncMock(
704
+ return_value={"username": "test@test.com", "password": "password"}
705
+ )
706
+ mock_verify.return_value = True
707
+
708
+ mock_user = MagicMock()
709
+ mock_user.is_superuser = False
710
+ mock_user.hashed_password = "hashed"
711
+
712
+ mock_session = MagicMock()
713
+ mock_session.query.return_value.filter.return_value.first.return_value = mock_user
714
+ mock_engine = MagicMock()
715
+ mock_get_engine.return_value = mock_engine
716
+
717
+ with patch("sqlalchemy.orm.Session") as mock_session_class:
718
+ mock_session_class.return_value.__enter__ = MagicMock(
719
+ return_value=mock_session
720
+ )
721
+ mock_session_class.return_value.__exit__ = MagicMock(return_value=False)
722
+
723
+ result = await auth_backend.login(mock_request)
724
+
725
+ assert result is False
726
+
727
+ @pytest.mark.anyio
728
+ @patch("app.admin.get_sync_engine")
729
+ @patch("app.admin.verify_password")
730
+ async def test_login_success_for_valid_superuser(
731
+ self, mock_verify, mock_get_engine, auth_backend, mock_request
732
+ ):
733
+ """Test that login succeeds for valid superuser."""
734
+ mock_request.form = AsyncMock(
735
+ return_value={"username": "admin@test.com", "password": "password"}
736
+ )
737
+ mock_verify.return_value = True
738
+
739
+ mock_user = MagicMock()
740
+ mock_user.id = "user-123"
741
+ mock_user.email = "admin@test.com"
742
+ mock_user.is_superuser = True
743
+ mock_user.hashed_password = "hashed"
744
+
745
+ mock_session = MagicMock()
746
+ mock_session.query.return_value.filter.return_value.first.return_value = mock_user
747
+ mock_engine = MagicMock()
748
+ mock_get_engine.return_value = mock_engine
749
+
750
+ with patch("sqlalchemy.orm.Session") as mock_session_class:
751
+ mock_session_class.return_value.__enter__ = MagicMock(
752
+ return_value=mock_session
753
+ )
754
+ mock_session_class.return_value.__exit__ = MagicMock(return_value=False)
755
+
756
+ result = await auth_backend.login(mock_request)
757
+
758
+ assert result is True
759
+ assert mock_request.session["admin_user_id"] == "user-123"
760
+ assert mock_request.session["admin_email"] == "admin@test.com"
761
+
762
+ @pytest.mark.anyio
763
+ async def test_logout_clears_session(self, auth_backend, mock_request):
764
+ """Test that logout clears the session."""
765
+ mock_request.session["admin_user_id"] = "user-123"
766
+ mock_request.session["admin_email"] = "test@test.com"
767
+
768
+ result = await auth_backend.logout(mock_request)
769
+
770
+ assert result is True
771
+ mock_request.session.clear.assert_called_once()
772
+
773
+ @pytest.mark.anyio
774
+ async def test_authenticate_returns_false_without_session(
775
+ self, auth_backend, mock_request
776
+ ):
777
+ """Test that authenticate fails without session."""
778
+ mock_request.session = {}
779
+
780
+ result = await auth_backend.authenticate(mock_request)
781
+
782
+ assert result is False
783
+
784
+ @pytest.mark.anyio
785
+ @patch("app.admin.get_sync_engine")
786
+ async def test_authenticate_returns_false_for_invalid_user(
787
+ self, mock_get_engine, auth_backend, mock_request
788
+ ):
789
+ """Test that authenticate fails for invalid user."""
790
+ mock_request.session = {"admin_user_id": "user-123"}
791
+
792
+ mock_session = MagicMock()
793
+ mock_session.query.return_value.filter.return_value.first.return_value = None
794
+ mock_engine = MagicMock()
795
+ mock_get_engine.return_value = mock_engine
796
+
797
+ with patch("sqlalchemy.orm.Session") as mock_session_class:
798
+ mock_session_class.return_value.__enter__ = MagicMock(
799
+ return_value=mock_session
800
+ )
801
+ mock_session_class.return_value.__exit__ = MagicMock(return_value=False)
802
+
803
+ result = await auth_backend.authenticate(mock_request)
804
+
805
+ assert result is False
806
+
807
+ @pytest.mark.anyio
808
+ @patch("app.admin.get_sync_engine")
809
+ async def test_authenticate_returns_false_for_inactive_user(
810
+ self, mock_get_engine, auth_backend, mock_request
811
+ ):
812
+ """Test that authenticate fails for inactive user."""
813
+ mock_request.session = {"admin_user_id": "user-123"}
814
+
815
+ mock_user = MagicMock()
816
+ mock_user.is_superuser = True
817
+ mock_user.is_active = False
818
+
819
+ mock_session = MagicMock()
820
+ mock_session.query.return_value.filter.return_value.first.return_value = mock_user
821
+ mock_engine = MagicMock()
822
+ mock_get_engine.return_value = mock_engine
823
+
824
+ with patch("sqlalchemy.orm.Session") as mock_session_class:
825
+ mock_session_class.return_value.__enter__ = MagicMock(
826
+ return_value=mock_session
827
+ )
828
+ mock_session_class.return_value.__exit__ = MagicMock(return_value=False)
829
+
830
+ result = await auth_backend.authenticate(mock_request)
831
+
832
+ assert result is False
833
+
834
+ @pytest.mark.anyio
835
+ @patch("app.admin.get_sync_engine")
836
+ async def test_authenticate_returns_false_for_non_superuser(
837
+ self, mock_get_engine, auth_backend, mock_request
838
+ ):
839
+ """Test that authenticate fails for non-superuser."""
840
+ mock_request.session = {"admin_user_id": "user-123"}
841
+
842
+ mock_user = MagicMock()
843
+ mock_user.is_superuser = False
844
+ mock_user.is_active = True
845
+
846
+ mock_session = MagicMock()
847
+ mock_session.query.return_value.filter.return_value.first.return_value = mock_user
848
+ mock_engine = MagicMock()
849
+ mock_get_engine.return_value = mock_engine
850
+
851
+ with patch("sqlalchemy.orm.Session") as mock_session_class:
852
+ mock_session_class.return_value.__enter__ = MagicMock(
853
+ return_value=mock_session
854
+ )
855
+ mock_session_class.return_value.__exit__ = MagicMock(return_value=False)
856
+
857
+ result = await auth_backend.authenticate(mock_request)
858
+
859
+ assert result is False
860
+
861
+ @pytest.mark.anyio
862
+ @patch("app.admin.get_sync_engine")
863
+ async def test_authenticate_success_for_valid_superuser(
864
+ self, mock_get_engine, auth_backend, mock_request
865
+ ):
866
+ """Test that authenticate succeeds for valid superuser."""
867
+ mock_request.session = {"admin_user_id": "user-123"}
868
+
869
+ mock_user = MagicMock()
870
+ mock_user.is_superuser = True
871
+ mock_user.is_active = True
872
+
873
+ mock_session = MagicMock()
874
+ mock_session.query.return_value.filter.return_value.first.return_value = mock_user
875
+ mock_engine = MagicMock()
876
+ mock_get_engine.return_value = mock_engine
877
+
878
+ with patch("sqlalchemy.orm.Session") as mock_session_class:
879
+ mock_session_class.return_value.__enter__ = MagicMock(
880
+ return_value=mock_session
881
+ )
882
+ mock_session_class.return_value.__exit__ = MagicMock(return_value=False)
883
+
884
+ result = await auth_backend.authenticate(mock_request)
885
+
886
+ assert result is True
887
+ {%- endif %}
888
+ {%- else %}
889
+ """Admin panel tests - not configured."""
890
+ {%- endif %}