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.
- fastapi_fullstack-0.1.7.dist-info/METADATA +739 -0
- fastapi_fullstack-0.1.7.dist-info/RECORD +241 -0
- fastapi_fullstack-0.1.7.dist-info/WHEEL +4 -0
- fastapi_fullstack-0.1.7.dist-info/entry_points.txt +2 -0
- fastapi_fullstack-0.1.7.dist-info/licenses/LICENSE +21 -0
- fastapi_gen/__init__.py +3 -0
- fastapi_gen/cli.py +442 -0
- fastapi_gen/config.py +356 -0
- fastapi_gen/generator.py +207 -0
- fastapi_gen/prompts.py +874 -0
- fastapi_gen/template/VARIABLES.md +276 -0
- fastapi_gen/template/cookiecutter.json +93 -0
- fastapi_gen/template/hooks/post_gen_project.py +355 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/.env.prod.example +56 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/.github/workflows/ci.yml +150 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/.gitignore +109 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/AGENTS.md +55 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/CLAUDE.md +99 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/Makefile +315 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/README.md +768 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/.dockerignore +60 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/.env.example +155 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/.pre-commit-config.yaml +32 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/Dockerfile +56 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/alembic/env.py +76 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/alembic/script.py.mako +30 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/alembic/versions/.gitkeep +0 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/alembic.ini +48 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/__init__.py +3 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/admin.py +447 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/__init__.py +23 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/assistant.py +226 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/langchain_assistant.py +226 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/prompts.py +10 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/tools/__init__.py +13 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/tools/datetime_tool.py +17 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/__init__.py +1 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/deps.py +541 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/exception_handlers.py +98 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/router.py +10 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/__init__.py +9 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/__init__.py +87 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/agent.py +902 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/auth.py +395 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/conversations.py +498 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/health.py +227 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/items.py +275 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/oauth.py +205 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/sessions.py +168 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/users.py +333 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/webhooks.py +477 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/ws.py +46 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/versioning.py +221 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/clients/__init__.py +14 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/clients/redis.py +88 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/commands/__init__.py +117 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/commands/cleanup.py +75 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/commands/example.py +28 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/commands/seed.py +266 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/__init__.py +5 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/cache.py +23 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/config.py +267 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/csrf.py +153 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/exceptions.py +122 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/logfire_setup.py +101 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/middleware.py +99 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/oauth.py +23 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/rate_limit.py +58 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/sanitize.py +271 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/security.py +102 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/__init__.py +7 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/base.py +41 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/__init__.py +31 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/conversation.py +319 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/item.py +96 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/session.py +126 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/user.py +218 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/webhook.py +244 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/session.py +130 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/main.py +334 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/pipelines/__init__.py +9 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/pipelines/base.py +73 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/__init__.py +49 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/base.py +154 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/conversation.py +838 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/item.py +222 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/session.py +318 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/user.py +322 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/webhook.py +358 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/schemas/__init__.py +50 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/schemas/base.py +57 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/schemas/conversation.py +192 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/schemas/item.py +52 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/schemas/session.py +42 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/schemas/token.py +31 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/schemas/user.py +64 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/schemas/webhook.py +89 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/__init__.py +38 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/conversation.py +850 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/item.py +246 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/session.py +333 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/user.py +432 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/webhook.py +561 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/__init__.py +5 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/celery_app.py +64 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/taskiq_app.py +38 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/tasks/__init__.py +25 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/tasks/examples.py +106 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/tasks/schedules.py +29 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/tasks/taskiq_examples.py +92 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/cli/__init__.py +1 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/cli/commands.py +438 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/pyproject.toml +180 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/scripts/.gitkeep +0 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/__init__.py +1 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/api/__init__.py +1 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/api/test_auth.py +242 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/api/test_exceptions.py +151 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/api/test_health.py +113 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/api/test_items.py +310 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/api/test_users.py +253 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/conftest.py +151 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_admin.py +890 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_agents.py +261 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_clients.py +183 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_commands.py +173 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_core.py +143 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_pipelines.py +118 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_repositories.py +181 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_security.py +124 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_services.py +363 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_worker.py +85 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.dev.yml +242 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.frontend.yml +31 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.prod.yml +435 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.yml +241 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/docs/adding_features.md +132 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/docs/architecture.md +63 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/docs/patterns.md +161 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/docs/testing.md +120 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/.dockerignore +40 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/.env.example +12 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/.gitignore +45 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/.prettierignore +19 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/.prettierrc +11 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/Dockerfile +44 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/README.md +648 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/e2e/auth.setup.ts +49 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/e2e/auth.spec.ts +134 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/e2e/chat.spec.ts +207 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/e2e/home.spec.ts +73 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/instrumentation.ts +14 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/messages/en.json +84 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/messages/pl.json +84 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/next.config.ts +76 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/package.json +69 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/playwright.config.ts +101 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/postcss.config.mjs +7 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/(auth)/layout.tsx +11 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/(auth)/login/page.tsx +5 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/(auth)/register/page.tsx +5 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/(dashboard)/chat/page.tsx +48 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/(dashboard)/dashboard/page.tsx +99 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/(dashboard)/layout.tsx +17 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/(dashboard)/profile/page.tsx +152 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/auth/callback/page.tsx +113 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/layout.tsx +46 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/page.tsx +73 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/login/route.ts +58 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/logout/route.ts +24 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/me/route.ts +39 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/oauth-callback/route.ts +50 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/refresh/route.ts +54 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/register/route.ts +26 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/conversations/[id]/messages/route.ts +41 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/conversations/[id]/route.ts +108 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/conversations/route.ts +73 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/health/route.ts +21 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/globals.css +323 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/layout.tsx +22 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/providers.tsx +29 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/auth/index.ts +2 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/auth/login-form.tsx +120 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/auth/register-form.tsx +153 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/chat-container.tsx +234 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/chat-input.tsx +72 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/conversation-sidebar.tsx +328 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/copy-button.tsx +46 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/index.ts +11 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/local-conversation-sidebar.tsx +295 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/markdown-content.tsx +167 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/message-item.tsx +79 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/message-list.tsx +18 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/tool-call-card.tsx +79 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/icons/google-icon.tsx +32 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/icons/index.ts +3 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/language-switcher.tsx +97 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/layout/header.tsx +65 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/layout/index.ts +2 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/layout/sidebar.tsx +82 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/theme/index.ts +7 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/theme/theme-provider.tsx +53 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/theme/theme-toggle.tsx +105 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/badge.tsx +35 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/button.test.tsx +75 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/button.tsx +56 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/card.tsx +82 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/index.ts +13 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/input.tsx +21 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/label.tsx +21 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/sheet.tsx +109 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/hooks/index.ts +7 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/hooks/use-auth.ts +97 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/hooks/use-chat.ts +203 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/hooks/use-conversations.ts +181 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/hooks/use-local-chat.ts +165 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/hooks/use-websocket.ts +105 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/i18n.ts +37 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/lib/api-client.ts +90 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/lib/constants.ts +39 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/lib/server-api.ts +78 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/lib/utils.test.ts +44 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/lib/utils.ts +44 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/middleware.ts +31 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/stores/auth-store.test.ts +72 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/stores/auth-store.ts +64 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/stores/chat-sidebar-store.ts +17 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/stores/chat-store.ts +65 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/stores/conversation-store.ts +76 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/stores/index.ts +9 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/stores/local-chat-store.ts +255 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/stores/sidebar-store.ts +17 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/stores/theme-store.ts +44 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/types/api.ts +27 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/types/auth.ts +52 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/types/chat.ts +83 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/types/conversation.ts +49 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/types/index.ts +10 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/tsconfig.json +28 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/vitest.config.ts +36 -0
- 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 %}
|