Skip to content

Commit 9cc25b5

Browse files
authored
Merge pull request #8 from tpaulshippy/flashcards
2 parents 60c58f9 + 7753011 commit 9cc25b5

37 files changed

Lines changed: 6458 additions & 178 deletions

OPENAPI_CONTRACT_TESTING.md

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# OpenAPI Contract Testing
2+
3+
This document describes the OpenAPI-based contract testing setup to catch frontend/backend API mismatches early.
4+
5+
## Overview
6+
7+
The system uses OpenAPI schema generated from Django REST Framework to:
8+
1. **Backend**: Validate API responses match expected contract
9+
2. **Frontend**: Provide realistic mock responses for testing
10+
11+
## Files Created
12+
13+
### Backend
14+
- `back/requirements.txt` - Added `drf-spectacular`
15+
- `back/server/settings.py` - Configured OpenAPI schema generation
16+
- `back/server/urls.py` - Added `/api/schema` endpoint
17+
- `back/bots/tests/test_flashcard_api.py` - API contract tests (12 tests)
18+
19+
### Frontend
20+
- `front/api/schema.yaml` - OpenAPI schema file
21+
- `front/__mocks__/handlers.ts` - MSW handlers for API mocking
22+
- `front/__tests__/api/flashcards.test.ts` - Frontend API tests (11 tests)
23+
24+
### Scripts
25+
- `scripts/generate_openapi_schema.sh` - Regenerate schema after API changes
26+
- `.git/hooks/pre-commit` - Pre-commit hook to run tests
27+
28+
## Running Tests
29+
30+
### Backend Tests
31+
```bash
32+
cd back
33+
venv/bin/python -m pytest bots/tests/test_flashcard_api.py -v
34+
```
35+
36+
### Frontend Tests
37+
```bash
38+
cd front
39+
npm test -- --testPathPattern="__tests__/api/" --ci
40+
```
41+
42+
## Schema Regeneration
43+
44+
After making changes to serializers, viewsets, or models that affect the API:
45+
46+
```bash
47+
./scripts/generate_openapi_schema.sh
48+
```
49+
50+
Then:
51+
1. Review `front/api/schema.yaml` for changes
52+
2. Update `front/__mocks__/handlers.ts` if needed
53+
3. Run tests to verify nothing broke
54+
55+
## Pre-commit Hook
56+
57+
To enable the pre-commit hook that runs API tests:
58+
59+
```bash
60+
cp .git/hooks/pre-commit .git/hooks/pre-commit.sample
61+
chmod +x .git/hooks/pre-commit
62+
```
63+
64+
## What These Tests Catch
65+
66+
The test suite specifically checks for the issues that occurred in commit 94d2cf5:
67+
68+
1. **Pagination Format**: Backend returns `{ results: [], count }` - frontend must handle this
69+
2. **Field Names**: `card_count` vs other field names
70+
3. **Required Fields**: `profile`, `chat` on deck creation
71+
4. **UUID Lookup**: Support for lookup by `deck_id` and `flashcard_id` (UUID)
72+
73+
## API Schema Endpoint
74+
75+
The OpenAPI schema is available at:
76+
- `/api/schema` - JSON schema
77+
- `/api/docs` - Swagger UI
78+
79+
## Adding New API Endpoints
80+
81+
When adding new endpoints:
82+
1. Add serializer and viewset as usual
83+
2. Run `scripts/generate_openapi_schema.sh`
84+
3. Add mock handlers in `front/__mocks__/handlers.ts`
85+
4. Add tests in appropriate test file

back/bots/admin.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from django.contrib import admin
2-
from .models import Chat, Message, Profile, Bot, UserAccount, UsageLimitHit, AiModel, Device, RevenueCatWebhookEvent
2+
from .models import Chat, Message, Profile, Bot, UserAccount, UsageLimitHit, AiModel, Device, RevenueCatWebhookEvent, Deck, Flashcard
33
from django.contrib.auth.models import User
44
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
55

