Skip to content

Commit 096feba

Browse files
authored
Merge pull request #961 from opsmill/develop
Merge develop into infrahub-develop
2 parents c065b3f + e78b9da commit 096feba

12 files changed

Lines changed: 800 additions & 581 deletions

File tree

.claude/rules

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../dev/rules
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
---
2+
paths:
3+
- "tests/integration/**/*.py"
4+
---
5+
6+
# Integration test rules
7+
8+
Integration tests run against a real Infrahub instance via testcontainers. Use them for anything that depends on Infrahub's actual behaviour: node creation, branch operations, schema loading, queries. Do not use `httpx_mock` in integration tests.
9+
10+
## Test class structure
11+
12+
Inherit from `TestInfrahubDockerClient` and mix in a schema class when your tests require a custom schema. Use class-scoped fixtures for dataset setup so Infrahub is only populated once per test class:
13+
14+
```python
15+
class TestInfrahubNode(TestInfrahubDockerClient, SchemaAnimal):
16+
@pytest.fixture(scope="class")
17+
async def base_dataset(
18+
self,
19+
client: InfrahubClient,
20+
load_schema: None,
21+
) -> None:
22+
await client.branch.create(branch_name="branch01")
23+
24+
async def test_query_branches(self, client: InfrahubClient, base_dataset: None) -> None:
25+
branches = await client.branch.all()
26+
assert "main" in branches
27+
```
28+
29+
## Client fixture
30+
31+
The `client` fixture provides an authenticated `InfrahubClient` connected to the testcontainer instance. Do not construct a client manually in integration tests.
32+
33+
## Cleanup
34+
35+
Clean up any nodes, branches, or schema changes created during a test class. Use class-scoped fixtures with `yield` to ensure teardown runs even on failure:
36+
37+
```python
38+
@pytest.fixture(scope="class")
39+
async def created_branch(self, client: InfrahubClient) -> AsyncGenerator[str, None]:
40+
await client.branch.create(branch_name="test-branch")
41+
yield "test-branch"
42+
await client.branch.delete(branch_name="test-branch")
43+
```

dev/rules/python-testing-unit.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
---
2+
paths:
3+
- "tests/unit/**/*.py"
4+
---
5+
6+
# Unit test rules
7+
8+
Unit tests cover pure logic, data transformations, schema parsing, and error handling. Use `httpx_mock` to simulate HTTP responses at the transport boundary. Do not substitute a mocked unit test for behaviour that depends on Infrahub's actual server responses — write an integration test for that.
9+
10+
## HTTP mocking with `httpx_mock`
11+
12+
Add the module-level marker when any fixture or test in the file reuses mocked responses:
13+
14+
```python
15+
pytestmark = pytest.mark.httpx_mock(can_send_already_matched_responses=True)
16+
```
17+
18+
Use `is_reusable=True` on fixtures that serve multiple tests:
19+
20+
```python
21+
@pytest.fixture
22+
async def mock_branch_list(httpx_mock: HTTPXMock) -> HTTPXMock:
23+
httpx_mock.add_response(
24+
method="POST",
25+
json={"data": {"Branch": [...]}},
26+
match_headers={"X-Infrahub-Tracker": "query-branch-all"},
27+
is_reusable=True,
28+
)
29+
return httpx_mock
30+
```
31+
32+
## Testing both async and sync clients
33+
34+
Use the `BothClients` fixture from `tests/unit/sdk/conftest.py` when behaviour must be verified for both client variants. Parametrize over `["standard", "sync"]`:
35+
36+
```python
37+
@pytest.mark.parametrize("client_type", ["standard", "sync"])
38+
async def test_branch_list(clients: BothClients, client_type: str, mock_branch_list: HTTPXMock) -> None:
39+
if client_type == "standard":
40+
branches = await clients.standard.branch.all()
41+
else:
42+
branches = clients.sync.branch.all()
43+
assert list(branches.keys()) == ["main", "branch01"]
44+
```
45+
46+
Assert the actual expected value. Assertions like `assert result is not None` or `assert result` do not verify behaviour — they only confirm something was returned.
47+
48+
## Test file layout
49+
50+
Mirror the source structure:
51+
52+
```text
53+
infrahub_sdk/client.py → tests/unit/sdk/test_client.py
54+
infrahub_sdk/ctl/commands/get.py → tests/unit/ctl/test_get.py
55+
```
56+
57+
## No external dependencies
58+
59+
Unit tests must not connect to external services, local file access is fine. If a test requires a running Infrahub instance, it belongs in `tests/integration/`.

