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,261 @@
|
|
|
1
|
+
{%- if cookiecutter.enable_ai_agent and cookiecutter.use_pydantic_ai %}
|
|
2
|
+
"""Tests for AI agent module (PydanticAI)."""
|
|
3
|
+
|
|
4
|
+
from unittest.mock import MagicMock, patch
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from app.agents.assistant import AssistantAgent, Deps, get_agent, run_agent
|
|
9
|
+
from app.agents.tools.datetime_tool import get_current_datetime
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TestDeps:
|
|
13
|
+
"""Tests for Deps dataclass."""
|
|
14
|
+
|
|
15
|
+
def test_deps_default_values(self):
|
|
16
|
+
"""Test Deps has correct default values."""
|
|
17
|
+
deps = Deps()
|
|
18
|
+
assert deps.user_id is None
|
|
19
|
+
assert deps.user_name is None
|
|
20
|
+
assert deps.metadata == {}
|
|
21
|
+
|
|
22
|
+
def test_deps_with_values(self):
|
|
23
|
+
"""Test Deps with custom values."""
|
|
24
|
+
deps = Deps(user_id="123", user_name="Test User", metadata={"key": "value"})
|
|
25
|
+
assert deps.user_id == "123"
|
|
26
|
+
assert deps.user_name == "Test User"
|
|
27
|
+
assert deps.metadata == {"key": "value"}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class TestGetCurrentDatetime:
|
|
31
|
+
"""Tests for get_current_datetime tool."""
|
|
32
|
+
|
|
33
|
+
def test_returns_formatted_string(self):
|
|
34
|
+
"""Test get_current_datetime returns formatted string."""
|
|
35
|
+
result = get_current_datetime()
|
|
36
|
+
assert isinstance(result, str)
|
|
37
|
+
# Should contain year, month, day
|
|
38
|
+
assert len(result) > 10
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class TestAssistantAgent:
|
|
42
|
+
"""Tests for AssistantAgent class."""
|
|
43
|
+
|
|
44
|
+
def test_init_with_defaults(self):
|
|
45
|
+
"""Test AssistantAgent initializes with defaults."""
|
|
46
|
+
agent = AssistantAgent()
|
|
47
|
+
assert agent.system_prompt == "You are a helpful assistant."
|
|
48
|
+
assert agent._agent is None
|
|
49
|
+
|
|
50
|
+
def test_init_with_custom_values(self):
|
|
51
|
+
"""Test AssistantAgent with custom configuration."""
|
|
52
|
+
agent = AssistantAgent(
|
|
53
|
+
model_name="gpt-4",
|
|
54
|
+
temperature=0.5,
|
|
55
|
+
system_prompt="Custom prompt",
|
|
56
|
+
)
|
|
57
|
+
assert agent.model_name == "gpt-4"
|
|
58
|
+
assert agent.temperature == 0.5
|
|
59
|
+
assert agent.system_prompt == "Custom prompt"
|
|
60
|
+
|
|
61
|
+
@patch("app.agents.assistant.OpenAIProvider")
|
|
62
|
+
@patch("app.agents.assistant.OpenAIChatModel")
|
|
63
|
+
def test_agent_property_creates_agent(self, mock_model, mock_provider):
|
|
64
|
+
"""Test agent property creates agent on first access."""
|
|
65
|
+
agent = AssistantAgent()
|
|
66
|
+
_ = agent.agent
|
|
67
|
+
assert agent._agent is not None
|
|
68
|
+
mock_model.assert_called_once()
|
|
69
|
+
|
|
70
|
+
@patch("app.agents.assistant.OpenAIProvider")
|
|
71
|
+
@patch("app.agents.assistant.OpenAIChatModel")
|
|
72
|
+
def test_agent_property_caches_agent(self, mock_model, mock_provider):
|
|
73
|
+
"""Test agent property caches the agent instance."""
|
|
74
|
+
agent = AssistantAgent()
|
|
75
|
+
agent1 = agent.agent
|
|
76
|
+
agent2 = agent.agent
|
|
77
|
+
assert agent1 is agent2
|
|
78
|
+
mock_model.assert_called_once()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class TestGetAgent:
|
|
82
|
+
"""Tests for get_agent factory function."""
|
|
83
|
+
|
|
84
|
+
def test_returns_assistant_agent(self):
|
|
85
|
+
"""Test get_agent returns AssistantAgent."""
|
|
86
|
+
agent = get_agent()
|
|
87
|
+
assert isinstance(agent, AssistantAgent)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class TestAgentRoutes:
|
|
91
|
+
"""Tests for agent WebSocket routes."""
|
|
92
|
+
|
|
93
|
+
@pytest.mark.anyio
|
|
94
|
+
async def test_agent_websocket_connection(self, client):
|
|
95
|
+
"""Test WebSocket connection to agent endpoint."""
|
|
96
|
+
# This test verifies the WebSocket endpoint is accessible
|
|
97
|
+
# Actual agent testing would require mocking OpenAI
|
|
98
|
+
pass
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class TestHistoryConversion:
|
|
102
|
+
"""Tests for conversation history conversion."""
|
|
103
|
+
|
|
104
|
+
def test_empty_history(self):
|
|
105
|
+
"""Test with empty history."""
|
|
106
|
+
_agent = AssistantAgent() # noqa: F841
|
|
107
|
+
# History conversion happens inside run/iter methods
|
|
108
|
+
# We test the structure here
|
|
109
|
+
history = []
|
|
110
|
+
assert len(history) == 0
|
|
111
|
+
|
|
112
|
+
def test_history_roles(self):
|
|
113
|
+
"""Test history with different roles."""
|
|
114
|
+
history = [
|
|
115
|
+
{"role": "user", "content": "Hello"},
|
|
116
|
+
{"role": "assistant", "content": "Hi there!"},
|
|
117
|
+
{"role": "system", "content": "You are helpful"},
|
|
118
|
+
]
|
|
119
|
+
assert len(history) == 3
|
|
120
|
+
assert all("role" in msg and "content" in msg for msg in history)
|
|
121
|
+
{%- elif cookiecutter.enable_ai_agent and cookiecutter.use_langchain %}
|
|
122
|
+
"""Tests for AI agent module (LangChain)."""
|
|
123
|
+
|
|
124
|
+
from unittest.mock import MagicMock, patch
|
|
125
|
+
|
|
126
|
+
import pytest
|
|
127
|
+
|
|
128
|
+
from app.agents.langchain_assistant import AgentContext, LangChainAssistant, get_agent, run_agent
|
|
129
|
+
from app.agents.tools.datetime_tool import get_current_datetime
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class TestAgentContext:
|
|
133
|
+
"""Tests for AgentContext TypedDict."""
|
|
134
|
+
|
|
135
|
+
def test_context_empty(self):
|
|
136
|
+
"""Test AgentContext can be empty."""
|
|
137
|
+
context: AgentContext = {}
|
|
138
|
+
assert "user_id" not in context
|
|
139
|
+
assert "user_name" not in context
|
|
140
|
+
|
|
141
|
+
def test_context_with_values(self):
|
|
142
|
+
"""Test AgentContext with values."""
|
|
143
|
+
context: AgentContext = {
|
|
144
|
+
"user_id": "123",
|
|
145
|
+
"user_name": "Test User",
|
|
146
|
+
"metadata": {"key": "value"},
|
|
147
|
+
}
|
|
148
|
+
assert context["user_id"] == "123"
|
|
149
|
+
assert context["user_name"] == "Test User"
|
|
150
|
+
assert context["metadata"] == {"key": "value"}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class TestGetCurrentDatetime:
|
|
154
|
+
"""Tests for get_current_datetime tool."""
|
|
155
|
+
|
|
156
|
+
def test_returns_formatted_string(self):
|
|
157
|
+
"""Test get_current_datetime returns formatted string."""
|
|
158
|
+
result = get_current_datetime()
|
|
159
|
+
assert isinstance(result, str)
|
|
160
|
+
# Should contain year, month, day
|
|
161
|
+
assert len(result) > 10
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class TestLangChainAssistant:
|
|
165
|
+
"""Tests for LangChainAssistant class."""
|
|
166
|
+
|
|
167
|
+
def test_init_with_defaults(self):
|
|
168
|
+
"""Test LangChainAssistant initializes with defaults."""
|
|
169
|
+
agent = LangChainAssistant()
|
|
170
|
+
assert agent.system_prompt == "You are a helpful assistant."
|
|
171
|
+
assert agent._agent is None
|
|
172
|
+
|
|
173
|
+
def test_init_with_custom_values(self):
|
|
174
|
+
"""Test LangChainAssistant with custom configuration."""
|
|
175
|
+
agent = LangChainAssistant(
|
|
176
|
+
model_name="gpt-4",
|
|
177
|
+
temperature=0.5,
|
|
178
|
+
system_prompt="Custom prompt",
|
|
179
|
+
)
|
|
180
|
+
assert agent.model_name == "gpt-4"
|
|
181
|
+
assert agent.temperature == 0.5
|
|
182
|
+
assert agent.system_prompt == "Custom prompt"
|
|
183
|
+
|
|
184
|
+
@patch("app.agents.langchain_assistant.ChatOpenAI")
|
|
185
|
+
@patch("app.agents.langchain_assistant.create_agent")
|
|
186
|
+
def test_agent_property_creates_agent(self, mock_create_agent, mock_chat):
|
|
187
|
+
"""Test agent property creates agent on first access."""
|
|
188
|
+
mock_create_agent.return_value = MagicMock()
|
|
189
|
+
agent = LangChainAssistant()
|
|
190
|
+
_ = agent.agent
|
|
191
|
+
assert agent._agent is not None
|
|
192
|
+
mock_create_agent.assert_called_once()
|
|
193
|
+
|
|
194
|
+
@patch("app.agents.langchain_assistant.ChatOpenAI")
|
|
195
|
+
@patch("app.agents.langchain_assistant.create_agent")
|
|
196
|
+
def test_agent_property_caches_agent(self, mock_create_agent, mock_chat):
|
|
197
|
+
"""Test agent property caches the agent instance."""
|
|
198
|
+
mock_create_agent.return_value = MagicMock()
|
|
199
|
+
agent = LangChainAssistant()
|
|
200
|
+
agent1 = agent.agent
|
|
201
|
+
agent2 = agent.agent
|
|
202
|
+
assert agent1 is agent2
|
|
203
|
+
mock_create_agent.assert_called_once()
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class TestGetAgent:
|
|
207
|
+
"""Tests for get_agent factory function."""
|
|
208
|
+
|
|
209
|
+
def test_returns_langchain_assistant(self):
|
|
210
|
+
"""Test get_agent returns LangChainAssistant."""
|
|
211
|
+
agent = get_agent()
|
|
212
|
+
assert isinstance(agent, LangChainAssistant)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class TestAgentRoutes:
|
|
216
|
+
"""Tests for agent WebSocket routes."""
|
|
217
|
+
|
|
218
|
+
@pytest.mark.anyio
|
|
219
|
+
async def test_agent_websocket_connection(self, client):
|
|
220
|
+
"""Test WebSocket connection to agent endpoint."""
|
|
221
|
+
# This test verifies the WebSocket endpoint is accessible
|
|
222
|
+
# Actual agent testing would require mocking OpenAI
|
|
223
|
+
pass
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class TestHistoryConversion:
|
|
227
|
+
"""Tests for conversation history conversion."""
|
|
228
|
+
|
|
229
|
+
def test_empty_history(self):
|
|
230
|
+
"""Test with empty history."""
|
|
231
|
+
_agent = LangChainAssistant() # noqa: F841
|
|
232
|
+
# History conversion happens inside run/stream methods
|
|
233
|
+
# We test the structure here
|
|
234
|
+
history = []
|
|
235
|
+
assert len(history) == 0
|
|
236
|
+
|
|
237
|
+
def test_history_roles(self):
|
|
238
|
+
"""Test history with different roles."""
|
|
239
|
+
history = [
|
|
240
|
+
{"role": "user", "content": "Hello"},
|
|
241
|
+
{"role": "assistant", "content": "Hi there!"},
|
|
242
|
+
{"role": "system", "content": "You are helpful"},
|
|
243
|
+
]
|
|
244
|
+
assert len(history) == 3
|
|
245
|
+
assert all("role" in msg and "content" in msg for msg in history)
|
|
246
|
+
|
|
247
|
+
def test_convert_history(self):
|
|
248
|
+
"""Test _convert_history method."""
|
|
249
|
+
agent = LangChainAssistant()
|
|
250
|
+
history = [
|
|
251
|
+
{"role": "user", "content": "Hello"},
|
|
252
|
+
{"role": "assistant", "content": "Hi there!"},
|
|
253
|
+
{"role": "system", "content": "You are helpful"},
|
|
254
|
+
]
|
|
255
|
+
messages = agent._convert_history(history)
|
|
256
|
+
assert len(messages) == 3
|
|
257
|
+
from langchain.messages import AIMessage, HumanMessage, SystemMessage
|
|
258
|
+
assert isinstance(messages[0], HumanMessage)
|
|
259
|
+
assert isinstance(messages[1], AIMessage)
|
|
260
|
+
assert isinstance(messages[2], SystemMessage)
|
|
261
|
+
{%- endif %}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
{%- if cookiecutter.enable_redis %}
|
|
2
|
+
"""Tests for client modules."""
|
|
3
|
+
|
|
4
|
+
from unittest.mock import AsyncMock, MagicMock, patch
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from app.clients.redis import RedisClient
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TestRedisClient:
|
|
12
|
+
"""Tests for RedisClient."""
|
|
13
|
+
|
|
14
|
+
@pytest.fixture
|
|
15
|
+
def redis_client(self) -> RedisClient:
|
|
16
|
+
"""Create a RedisClient instance."""
|
|
17
|
+
return RedisClient(url="redis://localhost:6379")
|
|
18
|
+
|
|
19
|
+
@pytest.fixture
|
|
20
|
+
def mock_aioredis(self) -> MagicMock:
|
|
21
|
+
"""Create a mock aioredis client."""
|
|
22
|
+
mock = MagicMock()
|
|
23
|
+
mock.get = AsyncMock(return_value="value")
|
|
24
|
+
mock.set = AsyncMock()
|
|
25
|
+
mock.delete = AsyncMock(return_value=1)
|
|
26
|
+
mock.exists = AsyncMock(return_value=1)
|
|
27
|
+
mock.ping = AsyncMock()
|
|
28
|
+
mock.close = AsyncMock()
|
|
29
|
+
return mock
|
|
30
|
+
|
|
31
|
+
@pytest.mark.anyio
|
|
32
|
+
async def test_connect(self, redis_client: RedisClient):
|
|
33
|
+
"""Test Redis connection."""
|
|
34
|
+
with patch("app.clients.redis.aioredis") as mock_aioredis:
|
|
35
|
+
mock_client = MagicMock()
|
|
36
|
+
mock_aioredis.from_url.return_value = mock_client
|
|
37
|
+
|
|
38
|
+
await redis_client.connect()
|
|
39
|
+
|
|
40
|
+
assert redis_client.client is not None
|
|
41
|
+
mock_aioredis.from_url.assert_called_once()
|
|
42
|
+
|
|
43
|
+
@pytest.mark.anyio
|
|
44
|
+
async def test_close(self, redis_client: RedisClient, mock_aioredis: MagicMock):
|
|
45
|
+
"""Test closing Redis connection."""
|
|
46
|
+
redis_client.client = mock_aioredis
|
|
47
|
+
|
|
48
|
+
await redis_client.close()
|
|
49
|
+
|
|
50
|
+
mock_aioredis.close.assert_called_once()
|
|
51
|
+
assert redis_client.client is None
|
|
52
|
+
|
|
53
|
+
@pytest.mark.anyio
|
|
54
|
+
async def test_close_when_not_connected(self, redis_client: RedisClient):
|
|
55
|
+
"""Test closing when not connected does nothing."""
|
|
56
|
+
redis_client.client = None
|
|
57
|
+
|
|
58
|
+
await redis_client.close() # Should not raise
|
|
59
|
+
|
|
60
|
+
@pytest.mark.anyio
|
|
61
|
+
async def test_get(self, redis_client: RedisClient, mock_aioredis: MagicMock):
|
|
62
|
+
"""Test getting a value."""
|
|
63
|
+
redis_client.client = mock_aioredis
|
|
64
|
+
|
|
65
|
+
result = await redis_client.get("test_key")
|
|
66
|
+
|
|
67
|
+
assert result == "value"
|
|
68
|
+
mock_aioredis.get.assert_called_once_with("test_key")
|
|
69
|
+
|
|
70
|
+
@pytest.mark.anyio
|
|
71
|
+
async def test_get_not_connected(self, redis_client: RedisClient):
|
|
72
|
+
"""Test getting when not connected raises error."""
|
|
73
|
+
redis_client.client = None
|
|
74
|
+
|
|
75
|
+
with pytest.raises(RuntimeError, match="not connected"):
|
|
76
|
+
await redis_client.get("test_key")
|
|
77
|
+
|
|
78
|
+
@pytest.mark.anyio
|
|
79
|
+
async def test_set(self, redis_client: RedisClient, mock_aioredis: MagicMock):
|
|
80
|
+
"""Test setting a value."""
|
|
81
|
+
redis_client.client = mock_aioredis
|
|
82
|
+
|
|
83
|
+
await redis_client.set("test_key", "test_value")
|
|
84
|
+
|
|
85
|
+
mock_aioredis.set.assert_called_once_with("test_key", "test_value", ex=None)
|
|
86
|
+
|
|
87
|
+
@pytest.mark.anyio
|
|
88
|
+
async def test_set_with_ttl(self, redis_client: RedisClient, mock_aioredis: MagicMock):
|
|
89
|
+
"""Test setting a value with TTL."""
|
|
90
|
+
redis_client.client = mock_aioredis
|
|
91
|
+
|
|
92
|
+
await redis_client.set("test_key", "test_value", ttl=60)
|
|
93
|
+
|
|
94
|
+
mock_aioredis.set.assert_called_once_with("test_key", "test_value", ex=60)
|
|
95
|
+
|
|
96
|
+
@pytest.mark.anyio
|
|
97
|
+
async def test_set_not_connected(self, redis_client: RedisClient):
|
|
98
|
+
"""Test setting when not connected raises error."""
|
|
99
|
+
redis_client.client = None
|
|
100
|
+
|
|
101
|
+
with pytest.raises(RuntimeError, match="not connected"):
|
|
102
|
+
await redis_client.set("test_key", "test_value")
|
|
103
|
+
|
|
104
|
+
@pytest.mark.anyio
|
|
105
|
+
async def test_delete(self, redis_client: RedisClient, mock_aioredis: MagicMock):
|
|
106
|
+
"""Test deleting a key."""
|
|
107
|
+
redis_client.client = mock_aioredis
|
|
108
|
+
|
|
109
|
+
result = await redis_client.delete("test_key")
|
|
110
|
+
|
|
111
|
+
assert result == 1
|
|
112
|
+
mock_aioredis.delete.assert_called_once_with("test_key")
|
|
113
|
+
|
|
114
|
+
@pytest.mark.anyio
|
|
115
|
+
async def test_delete_not_connected(self, redis_client: RedisClient):
|
|
116
|
+
"""Test deleting when not connected raises error."""
|
|
117
|
+
redis_client.client = None
|
|
118
|
+
|
|
119
|
+
with pytest.raises(RuntimeError, match="not connected"):
|
|
120
|
+
await redis_client.delete("test_key")
|
|
121
|
+
|
|
122
|
+
@pytest.mark.anyio
|
|
123
|
+
async def test_exists(self, redis_client: RedisClient, mock_aioredis: MagicMock):
|
|
124
|
+
"""Test checking if key exists."""
|
|
125
|
+
redis_client.client = mock_aioredis
|
|
126
|
+
|
|
127
|
+
result = await redis_client.exists("test_key")
|
|
128
|
+
|
|
129
|
+
assert result is True
|
|
130
|
+
mock_aioredis.exists.assert_called_once_with("test_key")
|
|
131
|
+
|
|
132
|
+
@pytest.mark.anyio
|
|
133
|
+
async def test_exists_not_connected(self, redis_client: RedisClient):
|
|
134
|
+
"""Test exists when not connected raises error."""
|
|
135
|
+
redis_client.client = None
|
|
136
|
+
|
|
137
|
+
with pytest.raises(RuntimeError, match="not connected"):
|
|
138
|
+
await redis_client.exists("test_key")
|
|
139
|
+
|
|
140
|
+
@pytest.mark.anyio
|
|
141
|
+
async def test_ping_connected(self, redis_client: RedisClient, mock_aioredis: MagicMock):
|
|
142
|
+
"""Test ping when connected."""
|
|
143
|
+
redis_client.client = mock_aioredis
|
|
144
|
+
|
|
145
|
+
result = await redis_client.ping()
|
|
146
|
+
|
|
147
|
+
assert result is True
|
|
148
|
+
mock_aioredis.ping.assert_called_once()
|
|
149
|
+
|
|
150
|
+
@pytest.mark.anyio
|
|
151
|
+
async def test_ping_not_connected(self, redis_client: RedisClient):
|
|
152
|
+
"""Test ping when not connected returns False."""
|
|
153
|
+
redis_client.client = None
|
|
154
|
+
|
|
155
|
+
result = await redis_client.ping()
|
|
156
|
+
|
|
157
|
+
assert result is False
|
|
158
|
+
|
|
159
|
+
@pytest.mark.anyio
|
|
160
|
+
async def test_ping_exception(self, redis_client: RedisClient, mock_aioredis: MagicMock):
|
|
161
|
+
"""Test ping when exception occurs returns False."""
|
|
162
|
+
redis_client.client = mock_aioredis
|
|
163
|
+
mock_aioredis.ping = AsyncMock(side_effect=Exception("Connection error"))
|
|
164
|
+
|
|
165
|
+
result = await redis_client.ping()
|
|
166
|
+
|
|
167
|
+
assert result is False
|
|
168
|
+
|
|
169
|
+
def test_raw_property(self, redis_client: RedisClient, mock_aioredis: MagicMock):
|
|
170
|
+
"""Test accessing raw client."""
|
|
171
|
+
redis_client.client = mock_aioredis
|
|
172
|
+
|
|
173
|
+
result = redis_client.raw
|
|
174
|
+
|
|
175
|
+
assert result == mock_aioredis
|
|
176
|
+
|
|
177
|
+
def test_raw_property_not_connected(self, redis_client: RedisClient):
|
|
178
|
+
"""Test accessing raw client when not connected raises error."""
|
|
179
|
+
redis_client.client = None
|
|
180
|
+
|
|
181
|
+
with pytest.raises(RuntimeError, match="not connected"):
|
|
182
|
+
_ = redis_client.raw
|
|
183
|
+
{%- endif %}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""Tests for CLI commands module."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from click.testing import CliRunner
|
|
5
|
+
|
|
6
|
+
from app.commands import (
|
|
7
|
+
command,
|
|
8
|
+
discover_commands,
|
|
9
|
+
error,
|
|
10
|
+
info,
|
|
11
|
+
register_commands,
|
|
12
|
+
success,
|
|
13
|
+
warning,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TestCommandDecorator:
|
|
18
|
+
"""Tests for the command decorator."""
|
|
19
|
+
|
|
20
|
+
def test_command_registers_function(self):
|
|
21
|
+
"""Test that @command decorator registers a click command."""
|
|
22
|
+
from app.commands import _commands
|
|
23
|
+
|
|
24
|
+
initial_count = len(_commands)
|
|
25
|
+
|
|
26
|
+
@command("test-cmd", help="Test command")
|
|
27
|
+
def test_func():
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
assert len(_commands) == initial_count + 1
|
|
31
|
+
assert _commands[-1].name == "test-cmd"
|
|
32
|
+
|
|
33
|
+
def test_command_uses_function_name_as_default(self):
|
|
34
|
+
"""Test that command name defaults to function name."""
|
|
35
|
+
from app.commands import _commands
|
|
36
|
+
|
|
37
|
+
@command()
|
|
38
|
+
def my_test_command():
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
assert _commands[-1].name == "my-test-command"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class TestHelperFunctions:
|
|
45
|
+
"""Tests for helper output functions."""
|
|
46
|
+
|
|
47
|
+
def test_success_prints_green(self, capsys):
|
|
48
|
+
"""Test success prints in green."""
|
|
49
|
+
success("Test message")
|
|
50
|
+
# Click uses escape codes for colors
|
|
51
|
+
captured = capsys.readouterr()
|
|
52
|
+
assert "Test message" in captured.out
|
|
53
|
+
|
|
54
|
+
def test_error_prints_red(self, capsys):
|
|
55
|
+
"""Test error prints in red."""
|
|
56
|
+
error("Error message")
|
|
57
|
+
captured = capsys.readouterr()
|
|
58
|
+
assert "Error message" in captured.out
|
|
59
|
+
|
|
60
|
+
def test_warning_prints_yellow(self, capsys):
|
|
61
|
+
"""Test warning prints in yellow."""
|
|
62
|
+
warning("Warning message")
|
|
63
|
+
captured = capsys.readouterr()
|
|
64
|
+
assert "Warning message" in captured.out
|
|
65
|
+
|
|
66
|
+
def test_info_prints_plain(self, capsys):
|
|
67
|
+
"""Test info prints plain text."""
|
|
68
|
+
info("Info message")
|
|
69
|
+
captured = capsys.readouterr()
|
|
70
|
+
assert "Info message" in captured.out
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class TestDiscoverCommands:
|
|
74
|
+
"""Tests for command discovery."""
|
|
75
|
+
|
|
76
|
+
def test_discover_commands_returns_list(self):
|
|
77
|
+
"""Test that discover_commands returns a list."""
|
|
78
|
+
commands = discover_commands()
|
|
79
|
+
assert isinstance(commands, list)
|
|
80
|
+
|
|
81
|
+
def test_discover_commands_caches_results(self):
|
|
82
|
+
"""Test that discover_commands caches on second call."""
|
|
83
|
+
commands1 = discover_commands()
|
|
84
|
+
commands2 = discover_commands()
|
|
85
|
+
assert commands1 is commands2
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class TestRegisterCommands:
|
|
89
|
+
"""Tests for registering commands."""
|
|
90
|
+
|
|
91
|
+
def test_register_commands_adds_to_group(self):
|
|
92
|
+
"""Test that register_commands adds discovered commands to CLI group."""
|
|
93
|
+
@click.group()
|
|
94
|
+
def cli():
|
|
95
|
+
pass
|
|
96
|
+
|
|
97
|
+
register_commands(cli)
|
|
98
|
+
# After registration, cli should have commands
|
|
99
|
+
# We can't assert exact count since it depends on what's discovered
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
{%- if cookiecutter.use_database %}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class TestSeedCommand:
|
|
106
|
+
"""Tests for the seed command."""
|
|
107
|
+
|
|
108
|
+
def test_seed_dry_run(self):
|
|
109
|
+
"""Test seed command with --dry-run."""
|
|
110
|
+
from app.commands.seed import seed
|
|
111
|
+
|
|
112
|
+
runner = CliRunner()
|
|
113
|
+
result = runner.invoke(seed, ["--dry-run", "--count", "5"])
|
|
114
|
+
assert result.exit_code == 0
|
|
115
|
+
assert "[DRY RUN]" in result.output
|
|
116
|
+
assert "5" in result.output
|
|
117
|
+
|
|
118
|
+
def test_seed_dry_run_with_clear(self):
|
|
119
|
+
"""Test seed command with --dry-run and --clear."""
|
|
120
|
+
from app.commands.seed import seed
|
|
121
|
+
|
|
122
|
+
runner = CliRunner()
|
|
123
|
+
result = runner.invoke(seed, ["--dry-run", "--clear"])
|
|
124
|
+
assert result.exit_code == 0
|
|
125
|
+
assert "Would clear existing data" in result.output
|
|
126
|
+
{%- endif %}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class TestHelloCommand:
|
|
130
|
+
"""Tests for the hello command."""
|
|
131
|
+
|
|
132
|
+
def test_hello_command_runs(self):
|
|
133
|
+
"""Test hello command executes."""
|
|
134
|
+
from app.commands.example import hello
|
|
135
|
+
|
|
136
|
+
runner = CliRunner()
|
|
137
|
+
result = runner.invoke(hello)
|
|
138
|
+
assert result.exit_code == 0
|
|
139
|
+
assert "Hello" in result.output
|
|
140
|
+
|
|
141
|
+
def test_hello_command_with_name(self):
|
|
142
|
+
"""Test hello command with --name option."""
|
|
143
|
+
from app.commands.example import hello
|
|
144
|
+
|
|
145
|
+
runner = CliRunner()
|
|
146
|
+
result = runner.invoke(hello, ["--name", "Alice"])
|
|
147
|
+
assert result.exit_code == 0
|
|
148
|
+
assert "Alice" in result.output
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
{%- if cookiecutter.use_database %}
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class TestCleanupCommand:
|
|
155
|
+
"""Tests for the cleanup command."""
|
|
156
|
+
|
|
157
|
+
def test_cleanup_dry_run(self):
|
|
158
|
+
"""Test cleanup command with --dry-run."""
|
|
159
|
+
from app.commands.cleanup import cleanup
|
|
160
|
+
|
|
161
|
+
runner = CliRunner()
|
|
162
|
+
result = runner.invoke(cleanup, ["--dry-run"])
|
|
163
|
+
assert result.exit_code == 0
|
|
164
|
+
assert "[DRY RUN]" in result.output
|
|
165
|
+
|
|
166
|
+
def test_cleanup_with_days_option(self):
|
|
167
|
+
"""Test cleanup command with --days option."""
|
|
168
|
+
from app.commands.cleanup import cleanup
|
|
169
|
+
|
|
170
|
+
runner = CliRunner()
|
|
171
|
+
result = runner.invoke(cleanup, ["--dry-run", "--days", "7"])
|
|
172
|
+
assert result.exit_code == 0
|
|
173
|
+
{%- endif %}
|