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,143 @@
|
|
|
1
|
+
"""Tests for core modules."""
|
|
2
|
+
|
|
3
|
+
from app.core.config import settings
|
|
4
|
+
from app.core.exceptions import (
|
|
5
|
+
AlreadyExistsError,
|
|
6
|
+
AppException,
|
|
7
|
+
AuthenticationError,
|
|
8
|
+
AuthorizationError,
|
|
9
|
+
NotFoundError,
|
|
10
|
+
ValidationError,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TestSettings:
|
|
15
|
+
"""Tests for settings configuration."""
|
|
16
|
+
|
|
17
|
+
def test_project_name_is_set(self):
|
|
18
|
+
"""Test project name is configured."""
|
|
19
|
+
assert settings.PROJECT_NAME == "{{ cookiecutter.project_name }}"
|
|
20
|
+
|
|
21
|
+
def test_api_v1_str_is_set(self):
|
|
22
|
+
"""Test API version string is set."""
|
|
23
|
+
assert settings.API_V1_STR == "/api/v1"
|
|
24
|
+
|
|
25
|
+
def test_debug_mode_default(self):
|
|
26
|
+
"""Test debug mode has default value."""
|
|
27
|
+
assert isinstance(settings.DEBUG, bool)
|
|
28
|
+
|
|
29
|
+
{%- if cookiecutter.enable_cors %}
|
|
30
|
+
def test_cors_origins_is_list(self):
|
|
31
|
+
"""Test CORS origins is a list."""
|
|
32
|
+
assert isinstance(settings.CORS_ORIGINS, list)
|
|
33
|
+
{%- endif %}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class TestExceptions:
|
|
37
|
+
"""Tests for custom exceptions."""
|
|
38
|
+
|
|
39
|
+
def test_app_exception(self):
|
|
40
|
+
"""Test AppException initialization."""
|
|
41
|
+
error = AppException(message="Test error", code="TEST_ERROR")
|
|
42
|
+
assert error.message == "Test error"
|
|
43
|
+
assert error.code == "TEST_ERROR"
|
|
44
|
+
assert str(error) == "Test error"
|
|
45
|
+
|
|
46
|
+
def test_not_found_error(self):
|
|
47
|
+
"""Test NotFoundError."""
|
|
48
|
+
error = NotFoundError(message="Item not found")
|
|
49
|
+
assert error.status_code == 404
|
|
50
|
+
assert error.code == "NOT_FOUND"
|
|
51
|
+
|
|
52
|
+
def test_already_exists_error(self):
|
|
53
|
+
"""Test AlreadyExistsError."""
|
|
54
|
+
error = AlreadyExistsError(message="Item already exists")
|
|
55
|
+
assert error.status_code == 409
|
|
56
|
+
assert error.code == "ALREADY_EXISTS"
|
|
57
|
+
|
|
58
|
+
def test_authentication_error(self):
|
|
59
|
+
"""Test AuthenticationError."""
|
|
60
|
+
error = AuthenticationError(message="Invalid credentials")
|
|
61
|
+
assert error.status_code == 401
|
|
62
|
+
assert error.code == "AUTHENTICATION_ERROR"
|
|
63
|
+
|
|
64
|
+
def test_authorization_error(self):
|
|
65
|
+
"""Test AuthorizationError."""
|
|
66
|
+
error = AuthorizationError(message="Not authorized")
|
|
67
|
+
assert error.status_code == 403
|
|
68
|
+
assert error.code == "AUTHORIZATION_ERROR"
|
|
69
|
+
|
|
70
|
+
def test_validation_error(self):
|
|
71
|
+
"""Test ValidationError."""
|
|
72
|
+
error = ValidationError(message="Invalid input")
|
|
73
|
+
assert error.status_code == 422
|
|
74
|
+
assert error.code == "VALIDATION_ERROR"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
{%- if cookiecutter.enable_redis %}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class TestCacheSetup:
|
|
81
|
+
"""Tests for cache setup."""
|
|
82
|
+
|
|
83
|
+
def test_setup_cache_function_exists(self):
|
|
84
|
+
"""Test setup_cache function exists."""
|
|
85
|
+
from app.core.cache import setup_cache
|
|
86
|
+
|
|
87
|
+
assert setup_cache is not None
|
|
88
|
+
assert callable(setup_cache)
|
|
89
|
+
{%- endif %}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class TestMiddleware:
|
|
93
|
+
"""Tests for middleware."""
|
|
94
|
+
|
|
95
|
+
def test_request_id_middleware_exists(self):
|
|
96
|
+
"""Test request ID middleware is configured."""
|
|
97
|
+
from app.core.middleware import RequestIDMiddleware
|
|
98
|
+
|
|
99
|
+
assert RequestIDMiddleware is not None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
{%- if cookiecutter.enable_rate_limiting %}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class TestRateLimit:
|
|
106
|
+
"""Tests for rate limiting."""
|
|
107
|
+
|
|
108
|
+
def test_limiter_exists(self):
|
|
109
|
+
"""Test rate limiter is configured."""
|
|
110
|
+
from app.core.rate_limit import limiter
|
|
111
|
+
|
|
112
|
+
assert limiter is not None
|
|
113
|
+
{%- endif %}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
{%- if cookiecutter.enable_logfire %}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
from unittest.mock import patch # noqa: E402
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class TestLogfireSetup:
|
|
123
|
+
"""Tests for Logfire setup."""
|
|
124
|
+
|
|
125
|
+
@patch("app.core.logfire_setup.logfire")
|
|
126
|
+
def test_setup_logfire_configures(self, mock_logfire):
|
|
127
|
+
"""Test setup_logfire calls configure."""
|
|
128
|
+
from app.core.logfire_setup import setup_logfire
|
|
129
|
+
|
|
130
|
+
setup_logfire()
|
|
131
|
+
mock_logfire.configure.assert_called_once()
|
|
132
|
+
|
|
133
|
+
@patch("app.core.logfire_setup.logfire")
|
|
134
|
+
def test_instrument_app_instruments_fastapi(self, mock_logfire):
|
|
135
|
+
"""Test instrument_app instruments FastAPI."""
|
|
136
|
+
from fastapi import FastAPI
|
|
137
|
+
|
|
138
|
+
from app.core.logfire_setup import instrument_app
|
|
139
|
+
|
|
140
|
+
app = FastAPI()
|
|
141
|
+
instrument_app(app)
|
|
142
|
+
mock_logfire.instrument_fastapi.assert_called()
|
|
143
|
+
{%- endif %}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Tests for pipeline infrastructure."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from app.pipelines.base import BasePipeline, PipelineResult
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TestPipelineResult:
|
|
9
|
+
"""Tests for PipelineResult dataclass."""
|
|
10
|
+
|
|
11
|
+
def test_success_rate_all_processed(self):
|
|
12
|
+
"""Test success rate when all items processed."""
|
|
13
|
+
result = PipelineResult(processed=10, failed=0)
|
|
14
|
+
assert result.success_rate == 100.0
|
|
15
|
+
|
|
16
|
+
def test_success_rate_with_failures(self):
|
|
17
|
+
"""Test success rate with some failures."""
|
|
18
|
+
result = PipelineResult(processed=8, failed=2)
|
|
19
|
+
assert result.success_rate == 80.0
|
|
20
|
+
|
|
21
|
+
def test_success_rate_all_failed(self):
|
|
22
|
+
"""Test success rate when all items failed."""
|
|
23
|
+
result = PipelineResult(processed=0, failed=10)
|
|
24
|
+
assert result.success_rate == 0.0
|
|
25
|
+
|
|
26
|
+
def test_success_rate_empty(self):
|
|
27
|
+
"""Test success rate with no items."""
|
|
28
|
+
result = PipelineResult(processed=0, failed=0)
|
|
29
|
+
assert result.success_rate == 100.0
|
|
30
|
+
|
|
31
|
+
def test_has_errors_with_failures(self):
|
|
32
|
+
"""Test has_errors returns True when failures exist."""
|
|
33
|
+
result = PipelineResult(processed=5, failed=1)
|
|
34
|
+
assert result.has_errors is True
|
|
35
|
+
|
|
36
|
+
def test_has_errors_with_error_messages(self):
|
|
37
|
+
"""Test has_errors returns True when error messages exist."""
|
|
38
|
+
result = PipelineResult(processed=5, failed=0, errors=["Error 1"])
|
|
39
|
+
assert result.has_errors is True
|
|
40
|
+
|
|
41
|
+
def test_has_errors_no_errors(self):
|
|
42
|
+
"""Test has_errors returns False when no errors."""
|
|
43
|
+
result = PipelineResult(processed=5, failed=0)
|
|
44
|
+
assert result.has_errors is False
|
|
45
|
+
|
|
46
|
+
def test_default_values(self):
|
|
47
|
+
"""Test default values are set correctly."""
|
|
48
|
+
result = PipelineResult(processed=5)
|
|
49
|
+
assert result.failed == 0
|
|
50
|
+
assert result.errors == []
|
|
51
|
+
assert result.metadata == {}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class TestBasePipeline:
|
|
55
|
+
"""Tests for BasePipeline abstract class."""
|
|
56
|
+
|
|
57
|
+
@pytest.mark.anyio
|
|
58
|
+
async def test_validate_returns_true_by_default(self):
|
|
59
|
+
"""Test validate method returns True by default."""
|
|
60
|
+
|
|
61
|
+
class TestPipeline(BasePipeline):
|
|
62
|
+
async def run(self) -> PipelineResult:
|
|
63
|
+
return PipelineResult(processed=0)
|
|
64
|
+
|
|
65
|
+
pipeline = TestPipeline()
|
|
66
|
+
assert await pipeline.validate() is True
|
|
67
|
+
|
|
68
|
+
@pytest.mark.anyio
|
|
69
|
+
async def test_cleanup_does_nothing_by_default(self):
|
|
70
|
+
"""Test cleanup method does nothing by default."""
|
|
71
|
+
|
|
72
|
+
class TestPipeline(BasePipeline):
|
|
73
|
+
async def run(self) -> PipelineResult:
|
|
74
|
+
return PipelineResult(processed=0)
|
|
75
|
+
|
|
76
|
+
pipeline = TestPipeline()
|
|
77
|
+
await pipeline.cleanup() # Should not raise
|
|
78
|
+
|
|
79
|
+
@pytest.mark.anyio
|
|
80
|
+
async def test_run_must_be_implemented(self):
|
|
81
|
+
"""Test that run method must be implemented by subclasses."""
|
|
82
|
+
# This test verifies the abstract method requirement
|
|
83
|
+
with pytest.raises(TypeError, match="Can't instantiate abstract class"):
|
|
84
|
+
BasePipeline()
|
|
85
|
+
|
|
86
|
+
@pytest.mark.anyio
|
|
87
|
+
async def test_custom_pipeline_implementation(self):
|
|
88
|
+
"""Test a custom pipeline implementation."""
|
|
89
|
+
|
|
90
|
+
class MyPipeline(BasePipeline):
|
|
91
|
+
def __init__(self, items: list):
|
|
92
|
+
self.items = items
|
|
93
|
+
|
|
94
|
+
async def run(self) -> PipelineResult:
|
|
95
|
+
processed = 0
|
|
96
|
+
failed = 0
|
|
97
|
+
errors = []
|
|
98
|
+
|
|
99
|
+
for item in self.items:
|
|
100
|
+
if item > 0:
|
|
101
|
+
processed += 1
|
|
102
|
+
else:
|
|
103
|
+
failed += 1
|
|
104
|
+
errors.append(f"Invalid item: {item}")
|
|
105
|
+
|
|
106
|
+
return PipelineResult(
|
|
107
|
+
processed=processed,
|
|
108
|
+
failed=failed,
|
|
109
|
+
errors=errors,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
pipeline = MyPipeline([1, 2, 3, -1, 5])
|
|
113
|
+
result = await pipeline.run()
|
|
114
|
+
|
|
115
|
+
assert result.processed == 4
|
|
116
|
+
assert result.failed == 1
|
|
117
|
+
assert len(result.errors) == 1
|
|
118
|
+
assert result.success_rate == 80.0
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
{%- if cookiecutter.use_database %}
|
|
2
|
+
"""Tests for repository layer."""
|
|
3
|
+
|
|
4
|
+
from unittest.mock import AsyncMock, MagicMock
|
|
5
|
+
from uuid import uuid4
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
|
|
10
|
+
from app.repositories.base import BaseRepository
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class MockModel:
|
|
14
|
+
"""Mock SQLAlchemy model for testing."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, **kwargs):
|
|
17
|
+
self.id = kwargs.get("id", uuid4())
|
|
18
|
+
for key, value in kwargs.items():
|
|
19
|
+
setattr(self, key, value)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class MockCreateSchema(BaseModel):
|
|
23
|
+
"""Mock create schema."""
|
|
24
|
+
|
|
25
|
+
name: str
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class MockUpdateSchema(BaseModel):
|
|
29
|
+
"""Mock update schema."""
|
|
30
|
+
|
|
31
|
+
name: str | None = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class TestBaseRepository:
|
|
35
|
+
"""Tests for BaseRepository."""
|
|
36
|
+
|
|
37
|
+
@pytest.fixture
|
|
38
|
+
def repository(self):
|
|
39
|
+
"""Create a test repository."""
|
|
40
|
+
return BaseRepository[MockModel, MockCreateSchema, MockUpdateSchema](MockModel)
|
|
41
|
+
|
|
42
|
+
@pytest.fixture
|
|
43
|
+
def mock_session(self):
|
|
44
|
+
"""Create a mock async session."""
|
|
45
|
+
session = MagicMock()
|
|
46
|
+
session.get = AsyncMock()
|
|
47
|
+
session.execute = AsyncMock()
|
|
48
|
+
session.add = MagicMock()
|
|
49
|
+
session.flush = AsyncMock()
|
|
50
|
+
session.refresh = AsyncMock()
|
|
51
|
+
session.delete = AsyncMock()
|
|
52
|
+
return session
|
|
53
|
+
|
|
54
|
+
@pytest.mark.anyio
|
|
55
|
+
async def test_get_returns_model(self, repository, mock_session):
|
|
56
|
+
"""Test get returns a model by ID."""
|
|
57
|
+
mock_obj = MockModel(name="test")
|
|
58
|
+
mock_session.get.return_value = mock_obj
|
|
59
|
+
|
|
60
|
+
result = await repository.get(mock_session, mock_obj.id)
|
|
61
|
+
|
|
62
|
+
assert result == mock_obj
|
|
63
|
+
mock_session.get.assert_called_once_with(MockModel, mock_obj.id)
|
|
64
|
+
|
|
65
|
+
@pytest.mark.anyio
|
|
66
|
+
async def test_get_returns_none_when_not_found(self, repository, mock_session):
|
|
67
|
+
"""Test get returns None when not found."""
|
|
68
|
+
mock_session.get.return_value = None
|
|
69
|
+
|
|
70
|
+
result = await repository.get(mock_session, uuid4())
|
|
71
|
+
|
|
72
|
+
assert result is None
|
|
73
|
+
|
|
74
|
+
# Note: test_get_multi_returns_list is skipped because it requires a real
|
|
75
|
+
# SQLAlchemy model. The select() function cannot work with a mock class.
|
|
76
|
+
# For proper integration testing, use actual SQLAlchemy models with a test DB.
|
|
77
|
+
|
|
78
|
+
@pytest.mark.anyio
|
|
79
|
+
async def test_create_adds_and_returns_model(self, repository, mock_session):
|
|
80
|
+
"""Test create adds a new model."""
|
|
81
|
+
create_data = MockCreateSchema(name="new item")
|
|
82
|
+
|
|
83
|
+
# Mock the model creation
|
|
84
|
+
async def refresh_side_effect(obj):
|
|
85
|
+
obj.id = uuid4()
|
|
86
|
+
|
|
87
|
+
mock_session.refresh.side_effect = refresh_side_effect
|
|
88
|
+
|
|
89
|
+
result = await repository.create(mock_session, obj_in=create_data)
|
|
90
|
+
|
|
91
|
+
assert result.name == "new item"
|
|
92
|
+
mock_session.add.assert_called_once()
|
|
93
|
+
mock_session.flush.assert_called_once()
|
|
94
|
+
mock_session.refresh.assert_called_once()
|
|
95
|
+
|
|
96
|
+
@pytest.mark.anyio
|
|
97
|
+
async def test_update_with_schema(self, repository, mock_session):
|
|
98
|
+
"""Test update with Pydantic schema."""
|
|
99
|
+
db_obj = MockModel(name="old name")
|
|
100
|
+
update_data = MockUpdateSchema(name="new name")
|
|
101
|
+
|
|
102
|
+
result = await repository.update(mock_session, db_obj=db_obj, obj_in=update_data)
|
|
103
|
+
|
|
104
|
+
assert result.name == "new name"
|
|
105
|
+
mock_session.add.assert_called_once()
|
|
106
|
+
mock_session.flush.assert_called_once()
|
|
107
|
+
|
|
108
|
+
@pytest.mark.anyio
|
|
109
|
+
async def test_update_with_dict(self, repository, mock_session):
|
|
110
|
+
"""Test update with dictionary."""
|
|
111
|
+
db_obj = MockModel(name="old name")
|
|
112
|
+
update_data = {"name": "new name"}
|
|
113
|
+
|
|
114
|
+
result = await repository.update(mock_session, db_obj=db_obj, obj_in=update_data)
|
|
115
|
+
|
|
116
|
+
assert result.name == "new name"
|
|
117
|
+
|
|
118
|
+
@pytest.mark.anyio
|
|
119
|
+
async def test_delete_removes_and_returns_model(self, repository, mock_session):
|
|
120
|
+
"""Test delete removes and returns model."""
|
|
121
|
+
mock_obj = MockModel(name="to delete")
|
|
122
|
+
mock_session.get.return_value = mock_obj
|
|
123
|
+
|
|
124
|
+
result = await repository.delete(mock_session, id=mock_obj.id)
|
|
125
|
+
|
|
126
|
+
assert result == mock_obj
|
|
127
|
+
mock_session.delete.assert_called_once_with(mock_obj)
|
|
128
|
+
mock_session.flush.assert_called_once()
|
|
129
|
+
|
|
130
|
+
@pytest.mark.anyio
|
|
131
|
+
async def test_delete_returns_none_when_not_found(self, repository, mock_session):
|
|
132
|
+
"""Test delete returns None when not found."""
|
|
133
|
+
mock_session.get.return_value = None
|
|
134
|
+
|
|
135
|
+
result = await repository.delete(mock_session, id=uuid4())
|
|
136
|
+
|
|
137
|
+
assert result is None
|
|
138
|
+
mock_session.delete.assert_not_called()
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
{%- if cookiecutter.use_jwt %}
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class TestUserRepository:
|
|
145
|
+
"""Tests for user repository functions."""
|
|
146
|
+
|
|
147
|
+
@pytest.fixture
|
|
148
|
+
def mock_session(self):
|
|
149
|
+
"""Create a mock async session."""
|
|
150
|
+
session = MagicMock()
|
|
151
|
+
session.execute = AsyncMock()
|
|
152
|
+
return session
|
|
153
|
+
|
|
154
|
+
@pytest.mark.anyio
|
|
155
|
+
async def test_get_by_email(self, mock_session):
|
|
156
|
+
"""Test get_by_email returns user."""
|
|
157
|
+
from app.repositories import user as user_repo
|
|
158
|
+
|
|
159
|
+
mock_user = MagicMock()
|
|
160
|
+
mock_result = MagicMock()
|
|
161
|
+
mock_result.scalar_one_or_none.return_value = mock_user
|
|
162
|
+
mock_session.execute.return_value = mock_result
|
|
163
|
+
|
|
164
|
+
result = await user_repo.get_by_email(mock_session, "test@example.com")
|
|
165
|
+
|
|
166
|
+
assert result == mock_user
|
|
167
|
+
|
|
168
|
+
@pytest.mark.anyio
|
|
169
|
+
async def test_get_by_email_not_found(self, mock_session):
|
|
170
|
+
"""Test get_by_email returns None when not found."""
|
|
171
|
+
from app.repositories import user as user_repo
|
|
172
|
+
|
|
173
|
+
mock_result = MagicMock()
|
|
174
|
+
mock_result.scalar_one_or_none.return_value = None
|
|
175
|
+
mock_session.execute.return_value = mock_result
|
|
176
|
+
|
|
177
|
+
result = await user_repo.get_by_email(mock_session, "notfound@example.com")
|
|
178
|
+
|
|
179
|
+
assert result is None
|
|
180
|
+
{%- endif %}
|
|
181
|
+
{%- endif %}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
{%- if cookiecutter.use_jwt %}
|
|
2
|
+
"""Tests for security module."""
|
|
3
|
+
|
|
4
|
+
from datetime import timedelta
|
|
5
|
+
|
|
6
|
+
from app.core.security import (
|
|
7
|
+
create_access_token,
|
|
8
|
+
create_refresh_token,
|
|
9
|
+
get_password_hash,
|
|
10
|
+
verify_password,
|
|
11
|
+
verify_token,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TestPasswordHashing:
|
|
16
|
+
"""Tests for password hashing functions."""
|
|
17
|
+
|
|
18
|
+
def test_hash_password(self):
|
|
19
|
+
"""Test password hashing."""
|
|
20
|
+
password = "mysecretpassword"
|
|
21
|
+
hashed = get_password_hash(password)
|
|
22
|
+
|
|
23
|
+
assert hashed != password
|
|
24
|
+
assert len(hashed) > 0
|
|
25
|
+
assert hashed.startswith("$2") # bcrypt prefix
|
|
26
|
+
|
|
27
|
+
def test_verify_password_correct(self):
|
|
28
|
+
"""Test verifying correct password."""
|
|
29
|
+
password = "mysecretpassword"
|
|
30
|
+
hashed = get_password_hash(password)
|
|
31
|
+
|
|
32
|
+
assert verify_password(password, hashed) is True
|
|
33
|
+
|
|
34
|
+
def test_verify_password_incorrect(self):
|
|
35
|
+
"""Test verifying incorrect password."""
|
|
36
|
+
password = "mysecretpassword"
|
|
37
|
+
wrong_password = "wrongpassword"
|
|
38
|
+
hashed = get_password_hash(password)
|
|
39
|
+
|
|
40
|
+
assert verify_password(wrong_password, hashed) is False
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class TestAccessToken:
|
|
44
|
+
"""Tests for access token functions."""
|
|
45
|
+
|
|
46
|
+
def test_create_access_token(self):
|
|
47
|
+
"""Test creating access token."""
|
|
48
|
+
subject = "user123"
|
|
49
|
+
token = create_access_token(subject)
|
|
50
|
+
|
|
51
|
+
assert isinstance(token, str)
|
|
52
|
+
assert len(token) > 0
|
|
53
|
+
|
|
54
|
+
def test_create_access_token_with_expires_delta(self):
|
|
55
|
+
"""Test creating access token with custom expiration."""
|
|
56
|
+
subject = "user123"
|
|
57
|
+
expires = timedelta(hours=2)
|
|
58
|
+
token = create_access_token(subject, expires_delta=expires)
|
|
59
|
+
|
|
60
|
+
assert isinstance(token, str)
|
|
61
|
+
payload = verify_token(token)
|
|
62
|
+
assert payload is not None
|
|
63
|
+
assert payload["sub"] == subject
|
|
64
|
+
assert payload["type"] == "access"
|
|
65
|
+
|
|
66
|
+
def test_verify_access_token(self):
|
|
67
|
+
"""Test verifying access token."""
|
|
68
|
+
subject = "user123"
|
|
69
|
+
token = create_access_token(subject)
|
|
70
|
+
payload = verify_token(token)
|
|
71
|
+
|
|
72
|
+
assert payload is not None
|
|
73
|
+
assert payload["sub"] == subject
|
|
74
|
+
assert payload["type"] == "access"
|
|
75
|
+
|
|
76
|
+
def test_verify_invalid_token(self):
|
|
77
|
+
"""Test verifying invalid token."""
|
|
78
|
+
payload = verify_token("invalid.token.here")
|
|
79
|
+
|
|
80
|
+
assert payload is None
|
|
81
|
+
|
|
82
|
+
def test_verify_expired_token(self):
|
|
83
|
+
"""Test verifying expired token."""
|
|
84
|
+
subject = "user123"
|
|
85
|
+
# Create token that expires immediately
|
|
86
|
+
token = create_access_token(subject, expires_delta=timedelta(seconds=-1))
|
|
87
|
+
payload = verify_token(token)
|
|
88
|
+
|
|
89
|
+
assert payload is None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class TestRefreshToken:
|
|
93
|
+
"""Tests for refresh token functions."""
|
|
94
|
+
|
|
95
|
+
def test_create_refresh_token(self):
|
|
96
|
+
"""Test creating refresh token."""
|
|
97
|
+
subject = "user123"
|
|
98
|
+
token = create_refresh_token(subject)
|
|
99
|
+
|
|
100
|
+
assert isinstance(token, str)
|
|
101
|
+
assert len(token) > 0
|
|
102
|
+
|
|
103
|
+
def test_create_refresh_token_with_expires_delta(self):
|
|
104
|
+
"""Test creating refresh token with custom expiration."""
|
|
105
|
+
subject = "user123"
|
|
106
|
+
expires = timedelta(days=7)
|
|
107
|
+
token = create_refresh_token(subject, expires_delta=expires)
|
|
108
|
+
|
|
109
|
+
assert isinstance(token, str)
|
|
110
|
+
payload = verify_token(token)
|
|
111
|
+
assert payload is not None
|
|
112
|
+
assert payload["sub"] == subject
|
|
113
|
+
assert payload["type"] == "refresh"
|
|
114
|
+
|
|
115
|
+
def test_verify_refresh_token(self):
|
|
116
|
+
"""Test verifying refresh token."""
|
|
117
|
+
subject = "user123"
|
|
118
|
+
token = create_refresh_token(subject)
|
|
119
|
+
payload = verify_token(token)
|
|
120
|
+
|
|
121
|
+
assert payload is not None
|
|
122
|
+
assert payload["sub"] == subject
|
|
123
|
+
assert payload["type"] == "refresh"
|
|
124
|
+
{%- endif %}
|