"""Unit tests for the role-suggestion service + endpoint. suggest_roles ranks every active role by the user's skill fit (reusing compute_gap_report) so the UI can show "Recommended for you" above the rest of the catalog, while every role stays reachable (search / switch-track case). """ import pytest from django.contrib.auth import get_user_model from rest_framework.test import APIClient from apps.analysis.services import suggest_roles from apps.roles.models import Role, RoleSkill from apps.skills.models import Skill, UserSkill User = get_user_model() pytestmark = pytest.mark.django_db URL = '/api/role-suggestions/' @pytest.fixture def user(): return User.objects.create_user( username='sugg@example.com', email='sugg@example.com', password='password123', name='Sugg Tester', ) @pytest.fixture def auth_client(user): client = APIClient() client.force_authenticate(user=user) return client def _skill(name, category='Programming'): return Skill.objects.create( skill_name=name, category=category, difficulty_level='BEGINNER', ) def _role(name, skill_specs, industry='Tech', is_active=True): """skill_specs: list of (Skill, level, weight, mandatory).""" role = Role.objects.create( role_name=name, industry=industry, is_active=is_active, ) for skill, level, weight, mandatory in skill_specs: RoleSkill.objects.create( role=role, skill=skill, required_level=level, weight=weight, is_mandatory=mandatory, ) return role def _set_prof(user, skill, proficiency, level='INTERMEDIATE'): UserSkill.objects.update_or_create( user=user, skill=skill, defaults={'proficiency': proficiency, 'user_level': level}, ) def test_better_fit_role_ranks_higher(user): python = _skill('Python') rust = _skill('Rust') _role('Backend (Python)', [(python, 'INTERMEDIATE', 1.0, True)]) _role('Systems (Rust)', [(rust, 'INTERMEDIATE', 1.0, True)]) _set_prof(user, python, 100) # fully meets the Python role; Rust unset → 0 result = suggest_roles(user) names = [r['role_name'] for r in result['roles']] assert names.index('Backend (Python)') < names.index('Systems (Rust)') good = next(r for r in result['roles'] if r['role_name'] == 'Backend (Python)') assert good['readiness'] == 100.0 assert good['matched_skills'] == 1 assert good['total_skills'] == 1 assert good['top_missing_skills'] == [] def test_top_missing_skills_lists_unmet(user): python = _skill('Python') sql = _skill('SQL') _role('Data Role', [ (python, 'INTERMEDIATE', 1.0, True), (sql, 'INTERMEDIATE', 1.0, True), ]) _set_prof(user, python, 100) # MET; SQL missing result = suggest_roles(user) row = next(r for r in result['roles'] if r['role_name'] == 'Data Role') assert row['matched_skills'] == 1 assert row['total_skills'] == 2 assert 'SQL' in row['top_missing_skills'] assert 'Python' not in row['top_missing_skills'] def test_no_requirements_role_sinks_to_bottom(user): python = _skill('Python') _role('Empty Role', []) # no RoleSkill → no_requirements, readiness=100 (spurious) _role('Backend (Python)', [(python, 'INTERMEDIATE', 1.0, True)]) _set_prof(user, python, 50) # partial fit on the real role result = suggest_roles(user) names = [r['role_name'] for r in result['roles']] assert names[-1] == 'Empty Role' empty = next(r for r in result['roles'] if r['role_name'] == 'Empty Role') assert empty['no_requirements'] is True def test_has_skills_false_when_user_has_no_skills(user): python = _skill('Python') _role('Backend (Python)', [(python, 'INTERMEDIATE', 1.0, True)]) result = suggest_roles(user) assert result['has_skills'] is False # Roles are still returned (all low fit) so the catalog stays browsable. assert len(result['roles']) == 1 def test_has_skills_true_after_setting_a_skill(user): python = _skill('Python') _role('Backend (Python)', [(python, 'INTERMEDIATE', 1.0, True)]) _set_prof(user, python, 70) assert suggest_roles(user)['has_skills'] is True def test_low_fit_role_still_listed_and_reachable(user): """The switch-track case: a strong analyst can still see (and pick) a poorly-fitting role — it's just ranked below the good matches.""" python = _skill('Python') k8s = _skill('Kubernetes') _role('Data Analyst', [(python, 'INTERMEDIATE', 1.0, True)]) _role('DevOps Engineer', [(k8s, 'ADVANCED', 1.0, True)]) _set_prof(user, python, 80) # strong analyst, zero DevOps result = suggest_roles(user) names = [r['role_name'] for r in result['roles']] assert 'DevOps Engineer' in names # reachable despite poor fit assert names.index('Data Analyst') < names.index('DevOps Engineer') def test_inactive_roles_excluded(user): python = _skill('Python') _role('Active Role', [(python, 'INTERMEDIATE', 1.0, True)], is_active=True) _role('Inactive Role', [(python, 'INTERMEDIATE', 1.0, True)], is_active=False) names = [r['role_name'] for r in suggest_roles(user)['roles']] assert 'Active Role' in names assert 'Inactive Role' not in names def test_endpoint_returns_ranked_roles(user, auth_client): python = _skill('Python') _role('Backend (Python)', [(python, 'INTERMEDIATE', 1.0, True)]) _set_prof(user, python, 100) r = auth_client.get(URL) assert r.status_code == 200, r.data assert r.data['has_skills'] is True assert r.data['roles'][0]['role_name'] == 'Backend (Python)' def test_endpoint_requires_authentication(): r = APIClient().get(URL) assert r.status_code == 401