feat(tests): add live subprocess integration test suite for services
Spins up each service's server.py in a real subprocess via a free ephemeral port (PORT env var), connects with real protocol clients, and asserts both correct protocol behavior and RFC 5424 log output. - 44 live tests across 10 services: http, ftp, smtp, redis, mqtt, mysql, postgres, mongodb, pop3, imap - Shared conftest.py: _ServiceProcess (bg reader thread + queue), free_port, live_service fixture, assert_rfc5424 helper - PORT env var added to all 10 targeted server.py templates - New pytest marker `live`; excluded from default addopts run - requirements-live-tests.txt: flask, twisted + protocol clients
This commit is contained in:
@@ -18,6 +18,7 @@ from decnet_logging import syslog_line, write_syslog_file, forward_syslog
|
||||
NODE_NAME = os.environ.get("NODE_NAME", "ftpserver")
|
||||
SERVICE_NAME = "ftp"
|
||||
LOG_TARGET = os.environ.get("LOG_TARGET", "")
|
||||
PORT = int(os.environ.get("PORT", "21"))
|
||||
BANNER = os.environ.get("FTP_BANNER", "220 (vsFTPd 3.0.3)")
|
||||
|
||||
def _log(event_type: str, severity: int = 6, **kwargs) -> None:
|
||||
@@ -71,6 +72,6 @@ class ServerFTPFactory(FTPFactory):
|
||||
|
||||
if __name__ == "__main__":
|
||||
twisted_log.startLogging(sys.stdout)
|
||||
_log("startup", msg=f"FTP server starting as {NODE_NAME} on port 21")
|
||||
reactor.listenTCP(21, ServerFTPFactory())
|
||||
_log("startup", msg=f"FTP server starting as {NODE_NAME} on port {PORT}")
|
||||
reactor.listenTCP(PORT, ServerFTPFactory())
|
||||
reactor.run()
|
||||
|
||||
@@ -10,11 +10,13 @@ import os
|
||||
from pathlib import Path
|
||||
|
||||
from flask import Flask, request, send_from_directory
|
||||
from werkzeug.serving import make_server, WSGIRequestHandler
|
||||
from decnet_logging import syslog_line, write_syslog_file, forward_syslog
|
||||
|
||||
NODE_NAME = os.environ.get("NODE_NAME", "webserver")
|
||||
SERVICE_NAME = "http"
|
||||
LOG_TARGET = os.environ.get("LOG_TARGET", "")
|
||||
PORT = int(os.environ.get("PORT", "80"))
|
||||
SERVER_HEADER = os.environ.get("SERVER_HEADER", "Apache/2.4.54 (Debian)")
|
||||
RESPONSE_CODE = int(os.environ.get("RESPONSE_CODE", "403"))
|
||||
FAKE_APP = os.environ.get("FAKE_APP", "")
|
||||
@@ -111,6 +113,13 @@ def catch_all(path):
|
||||
return body, RESPONSE_CODE, headers
|
||||
|
||||
|
||||
class _SilentHandler(WSGIRequestHandler):
|
||||
"""Suppress Werkzeug's Server header so Flask's after_request is the sole source."""
|
||||
def version_string(self) -> str:
|
||||
return ""
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
_log("startup", msg=f"HTTP server starting as {NODE_NAME}")
|
||||
app.run(host="0.0.0.0", port=80, debug=False) # nosec B104
|
||||
srv = make_server("0.0.0.0", PORT, app, request_handler=_SilentHandler) # nosec B104
|
||||
srv.serve_forever()
|
||||
|
||||
@@ -17,6 +17,7 @@ from decnet_logging import SEVERITY_WARNING, syslog_line, write_syslog_file, for
|
||||
NODE_NAME = os.environ.get("NODE_NAME", "mailserver")
|
||||
SERVICE_NAME = "imap"
|
||||
LOG_TARGET = os.environ.get("LOG_TARGET", "")
|
||||
PORT = int(os.environ.get("PORT", "143"))
|
||||
IMAP_BANNER = os.environ.get("IMAP_BANNER", f"* OK Dovecot ready.\r\n")
|
||||
_RAW_USERS = os.environ.get("IMAP_USERS", "admin:admin123,root:toor,mail:mail,user:user")
|
||||
|
||||
@@ -532,7 +533,7 @@ class IMAPProtocol(asyncio.Protocol):
|
||||
async def main():
|
||||
_log("startup", msg=f"IMAP server starting as {NODE_NAME}")
|
||||
loop = asyncio.get_running_loop()
|
||||
server = await loop.create_server(IMAPProtocol, "0.0.0.0", 143) # nosec B104
|
||||
server = await loop.create_server(IMAPProtocol, "0.0.0.0", PORT) # nosec B104
|
||||
async with server:
|
||||
await server.serve_forever()
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ from decnet_logging import syslog_line, write_syslog_file, forward_syslog
|
||||
NODE_NAME = os.environ.get("NODE_NAME", "mongodb")
|
||||
SERVICE_NAME = "mongodb"
|
||||
LOG_TARGET = os.environ.get("LOG_TARGET", "")
|
||||
PORT = int(os.environ.get("PORT", "27017"))
|
||||
|
||||
# Minimal BSON helpers
|
||||
def _bson_str(key: str, val: str) -> bytes:
|
||||
@@ -118,7 +119,7 @@ class MongoDBProtocol(asyncio.Protocol):
|
||||
async def main():
|
||||
_log("startup", msg=f"MongoDB server starting as {NODE_NAME}")
|
||||
loop = asyncio.get_running_loop()
|
||||
server = await loop.create_server(MongoDBProtocol, "0.0.0.0", 27017) # nosec B104
|
||||
server = await loop.create_server(MongoDBProtocol, "0.0.0.0", PORT) # nosec B104
|
||||
async with server:
|
||||
await server.serve_forever()
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ from decnet_logging import syslog_line, write_syslog_file, forward_syslog
|
||||
NODE_NAME = os.environ.get("NODE_NAME", "mqtt-broker")
|
||||
SERVICE_NAME = "mqtt"
|
||||
LOG_TARGET = os.environ.get("LOG_TARGET", "")
|
||||
PORT = int(os.environ.get("PORT", "1883"))
|
||||
MQTT_ACCEPT_ALL = os.environ.get("MQTT_ACCEPT_ALL", "1") == "1"
|
||||
MQTT_PERSONA = os.environ.get("MQTT_PERSONA", "water_plant")
|
||||
MQTT_CUSTOM_TOPICS = os.environ.get("MQTT_CUSTOM_TOPICS", "")
|
||||
@@ -264,7 +265,7 @@ class MQTTProtocol(asyncio.Protocol):
|
||||
async def main():
|
||||
_log("startup", msg=f"MQTT server starting as {NODE_NAME}")
|
||||
loop = asyncio.get_running_loop()
|
||||
server = await loop.create_server(MQTTProtocol, "0.0.0.0", 1883) # nosec B104
|
||||
server = await loop.create_server(MQTTProtocol, "0.0.0.0", PORT) # nosec B104
|
||||
async with server:
|
||||
await server.serve_forever()
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ from decnet_logging import syslog_line, write_syslog_file, forward_syslog
|
||||
NODE_NAME = os.environ.get("NODE_NAME", "dbserver")
|
||||
SERVICE_NAME = "mysql"
|
||||
LOG_TARGET = os.environ.get("LOG_TARGET", "")
|
||||
PORT = int(os.environ.get("PORT", "3306"))
|
||||
_MYSQL_VER = os.environ.get("MYSQL_VERSION", "5.7.38-log")
|
||||
|
||||
# Minimal MySQL server greeting (protocol v10) — version string is configurable
|
||||
@@ -102,7 +103,7 @@ class MySQLProtocol(asyncio.Protocol):
|
||||
async def main():
|
||||
_log("startup", msg=f"MySQL server starting as {NODE_NAME}")
|
||||
loop = asyncio.get_running_loop()
|
||||
server = await loop.create_server(MySQLProtocol, "0.0.0.0", 3306) # nosec B104
|
||||
server = await loop.create_server(MySQLProtocol, "0.0.0.0", PORT) # nosec B104
|
||||
async with server:
|
||||
await server.serve_forever()
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ from decnet_logging import SEVERITY_WARNING, syslog_line, write_syslog_file, for
|
||||
NODE_NAME = os.environ.get("NODE_NAME", "mailserver")
|
||||
SERVICE_NAME = "pop3"
|
||||
LOG_TARGET = os.environ.get("LOG_TARGET", "")
|
||||
PORT = int(os.environ.get("PORT", "110"))
|
||||
POP3_BANNER = os.environ.get("POP3_BANNER", f"+OK {NODE_NAME} Dovecot POP3 ready.")
|
||||
_RAW_USERS = os.environ.get("IMAP_USERS", "admin:admin123,root:toor,mail:mail,user:user")
|
||||
|
||||
@@ -405,7 +406,7 @@ class POP3Protocol(asyncio.Protocol):
|
||||
async def main():
|
||||
_log("startup", msg=f"POP3 server starting as {NODE_NAME}")
|
||||
loop = asyncio.get_running_loop()
|
||||
server = await loop.create_server(POP3Protocol, "0.0.0.0", 110) # nosec B104
|
||||
server = await loop.create_server(POP3Protocol, "0.0.0.0", PORT) # nosec B104
|
||||
async with server:
|
||||
await server.serve_forever()
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ from decnet_logging import syslog_line, write_syslog_file, forward_syslog
|
||||
NODE_NAME = os.environ.get("NODE_NAME", "pgserver")
|
||||
SERVICE_NAME = "postgres"
|
||||
LOG_TARGET = os.environ.get("LOG_TARGET", "")
|
||||
PORT = int(os.environ.get("PORT", "5432"))
|
||||
def _error_response(message: str) -> bytes:
|
||||
body = b"S" + b"FATAL\x00" + b"M" + message.encode() + b"\x00\x00"
|
||||
return b"E" + struct.pack(">I", len(body) + 4) + body
|
||||
@@ -110,7 +111,7 @@ class PostgresProtocol(asyncio.Protocol):
|
||||
async def main():
|
||||
_log("startup", msg=f"PostgreSQL server starting as {NODE_NAME}")
|
||||
loop = asyncio.get_running_loop()
|
||||
server = await loop.create_server(PostgresProtocol, "0.0.0.0", 5432) # nosec B104
|
||||
server = await loop.create_server(PostgresProtocol, "0.0.0.0", PORT) # nosec B104
|
||||
async with server:
|
||||
await server.serve_forever()
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ from decnet_logging import syslog_line, write_syslog_file, forward_syslog
|
||||
NODE_NAME = os.environ.get("NODE_NAME", "cache-server")
|
||||
SERVICE_NAME = "redis"
|
||||
LOG_TARGET = os.environ.get("LOG_TARGET", "")
|
||||
PORT = int(os.environ.get("PORT", "6379"))
|
||||
_REDIS_VER = os.environ.get("REDIS_VERSION", "7.2.7")
|
||||
_REDIS_OS = os.environ.get("REDIS_OS", "Linux 5.15.0")
|
||||
|
||||
@@ -185,7 +186,7 @@ class RedisProtocol(asyncio.Protocol):
|
||||
async def main():
|
||||
_log("startup", msg=f"Redis server starting as {NODE_NAME}")
|
||||
loop = asyncio.get_running_loop()
|
||||
server = await loop.create_server(RedisProtocol, "0.0.0.0", 6379) # nosec B104
|
||||
server = await loop.create_server(RedisProtocol, "0.0.0.0", PORT) # nosec B104
|
||||
async with server:
|
||||
await server.serve_forever()
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ from decnet_logging import SEVERITY_WARNING, syslog_line, write_syslog_file, for
|
||||
NODE_NAME = os.environ.get("NODE_NAME", "mailserver")
|
||||
SERVICE_NAME = "smtp"
|
||||
LOG_TARGET = os.environ.get("LOG_TARGET", "")
|
||||
PORT = int(os.environ.get("PORT", "25"))
|
||||
OPEN_RELAY = os.environ.get("SMTP_OPEN_RELAY", "0").strip() == "1"
|
||||
|
||||
_SMTP_BANNER = os.environ.get("SMTP_BANNER", f"220 {NODE_NAME} ESMTP Postfix (Debian/GNU)")
|
||||
@@ -245,7 +246,7 @@ async def main():
|
||||
mode = "open-relay" if OPEN_RELAY else "credential-harvester"
|
||||
_log("startup", msg=f"SMTP server starting as {NODE_NAME} ({mode})")
|
||||
loop = asyncio.get_running_loop()
|
||||
server = await loop.create_server(SMTPProtocol, "0.0.0.0", 25) # nosec B104
|
||||
server = await loop.create_server(SMTPProtocol, "0.0.0.0", PORT) # nosec B104
|
||||
async with server:
|
||||
await server.serve_forever()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user