Repository: abhinavsingh/proxy.py
Pull request #674 (2021-11-05 02:49:48)

Add tests for missing core modules
Additionally:

1. Rename GHA job names to be consistent across all workflows.
2. Update homebrew packaging
3. Simply `is_py3` logic
4. Fix an issue where empty plugin name was passed around and then ignored
5. Coverage for `main`, `entry_point` and `Proxy` context manager and `HttpParser`

codecov[bot]: # [Codecov](https://codecov.io/gh/abhinavsingh/proxy.py/pull/674?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh) Report
> Merging [#674](https://codecov.io/gh/abhinavsingh/proxy.py/pull/674?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh) (09c35f4) into [develop](https://codecov.io/gh/abhinavsingh/proxy.py/commit/bf4ee90e21fa06d194b36cd93d89992283e490a5?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh) (bf4ee90) will **increase** coverage by `0.21%`.
> The diff coverage is `100.00%`.

> :exclamation: Current head 09c35f4 differs from pull request most recent head 6baac3c. Consider uploading reports for the commit 6baac3c to get more accurate results
[![Impacted file tree graph](https://codecov.io/gh/abhinavsingh/proxy.py/pull/674/graphs/tree.svg?width=650&height=150&src=pr&token=Zh9J7b4la2&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh)](https://codecov.io/gh/abhinavsingh/proxy.py/pull/674?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh)

```diff
@@             Coverage Diff             @@
##           develop     #674      +/-   ##
===========================================
+ Coverage    74.06%   74.27%   +0.21%     
===========================================
  Files           86       86              
  Lines         3316     3316              
===========================================
+ Hits          2456     2463       +7     
+ Misses         860      853       -7     
```

| Flag | Coverage Δ | |
|---|---|---|
| pytest | `74.27% <100.00%> (+0.21%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/abhinavsingh/proxy.py/pull/674?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh) | Coverage Δ | |
|---|---|---|
| [proxy/http/parser.py](https://codecov.io/gh/abhinavsingh/proxy.py/pull/674/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh#diff-cHJveHkvaHR0cC9wYXJzZXIucHk=) | `97.56% <ø> (+4.26%)` | :arrow_up: |
| [proxy/proxy.py](https://codecov.io/gh/abhinavsingh/proxy.py/pull/674/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh#diff-cHJveHkvcHJveHkucHk=) | `91.90% <100.00%> (+0.47%)` | :arrow_up: |
| [proxy/common/types.py](https://codecov.io/gh/abhinavsingh/proxy.py/pull/674/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh#diff-cHJveHkvY29tbW9uL3R5cGVzLnB5) | `93.33% <0.00%> (-6.67%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/abhinavsingh/proxy.py/pull/674?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/abhinavsingh/proxy.py/pull/674?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh). Last update [bf4ee90...6baac3c](https://codecov.io/gh/abhinavsingh/proxy.py/pull/674?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh).


Patch:
diff --git a/helper/homebrew/develop/proxy.rb b/helper/homebrew/develop/proxy.rb
index d695e53..37cff9a 100644
--- a/helper/homebrew/develop/proxy.rb
+++ b/helper/homebrew/develop/proxy.rb
@@ -9,11 +9,6 @@ class Proxy < Formula
 
   depends_on "python"
 
-  resource "typing-extensions" do
-    url "https://files.pythonhosted.org/packages/6a/28/d32852f2af6b5ead85d396249d5bdf450833f3a69896d76eb480d9c5e406/typing_extensions-3.7.4.2.tar.gz"
-    sha256 "79ee589a3caca649a9bfd2a8de4709837400dfa00b6cc81962a1e6a1815969ae"
-  end
-
   def install
     virtualenv_install_with_resources
   end
diff --git a/helper/homebrew/stable/proxy.rb b/helper/homebrew/stable/proxy.rb
index 83b38cb..7d0e3a9 100644
--- a/helper/homebrew/stable/proxy.rb
+++ b/helper/homebrew/stable/proxy.rb
@@ -5,15 +5,11 @@ class Proxy < Formula
     Network monitoring, controls & Application development, testing, debugging."
   homepage "https://github.com/abhinavsingh/proxy.py"
   url "https://github.com/abhinavsingh/proxy.py/archive/master.zip"
-  version "2.2.0"
+  sha256 "715687cebd451285d266f29d6509a64becc93da21f61ba9b4414e7dc4ecaaeed"
+  version "2.3.1"
 
   depends_on "python"
 
-  resource "typing-extensions" do
-    url "https://files.pythonhosted.org/packages/6a/28/d32852f2af6b5ead85d396249d5bdf450833f3a69896d76eb480d9c5e406/typing_extensions-3.7.4.2.tar.gz"
-    sha256 "79ee589a3caca649a9bfd2a8de4709837400dfa00b6cc81962a1e6a1815969ae"
-  end
-
   def install
     virtualenv_install_with_resources
   end
diff --git a/proxy/core/acceptor/threadless.py b/proxy/core/acceptor/threadless.py
index 8caf217..5ed8ac3 100644
--- a/proxy/core/acceptor/threadless.py
+++ b/proxy/core/acceptor/threadless.py
@@ -198,8 +198,6 @@ class Threadless(multiprocessing.Process):
             self.loop = asyncio.get_event_loop()
             while not self.running.is_set():
                 self.run_once()
-        except KeyboardInterrupt:
-            pass
         finally:
             assert self.selector is not None
             self.selector.unregister(self.client_queue)
diff --git a/proxy/core/acceptor/work.py b/proxy/core/acceptor/work.py
index dcfabc2..5556a31 100644
--- a/proxy/core/acceptor/work.py
+++ b/proxy/core/acceptor/work.py
@@ -73,7 +73,7 @@ class Work(ABC):
         compatibility with threaded mode where work class is started as
         a separate thread.
         """
-        pass
+        pass    # pragma: no cover
 
     def publish_event(
             self,
diff --git a/proxy/http/parser.py b/proxy/http/parser.py
index 597e641..927ca44 100644
--- a/proxy/http/parser.py
+++ b/proxy/http/parser.py
@@ -180,7 +180,8 @@ class HttpParser:
                     more = False
                 else:
                     raise NotImplementedError(
-                        'Parser shouldn\'t have reached here',
+                        'Parser shouldn\'t have reached here. ' +
+                        'This can happen when content length header is missing but their is a body in the payload',
                     )
             else:
                 more, raw = self.process(raw)
@@ -285,7 +286,8 @@ class HttpParser:
             headers={} if not self.headers else {
                 self.headers[k][0]: self.headers[k][1] for k in self.headers
             },
-            body=self.body if not self.is_chunked_encoded() else ChunkParser.to_chunks(self.body),
+            body=self.body if not self.is_chunked_encoded(
+            ) else ChunkParser.to_chunks(self.body),
         )
 
     def has_host(self) -> bool:
diff --git a/proxy/proxy.py b/proxy/proxy.py
index 330c7a9..988dec4 100644
--- a/proxy/proxy.py
+++ b/proxy/proxy.py
@@ -23,7 +23,7 @@ import importlib
 import inspect
 
 from types import TracebackType
-from typing import Dict, List, Optional, Any, Tuple, Type, Union, cast
+from typing import Dict, List, Optional, Any, Type, Union, cast
 
 from proxy.core.acceptor.work import Work
 
@@ -205,7 +205,7 @@ class Proxy:
         if input_args is None:
             input_args = []
 
-        if not Proxy.is_py3():
+        if Proxy.is_py2():
             print(PY2_DEPRECATION_MESSAGE)
             sys.exit(1)
 
@@ -230,16 +230,15 @@ class Proxy:
         Proxy.set_open_file_limit(args.open_file_limit)
 
         # Load plugins
-        default_plugins = Proxy.get_default_plugins(args)
+        default_plugins = [bytes_(p) for p in Proxy.get_default_plugins(args)]
+        extra_plugins = [
+            p if isinstance(p, type) else bytes_(p)
+            for p in opts.get('plugins', args.plugins.split(text_(COMMA)))
+            if not (isinstance(p, str) and len(p) == 0)
+        ]
 
         # Load default plugins along with user provided --plugins
-        plugins = Proxy.load_plugins(
-            [bytes_(p) for p in collections.OrderedDict(default_plugins).keys()] +
-            [
-                p if isinstance(p, type) else bytes_(p)
-                for p in opts.get('plugins', args.plugins.split(text_(COMMA)))
-            ],
-        )
+        plugins = Proxy.load_plugins(default_plugins + extra_plugins)
 
         # proxy.py currently cannot serve over HTTPS and also perform TLS interception
         # at the same time.  Check if user is trying to enable both feature
@@ -413,8 +412,7 @@ class Proxy:
         }
         for plugin_ in plugins:
             klass, module_name = Proxy.import_plugin(plugin_)
-            if klass is None and module_name is None:
-                continue
+            assert klass and module_name
             mro = list(inspect.getmro(klass))
             mro.reverse()
             iterator = iter(mro)
@@ -433,8 +431,7 @@ class Proxy:
             klass = plugin
         else:
             plugin_ = text_(plugin.strip())
-            if plugin_ == '':
-                return (None, None)
+            assert plugin_ != ''
             module_name, klass_name = plugin_.rsplit(text_(DOT), 1)
             klass = getattr(
                 importlib.import_module(
@@ -449,36 +446,37 @@ class Proxy:
     @staticmethod
     def get_default_plugins(
             args: argparse.Namespace,
-    ) -> List[Tuple[str, bool]]:
-        # Prepare list of plugins to load based upon
-        # --enable-*, --disable-* and --basic-auth flags.
-        default_plugins: List[Tuple[str, bool]] = []
+    ) -> List[str]:
+        """Prepare list of plugins to load based upon
+        --enable-*, --disable-* and --basic-auth flags.
+        """
+        default_plugins: List[str] = []
         if args.basic_auth is not None:
-            default_plugins.append((PLUGIN_PROXY_AUTH, True))
+            default_plugins.append(PLUGIN_PROXY_AUTH)
         if args.enable_dashboard:
-            default_plugins.append((PLUGIN_WEB_SERVER, True))
+            default_plugins.append(PLUGIN_WEB_SERVER)
             args.enable_static_server = True
-            default_plugins.append((PLUGIN_DASHBOARD, True))
-            default_plugins.append((PLUGIN_INSPECT_TRAFFIC, True))
+            default_plugins.append(PLUGIN_DASHBOARD)
+            default_plugins.append(PLUGIN_INSPECT_TRAFFIC)
             args.enable_events = True
             args.enable_devtools = True
         if args.enable_devtools:
-            default_plugins.append((PLUGIN_DEVTOOLS_PROTOCOL, True))
-            default_plugins.append((PLUGIN_WEB_SERVER, True))
+            default_plugins.append(PLUGIN_DEVTOOLS_PROTOCOL)
+            default_plugins.append(PLUGIN_WEB_SERVER)
         if not args.disable_http_proxy:
-            default_plugins.append((PLUGIN_HTTP_PROXY, True))
+            default_plugins.append(PLUGIN_HTTP_PROXY)
         if args.enable_web_server or \
                 args.pac_file is not None or \
                 args.enable_static_server:
-            default_plugins.append((PLUGIN_WEB_SERVER, True))
+            default_plugins.append(PLUGIN_WEB_SERVER)
         if args.pac_file is not None:
-            default_plugins.append((PLUGIN_PAC_FILE, True))
-        return default_plugins
+            default_plugins.append(PLUGIN_PAC_FILE)
+        return list(collections.OrderedDict.fromkeys(default_plugins).keys())
 
     @staticmethod
-    def is_py3() -> bool:
+    def is_py2() -> bool:
         """Exists only to avoid mocking sys.version_info in tests."""
-        return sys.version_info[0] != 2
+        return sys.version_info[0] == 2
 
     @staticmethod
     def set_open_file_limit(soft_limit: int) -> None:
@@ -515,7 +513,7 @@ def main(
             # at runtime.  Example, updating flags, plugin
             # configuration etc.
             #
-            # TODO: Python shell within running proxy.py environment
+            # TODO: Python shell within running proxy.py environment?
             while True:
                 time.sleep(1)
     except KeyboardInterrupt:


Test patch:
diff --git a/.github/workflows/test-brew.yml b/.github/workflows/test-brew.yml
index c2cff08..7c31ce1 100644
--- a/.github/workflows/test-brew.yml
+++ b/.github/workflows/test-brew.yml
@@ -1,12 +1,12 @@
 ---
-name: Proxy.py Brew
+name: brew
 
 on: [push, pull_request]  # yamllint disable-line rule:truthy
 
 jobs:
   build:
     runs-on: ${{ matrix.os }}-latest
-    name: Brew - Python ${{ matrix.python }} on ${{ matrix.os }}
+    name: 🐍${{ matrix.python }} @ ${{ matrix.os }}
     strategy:
       matrix:
         os: [macOS]
diff --git a/.github/workflows/test-dashboard.yml b/.github/workflows/test-dashboard.yml
index 513256f..7965993 100644
--- a/.github/workflows/test-dashboard.yml
+++ b/.github/workflows/test-dashboard.yml
@@ -1,12 +1,12 @@
 ---
-name: Proxy.py Dashboard
+name: dashboard
 
 on: [push, pull_request]  # yamllint disable-line rule:truthy
 
 jobs:
   build:
     runs-on: ${{ matrix.os }}-latest
-    name: Dashboard - Node ${{ matrix.node }} on ${{ matrix.os }}
+    name: Node ${{ matrix.node }} @ ${{ matrix.os }}
     strategy:
       matrix:
         os: [macOS, ubuntu, windows]
diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml
index d6dfef6..2d81652 100644
--- a/.github/workflows/test-docker.yml
+++ b/.github/workflows/test-docker.yml
@@ -1,12 +1,12 @@
 ---
-name: Proxy.py Docker
+name: docker
 
 on: [push, pull_request]  # yamllint disable-line rule:truthy
 
 jobs:
   build:
     runs-on: ${{ matrix.os }}-latest
-    name: Docker - Python ${{ matrix.python }} on ${{ matrix.os }}
+    name: 🐍${{ matrix.python }} @ ${{ matrix.os }}
     strategy:
       matrix:
         os: [ubuntu]
diff --git a/tests/http/test_http_parser.py b/tests/http/test_http_parser.py
index ded49c8..29e0ceb 100644
--- a/tests/http/test_http_parser.py
+++ b/tests/http/test_http_parser.py
@@ -272,7 +272,10 @@ class TestHttpParser(unittest.TestCase):
         self.parser.parse(b'Content-Type: text/plain' + CRLF)
         self.assertEqual(self.parser.buffer, b'')
         self.assertEqual(
-            self.parser.headers[b'content-type'], (b'Content-Type', b'text/plain'),
+            self.parser.headers[b'content-type'], (
+                b'Content-Type',
+                b'text/plain',
+            ),
         )
         self.assertEqual(
             self.parser.state,
@@ -632,3 +635,33 @@ class TestHttpParser(unittest.TestCase):
         self.parser = HttpParser(httpParserTypes.RESPONSE_PARSER)
         self.parser.parse(response)
         self.assertEqual(self.parser.state, httpParserStates.COMPLETE)
+
+    def test_request_factory(self) -> None:
+        r = HttpParser.request(
+            b'POST http://localhost:12345 HTTP/1.1' + CRLF +
+            b'key: value' + CRLF +
+            b'Content-Length: 13' + CRLF + CRLF +
+            b'Hello from py',
+        )
+        self.assertEqual(r.host, b'localhost')
+        self.assertEqual(r.port, 12345)
+        self.assertEqual(r.path, b'/')
+        self.assertEqual(r.header(b'key'), b'value')
+        self.assertEqual(r.header(b'KEY'), b'value')
+        self.assertEqual(r.header(b'content-length'), b'13')
+        self.assertEqual(r.body, b'Hello from py')
+
+    def test_response_factory(self) -> None:
+        r = HttpParser.response(
+            b'HTTP/1.1 200 OK\r\nkey: value\r\n\r\n',
+        )
+        self.assertEqual(r.code, b'200')
+        self.assertEqual(r.reason, b'OK')
+        self.assertEqual(r.header(b'key'), b'value')
+
+    def test_parser_shouldnt_have_reached_here(self) -> None:
+        with self.assertRaises(NotImplementedError):
+            HttpParser.request(
+                b'POST http://localhost:12345 HTTP/1.1' + CRLF +
+                b'key: value' + CRLF + CRLF + b'Hello from py',
+            )
diff --git a/tests/http/test_http_proxy.py b/tests/http/test_http_proxy.py
index 13a55a5..5616b9e 100644
--- a/tests/http/test_http_proxy.py
+++ b/tests/http/test_http_proxy.py
@@ -115,3 +115,42 @@ class TestHttpProxyPlugin(unittest.TestCase):
         self.protocol_handler.run_once()
         mock_server_conn.assert_not_called()
         self.plugin.return_value.before_upstream_connection.assert_called()
+
+    def test_proxy_plugin_plugins_can_teardown_from_write_to_descriptors(self) -> None:
+        pass
+
+    def test_proxy_plugin_retries_on_ssl_want_write_error(self) -> None:
+        pass
+
+    def test_proxy_plugin_broken_pipe_error_on_write_will_teardown(self) -> None:
+        pass
+
+    def test_proxy_plugin_plugins_can_teardown_from_read_from_descriptors(self) -> None:
+        pass
+
+    def test_proxy_plugin_retries_on_ssl_want_read_error(self) -> None:
+        pass
+
+    def test_proxy_plugin_timeout_error_on_read_will_teardown(self) -> None:
+        pass
+
+    def test_proxy_plugin_invokes_handle_pipeline_response(self) -> None:
+        pass
+
+    def test_proxy_plugin_invokes_on_access_log(self) -> None:
+        pass
+
+    def test_proxy_plugin_skips_server_teardown_when_client_closes_and_server_never_initialized(self) -> None:
+        pass
+
+    def test_proxy_plugin_invokes_handle_client_data(self) -> None:
+        pass
+
+    def test_proxy_plugin_handles_pipeline_response(self) -> None:
+        pass
+
+    def test_proxy_plugin_invokes_resolve_dns(self) -> None:
+        pass
+
+    def test_proxy_plugin_require_both_host_port_to_connect(self) -> None:
+        pass
diff --git a/tests/test_main.py b/tests/test_main.py
index ec99edb..8f4ffba 100644
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -13,18 +13,17 @@ import tempfile
 import os
 
 from unittest import mock
-from typing import List
 
-from proxy.proxy import main, Proxy
+from proxy.proxy import main, Proxy, entry_point
 from proxy.common.utils import bytes_
 from proxy.http.handler import HttpProtocolHandler
 
-from proxy.common.constants import DEFAULT_LOG_LEVEL, DEFAULT_LOG_FILE, DEFAULT_LOG_FORMAT, DEFAULT_BASIC_AUTH
+from proxy.common.constants import DEFAULT_ENABLE_DASHBOARD, DEFAULT_LOG_LEVEL, DEFAULT_LOG_FILE, DEFAULT_LOG_FORMAT
 from proxy.common.constants import DEFAULT_TIMEOUT, DEFAULT_DEVTOOLS_WS_PATH, DEFAULT_DISABLE_HTTP_PROXY
 from proxy.common.constants import DEFAULT_ENABLE_STATIC_SERVER, DEFAULT_ENABLE_EVENTS, DEFAULT_ENABLE_DEVTOOLS
 from proxy.common.constants import DEFAULT_ENABLE_WEB_SERVER, DEFAULT_THREADLESS, DEFAULT_CERT_FILE, DEFAULT_KEY_FILE
 from proxy.common.constants import DEFAULT_CA_CERT_FILE, DEFAULT_CA_KEY_FILE, DEFAULT_CA_SIGNING_KEY_FILE
-from proxy.common.constants import DEFAULT_PAC_FILE, DEFAULT_PLUGINS, DEFAULT_PID_FILE, DEFAULT_PORT
+from proxy.common.constants import DEFAULT_PAC_FILE, DEFAULT_PLUGINS, DEFAULT_PID_FILE, DEFAULT_PORT, DEFAULT_BASIC_AUTH
 from proxy.common.constants import DEFAULT_NUM_WORKERS, DEFAULT_OPEN_FILE_LIMIT, DEFAULT_IPV6_HOSTNAME
 from proxy.common.constants import DEFAULT_SERVER_RECVBUF_SIZE, DEFAULT_CLIENT_RECVBUF_SIZE, PY2_DEPRECATION_MESSAGE
 from proxy.common.version import __version__
@@ -66,25 +65,46 @@ class TestMain(unittest.TestCase):
         mock_args.enable_static_server = DEFAULT_ENABLE_STATIC_SERVER
         mock_args.enable_devtools = DEFAULT_ENABLE_DEVTOOLS
         mock_args.enable_events = DEFAULT_ENABLE_EVENTS
+        mock_args.enable_dashboard = DEFAULT_ENABLE_DASHBOARD
 
     @mock.patch('time.sleep')
     @mock.patch('proxy.proxy.Proxy.initialize')
     @mock.patch('proxy.proxy.EventManager')
     @mock.patch('proxy.proxy.AcceptorPool')
-    @mock.patch('logging.basicConfig')
-    def test_init_with_no_arguments(
+    def test_entry_point(
             self,
-            mock_logging_config: mock.Mock,
             mock_acceptor_pool: mock.Mock,
             mock_event_manager: mock.Mock,
             mock_initialize: mock.Mock,
             mock_sleep: mock.Mock,
     ) -> None:
         mock_sleep.side_effect = KeyboardInterrupt()
+        mock_initialize.return_value.enable_events = False
+        entry_point()
+        mock_event_manager.assert_not_called()
+        mock_acceptor_pool.assert_called_with(
+            flags=mock_initialize.return_value,
+            work_klass=HttpProtocolHandler,
+            event_queue=None,
+        )
+        mock_acceptor_pool.return_value.setup.assert_called()
+        mock_acceptor_pool.return_value.shutdown.assert_called()
+        mock_sleep.assert_called()
 
-        input_args: List[str] = []
+    @mock.patch('time.sleep')
+    @mock.patch('proxy.proxy.Proxy.initialize')
+    @mock.patch('proxy.proxy.EventManager')
+    @mock.patch('proxy.proxy.AcceptorPool')
+    def test_main_with_no_arguments(
+            self,
+            mock_acceptor_pool: mock.Mock,
+            mock_event_manager: mock.Mock,
+            mock_initialize: mock.Mock,
+            mock_sleep: mock.Mock,
+    ) -> None:
+        mock_sleep.side_effect = KeyboardInterrupt()
         mock_initialize.return_value.enable_events = False
-        main(input_args)
+        main([])
         mock_event_manager.assert_not_called()
         mock_acceptor_pool.assert_called_with(
             flags=mock_initialize.return_value,
@@ -95,6 +115,101 @@ class TestMain(unittest.TestCase):
         mock_acceptor_pool.return_value.shutdown.assert_called()
         mock_sleep.assert_called()
 
+    @mock.patch('time.sleep')
+    @mock.patch('proxy.proxy.Proxy.initialize')
+    @mock.patch('proxy.proxy.EventManager')
+    @mock.patch('proxy.proxy.AcceptorPool')
+    def test_enable_events(
+        self,
+        mock_acceptor_pool: mock.Mock,
+        mock_event_manager: mock.Mock,
+        mock_initialize: mock.Mock,
+        mock_sleep: mock.Mock,
+    ) -> None:
+        mock_sleep.side_effect = KeyboardInterrupt()
+        mock_initialize.return_value.enable_events = True
+        main([])
+        mock_event_manager.assert_called_once()
+        mock_event_manager.return_value.start_event_dispatcher.assert_called_once()
+        mock_event_manager.return_value.stop_event_dispatcher.assert_called_once()
+        mock_acceptor_pool.assert_called_with(
+            flags=mock_initialize.return_value,
+            work_klass=HttpProtocolHandler,
+            event_queue=mock_event_manager.return_value.event_queue,
+        )
+        mock_acceptor_pool.return_value.setup.assert_called()
+        mock_acceptor_pool.return_value.shutdown.assert_called()
+        mock_sleep.assert_called()
+
+    @mock.patch('time.sleep')
+    @mock.patch('proxy.proxy.Proxy.load_plugins')
+    @mock.patch('proxy.common.flag.FlagParser.parse_args')
+    @mock.patch('proxy.proxy.EventManager')
+    @mock.patch('proxy.proxy.AcceptorPool')
+    def test_enable_dashboard(
+        self,
+        mock_acceptor_pool: mock.Mock,
+        mock_event_manager: mock.Mock,
+        mock_parse_args: mock.Mock,
+        mock_load_plugins: mock.Mock,
+        mock_sleep: mock.Mock,
+    ) -> None:
+        mock_sleep.side_effect = KeyboardInterrupt()
+        mock_args = mock_parse_args.return_value
+        self.mock_default_args(mock_args)
+        mock_args.enable_dashboard = True
+        main(['--enable-dashboard'])
+        mock_load_plugins.assert_called()
+        self.assertEqual(
+            mock_load_plugins.call_args_list[0][0][0], [
+                b'proxy.http.server.HttpWebServerPlugin',
+                b'proxy.dashboard.dashboard.ProxyDashboard',
+                b'proxy.dashboard.inspect_traffic.InspectTrafficPlugin',
+                b'proxy.http.inspector.DevtoolsProtocolPlugin',
+                b'proxy.http.proxy.HttpProxyPlugin',
+            ],
+        )
+        mock_parse_args.assert_called_once()
+        mock_acceptor_pool.assert_called()
+        mock_acceptor_pool.return_value.setup.assert_called()
+        # dashboard will also enable eventing
+        mock_event_manager.assert_called_once()
+        mock_event_manager.return_value.start_event_dispatcher.assert_called_once()
+        mock_event_manager.return_value.stop_event_dispatcher.assert_called_once()
+
+    @mock.patch('time.sleep')
+    @mock.patch('proxy.proxy.Proxy.load_plugins')
+    @mock.patch('proxy.common.flag.FlagParser.parse_args')
+    @mock.patch('proxy.proxy.EventManager')
+    @mock.patch('proxy.proxy.AcceptorPool')
+    def test_enable_devtools(
+        self,
+        mock_acceptor_pool: mock.Mock,
+        mock_event_manager: mock.Mock,
+        mock_parse_args: mock.Mock,
+        mock_load_plugins: mock.Mock,
+        mock_sleep: mock.Mock,
+    ) -> None:
+        mock_sleep.side_effect = KeyboardInterrupt()
+        mock_args = mock_parse_args.return_value
+        self.mock_default_args(mock_args)
+        mock_args.enable_devtools = True
+        main(['--enable-devtools'])
+        mock_load_plugins.assert_called()
+        print(mock_load_plugins.call_args_list[0][0][0])
+        self.assertEqual(
+            mock_load_plugins.call_args_list[0][0][0], [
+                b'proxy.http.inspector.DevtoolsProtocolPlugin',
+                b'proxy.http.server.HttpWebServerPlugin',
+                b'proxy.http.proxy.HttpProxyPlugin',
+            ],
+        )
+        mock_parse_args.assert_called_once()
+        mock_acceptor_pool.assert_called()
+        mock_acceptor_pool.return_value.setup.assert_called()
+        # Currently --enable-devtools alone doesn't enable eventing core
+        mock_event_manager.assert_not_called()
+
     @mock.patch('time.sleep')
     @mock.patch('os.remove')
     @mock.patch('os.path.exists')
@@ -117,7 +232,6 @@ class TestMain(unittest.TestCase):
         mock_args = mock_parse_args.return_value
         self.mock_default_args(mock_args)
         mock_args.pid_file = pid_file
-        mock_args.enable_dashboard = False
         main(['--pid-file', pid_file])
         mock_parse_args.assert_called_once()
         mock_acceptor_pool.assert_called()
@@ -133,7 +247,7 @@ class TestMain(unittest.TestCase):
     @mock.patch('time.sleep')
     @mock.patch('proxy.proxy.EventManager')
     @mock.patch('proxy.proxy.AcceptorPool')
-    def test_basic_auth(
+    def test_basic_auth_flag_is_base64_encoded(
             self,
             mock_acceptor_pool: mock.Mock,
             mock_event_manager: mock.Mock,
@@ -156,10 +270,10 @@ class TestMain(unittest.TestCase):
     @mock.patch('builtins.print')
     @mock.patch('proxy.proxy.EventManager')
     @mock.patch('proxy.proxy.AcceptorPool')
-    @mock.patch('proxy.proxy.Proxy.is_py3')
+    @mock.patch('proxy.proxy.Proxy.is_py2')
     def test_main_py3_runs(
             self,
-            mock_is_py3: mock.Mock,
+            mock_is_py2: mock.Mock,
             mock_acceptor_pool: mock.Mock,
             mock_event_manager: mock.Mock,
             mock_print: mock.Mock,
@@ -168,11 +282,11 @@ class TestMain(unittest.TestCase):
         mock_sleep.side_effect = KeyboardInterrupt()
 
         input_args = ['--basic-auth', 'user:pass']
-        mock_is_py3.return_value = True
+        mock_is_py2.return_value = False
 
         main(input_args, num_workers=1)
 
-        mock_is_py3.assert_called()
+        mock_is_py2.assert_called()
         mock_print.assert_not_called()
 
         mock_event_manager.assert_not_called()
@@ -180,18 +294,18 @@ class TestMain(unittest.TestCase):
         mock_acceptor_pool.return_value.setup.assert_called()
 
     @mock.patch('builtins.print')
-    @mock.patch('proxy.proxy.Proxy.is_py3')
+    @mock.patch('proxy.proxy.Proxy.is_py2')
     def test_main_py2_exit(
             self,
-            mock_is_py3: mock.Mock,
+            mock_is_py2: mock.Mock,
             mock_print: mock.Mock,
     ) -> None:
-        mock_is_py3.return_value = False
+        mock_is_py2.return_value = True
         with self.assertRaises(SystemExit) as e:
             main(num_workers=1)
         mock_print.assert_called_with(PY2_DEPRECATION_MESSAGE)
         self.assertEqual(e.exception.code, 1)
-        mock_is_py3.assert_called()
+        mock_is_py2.assert_called()
 
     @mock.patch('builtins.print')
     def test_main_version(
@@ -202,3 +316,12 @@ class TestMain(unittest.TestCase):
             main(['--version'])
             mock_print.assert_called_with(__version__)
         self.assertEqual(e.exception.code, 0)
+
+    # def test_pac_file(self) -> None:
+    #     pass
+
+    # def test_imports_plugin(self) -> None:
+    #     pass
+
+    # def test_cannot_enable_https_proxy_and_tls_interception_mutually(self) -> None:
+    #     pass