@@ -7,6 +7,7 @@ const { bucketGet } = require('../../../lib/api/bucketGet');
77const bucketGetCors = require ( '../../../lib/api/bucketGetCors' ) ;
88const { bucketPut } = require ( '../../../lib/api/bucketPut' ) ;
99const bucketPutCors = require ( '../../../lib/api/bucketPutCors' ) ;
10+ const DummyRequest = require ( '../DummyRequest' ) ;
1011const {
1112 CorsConfigTester,
1213 DummyRequestLogger,
@@ -86,7 +87,10 @@ function setupBucketWithCors(done) {
8687}
8788
8889function buildRequest ( spec ) {
89- const req = {
90+ // DummyRequest is an http.IncomingMessage stream that emits 'end'
91+ // synchronously. We need that because callApiMethod's waterfall
92+ // waits for the request body on non-objectPut paths.
93+ return new DummyRequest ( {
9094 bucketName,
9195 objectKey : spec . objectKey ,
9296 headers : {
@@ -96,12 +100,7 @@ function buildRequest(spec) {
96100 url : spec . url ,
97101 query : spec . query ,
98102 method : spec . httpMethod ,
99- actionImplicitDenies : false ,
100- socket : { remoteAddress : '127.0.0.1' , destroy : ( ) => { } } ,
101- } ;
102- // Pretend the HTTP body is already drained so on('end') does not fire.
103- req . on = ( ) => req ;
104- return req ;
103+ } , Buffer . alloc ( 0 ) ) ;
105104}
106105
107106function buildResponseSpy ( sandbox ) {
@@ -218,6 +217,63 @@ describe('CORS headers on 403 auth failures (api.callApiMethod)', () => {
218217 } ) ;
219218} ) ;
220219
220+ describe ( 'CORS headers on 403 via handler (fast path)' , ( ) => {
221+ // Verifies the wrapper's fast path: when auth succeeds but the
222+ // handler's own ACL/policy check denies, the handler has already
223+ // loaded the bucket and passed corsHeaders through its callback.
224+ // The wrapper should forward them without calling metadata.getBucket
225+ // itself.
226+ let sandbox ;
227+
228+ before ( done => setupBucketWithCors ( done ) ) ;
229+
230+ beforeEach ( ( ) => {
231+ sandbox = sinon . createSandbox ( ) ;
232+ // Stub auth to succeed as a *different* account. The handler then
233+ // runs standardMetadataValidateBucket which denies because the
234+ // bucket is owned by accessKey1.
235+ const otherAuth = makeAuthInfo ( 'accessKey2' ) ;
236+ const authServer = {
237+ doAuth : sandbox . stub ( ) . callsArgWith ( 2 , null , otherAuth ,
238+ [ { isAllowed : true , isImplicit : false } ] , null , { } ) ,
239+ } ;
240+ sandbox . stub ( auth , 'server' ) . value ( authServer ) ;
241+ } ) ;
242+
243+ afterEach ( ( ) => sandbox . restore ( ) ) ;
244+
245+ it ( 'forwards handler-provided corsHeaders without setting headers '
246+ + 'on the response directly' , done => {
247+ const request = buildRequest ( {
248+ apiMethod : 'bucketGet' , httpMethod : 'GET' ,
249+ url : '/' , query : { } ,
250+ } ) ;
251+ const response = buildResponseSpy ( sandbox ) ;
252+ const log = buildLog ( sandbox ) ;
253+
254+ api . callApiMethod ( 'bucketGet' , request , response , log ,
255+ ( err , xml , corsHeaders ) => {
256+ assert ( err , 'expected an error' ) ;
257+ assert ( err . is && err . is . AccessDenied ,
258+ `expected AccessDenied, got ${ err . code } ` ) ;
259+ assert ( corsHeaders ,
260+ 'handler should have supplied corsHeaders' ) ;
261+ assert . strictEqual (
262+ corsHeaders [ 'access-control-allow-origin' ] , origin ,
263+ 'corsHeaders should include access-control-allow-origin' ) ;
264+ // Fast path: the wrapper did not setHeader on the response.
265+ // The route-level transport is what would ultimately call
266+ // setCommonResponseHeaders in production.
267+ assert . strictEqual (
268+ response . getHeader ( 'access-control-allow-origin' ) ,
269+ undefined ,
270+ 'wrapper should not set CORS headers directly when the '
271+ + 'handler already provided them' ) ;
272+ done ( ) ;
273+ } ) ;
274+ } ) ;
275+ } ) ;
276+
221277describe ( 'CORS headers on 200 successful responses (per-handler)' , ( ) => {
222278 // Sanity-check that the existing per-handler path continues to work.
223279 // Pass-through tests: a request with matching CORS should receive
0 commit comments