Skip to content

Commit 0195c4f

Browse files
committed
Merge remote-tracking branch 'origin/main'
# Conflicts: # requirements.txt
2 parents 86e3eb8 + c4008f9 commit 0195c4f

File tree

5 files changed

+196
-49
lines changed

5 files changed

+196
-49
lines changed

.github/workflows/main.yml

Lines changed: 58 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2,45 +2,68 @@ name: CI with contracts run through command line
22

33
on:
44
push:
5-
branches: [ main ]
5+
branches: [main]
66

77
jobs:
88
build:
99
strategy:
1010
matrix:
11-
os: [ ubuntu-latest]
11+
os: [ubuntu-latest]
1212
runs-on: ${{ matrix.os }}
1313
steps:
14-
- uses: actions/checkout@v2
15-
with:
16-
path: main
17-
- name: Set up JRE 17
18-
uses: actions/setup-java@v3
19-
with:
20-
distribution: 'temurin'
21-
java-version: '17'
22-
java-package: 'jre'
23-
- name: Setup python
24-
uses: actions/setup-python@v4
25-
with:
26-
python-version: '3.11'
27-
cache: 'pip'
28-
- name: Run pip install
29-
working-directory: main
30-
run: pip install -r requirements.txt
31-
- name: Run contract as tests with Specmatic Python
32-
working-directory: main
33-
run: coverage run --branch -m pytest tests -v -s --junitxml contract-test-reports/TEST-junit-jupiter.xml
34-
- name: Publish contract test report
35-
uses: mikepenz/action-junit-report@v3
36-
if: always()
37-
with:
38-
report_paths: '**/contract-test-reports/TEST-*.xml'
39-
- name: Generate coverage report
40-
working-directory: main
41-
run: coverage html -d coverage-report
42-
- name: Upload coverage report
43-
uses: actions/upload-artifact@v4
44-
with:
45-
name: coverage-report
46-
path: main/coverage-report
14+
- uses: actions/checkout@v2
15+
16+
- name: Set up JRE 17
17+
uses: actions/setup-java@v3
18+
with:
19+
distribution: "temurin"
20+
java-version: "17"
21+
java-package: "jre"
22+
23+
- name: Setup python
24+
uses: actions/setup-python@v4
25+
with:
26+
python-version: "3.11"
27+
cache: "pip"
28+
29+
- name: Run pip install
30+
run: pip install -r requirements.txt
31+
32+
- name: Run contract as tests with Specmatic Python
33+
run: coverage run --branch -m pytest tests -v -s --junitxml contract-test-reports/TEST-junit-jupiter.xml
34+
35+
- name: Save Specmatic license
36+
if: runner.os == 'Linux'
37+
run: |
38+
mkdir -p ~/.specmatic
39+
echo "${{ secrets.SPECMATIC_LICENSE_KEY }}" > ~/.specmatic/specmatic-license.txt
40+
41+
- name: Run Specmatic Insights Build Reporter
42+
if: runner.os == 'Linux'
43+
run: |
44+
docker run \
45+
-v ${{ github.workspace }}:/workspace \
46+
-v ~/.specmatic:/root/.specmatic \
47+
-w /workspace \
48+
specmatic/specmatic-reporter \
49+
send-report \
50+
--metadata build_url=${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} \
51+
--branch-name ${{ github.ref_name }} \
52+
--repo-name ${{ github.event.repository.name }} \
53+
--repo-id ${{ github.repository_id }} \
54+
--repo-url ${{ github.event.repository.html_url }}
55+
56+
- name: Publish contract test report
57+
uses: mikepenz/action-junit-report@v3
58+
if: always()
59+
with:
60+
report_paths: "**/contract-test-reports/TEST-*.xml"
61+
62+
- name: Generate coverage report
63+
run: coverage html -d coverage-report
64+
65+
- name: Upload coverage report
66+
uses: actions/upload-artifact@v4
67+
with:
68+
name: coverage-report
69+
path: coverage-report

README.md

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
# Specmatic FastAPI with Redis Demo
22

