@@ -62,6 +62,15 @@ describe("S3Client", () => {
6262 expect ( client . getEndpointUrl ( ) ) . toBe ( "https://minio.example.com" ) ;
6363 } ) ;
6464
65+ it ( "应当保留 endpoint 中的路径前缀" , ( ) => {
66+ const client = new S3Client ( {
67+ ...defaultConfig ,
68+ endpoint : "https://abcdefg.supabase.co/storage/v1/s3" ,
69+ } ) ;
70+
71+ expect ( client . getEndpointUrl ( ) ) . toBe ( "https://abcdefg.supabase.co/storage/v1/s3" ) ;
72+ } ) ;
73+
6574 it ( "应当支持 http:// 协议的 endpoint" , ( ) => {
6675 const client = new S3Client ( {
6776 ...defaultConfig ,
@@ -280,6 +289,71 @@ describe("S3Client", () => {
280289 expect ( url ) . toBe ( "https://s3.us-west-2.amazonaws.com/my-bucket" ) ;
281290 } ) ;
282291
292+ it ( "应当在 endpoint 带路径前缀时把前缀带进 path-style 请求 URL" , async ( ) => {
293+ const prefixClient = new S3Client ( {
294+ ...defaultConfig ,
295+ endpoint : "https://abcdefg.supabase.co/storage/v1/s3" ,
296+ } ) ;
297+ fetchSpy . mockResolvedValue ( new Response ( "" , { status : 200 } ) ) ;
298+
299+ await prefixClient . request ( "GET" , "my-bucket" , "folder/file.txt" ) ;
300+
301+ const [ url ] = fetchSpy . mock . calls [ 0 ] ;
302+ expect ( url ) . toBe ( "https://abcdefg.supabase.co/storage/v1/s3/my-bucket/folder/file.txt" ) ;
303+ } ) ;
304+
305+ it ( "应当在 endpoint 带路径前缀时把前缀带进 virtual-hosted 请求 URL" , async ( ) => {
306+ const prefixClient = new S3Client ( {
307+ ...defaultConfig ,
308+ endpoint : "https://s3.example.com/gateway" ,
309+ forcePathStyle : false ,
310+ } ) ;
311+ fetchSpy . mockResolvedValue ( new Response ( "" , { status : 200 } ) ) ;
312+
313+ await prefixClient . request ( "GET" , "my-bucket" , "file.txt" ) ;
314+
315+ const [ url ] = fetchSpy . mock . calls [ 0 ] ;
316+ expect ( url ) . toBe ( "https://my-bucket.s3.example.com/gateway/file.txt" ) ;
317+ } ) ;
318+
319+ it ( "应当在 endpoint 带路径前缀时把前缀纳入签名的 canonical URI" , async ( ) => {
320+ // 两个 client 的绝对请求路径完全相同(/storage/v1/s3/my-bucket/file.txt),
321+ // 只有 endpoint 前缀与 bucket/key 的切分位置不同;签名只取决于绝对路径,因此必须一致。
322+ const viaEndpointPrefix = new S3Client ( {
323+ ...defaultConfig ,
324+ endpoint : "https://abcdefg.supabase.co/storage/v1/s3" ,
325+ } ) ;
326+ const viaBucketPath = new S3Client ( {
327+ ...defaultConfig ,
328+ endpoint : "https://abcdefg.supabase.co/storage/v1" ,
329+ } ) ;
330+ vi . useFakeTimers ( ) ;
331+ vi . setSystemTime ( new Date ( "2026-01-01T00:00:00Z" ) ) ;
332+ fetchSpy . mockResolvedValue ( new Response ( "" , { status : 200 } ) ) ;
333+
334+ await viaEndpointPrefix . request ( "GET" , "my-bucket" , "file.txt" ) ;
335+ await viaBucketPath . request ( "GET" , "s3" , "my-bucket/file.txt" ) ;
336+ vi . useRealTimers ( ) ;
337+
338+ const [ url1 , options1 ] = fetchSpy . mock . calls [ 0 ] ;
339+ const [ url2 , options2 ] = fetchSpy . mock . calls [ 1 ] ;
340+ expect ( url1 ) . toBe ( url2 ) ;
341+ expect ( options1 . headers [ "authorization" ] ) . toBe ( options2 . headers [ "authorization" ] ) ;
342+ } ) ;
343+
344+ it ( "应当忽略 endpoint 路径前缀末尾的斜杠" , async ( ) => {
345+ const prefixClient = new S3Client ( {
346+ ...defaultConfig ,
347+ endpoint : "https://abcdefg.supabase.co/storage/v1/s3/" ,
348+ } ) ;
349+ fetchSpy . mockResolvedValue ( new Response ( "" , { status : 200 } ) ) ;
350+
351+ await prefixClient . request ( "HEAD" , "my-bucket" ) ;
352+
353+ const [ url ] = fetchSpy . mock . calls [ 0 ] ;
354+ expect ( url ) . toBe ( "https://abcdefg.supabase.co/storage/v1/s3/my-bucket" ) ;
355+ } ) ;
356+
283357 it ( "应当正确处理包含特殊字符的 key" , async ( ) => {
284358 fetchSpy . mockResolvedValue ( new Response ( "" , { status : 200 } ) ) ;
285359
0 commit comments