1+ from collections .abc import AsyncGenerator
2+ import json
3+
4+ import httpx
5+ import pytest
6+ import pytest_asyncio
7+
8+ from magicalapi .services .resume_score_service import ResumeScoreService
9+ from magicalapi .types .base import ErrorResponse
10+ from magicalapi .types .resume_score import ResumeScoreResponse
11+ from magicalapi .types .schemas import HttpResponse
12+
13+
14+ def _make_resume_score_response () -> HttpResponse :
15+ response_body = {
16+ "data" : {
17+ "score" : 87 ,
18+ "summary" : "Strong match for the role" ,
19+ "strengths" : [{"text" : "Python experience" , "category" : "skills" }],
20+ "improvements" : [
21+ {"text" : "Add more leadership examples" , "category" : "other" }
22+ ],
23+ "job_match" : {
24+ "score" : 90 ,
25+ "summary" : "Job match is strong" ,
26+ "pros" : [{"type" : "title" , "message" : "Title aligns" }],
27+ "cons" : [
28+ {"type" : "gap" , "message" : "Missing one requirement" }
29+ ],
30+ "warns" : [
31+ {
32+ "type" : "note" ,
33+ "message" : "Consider tailoring summary" ,
34+ }
35+ ],
36+ },
37+ "skill_match" : {
38+ "score" : 85 ,
39+ "summary" : "Skills align" ,
40+ "skills" : {"all_skills" : ["Python" , "AWS" ]},
41+ "match_result" : {
42+ "miss_match" : ["Kubernetes" ],
43+ "partial_match" : ["Docker" ],
44+ "strong_match" : ["Python" ],
45+ },
46+ },
47+ "education_match" : {
48+ "score" : 80 ,
49+ "summary" : "Education is a fit" ,
50+ "pros" : [{"type" : "degree" , "message" : "Relevant degree" }],
51+ "cons" : [
52+ {
53+ "type" : "specialization" ,
54+ "message" : "Could be more specific" ,
55+ }
56+ ],
57+ "warns" : [{"type" : "note" , "message" : "No issues" }],
58+ },
59+ "more_information" : {
60+ "region" : "Europe" ,
61+ "overqualification_status" : False ,
62+ "qualification_reason" : "Meets the role requirements" ,
63+ },
64+ "jd_text" : "job description text" ,
65+ },
66+ "usage" : {"credits" : 10 },
67+ }
68+
69+ return HttpResponse (
70+ text = json .dumps (response_body ),
71+ status_code = 200 ,
72+ )
73+
74+
75+ @pytest_asyncio .fixture (scope = "function" )
76+ async def httpxclient () -> AsyncGenerator [httpx .AsyncClient ]:
77+ client = httpx .AsyncClient (headers = {"content-type" : "application/json" })
78+
79+ yield client
80+
81+ await client .aclose ()
82+ del client
83+
84+
85+ @pytest .mark .asyncio
86+ @pytest .mark .parametrize ("job_description" , ["a" * 99 , "a" * 5001 ])
87+ async def test_get_resume_score_rejects_invalid_job_description_length (
88+ httpxclient : httpx .AsyncClient , job_description : str
89+ ):
90+ service = ResumeScoreService (httpxclient )
91+
92+ with pytest .raises (ValueError , match = "between 100 and 5000 characters long" ):
93+ await service .get_resume_score (url = "https://example.com/resume.pdf" , job_description = job_description )
94+
95+
96+ @pytest .mark .asyncio
97+ @pytest .mark .parametrize ("job_description" , ["a" * 100 , "a" * 5000 ])
98+ async def test_get_resume_score_accepts_boundary_lengths (
99+ httpxclient : httpx .AsyncClient , monkeypatch : pytest .MonkeyPatch , job_description : str
100+ ):
101+ service = ResumeScoreService (httpxclient )
102+ captured_request : dict [str , str ] = {}
103+
104+ async def fake_send_post_request (path : str , data : dict [str , str ], headers : dict [str , str ] = {}):
105+ captured_request ["path" ] = path
106+ captured_request .update (data )
107+ return _make_resume_score_response ()
108+
109+ monkeypatch .setattr (service , "_send_post_request" , fake_send_post_request )
110+
111+ response = await service .get_resume_score (
112+ url = "https://example.com/resume.pdf" ,
113+ job_description = job_description ,
114+ )
115+
116+ assert isinstance (response , ResumeScoreResponse )
117+ assert response .data .score == 87
118+ assert response .usage .credits == 10
119+ assert captured_request ["path" ] == "resume-score"
120+ assert captured_request ["url" ] == "https://example.com/resume.pdf"
121+ assert captured_request ["job_description" ] == job_description
0 commit comments