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
39 changes: 24 additions & 15 deletions _refactored/core/clients/rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,18 @@ def _request(
json: Any | None = None,
params: dict[str, Any] | None = None,
headers: dict[str, str] | None = None,
files: dict[str, Any] | None = None,
) -> requests.Response:
merged_headers = {**self._auth.headers, **(headers or {})}
if files is not None:
merged_headers = {k: v for k, v in merged_headers.items() if k.lower() != "content-type"}
response = self._session.request(
method=method,
url=url,
json=json,
params=params,
headers=merged_headers,
files=files,
timeout=self._global_settings.requests_timeout,
verify=self._global_settings.verify_ssl,
)
Expand All @@ -59,9 +63,7 @@ def _parse_response(self, response: requests.Response) -> _ResponseBody | None:
try:
return response.json()
except requests.exceptions.JSONDecodeError as exc:
raise ValueError(
f"Response has Content-Type: application/json but body is not valid JSON: {exc}"
) from exc
raise ValueError(f"Response has Content-Type: application/json but body is not valid JSON: {exc}") from exc

def get(
self,
Expand All @@ -79,9 +81,7 @@ def post(
params: dict[str, Any] | None = None,
headers: dict[str, str] | None = None,
) -> _ResponseBody | None:
response = self._request(
method="POST", url=url, json=json, params=params, headers=headers
)
response = self._request(method="POST", url=url, json=json, params=params, headers=headers)
return self._parse_response(response=response)

def put(
Expand All @@ -91,9 +91,7 @@ def put(
params: dict[str, Any] | None = None,
headers: dict[str, str] | None = None,
) -> _ResponseBody | None:
response = self._request(
method="PUT", url=url, json=json, params=params, headers=headers
)
response = self._request(method="PUT", url=url, json=json, params=params, headers=headers)
return self._parse_response(response=response)

def patch(
Expand All @@ -103,9 +101,22 @@ def patch(
params: dict[str, Any] | None = None,
headers: dict[str, str] | None = None,
) -> _ResponseBody | None:
response = self._request(
method="PATCH", url=url, json=json, params=params, headers=headers
)
response = self._request(method="PATCH", url=url, json=json, params=params, headers=headers)
return self._parse_response(response=response)

def post_multipart(
self,
url: str,
files: dict[str, Any],
params: dict[str, Any] | None = None,
headers: dict[str, str] | None = None,
) -> _ResponseBody | None:
response = self._request(method="POST", url=url, params=params, headers=headers, files=files)
if not response.content:
return None
content_type = response.headers.get("Content-Type", "")
if "application/json" not in content_type:
return None
return self._parse_response(response=response)

def delete(
Expand All @@ -114,9 +125,7 @@ def delete(
params: dict[str, Any] | None = None,
headers: dict[str, str] | None = None,
) -> _ResponseBody | None:
response = self._request(
method="DELETE", url=url, params=params, headers=headers
)
response = self._request(method="DELETE", url=url, params=params, headers=headers)
return self._parse_response(response=response)

def close(self) -> None:
Expand Down
6 changes: 3 additions & 3 deletions _refactored/tests/restapi/catalog/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def _make(**overrides: Any) -> dict:

yield _make

for cid in created_ids:
for cid in reversed(created_ids):
try:
catalog_ops.delete(cid)
except Exception:
Expand All @@ -63,7 +63,7 @@ def _make(*, catalog: dict | None = None, **overrides: Any) -> dict:

yield _make

for cid in created_ids:
for cid in reversed(created_ids):
try:
category_ops.delete(cid)
except Exception:
Expand Down Expand Up @@ -95,7 +95,7 @@ def _make(*, category: dict | None = None, **overrides: Any) -> dict:

yield _make

for pid in created_ids:
for pid in reversed(created_ids):
try:
product_ops.delete(pid)
except Exception:
Expand Down
91 changes: 64 additions & 27 deletions _refactored/tests/restapi/catalog/test_assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,15 @@
- POST /api/assets?folderUrl={folder}&url={sourceUrl} (upload from URL)
- POST /api/assets?folderUrl={folder} (upload local file, multipart)
- GET /api/assets?folderUrl={folder}&keyword={kw} (list)
- DELETE /api/assets?urls={url} (delete)
"""

import uuid

import allure
import pytest

from core.auth import AuthProvider
from core.clients.rest import RestClient
from core.global_settings import GlobalSettings
from restapi.operations import ProductOperations


Expand Down Expand Up @@ -75,42 +74,46 @@ def test_asset_add_to_product(
product_ops: ProductOperations,
rest_client: RestClient,
backend_base_url: str,
admin_auth: AuthProvider,
global_settings: GlobalSettings,
):
product = make_product()
folder = f"catalog-{product['code']}"
asset_url = ""

with allure.step(f"POST /api/assets?folderUrl={folder} — multipart upload"):
response = rest_client._session.post(
uploaded = rest_client.post_multipart(
f"{backend_base_url}/api/assets",
params={"folderUrl": folder},
files={"file": (f"qa-{uuid.uuid4().hex[:6]}.txt", b"QA asset content", "text/plain")},
headers={"Authorization": admin_auth.headers.get("Authorization", "")},
timeout=global_settings.requests_timeout,
verify=global_settings.verify_ssl,
)
response.raise_for_status()
uploaded = response.json()
assert uploaded, "Asset upload response empty"
asset_info = uploaded[0] if isinstance(uploaded, list) else uploaded

with allure.step("POST /api/catalog/products — attach asset via product update"):
asset_entry = {
"name": asset_info.get("name"),
"url": asset_info.get("url"),
"relativeUrl": asset_info.get("relativeUrl"),
"mimeType": asset_info.get("contentType") or asset_info.get("mimeType"),
"size": asset_info.get("size", 0),
"group": "default",
"createdDate": "0001-01-01T00:00:00Z",
}
product_ops.update(product, assets=[asset_entry])

with allure.step("Verify asset attached to product"):
reloaded = product_ops.get_by_id(product["id"])
asset_names = [a.get("name") for a in reloaded.get("assets", [])]
assert asset_info.get("name") in asset_names
asset_url = asset_info.get("url") or asset_info.get("relativeUrl") or ""

try:
with allure.step("POST /api/catalog/products — attach asset via product update"):
asset_entry = {
"name": asset_info.get("name"),
"url": asset_info.get("url"),
"relativeUrl": asset_info.get("relativeUrl"),
"mimeType": asset_info.get("contentType") or asset_info.get("mimeType"),
"size": asset_info.get("size", 0),
"group": "default",
"createdDate": "0001-01-01T00:00:00Z",
}
product_ops.update(product, assets=[asset_entry])

with allure.step("Verify asset attached to product"):
reloaded = product_ops.get_by_id(product["id"])
asset_names = [a.get("name") for a in reloaded.get("assets", [])]
assert asset_info.get("name") in asset_names
finally:
with allure.step("Cleanup uploaded asset and folder"):
try:
if asset_url:
rest_client.delete(f"{backend_base_url}/api/assets", params={"urls": [asset_url]})
rest_client.delete(f"{backend_base_url}/api/assets", params={"urls": [folder]})
except Exception:
pass


@pytest.mark.restapi
Expand Down Expand Up @@ -138,3 +141,37 @@ def test_asset_remove_from_product(make_product, product_ops: ProductOperations)
with allure.step("Verify asset removed"):
reloaded = product_ops.get_by_id(product["id"])
assert reloaded.get("assets") in ([], None)


@pytest.mark.restapi
@allure.feature("Catalog / Assets (REST API)")
@allure.title("Delete uploaded asset from folder")
def test_asset_delete(rest_client: RestClient, backend_base_url: str):
folder = f"qa-catdel-{uuid.uuid4().hex[:6]}"
source_url = "https://raw.githubusercontent.com/VirtoCommerce/vc-testing-module/dev/README.md"

with allure.step(f"POST /api/assets?folderUrl={folder}&url=... — upload"):
uploaded = rest_client.post(
f"{backend_base_url}/api/assets",
json=None,
params={"folderUrl": folder, "url": source_url},
)
assert uploaded, "Asset upload response empty"
entry = uploaded[0] if isinstance(uploaded, list) else uploaded
asset_url = entry.get("url") or entry.get("relativeUrl") or ""
assert asset_url, "Uploaded asset url missing"

with allure.step("DELETE /api/assets?urls=..."):
rest_client.delete(f"{backend_base_url}/api/assets", params={"urls": [asset_url]})

with allure.step("Verify asset gone from listing"):
listing = rest_client.get(f"{backend_base_url}/api/assets", params={"folderUrl": folder})
results = listing.get("results", []) if isinstance(listing, dict) else (listing or [])
names = [item.get("name") for item in results]
assert entry.get("name") not in names

with allure.step("Cleanup folder"):
try:
rest_client.delete(f"{backend_base_url}/api/assets", params={"urls": [folder]})
except Exception:
pass
30 changes: 30 additions & 0 deletions _refactored/tests/restapi/catalog/test_category.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,36 @@ def test_category_get_template(make_catalog, category_ops: CategoryOperations):
assert template.get("seoObjectType") == "Category"


@pytest.mark.restapi
@allure.feature("Catalog / Categories (REST API)")
@allure.title("Create nested subcategory — parentId wired correctly")
def test_category_nested(make_category, category_ops: CategoryOperations):
parent = make_category()

with allure.step(f"POST /api/catalog/categories — child under parent {parent['id']}"):
child = make_category(
catalog={"id": parent["catalogId"]},
parentId=parent["id"],
)

with allure.step("Verify child has parentId and shares catalogId"):
reloaded = category_ops.get_by_id(child["id"])
assert reloaded["parentId"] == parent["id"]
assert reloaded["catalogId"] == parent["catalogId"]


@pytest.mark.restapi
@allure.feature("Catalog / Categories (REST API)")
@allure.title("Get category by non-existent id — expect 404")
def test_category_get_not_found(category_ops: CategoryOperations):
bogus_id = f"qa-missing-{uuid.uuid4().hex}"

with allure.step(f"GET /api/catalog/categories/{bogus_id}"):
with pytest.raises(HTTPError) as exc_info:
category_ops.get_by_id(bogus_id)
assert exc_info.value.response.status_code == 404


@pytest.mark.restapi
@allure.feature("Catalog / Categories (REST API)")
@allure.title("Delete category")
Expand Down
31 changes: 24 additions & 7 deletions _refactored/tests/restapi/catalog/test_product.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,15 +117,32 @@ def test_product_create_update_with_body(make_product, product_ops: ProductOpera
with allure.step("POST /api/catalog/products — persist clone"):
created = product_ops.create_or_update_with_body(clone_body)

with allure.step("Verify cloned product id"):
assert created.get("id")
assert created.get("id") != product["id"]
try:
with allure.step("Verify cloned product id"):
assert created.get("id")
assert created.get("id") != product["id"]
finally:
with allure.step("Cleanup cloned product"):
try:
product_ops.delete(created["id"])
except Exception:
pass


@pytest.mark.restapi
@allure.feature("Catalog / Products (REST API)")
@allure.title("Get product by non-existent id — expect empty result or 404")
def test_product_get_not_found(product_ops: ProductOperations):
bogus_id = f"qa-missing-{uuid.uuid4().hex}"

with allure.step("Cleanup cloned product"):
with allure.step(f"GET /api/catalog/products?ids={bogus_id}"):
try:
product_ops.delete(created["id"])
except Exception:
pass
results = product_ops.get_by_ids([bogus_id])
except HTTPError as exc:
assert exc.response.status_code == 404
else:
ids = [r.get("id") for r in (results or [])]
assert bogus_id not in ids


@pytest.mark.restapi
Expand Down
26 changes: 4 additions & 22 deletions _refactored/tests/restapi/platform/test_assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,7 @@
import pytest
import requests

from core.auth import AuthProvider
from core.clients.rest import RestClient
from core.global_settings import GlobalSettings


@pytest.mark.restapi
Expand Down Expand Up @@ -58,43 +56,27 @@ def test_asset_upload_url_image(rest_client: RestClient, backend_base_url: str):
@pytest.mark.restapi
@allure.feature("Platform / Assets (REST API)")
@allure.title("Upload local file")
def test_asset_upload_local(
rest_client: RestClient, backend_base_url: str, global_settings: GlobalSettings, admin_auth: AuthProvider
):
def test_asset_upload_local(rest_client: RestClient, backend_base_url: str):
folder = f"qa-local-{uuid.uuid4().hex[:6]}"

with allure.step("POST /api/assets?folderUrl=... — multipart upload"):
response = rest_client._session.post(
rest_client.post_multipart(
f"{backend_base_url}/api/assets",
params={"folderUrl": folder},
files={"file": ("qa-test-local.txt", b"QA test content", "text/plain")},
headers={"Authorization": admin_auth.headers.get("Authorization", "")},
timeout=global_settings.requests_timeout,
verify=global_settings.verify_ssl,
)

with allure.step("Verify upload response"):
assert response.status_code in (200, 204)


@pytest.mark.restapi
@allure.feature("Platform / Assets (REST API)")
@allure.title("Upload local file to storage")
def test_asset_upload_local_storage(
rest_client: RestClient, backend_base_url: str, global_settings: GlobalSettings, admin_auth: AuthProvider
):
def test_asset_upload_local_storage(rest_client: RestClient, backend_base_url: str):
with allure.step("POST /api/assets/localstorage — multipart upload"):
response = rest_client._session.post(
rest_client.post_multipart(
f"{backend_base_url}/api/assets/localstorage",
files={"file": ("qa-storage-test.txt", b"QA storage test", "text/plain")},
headers={"Authorization": admin_auth.headers.get("Authorization", "")},
timeout=global_settings.requests_timeout,
verify=global_settings.verify_ssl,
)

with allure.step("Verify"):
assert response.status_code in (200, 204)


@pytest.mark.restapi
@allure.feature("Platform / Assets (REST API)")
Expand Down
Loading