From e776cdbb864cdf745e6df07355733c3707987b8c Mon Sep 17 00:00:00 2001 From: reeshika-h Date: Thu, 25 Jun 2026 10:30:20 +0530 Subject: [PATCH 1/2] feat: add asset scan status tests and bulk publish validation for API version 3.2 --- test/sanity-check/api/asset-test.js | 80 +++++++++++++++++++ test/sanity-check/api/bulkOperation-test.js | 24 ++++++ test/unit/asset-test.js | 86 +++++++++++++++++++++ test/unit/bulkOperation-test.js | 21 +++++ 4 files changed, 211 insertions(+) diff --git a/test/sanity-check/api/asset-test.js b/test/sanity-check/api/asset-test.js index cc69946b..96dc9187 100644 --- a/test/sanity-check/api/asset-test.js +++ b/test/sanity-check/api/asset-test.js @@ -981,4 +981,84 @@ describe('Asset API Tests', () => { } }) }) + + // ========================================================================== + // ASSET SCAN STATUS + // ========================================================================== + + describe('Asset Scan Status', () => { + let assetUid + + before(async function () { + this.timeout(30000) + // Reuse an asset created by the Asset Upload block to avoid an extra upload. + // Fall back to creating a fresh one only if that block didn't succeed. + if (testData.assets.image && testData.assets.image.uid) { + assetUid = testData.assets.image.uid + return + } + try { + const asset = await stack.asset().create({ + upload: assetPath, + title: `Scan Status Test Asset ${Date.now()}`, + description: 'Asset for scan status testing' + }) + assetUid = asset.uid + } catch (err) { + // Asset creation failed — individual tests will skip themselves. + } + }) + + it('should accept include_asset_scan_status param on single asset fetch', async function () { + if (!assetUid) return this.skip() + const asset = await stack.asset(assetUid).fetch({ include_asset_scan_status: true }) + + expect(asset).to.be.an('object') + expect(asset.uid).to.equal(assetUid) + // _asset_scan_status is opt-in. When scanning is enabled: pending | clean | quarantined. + // When scanning is not enabled for the stack, the API returns 'not_scanned'. + if ('_asset_scan_status' in asset) { + expect(asset._asset_scan_status).to.be.a('string') + expect(['pending', 'clean', 'quarantined', 'not_scanned']).to.include(asset._asset_scan_status) + } + }) + + it('should accept include_asset_scan_status param on asset list query', async function () { + const response = await stack.asset().query({ include_asset_scan_status: true }).find() + + expect(response).to.be.an('object') + expect(response.items).to.be.an('array') + // pending | clean | quarantined when scanning is enabled; not_scanned otherwise. + if (response.items.length > 0 && '_asset_scan_status' in response.items[0]) { + expect(response.items[0]._asset_scan_status).to.be.a('string') + expect(['pending', 'clean', 'quarantined', 'not_scanned']).to.include(response.items[0]._asset_scan_status) + } + }) + + it('should not return _asset_scan_status when param is omitted', async function () { + if (!assetUid) return this.skip() + const asset = await stack.asset(assetUid).fetch() + + expect(asset).to.be.an('object') + expect(asset).to.not.have.property('_asset_scan_status') + }) + + it('should accept include_asset_scan_status param on asset upload', async function () { + this.timeout(30000) + const asset = await stack.asset().create( + { + upload: assetPath, + title: `Scan Status Upload Test ${Date.now()}` + }, + { include_asset_scan_status: true } + ) + + expect(asset).to.be.an('object') + expect(asset.uid).to.be.a('string') + // pending when scanning is enabled; not_scanned when scanning is not enabled for the stack. + if ('_asset_scan_status' in asset) { + expect(['pending', 'not_scanned']).to.include(asset._asset_scan_status) + } + }) + }) }) diff --git a/test/sanity-check/api/bulkOperation-test.js b/test/sanity-check/api/bulkOperation-test.js index af4a5270..bfb26223 100644 --- a/test/sanity-check/api/bulkOperation-test.js +++ b/test/sanity-check/api/bulkOperation-test.js @@ -544,6 +544,30 @@ describe('BulkOperation api test', () => { .catch(done) }) + // Asset Scanning: bulk publish must send api_version: 3.2 so the CDA runs scan validation. + // Without this header, quarantined assets incorrectly appear as published in the UI. + describe('Asset Scanning — Bulk Publish with api_version: 3.2', function () { + it('should bulk publish assets with api_version: 3.2 header for CDA scan validation', async function () { + this.timeout(30000) + const assets = assetsWithValidUids() + if (assets.length === 0) { + return this.skip() + } + const publishDetails = { + assets, + locales: ['en-us'], + environments: [envName] + } + + const response = await doBulkOperation().publish({ details: publishDetails, api_version: '3.2' }) + + // Bulk publish always returns a job_id regardless of individual asset scan status. + // Actual scan failures surface asynchronously in the Publish Queue UI. + expect(response.notice).to.be.a('string') + expect(response.job_id).to.be.a('string') + }) + }) + // DX-4430 regression: SDK was masking real API errors (401+error_code 161/294) with // generic "Session timed out, please login to proceed" / "Unable to refresh token". // Fix: NON_AUTH_401_ERROR_CODES={161,294} bypass token refresh and surface original error. diff --git a/test/unit/asset-test.js b/test/unit/asset-test.js index 6a7171cb..34114483 100644 --- a/test/unit/asset-test.js +++ b/test/unit/asset-test.js @@ -463,6 +463,92 @@ describe('Contentstack Asset test', () => { }) .catch(done) }) + + // Asset Scanning tests + it('Asset fetch sends include_asset_scan_status as query param and returns _asset_scan_status', done => { + var mock = new MockAdapter(Axios) + mock.onGet('/assets/UID').reply((config) => { + expect(config.params.include_asset_scan_status).to.equal(true) + return [200, { + asset: { + ...assetMock, + _asset_scan_status: 'clean' + } + }] + }) + makeAsset({ + asset: { ...systemUidMock }, + stackHeaders: stackHeadersMock + }) + .fetch({ include_asset_scan_status: true }) + .then((asset) => { + checkAsset(asset) + expect(asset._asset_scan_status).to.equal('clean') + done() + }) + .catch(done) + }) + + it('Asset query sends include_asset_scan_status as query param and returns _asset_scan_status', done => { + var mock = new MockAdapter(Axios) + mock.onGet('/assets').reply((config) => { + expect(config.params.include_asset_scan_status).to.equal(true) + return [200, { + assets: [{ + ...assetMock, + _asset_scan_status: 'clean' + }] + }] + }) + makeAsset() + .query({ include_asset_scan_status: true }) + .find() + .then((collection) => { + expect(collection.items[0]._asset_scan_status).to.equal('clean') + done() + }) + .catch(done) + }) + + it('Asset create sends include_asset_scan_status as query param and returns pending scan status', done => { + var mock = new MockAdapter(Axios) + mock.onPost('/assets').reply((config) => { + expect(config.params.include_asset_scan_status).to.equal(true) + return [200, { + asset: { + ...assetMock, + _asset_scan_status: 'pending' + } + }] + }) + makeAsset() + .create( + { upload: path.join(__dirname, '../sanity-check/mock/customUpload.html') }, + { include_asset_scan_status: true } + ) + .then((asset) => { + expect(asset._asset_scan_status).to.equal('pending') + done() + }) + .catch(done) + }) + + it('Asset fetch without include_asset_scan_status does not return _asset_scan_status', done => { + var mock = new MockAdapter(Axios) + mock.onGet('/assets/UID').reply(200, { + asset: { ...assetMock } + }) + makeAsset({ + asset: { ...systemUidMock }, + stackHeaders: stackHeadersMock + }) + .fetch() + .then((asset) => { + expect(asset._asset_scan_status).to.equal(undefined) + done() + }) + .catch(done) + }) }) function makeAsset (data) { diff --git a/test/unit/bulkOperation-test.js b/test/unit/bulkOperation-test.js index 6d067c59..ce4272d4 100644 --- a/test/unit/bulkOperation-test.js +++ b/test/unit/bulkOperation-test.js @@ -107,6 +107,27 @@ describe('Contentstack BulkOperation test', () => { expect(response.job_id).to.not.equal(undefined) }) + it('should send api_version 3.2 header when bulk publishing assets for scan validation', async () => { + const publishDetails = { + assets: [{ uid: 'asset_uid', version: 1, locale: 'en-us' }], + locales: ['en-us'], + environments: ['development'] + } + + const mock = new MockAdapter(Axios) + mock.onPost('/bulk/publish').reply((config) => { + expect(config.headers.api_version).to.equal('3.2') + return [200, { + notice: 'Your bulk publish request is in progress. Please check publish queue for more details.', + job_id: 'job_id' + }] + }) + + const response = await makeBulkOperation().publish({ details: publishDetails, api_version: '3.2' }) + expect(response.notice).to.include('bulk publish') + expect(response.job_id).to.not.equal(undefined) + }) + it('should unpublish items in bulk', async () => { const unpublishDetails = { entries: [ From dc5988aaadc5200561ccd1762f8ba35f935a3e06 Mon Sep 17 00:00:00 2001 From: reeshika-h Date: Thu, 25 Jun 2026 12:48:43 +0530 Subject: [PATCH 2/2] refactor: remove asset scanning tests for bulk publish with api_version: 3.2 --- test/sanity-check/api/bulkOperation-test.js | 24 --------------------- test/unit/bulkOperation-test.js | 21 ------------------ 2 files changed, 45 deletions(-) diff --git a/test/sanity-check/api/bulkOperation-test.js b/test/sanity-check/api/bulkOperation-test.js index bfb26223..af4a5270 100644 --- a/test/sanity-check/api/bulkOperation-test.js +++ b/test/sanity-check/api/bulkOperation-test.js @@ -544,30 +544,6 @@ describe('BulkOperation api test', () => { .catch(done) }) - // Asset Scanning: bulk publish must send api_version: 3.2 so the CDA runs scan validation. - // Without this header, quarantined assets incorrectly appear as published in the UI. - describe('Asset Scanning — Bulk Publish with api_version: 3.2', function () { - it('should bulk publish assets with api_version: 3.2 header for CDA scan validation', async function () { - this.timeout(30000) - const assets = assetsWithValidUids() - if (assets.length === 0) { - return this.skip() - } - const publishDetails = { - assets, - locales: ['en-us'], - environments: [envName] - } - - const response = await doBulkOperation().publish({ details: publishDetails, api_version: '3.2' }) - - // Bulk publish always returns a job_id regardless of individual asset scan status. - // Actual scan failures surface asynchronously in the Publish Queue UI. - expect(response.notice).to.be.a('string') - expect(response.job_id).to.be.a('string') - }) - }) - // DX-4430 regression: SDK was masking real API errors (401+error_code 161/294) with // generic "Session timed out, please login to proceed" / "Unable to refresh token". // Fix: NON_AUTH_401_ERROR_CODES={161,294} bypass token refresh and surface original error. diff --git a/test/unit/bulkOperation-test.js b/test/unit/bulkOperation-test.js index ce4272d4..6d067c59 100644 --- a/test/unit/bulkOperation-test.js +++ b/test/unit/bulkOperation-test.js @@ -107,27 +107,6 @@ describe('Contentstack BulkOperation test', () => { expect(response.job_id).to.not.equal(undefined) }) - it('should send api_version 3.2 header when bulk publishing assets for scan validation', async () => { - const publishDetails = { - assets: [{ uid: 'asset_uid', version: 1, locale: 'en-us' }], - locales: ['en-us'], - environments: ['development'] - } - - const mock = new MockAdapter(Axios) - mock.onPost('/bulk/publish').reply((config) => { - expect(config.headers.api_version).to.equal('3.2') - return [200, { - notice: 'Your bulk publish request is in progress. Please check publish queue for more details.', - job_id: 'job_id' - }] - }) - - const response = await makeBulkOperation().publish({ details: publishDetails, api_version: '3.2' }) - expect(response.notice).to.include('bulk publish') - expect(response.job_id).to.not.equal(undefined) - }) - it('should unpublish items in bulk', async () => { const unpublishDetails = { entries: [