Skip to content

Commit 70b20f9

Browse files
authored
Merge pull request #8 from Open-MBEE/develop
Develop
2 parents 91395bb + 319256b commit 70b20f9

File tree

5 files changed

+216
-14
lines changed

5 files changed

+216
-14
lines changed

.github/workflows/sonarqube.yml

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ jobs:
3434
- name: Install dependencies
3535
run: |
3636
python -m pip install --upgrade pip
37-
pip install requests
37+
pip install requests requests_mock
3838
pip install pytest pytest-cov pytest-check coverage
3939
pip install -e .
4040
@@ -44,6 +44,10 @@ jobs:
4444
distribution: temurin
4545
java-version: '21'
4646

47+
- name: Run tests with coverage
48+
run: |
49+
pytest --cov=src/sysmlv2_client --cov-report=xml:coverage.xml
50+
4751
- name: SonarQube Scan
4852
uses: SonarSource/sonarqube-scan-action@v6
4953
with:
@@ -53,8 +57,3 @@ jobs:
5357
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
5458
SONAR_SCANNER_SKIP_JRE_PROVISIONING: "true"
5559

56-
- name: Run tests with coverage
57-
run: |
58-
coverage run -m pytest
59-
coverage xml -o coverage.xml
60-

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,4 @@ flexo-setup/docker-compose/env/*.env
2525
/build
2626
/src/sysmlv2_python_client.egg-info
2727
/test-results
28+
coverage.xml

sonar-project.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
sonar.projectKey=sysmlv2-python-client
1+
sonar.projectKey=Open-MBEE_sysmlv2-python-client
22
sonar.organization=openmbee
33
sonar.host.url=https://sonarcloud.io
44

src/sysmlv2_client/client.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -126,21 +126,30 @@ def get_owned_elements(self, project_id: str, element_id: str, commit_id: str =
126126
else:
127127
return []
128128

129-
def create_commit(self, project_id: str, commit_data: Dict[str, Any], branch_id:str = None) -> Dict[str, Any]:
130-
if branch_id is None:
131-
# this takes the default branch
132-
endpoint = f"/projects/{project_id}/commits"
133-
else:
134-
endpoint = f"/projects/{project_id}/commits?branchId={branch_id}"
129+
def create_commit(self, project_id: str, commit_data: Dict[str, Any], branch_id:str = None, replace:bool = False) -> Dict[str, Any]:
130+
params = []
131+
132+
if replace:
133+
params.append("replace=true")
134+
135+
if branch_id is not None:
136+
params.append(f"branchId={branch_id}")
137+
138+
endpoint = f"/projects/{project_id}/commits"
139+
if params:
140+
endpoint += "?" + "&".join(params)
141+
135142
#print (">>> DEBUG create_commit")
136143
#print (endpoint)
137144
#print (commit_data)
145+
138146
return self._request(
139147
method="POST",
140148
endpoint=endpoint,
141149
data=commit_data,
142150
expected_status=200
143151
)
152+
144153
def get_commit_by_id(self, project_id: str, commit_id: str) -> Dict[str, Any]:
145154
endpoint = f"/projects/{project_id}/commits/{commit_id}"
146155
return self._request(method="GET", endpoint=endpoint, expected_status=200)

tests/test_client.py

Lines changed: 194 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import json
2+
13
import pytest
24
import requests_mock
35
import requests
@@ -93,6 +95,14 @@ def test_get_projects_success_no_elements_key(client, requests_mock):
9395

9496
assert projects == mock_response_data
9597

98+
def test_get_projects_success_unexpected_scalar_response(client, monkeypatch):
99+
"""Tests retrieving projects when the API returns a scalar value."""
100+
monkeypatch.setattr(client, "_request", lambda **kwargs: "unexpected")
101+
102+
projects = client.get_projects()
103+
104+
assert projects == []
105+
96106
def test_get_projects_auth_error(client, requests_mock):
97107
"""Tests authentication error during get_projects."""
98108
mock_url = f"{TEST_BASE_URL}/projects"
@@ -165,6 +175,17 @@ def test_create_project_api_error(client, requests_mock):
165175
with pytest.raises(SysMLV2APIError, match="Unexpected status code for POST /projects"):
166176
client.create_project(request_data)
167177

178+
def test_delete_project_success(client, requests_mock):
179+
"""Tests successfully deleting a project."""
180+
mock_url = f"{TEST_BASE_URL}/projects/{TEST_PROJECT_ID}"
181+
requests_mock.delete(mock_url, json={}, status_code=200)
182+
183+
result = client.delete_project(TEST_PROJECT_ID)
184+
185+
assert result == {}
186+
assert requests_mock.last_request.url == mock_url
187+
assert requests_mock.last_request.method == "DELETE"
188+
168189

169190
# --- Test Get Element ---
170191

@@ -229,6 +250,24 @@ def test_get_owned_elements_empty(client, requests_mock):
229250

230251
assert owned_elements == []
231252

253+
def test_get_owned_elements_success_list_response(client, requests_mock):
254+
"""Tests retrieving owned elements when the API returns a bare list."""
255+
mock_url = f"{TEST_BASE_URL}/projects/{TEST_PROJECT_ID}/commits/{TEST_COMMIT_ID}/elements/{TEST_ELEMENT_ID}/owned"
256+
mock_response_data = [{"id": "owned_elem_1"}]
257+
requests_mock.get(mock_url, json=mock_response_data, status_code=200)
258+
259+
owned_elements = client.get_owned_elements(TEST_PROJECT_ID, TEST_ELEMENT_ID, TEST_COMMIT_ID)
260+
261+
assert owned_elements == mock_response_data
262+
263+
def test_get_owned_elements_success_unexpected_scalar_response(client, monkeypatch):
264+
"""Tests retrieving owned elements when the API returns a scalar value."""
265+
monkeypatch.setattr(client, "_request", lambda **kwargs: "unexpected")
266+
267+
owned_elements = client.get_owned_elements(TEST_PROJECT_ID, TEST_ELEMENT_ID, TEST_COMMIT_ID)
268+
269+
assert owned_elements == []
270+
232271
# --- Test Create Commit ---
233272

234273
def test_create_commit_success(client, requests_mock):
@@ -245,6 +284,55 @@ def test_create_commit_success(client, requests_mock):
245284
assert requests_mock.last_request.method == "POST"
246285
assert requests_mock.last_request.json() == request_data
247286

287+
def test_create_commit_with_branch_id(client, requests_mock):
288+
"""Tests commit creation with a branch query parameter."""
289+
branch_id = "branch_123"
290+
mock_url = f"{TEST_BASE_URL}/projects/{TEST_PROJECT_ID}/commits?branchId={branch_id}"
291+
request_data = {"message": "Branch commit", "parentCommitId": None}
292+
response_data = {"id": "branch_commit_id", **request_data}
293+
requests_mock.post(mock_url, json=response_data, status_code=200)
294+
295+
created_commit = client.create_commit(TEST_PROJECT_ID, request_data, branch_id=branch_id)
296+
297+
assert created_commit == response_data
298+
assert requests_mock.last_request.url == mock_url
299+
assert requests_mock.last_request.method == "POST"
300+
assert requests_mock.last_request.json() == request_data
301+
302+
def test_create_commit_with_replace(client, requests_mock):
303+
"""Tests commit creation with the replace query parameter."""
304+
mock_url = f"{TEST_BASE_URL}/projects/{TEST_PROJECT_ID}/commits?replace=true"
305+
request_data = {"message": "Replace commit", "parentCommitId": None}
306+
response_data = {"id": "replace_commit_id", **request_data}
307+
requests_mock.post(mock_url, json=response_data, status_code=200)
308+
309+
created_commit = client.create_commit(TEST_PROJECT_ID, request_data, replace=True)
310+
311+
assert created_commit == response_data
312+
assert requests_mock.last_request.url == mock_url
313+
assert requests_mock.last_request.method == "POST"
314+
assert requests_mock.last_request.json() == request_data
315+
316+
def test_create_commit_with_branch_id_and_replace(client, requests_mock):
317+
"""Tests commit creation with both branchId and replace query parameters."""
318+
branch_id = "branch_123"
319+
mock_url = f"{TEST_BASE_URL}/projects/{TEST_PROJECT_ID}/commits?replace=true&branchId={branch_id}"
320+
request_data = {"message": "Replace branch commit", "parentCommitId": None}
321+
response_data = {"id": "replace_branch_commit_id", **request_data}
322+
requests_mock.post(mock_url, json=response_data, status_code=200)
323+
324+
created_commit = client.create_commit(
325+
TEST_PROJECT_ID,
326+
request_data,
327+
branch_id=branch_id,
328+
replace=True,
329+
)
330+
331+
assert created_commit == response_data
332+
assert requests_mock.last_request.url == mock_url
333+
assert requests_mock.last_request.method == "POST"
334+
assert requests_mock.last_request.json() == request_data
335+
248336
def test_create_commit_bad_request(client, requests_mock):
249337
"""Tests 400 Bad Request during commit creation."""
250338
mock_url = f"{TEST_BASE_URL}/projects/{TEST_PROJECT_ID}/commits"
@@ -329,6 +417,14 @@ def test_list_commits_success_dict_response(client, requests_mock):
329417

330418
assert commits == mock_response_data["elements"]
331419

420+
def test_list_commits_success_unexpected_scalar_response(client, monkeypatch):
421+
"""Tests listing commits when the API returns a scalar value."""
422+
monkeypatch.setattr(client, "_request", lambda **kwargs: "unexpected")
423+
424+
commits = client.list_commits(TEST_PROJECT_ID)
425+
426+
assert commits == []
427+
332428
def test_list_commits_project_not_found(client, requests_mock):
333429
"""Tests 404 when listing commits for a non-existent project."""
334430
mock_url = f"{TEST_BASE_URL}/projects/invalid_project/commits"
@@ -349,6 +445,18 @@ def test_list_branches_success(client, requests_mock):
349445
branches = client.list_branches(TEST_PROJECT_ID)
350446
assert branches == mock_response
351447

448+
def test_list_branches_success_dict_response(client, requests_mock):
449+
mock_url = f"{TEST_BASE_URL}/projects/{TEST_PROJECT_ID}/branches"
450+
mock_response = {"elements": [{"id": TEST_BRANCH_ID, "name": "develop"}]}
451+
requests_mock.get(mock_url, json=mock_response, status_code=200)
452+
branches = client.list_branches(TEST_PROJECT_ID)
453+
assert branches == mock_response["elements"]
454+
455+
def test_list_branches_success_unexpected_scalar_response(client, monkeypatch):
456+
monkeypatch.setattr(client, "_request", lambda **kwargs: "unexpected")
457+
branches = client.list_branches(TEST_PROJECT_ID)
458+
assert branches == []
459+
352460
def test_create_branch_success(client, requests_mock):
353461
mock_url = f"{TEST_BASE_URL}/projects/{TEST_PROJECT_ID}/branches"
354462
request_data = {"name": "feature-branch", "head": {"@id": TEST_COMMIT_ID}}
@@ -389,6 +497,18 @@ def test_list_tags_success(client, requests_mock):
389497
tags = client.list_tags(TEST_PROJECT_ID)
390498
assert tags == mock_response
391499

500+
def test_list_tags_success_dict_response(client, requests_mock):
501+
mock_url = f"{TEST_BASE_URL}/projects/{TEST_PROJECT_ID}/tags"
502+
mock_response = {"elements": [{"id": TEST_TAG_ID, "name": "v1.0"}]}
503+
requests_mock.get(mock_url, json=mock_response, status_code=200)
504+
tags = client.list_tags(TEST_PROJECT_ID)
505+
assert tags == mock_response["elements"]
506+
507+
def test_list_tags_success_unexpected_scalar_response(client, monkeypatch):
508+
monkeypatch.setattr(client, "_request", lambda **kwargs: "unexpected")
509+
tags = client.list_tags(TEST_PROJECT_ID)
510+
assert tags == []
511+
392512
def test_create_tag_success(client, requests_mock):
393513
mock_url = f"{TEST_BASE_URL}/projects/{TEST_PROJECT_ID}/tags"
394514
request_data = {"name": "v1.0-release", "taggedCommit": {"@id": TEST_COMMIT_ID}}
@@ -435,6 +555,20 @@ def test_list_elements_commit_not_found(client, requests_mock):
435555
with pytest.raises(SysMLV2NotFoundError):
436556
client.list_elements(TEST_PROJECT_ID, "invalid_commit")
437557

558+
def test_list_elements_success_dict_response(client, requests_mock):
559+
"""Tests listing elements when the API returns a dict with 'elements'."""
560+
mock_url = f"{TEST_BASE_URL}/projects/{TEST_PROJECT_ID}/commits/{TEST_COMMIT_ID}/elements"
561+
mock_response_data = {"elements": [{"id": "elem1"}, {"id": "elem2"}]}
562+
requests_mock.get(mock_url, json=mock_response_data, status_code=200)
563+
elements = client.list_elements(TEST_PROJECT_ID, TEST_COMMIT_ID)
564+
assert elements == mock_response_data["elements"]
565+
566+
def test_list_elements_success_unexpected_scalar_response(client, monkeypatch):
567+
"""Tests listing elements when the API returns a scalar value."""
568+
monkeypatch.setattr(client, "_request", lambda **kwargs: "unexpected")
569+
elements = client.list_elements(TEST_PROJECT_ID, TEST_COMMIT_ID)
570+
assert elements == []
571+
438572

439573
# --- Test List Relationships ---
440574

@@ -460,4 +594,63 @@ def test_list_relationships_element_not_found(client, requests_mock):
460594
mock_url = f"{TEST_BASE_URL}/projects/{TEST_PROJECT_ID}/commits/{TEST_COMMIT_ID}/elements/invalid_element/relationships?direction=both"
461595
requests_mock.get(mock_url, status_code=404)
462596
with pytest.raises(SysMLV2NotFoundError):
463-
client.list_relationships(TEST_PROJECT_ID, "invalid_element", TEST_COMMIT_ID)
597+
client.list_relationships(TEST_PROJECT_ID, "invalid_element", TEST_COMMIT_ID)
598+
599+
def test_list_relationships_success_dict_response(client, requests_mock):
600+
"""Tests listing relationships when the API returns a dict with 'elements'."""
601+
mock_url = f"{TEST_BASE_URL}/projects/{TEST_PROJECT_ID}/commits/{TEST_COMMIT_ID}/elements/{TEST_ELEMENT_ID}/relationships?direction=both"
602+
mock_response_data = {"elements": [{"id": "rel1"}]}
603+
requests_mock.get(mock_url, json=mock_response_data, status_code=200)
604+
relationships = client.list_relationships(TEST_PROJECT_ID, TEST_ELEMENT_ID, TEST_COMMIT_ID)
605+
assert relationships == mock_response_data["elements"]
606+
607+
def test_list_relationships_success_unexpected_scalar_response(client, monkeypatch):
608+
"""Tests listing relationships when the API returns a scalar value."""
609+
monkeypatch.setattr(client, "_request", lambda **kwargs: "unexpected")
610+
relationships = client.list_relationships(TEST_PROJECT_ID, TEST_ELEMENT_ID, TEST_COMMIT_ID)
611+
assert relationships == []
612+
613+
614+
# --- Test _request Edge Cases ---
615+
616+
def test_request_bad_request_uses_text_when_json_decode_fails(client, monkeypatch):
617+
"""Tests 400 handling falls back to response text when JSON decoding fails."""
618+
619+
class FakeResponse:
620+
status_code = 400
621+
text = "plain error text"
622+
content = b"plain error text"
623+
624+
def json(self):
625+
raise json.JSONDecodeError("Expecting value", "plain error text", 0)
626+
627+
monkeypatch.setattr(client._session, "request", lambda **kwargs: FakeResponse())
628+
629+
with pytest.raises(SysMLV2BadRequestError, match="plain error text"):
630+
client._request(method="GET", endpoint="/projects")
631+
632+
def test_request_network_error_wrapped(client, monkeypatch):
633+
"""Tests request-layer network exceptions are wrapped in SysMLV2Error."""
634+
def raise_request_exception(**kwargs):
635+
raise requests.exceptions.ConnectionError("connection dropped")
636+
637+
monkeypatch.setattr(client._session, "request", raise_request_exception)
638+
639+
with pytest.raises(SysMLV2Error, match="Network error during request"):
640+
client._request(method="GET", endpoint="/projects")
641+
642+
def test_request_success_json_decode_error_wrapped(client, monkeypatch):
643+
"""Tests invalid JSON on a successful response is wrapped in SysMLV2Error."""
644+
645+
class FakeResponse:
646+
status_code = 200
647+
text = "not json"
648+
content = b"not json"
649+
650+
def json(self):
651+
raise json.JSONDecodeError("Expecting value", "not json", 0)
652+
653+
monkeypatch.setattr(client._session, "request", lambda **kwargs: FakeResponse())
654+
655+
with pytest.raises(SysMLV2Error, match="Failed to decode JSON response"):
656+
client._request(method="GET", endpoint="/projects")

0 commit comments

Comments
 (0)