Skip to content

Commit d3bdfb9

Browse files
feat(tests): add local integration test suite for proxy and plugins (#13)
* feat(tests): add local integration test suite for proxy and plugins --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 5ade47a commit d3bdfb9

12 files changed

Lines changed: 559 additions & 84 deletions

File tree

Makefile

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
1+
SHELL := /bin/bash
12
VENV_DIR ?= .venv
23
VENV_RUN = . $(VENV_DIR)/bin/activate
34
PIP_CMD ?= pip
5+
PYTHON_CMD ?= python
6+
TEST_REQS ?= requirements-test.txt
7+
LINT_REQS ?= requirements-lint.txt
8+
9+
PG_TEST_CONTAINER ?= pg-proxy-local-tests
10+
PG_TEST_IMAGE ?= postgres:16
11+
PG_TEST_PORT ?= 55432
12+
PG_TEST_USER ?= postgres
13+
PG_TEST_PASSWORD ?= postgres
14+
PG_TEST_DB ?= postgres
415

516
usage: ## Show this help
617
@fgrep -h "##" $(MAKEFILE_LIST) | fgrep -v fgrep | sed -e 's/\\$$//' | sed -e 's/##//'
@@ -14,4 +25,57 @@ install: ## Install dependencies in local virtualenv folder
1425
publish: ## Publish the library to the central PyPi repository
1526
($(VENV_RUN); pip install twine; python ./setup.py sdist && twine upload dist/*)
1627

17-
.PHONY: usage install clean publish test lint
28+
install-test: install ## Install test dependencies in local virtualenv
29+
($(VENV_RUN); $(PIP_CMD) install -r $(TEST_REQS))
30+
31+
install-lint: install ## Install lint dependencies in local virtualenv
32+
($(VENV_RUN); $(PIP_CMD) install -r $(LINT_REQS))
33+
34+
lint: install-lint ## Format code with ruff
35+
$(VENV_DIR)/bin/ruff format postgresql_proxy tests plugins
36+
37+
start-postgres: ## Start local PostgreSQL test container and wait until ready
38+
@set -euo pipefail; \
39+
if docker ps -a --format '{{.Names}}' | grep -Fx '$(PG_TEST_CONTAINER)' >/dev/null 2>&1; then \
40+
echo "Container $(PG_TEST_CONTAINER) already exists; run 'make stop-postgres' first"; \
41+
exit 1; \
42+
fi; \
43+
docker run --name $(PG_TEST_CONTAINER) \
44+
-e POSTGRES_USER=$(PG_TEST_USER) \
45+
-e POSTGRES_PASSWORD=$(PG_TEST_PASSWORD) \
46+
-e POSTGRES_DB=$(PG_TEST_DB) \
47+
-p $(PG_TEST_PORT):5432 \
48+
-d $(PG_TEST_IMAGE) >/dev/null; \
49+
for i in $$(seq 1 45); do \
50+
if docker exec $(PG_TEST_CONTAINER) pg_isready -U $(PG_TEST_USER) >/dev/null 2>&1; then \
51+
echo "PostgreSQL ready on 127.0.0.1:$(PG_TEST_PORT)"; \
52+
break; \
53+
fi; \
54+
sleep 1; \
55+
done; \
56+
if ! docker exec $(PG_TEST_CONTAINER) pg_isready -U $(PG_TEST_USER) >/dev/null 2>&1; then \
57+
echo "PostgreSQL did not become ready in time"; \
58+
docker rm -f $(PG_TEST_CONTAINER) >/dev/null 2>&1 || true; \
59+
exit 1; \
60+
fi
61+
62+
stop-postgres: ## Stop and remove local PostgreSQL test container
63+
@docker rm -f $(PG_TEST_CONTAINER) >/dev/null 2>&1 || true
64+
65+
test: ## Run all tests against an already running PostgreSQL
66+
$(VENV_DIR)/bin/$(PYTHON_CMD) -m pytest -vv
67+
68+
start-pg-and-test: ## Start local PostgreSQL container, run all tests, and clean up
69+
@set -euo pipefail; \
70+
status=0; \
71+
$(MAKE) start-postgres; \
72+
E2E_PG_HOST=127.0.0.1 \
73+
E2E_PG_PORT=$(PG_TEST_PORT) \
74+
E2E_PG_USER=$(PG_TEST_USER) \
75+
E2E_PG_PASSWORD=$(PG_TEST_PASSWORD) \
76+
E2E_PG_DB=$(PG_TEST_DB) \
77+
$(MAKE) test || status=$$?; \
78+
$(MAKE) stop-postgres; \
79+
exit $$status
80+
81+
.PHONY: usage install install-test install-lint clean publish lint start-postgres stop-postgres test start-pg-and-test

README.md

Lines changed: 94 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,26 +5,30 @@ Serves as a proper server that Postgresql clients can connect to. Can modify pac
55
Currently used for rewriting queries to force proper use of postgres-hll module by external proprietary software that doesn't know about that functionality
66

77
## Installing
8-
### Linux
9-
1. Make sure you have [python3 and pip3 installed on your system](https://stackoverflow.com/questions/6587507/how-to-install-pip-with-python-3#6587528). It has been tested with Python3.6 but should also run on Python3.5.
10-
2. Clone it locally and cd to that directory
11-
```
12-
git clone git@github.com:kfzteile24/postgresql-proxy.git
13-
cd postgresql-proxy
14-
```
15-
3. Run [setup.sh](setup.sh)
16-
```
17-
./setup.sh
18-
```
19-
4. Make a copy of [config.yml.example](config.yml.example) called `config.yml` and configure your proxy instances. Create the log directories if they're not there.
8+
9+
Requires Python `3.13+`.
10+
11+
1. Clone the repository:
12+
```bash
13+
git clone git@github.com:localstack/postgresql-proxy.git
14+
cd postgresql-proxy
15+
```
16+
2. Install dependencies into a local virtualenv:
17+
```bash
18+
make install
19+
```
20+
3. Copy the example config and edit it for your environment:
21+
```bash
22+
cp config.yml.example config.yml
23+
```
2024

2125
## Configuring
2226
In the `config.yml` file you can define the following things
2327
### Plugins
2428
A list of dynamically loaded modules that reside in the [plugins](plugins) directory. These plugins can be used in later configuration, to intercept queries, commands, or responses. View plugin documentation for example plugins for more details on how to do that.
2529
### Settings
2630
General application settings. Currently the following settings are used
27-
* `log-level` - the log level for the general log. See [python logging](https://docs.python.org/3.6/library/logging.html) for more details about the logging functionality
31+
* `log-level` - the log level for the general log. See [python logging](https://docs.python.org/3/library/logging.html) for more details about the logging functionality
2832
* `general-log` - the location for the general log. All general messages go in there.
2933
* `intercept-log` - the location for the intercept log. Intercepted messages and return values from various enabled plugins will be written there. This log can be quite verbose as it contains the full binary messages being circulated.
3034

@@ -42,19 +46,22 @@ Make sure to manage the logs yourself, as they accumulate and take up disk space
4246

4347
Each interceptor definition must have a `plugin`, which should also be present in the [plugins](#Plugins) configuration, and a `function`, that is found directly in that module, that will be called each time with the intercepted message as a byte string, and a context variable that is an instance of the `Proxy` class, that contains connection information and other useful stuff.
4448

45-
## Running in testing mode
46-
If you want to test it, do this. Otherwise scroll down for instructions on how to install it as a service
47-
### Linux
48-
1. Activate the virtual environment
49-
```
50-
source .venv/bin/activate
51-
```
52-
2. Run it
53-
```
54-
python proxy.py
55-
```
56-
57-
### Changelog
49+
## Running
50+
51+
Activate the virtualenv and run the proxy directly:
52+
53+
```bash
54+
source .venv/bin/activate
55+
python -m postgresql_proxy
56+
```
57+
58+
Or run it without activating the venv:
59+
60+
```bash
61+
.venv/bin/python -m postgresql_proxy
62+
```
63+
64+
## Changelog
5865
- v0.3.1
5966
- Fix SSL COPY stalls by draining pending SSL buffer after recv [#11](https://github.com/localstack/postgresql-proxy/pull/11)
6067
- Fix intermittent `BlockingIOError` on macOS during SSL negotiation
@@ -81,3 +88,64 @@ If you want to test it, do this. Otherwise scroll down for instructions on how t
8188
- add stop() method to proxy; refactor logging
8289
- v0.0.2
8390
- fix socket file descriptors under Linux
91+
92+
## Testing
93+
94+
All tests require a real PostgreSQL server and are organized at the top level:
95+
96+
- `test_proxy.py`: proxy behavior tests (connection, SSL, hang regressions)
97+
- `test_plugins.py`: plugin integration tests (HLL rewrite behavior)
98+
99+
### Prerequisites
100+
101+
- Python `3.13` (same version as CI)
102+
- Docker (for local disposable PostgreSQL)
103+
- `psql` (`postgresql-client`)
104+
- `openssl` (SSL tests generate a temporary self-signed cert/key at runtime)
105+
106+
Install Python deps in the project virtualenv:
107+
108+
```bash
109+
make install-test
110+
```
111+
112+
### Which command should I use?
113+
114+
- One-command full local run with disposable Postgres: `make start-pg-and-test`
115+
- Run full suite against an already running Postgres: `make test`
116+
- Run only proxy tests (using your own Postgres): `python -m pytest tests/test_proxy.py -vv`
117+
- Run only plugin tests: `python -m pytest tests/test_plugins.py -vv`
118+
119+
#### 1) Full local suite (recommended)
120+
121+
`make start-pg-and-test` starts a temporary PostgreSQL container, waits for readiness, sets DB env vars, then runs:
122+
123+
```bash
124+
make test
125+
```
126+
127+
Use it when you want one command that matches normal contributor workflow.
128+
129+
```bash
130+
make start-pg-and-test
131+
```
132+
133+
#### 2) Run against an existing PostgreSQL
134+
135+
If you already have PostgreSQL running, set connection env vars and run the tests you need:
136+
137+
```bash
138+
export E2E_PG_HOST=127.0.0.1
139+
export E2E_PG_PORT=5432
140+
export E2E_PG_USER=postgres
141+
export E2E_PG_PASSWORD=postgres
142+
export E2E_PG_DB=postgres
143+
144+
# Proxy tests only
145+
python -m pytest tests/test_proxy.py -vv
146+
147+
# Plugin tests only
148+
python -m pytest tests/test_plugins.py -vv
149+
```
150+
151+
If PostgreSQL is not reachable, tests fail fast at startup.

plugins/tableau_hll/test.py

Lines changed: 0 additions & 34 deletions
This file was deleted.

postgresql_proxy/__main__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from postgresql_proxy.proxy import main
2+
3+
main()

postgresql_proxy/proxy.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,7 @@ def stop(self):
329329
self.running = False
330330

331331

332-
if __name__ == '__main__':
332+
def main():
333333
import importlib
334334
import yaml
335335
import os
@@ -370,3 +370,7 @@ def stop(self):
370370
logging.info("Starting proxy instance")
371371
proxy = Proxy(instance, plugins)
372372
proxy.listen()
373+
374+
375+
if __name__ == "__main__":
376+
main()

requirements-lint.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ruff==0.15.12

requirements-test.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
pytest==9.0.3
2+
pytest-timeout==2.4.0

setup.py

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
import os
2-
import re
3-
import sys
41
from setuptools import find_packages, setup
52

63
install_requires = []
@@ -16,14 +13,8 @@
1613
install_requires=install_requires,
1714
zip_safe=False,
1815
classifiers=[
19-
'Programming Language :: Python :: 2',
20-
'Programming Language :: Python :: 2.6',
21-
'Programming Language :: Python :: 2.7',
2216
'Programming Language :: Python :: 3',
23-
'Programming Language :: Python :: 3.3',
24-
'Programming Language :: Python :: 3.6',
25-
'Programming Language :: Python :: 3.7',
26-
'Programming Language :: Python :: 3.8',
17+
'Programming Language :: Python :: 3.13',
2718
'License :: OSI Approved :: Apache Software License',
2819
'Topic :: Software Development :: Testing',
2920
]

tests/conftest.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import os
2+
3+
import psycopg2
4+
import pytest
5+
6+
7+
@pytest.fixture(scope="session")
8+
def postgres_settings():
9+
"""PostgreSQL connection settings from environment or defaults."""
10+
return {
11+
"host": os.environ.get("E2E_PG_HOST", "127.0.0.1"),
12+
"port": int(os.environ.get("E2E_PG_PORT", "5432")),
13+
"user": os.environ.get("E2E_PG_USER", "postgres"),
14+
"password": os.environ.get("E2E_PG_PASSWORD", "postgres"),
15+
"dbname": os.environ.get("E2E_PG_DB", "postgres"),
16+
}
17+
18+
19+
@pytest.fixture(scope="session", autouse=True)
20+
def ensure_postgres_available(postgres_settings):
21+
"""Ensure PostgreSQL backend is available before running any tests."""
22+
try:
23+
with psycopg2.connect(
24+
connect_timeout=3, sslmode="disable", **postgres_settings
25+
) as conn:
26+
with conn.cursor() as cur:
27+
cur.execute("SELECT 1")
28+
assert cur.fetchone() == (1,)
29+
except Exception as err: # pragma: no cover - environment dependent
30+
pytest.fail(
31+
f"PostgreSQL backend is required for tests but is not reachable: {err}"
32+
)

tests/test_plugin.py

Lines changed: 0 additions & 12 deletions
This file was deleted.

0 commit comments

Comments
 (0)