feat(test): add end-to-end test coverage for container lifecycle, auth, buckets, objects, policies, credentials, status, and S3 compatibility
This commit is contained in:
215
test/test.objects.test.ts
Normal file
215
test/test.objects.test.ts
Normal file
@@ -0,0 +1,215 @@
|
||||
import { assertEquals, assertExists } from 'jsr:@std/assert';
|
||||
import { afterAll, beforeAll, describe, it } from 'jsr:@std/testing/bdd';
|
||||
import { TypedRequest } from '@api.global/typedrequest';
|
||||
import { createTestContainer, getTestPorts, loginAndGetIdentity } from './helpers/server.helper.ts';
|
||||
import { ObjectStorageContainer } from '../ts/index.ts';
|
||||
import type * as interfaces from '../ts_interfaces/index.ts';
|
||||
import type { IReq_CreateBucket, IReq_DeleteBucket } from '../ts_interfaces/requests/buckets.ts';
|
||||
import type {
|
||||
IReq_ListObjects,
|
||||
IReq_PutObject,
|
||||
IReq_GetObject,
|
||||
IReq_DeleteObject,
|
||||
IReq_DeletePrefix,
|
||||
IReq_GetObjectUrl,
|
||||
IReq_MoveObject,
|
||||
IReq_MovePrefix,
|
||||
} from '../ts_interfaces/requests/objects.ts';
|
||||
|
||||
const PORT_INDEX = 3;
|
||||
const ports = getTestPorts(PORT_INDEX);
|
||||
const url = `http://localhost:${ports.uiPort}/typedrequest`;
|
||||
const BUCKET = 'obj-test-bucket';
|
||||
|
||||
function toBase64(text: string): string {
|
||||
return btoa(text);
|
||||
}
|
||||
|
||||
function fromBase64(b64: string): string {
|
||||
return atob(b64);
|
||||
}
|
||||
|
||||
describe('Object operations', { sanitizeResources: false, sanitizeOps: false }, () => {
|
||||
let container: ObjectStorageContainer;
|
||||
let identity: interfaces.data.IIdentity;
|
||||
|
||||
beforeAll(async () => {
|
||||
container = createTestContainer(PORT_INDEX);
|
||||
await container.start();
|
||||
identity = await loginAndGetIdentity(ports.uiPort);
|
||||
|
||||
// Create test bucket
|
||||
const req = new TypedRequest<IReq_CreateBucket>(url, 'createBucket');
|
||||
await req.fire({ identity, bucketName: BUCKET });
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// Delete test bucket
|
||||
try {
|
||||
const req = new TypedRequest<IReq_DeleteBucket>(url, 'deleteBucket');
|
||||
await req.fire({ identity, bucketName: BUCKET });
|
||||
} catch { /* bucket may already be empty/deleted */ }
|
||||
await container.stop();
|
||||
});
|
||||
|
||||
it('should list objects (initially empty)', async () => {
|
||||
const req = new TypedRequest<IReq_ListObjects>(url, 'listObjects');
|
||||
const response = await req.fire({ identity, bucketName: BUCKET });
|
||||
assertEquals(response.result.objects.length, 0);
|
||||
assertEquals(response.result.commonPrefixes.length, 0);
|
||||
});
|
||||
|
||||
it('should put a text object', async () => {
|
||||
const req = new TypedRequest<IReq_PutObject>(url, 'putObject');
|
||||
const response = await req.fire({
|
||||
identity,
|
||||
bucketName: BUCKET,
|
||||
key: 'hello.txt',
|
||||
base64Content: toBase64('Hello World'),
|
||||
contentType: 'text/plain',
|
||||
});
|
||||
assertEquals(response.ok, true);
|
||||
});
|
||||
|
||||
it('should put nested objects', async () => {
|
||||
const req = new TypedRequest<IReq_PutObject>(url, 'putObject');
|
||||
await req.fire({
|
||||
identity,
|
||||
bucketName: BUCKET,
|
||||
key: 'folder/nested.txt',
|
||||
base64Content: toBase64('Nested content'),
|
||||
contentType: 'text/plain',
|
||||
});
|
||||
await req.fire({
|
||||
identity,
|
||||
bucketName: BUCKET,
|
||||
key: 'folder/a.txt',
|
||||
base64Content: toBase64('File A'),
|
||||
contentType: 'text/plain',
|
||||
});
|
||||
await req.fire({
|
||||
identity,
|
||||
bucketName: BUCKET,
|
||||
key: 'folder/b.txt',
|
||||
base64Content: toBase64('File B'),
|
||||
contentType: 'text/plain',
|
||||
});
|
||||
await req.fire({
|
||||
identity,
|
||||
bucketName: BUCKET,
|
||||
key: 'folder/sub/c.txt',
|
||||
base64Content: toBase64('File C'),
|
||||
contentType: 'text/plain',
|
||||
});
|
||||
});
|
||||
|
||||
it('should list objects at root with delimiter', async () => {
|
||||
const req = new TypedRequest<IReq_ListObjects>(url, 'listObjects');
|
||||
const response = await req.fire({ identity, bucketName: BUCKET, delimiter: '/' });
|
||||
// Root should have hello.txt as direct object
|
||||
const rootKeys = response.result.objects.map((o) => o.key);
|
||||
assertEquals(rootKeys.includes('hello.txt'), true);
|
||||
// folder/ should appear as a common prefix
|
||||
assertEquals(response.result.commonPrefixes.includes('folder/'), true);
|
||||
});
|
||||
|
||||
it('should list objects with prefix', async () => {
|
||||
const req = new TypedRequest<IReq_ListObjects>(url, 'listObjects');
|
||||
const response = await req.fire({
|
||||
identity,
|
||||
bucketName: BUCKET,
|
||||
prefix: 'folder/',
|
||||
delimiter: '/',
|
||||
});
|
||||
const keys = response.result.objects.map((o) => o.key);
|
||||
assertEquals(keys.includes('folder/nested.txt'), true);
|
||||
assertEquals(keys.includes('folder/a.txt'), true);
|
||||
assertEquals(keys.includes('folder/b.txt'), true);
|
||||
// sub/ should be a common prefix
|
||||
assertEquals(response.result.commonPrefixes.includes('folder/sub/'), true);
|
||||
});
|
||||
|
||||
it('should get a text object', async () => {
|
||||
const req = new TypedRequest<IReq_GetObject>(url, 'getObject');
|
||||
const response = await req.fire({ identity, bucketName: BUCKET, key: 'hello.txt' });
|
||||
assertEquals(fromBase64(response.content), 'Hello World');
|
||||
assertEquals(response.size > 0, true);
|
||||
assertExists(response.lastModified);
|
||||
});
|
||||
|
||||
it('should get object URL', async () => {
|
||||
const req = new TypedRequest<IReq_GetObjectUrl>(url, 'getObjectUrl');
|
||||
const response = await req.fire({ identity, bucketName: BUCKET, key: 'hello.txt' });
|
||||
assertExists(response.url);
|
||||
assertEquals(response.url.includes(BUCKET), true);
|
||||
assertEquals(response.url.includes('hello.txt'), true);
|
||||
});
|
||||
|
||||
it('should move an object', async () => {
|
||||
const req = new TypedRequest<IReq_MoveObject>(url, 'moveObject');
|
||||
const response = await req.fire({
|
||||
identity,
|
||||
bucketName: BUCKET,
|
||||
sourceKey: 'hello.txt',
|
||||
destKey: 'moved-hello.txt',
|
||||
});
|
||||
assertEquals(response.success, true);
|
||||
});
|
||||
|
||||
it('should verify moved object exists at new key', async () => {
|
||||
const req = new TypedRequest<IReq_GetObject>(url, 'getObject');
|
||||
const response = await req.fire({ identity, bucketName: BUCKET, key: 'moved-hello.txt' });
|
||||
assertEquals(fromBase64(response.content), 'Hello World');
|
||||
});
|
||||
|
||||
it('should verify source key no longer exists after move', async () => {
|
||||
const req = new TypedRequest<IReq_ListObjects>(url, 'listObjects');
|
||||
const response = await req.fire({ identity, bucketName: BUCKET, delimiter: '/' });
|
||||
const rootKeys = response.result.objects.map((o) => o.key);
|
||||
assertEquals(rootKeys.includes('hello.txt'), false);
|
||||
assertEquals(rootKeys.includes('moved-hello.txt'), true);
|
||||
});
|
||||
|
||||
it('should move a prefix', async () => {
|
||||
const req = new TypedRequest<IReq_MovePrefix>(url, 'movePrefix');
|
||||
const response = await req.fire({
|
||||
identity,
|
||||
bucketName: BUCKET,
|
||||
sourcePrefix: 'folder/',
|
||||
destPrefix: 'renamed/',
|
||||
});
|
||||
assertEquals(response.success, true);
|
||||
assertEquals((response.movedCount ?? 0) >= 4, true);
|
||||
});
|
||||
|
||||
it('should verify moved prefix contents', async () => {
|
||||
const req = new TypedRequest<IReq_ListObjects>(url, 'listObjects');
|
||||
const response = await req.fire({ identity, bucketName: BUCKET, prefix: 'renamed/' });
|
||||
assertEquals(response.result.objects.length >= 1, true);
|
||||
});
|
||||
|
||||
it('should verify old prefix is empty', async () => {
|
||||
const req = new TypedRequest<IReq_ListObjects>(url, 'listObjects');
|
||||
const response = await req.fire({ identity, bucketName: BUCKET, prefix: 'folder/' });
|
||||
assertEquals(response.result.objects.length, 0);
|
||||
assertEquals(response.result.commonPrefixes.length, 0);
|
||||
});
|
||||
|
||||
it('should delete a single object', async () => {
|
||||
const req = new TypedRequest<IReq_DeleteObject>(url, 'deleteObject');
|
||||
const response = await req.fire({ identity, bucketName: BUCKET, key: 'moved-hello.txt' });
|
||||
assertEquals(response.ok, true);
|
||||
});
|
||||
|
||||
it('should delete a prefix', async () => {
|
||||
const req = new TypedRequest<IReq_DeletePrefix>(url, 'deletePrefix');
|
||||
const response = await req.fire({ identity, bucketName: BUCKET, prefix: 'renamed/' });
|
||||
assertEquals(response.ok, true);
|
||||
});
|
||||
|
||||
it('should verify bucket is empty after cleanup', async () => {
|
||||
const req = new TypedRequest<IReq_ListObjects>(url, 'listObjects');
|
||||
const response = await req.fire({ identity, bucketName: BUCKET });
|
||||
assertEquals(response.result.objects.length, 0);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user