From ee30d10419cd422a2b49e2b968addb8ea1232255 Mon Sep 17 00:00:00 2001 From: Keenan Johnson Date: Sun, 23 Mar 2025 16:34:06 -0700 Subject: [PATCH 01/12] Update .gitmodules Update submodule --- .gitmodules | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.gitmodules b/.gitmodules index c54ae01..772f21e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,7 +1,6 @@ [submodule "vendor/micropython"] path = vendor/micropython - url = https://github.com/Ribbit-Network/micropython - branch = pr/mbedtls + url = https://github.com/micropython/micropython ignore = dirty [submodule "vendor/microdot"] path = vendor/microdot From 82b71a377a3c5aff2cb293c03ff01ae0c25e6a0b Mon Sep 17 00:00:00 2001 From: Keenan Johnson Date: Sun, 23 Mar 2025 17:02:42 -0700 Subject: [PATCH 02/12] Update micropython to main --- vendor/micropython | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/micropython b/vendor/micropython index 12f3c0f..f1018ee 160000 --- a/vendor/micropython +++ b/vendor/micropython @@ -1 +1 @@ -Subproject commit 12f3c0f51f1d84b83c7d78bcfdba60ede036d792 +Subproject commit f1018ee5c2dd0eabcc0cfe7e02c607594e489788 From 34f748def890786d05e84a7e1a3982d56a59c02a Mon Sep 17 00:00:00 2001 From: Keenan Johnson Date: Sun, 23 Mar 2025 17:11:53 -0700 Subject: [PATCH 03/12] test --- .github/workflows/build.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index eb1b63f..244ff9a 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -15,9 +15,9 @@ jobs: - name: Fetch submodules run: | - git submodule update --init --depth=1 - ( cd vendor/micropython/ports/unix ; make submodules ) - ( cd vendor/micropython/ports/esp32 ; make submodules ) + git submodule update --init #--depth=1 + # ( cd vendor/micropython/ports/unix ; make submodules ) + # ( cd vendor/micropython/ports/esp32 ; make submodules ) - name: Test run: | From d890b9e607b4562d118ffe8d80f5cef3fade334a Mon Sep 17 00:00:00 2001 From: Keenan Johnson Date: Sun, 23 Mar 2025 17:13:27 -0700 Subject: [PATCH 04/12] test --- .github/workflows/build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 244ff9a..684ac9a 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -15,7 +15,7 @@ jobs: - name: Fetch submodules run: | - git submodule update --init #--depth=1 + git submodule update --init --recursive #--depth=1 # ( cd vendor/micropython/ports/unix ; make submodules ) # ( cd vendor/micropython/ports/esp32 ; make submodules ) From ac98f56838f4d422ba1ad29e09e7ecabc5daf50b Mon Sep 17 00:00:00 2001 From: Keenan Johnson Date: Sun, 23 Mar 2025 17:20:22 -0700 Subject: [PATCH 05/12] Updated esp idf to v5.4 --- .github/workflows/build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 684ac9a..9e38954 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -26,7 +26,7 @@ jobs: - name: Build run: | rm -rf ./vendor/micropython/mpy-cross/build # Workaround build failure - docker run -t -u "$UID:$GID" -e "HOME=/app" -v "${GITHUB_WORKSPACE}:/app" -w "/app" espressif/idf:v5.1.2 make + docker run -t -u "$UID:$GID" -e "HOME=/app" -v "${GITHUB_WORKSPACE}:/app" -w "/app" espressif/idf:v5.4 make shell: bash - name: Upload to Golioth (Main) From b6d2b222c10247b4fcfcbcd9762ce63bd2a88d20 Mon Sep 17 00:00:00 2001 From: Keenan Johnson Date: Sun, 23 Mar 2025 17:37:30 -0700 Subject: [PATCH 06/12] Update to TLS module --- modules/ribbit/coap/__init__.py | 7 +++---- modules/ribbit/golioth/__init__.py | 6 +++--- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/modules/ribbit/coap/__init__.py b/modules/ribbit/coap/__init__.py index e5a7733..254de9d 100644 --- a/modules/ribbit/coap/__init__.py +++ b/modules/ribbit/coap/__init__.py @@ -3,13 +3,12 @@ import socket import random import os -import ssl import asyncio import asyncio.core as _asyncio_core +import tls from micropython import const - _HEADER_SIZE = const(4) _OPTION_HEADER_SIZE = const(1) _PAYLOAD_MARKER = const(0xFF) @@ -446,8 +445,8 @@ async def connect(self): if self._ssl is not False: ctx = self._ssl if ctx is True: - ctx = ssl.SSLContext( - ssl.PROTOCOL_DTLS_CLIENT + ctx = tls.TLSContext( + tls.PROTOCOL_DTLS_CLIENT ) sock = ctx.wrap_socket( diff --git a/modules/ribbit/golioth/__init__.py b/modules/ribbit/golioth/__init__.py index 950a65e..3ecb566 100644 --- a/modules/ribbit/golioth/__init__.py +++ b/modules/ribbit/golioth/__init__.py @@ -3,6 +3,7 @@ import time from micropython import const import asyncio +import tls import ribbit.config as _config import ribbit.coap as _coap @@ -87,9 +88,8 @@ async def _loop(self): if enabled: self._logger.info("Starting Golioth integration") - import ssl - ctx = ssl.SSLContext( - ssl.PROTOCOL_DTLS_CLIENT + ctx = tls.SSLContext( + tls.PROTOCOL_DTLS_CLIENT ) ctx.set_ciphers(["TLS-PSK-WITH-AES-128-CBC-SHA256"]) ctx.set_psk(user, password) From dfea88768f4d5f4f3396bc172a10d6e16ca12b93 Mon Sep 17 00:00:00 2001 From: Keenan Johnson Date: Sun, 23 Mar 2025 17:40:46 -0700 Subject: [PATCH 07/12] Add tests --- tests/modules/coap/test_dtls.py | 123 ++++++++++++++++++++ tests/modules/coap/test_dtls_integration.py | 119 +++++++++++++++++++ 2 files changed, 242 insertions(+) create mode 100644 tests/modules/coap/test_dtls.py create mode 100644 tests/modules/coap/test_dtls_integration.py diff --git a/tests/modules/coap/test_dtls.py b/tests/modules/coap/test_dtls.py new file mode 100644 index 0000000..62be735 --- /dev/null +++ b/tests/modules/coap/test_dtls.py @@ -0,0 +1,123 @@ +""" +Test the TLS module usage in CoAP and Golioth clients. + +This test verifies that the CoAP and Golioth modules correctly use +the TLS module for DTLS functionality after the upstream MicroPython +PR #15764 moved DTLS from the ssl module to the tls module. +""" + +import unittest +import socket +import io +from unittest.mock import patch, MagicMock + +# Import modules we need to test +import tls +from ribbit.coap import Coap +import ribbit.golioth as golioth + +class DummySocket(io.IOBase): + def __init__(self): + self.write_buffer = bytearray() + self.read_buffer = bytearray() + self.closed = False + + def send(self, data): + self.write_buffer.extend(data) + return len(data) + + def recv(self, size): + return b'' + + def close(self): + self.closed = True + + def setblocking(self, value): + pass + + def connect(self, addr): + pass + + def bind(self, addr): + pass + + +class TestCoAPDTLS(unittest.TestCase): + + def setUp(self): + self.original_getaddrinfo = socket.getaddrinfo + socket.getaddrinfo = MagicMock(return_value=[(None, None, None, None, ('127.0.0.1', 5684))]) + + def tearDown(self): + socket.getaddrinfo = self.original_getaddrinfo + + @patch('tls.SSLContext') + @patch('socket.socket') + def test_coap_uses_tls_module(self, mock_socket, mock_ssl_context): + """Test that CoAP client uses the TLS module for DTLS.""" + # Setup mock objects + mock_socket_instance = MagicMock() + mock_socket.return_value = mock_socket_instance + + mock_context = MagicMock() + mock_ssl_context.return_value = mock_context + mock_wrapped_socket = MagicMock() + mock_context.wrap_socket.return_value = mock_wrapped_socket + + # Create a CoAP client with DTLS enabled + coap = Coap( + host="example.org", + port=5684, + ssl=True # Enable DTLS + ) + + # This should trigger the creation of the SSLContext with DTLS client mode + try: + import asyncio + asyncio.run_until_complete = MagicMock() + asyncio.create_task = MagicMock() + asyncio.Event = MagicMock() + + # Call connect to initialize the socket with TLS + coap.connect() + except Exception: + # We expect an exception due to incomplete mocking of asyncio + pass + + # Verify that the TLS SSLContext was created with DTLS client mode + mock_ssl_context.assert_called_with(tls.PROTOCOL_DTLS_CLIENT) + + # Verify that the socket was wrapped with wrap_socket + mock_context.wrap_socket.assert_called() + + + @patch('tls.SSLContext') + def test_golioth_uses_tls_module(self, mock_ssl_context): + """Test that Golioth client uses the TLS module for DTLS.""" + # Setup mock objects + mock_context = MagicMock() + mock_ssl_context.return_value = mock_context + + # Create config mock + mock_config = MagicMock() + mock_watch = MagicMock() + mock_config.watch.return_value = mock_watch + mock_watch.__enter__.return_value = mock_watch + mock_watch.get.return_value = (True, "example.org", 5684, "user", "password", True) + + # Create OTA manager mock + mock_ota_manager = MagicMock() + + # Create a Golioth client + client = golioth.Golioth(config=mock_config, ota_manager=mock_ota_manager) + + # Verify that the TLS SSLContext was created with DTLS client mode + mock_ssl_context.assert_called_with(tls.PROTOCOL_DTLS_CLIENT) + + # Verify PSK was set + mock_context.set_ciphers.assert_called_with(["TLS-PSK-WITH-AES-128-CBC-SHA256"]) + mock_context.set_psk.assert_called_with("user", "password") + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/tests/modules/coap/test_dtls_integration.py b/tests/modules/coap/test_dtls_integration.py new file mode 100644 index 0000000..9db4f51 --- /dev/null +++ b/tests/modules/coap/test_dtls_integration.py @@ -0,0 +1,119 @@ +""" +Integration test for DTLS functionality in CoAP client. + +This test creates a basic server and client to verify proper DTLS operation. +To run this test, you need a proper DTLS server or a mock server. + +Note: This test is intended to be run manually when verifying DTLS changes. +""" + +import asyncio +import tls +import socket +from ribbit.coap import Coap, CoapPacket, TYPE_CON, METHOD_GET + +# This is a basic integration test that can be run manually +# to test the DTLS functionality with actual server + +async def test_coap_dtls_connection(): + """Test connecting to a CoAP server with DTLS enabled.""" + + # Create a TLS context for DTLS + ctx = tls.SSLContext(tls.PROTOCOL_DTLS_CLIENT) + + # You may need to configure the context with PSK or certificates + # ctx.set_ciphers(["TLS-PSK-WITH-AES-128-CBC-SHA256"]) + # ctx.set_psk("user_id", "password") + + # Replace with your CoAP+DTLS server address and port + coap_server = "coap.example.com" + coap_port = 5684 + + try: + # Create CoAP client with DTLS enabled + coap = Coap( + host=coap_server, + port=coap_port, + ssl=ctx + ) + + # Connect to the server + print(f"Connecting to {coap_server}:{coap_port} with DTLS...") + await coap.connect() + + print("Connected successfully. Sending ping...") + await coap.ping() + print("Ping successful!") + + # Try to fetch a resource + print("Fetching resource...") + response = await coap.get(".well-known/core") + print(f"Response received: {response}") + + # Clean up + await coap.disconnect() + print("Test completed successfully") + + except Exception as e: + print(f"Error in DTLS test: {e}") + raise + +# Local test server setup - you can implement a simple DTLS server here +# if needed for testing without depending on an external service + +class DtlsTestServer: + """Minimal DTLS test server for testing the client.""" + def __init__(self, host="0.0.0.0", port=5684): + self.host = host + self.port = port + self.socket = None + self.context = None + + async def start(self): + """Start the DTLS test server.""" + # Create a TLS context for DTLS server + self.context = tls.SSLContext(tls.PROTOCOL_DTLS_SERVER) + + # For testing, we can use a self-signed certificate + # self.context.load_cert_chain("cert.pem", "key.pem") + + # Or PSK for simpler setup + # self.context.set_ciphers(["TLS-PSK-WITH-AES-128-CBC-SHA256"]) + # self.context.set_psk("user_id", "password") + + # Create and bind the socket + self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.socket.bind((self.host, self.port)) + + print(f"DTLS test server starting on {self.host}:{self.port}") + + # Wrap the socket with DTLS + self.socket = self.context.wrap_socket( + self.socket, + server_side=True + ) + + # Handle incoming connections + while True: + try: + data, addr = self.socket.recvfrom(1024) + print(f"Received {len(data)} bytes from {addr}") + + # Send a response + response = b"\x60\x45\x00\x01\x00\x00\x00\x00\xFF\x68\x65\x6C\x6C\x6F" # CoAP response + self.socket.sendto(response, addr) + except Exception as e: + print(f"Error handling connection: {e}") + + def stop(self): + """Stop the DTLS test server.""" + if self.socket: + self.socket.close() + +if __name__ == "__main__": + # Run the client test + asyncio.run(test_coap_dtls_connection()) + + # Uncomment to run the server instead + # server = DtlsTestServer() + # asyncio.run(server.start()) \ No newline at end of file From 5f2e875d5ca7e4134aa6ab70e23b6df376ca18b5 Mon Sep 17 00:00:00 2001 From: Keenan Johnson Date: Sun, 23 Mar 2025 17:53:16 -0700 Subject: [PATCH 08/12] Add integration and unit tests for DTLS functionality in CoAP and Golioth clients --- .../ribbit/coap/dtls_integration_test.py | 1 + .../ribbit/coap/dtls_test.py | 36 ++----------- modules/ribbit/golioth/dtls_test.py | 50 +++++++++++++++++++ 3 files changed, 56 insertions(+), 31 deletions(-) rename tests/modules/coap/test_dtls_integration.py => modules/ribbit/coap/dtls_integration_test.py (99%) rename tests/modules/coap/test_dtls.py => modules/ribbit/coap/dtls_test.py (66%) create mode 100644 modules/ribbit/golioth/dtls_test.py diff --git a/tests/modules/coap/test_dtls_integration.py b/modules/ribbit/coap/dtls_integration_test.py similarity index 99% rename from tests/modules/coap/test_dtls_integration.py rename to modules/ribbit/coap/dtls_integration_test.py index 9db4f51..70d47bd 100644 --- a/tests/modules/coap/test_dtls_integration.py +++ b/modules/ribbit/coap/dtls_integration_test.py @@ -110,6 +110,7 @@ def stop(self): if self.socket: self.socket.close() +# Run the test if __name__ == "__main__": # Run the client test asyncio.run(test_coap_dtls_connection()) diff --git a/tests/modules/coap/test_dtls.py b/modules/ribbit/coap/dtls_test.py similarity index 66% rename from tests/modules/coap/test_dtls.py rename to modules/ribbit/coap/dtls_test.py index 62be735..0c9e2f1 100644 --- a/tests/modules/coap/test_dtls.py +++ b/modules/ribbit/coap/dtls_test.py @@ -1,7 +1,7 @@ """ -Test the TLS module usage in CoAP and Golioth clients. +Test the TLS module usage in CoAP clients. -This test verifies that the CoAP and Golioth modules correctly use +This test verifies that the CoAP module correctly uses the TLS module for DTLS functionality after the upstream MicroPython PR #15764 moved DTLS from the ssl module to the tls module. """ @@ -14,7 +14,6 @@ # Import modules we need to test import tls from ribbit.coap import Coap -import ribbit.golioth as golioth class DummySocket(io.IOBase): def __init__(self): @@ -91,33 +90,8 @@ def test_coap_uses_tls_module(self, mock_socket, mock_ssl_context): mock_context.wrap_socket.assert_called() - @patch('tls.SSLContext') - def test_golioth_uses_tls_module(self, mock_ssl_context): - """Test that Golioth client uses the TLS module for DTLS.""" - # Setup mock objects - mock_context = MagicMock() - mock_ssl_context.return_value = mock_context - - # Create config mock - mock_config = MagicMock() - mock_watch = MagicMock() - mock_config.watch.return_value = mock_watch - mock_watch.__enter__.return_value = mock_watch - mock_watch.get.return_value = (True, "example.org", 5684, "user", "password", True) - - # Create OTA manager mock - mock_ota_manager = MagicMock() - - # Create a Golioth client - client = golioth.Golioth(config=mock_config, ota_manager=mock_ota_manager) - - # Verify that the TLS SSLContext was created with DTLS client mode - mock_ssl_context.assert_called_with(tls.PROTOCOL_DTLS_CLIENT) - - # Verify PSK was set - mock_context.set_ciphers.assert_called_with(["TLS-PSK-WITH-AES-128-CBC-SHA256"]) - mock_context.set_psk.assert_called_with("user", "password") - +def run_tests(): + unittest.main() if __name__ == '__main__': - unittest.main() \ No newline at end of file + run_tests() \ No newline at end of file diff --git a/modules/ribbit/golioth/dtls_test.py b/modules/ribbit/golioth/dtls_test.py new file mode 100644 index 0000000..807325f --- /dev/null +++ b/modules/ribbit/golioth/dtls_test.py @@ -0,0 +1,50 @@ +""" +Test the TLS module usage in Golioth client. + +This test verifies that the Golioth client correctly uses +the TLS module for DTLS functionality after the upstream MicroPython +PR #15764 moved DTLS from the ssl module to the tls module. +""" + +import unittest +from unittest.mock import patch, MagicMock + +# Import modules we need to test +import tls +import ribbit.golioth as golioth + +class TestGoliothDTLS(unittest.TestCase): + + @patch('tls.SSLContext') + def test_golioth_uses_tls_module(self, mock_ssl_context): + """Test that Golioth client uses the TLS module for DTLS.""" + # Setup mock objects + mock_context = MagicMock() + mock_ssl_context.return_value = mock_context + + # Create config mock + mock_config = MagicMock() + mock_watch = MagicMock() + mock_config.watch.return_value = mock_watch + mock_watch.__enter__.return_value = mock_watch + mock_watch.get.return_value = (True, "example.org", 5684, "user", "password", True) + + # Create OTA manager mock + mock_ota_manager = MagicMock() + + # Create a Golioth client + client = golioth.Golioth(config=mock_config, ota_manager=mock_ota_manager) + + # Verify that the TLS SSLContext was created with DTLS client mode + mock_ssl_context.assert_called_with(tls.PROTOCOL_DTLS_CLIENT) + + # Verify PSK was set + mock_context.set_ciphers.assert_called_with(["TLS-PSK-WITH-AES-128-CBC-SHA256"]) + mock_context.set_psk.assert_called_with("user", "password") + + +def run_tests(): + unittest.main() + +if __name__ == '__main__': + run_tests() \ No newline at end of file From ea1305184fc8b420c9771654a9b2876ea60bc53f Mon Sep 17 00:00:00 2001 From: Keenan Johnson Date: Sun, 23 Mar 2025 17:59:32 -0700 Subject: [PATCH 09/12] Refactor DTLS tests to use custom MagicMock implementation and improve mocking structure --- modules/ribbit/coap/dtls_test.py | 67 +++++++++++++++++------------ modules/ribbit/golioth/dtls_test.py | 59 ++++++++++++++----------- modules/ribbit/utils/mock.py | 58 +++++++++++++++++++++++++ 3 files changed, 133 insertions(+), 51 deletions(-) create mode 100644 modules/ribbit/utils/mock.py diff --git a/modules/ribbit/coap/dtls_test.py b/modules/ribbit/coap/dtls_test.py index 0c9e2f1..4f4f252 100644 --- a/modules/ribbit/coap/dtls_test.py +++ b/modules/ribbit/coap/dtls_test.py @@ -9,7 +9,7 @@ import unittest import socket import io -from unittest.mock import patch, MagicMock +from ribbit.utils.mock import MagicMock, patch # Import modules we need to test import tls @@ -49,45 +49,58 @@ def setUp(self): def tearDown(self): socket.getaddrinfo = self.original_getaddrinfo - - @patch('tls.SSLContext') - @patch('socket.socket') - def test_coap_uses_tls_module(self, mock_socket, mock_ssl_context): + + def test_coap_uses_tls_module(self): """Test that CoAP client uses the TLS module for DTLS.""" - # Setup mock objects + # Create mock objects + mock_socket = MagicMock() mock_socket_instance = MagicMock() mock_socket.return_value = mock_socket_instance + original_socket = socket.socket + socket.socket = mock_socket + mock_context = MagicMock() - mock_ssl_context.return_value = mock_context - mock_wrapped_socket = MagicMock() - mock_context.wrap_socket.return_value = mock_wrapped_socket + mock_wrap_socket = MagicMock() + mock_context.wrap_socket = mock_wrap_socket - # Create a CoAP client with DTLS enabled - coap = Coap( - host="example.org", - port=5684, - ssl=True # Enable DTLS - ) + original_ssl_context = tls.SSLContext + tls.SSLContext = MagicMock(return_value=mock_context) - # This should trigger the creation of the SSLContext with DTLS client mode try: + # Create a CoAP client with DTLS enabled + coap = Coap( + host="example.org", + port=5684, + ssl=True # Enable DTLS + ) + + # Mock asyncio for connect import asyncio - asyncio.run_until_complete = MagicMock() + original_create_task = asyncio.create_task asyncio.create_task = MagicMock() + original_event = asyncio.Event asyncio.Event = MagicMock() - # Call connect to initialize the socket with TLS - coap.connect() - except Exception: - # We expect an exception due to incomplete mocking of asyncio - pass + try: + # Call connect to initialize the socket with TLS + coap.connect() + except Exception as e: + # We expect an exception due to incomplete mocking of asyncio + pass + + # Verify that the TLS SSLContext was created with DTLS client mode + tls.SSLContext.assert_called_with(tls.PROTOCOL_DTLS_CLIENT) - # Verify that the TLS SSLContext was created with DTLS client mode - mock_ssl_context.assert_called_with(tls.PROTOCOL_DTLS_CLIENT) - - # Verify that the socket was wrapped with wrap_socket - mock_context.wrap_socket.assert_called() + # Verify that wrap_socket was called + mock_context.wrap_socket.assert_called() + + finally: + # Restore original functions + socket.socket = original_socket + tls.SSLContext = original_ssl_context + asyncio.create_task = original_create_task + asyncio.Event = original_event def run_tests(): diff --git a/modules/ribbit/golioth/dtls_test.py b/modules/ribbit/golioth/dtls_test.py index 807325f..26eff5c 100644 --- a/modules/ribbit/golioth/dtls_test.py +++ b/modules/ribbit/golioth/dtls_test.py @@ -7,7 +7,7 @@ """ import unittest -from unittest.mock import patch, MagicMock +from ribbit.utils.mock import MagicMock, patch # Import modules we need to test import tls @@ -15,32 +15,43 @@ class TestGoliothDTLS(unittest.TestCase): - @patch('tls.SSLContext') - def test_golioth_uses_tls_module(self, mock_ssl_context): + def test_golioth_uses_tls_module(self): """Test that Golioth client uses the TLS module for DTLS.""" # Setup mock objects + original_ssl_context = tls.SSLContext mock_context = MagicMock() - mock_ssl_context.return_value = mock_context - - # Create config mock - mock_config = MagicMock() - mock_watch = MagicMock() - mock_config.watch.return_value = mock_watch - mock_watch.__enter__.return_value = mock_watch - mock_watch.get.return_value = (True, "example.org", 5684, "user", "password", True) - - # Create OTA manager mock - mock_ota_manager = MagicMock() - - # Create a Golioth client - client = golioth.Golioth(config=mock_config, ota_manager=mock_ota_manager) - - # Verify that the TLS SSLContext was created with DTLS client mode - mock_ssl_context.assert_called_with(tls.PROTOCOL_DTLS_CLIENT) - - # Verify PSK was set - mock_context.set_ciphers.assert_called_with(["TLS-PSK-WITH-AES-128-CBC-SHA256"]) - mock_context.set_psk.assert_called_with("user", "password") + tls.SSLContext = MagicMock(return_value=mock_context) + + try: + # Create config mock + mock_config = MagicMock() + mock_watch = MagicMock() + mock_config.watch.return_value = mock_watch + mock_watch.__enter__ = MagicMock(return_value=mock_watch) + mock_watch.get = MagicMock(return_value=(True, "example.org", 5684, "user", "password", True)) + + # Create OTA manager mock + mock_ota_manager = MagicMock() + + # Mock asyncio.create_task + import asyncio + original_create_task = asyncio.create_task + asyncio.create_task = MagicMock() + + # Create a Golioth client + client = golioth.Golioth(config=mock_config, ota_manager=mock_ota_manager) + + # Verify that the TLS SSLContext was created with DTLS client mode + tls.SSLContext.assert_called_with(tls.PROTOCOL_DTLS_CLIENT) + + # Verify PSK was set + mock_context.set_ciphers.assert_called() + mock_context.set_psk.assert_called() + + finally: + # Restore original functions + tls.SSLContext = original_ssl_context + asyncio.create_task = original_create_task def run_tests(): diff --git a/modules/ribbit/utils/mock.py b/modules/ribbit/utils/mock.py new file mode 100644 index 0000000..4134662 --- /dev/null +++ b/modules/ribbit/utils/mock.py @@ -0,0 +1,58 @@ +""" +Minimal mock implementation for MicroPython testing. + +This provides a basic implementation of mock objects to replace unittest.mock +which is not available in MicroPython. +""" + +class MagicMock: + """A simple mock class that records calls and returns configurable values.""" + + def __init__(self, return_value=None): + self._return_value = return_value + self._calls = [] + self._call_args = [] + self._call_kwargs = [] + self._attributes = {} + + def __call__(self, *args, **kwargs): + self._calls.append((args, kwargs)) + self._call_args.append(args) + self._call_kwargs.append(kwargs) + return self._return_value + + def __getattr__(self, name): + if name not in self._attributes: + self._attributes[name] = MagicMock() + return self._attributes[name] + + def assert_called_with(self, *args, **kwargs): + assert (args, kwargs) in self._calls, f"Expected call with {args}, {kwargs} but got {self._calls}" + + def assert_called(self): + assert len(self._calls) > 0, "Expected call but not called" + + +def patch(target, new_object=None): + """A simplified patch decorator that just returns the patched function.""" + if new_object is None: + new_object = MagicMock() + + class _PatchContext: + def __init__(self, mock): + self.mock = mock + + def __enter__(self): + return self.mock + + def __exit__(self, exc_type, exc_val, exc_tb): + pass + + # For now, this is just a simple decorator pattern for functions + # In real tests, we'll just use direct MagicMock objects + def decorator(func): + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + return wrapper + + return _PatchContext(new_object) if callable(getattr(new_object, "__enter__", None)) else decorator \ No newline at end of file From 7facddb74a7add0a8dcbd8363ca8015459c84f61 Mon Sep 17 00:00:00 2001 From: Keenan Johnson Date: Mon, 24 Mar 2025 12:31:07 -0700 Subject: [PATCH 10/12] Delete uneeded tests --- modules/ribbit/coap/dtls_test.py | 110 ---------------------------- modules/ribbit/golioth/dtls_test.py | 61 --------------- 2 files changed, 171 deletions(-) delete mode 100644 modules/ribbit/coap/dtls_test.py delete mode 100644 modules/ribbit/golioth/dtls_test.py diff --git a/modules/ribbit/coap/dtls_test.py b/modules/ribbit/coap/dtls_test.py deleted file mode 100644 index 4f4f252..0000000 --- a/modules/ribbit/coap/dtls_test.py +++ /dev/null @@ -1,110 +0,0 @@ -""" -Test the TLS module usage in CoAP clients. - -This test verifies that the CoAP module correctly uses -the TLS module for DTLS functionality after the upstream MicroPython -PR #15764 moved DTLS from the ssl module to the tls module. -""" - -import unittest -import socket -import io -from ribbit.utils.mock import MagicMock, patch - -# Import modules we need to test -import tls -from ribbit.coap import Coap - -class DummySocket(io.IOBase): - def __init__(self): - self.write_buffer = bytearray() - self.read_buffer = bytearray() - self.closed = False - - def send(self, data): - self.write_buffer.extend(data) - return len(data) - - def recv(self, size): - return b'' - - def close(self): - self.closed = True - - def setblocking(self, value): - pass - - def connect(self, addr): - pass - - def bind(self, addr): - pass - - -class TestCoAPDTLS(unittest.TestCase): - - def setUp(self): - self.original_getaddrinfo = socket.getaddrinfo - socket.getaddrinfo = MagicMock(return_value=[(None, None, None, None, ('127.0.0.1', 5684))]) - - def tearDown(self): - socket.getaddrinfo = self.original_getaddrinfo - - def test_coap_uses_tls_module(self): - """Test that CoAP client uses the TLS module for DTLS.""" - # Create mock objects - mock_socket = MagicMock() - mock_socket_instance = MagicMock() - mock_socket.return_value = mock_socket_instance - - original_socket = socket.socket - socket.socket = mock_socket - - mock_context = MagicMock() - mock_wrap_socket = MagicMock() - mock_context.wrap_socket = mock_wrap_socket - - original_ssl_context = tls.SSLContext - tls.SSLContext = MagicMock(return_value=mock_context) - - try: - # Create a CoAP client with DTLS enabled - coap = Coap( - host="example.org", - port=5684, - ssl=True # Enable DTLS - ) - - # Mock asyncio for connect - import asyncio - original_create_task = asyncio.create_task - asyncio.create_task = MagicMock() - original_event = asyncio.Event - asyncio.Event = MagicMock() - - try: - # Call connect to initialize the socket with TLS - coap.connect() - except Exception as e: - # We expect an exception due to incomplete mocking of asyncio - pass - - # Verify that the TLS SSLContext was created with DTLS client mode - tls.SSLContext.assert_called_with(tls.PROTOCOL_DTLS_CLIENT) - - # Verify that wrap_socket was called - mock_context.wrap_socket.assert_called() - - finally: - # Restore original functions - socket.socket = original_socket - tls.SSLContext = original_ssl_context - asyncio.create_task = original_create_task - asyncio.Event = original_event - - -def run_tests(): - unittest.main() - -if __name__ == '__main__': - run_tests() \ No newline at end of file diff --git a/modules/ribbit/golioth/dtls_test.py b/modules/ribbit/golioth/dtls_test.py deleted file mode 100644 index 26eff5c..0000000 --- a/modules/ribbit/golioth/dtls_test.py +++ /dev/null @@ -1,61 +0,0 @@ -""" -Test the TLS module usage in Golioth client. - -This test verifies that the Golioth client correctly uses -the TLS module for DTLS functionality after the upstream MicroPython -PR #15764 moved DTLS from the ssl module to the tls module. -""" - -import unittest -from ribbit.utils.mock import MagicMock, patch - -# Import modules we need to test -import tls -import ribbit.golioth as golioth - -class TestGoliothDTLS(unittest.TestCase): - - def test_golioth_uses_tls_module(self): - """Test that Golioth client uses the TLS module for DTLS.""" - # Setup mock objects - original_ssl_context = tls.SSLContext - mock_context = MagicMock() - tls.SSLContext = MagicMock(return_value=mock_context) - - try: - # Create config mock - mock_config = MagicMock() - mock_watch = MagicMock() - mock_config.watch.return_value = mock_watch - mock_watch.__enter__ = MagicMock(return_value=mock_watch) - mock_watch.get = MagicMock(return_value=(True, "example.org", 5684, "user", "password", True)) - - # Create OTA manager mock - mock_ota_manager = MagicMock() - - # Mock asyncio.create_task - import asyncio - original_create_task = asyncio.create_task - asyncio.create_task = MagicMock() - - # Create a Golioth client - client = golioth.Golioth(config=mock_config, ota_manager=mock_ota_manager) - - # Verify that the TLS SSLContext was created with DTLS client mode - tls.SSLContext.assert_called_with(tls.PROTOCOL_DTLS_CLIENT) - - # Verify PSK was set - mock_context.set_ciphers.assert_called() - mock_context.set_psk.assert_called() - - finally: - # Restore original functions - tls.SSLContext = original_ssl_context - asyncio.create_task = original_create_task - - -def run_tests(): - unittest.main() - -if __name__ == '__main__': - run_tests() \ No newline at end of file From 14f3bba8bda0ffd2b7a3fbcda414b7c47968e17f Mon Sep 17 00:00:00 2001 From: Keenan Johnson Date: Mon, 24 Mar 2025 12:34:28 -0700 Subject: [PATCH 11/12] Remove minimal mock implementation for MicroPython testing --- modules/ribbit/utils/mock.py | 58 ------------------------------------ 1 file changed, 58 deletions(-) delete mode 100644 modules/ribbit/utils/mock.py diff --git a/modules/ribbit/utils/mock.py b/modules/ribbit/utils/mock.py deleted file mode 100644 index 4134662..0000000 --- a/modules/ribbit/utils/mock.py +++ /dev/null @@ -1,58 +0,0 @@ -""" -Minimal mock implementation for MicroPython testing. - -This provides a basic implementation of mock objects to replace unittest.mock -which is not available in MicroPython. -""" - -class MagicMock: - """A simple mock class that records calls and returns configurable values.""" - - def __init__(self, return_value=None): - self._return_value = return_value - self._calls = [] - self._call_args = [] - self._call_kwargs = [] - self._attributes = {} - - def __call__(self, *args, **kwargs): - self._calls.append((args, kwargs)) - self._call_args.append(args) - self._call_kwargs.append(kwargs) - return self._return_value - - def __getattr__(self, name): - if name not in self._attributes: - self._attributes[name] = MagicMock() - return self._attributes[name] - - def assert_called_with(self, *args, **kwargs): - assert (args, kwargs) in self._calls, f"Expected call with {args}, {kwargs} but got {self._calls}" - - def assert_called(self): - assert len(self._calls) > 0, "Expected call but not called" - - -def patch(target, new_object=None): - """A simplified patch decorator that just returns the patched function.""" - if new_object is None: - new_object = MagicMock() - - class _PatchContext: - def __init__(self, mock): - self.mock = mock - - def __enter__(self): - return self.mock - - def __exit__(self, exc_type, exc_val, exc_tb): - pass - - # For now, this is just a simple decorator pattern for functions - # In real tests, we'll just use direct MagicMock objects - def decorator(func): - def wrapper(*args, **kwargs): - return func(*args, **kwargs) - return wrapper - - return _PatchContext(new_object) if callable(getattr(new_object, "__enter__", None)) else decorator \ No newline at end of file From efef4140edef8bc3bc77e9f849d370565468199f Mon Sep 17 00:00:00 2001 From: Keenan Johnson Date: Mon, 24 Mar 2025 12:34:52 -0700 Subject: [PATCH 12/12] Remove test --- modules/ribbit/coap/dtls_integration_test.py | 120 ------------------- 1 file changed, 120 deletions(-) delete mode 100644 modules/ribbit/coap/dtls_integration_test.py diff --git a/modules/ribbit/coap/dtls_integration_test.py b/modules/ribbit/coap/dtls_integration_test.py deleted file mode 100644 index 70d47bd..0000000 --- a/modules/ribbit/coap/dtls_integration_test.py +++ /dev/null @@ -1,120 +0,0 @@ -""" -Integration test for DTLS functionality in CoAP client. - -This test creates a basic server and client to verify proper DTLS operation. -To run this test, you need a proper DTLS server or a mock server. - -Note: This test is intended to be run manually when verifying DTLS changes. -""" - -import asyncio -import tls -import socket -from ribbit.coap import Coap, CoapPacket, TYPE_CON, METHOD_GET - -# This is a basic integration test that can be run manually -# to test the DTLS functionality with actual server - -async def test_coap_dtls_connection(): - """Test connecting to a CoAP server with DTLS enabled.""" - - # Create a TLS context for DTLS - ctx = tls.SSLContext(tls.PROTOCOL_DTLS_CLIENT) - - # You may need to configure the context with PSK or certificates - # ctx.set_ciphers(["TLS-PSK-WITH-AES-128-CBC-SHA256"]) - # ctx.set_psk("user_id", "password") - - # Replace with your CoAP+DTLS server address and port - coap_server = "coap.example.com" - coap_port = 5684 - - try: - # Create CoAP client with DTLS enabled - coap = Coap( - host=coap_server, - port=coap_port, - ssl=ctx - ) - - # Connect to the server - print(f"Connecting to {coap_server}:{coap_port} with DTLS...") - await coap.connect() - - print("Connected successfully. Sending ping...") - await coap.ping() - print("Ping successful!") - - # Try to fetch a resource - print("Fetching resource...") - response = await coap.get(".well-known/core") - print(f"Response received: {response}") - - # Clean up - await coap.disconnect() - print("Test completed successfully") - - except Exception as e: - print(f"Error in DTLS test: {e}") - raise - -# Local test server setup - you can implement a simple DTLS server here -# if needed for testing without depending on an external service - -class DtlsTestServer: - """Minimal DTLS test server for testing the client.""" - def __init__(self, host="0.0.0.0", port=5684): - self.host = host - self.port = port - self.socket = None - self.context = None - - async def start(self): - """Start the DTLS test server.""" - # Create a TLS context for DTLS server - self.context = tls.SSLContext(tls.PROTOCOL_DTLS_SERVER) - - # For testing, we can use a self-signed certificate - # self.context.load_cert_chain("cert.pem", "key.pem") - - # Or PSK for simpler setup - # self.context.set_ciphers(["TLS-PSK-WITH-AES-128-CBC-SHA256"]) - # self.context.set_psk("user_id", "password") - - # Create and bind the socket - self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - self.socket.bind((self.host, self.port)) - - print(f"DTLS test server starting on {self.host}:{self.port}") - - # Wrap the socket with DTLS - self.socket = self.context.wrap_socket( - self.socket, - server_side=True - ) - - # Handle incoming connections - while True: - try: - data, addr = self.socket.recvfrom(1024) - print(f"Received {len(data)} bytes from {addr}") - - # Send a response - response = b"\x60\x45\x00\x01\x00\x00\x00\x00\xFF\x68\x65\x6C\x6C\x6F" # CoAP response - self.socket.sendto(response, addr) - except Exception as e: - print(f"Error handling connection: {e}") - - def stop(self): - """Stop the DTLS test server.""" - if self.socket: - self.socket.close() - -# Run the test -if __name__ == "__main__": - # Run the client test - asyncio.run(test_coap_dtls_connection()) - - # Uncomment to run the server instead - # server = DtlsTestServer() - # asyncio.run(server.start()) \ No newline at end of file