Files

222 lines
6.6 KiB
Python

"""apps.forms 单元测试:线索提交 API(B2B 官网核心转化路径,测试覆盖率要求较高)。"""
from datetime import timedelta
import pytest
from django.core.cache import cache
from django.utils import timezone
from rest_framework.test import APIClient
from apps.forms.models import (
FormDefinition,
FormDefinitionField,
Lead,
LeadDeletionRequest,
)
pytestmark = pytest.mark.django_db
@pytest.fixture(autouse=True)
def _clear_throttle_cache():
"""ScopedRateThrottle 依赖默认缓存记录请求次数,避免测试间相互影响(forms scope 限流 5/min)。"""
cache.clear()
yield
@pytest.fixture
def contact_form():
form_def = FormDefinition.objects.create(
name="联系我们",
success_message="提交成功,我们会尽快联系您。",
)
FormDefinitionField.objects.create(
form=form_def, label="姓名", field_key="name", field_type="text", required=True
)
return form_def
def test_lead_submit_creates_lead_and_returns_success_message(contact_form):
client = APIClient()
response = client.post(
"/api/v1/custom/leads/",
{
"form_id": contact_form.pk,
"data": {"name": "张三"},
"source_url": "https://example.com/contact",
},
format="json",
)
assert response.status_code == 201
assert response.data["success"] is True
assert response.data["message"] == contact_form.success_message
lead = Lead.objects.get()
assert lead.form_id == contact_form.pk
assert lead.data == {"name": "张三"}
assert lead.source_url == "https://example.com/contact"
def test_lead_submit_returns_404_for_unknown_form():
client = APIClient()
response = client.post(
"/api/v1/custom/leads/",
{"form_id": 9999, "data": {}},
format="json",
)
assert response.status_code == 404
assert response.data["error"]["code"] == "NOT_FOUND"
def test_lead_submit_requires_form_id():
client = APIClient()
response = client.post(
"/api/v1/custom/leads/",
{"data": {"name": "张三"}},
format="json",
)
assert response.status_code == 400
def test_lead_submit_honeypot_filled_silently_rejects_without_creating_lead(
contact_form,
):
"""蜜罐字段非空视为机器人:返回“成功”假象但不落库、不发通知邮件。"""
client = APIClient()
response = client.post(
"/api/v1/custom/leads/",
{
"form_id": contact_form.pk,
"data": {"name": "张三"},
"website": "http://spam.example.com",
},
format="json",
)
assert response.status_code == 201
assert response.data["success"] is True
assert not Lead.objects.exists()
# --- PIPL 数据删除权(详见设计文档 §2.15) ---
def test_deletion_request_returns_generic_message_and_sends_email(
contact_form, mailoutbox
):
Lead.objects.create(
form=contact_form,
data={"name": "张三", "email": "zhangsan@example.com"},
)
client = APIClient()
response = client.post(
"/api/v1/custom/leads/deletion-requests/",
{"contact": "zhangsan@example.com"},
format="json",
)
assert response.status_code == 202
assert LeadDeletionRequest.objects.filter(contact="zhangsan@example.com").exists()
assert len(mailoutbox) == 1
assert mailoutbox[0].to == ["zhangsan@example.com"]
def test_deletion_request_returns_same_generic_message_for_unknown_contact(mailoutbox):
"""未提交过表单的邮箱也返回相同提示,避免探测。"""
client = APIClient()
response_known = client.post(
"/api/v1/custom/leads/deletion-requests/",
{"contact": "known@example.com"},
format="json",
)
response_unknown = client.post(
"/api/v1/custom/leads/deletion-requests/",
{"contact": "unknown@example.com"},
format="json",
)
assert response_known.status_code == response_unknown.status_code == 202
assert response_known.data["message"] == response_unknown.data["message"]
def test_deletion_confirm_deletes_matching_leads_only(contact_form):
matching_lead = Lead.objects.create(
form=contact_form,
data={"name": "张三", "email": "zhangsan@example.com"},
)
other_lead = Lead.objects.create(
form=contact_form,
data={"name": "李四", "email": "lisi@example.com"},
)
deletion_request = LeadDeletionRequest.objects.create(contact="zhangsan@example.com")
client = APIClient()
response = client.post(
"/api/v1/custom/leads/deletion-requests/confirm/",
{"token": str(deletion_request.token)},
format="json",
)
assert response.status_code == 200
assert response.data["success"] is True
assert response.data["deleted_count"] == 1
assert not Lead.objects.filter(pk=matching_lead.pk).exists()
assert Lead.objects.filter(pk=other_lead.pk).exists()
deletion_request.refresh_from_db()
assert deletion_request.status == LeadDeletionRequest.STATUS_COMPLETED
assert deletion_request.deleted_count == 1
assert deletion_request.completed_at is not None
def test_deletion_confirm_invalid_token_returns_404():
client = APIClient()
response = client.post(
"/api/v1/custom/leads/deletion-requests/confirm/",
{"token": "00000000-0000-0000-0000-000000000000"},
format="json",
)
assert response.status_code == 404
assert response.data["error"]["code"] == "INVALID_TOKEN"
def test_deletion_confirm_expired_token_returns_400():
deletion_request = LeadDeletionRequest.objects.create(contact="zhangsan@example.com")
LeadDeletionRequest.objects.filter(pk=deletion_request.pk).update(
requested_at=timezone.now() - timedelta(hours=25)
)
client = APIClient()
response = client.post(
"/api/v1/custom/leads/deletion-requests/confirm/",
{"token": str(deletion_request.token)},
format="json",
)
assert response.status_code == 400
assert response.data["error"]["code"] == "TOKEN_EXPIRED"
deletion_request.refresh_from_db()
assert deletion_request.status == LeadDeletionRequest.STATUS_EXPIRED
def test_deletion_confirm_already_processed_returns_400():
deletion_request = LeadDeletionRequest.objects.create(
contact="zhangsan@example.com",
status=LeadDeletionRequest.STATUS_COMPLETED,
)
client = APIClient()
response = client.post(
"/api/v1/custom/leads/deletion-requests/confirm/",
{"token": str(deletion_request.token)},
format="json",
)
assert response.status_code == 400
assert response.data["error"]["code"] == "ALREADY_PROCESSED"