Skip to content

test(integration): dynamic-stack CMA SDK sanity suite + AM 2.0 coverage + asset Content-Type fix (v1.10.1)#168

Open
aniket-shikhare-cstk wants to merge 5 commits into
developmentfrom
test/cma-python-integration-rewrite
Open

test(integration): dynamic-stack CMA SDK sanity suite + AM 2.0 coverage + asset Content-Type fix (v1.10.1)#168
aniket-shikhare-cstk wants to merge 5 commits into
developmentfrom
test/cma-python-integration-rewrite

Conversation

@aniket-shikhare-cstk

@aniket-shikhare-cstk aniket-shikhare-cstk commented Jun 26, 2026

Copy link
Copy Markdown

Summary

Adds a comprehensive, dynamic-stack live integration ("sanity") test suite for the CMA Python SDK under tests/integration/, replacing the ~2-year-old API tests. Each run creates a fresh stack, exercises every SDK resource method (positive / negative / edge cases) against the live CMA API, then tears the stack down. Modeled on the JS CMA SDK sanity suite, adapted to pytest.

Note: Targets development directly. This branch also contains PR #167's asset-scanning unit tests (already incorporated here), so #167 can be closed as superseded once this merges.

What's included

Framework (tests/integration/framework/)

  • setup.py — login → create stack → management token → (optional) Personalize project → flag-gated teardown
  • capture.py — monkeypatches requests to record every call (method/url/headers/body/cURL) + transient-error retries
  • report.py — self-contained dashboard HTML report (summary, coverage-by-resource, per-test request/response/cURL); honest status (passed-with-warnings, xfail)
  • helpers.py — generators, response/error validators, tracked_assert, scan-status polling
  • conftest.py — session fixtures (ctx, stack, store, AM stack, EICAR file), pytest-order sequencing, per-test header isolation, report hooks

Coverage (tests/integration/api/, 31 files) — user, org, stack, locale, environment, asset, taxonomy, terms, extension, webhook, global field, content type, label, entry, variant group, variants, entry variants, branch, alias, role, workflow, delivery/management tokens, release, release items, bulk ops, publish queue, metadata, audit log, oauth, AM 2.0 assets. Complex content-type schemas (modular blocks, groups, references, JSON RTE) round-tripped through entries.

AM 2.0 / asset scanning — covers include_asset_scan_status=true surfacing _asset_scan_status (pending → clean | quarantined), am-prefixed UIDs in DAM-enabled orgs, and the publish-only api_version: 3.2 header. The asset-scan suite skips gracefully when the DAM-enabled org isn't configured.

SDK bug fix (v1.10.1) — included here, see commit fix(assets): …

  • Asset.update() sent a JSON body with Content-Type: multipart/form-data → API 422. Fixed to application/json.
  • Asset.replace() set a bare multipart/form-data (no boundary) → API 422. Fixed to let the HTTP layer set the boundary.
  • Both verified against the live API and Contentstack's own JS SDK; version bumped 1.10.0 → 1.10.1 + CHANGELOG + unit-test assertion updated. Backward-compatible (the methods were previously always failing).

Notes for reviewers

OMpawar-21 and others added 5 commits June 25, 2026 14:30
Add a self-contained pytest integration suite under tests/integration that
creates a fresh stack per run, exercises every SDK resource method (positive,
negative, and edge cases) against the live CMA API, and tears the stack down.

- framework/: dynamic stack setup/teardown, request+cURL capture, response/error
  validators, tracked assertions, and a custom dashboard HTML report
- api/: 30 resource files with full method coverage
- data/: complex content-type schemas (modular blocks, groups, references,
  JSON RTE) and entry payloads
- strict, bug-catching assertions; genuine SDK/environment issues tracked via xfail
- timestamped HTML report + cURL log written to repo root (gitignored)

Also: update AGENTS.md to document the sanity suite + env vars, add
pytest/pytest-order to requirements, gitignore secrets/reports/docs.
Cover the asset scanning feature (DAM/AM 2.0) against both the normal org and
the AM 2.0 (DAM-enabled) org:

