Skip to content

Commit 03dd875

Browse files
zeevdrclaude
andcommitted
feat(tests): add integration pytest marker and live-server tests
- Register `integration` marker in pyproject.toml; auto-skip when DECREE_TEST_ADDR is unset so the unit-test suite is unaffected. - Add session-scoped fixtures to conftest: grpc_channel, schema_stub, live_schema (creates + publishes schema, deletes on teardown), live_tenant (creates tenant, deletes on teardown). - Add tests/test_integration.py covering sync and async ConfigClient: get/set (string, int, bool, float), get_all, set_many, set_null, NotFoundError, context-manager idiom. - Add `make integration` target (requires DECREE_TEST_ADDR). - Add optional integration job to CI (workflow_dispatch only): checks out decree, builds server image with registry cache, starts docker-compose, runs tests, tears down. Closes #72 Co-Authored-By: Claude <noreply@anthropic.com>
1 parent f353484 commit 03dd875

5 files changed

Lines changed: 357 additions & 2 deletions

File tree

.github/workflows/ci.yml

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
# CI pipeline for the OpenDecree Python SDK.
22
#
33
# Jobs: lint, typecheck, test (matrix: 3.11-3.13), examples → check (alls-green gate)
4-
# The check job aggregates all results for branch protection.
4+
# Integration job is optional — runs on workflow_dispatch or when
5+
# DECREE_TEST_ADDR secret is set, starting a live server via docker-compose.
56

67
name: CI
78

@@ -11,6 +12,12 @@ on:
1112
pull_request:
1213
branches: [main]
1314
workflow_call:
15+
workflow_dispatch:
16+
inputs:
17+
run-integration:
18+
description: "Run integration tests against a live server"
19+
type: boolean
20+
default: false
1421

1522
concurrency:
1623
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}
@@ -162,6 +169,91 @@ jobs:
162169
print(f"OK: {found[0]}")
163170
EOF
164171
172+
integration:
173+
name: Integration tests
174+
runs-on: ubuntu-latest
175+
if: >-
176+
github.event_name == 'workflow_dispatch' &&
177+
inputs.run-integration == true
178+
timeout-minutes: 20
179+
permissions:
180+
contents: read
181+
packages: read
182+
steps:
183+
- name: Checkout decree-python
184+
uses: actions/checkout@v6
185+
with:
186+
persist-credentials: false
187+
188+
- name: Checkout decree (for docker-compose + server)
189+
uses: actions/checkout@v6
190+
with:
191+
repository: opendecree/decree
192+
path: decree
193+
persist-credentials: false
194+
195+
- name: Set up Python
196+
uses: actions/setup-python@v6
197+
with:
198+
python-version: "3.12"
199+
cache: pip
200+
cache-dependency-path: sdk/pyproject.toml
201+
202+
- name: Install SDK with dev dependencies
203+
run: pip install -e "sdk[dev]"
204+
205+
- name: Log in to ghcr.io
206+
uses: docker/login-action@v4
207+
with:
208+
registry: ghcr.io
209+
username: ${{ github.actor }}
210+
password: ${{ secrets.GITHUB_TOKEN }}
211+
212+
- name: Set up Docker Buildx
213+
uses: docker/setup-buildx-action@v4
214+
with:
215+
driver: docker-container
216+
driver-opts: network=host
217+
218+
- name: Build server image
219+
uses: docker/build-push-action@v7
220+
with:
221+
context: decree
222+
file: decree/build/Dockerfile
223+
load: true
224+
tags: decree-server
225+
cache-from: |
226+
type=registry,ref=ghcr.io/opendecree/decree:buildcache
227+
type=gha,scope=py-integ-server
228+
cache-to: type=gha,scope=py-integ-server,mode=max
229+
230+
- name: Build tools image (for migrations)
231+
uses: docker/build-push-action@v7
232+
with:
233+
context: decree/build
234+
file: decree/build/Dockerfile.tools
235+
load: true
236+
tags: decree-tools
237+
cache-from: |
238+
type=registry,ref=ghcr.io/opendecree/decree-tools:buildcache
239+
type=gha,scope=py-integ-tools
240+
cache-to: type=gha,scope=py-integ-tools,mode=max
241+
242+
- name: Start decree service
243+
run: docker compose -f decree/docker-compose.yml up -d --wait service
244+
env:
245+
SERVICE_IMAGE: decree-server
246+
TOOLS_IMAGE: decree-tools
247+
248+
- name: Run integration tests
249+
run: cd sdk && pytest -m integration -v
250+
env:
251+
DECREE_TEST_ADDR: "localhost:9090"
252+
253+
- name: Tear down services
254+
if: always()
255+
run: docker compose -f decree/docker-compose.yml down -v
256+
165257
check:
166258
name: CI check
167259
if: always()

