diff --git a/.gitignore b/.gitignore index a080e6443..5f0351189 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .DS_Store .idea +.vscode receptor receptor.exe receptor.app diff --git a/Makefile b/Makefile index e24354143..618db45ce 100644 --- a/Makefile +++ b/Makefile @@ -130,7 +130,9 @@ receptorctl-test-venv/bin/pytest: receptorctl-test-venv/bin/pip install -r receptorctl/test-requirements.txt receptorctl-tests: receptor receptorctl-test-venv/bin/pytest - cd receptorctl && ../receptorctl-test-venv/bin/pytest tests/tests.py + cd receptorctl && \ + PATH=$(PATH):$(PWD) \ + ../receptorctl-test-venv/bin/pytest tests/ clean: @rm -fv receptor receptor.exe receptor.app net diff --git a/receptorctl/tests/conftest.py b/receptorctl/tests/conftest.py new file mode 100644 index 000000000..a04087459 --- /dev/null +++ b/receptorctl/tests/conftest.py @@ -0,0 +1,326 @@ +import sys + +sys.path.append("../receptorctl") + +import receptorctl + +import pytest +import subprocess +import os +import shutil +import time +import json +from click.testing import CliRunner + +tmpDir = "/tmp/receptorctltest" + + +@pytest.fixture(scope="session") +def create_empty_dir(): + def check_dependencies(): + """Check if we have the required dependencies + raise an exception if we don't + """ + + # Check if openssl binary is on the path + try: + subprocess.check_output(["openssl", "version"]) + except: + raise Exception( + "openssl binary not found\n" 'Consider run "sudo dnf install openssl"' + ) + + check_dependencies() + + # Clean up tmp directory and create a new one + if os.path.exists(tmpDir): + shutil.rmtree(tmpDir) + os.mkdir(tmpDir) + + +@pytest.fixture(scope="session") +def create_certificate(create_empty_dir): + def generate_cert(name, commonName): + keyPath = os.path.join(tmpDir, name + ".key") + crtPath = os.path.join(tmpDir, name + ".crt") + subprocess.check_output(["openssl", "genrsa", "-out", keyPath, "2048"]) + subprocess.check_output( + [ + "openssl", + "req", + "-x509", + "-new", + "-nodes", + "-key", + keyPath, + "-subj", + "/C=/ST=/L=/O=/OU=ReceptorTesting/CN=ca", + "-sha256", + "-out", + crtPath, + ] + ) + return keyPath, crtPath + + def generate_cert_with_ca(name, caKeyPath, caCrtPath, commonName): + keyPath = os.path.join(tmpDir, name + ".key") + crtPath = os.path.join(tmpDir, name + ".crt") + csrPath = os.path.join(tmpDir, name + ".csa") + extPath = os.path.join(tmpDir, name + ".ext") + + # create x509 extension + with open(extPath, "w") as ext: + ext.write("subjectAltName=DNS:" + commonName) + ext.close() + subprocess.check_output(["openssl", "genrsa", "-out", keyPath, "2048"]) + + # create cert request + subprocess.check_output( + [ + "openssl", + "req", + "-new", + "-sha256", + "-key", + keyPath, + "-subj", + "/C=/ST=/L=/O=/OU=ReceptorTesting/CN=" + commonName, + "-out", + csrPath, + ] + ) + + # sign cert request + subprocess.check_output( + [ + "openssl", + "x509", + "-req", + "-extfile", + extPath, + "-in", + csrPath, + "-CA", + caCrtPath, + "-CAkey", + caKeyPath, + "-CAcreateserial", + "-out", + crtPath, + "-sha256", + ] + ) + + return keyPath, crtPath + + # Create a new CA + caKeyPath, caCrtPath = generate_cert("ca", "ca") + clientKeyPath, clientCrtPath = generate_cert_with_ca( + "client", caKeyPath, caCrtPath, "localhost" + ) + generate_cert_with_ca("server", caKeyPath, caCrtPath, "localhost") + + return { + "caKeyPath": caKeyPath, + "caCrtPath": caCrtPath, + "clientKeyPath": clientKeyPath, + "clientCrtPath": clientCrtPath, + } + + +@pytest.fixture(scope="session") +def certificate_files(create_certificate): + """Returns a dict with the certificate files + + The dict contains the following keys: + caKeyPath + caCrtPath + clientKeyPath + clientCrtPath + """ + return create_certificate + + +@pytest.fixture(scope="session") +def prepare_environment(certificate_files): + pass + + +@pytest.fixture(scope="session") +def receptor_bin_path(): + """Returns the path to the receptor binary + + This fixture was created to make possible the use of + multiple receptor binaries files. + + The default priority order is: + - ../../tests/artifacts-output + - The "receptor" available in the PATH + + Returns: + str: Path to the receptor binary + """ + + # Check if the receptor binary is in '../../tests/artifacts-output' and returns + # the path to the binary if it is found. + receptor_bin_path_from_test_dir = os.path.join( + os.path.dirname(os.path.abspath(__file__)), + "../../tests/artifacts-output", + "receptor", + ) + if os.path.exists(receptor_bin_path_from_test_dir): + return receptor_bin_path_from_test_dir + + # Check if the receptor binary is in the path + try: + subprocess.check_output(["receptor", "--version"]) + return "receptor" + except subprocess.CalledProcessError: + raise Exception( + "Receptor binary not found in $PATH or in '../../tests/artifacts-output'" + ) + + +@pytest.fixture(scope="class") +def default_socket_unix(): + return "unix://" + os.path.join(tmpDir, "node1.sock") + + +@pytest.fixture(scope="class") +def default_receptor_controller_unix(default_socket_unix): + return receptorctl.ReceptorControl(default_socket_unix) + + +@pytest.fixture(scope="class") +def default_socket_tcp(): + return "tcp://localhost:11112" + + +@pytest.fixture(scope="class") +def default_receptor_controller_tcp(default_socket_tcp): + return receptorctl.ReceptorControl(default_socket_tcp) + + +@pytest.fixture(scope="class") +def default_receptor_controller_tcp_tls(default_socket_tcp, certificate_files): + socketaddress = default_socket_tcp + rootcas = certificate_files["caCrtPath"] + key = certificate_files["clientKeyPath"] + cert = certificate_files["clientCrtPath"] + insecureskipverify = True + + controller = receptorctl.ReceptorControl( + default_socket_tcp, + rootcas=rootcas, + key=key, + cert=cert, + insecureskipverify=insecureskipverify, + ) + + return controller + + +@pytest.fixture(scope="class") +def receptor_mesh( + prepare_environment, receptor_bin_path, default_receptor_controller_unix +): + + node1 = subprocess.Popen( + [receptor_bin_path, "-c", "tests/mesh-definitions/mesh1/node1.yaml"] + ) + node2 = subprocess.Popen( + [receptor_bin_path, "-c", "tests/mesh-definitions/mesh1/node2.yaml"] + ) + node3 = subprocess.Popen( + [receptor_bin_path, "-c", "tests/mesh-definitions/mesh1/node3.yaml"] + ) + + time.sleep(0.5) + node1_controller = default_receptor_controller_unix + + while True: + status = node1_controller.simple_command("status") + if status["RoutingTable"] == {"node2": "node2", "node3": "node2"}: + break + time.sleep(0.5) + + node1_controller.close() + + # Debug mesh data + print("# Mesh nodes: {}".format(str(status["KnownConnectionCosts"].keys()))) + + yield + + node1.kill() + node2.kill() + node1.wait() + node2.wait() + + +@pytest.fixture(scope="function") +def receptor_control_args(): + args = { + "--socket": "/tmp/receptorctltest/node1.sock", + "--config": None, + "--tls": None, + "--rootcas": None, + "--key": None, + "--cert": None, + "--insecureskipverify": None, + } + return args + + +@pytest.fixture(scope="function") +def invoke(receptor_control_args): + def f_invoke(command, args: list = []): + """Invoke a command and return the original result. + + Args: + command (click command): The command to invoke. + args (list): The arguments to pass to the command. + + Returns: + click.testing: The original result. + """ + + def parse_args_to_list(args: dict): + """Parse the args (dict) to a list of strings.""" + arg_list = [] + for k, v in args.items(): + if v is not None: + arg_list.append(str(k)) + arg_list.append(str(v)) + return arg_list + + runner = CliRunner() + + out = runner.invoke( + receptorctl.cli.cli, + parse_args_to_list(receptor_control_args) + [command.name] + args, + ) + return out + + return f_invoke + + +@pytest.fixture(scope="function") +def invoke_as_json(invoke): + def f_invoke_as_json(command, args: list = []): + """Invoke a command and return the original result and the json output. + + Args: + command (click command): The command to invoke. + args (list): The arguments to pass to the command. + + Returns: + tuple: Tuple of the original result and the json output. + """ + result = invoke(command, ["--json"] + args) + try: + json_output = json.loads(result.output) + except json.decoder.JSONDecodeError: + pytest.fail("The command is not in json format") + return result, json_output + + return f_invoke_as_json diff --git a/receptorctl/tests/mesh-definitions/mesh1/node1.yaml b/receptorctl/tests/mesh-definitions/mesh1/node1.yaml index 01fed3325..4fd4621e5 100644 --- a/receptorctl/tests/mesh-definitions/mesh1/node1.yaml +++ b/receptorctl/tests/mesh-definitions/mesh1/node1.yaml @@ -1,19 +1,24 @@ - node: id: node1 + - tcp-listener: port: 11111 + - control-service: filename: /tmp/receptorctltest/node1.sock + - tcp-server: port: 11112 remotenode: localhost remoteservice: control + - tls-server: name: tlsserver key: /tmp/receptorctltest/server.key cert: /tmp/receptorctltest/server.crt requireclientcert: true clientcas: /tmp/receptorctltest/ca.crt + - control-service: service: ctltls tcplisten: 11113 diff --git a/receptorctl/tests/mesh-definitions/mesh1/node2.yaml b/receptorctl/tests/mesh-definitions/mesh1/node2.yaml index f7ca2016e..52cb4dff8 100644 --- a/receptorctl/tests/mesh-definitions/mesh1/node2.yaml +++ b/receptorctl/tests/mesh-definitions/mesh1/node2.yaml @@ -1,6 +1,11 @@ - node: id: node2 + - tcp-peer: address: localhost:11111 + +- tcp-listener: + port: 11121 + - control-service: filename: /tmp/receptorctltest/node2.sock diff --git a/receptorctl/tests/mesh-definitions/mesh1/node3.yaml b/receptorctl/tests/mesh-definitions/mesh1/node3.yaml new file mode 100644 index 000000000..1af76c119 --- /dev/null +++ b/receptorctl/tests/mesh-definitions/mesh1/node3.yaml @@ -0,0 +1,8 @@ +- node: + id: node3 + +- tcp-peer: + address: localhost:11121 + +- control-service: + filename: /tmp/receptorctltest/node3.sock diff --git a/receptorctl/tests/test_cli.py b/receptorctl/tests/test_cli.py new file mode 100644 index 000000000..092f876f9 --- /dev/null +++ b/receptorctl/tests/test_cli.py @@ -0,0 +1,32 @@ +import sys + +sys.path.append("../receptorctl") + +from receptorctl import cli as commands +import receptorctl + +# The goal is to write tests following the click documentation: +# https://click.palletsprojects.com/en/8.0.x/testing/ + +import pytest + + +@pytest.mark.usefixtures("receptor_mesh") +class TestCommands: + def test_cmd_status(self, invoke_as_json): + result, json_output = invoke_as_json(commands.status, []) + assert result.exit_code == 0 + assert set( + [ + "Advertisements", + "Connections", + "KnownConnectionCosts", + "NodeID", + "RoutingTable", + "SystemCPUCount", + "SystemMemoryMiB", + "Version", + ] + ) == set( + json_output.keys() + ), "The command returned unexpected keys from json output" diff --git a/receptorctl/tests/test_connection.py b/receptorctl/tests/test_connection.py new file mode 100644 index 000000000..e68b602ed --- /dev/null +++ b/receptorctl/tests/test_connection.py @@ -0,0 +1,69 @@ +import pytest + + +@pytest.mark.usefixtures("receptor_mesh") +class TestReceptorCtlConnection: + def test_connect_to_service(self, default_receptor_controller_unix): + node1_controller = default_receptor_controller_unix + node1_controller.connect_to_service("node2", "control", "") + node1_controller.handshake() + status = node1_controller.simple_command("status") + node1_controller.close() + assert status["NodeID"] == "node2" + + def test_simple_command(self, default_receptor_controller_unix): + node1_controller = default_receptor_controller_unix + status = node1_controller.simple_command("status") + node1_controller.close() + assert not ( + set( + [ + "Advertisements", + "Connections", + "KnownConnectionCosts", + "NodeID", + "RoutingTable", + ] + ) + - status.keys() + ) + + def test_simple_command_fail(self, default_receptor_controller_unix): + node1_controller = default_receptor_controller_unix + with pytest.raises(RuntimeError): + node1_controller.simple_command("doesnotexist") + node1_controller.close() + + def test_tcp_control_service(self, default_receptor_controller_tcp): + node1_controller = default_receptor_controller_tcp + status = node1_controller.simple_command("status") + node1_controller.close() + assert not ( + set( + [ + "Advertisements", + "Connections", + "KnownConnectionCosts", + "NodeID", + "RoutingTable", + ] + ) + - status.keys() + ) + + def test_tcp_control_service_tls(self, default_receptor_controller_tcp_tls): + node1_controller = default_receptor_controller_tcp_tls + status = node1_controller.simple_command("status") + node1_controller.close() + assert not ( + set( + [ + "Advertisements", + "Connections", + "KnownConnectionCosts", + "NodeID", + "RoutingTable", + ] + ) + - status.keys() + ) diff --git a/receptorctl/tests/tests.py b/receptorctl/tests/tests.py deleted file mode 100644 index baa3ca9fc..000000000 --- a/receptorctl/tests/tests.py +++ /dev/null @@ -1,112 +0,0 @@ -import pytest -import subprocess -import os -import shutil -from receptorctl import ReceptorControl -import time - -connDict = { - "socket":None, - "rootcas":None, - "key":None, - "cert":None, - "insecureskipverify":False, -} - -tmpDir = "/tmp/receptorctltest" -if os.path.exists(tmpDir): - shutil.rmtree(tmpDir) -os.mkdir(tmpDir) - -def generate_cert(name, commonName): - keyPath = os.path.join(tmpDir, name + ".key") - crtPath = os.path.join(tmpDir, name + ".crt") - os.system("openssl genrsa -out " + keyPath + " 2048") - os.system("openssl req -x509 -new -nodes -key " + keyPath + " -subj /C=/ST=/L=/O=/OU=ReceptorTesting/CN=ca -sha256 -out " + crtPath) - return keyPath, crtPath - -def generate_cert_with_ca(name, caKeyPath, caCrtPath, commonName): - keyPath = os.path.join(tmpDir, name + ".key") - crtPath = os.path.join(tmpDir, name + ".crt") - csrPath = os.path.join(tmpDir, name + ".csa") - extPath = os.path.join(tmpDir, name + ".ext") - # create x509 extension - with open(extPath, "w") as ext: - ext.write("subjectAltName=DNS:" + commonName) - ext.close() - os.system("openssl genrsa -out " + keyPath + " 2048") - # create cert request - os.system("openssl req -new -sha256 -key " + keyPath + " -subj /C=/ST=/L=/O=/OU=ReceptorTesting/CN=" + commonName + " -out " + csrPath) - # sign cert request - os.system("openssl x509 -req -extfile " + extPath + " -in " + csrPath + " -CA " + caCrtPath + " -CAkey " + caKeyPath + " -CAcreateserial -out " + crtPath + " -sha256") - return keyPath, crtPath - -caKeyPath, caCrtPath = generate_cert("ca", "ca") -clientKeyPath, clientCrtPath = generate_cert_with_ca("client", caKeyPath, caCrtPath, "localhost") -generate_cert_with_ca("server", caKeyPath, caCrtPath, "localhost") - -@pytest.fixture(scope="class") -def receptor_mesh(request): - - node1 = subprocess.Popen(["receptor", "-c", "tests/mesh-definitions/mesh1/node1.yaml"]) - node2 = subprocess.Popen(["receptor", "-c", "tests/mesh-definitions/mesh1/node2.yaml"]) - - time.sleep(0.5) - socketaddress = "unix://" + os.path.join(tmpDir, "node1.sock") - node1_controller = ReceptorControl(socketaddress) - - while True: - status = node1_controller.simple_command("status") - if status["RoutingTable"] == {"node2":"node2"}: - break - - node1_controller.close() - yield - - node1.kill() - node2.kill() - node1.wait() - node2.wait() - -@pytest.mark.usefixtures('receptor_mesh') -class TestReceptorCTL: - def test_simple_command(self): - socketaddress = "unix://" + os.path.join(tmpDir, "node1.sock") - node1_controller = ReceptorControl(socketaddress) - status = node1_controller.simple_command("status") - node1_controller.close() - assert not (set(["Advertisements", "Connections", "KnownConnectionCosts", "NodeID", "RoutingTable"]) - status.keys()) - - def test_simple_command_fail(self): - socketaddress = "unix://" + os.path.join(tmpDir, "node1.sock") - node1_controller = ReceptorControl(socketaddress) - with pytest.raises(RuntimeError): - node1_controller.simple_command("doesnotexist") - node1_controller.close() - - def test_tcp_control_service(self): - socketaddress = "tcp://localhost:11112" - node1_controller = ReceptorControl(socketaddress) - status = node1_controller.simple_command("status") - node1_controller.close() - assert not (set(["Advertisements", "Connections", "KnownConnectionCosts", "NodeID", "RoutingTable"]) - status.keys()) - - def test_tcp_control_service_tls(self): - socketaddress = "tls://localhost:11113" - rootcas = caCrtPath - key = clientKeyPath - cert = clientCrtPath - insecureskipverify = True - node1_controller = ReceptorControl(socketaddress, rootcas=rootcas, key=key, cert=cert, insecureskipverify=insecureskipverify) - status = node1_controller.simple_command("status") - node1_controller.close() - assert not (set(["Advertisements", "Connections", "KnownConnectionCosts", "NodeID", "RoutingTable"]) - status.keys()) - - def test_connect_to_service(self): - socketaddress = "unix://" + os.path.join(tmpDir, "node1.sock") - node1_controller = ReceptorControl(socketaddress) - node1_controller.connect_to_service("node2", "control", "") - node1_controller.handshake() - status = node1_controller.simple_command("status") - node1_controller.close() - assert status["NodeID"] == "node2" diff --git a/tests/.gitignore b/tests/.gitignore index d4f588edf..c3163b04d 100644 --- a/tests/.gitignore +++ b/tests/.gitignore @@ -1 +1 @@ -artifacts/ +artifacts-output/ diff --git a/tests/Makefile b/tests/Makefile index adc153343..d85882749 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -8,11 +8,12 @@ GO_FUNCTIONAL_TESTS_DIRS:=$$(find ./functional -type d) # Generate artifacts artifacts: container-image-builder - mkdir -p $(PWD)/artifacts + mkdir -p $(PWD)/artifacts-output $(CONTAINER_RUN) run --rm \ - -v $(PWD)/../:/source/:ro \ - -v $(PWD)/artifacts/:/artifacts/:rw \ + -v $(PWD)/../:/source/ \ + -v $(PWD)/artifacts-output/:/artifacts/:rw \ -v receptor_go_root_cache:/root/go:rw \ + -e OUTPUT_UID=$$(id -u) \ $(CONTAINER_IMAGE_TAG_builder) \ /build-artifacts.sh diff --git a/tests/environments/container-builder/build-artifacts.sh b/tests/environments/container-builder/build-artifacts.sh index 559dd65b8..28965ff43 100644 --- a/tests/environments/container-builder/build-artifacts.sh +++ b/tests/environments/container-builder/build-artifacts.sh @@ -5,6 +5,8 @@ set -ex SOURCE_DIR=/source BUILD_DIR=/build ARTIFACTS_DIR=/artifacts +OUTPUT_UID=${OUTPUT_UID:-1000} +OUTPUT_GID=${OUTPUT_GID:-$OUTPUT_UID} # Copy all content cp -r ${SOURCE_DIR}/ ${BUILD_DIR} @@ -25,3 +27,6 @@ rm -f ${ARTIFACTS_DIR}/receptor cp ${BUILD_DIR}/receptor ${ARTIFACTS_DIR}/receptor rm -rf ${ARTIFACTS_DIR}/dist cp -r ${BUILD_DIR}/receptorctl/dist/ ${ARTIFACTS_DIR}/dist + +# Fix permissions +chown -R ${OUTPUT_UID}:${OUTPUT_GID} ${ARTIFACTS_DIR} diff --git a/tests/receptor-tester.sh b/tests/receptor-tester.sh index 806b4d003..8ac7bae62 100755 --- a/tests/receptor-tester.sh +++ b/tests/receptor-tester.sh @@ -15,10 +15,10 @@ tests_dirs=$(echo $tests_dirs | sed 's/ /\n/g' | uniq) tests_files=$(find . -name *_test.go) # Switch between podman or docker -if command -v podman &> /dev/null ; then - export CONTAINER_RUN=podman -elif command -v docker &> /dev/null ; then +if command -v docker &> /dev/null ; then export CONTAINER_RUN=docker +elif command -v podman &> /dev/null ; then + export CONTAINER_RUN=podman fi # Logs. Based on: