Skip to content

Commit 8b3bf59

Browse files
authored
Automate API tests (#1028)
1 parent a203023 commit 8b3bf59

File tree

6 files changed

+1742
-20
lines changed

6 files changed

+1742
-20
lines changed

.github/resources/.prettierignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,5 @@
1212
**/CMakeUserPresets.json
1313
**/CMakePresets.json
1414
**/CMakeFiles/
15+
16+
**/tests/api/README.md

scene_common/src/scene_common/rest_client.py

Lines changed: 85 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,53 @@
1-
# SPDX-FileCopyrightText: (C) 2023 - 2025 Intel Corporation
1+
# SPDX-FileCopyrightText: (C) 2023 - 2026 Intel Corporation
22
# SPDX-License-Identifier: Apache-2.0
33

44
import os
55
import json
66
import re
77
import requests
8-
import sys
98
from http import HTTPStatus
109
from urllib.parse import urljoin
1110

11+
1212
class RESTResult(dict):
1313
def __init__(self, statusCode, errors=None):
1414
super().__init__()
1515
self.statusCode = statusCode
1616
self.errors = errors
1717
return
1818

19+
@property
20+
def status_code(self):
21+
return self.statusCode
22+
23+
def json(self):
24+
return dict(self)
25+
26+
@property
27+
def text(self):
28+
return json.dumps(dict(self))
29+
30+
1931
class RESTClient:
20-
def __init__(self, url, rootcert=None, auth=None):
32+
def __init__(self, url=None, token=None, auth=None,
33+
rootcert=None, verify_ssl=False, timeout=10):
2134
self.url = url
22-
self.rootcert = rootcert
23-
if not self.url.endswith("/"):
35+
36+
if self.url and not self.url.endswith("/"):
2437
self.url = self.url + "/"
38+
39+
# Handle SSL verification (support both bool and path)
40+
self.verify_ssl = verify_ssl if verify_ssl is not False else False
41+
if rootcert:
42+
self.verify_ssl = rootcert
43+
44+
self.timeout = timeout
2545
self.session = requests.session()
26-
if auth:
46+
47+
# If token provided directly, use it (skip authentication)
48+
if token:
49+
self.token = token
50+
elif auth:
2751
self._parseAuth(auth)
2852
return
2953

@@ -46,10 +70,10 @@ def _parseAuth(self, auth):
4670
res = self.authenticate(user, pw)
4771
if not res:
4872
error_message = (
49-
f"Failed to authenticate\n"
50-
f" URL: {self.url}\n"
51-
f" status: {res.statusCode}\n"
52-
f" errors: {res.errors}"
73+
f"Failed to authenticate\n"
74+
f" URL: {self.url}\n"
75+
f" status: {res.statusCode}\n"
76+
f" errors: {res.errors}"
5377
)
5478
raise RuntimeError(error_message)
5579
return
@@ -58,6 +82,38 @@ def _parseAuth(self, auth):
5882
def isAuthenticated(self):
5983
return hasattr(self, 'token') and self.token is not None
6084

85+
def _headers(self):
86+
headers = {
87+
"Content-Type": "application/json"
88+
}
89+
if hasattr(self, 'token') and self.token:
90+
headers["Authorization"] = f"Token {self.token}"
91+
return headers
92+
93+
def request(self, method, path, **kwargs):
94+
"""
95+
Returns raw requests.Response object for compatibility with API tests
96+
"""
97+
# Ensure path starts with /
98+
if not path.startswith('/'):
99+
path = '/' + path
100+
101+
url = urljoin(self.url, path.lstrip('/'))
102+
103+
# Merge headers
104+
headers = self._headers()
105+
if 'headers' in kwargs:
106+
headers.update(kwargs.pop('headers'))
107+
108+
return self.session.request(
109+
method=method,
110+
url=url,
111+
headers=headers,
112+
verify=self.verify_ssl,
113+
timeout=self.timeout,
114+
**kwargs
115+
)
116+
61117
def decodeReply(self, reply, expectedStatus, successContent=None):
62118
result = RESTResult(statusCode=reply.status_code)
63119
decoded = False
@@ -69,10 +125,11 @@ def decodeReply(self, reply, expectedStatus, successContent=None):
69125
content = reply.content
70126
else:
71127
content = {
72-
'data': reply.content,
128+
'data': reply.content,
73129
}
74130
if 'Content-Disposition' in reply.headers:
75-
fname = re.findall("filename=(.+)", reply.headers['Content-Disposition'])[0]
131+
fname = re.findall("filename=(.+)",
132+
reply.headers['Content-Disposition'])[0]
76133
content['filename'] = fname
77134
decoded = True
78135

@@ -99,11 +156,15 @@ def authenticate(self, user, password):
99156
auth_url = urljoin(self.url, "auth")
100157
try:
101158
reply = self.session.post(auth_url, data={'username': user, 'password': password},
102-
verify=self.rootcert)
159+
verify=self.verify_ssl)
103160
except requests.exceptions.ConnectionError as err:
104-
result = RESTResult("ConnectionError", errors=("Connection error", str(err)))
161+
result = RESTResult(
162+
"ConnectionError", errors=(
163+
"Connection error", str(err)))
105164
else:
106-
result = self.decodeReply(reply, HTTPStatus.OK, successContent={'authenticated': True})
165+
result = self.decodeReply(
166+
reply, HTTPStatus.OK, successContent={
167+
'authenticated': True})
107168
if reply.status_code == HTTPStatus.OK:
108169
data = json.loads(reply.content)
109170
self.token = data['token']
@@ -120,7 +181,8 @@ def prepareDataArgs(self, data, files):
120181
if not files:
121182
data_args = {'json': data}
122183
elif self.dataIsNested(data):
123-
raise ValueError("requests library can't combine files and nested dictionaries")
184+
raise ValueError(
185+
"requests library can't combine files and nested dictionaries")
124186
return data_args
125187

126188
def _create(self, endpoint, data, files=None):
@@ -137,7 +199,7 @@ def _create(self, endpoint, data, files=None):
137199
headers = {'Authorization': f"Token {self.token}"}
138200
data_args = self.prepareDataArgs(data, files)
139201
reply = self.session.post(full_path, **data_args, files=files,
140-
headers=headers, verify=self.rootcert)
202+
headers=headers, verify=self.verify_ssl)
141203
return self.decodeReply(reply, HTTPStatus.CREATED)
142204

143205
def _get(self, endpoint, parameters):
@@ -152,7 +214,7 @@ def _get(self, endpoint, parameters):
152214
full_path = urljoin(self.url, endpoint)
153215
headers = {'Authorization': f"Token {self.token}"}
154216
reply = self.session.get(full_path, params=parameters, headers=headers,
155-
verify=self.rootcert)
217+
verify=self.verify_ssl)
156218
return self.decodeReply(reply, HTTPStatus.OK)
157219

158220
def _update(self, endpoint, data, files=None):
@@ -169,7 +231,7 @@ def _update(self, endpoint, data, files=None):
169231
headers = {'Authorization': f"Token {self.token}"}
170232
data_args = self.prepareDataArgs(data, files)
171233
reply = self.session.post(full_path, **data_args, files=files,
172-
headers=headers, verify=self.rootcert)
234+
headers=headers, verify=self.verify_ssl)
173235
return self.decodeReply(reply, HTTPStatus.OK)
174236

175237
def _delete(self, endpoint):
@@ -181,7 +243,10 @@ def _delete(self, endpoint):
181243
"""
182244
full_path = urljoin(self.url, endpoint)
183245
headers = {'Authorization': f"Token {self.token}"}
184-
reply = self.session.delete(full_path, headers=headers, verify=self.rootcert)
246+
reply = self.session.delete(
247+
full_path,
248+
headers=headers,
249+
verify=self.verify_ssl)
185250
return self.decodeReply(reply, HTTPStatus.OK)
186251

187252
def _separateFiles(self, data, fields):

0 commit comments

Comments
 (0)