from __future__ import annotations from typing import Any from unittest.mock import Mock, patch import pycurl from django.test import SimpleTestCase from django.test.utils import override_settings from hc.lib.curl import CurlError, request class FakeCurl: def __init__(self, ip: str = "1.2.3.4") -> None: self.opts: dict[int, Any] = {} self.ip = ip def setopt(self, k: int, v: Any) -> None: self.opts[k] = v def perform(self) -> None: if pycurl.OPENSOCKETFUNCTION in self.opts: # Simulate what libcurl would be doing here: # - if OPENSOCKETFUNCTION is defined, call it and pass it the ip address # - if the function returns pycurl.SOCKET_BAD, raise an error # # This is needed for test cases that exercise the # INTEGRATIONS_ALLOW_PRIVATE_IPS setting. callback = self.opts[pycurl.OPENSOCKETFUNCTION] address = (self.ip, 80) with patch("hc.lib.curl.socket"): sock = callback(pycurl.SOCKTYPE_IPCXN, (None, None, None, address)) if sock == pycurl.SOCKET_BAD: raise pycurl.error(pycurl.E_COULDNT_CONNECT) if pycurl.WRITEDATA in self.opts: self.opts[pycurl.WRITEDATA].write(b"hello world") def getinfo(self, _: int) -> int: return 200 def close(self) -> None: pass class CurlTestCase(SimpleTestCase): @patch("hc.lib.curl.pycurl.Curl") def test_get_works(self, mock: Mock) -> None: mock.return_value = obj = FakeCurl() response = request("get", "http://example.org") # URL should have been encoded to bytes self.assertEqual(obj.opts[pycurl.URL], b"http://example.org") # Default user agent self.assertEqual(obj.opts[pycurl.HTTPHEADER], [b"User-Agent:healthchecks.io"]) # It should allow redirects self.assertEqual(obj.opts[pycurl.FOLLOWLOCATION], True) self.assertEqual(obj.opts[pycurl.MAXREDIRS], 3) self.assertEqual(response.text, "hello world") @patch("hc.lib.curl.pycurl.Curl") def test_it_handles_params(self, mock: Mock) -> None: mock.return_value = obj = FakeCurl() request("get", "http://example.org", params={"a": "b", "c": "d"}) self.assertEqual(obj.opts[pycurl.URL], b"http://example.org?a=b&c=d") @patch("hc.lib.curl.pycurl.Curl") def test_it_handles_auth(self, mock: Mock) -> None: mock.return_value = obj = FakeCurl() request("get", "http://example.org", auth=("alice", "pass")) self.assertEqual(obj.opts[pycurl.USERPWD], "alice:pass") @patch("hc.lib.curl.pycurl.Curl") def test_it_allows_custom_ua(self, mock: Mock) -> None: mock.return_value = obj = FakeCurl() request("get", "http://example.org", headers={"User-Agent": "my-ua"}) # The custom UA should override the default one self.assertEqual(obj.opts[pycurl.HTTPHEADER], [b"User-Agent:my-ua"]) @patch("hc.lib.curl.pycurl.Curl") def test_it_encodes_header_values_to_latin1(self, mock: Mock) -> None: mock.return_value = obj = FakeCurl() request("get", "http://example.org", headers={"User-Agent": "À"}) self.assertEqual(obj.opts[pycurl.HTTPHEADER], [b"User-Agent:\xc0"]) @patch("hc.lib.curl.pycurl.Curl") def test_it_sets_timeout(self, mock: Mock) -> None: mock.return_value = obj = FakeCurl() request("get", "http://example.org", timeout=15) self.assertEqual(obj.opts[pycurl.TIMEOUT], 15) @patch("hc.lib.curl.pycurl.Curl") def test_it_posts_form(self, mock: Mock) -> None: mock.return_value = obj = FakeCurl() request("post", "http://example.org", data={"a": "b", "c": "d"}) self.assertEqual(obj.opts[pycurl.CUSTOMREQUEST], "POST") self.assertEqual(obj.opts[pycurl.POSTFIELDS], "a=b&c=d") @patch("hc.lib.curl.pycurl.Curl") def test_it_posts_str(self, mock: Mock) -> None: mock.return_value = obj = FakeCurl() request("post", "http://example.org", data="hello") self.assertEqual(obj.opts[pycurl.CUSTOMREQUEST], "POST") self.assertEqual(obj.opts[pycurl.READDATA].getvalue(), b"hello") self.assertEqual(obj.opts[pycurl.INFILESIZE], 5) @patch("hc.lib.curl.pycurl.Curl") def test_it_posts_bytes(self, mock: Mock) -> None: mock.return_value = obj = FakeCurl() request("post", "http://example.org", data=b"hello") self.assertEqual(obj.opts[pycurl.CUSTOMREQUEST], "POST") self.assertEqual(obj.opts[pycurl.READDATA].getvalue(), b"hello") self.assertEqual(obj.opts[pycurl.INFILESIZE], 5) @patch("hc.lib.curl.pycurl.Curl") def test_it_posts_json(self, mock: Mock) -> None: mock.return_value = obj = FakeCurl() request("post", "http://example.org", json=[1, 2, 3]) self.assertEqual(obj.opts[pycurl.CUSTOMREQUEST], "POST") self.assertEqual(obj.opts[pycurl.READDATA].getvalue(), b"[1, 2, 3]") self.assertEqual(obj.opts[pycurl.INFILESIZE], 9) @patch("hc.lib.curl.pycurl.Curl") def test_it_puts_form(self, mock: Mock) -> None: mock.return_value = obj = FakeCurl() request("put", "http://example.org", data={"a": "b", "c": "d"}) self.assertEqual(obj.opts[pycurl.CUSTOMREQUEST], "PUT") self.assertEqual(obj.opts[pycurl.POSTFIELDS], "a=b&c=d") @patch("hc.lib.curl.pycurl.Curl") def test_it_puts_str(self, mock: Mock) -> None: mock.return_value = obj = FakeCurl() request("put", "http://example.org", data="hello") self.assertEqual(obj.opts[pycurl.CUSTOMREQUEST], "PUT") self.assertEqual(obj.opts[pycurl.READDATA].getvalue(), b"hello") self.assertEqual(obj.opts[pycurl.INFILESIZE], 5) @patch("hc.lib.curl.pycurl.Curl") def test_it_puts_bytes(self, mock: Mock) -> None: mock.return_value = obj = FakeCurl() request("put", "http://example.org", data=b"hello") self.assertEqual(obj.opts[pycurl.CUSTOMREQUEST], "PUT") self.assertEqual(obj.opts[pycurl.READDATA].getvalue(), b"hello") self.assertEqual(obj.opts[pycurl.INFILESIZE], 5) @patch("hc.lib.curl.pycurl.Curl") def test_it_puts_json(self, mock: Mock) -> None: mock.return_value = obj = FakeCurl() request("put", "http://example.org", json=[1, 2, 3]) self.assertEqual(obj.opts[pycurl.CUSTOMREQUEST], "PUT") self.assertEqual(obj.opts[pycurl.READDATA].getvalue(), b"[1, 2, 3]") self.assertEqual(obj.opts[pycurl.INFILESIZE], 9) @override_settings(INTEGRATIONS_ALLOW_PRIVATE_IPS=False) @patch("hc.lib.curl.pycurl.Curl") def test_it_rejects_private_ip(self, mock: Mock) -> None: mock.return_value = FakeCurl(ip="127.0.0.1") with self.assertRaises(CurlError) as cm: request("get", "http://example.org") self.assertEqual( cm.exception.message, "Connections to private IP addresses are not allowed", ) @override_settings(INTEGRATIONS_ALLOW_PRIVATE_IPS=True) @patch("hc.lib.curl.pycurl.Curl") def test_it_accepts_private_ip(self, mock: Mock) -> None: mock.return_value = FakeCurl(ip="127.0.0.1") request("get", "http://example.org")