Skip to content

Commit

Permalink
Test Rust proxy
Browse files Browse the repository at this point in the history
Rewrite the existing tests to be integration tests against a compiled
Rust binary. We use the httpbin library to start up a Python webserver
and instruct the proxy to connect to it. This allows to test connection
properties that aren't recordable in the VCR format, like timeouts or
streamed responses.

The tests are reorganized to be split into proxy handling and error
handling.
  • Loading branch information
legoktm committed May 2, 2024
1 parent 5e438b3 commit b4f9924
Show file tree
Hide file tree
Showing 8 changed files with 1,116 additions and 1,144 deletions.
21 changes: 3 additions & 18 deletions proxy/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,11 @@ mypy: ## Run mypy static type checker
@poetry run mypy --ignore-missing-imports securedrop_proxy

.PHONY: test
test: clean .coverage ## Runs tests with coverage

.coverage:
@poetry run coverage run --source securedrop_proxy -m unittest

.PHONY: browse-coverage
browse-coverage: .coverage ## Generates and opens HTML coverage report
@poetry run coverage html
@xdg-open htmlcov/index.html 2>/dev/null || open htmlcov/index.html 2>/dev/null
test: ## Runs tests with pytest
@poetry run pytest

.PHONY: check
check: clean lint test mypy ## Runs all tests and code checkers

.PHONY: clean
clean: ## Clean the workspace of generated resources
@rm -rf .mypy_cache build dist *.egg-info .coverage .eggs docs/_build .pytest_cache lib htmlcov .cache && \
find . \( -name '*.py[co]' -o -name dropin.cache \) -delete && \
find . \( -name '*.bak' -o -name dropin.cache \) -delete && \
find . \( -name '*.tgz' -o -name dropin.cache \) -delete && \
find . -name __pycache__ -print0 | xargs -0 rm -rf
check: lint test mypy ## Runs all tests and code checkers

# Explanation of the below shell command should it ever break.
# 1. Set the field separator to ": ##" and any make targets that might appear between : and ##
Expand Down
1,278 changes: 890 additions & 388 deletions proxy/poetry.lock

Large diffs are not rendered by default.

11 changes: 4 additions & 7 deletions proxy/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,10 @@ readme = "README.md"

[tool.poetry.dependencies]
python = "^3.11"
pyyaml = "^5.4.1"
requests = "^2.31.0"

[tool.poetry.group.dev.dependencies]
coverage = "^7.3.0"
flake8 = "^6.1.0"
mypy = "^1.5.1"
types-PyYAML = "^6.0.12.11"
types-requests = "^2.31.0.2"
vcrpy = "^5.1.0"
pytest = "^7.4.3"
pytest-httpbin = "^2.0.0"
# needed for httpbin, see https://github.com/psf/httpbin/issues/36
werkzeug = "<2.1.0"
32 changes: 32 additions & 0 deletions proxy/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import json
import os
import subprocess
from typing import Optional, Union

import pytest


@pytest.fixture(scope="session")
def proxy_bin() -> str:
if "PROXY_BIN" in os.environ:
# allow running tests against e.g. a packaged binary
return os.environ["PROXY_BIN"]
# default debug path, expects `cargo build` to already have been run
metadata = subprocess.check_output(["cargo", "metadata"])
return json.loads(metadata)["target_directory"] + "/debug/securedrop-proxy"


@pytest.fixture
def proxy_request(httpbin, proxy_bin):
def proxy_(
input: Union[bytes, dict], origin: Optional[str] = None
) -> subprocess.CompletedProcess:
if isinstance(input, dict):
input = json.dumps(input).encode()
if origin is None:
origin = httpbin.url
return subprocess.run(
[proxy_bin], env={"SD_PROXY_ORIGIN": origin}, input=input, capture_output=True
)

return proxy_
70 changes: 70 additions & 0 deletions proxy/tests/test_errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import os
import subprocess


def test_missing_config(proxy_bin):
env = os.environ.copy()
if "SD_PROXY_ORIGIN" in env:
del env["SD_PROXY_ORIGIN"]
result = subprocess.run([proxy_bin], env=env, capture_output=True)
assert result.returncode == 1
assert result.stderr.decode().strip() == '{"error":"environment variable not found"}'


def test_empty_input(proxy_request):
result = proxy_request(input=b"")
assert result.returncode == 1
assert (
result.stderr.decode().strip() == '{"error":"EOF while parsing a value at line 1 column 0"}'
)


def test_input_invalid(proxy_request):
test_input = b'"foo": "bar", "baz": "bliff"}'
result = proxy_request(input=test_input)
assert result.returncode == 1
assert (
result.stderr.decode().strip()
== '{"error":"invalid type: string \\"foo\\", '
+ 'expected struct IncomingRequest at line 1 column 5"}'
)


def test_input_missing_keys(proxy_request):
test_input = b'{"foo": "bar", "baz": "bliff"}'
result = proxy_request(input=test_input)
assert result.returncode == 1
assert (
result.stderr.decode().strip()
== '{"error":"unknown field `foo`, expected one of `method`, '
+ '`path_query`, `stream`, `headers`, `body`, `timeout` at line 1 column 6"}'
)


def test_invalid_origin(proxy_request):
test_input = {
"method": "GET",
"path_query": "/status/200",
"stream": False,
}
# invalid port
result = proxy_request(input=test_input, origin="http://127.0.0.1:-1/foo")
assert result.returncode == 1
assert result.stderr.decode().strip() == '{"error":"invalid port number"}'


def test_cannot_connect(proxy_request):
test_input = {
"method": "GET",
"path_query": "/",
"stream": False,
}
# .test is a reserved TLD, so it should never resolve
result = proxy_request(input=test_input, origin="http://missing.test/foo")
assert result.returncode == 1
assert (
result.stderr.decode().strip()
== '{"error":"error sending request for url (http://missing.test/): '
+ "error trying to connect: dns error: failed to lookup address information: "
+ 'Name or service not known"}'
)
21 changes: 0 additions & 21 deletions proxy/tests/test_json.py

This file was deleted.

Loading

0 comments on commit b4f9924

Please sign in to comment.