11import { expect } from 'chai' ;
22import sinon from 'sinon' ;
3- import { configHandler } from '@contentstack/cli-utilities' ;
43
54import ExportAssets from '../../../src/export/assets' ;
65import { CSAssetsExportAdapter } from '../../../src/export/base' ;
76import * as chunkedJsonReader from '../../../src/utils/chunked-json-reader' ;
7+ import * as exportHelpers from '../../../src/utils/export-helpers' ;
88import * as retryModule from '../../../src/utils/retry' ;
99
1010import type { CSAssetsAPIConfig , LinkedWorkspace } from '../../../src/types/cs-assets-api' ;
@@ -230,7 +230,7 @@ describe('ExportAssets', () => {
230230 } ) ;
231231
232232 it ( 'should append authtoken to URL when securedAssets is true' , async ( ) => {
233- sinon . stub ( configHandler , 'get ' ) . returns ( 'my-auth-token' ) ;
233+ sinon . stub ( exportHelpers , 'getSecuredAssetAuth ' ) . resolves ( { authtoken : 'my-auth-token' } ) ;
234234 wireStreaming ( [ { uid : 'a1' , url : 'https://cdn.example.com/a1.png' , filename : 'img.png' } ] as any ) ;
235235 fetchStub . callsFake ( async ( ) => makeFetchResponse ( ) as any ) ;
236236
@@ -241,7 +241,7 @@ describe('ExportAssets', () => {
241241 } ) ;
242242
243243 it ( 'should use "&" separator when URL already contains "?"' , async ( ) => {
244- sinon . stub ( configHandler , 'get ' ) . returns ( 'my-token' ) ;
244+ sinon . stub ( exportHelpers , 'getSecuredAssetAuth ' ) . resolves ( { authtoken : 'my-token' } ) ;
245245 wireStreaming ( [ { uid : 'a1' , url : 'https://cdn.example.com/a1?v=1' , filename : 'img.png' } ] as any ) ;
246246 fetchStub . callsFake ( async ( ) => makeFetchResponse ( ) as any ) ;
247247
@@ -279,4 +279,87 @@ describe('ExportAssets', () => {
279279 expect ( assetTicks [ 0 ] . args [ 2 ] ) . to . equal ( 'No response body' ) ;
280280 } ) ;
281281 } ) ;
282+
283+ describe ( 'secured asset downloads' , ( ) => {
284+ const securedContext : ExportContext = { ...exportContext , securedAssets : true } ;
285+ const asset = { uid : 'a1' , url : 'https://cdn.example.com/a1.png' , filename : 'img.png' } ;
286+ const make401 = ( ) => ( { ok : false , status : 401 , headers : { get : ( ) : string | null => null } } ) ;
287+
288+ it ( 'should send the Authorization header (and no authtoken param) for OAuth' , async ( ) => {
289+ wireStreaming ( [ asset ] as any ) ;
290+ sinon . stub ( exportHelpers , 'getSecuredAssetAuth' ) . resolves ( { headers : { authorization : 'Bearer oauth-token' } } ) ;
291+ fetchStub . callsFake ( async ( ) => makeFetchResponse ( ) as any ) ;
292+
293+ const exporter = new ExportAssets ( apiConfig , securedContext ) ;
294+ await exporter . start ( workspace , spaceDir ) ;
295+
296+ const [ url , init ] = fetchStub . firstCall . args ;
297+ expect ( url ) . to . equal ( asset . url ) ;
298+ expect ( init ) . to . deep . equal ( { headers : { authorization : 'Bearer oauth-token' } } ) ;
299+ } ) ;
300+
301+ it ( 'should append ?authtoken= (and no headers) for basic auth' , async ( ) => {
302+ wireStreaming ( [ asset ] as any ) ;
303+ sinon . stub ( exportHelpers , 'getSecuredAssetAuth' ) . resolves ( { authtoken : 'basic-token' } ) ;
304+ fetchStub . callsFake ( async ( ) => makeFetchResponse ( ) as any ) ;
305+
306+ const exporter = new ExportAssets ( apiConfig , securedContext ) ;
307+ await exporter . start ( workspace , spaceDir ) ;
308+
309+ const [ url , init ] = fetchStub . firstCall . args ;
310+ expect ( url ) . to . equal ( `${ asset . url } ?authtoken=basic-token` ) ;
311+ expect ( init ) . to . be . undefined ;
312+ } ) ;
313+
314+ it ( 'should not resolve auth or attach credentials for unsecured exports' , async ( ) => {
315+ wireStreaming ( [ asset ] as any ) ;
316+ const authStub = sinon . stub ( exportHelpers , 'getSecuredAssetAuth' ) ;
317+ fetchStub . callsFake ( async ( ) => makeFetchResponse ( ) as any ) ;
318+
319+ const exporter = new ExportAssets ( apiConfig , exportContext ) ;
320+ await exporter . start ( workspace , spaceDir ) ;
321+
322+ expect ( authStub . called ) . to . be . false ;
323+ const [ url , init ] = fetchStub . firstCall . args ;
324+ expect ( url ) . to . equal ( asset . url ) ;
325+ expect ( init ) . to . be . undefined ;
326+ } ) ;
327+
328+ it ( 'should force-refresh once on 401 and succeed with the fresh token' , async ( ) => {
329+ wireStreaming ( [ asset ] as any ) ;
330+ const authStub = sinon
331+ . stub ( exportHelpers , 'getSecuredAssetAuth' )
332+ . callsFake ( async ( force ?: boolean ) =>
333+ force ? { headers : { authorization : 'Bearer fresh' } } : { headers : { authorization : 'Bearer stale' } } ,
334+ ) ;
335+ fetchStub . callsFake ( async ( _url : any , init : any ) =>
336+ init ?. headers ?. authorization === 'Bearer fresh' ? ( makeFetchResponse ( ) as any ) : ( make401 ( ) as any ) ,
337+ ) ;
338+
339+ const exporter = new ExportAssets ( apiConfig , securedContext ) ;
340+ await exporter . start ( workspace , spaceDir ) ;
341+
342+ expect ( authStub . calledWith ( true ) ) . to . be . true ;
343+ expect ( fetchStub . callCount ) . to . equal ( 2 ) ;
344+ const tickStub = ( CSAssetsExportAdapter . prototype as any ) . tick as sinon . SinonStub ;
345+ const assetTicks = tickStub . getCalls ( ) . filter ( ( c ) => String ( c . args [ 1 ] ) . startsWith ( 'asset:' ) ) ;
346+ expect ( assetTicks ) . to . have . length ( 1 ) ;
347+ expect ( assetTicks [ 0 ] . args [ 0 ] ) . to . be . true ;
348+ } ) ;
349+
350+ it ( 'should abort the space with SecuredAssetAuthError when 401 persists after refresh' , async ( ) => {
351+ wireStreaming ( assetItems ) ;
352+ sinon . stub ( exportHelpers , 'getSecuredAssetAuth' ) . resolves ( { headers : { authorization : 'Bearer bad' } } ) ;
353+ fetchStub . callsFake ( async ( ) => make401 ( ) as any ) ;
354+
355+ const exporter = new ExportAssets ( apiConfig , securedContext ) ;
356+ try {
357+ await exporter . start ( workspace , spaceDir ) ;
358+ expect . fail ( 'should have thrown' ) ;
359+ } catch ( e : any ) {
360+ expect ( e ) . to . be . instanceOf ( exportHelpers . SecuredAssetAuthError ) ;
361+ expect ( e . message ) . to . include ( '401' ) ;
362+ }
363+ } ) ;
364+ } ) ;
282365} ) ;
0 commit comments