"""Cross-module smoke test: seed skills → seed roles → seed resources → gap report keys match recommendation keys → recommended IDs resolve through /api/resources//. Different from the unit tests: this exercises the full seed chain against the real curated YAML files, not hand-crafted fixtures. """ from io import StringIO import pytest from django.core.management import call_command from rest_framework.test import APIClient from apps.resources.models import Resource from apps.roles.models import Role, UserTargetRole from apps.skills.models import Skill, UserSkill pytestmark = pytest.mark.django_db REGISTER = '/api/auth/register/' USER_SKILLS = '/api/user-skills/' TARGET_ROLE = '/api/target-role/' ANALYSIS = '/api/analysis/' RECOMMENDATIONS = '/api/recommendations/' RESOURCES = '/api/resources/' @pytest.fixture def seeded(db): """Full curated seed — skills, roles, resources.""" call_command('seed_initial_skills', stdout=StringIO()) call_command('seed_initial_roles', stdout=StringIO()) call_command('seed_initial_resources', stdout=StringIO()) return True def _auth_client(): c = APIClient() r = c.post(REGISTER, data={ 'name': 'Cross', 'email': 'cross@x.com', 'password': 'StrongPass123!', 'password_confirm': 'StrongPass123!', }, format='json') assert r.status_code == 201, r.data c.credentials(HTTP_AUTHORIZATION=f'Bearer {r.data["access"]}') return c def test_gap_skills_match_recommendation_skills(seeded): """Every gap skill (MISSING/INSUFFICIENT) in the report appears as a key in the recommendations payload. MET skills must NOT appear.""" c = _auth_client() role = Role.objects.get(role_name='Data Scientist') c.post(TARGET_ROLE, data={'role_id': role.id}, format='json') # Give the user partial skills so some gaps exist, some are MET. python_id = Skill.objects.get(skill_name='Python').id c.post(USER_SKILLS, data={'skill_id': python_id, 'proficiency': 100}, format='json') gap_report = c.get(ANALYSIS).data rec_payload = c.get(RECOMMENDATIONS).data gap_skill_ids = {g['skill_id'] for g in gap_report['gaps'] if g['gap_type'] != 'MET'} met_skill_ids = {g['skill_id'] for g in gap_report['gaps'] if g['gap_type'] == 'MET'} rec_skill_ids = {int(k) for k in rec_payload['recommendations']} assert gap_skill_ids == rec_skill_ids assert rec_skill_ids.isdisjoint(met_skill_ids) def test_recommended_resource_ids_match_resources_endpoint(seeded): c = _auth_client() role = Role.objects.get(role_name='Data Scientist') c.post(TARGET_ROLE, data={'role_id': role.id}, format='json') rec_payload = c.get(RECOMMENDATIONS).data for skill_id, items in rec_payload['recommendations'].items(): for item in items: r = c.get(f'{RESOURCES}{item["resource_id"]}/') assert r.status_code == 200 assert r.data['id'] == item['resource_id'] assert r.data['url'] == item['url'] def test_mandatory_gap_gets_recommendations(seeded): """Python is mandatory for Data Scientist; with 0 proficiency, the recommendation payload MUST contain ≥1 Python-linked resource.""" c = _auth_client() role = Role.objects.get(role_name='Data Scientist') c.post(TARGET_ROLE, data={'role_id': role.id}, format='json') python_id = Skill.objects.get(skill_name='Python').id rec_payload = c.get(RECOMMENDATIONS).data python_recs = rec_payload['recommendations'].get(str(python_id)) \ or rec_payload['recommendations'].get(python_id) assert python_recs, 'Python gap should get ≥1 recommendation from curated catalog' def test_severity_does_not_filter_recommendations(seeded): """Low / Medium / High severity all deserve recommendations — rank, don't silently drop.""" c = _auth_client() role = Role.objects.get(role_name='Data Scientist') c.post(TARGET_ROLE, data={'role_id': role.id}, format='json') # Boost a skill just below threshold → LOW/MEDIUM severity, not 0. python_id = Skill.objects.get(skill_name='Python').id c.post(USER_SKILLS, data={'skill_id': python_id, 'proficiency': 55}, format='json') report = c.get(ANALYSIS).data python_gap = next(g for g in report['gaps'] if g['skill_id'] == python_id) assert python_gap['gap_type'] == 'INSUFFICIENT' # just below 60 threshold rec = c.get(RECOMMENDATIONS).data py_recs = rec['recommendations'].get(str(python_id)) \ or rec['recommendations'].get(python_id) assert py_recs, 'low-severity insufficient gap should still surface recs' def test_role_param_routes_to_different_role(seeded): """?role= overrides the active target role.""" c = _auth_client() ds = Role.objects.get(role_name='Data Scientist') fe = Role.objects.get(role_name='Frontend Developer') c.post(TARGET_ROLE, data={'role_id': ds.id}, format='json') r = c.get(f'{RECOMMENDATIONS}?role={fe.id}') assert r.status_code == 200 assert r.data['role_id'] == fe.id assert r.data['role_name'] == 'Frontend Developer'