Makefile

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ DOCKER_RUN_ROOT := docker run --rm -v $(CURDIR):/workspace -v $(CURDIR)/../decre
66
PROTO_DIR := /proto
77
GEN_DIR := sdk/src/opendecree/_generated
88

9-
.PHONY: all generate lint format typecheck test build clean tools docs pre-commit help
9+
.PHONY: all generate lint format typecheck test integration build clean tools docs pre-commit help
1010

1111
all: generate lint typecheck test
1212

@@ -55,6 +55,11 @@ typecheck: $(TOOLS_SENTINEL)
5555
test: $(TOOLS_SENTINEL)
5656
$(DOCKER_RUN_ROOT) sh -c "cd sdk && pip install -e . -q 2>/dev/null && pytest --cov --cov-report=term-missing"
5757

58+
## integration: Run integration tests against a live server (DECREE_TEST_ADDR required)
59+
integration: $(TOOLS_SENTINEL)
60+
@test -n "$(DECREE_TEST_ADDR)" || (echo "Set DECREE_TEST_ADDR=host:port" && exit 1)
61+
$(DOCKER_RUN_ROOT) sh -c "cd sdk && pip install -e . -q 2>/dev/null && DECREE_TEST_ADDR=$(DECREE_TEST_ADDR) pytest -m integration -v"
62+
5863
## docs: Generate API reference HTML from docstrings (pdoc)
5964
docs: $(TOOLS_SENTINEL)
6065
@mkdir -p sdk/docs/api

sdk/pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@ ignore_missing_imports = true
8383
[tool.pytest.ini_options]
8484
testpaths = ["tests"]
8585
asyncio_mode = "auto"
86+
markers = [
87+
"integration: live server required — set DECREE_TEST_ADDR to run",
88+
]
8689

8790
[tool.coverage.run]
8891
source = ["opendecree"]

sdk/tests/conftest.py

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,141 @@
22

33
from __future__ import annotations
44

