Files
resume-agent/backend/tests/test_offerpai_resume.py

923 lines
32 KiB
Python

from __future__ import annotations
from copy import deepcopy
import json
from typing import Any
import httpx
import pytest
from app.offerpai_resume import (
OfferPaiResumeClient,
OfferPaiResumeError,
OfferPaiResumeProvider,
build_offerpai_resume_payload,
map_v3_resume_to_offerpai,
merge_offerpai_resume_snapshot,
offerpai_payload_hash,
offerpai_update_marker,
)
TOKEN = "header.payload.signature-value"
BASE_URL = "https://test.offerpai.com.cn/api/"
def test_client_uses_api_base_path_cookie_and_normalizes_ids() -> None:
requests: list[httpx.Request] = []
def handler(request: httpx.Request) -> httpx.Response:
requests.append(request)
assert request.headers.get("cookie") == f"Token={TOKEN}"
if request.url.path == "/api/resume/canCreate":
return httpx.Response(200, json=True)
if request.url.path == "/api/resume/list":
return httpx.Response(
200,
json={
"code": "0",
"data": [
{"id": 2081575100391407617, "resumeName": "First"},
{"resumeId": "resume-local", "resumeName": "Second"},
],
},
)
if request.url.path == "/api/resume":
return httpx.Response(
200,
json={
"code": "0",
"data": {"resumeId": 2081575100391407618},
},
)
if request.url.path == "/api/resume/work":
return httpx.Response(200, json={"id": "2081575100391407618"})
if request.url.path == "/api/resume/delete":
return httpx.Response(200, json={"code": "0", "data": True})
return httpx.Response(404)
main_payload = {
"resumeId": "2081575100391407617",
"resumeName": "Backend Resume",
}
with httpx.Client(
base_url=BASE_URL,
transport=httpx.MockTransport(handler),
) as http_client:
provider: OfferPaiResumeProvider = OfferPaiResumeClient(
BASE_URL, client=http_client
)
assert provider.can_create(TOKEN) is True
assert provider.list_resumes(TOKEN) == [
{"id": "2081575100391407617", "resumeName": "First"},
{
"resumeId": "resume-local",
"resumeName": "Second",
"id": "resume-local",
},
]
assert provider.save_main(TOKEN, main_payload) == "2081575100391407618"
assert (
provider.replace_section(
TOKEN,
"work",
resume_id="2081575100391407618",
items=[{"companyName": "OfferPai"}],
)
== "2081575100391407618"
)
assert provider.delete_resume(TOKEN, "2081575100391407618") is None
assert main_payload["resumeId"] == "2081575100391407617"
assert [request.url.path for request in requests] == [
"/api/resume/canCreate",
"/api/resume/list",
"/api/resume",
"/api/resume/work",
"/api/resume/delete",
]
assert json.loads(requests[2].content) == {
"resumeId": 2081575100391407617,
"resumeName": "Backend Resume",
}
assert json.loads(requests[3].content) == {
"resumeId": 2081575100391407618,
"items": [{"companyName": "OfferPai"}],
}
assert requests[4].url.params["resumeId"] == "2081575100391407618"
def test_client_accepts_raw_list_and_enveloped_boolean() -> None:
def handler(request: httpx.Request) -> httpx.Response:
if request.url.path.endswith("/canCreate"):
return httpx.Response(200, json={"code": 0, "data": False})
return httpx.Response(200, json=[{"id": 7}])
with httpx.Client(
base_url=BASE_URL,
transport=httpx.MockTransport(handler),
) as http_client:
client = OfferPaiResumeClient(BASE_URL, client=http_client)
assert client.can_create(TOKEN) is False
assert client.list_resumes(TOKEN) == [{"id": "7"}]
@pytest.mark.parametrize(
("status", "code", "public_status"),
[
(401, "external_auth_invalid", 401),
(403, "external_auth_invalid", 401),
(404, "offerpai_resume_not_found", 404),
(409, "offerpai_resume_conflict", 409),
(422, "offerpai_resume_rejected", 422),
(500, "offerpai_resume_unavailable", 502),
],
)
def test_http_errors_are_stable_and_do_not_leak_token(
status: int, code: str, public_status: int
) -> None:
def handler(_request: httpx.Request) -> httpx.Response:
return httpx.Response(status, text=f"upstream accidentally echoed {TOKEN}")
with httpx.Client(
base_url=BASE_URL,
transport=httpx.MockTransport(handler),
) as http_client:
with pytest.raises(OfferPaiResumeError) as captured:
OfferPaiResumeClient(BASE_URL, client=http_client).can_create(TOKEN)
error = captured.value
assert error.code == code
assert error.status_code == public_status
assert TOKEN not in str(error)
assert TOKEN not in error.public_message
assert TOKEN not in repr(error)
def test_business_error_and_invalid_json_are_token_free() -> None:
responses = iter(
[
httpx.Response(
200,
json={"code": "RESUME_REJECTED", "msg": TOKEN, "data": None},
),
httpx.Response(200, text=f"not-json-{TOKEN}"),
]
)
def handler(_request: httpx.Request) -> httpx.Response:
return next(responses)
with httpx.Client(
base_url=BASE_URL,
transport=httpx.MockTransport(handler),
) as http_client:
client = OfferPaiResumeClient(BASE_URL, client=http_client)
with pytest.raises(OfferPaiResumeError) as business_error:
client.can_create(TOKEN)
with pytest.raises(OfferPaiResumeError) as invalid_json_error:
client.can_create(TOKEN)
assert business_error.value.code == "offerpai_resume_rejected"
assert business_error.value.upstream_code == "RESUME_REJECTED"
assert invalid_json_error.value.code == "offerpai_resume_invalid_response"
assert TOKEN not in str(business_error.value)
assert TOKEN not in str(invalid_json_error.value)
def test_timeout_and_bad_local_token_have_stable_errors() -> None:
def timeout_handler(request: httpx.Request) -> httpx.Response:
raise httpx.ReadTimeout(f"timeout with {TOKEN}", request=request)
with httpx.Client(
base_url=BASE_URL,
transport=httpx.MockTransport(timeout_handler),
) as http_client:
with pytest.raises(OfferPaiResumeError) as timeout_error:
OfferPaiResumeClient(BASE_URL, client=http_client).list_resumes(TOKEN)
assert timeout_error.value.code == "offerpai_resume_timeout"
assert timeout_error.value.status_code == 504
assert TOKEN not in str(timeout_error.value)
with pytest.raises(OfferPaiResumeError) as token_error:
OfferPaiResumeClient(BASE_URL).can_create("short token")
assert token_error.value.code == "external_auth_invalid"
assert token_error.value.status_code == 401
assert "short token" not in str(token_error.value)
def test_build_payload_maps_v3_content_without_mutating_inputs() -> None:
content: dict[str, Any] = {
"basics": {
"name": "Ada Lovelace",
"email": "ada@example.com",
"phone": "134****2384",
"city": "Shenzhen",
"wechat_number": "ada-wechat",
"portfolio_url": "https://example.com/ada",
},
"target": {"position": "Backend Engineer"},
"profile_summary": {"content": "Builds reliable systems."},
"skills": ["python"],
"skill_groups": [
{"category": "Languages", "skills": ["Python", "SQL"]}
],
"certificates": ["CET-6"],
"sections": [
{
"kind": "education",
"items": [
{
"id": "edu-1",
"school": "Example University",
"major": "Computer Science",
"degree": "Bachelor",
"study_type": "Full-time",
"start_date": "2020-09",
"end_date_or_present": "2024-06",
"description": [
{"id": "paragraph-kept", "text": "Top 10%."}
],
}
],
},
{
"kind": "work_experience",
"items": [
{
"company": "OfferPai",
"position": "Engineer",
"start_date": "2024-07",
"end_date_or_present": "present",
"resume_bullets": "Built APIs.\nReduced latency.",
}
],
},
{
"kind": "internship_experience",
"items": [
{
"id": "intern-1",
"company": "Example Labs",
"role": "Intern",
"start_date": "2023-01",
"end_date_or_present": "\u81f3\u4eca",
"description": ["Shipped a service."],
}
],
},
{
"kind": "project_experience",
"items": [
{
"id": "project-1",
"project_name": "Resume Agent",
"project_role": "Lead",
"organization": "Personal",
"start_date": "2025-01",
"end_date": "now",
"description": {
"id": "project-paragraph",
"text": "Designed the workflow.",
},
}
],
},
{
"kind": "competition",
"items": [
{
"id": "competition-1",
"name": "Hackathon",
"award": "Gold",
"date": "2024-05",
"highlights": ["Won first place."],
}
],
},
{"kind": "skills", "items": [{"name": "Go"}, "SQL"]},
{"kind": "certificates", "items": [{"value": "AWS"}]},
{"kind": "campus_experience", "items": []},
{"kind": "additional_experience", "items": []},
],
}
profile: dict[str, Any] = {
"phone": "134****2384",
"account_phone": "13421012384",
"target_position": "Platform Engineer",
"external_account": {"user_id": "2081575100391407617"},
"tags": {
"skills": ["SQL", "Docker"],
"certificates": ["CET-6", "PMP"],
},
}
original_content = deepcopy(content)
original_profile = deepcopy(profile)
main, sections, unsupported = build_offerpai_resume_payload(
content,
profile,
resume_name="Candidate Resume",
resume_id="2081575100391407617",
)
second = map_v3_resume_to_offerpai(
content,
profile,
resume_name="Candidate Resume",
resume_id="2081575100391407617",
)
assert content == original_content
assert profile == original_profile
assert main == {
"resumeName": "Candidate Resume",
"targetPosition": "Platform Engineer",
"avatarUrl": "",
"name": "Ada Lovelace",
"email": "ada@example.com",
"mobileNumber": "13421012384",
"city": "Shenzhen",
"wechatNumber": "ada-wechat",
"portfolioUrl": "https://example.com/ada",
"skills": ["Python", "SQL", "Docker", "Go"],
"certificates": ["CET-6", "PMP", "AWS"],
"summary": "Builds reliable systems.",
"resumeId": "2081575100391407617",
}
assert tuple(sections) == (
"education",
"work",
"internship",
"project",
"competition",
)
assert all(
payload["resumeId"] == "2081575100391407617"
for payload in sections.values()
)
assert sections["education"]["items"] == [
{
"school": "Example University",
"major": "Computer Science",
"degree": "Bachelor",
"studyType": "Full-time",
"startDate": "2020.09",
"endDate": "2024.06",
"description": [{"id": "paragraph-kept", "text": "Top 10%."}],
}
]
work = sections["work"]["items"][0]
assert work["companyName"] == "OfferPai"
assert work["position"] == "Engineer"
assert work["startDate"] == "2024.07"
assert work["endDate"] == ""
assert [paragraph["text"] for paragraph in work["description"]] == [
"Built APIs.",
"Reduced latency.",
]
assert work["description"] == second.sections["work"]["items"][0]["description"]
assert all(
paragraph["id"].startswith("desc_") for paragraph in work["description"]
)
assert sections["internship"]["items"][0]["endDate"] == ""
assert sections["project"]["items"][0] == {
"companyName": "Personal",
"projectName": "Resume Agent",
"role": "Lead",
"startDate": "2025.01",
"endDate": "",
"description": [
{"id": "project-paragraph", "text": "Designed the workflow."}
],
}
assert sections["competition"]["items"][0]["awardDate"] == "2024.05"
assert unsupported == ("campus_experience", "additional_experience")
def test_payload_never_sends_a_masked_phone() -> None:
main, _sections, _unsupported = build_offerpai_resume_payload(
{"basics": {"phone": "13421012384"}},
{"phone": "134****2384"},
resume_name="Resume",
)
assert main["mobileNumber"] == ""
def test_payload_accepts_raw_phone_only_from_session_profile() -> None:
main, _sections, _unsupported = build_offerpai_resume_payload(
{"basics": {"phone": "134****2384"}},
{
"phone": "134****2384",
"external_account": {"mobileNumber": "13421012384"},
},
resume_name="Resume",
)
assert main["mobileNumber"] == "13421012384"
def test_client_gets_main_and_all_sections_with_cookie_query_and_normalized_ids() -> None:
requests: list[httpx.Request] = []
section_names = ("education", "work", "internship", "project", "competition")
def handler(request: httpx.Request) -> httpx.Response:
requests.append(request)
assert request.method == "GET"
assert request.headers.get("cookie") == f"Token={TOKEN}"
assert request.url.params["resumeId"] == "2081575100391407617"
if request.url.path == "/api/resume":
return httpx.Response(
200,
json={
"code": "0",
"data": {
"id": 2081575100391407617,
"resumeId": 2081575100391407617,
"resumeName": "Remote resume",
},
},
)
section = request.url.path.rsplit("/", 1)[-1]
assert section in section_names
data = [
{
"id": 2081575100391407700 + section_names.index(section),
"resumeId": 2081575100391407617,
"description": [
{"id": 700 + section_names.index(section), "text": section}
],
}
]
# Exercise both documented envelopes and direct/raw response bodies.
if section_names.index(section) % 2:
return httpx.Response(200, json={"code": 0, "data": data})
return httpx.Response(200, json=data)
with httpx.Client(
base_url=BASE_URL,
transport=httpx.MockTransport(handler),
) as http_client:
client = OfferPaiResumeClient(BASE_URL, client=http_client)
main = client.get_main(TOKEN, "2081575100391407617")
section_results = {
section: client.list_section(
TOKEN,
section, # type: ignore[arg-type]
resume_id="2081575100391407617",
)
for section in section_names
}
assert main["id"] == "2081575100391407617"
assert main["resumeId"] == "2081575100391407617"
for index, section in enumerate(section_names):
item = section_results[section][0]
assert item["id"] == str(2081575100391407700 + index)
assert item["resumeId"] == "2081575100391407617"
assert item["description"] == [{"id": str(700 + index), "text": section}]
assert [request.url.path for request in requests] == [
"/api/resume",
"/api/resume/education",
"/api/resume/work",
"/api/resume/internship",
"/api/resume/project",
"/api/resume/competition",
]
def test_client_get_main_accepts_a_raw_record() -> None:
def handler(_request: httpx.Request) -> httpx.Response:
return httpx.Response(
200,
json={"id": 2081575100391407617, "resumeName": "Raw resume"},
)
with httpx.Client(
base_url=BASE_URL,
transport=httpx.MockTransport(handler),
) as http_client:
result = OfferPaiResumeClient(BASE_URL, client=http_client).get_main(
TOKEN, 2081575100391407617
)
assert result == {
"id": "2081575100391407617",
"resumeName": "Raw resume",
}
def test_update_marker_supports_instant_and_scalar_values() -> None:
assert offerpai_update_marker(
{"updateTime": {"seconds": 1785912615, "nanos": 42}}
) == "1785912615:42"
assert offerpai_update_marker({"updateTime": "2026-08-05T12:00:00Z"}) == (
"2026-08-05T12:00:00Z"
)
assert offerpai_update_marker({"updateTime": None}) is None
def test_external_description_edit_keeps_local_entry_id_by_paragraph_id() -> None:
existing_content = {
"schema_version": 3,
"basics": {},
"target": {},
"skill_groups": [],
"sections": [
{
"id": "section-work",
"kind": "work_experience",
"heading": "Work",
"items": [
{
"id": "local-alpha",
"company": "Alpha",
"position": "Engineer",
"start_date": "2024-01",
"end_date_or_present": "present",
"description": "Original text",
"offerpai_record_id": "old-alpha-row",
"offerpai_description_ids": ["stable-alpha-paragraph"],
"pending_proposal": {"content": "stale proposal"},
"previous_version": {"description": "older text"},
"gap_report": {"missing": ["metric"]},
},
{
"id": "local-beta",
"company": "Beta",
"position": "Engineer",
"start_date": "2023-01",
"end_date_or_present": "2023-12",
"description": "Beta text",
"offerpai_record_id": "new-alpha-row",
"offerpai_description_ids": ["stable-beta-paragraph"],
},
],
}
],
}
remote_sections = {
"education": [],
"work": [
{
# The replacement row ID collides with Beta's old row ID. The
# stable paragraph ID must still associate this record to Alpha.
"id": "new-alpha-row",
"companyName": "Alpha",
"position": "Engineer",
"startDate": "2024.01",
"endDate": "",
"description": [
{
"id": "stable-alpha-paragraph",
"text": "Externally edited text",
}
],
}
],
"internship": [],
"project": [],
"competition": [],
}
merged, _profile, _unsupported = merge_offerpai_resume_snapshot(
existing_content,
{},
{"skills": [], "certificates": []},
remote_sections,
)
work_section = next(
section
for section in merged["sections"]
if section["kind"] == "work_experience"
)
assert len(work_section["items"]) == 1
item = work_section["items"][0]
assert item["id"] == "local-alpha"
assert item["offerpai_record_id"] == "new-alpha-row"
assert item["offerpai_description_ids"] == ["stable-alpha-paragraph"]
assert item["description"] == "Externally edited text"
assert item["provenance"] == "external_synced"
assert "pending_proposal" not in item
assert "previous_version" not in item
assert item["gap_report"] == {"missing": ["metric"]}
def test_local_remote_pull_roundtrip_preserves_dates_paragraphs_and_unsupported_sections() -> None:
content: dict[str, Any] = {
"schema_version": 3,
"resume_name": "Candidate Resume",
"basics": {
"name": "Ada Lovelace",
"email": "ada@example.com",
"city": "Shenzhen",
"avatar_url": "https://example.com/avatar.png",
"wechat_number": "ada-wechat",
"portfolio_url": "https://example.com/ada",
},
"target": {"position": "Platform Engineer"},
"profile_summary": {"content": "Builds reliable systems."},
# Deliberately cross category order; hash comparison is semantic.
"skill_groups": [
{"category": "Tools", "skills": ["Docker", "Python"]}
],
"sections": [
{
"id": "section-education",
"kind": "education",
"heading": "Education",
"items": [
{
"id": "local-education",
"school": "Example University",
"major": "Computer Science",
"degree": "Bachelor",
"study_type": "Full-time",
"start_date": "2020-09",
"end_date_or_present": "2024-06",
"description": [
{"id": "education-paragraph", "text": "Top 10%."}
],
}
],
},
{
"id": "section-work",
"kind": "work_experience",
"heading": "Work",
"items": [
{
"id": "local-work",
"company": "OfferPai",
"position": "Engineer",
"start_date": "2024-07",
"end_date_or_present": "present",
"description": [
{"id": "work-paragraph", "text": "Built APIs."}
],
}
],
},
{
"id": "section-internship",
"kind": "internship_experience",
"heading": "Internship",
"items": [
{
"id": "local-internship",
"company": "Example Labs",
"position": "Intern",
"start_date": "2023-01",
"end_date_or_present": "2023-06",
"description": [
{
"id": "internship-paragraph",
"text": "Shipped a service.",
}
],
}
],
},
{
"id": "section-project",
"kind": "project_experience",
"heading": "Project",
"items": [
{
"id": "local-project",
"company": "Personal",
"project_name": "Resume Agent",
"project_role": "Lead",
"start_date": "2025-01",
"end_date_or_present": "present",
"description": [
{
"id": "project-paragraph",
"text": "Designed the workflow.",
}
],
}
],
},
{
"id": "section-competition",
"kind": "competition",
"heading": "Competition",
"items": [
{
"id": "local-competition",
"name": "Hackathon",
"award": "Gold",
"date": "2024-05",
"description": [
{
"id": "competition-paragraph",
"text": "Won first place.",
}
],
}
],
},
{
"id": "section-certificates",
"kind": "certificates",
"heading": "Certificates",
"items": [{"id": "local-certificate", "value": "CET-6"}],
},
{
"id": "section-campus",
"kind": "campus_experience",
"heading": "Campus",
"items": [
{
"id": "local-campus",
"organization": "Student Union",
"role": "Member",
}
],
},
],
}
profile: dict[str, Any] = {
"phone": "13421012384",
"phone_source": "account",
"account_phone": "13421012384",
"tags": {"skills": [], "certificates": []},
}
outbound = map_v3_resume_to_offerpai(
content,
profile,
resume_name="Candidate Resume",
resume_id="2081575100391407617",
)
remote_main = deepcopy(outbound.main)
remote_main.pop("resumeId")
remote_main["id"] = 2081575100391407617
remote_sections: dict[str, list[dict[str, Any]]] = {}
for section_index, (kind, payload) in enumerate(outbound.sections.items()):
items = deepcopy(payload["items"])
for item_index, item in enumerate(items):
item["id"] = 2081575100391407700 + section_index * 10 + item_index
remote_sections[kind] = items
merged_content, merged_profile, unsupported = merge_offerpai_resume_snapshot(
content,
profile,
remote_main,
remote_sections, # type: ignore[arg-type]
)
roundtrip = map_v3_resume_to_offerpai(
merged_content,
merged_profile,
resume_name="Candidate Resume",
resume_id="2081575100391407617",
)
assert offerpai_payload_hash(remote_main, remote_sections) == (
offerpai_payload_hash(roundtrip.main, roundtrip.sections)
)
assert unsupported == ("campus_experience",)
campus = next(
section
for section in merged_content["sections"]
if section["kind"] == "campus_experience"
)
assert campus == content["sections"][-1]
by_kind = {section["kind"]: section for section in merged_content["sections"]}
assert by_kind["education"]["items"][0]["id"] == "local-education"
assert by_kind["education"]["items"][0]["start_date"] == "2020-09"
assert by_kind["education"]["items"][0]["end_date_or_present"] == "2024-06"
assert by_kind["work_experience"]["items"][0]["end_date_or_present"] == (
"present"
)
assert by_kind["project_experience"]["items"][0]["project_role"] == "Lead"
assert by_kind["competition"]["items"][0]["date"] == "2024-05"
for kind, payload in outbound.sections.items():
assert [item["description"] for item in roundtrip.sections[kind]["items"]] == [
item["description"] for item in payload["items"]
]
assert all(
isinstance(
by_kind[local_kind]["items"][0]["offerpai_record_id"], str
)
for local_kind in (
"education",
"work_experience",
"internship_experience",
"project_experience",
"competition",
)
)
def test_remote_main_clears_converge_without_reusing_account_identity_phone() -> None:
existing_content = {
"schema_version": 3,
"resume_name": "Old resume",
"basics": {
"name": "Old name",
"email": "old@example.com",
"city": "Old city",
"avatar_url": "old-avatar",
"wechat_number": "old-wechat",
"portfolio_url": "old-portfolio",
"masked_phone": "134****2384",
},
"target": {"position": "Old target"},
"profile_summary": {"content": "Old summary"},
"summary": "Old legacy summary",
"skills": ["Old root skill"],
"certificates": ["Old root certificate"],
"skill_groups": [{"category": "Old", "skills": ["Old grouped skill"]}],
"sections": [
{
"id": "old-certificates",
"kind": "certificates",
"heading": "Certificates",
"items": [{"id": "old-certificate", "value": "Old certificate"}],
}
],
}
profile = {
"name": "Old name",
"email": "old@example.com",
"city": "Old city",
"avatar_url": "old-avatar",
"wechat_number": "old-wechat",
"portfolio_url": "old-portfolio",
"phone": "13421012384",
"phone_source": "account",
"account_phone": "13421012384",
"external_account": {"mobile_number": "13421012384"},
"target_position": "Old target",
"resume_name": "Old resume",
"summary": "Old summary",
"tags": {
"skills": ["Old tagged skill"],
"certificates": ["Old tagged certificate"],
},
}
remote_main = {
"id": "2081575100391407617",
"resumeName": "",
"targetPosition": "",
"avatarUrl": "",
"name": "",
"email": "",
"mobileNumber": "",
"city": "",
"wechatNumber": "",
"portfolioUrl": "",
"skills": [],
"certificates": [],
"summary": "",
}
remote_sections = {
"education": [],
"work": [],
"internship": [],
"project": [],
"competition": [],
}
merged_content, merged_profile, _unsupported = merge_offerpai_resume_snapshot(
existing_content,
profile,
remote_main,
remote_sections,
)
outbound = map_v3_resume_to_offerpai(
merged_content,
merged_profile,
resume_id="2081575100391407617",
)
assert merged_profile["account_phone"] == "13421012384"
assert merged_profile["external_account"] == {
"mobile_number": "13421012384"
}
assert merged_profile["phone"] == ""
assert merged_profile["phone_source"] == "offerpai_resume"
assert outbound.main == {
"resumeName": "",
"targetPosition": "",
"avatarUrl": "",
"name": "",
"email": "",
"mobileNumber": "",
"city": "",
"wechatNumber": "",
"portfolioUrl": "",
"skills": [],
"certificates": [],
"summary": "",
"resumeId": "2081575100391407617",
}
assert offerpai_payload_hash(remote_main, remote_sections) == (
offerpai_payload_hash(outbound.main, outbound.sections)
)