Skip to content

Commit 24bae62

Browse files
authored
Integration test implementation (#110)
* Integration tests for EventGate project.
1 parent 514e7cf commit 24bae62

36 files changed

+1169
-39
lines changed

.github/copilot-instructions.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ Python style
1919
- All imports at top of file (never inside functions)
2020
- Apache 2.0 license header in every .py file (including `__init__.py`)
2121
- Docstrings must start with a short summary line
22+
- End all log messages with a period: `logger.info("Message.")`
2223

2324
Patterns
2425
- `__init__` methods must not raise exceptions; defer validation and connection to first use (lazy init)
@@ -31,8 +32,9 @@ Patterns
3132
- Keep error response format stable: `{"success": false, "statusCode": int, "errors": [...]}`
3233

3334
Testing
34-
- Mirror src structure: `src/handlers/` -> `tests/handlers/`
35-
- Mock external services via `conftest.py`: Kafka, EventBridge, PostgreSQL, S3
35+
- Mirror src structure: `src/handlers/` -> `tests/unit/handlers/`
36+
- Unit tests: mock external services via `conftest.py` (Kafka, EventBridge, PostgreSQL, S3)
37+
- Integration tests: call `lambda_handler` directly with real containers (testcontainers-python for Kafka, PostgreSQL, LocalStack)
3638
- No real API/DB calls in unit tests
3739
- Use `mocker.patch("module.dependency")` or `mocker.patch.object(Class, "method")`
3840
- Assert pattern: `assert expected == actual`

.github/workflows/check_python.yml

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,32 @@ jobs:
111111
id: check-format
112112
run: black --check $(git ls-files '*.py')
113113

114-
pytest-test:
114+
mypy-check:
115+
name: Mypy Type Check
116+
needs: detect
117+
if: needs.detect.outputs.python_changed == 'true'
118+
runs-on: ubuntu-latest
119+
steps:
120+
- name: Checkout repository
121+
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
122+
with:
123+
persist-credentials: false
124+
fetch-depth: 0
125+
126+
- name: Set up Python
127+
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548
128+
with:
129+
python-version: '3.13'
130+
cache: 'pip'
131+
132+
- name: Install dependencies
133+
run: pip install -r requirements.txt
134+
135+
- name: Check types with Mypy
136+
id: check-types
137+
run: mypy .
138+
139+
unit-tests:
115140
name: Pytest Unit Tests with Coverage
116141
needs: detect
117142
if: needs.detect.outputs.python_changed == 'true'
@@ -123,7 +148,8 @@ jobs:
123148
persist-credentials: false
124149
fetch-depth: 0
125150

126-
- uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548
151+
- name: Set up Python
152+
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548
127153
with:
128154
python-version: '3.13'
129155
cache: 'pip'
@@ -132,13 +158,14 @@ jobs:
132158
run: pip install -r requirements.txt
133159

134160
- name: Check code coverage with Pytest
135-
run: pytest --cov=. -v tests/ --cov-fail-under=80
161+
run: pytest --cov=. -v tests/unit/ --cov-fail-under=80
136162

137-
mypy-check:
138-
name: Mypy Type Check
163+
integration-tests:
164+
name: Pytest Integration Tests
139165
needs: detect
140166
if: needs.detect.outputs.python_changed == 'true'
141167
runs-on: ubuntu-latest
168+
timeout-minutes: 15
142169
steps:
143170
- name: Checkout repository
144171
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
@@ -155,9 +182,8 @@ jobs:
155182
- name: Install dependencies
156183
run: pip install -r requirements.txt
157184

158-
- name: Check types with Mypy
159-
id: check-types
160-
run: mypy .
185+
- name: Run integration tests
186+
run: pytest tests/integration/ -v --tb=short --log-cli-level=INFO
161187

162188
noop:
163189
name: No Operation

DEVELOPER.md

Lines changed: 72 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55
- [Run Pylint Tool Locally](#run-pylint-tool-locally)
66
- [Run Black Tool Locally](#run-black-tool-locally)
77
- [Run mypy Tool Locally](#run-mypy-tool-locally)
8-
- [Run Unit Test](#running-unit-test)
8+
- [Run Unit Test Locally](#run-unit-test-locally)
99
- [Code Coverage](#code-coverage)
10+
- [Run Integration Test Locally](#run-integration-test-locally)
1011

1112
## Get Started
1213

@@ -45,7 +46,7 @@ To run Pylint on a specific file, follow the pattern `pylint <path_to_file>/<nam
4546

4647
Example:
4748
```shell
48-
pylint src/writer_kafka.py
49+
pylint src/event_gate_lambda.py
4950
```
5051

5152
## Run Black Tool Locally
@@ -68,7 +69,7 @@ To run Black on a specific file, follow the pattern `black <path_to_file>/<name_
6869

6970
Example:
7071
```shell
71-
black src/writer_kafka.py
72+
black src/writers/writer_kafka.py
7273
```
7374

7475
### Expected Output
@@ -100,39 +101,39 @@ To run mypy on a specific file, follow the pattern `mypy <path_to_file>/<name_of
100101

101102
Example:
102103
```shell
103-
mypy src/writer_kafka.py
104+
mypy src/handlers/handler_token.py
104105
```
105106

106-
## Running Unit Test
107+
## Run Unit Test Locally
107108

108109
Unit tests are written using pytest. To run the tests, use the following command:
109110

110111
```shell
111-
pytest tests/
112+
pytest tests/unit/
112113
```
113114

114-
This will execute all tests located in the tests directory.
115+
This will execute all unit tests located in the tests/unit/ directory.
115116

116117
### Focused / Selective Test Runs
117118
Run a single test file:
118119
```shell
119-
pytest tests/test_writer_kafka.py -q
120+
pytest tests/unit/writers/test_writer_kafka.py
120121
```
121122
Filter by keyword expression:
122123
```shell
123-
pytest -k kafka -q
124+
pytest -k kafka
124125
```
125126
Run a single test function (node id):
126127
```shell
127-
pytest tests/test_event_gate_lambda.py::test_post_multiple_writer_failures -q
128+
pytest tests/unit/writers/test_writer_eventbridge.py::test_write_success
128129
```
129130

130131
## Code Coverage
131132

132133
Code coverage is collected using the pytest-cov coverage tool. To run the tests and collect coverage information, use the following command:
133134

134135
```shell
135-
pytest --cov=. -v tests/ --cov-fail-under=80 --cov-report=term-missing
136+
pytest --cov=. -v tests/unit/ --cov-fail-under=80 --cov-report=html
136137
```
137138

138139
This will execute all tests in the tests directory and generate a code coverage report with missing line details and enforce a minimum 80% threshold.
@@ -141,3 +142,63 @@ Open the HTML coverage report:
141142
```shell
142143
open htmlcov/index.html
143144
```
145+
146+
## Run Integration Test Locally
147+
148+
Integration tests validate EventGate against real service dependencies using testcontainers-python.
149+
150+
### Integration Test Approach
151+
152+
EventGate uses a **direct invocation approach** for integration testing:
153+
- **Lambda handler is called directly** in Python (not run in a container)
154+
- **External dependencies run in Docker containers**: Kafka, PostgreSQL, LocalStack (EventBridge)
155+
- **Mock JWT provider runs in-process** as a background thread (no container)
156+
- Test configuration is dynamically generated and injected via environment variables
157+
158+
### Prerequisites
159+
- Docker running (Docker Desktop on macOS/Windows, or Docker Engine on Linux)
160+
- Python 3.13 with dependencies installed
161+
162+
### Run Integration Tests
163+
164+
Containers start and stop automatically:
165+
```shell
166+
pytest tests/integration/ -v
167+
```
168+
169+
With detailed logging:
170+
```shell
171+
pytest tests/integration/ -v --log-cli-level=INFO
172+
```
173+
174+
### Run Specific Integration Tests
175+
176+
Run a single test file:
177+
```shell
178+
pytest tests/integration/test_health_endpoint.py -v
179+
```
180+
181+
Run a specific test function:
182+
```shell
183+
pytest tests/integration/test_topics_endpoint.py::TestPostEventEndpoint::test_post_event_with_valid_token_returns_202 -v
184+
```
185+
186+
### Troubleshooting
187+
188+
If containers fail to start, check Docker is running:
189+
```shell
190+
docker info
191+
```
192+
193+
If image pulls fail with TLS or timeout errors, pre-pull the required images manually:
194+
```shell
195+
docker pull testcontainers/ryuk:0.8.1
196+
docker pull postgres:16
197+
docker pull confluentinc/cp-kafka:7.6.0
198+
docker pull localstack/localstack:latest
199+
```
200+
201+
View container logs in pytest output by increasing log level:
202+
```shell
203+
pytest tests/integration/ -v --log-cli-level=DEBUG
204+
```

README.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -146,10 +146,9 @@ Use when Kafka access needs Kerberos / SASL_SSL or custom `librdkafka` build.
146146
| Static code analysis (Pylint) | [Run Pylint Tool Locally](./DEVELOPER.md#run-pylint-tool-locally) |
147147
| Formatting (Black) | [Run Black Tool Locally](./DEVELOPER.md#run-black-tool-locally) |
148148
| Type checking (mypy) | [Run mypy Tool Locally](./DEVELOPER.md#run-mypy-tool-locally) |
149-
| Terraform Linter (TFLint) | [Run TFLint Tool Locally](./DEVELOPER.md#run-tflint-tool-locally) |
150-
| Security Scanner (Trivy) | [Run Trivy Tool Locally](./DEVELOPER.md#run-trivy-tool-locally) |
151-
| Unit tests | [Running Unit Test](./DEVELOPER.md#running-unit-test) |
149+
| Unit tests | [Run Unit Test Locally](./DEVELOPER.md#run-unit-test-locally) |
152150
| Code coverage | [Code Coverage](./DEVELOPER.md#code-coverage) |
151+
| Integration tests | [Run Integration Test Locally](./DEVELOPER.md#run-integration-test-locally) |
153152

154153
## Security & Authorization
155154
- JWT tokens must be RS256 signed; current and previous public keys are fetched at cold start from `token_public_keys_url` as DER base64 values (list `keys[*].key`, with single-key fallback `{ "key": "..." }`).

conf/access.json

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,5 @@
11
{
2-
"public.cps.za.runs": [
3-
"FooBarUser"
4-
],
5-
"public.cps.za.dlchange": [
6-
"FooUser",
7-
"BarUser"
8-
],
9-
"public.cps.za.test": [
10-
"TestUser"
11-
]
12-
}
2+
"public.cps.za.runs": ["FooBarUser", "IntegrationTestUser"],
3+
"public.cps.za.dlchange": ["FooUser", "BarUser"],
4+
"public.cps.za.test": ["TestUser"]
5+
}

requirements.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
pytest==8.4.2
1+
pytest==9.0.2
22
pytest-cov==6.3.0
33
pytest-mock==3.15.0
44
pylint==3.3.8
@@ -12,5 +12,7 @@ PyJWT==2.10.1
1212
requests==2.32.5
1313
boto3==1.40.25
1414
confluent-kafka==2.12.1
15+
testcontainers==4.14.1
16+
docker==7.1.0
1517
# psycopg2-binary==2.9.10 # Ideal for local development, but not for long-term production use
1618
psycopg2==2.9.10

tests/integration/__init__.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#
2+
# Copyright 2026 ABSA Group Limited
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
#

0 commit comments

Comments
 (0)