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,180 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "{{ cookiecutter.project_slug }}"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "{{ cookiecutter.project_description }}"
|
|
5
|
+
requires-python = ">={{ cookiecutter.python_version }}"
|
|
6
|
+
license = { text = "MIT" }
|
|
7
|
+
authors = [{ name = "{{ cookiecutter.author_name }}", email = "{{ cookiecutter.author_email }}" }]
|
|
8
|
+
|
|
9
|
+
dependencies = [
|
|
10
|
+
"fastapi>=0.115.0",
|
|
11
|
+
"uvicorn[standard]>=0.32.0",
|
|
12
|
+
"pydantic[email]>=2.10.0",
|
|
13
|
+
"pydantic-settings>=2.6.0",
|
|
14
|
+
{%- if cookiecutter.enable_orjson %}
|
|
15
|
+
"orjson>=3.10.0",
|
|
16
|
+
{%- endif %}
|
|
17
|
+
{%- if cookiecutter.enable_logfire %}
|
|
18
|
+
{%- set logfire_extras = ['fastapi'] %}
|
|
19
|
+
{%- if cookiecutter.use_postgresql and cookiecutter.logfire_database %}
|
|
20
|
+
{%- set _ = logfire_extras.append('asyncpg') %}
|
|
21
|
+
{%- endif %}
|
|
22
|
+
{%- if cookiecutter.enable_redis and cookiecutter.logfire_redis %}
|
|
23
|
+
{%- set _ = logfire_extras.append('redis') %}
|
|
24
|
+
{%- endif %}
|
|
25
|
+
{%- if cookiecutter.use_celery and cookiecutter.logfire_celery %}
|
|
26
|
+
{%- set _ = logfire_extras.append('celery') %}
|
|
27
|
+
{%- endif %}
|
|
28
|
+
{%- if cookiecutter.logfire_httpx %}
|
|
29
|
+
{%- set _ = logfire_extras.append('httpx') %}
|
|
30
|
+
{%- endif %}
|
|
31
|
+
"logfire[{{ logfire_extras | join(',') }}]>=2.0.0",
|
|
32
|
+
{%- endif %}
|
|
33
|
+
{%- if cookiecutter.use_postgresql %}
|
|
34
|
+
"sqlalchemy[asyncio]>=2.0.0",
|
|
35
|
+
"asyncpg>=0.30.0",
|
|
36
|
+
"psycopg2-binary>=2.9.0",
|
|
37
|
+
"alembic>=1.14.0",
|
|
38
|
+
"greenlet>=3.0.0",
|
|
39
|
+
{%- endif %}
|
|
40
|
+
{%- if cookiecutter.use_mongodb %}
|
|
41
|
+
"motor>=3.6.0",
|
|
42
|
+
"beanie>=1.27.0",
|
|
43
|
+
{%- endif %}
|
|
44
|
+
{%- if cookiecutter.use_sqlite %}
|
|
45
|
+
"sqlalchemy>=2.0.0",
|
|
46
|
+
"alembic>=1.14.0",
|
|
47
|
+
{%- endif %}
|
|
48
|
+
{%- if cookiecutter.use_jwt %}
|
|
49
|
+
"pyjwt>=2.9.0",
|
|
50
|
+
"bcrypt>=4.0.0",
|
|
51
|
+
"python-multipart>=0.0.12",
|
|
52
|
+
{%- endif %}
|
|
53
|
+
{%- if cookiecutter.enable_oauth %}
|
|
54
|
+
"authlib>=1.3.0",
|
|
55
|
+
"httpx>=0.27.0",
|
|
56
|
+
{%- endif %}
|
|
57
|
+
{%- if cookiecutter.enable_redis %}
|
|
58
|
+
"redis>=5.2.0",
|
|
59
|
+
{%- endif %}
|
|
60
|
+
{%- if cookiecutter.enable_caching %}
|
|
61
|
+
"fastapi-cache2>=0.2.2",
|
|
62
|
+
{%- endif %}
|
|
63
|
+
{%- if cookiecutter.enable_rate_limiting %}
|
|
64
|
+
"slowapi>=0.1.9",
|
|
65
|
+
{%- endif %}
|
|
66
|
+
{%- if cookiecutter.enable_pagination %}
|
|
67
|
+
"fastapi-pagination>=0.12.31",
|
|
68
|
+
{%- endif %}
|
|
69
|
+
{%- if cookiecutter.enable_sentry %}
|
|
70
|
+
"sentry-sdk[fastapi]>=2.18.0",
|
|
71
|
+
{%- endif %}
|
|
72
|
+
{%- if cookiecutter.enable_admin_panel %}
|
|
73
|
+
"sqladmin>=0.19.0",
|
|
74
|
+
{%- if cookiecutter.admin_require_auth %}
|
|
75
|
+
"itsdangerous>=2.2.0",
|
|
76
|
+
{%- endif %}
|
|
77
|
+
{%- endif %}
|
|
78
|
+
{%- if cookiecutter.use_celery %}
|
|
79
|
+
"celery[redis]>=5.4.0",
|
|
80
|
+
"flower>=2.0.0",
|
|
81
|
+
{%- endif %}
|
|
82
|
+
{%- if cookiecutter.use_taskiq %}
|
|
83
|
+
"taskiq>=0.11.0",
|
|
84
|
+
"taskiq-redis>=1.0.0",
|
|
85
|
+
"taskiq-dependencies>=0.1.0",
|
|
86
|
+
{%- endif %}
|
|
87
|
+
{%- if cookiecutter.use_arq %}
|
|
88
|
+
"arq>=0.26.0",
|
|
89
|
+
{%- endif %}
|
|
90
|
+
{%- if cookiecutter.enable_file_storage %}
|
|
91
|
+
"boto3>=1.35.0",
|
|
92
|
+
{%- endif %}
|
|
93
|
+
{%- if cookiecutter.enable_ai_agent and cookiecutter.use_pydantic_ai %}
|
|
94
|
+
{%- if cookiecutter.use_openai %}
|
|
95
|
+
"pydantic-ai>=0.0.39",
|
|
96
|
+
{%- endif %}
|
|
97
|
+
{%- if cookiecutter.use_anthropic %}
|
|
98
|
+
"pydantic-ai-slim[anthropic]>=0.0.39",
|
|
99
|
+
{%- endif %}
|
|
100
|
+
{%- if cookiecutter.use_openrouter %}
|
|
101
|
+
"pydantic-ai-slim[openrouter]>=0.0.39",
|
|
102
|
+
{%- endif %}
|
|
103
|
+
{%- endif %}
|
|
104
|
+
{%- if cookiecutter.enable_ai_agent and cookiecutter.use_langchain %}
|
|
105
|
+
"langchain>=0.3.0",
|
|
106
|
+
{%- if cookiecutter.use_openai %}
|
|
107
|
+
"langchain-openai>=0.3.0",
|
|
108
|
+
{%- endif %}
|
|
109
|
+
{%- if cookiecutter.use_anthropic %}
|
|
110
|
+
"langchain-anthropic>=0.3.0",
|
|
111
|
+
{%- endif %}
|
|
112
|
+
"langgraph>=0.2.0",
|
|
113
|
+
{%- endif %}
|
|
114
|
+
{%- if cookiecutter.logfire_httpx or cookiecutter.enable_file_storage %}
|
|
115
|
+
"httpx>=0.27.0",
|
|
116
|
+
{%- endif %}
|
|
117
|
+
"click>=8.1.0",
|
|
118
|
+
"tabulate>=0.9.0",
|
|
119
|
+
]
|
|
120
|
+
|
|
121
|
+
[project.optional-dependencies]
|
|
122
|
+
dev = [
|
|
123
|
+
"pytest>=8.3.0",
|
|
124
|
+
"anyio[trio]>=4.0.0",
|
|
125
|
+
"pytest-cov>=6.0.0",
|
|
126
|
+
"httpx>=0.27.0",
|
|
127
|
+
"ruff>=0.8.0",
|
|
128
|
+
"mypy>=1.13.0",
|
|
129
|
+
{%- if cookiecutter.enable_precommit %}
|
|
130
|
+
"pre-commit>=4.0.0",
|
|
131
|
+
{%- endif %}
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
[project.scripts]
|
|
135
|
+
{{ cookiecutter.project_slug }} = "cli.commands:main"
|
|
136
|
+
|
|
137
|
+
[build-system]
|
|
138
|
+
requires = ["hatchling"]
|
|
139
|
+
build-backend = "hatchling.build"
|
|
140
|
+
|
|
141
|
+
[tool.hatch.build.targets.wheel]
|
|
142
|
+
packages = ["app", "cli"]
|
|
143
|
+
|
|
144
|
+
[tool.ruff]
|
|
145
|
+
target-version = "py311"
|
|
146
|
+
line-length = 100
|
|
147
|
+
|
|
148
|
+
[tool.ruff.lint]
|
|
149
|
+
select = [
|
|
150
|
+
"E", # pycodestyle errors
|
|
151
|
+
"W", # pycodestyle warnings
|
|
152
|
+
"F", # pyflakes
|
|
153
|
+
"I", # isort
|
|
154
|
+
"B", # flake8-bugbear
|
|
155
|
+
"C4", # flake8-comprehensions
|
|
156
|
+
"UP", # pyupgrade
|
|
157
|
+
"SIM", # flake8-simplify
|
|
158
|
+
"RUF", # ruff-specific rules
|
|
159
|
+
]
|
|
160
|
+
ignore = [
|
|
161
|
+
"E501", # line too long (handled by formatter)
|
|
162
|
+
"B008", # function call in default argument (needed for FastAPI Depends)
|
|
163
|
+
]
|
|
164
|
+
|
|
165
|
+
[tool.ruff.lint.isort]
|
|
166
|
+
known-first-party = ["app", "cli"]
|
|
167
|
+
|
|
168
|
+
[tool.mypy]
|
|
169
|
+
python_version = "3.11"
|
|
170
|
+
strict = true
|
|
171
|
+
warn_return_any = true
|
|
172
|
+
warn_unused_configs = true
|
|
173
|
+
ignore_missing_imports = true
|
|
174
|
+
|
|
175
|
+
[tool.pytest.ini_options]
|
|
176
|
+
testpaths = ["tests"]
|
|
177
|
+
|
|
178
|
+
[tool.{{ cookiecutter.generator_name }}]
|
|
179
|
+
generator_version = "{{ cookiecutter.generator_version }}"
|
|
180
|
+
generated_at = "{{ cookiecutter.generated_at }}"
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Tests package."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""API tests package."""
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
{%- if cookiecutter.use_jwt %}
|
|
2
|
+
# ruff: noqa: I001 - Imports structured for Jinja2 template conditionals
|
|
3
|
+
"""Tests for authentication routes."""
|
|
4
|
+
|
|
5
|
+
from datetime import UTC, datetime
|
|
6
|
+
from unittest.mock import AsyncMock, MagicMock
|
|
7
|
+
from uuid import uuid4
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
from httpx import AsyncClient
|
|
11
|
+
|
|
12
|
+
from app.api.deps import get_user_service
|
|
13
|
+
from app.core.config import settings
|
|
14
|
+
from app.core.security import create_access_token, create_refresh_token
|
|
15
|
+
from app.main import app
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class MockUser:
|
|
19
|
+
"""Mock user for testing."""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
id=None,
|
|
24
|
+
email="test@example.com",
|
|
25
|
+
full_name="Test User",
|
|
26
|
+
is_active=True,
|
|
27
|
+
is_superuser=False,
|
|
28
|
+
):
|
|
29
|
+
self.id = id or uuid4()
|
|
30
|
+
self.email = email
|
|
31
|
+
self.full_name = full_name
|
|
32
|
+
self.is_active = is_active
|
|
33
|
+
self.is_superuser = is_superuser
|
|
34
|
+
self.hashed_password = "hashed"
|
|
35
|
+
self.created_at = datetime.now(UTC)
|
|
36
|
+
self.updated_at = datetime.now(UTC)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@pytest.fixture
|
|
40
|
+
def mock_user() -> MockUser:
|
|
41
|
+
"""Create a mock user."""
|
|
42
|
+
return MockUser()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@pytest.fixture
|
|
46
|
+
def mock_user_service(mock_user: MockUser) -> MagicMock:
|
|
47
|
+
"""Create a mock user service."""
|
|
48
|
+
service = MagicMock()
|
|
49
|
+
service.authenticate = AsyncMock(return_value=mock_user)
|
|
50
|
+
service.register = AsyncMock(return_value=mock_user)
|
|
51
|
+
service.get_by_id = AsyncMock(return_value=mock_user)
|
|
52
|
+
service.get_by_email = AsyncMock(return_value=mock_user)
|
|
53
|
+
return service
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@pytest.fixture
|
|
57
|
+
async def client_with_mock_service(
|
|
58
|
+
mock_user_service: MagicMock,
|
|
59
|
+
{%- if cookiecutter.enable_redis %}
|
|
60
|
+
mock_redis: MagicMock,
|
|
61
|
+
{%- endif %}
|
|
62
|
+
{%- if cookiecutter.use_database %}
|
|
63
|
+
mock_db_session,
|
|
64
|
+
{%- endif %}
|
|
65
|
+
) -> AsyncClient:
|
|
66
|
+
"""Client with mocked user service."""
|
|
67
|
+
{%- if cookiecutter.enable_redis %}
|
|
68
|
+
from app.api.deps import get_redis
|
|
69
|
+
{%- endif %}
|
|
70
|
+
{%- if cookiecutter.use_database %}
|
|
71
|
+
from app.api.deps import get_db_session
|
|
72
|
+
{%- endif %}
|
|
73
|
+
from httpx import ASGITransport
|
|
74
|
+
|
|
75
|
+
app.dependency_overrides[get_user_service] = lambda: mock_user_service
|
|
76
|
+
{%- if cookiecutter.enable_redis %}
|
|
77
|
+
app.dependency_overrides[get_redis] = lambda: mock_redis
|
|
78
|
+
{%- endif %}
|
|
79
|
+
{%- if cookiecutter.use_database %}
|
|
80
|
+
app.dependency_overrides[get_db_session] = lambda: mock_db_session
|
|
81
|
+
{%- endif %}
|
|
82
|
+
|
|
83
|
+
async with AsyncClient(
|
|
84
|
+
transport=ASGITransport(app=app),
|
|
85
|
+
base_url="http://test",
|
|
86
|
+
) as ac:
|
|
87
|
+
yield ac
|
|
88
|
+
|
|
89
|
+
app.dependency_overrides.clear()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@pytest.mark.anyio
|
|
93
|
+
async def test_login_success(client_with_mock_service: AsyncClient):
|
|
94
|
+
"""Test successful login."""
|
|
95
|
+
response = await client_with_mock_service.post(
|
|
96
|
+
f"{settings.API_V1_STR}/auth/login",
|
|
97
|
+
data={"username": "test@example.com", "password": "password123"},
|
|
98
|
+
)
|
|
99
|
+
assert response.status_code == 200
|
|
100
|
+
data = response.json()
|
|
101
|
+
assert "access_token" in data
|
|
102
|
+
assert "refresh_token" in data
|
|
103
|
+
assert data["token_type"] == "bearer"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@pytest.mark.anyio
|
|
107
|
+
async def test_login_invalid_credentials(
|
|
108
|
+
client_with_mock_service: AsyncClient,
|
|
109
|
+
mock_user_service: MagicMock,
|
|
110
|
+
):
|
|
111
|
+
"""Test login with invalid credentials."""
|
|
112
|
+
from app.core.exceptions import AuthenticationError
|
|
113
|
+
|
|
114
|
+
mock_user_service.authenticate = AsyncMock(
|
|
115
|
+
side_effect=AuthenticationError(message="Invalid credentials")
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
response = await client_with_mock_service.post(
|
|
119
|
+
f"{settings.API_V1_STR}/auth/login",
|
|
120
|
+
data={"username": "test@example.com", "password": "wrongpassword"},
|
|
121
|
+
)
|
|
122
|
+
assert response.status_code == 401
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@pytest.mark.anyio
|
|
126
|
+
async def test_register_success(client_with_mock_service: AsyncClient):
|
|
127
|
+
"""Test successful registration."""
|
|
128
|
+
response = await client_with_mock_service.post(
|
|
129
|
+
f"{settings.API_V1_STR}/auth/register",
|
|
130
|
+
json={
|
|
131
|
+
"email": "new@example.com",
|
|
132
|
+
"password": "password123",
|
|
133
|
+
"full_name": "New User",
|
|
134
|
+
},
|
|
135
|
+
)
|
|
136
|
+
assert response.status_code == 201
|
|
137
|
+
data = response.json()
|
|
138
|
+
assert data["email"] == "test@example.com" # From mock
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@pytest.mark.anyio
|
|
142
|
+
async def test_register_duplicate_email(
|
|
143
|
+
client_with_mock_service: AsyncClient,
|
|
144
|
+
mock_user_service: MagicMock,
|
|
145
|
+
):
|
|
146
|
+
"""Test registration with duplicate email."""
|
|
147
|
+
from app.core.exceptions import AlreadyExistsError
|
|
148
|
+
|
|
149
|
+
mock_user_service.register = AsyncMock(
|
|
150
|
+
side_effect=AlreadyExistsError(message="Email already registered")
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
response = await client_with_mock_service.post(
|
|
154
|
+
f"{settings.API_V1_STR}/auth/register",
|
|
155
|
+
json={
|
|
156
|
+
"email": "existing@example.com",
|
|
157
|
+
"password": "password123",
|
|
158
|
+
"full_name": "Test User",
|
|
159
|
+
},
|
|
160
|
+
)
|
|
161
|
+
assert response.status_code == 409
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@pytest.mark.anyio
|
|
165
|
+
async def test_refresh_token_success(
|
|
166
|
+
client_with_mock_service: AsyncClient,
|
|
167
|
+
mock_user: MockUser,
|
|
168
|
+
):
|
|
169
|
+
"""Test successful token refresh."""
|
|
170
|
+
refresh_token = create_refresh_token(subject=str(mock_user.id))
|
|
171
|
+
|
|
172
|
+
response = await client_with_mock_service.post(
|
|
173
|
+
f"{settings.API_V1_STR}/auth/refresh",
|
|
174
|
+
json={"refresh_token": refresh_token},
|
|
175
|
+
)
|
|
176
|
+
assert response.status_code == 200
|
|
177
|
+
data = response.json()
|
|
178
|
+
assert "access_token" in data
|
|
179
|
+
assert "refresh_token" in data
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@pytest.mark.anyio
|
|
183
|
+
async def test_refresh_token_invalid(client_with_mock_service: AsyncClient):
|
|
184
|
+
"""Test refresh with invalid token."""
|
|
185
|
+
response = await client_with_mock_service.post(
|
|
186
|
+
f"{settings.API_V1_STR}/auth/refresh",
|
|
187
|
+
json={"refresh_token": "invalid.token.here"},
|
|
188
|
+
)
|
|
189
|
+
assert response.status_code == 401
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@pytest.mark.anyio
|
|
193
|
+
async def test_refresh_token_wrong_type(
|
|
194
|
+
client_with_mock_service: AsyncClient,
|
|
195
|
+
mock_user: MockUser,
|
|
196
|
+
):
|
|
197
|
+
"""Test refresh with access token instead of refresh token."""
|
|
198
|
+
access_token = create_access_token(subject=str(mock_user.id))
|
|
199
|
+
|
|
200
|
+
response = await client_with_mock_service.post(
|
|
201
|
+
f"{settings.API_V1_STR}/auth/refresh",
|
|
202
|
+
json={"refresh_token": access_token},
|
|
203
|
+
)
|
|
204
|
+
assert response.status_code == 401
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
@pytest.mark.anyio
|
|
208
|
+
async def test_refresh_token_inactive_user(
|
|
209
|
+
client_with_mock_service: AsyncClient,
|
|
210
|
+
mock_user_service: MagicMock,
|
|
211
|
+
):
|
|
212
|
+
"""Test refresh token for inactive user."""
|
|
213
|
+
inactive_user = MockUser(is_active=False)
|
|
214
|
+
mock_user_service.get_by_id = AsyncMock(return_value=inactive_user)
|
|
215
|
+
refresh_token = create_refresh_token(subject=str(inactive_user.id))
|
|
216
|
+
|
|
217
|
+
response = await client_with_mock_service.post(
|
|
218
|
+
f"{settings.API_V1_STR}/auth/refresh",
|
|
219
|
+
json={"refresh_token": refresh_token},
|
|
220
|
+
)
|
|
221
|
+
assert response.status_code == 401
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
@pytest.mark.anyio
|
|
225
|
+
async def test_get_current_user(
|
|
226
|
+
client_with_mock_service: AsyncClient,
|
|
227
|
+
mock_user: MockUser,
|
|
228
|
+
mock_user_service: MagicMock,
|
|
229
|
+
):
|
|
230
|
+
"""Test getting current user info."""
|
|
231
|
+
from app.api.deps import get_current_user
|
|
232
|
+
|
|
233
|
+
# Override get_current_user to return mock user
|
|
234
|
+
app.dependency_overrides[get_current_user] = lambda: mock_user
|
|
235
|
+
|
|
236
|
+
response = await client_with_mock_service.get(
|
|
237
|
+
f"{settings.API_V1_STR}/auth/me",
|
|
238
|
+
)
|
|
239
|
+
assert response.status_code == 200
|
|
240
|
+
data = response.json()
|
|
241
|
+
assert data["email"] == mock_user.email
|
|
242
|
+
{%- endif %}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""Exception handler tests."""
|
|
2
|
+
{%- if cookiecutter.use_jwt or cookiecutter.use_api_key %}
|
|
3
|
+
# ruff: noqa: I001, E402 - Imports structured for Jinja2 template conditionals
|
|
4
|
+
{%- endif %}
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
from httpx import AsyncClient
|
|
8
|
+
|
|
9
|
+
from app.core.config import settings
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.mark.anyio
|
|
13
|
+
async def test_not_found_error_format(client: AsyncClient):
|
|
14
|
+
"""Test that 404 errors return proper JSON format."""
|
|
15
|
+
response = await client.get(f"{settings.API_V1_STR}/nonexistent-endpoint")
|
|
16
|
+
assert response.status_code == 404
|
|
17
|
+
# FastAPI returns 404 for unknown routes
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
{%- if cookiecutter.use_jwt %}
|
|
21
|
+
|
|
22
|
+
from unittest.mock import AsyncMock, MagicMock
|
|
23
|
+
|
|
24
|
+
from httpx import ASGITransport
|
|
25
|
+
|
|
26
|
+
{%- if cookiecutter.enable_redis %}
|
|
27
|
+
from app.api.deps import get_redis
|
|
28
|
+
{%- endif %}
|
|
29
|
+
{%- if cookiecutter.use_database %}
|
|
30
|
+
from app.api.deps import get_db_session
|
|
31
|
+
{%- endif %}
|
|
32
|
+
from app.main import app
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@pytest.mark.anyio
|
|
36
|
+
async def test_authentication_error_returns_401(client: AsyncClient):
|
|
37
|
+
"""Test that authentication errors return 401 with proper headers."""
|
|
38
|
+
response = await client.get(
|
|
39
|
+
f"{settings.API_V1_STR}/users/me",
|
|
40
|
+
headers={"Authorization": "Bearer invalid-token"},
|
|
41
|
+
)
|
|
42
|
+
assert response.status_code == 401
|
|
43
|
+
assert "WWW-Authenticate" in response.headers
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@pytest.mark.anyio
|
|
47
|
+
async def test_missing_auth_returns_401(client: AsyncClient):
|
|
48
|
+
"""Test that missing authentication returns 401."""
|
|
49
|
+
response = await client.get(f"{settings.API_V1_STR}/users/me")
|
|
50
|
+
assert response.status_code == 401
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@pytest.fixture
|
|
54
|
+
def mock_user_service_with_errors() -> MagicMock:
|
|
55
|
+
"""Create mock user service that raises errors."""
|
|
56
|
+
from app.core.exceptions import AlreadyExistsError, AuthenticationError
|
|
57
|
+
|
|
58
|
+
service = MagicMock()
|
|
59
|
+
service.register = AsyncMock(
|
|
60
|
+
side_effect=AlreadyExistsError(message="Email already registered")
|
|
61
|
+
)
|
|
62
|
+
service.authenticate = AsyncMock(
|
|
63
|
+
side_effect=AuthenticationError(message="Invalid credentials")
|
|
64
|
+
)
|
|
65
|
+
return service
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@pytest.fixture
|
|
69
|
+
async def error_client(
|
|
70
|
+
mock_user_service_with_errors: MagicMock,
|
|
71
|
+
{%- if cookiecutter.enable_redis %}
|
|
72
|
+
mock_redis: MagicMock,
|
|
73
|
+
{%- endif %}
|
|
74
|
+
{%- if cookiecutter.use_database %}
|
|
75
|
+
mock_db_session,
|
|
76
|
+
{%- endif %}
|
|
77
|
+
) -> AsyncClient:
|
|
78
|
+
"""Client with mocked services that raise errors."""
|
|
79
|
+
from app.api.deps import get_user_service
|
|
80
|
+
|
|
81
|
+
app.dependency_overrides[get_user_service] = lambda: mock_user_service_with_errors
|
|
82
|
+
{%- if cookiecutter.enable_redis %}
|
|
83
|
+
app.dependency_overrides[get_redis] = lambda: mock_redis
|
|
84
|
+
{%- endif %}
|
|
85
|
+
{%- if cookiecutter.use_database %}
|
|
86
|
+
app.dependency_overrides[get_db_session] = lambda: mock_db_session
|
|
87
|
+
{%- endif %}
|
|
88
|
+
|
|
89
|
+
async with AsyncClient(
|
|
90
|
+
transport=ASGITransport(app=app),
|
|
91
|
+
base_url="http://test",
|
|
92
|
+
) as ac:
|
|
93
|
+
yield ac
|
|
94
|
+
|
|
95
|
+
app.dependency_overrides.clear()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@pytest.mark.anyio
|
|
99
|
+
async def test_register_duplicate_email_returns_409(error_client: AsyncClient):
|
|
100
|
+
"""Test that registering with existing email returns 409."""
|
|
101
|
+
response = await error_client.post(
|
|
102
|
+
f"{settings.API_V1_STR}/auth/register",
|
|
103
|
+
json={
|
|
104
|
+
"email": "existing@example.com",
|
|
105
|
+
"password": "password123",
|
|
106
|
+
"full_name": "Test User",
|
|
107
|
+
},
|
|
108
|
+
)
|
|
109
|
+
assert response.status_code == 409
|
|
110
|
+
data = response.json()
|
|
111
|
+
assert "error" in data
|
|
112
|
+
assert data["error"]["code"] == "ALREADY_EXISTS"
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@pytest.mark.anyio
|
|
116
|
+
async def test_invalid_login_returns_401(error_client: AsyncClient):
|
|
117
|
+
"""Test that invalid login credentials return 401."""
|
|
118
|
+
response = await error_client.post(
|
|
119
|
+
f"{settings.API_V1_STR}/auth/login",
|
|
120
|
+
data={
|
|
121
|
+
"username": "nonexistent@example.com",
|
|
122
|
+
"password": "wrongpassword",
|
|
123
|
+
},
|
|
124
|
+
)
|
|
125
|
+
assert response.status_code == 401
|
|
126
|
+
data = response.json()
|
|
127
|
+
assert "error" in data
|
|
128
|
+
assert data["error"]["code"] == "AUTHENTICATION_ERROR"
|
|
129
|
+
{%- endif %}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
{%- if cookiecutter.use_api_key %}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@pytest.mark.anyio
|
|
136
|
+
async def test_missing_api_key_returns_401(client: AsyncClient):
|
|
137
|
+
"""Test that missing API key returns 401."""
|
|
138
|
+
# Health endpoint might not require auth, but we test the middleware
|
|
139
|
+
# For endpoints that require API key, they should return 401
|
|
140
|
+
await client.get(f"{settings.API_V1_STR}/health")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@pytest.mark.anyio
|
|
144
|
+
async def test_invalid_api_key_returns_403(client: AsyncClient):
|
|
145
|
+
"""Test that invalid API key returns 403."""
|
|
146
|
+
# Health endpoint might not require auth
|
|
147
|
+
await client.get(
|
|
148
|
+
f"{settings.API_V1_STR}/health",
|
|
149
|
+
headers={settings.API_KEY_HEADER: "invalid-key"},
|
|
150
|
+
)
|
|
151
|
+
{%- endif %}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Health endpoint tests."""
|
|
2
|
+
{%- if cookiecutter.enable_redis or cookiecutter.use_postgresql or cookiecutter.use_mongodb %}
|
|
3
|
+
|
|
4
|
+
from unittest.mock import AsyncMock
|
|
5
|
+
{%- endif %}
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
from httpx import AsyncClient
|
|
9
|
+
|
|
10
|
+
from app.core.config import settings
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@pytest.mark.anyio
|
|
14
|
+
async def test_health_check(client: AsyncClient):
|
|
15
|
+
"""Test liveness probe."""
|
|
16
|
+
response = await client.get(f"{settings.API_V1_STR}/health")
|
|
17
|
+
assert response.status_code == 200
|
|
18
|
+
data = response.json()
|
|
19
|
+
assert data["status"] == "healthy"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@pytest.mark.anyio
|
|
23
|
+
async def test_readiness_check(client: AsyncClient):
|
|
24
|
+
"""Test readiness probe with mocked dependencies."""
|
|
25
|
+
response = await client.get(f"{settings.API_V1_STR}/ready")
|
|
26
|
+
assert response.status_code == 200
|
|
27
|
+
data = response.json()
|
|
28
|
+
assert data["status"] in ["ready", "degraded"]
|
|
29
|
+
assert "checks" in data
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
{%- if cookiecutter.enable_redis %}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@pytest.mark.anyio
|
|
36
|
+
async def test_readiness_check_redis_healthy(client: AsyncClient, mock_redis):
|
|
37
|
+
"""Test readiness when Redis is healthy."""
|
|
38
|
+
mock_redis.ping = AsyncMock(return_value=True)
|
|
39
|
+
|
|
40
|
+
response = await client.get(f"{settings.API_V1_STR}/ready")
|
|
41
|
+
assert response.status_code == 200
|
|
42
|
+
data = response.json()
|
|
43
|
+
assert data["checks"]["redis"] is True
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@pytest.mark.anyio
|
|
47
|
+
async def test_readiness_check_redis_unhealthy(client: AsyncClient, mock_redis):
|
|
48
|
+
"""Test readiness when Redis is unhealthy."""
|
|
49
|
+
mock_redis.ping = AsyncMock(side_effect=Exception("Connection failed"))
|
|
50
|
+
|
|
51
|
+
response = await client.get(f"{settings.API_V1_STR}/ready")
|
|
52
|
+
# Should return 503 when Redis is down
|
|
53
|
+
assert response.status_code == 503
|
|
54
|
+
data = response.json()
|
|
55
|
+
assert data["status"] == "degraded"
|
|
56
|
+
assert data["checks"]["redis"] is False
|
|
57
|
+
{%- endif %}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
{%- if cookiecutter.use_postgresql %}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@pytest.mark.anyio
|
|
64
|
+
async def test_readiness_check_db_healthy(client: AsyncClient, mock_db_session):
|
|
65
|
+
"""Test readiness when database is healthy."""
|
|
66
|
+
# Mock successful DB query
|
|
67
|
+
mock_db_session.execute = AsyncMock()
|
|
68
|
+
|
|
69
|
+
response = await client.get(f"{settings.API_V1_STR}/ready")
|
|
70
|
+
assert response.status_code == 200
|
|
71
|
+
data = response.json()
|
|
72
|
+
assert data["checks"]["database"]["status"] == "healthy"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@pytest.mark.anyio
|
|
76
|
+
async def test_readiness_check_db_unhealthy(client: AsyncClient, mock_db_session):
|
|
77
|
+
"""Test readiness when database is unhealthy."""
|
|
78
|
+
mock_db_session.execute = AsyncMock(side_effect=Exception("DB connection failed"))
|
|
79
|
+
|
|
80
|
+
response = await client.get(f"{settings.API_V1_STR}/ready")
|
|
81
|
+
# Should return 503 when DB is down
|
|
82
|
+
assert response.status_code == 503
|
|
83
|
+
data = response.json()
|
|
84
|
+
assert data["status"] == "not_ready"
|
|
85
|
+
assert data["checks"]["database"]["status"] == "unhealthy"
|
|
86
|
+
{%- endif %}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
{%- if cookiecutter.use_mongodb %}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@pytest.mark.anyio
|
|
93
|
+
async def test_readiness_check_db_healthy(client: AsyncClient, mock_db_session):
|
|
94
|
+
"""Test readiness when MongoDB is healthy."""
|
|
95
|
+
mock_db_session.command = AsyncMock(return_value={"ok": 1})
|
|
96
|
+
|
|
97
|
+
response = await client.get(f"{settings.API_V1_STR}/ready")
|
|
98
|
+
assert response.status_code == 200
|
|
99
|
+
data = response.json()
|
|
100
|
+
assert data["checks"]["database"] is True
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@pytest.mark.anyio
|
|
104
|
+
async def test_readiness_check_db_unhealthy(client: AsyncClient, mock_db_session):
|
|
105
|
+
"""Test readiness when MongoDB is unhealthy."""
|
|
106
|
+
mock_db_session.command = AsyncMock(side_effect=Exception("MongoDB connection failed"))
|
|
107
|
+
|
|
108
|
+
response = await client.get(f"{settings.API_V1_STR}/ready")
|
|
109
|
+
assert response.status_code == 503
|
|
110
|
+
data = response.json()
|
|
111
|
+
assert data["status"] == "degraded"
|
|
112
|
+
assert data["checks"]["database"] is False
|
|
113
|
+
{%- endif %}
|