Skip to content

Commit fc61aff

Browse files
authored
Merge pull request #183 from opsmill/dga-20241223-testing
Add testing directory based on infrahub_testcontainers
2 parents cb9fe15 + 039a9d9 commit fc61aff

File tree

10 files changed

+1380
-945
lines changed

10 files changed

+1380
-945
lines changed

infrahub_sdk/schema/main.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from enum import Enum
55
from typing import TYPE_CHECKING, Any, Optional, Union
66

7-
from pydantic import BaseModel, Field
7+
from pydantic import BaseModel, ConfigDict, Field
88

99
if TYPE_CHECKING:
1010
from ..node import InfrahubNode, InfrahubNodeSync
@@ -89,6 +89,8 @@ class RelationshipDeleteBehavior(str, Enum):
8989

9090

9191
class AttributeSchema(BaseModel):
92+
model_config = ConfigDict(use_enum_values=True)
93+
9294
id: Optional[str] = None
9395
state: SchemaState = SchemaState.PRESENT
9496
name: str
@@ -108,12 +110,16 @@ class AttributeSchema(BaseModel):
108110

109111

110112
class AttributeSchemaAPI(AttributeSchema):
113+
model_config = ConfigDict(use_enum_values=True)
114+
111115
inherited: bool = False
112116
read_only: bool = False
113117
allow_override: AllowOverrideType = AllowOverrideType.ANY
114118

115119

116120
class RelationshipSchema(BaseModel):
121+
model_config = ConfigDict(use_enum_values=True)
122+
117123
id: Optional[str] = None
118124
state: SchemaState = SchemaState.PRESENT
119125
name: str
@@ -133,6 +139,8 @@ class RelationshipSchema(BaseModel):
133139

134140

135141
class RelationshipSchemaAPI(RelationshipSchema):
142+
model_config = ConfigDict(use_enum_values=True)
143+
136144
inherited: bool = False
137145
read_only: bool = False
138146
hierarchical: Optional[str] = None
@@ -245,6 +253,8 @@ def unique_attributes(self) -> list[AttributeSchemaAPI]:
245253

246254

247255
class BaseSchema(BaseModel):
256+
model_config = ConfigDict(use_enum_values=True)
257+
248258
id: Optional[str] = None
249259
state: SchemaState = SchemaState.PRESENT
250260
name: str
@@ -275,6 +285,8 @@ class GenericSchemaAPI(BaseSchema, BaseSchemaAttrRelAPI):
275285

276286

277287
class BaseNodeSchema(BaseSchema):
288+
model_config = ConfigDict(use_enum_values=True)
289+
278290
inherit_from: list[str] = Field(default_factory=list)
279291
branch: Optional[BranchSupportType] = None
280292
default_filter: Optional[str] = None
@@ -299,6 +311,8 @@ class ProfileSchemaAPI(BaseSchema, BaseSchemaAttrRelAPI):
299311

300312

301313
class NodeExtensionSchema(BaseModel):
314+
model_config = ConfigDict(use_enum_values=True)
315+
302316
name: Optional[str] = None
303317
kind: str
304318
description: Optional[str] = None
@@ -311,13 +325,20 @@ class NodeExtensionSchema(BaseModel):
311325

312326

313327
class SchemaRoot(BaseModel):
328+
model_config = ConfigDict(use_enum_values=True)
329+
314330
version: str
315331
generics: list[GenericSchema] = Field(default_factory=list)
316332
nodes: list[NodeSchema] = Field(default_factory=list)
317333
node_extensions: list[NodeExtensionSchema] = Field(default_factory=list)
318334

335+
def to_schema_dict(self) -> dict[str, Any]:
336+
return self.model_dump(exclude_unset=True, exclude_defaults=True)
337+
319338

320339
class SchemaRootAPI(BaseModel):
340+
model_config = ConfigDict(use_enum_values=True)
341+
321342
version: str
322343
generics: list[GenericSchemaAPI] = Field(default_factory=list)
323344
nodes: list[NodeSchemaAPI] = Field(default_factory=list)

infrahub_sdk/testing/__init__.py

Whitespace-only changes.

infrahub_sdk/testing/docker.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import pytest
2+
from infrahub_testcontainers.helpers import TestInfrahubDocker
3+
4+
from .. import Config, InfrahubClient, InfrahubClientSync
5+
6+
7+
class TestInfrahubDockerClient(TestInfrahubDocker):
8+
@pytest.fixture(scope="class")
9+
def client(self, infrahub_port: int) -> InfrahubClient:
10+
return InfrahubClient(
11+
config=Config(username="admin", password="infrahub", address=f"http://localhost:{infrahub_port}") # noqa: S106
12+
)
13+
14+
@pytest.fixture(scope="class")
15+
def client_sync(self, infrahub_port: int) -> InfrahubClientSync:
16+
return InfrahubClientSync(
17+
config=Config(username="admin", password="infrahub", address=f"http://localhost:{infrahub_port}") # noqa: S106
18+
)

