Files
openmaic/OpenMAIC/packages/@openmaic/storage/test/s3-asset-bytes.s3.test.ts
2026-08-16 14:58:47 +08:00

84 lines
3.1 KiB
TypeScript

import { afterAll, beforeAll, describe, expect, test } from 'vitest';
import {
DeleteObjectCommand,
GetObjectCommand,
HeadBucketCommand,
PutObjectCommand,
S3Client,
} from '@aws-sdk/client-s3';
import { contentHashOf, type ContentHash } from '../src/asset/blob.js';
import { S3AssetByteStore } from '../src/asset/s3-bytes.js';
const endpoint = process.env.S3_CONTRACT_ENDPOINT;
const bucket = process.env.S3_CONTRACT_BUCKET;
const configured = Boolean(endpoint && bucket);
if (process.env.STORAGE_S3_CONTRACT_REQUIRED === '1' && !configured) {
throw new Error(
'@openmaic/storage: STORAGE_S3_CONTRACT_REQUIRED=1 requires S3_CONTRACT_ENDPOINT and S3_CONTRACT_BUCKET; refusing to skip the S3 asset suite',
);
}
describe.skipIf(!configured)('S3AssetByteStore with an S3-compatible server', () => {
let client: S3Client;
let store: S3AssetByteStore;
const written = new Set<ContentHash>();
beforeAll(async () => {
client = new S3Client({
endpoint,
forcePathStyle: true,
region: process.env.S3_CONTRACT_REGION ?? 'us-east-1',
credentials:
process.env.S3_CONTRACT_ACCESS_KEY && process.env.S3_CONTRACT_SECRET_KEY
? {
accessKeyId: process.env.S3_CONTRACT_ACCESS_KEY,
secretAccessKey: process.env.S3_CONTRACT_SECRET_KEY,
}
: undefined,
});
await client.send(new HeadBucketCommand({ Bucket: bucket! }));
store = new S3AssetByteStore({
client: client as never,
commands: {
put: (input) => new PutObjectCommand(input),
get: (input) => new GetObjectCommand(input),
delete: (input) => new DeleteObjectCommand(input),
},
bucket: bucket!,
});
});
afterAll(async () => {
await Promise.all([...written].map((hash) => store.delete(hash)));
client.destroy();
});
test('round-trips and deletes an object against real S3 semantics', async () => {
const data = new Blob([`s3-real-${crypto.randomUUID()}`]);
const { contentHash, bytes } = await contentHashOf(data);
written.add(contentHash);
await store.write(contentHash, new Uint8Array(bytes));
expect(await store.read(contentHash)).toEqual(new Uint8Array(bytes));
await store.delete(contentHash);
written.delete(contentHash);
expect(await store.read(contentHash)).toBeNull();
});
test('plain PUT is idempotent for an existing hash-named key', async () => {
const data = new Blob([`s3-idempotent-${crypto.randomUUID()}`]);
const { contentHash, bytes } = await contentHashOf(data);
written.add(contentHash);
await store.write(contentHash, new Uint8Array(bytes));
await expect(store.write(contentHash, new Uint8Array(bytes))).resolves.toBeUndefined();
expect(await store.read(contentHash)).toEqual(new Uint8Array(bytes));
});
test('missing reads and repeated deletes are idempotent', async () => {
const { contentHash } = await contentHashOf(new Blob([`s3-missing-${crypto.randomUUID()}`]));
expect(await store.read(contentHash)).toBeNull();
await expect(store.delete(contentHash)).resolves.toBeUndefined();
await expect(store.delete(contentHash)).resolves.toBeUndefined();
});
});