3-
This is a Python implementation of the [Specmatic Order BFF Service](https://github.com/znsio/specmatic-order-ui)
4-
project.
3+
This is a Python implementation of the [Specmatic Order BFF Service](https://github.com/znsio/specmatic-order-bff-java) project.
54
The implementation is based on the [FastApi](https://fastapi.tiangolo.com/) framework.
65

76
The open api contract for the services is defined in
8-
the [Specmatic Central Contract Repository](https://github.com/znsio/specmatic-order-contracts/blob/main/in/specmatic/examples/store/api_order_v1.yaml)
7+
the [Specmatic Central Contract Repository](https://github.com/specmatic/specmatic-order-contracts/blob/main/io/specmatic/examples/store/openapi/product_search_bff_v4.yaml)
98

109
The bff service internally calls the order api service (on port 8080).
1110

@@ -17,20 +16,44 @@ while mocking out the order api service at the same time.
1716

1817
2. Demonstrate how to mock out the Redis server using Specmatic Redis Mock and Test Containers.
1918

20-
2119
## Prerequisites
22-
2320
- Python 3.11+
2421
- JRE 17+.
22+
- Docker Desktop
23+
24+
1. ### Create a virtual environment named ".venv" by executing the following command in the terminal from the project's root directory
2525

26-
1. Create a virtual environment and install all dependencies:
26+
```shell
27+
python -m venv .venv
28+
```
2729

28-
```bash
29-
python -m venv .venv
30+
2. ### Activate virtual environment by executing
31+
32+
* **on MacOS and Linux**
33+
34+
```shell
3035
source .venv/bin/activate
31-
pip install -r requirements.txt
3236
```
3337

38+
* **on Windows CMD**
39+
40+
```cmd
41+
.venv\Scripts\activate.bat
42+
```
43+
44+
* **on Windows Powershell (you may need to adjust the ExecutionPolicy)**
45+
46+
```powershell
47+
.\.venv\Scripts\Activate.ps1
48+
```
49+
50+
3. ### Install Dependencies
51+
52+
To install all necessary dependencies for this project, navigate to the project's root directory in your terminal and execute
53+
```shell
54+
pip install -r requirements.txt
55+
```
56+
3457
## Running the contract suite
3558

3659
Run the tests using pytest:

requirements.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@ fastapi>=0.110
22
pytest>=7.4
33
python-dotenv>=1.0
44
specmatic==2.31.1
5-
redis==6.4.0
5+
redis==7.0.1
66
testcontainers==4.13.2
7-
coverage==7.10.7
7+
coverage==7.11.0
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import multiprocessing
2+
import os
3+
import sys
4+
import threading
5+
from pathlib import Path
6+
7+
import pytest
8+
import uvicorn
9+
from testcontainers.core.container import DockerContainer
10+
from testcontainers.core.wait_strategies import HttpWaitStrategy, LogMessageWaitStrategy
11+
12+
APPLICATION_HOST = "0.0.0.0"
13+
APPLICATION_PORT = 8000
14+
HTTP_STUB_PORT = 8080
15+
16+
17+
class UvicornServer(multiprocessing.Process):
18+
def __init__(self, config: uvicorn.Config):
19+
super().__init__()
20+
self.server = uvicorn.Server(config=config)
21+
self.config = config
22+
23+
def stop(self):
24+
self.terminate()
25+
26+
def run(self, *args, **kwargs):
27+
self.server.run()
28+
29+
30+
def stream_container_logs(container: DockerContainer, name=None):
31+
def _stream():
32+
for line in container.get_wrapped_container().logs(stream=True, follow=True):
33+
text = line.decode(errors="ignore").rstrip()
34+
prefix = f"[{name}] " if name else ""
35+
print(f"{prefix}{text}")
36+
37+
thread = threading.Thread(target=_stream, daemon=True)
38+
thread.start()
39+
return thread
40+
41+
42+
@pytest.fixture(scope="module")
43+
def api_service():
44+
config = uvicorn.Config("app.main:app", host=APPLICATION_HOST, port=APPLICATION_PORT, log_level="info")
45+
server = UvicornServer(config)
46+
server.start()
47+
yield server
48+
server.stop()
49+
50+
51+
@pytest.fixture(scope="module")
52+
def stub_container():
53+
examples_path = Path("test/contract/data").resolve()
54+
specmatic_yaml_path = Path("specmatic.yaml").resolve()
55+
build_reports_path = Path("build/reports/specmatic").resolve()
56+
container = (
57+
DockerContainer("specmatic/specmatic")
58+
.with_command(["virtualize", "--examples=examples", f"--port={HTTP_STUB_PORT}"])
59+
.with_bind_ports(HTTP_STUB_PORT, HTTP_STUB_PORT)
60+
.with_volume_mapping(examples_path, "/usr/src/app/examples", mode="ro")
61+
.with_volume_mapping(specmatic_yaml_path, "/usr/src/app/specmatic.yaml", mode="ro")
62+
.with_volume_mapping(build_reports_path, "/usr/src/app/build/reports/specmatic", mode="rw")
63+
.waiting_for(HttpWaitStrategy(HTTP_STUB_PORT, path="/actuator/health").with_method("GET").for_status_code(200))
64+
)
65+
container.start()
66+
thread = stream_container_logs(container, name="specmatic-stub")
67+
yield container
68+
container.stop()
69+
thread.join()
70+
71+
72+
@pytest.fixture(scope="module")
73+
def test_container():
74+
specmatic_yaml_path = Path("specmatic.yaml").resolve()
75+
build_reports_path = Path("build/reports/specmatic").resolve()
76+
container = (
77+
DockerContainer("specmatic/specmatic")
78+
.with_command(["test", "--host=host.docker.internal", f"--port={APPLICATION_PORT}"])
79+
.with_env("SPECMATIC_GENERATIVE_TESTS", "true")
80+
.with_volume_mapping(specmatic_yaml_path, "/usr/src/app/specmatic.yaml", mode="ro")
81+
.with_volume_mapping(build_reports_path, "/usr/src/app/build/reports/specmatic", mode="rw")
82+
.with_kwargs(extra_hosts={"host.docker.internal": "host-gateway"})
83+
.waiting_for(LogMessageWaitStrategy("Tests run:"))
84+
)
85+
container.start()
86+
thread = stream_container_logs(container, name="specmatic-test")
87+
yield container
88+
container.stop()
89+
thread.join()
90+
91+
92+
@pytest.mark.skipif(
93+
os.environ.get("CI") == "true" and not sys.platform.startswith("linux"),
94+
reason="Run only on Linux CI; all platforms allowed locally",
95+
)
96+
def test_contract(api_service, stub_container, test_container):
97+
stdout, stderr = test_container.get_logs()
98+
stdout = stdout.decode("utf-8")
99+
stderr = stderr.decode("utf-8")
100+
if stderr or "Failures: 0" not in stdout:
101+
raise AssertionError(f"Contract tests failed; container logs:\n{stdout}\n{stderr}") # noqa: EM102

tests/redis/test_redis_service.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
REDIS_HOST = "0.0.0.0"
1212
REDIS_PORT = 6379
13-
TEST_DATA_DIR = ROOT_DIR + "/tests/redis/data"
13+
TEST_DATA_DIR = "/tests/redis/data"
1414

1515
logger = logging.getLogger("specmatic.redis.mock")
1616
logger.setLevel(logging.DEBUG)
@@ -25,7 +25,7 @@ def redis_service(self):
2525
DockerContainer(f"specmatic/specmatic-redis:{SPECMATIC_REDIS_VERSION}")
2626
.with_command(f"virtualize --host {REDIS_HOST} --port {REDIS_PORT} --data {TEST_DATA_DIR}")
2727
.with_exposed_ports(REDIS_PORT)
28-
.with_volume_mapping(TEST_DATA_DIR, TEST_DATA_DIR)
28+
.with_volume_mapping(ROOT_DIR + TEST_DATA_DIR, TEST_DATA_DIR)
2929
.waiting_for(LogMessageWaitStrategy(r"Specmatic Redis has started on .*:\d+").with_startup_timeout(10))
3030
)
3131

@@ -35,7 +35,7 @@ def redis_service(self):
3535
container.start()
3636
print_container_logs(container)
3737
port = container.get_exposed_port(REDIS_PORT)
38-
redis_client = Redis(host=REDIS_HOST, port=port, decode_responses=True)
38+
redis_client = Redis(host="localhost", port=port, decode_responses=True)
3939
service = RedisService(redis_client)
4040
yield service
4141

0 commit comments

Comments
 (0)