infrahub_sdk/testing/repository.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import asyncio
2+
import shutil
3+
from dataclasses import dataclass, field
4+
from enum import Enum
5+
from pathlib import Path
6+
from typing import Optional
7+
8+
from git.repo import Repo
9+
10+
from infrahub_sdk import InfrahubClient
11+
from infrahub_sdk.graphql import Mutation
12+
from infrahub_sdk.protocols import CoreGenericRepository
13+
14+
15+
# NOTE we shouldn't duplicate this, need to figure out a better solution
16+
class RepositorySyncStatus(str, Enum):
17+
UNKNOWN = "unknown"
18+
IN_SYNC = "in-sync"
19+
ERROR_IMPORT = "error-import"
20+
SYNCING = "syncing"
21+
22+
23+
class GitRepoType(str, Enum):
24+
INTEGRATED = "CoreRepository"
25+
READ_ONLY = "CoreReadOnlyRepository"
26+
27+
28+
@dataclass
29+
class GitRepo:
30+
name: str
31+
src_directory: Path
32+
dst_directory: Path
33+
34+
type: GitRepoType = GitRepoType.INTEGRATED
35+
36+
_repo: Optional[Repo] = None
37+
initial_branch: str = "main"
38+
directories_to_ignore: list[str] = field(default_factory=list)
39+
remote_directory_name: str = "/remote"
40+
_branches: list[str] = field(default_factory=list)
41+
42+
@property
43+
def repo(self) -> Repo:
44+
if self._repo:
45+
return self._repo
46+
raise ValueError("Repo hasn't been initialized yet")
47+
48+
def __post_init__(self) -> None:
49+
self.init()
50+
51+
@property
52+
def path(self) -> str:
53+
return str(self.src_directory / self.name)
54+
55+
def init(self) -> None:
56+
shutil.copytree(
57+
src=self.src_directory,
58+
dst=self.dst_directory / self.name,
59+
ignore=shutil.ignore_patterns(".git"),
60+
)
61+
self._repo = Repo.init(self.dst_directory / self.name, initial_branch=self.initial_branch)
62+
for untracked in self.repo.untracked_files:
63+
self.repo.index.add(untracked)
64+
self.repo.index.commit("First commit")
65+
66+
self.repo.git.checkout(self.initial_branch)
67+
68+
async def add_to_infrahub(self, client: InfrahubClient, branch: Optional[str] = None) -> dict:
69+
input_data = {
70+
"data": {
71+
"name": {"value": self.name},
72+
"location": {"value": f"{self.remote_directory_name}/{self.name}"},
73+
},
74+
}
75+
76+
query = Mutation(
77+
mutation=f"{self.type.value}Create",
78+
input_data=input_data,
79+
query={"ok": None},
80+
)
81+
82+
return await client.execute_graphql(
83+
query=query.render(), branch_name=branch or self.initial_branch, tracker="mutation-repository-create"
84+
)
85+
86+
async def wait_for_sync_to_complete(
87+
self, client: InfrahubClient, branch: Optional[str] = None, interval: int = 5, retries: int = 6
88+
) -> bool:
89+
for _ in range(retries):
90+
repo = await client.get(
91+
kind=CoreGenericRepository, # type: ignore[type-abstract]
92+
name__value=self.name,
93+
branch=branch or self.initial_branch,
94+
)
95+
status = repo.sync_status.value
96+
if status == RepositorySyncStatus.IN_SYNC.value:
97+
return True
98+
if status == RepositorySyncStatus.ERROR_IMPORT.value:
99+
return False
100+
101+
await asyncio.sleep(interval)
102+
103+
return False

infrahub_sdk/testing/schemas/__init__.py

