Skip to content

Commit aaf3e10

Browse files
committed
Improve tests
1 parent 829eac9 commit aaf3e10

File tree

4 files changed

+357
-3
lines changed

4 files changed

+357
-3
lines changed

base-6.0.cfg

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,12 @@ environment-vars =
3333
zope_i18n_compile_mo_files true
3434
eggs =
3535
Plone
36+
horse-with-no-namespace
3637
Pillow
3738
imio.smartweb.common
3839
zcml =
3940
imio.smartweb.common
41+
initialization = import horse_with_no_namespace
4042

4143
[vscode]
4244
recipe = collective.recipe.vscode
@@ -62,4 +64,5 @@ scripts =
6264

6365
[versions]
6466
# Don't use a released version of imio.smartweb.common
67+
horse-with-no-namespace = 20250705.0
6568
imio.smartweb.common =

src/imio/smartweb/common/rest/endpoint.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,6 @@ def search(self, query=None):
2525
if not types:
2626
raise Unauthorized("No types found, you are not allowed to search")
2727
self._constrain_query_by_path(query)
28-
if not query.get("path"):
29-
query["path"] = {"query": "/Plone"}
3028
# query = self._parse_query(query)
3129
if query.get("type_of_request") == "count_contents_types":
3230
results = self.count_contents_types(query)
@@ -164,7 +162,6 @@ def check_value_of_field(self, portal_type, field_name, expected_values):
164162
count = counter.get(key, 0)
165163
percent = (count / total) * 100 if total > 0 else 0
166164
result[label] = {"count": count, "percent": round(percent, 2)}
167-
168165
return result
169166

