1
0

test_parser_agent.py 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  1. """ParserAgent 解析 OpenAPI 文档的单元测试"""
  2. import json
  3. from pathlib import Path
  4. from src.agents.parser_agent import ParserAgent
  5. def _parser():
  6. return ParserAgent()
  7. # --- parse_text:空输入 / JSON / YAML / 非 dict ---
  8. def test_parse_text_empty():
  9. assert _parser().parse_text("") == []
  10. assert _parser().parse_text(" ") == []
  11. def test_parse_text_json():
  12. doc = '{"openapi": "3.0.0", "paths": {"/users": {"get": {"summary": "list", "responses": {"200": {"description": "ok"}}}}}}'
  13. endpoints = _parser().parse_text(doc)
  14. assert len(endpoints) == 1
  15. assert endpoints[0]["path"] == "/users"
  16. assert endpoints[0]["method"] == "GET"
  17. def test_parse_text_yaml():
  18. doc = """
  19. openapi: 3.0.0
  20. paths:
  21. /users:
  22. get:
  23. summary: list users
  24. responses:
  25. '200':
  26. description: ok
  27. """
  28. endpoints = _parser().parse_text(doc)
  29. assert len(endpoints) == 1
  30. assert endpoints[0]["method"] == "GET"
  31. def test_parse_text_non_dict():
  32. # "null" 会被解析成 None,应返回空列表而不是崩溃
  33. assert _parser().parse_text("null") == []
  34. # --- extract_endpoints:类型容错 + requestBody ---
  35. def test_extract_endpoints_non_dict_input():
  36. p = _parser()
  37. assert p.extract_endpoints(None) == []
  38. assert p.extract_endpoints("not a dict") == []
  39. assert p.extract_endpoints([]) == []
  40. def test_extract_endpoints_request_body_and_content_type():
  41. doc = {
  42. "paths": {
  43. "/create": {
  44. "post": {
  45. "requestBody": {
  46. "content": {
  47. "application/json": {
  48. "schema": {
  49. "type": "object",
  50. "required": ["name"],
  51. "properties": {"name": {"type": "string"}},
  52. }
  53. }
  54. }
  55. },
  56. "responses": {"200": {}},
  57. }
  58. }
  59. }
  60. }
  61. endpoints = _parser().extract_endpoints(doc)
  62. assert len(endpoints) == 1
  63. ep = endpoints[0]
  64. assert ep["method"] == "POST"
  65. # 请求体被解析成真正的 schema,而不是原始 requestBody 对象
  66. assert ep["request_body"] == {
  67. "type": "object",
  68. "required": ["name"],
  69. "properties": {"name": {"type": "string"}},
  70. }
  71. assert ep["request_content_type"] == "application/json"
  72. # --- $ref 展开 ---
  73. def _ref_doc():
  74. """一个带 components/schemas 和 $ref 的最小文档"""
  75. return {
  76. "components": {
  77. "schemas": {
  78. "User": {
  79. "type": "object",
  80. "required": ["username", "password"],
  81. "properties": {
  82. "username": {"type": "string"},
  83. "password": {"type": "string"},
  84. },
  85. },
  86. "Nested": {
  87. "type": "object",
  88. "properties": {"user": {"$ref": "#/components/schemas/User"}},
  89. },
  90. }
  91. },
  92. "paths": {
  93. "/register": {
  94. "post": {
  95. "requestBody": {
  96. "content": {
  97. "application/json": {
  98. "schema": {"$ref": "#/components/schemas/User"},
  99. }
  100. }
  101. },
  102. "responses": {
  103. "201": {
  104. "content": {
  105. "application/json": {
  106. "schema": {"$ref": "#/components/schemas/Nested"},
  107. }
  108. }
  109. }
  110. },
  111. }
  112. }
  113. },
  114. }
  115. def test_request_body_ref_resolved():
  116. endpoints = _parser().extract_endpoints(_ref_doc())
  117. ep = endpoints[0]
  118. # $ref 被展开,字段直接可见
  119. assert ep["request_body"]["required"] == ["username", "password"]
  120. assert "username" in ep["request_body"]["properties"]
  121. def test_response_schema_ref_resolved():
  122. endpoints = _parser().extract_endpoints(_ref_doc())
  123. ep = endpoints[0]
  124. # 主成功响应(201)的 schema 被展开,且嵌套 $ref 也递归展开
  125. assert ep["response_schema"]["properties"]["user"]["type"] == "object"
  126. assert "username" in ep["response_schema"]["properties"]["user"]["properties"]
  127. def test_get_response_schema_by_status():
  128. endpoints = _parser().extract_endpoints(_ref_doc())
  129. ep = endpoints[0]
  130. assert _parser().get_response_schema(ep, 201)["properties"]["user"]["type"] == "object"
  131. # 没有声明的状态码返回 None
  132. assert _parser().get_response_schema(ep, 404) is None
  133. # --- get_expected_status ---
  134. def test_expected_status_normal_prefers_2xx():
  135. ep = {"responses": {"200": {}, "400": {}}}
  136. assert _parser().get_expected_status(ep, "normal") == 200
  137. def test_expected_status_boundary_prefers_2xx():
  138. ep = {"responses": {"201": {}, "422": {}}}
  139. assert _parser().get_expected_status(ep, "boundary") == 201
  140. def test_expected_status_error_prefers_4xx():
  141. ep = {"responses": {"200": {}, "400": {}}}
  142. assert _parser().get_expected_status(ep, "error") == 400
  143. def test_expected_status_error_with_body_validation():
  144. # 有必填请求体字段 → 缺字段 → 校验失败 422
  145. ep = {
  146. "responses": {"201": {}, "422": {}},
  147. "request_body": {"required": ["name"], "properties": {"name": {"type": "string"}}},
  148. "parameters": [],
  149. }
  150. assert _parser().get_expected_status(ep, "error") == 422
  151. def test_expected_status_error_with_only_path_param():
  152. # 只有必填路径参数 → 缺路径参数 → 路由不匹配 404(而非 422)
  153. ep = {
  154. "responses": {"200": {}, "422": {}},
  155. "request_body": None,
  156. "parameters": [{"name": "id", "in": "path", "required": True, "schema": {"type": "string"}}],
  157. }
  158. assert _parser().get_expected_status(ep, "error") == 404
  159. def test_expected_status_default_200_when_no_responses():
  160. assert _parser().get_expected_status({"responses": {}}, "normal") == 200
  161. # --- 结构判断辅助 ---
  162. def test_has_testable_inputs():
  163. # 没参数没请求体 → 无可测输入
  164. assert _parser().has_testable_inputs({"parameters": [], "request_body": None}) is False
  165. # 有请求体 → 可测
  166. assert _parser().has_testable_inputs({"parameters": [], "request_body": {"type": "object"}}) is True
  167. # 有 query 参数 → 可测
  168. assert _parser().has_testable_inputs(
  169. {"parameters": [{"in": "query", "name": "limit"}], "request_body": None}
  170. ) is True
  171. def test_has_validation_input():
  172. assert _parser().has_validation_input(
  173. {"parameters": [], "request_body": {"required": ["name"]}}
  174. ) is True
  175. assert _parser().has_validation_input({"parameters": [], "request_body": None}) is False
  176. def test_has_required_path_param():
  177. assert _parser().has_required_path_param(
  178. {"parameters": [{"in": "path", "name": "id", "required": True}]}
  179. ) is True
  180. assert _parser().has_required_path_param(
  181. {"parameters": [{"in": "query", "name": "limit"}]}
  182. ) is False
  183. # --- parse_file ---
  184. def test_parse_file(tmp_path):
  185. f = tmp_path / "api.yaml"
  186. f.write_text(
  187. "openapi: 3.0.0\npaths:\n /a:\n get:\n responses:\n '200': {}\n",
  188. encoding="utf-8",
  189. )
  190. endpoints = _parser().parse_file(str(f))
  191. assert len(endpoints) == 1
  192. assert endpoints[0]["path"] == "/a"
  193. # --- 真实 Chat openapi.json 集成解析 ---
  194. def _chat_endpoints():
  195. fixture = Path(__file__).parent / "fixtures" / "chat_openapi.json"
  196. doc = json.loads(fixture.read_text(encoding="utf-8"))
  197. return _parser().extract_endpoints(doc)
  198. def test_chat_register_body_ref_resolved():
  199. ep = next(e for e in _chat_endpoints() if e["path"] == "/api/auth/register")
  200. # 注册请求体应能看到真实字段,而不是 $ref 字符串
  201. assert ep["request_body"]["required"] == [
  202. "username", "display_name", "password", "confirm_password"
  203. ]
  204. assert set(ep["request_body"]["properties"]) == {
  205. "username", "display_name", "password", "confirm_password"
  206. }
  207. def test_chat_login_response_schema_resolved():
  208. ep = next(e for e in _chat_endpoints() if e["path"] == "/api/auth/login")
  209. # 登录成功响应的 schema 展开后应包含 access_token 字段
  210. assert "access_token" in ep["response_schema"]["properties"]
  211. def test_chat_health_has_no_testable_inputs():
  212. ep = next(e for e in _chat_endpoints() if e["path"] == "/api/health")
  213. assert _parser().has_testable_inputs(ep) is False
  214. assert _parser().has_validation_input(ep) is False
  215. assert _parser().has_required_path_param(ep) is False
  216. def test_chat_files_images_is_multipart():
  217. ep = next(e for e in _chat_endpoints() if e["path"] == "/api/files/images")
  218. assert ep["request_content_type"] == "multipart/form-data"
  219. # 文件字段用 contentMediaType 标识
  220. assert "contentMediaType" in ep["request_body"]["properties"]["file"]