- normal-org scan tests (test_06_asset): upload returns _asset_scan_status
  'pending'; field absent unless include_asset_scan_status=true; clean file
  scans 'clean'; EICAR test file scans 'quarantined'; listing includes status
- test_31_am_assets: AM-org assets get 'am'-prefixed UIDs, full CRUD round-trip,
  the same scan lifecycle, publish() with the api_version: 3.2 header (publish-
  only; 404 on fetch)
- framework: am_stack fixture (stack in AM_ORG_UID; whole suite skips when
  unset), runtime-generated EICAR fixture (base64-encoded so the signature is
  not committed raw), wait_for_scan() polling helper, and api_version header
  reset for per-test isolation

Note: the correct query param is include_asset_scan_status (the response field
is _asset_scan_status); verified live.
Asset.update() forced Content-Type: multipart/form-data while sending a JSON
body, and Asset.replace() set a bare multipart/form-data header (no boundary)
while passing files=. Both made the CMA API reject the request with 422
'Please send a valid multipart/form-data payload', and both leaked the wrong
Content-Type onto subsequent requests on the shared client.

- update(): send the JSON body as application/json (matches the JS SDK and the
  live API, which returns 200)
- replace(): let the HTTP layer build the multipart body with a proper boundary

Bump version to 1.10.1 and update the asset update unit test accordingly.
- taxonomy/terms/global-field delete: drop Content-Type on the body-less DELETE
  (the SDK merges application/json, which the API rejects 400/500; the JS SDK
  omits it) — these now pass instead of xfail
- bulk update_workflow: provide a real target stage uid + notify field
- branch delete: poll-retry the transient 'branch not valid' (905) while the
  branch finishes async provisioning
- asset update/replace: flip from xfail to passing (fixed in the SDK, v1.10.1)
- capture/setup: retry transient network errors (ReadTimeout/ConnectionError)
  2x with backoff and bump the client timeout to 120s so dev11 blips don't fail
  the run
@aniket-shikhare-cstk aniket-shikhare-cstk requested a review from a team as a code owner June 26, 2026 12:34
@github-actions

Copy link
Copy Markdown

🔒 Security Scan Results

ℹ️ Note: Only vulnerabilities with available fixes (upgrades or patches) are counted toward thresholds.

Check Type Count (with fixes) Without fixes Threshold Result
🔴 Critical Severity 0 0 10 ✅ Passed
🟠 High Severity 0 1 25 ✅ Passed
🟡 Medium Severity 0 3 500 ✅ Passed
🔵 Low Severity 0 0 1000 ✅ Passed

⏱️ SLA Breach Summary

✅ No SLA breaches detected. All vulnerabilities are within acceptable time thresholds.

Severity Breaches (with fixes) Breaches (no fixes) SLA Threshold (with/no fixes) Status
🔴 Critical 0 0 15 / 30 days ✅ Passed
🟠 High 0 0 30 / 120 days ✅ Passed
🟡 Medium 0 0 90 / 365 days ✅ Passed
🔵 Low 0 0 180 / 365 days ✅ Passed

ℹ️ Vulnerabilities Without Available Fixes (Informational Only)

The following vulnerabilities were detected but do not have fixes available (no upgrade or patch). These are excluded from failure thresholds:

  • Critical without fixes: 0
  • High without fixes: 1
  • Medium without fixes: 3
  • Low without fixes: 0

✅ BUILD PASSED - All security checks passed

@aniket-shikhare-cstk aniket-shikhare-cstk changed the base branch from enhc/DX-8751 to development June 26, 2026 13:01

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new live “sanity” integration test suite under tests/integration/ that provisions a fresh stack per run, captures all CMA traffic into an HTML/cURL report, and exercises broad SDK resource coverage. It also includes an SDK fix for asset Content-Type handling (replace() boundary + update() JSON), bumps the package version to 1.10.1, and updates the changelog/agent docs.

Changes:

  • Introduces a full dynamic-stack integration framework (setup/teardown, request capture, HTML reporting, shared store/fixtures).
  • Adds ordered, per-resource live API coverage tests (including AM 2.0 asset scanning/publish behavior).
  • Fixes asset request header behavior for replace() and update(), and updates versioning/docs.

