feat(agent-runtime): 补全测试替身编排闭环
This commit is contained in:
+2
-1
@@ -15,9 +15,10 @@ POSTGRES_PORT=5432
|
|||||||
POSTGRES_SSLMODE=prefer
|
POSTGRES_SSLMODE=prefer
|
||||||
POSTGRES_CONN_MAX_AGE=60
|
POSTGRES_CONN_MAX_AGE=60
|
||||||
|
|
||||||
# OpenAI Agents SDK:第一阶段仅预留配置,不执行真实模型调用
|
# OpenAI Agents SDK:第一阶段使用测试替身验证持久化编排,不执行真实模型调用
|
||||||
OPENAI_API_KEY=请替换为OpenAI_API_Key
|
OPENAI_API_KEY=请替换为OpenAI_API_Key
|
||||||
OPENAI_MODEL=
|
OPENAI_MODEL=
|
||||||
|
AGENT_DEFAULT_NAME=job_research
|
||||||
|
|
||||||
# 生产安全配置
|
# 生产安全配置
|
||||||
DJANGO_SECURE_SSL_REDIRECT=true
|
DJANGO_SECURE_SSL_REDIRECT=true
|
||||||
|
|||||||
@@ -87,7 +87,12 @@ WSGI_APPLICATION = "JobRadar.wsgi.application"
|
|||||||
ASGI_APPLICATION = "JobRadar.asgi.application"
|
ASGI_APPLICATION = "JobRadar.asgi.application"
|
||||||
|
|
||||||
AUTH_PASSWORD_VALIDATORS = [
|
AUTH_PASSWORD_VALIDATORS = [
|
||||||
{"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"},
|
{
|
||||||
|
"NAME": (
|
||||||
|
"django.contrib.auth.password_validation."
|
||||||
|
"UserAttributeSimilarityValidator"
|
||||||
|
)
|
||||||
|
},
|
||||||
{"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
|
{"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
|
||||||
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
|
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
|
||||||
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
|
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
|
||||||
@@ -110,13 +115,22 @@ LOGIN_URL = "accounts:login"
|
|||||||
LOGIN_REDIRECT_URL = "agent_runtime:run-list"
|
LOGIN_REDIRECT_URL = "agent_runtime:run-list"
|
||||||
LOGOUT_REDIRECT_URL = "accounts:login"
|
LOGOUT_REDIRECT_URL = "accounts:login"
|
||||||
|
|
||||||
|
# Agent 配置只从运行环境读取。第一阶段的自动化测试使用 Stub Gateway,
|
||||||
|
# 因此缺少真实密钥不能影响登录、资料和运行记录查询等非 Agent 功能。
|
||||||
|
OPENAI_API_KEY = env("OPENAI_API_KEY", "")
|
||||||
|
OPENAI_MODEL = env("OPENAI_MODEL", "")
|
||||||
|
AGENT_DEFAULT_NAME = env("AGENT_DEFAULT_NAME", "job_research")
|
||||||
|
|
||||||
# 日志只输出可关联的结构化键值,不记录请求正文、Cookie 或认证头。
|
# 日志只输出可关联的结构化键值,不记录请求正文、Cookie 或认证头。
|
||||||
LOGGING = {
|
LOGGING = {
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"disable_existing_loggers": False,
|
"disable_existing_loggers": False,
|
||||||
"formatters": {
|
"formatters": {
|
||||||
"structured": {
|
"structured": {
|
||||||
"format": "time={asctime} level={levelname} logger={name} request_id={request_id} message={message}",
|
"format": (
|
||||||
|
"time={asctime} level={levelname} logger={name} "
|
||||||
|
"request_id={request_id} message={message}"
|
||||||
|
),
|
||||||
"style": "{",
|
"style": "{",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -220,4 +220,4 @@ JobRadar 仅用于个人岗位信息整理与求职辅助。使用前应确认
|
|||||||
|
|
||||||
当前版本:`0.1.0-dev`
|
当前版本:`0.1.0-dev`
|
||||||
|
|
||||||
当前阶段:架构与 Agent 契约已确定,阶段一的配置与依赖基线已落盘,下一步是用户认证与数据归属设计。
|
当前阶段:第一阶段工程基线及 SDK 测试替身编排闭环已经落地,正在完成质量工具环境同步、生产配置检查、页面人工验收和 PostgreSQL 集成验证;通过全部验收后进入第二阶段最小 Agent 闭环及真实 Agents SDK 接入。
|
||||||
|
|||||||
+26
-2
@@ -7,7 +7,10 @@ class ProfileTests(TestCase):
|
|||||||
"""验证资料显式创建、认证保护和更新边界。"""
|
"""验证资料显式创建、认证保护和更新边界。"""
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.user = get_user_model().objects.create_user(username="alice", password="safe-pass-123")
|
self.user = get_user_model().objects.create_user(
|
||||||
|
username="alice",
|
||||||
|
password="safe-pass-123",
|
||||||
|
)
|
||||||
|
|
||||||
def test_profile_requires_login(self):
|
def test_profile_requires_login(self):
|
||||||
response = self.client.get(reverse("accounts:profile"))
|
response = self.client.get(reverse("accounts:profile"))
|
||||||
@@ -15,7 +18,28 @@ class ProfileTests(TestCase):
|
|||||||
|
|
||||||
def test_profile_is_created_and_updated(self):
|
def test_profile_is_created_and_updated(self):
|
||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
response = self.client.post(reverse("accounts:profile"), {"display_name": "爱丽丝", "timezone": "Asia/Shanghai"})
|
response = self.client.post(
|
||||||
|
reverse("accounts:profile"),
|
||||||
|
{"display_name": "爱丽丝", "timezone": "Asia/Shanghai"},
|
||||||
|
)
|
||||||
self.assertRedirects(response, reverse("accounts:profile"))
|
self.assertRedirects(response, reverse("accounts:profile"))
|
||||||
self.user.refresh_from_db()
|
self.user.refresh_from_db()
|
||||||
self.assertEqual(self.user.profile.display_name, "爱丽丝")
|
self.assertEqual(self.user.profile.display_name, "爱丽丝")
|
||||||
|
|
||||||
|
def test_profile_rejects_unknown_timezone(self):
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("accounts:profile"),
|
||||||
|
{"display_name": "爱丽丝", "timezone": "Unknown/Timezone"},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, "选择一个有效的选项")
|
||||||
|
|
||||||
|
def test_login_and_post_logout_flow(self):
|
||||||
|
login_response = self.client.post(
|
||||||
|
reverse("accounts:login"),
|
||||||
|
{"username": "alice", "password": "safe-pass-123"},
|
||||||
|
)
|
||||||
|
self.assertRedirects(login_response, reverse("agent_runtime:run-list"))
|
||||||
|
logout_response = self.client.post(reverse("accounts:logout"))
|
||||||
|
self.assertRedirects(logout_response, reverse("accounts:login"))
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
"""Agent 运行配置边界。"""
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
from common.exceptions import AgentConfigurationError
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class AgentRuntimeConfig:
|
||||||
|
"""集中表达真实 SDK 执行所需配置,避免业务服务直接读取环境变量。"""
|
||||||
|
|
||||||
|
api_key: str
|
||||||
|
model: str
|
||||||
|
agent_name: str
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_settings(cls) -> "AgentRuntimeConfig":
|
||||||
|
"""从 Django 设置构建配置;这里只读取,不在日志或异常中回显秘密。"""
|
||||||
|
|
||||||
|
return cls(
|
||||||
|
api_key=str(settings.OPENAI_API_KEY or "").strip(),
|
||||||
|
model=str(settings.OPENAI_MODEL or "").strip(),
|
||||||
|
agent_name=str(settings.AGENT_DEFAULT_NAME or "job_research").strip(),
|
||||||
|
)
|
||||||
|
|
||||||
|
def require_real_execution(self) -> "AgentRuntimeConfig":
|
||||||
|
"""真实执行前快速失败;非 Agent 页面无需调用此方法。"""
|
||||||
|
|
||||||
|
missing = []
|
||||||
|
if not self.api_key:
|
||||||
|
missing.append("OPENAI_API_KEY")
|
||||||
|
if not self.model:
|
||||||
|
missing.append("OPENAI_MODEL")
|
||||||
|
if missing:
|
||||||
|
raise AgentConfigurationError(f"真实 Agent 执行缺少配置:{', '.join(missing)}。")
|
||||||
|
return self
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
"""Gateway 执行与本地 Agent Run 持久化之间的编排服务。"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from common.logging import sanitize_summary
|
||||||
|
|
||||||
|
from .gateway import AgentExecutionRequest, AgentRunnerGateway
|
||||||
|
from .models import AgentRun, RunStatus
|
||||||
|
from .services import transition_run
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def execute_run(run_id, gateway: AgentRunnerGateway) -> AgentRun:
|
||||||
|
"""使用注入的 Gateway 驱动一次运行,并确保所有结果都进入稳定终态。
|
||||||
|
|
||||||
|
外部调用发生在两个短事务之间:开始状态先提交,Gateway 返回或抛错后再用
|
||||||
|
独立事务保存成功或失败结果。该函数不重试,避免第一阶段产生重复外部副作用。
|
||||||
|
"""
|
||||||
|
|
||||||
|
running = transition_run(run_id, RunStatus.RUNNING)
|
||||||
|
request = AgentExecutionRequest(
|
||||||
|
agent_run_id=str(running.id),
|
||||||
|
user_id=running.owner_id,
|
||||||
|
input_summary=sanitize_summary(running.input_summary),
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
result = gateway.run(request)
|
||||||
|
except Exception as exc: # Gateway 是外部边界,必须把未知异常转换为可审计失败。
|
||||||
|
# 外部异常消息可能夹带请求参数或认证信息,因此日志和数据库只保留异常类型。
|
||||||
|
error_type = type(exc).__name__
|
||||||
|
logger.error(
|
||||||
|
"Agent Gateway 执行异常,异常类型=%s",
|
||||||
|
error_type,
|
||||||
|
extra={"agent_run_id": str(running.id)},
|
||||||
|
)
|
||||||
|
return transition_run(
|
||||||
|
running.id,
|
||||||
|
RunStatus.FAILED,
|
||||||
|
error_code="gateway_error",
|
||||||
|
error_summary=f"Agent Gateway 执行异常:{error_type}",
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.succeeded:
|
||||||
|
return transition_run(running.id, RunStatus.SUCCEEDED, output=result.output_summary)
|
||||||
|
return transition_run(
|
||||||
|
running.id,
|
||||||
|
RunStatus.FAILED,
|
||||||
|
error_code=result.error_code or "agent_failed",
|
||||||
|
error_summary=result.error_summary,
|
||||||
|
)
|
||||||
+110
-4
@@ -1,11 +1,29 @@
|
|||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.test import TestCase
|
from django.db import IntegrityError, transaction
|
||||||
|
from django.test import SimpleTestCase, TestCase, override_settings
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from common.exceptions import InvalidStateTransition
|
from common.exceptions import AgentConfigurationError, InvalidStateTransition, PermissionDenied
|
||||||
|
|
||||||
from .models import RunStatus
|
from .config import AgentRuntimeConfig
|
||||||
from .services import create_run, finish_tool_call, start_tool_call, transition_run
|
from .gateway import AgentExecutionResult, StubAgentRunnerGateway
|
||||||
|
from .models import ApprovalStatus, RunStatus, ToolCallStatus
|
||||||
|
from .orchestration import execute_run
|
||||||
|
from .services import (
|
||||||
|
create_run,
|
||||||
|
finish_tool_call,
|
||||||
|
request_approval,
|
||||||
|
resolve_approval,
|
||||||
|
start_tool_call,
|
||||||
|
transition_run,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ExplodingGateway:
|
||||||
|
"""模拟外部边界抛错,并故意在异常文本中携带不应落库的秘密。"""
|
||||||
|
|
||||||
|
def run(self, request):
|
||||||
|
raise RuntimeError("api_key=should-not-be-stored")
|
||||||
|
|
||||||
|
|
||||||
class AgentRunTests(TestCase):
|
class AgentRunTests(TestCase):
|
||||||
@@ -32,6 +50,17 @@ class AgentRunTests(TestCase):
|
|||||||
response = self.client.get(reverse("agent_runtime:run-detail", args=(run.id,)))
|
response = self.client.get(reverse("agent_runtime:run-detail", args=(run.id,)))
|
||||||
self.assertEqual(response.status_code, 404)
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
|
def test_run_list_only_contains_current_user_data(self):
|
||||||
|
create_run(self.alice, "爱丽丝的运行")
|
||||||
|
create_run(self.bob, "鲍勃的运行")
|
||||||
|
self.client.force_login(self.alice)
|
||||||
|
|
||||||
|
response = self.client.get(reverse("agent_runtime:run-list"))
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, "爱丽丝的运行")
|
||||||
|
self.assertNotContains(response, "鲍勃的运行")
|
||||||
|
|
||||||
def test_tool_call_is_recorded_and_sanitized(self):
|
def test_tool_call_is_recorded_and_sanitized(self):
|
||||||
run = create_run(self.alice, "工具运行")
|
run = create_run(self.alice, "工具运行")
|
||||||
transition_run(run.id, RunStatus.RUNNING)
|
transition_run(run.id, RunStatus.RUNNING)
|
||||||
@@ -40,3 +69,80 @@ class AgentRunTests(TestCase):
|
|||||||
finished = finish_tool_call(call.id, result={"count": 1})
|
finished = finish_tool_call(call.id, result={"count": 1})
|
||||||
self.assertEqual(finished.status, "succeeded")
|
self.assertEqual(finished.status, "succeeded")
|
||||||
self.assertEqual(finished.result_summary, {"count": 1})
|
self.assertEqual(finished.result_summary, {"count": 1})
|
||||||
|
|
||||||
|
def test_tool_call_failure_and_duplicate_finish_are_recorded(self):
|
||||||
|
run = create_run(self.alice, "失败工具运行")
|
||||||
|
transition_run(run.id, RunStatus.RUNNING)
|
||||||
|
call = start_tool_call(run.id, "call-1", "demo_tool", idempotency_key="same-operation")
|
||||||
|
failed = finish_tool_call(call.id, error_code="timeout", error_summary="请求超时")
|
||||||
|
self.assertEqual(failed.status, ToolCallStatus.FAILED)
|
||||||
|
self.assertEqual(failed.error_code, "timeout")
|
||||||
|
with self.assertRaises(InvalidStateTransition):
|
||||||
|
finish_tool_call(call.id, result={"unexpected": True})
|
||||||
|
|
||||||
|
def test_duplicate_tool_idempotency_key_is_rejected(self):
|
||||||
|
run = create_run(self.alice, "幂等工具运行")
|
||||||
|
transition_run(run.id, RunStatus.RUNNING)
|
||||||
|
start_tool_call(run.id, "call-1", "demo_tool", idempotency_key="same-operation")
|
||||||
|
with self.assertRaises(IntegrityError), transaction.atomic():
|
||||||
|
start_tool_call(run.id, "call-2", "demo_tool", idempotency_key="same-operation")
|
||||||
|
|
||||||
|
def test_approval_can_be_approved_once_by_owner(self):
|
||||||
|
run = create_run(self.alice, "确认运行")
|
||||||
|
transition_run(run.id, RunStatus.RUNNING)
|
||||||
|
approval = request_approval(run.id, "approval-1", "external_action", {"token": "secret"})
|
||||||
|
self.assertEqual(approval.request_summary["token"], "***")
|
||||||
|
resolved = resolve_approval(self.alice, approval.id, True, {"reason": "允许"})
|
||||||
|
self.assertEqual(resolved.status, ApprovalStatus.APPROVED)
|
||||||
|
run.refresh_from_db()
|
||||||
|
self.assertEqual(run.status, RunStatus.RUNNING)
|
||||||
|
with self.assertRaises(InvalidStateTransition):
|
||||||
|
resolve_approval(self.alice, approval.id, True)
|
||||||
|
|
||||||
|
def test_approval_rejects_cross_user_and_can_cancel_run(self):
|
||||||
|
run = create_run(self.alice, "拒绝确认运行")
|
||||||
|
transition_run(run.id, RunStatus.RUNNING)
|
||||||
|
approval = request_approval(run.id, "approval-1", "external_action")
|
||||||
|
with self.assertRaises(PermissionDenied):
|
||||||
|
resolve_approval(self.bob, approval.id, False)
|
||||||
|
resolve_approval(self.alice, approval.id, False)
|
||||||
|
run.refresh_from_db()
|
||||||
|
self.assertEqual(run.status, RunStatus.CANCELLED)
|
||||||
|
|
||||||
|
def test_stub_gateway_drives_successful_persistent_run(self):
|
||||||
|
run = create_run(self.alice, "Stub 成功运行", {"query": "Python"})
|
||||||
|
gateway = StubAgentRunnerGateway(AgentExecutionResult(True, {"count": 2}))
|
||||||
|
finished = execute_run(run.id, gateway)
|
||||||
|
self.assertEqual(finished.status, RunStatus.SUCCEEDED)
|
||||||
|
self.assertEqual(finished.output_summary, {"count": 2})
|
||||||
|
self.assertEqual(finished.events.count(), 3)
|
||||||
|
|
||||||
|
def test_stub_gateway_failure_and_exception_reach_failed_state(self):
|
||||||
|
failed_run = create_run(self.alice, "Stub 失败运行")
|
||||||
|
failed_gateway = StubAgentRunnerGateway(
|
||||||
|
AgentExecutionResult(False, error_code="model_error", error_summary="模型失败")
|
||||||
|
)
|
||||||
|
failed = execute_run(failed_run.id, failed_gateway)
|
||||||
|
self.assertEqual(failed.status, RunStatus.FAILED)
|
||||||
|
self.assertEqual(failed.error_code, "model_error")
|
||||||
|
|
||||||
|
exploding_run = create_run(self.alice, "Stub 异常运行")
|
||||||
|
exploded = execute_run(exploding_run.id, ExplodingGateway())
|
||||||
|
self.assertEqual(exploded.status, RunStatus.FAILED)
|
||||||
|
self.assertNotIn("should-not-be-stored", exploded.error_summary)
|
||||||
|
|
||||||
|
|
||||||
|
class AgentRuntimeConfigTests(SimpleTestCase):
|
||||||
|
"""验证真实执行配置只在调用边界检查,不影响其他 Django 功能。"""
|
||||||
|
|
||||||
|
@override_settings(OPENAI_API_KEY="", OPENAI_MODEL="", AGENT_DEFAULT_NAME="job_research")
|
||||||
|
def test_real_execution_requires_key_and_model(self):
|
||||||
|
with self.assertRaises(AgentConfigurationError):
|
||||||
|
AgentRuntimeConfig.from_settings().require_real_execution()
|
||||||
|
|
||||||
|
@override_settings(
|
||||||
|
OPENAI_API_KEY="test-key", OPENAI_MODEL="test-model", AGENT_DEFAULT_NAME="job_research"
|
||||||
|
)
|
||||||
|
def test_complete_configuration_is_accepted_without_external_call(self):
|
||||||
|
config = AgentRuntimeConfig.from_settings().require_real_execution()
|
||||||
|
self.assertEqual(config.model, "test-model")
|
||||||
|
|||||||
@@ -15,3 +15,7 @@ class DuplicateOperation(DomainError):
|
|||||||
|
|
||||||
class PermissionDenied(DomainError):
|
class PermissionDenied(DomainError):
|
||||||
"""操作者无权执行领域命令。"""
|
"""操作者无权执行领域命令。"""
|
||||||
|
|
||||||
|
|
||||||
|
class AgentConfigurationError(DomainError):
|
||||||
|
"""真实 Agent 执行所需配置缺失或不合法。"""
|
||||||
|
|||||||
+10
-1
@@ -12,6 +12,15 @@ request_id_context: contextvars.ContextVar[str] = contextvars.ContextVar(
|
|||||||
SENSITIVE_KEYS = {"api_key", "authorization", "cookie", "password", "secret", "token"}
|
SENSITIVE_KEYS = {"api_key", "authorization", "cookie", "password", "secret", "token"}
|
||||||
|
|
||||||
|
|
||||||
|
def _is_sensitive_key(key: object) -> bool:
|
||||||
|
"""识别常见秘密字段,同时避免把 `token_count` 等统计字段误判为秘密。"""
|
||||||
|
|
||||||
|
normalized = str(key).strip().lower().replace("-", "_")
|
||||||
|
if normalized in SENSITIVE_KEYS:
|
||||||
|
return True
|
||||||
|
return normalized.endswith(("_api_key", "_password", "_secret", "_cookie", "_token"))
|
||||||
|
|
||||||
|
|
||||||
class RequestContextFilter(logging.Filter):
|
class RequestContextFilter(logging.Filter):
|
||||||
"""向每条日志补充请求关联标识,后台任务没有请求时使用短横线。"""
|
"""向每条日志补充请求关联标识,后台任务没有请求时使用短横线。"""
|
||||||
|
|
||||||
@@ -25,7 +34,7 @@ def sanitize_summary(value: Any) -> Any:
|
|||||||
|
|
||||||
if isinstance(value, Mapping):
|
if isinstance(value, Mapping):
|
||||||
return {
|
return {
|
||||||
str(key): "***" if str(key).lower() in SENSITIVE_KEYS else sanitize_summary(item)
|
str(key): "***" if _is_sensitive_key(key) else sanitize_summary(item)
|
||||||
for key, item in value.items()
|
for key, item in value.items()
|
||||||
}
|
}
|
||||||
if isinstance(value, list):
|
if isinstance(value, list):
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
"""公共日志与请求上下文测试。"""
|
||||||
|
|
||||||
|
from django.test import SimpleTestCase
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from .logging import request_id_context, sanitize_summary
|
||||||
|
|
||||||
|
|
||||||
|
class LoggingTests(SimpleTestCase):
|
||||||
|
"""验证敏感摘要递归脱敏,且统计字段不会被误删。"""
|
||||||
|
|
||||||
|
def test_nested_sensitive_values_are_masked(self):
|
||||||
|
result = sanitize_summary(
|
||||||
|
{
|
||||||
|
"access_token": "secret",
|
||||||
|
"nested": {"password": "secret", "token_count": 12},
|
||||||
|
"items": [{"api-key": "secret"}],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.assertEqual(result["access_token"], "***")
|
||||||
|
self.assertEqual(result["nested"]["password"], "***")
|
||||||
|
self.assertEqual(result["nested"]["token_count"], 12)
|
||||||
|
self.assertEqual(result["items"][0]["api-key"], "***")
|
||||||
|
|
||||||
|
|
||||||
|
class RequestContextMiddlewareTests(SimpleTestCase):
|
||||||
|
"""验证入站关联标识的复用、非法值替换和请求结束后的上下文清理。"""
|
||||||
|
|
||||||
|
def test_valid_request_id_is_returned(self):
|
||||||
|
response = self.client.get(
|
||||||
|
reverse("accounts:login"),
|
||||||
|
headers={"X-Request-ID": "request-123"},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.headers["X-Request-ID"], "request-123")
|
||||||
|
self.assertEqual(request_id_context.get(), "-")
|
||||||
|
|
||||||
|
def test_invalid_request_id_is_replaced(self):
|
||||||
|
response = self.client.get(
|
||||||
|
reverse("accounts:login"),
|
||||||
|
headers={"X-Request-ID": "invalid value"},
|
||||||
|
)
|
||||||
|
generated = response.headers["X-Request-ID"]
|
||||||
|
self.assertNotEqual(generated, "invalid value")
|
||||||
|
self.assertEqual(len(generated), 32)
|
||||||
Reference in New Issue
Block a user