Skip to content

Commit

Permalink
Replace httpretty with mocket
Browse files Browse the repository at this point in the history
httpretty has not been actively maintained for a while (there are a lot
of outstanding PRs and issues, and the last commit on main was 3 years
ago). We're already pinning it because of a bug, and it's now
incompatible with the latest version of urllib3
gabrielfalcao/HTTPretty#484

mocket does all the same things as httpretty and more (it does have a
plugin that aims to be a dropin replacement for httpretty, but it
doesn't cover all the ways that we use it). In any case, replacing our
use of httpretty with mocket is quite straightforward.
  • Loading branch information
rebkwok committed Jan 10, 2025
1 parent ff6a06a commit 5b7e82e
Show file tree
Hide file tree
Showing 8 changed files with 116 additions and 95 deletions.
3 changes: 1 addition & 2 deletions requirements.dev.in
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,4 @@ pytest
pytest-cov
pytest-env
pytest-freezer
# pinning to 1.0.5 pending this issue https://github.com/gabrielfalcao/HTTPretty/issues/425
httpretty==1.0.5
mocket
25 changes: 22 additions & 3 deletions requirements.dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ coverage[toml]==7.6.1 \
--hash=sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234 \
--hash=sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc
# via pytest-cov
decorator==5.1.1 \
--hash=sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330 \
--hash=sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186
# via mocket
distlib==0.3.8 \
--hash=sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784 \
--hash=sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64
Expand All @@ -104,9 +108,10 @@ freezegun==1.5.1 \
--hash=sha256:b29dedfcda6d5e8e083ce71b2b542753ad48cfec44037b3fc79702e2980a89e9 \
--hash=sha256:bf111d7138a8abe55ab48a71755673dbaa4ab87f4cff5634a4442dfec34c15f1
# via pytest-freezer
httpretty==1.0.5 \
--hash=sha256:e53c927c4d3d781a0761727f1edfad64abef94e828718e12b672a678a8b3e0b5
# via -r requirements.dev.in
h11==0.14.0 \
--hash=sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d \
--hash=sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761
# via mocket
identify==2.6.0 \
--hash=sha256:cb171c685bdc31bcc4c1734698736a7d5b6c8bf2e0c15117f4d469c8640ae5cf \
--hash=sha256:e79ae4406387a9d300332b5fd366d8994f1525e8414984e1a59e058b2eda2dd0
Expand All @@ -115,6 +120,10 @@ iniconfig==2.0.0 \
--hash=sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3 \
--hash=sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374
# via pytest
mocket==3.13.2 \
--hash=sha256:1a6b3658e668c2bc1fe8442df7840b5b77318544f5c157a9952776b17ebff2a2 \
--hash=sha256:e3b61cb2af2aa696a877b1d2723c0efebbb768e8dc20f076ff2da8d0421742c0
# via -r requirements.dev.in
nodeenv==1.9.1 \
--hash=sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f \
--hash=sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9
Expand Down Expand Up @@ -142,6 +151,10 @@ pre-commit==4.0.1 \
--hash=sha256:80905ac375958c0444c65e9cebebd948b3cdb518f335a091a670a89d652139d2 \
--hash=sha256:efde913840816312445dc98787724647c65473daefe420785f885e8ed9a06878
# via -r requirements.dev.in
puremagic==1.28 \
--hash=sha256:195893fc129657f611b86b959aab337207d6df7f25372209269ed9e303c1a8c0 \
--hash=sha256:e16cb9708ee2007142c37931c58f07f7eca956b3472489106a7245e5c3aa1241
# via mocket
pyproject-hooks==1.1.0 \
--hash=sha256:4b37730834edbd6bd37f26ece6b44802fb1c1ee2ece0e54ddff8bfc06db86965 \
--hash=sha256:7ceeefe9aec63a1064c18d939bdc3adf2d8aa1988a510afec15151578b232aa2
Expand Down Expand Up @@ -253,6 +266,12 @@ six==1.16.0 \
# via
# -c requirements.prod.txt
# python-dateutil
urllib3==2.2.2 \
--hash=sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472 \
--hash=sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168
# via
# -c requirements.prod.txt
# mocket
virtualenv==20.26.3 \
--hash=sha256:4c43a2a236279d9ea36a0d76f98d84bd6ca94ac4e0f4a3b9d46d05e10fea542a \
--hash=sha256:8cc4a31139e796e9a7de2cd5cf2489de1217193116a8fd42328f1bd65f434589
Expand Down
32 changes: 17 additions & 15 deletions tests/mock_http_request.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import json
from urllib.parse import parse_qs

import httpretty
from mocket import Mocket
from mocket.mockhttp import Entry

from .time_helpers import TS

Expand Down Expand Up @@ -43,7 +45,7 @@
}