@@ -61,6 +61,20 @@ def get_readonly_fields(self, request, obj=None):
6161
def get_list_display(self, request):
6262
return ['created_at', 'raw_event'] + list(super().get_list_display(request))
6363

64+
class DeckAdmin(admin.ModelAdmin):
65+
def get_readonly_fields(self, request, obj=None):
66+
return ['deck_id', 'created_at', 'updated_at']
67+
68+
def get_list_display(self, request):
69+
return ['deck_id', 'name', 'profile', 'chat', 'card_count', 'created_at', 'updated_at'] + list(super().get_list_display(request))
70+
71+
class FlashcardAdmin(admin.ModelAdmin):
72+
def get_readonly_fields(self, request, obj=None):
73+
return ['flashcard_id', 'created_at', 'updated_at']
74+
75+
def get_list_display(self, request):
76+
return ['flashcard_id', 'deck', 'front', 'order', 'created_at', 'updated_at'] + list(super().get_list_display(request))
77+
6478
class UserAccountAdmin(admin.ModelAdmin):
6579
def get_list_display(self, request):
6680
return ['user_id', 'pin', 'subscription_level', 'timezone'] + list(super().get_list_display(request))
@@ -78,5 +92,7 @@ class UserAdmin(BaseUserAdmin):
7892
admin.site.register(AiModel, AiModelAdmin)
7993
admin.site.register(UsageLimitHit, UsageLimitHitAdmin)
8094
admin.site.register(RevenueCatWebhookEvent, RevenueCatWebhookEventAdmin)
95+
admin.site.register(Deck, DeckAdmin)
96+
admin.site.register(Flashcard, FlashcardAdmin)
8197
admin.site.unregister(User)
8298
admin.site.register(User, UserAdmin)
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Generated by Django 6.0.3 on 2026-04-16 05:30
2+
3+
import django.db.models.deletion
4+
import uuid
5+
from django.db import migrations, models
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
dependencies = [
11+
('bots', '0034_profile_oauth_email'),
12+
]
13+
14+
operations = [
15+
migrations.CreateModel(
16+
name='Deck',
17+
fields=[
18+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
19+
('deck_id', models.UUIDField(default=uuid.uuid4, unique=True)),
20+
('name', models.CharField(max_length=255)),
21+
('description', models.TextField(blank=True, default='')),
22+
('created_at', models.DateTimeField(auto_now_add=True)),
23+
('updated_at', models.DateTimeField(auto_now=True)),
24+
('chat', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='decks', to='bots.chat')),
25+
('profile', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='decks', to='bots.profile')),
26+
],
27+
),
28+
migrations.CreateModel(
29+
name='Flashcard',
30+
fields=[
31+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
32+
('flashcard_id', models.UUIDField(default=uuid.uuid4, unique=True)),
33+
('front', models.TextField()),
34+
('back', models.TextField()),
35+
('order', models.PositiveIntegerField(default=0)),
36+
('created_at', models.DateTimeField(auto_now_add=True)),
37+
('updated_at', models.DateTimeField(auto_now=True)),
38+
('deck', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='flashcards', to='bots.deck')),
39+
],
40+
options={
41+
'indexes': [models.Index(fields=['deck', 'order'], name='bots_flashc_deck_id_a95ed4_idx')],
42+
'constraints': [models.UniqueConstraint(fields=('deck', 'order'), name='unique_flashcard_order_per_deck')],
43+
},
44+
),
45+
]

back/bots/models/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
from .usage_limit_hit import UsageLimitHit
77
from .ai_model import AiModel
88
from .device import Device
9+
from .deck import Deck
10+
from .flashcard import Flashcard
911

1012
__all__ = [
1113
'Chat',
@@ -16,5 +18,7 @@
1618
'UsageLimitHit',
1719
'AiModel',
1820
'Device',
19-
'RevenueCatWebhookEvent'
21+
'RevenueCatWebhookEvent',
22+
'Deck',
23+
'Flashcard',
2024
]

0 commit comments

Comments
 (0)