Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 8 additions & 10 deletions api/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
BORDER_COLOR_TEST_CASE,
BORDER_COLOR_TEST_SPECIFICATION,
code_to_html,
combine_tmt_path,
document_to_html,
extend_unmapped_sections_for_auto_fix,
get_api_specification,
Expand Down Expand Up @@ -4118,7 +4119,7 @@ def post(
)
return api_response.return_conflict()

test_case_path = os.path.join(repository, relative_path)
test_case_path = combine_tmt_path(repository, relative_path)
# In case of local file
if test_case_path.startswith(os.path.sep):
if not is_safe_local_user_file_path(test_case_path):
Expand Down Expand Up @@ -4216,10 +4217,7 @@ def put(
setattr(test_case, field, request_data["test-case"][field.replace("_", "-")])

if modified_tc:
test_case_path = os.path.join(
test_case.repository,
str(test_case.relative_path).lstrip(os.path.sep),
)
test_case_path = combine_tmt_path(test_case.repository, test_case.relative_path)
# In case of local file
if test_case_path.startswith(os.path.sep):
if not is_safe_local_user_file_path(test_case_path):
Expand Down Expand Up @@ -6291,7 +6289,7 @@ def get(

# if repository is a local path, return the file content
if test_case_mapping.test_case.repository.startswith("/"):
test_case_path = os.path.join(
test_case_path = combine_tmt_path(
test_case_mapping.test_case.repository, test_case_mapping.test_case.relative_path
)

Expand Down Expand Up @@ -7204,7 +7202,7 @@ def post(self, api_response: ApiResponse = None):
)
return api_response.return_conflict()

test_case_path = os.path.join(repository, relative_path)
test_case_path = combine_tmt_path(repository, relative_path)
# In case of local file
if test_case_path.startswith(os.path.sep):
if not is_safe_local_user_file_path(test_case_path):
Expand Down Expand Up @@ -7321,7 +7319,7 @@ def put(self, api_response: ApiResponse = None):
setattr(test_case, field, request_data["test-case"][field.replace("_", "-")])

if modified_tc:
test_case_path = os.path.join(
test_case_path = combine_tmt_path(
request_data["test-case"]["repository"], request_data["test-case"]["relative-path"]
)
# In case of local file
Expand Down Expand Up @@ -7585,7 +7583,7 @@ def post(
)
return api_response.return_conflict()

test_case_path = os.path.join(repository, relative_path)
test_case_path = combine_tmt_path(repository, relative_path)
# In case of local file
if test_case_path.startswith(os.path.sep):
if not is_safe_local_user_file_path(test_case_path):
Expand Down Expand Up @@ -7698,7 +7696,7 @@ def put(
setattr(test_case, field, request_data["test-case"][field.replace("_", "-")])

if modified_tc:
test_case_path = os.path.join(
test_case_path = combine_tmt_path(
request_data["test-case"]["repository"], request_data["test-case"]["relative-path"]
)
# In case of local file
Expand Down
18 changes: 18 additions & 0 deletions api/api_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -786,6 +786,24 @@ def is_safe_user_path(user_root, requested_path):
return abs_target == abs_root or abs_target.startswith(abs_root + os.sep)


def combine_tmt_path(repository: str, relative_path: str) -> str:
"""Join a TMT test repository path with a relative path.

User-file relative paths from the UI often start with a leading separator
(for example ``/api/user-files/<id>/tmt-dummy-test``). ``os.path.join``
treats a later absolute segment as a new root and would discard
*repository*. This helper strips leading separators from *relative_path*
before joining so the result stays under *repository*.
"""
repo = str(repository or "")
rel = str(relative_path or "").lstrip("/" + os.sep)
if not repo:
return rel
if not rel:
return repo
return os.path.join(repo, rel)


def is_safe_local_user_file_path(path: str) -> bool:
from api import USER_FILES_BASE_DIR
return path.startswith(os.path.abspath(USER_FILES_BASE_DIR) + os.sep)
Expand Down
39 changes: 39 additions & 0 deletions api/test/test_api_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from api_utils import (
LINK_BASIL_INSTANCE_HTML_MESSAGE,
add_html_link_to_email_body,
combine_tmt_path,
load_settings
)

Expand Down Expand Up @@ -83,3 +84,41 @@ def test_add_html_link_to_email_body(client, admin_authentication):
body = add_html_link_to_email_body(settings=settings, body=initial_body)
assert body != ""
assert LINK_BASIL_INSTANCE_HTML_MESSAGE in body


@pytest.mark.parametrize(
"repository, relative_path, expected",
[
(
"/BASIL-API",
"/api/user-files/2/tmt/tmt-dummy-test",
"/BASIL-API/api/user-files/2/tmt/tmt-dummy-test",
),
(
"/BASIL-API",
"api/user-files/2/tmt/tmt-dummy-test",
"/BASIL-API/api/user-files/2/tmt/tmt-dummy-test",
),
(
"/opt/basil",
"examples/tmt/local/tmt-dummy-test.fmf",
"/opt/basil/examples/tmt/local/tmt-dummy-test.fmf",
),
("/repo", "", "/repo"),
("", "tests/foo.fmf", "tests/foo.fmf"),
("/repo/", "/nested/test", "/repo/nested/test"),
(None, "/api/user-files/2/test", "api/user-files/2/test"),
],
)
def test_combine_tmt_path(repository, relative_path, expected):
assert combine_tmt_path(repository, relative_path) == expected


def test_combine_tmt_path_does_not_drop_repository_when_relative_is_absolute():
"""os.path.join discards repository when relative_path is absolute; combine_tmt_path must not."""
repository = "/BASIL-API"
relative_path = "/api/user-files/2/tmt/tmt-dummy-test"
assert os.path.join(repository, relative_path) == relative_path
assert combine_tmt_path(repository, relative_path) == (
"/BASIL-API/api/user-files/2/tmt/tmt-dummy-test"
)
30 changes: 30 additions & 0 deletions api/test/test_sw_requirement_test_case_mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,36 @@ def test_put_ok(client, client_db, user_authentication, mapped_api_sr_tc_db, uti
assert response.status_code == HTTPStatus.OK


def test_put_user_file_relative_path(client, client_db, user_authentication, mapped_api_sr_tc_db):
"""Edit Test Case from user files sends repository=BASIL root and /api/user-files/... relative-path."""
import api as basil_api

api, sw_requirement, api_sr_mapping, sr_tc_mapping = mapped_api_sr_tc_db
auth = user_authentication.json
user_id = auth["id"]
basil_root = os.path.dirname(os.path.dirname(os.path.abspath(basil_api.USER_FILES_BASE_DIR)))
relative_path = f"/api/user-files/{user_id}/tmt/tmt-dummy-test"

ut_test_case_dict = sr_tc_mapping.test_case.as_dict()
ut_test_case_dict = {k.replace("_", "-"): v for k, v in ut_test_case_dict.items()}
ut_test_case_dict["repository"] = basil_root
ut_test_case_dict["relative-path"] = relative_path

mapping_data = {
"api-id": api.id,
"coverage": sr_tc_mapping.coverage,
"relation-id": sr_tc_mapping.id,
"sw-requirement": {"id": sw_requirement.id},
"test-case": ut_test_case_dict,
"user-id": auth["id"],
"token": auth["token"],
}
response = client.put(_MAPPING_SW_REQUIREMENT_TEST_CASES_URL, json=mapping_data)
assert response.status_code == HTTPStatus.OK
assert response.json["test_case"]["repository"] == basil_root
assert response.json["test_case"]["relative_path"] == relative_path


# Test DELETE


Expand Down
29 changes: 29 additions & 0 deletions api/test/test_test_case_local_file_implementation.py
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,35 @@ def test_get_ok_api_relation_local_file(
_remove_if_exists(path)


def test_get_ok_api_relation_local_file_ui_style_relative_path(
client, client_db, user_authentication, mapped_api_tc_db, utilities
):
"""UI stores repository as BASIL root and relative_path as /api/user-files/<id>/file."""
api, test_case, api_tc_mapping = mapped_api_tc_db
auth = user_authentication.json
user_id = auth["id"]
content = "ui-style-path\n"
base = os.path.join(os.path.abspath(basil_api.USER_FILES_BASE_DIR), str(user_id))
os.makedirs(base, exist_ok=True)
filename = f"ut_tclocal_{utilities.generate_random_hex_string8()}.txt"
path = os.path.join(base, filename)
with open(path, "w", encoding="utf-8") as f:
f.write(content)
basil_root = os.path.dirname(os.path.dirname(os.path.abspath(basil_api.USER_FILES_BASE_DIR)))
test_case.repository = basil_root
test_case.relative_path = f"/api/user-files/{user_id}/{filename}"
client_db.session.add(test_case)
client_db.session.commit()
try:
response = _get_local_file_impl(
client, auth, api.id, test_case.id, api_tc_mapping.id, "api"
)
assert response.status_code == HTTPStatus.OK
assert response.get_data(as_text=True) == content
finally:
_remove_if_exists(path)


def test_get_bad_request_unsafe_local_path(
client, client_db, user_authentication, mapped_api_tc_db, utilities
):
Expand Down
30 changes: 30 additions & 0 deletions api/test/test_test_specification_test_case_mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,36 @@ def test_put_ok(client, client_db, user_authentication, mapped_api_ts_tc_db, uti
assert response.status_code == HTTPStatus.OK


def test_put_user_file_relative_path(client, client_db, user_authentication, mapped_api_ts_tc_db):
"""Edit Test Case from user files sends repository=BASIL root and /api/user-files/... relative-path."""
import api as basil_api

api, test_specification, api_ts_mapping, ts_tc_mapping = mapped_api_ts_tc_db
auth = user_authentication.json
user_id = auth["id"]
basil_root = os.path.dirname(os.path.dirname(os.path.abspath(basil_api.USER_FILES_BASE_DIR)))
relative_path = f"/api/user-files/{user_id}/tmt/tmt-dummy-test"

ut_test_case_dict = ts_tc_mapping.test_case.as_dict()
ut_test_case_dict = {k.replace("_", "-"): v for k, v in ut_test_case_dict.items()}
ut_test_case_dict["repository"] = basil_root
ut_test_case_dict["relative-path"] = relative_path

mapping_data = {
"api-id": api.id,
"coverage": ts_tc_mapping.coverage,
"relation-id": ts_tc_mapping.id,
"test-specification": {"id": test_specification.id},
"test-case": ut_test_case_dict,
"user-id": auth["id"],
"token": auth["token"],
}
response = client.put(_MAPPING_TEST_SPECIFICATION_TEST_CASES_URL, json=mapping_data)
assert response.status_code == HTTPStatus.OK
assert response.json["test_case"]["repository"] == basil_root
assert response.json["test_case"]["relative_path"] == relative_path


# Test DELETE


Expand Down
28 changes: 28 additions & 0 deletions api/test/test_testrun_tmt_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,34 @@ def test_user_can_access_own_folder(self, basil_root_path, user_x):
except SystemExit:
pytest.fail("User should be able to access their own folder")

def test_user_can_access_own_folder_with_ui_style_path(self, basil_root_path, user_x):
"""UI stores repository as BASIL root and relative_path as /api/user-files/<id>/..."""

test_case = MockTestCase(
repository=basil_root_path,
relative_path=f"/api/user-files/{user_x.id}/tmt/tmt-dummy-test",
)

config = {
"id": 1,
"title": "Test Config",
"provision_type": "container",
"context": {"plan_type": "local"},
"git_repo_ref": "",
"env": {
"basil_test_repo_path": test_case.repository,
"basil_test_relative_path": test_case.relative_path,
},
}

runner = MockRunner(user_x, test_case, config)

try:
plugin = TestRunnerTmtPlugin(runner=runner)
assert plugin is not None
except SystemExit:
pytest.fail("User should be able to access their own folder via UI-style TMT path")

def test_user_can_access_basil_examples(self, basil_root_path, user_x):
"""Test that any user can access BASIL example files"""

Expand Down
6 changes: 3 additions & 3 deletions api/testrun_lava.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import requests
import yaml
from api_utils import get_api_specification
from api_utils import combine_tmt_path, get_api_specification
from testrun_base import TestRunnerBasePlugin

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -117,8 +117,8 @@ def __init__(self, runner=None, *args, **kwargs):
}
else:
# read the file
test_file_path = os.path.join(
self.runner.mapping.test_case.repository, self.runner.mapping.test_case.relative_path.lstrip("/")
test_file_path = combine_tmt_path(
self.runner.mapping.test_case.repository, self.runner.mapping.test_case.relative_path
)

if os.path.exists(test_file_path):
Expand Down
3 changes: 2 additions & 1 deletion api/testrun_tmt.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import subprocess

import yaml
from api_utils import combine_tmt_path
from testrun_base import TestRunnerBasePlugin

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -108,7 +109,7 @@ def internal_validate(self):
# Calculate the user folder path
user_folder_path = os.path.join(basil_path, "api", "user-files", user_id_str)
# Calculate resulting test path considering also possible ../ (or multiple ../)
test_path = os.path.join(
test_path = combine_tmt_path(
self.config["env"]["basil_test_repo_path"], self.config["env"]["basil_test_relative_path"]
)

Expand Down
Loading
Loading