gapguide-api / apps /analysis /tests /test_role_suggestions.py
arifRB's picture
Deploy GapGuide backend (Docker)
ffd36e0 verified
Raw
History Blame Contribute Delete
5.73 kB
"""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