dev/rules/python-testing.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
---
2+
paths:
3+
- "tests/**/*.py"
4+
---
5+
6+
# Python testing rules
7+
8+
## No `unittest.mock`
9+
10+
Do not use `unittest.mock`, `MagicMock`, or `patch`. The only sanctioned mocking tools are:
11+
12+
- `httpx_mock` (pytest-httpx) — for intercepting HTTP calls at the transport layer
13+
- `monkeypatch` — for patching stdlib functions (for example: `ssl.create_default_context`)
14+
15+
## Async tests
16+
17+
`asyncio_mode = "auto"` is configured globally. Do not add `@pytest.mark.asyncio`. Do not add loop scope markers manually — this is handled in `conftest.py`.
18+
19+
```python
20+
# Correct
21+
async def test_client_fetches_branch(httpx_mock: HTTPXMock) -> None:
22+
...
23+
24+
# Wrong — decorator not needed
25+
@pytest.mark.asyncio
26+
async def test_client_fetches_branch(httpx_mock: HTTPXMock) -> None:
27+
...
28+
```
29+
30+
## Parametrized tests
31+
32+
Use a dataclass with `name` as the first field and pass it as the `id` in `pytest.param`. Always use keyword arguments when constructing cases:
33+
34+
```python
35+
@dataclass
36+
class BranchCase:
37+
name: str
38+
branch_name: str
39+
expected_conflict: bool
40+
41+
BRANCH_CASES = [
42+
BranchCase(name="no-conflict", branch_name="feature-x", expected_conflict=False),
43+
BranchCase(name="conflict", branch_name="main", expected_conflict=True),
44+
]
45+
46+
@pytest.mark.parametrize("case", [pytest.param(tc, id=tc.name) for tc in BRANCH_CASES])
47+
async def test_branch_conflict(case: BranchCase) -> None:
48+
...
49+
```
50+
51+
## Exception assertions
52+
53+
Always pass `match=` to `pytest.raises()`:
54+
55+
```python
56+
with pytest.raises(NodeNotFoundError, match="Could not find node with id"):
57+
await client.get(kind="NetworkDevice", id="missing")
58+
```
59+
60+
## Fixtures and helpers
61+
62+
- Shared fixtures live in the nearest `conftest.py` to the tests that use them.
63+
- JSON/YAML test data belongs in `tests/fixtures/` and is loaded via `read_fixture()` from `tests/helpers/fixtures.py`.
64+
- Use `change_directory()` and `temp_repo_and_cd()` from `tests/helpers/utils.py` for filesystem-dependent tests.
65+
- Do not duplicate fixture data inline when a fixture file already exists.
66+
67+
## Naming
68+
69+
Do not reference issue numbers, GitHub URLs, or ticket identifiers in test names or docstrings.

docs/CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@AGENTS.md

docs/docs_generation/helpers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ def get_env_vars() -> dict[str, list[str]]:
1818
settings = ConfigBase()
1919
env_settings = EnvSettingsSource(settings.__class__, env_prefix=settings.model_config.get("env_prefix", ""))
2020

21-
for field_name, field in settings.model_fields.items():
21+
for field_name, field in ConfigBase.model_fields.items():
2222
for field_key, field_env_name, _ in env_settings._extract_field_info(field, field_name):
2323
env_vars[field_key].append(field_env_name.upper())
2424

0 commit comments

Comments
 (0)