diff --git a/.github/.wordlist.txt b/.github/.wordlist.txt
index 5c24cdec825ac1..21e4b6c71059c8 100644
--- a/.github/.wordlist.txt
+++ b/.github/.wordlist.txt
@@ -326,6 +326,7 @@ CurrentHue
CurrentLevel
CurrentSaturation
customAcl
+customizable
customizations
cvfJ
cxx
@@ -1039,6 +1040,7 @@ otatesting
otaURL
OTBR
otcli
+OU
outform
outgoingCommands
overridable
@@ -1150,6 +1152,7 @@ PyObject
pypi
PyRun
pytest
+PYTHONPATH
QEMU
Qorvo
QPG
@@ -1326,6 +1329,7 @@ SRP
SRV
SSBL
SSID
+SSL
startoffset
StartScan
startsWith
@@ -1490,6 +1494,7 @@ unfocus
Unicast
UniFlash
UnitLocalization
+unittest
unpair
unprovisioned
Unsecure
diff --git a/.vscode/launch.json b/.vscode/launch.json
index b86e385f9921e4..5e15018e96c46f 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -4,12 +4,37 @@
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
+ {
+ "name": "Python: Mock Server Tests",
+ "type": "debugpy",
+ "request": "launch",
+ "module": "unittest",
+ "args": [
+ "${workspaceFolder}/integrations/mock_server/tests/test_mock_server.py"
+ ],
+ "env": {
+ "PYTHONPATH": "${workspaceFolder}/integrations/mock_server/src:${PYTHONPATH}"
+ },
+ "console": "integratedTerminal",
+ "cwd": "${workspaceFolder}"
+ },
{
"name": "Python Debugger: test_dcl_server",
"type": "debugpy",
"request": "launch",
- "program": "/workspace/connectedhomeip/examples/chip-tool/commands/dcl/test_dcl_server.py",
- "args": [],
+ "program": "${workspaceFolder}/integrations/mock_server/src/main.py",
+ "args": [
+ "--port",
+ "8443",
+ "--config",
+ "${workspaceFolder}/integrations/mock_server/configurations/server_config.json",
+ "--routing-config-dir",
+ "${workspaceFolder}/integrations/mock_server/configurations/fake_distributed_compliance_ledger",
+ "--cert",
+ "${workspaceFolder}/server.crt",
+ "--key",
+ "${workspaceFolder}/server.key"
+ ],
"console": "integratedTerminal"
},
{
diff --git a/examples/chip-tool/commands/dcl/test_dcl_server.py b/examples/chip-tool/commands/dcl/test_dcl_server.py
deleted file mode 100755
index 1b68904b4f64b0..00000000000000
--- a/examples/chip-tool/commands/dcl/test_dcl_server.py
+++ /dev/null
@@ -1,251 +0,0 @@
-#!/usr/bin/env -S python3 -B
-
-# Copyright (c) 2025 Project CHIP Authors
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-import base64
-import hashlib
-import http.server
-import json
-import os
-import re
-import ssl
-
-DEFAULT_HOSTNAME = "localhost"
-DEFAULT_PORT = 4443
-
-
-TC = {
- 0XFFF1: {
- 0x8001: {
- "schemaVersion": 1,
- "esfRevision": 1,
- "defaultCountry": "US",
- "countryEntries": {
- "US": {
- "defaultLanguage": "en",
- "languageEntries": {
- "en": [
- {
- "ordinal": 0,
- "required": True,
- "title": "Terms and Conditions",
- "text": "
Feature 1 Text
Please accept these.
"
- },
- {
- "ordinal": 1,
- "required": False,
- "title": "Privacy Policy",
- "text": "Feature 2 Text
"
- }
- ],
- "es": [
- {
- "ordinal": 0,
- "required": True,
- "title": "Términos y condiciones",
- "text": "Característica 1 Texto
Por favor acéptelos.
"
- },
- {
- "ordinal": 1,
- "required": False,
- "title": "Política de privacidad",
- "text": "Característica 2 Texto
"
- }
- ]
- }
- },
- "MX": {
- "defaultLanguage": "es",
- "languageEntries": {
- "es": [
- {
- "ordinal": 0,
- "required": True,
- "title": "Términos y condiciones",
- "text": "Característica 1 Texto
Por favor acéptelos.
"
- }
- ]
- }
- },
- "CN": {
- "defaultLanguage": "zh",
- "languageEntries": {
- "zh": [
- {
- "ordinal": 0,
- "required": True,
- "title": "条款和条件",
- "text": "产品1文字
"
- },
- {
- "ordinal": 1,
- "required": False,
- "title": "隐私条款",
- "text": "产品2文字
"
- }
- ]
- }
- },
- "RU": {
- "defaultLanguage": "ru",
- "languageEntries": {
- "ru": [
- {
- "ordinal": 0,
- "required": True,
- "title": "Условия и положения",
- "text": "Текст функции 1
Пожалуйста, примите эти условия пользования.
"
- },
- {
- "ordinal": 1,
- "required": False,
- "title": "Положение о конфиденциальности",
- "text": "Текст функции 2
"
- }
- ]
- }
- }
- }
- }
- }
-}
-
-MODELS = {
- 0XFFF1: {
- 0x8001: {
- "model":
- {
- "vid": 65521,
- "pid": 32769,
- "deviceTypeId": 65535,
- "productName": "TEST_PRODUCT",
- "productLabel": "All Clusters App",
- "partNumber": "",
- "commissioningCustomFlow": 2,
- "commissioningCustomFlowUrl": "",
- "commissioningModeInitialStepsHint": 0,
- "commissioningModeInitialStepsInstruction": "",
- "commissioningModeSecondaryStepsHint": 0,
- "commissioningModeSecondaryStepsInstruction": "",
- "creator": "chip project",
- "lsfRevision": 0,
- "lsfUrl": "",
- "productUrl": "https://github.com/project-chip/connectedhomeip/tree/master/examples/all-clusters-app",
- "supportUrl": "https://github.com/project-chip/connectedhomeip/",
- "userManualUrl": "",
- "enhancedSetupFlowOptions": 1,
- "enhancedSetupFlowTCUrl": f"https://{DEFAULT_HOSTNAME}:{DEFAULT_PORT}/tc/65521/32769",
- "enhancedSetupFlowTCRevision": 1,
- "enhancedSetupFlowTCDigest": "",
- "enhancedSetupFlowTCFileSize": 0,
- "enhancedSetupFlowMaintenanceUrl": ""
- }
- }
- }
-}
-
-
-class RESTRequestHandler(http.server.BaseHTTPRequestHandler):
- def __init__(self, *args, **kwargs):
- self.routes = {
- r"/dcl/model/models/(\d+)/(\d+)": self.handle_model_request,
- r"/tc/(\d+)/(\d+)": self.handle_tc_request,
- }
- super().__init__(*args, **kwargs)
-
- def do_GET(self):
- for pattern, handler in self.routes.items():
- match = re.match(pattern, self.path)
- if match:
- response = handler(*match.groups())
- if response:
- self.send_response(200)
- self.send_header("Content-Type", "application/json")
- self.end_headers()
- self.wfile.write(json.dumps(response).encode("utf-8"))
- return
-
- # Handle 404 for unmatched paths
- self.send_response(404)
- self.send_header("Content-Type", "application/json")
- self.end_headers()
- self.wfile.write(json.dumps({"error": "Not found"}).encode("utf-8"))
-
- def handle_model_request(self, vendor_id, product_id):
- vendor_id = int(vendor_id)
- product_id = int(product_id)
- if vendor_id in MODELS and product_id in MODELS[vendor_id]:
- model = MODELS[int(vendor_id)][int(product_id)]
- # We will return a model that contains the file size and the digest of the TC.
- # Instead of manually setting them, it is calculated on the fly.
- tc = TC[int(vendor_id)][int(product_id)]
- tc_encoded = json.dumps(tc).encode("utf-8")
- sha256_hash = hashlib.sha256(tc_encoded).digest()
- model['model']['enhancedSetupFlowTCFileSize'] = len(tc_encoded)
- model['model']['enhancedSetupFlowTCDigest'] = base64.b64encode(
- sha256_hash).decode("utf-8")
-
- return model
-
- return None
-
- def handle_tc_request(self, vendor_id, product_id):
- vendor_id = int(vendor_id)
- product_id = int(product_id)
- if vendor_id in TC and product_id in TC[vendor_id]:
- return TC[int(vendor_id)][int(product_id)]
-
- return None
-
-
-def run_https_server(cert_file="cert.pem", key_file="key.pem"):
- # Creates a basic HTTP server instance that listens on DEFAULT_HOSTNAME and DEFAULT_PORT
- # RESTRequestHandler handles incoming HTTP requests
- httpd = http.server.HTTPServer((DEFAULT_HOSTNAME, DEFAULT_PORT), RESTRequestHandler)
-
- # Creates an SSL context using TLS protocol for secure communications
- context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
-
- # Loads the SSL certificate and private key for the server
- # cert_file: contains the server's public certificate
- # key_file: contains the server's private key
- context.load_cert_chain(certfile=cert_file, keyfile=key_file)
-
- # Uses a context manager (with statement) to wrap the HTTP server's socket with SSL
- # server_side=True indicates this is a server socket
- # The wrapped socket is automatically closed when exiting the with block
- with context.wrap_socket(httpd.socket, server_side=True) as httpd.socket:
- print(f"Serving on https://{DEFAULT_HOSTNAME}:{DEFAULT_PORT}")
- # Starts the server and runs indefinitely, handling incoming HTTPS requests
- httpd.serve_forever()
-
-
-# Generate self-signed certificates if needed
-def generate_self_signed_cert(cert_file="cert.pem", key_file="key.pem"):
- from subprocess import run
- run([
- "openssl", "req", "-x509", "-nodes", "-days", "365", "-newkey", "rsa:2048",
- "-keyout", key_file, "-out", cert_file,
- "-subj", f"/C=US/ST=Test/L=Test/O=Test/OU=Test/CN={DEFAULT_HOSTNAME}"
- ])
-
-
-# Check if certificates exist; if not, generate them
-if not os.path.exists("cert.pem") or not os.path.exists("key.pem"):
- print("Generating self-signed certificates...")
- generate_self_signed_cert()
-
-# Run the server
-run_https_server()
diff --git a/integrations/mock_server/README.md b/integrations/mock_server/README.md
new file mode 100644
index 00000000000000..49945d190dd776
--- /dev/null
+++ b/integrations/mock_server/README.md
@@ -0,0 +1,56 @@
+# Mock HTTP/HTTPS Server
+
+## Overview
+
+This project provides a configurable mock HTTP/HTTPS server designed for API
+testing, dynamic response generation, and automated request handling. It
+supports static responses, dynamic custom response handlers, query parameter
+matching, request body validation (including regex), and both HTTP and HTTPS
+protocols.
+
+## Setup
+
+```bash
+openssl req -x509 -newkey rsa:2048 -keyout server.key -out server.crt -days 365 -nodes -subj "/C=US/ST= /L= /O= /OU= /CN=localhost"
+```
+
+## Features
+
+### Server Functionality
+
+- Secure HTTPS with TLS support
+- CLI-driven execution with customizable options (port, configuration files,
+ SSL options)
+- Handles GET, POST, PUT, and DELETE requests
+- Concurrent request handling via threading
+
+### Route Matching
+
+- Exact path and wildcard (\*) path matching
+- Query parameter validation
+- Priority-based route matching
+
+### Configuration
+
+- Main server configuration file
+- Separate routing configuration directory
+- JSON-based configuration format
+- Dynamic route loading
+
+### Security & Logging
+
+- TLS encryption (HTTPS only)
+- Structured logging with DEBUG level
+- Graceful error handling for invalid routes
+
+## Running Tests
+
+### Test Execution
+
+You can run the tests using one of these methods:
+
+1. Using Python unittest with PYTHONPATH:
+
+```bash
+PYTHONPATH=$PYTHONPATH:/workspace/connectedhomeip/integrations/mock_server/src python3 -m unittest integrations/mock_server/tests/test_mock_server.py
+```
diff --git a/integrations/mock_server/configurations/fake_distributed_compliance_ledger/dcl-model-models-65521-32769.json b/integrations/mock_server/configurations/fake_distributed_compliance_ledger/dcl-model-models-65521-32769.json
new file mode 100644
index 00000000000000..1b558438b7bb7b
--- /dev/null
+++ b/integrations/mock_server/configurations/fake_distributed_compliance_ledger/dcl-model-models-65521-32769.json
@@ -0,0 +1,42 @@
+{
+ "routes": [
+ {
+ "method": "GET",
+ "path": "/dcl/model/models/65521/32769",
+ "response": {
+ "status": 200,
+ "headers": {
+ "Content-Type": "application/json"
+ },
+ "body": {
+ "model": {
+ "vid": 65521,
+ "pid": 32769,
+ "deviceTypeId": 259,
+ "productName": "TEST_PRODUCT",
+ "productLabel": "Terms and Conditions App",
+ "partNumber": "",
+ "commissioningCustomFlow": 2,
+ "commissioningCustomFlowUrl": "",
+ "commissioningModeInitialStepsHint": 0,
+ "commissioningModeInitialStepsInstruction": "",
+ "commissioningModeSecondaryStepsHint": 0,
+ "commissioningModeSecondaryStepsInstruction": "",
+ "creator": "chip project",
+ "lsfRevision": 0,
+ "lsfUrl": "",
+ "productUrl": "https://github.com/project-chip/connectedhomeip/tree/master/examples/terms-and-conditions-app",
+ "supportUrl": "https://github.com/project-chip/connectedhomeip/",
+ "userManualUrl": "",
+ "enhancedSetupFlowOptions": 1,
+ "enhancedSetupFlowTCUrl": "https://localhost:44538/tc/65521/32769",
+ "enhancedSetupFlowTCRevision": 1,
+ "enhancedSetupFlowTCDigest": "Q+zDGwvJk+tZtBV28b/L4YLw5LIUklk46bTZfyDpJkE=",
+ "enhancedSetupFlowTCFileSize": 2060,
+ "enhancedSetupFlowMaintenanceUrl": ""
+ }
+ }
+ }
+ }
+ ]
+}
diff --git a/integrations/mock_server/configurations/fake_distributed_compliance_ledger/dcl-model-models-65521-32785.json b/integrations/mock_server/configurations/fake_distributed_compliance_ledger/dcl-model-models-65521-32785.json
new file mode 100644
index 00000000000000..67eaa4531f1e04
--- /dev/null
+++ b/integrations/mock_server/configurations/fake_distributed_compliance_ledger/dcl-model-models-65521-32785.json
@@ -0,0 +1,42 @@
+{
+ "routes": [
+ {
+ "method": "GET",
+ "path": "/dcl/model/models/65521/32785",
+ "response": {
+ "status": 200,
+ "headers": {
+ "Content-Type": "application/json"
+ },
+ "body": {
+ "model": {
+ "vid": 65521,
+ "pid": 32785,
+ "deviceTypeId": 259,
+ "productName": "TEST_PRODUCT",
+ "productLabel": "Terms and Conditions App",
+ "partNumber": "",
+ "commissioningCustomFlow": 2,
+ "commissioningCustomFlowUrl": "",
+ "commissioningModeInitialStepsHint": 0,
+ "commissioningModeInitialStepsInstruction": "",
+ "commissioningModeSecondaryStepsHint": 0,
+ "commissioningModeSecondaryStepsInstruction": "",
+ "creator": "chip project",
+ "lsfRevision": 0,
+ "lsfUrl": "",
+ "productUrl": "https://github.com/project-chip/connectedhomeip/tree/master/examples/terms-and-conditions-app",
+ "supportUrl": "https://github.com/project-chip/connectedhomeip/",
+ "userManualUrl": "",
+ "enhancedSetupFlowOptions": 1,
+ "enhancedSetupFlowTCUrl": "https://localhost:44538/tc/65521/32785",
+ "enhancedSetupFlowTCRevision": 1,
+ "enhancedSetupFlowTCDigest": "Q+zDGwvJk+tZtBV28b/L4YLw5LIUklk46bTZfyDpJkE=",
+ "enhancedSetupFlowTCFileSize": 2060,
+ "enhancedSetupFlowMaintenanceUrl": ""
+ }
+ }
+ }
+ }
+ ]
+}
diff --git a/integrations/mock_server/configurations/fake_distributed_compliance_ledger/dcl-model-models-65521-32786.json b/integrations/mock_server/configurations/fake_distributed_compliance_ledger/dcl-model-models-65521-32786.json
new file mode 100644
index 00000000000000..854099117fc670
--- /dev/null
+++ b/integrations/mock_server/configurations/fake_distributed_compliance_ledger/dcl-model-models-65521-32786.json
@@ -0,0 +1,42 @@
+{
+ "routes": [
+ {
+ "method": "GET",
+ "path": "/dcl/model/models/65521/32786",
+ "response": {
+ "status": 200,
+ "headers": {
+ "Content-Type": "application/json"
+ },
+ "body": {
+ "model": {
+ "vid": 65521,
+ "pid": 32786,
+ "deviceTypeId": 259,
+ "productName": "TEST_PRODUCT",
+ "productLabel": "Terms and Conditions App",
+ "partNumber": "",
+ "commissioningCustomFlow": 2,
+ "commissioningCustomFlowUrl": "",
+ "commissioningModeInitialStepsHint": 0,
+ "commissioningModeInitialStepsInstruction": "",
+ "commissioningModeSecondaryStepsHint": 0,
+ "commissioningModeSecondaryStepsInstruction": "",
+ "creator": "chip project",
+ "lsfRevision": 0,
+ "lsfUrl": "",
+ "productUrl": "https://github.com/project-chip/connectedhomeip/tree/master/examples/terms-and-conditions-app",
+ "supportUrl": "https://github.com/project-chip/connectedhomeip/",
+ "userManualUrl": "",
+ "enhancedSetupFlowOptions": 1,
+ "enhancedSetupFlowTCUrl": "https://localhost:44538/tc/65521/32786",
+ "enhancedSetupFlowTCRevision": 1,
+ "enhancedSetupFlowTCDigest": "Q+zDGwvJk+tZtBV28b/L4YLw5LIUklk46bTZfyDpJkE=",
+ "enhancedSetupFlowTCFileSize": 2060,
+ "enhancedSetupFlowMaintenanceUrl": ""
+ }
+ }
+ }
+ }
+ ]
+}
diff --git a/integrations/mock_server/configurations/fake_distributed_compliance_ledger/dcl-model-models-65522-32769.json b/integrations/mock_server/configurations/fake_distributed_compliance_ledger/dcl-model-models-65522-32769.json
new file mode 100644
index 00000000000000..3511e6fb8c7b1c
--- /dev/null
+++ b/integrations/mock_server/configurations/fake_distributed_compliance_ledger/dcl-model-models-65522-32769.json
@@ -0,0 +1,42 @@
+{
+ "routes": [
+ {
+ "method": "GET",
+ "path": "/dcl/model/models/65522/32769",
+ "response": {
+ "status": 200,
+ "headers": {
+ "Content-Type": "application/json"
+ },
+ "body": {
+ "model": {
+ "vid": 65522,
+ "pid": 32769,
+ "deviceTypeId": 259,
+ "productName": "TEST_PRODUCT",
+ "productLabel": "Terms and Conditions App",
+ "partNumber": "",
+ "commissioningCustomFlow": 2,
+ "commissioningCustomFlowUrl": "",
+ "commissioningModeInitialStepsHint": 0,
+ "commissioningModeInitialStepsInstruction": "",
+ "commissioningModeSecondaryStepsHint": 0,
+ "commissioningModeSecondaryStepsInstruction": "",
+ "creator": "chip project",
+ "lsfRevision": 0,
+ "lsfUrl": "",
+ "productUrl": "https://github.com/project-chip/connectedhomeip/tree/master/examples/terms-and-conditions-app",
+ "supportUrl": "https://github.com/project-chip/connectedhomeip/",
+ "userManualUrl": "",
+ "enhancedSetupFlowOptions": 1,
+ "enhancedSetupFlowTCUrl": "https://localhost:44538/tc/65522/32769",
+ "enhancedSetupFlowTCRevision": 1,
+ "enhancedSetupFlowTCDigest": "fcqhuEAkq6nNQARcqrGGMcZc4KCFZCfJmrmK6ysd7zo=",
+ "enhancedSetupFlowTCFileSize": 279,
+ "enhancedSetupFlowMaintenanceUrl": ""
+ }
+ }
+ }
+ }
+ ]
+}
diff --git a/integrations/mock_server/configurations/fake_product_server/terms-and-conditions-65521-32769-v1.json b/integrations/mock_server/configurations/fake_product_server/terms-and-conditions-65521-32769-v1.json
new file mode 100644
index 00000000000000..f17028df7583f9
--- /dev/null
+++ b/integrations/mock_server/configurations/fake_product_server/terms-and-conditions-65521-32769-v1.json
@@ -0,0 +1,105 @@
+{
+ "routes": [
+ {
+ "method": "GET",
+ "path": "/tc/65521/32769",
+ "response": {
+ "status": 200,
+ "headers": {
+ "Content-Type": "application/json"
+ },
+ "body": {
+ "schemaVersion": 1,
+ "esfRevision": 1,
+ "defaultCountry": "US",
+ "countryEntries": {
+ "US": {
+ "defaultLanguage": "en",
+ "languageEntries": {
+ "en": [
+ {
+ "ordinal": 0,
+ "required": true,
+ "title": "Terms and Conditions",
+ "text": "Feature 1 Text
Please accept these.
"
+ },
+ {
+ "ordinal": 1,
+ "required": false,
+ "title": "Privacy Policy",
+ "text": "Feature 2 Text
"
+ }
+ ],
+ "es": [
+ {
+ "ordinal": 0,
+ "required": true,
+ "title": "Términos y condiciones",
+ "text": "Característica 1 Texto
Por favor acéptelos.
"
+ },
+ {
+ "ordinal": 1,
+ "required": false,
+ "title": "Política de privacidad",
+ "text": "Característica 2 Texto
"
+ }
+ ]
+ }
+ },
+ "MX": {
+ "defaultLanguage": "es",
+ "languageEntries": {
+ "es": [
+ {
+ "ordinal": 0,
+ "required": true,
+ "title": "Términos y condiciones",
+ "text": "Característica 1 Texto
Por favor acéptelos.
"
+ }
+ ]
+ }
+ },
+ "CN": {
+ "defaultLanguage": "zh",
+ "languageEntries": {
+ "zh": [
+ {
+ "ordinal": 0,
+ "required": true,
+ "title": "条款和条件",
+ "text": "产品1文字
"
+ },
+ {
+ "ordinal": 1,
+ "required": false,
+ "title": "隐私条款",
+ "text": "产品2文字
"
+ }
+ ]
+ }
+ },
+ "RU": {
+ "defaultLanguage": "ru",
+ "languageEntries": {
+ "ru": [
+ {
+ "ordinal": 0,
+ "required": true,
+ "title": "Условия и положения",
+ "text": "Текст функции 1
Пожалуйста, примите эти условия пользования.
"
+ },
+ {
+ "ordinal": 1,
+ "required": false,
+ "title": "оложение о конфиденциальности",
+ "text": "Текст функции 2
"
+ }
+ ]
+ }
+ }
+ }
+ }
+ }
+ }
+ ]
+}
diff --git a/integrations/mock_server/configurations/fake_product_server/terms-and-conditions-65521-32785-v1.json b/integrations/mock_server/configurations/fake_product_server/terms-and-conditions-65521-32785-v1.json
new file mode 100644
index 00000000000000..cbd762900b99c9
--- /dev/null
+++ b/integrations/mock_server/configurations/fake_product_server/terms-and-conditions-65521-32785-v1.json
@@ -0,0 +1,105 @@
+{
+ "routes": [
+ {
+ "method": "GET",
+ "path": "/tc/65521/32785",
+ "response": {
+ "status": 200,
+ "headers": {
+ "Content-Type": "application/json"
+ },
+ "body": {
+ "schemaVersion": 1,
+ "esfRevision": 1,
+ "defaultCountry": "US",
+ "countryEntries": {
+ "US": {
+ "defaultLanguage": "en",
+ "languageEntries": {
+ "en": [
+ {
+ "ordinal": 0,
+ "required": true,
+ "title": "Terms and Conditions",
+ "text": "Feature 1 Text
Please accept these.
"
+ },
+ {
+ "ordinal": 1,
+ "required": false,
+ "title": "Privacy Policy",
+ "text": "Feature 2 Text
"
+ }
+ ],
+ "es": [
+ {
+ "ordinal": 0,
+ "required": true,
+ "title": "Términos y condiciones",
+ "text": "Característica 1 Texto
Por favor acéptelos.
"
+ },
+ {
+ "ordinal": 1,
+ "required": false,
+ "title": "Política de privacidad",
+ "text": "Característica 2 Texto
"
+ }
+ ]
+ }
+ },
+ "MX": {
+ "defaultLanguage": "es",
+ "languageEntries": {
+ "es": [
+ {
+ "ordinal": 0,
+ "required": true,
+ "title": "Términos y condiciones",
+ "text": "Característica 1 Texto
Por favor acéptelos.
"
+ }
+ ]
+ }
+ },
+ "CN": {
+ "defaultLanguage": "zh",
+ "languageEntries": {
+ "zh": [
+ {
+ "ordinal": 0,
+ "required": true,
+ "title": "条款和条件",
+ "text": "产品1文字
"
+ },
+ {
+ "ordinal": 1,
+ "required": false,
+ "title": "隐私条款",
+ "text": "产品2文字
"
+ }
+ ]
+ }
+ },
+ "RU": {
+ "defaultLanguage": "ru",
+ "languageEntries": {
+ "ru": [
+ {
+ "ordinal": 0,
+ "required": true,
+ "title": "Условия и положения",
+ "text": "Текст функции 1
Пожалуйста, примите эти условия пользования.
"
+ },
+ {
+ "ordinal": 1,
+ "required": false,
+ "title": "оложение о конфиденциальности",
+ "text": "Текст функции 2
"
+ }
+ ]
+ }
+ }
+ }
+ }
+ }
+ }
+ ]
+}
diff --git a/integrations/mock_server/configurations/fake_product_server/terms-and-conditions-65521-32786-v1.json b/integrations/mock_server/configurations/fake_product_server/terms-and-conditions-65521-32786-v1.json
new file mode 100644
index 00000000000000..f2166dd201ed18
--- /dev/null
+++ b/integrations/mock_server/configurations/fake_product_server/terms-and-conditions-65521-32786-v1.json
@@ -0,0 +1,105 @@
+{
+ "routes": [
+ {
+ "method": "GET",
+ "path": "/tc/65521/32786",
+ "response": {
+ "status": 200,
+ "headers": {
+ "Content-Type": "application/json"
+ },
+ "body": {
+ "schemaVersion": 1,
+ "esfRevision": 1,
+ "defaultCountry": "US",
+ "countryEntries": {
+ "US": {
+ "defaultLanguage": "en",
+ "languageEntries": {
+ "en": [
+ {
+ "ordinal": 0,
+ "required": true,
+ "title": "Terms and Conditions",
+ "text": "Feature 1 Text
Please accept these.
"
+ },
+ {
+ "ordinal": 1,
+ "required": false,
+ "title": "Privacy Policy",
+ "text": "Feature 2 Text
"
+ }
+ ],
+ "es": [
+ {
+ "ordinal": 0,
+ "required": true,
+ "title": "Términos y condiciones",
+ "text": "Característica 1 Texto
Por favor acéptelos.
"
+ },
+ {
+ "ordinal": 1,
+ "required": false,
+ "title": "Política de privacidad",
+ "text": "Característica 2 Texto
"
+ }
+ ]
+ }
+ },
+ "MX": {
+ "defaultLanguage": "es",
+ "languageEntries": {
+ "es": [
+ {
+ "ordinal": 0,
+ "required": true,
+ "title": "Términos y condiciones",
+ "text": "Característica 1 Texto
Por favor acéptelos.
"
+ }
+ ]
+ }
+ },
+ "CN": {
+ "defaultLanguage": "zh",
+ "languageEntries": {
+ "zh": [
+ {
+ "ordinal": 0,
+ "required": true,
+ "title": "条款和条件",
+ "text": "产品1文字
"
+ },
+ {
+ "ordinal": 1,
+ "required": false,
+ "title": "隐私条款",
+ "text": "产品2文字
"
+ }
+ ]
+ }
+ },
+ "RU": {
+ "defaultLanguage": "ru",
+ "languageEntries": {
+ "ru": [
+ {
+ "ordinal": 0,
+ "required": true,
+ "title": "Условия и положения",
+ "text": "Текст функции 1
Пожалуйста, примите эти условия пользования.
"
+ },
+ {
+ "ordinal": 1,
+ "required": false,
+ "title": "оложение о конфиденциальности",
+ "text": "Текст функции 2
"
+ }
+ ]
+ }
+ }
+ }
+ }
+ }
+ }
+ ]
+}
diff --git a/integrations/mock_server/configurations/fake_product_server/terms-and-conditions-65522-32769-v1.json b/integrations/mock_server/configurations/fake_product_server/terms-and-conditions-65522-32769-v1.json
new file mode 100644
index 00000000000000..a8dd6d541c9791
--- /dev/null
+++ b/integrations/mock_server/configurations/fake_product_server/terms-and-conditions-65522-32769-v1.json
@@ -0,0 +1,34 @@
+{
+ "routes": [
+ {
+ "method": "GET",
+ "path": "/tc/65522/32769",
+ "response": {
+ "status": 200,
+ "headers": {
+ "Content-Type": "application/json"
+ },
+ "body": {
+ "schemaVersion": 1,
+ "esfRevision": 1,
+ "defaultCountry": "US",
+ "countryEntries": {
+ "US": {
+ "defaultLanguage": "en",
+ "languageEntries": {
+ "en": [
+ {
+ "ordinal": 0,
+ "required": true,
+ "title": "Terms and Conditions",
+ "text": "Feature 1 Text
Please accept these.
"
+ }
+ ]
+ }
+ }
+ }
+ }
+ }
+ }
+ ]
+}
diff --git a/integrations/mock_server/configurations/server_config.json b/integrations/mock_server/configurations/server_config.json
new file mode 100644
index 00000000000000..0967ef424bce67
--- /dev/null
+++ b/integrations/mock_server/configurations/server_config.json
@@ -0,0 +1 @@
+{}
diff --git a/integrations/mock_server/configurations/unittest/config.json b/integrations/mock_server/configurations/unittest/config.json
new file mode 100644
index 00000000000000..019f0909c143fd
--- /dev/null
+++ b/integrations/mock_server/configurations/unittest/config.json
@@ -0,0 +1,17 @@
+{
+ "routes": [
+ {
+ "method": "GET",
+ "path": "/api/data",
+ "response": {
+ "status": 200,
+ "headers": {
+ "Content-Type": "application/json"
+ },
+ "body": {
+ "message": "Hello, World!"
+ }
+ }
+ }
+ ]
+}
diff --git a/integrations/mock_server/src/__init__.py b/integrations/mock_server/src/__init__.py
new file mode 100644
index 00000000000000..1c89d20d346a80
--- /dev/null
+++ b/integrations/mock_server/src/__init__.py
@@ -0,0 +1,13 @@
+# Copyright (c) 2025 Project CHIP Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/integrations/mock_server/src/handler.py b/integrations/mock_server/src/handler.py
new file mode 100644
index 00000000000000..076f1ad8650428
--- /dev/null
+++ b/integrations/mock_server/src/handler.py
@@ -0,0 +1,112 @@
+# Copyright (c) 2025 Project CHIP Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import http.server
+import json
+import urllib.parse
+from typing import List, Optional, Type
+
+from route_configuration import Configuration, Route
+from router import match_route
+
+
+def createMockServerHandler(config: Configuration) -> Type[http.server.BaseHTTPRequestHandler]:
+ """
+ Creates a custom HTTP request handler class configured with predefined routes and responses.
+
+ This factory function generates a new HTTP request handler class that processes incoming
+ HTTP requests according to the provided routing configuration. The handler supports
+ route matching based on HTTP method, path, and query parameters, returning predefined
+ responses for matched routes.
+
+ Args:
+ config (Configuration): A Configuration object containing route definitions.
+ Each route should specify:
+ - HTTP method (GET, POST, PUT, DELETE)
+ - URL path pattern
+ - Response details (status code, headers, body)
+ - Optional query parameters for matching
+
+ Returns:
+ Type[http.server.BaseHTTPRequestHandler]: A new handler class that:
+ - Processes standard HTTP methods (GET, POST, PUT, DELETE)
+ - Matches incoming requests against configured routes
+ - Returns JSON or plain text responses based on configuration
+ - Provides 404 responses for unmatched routes
+
+ Example Configuration:
+ {
+ "routing": [
+ {
+ "method": "GET",
+ "path": "/api/users",
+ "response": {
+ "status": 200,
+ "headers": {"Content-Type": "application/json"},
+ "body": {"users": []}
+ }
+ }
+ ]
+ }
+
+ Note:
+ - The handler automatically sets appropriate Content-Type headers
+ - JSON responses are automatically encoded to UTF-8
+ - Non-JSON responses are converted to strings before encoding
+ - All responses include standard HTTP headers and status codes
+ """
+
+ class MockServerHandler(http.server.BaseHTTPRequestHandler):
+ def _set_headers(self, status_code=200, headers=None) -> None:
+ self.send_response(status_code)
+ if headers:
+ for key, value in headers.items():
+ self.send_header(key, value)
+ self.end_headers()
+
+ def do_GET(self) -> None:
+ self.handle_request()
+
+ def do_POST(self) -> None:
+ self.handle_request()
+
+ def do_PUT(self) -> None:
+ self.handle_request()
+
+ def do_DELETE(self) -> None:
+ self.handle_request()
+
+ def handle_request(self) -> None:
+ parsed_path: urllib.parse.ParseResult = urllib.parse.urlparse(self.path)
+ path: str = parsed_path.path
+ query_params: dict[str, list[str]] = urllib.parse.parse_qs(parsed_path.query)
+
+ # Find the matching route from the configuration
+ routes: List[Route] = config.routing
+ route: Optional[Route] = match_route(routes, self.command, path, query_params)
+
+ if not route:
+ # No matching route found; return a 404 error response
+ self._set_headers(404, {"Content-Type": "application/json"})
+ self.wfile.write(json.dumps({}).encode("utf-8"))
+ return
+
+ # Use the static response defined in the configuration
+ self._set_headers(route.response.status, route.response.headers)
+ if route.response.headers.get("Content-Type") == "application/json":
+ self.wfile.write(json.dumps(route.response.body).encode("utf-8"))
+ else:
+ self.wfile.write(str(route.response.body).encode("utf-8"))
+
+ return MockServerHandler
diff --git a/integrations/mock_server/src/main.py b/integrations/mock_server/src/main.py
new file mode 100644
index 00000000000000..d5ffb24989ccf5
--- /dev/null
+++ b/integrations/mock_server/src/main.py
@@ -0,0 +1,83 @@
+# Copyright (c) 2025 Project CHIP Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import argparse
+from pathlib import Path
+
+from server import run_server
+
+"""
+Mock HTTP/HTTPS Server for API Testing
+
+This module serves as the entry point for a configurable mock HTTPS server designed
+for API testing. It provides a secure, flexible way to simulate API endpoints with
+configurable responses.
+
+The server supports:
+- Custom routing configurations via JSON files
+- HTTPS with TLS certificate support
+- Configurable port binding
+- Multiple concurrent connections
+- Dynamic response configuration
+
+Usage:
+ python main.py [--port PORT] [--config CONFIG_FILE]
+ [--routing-config-dir ROUTE_DIR]
+ [--cert CERT_FILE] [--key KEY_FILE]
+
+Arguments:
+ --port PORT Port number to listen on (default: 8443)
+ --config CONFIG_FILE Path to main configuration file (default: config.json)
+ --routing-config-dir DIR Directory containing route configurations
+ (default: config.json)
+ --cert CERT_FILE Path to SSL certificate file (default: server.crt)
+ --key KEY_FILE Path to SSL private key file (default: server.key)
+
+Example:
+ python main.py --port 8443 --config ./config/main.json
+ --routing-config-dir ./config/routes
+ --cert ./certs/server.crt --key ./certs/server.key
+
+Configuration:
+ The server requires two types of configuration:
+ 1. Main configuration file (--config):
+ Contains server-wide settings and default behaviors
+
+ 2. Route configuration directory (--routing-config-dir):
+ Contains JSON files defining endpoint behaviors, responses,
+ and matching criteria
+
+Security:
+ - HTTPS only, no HTTP support
+ - Requires valid SSL certificate and private key
+ - TLS configuration uses server's default security settings
+
+Notes:
+ - Server runs until interrupted (Ctrl+C)
+ - All endpoints return JSON by default
+ - Logs to stdout with DEBUG level
+ - Supports concurrent requests via threading
+"""
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description="Mock HTTP/HTTPS Server for API testing")
+ parser.add_argument("--port", type=int, default=8443, help="Set the server port")
+ parser.add_argument("--config", type=str, default="config.json", help="Path to the common config file")
+ parser.add_argument("--routing-config-dir", type=str, default="config.json", help="Path to the common config file")
+ parser.add_argument("--cert", type=str, default="server.crt", help="SSL Certificate file")
+ parser.add_argument("--key", type=str, default="server.key", help="SSL Private Key file")
+
+ args = parser.parse_args()
+ run_server(args.port, Path(args.config), Path(args.routing_config_dir), Path(args.cert), Path(args.key))
diff --git a/integrations/mock_server/src/route_configuration.py b/integrations/mock_server/src/route_configuration.py
new file mode 100644
index 00000000000000..6a885d63b0c973
--- /dev/null
+++ b/integrations/mock_server/src/route_configuration.py
@@ -0,0 +1,181 @@
+# Copyright (c) 2025 Project CHIP Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import json
+import logging
+from dataclasses import dataclass, field
+from pathlib import Path
+from typing import Any, Dict, List, Optional
+
+
+@dataclass
+class RouteResponse:
+ status: int
+ headers: Dict[str, str]
+ body: Dict[str, Any]
+
+
+@dataclass
+class QueryConfig:
+ params: Dict[str, Any]
+
+
+@dataclass
+class Route:
+ method: str
+ path: str
+ response: RouteResponse
+ body: Optional[Any] = None
+ query: Optional[QueryConfig] = None
+
+
+@dataclass
+class Configuration:
+ routing: List[Route] = field(default_factory=list)
+
+
+def load_configurations(config_path: Path, routing_config_dir: Path) -> Configuration:
+ """
+ Load and combine configuration files from specified paths.
+
+ Args:
+ config_path (Path): Path to the main configuration file.
+ routing_config_dir (Path): Directory containing routing configuration files.
+
+ Returns:
+ Configuration: A Configuration object containing all merged routing configurations.
+
+ Raises:
+ ValueError: If config_path is not a file or routing_config_dir is not a directory.
+ """
+ if not config_path.is_file():
+ raise ValueError(f"'{config_path}' is not a file")
+
+ if not routing_config_dir.is_dir():
+ raise ValueError(f"'{routing_config_dir}' is not a directory")
+
+ # Load routing configuration
+ routing_config: List[Route] = load_routing_configuration_dir(routing_config_dir)
+
+ # Create and return the Configuration object
+ return Configuration(routing=routing_config)
+
+
+def load_routing_configuration_dir(directory: Path) -> List[Route]:
+ """
+ Load and merge all routing configuration files from a specified directory.
+
+ Args:
+ directory (Path): Directory path containing JSON configuration files.
+
+ Returns:
+ List[Route]: A list of Route objects containing all merged routing configurations.
+
+ Raises:
+ ValueError: If the specified directory path is not a directory.
+
+ Note:
+ - Processes all .json files in the specified directory
+ - Continues processing even if one file fails to load, logging the error
+ """
+ if not directory.is_dir():
+ raise ValueError(f"'{directory}' is not a directory")
+
+ all_routes: List[Route] = []
+
+ # Process all JSON files in the directory
+ for file_path in directory.glob("*.json"):
+ try:
+ routes = load_routing_configuration_file(file_path)
+ all_routes.extend(routes)
+ except Exception as e:
+ print(f"Error loading configuration from {file_path}: {str(e)}")
+ continue
+
+ return all_routes
+
+
+def load_routing_configuration_file(file_path: Path) -> List[Route]:
+ """
+ Load and parse a single routing configuration JSON file.
+
+ Args:
+ file_path (Path): Path to the JSON configuration file.
+
+ Returns:
+ List[Route]: A list of Route objects parsed from the configuration file.
+
+ Raises:
+ FileNotFoundError: If the configuration file doesn't exist.
+ json.JSONDecodeError: If the file contains invalid JSON.
+ KeyError: If the required 'routes' key is missing in the configuration.
+
+ Format:
+ The JSON file should contain:
+ {
+ "routes": [
+ {
+ "method": str,
+ "path": str,
+ "response": {
+ "status": int,
+ "headers": dict[str, str],
+ "body": dict[str, Any]
+ },
+ "body": Any, # Optional
+ "query": dict[str, Any] # Optional
+ }
+ ]
+ }
+ """
+ try:
+ with open(file_path, "r") as file:
+ config = json.load(file)
+ if "routes" not in config:
+ logging.error("Missing required 'routes' key in configuration file: %s", file_path)
+ raise KeyError("Configuration file must contain 'routes' key")
+
+ logging.debug("Routes configuration loaded successfully from %s", file_path)
+ routes = []
+ for route_config in config["routes"]:
+ # Create RouteResponse object
+ response = RouteResponse(
+ status=route_config["response"]["status"],
+ headers=route_config["response"].get("headers", {}),
+ body=route_config["response"].get("body", {}),
+ )
+
+ # Create QueryConfig if query params exist
+ query = None
+ if "query" in route_config:
+ query = QueryConfig(params=route_config["query"])
+
+ # Create Route object
+ route = Route(
+ method=route_config["method"],
+ path=route_config["path"],
+ response=response,
+ body=route_config.get("body"),
+ query=query,
+ )
+ routes.append(route)
+
+ return routes
+
+ except FileNotFoundError:
+ logging.error("Configuration file not found: %s", file_path)
+ raise
+ except json.JSONDecodeError as e:
+ logging.error("Invalid JSON in configuration file: %s", file_path)
+ raise e
diff --git a/integrations/mock_server/src/router.py b/integrations/mock_server/src/router.py
new file mode 100644
index 00000000000000..429d7699a54ffc
--- /dev/null
+++ b/integrations/mock_server/src/router.py
@@ -0,0 +1,106 @@
+# Copyright (c) 2025 Project CHIP Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from typing import Any, Dict, List, Optional
+from urllib.parse import parse_qs
+
+from route_configuration import Route
+
+
+def match_route(routing: List[Route], method: str, path: str, query: Optional[Dict[str, Any] | str] = None) -> Optional[Route]:
+ """
+ Finds the best matching route configuration for an incoming HTTP request.
+
+ Evaluates incoming requests against a list of routes based on three criteria
+ in order of priority:
+ 1. HTTP method (e.g., GET, POST, PUT, DELETE)
+ 2. URL path pattern (including wildcard support)
+ 3. Query parameters (if specified in route configuration)
+
+ Args:
+ routing (List[Route]): List of available route configurations
+ method (str): HTTP method from the incoming request
+ path (str): URL path from the incoming request
+ query (Optional[Dict[str, Any] | str]): Query parameters from the request,
+ either as a dictionary or URL-encoded string. Defaults to None.
+
+ Returns:
+ Optional[Route]: The first matching Route object that satisfies all criteria,
+ or None if no match is found. When multiple routes match, returns the
+ first matching route in the original routing list order.
+
+ Examples:
+ # Available routes
+ routes = [
+ Route(method="GET", path="/api/device/*", query=None),
+ Route(method="GET", path="/api/device/status", query={"type": "sensor"})
+ ]
+
+ # These would match:
+ match_route(routes, "GET", "/api/device/123") # Returns first route
+ match_route(routes, "GET", "/api/device/status", {"type": "sensor"}) # Returns second route
+
+ # These would return None:
+ match_route(routes, "POST", "/api/device/123")
+ match_route(routes, "GET", "/api/other")
+
+ Note:
+ - Routes are evaluated in order, returning the first match
+ - Wildcard paths (ending in *) match any path with the specified prefix
+ - Query parameters must match exactly if specified in the route
+ - Method matching is case-sensitive
+ - Path matching is case-sensitive
+ - Query strings are automatically parsed into dictionaries
+ """
+
+ # Filter routes to only those matching the HTTP method
+ method_routes: List[Route] = []
+ for route in routing:
+ if route.method == method:
+ method_routes.append(route)
+ if not method_routes:
+ return None
+
+ # Filter routes to only those matching the path
+ path_routes: List[Route] = []
+ for route in method_routes:
+ if route.path == path:
+ path_routes.append(route)
+ elif route.path.endswith("*"):
+ # Handle wildcard paths
+ if path.startswith(route.path[:-1]):
+ path_routes.append(route)
+ if not path_routes:
+ return None
+
+ # Parse query parameters if present
+ query_params = {}
+ if query:
+ if isinstance(query, str):
+ # parse_qs returns values as lists, we'll take the first value for each parameter
+ parsed = parse_qs(query)
+ query_params = {k: v[0] if v else "" for k, v in parsed.items()}
+ else:
+ query_params = query
+
+ # Find the first route that matches the path and query parameters
+ for route in path_routes:
+ if not route.query:
+ return route
+
+ # Check if all required query parameters are present
+ if all(param in query_params for param in route.query.params):
+ return route
+
+ return None
diff --git a/integrations/mock_server/src/server.py b/integrations/mock_server/src/server.py
new file mode 100644
index 00000000000000..b6a7d090d79ec6
--- /dev/null
+++ b/integrations/mock_server/src/server.py
@@ -0,0 +1,97 @@
+# Copyright (c) 2025 Project CHIP Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import http.server
+import logging
+import socketserver
+import ssl
+from pathlib import Path
+
+from handler import createMockServerHandler
+from route_configuration import Configuration, load_configurations
+
+
+def run_server(port: int, config_path: Path, routing_config_dir: Path, cert_path: Path, key_path: Path) -> None:
+ """
+ Starts a secure HTTPS server with mock endpoints defined by configuration files.
+
+ Initializes and runs a threaded HTTPS server that handles requests according to
+ configured routes. The server uses TLS for secure communication and supports
+ multiple concurrent connections.
+
+ Args:
+ port (int): Port number on which the server will listen
+ config_path (Path): Path to the main configuration file
+ routing_config_dir (Path): Directory containing additional routing configuration files
+ cert_path (Path): Path to the SSL/TLS certificate file
+ key_path (Path): Path to the SSL/TLS private key file
+
+ Returns:
+ None
+
+ Raises:
+ ssl.SSLError: If there are issues with the SSL/TLS certificate or key
+ OSError: If the port is already in use or permission is denied
+ ValueError: If configuration files are invalid or missing
+ KeyboardInterrupt: When the server is stopped using Ctrl+C
+
+ Example:
+ run_server(
+ port=8443,
+ config_path=Path("config/main.json"),
+ routing_config_dir=Path("config/routes"),
+ cert_path=Path("certs/server.crt"),
+ key_path=Path("certs/server.key")
+ )
+
+ Note:
+ - Server runs until interrupted by keyboard (Ctrl+C)
+ - Logs are written to stdout with DEBUG level
+ - Server binds to all available network interfaces ("")
+ - Uses ThreadingHTTPServer for concurrent request handling
+ - TLS configuration uses server's default security settings
+ - All endpoints are HTTPS-only
+ """
+
+ logging.basicConfig(level=logging.DEBUG, format="[%(levelname)s] %(message)s")
+
+ if not config_path.is_file():
+ raise ValueError(f"'{config_path}' is not a file")
+
+ if not routing_config_dir.is_dir():
+ raise ValueError(f"'{routing_config_dir}' is not a directory")
+
+ if not cert_path.is_file():
+ raise ValueError(f"'{cert_path}' is not a file")
+
+ if not key_path.is_file():
+ raise ValueError(f"'{key_path}' is not a file")
+
+ config: Configuration = load_configurations(config_path, routing_config_dir)
+ server_address: socketserver._AfInetAddress = ("", port)
+ context: ssl.SSLContext = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
+ context.load_cert_chain(certfile=cert_path, keyfile=key_path)
+
+ theMockServerHandler = createMockServerHandler(config)
+ httpd = http.server.ThreadingHTTPServer(server_address, theMockServerHandler)
+
+ logging.info("Server starting on port %s", port)
+ with context.wrap_socket(httpd.socket, server_side=True) as httpd.socket:
+ logging.info("Server started on port %s", port)
+ logging.info("HTTPS enabled with cert: %s and key: %s", cert_path, key_path)
+ try:
+ httpd.serve_forever()
+ except KeyboardInterrupt:
+ logging.info("Server is shutting down due to keyboard interrupt.")
+ httpd.server_close()
diff --git a/integrations/mock_server/tests/__init__.py b/integrations/mock_server/tests/__init__.py
new file mode 100644
index 00000000000000..1c89d20d346a80
--- /dev/null
+++ b/integrations/mock_server/tests/__init__.py
@@ -0,0 +1,13 @@
+# Copyright (c) 2025 Project CHIP Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/integrations/mock_server/tests/test_mock_server.py b/integrations/mock_server/tests/test_mock_server.py
new file mode 100644
index 00000000000000..5b27f5a11c21ca
--- /dev/null
+++ b/integrations/mock_server/tests/test_mock_server.py
@@ -0,0 +1,151 @@
+# Copyright (c) 2025 Project CHIP Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import json
+import shutil
+import tempfile
+import unittest
+from pathlib import Path
+from typing import List
+
+from route_configuration import QueryConfig, Route, RouteResponse, load_configurations
+from router import match_route
+
+
+class TestConfigLoader(unittest.TestCase):
+ def setUp(self):
+ # Create a temporary directory for our test files
+ self.test_dir = tempfile.mkdtemp()
+ self.config_dir = Path(self.test_dir) / "routes"
+ self.config_dir.mkdir()
+
+ # Create an empty config.json
+ self.empty_config = Path(self.test_dir) / "config.json"
+ self.empty_config.write_text("{}")
+
+ # Create two route configuration files
+ simple_route = {
+ "routes": [
+ {
+ "method": "GET",
+ "path": "/test",
+ "response": {
+ "status": 200,
+ "headers": {"Content-Type": "application/json"},
+ "body": {"message": "ok"},
+ },
+ }
+ ]
+ }
+
+ route_with_query = {
+ "routes": [
+ {
+ "method": "GET",
+ "path": "/api/data",
+ "query": {},
+ "response": {
+ "status": 200,
+ "headers": {"Content-Type": "application/json"},
+ "body": {"version": "1.0", "data": "sample"},
+ },
+ }
+ ]
+ }
+
+ # Write the route configurations to files
+ (self.config_dir / "simple_route.json").write_text(json.dumps(simple_route))
+ (self.config_dir / "data_route.json").write_text(json.dumps(route_with_query))
+
+ def tearDown(self):
+ # Clean up temporary files and directory
+ shutil.rmtree(self.test_dir)
+
+ def test_load_valid_config(self):
+ # Test loading empty config file
+ loaded_config = load_configurations(self.empty_config, self.config_dir)
+
+ # Verify we loaded both routes
+ self.assertEqual(len(loaded_config.routing), 2)
+
+ # Verify simple route
+ simple_route = match_route(loaded_config.routing, "GET", "/test", None)
+ self.assertIsNotNone(simple_route)
+ self.assertIsNotNone(simple_route.response) # type: ignore
+ self.assertEqual(simple_route.response.status, 200) # type: ignore
+ self.assertEqual(simple_route.response.body["message"], "ok") # type: ignore
+
+ # Verify route with query parameters
+ query_route = match_route(loaded_config.routing, "GET", "/api/data", "{}")
+ self.assertIsNotNone(query_route)
+ self.assertEqual(query_route.response.status, 200) # type: ignore
+ self.assertEqual(query_route.response.body["version"], "1.0") # type: ignore
+
+
+class TestRouter(unittest.TestCase):
+ routes: List[Route] = []
+
+ def setUp(self):
+ self.routes = [
+ Route(
+ method="GET",
+ path="/api/data",
+ response=RouteResponse(status=200, headers={}, body={"message": "Hello, World!"}),
+ ),
+ Route(
+ method="GET",
+ path="/api/query",
+ query=QueryConfig(params={"key": "value"}),
+ response=RouteResponse(status=200, headers={}, body={"message": "Query matched"}),
+ ),
+ Route(
+ method="GET",
+ path="/api/*",
+ response=RouteResponse(status=200, headers={}, body={"message": "Wildcard matched"}),
+ ),
+ Route(
+ method="POST",
+ path="/api/echo",
+ body={"regex": ".*hello.*"},
+ response=RouteResponse(status=200, headers={}, body={"message": "Echo"}),
+ ),
+ ]
+
+ def test_exact_route_match(self):
+ route = match_route(self.routes, "GET", "/api/data", {})
+ self.assertIsNotNone(route)
+ self.assertEqual(route.response.body, {"message": "Hello, World!"}) # type: ignore
+
+ def test_wildcard_route_match(self):
+ route = match_route(self.routes, "GET", "/api/anything", {})
+ self.assertIsNotNone(route)
+ self.assertEqual(route.response.body, {"message": "Wildcard matched"}) # type: ignore
+
+ def test_query_parameter_matching(self):
+ route = match_route(self.routes, "GET", "/api/query", {"key": ["value"]})
+ self.assertIsNotNone(route)
+ self.assertEqual(route.response.body, {"message": "Query matched"}) # type: ignore
+
+ def test_body_regex_match(self):
+ route = match_route(self.routes, "POST", "/api/echo", "This is a hello message")
+ self.assertIsNotNone(route)
+ self.assertEqual(route.response.body, {"message": "Echo"}) # type: ignore
+
+ def test_no_match(self):
+ route = match_route(self.routes, "DELETE", "/nonexistent", {})
+ self.assertIsNone(route)
+
+
+if __name__ == "__main__":
+ unittest.main()