Spaces:
Sleeping
Sleeping
| """Cross-module smoke test: seed skills β seed roles β seed resources β | |
| gap report keys match recommendation keys β recommended IDs resolve through | |
| /api/resources/<id>/. | |
| 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/' | |
| 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=<id> 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' | |