@@ -10,6 +10,7 @@ import {
1010 generatePresignedUrl ,
1111 hasObjectStoreClient ,
1212 parseStorageUri ,
13+ resolveStoreProtocolForPacketPresign ,
1314 uploadPacketToObjectStore ,
1415} from "~/v3/objectStore.server" ;
1516
@@ -103,6 +104,36 @@ describe("Object Storage", () => {
103104 } ) ;
104105 } ) ;
105106
107+ describe ( "resolveStoreProtocolForPacketPresign" , ( ) => {
108+ afterEach ( ( ) => {
109+ env . OBJECT_STORE_DEFAULT_PROTOCOL = originalEnvObj . OBJECT_STORE_DEFAULT_PROTOCOL ;
110+ } ) ;
111+
112+ it ( "GET uses legacy default for unprefixed keys even when OBJECT_STORE_DEFAULT_PROTOCOL is set" , ( ) => {
113+ env . OBJECT_STORE_DEFAULT_PROTOCOL = "s3" ;
114+ expect ( resolveStoreProtocolForPacketPresign ( "a/b.json" , "GET" ) . storeProtocol ) . toBeUndefined ( ) ;
115+ } ) ;
116+
117+ it ( "PUT without forceNoPrefix uses OBJECT_STORE_DEFAULT_PROTOCOL for unprefixed keys" , ( ) => {
118+ env . OBJECT_STORE_DEFAULT_PROTOCOL = "s3" ;
119+ expect ( resolveStoreProtocolForPacketPresign ( "a/b.json" , "PUT" , false ) . storeProtocol ) . toBe (
120+ "s3"
121+ ) ;
122+ } ) ;
123+
124+ it ( "PUT with forceNoPrefix skips OBJECT_STORE_DEFAULT_PROTOCOL for unprefixed keys" , ( ) => {
125+ env . OBJECT_STORE_DEFAULT_PROTOCOL = "s3" ;
126+ expect ( resolveStoreProtocolForPacketPresign ( "a/b.json" , "PUT" , true ) . storeProtocol ) . toBeUndefined ( ) ;
127+ } ) ;
128+
129+ it ( "explicit protocol in key wins for PUT with forceNoPrefix" , ( ) => {
130+ env . OBJECT_STORE_DEFAULT_PROTOCOL = "r2" ;
131+ expect ( resolveStoreProtocolForPacketPresign ( "s3://x/y.json" , "PUT" , true ) . storeProtocol ) . toBe (
132+ "s3"
133+ ) ;
134+ } ) ;
135+ } ) ;
136+
106137 postgresAndMinioTest (
107138 "should upload and download data without protocol (legacy)" ,
108139 async ( { minioConfig, prisma } ) => {
@@ -356,6 +387,7 @@ describe("Object Storage", () => {
356387 const putResult = await generatePresignedUrl ( projectRef , envSlug , filename , "PUT" ) ;
357388 expect ( putResult . success ) . toBe ( true ) ;
358389 if ( ! putResult . success ) throw new Error ( putResult . error ) ;
390+ expect ( putResult . storagePath ) . toBe ( filename ) ;
359391
360392 const putResponse = await fetch ( putResult . url , {
361393 method : "PUT" ,
@@ -368,13 +400,106 @@ describe("Object Storage", () => {
368400 const getResult = await generatePresignedUrl ( projectRef , envSlug , filename , "GET" ) ;
369401 expect ( getResult . success ) . toBe ( true ) ;
370402 if ( ! getResult . success ) throw new Error ( getResult . error ) ;
403+ expect ( getResult . storagePath ) . toBeUndefined ( ) ;
371404
372405 const getResponse = await fetch ( getResult . url ) ;
373406 expect ( getResponse . ok ) . toBe ( true ) ;
374407 expect ( await getResponse . text ( ) ) . toBe ( data ) ;
375408 }
376409 ) ;
377410
411+ postgresAndMinioTest (
412+ "generatePresignedUrl - PUT unprefixed with OBJECT_STORE_DEFAULT_PROTOCOL returns s3 storagePath" ,
413+ async ( { minioConfig } ) => {
414+ env . OBJECT_STORE_BASE_URL = undefined ;
415+ env . OBJECT_STORE_ACCESS_KEY_ID = undefined ;
416+ env . OBJECT_STORE_SECRET_ACCESS_KEY = undefined ;
417+ env . OBJECT_STORE_REGION = undefined ;
418+
419+ process . env . OBJECT_STORE_S3_BASE_URL = minioConfig . baseUrl ;
420+ process . env . OBJECT_STORE_S3_ACCESS_KEY_ID = minioConfig . accessKeyId ;
421+ process . env . OBJECT_STORE_S3_SECRET_ACCESS_KEY = minioConfig . secretAccessKey ;
422+ process . env . OBJECT_STORE_S3_REGION = minioConfig . region ;
423+ process . env . OBJECT_STORE_S3_SERVICE = "s3" ;
424+
425+ env . OBJECT_STORE_DEFAULT_PROTOCOL = "s3" ;
426+
427+ const projectRef = "proj_presign_v2_style" ;
428+ const envSlug = "dev" ;
429+ const filename = "v2-style/payload.json" ;
430+ const data = JSON . stringify ( { v2 : true } ) ;
431+
432+ const putResult = await generatePresignedUrl ( projectRef , envSlug , filename , "PUT" ) ;
433+ expect ( putResult . success ) . toBe ( true ) ;
434+ if ( ! putResult . success ) throw new Error ( putResult . error ) ;
435+ expect ( putResult . storagePath ) . toBe ( `s3://${ filename } ` ) ;
436+
437+ const putResponse = await fetch ( putResult . url , {
438+ method : "PUT" ,
439+ headers : { "Content-Type" : "application/json" } ,
440+ body : data ,
441+ } ) ;
442+ expect ( putResponse . ok ) . toBe ( true ) ;
443+
444+ const getResult = await generatePresignedUrl ( projectRef , envSlug , putResult . storagePath ! , "GET" ) ;
445+ expect ( getResult . success ) . toBe ( true ) ;
446+ if ( ! getResult . success ) throw new Error ( getResult . error ) ;
447+
448+ const getResponse = await fetch ( getResult . url ) ;
449+ expect ( getResponse . ok ) . toBe ( true ) ;
450+ expect ( await getResponse . text ( ) ) . toBe ( data ) ;
451+
452+ delete process . env . OBJECT_STORE_S3_BASE_URL ;
453+ delete process . env . OBJECT_STORE_S3_ACCESS_KEY_ID ;
454+ delete process . env . OBJECT_STORE_S3_SECRET_ACCESS_KEY ;
455+ delete process . env . OBJECT_STORE_S3_REGION ;
456+ delete process . env . OBJECT_STORE_S3_SERVICE ;
457+ env . OBJECT_STORE_DEFAULT_PROTOCOL = undefined ;
458+ }
459+ ) ;
460+
461+ postgresAndMinioTest (
462+ "generatePresignedUrl - forceNoPrefix PUT fails without legacy default when only S3 named" ,
463+ async ( { minioConfig } ) => {
464+ env . OBJECT_STORE_BASE_URL = undefined ;
465+ env . OBJECT_STORE_ACCESS_KEY_ID = undefined ;
466+ env . OBJECT_STORE_SECRET_ACCESS_KEY = undefined ;
467+ env . OBJECT_STORE_REGION = undefined ;
468+
469+ process . env . OBJECT_STORE_S3_BASE_URL = minioConfig . baseUrl ;
470+ process . env . OBJECT_STORE_S3_ACCESS_KEY_ID = minioConfig . accessKeyId ;
471+ process . env . OBJECT_STORE_S3_SECRET_ACCESS_KEY = minioConfig . secretAccessKey ;
472+ process . env . OBJECT_STORE_S3_REGION = minioConfig . region ;
473+ process . env . OBJECT_STORE_S3_SERVICE = "s3" ;
474+
475+ env . OBJECT_STORE_DEFAULT_PROTOCOL = "s3" ;
476+
477+ const putLegacy = await generatePresignedUrl (
478+ "proj_force_noprefix" ,
479+ "dev" ,
480+ "only-legacy/payload.json" ,
481+ "PUT" ,
482+ { forceNoPrefix : true }
483+ ) ;
484+ expect ( putLegacy . success ) . toBe ( false ) ;
485+
486+ const getUnprefixed = await generatePresignedUrl (
487+ "proj_force_noprefix" ,
488+ "dev" ,
489+ "any.json" ,
490+ "GET"
491+ ) ;
492+ expect ( getUnprefixed . success ) . toBe ( false ) ;
493+
494+ delete process . env . OBJECT_STORE_S3_BASE_URL ;
495+ delete process . env . OBJECT_STORE_S3_ACCESS_KEY_ID ;
496+ delete process . env . OBJECT_STORE_S3_SECRET_ACCESS_KEY ;
497+ delete process . env . OBJECT_STORE_S3_REGION ;
498+ delete process . env . OBJECT_STORE_S3_SERVICE ;
499+ env . OBJECT_STORE_DEFAULT_PROTOCOL = undefined ;
500+ }
501+ ) ;
502+
378503 postgresAndMinioTest (
379504 "generatePresignedUrl - PUT then GET round-trip (IAM credential chain / AWS SDK path)" ,
380505 async ( { minioConfig } ) => {
@@ -398,6 +523,7 @@ describe("Object Storage", () => {
398523 const putResult = await generatePresignedUrl ( projectRef , envSlug , filename , "PUT" ) ;
399524 expect ( putResult . success ) . toBe ( true ) ;
400525 if ( ! putResult . success ) throw new Error ( putResult . error ) ;
526+ expect ( putResult . storagePath ) . toBe ( filename ) ;
401527
402528 const putResponse = await fetch ( putResult . url , {
403529 method : "PUT" ,
0 commit comments