5+
import os
6+
import uuid
7+
58
import grpc
69
import grpc.aio
10+
import pytest
11+
12+
from opendecree._generated.centralconfig.v1 import (
13+
schema_service_pb2,
14+
schema_service_pb2_grpc,
15+
types_pb2,
16+
)
17+
18+
# ---------------------------------------------------------------------------
19+
# Integration fixtures — skipped unless DECREE_TEST_ADDR is set
20+
# ---------------------------------------------------------------------------
21+
22+
23+
def _integration_addr() -> str | None:
24+
return os.environ.get("DECREE_TEST_ADDR")
25+
26+
27+
def pytest_collection_modifyitems(items: list[pytest.Item]) -> None:
28+
"""Auto-skip integration tests when DECREE_TEST_ADDR is not set."""
29+
if _integration_addr():
30+
return
31+
skip = pytest.mark.skip(reason="DECREE_TEST_ADDR not set")
32+
for item in items:
33+
if item.get_closest_marker("integration"):
34+
item.add_marker(skip)
35+
36+
37+
@pytest.fixture(scope="session")
38+
def decree_addr() -> str:
39+
addr = _integration_addr()
40+
if not addr:
41+
pytest.skip("DECREE_TEST_ADDR not set")
42+
return addr
43+
44+
45+
@pytest.fixture(scope="session")
46+
def grpc_channel(decree_addr: str) -> grpc.Channel:
47+
channel = grpc.insecure_channel(decree_addr)
48+
yield channel
49+
channel.close()
50+
51+
52+
@pytest.fixture(scope="session")
53+
def schema_stub(grpc_channel: grpc.Channel) -> schema_service_pb2_grpc.SchemaServiceStub:
54+
return schema_service_pb2_grpc.SchemaServiceStub(grpc_channel)
55+
56+
57+
def _superadmin_metadata() -> list[tuple[str, str]]:
58+
return [("x-decree-subject", "pytest"), ("x-decree-role", "superadmin")]
59+
60+
61+
@pytest.fixture(scope="session")
62+
def live_schema(
63+
schema_stub: schema_service_pb2_grpc.SchemaServiceStub,
64+
) -> tuple[str, int]:
65+
"""Create + publish a schema; return (schema_id, version).
66+
67+
Cleaned up after the session via DeleteSchema.
68+
"""
69+
meta = _superadmin_metadata()
70+
tag = uuid.uuid4().hex[:8]
71+
resp = schema_stub.CreateSchema(
72+
schema_service_pb2.CreateSchemaRequest(
73+
name=f"pytest-{tag}",
74+
description="Created by pytest integration suite",
75+
fields=[
76+
types_pb2.SchemaField(
77+
path="greeting",
78+
type=types_pb2.FIELD_TYPE_STRING,
79+
),
80+
types_pb2.SchemaField(
81+
path="count",
82+
type=types_pb2.FIELD_TYPE_INT,
83+
),
84+
types_pb2.SchemaField(
85+
path="ratio",
86+
type=types_pb2.FIELD_TYPE_NUMBER,
87+
nullable=True,
88+
),
89+
types_pb2.SchemaField(
90+
path="enabled",
91+
type=types_pb2.FIELD_TYPE_BOOL,
92+
),
93+
],
94+
),
95+
metadata=meta,
96+
)
97+
schema_id: str = resp.schema.id
98+
version: int = resp.schema.current_version
99+
100+
schema_stub.PublishSchema(
101+
schema_service_pb2.PublishSchemaRequest(id=schema_id),
102+
metadata=meta,
103+
)
104+
105+
yield schema_id, version
106+
107+
schema_stub.DeleteSchema(
108+
schema_service_pb2.DeleteSchemaRequest(id=schema_id),
109+
metadata=meta,
110+
)
111+
112+
113+
@pytest.fixture(scope="session")
114+
def live_tenant(
115+
schema_stub: schema_service_pb2_grpc.SchemaServiceStub,
116+
live_schema: tuple[str, int],
117+
) -> str:
118+
"""Create a tenant against the live schema; return tenant_id (name slug).
119+
120+
Cleaned up after the session via DeleteTenant.
121+
"""
122+
meta = _superadmin_metadata()
123+
schema_id, version = live_schema
124+
tag = uuid.uuid4().hex[:8]
125+
name = f"pytest-{tag}"
126+
resp = schema_stub.CreateTenant(
127+
schema_service_pb2.CreateTenantRequest(
128+
name=name,
129+
schema_id=schema_id,
130+
schema_version=version,
131+
),
132+
metadata=meta,
133+
)
134+
tenant_id: str = resp.tenant.id
135+
yield name
136+
schema_stub.DeleteTenant(
137+
schema_service_pb2.DeleteTenantRequest(id=tenant_id),
138+
metadata=meta,
139+
)
7140

8141

9142
class FakeRpcError(grpc.aio.AioRpcError):

0 commit comments

Comments
 (0)