Reviewed changes

Copilot reviewed 46 out of 51 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
tests/unit/assets/test_assets_unit.py Updates asset unit assertions (incl. new scan-status/api_version coverage).
tests/integration/pytest.ini Integration-only pytest config (test discovery, markers, opts).
tests/integration/conftest.py Session fixtures, dynamic setup/teardown, per-test header isolation, report hooks.
tests/integration/framework/init.py Framework package marker.
tests/integration/framework/context.py Shared run context + cross-file UID store.
tests/integration/framework/helpers.py Generators, retry/wait helpers, response validators, tracked assertions.
tests/integration/framework/capture.py Monkeypatches requests.request to capture traffic + generate cURL + retry transients.
tests/integration/framework/report.py Renders a self-contained HTML dashboard + plain cURL log.
tests/integration/framework/setup.py Login + dynamic stack lifecycle + management token + optional Personalize project.
tests/integration/data/init.py Data package marker.
tests/integration/data/content_types.py Content-type schema payload factories for integration coverage.
tests/integration/data/entries.py Entry payload factories for integration coverage.
tests/integration/data/assets/extension.html HTML asset used for extension upload tests.
tests/integration/api/test_01_user.py Live tests for user endpoints and safe auth ops.
tests/integration/api/test_02_organization.py Live tests for organization endpoints and safe ownership negatives.
tests/integration/api/test_03_stack.py Live tests for stack endpoints (settings/users/sharing).
tests/integration/api/test_04_locale.py Live tests for locale CRUD + fallback operations.
tests/integration/api/test_05_environment.py Live tests for environment CRUD.
tests/integration/api/test_06_asset.py Live tests for asset CRUD, folders, versions, publish, and scan status.
tests/integration/api/test_07_taxonomy.py Live tests for taxonomy CRUD.
tests/integration/api/test_08_terms.py Live tests for taxonomy terms CRUD + hierarchy/search.
tests/integration/api/test_09_extension.py Live tests for extensions CRUD + upload.
tests/integration/api/test_10_webhook.py Live tests for webhook CRUD + executions/logs/retry.
tests/integration/api/test_11_global_field.py Live tests for global field CRUD + export.
tests/integration/api/test_12_content_type.py Live tests for content type CRUD + complex schema round-trip.
tests/integration/api/test_13_label.py Live tests for label CRUD.
tests/integration/api/test_14_entry.py Live tests for entry CRUD, complex content, atomic ops, localization, publish.
tests/integration/api/test_15_variant_group.py Live tests for variant group CRUD (Personalize-gated).
tests/integration/api/test_16_variants.py Live tests for variants CRUD (Personalize-gated).
tests/integration/api/test_17_entry_variants.py Live tests for entry variants endpoints (Personalize-gated).
tests/integration/api/test_18_branch.py Live tests for branch CRUD with force delete + retries.
tests/integration/api/test_19_alias.py Live tests for branch alias assign/find/fetch/delete.
tests/integration/api/test_20_role.py Live tests for role CRUD.
tests/integration/api/test_21_workflow.py Live tests for workflow CRUD, stages, publish rules, tasks.
tests/integration/api/test_22_delivery_token.py Live tests for delivery token CRUD.
tests/integration/api/test_23_management_token.py Live tests for management token CRUD.
tests/integration/api/test_24_release.py Live tests for release CRUD + clone.
tests/integration/api/test_25_release_item.py Live tests for release items operations.
tests/integration/api/test_26_bulk_operation.py Live tests for bulk publish/unpublish/delete/workflow update.
tests/integration/api/test_27_publish_queue.py Live tests for publish queue find/fetch/cancel.
tests/integration/api/test_28_metadata.py Live tests for metadata endpoints (extension-backed).
tests/integration/api/test_29_auditlog.py Live tests for audit log find/fetch.
tests/integration/api/test_30_oauth.py Live tests for OAuth handler surface (authorize URL, token accessors).
tests/integration/api/test_31_am_assets.py Live tests for AM 2.0 assets (UID prefix, scan, publish-only api_version header).
requirements.txt Adds integration-suite deps (pytest, pytest-order).
contentstack_management/assets/assets.py Fixes asset replace() multipart boundary handling and update() Content-Type logic.
contentstack_management/init.py Bumps SDK version to 1.10.1.
CHANGELOG.md Adds v1.10.1 entry describing asset header fixes.
AGENTS.md Documents new integration suite usage and env vars.
.gitignore Ignores integration outputs/env files and timestamped reports.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +14 to +16
Env vars (see .env.example):
Required: EMAIL, PASSWORD, HOST, ORGANIZATION
Optional: MFA_SECRET, DELETE_DYNAMIC_RESOURCES (default 'true')
Comment on lines +48 to +51
raise RuntimeError(
f"Missing required environment variables: {', '.join(missing)}. "
f"See tests/integration/.env.example."
)
Comment on lines +180 to 182
self.client.headers.pop("Content-Type", None)
files = {"asset": open(f"{file_path}",'rb')}
return self.client.put(url, headers = self.client.headers, params = self.params, files = files)
Comment on lines 412 to 416
url = f"assets/{self.asset_uid}"
Parameter.add_header(self, "Content-Type", "multipart/form-data")
# Updating an asset's title/description sends a JSON body, so it must use
# application/json. Forcing multipart/form-data here makes the API reject
# the request with 422 "Please send a valid multipart/form-data payload".
return self.client.put(url, headers = self.client.headers, params = self.params, data = data)
Comment on lines +138 to +142
for key, val in list(files.items()):
name = getattr(val[1] if isinstance(val, (tuple, list)) else val, "name", None)
if name and os.path.exists(name):
kwargs["files"][key] = (val[0], open(name, "rb"), val[2]) if isinstance(val, (tuple, list)) else open(name, "rb")
raise last_exc
Comment on lines +262 to +283
def test_fetch_includes_scan_status_param(self):
asset = self.client.stack(api_key).assets(asset_uid)
asset.add_param("_asset_scan_status", True)
response = asset.fetch()
self.assertIn("_asset_scan_status=True", response.request.url)
self.assertEqual(response.request.method, "GET")

