mirror of
https://github.com/Grasscutters/mitmproxy.git
synced 2025-01-30 23:09:44 +00:00
Merge pull request #2003 from Kriechi/coverage++
test refactoring and coverage++
This commit is contained in:
commit
380ff50e57
@ -238,7 +238,7 @@ class Dumper:
|
||||
def websocket_message(self, f):
|
||||
if self.match(f):
|
||||
message = f.messages[-1]
|
||||
self.echo(message.info)
|
||||
self.echo(f.message_info(message))
|
||||
if self.flow_detail >= 3:
|
||||
self._echo_message(message)
|
||||
|
||||
|
@ -8,7 +8,7 @@ import types
|
||||
|
||||
from mitmproxy import exceptions
|
||||
from mitmproxy import ctx
|
||||
from mitmproxy import events
|
||||
from mitmproxy import eventsequence
|
||||
|
||||
|
||||
import watchdog.events
|
||||
@ -141,7 +141,7 @@ class Script:
|
||||
self.last_options = None
|
||||
self.should_reload = threading.Event()
|
||||
|
||||
for i in events.Events:
|
||||
for i in eventsequence.Events:
|
||||
if not hasattr(self, i):
|
||||
def mkprox():
|
||||
evt = i
|
||||
@ -211,7 +211,7 @@ class ScriptLoader:
|
||||
raise ValueError(str(e))
|
||||
sc.load_script()
|
||||
for f in flows:
|
||||
for evt, o in events.event_sequence(f):
|
||||
for evt, o in eventsequence.iterate(f):
|
||||
sc.run(evt, o)
|
||||
sc.done()
|
||||
return sc
|
||||
|
@ -1,6 +1,5 @@
|
||||
import time
|
||||
|
||||
import copy
|
||||
import os
|
||||
|
||||
from mitmproxy import stateobject
|
||||
@ -82,9 +81,6 @@ class ClientConnection(tcp.BaseHandler, stateobject.StateObject):
|
||||
tls_version=str,
|
||||
)
|
||||
|
||||
def copy(self):
|
||||
return copy.copy(self)
|
||||
|
||||
def send(self, message):
|
||||
if isinstance(message, list):
|
||||
message = b''.join(message)
|
||||
@ -222,9 +218,6 @@ class ServerConnection(tcp.TCPClient, stateobject.StateObject):
|
||||
via=None
|
||||
))
|
||||
|
||||
def copy(self):
|
||||
return copy.copy(self)
|
||||
|
||||
def connect(self):
|
||||
self.timestamp_start = time.time()
|
||||
tcp.TCPClient.connect(self)
|
||||
|
@ -37,7 +37,7 @@ Events = frozenset([
|
||||
])
|
||||
|
||||
|
||||
def event_sequence(f):
|
||||
def iterate(f):
|
||||
if isinstance(f, http.HTTPFlow):
|
||||
if f.request:
|
||||
yield "requestheaders", f
|
||||
@ -70,4 +70,4 @@ def event_sequence(f):
|
||||
yield "tcp_error", f
|
||||
yield "tcp_end", f
|
||||
else:
|
||||
raise NotImplementedError
|
||||
raise TypeError()
|
@ -1,5 +1,4 @@
|
||||
import time
|
||||
import copy
|
||||
import uuid
|
||||
|
||||
from mitmproxy import controller # noqa
|
||||
@ -7,7 +6,7 @@ from mitmproxy import stateobject
|
||||
from mitmproxy import connections
|
||||
from mitmproxy import version
|
||||
|
||||
import typing # noqa
|
||||
import typing # noqa
|
||||
|
||||
|
||||
class Error(stateobject.StateObject):
|
||||
@ -53,10 +52,6 @@ class Error(stateobject.StateObject):
|
||||
f.set_state(state)
|
||||
return f
|
||||
|
||||
def copy(self):
|
||||
c = copy.copy(self)
|
||||
return c
|
||||
|
||||
|
||||
class Flow(stateobject.StateObject):
|
||||
|
||||
@ -116,16 +111,9 @@ class Flow(stateobject.StateObject):
|
||||
return f
|
||||
|
||||
def copy(self):
|
||||
f = copy.copy(self)
|
||||
|
||||
f = super().copy()
|
||||
f.id = str(uuid.uuid4())
|
||||
f.live = False
|
||||
f.client_conn = self.client_conn.copy()
|
||||
f.server_conn = self.server_conn.copy()
|
||||
f.metadata = self.metadata.copy()
|
||||
|
||||
if self.error:
|
||||
f.error = self.error.copy()
|
||||
return f
|
||||
|
||||
def modified(self):
|
||||
|
@ -7,7 +7,7 @@ import sys
|
||||
from mitmproxy import addonmanager
|
||||
from mitmproxy import options
|
||||
from mitmproxy import controller
|
||||
from mitmproxy import events
|
||||
from mitmproxy import eventsequence
|
||||
from mitmproxy import exceptions
|
||||
from mitmproxy import connections
|
||||
from mitmproxy import http
|
||||
@ -91,7 +91,7 @@ class Master:
|
||||
changed = False
|
||||
try:
|
||||
mtype, obj = self.event_queue.get(timeout=timeout)
|
||||
if mtype not in events.Events:
|
||||
if mtype not in eventsequence.Events:
|
||||
raise exceptions.ControlException(
|
||||
"Unknown event %s" % repr(mtype)
|
||||
)
|
||||
@ -153,7 +153,7 @@ class Master:
|
||||
f.request.port = self.server.config.upstream_server.address.port
|
||||
f.request.scheme = self.server.config.upstream_server.scheme
|
||||
f.reply = controller.DummyReply()
|
||||
for e, o in events.event_sequence(f):
|
||||
for e, o in eventsequence.iterate(f):
|
||||
getattr(self, e)(o)
|
||||
|
||||
def load_flows(self, fr: io.FlowReader) -> int:
|
||||
|
@ -158,8 +158,9 @@ def connection_close(http_version, headers):
|
||||
"""
|
||||
Checks the message to see if the client connection should be closed
|
||||
according to RFC 2616 Section 8.1.
|
||||
If we don't have a Connection header, HTTP 1.1 connections are assumed
|
||||
to be persistent.
|
||||
"""
|
||||
# At first, check if we have an explicit Connection header.
|
||||
if "connection" in headers:
|
||||
tokens = get_header_tokens(headers, "connection")
|
||||
if "close" in tokens:
|
||||
@ -167,9 +168,7 @@ def connection_close(http_version, headers):
|
||||
elif "keep-alive" in tokens:
|
||||
return False
|
||||
|
||||
# If we don't have a Connection header, HTTP 1.1 connections are assumed to
|
||||
# be persistent
|
||||
return http_version != "HTTP/1.1" and http_version != b"HTTP/1.1" # FIXME: Remove one case.
|
||||
return http_version != "HTTP/1.1" and http_version != b"HTTP/1.1"
|
||||
|
||||
|
||||
def expected_http_body_size(request, response=None):
|
||||
|
@ -21,7 +21,6 @@ def parse_headers(headers):
|
||||
first_line_format = "relative"
|
||||
else:
|
||||
first_line_format = "absolute"
|
||||
# FIXME: verify if path or :host contains what we need
|
||||
scheme, host, port, _ = url.parse(path)
|
||||
|
||||
if authority:
|
||||
|
@ -8,7 +8,7 @@ from mitmproxy import flow
|
||||
from mitmproxy.proxy.protocol import base
|
||||
from mitmproxy.net import tcp
|
||||
from mitmproxy.net import websockets
|
||||
from mitmproxy.websocket import WebSocketFlow, WebSocketBinaryMessage, WebSocketTextMessage
|
||||
from mitmproxy.websocket import WebSocketFlow, WebSocketMessage
|
||||
|
||||
|
||||
class WebSocketLayer(base.Layer):
|
||||
@ -65,12 +65,7 @@ class WebSocketLayer(base.Layer):
|
||||
compressed_message = fb[0].header.rsv1
|
||||
fb.clear()
|
||||
|
||||
if message_type == websockets.OPCODE.TEXT:
|
||||
t = WebSocketTextMessage
|
||||
else:
|
||||
t = WebSocketBinaryMessage
|
||||
|
||||
websocket_message = t(self.flow, not is_server, payload)
|
||||
websocket_message = WebSocketMessage(message_type, not is_server, payload)
|
||||
length = len(websocket_message.content)
|
||||
self.flow.messages.append(websocket_message)
|
||||
self.channel.ask("websocket_message", self.flow)
|
||||
|
@ -3,7 +3,7 @@ This module provides a @concurrent decorator primitive to
|
||||
offload computations from mitmproxy's main master thread.
|
||||
"""
|
||||
|
||||
from mitmproxy import events
|
||||
from mitmproxy import eventsequence
|
||||
from mitmproxy.types import basethread
|
||||
|
||||
|
||||
@ -12,7 +12,7 @@ class ScriptThread(basethread.BaseThread):
|
||||
|
||||
|
||||
def concurrent(fn):
|
||||
if fn.__name__ not in events.Events - {"start", "configure", "tick"}:
|
||||
if fn.__name__ not in eventsequence.Events - {"start", "configure", "tick"}:
|
||||
raise NotImplementedError(
|
||||
"Concurrent decorator not supported for '%s' method." % fn.__name__
|
||||
)
|
||||
|
@ -9,8 +9,8 @@ from mitmproxy.types import serializable
|
||||
class TCPMessage(serializable.Serializable):
|
||||
|
||||
def __init__(self, from_client, content, timestamp=None):
|
||||
self.content = content
|
||||
self.from_client = from_client
|
||||
self.content = content
|
||||
self.timestamp = timestamp or time.time()
|
||||
|
||||
@classmethod
|
||||
@ -21,9 +21,7 @@ class TCPMessage(serializable.Serializable):
|
||||
return self.from_client, self.content, self.timestamp
|
||||
|
||||
def set_state(self, state):
|
||||
self.from_client = state.pop("from_client")
|
||||
self.content = state.pop("content")
|
||||
self.timestamp = state.pop("timestamp")
|
||||
self.from_client, self.content, self.timestamp = state
|
||||
|
||||
def __repr__(self):
|
||||
return "{direction} {content}".format(
|
||||
|
@ -3,7 +3,7 @@ import contextlib
|
||||
import mitmproxy.master
|
||||
import mitmproxy.options
|
||||
from mitmproxy import proxy
|
||||
from mitmproxy import events
|
||||
from mitmproxy import eventsequence
|
||||
from mitmproxy import exceptions
|
||||
|
||||
|
||||
@ -57,7 +57,7 @@ class context:
|
||||
is taken (as in flow interception).
|
||||
"""
|
||||
f.reply._state = "handled"
|
||||
for evt, arg in events.event_sequence(f):
|
||||
for evt, arg in eventsequence.iterate(f):
|
||||
h = getattr(addon, evt, None)
|
||||
if h:
|
||||
h(arg)
|
||||
|
@ -1,3 +1,4 @@
|
||||
from mitmproxy.net import websockets
|
||||
from mitmproxy.test import tutils
|
||||
from mitmproxy import tcp
|
||||
from mitmproxy import websocket
|
||||
@ -70,8 +71,8 @@ def twebsocketflow(client_conn=True, server_conn=True, messages=True, err=None,
|
||||
|
||||
if messages is True:
|
||||
messages = [
|
||||
websocket.WebSocketBinaryMessage(f, True, b"hello binary"),
|
||||
websocket.WebSocketTextMessage(f, False, "hello text".encode()),
|
||||
websocket.WebSocketMessage(websockets.OPCODE.BINARY, True, b"hello binary"),
|
||||
websocket.WebSocketMessage(websockets.OPCODE.TEXT, False, "hello text".encode()),
|
||||
]
|
||||
if err is True:
|
||||
err = terr()
|
||||
|
@ -1,63 +1,38 @@
|
||||
import time
|
||||
|
||||
from typing import List
|
||||
from typing import List, Optional
|
||||
|
||||
from mitmproxy import flow
|
||||
from mitmproxy.http import HTTPFlow
|
||||
from mitmproxy.net import websockets
|
||||
from mitmproxy.utils import strutils
|
||||
from mitmproxy.types import serializable
|
||||
from mitmproxy.utils import strutils
|
||||
|
||||
|
||||
class WebSocketMessage(serializable.Serializable):
|
||||
|
||||
def __init__(self, flow, from_client, content, timestamp=None):
|
||||
self.flow = flow
|
||||
self.content = content
|
||||
def __init__(self, type: int, from_client: bool, content: bytes, timestamp: Optional[int]=None):
|
||||
self.type = type
|
||||
self.from_client = from_client
|
||||
self.timestamp = timestamp or time.time()
|
||||
self.content = content
|
||||
self.timestamp = timestamp or time.time() # type: int
|
||||
|
||||
@classmethod
|
||||
def from_state(cls, state):
|
||||
return cls(*state)
|
||||
|
||||
def get_state(self):
|
||||
return self.from_client, self.content, self.timestamp
|
||||
return self.type, self.from_client, self.content, self.timestamp
|
||||
|
||||
def set_state(self, state):
|
||||
self.from_client = state.pop("from_client")
|
||||
self.content = state.pop("content")
|
||||
self.timestamp = state.pop("timestamp")
|
||||
|
||||
@property
|
||||
def info(self):
|
||||
return "{client} {direction} WebSocket {type} message {direction} {server}{endpoint}".format(
|
||||
type=self.type,
|
||||
client=repr(self.flow.client_conn.address),
|
||||
server=repr(self.flow.server_conn.address),
|
||||
direction="->" if self.from_client else "<-",
|
||||
endpoint=self.flow.handshake_flow.request.path,
|
||||
)
|
||||
|
||||
|
||||
class WebSocketBinaryMessage(WebSocketMessage):
|
||||
|
||||
type = 'binary'
|
||||
self.type, self.from_client, self.content, self.timestamp = state
|
||||
|
||||
def __repr__(self):
|
||||
return "binary message: {}".format(strutils.bytes_to_escaped_str(self.content))
|
||||
|
||||
|
||||
class WebSocketTextMessage(WebSocketMessage):
|
||||
|
||||
type = 'text'
|
||||
|
||||
def __repr__(self):
|
||||
return "text message: {}".format(repr(self.content))
|
||||
if self.type == websockets.OPCODE.TEXT:
|
||||
return "text message: {}".format(repr(self.content))
|
||||
else:
|
||||
return "binary message: {}".format(strutils.bytes_to_escaped_str(self.content))
|
||||
|
||||
|
||||
class WebSocketFlow(flow.Flow):
|
||||
|
||||
"""
|
||||
A WebsocketFlow is a simplified representation of a Websocket session.
|
||||
"""
|
||||
@ -70,18 +45,55 @@ class WebSocketFlow(flow.Flow):
|
||||
self.close_message = '(message missing)'
|
||||
self.close_reason = 'unknown status code'
|
||||
self.handshake_flow = handshake_flow
|
||||
self.client_key = websockets.get_client_key(self.handshake_flow.request.headers)
|
||||
self.client_protocol = websockets.get_protocol(self.handshake_flow.request.headers)
|
||||
self.client_extensions = websockets.get_extensions(self.handshake_flow.request.headers)
|
||||
self.server_accept = websockets.get_server_accept(self.handshake_flow.response.headers)
|
||||
self.server_protocol = websockets.get_protocol(self.handshake_flow.response.headers)
|
||||
self.server_extensions = websockets.get_extensions(self.handshake_flow.response.headers)
|
||||
|
||||
_stateobject_attributes = flow.Flow._stateobject_attributes.copy()
|
||||
_stateobject_attributes.update(
|
||||
messages=List[WebSocketMessage],
|
||||
close_sender=str,
|
||||
close_code=str,
|
||||
close_message=str,
|
||||
close_reason=str,
|
||||
handshake_flow=HTTPFlow,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_state(cls, state):
|
||||
f = cls(None, None, None)
|
||||
f.set_state(state)
|
||||
return f
|
||||
|
||||
def __repr__(self):
|
||||
return "WebSocketFlow ({} messages)".format(len(self.messages))
|
||||
return "<WebSocketFlow ({} messages)>".format(len(self.messages))
|
||||
|
||||
@property
|
||||
def client_key(self):
|
||||
return websockets.get_client_key(self.handshake_flow.request.headers)
|
||||
|
||||
@property
|
||||
def client_protocol(self):
|
||||
return websockets.get_protocol(self.handshake_flow.request.headers)
|
||||
|
||||
@property
|
||||
def client_extensions(self):
|
||||
return websockets.get_extensions(self.handshake_flow.request.headers)
|
||||
|
||||
@property
|
||||
def server_accept(self):
|
||||
return websockets.get_server_accept(self.handshake_flow.response.headers)
|
||||
|
||||
@property
|
||||
def server_protocol(self):
|
||||
return websockets.get_protocol(self.handshake_flow.response.headers)
|
||||
|
||||
@property
|
||||
def server_extensions(self):
|
||||
return websockets.get_extensions(self.handshake_flow.response.headers)
|
||||
|
||||
def message_info(self, message: WebSocketMessage) -> str:
|
||||
return "{client} {direction} WebSocket {type} message {direction} {server}{endpoint}".format(
|
||||
type=message.type,
|
||||
client=repr(self.client_conn.address),
|
||||
server=repr(self.server_conn.address),
|
||||
direction="->" if message.from_client else "<-",
|
||||
endpoint=self.handshake_flow.request.path,
|
||||
)
|
||||
|
@ -103,15 +103,17 @@ def pytest_runtestloop(session):
|
||||
measured_files = [os.path.normpath(os.path.relpath(f, prefix)) for f in cov.get_data().measured_files()]
|
||||
measured_files = [f for f in measured_files if not any(f.startswith(excluded_f) for excluded_f in excluded_files)]
|
||||
|
||||
for name in pytest.config.option.full_cov:
|
||||
for name in coverage_values.keys():
|
||||
files = [f for f in measured_files if f.startswith(os.path.normpath(name))]
|
||||
try:
|
||||
with open(os.devnull, 'w') as null:
|
||||
coverage_values[name] = cov.report(files, ignore_errors=True, file=null)
|
||||
overall = cov.report(files, ignore_errors=True, file=null)
|
||||
singles = [(s, cov.report(s, ignore_errors=True, file=null)) for s in files]
|
||||
coverage_values[name] = (overall, singles)
|
||||
except:
|
||||
pass
|
||||
|
||||
if any(v < 100 for v in coverage_values.values()):
|
||||
if any(v < 100 for v, _ in coverage_values.values()):
|
||||
# make sure we get the EXIT_TESTSFAILED exit code
|
||||
session.testsfailed += 1
|
||||
else:
|
||||
@ -132,12 +134,15 @@ def pytest_terminal_summary(terminalreporter, exitstatus):
|
||||
msg = "FAIL: Full test coverage not reached!\n"
|
||||
terminalreporter.write(msg, **markup)
|
||||
|
||||
for name, value in coverage_values.items():
|
||||
if value < 100:
|
||||
for name in sorted(coverage_values.keys()):
|
||||
msg = 'Coverage for {}: {:.2f}%\n'.format(name, coverage_values[name][0])
|
||||
if coverage_values[name][0] < 100:
|
||||
markup = {'red': True, 'bold': True}
|
||||
for s, v in sorted(coverage_values[name][1]):
|
||||
if v < 100:
|
||||
msg += ' {}: {:.2f}%\n'.format(s, v)
|
||||
else:
|
||||
markup = {'green': True}
|
||||
msg = 'Coverage for {}: {:.2f}%\n'.format(name, value)
|
||||
terminalreporter.write(msg, **markup)
|
||||
else:
|
||||
markup = {'green': True}
|
||||
@ -145,6 +150,7 @@ def pytest_terminal_summary(terminalreporter, exitstatus):
|
||||
msg += '{}\n\n'.format('\n'.join(pytest.config.option.full_cov))
|
||||
terminalreporter.write(msg, **markup)
|
||||
|
||||
msg = 'Excluded files:\n'
|
||||
msg += '{}\n'.format('\n'.join(pytest.config.option.no_full_cov))
|
||||
msg = '\nExcluded files:\n'
|
||||
for s in sorted(pytest.config.option.no_full_cov):
|
||||
msg += " {}\n".format(s)
|
||||
terminalreporter.write(msg)
|
||||
|
@ -39,7 +39,6 @@ class TestStickyCookie:
|
||||
assert "cookie" not in f.request.headers
|
||||
|
||||
f = f.copy()
|
||||
f.reply.acked = False
|
||||
sc.request(f)
|
||||
assert f.request.headers["cookie"] == "foo=bar"
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
from mitmproxy import controller
|
||||
from mitmproxy import events
|
||||
from mitmproxy import eventsequence
|
||||
from mitmproxy import ctx
|
||||
import sys
|
||||
|
||||
@ -11,7 +11,7 @@ class CallLogger:
|
||||
self.name = name
|
||||
|
||||
def __getattr__(self, attr):
|
||||
if attr in events.Events:
|
||||
if attr in eventsequence.Events:
|
||||
def prox(*args, **kwargs):
|
||||
lg = (self.name, attr, args, kwargs)
|
||||
if attr != "log":
|
||||
|
@ -179,16 +179,15 @@ class TestSimple(_WebSocketTest):
|
||||
assert isinstance(self.master.state.flows[1], WebSocketFlow)
|
||||
assert len(self.master.state.flows[1].messages) == 5
|
||||
assert self.master.state.flows[1].messages[0].content == b'server-foobar'
|
||||
assert self.master.state.flows[1].messages[0].type == 'text'
|
||||
assert self.master.state.flows[1].messages[0].type == websockets.OPCODE.TEXT
|
||||
assert self.master.state.flows[1].messages[1].content == b'client-foobar'
|
||||
assert self.master.state.flows[1].messages[1].type == 'text'
|
||||
assert self.master.state.flows[1].messages[1].type == websockets.OPCODE.TEXT
|
||||
assert self.master.state.flows[1].messages[2].content == b'client-foobar'
|
||||
assert self.master.state.flows[1].messages[2].type == 'text'
|
||||
assert self.master.state.flows[1].messages[2].type == websockets.OPCODE.TEXT
|
||||
assert self.master.state.flows[1].messages[3].content == b'\xde\xad\xbe\xef'
|
||||
assert self.master.state.flows[1].messages[3].type == 'binary'
|
||||
assert self.master.state.flows[1].messages[3].type == websockets.OPCODE.BINARY
|
||||
assert self.master.state.flows[1].messages[4].content == b'\xde\xad\xbe\xef'
|
||||
assert self.master.state.flows[1].messages[4].type == 'binary'
|
||||
assert [m.info for m in self.master.state.flows[1].messages]
|
||||
assert self.master.state.flows[1].messages[4].type == websockets.OPCODE.BINARY
|
||||
|
||||
|
||||
class TestSimpleTLS(_WebSocketTest):
|
||||
|
@ -1,81 +1,57 @@
|
||||
from mitmproxy import events
|
||||
import contextlib
|
||||
from . import tservers
|
||||
import pytest
|
||||
|
||||
from mitmproxy import eventsequence
|
||||
from mitmproxy.test import tflow
|
||||
|
||||
|
||||
class Eventer:
|
||||
def __init__(self, **handlers):
|
||||
self.failure = None
|
||||
self.called = []
|
||||
self.handlers = handlers
|
||||
for i in events.Events - {"tick"}:
|
||||
def mkprox():
|
||||
evt = i
|
||||
|
||||
def prox(*args, **kwargs):
|
||||
self.called.append(evt)
|
||||
if evt in self.handlers:
|
||||
try:
|
||||
handlers[evt](*args, **kwargs)
|
||||
except AssertionError as e:
|
||||
self.failure = e
|
||||
return prox
|
||||
setattr(self, i, mkprox())
|
||||
|
||||
def fail(self):
|
||||
pass
|
||||
@pytest.mark.parametrize("resp, err", [
|
||||
(False, False),
|
||||
(True, False),
|
||||
(False, True),
|
||||
(True, True),
|
||||
])
|
||||
def test_http_flow(resp, err):
|
||||
f = tflow.tflow(resp=resp, err=err)
|
||||
i = eventsequence.iterate(f)
|
||||
assert next(i) == ("requestheaders", f)
|
||||
assert next(i) == ("request", f)
|
||||
if resp:
|
||||
assert next(i) == ("responseheaders", f)
|
||||
assert next(i) == ("response", f)
|
||||
if err:
|
||||
assert next(i) == ("error", f)
|
||||
|
||||
|
||||
class SequenceTester:
|
||||
@contextlib.contextmanager
|
||||
def addon(self, addon):
|
||||
self.master.addons.add(addon)
|
||||
yield
|
||||
self.master.addons.remove(addon)
|
||||
if addon.failure:
|
||||
raise addon.failure
|
||||
@pytest.mark.parametrize("err", [False, True])
|
||||
def test_websocket_flow(err):
|
||||
f = tflow.twebsocketflow(err=err)
|
||||
i = eventsequence.iterate(f)
|
||||
assert next(i) == ("websocket_start", f)
|
||||
assert len(f.messages) == 0
|
||||
assert next(i) == ("websocket_message", f)
|
||||
assert len(f.messages) == 1
|
||||
assert next(i) == ("websocket_message", f)
|
||||
assert len(f.messages) == 2
|
||||
if err:
|
||||
assert next(i) == ("websocket_error", f)
|
||||
assert next(i) == ("websocket_end", f)
|
||||
|
||||
|
||||
class TestBasic(tservers.HTTPProxyTest, SequenceTester):
|
||||
ssl = True
|
||||
@pytest.mark.parametrize("err", [False, True])
|
||||
def test_tcp_flow(err):
|
||||
f = tflow.ttcpflow(err=err)
|
||||
i = eventsequence.iterate(f)
|
||||
assert next(i) == ("tcp_start", f)
|
||||
assert len(f.messages) == 0
|
||||
assert next(i) == ("tcp_message", f)
|
||||
assert len(f.messages) == 1
|
||||
assert next(i) == ("tcp_message", f)
|
||||
assert len(f.messages) == 2
|
||||
if err:
|
||||
assert next(i) == ("tcp_error", f)
|
||||
assert next(i) == ("tcp_end", f)
|
||||
|
||||
def test_requestheaders(self):
|
||||
|
||||
def hdrs(f):
|
||||
assert f.request.headers
|
||||
assert not f.request.content
|
||||
|
||||
def req(f):
|
||||
assert f.request.headers
|
||||
assert f.request.content
|
||||
|
||||
with self.addon(Eventer(requestheaders=hdrs, request=req)):
|
||||
p = self.pathoc()
|
||||
with p.connect():
|
||||
assert p.request("get:'/p/200':b@10").status_code == 200
|
||||
|
||||
def test_100_continue_fail(self):
|
||||
e = Eventer()
|
||||
with self.addon(e):
|
||||
p = self.pathoc()
|
||||
with p.connect():
|
||||
p.request(
|
||||
"""
|
||||
get:'/p/200'
|
||||
h'expect'='100-continue'
|
||||
h'content-length'='1000'
|
||||
da
|
||||
"""
|
||||
)
|
||||
assert "requestheaders" in e.called
|
||||
assert "responseheaders" not in e.called
|
||||
|
||||
def test_connect(self):
|
||||
e = Eventer()
|
||||
with self.addon(e):
|
||||
p = self.pathoc()
|
||||
with p.connect():
|
||||
p.request("get:'/p/200:b@1'")
|
||||
assert "http_connect" in e.called
|
||||
assert e.called.count("requestheaders") == 1
|
||||
assert e.called.count("request") == 1
|
||||
def test_invalid():
|
||||
with pytest.raises(TypeError):
|
||||
next(eventsequence.iterate(42))
|
||||
|
@ -10,6 +10,7 @@ from mitmproxy.exceptions import FlowReadException, Kill
|
||||
from mitmproxy import flow
|
||||
from mitmproxy import http
|
||||
from mitmproxy import connections
|
||||
from mitmproxy import tcp
|
||||
from mitmproxy.proxy import ProxyConfig
|
||||
from mitmproxy.proxy.server import DummyServer
|
||||
from mitmproxy import master
|
||||
@ -156,8 +157,99 @@ class TestHTTPFlow:
|
||||
assert f.response.raw_content == b"abarb"
|
||||
|
||||
|
||||
class TestWebSocketFlow:
|
||||
|
||||
def test_copy(self):
|
||||
f = tflow.twebsocketflow()
|
||||
f.get_state()
|
||||
f2 = f.copy()
|
||||
a = f.get_state()
|
||||
b = f2.get_state()
|
||||
del a["id"]
|
||||
del b["id"]
|
||||
del a["handshake_flow"]["id"]
|
||||
del b["handshake_flow"]["id"]
|
||||
assert a == b
|
||||
assert not f == f2
|
||||
assert f is not f2
|
||||
|
||||
assert f.client_key == f2.client_key
|
||||
assert f.client_protocol == f2.client_protocol
|
||||
assert f.client_extensions == f2.client_extensions
|
||||
assert f.server_accept == f2.server_accept
|
||||
assert f.server_protocol == f2.server_protocol
|
||||
assert f.server_extensions == f2.server_extensions
|
||||
assert f.messages is not f2.messages
|
||||
assert f.handshake_flow is not f2.handshake_flow
|
||||
|
||||
for m in f.messages:
|
||||
m2 = m.copy()
|
||||
m2.set_state(m2.get_state())
|
||||
assert m is not m2
|
||||
assert m.get_state() == m2.get_state()
|
||||
|
||||
f = tflow.twebsocketflow(err=True)
|
||||
f2 = f.copy()
|
||||
assert f is not f2
|
||||
assert f.handshake_flow is not f2.handshake_flow
|
||||
assert f.error.get_state() == f2.error.get_state()
|
||||
assert f.error is not f2.error
|
||||
|
||||
def test_match(self):
|
||||
f = tflow.twebsocketflow()
|
||||
assert not flowfilter.match("~b nonexistent", f)
|
||||
assert flowfilter.match(None, f)
|
||||
assert not flowfilter.match("~b nonexistent", f)
|
||||
|
||||
f = tflow.twebsocketflow(err=True)
|
||||
assert flowfilter.match("~e", f)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
flowfilter.match("~", f)
|
||||
|
||||
def test_repr(self):
|
||||
f = tflow.twebsocketflow()
|
||||
assert 'WebSocketFlow' in repr(f)
|
||||
assert 'binary message: ' in repr(f.messages[0])
|
||||
assert 'text message: ' in repr(f.messages[1])
|
||||
|
||||
|
||||
class TestTCPFlow:
|
||||
|
||||
def test_copy(self):
|
||||
f = tflow.ttcpflow()
|
||||
f.get_state()
|
||||
f2 = f.copy()
|
||||
a = f.get_state()
|
||||
b = f2.get_state()
|
||||
del a["id"]
|
||||
del b["id"]
|
||||
assert a == b
|
||||
assert not f == f2
|
||||
assert f is not f2
|
||||
|
||||
assert f.messages is not f2.messages
|
||||
|
||||
for m in f.messages:
|
||||
assert m.get_state()
|
||||
m2 = m.copy()
|
||||
assert not m == m2
|
||||
assert m is not m2
|
||||
|
||||
a = m.get_state()
|
||||
b = m2.get_state()
|
||||
assert a == b
|
||||
|
||||
m = tcp.TCPMessage(False, 'foo')
|
||||
m.set_state(f.messages[0].get_state())
|
||||
assert m.timestamp == f.messages[0].timestamp
|
||||
|
||||
f = tflow.ttcpflow(err=True)
|
||||
f2 = f.copy()
|
||||
assert f is not f2
|
||||
assert f.error.get_state() == f2.error.get_state()
|
||||
assert f.error is not f2.error
|
||||
|
||||
def test_match(self):
|
||||
f = tflow.ttcpflow()
|
||||
assert not flowfilter.match("~b nonexistent", f)
|
||||
@ -170,6 +262,11 @@ class TestTCPFlow:
|
||||
with pytest.raises(ValueError):
|
||||
flowfilter.match("~", f)
|
||||
|
||||
def test_repr(self):
|
||||
f = tflow.ttcpflow()
|
||||
assert 'TCPFlow' in repr(f)
|
||||
assert '-> ' in repr(f.messages[0])
|
||||
|
||||
|
||||
class TestSerialize:
|
||||
|
||||
@ -308,11 +405,11 @@ class TestFlowMaster:
|
||||
fm.clientconnect(f.client_conn)
|
||||
f.request = http.HTTPRequest.wrap(mitmproxy.test.tutils.treq())
|
||||
fm.request(f)
|
||||
assert s.flow_count() == 1
|
||||
assert len(s.flows) == 1
|
||||
|
||||
f.response = http.HTTPResponse.wrap(mitmproxy.test.tutils.tresp())
|
||||
fm.response(f)
|
||||
assert s.flow_count() == 1
|
||||
assert len(s.flows) == 1
|
||||
|
||||
fm.clientdisconnect(f.client_conn)
|
||||
|
||||
|
@ -984,16 +984,16 @@ class TestUpstreamProxySSL(
|
||||
assert req.status_code == 418
|
||||
|
||||
# CONNECT from pathoc to chain[0],
|
||||
assert self.proxy.tmaster.state.flow_count() == 1
|
||||
assert len(self.proxy.tmaster.state.flows) == 1
|
||||
assert self.proxy.tmaster.state.flows[0].server_conn.via
|
||||
# request from pathoc to chain[0]
|
||||
# CONNECT from proxy to chain[1],
|
||||
assert self.chain[0].tmaster.state.flow_count() == 1
|
||||
assert len(self.chain[0].tmaster.state.flows) == 1
|
||||
assert self.chain[0].tmaster.state.flows[0].server_conn.via
|
||||
# request from proxy to chain[1]
|
||||
# request from chain[0] (regular proxy doesn't store CONNECTs)
|
||||
assert not self.chain[1].tmaster.state.flows[0].server_conn.via
|
||||
assert self.chain[1].tmaster.state.flow_count() == 1
|
||||
assert len(self.chain[1].tmaster.state.flows) == 1
|
||||
|
||||
def test_change_upstream_proxy_connect(self):
|
||||
# skip chain[0].
|
||||
@ -1050,17 +1050,17 @@ class TestProxyChainingSSLReconnect(tservers.HTTPUpstreamProxyTest):
|
||||
assert req.status_code == 418
|
||||
|
||||
# First request goes through all three proxies exactly once
|
||||
assert self.proxy.tmaster.state.flow_count() == 1
|
||||
assert self.chain[0].tmaster.state.flow_count() == 1
|
||||
assert self.chain[1].tmaster.state.flow_count() == 1
|
||||
assert len(self.proxy.tmaster.state.flows) == 1
|
||||
assert len(self.chain[0].tmaster.state.flows) == 1
|
||||
assert len(self.chain[1].tmaster.state.flows) == 1
|
||||
|
||||
req = p.request("get:'/p/418:b\"content2\"'")
|
||||
assert req.status_code == 502
|
||||
|
||||
assert self.proxy.tmaster.state.flow_count() == 2
|
||||
assert self.chain[0].tmaster.state.flow_count() == 2
|
||||
assert len(self.proxy.tmaster.state.flows) == 2
|
||||
assert len(self.chain[0].tmaster.state.flows) == 2
|
||||
# Upstream sees two requests due to reconnection attempt
|
||||
assert self.chain[1].tmaster.state.flow_count() == 3
|
||||
assert len(self.chain[1].tmaster.state.flows) == 3
|
||||
assert not self.chain[1].tmaster.state.flows[-1].response
|
||||
assert not self.chain[1].tmaster.state.flows[-2].response
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
from typing import List
|
||||
import pytest
|
||||
|
||||
from mitmproxy.stateobject import StateObject
|
||||
|
||||
@ -67,3 +68,14 @@ def test_container_list():
|
||||
assert len(copy.children) == 2
|
||||
assert copy.children is not a.children
|
||||
assert copy.children[0] is not a.children[0]
|
||||
|
||||
|
||||
def test_too_much_state():
|
||||
a = Container()
|
||||
a.child = Child(42)
|
||||
s = a.get_state()
|
||||
s['foo'] = 'bar'
|
||||
b = Container()
|
||||
|
||||
with pytest.raises(RuntimeWarning):
|
||||
b.set_state(s)
|
||||
|
10
test/mitmproxy/test_version.py
Normal file
10
test/mitmproxy/test_version.py
Normal file
@ -0,0 +1,10 @@
|
||||
import runpy
|
||||
|
||||
from mitmproxy import version
|
||||
|
||||
|
||||
def test_version(capsys):
|
||||
runpy.run_module('mitmproxy.version', run_name='__main__')
|
||||
stdout, stderr = capsys.readouterr()
|
||||
assert len(stdout) > 0
|
||||
assert stdout.strip() == version.VERSION
|
@ -35,10 +35,6 @@ class TestState:
|
||||
# if f not in self.flows:
|
||||
# self.flows.append(f)
|
||||
|
||||
# FIXME: compat with old state - remove in favor of len(state.flows)
|
||||
def flow_count(self):
|
||||
return len(self.flows)
|
||||
|
||||
|
||||
class TestMaster(master.Master):
|
||||
|
||||
|
21
tox.ini
21
tox.ini
@ -12,21 +12,12 @@ setenv = HOME = {envtmpdir}
|
||||
commands =
|
||||
mitmdump --version
|
||||
pytest --timeout 60 --cov-report='' --cov=mitmproxy --cov=pathod \
|
||||
--full-cov=mitmproxy/addons/ \
|
||||
--full-cov=mitmproxy/contentviews/ --no-full-cov=mitmproxy/contentviews/__init__.py --no-full-cov=mitmproxy/contentviews/protobuf.py --no-full-cov=mitmproxy/contentviews/wbxml.py --no-full-cov=mitmproxy/contentviews/xml_html.py \
|
||||
--full-cov=mitmproxy/net/ --no-full-cov=mitmproxy/net/tcp.py --no-full-cov=mitmproxy/net/http/cookies.py --no-full-cov=mitmproxy/net/http/encoding.py --no-full-cov=mitmproxy/net/http/message.py --no-full-cov=mitmproxy/net/http/request.py --no-full-cov=mitmproxy/net/http/response.py --no-full-cov=mitmproxy/net/http/url.py \
|
||||
--full-cov=mitmproxy/proxy/ --no-full-cov=mitmproxy/proxy/protocol/ --no-full-cov=mitmproxy/proxy/config.py --no-full-cov=mitmproxy/proxy/root_context.py --no-full-cov=mitmproxy/proxy/server.py \
|
||||
--full-cov=mitmproxy/script/ \
|
||||
--full-cov=mitmproxy/test/ \
|
||||
--full-cov=mitmproxy/types/ \
|
||||
--full-cov=mitmproxy/utils/ \
|
||||
--full-cov=mitmproxy/__init__.py \
|
||||
--full-cov=mitmproxy/addonmanager.py \
|
||||
--full-cov=mitmproxy/ctx.py \
|
||||
--full-cov=mitmproxy/exceptions.py \
|
||||
--full-cov=mitmproxy/io.py \
|
||||
--full-cov=mitmproxy/log.py \
|
||||
--full-cov=mitmproxy/options.py \
|
||||
--full-cov=mitmproxy/ \
|
||||
--no-full-cov=mitmproxy/contentviews/__init__.py --no-full-cov=mitmproxy/contentviews/protobuf.py --no-full-cov=mitmproxy/contentviews/wbxml.py --no-full-cov=mitmproxy/contentviews/xml_html.py \
|
||||
--no-full-cov=mitmproxy/net/tcp.py --no-full-cov=mitmproxy/net/http/cookies.py --no-full-cov=mitmproxy/net/http/encoding.py --no-full-cov=mitmproxy/net/http/message.py --no-full-cov=mitmproxy/net/http/request.py --no-full-cov=mitmproxy/net/http/response.py --no-full-cov=mitmproxy/net/http/url.py \
|
||||
--no-full-cov=mitmproxy/proxy/protocol/ --no-full-cov=mitmproxy/proxy/config.py --no-full-cov=mitmproxy/proxy/root_context.py --no-full-cov=mitmproxy/proxy/server.py \
|
||||
--no-full-cov=mitmproxy/tools/ \
|
||||
--no-full-cov=mitmproxy/certs.py --no-full-cov=mitmproxy/connections.py --no-full-cov=mitmproxy/controller.py --no-full-cov=mitmproxy/export.py --no-full-cov=mitmproxy/flow.py --no-full-cov=mitmproxy/flowfilter.py --no-full-cov=mitmproxy/http.py --no-full-cov=mitmproxy/io_compat.py --no-full-cov=mitmproxy/master.py --no-full-cov=mitmproxy/optmanager.py \
|
||||
--full-cov=pathod/ --no-full-cov=pathod/pathoc.py --no-full-cov=pathod/pathod.py --no-full-cov=pathod/test.py --no-full-cov=pathod/protocols/http2.py \
|
||||
{posargs}
|
||||
{env:CI_COMMANDS:python -c ""}
|
||||
|
Loading…
Reference in New Issue
Block a user