Research
Security News
Malicious npm Package Targets Solana Developers and Hijacks Funds
A malicious npm package targets Solana developers, rerouting funds in 2% of transactions to a hardcoded address.
pytest-tcpclient
is a pytest
plugin that helps you write better TCP clients.
It provides two fixtures, tcpserver
and tcpserver_factory
.
Behind the scenes, the tcpserver
fixture creates an in-process TCP server
that listens on a port to which the client can connect and send messages and
from which it can receive replies.
The tcpserver
fixture is used to express expectations about what messages the
client sends and also to send replies to it. If any expectation is unfulfilled, the
test will fail with a diagnostic message.
Hello <hello_>
_Test Failure <test_failure_>
_Sending and receiving length-delimited "frames" <framing_>
_Development <development_>
_.. _hello:
Here's an example of a passing test that uses the tcpserver
fixture:
.. code-block:: python
import asyncio
import pytest
@pytest.mark.asyncio()
async def test_hello(tcpserver):
# ==========================================================================
# Establish expectations and replies on the server side.
#
# In this case, the client is expected to connect and then send a specific
# message. If that occurs, the server will send a reply. It is expected
# that the client will then disconnect.
tcpserver.expect_connect()
tcpserver.expect_bytes(b"Hello, server!")
tcpserver.send_bytes(b"Hello, client!")
tcpserver.expect_disconnect()
# ==========================================================================
# Client interacts with server.
#
# It connects, sends the expected message and then receives the reply.
# Finally, it disconnects from the server.
reader, writer = await asyncio.open_connection(None, tcpserver.service_port)
writer.write(b"Hello, server!")
assert await reader.readexactly(14) == b"Hello, client!"
writer.close()
await writer.wait_closed()
# ==========================================================================
# Synchronise with server. Test failures, if any, will be reported here. In
# this case, there are no failures.
await tcpserver.join()
Here's the result:
.. code-block:: python
============================= test session starts ==============================
platform linux -- Python 3.8.10, pytest-7.2.0, pluggy-1.0.0
rootdir: /home/anders/src/pytest-tcpclient, configfile: pyproject.toml
plugins: mock-3.10.0, asyncio-0.20.2, cov-4.0.0, tcpclient-0.7.26.dev1+gad4c8b2.d20221115
asyncio: mode=strict
collected 1 item
examples/test_hello.py . [100%]
============================== 1 passed in 0.03s ===============================
.. _test_failure:
This example demonstrates test failure.
It is similar to the previous example except that the client does not send the expected message. As a result, the server times out while waiting for that message and the test fails.
.. code-block:: python
import asyncio
import pytest
@pytest.mark.asyncio()
async def test_expect_bytes_times_out(tcpserver):
# --------------------------------------------------------------------------
# Server expectations. The server just expects the client to connect, send
# a message and then disconnect.
tcpserver.expect_connect()
tcpserver.expect_bytes(b"Hello, world!")
tcpserver.expect_disconnect()
# --------------------------------------------------------------------------
# The client connects but it does not send the message and it does not close
# the connection.
reader, writer = await asyncio.open_connection(None, tcpserver.service_port)
# --------------------------------------------------------------------------
# The server will time out waiting for the expected message. The test will
# fail with a diagnostic message.
await tcpserver.join()
Here's the result:
.. code-block:: python
============================= test session starts ==============================
platform linux -- Python 3.8.10, pytest-7.2.0, pluggy-1.0.0
rootdir: /home/anders/src/pytest-tcpclient, configfile: pyproject.toml
plugins: mock-3.10.0, asyncio-0.20.2, cov-4.0.0, tcpclient-0.7.26.dev1+gad4c8b2.d20221115
asyncio: mode=strict
collected 1 item
examples/test_expect_bytes_times_out.py F [100%]
=================================== FAILURES ===================================
_________________________ test_expect_bytes_times_out __________________________
tcpserver = <pytest_tcpclient.plugin.MockTcpServer object at 0x7fda575068b0>
@pytest.mark.asyncio()
async def test_expect_bytes_times_out(tcpserver):
# --------------------------------------------------------------------------
# Server expectations. The server just expects the client to connect, send
# a message and then disconnect.
tcpserver.expect_connect()
tcpserver.expect_bytes(b"Hello, world!")
tcpserver.expect_disconnect()
# --------------------------------------------------------------------------
# The client connects but it does not send the message and it does not close
# the connection.
reader, writer = await asyncio.open_connection(None, tcpserver.service_port)
# --------------------------------------------------------------------------
# The server will time out waiting for the expected message. The test will
# fail with a diagnostic message.
> await tcpserver.join()
E Failed: Timed out waiting for b'Hello, world!'
examples/test_expect_bytes_times_out.py:26: Failed
=========================== short test summary info ============================
FAILED examples/test_expect_bytes_times_out.py::test_expect_bytes_times_out
============================== 1 failed in 1.04s ===============================
.. _framing:
A common pattern for TCP communication is to send and receive discrete messages. One
way to represent the boundaries of these messages is to prepend a 4-byte binary
integer in network (big-endian) ordering that is the length of the payload. In
pytest-tcpclient
this convention is called "framing".
Here's an example of testing that a client sends a frame (expect_frame
):
.. code-block:: python
import asyncio
import pytest
import struct
@pytest.mark.asyncio()
async def test_expect_frame_success(tcpserver):
tcpserver.expect_connect()
tcpserver.expect_frame(b"Goodbye, world")
reader, writer = await asyncio.open_connection(None, tcpserver.service_port)
payload = b"Goodbye, world"
# Here's the 4-byte length header
writer.write(struct.pack(">I", len(payload)))
# Here's the payload
writer.write(payload)
# Done
writer.close()
await writer.wait_closed()
await tcpserver.join()
Here's the result:
.. code-block:: python
============================= test session starts ==============================
platform linux -- Python 3.8.10, pytest-7.2.0, pluggy-1.0.0
rootdir: /home/anders/src/pytest-tcpclient, configfile: pyproject.toml
plugins: mock-3.10.0, asyncio-0.20.2, cov-4.0.0, tcpclient-0.7.26.dev1+gad4c8b2.d20221115
asyncio: mode=strict
collected 1 item
examples/test_expect_frame_success.py . [100%]
============================== 1 passed in 0.02s ===============================
Here's an example where the server sends a frame (send_frame
) and the client
receives it:
.. code-block:: python
import asyncio
import pytest
import struct
@pytest.mark.asyncio()
async def test_send_frame_success(tcpserver):
tcpserver.expect_connect()
reader, writer = await asyncio.open_connection(None, tcpserver.service_port)
# The server immediately sends a frame
tcpserver.send_frame(b"Hello")
# The client receives the frame. First the header and then the payload.
header_bytes = await reader.readexactly(4)
message_length, = struct.unpack(">I", header_bytes)
assert await reader.readexactly(message_length) == b"Hello"
writer.close()
await writer.wait_closed()
Here's the result:
.. code-block:: python
============================= test session starts ==============================
platform linux -- Python 3.8.10, pytest-7.2.0, pluggy-1.0.0
rootdir: /home/anders/src/pytest-tcpclient, configfile: pyproject.toml
plugins: mock-3.10.0, asyncio-0.20.2, cov-4.0.0, tcpclient-0.7.26.dev1+gad4c8b2.d20221115
asyncio: mode=strict
collected 1 item
examples/test_send_frame_success.py . [100%]
============================== 1 passed in 0.02s ===============================
.. _development:
If you want to use a virtual environment, do that first and activate it. You
can use any virtual environment system you like. However, if you want to use
virtualenv
(and you already have virtualenv
installed) you could do this:
.. code-block:: sh
$ virtualenv -p3.8 venv
Next, make the project:
.. code-block:: sh
$ make
That will do the following:
Build configuration +++++++++++++++++++
Build configuration is mostly stored in pyproject.toml
. Idealy, it would all be in
there. However, there are two exceptions.
First, setuptools
has been chosen as the build system. Unfortunately, to install
pytest-tcpclient
in editable mode, a minimial setup.py
must be and has been provided.
Packages that pytest-tcpclient
requires to run are listed in pyproject.toml
.
Packages required for development (testing, coverage and linting) are listed in
dev_dependencies.txt
.
tox
has been configured (in tox.ini
) to install those packages before running
the tests.
setuptools
has been configured to supply the option dev
for those extra packages.
For example, the Makefile
has the following command to initialise the virtual
environment:
.. code-block:: sh
$ python -m pip install -e .[dev]
Default make
target is style_and_test
+++++++++++++++++++++++++++++++++++++++++++++
The default target in the Makfile
is style_and_test
which first calls
the linter, then runs the tests and, finally, checks that code coverage is 100%
tox
+++++++
tox
is only used for CI builds. See .github/workflows/build.yml
.
make
detects changes to configuration files
+++++++++++++++++++++++++++++++++++++++++++++++
If any of the build system configuration files, make
will reinstall all dependencies.
Continuous Integration and Deployment +++++++++++++++++++++++++++++++++++++
There is a workflow (.github/workflows/build.yml
) that will build and test pull
requests with tox
.
There is another workflow (.github/workflows/publish.yml
) that is triggered
by the appearance of new version tags on the main
branch. It will
build and test the code and additionally publish the package to
pypi
.
FAQs
A pytest plugin for testing TCP clients
We found that pytest-tcpclient demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Research
Security News
A malicious npm package targets Solana developers, rerouting funds in 2% of transactions to a hardcoded address.
Security News
Research
Socket researchers have discovered malicious npm packages targeting crypto developers, stealing credentials and wallet data using spyware delivered through typosquats of popular cryptographic libraries.
Security News
Socket's package search now displays weekly downloads for npm packages, helping developers quickly assess popularity and make more informed decisions.