mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 10:28:47 +00:00
Add comprehensive integration tests for S3 contentLength restrictions
- Add tests that verify actual HTTP behavior with different payload sizes - Test exact contentLength (should succeed), less than (should fail), more than (should fail) - Cover both Bun.s3() and S3Client APIs - Test both contentLength and ContentLength parameter styles - Add validation tests for negative values - Include the exact use case from GitHub issue #18240 These tests ensure the contentLength restriction actually works at the HTTP level, not just URL generation, providing full coverage of the feature behavior. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -455,6 +455,83 @@ for (let credentials of allCredentials) {
|
||||
}
|
||||
});
|
||||
|
||||
it("should enforce contentLength restrictions on S3Client presigned URLs", async () => {
|
||||
const testContent = "Test data for S3Client"; // 23 bytes
|
||||
const contentLength = testContent.length;
|
||||
const uploadFilename = bucketInName ? `${S3Bucket}/${randomUUID()}-s3client` : `${randomUUID()}-s3client`;
|
||||
|
||||
// Test 1: Upload exactly contentLength bytes - should succeed
|
||||
{
|
||||
const presignedUrl = bucket.presign(uploadFilename, {
|
||||
method: "PUT",
|
||||
expiresIn: 3600,
|
||||
contentLength: contentLength,
|
||||
});
|
||||
|
||||
expect(presignedUrl.includes(`Content-Length=${contentLength}`)).toBe(true);
|
||||
|
||||
const response = await fetch(presignedUrl, {
|
||||
method: "PUT",
|
||||
body: testContent, // Exactly 23 bytes
|
||||
headers: {
|
||||
"Content-Type": "text/plain",
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
|
||||
// Verify the content was uploaded correctly
|
||||
const file = bucket.file(uploadFilename, options);
|
||||
const downloaded = await file.text();
|
||||
expect(downloaded).toBe(testContent);
|
||||
|
||||
// Clean up
|
||||
await file.unlink();
|
||||
}
|
||||
|
||||
// Test 2: Upload less than contentLength - should fail
|
||||
{
|
||||
const presignedUrl = bucket.presign(uploadFilename + "-less", {
|
||||
method: "PUT",
|
||||
expiresIn: 3600,
|
||||
contentLength: contentLength,
|
||||
});
|
||||
|
||||
const shortContent = "Short"; // Only 5 bytes, less than 23
|
||||
const response = await fetch(presignedUrl, {
|
||||
method: "PUT",
|
||||
body: shortContent,
|
||||
headers: {
|
||||
"Content-Type": "text/plain",
|
||||
},
|
||||
});
|
||||
|
||||
// Should fail with 400 Bad Request due to content length mismatch
|
||||
expect([400, 403]).toContain(response.status);
|
||||
}
|
||||
|
||||
// Test 3: Upload more than contentLength - should fail
|
||||
{
|
||||
const presignedUrl = bucket.presign(uploadFilename + "-more", {
|
||||
method: "PUT",
|
||||
expiresIn: 3600,
|
||||
contentLength: contentLength,
|
||||
});
|
||||
|
||||
const longContent = "This content is definitely much longer than the expected 23 bytes and should cause a failure";
|
||||
const response = await fetch(presignedUrl, {
|
||||
method: "PUT",
|
||||
body: longContent,
|
||||
headers: {
|
||||
"Content-Type": "text/plain",
|
||||
},
|
||||
});
|
||||
|
||||
// Should fail with 400 Bad Request due to content length mismatch
|
||||
expect([400, 403]).toContain(response.status);
|
||||
}
|
||||
});
|
||||
|
||||
it("should be able to upload large files using bucket.write + readable Request", async () => {
|
||||
{
|
||||
await bucket.write(
|
||||
@@ -726,6 +803,117 @@ for (let credentials of allCredentials) {
|
||||
}
|
||||
});
|
||||
|
||||
it("should enforce contentLength restrictions on PUT presigned URLs", async () => {
|
||||
const testContent = "Hello, Bun!"; // 12 bytes
|
||||
const contentLength = testContent.length;
|
||||
const uploadFilename = tmp_filename + "-contentlength-test";
|
||||
|
||||
// Test 1: Upload exactly contentLength bytes - should succeed
|
||||
{
|
||||
const s3file = s3(uploadFilename, options);
|
||||
const presignedUrl = s3file.presign({
|
||||
method: "PUT",
|
||||
expiresIn: 3600,
|
||||
contentLength: contentLength,
|
||||
});
|
||||
|
||||
expect(presignedUrl.includes(`Content-Length=${contentLength}`)).toBe(true);
|
||||
|
||||
const response = await fetch(presignedUrl, {
|
||||
method: "PUT",
|
||||
body: testContent, // Exactly 12 bytes
|
||||
headers: {
|
||||
"Content-Type": "text/plain",
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
|
||||
// Verify the content was uploaded correctly
|
||||
const downloaded = await s3file.text();
|
||||
expect(downloaded).toBe(testContent);
|
||||
|
||||
// Clean up
|
||||
await s3file.unlink();
|
||||
}
|
||||
|
||||
// Test 2: Upload less than contentLength - should fail
|
||||
{
|
||||
const s3file = s3(uploadFilename + "-less", options);
|
||||
const presignedUrl = s3file.presign({
|
||||
method: "PUT",
|
||||
expiresIn: 3600,
|
||||
contentLength: contentLength,
|
||||
});
|
||||
|
||||
const shortContent = "Short"; // Only 5 bytes, less than 12
|
||||
const response = await fetch(presignedUrl, {
|
||||
method: "PUT",
|
||||
body: shortContent,
|
||||
headers: {
|
||||
"Content-Type": "text/plain",
|
||||
},
|
||||
});
|
||||
|
||||
// Should fail with 400 Bad Request due to content length mismatch
|
||||
expect([400, 403]).toContain(response.status);
|
||||
}
|
||||
|
||||
// Test 3: Upload more than contentLength - should fail
|
||||
{
|
||||
const s3file = s3(uploadFilename + "-more", options);
|
||||
const presignedUrl = s3file.presign({
|
||||
method: "PUT",
|
||||
expiresIn: 3600,
|
||||
contentLength: contentLength,
|
||||
});
|
||||
|
||||
const longContent = "This is a much longer content than expected"; // Much more than 12 bytes
|
||||
const response = await fetch(presignedUrl, {
|
||||
method: "PUT",
|
||||
body: longContent,
|
||||
headers: {
|
||||
"Content-Type": "text/plain",
|
||||
},
|
||||
});
|
||||
|
||||
// Should fail with 400 Bad Request due to content length mismatch
|
||||
expect([400, 403]).toContain(response.status);
|
||||
}
|
||||
});
|
||||
|
||||
it("should work with ContentLength (AWS SDK style) restrictions", async () => {
|
||||
const testData = Buffer.alloc(100, 'x'); // Exactly 100 bytes
|
||||
const uploadFilename = tmp_filename + "-aws-style";
|
||||
|
||||
// Test with AWS SDK style "ContentLength"
|
||||
const s3file = s3(uploadFilename, options);
|
||||
const presignedUrl = s3file.presign({
|
||||
method: "PUT",
|
||||
expiresIn: 3600,
|
||||
ContentLength: 100, // AWS SDK style (PascalCase)
|
||||
});
|
||||
|
||||
expect(presignedUrl.includes("Content-Length=100")).toBe(true);
|
||||
|
||||
const response = await fetch(presignedUrl, {
|
||||
method: "PUT",
|
||||
body: testData, // Exactly 100 bytes
|
||||
headers: {
|
||||
"Content-Type": "application/octet-stream",
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
|
||||
// Verify upload
|
||||
const stat = await s3file.stat();
|
||||
expect(stat.size).toBe(100);
|
||||
|
||||
// Clean up
|
||||
await s3file.unlink();
|
||||
});
|
||||
|
||||
it("should be able to upload large files in one go using Bun.write", async () => {
|
||||
{
|
||||
const s3file = s3(tmp_filename, options);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { S3Client } from "bun";
|
||||
import { describe, expect, it } from "bun:test";
|
||||
|
||||
describe("S3 contentLength option in presign", () => {
|
||||
describe("S3 contentLength option in presign (Issue #18240)", () => {
|
||||
const s3Client = new S3Client({
|
||||
accessKeyId: "test-key",
|
||||
secretAccessKey: "test-secret",
|
||||
@@ -46,4 +46,45 @@ describe("S3 contentLength option in presign", () => {
|
||||
expect(url.includes("Content-Length=")).toBe(false);
|
||||
expect(url.includes("X-Amz-Expires=3600")).toBe(true);
|
||||
});
|
||||
|
||||
it("should validate contentLength is positive", () => {
|
||||
expect(() => {
|
||||
s3Client.presign("test/abc", {
|
||||
expiresIn: 3600,
|
||||
method: "PUT",
|
||||
contentLength: -1, // Invalid negative value
|
||||
});
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it("should validate ContentLength is positive", () => {
|
||||
expect(() => {
|
||||
s3Client.presign("test/abc", {
|
||||
expiresIn: 3600,
|
||||
method: "PUT",
|
||||
ContentLength: -100, // Invalid negative value
|
||||
});
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it("should match the exact use case from issue #18240", () => {
|
||||
// This is the exact code snippet from the GitHub issue
|
||||
const url = s3Client.presign('test/abc', {
|
||||
expiresIn: 3600, // 1 hour
|
||||
method: 'PUT',
|
||||
ContentLength: 200 // THIS IS NOW WORKING
|
||||
});
|
||||
|
||||
expect(url).toBeDefined();
|
||||
expect(typeof url).toBe("string");
|
||||
expect(url.includes("Content-Length=200")).toBe(true);
|
||||
|
||||
// Verify other required AWS S3 signature components are present
|
||||
expect(url.includes("X-Amz-Expires=3600")).toBe(true);
|
||||
expect(url.includes("X-Amz-Algorithm=AWS4-HMAC-SHA256")).toBe(true);
|
||||
expect(url.includes("X-Amz-Credential")).toBe(true);
|
||||
expect(url.includes("X-Amz-Date")).toBe(true);
|
||||
expect(url.includes("X-Amz-SignedHeaders")).toBe(true);
|
||||
expect(url.includes("X-Amz-Signature")).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user