Spaces:
Runtime error
Runtime error
| """Stress / adversarial regression tests for suggest_roles (6ac0e63). | |
| Pins the ranking contract (no_requirements last, readiness desc, role_name asc), | |
| the top_missing_skills cap + severity ordering, matched/total counts at the | |
| threshold boundary, and behaviour at catalog scale and under hostile input. | |
| Complements the behavioural cases in test_role_suggestions.py. | |
| """ | |
| 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/' | |
| def user(): | |
| return User.objects.create_user( | |
| username='sgstress@example.com', email='sgstress@example.com', | |
| password='password123', name='SuggStress', | |
| ) | |
| 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 _sort_key(row): | |
| return (row['no_requirements'], -row['readiness'], row['role_name']) | |
| class TestScale: | |
| def test_fifty_roles_ranked_without_error(self, user): | |
| """50 active roles: a mix of fully-met and unmet fit, returned in the | |
| documented order, count exact, no error.""" | |
| core = _skill('Core') | |
| _set_prof(user, core, 100) | |
| for i in range(25): # met β readiness 100 | |
| _role(f'met-{i:02d}', [(core, 'INTERMEDIATE', 1.0, True)]) | |
| for i in range(25): # unmet unique skill β readiness 0 | |
| _role(f'unmet-{i:02d}', [(_skill(f'Niche{i}'), 'INTERMEDIATE', 1.0, True)]) | |
| result = suggest_roles(user) | |
| rows = result['roles'] | |
| assert len(rows) == 50 | |
| # globally sorted by the documented key | |
| assert [_sort_key(r) for r in rows] == sorted(_sort_key(r) for r in rows) | |
| # the 25 fully-met roles sit at the top, the 25 unmet at the bottom | |
| assert all(r['readiness'] == 100.0 for r in rows[:25]) | |
| assert all(r['readiness'] == 0.0 for r in rows[25:]) | |
| class TestTieBreaking: | |
| def test_equal_readiness_breaks_alphabetically(self, user): | |
| """All readiness tied at 0 β strict role_name ascending, regardless of | |
| insertion order.""" | |
| for name in ('Zebra', 'Alpha', 'Mango'): | |
| _role(name, [(_skill(f'S-{name}'), 'INTERMEDIATE', 1.0, True)]) | |
| names = [r['role_name'] for r in suggest_roles(user)['roles']] | |
| assert names == ['Alpha', 'Mango', 'Zebra'] | |
| class TestNoRequirementsMix: | |
| def test_empty_role_sinks_below_fully_met_real_role(self, user): | |
| py = _skill('Python') | |
| _set_prof(user, py, 100) | |
| _role('Empty', []) # no_requirements | |
| _role('Real', [(py, 'INTERMEDIATE', 1.0, True)]) # fully met, real | |
| rows = suggest_roles(user)['roles'] | |
| top, bottom = rows[0], rows[-1] | |
| assert top['role_name'] == 'Real' | |
| assert top['no_requirements'] is False | |
| assert top['readiness'] == 100.0 | |
| assert bottom['role_name'] == 'Empty' | |
| assert bottom['no_requirements'] is True | |
| class TestTopMissingSkills: | |
| def test_capped_at_three_and_severity_ordered(self, user): | |
| crit_a = _skill('CritA') | |
| crit_b = _skill('CritB') | |
| high_c = _skill('HighC') | |
| med_d = _skill('MedD') | |
| low_e = _skill('LowE') | |
| role = _role('Mixed', [ | |
| (crit_a, 'INTERMEDIATE', 1.0, True), # prof 0 β MISSING CRITICAL | |
| (crit_b, 'INTERMEDIATE', 1.0, True), # prof 0 β MISSING CRITICAL | |
| (high_c, 'INTERMEDIATE', 1.0, True), # prof 10 β INSUFFICIENT HIGH | |
| (med_d, 'INTERMEDIATE', 1.0, False), # prof 0 β MISSING MEDIUM | |
| (low_e, 'INTERMEDIATE', 1.0, False), # prof 55 β INSUFFICIENT LOW | |
| ]) | |
| _set_prof(user, high_c, 10) | |
| _set_prof(user, low_e, 55) | |
| row = next(r for r in suggest_roles(user)['roles'] if r['role_name'] == 'Mixed') | |
| assert row['matched_skills'] == 0 | |
| assert row['total_skills'] == 5 | |
| # capped at 3, most-critical first; the MEDIUM + LOW skills drop off. | |
| assert row['top_missing_skills'] == ['CritA', 'CritB', 'HighC'] | |
| def test_empty_for_fully_met_role(self, user): | |
| py = _skill('Python') | |
| _set_prof(user, py, 100) | |
| _role('Met', [(py, 'INTERMEDIATE', 1.0, True)]) | |
| row = next(r for r in suggest_roles(user)['roles'] if r['role_name'] == 'Met') | |
| assert row['top_missing_skills'] == [] | |
| assert row['matched_skills'] == 1 | |
| assert row['total_skills'] == 1 | |
| class TestThresholdBoundary: | |
| def test_matched_count_at_threshold(self, user, prof, matched, missing): | |
| py = _skill('Python') | |
| _role('Boundary', [(py, 'INTERMEDIATE', 1.0, True)]) # threshold 60 | |
| _set_prof(user, py, prof) | |
| row = next(r for r in suggest_roles(user)['roles'] | |
| if r['role_name'] == 'Boundary') | |
| assert row['matched_skills'] == matched | |
| assert row['total_skills'] == 1 | |
| assert ('Python' in row['top_missing_skills']) is missing | |
| class TestEndpointAdversarial: | |
| def test_response_shape_stable(self, user, auth_client): | |
| py = _skill('Python') | |
| _role('Backend', [(py, 'INTERMEDIATE', 1.0, True)]) | |
| _set_prof(user, py, 70) | |
| r = auth_client.get(URL) | |
| assert r.status_code == 200, r.data | |
| assert set(r.data.keys()) == {'has_skills', 'roles'} | |
| row = r.data['roles'][0] | |
| assert set(row.keys()) == { | |
| 'role_id', 'role_name', 'industry', 'readiness', 'band', | |
| 'matched_skills', 'total_skills', 'top_missing_skills', | |
| 'no_requirements', | |
| } | |
| def test_xss_skill_name_round_trips_unescaped(self, user, auth_client): | |
| xss = '<script>alert(1)</script>' | |
| skill = _skill(xss) | |
| _role('XSS Role', [(skill, 'INTERMEDIATE', 1.0, True)]) # prof 0 β missing | |
| r = auth_client.get(URL) | |
| assert r.status_code == 200 | |
| row = next(x for x in r.data['roles'] if x['role_name'] == 'XSS Role') | |
| assert row['top_missing_skills'] == [xss] | |
| assert b'<script>alert(1)</script>' in r.content | |
| assert b'<script>' not in r.content | |