@smicolon/ai-kit 0.1.0 → 0.2.0
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.
- package/.claude-plugin/CLAUDE.md +7 -0
- package/.claude-plugin/marketplace.json +373 -0
- package/README.md +26 -16
- package/dist/index.js +146 -38
- package/package.json +4 -3
- package/packs/architect/CHANGELOG.md +17 -0
- package/packs/architect/README.md +58 -0
- package/packs/architect/agents/system-architect.md +768 -0
- package/packs/architect/commands/diagram-create.md +300 -0
- package/packs/better-auth/.claude-plugin/plugin.json +14 -0
- package/packs/better-auth/.mcp.json +14 -0
- package/packs/better-auth/CHANGELOG.md +26 -0
- package/packs/better-auth/README.md +125 -0
- package/packs/better-auth/agents/auth-architect.md +278 -0
- package/packs/better-auth/commands/auth-provider-add.md +265 -0
- package/packs/better-auth/commands/auth-setup.md +298 -0
- package/packs/better-auth/skills/auth-security/SKILL.md +425 -0
- package/packs/better-auth/skills/better-auth-patterns/SKILL.md +455 -0
- package/packs/dev-loop/.claude-plugin/plugin.json +10 -0
- package/packs/dev-loop/CHANGELOG.md +69 -0
- package/packs/dev-loop/README.md +155 -0
- package/packs/dev-loop/commands/cancel-dev.md +21 -0
- package/packs/dev-loop/commands/dev-loop.md +72 -0
- package/packs/dev-loop/commands/dev-plan.md +351 -0
- package/packs/dev-loop/hooks/hooks.json +15 -0
- package/packs/dev-loop/hooks/stop-hook.sh +178 -0
- package/packs/dev-loop/scripts/setup-dev-loop.sh +194 -0
- package/packs/dev-loop/skills/tdd-planner/SKILL.md +249 -0
- package/packs/dev-loop/skills/tdd-planner/references/framework-patterns.md +874 -0
- package/packs/dev-loop/skills/tdd-planner/references/good-example.md +260 -0
- package/packs/dev-loop/skills/tdd-planner/references/plan-template.md +275 -0
- package/packs/django/CHANGELOG.md +39 -0
- package/packs/django/README.md +92 -0
- package/packs/django/agents/django-architect.md +182 -0
- package/packs/django/agents/django-builder.md +250 -0
- package/packs/django/agents/django-feature-based.md +420 -0
- package/packs/django/agents/django-reviewer.md +253 -0
- package/packs/django/agents/django-tester.md +230 -0
- package/packs/django/commands/api-endpoint.md +285 -0
- package/packs/django/commands/model-create.md +178 -0
- package/packs/django/commands/test-generate.md +325 -0
- package/packs/django/rules/migrations.md +138 -0
- package/packs/django/rules/models.md +167 -0
- package/packs/django/rules/serializers.md +126 -0
- package/packs/django/rules/services.md +131 -0
- package/packs/django/rules/tests.md +140 -0
- package/packs/django/rules/views.md +102 -0
- package/packs/django/skills/import-convention-enforcer/SKILL.md +226 -0
- package/packs/django/skills/import-convention-enforcer/patterns/django-imports.md +343 -0
- package/packs/django/skills/migration-safety-checker/SKILL.md +375 -0
- package/packs/django/skills/model-entity-validator/SKILL.md +298 -0
- package/packs/django/skills/performance-optimizer/SKILL.md +447 -0
- package/packs/django/skills/red-phase-verifier/SKILL.md +180 -0
- package/packs/django/skills/security-first-validator/SKILL.md +435 -0
- package/packs/django/skills/test-coverage-advisor/SKILL.md +394 -0
- package/packs/django/skills/test-validity-checker/SKILL.md +194 -0
- package/packs/failure-log/.claude-plugin/plugin.json +14 -0
- package/packs/failure-log/CHANGELOG.md +20 -0
- package/packs/failure-log/README.md +168 -0
- package/packs/failure-log/commands/failure-add.md +106 -0
- package/packs/failure-log/commands/failure-list.md +89 -0
- package/packs/failure-log/hooks/hooks.json +16 -0
- package/packs/failure-log/hooks/scripts/inject-failures.sh +64 -0
- package/packs/failure-log/skills/failure-log-manager/SKILL.md +164 -0
- package/packs/flutter/.claude-plugin/plugin.json +10 -0
- package/packs/flutter/CHANGELOG.md +19 -0
- package/packs/flutter/README.md +170 -0
- package/packs/flutter/agents/flutter-architect.md +166 -0
- package/packs/flutter/agents/flutter-builder.md +303 -0
- package/packs/flutter/agents/release-manager.md +355 -0
- package/packs/flutter/commands/fastlane-setup.md +188 -0
- package/packs/flutter/commands/flutter-build.md +90 -0
- package/packs/flutter/commands/flutter-deploy.md +133 -0
- package/packs/flutter/commands/flutter-test.md +117 -0
- package/packs/flutter/commands/signing-setup.md +209 -0
- package/packs/flutter/hooks/hooks.json +17 -0
- package/packs/flutter/skills/fastlane-knowledge/SKILL.md +193 -0
- package/packs/flutter/skills/flutter-architecture/SKILL.md +127 -0
- package/packs/flutter/skills/store-publishing/SKILL.md +163 -0
- package/packs/hono/.claude-plugin/plugin.json +19 -0
- package/packs/hono/CHANGELOG.md +19 -0
- package/packs/hono/README.md +143 -0
- package/packs/hono/agents/hono-architect.md +240 -0
- package/packs/hono/agents/hono-builder.md +285 -0
- package/packs/hono/agents/hono-reviewer.md +279 -0
- package/packs/hono/agents/hono-tester.md +346 -0
- package/packs/hono/commands/middleware-create.md +223 -0
- package/packs/hono/commands/project-init.md +306 -0
- package/packs/hono/commands/route-create.md +153 -0
- package/packs/hono/commands/rpc-client.md +263 -0
- package/packs/hono/hooks/hooks.json +4 -0
- package/packs/hono/skills/cloudflare-bindings/SKILL.md +408 -0
- package/packs/hono/skills/hono-patterns/SKILL.md +309 -0
- package/packs/hono/skills/rpc-typesafe/SKILL.md +388 -0
- package/packs/hono/skills/zod-validation/SKILL.md +332 -0
- package/packs/nestjs/CHANGELOG.md +29 -0
- package/packs/nestjs/README.md +75 -0
- package/packs/nestjs/agents/nestjs-architect.md +402 -0
- package/packs/nestjs/agents/nestjs-builder.md +301 -0
- package/packs/nestjs/agents/nestjs-tester.md +437 -0
- package/packs/nestjs/commands/module-create.md +369 -0
- package/packs/nestjs/rules/controllers.md +92 -0
- package/packs/nestjs/rules/dto.md +124 -0
- package/packs/nestjs/rules/entities.md +102 -0
- package/packs/nestjs/rules/services.md +106 -0
- package/packs/nestjs/skills/barrel-export-manager/SKILL.md +389 -0
- package/packs/nestjs/skills/import-convention-enforcer/SKILL.md +365 -0
- package/packs/nextjs/CHANGELOG.md +36 -0
- package/packs/nextjs/README.md +76 -0
- package/packs/nextjs/agents/frontend-tester.md +680 -0
- package/packs/nextjs/agents/frontend-visual.md +820 -0
- package/packs/nextjs/agents/nextjs-architect.md +331 -0
- package/packs/nextjs/agents/nextjs-modular.md +433 -0
- package/packs/nextjs/commands/component-create.md +398 -0
- package/packs/nextjs/rules/api-routes.md +129 -0
- package/packs/nextjs/rules/components.md +106 -0
- package/packs/nextjs/rules/hooks.md +132 -0
- package/packs/nextjs/skills/accessibility-validator/SKILL.md +445 -0
- package/packs/nextjs/skills/import-convention-enforcer/SKILL.md +399 -0
- package/packs/nextjs/skills/react-form-validator/SKILL.md +569 -0
- package/packs/nuxtjs/CHANGELOG.md +30 -0
- package/packs/nuxtjs/README.md +56 -0
- package/packs/nuxtjs/agents/frontend-tester.md +680 -0
- package/packs/nuxtjs/agents/frontend-visual.md +820 -0
- package/packs/nuxtjs/agents/nuxtjs-architect.md +537 -0
- package/packs/nuxtjs/commands/component-create.md +223 -0
- package/packs/nuxtjs/rules/components.md +101 -0
- package/packs/nuxtjs/rules/composables.md +118 -0
- package/packs/nuxtjs/rules/server-routes.md +127 -0
- package/packs/nuxtjs/skills/accessibility-validator/SKILL.md +183 -0
- package/packs/nuxtjs/skills/import-convention-enforcer/SKILL.md +196 -0
- package/packs/nuxtjs/skills/veevalidate-form-validator/SKILL.md +190 -0
- package/packs/onboard/CHANGELOG.md +22 -0
- package/packs/onboard/README.md +103 -0
- package/packs/onboard/agents/onboard-guide.md +118 -0
- package/packs/onboard/commands/onboard.md +313 -0
- package/packs/onboard/skills/onboard-context-provider/SKILL.md +98 -0
- package/packs/tanstack-router/.claude-plugin/plugin.json +14 -0
- package/packs/tanstack-router/CHANGELOG.md +30 -0
- package/packs/tanstack-router/README.md +113 -0
- package/packs/tanstack-router/agents/tanstack-architect.md +173 -0
- package/packs/tanstack-router/agents/tanstack-builder.md +360 -0
- package/packs/tanstack-router/agents/tanstack-tester.md +454 -0
- package/packs/tanstack-router/commands/form-create.md +313 -0
- package/packs/tanstack-router/commands/query-create.md +263 -0
- package/packs/tanstack-router/commands/route-create.md +190 -0
- package/packs/tanstack-router/commands/table-create.md +413 -0
- package/packs/tanstack-router/skills/ai-patterns/SKILL.md +370 -0
- package/packs/tanstack-router/skills/db-patterns/SKILL.md +346 -0
- package/packs/tanstack-router/skills/devtools-patterns/SKILL.md +415 -0
- package/packs/tanstack-router/skills/form-patterns/SKILL.md +425 -0
- package/packs/tanstack-router/skills/pacer-patterns/SKILL.md +341 -0
- package/packs/tanstack-router/skills/query-patterns/SKILL.md +359 -0
- package/packs/tanstack-router/skills/router-patterns/SKILL.md +285 -0
- package/packs/tanstack-router/skills/store-patterns/SKILL.md +351 -0
- package/packs/tanstack-router/skills/table-patterns/SKILL.md +531 -0
- package/packs/tanstack-router/skills/tanstack-conventions/SKILL.md +428 -0
- package/packs/tanstack-router/skills/virtual-patterns/SKILL.md +490 -0
- package/packs/worktree/.claude-plugin/plugin.json +19 -0
- package/packs/worktree/CHANGELOG.md +24 -0
- package/packs/worktree/README.md +110 -0
- package/packs/worktree/commands/wt.md +73 -0
- package/packs/worktree/scripts/wt.sh +396 -0
- package/packs/worktree/skills/worktree-manager/SKILL.md +68 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "**/serializers.py"
|
|
4
|
+
- "**/serializers/*.py"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Django Serializer Standards
|
|
8
|
+
|
|
9
|
+
## Structure
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
import users.models as _users_models
|
|
13
|
+
from rest_framework import serializers
|
|
14
|
+
|
|
15
|
+
class UserSerializer(serializers.ModelSerializer):
|
|
16
|
+
"""Read serializer for User."""
|
|
17
|
+
|
|
18
|
+
class Meta:
|
|
19
|
+
model = _users_models.User
|
|
20
|
+
fields = ['id', 'email', 'first_name', 'last_name', 'created_at']
|
|
21
|
+
read_only_fields = ['id', 'created_at']
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class CreateUserSerializer(serializers.Serializer):
|
|
25
|
+
"""Write serializer for creating User."""
|
|
26
|
+
|
|
27
|
+
email = serializers.EmailField()
|
|
28
|
+
password = serializers.CharField(write_only=True, min_length=8)
|
|
29
|
+
first_name = serializers.CharField(max_length=100)
|
|
30
|
+
|
|
31
|
+
def validate_email(self, value):
|
|
32
|
+
if _users_models.User.objects.filter(email=value).exists():
|
|
33
|
+
raise serializers.ValidationError("Email already exists")
|
|
34
|
+
return value.lower()
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Import Pattern
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
import users.models as _users_models
|
|
41
|
+
from rest_framework import serializers
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Rules
|
|
45
|
+
|
|
46
|
+
- Separate read/write serializers when needed
|
|
47
|
+
- All validation in serializer, not view
|
|
48
|
+
- Use `validate_<field>` for field-level validation
|
|
49
|
+
- Use `validate` for cross-field validation
|
|
50
|
+
- Never expose sensitive fields (password, tokens)
|
|
51
|
+
|
|
52
|
+
## Naming Convention
|
|
53
|
+
|
|
54
|
+
- Read serializers: `{Model}Serializer`
|
|
55
|
+
- Create serializers: `Create{Model}Serializer`
|
|
56
|
+
- Update serializers: `Update{Model}Serializer`
|
|
57
|
+
- List serializers (minimal): `{Model}ListSerializer`
|
|
58
|
+
|
|
59
|
+
## Validation Examples
|
|
60
|
+
|
|
61
|
+
### Field-Level Validation
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
class CreateUserSerializer(serializers.Serializer):
|
|
65
|
+
email = serializers.EmailField()
|
|
66
|
+
password = serializers.CharField(write_only=True)
|
|
67
|
+
|
|
68
|
+
def validate_email(self, value):
|
|
69
|
+
"""Normalize and validate email."""
|
|
70
|
+
value = value.lower().strip()
|
|
71
|
+
if _users_models.User.objects.filter(email=value).exists():
|
|
72
|
+
raise serializers.ValidationError("Email already exists")
|
|
73
|
+
return value
|
|
74
|
+
|
|
75
|
+
def validate_password(self, value):
|
|
76
|
+
"""Validate password strength."""
|
|
77
|
+
if len(value) < 8:
|
|
78
|
+
raise serializers.ValidationError("Password must be at least 8 characters")
|
|
79
|
+
if not any(c.isdigit() for c in value):
|
|
80
|
+
raise serializers.ValidationError("Password must contain a digit")
|
|
81
|
+
return value
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Cross-Field Validation
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
class PasswordChangeSerializer(serializers.Serializer):
|
|
88
|
+
old_password = serializers.CharField(write_only=True)
|
|
89
|
+
new_password = serializers.CharField(write_only=True)
|
|
90
|
+
confirm_password = serializers.CharField(write_only=True)
|
|
91
|
+
|
|
92
|
+
def validate(self, data):
|
|
93
|
+
if data['new_password'] != data['confirm_password']:
|
|
94
|
+
raise serializers.ValidationError({
|
|
95
|
+
"confirm_password": "Passwords do not match"
|
|
96
|
+
})
|
|
97
|
+
if data['old_password'] == data['new_password']:
|
|
98
|
+
raise serializers.ValidationError({
|
|
99
|
+
"new_password": "New password must be different"
|
|
100
|
+
})
|
|
101
|
+
return data
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Nested Serializers
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
class OrderItemSerializer(serializers.ModelSerializer):
|
|
108
|
+
class Meta:
|
|
109
|
+
model = _models.OrderItem
|
|
110
|
+
fields = ['id', 'product', 'quantity', 'price']
|
|
111
|
+
|
|
112
|
+
class OrderSerializer(serializers.ModelSerializer):
|
|
113
|
+
items = OrderItemSerializer(many=True, read_only=True)
|
|
114
|
+
total = serializers.DecimalField(max_digits=10, decimal_places=2, read_only=True)
|
|
115
|
+
|
|
116
|
+
class Meta:
|
|
117
|
+
model = _models.Order
|
|
118
|
+
fields = ['id', 'user', 'items', 'total', 'created_at']
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Forbidden Patterns
|
|
122
|
+
|
|
123
|
+
- Business logic in serializers (use services)
|
|
124
|
+
- Direct database queries (use services)
|
|
125
|
+
- Exposing sensitive fields
|
|
126
|
+
- Using ModelSerializer for write operations with complex logic
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "**/services.py"
|
|
4
|
+
- "**/services/*.py"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Django Service Layer Standards
|
|
8
|
+
|
|
9
|
+
## Purpose
|
|
10
|
+
|
|
11
|
+
Services contain business logic. Views delegate to services.
|
|
12
|
+
|
|
13
|
+
## Structure
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
import users.models as _users_models
|
|
17
|
+
from django.db import transaction
|
|
18
|
+
|
|
19
|
+
class UserService:
|
|
20
|
+
"""
|
|
21
|
+
Business logic for User operations.
|
|
22
|
+
All methods are static or class methods.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
@staticmethod
|
|
26
|
+
@transaction.atomic
|
|
27
|
+
def create_user(data: dict) -> _users_models.User:
|
|
28
|
+
"""
|
|
29
|
+
Create a new user with validation.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
data: Validated user data from serializer
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Created User instance
|
|
36
|
+
|
|
37
|
+
Raises:
|
|
38
|
+
ValidationError: If email already exists
|
|
39
|
+
"""
|
|
40
|
+
# Business logic here
|
|
41
|
+
user = _users_models.User.objects.create(**data)
|
|
42
|
+
# Side effects (emails, notifications)
|
|
43
|
+
return user
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Requirements
|
|
47
|
+
|
|
48
|
+
- All database operations use `@transaction.atomic`
|
|
49
|
+
- Methods have type hints
|
|
50
|
+
- Docstrings explain purpose, args, returns, raises
|
|
51
|
+
- Static methods preferred (no instance state)
|
|
52
|
+
- Import pattern: `import app.models as _models`
|
|
53
|
+
|
|
54
|
+
## Forbidden Patterns
|
|
55
|
+
|
|
56
|
+
- Service methods calling views
|
|
57
|
+
- Circular service dependencies
|
|
58
|
+
- HTTP-specific code (request, response)
|
|
59
|
+
- Direct print statements (use logging)
|
|
60
|
+
- Catching broad exceptions without re-raising
|
|
61
|
+
|
|
62
|
+
## Method Patterns
|
|
63
|
+
|
|
64
|
+
### Query Methods
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
@staticmethod
|
|
68
|
+
def get_by_id(user_id: uuid.UUID) -> _users_models.User:
|
|
69
|
+
"""Get user by ID or raise DoesNotExist."""
|
|
70
|
+
return _users_models.User.objects.get(id=user_id, is_deleted=False)
|
|
71
|
+
|
|
72
|
+
@staticmethod
|
|
73
|
+
def get_active_users() -> QuerySet[_users_models.User]:
|
|
74
|
+
"""Get all active users."""
|
|
75
|
+
return _users_models.User.objects.filter(is_active=True, is_deleted=False)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Mutation Methods
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
@staticmethod
|
|
82
|
+
@transaction.atomic
|
|
83
|
+
def update_user(user_id: uuid.UUID, data: dict) -> _users_models.User:
|
|
84
|
+
"""Update user fields."""
|
|
85
|
+
user = _users_models.User.objects.select_for_update().get(id=user_id)
|
|
86
|
+
for key, value in data.items():
|
|
87
|
+
setattr(user, key, value)
|
|
88
|
+
user.save()
|
|
89
|
+
return user
|
|
90
|
+
|
|
91
|
+
@staticmethod
|
|
92
|
+
@transaction.atomic
|
|
93
|
+
def soft_delete_user(user_id: uuid.UUID) -> None:
|
|
94
|
+
"""Soft delete a user."""
|
|
95
|
+
_users_models.User.objects.filter(id=user_id).update(is_deleted=True)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Error Handling
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
import logging
|
|
102
|
+
from django.core.exceptions import ValidationError
|
|
103
|
+
|
|
104
|
+
logger = logging.getLogger(__name__)
|
|
105
|
+
|
|
106
|
+
class UserService:
|
|
107
|
+
@staticmethod
|
|
108
|
+
@transaction.atomic
|
|
109
|
+
def create_user(data: dict) -> _users_models.User:
|
|
110
|
+
try:
|
|
111
|
+
user = _users_models.User.objects.create(**data)
|
|
112
|
+
logger.info(f"Created user: {user.id}")
|
|
113
|
+
return user
|
|
114
|
+
except IntegrityError as e:
|
|
115
|
+
logger.error(f"Failed to create user: {e}")
|
|
116
|
+
raise ValidationError("User with this email already exists")
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Testing Services
|
|
120
|
+
|
|
121
|
+
Services should be the primary unit test target:
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
@pytest.mark.django_db
|
|
125
|
+
class TestUserService:
|
|
126
|
+
def test_create_user_success(self):
|
|
127
|
+
data = {'email': 'test@example.com', 'password': 'secure123'}
|
|
128
|
+
user = UserService.create_user(data)
|
|
129
|
+
assert user.id is not None
|
|
130
|
+
assert user.email == 'test@example.com'
|
|
131
|
+
```
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "**/test*.py"
|
|
4
|
+
- "**/tests/**/*.py"
|
|
5
|
+
- "**/*_test.py"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Django Test Standards
|
|
9
|
+
|
|
10
|
+
## Coverage Target: 90%+
|
|
11
|
+
|
|
12
|
+
## Structure
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
import pytest
|
|
16
|
+
import users.models as _users_models
|
|
17
|
+
import users.services as _users_services
|
|
18
|
+
from rest_framework.test import APIClient
|
|
19
|
+
from rest_framework import status
|
|
20
|
+
|
|
21
|
+
@pytest.mark.django_db
|
|
22
|
+
class TestUserService:
|
|
23
|
+
"""Tests for UserService."""
|
|
24
|
+
|
|
25
|
+
def test_create_user_success(self):
|
|
26
|
+
"""Test successful user creation."""
|
|
27
|
+
data = {'email': 'test@example.com', 'password': 'secure123'}
|
|
28
|
+
|
|
29
|
+
user = _users_services.UserService.create_user(data)
|
|
30
|
+
|
|
31
|
+
assert user.id is not None
|
|
32
|
+
assert user.email == 'test@example.com'
|
|
33
|
+
assert user.created_at is not None
|
|
34
|
+
|
|
35
|
+
def test_create_user_duplicate_email(self):
|
|
36
|
+
"""Test creation fails with duplicate email."""
|
|
37
|
+
_users_models.User.objects.create(email='existing@example.com')
|
|
38
|
+
|
|
39
|
+
with pytest.raises(ValidationError) as exc:
|
|
40
|
+
_users_services.UserService.create_user({'email': 'existing@example.com'})
|
|
41
|
+
|
|
42
|
+
assert 'email' in str(exc.value)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Requirements
|
|
46
|
+
|
|
47
|
+
- Use pytest, not unittest
|
|
48
|
+
- Import pattern: `import app.models as _app_models`
|
|
49
|
+
- Minimum 2 assertions per test
|
|
50
|
+
- Test happy path AND error paths
|
|
51
|
+
- Use factories for complex data (factory_boy)
|
|
52
|
+
- Mark DB tests with `@pytest.mark.django_db`
|
|
53
|
+
|
|
54
|
+
## Test Categories
|
|
55
|
+
|
|
56
|
+
1. Unit tests (80%): Models, services, utilities
|
|
57
|
+
2. Integration tests (15%): API endpoints
|
|
58
|
+
3. Edge cases (5%): Boundaries, errors
|
|
59
|
+
|
|
60
|
+
## Forbidden Patterns
|
|
61
|
+
|
|
62
|
+
- Tests without assertions
|
|
63
|
+
- `assert True` or `assert obj` (meaningless)
|
|
64
|
+
- Shared mutable state between tests
|
|
65
|
+
- Skipped tests without explanation
|
|
66
|
+
|
|
67
|
+
## Test Naming
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
# Pattern: test_{method}_{scenario}_{expected_result}
|
|
71
|
+
def test_create_user_success(self):
|
|
72
|
+
def test_create_user_duplicate_email_raises_validation_error(self):
|
|
73
|
+
def test_get_user_not_found_raises_does_not_exist(self):
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## API Test Pattern
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
@pytest.mark.django_db
|
|
80
|
+
class TestUserAPI:
|
|
81
|
+
"""API tests for User endpoints."""
|
|
82
|
+
|
|
83
|
+
@pytest.fixture
|
|
84
|
+
def client(self):
|
|
85
|
+
return APIClient()
|
|
86
|
+
|
|
87
|
+
@pytest.fixture
|
|
88
|
+
def authenticated_client(self, user):
|
|
89
|
+
client = APIClient()
|
|
90
|
+
client.force_authenticate(user=user)
|
|
91
|
+
return client
|
|
92
|
+
|
|
93
|
+
def test_list_users_unauthenticated_returns_401(self, client):
|
|
94
|
+
response = client.get('/api/users/')
|
|
95
|
+
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
|
96
|
+
|
|
97
|
+
def test_list_users_authenticated_returns_200(self, authenticated_client):
|
|
98
|
+
response = authenticated_client.get('/api/users/')
|
|
99
|
+
assert response.status_code == status.HTTP_200_OK
|
|
100
|
+
assert 'results' in response.data
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Factory Pattern
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
import factory
|
|
107
|
+
from factory.django import DjangoModelFactory
|
|
108
|
+
import users.models as _users_models
|
|
109
|
+
|
|
110
|
+
class UserFactory(DjangoModelFactory):
|
|
111
|
+
class Meta:
|
|
112
|
+
model = _users_models.User
|
|
113
|
+
|
|
114
|
+
email = factory.Sequence(lambda n: f'user{n}@example.com')
|
|
115
|
+
first_name = factory.Faker('first_name')
|
|
116
|
+
last_name = factory.Faker('last_name')
|
|
117
|
+
is_active = True
|
|
118
|
+
|
|
119
|
+
# Usage in tests
|
|
120
|
+
def test_list_users(self, authenticated_client):
|
|
121
|
+
UserFactory.create_batch(5)
|
|
122
|
+
response = authenticated_client.get('/api/users/')
|
|
123
|
+
assert len(response.data['results']) == 5
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Fixtures
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
@pytest.fixture
|
|
130
|
+
def user():
|
|
131
|
+
return UserFactory()
|
|
132
|
+
|
|
133
|
+
@pytest.fixture
|
|
134
|
+
def admin_user():
|
|
135
|
+
return UserFactory(is_staff=True, is_superuser=True)
|
|
136
|
+
|
|
137
|
+
@pytest.fixture
|
|
138
|
+
def product(user):
|
|
139
|
+
return ProductFactory(owner=user)
|
|
140
|
+
```
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "**/views.py"
|
|
4
|
+
- "**/views/*.py"
|
|
5
|
+
- "**/viewsets.py"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Django View Standards
|
|
9
|
+
|
|
10
|
+
## Required Security
|
|
11
|
+
|
|
12
|
+
EVERY view/viewset MUST have:
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
from rest_framework.permissions import IsAuthenticated
|
|
16
|
+
from rest_framework.throttling import UserRateThrottle
|
|
17
|
+
|
|
18
|
+
class YourViewSet(viewsets.ModelViewSet):
|
|
19
|
+
# REQUIRED - No exceptions
|
|
20
|
+
permission_classes = [IsAuthenticated]
|
|
21
|
+
throttle_classes = [UserRateThrottle]
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Import Pattern
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
# CORRECT - Modular imports with aliases
|
|
28
|
+
import users.models as _users_models
|
|
29
|
+
import users.services as _users_services
|
|
30
|
+
import users.serializers as _users_serializers
|
|
31
|
+
|
|
32
|
+
# Usage
|
|
33
|
+
queryset = _users_users_models.User.objects.filter(is_deleted=False)
|
|
34
|
+
serializer_class = _users_users_serializers.UserSerializer
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## View Structure
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
class UserViewSet(viewsets.ModelViewSet):
|
|
41
|
+
permission_classes = [IsAuthenticated]
|
|
42
|
+
throttle_classes = [UserRateThrottle]
|
|
43
|
+
|
|
44
|
+
# Use service layer - no raw ORM in views
|
|
45
|
+
def create(self, request):
|
|
46
|
+
serializer = _users_serializers.CreateUserSerializer(data=request.data)
|
|
47
|
+
serializer.is_valid(raise_exception=True)
|
|
48
|
+
user = _users_users_services.UserService.create(serializer.validated_data)
|
|
49
|
+
return Response(_users_users_serializers.UserSerializer(user).data, status=201)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Forbidden Patterns
|
|
53
|
+
|
|
54
|
+
- Views without permission_classes
|
|
55
|
+
- Direct `request.data` access without serializer
|
|
56
|
+
- ORM queries in view methods (use services)
|
|
57
|
+
- `@api_view` without `@permission_classes`
|
|
58
|
+
- Hardcoded status codes (use `status.HTTP_*`)
|
|
59
|
+
|
|
60
|
+
## Response Patterns
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from rest_framework import status
|
|
64
|
+
from rest_framework.response import Response
|
|
65
|
+
|
|
66
|
+
# CORRECT
|
|
67
|
+
return Response(data, status=status.HTTP_201_CREATED)
|
|
68
|
+
return Response(status=status.HTTP_204_NO_CONTENT)
|
|
69
|
+
|
|
70
|
+
# WRONG
|
|
71
|
+
return Response(data, status=201) # Use named constants
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Error Handling
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from rest_framework.exceptions import NotFound, PermissionDenied
|
|
78
|
+
|
|
79
|
+
class UserViewSet(viewsets.ModelViewSet):
|
|
80
|
+
def retrieve(self, request, pk=None):
|
|
81
|
+
try:
|
|
82
|
+
user = _users_users_services.UserService.get_by_id(pk)
|
|
83
|
+
except _users_users_models.User.DoesNotExist:
|
|
84
|
+
raise NotFound("User not found")
|
|
85
|
+
return Response(_users_users_serializers.UserSerializer(user).data)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Pagination
|
|
89
|
+
|
|
90
|
+
Always use pagination for list views:
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
from rest_framework.pagination import PageNumberPagination
|
|
94
|
+
|
|
95
|
+
class StandardPagination(PageNumberPagination):
|
|
96
|
+
page_size = 20
|
|
97
|
+
page_size_query_param = 'page_size'
|
|
98
|
+
max_page_size = 100
|
|
99
|
+
|
|
100
|
+
class UserViewSet(viewsets.ModelViewSet):
|
|
101
|
+
pagination_class = StandardPagination
|
|
102
|
+
```
|