@@ -104,6 +104,33 @@ INSTANTIATE_TEST_SUITE_P(
104104 },
105105 },
106106 " with_absolute_endpoint_uri" ,
107+ },
108+ ParsedS3URLTestCase{
109+ " s3://bucket/key?addressing-style=virtual" ,
110+ {
111+ .bucket = " bucket" ,
112+ .key = {" key" },
113+ .addressingStyle = S3AddressingStyle::Virtual,
114+ },
115+ " with_addressing_style_virtual" ,
116+ },
117+ ParsedS3URLTestCase{
118+ " s3://bucket/key?addressing-style=path" ,
119+ {
120+ .bucket = " bucket" ,
121+ .key = {" key" },
122+ .addressingStyle = S3AddressingStyle::Path,
123+ },
124+ " with_addressing_style_path" ,
125+ },
126+ ParsedS3URLTestCase{
127+ " s3://bucket/key?addressing-style=auto" ,
128+ {
129+ .bucket = " bucket" ,
130+ .key = {" key" },
131+ .addressingStyle = S3AddressingStyle::Auto,
132+ },
133+ " with_addressing_style_auto" ,
107134 }),
108135 [](const ::testing::TestParamInfo<ParsedS3URLTestCase> & info) { return info.param .description ; });
109136
@@ -138,6 +165,26 @@ INSTANTIATE_TEST_SUITE_P(
138165 InvalidS3URLTestCase{" s3://bucket" , " error: URI has a missing or invalid key" , " missing_key" }),
139166 [](const ::testing::TestParamInfo<InvalidS3URLTestCase> & info) { return info.param .description ; });
140167
168+ TEST (ParsedS3URLTest, invalidAddressingStyleThrows)
169+ {
170+ ASSERT_THROW (ParsedS3URL::parse (parseURL (" s3://bucket/key?addressing-style=bogus" )), InvalidS3AddressingStyle);
171+ }
172+
173+ TEST (ParsedS3URLTest, virtualStyleWithAuthoritylessEndpointThrows)
174+ {
175+ ParsedS3URL input{
176+ .bucket = " bucket" ,
177+ .key = {" key" },
178+ .addressingStyle = S3AddressingStyle::Virtual,
179+ .endpoint =
180+ ParsedURL{
181+ .scheme = " file" ,
182+ .path = {" " , " some" , " path" },
183+ },
184+ };
185+ ASSERT_THROW (input.toHttpsUrl (), nix::Error);
186+ }
187+
141188// =============================================================================
142189// S3 URL to HTTPS Conversion Tests
143190// =============================================================================
@@ -166,17 +213,18 @@ INSTANTIATE_TEST_SUITE_P(
166213 S3ToHttpsConversion,
167214 S3ToHttpsConversionTest,
168215 ::testing::Values (
216+ // Default (auto) addressing style: virtual-hosted for standard AWS endpoints
169217 S3ToHttpsConversionTestCase{
170218 ParsedS3URL{
171219 .bucket = " my-bucket" ,
172220 .key = {" my-key.txt" },
173221 },
174222 ParsedURL{
175223 .scheme = " https" ,
176- .authority = ParsedURL::Authority{.host = " s3.us-east-1.amazonaws.com" },
177- .path = {" " , " my-bucket " , " my- key.txt" },
224+ .authority = ParsedURL::Authority{.host = " my-bucket. s3.us-east-1.amazonaws.com" },
225+ .path = {" " , " my-key.txt" },
178226 },
179- " https://s3.us-east-1.amazonaws.com/my-bucket /my-key.txt" ,
227+ " https://my-bucket. s3.us-east-1.amazonaws.com/my-key.txt" ,
180228 " basic_s3_default_region" ,
181229 },
182230 S3ToHttpsConversionTestCase{
@@ -187,12 +235,13 @@ INSTANTIATE_TEST_SUITE_P(
187235 },
188236 ParsedURL{
189237 .scheme = " https" ,
190- .authority = ParsedURL::Authority{.host = " s3.eu-west-1.amazonaws.com" },
191- .path = {" " , " prod-cache " , " nix" , " store" , " abc123.nar.xz" },
238+ .authority = ParsedURL::Authority{.host = " prod-cache. s3.eu-west-1.amazonaws.com" },
239+ .path = {" " , " nix" , " store" , " abc123.nar.xz" },
192240 },
193- " https://s3.eu-west-1.amazonaws.com/prod-cache /nix/store/abc123.nar.xz" ,
241+ " https://prod-cache. s3.eu-west-1.amazonaws.com/nix/store/abc123.nar.xz" ,
194242 " with_eu_west_1_region" ,
195243 },
244+ // Custom endpoint authority: path-style by default
196245 S3ToHttpsConversionTestCase{
197246 ParsedS3URL{
198247 .bucket = " bucket" ,
@@ -208,6 +257,7 @@ INSTANTIATE_TEST_SUITE_P(
208257 " http://custom.s3.com/bucket/key" ,
209258 " custom_endpoint_authority" ,
210259 },
260+ // Custom endpoint URL: path-style by default
211261 S3ToHttpsConversionTestCase{
212262 ParsedS3URL{
213263 .bucket = " bucket" ,
@@ -236,10 +286,10 @@ INSTANTIATE_TEST_SUITE_P(
236286 },
237287 ParsedURL{
238288 .scheme = " https" ,
239- .authority = ParsedURL::Authority{.host = " s3.ap-southeast-2.amazonaws.com" },
240- .path = {" " , " bucket " , " path" , " to" , " file.txt" },
289+ .authority = ParsedURL::Authority{.host = " bucket. s3.ap-southeast-2.amazonaws.com" },
290+ .path = {" " , " path" , " to" , " file.txt" },
241291 },
242- " https://s3.ap-southeast-2.amazonaws.com/bucket /path/to/file.txt" ,
292+ " https://bucket. s3.ap-southeast-2.amazonaws.com/path/to/file.txt" ,
243293 " complex_path_and_region" ,
244294 },
245295 S3ToHttpsConversionTestCase{
@@ -250,11 +300,11 @@ INSTANTIATE_TEST_SUITE_P(
250300 },
251301 ParsedURL{
252302 .scheme = " https" ,
253- .authority = ParsedURL::Authority{.host = " s3.us-east-1.amazonaws.com" },
254- .path = {" " , " my-bucket " , " my- key.txt" },
303+ .authority = ParsedURL::Authority{.host = " my-bucket. s3.us-east-1.amazonaws.com" },
304+ .path = {" " , " my-key.txt" },
255305 .query = {{" versionId" , " abc123xyz" }},
256306 },
257- " https://s3.us-east-1.amazonaws.com/my-bucket /my-key.txt?versionId=abc123xyz" ,
307+ " https://my-bucket. s3.us-east-1.amazonaws.com/my-key.txt?versionId=abc123xyz" ,
258308 " with_versionId" ,
259309 },
260310 S3ToHttpsConversionTestCase{
@@ -266,13 +316,185 @@ INSTANTIATE_TEST_SUITE_P(
266316 },
267317 ParsedURL{
268318 .scheme = " https" ,
269- .authority = ParsedURL::Authority{.host = " s3.eu-west-1.amazonaws.com" },
270- .path = {" " , " versioned-bucket " , " path" , " to" , " object" },
319+ .authority = ParsedURL::Authority{.host = " versioned-bucket. s3.eu-west-1.amazonaws.com" },
320+ .path = {" " , " path" , " to" , " object" },
271321 .query = {{" versionId" , " version456" }},
272322 },
273- " https://s3.eu-west-1.amazonaws.com/versioned-bucket /path/to/object?versionId=version456" ,
323+ " https://versioned-bucket. s3.eu-west-1.amazonaws.com/path/to/object?versionId=version456" ,
274324 " with_region_and_versionId" ,
325+ },
326+ // Explicit addressing-style=path forces path-style on standard AWS endpoints
327+ S3ToHttpsConversionTestCase{
328+ ParsedS3URL{
329+ .bucket = " my-bucket" ,
330+ .key = {" my-key.txt" },
331+ .region = " us-west-2" ,
332+ .addressingStyle = S3AddressingStyle::Path,
333+ },
334+ ParsedURL{
335+ .scheme = " https" ,
336+ .authority = ParsedURL::Authority{.host = " s3.us-west-2.amazonaws.com" },
337+ .path = {" " , " my-bucket" , " my-key.txt" },
338+ },
339+ " https://s3.us-west-2.amazonaws.com/my-bucket/my-key.txt" ,
340+ " explicit_path_style" ,
341+ },
342+ // Explicit addressing-style=virtual forces virtual-hosted-style on custom endpoints
343+ S3ToHttpsConversionTestCase{
344+ ParsedS3URL{
345+ .bucket = " bucket" ,
346+ .key = {" key" },
347+ .scheme = " http" ,
348+ .addressingStyle = S3AddressingStyle::Virtual,
349+ .endpoint = ParsedURL::Authority{.host = " custom.s3.com" },
350+ },
351+ ParsedURL{
352+ .scheme = " http" ,
353+ .authority = ParsedURL::Authority{.host = " bucket.custom.s3.com" },
354+ .path = {" " , " key" },
355+ },
356+ " http://bucket.custom.s3.com/key" ,
357+ " explicit_virtual_style_custom_endpoint" ,
358+ },
359+ // Explicit addressing-style=virtual with full endpoint URL
360+ S3ToHttpsConversionTestCase{
361+ ParsedS3URL{
362+ .bucket = " bucket" ,
363+ .key = {" key" },
364+ .addressingStyle = S3AddressingStyle::Virtual,
365+ .endpoint =
366+ ParsedURL{
367+ .scheme = " http" ,
368+ .authority = ParsedURL::Authority{.host = " server" , .port = 9000 },
369+ .path = {" " },
370+ },
371+ },
372+ ParsedURL{
373+ .scheme = " http" ,
374+ .authority = ParsedURL::Authority{.host = " bucket.server" , .port = 9000 },
375+ .path = {" " , " key" },
376+ },
377+ " http://bucket.server:9000/key" ,
378+ " explicit_virtual_style_full_endpoint_url" ,
379+ },
380+ // Dotted bucket names work normally with explicit path-style
381+ S3ToHttpsConversionTestCase{
382+ ParsedS3URL{
383+ .bucket = " my.bucket" ,
384+ .key = {" key" },
385+ .addressingStyle = S3AddressingStyle::Path,
386+ },
387+ ParsedURL{
388+ .scheme = " https" ,
389+ .authority = ParsedURL::Authority{.host = " s3.us-east-1.amazonaws.com" },
390+ .path = {" " , " my.bucket" , " key" },
391+ },
392+ " https://s3.us-east-1.amazonaws.com/my.bucket/key" ,
393+ " dotted_bucket_with_path_style" ,
394+ },
395+ // Dotted bucket names fall back to path-style with auto on standard AWS endpoints
396+ S3ToHttpsConversionTestCase{
397+ ParsedS3URL{
398+ .bucket = " my.bucket.name" ,
399+ .key = {" key" },
400+ },
401+ ParsedURL{
402+ .scheme = " https" ,
403+ .authority = ParsedURL::Authority{.host = " s3.us-east-1.amazonaws.com" },
404+ .path = {" " , " my.bucket.name" , " key" },
405+ },
406+ " https://s3.us-east-1.amazonaws.com/my.bucket.name/key" ,
407+ " dotted_bucket_with_auto_style_on_aws" ,
408+ },
409+ // Dotted bucket names work with auto style on custom endpoints (auto = path-style)
410+ S3ToHttpsConversionTestCase{
411+ ParsedS3URL{
412+ .bucket = " my.bucket" ,
413+ .key = {" key" },
414+ .endpoint = ParsedURL::Authority{.host = " minio.local" },
415+ },
416+ ParsedURL{
417+ .scheme = " https" ,
418+ .authority = ParsedURL::Authority{.host = " minio.local" },
419+ .path = {" " , " my.bucket" , " key" },
420+ },
421+ " https://minio.local/my.bucket/key" ,
422+ " dotted_bucket_with_auto_style_custom_endpoint" ,
275423 }),
276424 [](const ::testing::TestParamInfo<S3ToHttpsConversionTestCase> & info) { return info.param .description ; });
277425
426+ // =============================================================================
427+ // S3 URL to HTTPS Conversion Error Tests
428+ // =============================================================================
429+
430+ struct S3ToHttpsConversionErrorTestCase
431+ {
432+ ParsedS3URL input;
433+ std::string description;
434+ };
435+
436+ class S3ToHttpsConversionErrorTest : public ::testing::WithParamInterface<S3ToHttpsConversionErrorTestCase>,
437+ public ::testing::Test
438+ {};
439+
440+ TEST_P (S3ToHttpsConversionErrorTest, ThrowsOnConversion)
441+ {
442+ auto & [input, description] = GetParam ();
443+ ASSERT_THROW (input.toHttpsUrl (), nix::Error);
444+ }
445+
446+ INSTANTIATE_TEST_SUITE_P (
447+ S3ToHttpsConversionErrors,
448+ S3ToHttpsConversionErrorTest,
449+ ::testing::Values (
450+ S3ToHttpsConversionErrorTestCase{
451+ ParsedS3URL{
452+ .bucket = " bucket" ,
453+ .key = {" key" },
454+ .addressingStyle = S3AddressingStyle::Virtual,
455+ .endpoint = ParsedURL::Authority{.host = " " },
456+ },
457+ " virtual_style_with_empty_host_authority" ,
458+ },
459+ S3ToHttpsConversionErrorTestCase{
460+ ParsedS3URL{
461+ .bucket = " my.bucket" ,
462+ .key = {" key" },
463+ .addressingStyle = S3AddressingStyle::Virtual,
464+ },
465+ " dotted_bucket_with_explicit_virtual_style" ,
466+ },
467+ S3ToHttpsConversionErrorTestCase{
468+ ParsedS3URL{
469+ .bucket = " my.bucket.name" ,
470+ .key = {" key" },
471+ .addressingStyle = S3AddressingStyle::Virtual,
472+ },
473+ " dotted_bucket_with_explicit_virtual_style_multi_dot" ,
474+ },
475+ S3ToHttpsConversionErrorTestCase{
476+ ParsedS3URL{
477+ .bucket = " my.bucket" ,
478+ .key = {" key" },
479+ .addressingStyle = S3AddressingStyle::Virtual,
480+ .endpoint = ParsedURL::Authority{.host = " minio.local" },
481+ },
482+ " dotted_bucket_with_explicit_virtual_style_custom_authority" ,
483+ },
484+ S3ToHttpsConversionErrorTestCase{
485+ ParsedS3URL{
486+ .bucket = " my.bucket" ,
487+ .key = {" key" },
488+ .addressingStyle = S3AddressingStyle::Virtual,
489+ .endpoint =
490+ ParsedURL{
491+ .scheme = " http" ,
492+ .authority = ParsedURL::Authority{.host = " minio.local" , .port = 9000 },
493+ .path = {" " },
494+ },
495+ },
496+ " dotted_bucket_with_explicit_virtual_style_full_endpoint_url" ,
497+ }),
498+ [](const ::testing::TestParamInfo<S3ToHttpsConversionErrorTestCase> & info) { return info.param .description ; });
499+
278500} // namespace nix
0 commit comments