def test_find_includes_scan_status_param(self):
asset = self.client.stack(api_key).assets()
asset.add_param("_asset_scan_status", True)
response = asset.find()
self.assertIn("_asset_scan_status=True", response.request.url)
self.assertEqual(response.request.method, "GET")

def test_upload_includes_scan_status_param(self):
file_path = "tests/resources/mock_assets/chaat.jpeg"
asset = self.client.stack(api_key).assets()
asset.add_param("_asset_scan_status", True)
response = asset.upload(file_path)
self.assertIn("_asset_scan_status=True", response.request.url)
self.assertEqual(response.request.method, "POST")

Comment on lines +284 to +307
def test_fetch_without_scan_status_param_field_absent(self):
response = self.client.stack(api_key).assets(asset_uid).fetch()
self.assertNotIn("_asset_scan_status", response.request.url)
self.assertEqual(response.request.method, "GET")

def test_find_without_scan_status_param_field_absent(self):
response = self.client.stack(api_key).assets().find()
self.assertNotIn("_asset_scan_status", response.request.url)
self.assertEqual(response.request.method, "GET")

def test_upload_without_scan_status_param_field_absent(self):
file_path = "tests/resources/mock_assets/chaat.jpeg"
response = self.client.stack(api_key).assets().upload(file_path)
self.assertNotIn("_asset_scan_status", response.request.url)
self.assertEqual(response.request.method, "POST")

def test_scan_status_param_coexists_with_other_params(self):
asset = self.client.stack(api_key).assets(asset_uid)
asset.add_param("locale", "en-us")
asset.add_param("_asset_scan_status", True)
response = asset.fetch()
self.assertIn("locale=en-us", response.request.url)
self.assertIn("_asset_scan_status=True", response.request.url)
self.assertEqual(response.request.method, "GET")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants