gapguide-api / apps /resources /tests /test_resources.py
arifRB's picture
Deploy GapGuide backend (Docker)
ffd36e0 verified
Raw
History Blame Contribute Delete
9.11 kB
import pytest
from django.contrib.auth import get_user_model
from django.contrib.admin.sites import AdminSite
from django.contrib.messages.storage.fallback import FallbackStorage
from django.test import RequestFactory
from rest_framework import status
from rest_framework.test import APIClient
from apps.resources.admin import ResourceAdmin, ResourceAdminForm
from apps.resources.models import Resource, ResourceCheckpoint, SkillResource
from apps.skills.models import Skill
def _admin_request(method='post', path='/admin/'):
"""Build a RequestFactory request with messages storage wired up so
admin views that call self.message_user don't explode."""
request = getattr(RequestFactory(), method)(path)
setattr(request, 'session', {})
setattr(request, '_messages', FallbackStorage(request))
return request
User = get_user_model()
pytestmark = pytest.mark.django_db
@pytest.fixture
def user():
return User.objects.create_user(
username='r@x.com', email='r@x.com', password='pw', name='R',
)
@pytest.fixture
def auth_client(user):
c = APIClient()
c.force_authenticate(user=user)
return c
@pytest.fixture
def python_skill():
return Skill.objects.create(skill_name='Python', category='Programming', difficulty_level='BEGINNER')
@pytest.fixture
def sql_skill():
return Skill.objects.create(skill_name='SQL', category='Database', difficulty_level='BEGINNER')
@pytest.fixture
def resource(python_skill):
r = Resource.objects.create(
title='Python for Everybody',
provider='Coursera',
url='https://coursera.org/python-for-everybody',
difficulty_level='BEGINNER',
duration=1200,
type='COURSE',
rating=4.7,
)
SkillResource.objects.create(skill=python_skill, resource=r, relevance_score=0.95)
return r
class TestResourceAPI:
def test_list_requires_auth(self):
assert APIClient().get('/api/resources/').status_code == status.HTTP_401_UNAUTHORIZED
def test_list_resources(self, auth_client, resource):
response = auth_client.get('/api/resources/')
assert response.status_code == status.HTTP_200_OK
assert response.data['count'] == 1
results = response.data['results']
assert results[0]['title'] == 'Python for Everybody'
assert results[0]['has_checkpoints'] is False
def test_filter_by_skill(self, auth_client, resource, python_skill, sql_skill):
# Another resource mapped to SQL only.
other = Resource.objects.create(
title='SQL Tutorial', provider='W3Schools',
url='https://w3schools.com/sql',
difficulty_level='BEGINNER', duration=60, type='DOCS',
)
SkillResource.objects.create(skill=sql_skill, resource=other, relevance_score=0.8)
response = auth_client.get(f'/api/resources/?skill={python_skill.id}')
assert response.status_code == status.HTTP_200_OK
assert response.data['count'] == 1
assert response.data['results'][0]['id'] == resource.id
def test_filter_by_type_and_level(self, auth_client, resource):
Resource.objects.create(
title='Python Basics Video', provider='YouTube',
url='https://youtube.com/p1',
difficulty_level='ADVANCED', duration=30, type='VIDEO',
)
r1 = auth_client.get('/api/resources/?type=COURSE')
assert r1.data['count'] == 1 and r1.data['results'][0]['id'] == resource.id
r2 = auth_client.get('/api/resources/?difficulty_level=ADVANCED')
assert r2.data['count'] == 1 and r2.data['results'][0]['title'] == 'Python Basics Video'
def test_detail_includes_checkpoints_and_skills(self, auth_client, resource, python_skill):
ResourceCheckpoint.objects.create(resource=resource, order_index=1, title='Intro')
ResourceCheckpoint.objects.create(resource=resource, order_index=2, title='Variables')
response = auth_client.get(f'/api/resources/{resource.id}/')
assert response.status_code == status.HTTP_200_OK
assert len(response.data['checkpoints']) == 2
assert response.data['checkpoints'][0]['order_index'] == 1
assert len(response.data['skill_mappings']) == 1
assert response.data['skill_mappings'][0]['skill']['skill_name'] == 'Python'
assert response.data['skill_mappings'][0]['relevance_score'] == 0.95
class TestModelConstraints:
def test_url_unique(self, resource):
with pytest.raises(Exception):
Resource.objects.create(
title='Dup', provider='X', url=resource.url,
difficulty_level='BEGINNER', duration=10, type='DOCS',
)
def test_checkpoint_order_unique_per_resource(self, resource):
ResourceCheckpoint.objects.create(resource=resource, order_index=1, title='A')
with pytest.raises(Exception):
ResourceCheckpoint.objects.create(resource=resource, order_index=1, title='B')
def test_same_order_ok_across_resources(self, python_skill):
r1 = Resource.objects.create(title='R1', provider='P', url='https://a/x',
difficulty_level='BEGINNER', duration=10, type='DOCS')
r2 = Resource.objects.create(title='R2', provider='P', url='https://a/y',
difficulty_level='BEGINNER', duration=10, type='DOCS')
ResourceCheckpoint.objects.create(resource=r1, order_index=1, title='X')
ResourceCheckpoint.objects.create(resource=r2, order_index=1, title='Y')
assert ResourceCheckpoint.objects.count() == 2
class TestResourceAdminBulkPaste:
def test_bulk_paste_appends_checkpoints(self, resource):
site = AdminSite()
admin = ResourceAdmin(Resource, site)
request = _admin_request()
request.user = User.objects.create_superuser(
username='admin@x.com', email='admin@x.com', password='pw',
)
form = ResourceAdminForm(
instance=resource,
data={
'title': resource.title, 'provider': resource.provider,
'url': resource.url, 'difficulty_level': resource.difficulty_level,
'duration': resource.duration, 'type': resource.type,
'rating': resource.rating,
'bulk_checkpoints': 'Module 1: Intro\n\nModule 2: Variables\n \nModule 3: Loops',
},
)
assert form.is_valid(), form.errors
admin.save_model(request, resource, form, change=True)
cps = list(resource.checkpoints.order_by('order_index'))
assert [c.title for c in cps] == [
'Module 1: Intro', 'Module 2: Variables', 'Module 3: Loops',
]
assert [c.order_index for c in cps] == [1, 2, 3]
assert all(c.source == 'manual' for c in cps)
def test_bulk_paste_refused_when_checkpoints_exist(self, resource):
"""Re-pasting onto a resource that already has checkpoints is
rejected — admins must edit the inline rows instead. Prevents
silent duplication and orphaning of UserCheckpointProgress."""
ResourceCheckpoint.objects.create(resource=resource, order_index=1, title='Existing')
site = AdminSite()
admin = ResourceAdmin(Resource, site)
request = _admin_request()
request.user = User.objects.create_superuser(
username='admin2@x.com', email='admin2@x.com', password='pw',
)
form = ResourceAdminForm(
instance=resource,
data={
'title': resource.title, 'provider': resource.provider,
'url': resource.url, 'difficulty_level': resource.difficulty_level,
'duration': resource.duration, 'type': resource.type,
'rating': resource.rating,
'bulk_checkpoints': 'New A\nNew B',
},
)
assert form.is_valid(), form.errors
admin.save_model(request, resource, form, change=True)
# Only the pre-existing checkpoint remains — the paste was rejected.
orders = list(resource.checkpoints.order_by('order_index').values_list('order_index', 'title'))
assert orders == [(1, 'Existing')]
def test_bulk_paste_empty_noop(self, resource):
site = AdminSite()
admin = ResourceAdmin(Resource, site)
request = _admin_request()
request.user = User.objects.create_superuser(
username='admin3@x.com', email='admin3@x.com', password='pw',
)
form = ResourceAdminForm(
instance=resource,
data={
'title': resource.title, 'provider': resource.provider,
'url': resource.url, 'difficulty_level': resource.difficulty_level,
'duration': resource.duration, 'type': resource.type,
'rating': resource.rating,
'bulk_checkpoints': ' \n\n ',
},
)
assert form.is_valid(), form.errors
admin.save_model(request, resource, form, change=True)
assert resource.checkpoints.count() == 0