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:
Claude Bot
2025-08-06 22:07:09 +00:00
parent e3df3785cf
commit 5e697bb3a9
2 changed files with 230 additions and 1 deletions

View File

@@ -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);

View File

@@ -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);
});
});