You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

test_http.py 21KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698
  1. import time
  2. import uuid
  3. from urllib.parse import urlencode
  4. import pytest
  5. from core.app.entities.app_invoke_entities import InvokeFrom
  6. from core.workflow.entities.variable_pool import VariablePool
  7. from core.workflow.graph_engine.entities.graph import Graph
  8. from core.workflow.graph_engine.entities.graph_init_params import GraphInitParams
  9. from core.workflow.graph_engine.entities.graph_runtime_state import GraphRuntimeState
  10. from core.workflow.nodes.http_request.node import HttpRequestNode
  11. from core.workflow.system_variable import SystemVariable
  12. from models.enums import UserFrom
  13. from models.workflow import WorkflowType
  14. from tests.integration_tests.workflow.nodes.__mock.http import setup_http_mock
  15. def init_http_node(config: dict):
  16. graph_config = {
  17. "edges": [
  18. {
  19. "id": "start-source-next-target",
  20. "source": "start",
  21. "target": "1",
  22. },
  23. ],
  24. "nodes": [{"data": {"type": "start"}, "id": "start"}, config],
  25. }
  26. graph = Graph.init(graph_config=graph_config)
  27. init_params = GraphInitParams(
  28. tenant_id="1",
  29. app_id="1",
  30. workflow_type=WorkflowType.WORKFLOW,
  31. workflow_id="1",
  32. graph_config=graph_config,
  33. user_id="1",
  34. user_from=UserFrom.ACCOUNT,
  35. invoke_from=InvokeFrom.DEBUGGER,
  36. call_depth=0,
  37. )
  38. # construct variable pool
  39. variable_pool = VariablePool(
  40. system_variables=SystemVariable(user_id="aaa", files=[]),
  41. user_inputs={},
  42. environment_variables=[],
  43. conversation_variables=[],
  44. )
  45. variable_pool.add(["a", "args1"], 1)
  46. variable_pool.add(["a", "args2"], 2)
  47. node = HttpRequestNode(
  48. id=str(uuid.uuid4()),
  49. graph_init_params=init_params,
  50. graph=graph,
  51. graph_runtime_state=GraphRuntimeState(variable_pool=variable_pool, start_at=time.perf_counter()),
  52. config=config,
  53. )
  54. # Initialize node data
  55. if "data" in config:
  56. node.init_node_data(config["data"])
  57. return node
  58. @pytest.mark.parametrize("setup_http_mock", [["none"]], indirect=True)
  59. def test_get(setup_http_mock):
  60. node = init_http_node(
  61. config={
  62. "id": "1",
  63. "data": {
  64. "title": "http",
  65. "desc": "",
  66. "method": "get",
  67. "url": "http://example.com",
  68. "authorization": {
  69. "type": "api-key",
  70. "config": {
  71. "type": "basic",
  72. "api_key": "ak-xxx",
  73. "header": "api-key",
  74. },
  75. },
  76. "headers": "X-Header:123",
  77. "params": "A:b",
  78. "body": None,
  79. },
  80. }
  81. )
  82. result = node._run()
  83. assert result.process_data is not None
  84. data = result.process_data.get("request", "")
  85. assert "?A=b" in data
  86. assert "X-Header: 123" in data
  87. @pytest.mark.parametrize("setup_http_mock", [["none"]], indirect=True)
  88. def test_no_auth(setup_http_mock):
  89. node = init_http_node(
  90. config={
  91. "id": "1",
  92. "data": {
  93. "title": "http",
  94. "desc": "",
  95. "method": "get",
  96. "url": "http://example.com",
  97. "authorization": {
  98. "type": "no-auth",
  99. "config": None,
  100. },
  101. "headers": "X-Header:123",
  102. "params": "A:b",
  103. "body": None,
  104. },
  105. }
  106. )
  107. result = node._run()
  108. assert result.process_data is not None
  109. data = result.process_data.get("request", "")
  110. assert "?A=b" in data
  111. assert "X-Header: 123" in data
  112. @pytest.mark.parametrize("setup_http_mock", [["none"]], indirect=True)
  113. def test_custom_authorization_header(setup_http_mock):
  114. node = init_http_node(
  115. config={
  116. "id": "1",
  117. "data": {
  118. "title": "http",
  119. "desc": "",
  120. "method": "get",
  121. "url": "http://example.com",
  122. "authorization": {
  123. "type": "api-key",
  124. "config": {
  125. "type": "custom",
  126. "api_key": "Auth",
  127. "header": "X-Auth",
  128. },
  129. },
  130. "headers": "X-Header:123",
  131. "params": "A:b",
  132. "body": None,
  133. },
  134. }
  135. )
  136. result = node._run()
  137. assert result.process_data is not None
  138. data = result.process_data.get("request", "")
  139. assert "?A=b" in data
  140. assert "X-Header: 123" in data
  141. # Custom authorization header should be set (may be masked)
  142. assert "X-Auth:" in data
  143. @pytest.mark.parametrize("setup_http_mock", [["none"]], indirect=True)
  144. def test_custom_auth_with_empty_api_key_does_not_set_header(setup_http_mock):
  145. """Test: In custom authentication mode, when the api_key is empty, no header should be set."""
  146. from core.workflow.entities.variable_pool import VariablePool
  147. from core.workflow.nodes.http_request.entities import (
  148. HttpRequestNodeAuthorization,
  149. HttpRequestNodeData,
  150. HttpRequestNodeTimeout,
  151. )
  152. from core.workflow.nodes.http_request.executor import Executor
  153. from core.workflow.system_variable import SystemVariable
  154. # Create variable pool
  155. variable_pool = VariablePool(
  156. system_variables=SystemVariable(user_id="test", files=[]),
  157. user_inputs={},
  158. environment_variables=[],
  159. conversation_variables=[],
  160. )
  161. # Create node data with custom auth and empty api_key
  162. node_data = HttpRequestNodeData(
  163. title="http",
  164. desc="",
  165. url="http://example.com",
  166. method="get",
  167. authorization=HttpRequestNodeAuthorization(
  168. type="api-key",
  169. config={
  170. "type": "custom",
  171. "api_key": "", # Empty api_key
  172. "header": "X-Custom-Auth",
  173. },
  174. ),
  175. headers="",
  176. params="",
  177. body=None,
  178. ssl_verify=True,
  179. )
  180. # Create executor
  181. executor = Executor(
  182. node_data=node_data, timeout=HttpRequestNodeTimeout(connect=10, read=30, write=10), variable_pool=variable_pool
  183. )
  184. # Get assembled headers
  185. headers = executor._assembling_headers()
  186. # When api_key is empty, the custom header should NOT be set
  187. assert "X-Custom-Auth" not in headers
  188. @pytest.mark.parametrize("setup_http_mock", [["none"]], indirect=True)
  189. def test_bearer_authorization_with_custom_header_ignored(setup_http_mock):
  190. """
  191. Test that when switching from custom to bearer authorization,
  192. the custom header settings don't interfere with bearer token.
  193. This test verifies the fix for issue #23554.
  194. """
  195. node = init_http_node(
  196. config={
  197. "id": "1",
  198. "data": {
  199. "title": "http",
  200. "desc": "",
  201. "method": "get",
  202. "url": "http://example.com",
  203. "authorization": {
  204. "type": "api-key",
  205. "config": {
  206. "type": "bearer",
  207. "api_key": "test-token",
  208. "header": "", # Empty header - should default to Authorization
  209. },
  210. },
  211. "headers": "",
  212. "params": "",
  213. "body": None,
  214. },
  215. }
  216. )
  217. result = node._run()
  218. assert result.process_data is not None
  219. data = result.process_data.get("request", "")
  220. # In bearer mode, should use Authorization header (value is masked with *)
  221. assert "Authorization: " in data
  222. # Should contain masked Bearer token
  223. assert "*" in data
  224. @pytest.mark.parametrize("setup_http_mock", [["none"]], indirect=True)
  225. def test_basic_authorization_with_custom_header_ignored(setup_http_mock):
  226. """
  227. Test that when switching from custom to basic authorization,
  228. the custom header settings don't interfere with basic auth.
  229. This test verifies the fix for issue #23554.
  230. """
  231. node = init_http_node(
  232. config={
  233. "id": "1",
  234. "data": {
  235. "title": "http",
  236. "desc": "",
  237. "method": "get",
  238. "url": "http://example.com",
  239. "authorization": {
  240. "type": "api-key",
  241. "config": {
  242. "type": "basic",
  243. "api_key": "user:pass",
  244. "header": "", # Empty header - should default to Authorization
  245. },
  246. },
  247. "headers": "",
  248. "params": "",
  249. "body": None,
  250. },
  251. }
  252. )
  253. result = node._run()
  254. assert result.process_data is not None
  255. data = result.process_data.get("request", "")
  256. # In basic mode, should use Authorization header (value is masked with *)
  257. assert "Authorization: " in data
  258. # Should contain masked Basic credentials
  259. assert "*" in data
  260. @pytest.mark.parametrize("setup_http_mock", [["none"]], indirect=True)
  261. def test_custom_authorization_with_empty_api_key(setup_http_mock):
  262. """
  263. Test that custom authorization doesn't set header when api_key is empty.
  264. This test verifies the fix for issue #23554.
  265. """
  266. node = init_http_node(
  267. config={
  268. "id": "1",
  269. "data": {
  270. "title": "http",
  271. "desc": "",
  272. "method": "get",
  273. "url": "http://example.com",
  274. "authorization": {
  275. "type": "api-key",
  276. "config": {
  277. "type": "custom",
  278. "api_key": "", # Empty api_key
  279. "header": "X-Custom-Auth",
  280. },
  281. },
  282. "headers": "",
  283. "params": "",
  284. "body": None,
  285. },
  286. }
  287. )
  288. result = node._run()
  289. assert result.process_data is not None
  290. data = result.process_data.get("request", "")
  291. # Custom header should NOT be set when api_key is empty
  292. assert "X-Custom-Auth:" not in data
  293. @pytest.mark.parametrize("setup_http_mock", [["none"]], indirect=True)
  294. def test_template(setup_http_mock):
  295. node = init_http_node(
  296. config={
  297. "id": "1",
  298. "data": {
  299. "title": "http",
  300. "desc": "",
  301. "method": "get",
  302. "url": "http://example.com/{{#a.args2#}}",
  303. "authorization": {
  304. "type": "api-key",
  305. "config": {
  306. "type": "basic",
  307. "api_key": "ak-xxx",
  308. "header": "api-key",
  309. },
  310. },
  311. "headers": "X-Header:123\nX-Header2:{{#a.args2#}}",
  312. "params": "A:b\nTemplate:{{#a.args2#}}",
  313. "body": None,
  314. },
  315. }
  316. )
  317. result = node._run()
  318. assert result.process_data is not None
  319. data = result.process_data.get("request", "")
  320. assert "?A=b" in data
  321. assert "Template=2" in data
  322. assert "X-Header: 123" in data
  323. assert "X-Header2: 2" in data
  324. @pytest.mark.parametrize("setup_http_mock", [["none"]], indirect=True)
  325. def test_json(setup_http_mock):
  326. node = init_http_node(
  327. config={
  328. "id": "1",
  329. "data": {
  330. "title": "http",
  331. "desc": "",
  332. "method": "post",
  333. "url": "http://example.com",
  334. "authorization": {
  335. "type": "api-key",
  336. "config": {
  337. "type": "basic",
  338. "api_key": "ak-xxx",
  339. "header": "api-key",
  340. },
  341. },
  342. "headers": "X-Header:123",
  343. "params": "A:b",
  344. "body": {
  345. "type": "json",
  346. "data": [
  347. {
  348. "key": "",
  349. "type": "text",
  350. "value": '{"a": "{{#a.args1#}}"}',
  351. },
  352. ],
  353. },
  354. },
  355. }
  356. )
  357. result = node._run()
  358. assert result.process_data is not None
  359. data = result.process_data.get("request", "")
  360. assert '{"a": "1"}' in data
  361. assert "X-Header: 123" in data
  362. @pytest.mark.parametrize("setup_http_mock", [["none"]], indirect=True)
  363. def test_x_www_form_urlencoded(setup_http_mock):
  364. node = init_http_node(
  365. config={
  366. "id": "1",
  367. "data": {
  368. "title": "http",
  369. "desc": "",
  370. "method": "post",
  371. "url": "http://example.com",
  372. "authorization": {
  373. "type": "api-key",
  374. "config": {
  375. "type": "basic",
  376. "api_key": "ak-xxx",
  377. "header": "api-key",
  378. },
  379. },
  380. "headers": "X-Header:123",
  381. "params": "A:b",
  382. "body": {
  383. "type": "x-www-form-urlencoded",
  384. "data": [
  385. {
  386. "key": "a",
  387. "type": "text",
  388. "value": "{{#a.args1#}}",
  389. },
  390. {
  391. "key": "b",
  392. "type": "text",
  393. "value": "{{#a.args2#}}",
  394. },
  395. ],
  396. },
  397. },
  398. }
  399. )
  400. result = node._run()
  401. assert result.process_data is not None
  402. data = result.process_data.get("request", "")
  403. assert "a=1&b=2" in data
  404. assert "X-Header: 123" in data
  405. @pytest.mark.parametrize("setup_http_mock", [["none"]], indirect=True)
  406. def test_form_data(setup_http_mock):
  407. node = init_http_node(
  408. config={
  409. "id": "1",
  410. "data": {
  411. "title": "http",
  412. "desc": "",
  413. "method": "post",
  414. "url": "http://example.com",
  415. "authorization": {
  416. "type": "api-key",
  417. "config": {
  418. "type": "basic",
  419. "api_key": "ak-xxx",
  420. "header": "api-key",
  421. },
  422. },
  423. "headers": "X-Header:123",
  424. "params": "A:b",
  425. "body": {
  426. "type": "form-data",
  427. "data": [
  428. {
  429. "key": "a",
  430. "type": "text",
  431. "value": "{{#a.args1#}}",
  432. },
  433. {
  434. "key": "b",
  435. "type": "text",
  436. "value": "{{#a.args2#}}",
  437. },
  438. ],
  439. },
  440. },
  441. }
  442. )
  443. result = node._run()
  444. assert result.process_data is not None
  445. data = result.process_data.get("request", "")
  446. assert 'form-data; name="a"' in data
  447. assert "1" in data
  448. assert 'form-data; name="b"' in data
  449. assert "2" in data
  450. assert "X-Header: 123" in data
  451. @pytest.mark.parametrize("setup_http_mock", [["none"]], indirect=True)
  452. def test_none_data(setup_http_mock):
  453. node = init_http_node(
  454. config={
  455. "id": "1",
  456. "data": {
  457. "title": "http",
  458. "desc": "",
  459. "method": "post",
  460. "url": "http://example.com",
  461. "authorization": {
  462. "type": "api-key",
  463. "config": {
  464. "type": "basic",
  465. "api_key": "ak-xxx",
  466. "header": "api-key",
  467. },
  468. },
  469. "headers": "X-Header:123",
  470. "params": "A:b",
  471. "body": {"type": "none", "data": []},
  472. },
  473. }
  474. )
  475. result = node._run()
  476. assert result.process_data is not None
  477. data = result.process_data.get("request", "")
  478. assert "X-Header: 123" in data
  479. assert "123123123" not in data
  480. @pytest.mark.parametrize("setup_http_mock", [["none"]], indirect=True)
  481. def test_mock_404(setup_http_mock):
  482. node = init_http_node(
  483. config={
  484. "id": "1",
  485. "data": {
  486. "title": "http",
  487. "desc": "",
  488. "method": "get",
  489. "url": "http://404.com",
  490. "authorization": {
  491. "type": "no-auth",
  492. "config": None,
  493. },
  494. "body": None,
  495. "params": "",
  496. "headers": "X-Header:123",
  497. },
  498. }
  499. )
  500. result = node._run()
  501. assert result.outputs is not None
  502. resp = result.outputs
  503. assert resp.get("status_code") == 404
  504. assert "Not Found" in resp.get("body", "")
  505. @pytest.mark.parametrize("setup_http_mock", [["none"]], indirect=True)
  506. def test_multi_colons_parse(setup_http_mock):
  507. node = init_http_node(
  508. config={
  509. "id": "1",
  510. "data": {
  511. "title": "http",
  512. "desc": "",
  513. "method": "get",
  514. "url": "http://example.com",
  515. "authorization": {
  516. "type": "no-auth",
  517. "config": None,
  518. },
  519. "params": "Referer:http://example1.com\nRedirect:http://example2.com",
  520. "headers": "Referer:http://example3.com\nRedirect:http://example4.com",
  521. "body": {
  522. "type": "form-data",
  523. "data": [
  524. {
  525. "key": "Referer",
  526. "type": "text",
  527. "value": "http://example5.com",
  528. },
  529. {
  530. "key": "Redirect",
  531. "type": "text",
  532. "value": "http://example6.com",
  533. },
  534. ],
  535. },
  536. },
  537. }
  538. )
  539. result = node._run()
  540. assert result.process_data is not None
  541. assert result.outputs is not None
  542. assert urlencode({"Redirect": "http://example2.com"}) in result.process_data.get("request", "")
  543. assert 'form-data; name="Redirect"\r\n\r\nhttp://example6.com' in result.process_data.get("request", "")
  544. # resp = result.outputs
  545. # assert "http://example3.com" == resp.get("headers", {}).get("referer")
  546. @pytest.mark.parametrize("setup_http_mock", [["none"]], indirect=True)
  547. def test_nested_object_variable_selector(setup_http_mock):
  548. """Test variable selector functionality with nested object properties."""
  549. # Create independent test setup without affecting other tests
  550. graph_config = {
  551. "edges": [
  552. {
  553. "id": "start-source-next-target",
  554. "source": "start",
  555. "target": "1",
  556. },
  557. ],
  558. "nodes": [
  559. {"data": {"type": "start"}, "id": "start"},
  560. {
  561. "id": "1",
  562. "data": {
  563. "title": "http",
  564. "desc": "",
  565. "method": "get",
  566. "url": "http://example.com/{{#a.args2#}}/{{#a.args3.nested#}}",
  567. "authorization": {
  568. "type": "api-key",
  569. "config": {
  570. "type": "basic",
  571. "api_key": "ak-xxx",
  572. "header": "api-key",
  573. },
  574. },
  575. "headers": "X-Header:{{#a.args3.nested#}}",
  576. "params": "nested_param:{{#a.args3.nested#}}",
  577. "body": None,
  578. },
  579. },
  580. ],
  581. }
  582. graph = Graph.init(graph_config=graph_config)
  583. init_params = GraphInitParams(
  584. tenant_id="1",
  585. app_id="1",
  586. workflow_type=WorkflowType.WORKFLOW,
  587. workflow_id="1",
  588. graph_config=graph_config,
  589. user_id="1",
  590. user_from=UserFrom.ACCOUNT,
  591. invoke_from=InvokeFrom.DEBUGGER,
  592. call_depth=0,
  593. )
  594. # Create independent variable pool for this test only
  595. variable_pool = VariablePool(
  596. system_variables=SystemVariable(user_id="aaa", files=[]),
  597. user_inputs={},
  598. environment_variables=[],
  599. conversation_variables=[],
  600. )
  601. variable_pool.add(["a", "args1"], 1)
  602. variable_pool.add(["a", "args2"], 2)
  603. variable_pool.add(["a", "args3"], {"nested": "nested_value"}) # Only for this test
  604. node = HttpRequestNode(
  605. id=str(uuid.uuid4()),
  606. graph_init_params=init_params,
  607. graph=graph,
  608. graph_runtime_state=GraphRuntimeState(variable_pool=variable_pool, start_at=time.perf_counter()),
  609. config=graph_config["nodes"][1],
  610. )
  611. # Initialize node data
  612. if "data" in graph_config["nodes"][1]:
  613. node.init_node_data(graph_config["nodes"][1]["data"])
  614. result = node._run()
  615. assert result.process_data is not None
  616. data = result.process_data.get("request", "")
  617. # Verify nested object property is correctly resolved
  618. assert "/2/nested_value" in data # URL path should contain resolved nested value
  619. assert "X-Header: nested_value" in data # Header should contain nested value
  620. assert "nested_param=nested_value" in data # Param should contain nested value