Whitespace-only changes.
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import pytest
2+
3+
from infrahub_sdk import InfrahubClient
4+
from infrahub_sdk.node import InfrahubNode
5+
from infrahub_sdk.schema.main import AttributeKind, NodeSchema, RelationshipKind, SchemaRoot
6+
from infrahub_sdk.schema.main import AttributeSchema as Attr
7+
from infrahub_sdk.schema.main import RelationshipSchema as Rel
8+
9+
NAMESPACE = "Testing"
10+
11+
TESTING_MANUFACTURER = f"{NAMESPACE}Manufacturer"
12+
TESTING_PERSON = f"{NAMESPACE}Person"
13+
TESTING_CAR = f"{NAMESPACE}Car"
14+
15+
16+
class SchemaCarPerson:
17+
@pytest.fixture(scope="class")
18+
def schema_person_base(self) -> NodeSchema:
19+
return NodeSchema(
20+
name="Person",
21+
namespace=NAMESPACE,
22+
include_in_menu=True,
23+
label="Person",
24+
human_friendly_id=["name__value"],
25+
attributes=[
26+
Attr(name="name", kind=AttributeKind.TEXT, unique=True),
27+
Attr(name="description", kind=AttributeKind.TEXT, optional=True),
28+
Attr(name="height", kind=AttributeKind.NUMBER, optional=True),
29+
Attr(name="age", kind=AttributeKind.NUMBER, optional=True),
30+
],
31+
relationships=[
32+
Rel(name="cars", kind=RelationshipKind.GENERIC, optional=True, peer=TESTING_CAR, cardinality="many")
33+
],
34+
)
35+
36+
@pytest.fixture(scope="class")
37+
def schema_car_base(self) -> NodeSchema:
38+
return NodeSchema(
39+
name="Car",
40+
namespace=NAMESPACE,
41+
include_in_menu=True,
42+
default_filter="name__value",
43+
human_friendly_id=["owner__name__value", "name__value"],
44+
label="Car",
45+
attributes=[
46+
Attr(name="name", kind=AttributeKind.TEXT),
47+
Attr(name="description", kind=AttributeKind.TEXT, optional=True),
48+
Attr(name="color", kind=AttributeKind.TEXT),
49+
],
50+
relationships=[
51+
Rel(
52+
name="owner",
53+
kind=RelationshipKind.ATTRIBUTE,
54+
optional=False,
55+
peer=TESTING_PERSON,
56+
cardinality="one",
57+
),
58+
Rel(
59+
name="manufacturer",
60+
kind=RelationshipKind.ATTRIBUTE,
61+
optional=False,
62+
peer=TESTING_MANUFACTURER,
63+
cardinality="one",
64+
identifier="car__manufacturer",
65+
),
66+
],
67+
)
68+
69+
@pytest.fixture(scope="class")
70+
def schema_manufacturer_base(self) -> NodeSchema:
71+
return NodeSchema(
72+
name="Manufacturer",
73+
namespace=NAMESPACE,
74+
include_in_menu=True,
75+
label="Manufacturer",
76+
human_friendly_id=["name__value"],
77+
attributes=[
78+
Attr(name="name", kind=AttributeKind.TEXT),
79+
Attr(name="description", kind=AttributeKind.TEXT, optional=True),
80+
],
81+
relationships=[
82+
Rel(
83+
name="cars",
84+
kind=RelationshipKind.GENERIC,
85+
optional=True,
86+
peer=TESTING_CAR,
87+
cardinality="many",
88+
identifier="car__manufacturer",
89+
),
90+
Rel(
91+
name="customers",
92+
kind=RelationshipKind.GENERIC,
93+
optional=True,
94+
peer=TESTING_PERSON,
95+
cardinality="many",
96+
identifier="person__manufacturer",
97+
),
98+
],
99+
)
100+
101+
@pytest.fixture(scope="class")
102+
def schema_base(
103+
self,
104+
schema_car_base: NodeSchema,
105+
schema_person_base: NodeSchema,
106+
schema_manufacturer_base: NodeSchema,
107+
) -> SchemaRoot:
108+
return SchemaRoot(version="1.0", nodes=[schema_car_base, schema_person_base, schema_manufacturer_base])
109+
110+
async def create_persons(self, client: InfrahubClient, branch: str) -> list[InfrahubNode]:
111+
john = await client.create(kind=TESTING_PERSON, name="John Doe", branch=branch)
112+
await john.save()
113+
114+
jane = await client.create(kind=TESTING_PERSON, name="Jane Doe", branch=branch)
115+
await jane.save()
116+
117+
return [john, jane]
118+
119+
async def create_manufacturers(self, client: InfrahubClient, branch: str) -> list[InfrahubNode]:
120+
obj1 = await client.create(kind=TESTING_MANUFACTURER, name="Volkswagen", branch=branch)
121+
await obj1.save()
122+
123+
obj2 = await client.create(kind=TESTING_MANUFACTURER, name="Renault", branch=branch)
124+
await obj2.save()
125+
126+
obj3 = await client.create(kind=TESTING_MANUFACTURER, name="Mercedes", branch=branch)
127+
await obj3.save()
128+
129+
return [obj1, obj2, obj3]
130+
131+
async def create_initial_data(self, client: InfrahubClient, branch: str) -> dict[str, list[InfrahubNode]]:
132+
persons = await self.create_persons(client=client, branch=branch)
133+
manufacturers = await self.create_manufacturers(client=client, branch=branch)
134+
135+
car10 = await client.create(
136+
kind=TESTING_CAR, name="Golf", color="Black", manufacturer=manufacturers[0].id, owner=persons[0].id
137+
)
138+
await car10.save()
139+
140+
car20 = await client.create(
141+
kind=TESTING_CAR, name="Megane", color="Red", manufacturer=manufacturers[1].id, owner=persons[1].id
142+
)
143+
await car20.save()
144+
145+
return {TESTING_PERSON: persons, TESTING_CAR: [car10, car20], TESTING_MANUFACTURER: manufacturers}

0 commit comments

Comments
 (0)