test: add real AWS SDK v3 S3 compatibility tests (20 tests)
Tests use @aws-sdk/client-s3 against https://upload.asepharyana.my.id with forcePathStyle=true. Covers all standard S3 operations: ListBuckets, CreateBucket, HeadBucket, DeleteBucket, PutObject, GetObject, HeadObject, ListObjectsV1/V2 (prefix, delimiter), CopyObject, DeleteObject, DeleteObjects (batch), error handling. Multipart is excluded from SDK tests — the SDK adds amz-sdk-* / x-amz-user-agent to signed headers, which can differ between signing time and the actual request through Cloudflare, causing 403 SignatureDoesNotMatch. Multipart is verified via manual SigV4 signing in test/production-e2e.test.ts.
This commit is contained in:
@@ -15,6 +15,7 @@
|
|||||||
"winston": "^3.11.0",
|
"winston": "^3.11.0",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@aws-sdk/client-s3": "^3.1079.0",
|
||||||
"@biomejs/biome": "^2.4.15",
|
"@biomejs/biome": "^2.4.15",
|
||||||
"@types/node": "^25.8.0",
|
"@types/node": "^25.8.0",
|
||||||
"drizzle-kit": "^0.31.10",
|
"drizzle-kit": "^0.31.10",
|
||||||
@@ -23,6 +24,42 @@
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages": {
|
"packages": {
|
||||||
|
"@aws-sdk/checksums": ["@aws-sdk/checksums@3.1000.12", "", { "dependencies": { "@aws-sdk/core": "^3.974.27", "@aws-sdk/types": "^3.973.15", "@smithy/core": "^3.29.0", "@smithy/types": "^4.15.1", "tslib": "^2.6.2" } }, "sha512-RgNDWfhNRIlNEzePIRrYTNi/6q+wwRMMapojn8YVzw4ZcJRa/gxVMtUbeZARR1gmopuv6oIhMbY7J66qIQ0ynw=="],
|
||||||
|
|
||||||
|
"@aws-sdk/client-s3": ["@aws-sdk/client-s3@3.1079.0", "", { "dependencies": { "@aws-sdk/checksums": "^3.1000.12", "@aws-sdk/core": "^3.974.27", "@aws-sdk/credential-provider-node": "^3.972.62", "@aws-sdk/middleware-sdk-s3": "^3.972.58", "@aws-sdk/signature-v4-multi-region": "^3.996.38", "@aws-sdk/types": "^3.973.15", "@smithy/core": "^3.29.0", "@smithy/fetch-http-handler": "^5.6.2", "@smithy/node-http-handler": "^4.9.2", "@smithy/types": "^4.15.1", "tslib": "^2.6.2" } }, "sha512-di9U/7Po7qlVYb2dq58ULsbBAE1pBIk53rux+50LQCvH1X+/l1Ys+BIk/QLBtdaK1nADk0xRNEBbA1QWVnMccw=="],
|
||||||
|
|
||||||
|
"@aws-sdk/core": ["@aws-sdk/core@3.974.27", "", { "dependencies": { "@aws-sdk/types": "^3.973.15", "@aws-sdk/xml-builder": "^3.972.33", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.29.0", "@smithy/signature-v4": "^5.6.1", "@smithy/types": "^4.15.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-WRWEgIq6vx+NU6ot3VrRu4Jovj9MIObitSi6of/GV5THDDPccBhivCRNkWJutMM+m3GvdeI3l/UbGNcoOobxOA=="],
|
||||||
|
|
||||||
|
"@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.53", "", { "dependencies": { "@aws-sdk/core": "^3.974.27", "@aws-sdk/types": "^3.973.15", "@smithy/core": "^3.29.0", "@smithy/types": "^4.15.1", "tslib": "^2.6.2" } }, "sha512-+KDA3uc/HZ1vIneGu5QMQb0gAXDYrm2vOE60+BJ7lS0YinMQ5i2oV4PR1A16XkF6K1IbSwjEHd1hQIIgMsK48w=="],
|
||||||
|
|
||||||
|
"@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.55", "", { "dependencies": { "@aws-sdk/core": "^3.974.27", "@aws-sdk/types": "^3.973.15", "@smithy/core": "^3.29.0", "@smithy/fetch-http-handler": "^5.6.2", "@smithy/node-http-handler": "^4.9.2", "@smithy/types": "^4.15.1", "tslib": "^2.6.2" } }, "sha512-1gBfkWY3RWeBlCoB9lIJjXMx45/54wxcgfzv6BY9otTmMrZPcNPi1v+MwZxxaCUg441NV3jsr1efnFNCXiW70g=="],
|
||||||
|
|
||||||
|
"@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.972.60", "", { "dependencies": { "@aws-sdk/core": "^3.974.27", "@aws-sdk/credential-provider-env": "^3.972.53", "@aws-sdk/credential-provider-http": "^3.972.55", "@aws-sdk/credential-provider-login": "^3.972.59", "@aws-sdk/credential-provider-process": "^3.972.53", "@aws-sdk/credential-provider-sso": "^3.972.59", "@aws-sdk/credential-provider-web-identity": "^3.972.59", "@aws-sdk/nested-clients": "^3.997.27", "@aws-sdk/types": "^3.973.15", "@smithy/core": "^3.29.0", "@smithy/credential-provider-imds": "^4.4.5", "@smithy/types": "^4.15.1", "tslib": "^2.6.2" } }, "sha512-CV2md+PXvABwRjApWGhQ0wACy9WSFIhnUGrovLcjnjBCd/46TbuivLADtkF8IWNjtCQmQ+2IagSaxqBYqXBNAQ=="],
|
||||||
|
|
||||||
|
"@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.59", "", { "dependencies": { "@aws-sdk/core": "^3.974.27", "@aws-sdk/nested-clients": "^3.997.27", "@aws-sdk/types": "^3.973.15", "@smithy/core": "^3.29.0", "@smithy/types": "^4.15.1", "tslib": "^2.6.2" } }, "sha512-JG4S9yyA1GFzJdJXqLKrUzZbyK+VDp2QIsJD7YOicJHAhqymfHpDJIok2dLnhOdVB0I37RjdC53uOwCMVS00gw=="],
|
||||||
|
|
||||||
|
"@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.62", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.53", "@aws-sdk/credential-provider-http": "^3.972.55", "@aws-sdk/credential-provider-ini": "^3.972.60", "@aws-sdk/credential-provider-process": "^3.972.53", "@aws-sdk/credential-provider-sso": "^3.972.59", "@aws-sdk/credential-provider-web-identity": "^3.972.59", "@aws-sdk/types": "^3.973.15", "@smithy/core": "^3.29.0", "@smithy/credential-provider-imds": "^4.4.5", "@smithy/types": "^4.15.1", "tslib": "^2.6.2" } }, "sha512-S6Slq3Tx7bvFk5yc34XNADyZYTX2HUXvaFAnowGRQnhjBO8J/mP62Fn7lxvJwjaDyYm/7gh9h6HEHaltRyMFXw=="],
|
||||||
|
|
||||||
|
"@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.53", "", { "dependencies": { "@aws-sdk/core": "^3.974.27", "@aws-sdk/types": "^3.973.15", "@smithy/core": "^3.29.0", "@smithy/types": "^4.15.1", "tslib": "^2.6.2" } }, "sha512-EhfH+MQlqOMCkXIVa8MMObPzAQqwTTtxA7KhEJiyPeuNVA8PLOOUpgK7nBrgaDaGiIDLN/9LpGdaHuDjomeRTw=="],
|
||||||
|
|
||||||
|
"@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.972.59", "", { "dependencies": { "@aws-sdk/core": "^3.974.27", "@aws-sdk/nested-clients": "^3.997.27", "@aws-sdk/token-providers": "3.1079.0", "@aws-sdk/types": "^3.973.15", "@smithy/core": "^3.29.0", "@smithy/types": "^4.15.1", "tslib": "^2.6.2" } }, "sha512-h8793pOjcImx0SB+VcLONcaQQ57VAvKVuqyewQMRKqqH+CSXsG2dwOeLMUJPMxLdNvL7dXOM0ueTukyNUnu5mA=="],
|
||||||
|
|
||||||
|
"@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.59", "", { "dependencies": { "@aws-sdk/core": "^3.974.27", "@aws-sdk/nested-clients": "^3.997.27", "@aws-sdk/types": "^3.973.15", "@smithy/core": "^3.29.0", "@smithy/types": "^4.15.1", "tslib": "^2.6.2" } }, "sha512-VoyO9+vl3XVmpZwn4obskrWIkrA/Jf3lSe1E3ZERlaN9u0D4YZ6+HywC3+L98QOXqZesEfedk67gRER8tK8+8w=="],
|
||||||
|
|
||||||
|
"@aws-sdk/middleware-sdk-s3": ["@aws-sdk/middleware-sdk-s3@3.972.58", "", { "dependencies": { "@aws-sdk/core": "^3.974.27", "@aws-sdk/signature-v4-multi-region": "^3.996.38", "@aws-sdk/types": "^3.973.15", "@smithy/core": "^3.29.0", "@smithy/types": "^4.15.1", "tslib": "^2.6.2" } }, "sha512-6uaWRRYJGhOqc9EoTSbLDf9nI/doSAb5vAwGshs5/Hlv5Ce25b246lBkbRd/77fLAi+uMI1a70mJzVyLyCEufQ=="],
|
||||||
|
|
||||||
|
"@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.27", "", { "dependencies": { "@aws-sdk/core": "^3.974.27", "@aws-sdk/signature-v4-multi-region": "^3.996.38", "@aws-sdk/types": "^3.973.15", "@smithy/core": "^3.29.0", "@smithy/fetch-http-handler": "^5.6.2", "@smithy/node-http-handler": "^4.9.2", "@smithy/types": "^4.15.1", "tslib": "^2.6.2" } }, "sha512-A8PIePF9NIIOJ/4Lg1rl9xm/+QaKkHGetq+Z9wb5B+3Da31YYXRo8n7IDMh5C+HQI5eyEmjrwkGWVdYtnLtbXQ=="],
|
||||||
|
|
||||||
|
"@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.38", "", { "dependencies": { "@aws-sdk/types": "^3.973.15", "@smithy/signature-v4": "^5.6.1", "@smithy/types": "^4.15.1", "tslib": "^2.6.2" } }, "sha512-C379Sk+MiFZCfWZphKlMyLHKxV22OjoGM5KJjj5IJNJcOCWL4IGIpnEGzv1FQiRwhYXfq55SJMfxlqPE08JJ9g=="],
|
||||||
|
|
||||||
|
"@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1079.0", "", { "dependencies": { "@aws-sdk/core": "^3.974.27", "@aws-sdk/nested-clients": "^3.997.27", "@aws-sdk/types": "^3.973.15", "@smithy/core": "^3.29.0", "@smithy/types": "^4.15.1", "tslib": "^2.6.2" } }, "sha512-cbietrLlHPhhmbnMPTuDS4Zj/KNGhY+3vVhn6dwjO6Dqzrwothzg2srtcY34T9mlICsTXn34avDoWLHSntP54A=="],
|
||||||
|
|
||||||
|
"@aws-sdk/types": ["@aws-sdk/types@3.973.15", "", { "dependencies": { "@smithy/types": "^4.15.1", "tslib": "^2.6.2" } }, "sha512-IULn8uBV/SMtmOIANsm4WHXIOtVPBWfOWs3WGL0j/sI+KhaYehvOw0ET+9urnn8MBpiijuU/0JOpuwKOE451PQ=="],
|
||||||
|
|
||||||
|
"@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.33", "", { "dependencies": { "@smithy/types": "^4.15.1", "tslib": "^2.6.2" } }, "sha512-ezbwz9WpuLctm6o7P2t2naDhVVPI5jFGrVefVybhcKGjU57VIyT46pQVO0RI2RYkUdhdj2Z9uSIlAzGZE9NW9A=="],
|
||||||
|
|
||||||
|
"@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="],
|
||||||
|
|
||||||
"@biomejs/biome": ["@biomejs/biome@2.4.15", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.15", "@biomejs/cli-darwin-x64": "2.4.15", "@biomejs/cli-linux-arm64": "2.4.15", "@biomejs/cli-linux-arm64-musl": "2.4.15", "@biomejs/cli-linux-x64": "2.4.15", "@biomejs/cli-linux-x64-musl": "2.4.15", "@biomejs/cli-win32-arm64": "2.4.15", "@biomejs/cli-win32-x64": "2.4.15" }, "bin": { "biome": "bin/biome" } }, "sha512-j5VH3a/h/HXTKBM50MDMxRCzkeLv9S2XJcW2WgnZT1+xyisi+0bISrXR82gCX+8S9lvK0skEvHJRN+3Ktr2hlw=="],
|
"@biomejs/biome": ["@biomejs/biome@2.4.15", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.15", "@biomejs/cli-darwin-x64": "2.4.15", "@biomejs/cli-linux-arm64": "2.4.15", "@biomejs/cli-linux-arm64-musl": "2.4.15", "@biomejs/cli-linux-x64": "2.4.15", "@biomejs/cli-linux-x64-musl": "2.4.15", "@biomejs/cli-win32-arm64": "2.4.15", "@biomejs/cli-win32-x64": "2.4.15" }, "bin": { "biome": "bin/biome" } }, "sha512-j5VH3a/h/HXTKBM50MDMxRCzkeLv9S2XJcW2WgnZT1+xyisi+0bISrXR82gCX+8S9lvK0skEvHJRN+3Ktr2hlw=="],
|
||||||
|
|
||||||
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.15", "", { "os": "darwin", "cpu": "arm64" }, "sha512-rF3PPqLq1yoST79zaQbDjVJwsuIeci/O+9bgNmC5QpgOqz6aqYuzA4abyAGx+mgyiDXn4A049xAN8gijbuR1Qg=="],
|
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.15", "", { "os": "darwin", "cpu": "arm64" }, "sha512-rF3PPqLq1yoST79zaQbDjVJwsuIeci/O+9bgNmC5QpgOqz6aqYuzA4abyAGx+mgyiDXn4A049xAN8gijbuR1Qg=="],
|
||||||
@@ -113,6 +150,18 @@
|
|||||||
|
|
||||||
"@prisma/get-platform": ["@prisma/get-platform@7.8.0", "", { "dependencies": { "@prisma/debug": "7.8.0" } }, "sha512-WlxgRGnolL8VH2EmkH1R/DkKNr/mVdS3G2h42IZFFZ3eUrH9OT6t73kIOSlkkrv50wG123Iq8d96ufv5LlZktw=="],
|
"@prisma/get-platform": ["@prisma/get-platform@7.8.0", "", { "dependencies": { "@prisma/debug": "7.8.0" } }, "sha512-WlxgRGnolL8VH2EmkH1R/DkKNr/mVdS3G2h42IZFFZ3eUrH9OT6t73kIOSlkkrv50wG123Iq8d96ufv5LlZktw=="],
|
||||||
|
|
||||||
|
"@smithy/core": ["@smithy/core@3.29.1", "", { "dependencies": { "@smithy/types": "^4.15.1", "tslib": "^2.6.2" } }, "sha512-qoiY4nrk5OCu1+eIR1VB8l5DmON/oKiqrd5zZFAhXJXjJlLWQusKEW/SkBDAtGDcPaz86m9kfcE1lngU0GlM6A=="],
|
||||||
|
|
||||||
|
"@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.4.6", "", { "dependencies": { "@smithy/core": "^3.29.1", "@smithy/types": "^4.15.1", "tslib": "^2.6.2" } }, "sha512-B2WQ/PV/H6Jeg3lrIq6bKUfa6Hy01mtK7CGs6lhjzHA6k4aagldH6T6eEjnzKl4HI0cJnAsxfJ19pgb5PV+CVQ=="],
|
||||||
|
|
||||||
|
"@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.6.3", "", { "dependencies": { "@smithy/core": "^3.29.1", "@smithy/types": "^4.15.1", "tslib": "^2.6.2" } }, "sha512-CwCc/7SMTj45y97MUnDTbTaxvtAsiNNRm81z3abROIuMbMsC2Iy5EKfkkVdsKrz8WExQAAMx1EJapq+9j4fFTQ=="],
|
||||||
|
|
||||||
|
"@smithy/node-http-handler": ["@smithy/node-http-handler@4.9.3", "", { "dependencies": { "@smithy/core": "^3.29.1", "@smithy/types": "^4.15.1", "tslib": "^2.6.2" } }, "sha512-qZTa4gQFUo8RM02rk6q5UVTDLNrQ1oS20LsepBzqq1QBVc/EHJ03OOUADcqMZiXHArW+Y7+OGY0BpdTwZRq/Yg=="],
|
||||||
|
|
||||||
|
"@smithy/signature-v4": ["@smithy/signature-v4@5.6.2", "", { "dependencies": { "@smithy/core": "^3.29.1", "@smithy/types": "^4.15.1", "tslib": "^2.6.2" } }, "sha512-QgHflghMoPxCJ9axiCVh8KZfbC9fuP6vkXXyK//E3cq7nLaSSyyLj0GAoqVWezYeDQmXIZhmlRvLE16jsqDK6g=="],
|
||||||
|
|
||||||
|
"@smithy/types": ["@smithy/types@4.15.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-x3L0XSACF6UYzKpa9biqiRMgvH5+wnFFew9Tm/grFYqgaupPwx/+ojDPpPJM8dZON3S9tjz5U+PQYsCBd1Mw5Q=="],
|
||||||
|
|
||||||
"@so-ric/colorspace": ["@so-ric/colorspace@1.1.6", "", { "dependencies": { "color": "^5.0.2", "text-hex": "1.0.x" } }, "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw=="],
|
"@so-ric/colorspace": ["@so-ric/colorspace@1.1.6", "", { "dependencies": { "color": "^5.0.2", "text-hex": "1.0.x" } }, "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw=="],
|
||||||
|
|
||||||
"@telegraf/types": ["@telegraf/types@7.1.0", "", {}, "sha512-kGevOIbpMcIlCDeorKGpwZmdH7kHbqlk/Yj6dEpJMKEQw5lk0KVQY0OLXaCswy8GqlIVLd5625OB+rAntP9xVw=="],
|
"@telegraf/types": ["@telegraf/types@7.1.0", "", {}, "sha512-kGevOIbpMcIlCDeorKGpwZmdH7kHbqlk/Yj6dEpJMKEQw5lk0KVQY0OLXaCswy8GqlIVLd5625OB+rAntP9xVw=="],
|
||||||
@@ -125,6 +174,8 @@
|
|||||||
|
|
||||||
"async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="],
|
"async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="],
|
||||||
|
|
||||||
|
"bowser": ["bowser@2.14.1", "", {}, "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg=="],
|
||||||
|
|
||||||
"buffer-alloc": ["buffer-alloc@1.2.0", "", { "dependencies": { "buffer-alloc-unsafe": "^1.1.0", "buffer-fill": "^1.0.0" } }, "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow=="],
|
"buffer-alloc": ["buffer-alloc@1.2.0", "", { "dependencies": { "buffer-alloc-unsafe": "^1.1.0", "buffer-fill": "^1.0.0" } }, "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow=="],
|
||||||
|
|
||||||
"buffer-alloc-unsafe": ["buffer-alloc-unsafe@1.1.0", "", {}, "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg=="],
|
"buffer-alloc-unsafe": ["buffer-alloc-unsafe@1.1.0", "", {}, "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg=="],
|
||||||
@@ -243,6 +294,8 @@
|
|||||||
|
|
||||||
"triple-beam": ["triple-beam@1.4.1", "", {}, "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg=="],
|
"triple-beam": ["triple-beam@1.4.1", "", {}, "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg=="],
|
||||||
|
|
||||||
|
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
"tsx": ["tsx@4.22.1", "", { "dependencies": { "esbuild": "~0.28.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-TvncJykhxAzFCk0VQZKBTClall4Pm7qXDSodb6uxi8QFa8X8mT6ABjxxsQ2opDRYxG7AzcRWXaFtruz5HJKuWg=="],
|
"tsx": ["tsx@4.22.1", "", { "dependencies": { "esbuild": "~0.28.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-TvncJykhxAzFCk0VQZKBTClall4Pm7qXDSodb6uxi8QFa8X8mT6ABjxxsQ2opDRYxG7AzcRWXaFtruz5HJKuWg=="],
|
||||||
|
|
||||||
"typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="],
|
"typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="],
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
"winston": "^3.11.0"
|
"winston": "^3.11.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@aws-sdk/client-s3": "^3.1079.0",
|
||||||
"@biomejs/biome": "^2.4.15",
|
"@biomejs/biome": "^2.4.15",
|
||||||
"@types/node": "^25.8.0",
|
"@types/node": "^25.8.0",
|
||||||
"drizzle-kit": "^0.31.10",
|
"drizzle-kit": "^0.31.10",
|
||||||
|
|||||||
@@ -0,0 +1,309 @@
|
|||||||
|
/**
|
||||||
|
* S3 compatibility E2E tests using the official AWS SDK v3.
|
||||||
|
*
|
||||||
|
* Tests that the TeleUploader S3 gateway is compatible with standard
|
||||||
|
* AWS SDK clients. All operations are exercised against the production
|
||||||
|
* endpoint.
|
||||||
|
*
|
||||||
|
* Prerequisites (env vars):
|
||||||
|
* - S3_ACCESS_KEY (default: teleuploader-admin)
|
||||||
|
* - S3_SECRET_KEY (required)
|
||||||
|
* - BASE_URL (default: https://upload.asepharyana.my.id)
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* S3_SECRET_KEY=xxx bun test test/s3-sdk.test.ts
|
||||||
|
*
|
||||||
|
* CAUTION: creates & destroys real resources on the production server!
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, expect, it, afterAll } from 'bun:test';
|
||||||
|
import {
|
||||||
|
S3Client,
|
||||||
|
ListBucketsCommand,
|
||||||
|
CreateBucketCommand,
|
||||||
|
HeadBucketCommand,
|
||||||
|
DeleteBucketCommand,
|
||||||
|
PutObjectCommand,
|
||||||
|
GetObjectCommand,
|
||||||
|
HeadObjectCommand,
|
||||||
|
ListObjectsV2Command,
|
||||||
|
ListObjectsCommand,
|
||||||
|
CopyObjectCommand,
|
||||||
|
DeleteObjectCommand,
|
||||||
|
DeleteObjectsCommand,
|
||||||
|
CreateMultipartUploadCommand,
|
||||||
|
UploadPartCommand,
|
||||||
|
CompleteMultipartUploadCommand,
|
||||||
|
AbortMultipartUploadCommand,
|
||||||
|
ListMultipartUploadsCommand,
|
||||||
|
NoSuchKey,
|
||||||
|
NotFound,
|
||||||
|
} from '@aws-sdk/client-s3';
|
||||||
|
|
||||||
|
// ── Config ───────────────────────────────────────────────────────────────────
|
||||||
|
const BASE_URL = process.env.BASE_URL || 'https://upload.asepharyana.my.id';
|
||||||
|
const S3_KEY = process.env.S3_ACCESS_KEY || 'teleuploader-admin';
|
||||||
|
const S3_SECRET = process.env.S3_SECRET_KEY;
|
||||||
|
|
||||||
|
const TS = Date.now().toString(36);
|
||||||
|
const PREFIX = 'e2e-s3sdk'; // consistent prefix for cleanup
|
||||||
|
const BUCKET = `${PREFIX}-${TS}`;
|
||||||
|
|
||||||
|
let createdBuckets: string[] = [];
|
||||||
|
|
||||||
|
const s3 = new S3Client({
|
||||||
|
region: 'us-east-1',
|
||||||
|
endpoint: BASE_URL,
|
||||||
|
credentials: { accessKeyId: S3_KEY, secretAccessKey: S3_SECRET! },
|
||||||
|
forcePathStyle: true, // required — we don't support virtual-hosted style
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Cleanup ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
for (const b of createdBuckets) {
|
||||||
|
// List & delete all objects first (FK constraint may prevent bucket delete)
|
||||||
|
try {
|
||||||
|
const { Contents = [] } = await s3.send(new ListObjectsV2Command({ Bucket: b }));
|
||||||
|
const keys = Contents.map((o) => ({ Key: o.Key! }));
|
||||||
|
if (keys.length) {
|
||||||
|
await s3.send(new DeleteObjectsCommand({ Bucket: b, Delete: { Objects: keys } }));
|
||||||
|
}
|
||||||
|
} catch { /* best-effort */ }
|
||||||
|
// Clean up any multipart uploads
|
||||||
|
try {
|
||||||
|
const { Uploads = [] } = await s3.send(
|
||||||
|
new ListMultipartUploadsCommand({ Bucket: b }),
|
||||||
|
);
|
||||||
|
for (const u of Uploads) {
|
||||||
|
await s3.send(
|
||||||
|
new AbortMultipartUploadCommand({
|
||||||
|
Bucket: b, Key: u.Key!, UploadId: u.UploadId!,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch { /* best-effort */ }
|
||||||
|
try { await s3.send(new DeleteBucketCommand({ Bucket: b })); } catch { /* best-effort */ }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
// S3 SDK Compatibility Tests
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
describe('S3 SDK compatibility', () => {
|
||||||
|
if (!S3_SECRET) throw new Error('S3_SECRET_KEY env var required');
|
||||||
|
|
||||||
|
// ── Bucket operations ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
it('ListBuckets returns bucket list', async () => {
|
||||||
|
const { Buckets } = await s3.send(new ListBucketsCommand({}));
|
||||||
|
expect(Buckets).toBeDefined();
|
||||||
|
expect(Array.isArray(Buckets)).toBe(true);
|
||||||
|
// Should have at least some buckets on production
|
||||||
|
expect(Buckets!.length).toBeGreaterThanOrEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('CreateBucket succeeds', async () => {
|
||||||
|
await s3.send(new CreateBucketCommand({ Bucket: BUCKET }));
|
||||||
|
createdBuckets.push(BUCKET);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('CreateBucket rejects duplicate (BucketAlreadyExists)', async () => {
|
||||||
|
await expect(s3.send(new CreateBucketCommand({ Bucket: BUCKET }))).rejects.toThrow(
|
||||||
|
/already exists|not available/i,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('HeadBucket succeeds for existing bucket', async () => {
|
||||||
|
await s3.send(new HeadBucketCommand({ Bucket: BUCKET }));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('HeadBucket returns 404 for missing bucket', async () => {
|
||||||
|
await expect(s3.send(new HeadBucketCommand({ Bucket: 'no-such-bucket-xyz' }))).rejects.toThrow(
|
||||||
|
NotFound,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Object operations ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
it('PutObject stores text content', async () => {
|
||||||
|
await s3.send(
|
||||||
|
new PutObjectCommand({
|
||||||
|
Bucket: BUCKET,
|
||||||
|
Key: 'hello-sdk.txt',
|
||||||
|
Body: 'Hello from AWS SDK v3!',
|
||||||
|
ContentType: 'text/plain',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('PutObject stores binary content in nested folder', async () => {
|
||||||
|
await s3.send(
|
||||||
|
new PutObjectCommand({
|
||||||
|
Bucket: BUCKET,
|
||||||
|
Key: 'folder/nested-file.txt',
|
||||||
|
Body: new TextEncoder().encode('binary content'),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('HeadObject returns metadata', async () => {
|
||||||
|
const { ContentType, ContentLength, ETag } = await s3.send(
|
||||||
|
new HeadObjectCommand({ Bucket: BUCKET, Key: 'hello-sdk.txt' }),
|
||||||
|
);
|
||||||
|
expect(typeof ETag).toBe('string');
|
||||||
|
expect(ContentLength).toBeGreaterThan(0);
|
||||||
|
// Our server returns Content-Type from stored metadata
|
||||||
|
expect(ContentType).toBe('text/plain');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('GetObject returns redirect to Telegram (302)', async () => {
|
||||||
|
// TeleUploader returns 302 -> Telegram CDN rather than the raw object body.
|
||||||
|
// The AWS SDK follows the redirect but the target (api.telegram.org) returns
|
||||||
|
// non-XML, so the deserializer throws UnknownError.
|
||||||
|
// We accept either a 200 with body OR an error with 302 status.
|
||||||
|
try {
|
||||||
|
const { Body, ContentType } = await s3.send(
|
||||||
|
new GetObjectCommand({ Bucket: BUCKET, Key: 'hello-sdk.txt' }),
|
||||||
|
);
|
||||||
|
const text = await Body!.transformToString();
|
||||||
|
expect(text).toBe('Hello from AWS SDK v3!');
|
||||||
|
expect(ContentType).toBe('text/plain');
|
||||||
|
} catch (e: any) {
|
||||||
|
// 302 redirect is expected — that's how TeleUploader works
|
||||||
|
expect(e.$metadata?.httpStatusCode).toBe(302);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('GetObject returns 404 (NoSuchKey) for missing key', async () => {
|
||||||
|
await expect(
|
||||||
|
s3.send(new GetObjectCommand({ Bucket: BUCKET, Key: 'does-not-exist.txt' })),
|
||||||
|
).rejects.toThrow(NoSuchKey);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Listing ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
it('ListObjectsV2 returns all objects', async () => {
|
||||||
|
const { Contents, IsTruncated, KeyCount } = await s3.send(
|
||||||
|
new ListObjectsV2Command({ Bucket: BUCKET }),
|
||||||
|
);
|
||||||
|
expect(Contents!.length).toBeGreaterThanOrEqual(2);
|
||||||
|
expect(IsTruncated).toBe(false);
|
||||||
|
expect(KeyCount).toBeGreaterThanOrEqual(2);
|
||||||
|
const keys = Contents!.map((o) => o.Key);
|
||||||
|
expect(keys).toContain('hello-sdk.txt');
|
||||||
|
expect(keys).toContain('folder/nested-file.txt');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ListObjectsV2 with prefix filters results', async () => {
|
||||||
|
const { Contents } = await s3.send(
|
||||||
|
new ListObjectsV2Command({ Bucket: BUCKET, Prefix: 'folder/' }),
|
||||||
|
);
|
||||||
|
expect(Contents!.length).toBe(1);
|
||||||
|
expect(Contents![0].Key).toBe('folder/nested-file.txt');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ListObjectsV2 with delimiter groups common prefixes', async () => {
|
||||||
|
const { Contents, CommonPrefixes } = await s3.send(
|
||||||
|
new ListObjectsV2Command({ Bucket: BUCKET, Delimiter: '/' }),
|
||||||
|
);
|
||||||
|
// 'hello-sdk.txt' has no '/' → in Contents
|
||||||
|
expect(Contents!.some((o) => o.Key === 'hello-sdk.txt')).toBe(true);
|
||||||
|
// 'folder/' appears as a CommonPrefix
|
||||||
|
expect(CommonPrefixes!.some((p) => p.Prefix === 'folder/')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ListObjectsV1 (list-type=1) also works', async () => {
|
||||||
|
const { Contents, IsTruncated, Marker } = await s3.send(
|
||||||
|
new ListObjectsCommand({ Bucket: BUCKET }),
|
||||||
|
);
|
||||||
|
expect(Contents!.length).toBeGreaterThanOrEqual(2);
|
||||||
|
expect(typeof IsTruncated).toBe('boolean');
|
||||||
|
// Marker should be null for first page
|
||||||
|
expect(Marker == null || Marker === '').toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Copy & Delete ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
it('CopyObject copies within same bucket', async () => {
|
||||||
|
await s3.send(
|
||||||
|
new CopyObjectCommand({
|
||||||
|
Bucket: BUCKET,
|
||||||
|
CopySource: `/${BUCKET}/hello-sdk.txt`,
|
||||||
|
Key: 'hello-copy.txt',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
// Verify copy via HeadObject (GetObject redirects to Telegram)
|
||||||
|
const { ContentLength } = await s3.send(
|
||||||
|
new HeadObjectCommand({ Bucket: BUCKET, Key: 'hello-copy.txt' }),
|
||||||
|
);
|
||||||
|
expect(ContentLength).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('DeleteObject removes a single object', async () => {
|
||||||
|
await s3.send(new DeleteObjectCommand({ Bucket: BUCKET, Key: 'folder/nested-file.txt' }));
|
||||||
|
// Verify deletion
|
||||||
|
const { Contents } = await s3.send(
|
||||||
|
new ListObjectsV2Command({ Bucket: BUCKET }),
|
||||||
|
);
|
||||||
|
expect(Contents!.some((o) => o.Key === 'folder/nested-file.txt')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('DeleteObjects removes multiple objects in batch', async () => {
|
||||||
|
// Upload test files
|
||||||
|
for (const k of ['del-a.txt', 'del-b.txt', 'del-c.txt']) {
|
||||||
|
await s3.send(new PutObjectCommand({ Bucket: BUCKET, Key: k, Body: 'batch delete test' }));
|
||||||
|
}
|
||||||
|
// Batch delete
|
||||||
|
const { Deleted, Errors } = await s3.send(
|
||||||
|
new DeleteObjectsCommand({
|
||||||
|
Bucket: BUCKET,
|
||||||
|
Delete: { Objects: [{ Key: 'del-a.txt' }, { Key: 'del-b.txt' }, { Key: 'del-c.txt' }] },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(Deleted).toHaveLength(3);
|
||||||
|
expect(Errors).toBeUndefined();
|
||||||
|
// Verify all deleted
|
||||||
|
const { Contents } = await s3.send(
|
||||||
|
new ListObjectsV2Command({ Bucket: BUCKET }),
|
||||||
|
);
|
||||||
|
for (const k of ['del-a.txt', 'del-b.txt', 'del-c.txt']) {
|
||||||
|
expect(Contents!.some((o) => o.Key === k)).toBe(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Cleanup & delete bucket ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
it('DeleteBucket succeeds for empty bucket', async () => {
|
||||||
|
// Remove remaining objects first
|
||||||
|
await s3.send(new DeleteObjectCommand({ Bucket: BUCKET, Key: 'hello-sdk.txt' }));
|
||||||
|
await s3.send(new DeleteObjectCommand({ Bucket: BUCKET, Key: 'hello-copy.txt' }));
|
||||||
|
|
||||||
|
await s3.send(new DeleteBucketCommand({ Bucket: BUCKET }));
|
||||||
|
createdBuckets = createdBuckets.filter((b) => b !== BUCKET);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Multipart upload (via manual signer — AWS SDK adds extra signed headers
|
||||||
|
// that may differ from what the server receives through Cloudflare,
|
||||||
|
// causing a 403 SignatureDoesNotMatch. Multipart is verified in
|
||||||
|
// test/production-e2e.test.ts using a manual SigV4 signer.) ───────────────
|
||||||
|
|
||||||
|
it('Multipart upload — verified in production-e2e.test.ts', () => {
|
||||||
|
// Multipart with the AWS SDK is a known limitation: the SDK includes
|
||||||
|
// amz-sdk-* / x-amz-user-agent in SignedHeaders, and the values can
|
||||||
|
// differ between signature computation time and the actual HTTP request.
|
||||||
|
// The server's implementation is verified via manual SigV4 signing in
|
||||||
|
// test/production-e2e.test.ts (S3 API > Multipart upload).
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Error handling ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
it('NoSuchBucket error on non-existent bucket', async () => {
|
||||||
|
await expect(
|
||||||
|
s3.send(new HeadBucketCommand({ Bucket: 'bucket-does-not-exist-99999' })),
|
||||||
|
).rejects.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
console.info(`\nℹ️ S3 SDK E2E — ${BASE_URL} bucket: ${BUCKET}`);
|
||||||
Reference in New Issue
Block a user