170167

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
# -*- coding: utf-8 -*-
2+
from imio.smartweb.common.rest.endpoint import FindEndpointHandler
3+
from imio.smartweb.common.testing import IMIO_SMARTWEB_COMMON_INTEGRATION_TESTING
4+
from plone import api
5+
from plone.app.testing import setRoles
6+
from plone.app.testing import TEST_USER_ID
7+
from plone.namedfile.file import NamedBlobFile, NamedBlobImage
8+
from unittest.mock import patch
9+
from zExceptions import Unauthorized
10+
11+
import unittest
12+
13+
14+
class TestRestEndpoint(unittest.TestCase):
15+
layer = IMIO_SMARTWEB_COMMON_INTEGRATION_TESTING
16+
17+
def setUp(self):
18+
self.request = self.layer["request"]
19+
self.portal = self.layer["portal"]
20+
21+
setRoles(self.portal, TEST_USER_ID, ["Manager"])
22+
# Simule un header d'auth présent dans la requête
23+
self.request._orig_env = {"HTTP_AUTHORIZATION": "Bearer TEST"}
24+
self.folder = api.content.create(
25+
container=self.portal,
26+
type="Folder",
27+
title="Folder",
28+
)
29+
self.doc1 = api.content.create(
30+
container=self.folder, type="Document", title="Doc 1"
31+
)
32+
self.doc2 = api.content.create(
33+
container=self.folder, type="Document", title="Doc 2"
34+
)
35+
self.page = api.content.create(
36+
container=self.folder, type="Document", title="Doc 3"
37+
)
38+
39+
def test_no_types(self):
40+
query = {
41+
"type_of_request": "count_contents_types",
42+
"portal_type": "Document",
43+
"path": {"query": self.portal.absolute_url_path()},
44+
"operator": "and",
45+
}
46+
handler = FindEndpointHandler(self.portal, self.request)
47+
self.assertRaises(Unauthorized, handler.search, query)
48+
49+
# ------------------------------
50+
# count_contents_types
51+
# ------------------------------
52+
@patch("imio.smartweb.common.rest.endpoint.get_json")
53+
def test_count_contents_types_operator_and(self, mjson):
54+
mjson.return_value = [
55+
{
56+
"@id": f"https://{self.portal.absolute_url()}/@types/Document",
57+
"addable": "false",
58+
"id": "Document",
59+
"immediately_addable": "false",
60+
"title": "Document",
61+
}
62+
]
63+
handler = FindEndpointHandler(self.portal, self.request)
64+
query = {
65+
"type_of_request": "count_contents_types",
66+
"portal_type": "Document",
67+
"path": {"query": self.portal.absolute_url_path()},
68+
"operator": "and",
69+
}
70+
res = handler.search(query)
71+
self.assertEqual(res, {"items": [{"portal_type": "Document", "nb_items": 3}]})
72+
73+
# Test without path in query.
74+
# Path is automaticaly set.
75+
query = {
76+
"type_of_request": "count_contents_types",
77+
"portal_type": "Document",
78+
"operator": "and",
79+
}
80+
res = handler.search(query)
81+
self.assertEqual(res, {"items": [{"portal_type": "Document", "nb_items": 3}]})
82+
83+
@patch("imio.smartweb.common.rest.endpoint.get_json")
84+
def test_count_contents_types_operator_or_multiple_types(self, mjson):
85+
mjson.return_value = [
86+
{
87+
"@id": f"https://{self.portal.absolute_url()}/@types/Document",
88+
"addable": "false",
89+
"id": "Document",
90+
"immediately_addable": "false",
91+
"title": "Document",
92+
},
93+
{
94+
"@id": f"https://{self.portal.absolute_url()}/@types/Folder",
95+
"addable": "false",
96+
"id": "Folder",
97+
"immediately_addable": "false",
98+
"title": "Folder",
99+
},
100+
]
101+
handler = FindEndpointHandler(self.portal, self.request)
102+
query = {
103+
"type_of_request": "count_contents_types",
104+
"portal_type": ["Document", "Folder"],
105+
"path": {"query": self.portal.absolute_url_path()},
106+
"operator": "and",
107+
}
108+
res = handler.search(query)
109+
self.assertEqual(
110+
res, {"items": [{"portal_type": ["Document", "Folder"], "nb_items": 4}]}
111+
)
112+
113+
@patch("imio.smartweb.common.rest.endpoint.get_json")
114+
def test_get_max_depth(self, mjson):
115+
mjson.return_value = [
116+
{
117+
"@id": f"https://{self.portal.absolute_url()}/@types/Document",
118+
"addable": "false",
119+
"id": "Document",
120+
"immediately_addable": "false",
121+
"title": "Document",
122+
}
123+
]
124+
handler = FindEndpointHandler(self.portal, self.request)
125+
res = handler.search({"type_of_request": "get_max_depth"})
126+
self.assertEqual(res["max_depth"], 3)
127+
128+
paths = {i["path"] for i in res["items"]}
129+
self.assertEqual(
130+
paths, {"/plone/folder/doc-2", "/plone/folder/doc-3", "/plone/folder/doc-1"}
131+
)
132+
133+
folder2 = api.content.create(
134+
container=self.folder,
135+
type="Folder",
136+
title="Folder 2",
137+
)
138+
139+
api.content.create(
140+
container=folder2,
141+
type="Document",
142+
title="Kamoulox",
143+
)
144+
handler = FindEndpointHandler(self.portal, self.request)
145+
res = handler.search({"type_of_request": "get_max_depth"})
146+
self.assertEqual(res["max_depth"], 4)
147+
148+
paths = {i["path"] for i in res["items"]}
149+
self.assertEqual(paths, {"/plone/folder/folder-2/kamoulox"})
150+
151+
@patch("imio.smartweb.common.rest.endpoint.get_json")
152+
def test_check_value_of_field_counts_and_percents(self, mjson):
153+
mjson.return_value = [
154+
{
155+
"@id": f"https://{self.portal.absolute_url()}/@types/Document",
156+
"addable": "false",
157+
"id": "Document",
158+
"immediately_addable": "false",
159+
"title": "Document",
160+
}
161+
]
162+
api.content.create(
163+
container=self.folder,
164+
type="Document",
165+
title="Doc 2",
166+
)
167+
handler = FindEndpointHandler(self.portal, self.request)
168+
q = {
169+
"type_of_request": "check_value_of_field",
170+
"portal_type": "Document",
171+
"field_name": "title",
172+
"expected_values": ["Doc 1", "Doc 2", "Doc 3"],
173+
}
174+
res = handler.search(q)
175+
# Number
176+
self.assertEqual(res["Doc 1"]["count"], 1)
177+
self.assertEqual(res["Doc 2"]["count"], 2)
178+
self.assertEqual(res["Doc 3"]["count"], 1)
179+
180+
# Pourcentages
181+
self.assertEqual(res["Doc 1"]["percent"], round(1 / 4 * 100, 2)) # 25%
182+
self.assertEqual(res["Doc 2"]["percent"], round(2 / 4 * 100, 2)) # 50%
183+
self.assertEqual(res["Doc 3"]["percent"], round(1 / 4 * 100, 2)) # 25%
184+
185+
@patch("imio.smartweb.common.rest.endpoint.get_json")
186+
def test_find_big_files_or_images(self, mjson):
187+
mjson.return_value = [
188+
{
189+
"@id": f"https://{self.portal.absolute_url()}/@types/Image",
190+
"addable": "false",
191+
"id": "Image",
192+
"immediately_addable": "false",
193+
"title": "Image",
194+
},
195+
{
196+
"@id": f"https://{self.portal.absolute_url()}/@types/File",
197+
"addable": "false",
198+
"id": "File",
199+
"immediately_addable": "false",
200+
"title": "File",
201+
},
202+
]
203+
api.content.create(
204+
container=self.folder,
205+
type="File",
206+
id="bigfile",
207+
title="Big file",
208+
file=NamedBlobFile(data=b"x" * 2500000, filename="big.pdf"), # 2,5 Mo
209+
)
210+
211+
api.content.create(
212+
container=self.folder,
213+
type="Image",
214+
id="smallimage",
215+
title="Small image",
216+
image=NamedBlobImage(data=b"x" * 100000, filename="small.jpg"), # 0,1 Mo
217+
)
218+
219+
api.content.create(
220+
container=self.folder,
221+
type="Image",
222+
id="bigimage",
223+
title="Big image",
224+
image=NamedBlobImage(data=b"x" * 3100000, filename="big.jpg"), # 3,1 Mo
225+
)
226+
handler = FindEndpointHandler(self.portal, self.request)
227+
q = {
228+
"type_of_request": "find_big_files_or_images",
229+
"portal_type": ["Image", "File"],
230+
"size": 1000000,
231+
}
232+
res = handler.search(q)
233+
# On attend seulement les gros
234+
titles = [it["title"] for it in res["items"]]
235+
self.assertEqual(set(titles), {"Big file", "Big image"})
236+
# Vérifie calcul Mo arrondi (2_500_000 bytes ≈ 2.38 Mo)
237+
big_entry = next(i for i in res["items"] if i["title"] == "Big file")
238+
self.assertAlmostEqual(big_entry["size"], round(2_500_000 / (1024 * 1024), 2))
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
# -*- coding: utf-8 -*-
2+
3+
from imio.smartweb.common.testing import IMIO_SMARTWEB_COMMON_ACCEPTANCE_TESTING
4+
from imio.smartweb.common.rest.utils import batch_results
5+
from imio.smartweb.common.rest.utils import get_json
6+
from imio.smartweb.common.rest.utils import hash_md5
7+
from unittest.mock import patch
8+
from unittest.mock import Mock
9+
10+
import json
11+
import requests
12+
import unittest
13+
14+
15+
class TestRestUtils(unittest.TestCase):
16+
layer = IMIO_SMARTWEB_COMMON_ACCEPTANCE_TESTING
17+
18+
def setUp(self):
19+
self.request = self.layer["request"]
20+
self.portal = self.layer["portal"]
21+
self.portal_url = self.portal.absolute_url()
22+
self.url = "https://api.kamoulox.test/endpoint"
23+
24+
@patch("imio.smartweb.common.rest.utils.requests.get")
25+
def test_get_json_success(self, mock_get):
26+
payload = {"ok": True, "items": [1, 2]}
27+
mock_get.return_value = Mock(status_code=200, text=json.dumps(payload))
28+
result = get_json(self.url)
29+
self.assertEqual(result, payload)
30+
mock_get.assert_called_once()
31+
(called_url,) = mock_get.call_args[0]
32+
self.assertEqual(called_url, self.url)
33+
kwargs = mock_get.call_args.kwargs
34+
self.assertEqual(kwargs["timeout"], 5)
35+
self.assertEqual(kwargs["headers"]["Accept"], "application/json")
36+
self.assertNotIn("Authorization", kwargs["headers"])
37+
38+
@patch("imio.smartweb.common.rest.utils.requests.get")
39+
def test_get_json_non_200_returns_none(self, mock_get):
40+
mock_get.return_value = Mock(status_code=404, text="Not found")
41+
result = get_json(self.url)
42+
self.assertIsNone(result)
43+
44+
@patch("imio.smartweb.common.rest.utils.logger")
45+
@patch("imio.smartweb.common.rest.utils.requests.get")
46+
def test_get_json_timeout_returns_none_and_logs(self, mock_get, mock_logger):
47+
mock_get.side_effect = requests.exceptions.Timeout()
48+
result = get_json(self.url)
49+
self.assertIsNone(result)
50+
mock_logger.warning.assert_called_once()
51+
# check if url is in log
52+
self.assertIn(self.url, mock_logger.warning.call_args[0][0])
53+
54+
@patch("imio.smartweb.common.rest.utils.requests.get")
55+
def test_get_json_other_exception_returns_none(self, mock_get):
56+
mock_get.side_effect = RuntimeError("kamoulox")
57+
result = get_json(self.url)
58+
self.assertIsNone(result)
59+
60+
@patch("imio.smartweb.common.rest.utils.requests.get")
61+
def test_get_json_sets_auth_header_when_provided(self, mock_get):
62+
mock_get.return_value = Mock(status_code=200, text="{}")
63+
auth = "Bearer TOKEN123"
64+
_ = get_json(self.url, auth=auth, timeout=10)
65+
mock_get.assert_called_once()
66+
kwargs = mock_get.call_args.kwargs
67+
self.assertEqual(kwargs["timeout"], 10)
68+
self.assertEqual(kwargs["headers"]["Authorization"], auth)
69+
self.assertEqual(kwargs["headers"]["Accept"], "application/json")
70+
71+
@patch("imio.smartweb.common.rest.utils.requests.get")
72+
def test_get_json_custom_timeout_is_used(self, mock_get):
73+
mock_get.return_value = Mock(status_code=200, text="{}")
74+
_ = get_json(self.url, timeout=2.5)
75+
self.assertEqual(mock_get.call_args.kwargs["timeout"], 2.5)
76+
77+
def test_batch_results_exact_division(self):
78+
data = [1, 2, 3, 4]
79+
result = batch_results(data, 2)
80+
self.assertEqual(result, [[1, 2], [3, 4]])
81+
82+
def test_batch_results_not_exact(self):
83+
data = [1, 2, 3, 4, 5]
84+
result = batch_results(data, 2)
85+
self.assertEqual(result, [[1, 2], [3, 4], [5]])
86+
87+
def test_batch_results_batch_size_larger_than_list(self):
88+
data = [1, 2, 3]
89+
result = batch_results(data, 10)
90+
self.assertEqual(result, [[1, 2, 3]])
91+
92+
def test_batch_results_empty_iterable(self):
93+
data = []
94+
result = batch_results(data, 3)
95+
self.assertEqual(result, [])
96+
97+
def test_batch_results_with_generator(self):
98+
gen = (i for i in range(5))
99+
result = batch_results(gen, 2)
100+
self.assertEqual(result, [[0, 1], [2, 3], [4]])
101+
102+
def test_hash_md5_basic_string(self):
103+
result = hash_md5("hello")
104+
self.assertEqual(result, "5d41402abc4b2a76b9719d911017c592")
105+
106+
def test_hash_md5_empty_string(self):
107+
result = hash_md5("")
108+
self.assertEqual(result, "d41d8cd98f00b204e9800998ecf8427e")
109+
110+
def test_hash_md5_unicode_string(self):
111+
result = hash_md5("éèà")
112+
# tu peux vérifier avec hashlib directement
113+
import hashlib
114+
115+
expected = hashlib.md5("éèà".encode()).hexdigest()
116+
self.assertEqual(result, expected)

0 commit comments

Comments
 (0)