def httpretty_register(responses_dict):
def mocket_register(responses_dict):
"""
A helper to register slack URIs
Called with a dict of endpoints and mock response json:
Expand All @@ -62,14 +64,11 @@ def httpretty_register(responses_dict):
a different response each time.
"""
for endpoint, responses in responses_dict.items():
httpretty.register_uri(
httpretty.POST,
Entry.register(
Entry.POST,
f"https://slack.com/api/{endpoint}",
responses=[
httpretty.Response(body=json.dumps(response)) for response in responses
],
*[json.dumps(response) for response in responses],
)
return httpretty


def register_bot_uris():
Expand All @@ -81,7 +80,7 @@ def register_bot_uris():
Tests that need this method will need to register it themselves in order
to ensure it returns the expected user(s).
"""
httpretty_register(
mocket_register(
{
# authenticate
"auth.test": [{"ok": True}],
Expand Down Expand Up @@ -140,7 +139,7 @@ def register_dispatcher_uris():
the output or to report success. If a job fails, it will also call
getPermalink to get the message's URL in order to report to tech-support.
"""
httpretty_register(
mocket_register(
{
"chat.postMessage": [{"ok": True, "ts": TS, "channel": "channel"}],
"chat.getPermalink": [
Expand All @@ -154,7 +153,7 @@ def register_dispatcher_uris():
def get_mock_received_requests():
"""
Return a dict of {apipath: body} for each request received by
httpretty.
mocket.
Note that the slack_sdk uses params for its api calls for most methods.
It uses json for calls that use (or can use) blocks. For our purposes, this
if just the chat.postMessage calls.
Expand All @@ -167,9 +166,12 @@ def get_mock_received_requests():
https://github.com/slackapi/python-slack-sdk/blob/c47ea206491ca3e0be48749169041cf84925acd0/slack_sdk/web/client.py#L2709
"""
requests_by_path = {}
for request in httpretty.latest_requests():
body = request.parsed_body
if request.path == "/api/chat.postMessage":
body = json.loads(body)
for request in Mocket.request_list():
if request.headers["content-length"] == "0":
body = ""
elif request.path == "/api/chat.postMessage":
body = json.loads(request.body)
else:
body = parse_qs(request.body)
requests_by_path.setdefault(request.path, []).append(body)
return requests_by_path
29 changes: 13 additions & 16 deletions tests/test_bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
from datetime import datetime, timedelta
from unittest.mock import Mock, patch

import httpretty
import pytest
from mocket import Mocketizer, mocketize
from slack_bolt import App
from slack_bolt.request import BoltRequest
from slack_sdk.errors import SlackApiError
Expand All @@ -21,7 +21,12 @@
assert_suppression_matches,
)
from .job_configs import config
from .mock_http_request import USERS, get_mock_received_requests, register_bot_uris
from .mock_http_request import (
USERS,
get_mock_received_requests,
mocket_register,
register_bot_uris,
)
from .time_helpers import T0, TS, T


Expand All @@ -40,12 +45,10 @@ def get_mock_app():

@pytest.fixture
def mock_app():
httpretty.enable(allow_net_connect=False)
register_bot_uris()
mock_app = get_mock_app()
yield mock_app
httpretty.disable()
httpretty.reset()
with Mocketizer(strict_mode=True):
mock_app = get_mock_app()
yield mock_app


def test_joined_channels(mock_app):
Expand All @@ -56,7 +59,7 @@ def test_joined_channels(mock_app):
)


@httpretty.activate(allow_net_connect=False)
@mocketize(strict_mode=True)
@pytest.mark.parametrize(
"setting,value",
[
Expand All @@ -81,7 +84,7 @@ def test_register_listeners_support_channel_settings(setting, value):
bot.register_listeners(app, config, channels, bot_user_id, internal_user_ids)


@httpretty.activate(allow_net_connect=False)
@mocketize(strict_mode=True)
@pytest.mark.parametrize(
"setting,value",
[
Expand Down Expand Up @@ -840,13 +843,7 @@ def test_remove_non_existent_job(mock_app):
def test_restricted_jobs_only_scheduled_for_internal_users(
mock_app, user, command, reaction_count, scheduled_job_count
):
httpretty.register_uri(
httpretty.POST,
"https://slack.com/api/users.info",
responses=[
httpretty.Response(body=json.dumps({"ok": True, "user": USERS[user]}))
],
)
mocket_register({"users.info": [{"ok": True, "user": USERS[user]}]})

assert not scheduler.get_jobs()
handle_message(
Expand Down
31 changes: 17 additions & 14 deletions tests/test_dispatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
from pathlib import Path
from unittest.mock import Mock, patch

import httpretty
import pytest
from mocket import Mocket, Mocketizer
from mocket.mockhttp import Entry

from bennettbot import scheduler, settings
from bennettbot.dispatcher import JobDispatcher, MessageChecker, run_once
Expand All @@ -16,7 +17,7 @@
from .job_configs import config
from .mock_http_request import (
get_mock_received_requests,
httpretty_register,
mocket_register,
register_dispatcher_uris,
)
from .time_helpers import T0, TS, T
Expand All @@ -33,11 +34,9 @@ def remove_logs_dir():

@pytest.fixture(autouse=True)
def mock_http():
httpretty.enable(allow_net_connect=False)
register_dispatcher_uris()
yield
httpretty.disable()
httpretty.reset()
with Mocketizer(strict_mode=True):
yield


def test_run_once():
Expand Down Expand Up @@ -168,7 +167,11 @@ def test_job_success_with_slack_exception():
# Test that the job still succeeds even if notifying slack errors
# We mock the MAX_SLACK_NOTIFY_RETRIES so that this test doesn't do the
# (time-consuming) retrying in slack.py
httpretty_register(

# reset Mocket so we can override the chat.postMessage set in the
# autoused mock_http fixture
Mocket.reset()
mocket_register(
{"chat.postMessage": [{"ok": False, "error": "error"}]},
)

Expand Down Expand Up @@ -476,7 +479,7 @@ def test_job_with_code_format():


def test_job_with_long_code_output_is_uploaded_as_file():
httpretty_register(
mocket_register(
{
"files.getUploadURLExternal": [
{
Expand All @@ -490,8 +493,8 @@ def test_job_with_long_code_output_is_uploaded_as_file():
],
}
)
httpretty.register_uri(
httpretty.POST,
Entry.single_register(
Entry.POST,
"https://files.example.com/upload/v1/ABC123",
)

Expand Down Expand Up @@ -528,7 +531,7 @@ def build_log_dir(job_type_with_namespace):

def test_message_checker_run(freezer):
freezer.move_to("2024-10-08 23:30")
httpretty_register(
mocket_register(
{
"search.messages": [
{"ok": True, "messages": {"matches": []}},
Expand All @@ -544,7 +547,7 @@ def test_message_checker_run(freezer):

# search.messages is called twice for each run of the checker
# no matches, so no reactions or messages reposted.
assert len(httpretty.latest_requests()) == 4
assert len(Mocket.request_list()) == 4
requests_by_path = get_mock_received_requests()
last_search_query = requests_by_path["/api/search.messages"][-1]["query"][0]
assert "after:2024-10-06" in last_search_query
Expand All @@ -558,7 +561,7 @@ def test_message_checker_run(freezer):
),
)
def test_message_checker_matched_messages(keyword, support_channel, reaction):
httpretty_register(
mocket_register(
{
"search.messages": [
{
Expand Down Expand Up @@ -599,7 +602,7 @@ def test_message_checker_matched_messages(keyword, support_channel, reaction):
# search.messages is called once
# other 3 endpoints called once each for 2 matched messages requiring
# reaction and reposting.
assert len(httpretty.latest_requests()) == 7
assert len(Mocket.request_list()) == 7

requests_by_path = get_mock_received_requests()
assert requests_by_path["/api/search.messages"] == [
Expand Down
Loading

0 comments on commit 5b7e82e

Please sign in to comment.