second first commit
This commit is contained in:
@@ -0,0 +1,277 @@
|
||||
/**
|
||||
* S3 integration tests.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||
import {
|
||||
S3Client,
|
||||
CreateBucketCommand,
|
||||
PutObjectCommand,
|
||||
GetObjectCommand,
|
||||
DeleteObjectCommand,
|
||||
ListObjectsV2Command,
|
||||
DeleteBucketCommand,
|
||||
HeadObjectCommand,
|
||||
HeadBucketCommand,
|
||||
ListBucketsCommand,
|
||||
CopyObjectCommand,
|
||||
GetBucketLocationCommand,
|
||||
CreateMultipartUploadCommand,
|
||||
UploadPartCopyCommand,
|
||||
CompleteMultipartUploadCommand,
|
||||
AbortMultipartUploadCommand,
|
||||
} from '@aws-sdk/client-s3';
|
||||
import { makeClient, uniqueName, CLIENT_CONFIG } from './setup';
|
||||
|
||||
describe('S3', () => {
|
||||
let s3: S3Client;
|
||||
let euS3: S3Client;
|
||||
let bucketName: string;
|
||||
let euBucketName: string;
|
||||
let signedRegionBucket: string;
|
||||
|
||||
beforeAll(() => {
|
||||
s3 = makeClient(S3Client, { forcePathStyle: true });
|
||||
euS3 = new S3Client({ ...CLIENT_CONFIG, region: 'eu-central-1', forcePathStyle: true });
|
||||
bucketName = `test-bucket-${uniqueName()}`;
|
||||
euBucketName = `test-bucket-eu-${uniqueName()}`;
|
||||
signedRegionBucket = `test-bucket-signed-${uniqueName()}`;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// Cleanup buckets
|
||||
for (const bucket of [bucketName, euBucketName, signedRegionBucket]) {
|
||||
try {
|
||||
const list = await s3.send(new ListObjectsV2Command({ Bucket: bucket }));
|
||||
for (const obj of list.Contents || []) {
|
||||
await s3.send(new DeleteObjectCommand({ Bucket: bucket, Key: obj.Key }));
|
||||
}
|
||||
await s3.send(new DeleteBucketCommand({ Bucket: bucket }));
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('should create bucket', async () => {
|
||||
const response = await s3.send(new CreateBucketCommand({ Bucket: bucketName }));
|
||||
expect(response.Location).toBe(`/${bucketName}`);
|
||||
});
|
||||
|
||||
it('should create bucket with LocationConstraint', async () => {
|
||||
const response = await s3.send(
|
||||
new CreateBucketCommand({
|
||||
Bucket: euBucketName,
|
||||
CreateBucketConfiguration: { LocationConstraint: 'eu-central-1' },
|
||||
})
|
||||
);
|
||||
expect(response.Location).toBe(`/${euBucketName}`);
|
||||
});
|
||||
|
||||
it('should get bucket location', async () => {
|
||||
const response = await s3.send(new GetBucketLocationCommand({ Bucket: euBucketName }));
|
||||
expect(response.LocationConstraint).toBe('eu-central-1');
|
||||
});
|
||||
|
||||
it('should create bucket using signing region when body empty', async () => {
|
||||
await euS3.send(new CreateBucketCommand({ Bucket: signedRegionBucket }));
|
||||
const head = await euS3.send(new HeadBucketCommand({ Bucket: signedRegionBucket }));
|
||||
expect(head.BucketRegion).toBe('eu-central-1');
|
||||
const loc = await euS3.send(new GetBucketLocationCommand({ Bucket: signedRegionBucket }));
|
||||
expect(loc.LocationConstraint).toBe('eu-central-1');
|
||||
});
|
||||
|
||||
it('should reject explicit us-east-1 LocationConstraint', async () => {
|
||||
await expect(
|
||||
s3.send(
|
||||
new CreateBucketCommand({
|
||||
Bucket: `invalid-loc-${uniqueName()}`,
|
||||
CreateBucketConfiguration: { LocationConstraint: 'us-east-1' },
|
||||
})
|
||||
)
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('should list buckets', async () => {
|
||||
const response = await s3.send(new ListBucketsCommand({}));
|
||||
expect(response.Buckets?.some((b) => b.Name === bucketName)).toBe(true);
|
||||
});
|
||||
|
||||
it('should put object', async () => {
|
||||
await s3.send(
|
||||
new PutObjectCommand({ Bucket: bucketName, Key: 'test.txt', Body: 'hello from test' })
|
||||
);
|
||||
});
|
||||
|
||||
it('should head object', async () => {
|
||||
const response = await s3.send(
|
||||
new HeadObjectCommand({ Bucket: bucketName, Key: 'test.txt' })
|
||||
);
|
||||
expect(response.ContentLength).toBeGreaterThan(0);
|
||||
expect(response.LastModified).toBeInstanceOf(Date);
|
||||
});
|
||||
|
||||
it('should get object', async () => {
|
||||
const response = await s3.send(
|
||||
new GetObjectCommand({ Bucket: bucketName, Key: 'test.txt' })
|
||||
);
|
||||
const body = await response.Body?.transformToString();
|
||||
expect(body).toBe('hello from test');
|
||||
});
|
||||
|
||||
it('should copy object', async () => {
|
||||
await s3.send(
|
||||
new CopyObjectCommand({
|
||||
CopySource: `${bucketName}/test.txt`,
|
||||
Bucket: bucketName,
|
||||
Key: 'test-copy.txt',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should copy object with non-ASCII key', async () => {
|
||||
const nonAsciiKey = 'src/テスト画像.png';
|
||||
const nonAsciiDst = 'dst/テスト画像.png';
|
||||
|
||||
await s3.send(
|
||||
new PutObjectCommand({
|
||||
Bucket: bucketName,
|
||||
Key: nonAsciiKey,
|
||||
Body: Buffer.from('non-ascii content'),
|
||||
})
|
||||
);
|
||||
await s3.send(
|
||||
new CopyObjectCommand({
|
||||
CopySource: `${bucketName}/${encodeURIComponent(nonAsciiKey)}`,
|
||||
Bucket: bucketName,
|
||||
Key: nonAsciiDst,
|
||||
})
|
||||
);
|
||||
const response = await s3.send(
|
||||
new GetObjectCommand({ Bucket: bucketName, Key: nonAsciiDst })
|
||||
);
|
||||
const body = await response.Body?.transformToString();
|
||||
expect(body).toBe('non-ascii content');
|
||||
|
||||
// Cleanup
|
||||
await s3.send(new DeleteObjectCommand({ Bucket: bucketName, Key: nonAsciiKey }));
|
||||
await s3.send(new DeleteObjectCommand({ Bucket: bucketName, Key: nonAsciiDst }));
|
||||
});
|
||||
|
||||
it('should multipart copy object with non-ASCII key', async () => {
|
||||
const srcKey = 'src/テスト画像.bin';
|
||||
const dstKey = 'dst/テスト画像.bin';
|
||||
const body = Buffer.alloc(6 * 1024 * 1024, 'a');
|
||||
let uploadId: string | undefined;
|
||||
|
||||
try {
|
||||
await s3.send(
|
||||
new PutObjectCommand({
|
||||
Bucket: bucketName,
|
||||
Key: srcKey,
|
||||
Body: body,
|
||||
})
|
||||
);
|
||||
|
||||
const create = await s3.send(
|
||||
new CreateMultipartUploadCommand({
|
||||
Bucket: bucketName,
|
||||
Key: dstKey,
|
||||
})
|
||||
);
|
||||
uploadId = create.UploadId;
|
||||
expect(uploadId).toBeTruthy();
|
||||
|
||||
const part = await s3.send(
|
||||
new UploadPartCopyCommand({
|
||||
Bucket: bucketName,
|
||||
Key: dstKey,
|
||||
UploadId: uploadId,
|
||||
PartNumber: 1,
|
||||
CopySource: `${bucketName}/${encodeURIComponent(srcKey)}`,
|
||||
})
|
||||
);
|
||||
expect(part.CopyPartResult?.ETag).toBeTruthy();
|
||||
|
||||
await s3.send(
|
||||
new CompleteMultipartUploadCommand({
|
||||
Bucket: bucketName,
|
||||
Key: dstKey,
|
||||
UploadId: uploadId,
|
||||
MultipartUpload: {
|
||||
Parts: [
|
||||
{
|
||||
ETag: part.CopyPartResult?.ETag,
|
||||
PartNumber: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
);
|
||||
uploadId = undefined;
|
||||
|
||||
const response = await s3.send(
|
||||
new GetObjectCommand({ Bucket: bucketName, Key: dstKey })
|
||||
);
|
||||
const copied = await response.Body?.transformToByteArray();
|
||||
expect(copied && Buffer.from(copied)).toEqual(body);
|
||||
} finally {
|
||||
if (uploadId) {
|
||||
await s3.send(
|
||||
new AbortMultipartUploadCommand({
|
||||
Bucket: bucketName,
|
||||
Key: dstKey,
|
||||
UploadId: uploadId,
|
||||
})
|
||||
).catch(() => {});
|
||||
}
|
||||
await s3.send(new DeleteObjectCommand({ Bucket: bucketName, Key: srcKey })).catch(() => {});
|
||||
await s3.send(new DeleteObjectCommand({ Bucket: bucketName, Key: dstKey })).catch(() => {});
|
||||
}
|
||||
});
|
||||
|
||||
it('should list objects', async () => {
|
||||
const response = await s3.send(new ListObjectsV2Command({ Bucket: bucketName }));
|
||||
expect(response.KeyCount).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
it('should upload large object (25 MB)', async () => {
|
||||
await s3.send(
|
||||
new PutObjectCommand({
|
||||
Bucket: bucketName,
|
||||
Key: 'large-object-25mb.bin',
|
||||
Body: Buffer.alloc(25 * 1024 * 1024),
|
||||
ContentType: 'application/octet-stream',
|
||||
})
|
||||
);
|
||||
|
||||
const response = await s3.send(
|
||||
new HeadObjectCommand({ Bucket: bucketName, Key: 'large-object-25mb.bin' })
|
||||
);
|
||||
expect(response.ContentLength).toBe(25 * 1024 * 1024);
|
||||
|
||||
await s3.send(
|
||||
new DeleteObjectCommand({ Bucket: bucketName, Key: 'large-object-25mb.bin' })
|
||||
);
|
||||
});
|
||||
|
||||
it('should delete object', async () => {
|
||||
await s3.send(new DeleteObjectCommand({ Bucket: bucketName, Key: 'test.txt' }));
|
||||
await s3.send(new DeleteObjectCommand({ Bucket: bucketName, Key: 'test-copy.txt' }));
|
||||
});
|
||||
|
||||
it('should fail for missing object', async () => {
|
||||
await expect(
|
||||
s3.send(new GetObjectCommand({ Bucket: bucketName, Key: 'missing.txt' }))
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('should delete bucket', async () => {
|
||||
await s3.send(new DeleteBucketCommand({ Bucket: bucketName }));
|
||||
await s3.send(new DeleteBucketCommand({ Bucket: euBucketName }));
|
||||
await euS3.send(new DeleteBucketCommand({ Bucket: signedRegionBucket }));
|
||||
bucketName = '';
|
||||
euBucketName = '';
|
||||
signedRegionBucket = '';
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user