Skip to content

Commit 4f1e276

Browse files
Add unit tests for policy_set
1 parent c88c85d commit 4f1e276

1 file changed

Lines changed: 327 additions & 0 deletions

File tree

tests/units/test_policy_set.py

Lines changed: 327 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,327 @@
1+
"""Unit tests for the PolicySets resource."""
2+
3+
from unittest.mock import Mock
4+
5+
import pytest
6+
7+
from pytfe._http import HTTPTransport
8+
from pytfe.errors import (
9+
InvalidNameError,
10+
InvalidOrgError,
11+
InvalidPolicySetIDError,
12+
RequiredNameError,
13+
)
14+
from pytfe.models.policy_set import (
15+
PolicySet,
16+
PolicySetCreateOptions,
17+
PolicySetListOptions,
18+
PolicySetReadOptions,
19+
PolicySetUpdateOptions,
20+
)
21+
from pytfe.models.policy_types import PolicyKind
22+
from pytfe.resources.policy_set import PolicySets
23+
24+
25+
class TestPolicySets:
26+
"""Test the PolicySets service class."""
27+
28+
@pytest.fixture
29+
def mock_transport(self):
30+
"""Create a mock HTTPTransport."""
31+
return Mock(spec=HTTPTransport)
32+
33+
@pytest.fixture
34+
def service(self, mock_transport):
35+
"""Create a PolicySets service with mocked transport."""
36+
return PolicySets(mock_transport)
37+
38+
# ──────────────────────────────────────────────────────────────────────────
39+
# Helpers
40+
# ──────────────────────────────────────────────────────────────────────────
41+
42+
@staticmethod
43+
def _policy_set_data(
44+
ps_id: str = "ps-abc123",
45+
name: str = "example-policy-set",
46+
kind: str = "sentinel",
47+
) -> dict:
48+
"""Minimal JSON:API policy-set dict as returned by the API."""
49+
return {
50+
"id": ps_id,
51+
"type": "policy-sets",
52+
"attributes": {
53+
"name": name,
54+
"description": "A test policy set",
55+
"kind": kind,
56+
"global": False,
57+
"overridable": False,
58+
"agent-enabled": False,
59+
"policy-count": 0,
60+
"workspace-count": 0,
61+
"project-count": 0,
62+
"policy-tool-version": None,
63+
"policies-path": None,
64+
"created-at": "2024-01-01T00:00:00Z",
65+
"updated-at": "2024-01-01T00:00:00Z",
66+
},
67+
"relationships": {
68+
"organization": {"data": {"id": "org-test", "type": "organizations"}},
69+
"workspaces": {"data": []},
70+
"projects": {"data": []},
71+
"policies": {"data": []},
72+
"workspace-exclusions": {"data": []},
73+
},
74+
}
75+
76+
# ──────────────────────────────────────────────────────────────────────────
77+
# list()
78+
# ──────────────────────────────────────────────────────────────────────────
79+
80+
def test_list_invalid_org_empty_string(self, service):
81+
"""list() raises InvalidOrgError for an empty organization."""
82+
with pytest.raises(InvalidOrgError):
83+
list(service.list(""))
84+
85+
def test_list_invalid_org_none(self, service):
86+
"""list() raises InvalidOrgError for None organization."""
87+
with pytest.raises(InvalidOrgError):
88+
list(service.list(None))
89+
90+
def test_list_returns_iterator_of_policy_sets(self, service):
91+
"""list() returns an iterator that yields PolicySet objects."""
92+
raw = [
93+
self._policy_set_data("ps-1", "ps-one"),
94+
self._policy_set_data("ps-2", "ps-two"),
95+
]
96+
service._list = Mock(return_value=raw)
97+
98+
result = list(service.list("my-org"))
99+
100+
assert len(result) == 2
101+
assert all(isinstance(ps, PolicySet) for ps in result)
102+
assert result[0].id == "ps-1"
103+
assert result[0].name == "ps-one"
104+
assert result[1].id == "ps-2"
105+
assert result[1].name == "ps-two"
106+
107+
def test_list_hits_correct_endpoint(self, service):
108+
"""list() calls _list with the correct path."""
109+
service._list = Mock(return_value=[])
110+
111+
list(service.list("my-org"))
112+
113+
service._list.assert_called_once()
114+
call_path = service._list.call_args[0][0]
115+
assert call_path == "/api/v2/organizations/my-org/policy-sets"
116+
117+
def test_list_with_search_option_passes_param(self, service):
118+
"""list() with a search option passes the correct params to _list."""
119+
service._list = Mock(return_value=[])
120+
options = PolicySetListOptions(search="my-prefix")
121+
122+
list(service.list("my-org", options))
123+
124+
service._list.assert_called_once()
125+
call_kwargs = service._list.call_args[1]
126+
assert call_kwargs.get("params", {}).get("search[name]") == "my-prefix"
127+
128+
def test_list_with_kind_filter(self, service):
129+
"""list() with a kind filter passes filter[kind] param."""
130+
service._list = Mock(return_value=[])
131+
options = PolicySetListOptions(kind=PolicyKind.OPA)
132+
133+
list(service.list("my-org", options))
134+
135+
service._list.assert_called_once()
136+
params = service._list.call_args[1].get("params", {})
137+
assert params.get("filter[kind]") == PolicyKind.OPA
138+
139+
def test_list_page_number_stripped_from_params(self, service):
140+
"""list() strips page[number] from params so _list handles pagination."""
141+
service._list = Mock(return_value=[])
142+
options = PolicySetListOptions(page_number=3, page_size=20)
143+
144+
list(service.list("my-org", options))
145+
146+
params = service._list.call_args[1].get("params", {})
147+
assert "page[number]" not in params
148+
assert params.get("page[size]") == 20
149+
150+
# ──────────────────────────────────────────────────────────────────────────
151+
# read()
152+
# ──────────────────────────────────────────────────────────────────────────
153+
154+
def test_read_invalid_id(self, service):
155+
"""read() raises InvalidPolicySetIDError for an invalid ID."""
156+
with pytest.raises(InvalidPolicySetIDError):
157+
service.read("")
158+
159+
with pytest.raises(InvalidPolicySetIDError):
160+
service.read(None)
161+
162+
def test_read_hits_correct_endpoint(self, service, mock_transport):
163+
"""read() calls GET /api/v2/policy-sets/{id}."""
164+
mock_response = Mock()
165+
mock_response.json.return_value = {"data": self._policy_set_data("ps-abc123")}
166+
mock_transport.request.return_value = mock_response
167+
168+
service.read("ps-abc123")
169+
170+
mock_transport.request.assert_called_once_with(
171+
"GET",
172+
"/api/v2/policy-sets/ps-abc123",
173+
params=None,
174+
)
175+
176+
def test_read_returns_policy_set(self, service, mock_transport):
177+
"""read() parses and returns a PolicySet model."""
178+
mock_response = Mock()
179+
mock_response.json.return_value = {
180+
"data": self._policy_set_data("ps-abc123", "my-ps", "sentinel")
181+
}
182+
mock_transport.request.return_value = mock_response
183+
184+
result = service.read("ps-abc123")
185+
186+
assert isinstance(result, PolicySet)
187+
assert result.id == "ps-abc123"
188+
assert result.name == "my-ps"
189+
assert result.kind == PolicyKind.SENTINEL
190+
191+
def test_read_with_options_passes_include_param(self, service, mock_transport):
192+
"""read_with_options() passes include param when provided."""
193+
from pytfe.models.policy_set import PolicySetIncludeOpt
194+
195+
mock_response = Mock()
196+
mock_response.json.return_value = {"data": self._policy_set_data("ps-xyz")}
197+
mock_transport.request.return_value = mock_response
198+
199+
options = PolicySetReadOptions(
200+
include=[PolicySetIncludeOpt.POLICY_SET_POLICIES]
201+
)
202+
service.read_with_options("ps-xyz", options)
203+
204+
call_kwargs = mock_transport.request.call_args[1]
205+
assert call_kwargs.get("params") is not None
206+
207+
# ──────────────────────────────────────────────────────────────────────────
208+
# create()
209+
# ──────────────────────────────────────────────────────────────────────────
210+
211+
def test_create_invalid_org(self, service):
212+
"""create() raises InvalidOrgError for an invalid organization."""
213+
options = PolicySetCreateOptions(name="valid-name")
214+
with pytest.raises(InvalidOrgError):
215+
service.create("", options)
216+
217+
def test_create_missing_name(self, service):
218+
"""create() raises RequiredNameError when name is empty."""
219+
options = PolicySetCreateOptions(name="")
220+
with pytest.raises((RequiredNameError, InvalidNameError)):
221+
service.create("my-org", options)
222+
223+
def test_create_success(self, service, mock_transport):
224+
"""create() POSTs to the correct endpoint and returns a PolicySet."""
225+
mock_response = Mock()
226+
mock_response.json.return_value = {
227+
"data": self._policy_set_data("ps-new", "new-policy-set")
228+
}
229+
mock_transport.request.return_value = mock_response
230+
231+
options = PolicySetCreateOptions(name="new-policy-set")
232+
result = service.create("my-org", options)
233+
234+
mock_transport.request.assert_called_once()
235+
call_args = mock_transport.request.call_args
236+
assert call_args[0][0] == "POST"
237+
assert call_args[0][1] == "/api/v2/organizations/my-org/policy-sets"
238+
239+
assert isinstance(result, PolicySet)
240+
assert result.id == "ps-new"
241+
assert result.name == "new-policy-set"
242+
243+
def test_create_payload_shape(self, service, mock_transport):
244+
"""create() sends a correctly shaped JSON:API payload."""
245+
mock_response = Mock()
246+
mock_response.json.return_value = {"data": self._policy_set_data("ps-123")}
247+
mock_transport.request.return_value = mock_response
248+
249+
options = PolicySetCreateOptions(name="shaped-ps", kind=PolicyKind.OPA)
250+
service.create("my-org", options)
251+
252+
payload = mock_transport.request.call_args[1]["json_body"]
253+
assert "data" in payload
254+
data = payload["data"]
255+
assert data["type"] == "policy-sets"
256+
assert "attributes" in data
257+
assert data["attributes"]["name"] == "shaped-ps"
258+
259+
# ──────────────────────────────────────────────────────────────────────────
260+
# update()
261+
# ──────────────────────────────────────────────────────────────────────────
262+
263+
def test_update_invalid_id(self, service):
264+
"""update() raises InvalidPolicySetIDError for an invalid ID."""
265+
options = PolicySetUpdateOptions(name="new-name")
266+
with pytest.raises(InvalidPolicySetIDError):
267+
service.update("", options)
268+
269+
def test_update_success(self, service, mock_transport):
270+
"""update() PATCHes the correct endpoint and returns a PolicySet."""
271+
mock_response = Mock()
272+
mock_response.json.return_value = {
273+
"data": self._policy_set_data("ps-abc123", "updated-name")
274+
}
275+
mock_transport.request.return_value = mock_response
276+
277+
options = PolicySetUpdateOptions(name="updated-name")
278+
result = service.update("ps-abc123", options)
279+
280+
call_args = mock_transport.request.call_args
281+
assert call_args[0][0] == "PATCH"
282+
assert call_args[0][1] == "/api/v2/policy-sets/ps-abc123"
283+
284+
payload = call_args[1]["json_body"]
285+
assert payload["data"]["type"] == "policy-sets"
286+
assert payload["data"]["id"] == "ps-abc123"
287+
assert payload["data"]["attributes"]["name"] == "updated-name"
288+
289+
assert isinstance(result, PolicySet)
290+
assert result.name == "updated-name"
291+
292+
def test_update_no_attributes_raises(self, service):
293+
"""update() raises ValueError when no attributes are provided."""
294+
options = PolicySetUpdateOptions() # all None
295+
with pytest.raises(ValueError):
296+
service.update("ps-abc123", options)
297+
298+
# ──────────────────────────────────────────────────────────────────────────
299+
# delete()
300+
# ──────────────────────────────────────────────────────────────────────────
301+
302+
def test_delete_invalid_id(self, service):
303+
"""delete() raises InvalidPolicySetIDError for an invalid ID."""
304+
with pytest.raises(InvalidPolicySetIDError):
305+
service.delete("")
306+
307+
with pytest.raises(InvalidPolicySetIDError):
308+
service.delete(None)
309+
310+
def test_delete_hits_correct_endpoint(self, service, mock_transport):
311+
"""delete() calls DELETE /api/v2/policy-sets/{id}."""
312+
mock_transport.request.return_value = Mock()
313+
314+
service.delete("ps-abc123")
315+
316+
mock_transport.request.assert_called_once_with(
317+
"DELETE",
318+
"/api/v2/policy-sets/ps-abc123",
319+
)
320+
321+
def test_delete_returns_none(self, service, mock_transport):
322+
"""delete() returns None on success."""
323+
mock_transport.request.return_value = Mock()
324+
325+
result = service.delete("ps-abc123")
326+
327+
assert result is None

0 commit comments

Comments
 (0)