diff --git a/clients/client-s3/test/e2e/s3-bucket-features.e2e.spec.ts b/clients/client-s3/test/e2e/s3-bucket-features.e2e.spec.ts new file mode 100644 index 0000000000000..b71d6ac45047b --- /dev/null +++ b/clients/client-s3/test/e2e/s3-bucket-features.e2e.spec.ts @@ -0,0 +1,367 @@ +import { S3, waitUntilBucketExists, waitUntilBucketNotExists } from "@aws-sdk/client-s3"; +import { type GetCallerIdentityCommandOutput, STS } from "@aws-sdk/client-sts"; +import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import { HttpRequest } from "@smithy/types"; + +describe("@aws-sdk/client-s3 - Working with Buckets", () => { + const s3 = new S3({ + region: "us-west-2", + }); + const s3East = new S3({ + region: "us-east-1", + followRegionRedirects: true, + }); + const s3PathStyle = new S3({ + region: "us-west-2", + forcePathStyle: true, + }); + const stsClient = new STS({ + region: "us-west-2", + }); + + function getBucketName(id: string, region = "us-west-2") { + const alphabet = "abcdefghijklmnopqrstuvwxyz"; + const randId = Array.from({ length: 6 }, () => alphabet[(Math.random() * alphabet.length) | 0]).join(""); + return `${callerID.Account}-${randId}-${id}-${region}-${(Date.now() / 1000) | 0}`; + } + + let Bucket: string; + let callerID: GetCallerIdentityCommandOutput; + + beforeAll(async () => { + callerID = await stsClient.getCallerIdentity({}); + Bucket = getBucketName(`js-sdk-e2e`); + }); + + describe("CRUD buckets using classic endpoint", () => { + let bucketEast: string | undefined; + + beforeAll(async () => { + bucketEast = Bucket.replace("us-west-2", "us-east-1"); + }); + + afterAll(async () => { + await s3East.deleteBucket({ + Bucket: bucketEast, + }); + await waitUntilBucketNotExists( + { + client: s3East, + maxWaitTime: 60, + }, + { + Bucket: bucketEast, + } + ); + }); + + it("should create and verify bucket in us-east-1", async () => { + await s3East.createBucket({ + Bucket: bucketEast, + }); + await waitUntilBucketExists( + { + client: s3East, + maxWaitTime: 60, + }, + { + Bucket: bucketEast, + } + ); + await s3East.headBucket({ Bucket: bucketEast }); + }); + }); + + describe("CRUD buckets using regional endpoint", () => { + afterAll(async () => { + await s3.deleteBucket({ + Bucket, + }); + await waitUntilBucketNotExists( + { + client: s3, + maxWaitTime: 60, + }, + { + Bucket, + } + ); + }); + it("should create and verify bucket in us-west-2", async () => { + await s3.createBucket({ + Bucket, + }); + await waitUntilBucketExists( + { + client: s3, + maxWaitTime: 60, + }, + { + Bucket, + } + ); + await s3.headBucket({ Bucket }); + }); + }); + + describe("Bucket CORS", () => { + let corsBucket: string; + + beforeAll(async () => { + corsBucket = getBucketName("cors"); + }); + + afterAll(async () => { + await s3.deleteBucket({ + Bucket: corsBucket, + }); + }); + + it("should configure and verify CORS settings", async () => { + await s3.createBucket({ + Bucket: corsBucket, + }); + await waitUntilBucketExists( + { + client: s3, + maxWaitTime: 60, + }, + { + Bucket: corsBucket, + } + ); + + await s3.putBucketCors({ + Bucket: corsBucket, + CORSConfiguration: { + CORSRules: [ + { + AllowedMethods: ["DELETE", "POST", "PUT"], + AllowedOrigins: ["http://example.com"], + AllowedHeaders: ["*"], + ExposeHeaders: ["x-amz-server-side-encryption"], + MaxAgeSeconds: 5000, + }, + ], + }, + }); + const getBucketCors = await s3.getBucketCors({ + Bucket: corsBucket, + }); + const corsConfig = getBucketCors.CORSRules?.[0]; + + expect(corsConfig?.AllowedMethods).toContain("DELETE"); + expect(corsConfig?.AllowedMethods).toContain("POST"); + expect(corsConfig?.AllowedMethods).toContain("PUT"); + expect(corsConfig?.AllowedOrigins?.[0]).toBe("http://example.com"); + expect(corsConfig?.AllowedHeaders?.[0]).toBe("*"); + expect(corsConfig?.ExposeHeaders?.[0]).toBe("x-amz-server-side-encryption"); + expect(corsConfig?.MaxAgeSeconds).toBe(5000); + }); + }); + + describe("Bucket lifecycles", () => { + let lifecycleBucket: string; + + beforeAll(async () => { + lifecycleBucket = getBucketName("lifecyc"); + }); + + afterAll(async () => { + await s3.deleteBucket({ + Bucket: lifecycleBucket, + }); + }); + + it("should configure and verify lifecycle rules", async () => { + await s3.createBucket({ + Bucket: lifecycleBucket, + }); + await waitUntilBucketExists( + { + client: s3, + maxWaitTime: 60, + }, + { + Bucket: lifecycleBucket, + } + ); + await s3.putBucketLifecycleConfiguration({ + Bucket: lifecycleBucket, + LifecycleConfiguration: { + Rules: [ + { + Filter: { + Prefix: "/", + }, + Status: "Enabled", + Transitions: [ + { + Days: 0, + StorageClass: "GLACIER", + }, + ], + }, + ], + }, + }); + const lcConfig = await s3.getBucketLifecycleConfiguration({ + Bucket: lifecycleBucket, + }); + + expect(lcConfig?.Rules?.[0]?.Transitions?.[0]?.Days).toBe(0); + expect(lcConfig?.Rules?.[0]?.Transitions?.[0]?.StorageClass).toBe("GLACIER"); + }); + }); + + describe("Bucket Tagging", () => { + let taggingBucket: string; + + beforeAll(async () => { + taggingBucket = getBucketName("tagging"); + }); + + afterAll(async () => { + await s3.deleteBucket({ + Bucket: taggingBucket, + }); + }); + + it("should set and verify bucket tags", async () => { + await s3.createBucket({ + Bucket: taggingBucket, + }); + await s3.putBucketTagging({ + Bucket: taggingBucket, + Tagging: { + TagSet: [ + { + Key: "KEY", + Value: "VALUE", + }, + ], + }, + }); + const tags = await s3.getBucketTagging({ + Bucket: taggingBucket, + }); + + expect(tags.TagSet?.[0]).toEqual({ + Key: "KEY", + Value: "VALUE", + }); + }); + }); + + describe("Access bucket following 307 redirects", () => { + let locationConstrained: string; + + beforeAll(async () => { + locationConstrained = getBucketName("loc-con", "eu-west-1"); + }); + + afterAll(async () => { + await s3East.deleteBucket({ + Bucket: locationConstrained, + }); + }); + + it("should handle bucket creation with location constraint", async () => { + await s3East.createBucket({ + Bucket: locationConstrained, + CreateBucketConfiguration: { + LocationConstraint: "eu-west-1" as const, + }, + }); + + await waitUntilBucketExists( + { client: s3East, maxWaitTime: 60 }, + { + Bucket: locationConstrained, + } + ); + + const headBucket = await s3East.headBucket({ + Bucket: locationConstrained, + }); + expect(headBucket.BucketRegion).toEqual("eu-west-1"); + + const bucketLocation = await s3.getBucketLocation({ + Bucket: locationConstrained, + }); + expect(bucketLocation.LocationConstraint).toEqual("eu-west-1"); + }); + }); + + describe("Working with bucket names containing dots", () => { + let dottedName: string; + + beforeAll(async () => { + dottedName = getBucketName("x.y.z"); + }); + + afterAll(async () => { + await s3.deleteBucket({ + Bucket: dottedName, + }); + }); + + it("should create bucket with DNS compatible dotted name", async () => { + await s3.createBucket({ + Bucket: dottedName, + }); + await waitUntilBucketExists( + { client: s3, maxWaitTime: 60 }, + { + Bucket: dottedName, + } + ); + }); + }); + + describe("Operating on bucket using path style", () => { + let pathStyle: string; + + beforeAll(async () => { + pathStyle = getBucketName("path-style"); + s3PathStyle.middlewareStack.add( + (next) => async (args) => { + const request = args.request as HttpRequest; + expect(request.path).toContain(pathStyle); + expect(request.hostname).not.toContain(pathStyle); + return next(args); + }, + { + step: "finalizeRequest", + override: true, + name: "assertionMiddleware", + } + ); + }); + + afterAll(async () => { + await s3PathStyle.deleteBucket({ + Bucket: pathStyle, + }); + }); + + it("should use path style addressing", async () => { + await s3PathStyle.createBucket({ + Bucket: pathStyle, + }); + + await s3PathStyle.putObject({ + Bucket: pathStyle, + Key: "hello", + Body: "abc", + }); + + await s3PathStyle.deleteObject({ + Bucket: pathStyle, + Key: "hello", + }); + + expect.assertions(6); + }); + }); +}, 60_000); diff --git a/clients/client-s3/test/e2e/s3-object-features.e2e.spec.ts b/clients/client-s3/test/e2e/s3-object-features.e2e.spec.ts new file mode 100644 index 0000000000000..7ea525b3cc740 --- /dev/null +++ b/clients/client-s3/test/e2e/s3-object-features.e2e.spec.ts @@ -0,0 +1,280 @@ +import crypto from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; +import { GetObjectCommand, PutObjectCommand, S3 } from "@aws-sdk/client-s3"; +import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; +import { afterAll, beforeAll, describe, expect, test as it } from "vitest"; + +import { getIntegTestResources } from "../../../../tests/e2e/get-integ-test-resources"; + +describe("@aws-sdk/client-s3", () => { + let client: S3; + let Bucket: string; + let region: string; + + beforeAll(async () => { + const integTestResourcesEnv = await getIntegTestResources(); + Object.assign(process.env, integTestResourcesEnv); + + region = process?.env?.AWS_SMOKE_TEST_REGION as string; + Bucket = process?.env?.AWS_SMOKE_TEST_BUCKET as string; + + client = new S3({ region }); + }); + + async function putObject(Body: string, Key: string) { + await client.putObject({ + Bucket, + Key, + Body, + }); + } + + async function putBuffer(Body: Uint8Array, Key: string) { + await client.putObject({ + Bucket, + Key, + Body, + }); + } + + async function copyObject(from: string, to: string) { + await client.copyObject({ + Bucket, + Key: to, + CopySource: `/${Bucket}/${from}`, + }); + } + + async function getObject(Key: string) { + return ( + await client.getObject({ + Bucket, + Key, + }) + ).Body?.transformToString(); + } + + async function objectExists(Key: string) { + try { + await client.headObject({ + Bucket, + Key, + }); + return true; + } catch (e) { + return false; + } + } + + async function deleteObject(Key: string) { + await client.deleteObject({ + Bucket, + Key, + }); + } + + describe("CRUD operations", () => { + it("should perform basic CRUD operations on objects", async () => { + await putObject("world", "hello"); + expect(await objectExists("hello")).toBe(true); + + const obj = await getObject("hello"); + expect(obj).toBe("world"); + + await putObject("new world", "hello"); + expect(await objectExists("hello")).toBe(true); + + const updatedObj = await getObject("hello"); + expect(updatedObj).toBe("new world"); + + await deleteObject("hello"); + expect(await objectExists("hello")).toBe(false); + }); + }); + + describe("Content length", () => { + it("should handle content length", async () => { + await client.putObject({ + Bucket, + Key: "contentlength", + Body: "foo", + ContentLength: 3, + }); + expect(await objectExists("contentlength")).toBe(true); + + const obj = await getObject("contentlength"); + expect(obj).toBe("foo"); + }); + }); + + describe("Multi-byte strings", () => { + it("should handle multi-byte strings", async () => { + await putObject("åß∂ƒ©", "multi"); + expect(await objectExists("multi")).toBe(true); + + const obj = await client.getObject({ + Bucket, + Key: "multi", + }); + + const str = String(await obj.Body?.transformToString()); + + expect(str).toBe("åß∂ƒ©"); + expect(str.length).toBe(5); + expect(obj.ContentLength).toBe(11); + }); + }); + + describe("Object copying", () => { + it("should copy objects", async () => { + await putObject("world", "hello"); + await copyObject("hello", "byebye"); + + expect(await objectExists("byebye")).toBe(true); + + const obj = await getObject("byebye"); + expect(obj).toBe("world"); + + await deleteObject("byebye"); + }); + }); + + describe("Empty objects", () => { + it("should handle empty strings", async () => { + await putObject("", "blank"); + expect(await objectExists("blank")).toBe(true); + + const obj = await getObject("blank"); + expect(obj).toBe(""); + expect(obj).toHaveLength(0); + }); + }); + + describe("Buffers", () => { + it("should handle empty buffers", async () => { + await putBuffer(Buffer.alloc(0), "emptybuffer"); + expect(await objectExists("emptybuffer")).toBe(true); + + const obj = await getObject("emptybuffer"); + expect(obj).toHaveLength(0); + }); + + it("should handle small buffers", async () => { + await putBuffer(Buffer.alloc(1048576), "smallbuffer"); + expect(await objectExists("smallbuffer")).toBe(true); + + const obj = await getObject("smallbuffer"); + expect(obj).toHaveLength(1048576); + }); + + it("should handle large buffers", async () => { + await putBuffer(Buffer.alloc(20971520), "largebuffer"); + expect(await objectExists("largebuffer")).toBe(true); + + const obj = await getObject("largebuffer"); + expect(obj).toHaveLength(20971520); + }); + }); + + describe("Files", () => { + beforeAll(async () => { + fs.writeFileSync(path.join(__dirname, "emptyfile"), "a".repeat(0)); + fs.writeFileSync(path.join(__dirname, "smallfile"), "a".repeat(1048576)); + fs.writeFileSync(path.join(__dirname, "largefile"), "a".repeat(20971520)); + }); + + afterAll(async () => { + fs.rmSync(path.join(__dirname, "emptyfile")); + fs.rmSync(path.join(__dirname, "smallfile")); + fs.rmSync(path.join(__dirname, "largefile")); + }); + + it("should handle empty files", async () => { + await client.putObject({ + Bucket, + Key: "emptyfile", + Body: fs.createReadStream(path.join(__dirname, "emptyfile")), + }); + expect(await objectExists("emptyfile")).toBe(true); + + const obj = await getObject("emptyfile"); + expect(obj).toHaveLength(0); + }); + + it("should handle small files", async () => { + await client.putObject({ + Bucket, + Key: "smallfile", + Body: fs.createReadStream(path.join(__dirname, "smallfile")), + }); + expect(await objectExists("smallfile")).toBe(true); + + const obj = await getObject("smallfile"); + expect(obj).toHaveLength(1048576); + }); + + it("should handle large files", async () => { + await client.putObject({ + Bucket, + Key: "largefile", + Body: fs.createReadStream(path.join(__dirname, "largefile")), + }); + expect(await objectExists("largefile")).toBe(true); + + const obj = await getObject("largefile"); + expect(obj).toHaveLength(20971520); + }); + }); + + describe("Checksums", () => { + it("should have data integrity", async () => { + const data = "SOME SAMPLE DATA"; + const checksum = crypto.createHash("sha256").update(data).digest("hex"); + + await putObject(data, "checksummed_data"); + expect(await objectExists("checksummed_data")).toBe(true); + + const objectContents = await getObject("checksummed_data"); + expect(objectContents).toBe(data); + expect(objectContents).toHaveLength(16); + expect( + crypto + .createHash("sha256") + .update(objectContents ?? "") + .digest("hex") + ).toBe(checksum); + }); + }); + + describe("Pre-signed URLs", () => { + it("should handle pre-signed PUT/GET", async () => { + const psu = await getSignedUrl( + client, + new PutObjectCommand({ + Bucket, + Key: "presigned", + Body: "", + }) + ); + + await fetch(psu, { + method: "PUT", + body: "PRESIGNED BODY CONTENTS", + }); + + expect(await getObject("presigned")).toEqual("PRESIGNED BODY CONTENTS"); + + const getPsu = await getSignedUrl( + client, + new GetObjectCommand({ + Bucket, + Key: "presigned", + }) + ); + + const response = await fetch(getPsu); + expect(await response.text()).toEqual("PRESIGNED BODY CONTENTS"); + }); + }); +}, 60_000); diff --git a/features/s3/buckets.feature b/features/s3/buckets.feature deleted file mode 100644 index c4389d000188f..0000000000000 --- a/features/s3/buckets.feature +++ /dev/null @@ -1,72 +0,0 @@ -# language: en -@s3 @buckets -Feature: Working with Buckets - - Scenario: CRUD buckets using the classic endpoint - Given I am using the S3 "us-east-1" region - When I create a bucket - Then the bucket should exist - # Then the bucket should not exist - - Scenario: CRUD buckets using a regional endpoint - Given I am using the S3 "us-west-2" region - When I create a bucket - Then the bucket should exist - # Then the bucket should not exist - - @cors - Scenario: Bucket CORS - When I create a bucket - And I put a bucket CORS configuration - And I get the bucket CORS configuration - Then the AllowedMethods list should inclue "DELETE POST PUT" - Then the AllowedOrigin value should equal "http://example.com" - Then the AllowedHeader value should equal "*" - Then the ExposeHeader value should equal "x-amz-server-side-encryption" - Then the MaxAgeSeconds value should equal 5000 - - @lifecycle - Scenario: Bucket lifecycles - When I create a bucket - And I put a transition lifecycle configuration on the bucket with prefix "/" - And I get the transition lifecycle configuration on the bucket - Then the lifecycle configuration should have transition days of 0 - And the lifecycle configuration should have transition storage class of "GLACIER" - - @tagging - Scenario: Bucket Tagging - When I create a bucket - And I put a bucket tag with key "KEY" and value "VALUE" - And I get the bucket tagging - Then the first tag in the tag set should have key and value "KEY", "VALUE" - - Scenario: Access bucket following 307 redirects - Given I am using the S3 "us-east-1" region with signatureVersion "s3" - When I create a bucket with the location constraint "eu-west-1" - Then the bucket should exist in region "eu-west-1" - Then the bucket should have a location constraint of "eu-west-1" - Then I delete the bucket in region "eu-west-1" - - Scenario: Working with bucket names that contain '.' - When I create a bucket with a DNS compatible name that contains a dot - Then the bucket should exist - # Then the bucket should not exist - - @path-style - Scenario: Operating on a bucket using path style - Given I force path style requests - And I create a bucket - When I put "abc" to the key "hello" in the bucket - And I get the key "hello" in the bucket - Then the bucket name should be in the request path - And the bucket name should not be in the request host - Then I delete the object "hello" from the bucket - - # Known bug: https://github.com/aws/aws-sdk-js-v3/issues/1802 - # Scenario: Follow 307 redirect on new buckets - # Given I am using the S3 "us-east-1" region with signatureVersion "s3" - # When I create a bucket with the location constraint "us-west-2" - # And I put a large buffer to the key "largeobject" in the bucket - # Then the object "largeobject" should exist in the bucket - # Then I delete the object "largeobject" from the bucket - # Then I delete the bucket diff --git a/features/s3/objects.feature b/features/s3/objects.feature deleted file mode 100644 index 3f9bf3f2dbbc6..0000000000000 --- a/features/s3/objects.feature +++ /dev/null @@ -1,154 +0,0 @@ -# language: en -@s3 @objects -Feature: Working with Objects in S3 - - As a user of S3 I need to be able to work with objects in a bucket. - - Background: - Given I create a shared bucket - - @crud - Scenario: CRUD objects - When I put "world" to the key "hello" - Then the object "hello" should exist - Then I get the object "hello" - And the object "hello" should contain "world" - When I put "new world" to the key "hello" - Then the object "hello" should exist - Then I get the object "hello" - Then the object "hello" should contain "new world" - Then I delete the object "hello" - Then the object "hello" should not exist - - @content-length - Scenario: Content length - When I put "foo" to the key "contentlength" with ContentLength 3 - Then the object "contentlength" should exist - Then I get the object "contentlength" - And the object "contentlength" should contain "foo" - - @multi-byte - Scenario: Putting a multi-byte string to an object - When I put "åß∂ƒ©" to the key "multi" - Then the object "multi" should exist - Then I get the object "multi" - Then the object "multi" should contain "åß∂ƒ©" - And the HTTP response should have a content length of 11 - - @copy - Scenario: Copying an object - Given I put "world" to the key "hello" - When I copy the object "hello" to "byebye" - Then the object "byebye" should exist - Then I get the object "byebye" - Then the object "byebye" should contain "world" - Then I delete the object "byebye" - - # Blocked on the support for makeUnauthenticatedRequest https://github.com/aws/aws-sdk-js-v3/issues/984 - # @unauthenticated - # Scenario: Unauthenticated requests - # When I put "world" to the public key "hello" - # And I make an unauthenticated request to read object "hello" - # Then the object "hello" should contain "world" - - @blank - Scenario: Putting nothing to an object - When I put "" to the key "blank" - Then the object "blank" should exist - Then I get the object "blank" - Then the object "blank" should contain "" - And the HTTP response should have a content length of 0 - - @buffer - Scenario: Putting and getting an empty buffer - When I put an empty buffer to the key "emptybuffer" - Then the object "emptybuffer" should exist - Then I get the object "emptybuffer" - And the HTTP response should have a content length of 0 - - @buffer - Scenario: Putting and getting a small buffer - When I put a small buffer to the key "smallbuffer" - Then the object "smallbuffer" should exist - Then I get the object "smallbuffer" - And the HTTP response should have a content length of 1048576 - - @buffer - Scenario: Putting and getting a large buffer - When I put a large buffer to the key "largebuffer" - Then the object "largebuffer" should exist - Then I get the object "largebuffer" - And the HTTP response should have a content length of 20971520 - - @file - Scenario: Putting and getting an empty file - When I put an empty file to the key "emptyfile" - Then the object "emptyfile" should exist - Then I get the object "emptyfile" - And the HTTP response should have a content length of 0 - - @file - Scenario: Putting and getting a small file - When I put a small file to the key "smallfile" - Then the object "smallfile" should exist - Then I get the object "smallfile" - And the HTTP response should have a content length of 1048576 - - @file - Scenario: Putting and getting a large file - When I put a large file to the key "largefile" - Then the object "largefile" should exist - Then I get the object "largefile" - And the HTTP response should have a content length of 20971520 - - @checksum - Scenario: Verifying data integrity - Given I generate the MD5 checksum of "SOME SAMPLE DATA" - And I put "SOME SAMPLE DATA" to the key "checksummed_data" - Then the object "checksummed_data" should exist - When I get the object "checksummed_data" - Then the object "checksummed_data" should contain "SOME SAMPLE DATA" - Then the HTTP response should have a content length of 16 - And the MD5 checksum of the response data should equal the generated checksum - - @presigned - Scenario: Putting to a pre-signed URL - Given I get a pre-signed URL to PUT the key "presigned" with data "" - And I access the URL via HTTP PUT with data "PRESIGNED BODY CONTENTS" - Then I get a pre-signed URL to GET the key "presigned" - And I access the URL via HTTP GET - Then the HTTP response should equal "PRESIGNED BODY CONTENTS" - - @presigned @checksum - Scenario: Pre-signed URLs with checksum - Given I get a pre-signed URL to PUT the key "hello" with data "CHECKSUMMED" - And I access the URL via HTTP PUT with data "NOT CHECKSUMMED" - Then the HTTP response should contain "SignatureDoesNotMatch" - - # Blocked on parity https://github.com/aws/aws-sdk-js-v3/issues/1001 - # @presigned_post - # Scenario: POSTing an object with a presigned form - # Given I create a presigned form to POST the key "presignedPost" with the data "PRESIGNED POST CONTENTS" - # And I POST the form - # Then the object "presignedPost" should exist - # When I get the object "presignedPost" - # Then the object "presignedPost" should contain "PRESIGNED POST CONTENTS" - # Then the HTTP response should have a content length of 23 - - @proxy - Scenario: Proxy support - When I put "world" to the key "proxy_object" - Then the object "proxy_object" should exist - Then I get the object "proxy_object" - And the object "proxy_object" should contain "world" - - When I delete the object "proxy_object" - Then the object "proxy_object" should not exist - - And I teardown the local proxy server - - # ToDo: Investigate why $metadata is not populated for this case - #@error - #Scenario: Error handling - # Given I put "data" to the key "" - # Then the error status code should be 404 \ No newline at end of file diff --git a/features/s3/step_definitions/buckets.js b/features/s3/step_definitions/buckets.js deleted file mode 100644 index abfb96e8b9285..0000000000000 --- a/features/s3/step_definitions/buckets.js +++ /dev/null @@ -1,308 +0,0 @@ -const { After, Before, Given, Then, When } = require("@cucumber/cucumber"); - -Before({ tags: "@buckets" }, function () { - const { S3 } = require("../../../clients/client-s3"); - this.S3 = S3; -}); - -After({ tags: "@buckets" }, function (callback) { - const _callback = typeof callback === "function" ? callback : () => {}; - if (this.bucket) { - this.s3 - .deleteBucket({ Bucket: this.bucket }) - .catch(() => {}) - .then(_callback); - this.bucket = undefined; - } else { - _callback(); - } -}); - -Given("I am using the S3 {string} region", function (region, callback) { - this.s3 = new this.S3({ - region: region, - requestChecksumCalculation: "WHEN_REQUIRED", - responseChecksumValidation: "WHEN_REQUIRED", - }); - callback(); -}); - -Given( - "I am using the S3 {string} region with signatureVersion {string}", - function (region, signatureVersion, callback) { - this.s3 = new this.S3({ - region: region, - signatureVersion: signatureVersion, - }); - callback(); - } -); - -When("I create a bucket with the location constraint {string}", function (location, callback) { - this.bucket = this.uniqueName("aws-sdk-js-integration"); - const params = { - Bucket: this.bucket, - CreateBucketConfiguration: { - LocationConstraint: location, - }, - }; - this.request("s3", "createBucket", params, function (err, data) { - if (err) { - return callback(err); - } - callback(); - }); -}); - -Then("the bucket should exist in region {string}", function (location, next) { - // Bug: https://github.com/aws/aws-sdk-js-v3/issues/1799 - this.waitForBucketExists(new this.S3({ region: location }), { Bucket: this.bucket }, next); -}); - -Then("the bucket should have a location constraint of {string}", function (loc, callback) { - const self = this; - self.s3.getBucketLocation( - { - Bucket: self.bucket, - }, - function (err, data) { - if (err) callback(err); - self.assert.equal(data.LocationConstraint, loc); - callback(); - } - ); -}); - -When("I delete the bucket in region {string}", function (location, callback) { - // Bug: https://github.com/aws/aws-sdk-js-v3/issues/1799 - this.request(new this.S3({ region: location }), "deleteBucket", { Bucket: this.bucket }, callback); -}); - -When("I put a transition lifecycle configuration on the bucket with prefix {string}", function (prefix, callback) { - const params = { - Bucket: this.bucket, - LifecycleConfiguration: { - Rules: [ - { - Filter: { - Prefix: prefix, - }, - Status: "Enabled", - Transitions: [ - { - Days: 0, - StorageClass: "GLACIER", - }, - ], - }, - ], - }, - }; - this.request("s3", "putBucketLifecycleConfiguration", params, callback); -}); - -When("I get the transition lifecycle configuration on the bucket", function (callback) { - this.eventually(callback, function (next) { - this.request( - "s3", - "getBucketLifecycleConfiguration", - { - Bucket: this.bucket, - }, - next - ); - }); -}); - -Then("the lifecycle configuration should have transition days of {int}", function (days, callback) { - this.assert.equal(this.data.Rules[0].Transitions[0].Days, 0); - callback(); -}); - -Then("the lifecycle configuration should have transition storage class of {string}", function (value, callback) { - this.assert.equal(this.data.Rules[0].Transitions[0].StorageClass, value); - callback(); -}); - -When("I put a bucket CORS configuration", function (callback) { - const params = { - Bucket: this.bucket, - CORSConfiguration: { - CORSRules: [ - { - AllowedMethods: ["DELETE", "POST", "PUT"], - AllowedOrigins: ["http://example.com"], - AllowedHeaders: ["*"], - ExposeHeaders: ["x-amz-server-side-encryption"], - MaxAgeSeconds: 5000, - }, - ], - }, - }; - this.request("s3", "putBucketCors", params, callback); -}); - -When("I get the bucket CORS configuration", function (callback) { - this.request( - "s3", - "getBucketCors", - { - Bucket: this.bucket, - }, - callback - ); -}); - -Then("the AllowedMethods list should inclue {string}", function (value, callback) { - this.assert.equal(this.data.CORSRules[0].AllowedMethods.sort().join(" "), "DELETE POST PUT"); - callback(); -}); - -Then("the AllowedOrigin value should equal {string}", function (value, callback) { - this.assert.equal(this.data.CORSRules[0].AllowedOrigins[0], value); - callback(); -}); - -Then("the AllowedHeader value should equal {string}", function (value, callback) { - this.assert.equal(this.data.CORSRules[0].AllowedHeaders[0], value); - callback(); -}); - -Then("the ExposeHeader value should equal {string}", function (value, callback) { - this.assert.equal(this.data.CORSRules[0].ExposeHeaders[0], value); - callback(); -}); - -Then("the MaxAgeSeconds value should equal {int}", function (value, callback) { - this.assert.equal(this.data.CORSRules[0].MaxAgeSeconds, parseInt(value)); - callback(); -}); - -When("I put a bucket tag with key {string} and value {string}", function (key, value, callback) { - const params = { - Bucket: this.bucket, - Tagging: { - TagSet: [ - { - Key: key, - Value: value, - }, - ], - }, - }; - - this.request("s3", "putBucketTagging", params, callback); -}); - -When("I get the bucket tagging", function (callback) { - this.request( - "s3", - "getBucketTagging", - { - Bucket: this.bucket, - }, - callback - ); -}); - -Then("the first tag in the tag set should have key and value {string}, {string}", function (key, value, callback) { - this.assert.equal(this.data.TagSet[0].Key, key); - this.assert.equal(this.data.TagSet[0].Value, value); - callback(); -}); - -When("I create a bucket with a DNS compatible name that contains a dot", function (callback) { - const bucket = (this.bucket = this.uniqueName("aws-sdk-js.integration")); - this.request( - "s3", - "createBucket", - { - Bucket: this.bucket, - }, - function (err, data) { - if (err) { - return callback(err); - } - this.waitForBucketExists( - this.s3, - { - Bucket: bucket, - }, - callback - ); - } - ); -}); - -Given("I force path style requests", function (callback) { - this.s3 = new this.S3({ - forcePathStyle: true, - requestChecksumCalculation: "WHEN_REQUIRED", - responseChecksumValidation: "WHEN_REQUIRED", - }); - callback(); -}); - -Then("the bucket name should be in the request path", function (callback) { - const path = this.data.Body.req.path.split("/"); - this.assert.equal(path[1], this.bucket); - callback(); -}); - -Then("the bucket name should not be in the request host", function (callback) { - const host = this.data.Body.client.servername; - this.assert.compare(host.indexOf(this.bucket), "<", 0); - callback(); -}); - -When("I put {string} to the key {string} in the bucket", function (data, key, next) { - const params = { - Bucket: this.bucket, - Key: key, - Body: data, - }; - this.request("s3", "putObject", params, next, false); -}); - -When("I get the key {string} in the bucket", function (key, next) { - const params = { - Bucket: this.bucket, - Key: key, - }; - this.request("s3", "getObject", params, next, false); -}); - -Then("I delete the object {string} from the bucket", function (key, next) { - const params = { - Bucket: this.bucket, - Key: key, - }; - this.request("s3", "deleteObject", params, next); -}); - -When(/^I put a (small|large) buffer to the key "([^"]*)" in the bucket$/, function (size, key, next) { - const body = this.createBuffer(size); - const params = { - Bucket: this.bucket, - Key: key, - Body: body, - }; - this.request("s3", "putObject", params, next); -}); - -Then(/^the object "([^"]*)" should (not )?exist in the bucket$/, function (key, shouldNotExist, next) { - const params = { - Bucket: this.bucket, - Key: key, - }; - this.eventually(next, function (retry) { - retry.condition = function () { - if (shouldNotExist) { - return this.error && this.error.code === "NotFound"; - } else { - return !this.error; - } - }; - this.request("s3", "headObject", params, retry, false); - }); -}); diff --git a/features/s3/step_definitions/hooks.js b/features/s3/step_definitions/hooks.js deleted file mode 100644 index d422d69d3143c..0000000000000 --- a/features/s3/step_definitions/hooks.js +++ /dev/null @@ -1,11 +0,0 @@ -const { Before } = require("@cucumber/cucumber"); - -Before({ tags: "@s3" }, function (scenario, callback) { - const { S3 } = require("../../../clients/client-s3"); - this.service = this.s3 = new S3({ - maxRetries: 100, - requestChecksumCalculation: "WHEN_REQUIRED", - responseChecksumValidation: "WHEN_REQUIRED", - }); - callback(); -}); diff --git a/features/s3/step_definitions/objects.js b/features/s3/step_definitions/objects.js deleted file mode 100644 index 2cc5cb4972a8b..0000000000000 --- a/features/s3/step_definitions/objects.js +++ /dev/null @@ -1,458 +0,0 @@ -const { Before, Given, Then, When } = require("@cucumber/cucumber"); - -function getSignedUrl(client, command, params, callback) { - const { S3RequestPresigner } = require("../../../packages/s3-request-presigner"); - const { createRequest } = require("../../../packages/util-create-request"); - const { formatUrl } = require("../../../packages/util-format-url"); - const signer = new S3RequestPresigner({ ...client.config }); - createRequest(client, new command(params)) - .then((request) => { - const expiration = new Date(Date.now() + 1 * 60 * 60 * 1000); - signer - .presign(request, expiration) - .then((data) => { - callback(null, formatUrl(data)); - }) - .catch((err) => { - callback(err); - }); - }) - .catch((err) => { - callback(err); - }); -} - -Before({ tags: "@objects" }, function (scenario, callback) { - const { S3, GetObjectCommand, PutObjectCommand } = require("../../../clients/client-s3"); - const { streamCollector } = require("@smithy/node-http-handler"); - const { toUtf8 } = require("@smithy/util-utf8"); - const { Md5 } = require("@smithy/md5-js"); - this.S3 = S3; - this.GetObjectCommand = GetObjectCommand; - this.PutObjectCommand = PutObjectCommand; - this.streamCollector = streamCollector; - this.toUtf8 = toUtf8; - this.Md5 = Md5; - callback(); -}); - -When("I put {string} to the key {string}", function (data, key, next) { - const params = { - Bucket: this.sharedBucket, - Key: key, - Body: data, - }; - this.request("s3", "putObject", params, next, false); -}); - -When("I get the object {string}", function (key, next) { - const params = { - Bucket: this.sharedBucket, - Key: key, - }; - this.request("s3", "getObject", params, next, false); -}); - -When(/^I put (?:a |an )(empty|small|large|\d+KB|\d+MB) buffer to the key "([^"]*)"$/, function (size, key, next) { - const body = this.createBuffer(size); - const params = { - Bucket: this.sharedBucket, - Key: key, - Body: body, - }; - this.request("s3", "putObject", params, next); -}); - -When(/^I put (?:a |an )(empty|small|large) file to the key "([^"]*)"$/, function (size, key, next) { - const fs = require("fs"); - const filename = this.createFile(size, key); - const params = { - Bucket: this.sharedBucket, - Key: key, - Body: fs.createReadStream(filename), - }; - this.request("s3", "putObject", params, next); -}); - -When("I put {string} to the key {string} with ContentLength {int}", function (contents, key, contentLength, next) { - const params = { - Bucket: this.sharedBucket, - Key: key, - Body: contents, - ContentLength: parseInt(contentLength), - }; - this.s3nochecksums = new this.S3({ - computeChecksums: false, - }); - this.request("s3nochecksums", "putObject", params, next); -}); - -Then("the object {string} should contain {string}", function (key, contents, next) { - this.streamCollector(this.data.Body).then((body) => { - this.assert.equal(this.toUtf8(body), contents); - next(); - }); -}); - -Then("the HTTP response should have a content length of {int}", function (contentLength, next) { - this.assert.equal(this.data.ContentLength, contentLength); - next(); -}); - -When("I copy the object {string} to {string}", function (key1, key2, next) { - const params = { - Bucket: this.sharedBucket, - Key: key2, - CopySource: "/" + this.sharedBucket + "/" + key1, - }; - this.request("s3", "copyObject", params, next); -}); - -When("I delete the object {string}", function (key, next) { - const params = { - Bucket: this.sharedBucket, - Key: key, - }; - this.request("s3", "deleteObject", params, next); -}); - -Then(/^the object "([^"]*)" should (not )?exist$/, function (key, shouldNotExist, next) { - const params = { - Bucket: this.sharedBucket, - Key: key, - }; - this.eventually(next, function (retry) { - retry.condition = function () { - if (shouldNotExist) { - return this.error && this.error.name === "NotFound"; - } else { - return !this.error; - } - }; - this.request("s3", "headObject", params, retry, false); - }); -}); - -When("I stream key {string}", function (key, callback) { - const params = { - Bucket: this.sharedBucket, - Key: key, - }; - const world = this; - this.result = ""; - const s = this.service.getObject(params); - - setTimeout(function () { - s.on("end", function () { - callback(); - }); - s.on("data", function (d) { - world.result += d.toString(); - }); - }, 2000); // delay streaming to ensure it is buffered -}); - -When("I stream2 key {string}", function (key, callback) { - if (!require("stream").Readable) return callback(); - const params = { - Bucket: this.sharedBucket, - Key: key, - }; - const world = this; - this.result = ""; - const stream = this.service.getObject(params).createReadStream(); - setTimeout(function () { - stream.on("end", function () { - callback(); - }); - stream.on("readable", function () { - const v = stream.read(); - if (v) world.result += v; - }); - }, 2000); // delay streaming to ensure it is buffered -}); - -Then("the streamed data should contain {string}", function (data, callback) { - this.assert.equal(this.result.replace("\n", ""), data); - callback(); -}); - -Then("the streamed data content length should equal {int}", function (length, callback) { - this.assert.equal(this.result.length, length); - callback(); -}); - -When("I get a pre-signed URL to GET the key {string}", function (key, callback) { - const world = this; - getSignedUrl( - this.s3, - this.GetObjectCommand, - { - Bucket: this.sharedBucket, - Key: key, - }, - function (err, url) { - world.signedUrl = url; - callback(); - } - ); -}); - -When("I access the URL via HTTP GET", function (callback) { - const world = this; - this.data = ""; - require("https") - .get(this.signedUrl, function (res) { - res - .on("data", function (chunk) { - world.data += chunk.toString(); - }) - .on("end", callback); - }) - .on("error", callback); -}); - -Given("I get a pre-signed URL to PUT the key {string} with data {string}", function (key, body, callback) { - const world = this; - const params = { - Bucket: this.sharedBucket, - Key: key, - }; - if (body) params.Body = body; - getSignedUrl(this.s3, this.PutObjectCommand, params, function (err, url) { - world.signedUrl = url; - callback(); - }); -}); - -Given("I access the URL via HTTP PUT with data {string}", function (body, callback) { - const world = this; - this.data = ""; - - const data = body; - const options = require("url").parse(this.signedUrl); - options.method = "PUT"; - options.headers = { - "Content-Length": data.length, - }; - - require("https") - .request(options, function (res) { - res - .on("data", function (chunk) { - world.data += chunk.toString(); - }) - .on("end", callback); - }) - .on("error", callback) - .end(data); -}); - -Given("I create a presigned form to POST the key {string} with the data {string}", function (key, data, callback) { - const world = this; - const boundary = (this.postBoundary = "----WebKitFormBoundaryLL0mBKIuuLUKr7be"); - const conditions = [["content-length-range", data.length - 1, data.length + 1]], - params = { - Bucket: this.sharedBucket, - Fields: { - key: key, - }, - Conditions: conditions, - }; - this.s3.createPresignedPost(params, function (err, postData) { - const body = Object.keys(postData.fields).reduce(function (body, fieldName) { - body += "--" + boundary + "\r\n"; - body += 'Content-Disposition: form-data; name="' + fieldName + '"\r\n\r\n'; - return body + postData.fields[fieldName] + "\r\n"; - }, ""); - body += "--" + world.postBoundary + "\r\n"; - body += 'Content-Disposition: form-data; name="file"; filename="' + key + '"\r\n'; - body += "Content-Type: text/plain\r\n\r\n"; - body += data + "\r\n"; - body += "--" + world.postBoundary + "\r\n"; - body += 'Content-Disposition: form-data; name="submit"\r\n'; - body += "Content-Type: text/plain\r\n\r\n"; - body += "submit\r\n"; - body += "--" + world.postBoundary + "--\r\n"; - world.postBody = body; - world.postAction = postData.url; - callback(); - }); -}); - -Given("I POST the form", function (callback) { - const world = this; - const options = require("url").parse(this.postAction); - options.method = "POST"; - options.headers = { - "Content-Type": "multipart/form-data; boundary=" + this.postBoundary, - "Content-Length": this.postBody.length, - }; - require("https") - .request(options, function (res) { - res - .on("data", function (chunk) { - world.data += chunk.toString(); - }) - .on("end", callback); - }) - .on("error", callback) - .end(this.postBody); -}); - -Then("the HTTP response should equal {string}", function (data, callback) { - this.assert.equal(this.data, data); - callback(); -}); - -Then("the HTTP response should contain {string}", function (data, callback) { - this.assert.match(this.data, data); - callback(); -}); - -Given("I setup the listObjects request for the bucket", function (callback) { - this.params = { - Bucket: this.sharedBucket, - }; - callback(); -}); - -// progress events - -When( - /^I put (?:a |an )(empty|small|large|\d+KB|\d+MB) buffer to the key "([^"]*)" with progress events$/, - function (size, key, callback) { - const self = this; - const body = self.createBuffer(size); - this.progress = []; - const req = this.s3.putObject({ - Bucket: this.sharedBucket, - Key: key, - Body: body, - }); - req.on("httpUploadProgress", function (p) { - self.progress.push(p); - }); - req.send(callback); - } -); - -Then("more than {int} {string} event should fire", function (numEvents, eventName, callback) { - this.assert.compare(this.progress.length, ">", numEvents); - callback(); -}); - -Then("the {string} value of the progress event should equal {int}MB", function (prop, mb, callback) { - this.assert.equal(this.progress[0][prop], mb * 1024 * 1024); - callback(); -}); - -Then( - "the {string} value of the first progress event should be greater than {int} bytes", - function (prop, bytes, callback) { - this.assert.compare(this.progress[0][prop], ">", bytes); - callback(); - } -); - -When("I read the key {string} with progress events", function (key, callback) { - const self = this; - this.progress = []; - const req = this.s3.getObject({ - Bucket: this.sharedBucket, - Key: key, - }); - req.on("httpDownloadProgress", function (p) { - self.progress.push(p); - }); - req.send(callback); -}); - -When("I put {string} to the (public|private) key {string}", function (data, access, key, next) { - let acl; - if (access === "public") acl = "public-read"; - else if (access === "private") acl = access; - const params = { - Bucket: this.sharedBucket, - Key: key, - Body: data, - ACL: acl, - }; - this.request("s3", "putObject", params, next); -}); - -When("I put {string} to the key {string} with an AES key", function (data, key, next) { - const params = { - Bucket: this.sharedBucket, - Key: key, - Body: data, - SSECustomerAlgorithm: "AES256", - SSECustomerKey: "aaaabbbbccccddddaaaabbbbccccdddd", - }; - this.request("s3", "putObject", params, next); -}); - -When("I read the object {string} with the AES key", function (key, next) { - const params = { - Bucket: this.sharedBucket, - Key: key, - SSECustomerAlgorithm: "AES256", - SSECustomerKey: "aaaabbbbccccddddaaaabbbbccccdddd", - }; - this.request("s3", "getObject", params, next); -}); - -Then("I make an unauthenticated request to read object {string}", function (key, next) { - const params = { - Bucket: this.sharedBucket, - Key: key, - }; - this.s3.makeUnauthenticatedRequest( - "getObject", - params, - function (err, data) { - if (err) return next(err); - this.data = data; - next(); - }.bind(this) - ); -}); - -Given("I generate the MD5 checksum of {string}", function (data, next) { - const hash = new this.Md5(); - hash.update(data); - this.sentContentMD5 = hash.digest().toString(); - next(); -}); - -Then("the MD5 checksum of the response data should equal the generated checksum", function (next) { - const hash = new this.Md5(); - this.streamCollector(this.data.Body).then((body) => { - hash.update(body); - this.assert.equal(hash.digest(), this.sentContentMD5); - next(); - }); -}); - -Given("an empty bucket", function (next) { - const self = this; - const params = { - Bucket: this.sharedBucket, - }; - self.s3.listObjects(params, function (err, data) { - if (err) return next(err); - if (data.Contents.length > 0) { - params.Delete = { - Objects: [], - }; - data.Contents.forEach(function (item) { - params.Delete.Objects.push({ - Key: item.Key, - }); - }); - self.request("s3", "deleteObjects", params, next); - } else { - next(); - } - }); -}); diff --git a/features/s3/step_definitions/proxy.js b/features/s3/step_definitions/proxy.js deleted file mode 100644 index 95f4c549927bf..0000000000000 --- a/features/s3/step_definitions/proxy.js +++ /dev/null @@ -1,50 +0,0 @@ -const url = require("url"); -const http = require("http"); -const { Before, Then } = require("@cucumber/cucumber"); - -Before({ tags: "@s3 or @proxy" }, function (scenario, callback) { - const { S3 } = require("../../../clients/client-s3"); - setupProxyServer.call(this); - - this.service = this.s3 = new S3({ - httpOptions: { - proxy: "http://localhost:" + this.proxyPort, - }, - }); - this.S3 = S3; - callback(); -}); - -Then("I teardown the local proxy server", function (callback) { - this.service = this.s3 = new this.S3(); - this.proxyServer.close(callback); -}); - -function setupProxyServer() { - if (this.proxyServer) return; - this.proxyPort = 8000 + parseInt(Math.random() * 100); - this.proxyServer = http.createServer(function (req, res) { - const uri = url.parse(req.url); - const options = { - host: uri.hostname, - port: uri.port || 80, - method: req.method, - path: uri.path, - headers: req.headers, - }; - options.headers.host = uri.hostname; - - const s = http.request(options, function (res2) { - res.writeHead(res2.statusCode, res2.headers); - res2 - .on("data", function (ch) { - res.write(ch); - }) - .on("end", function () { - res.end(); - }); - }); - req.pipe(s); - }); - this.proxyServer.listen(this.proxyPort); -}