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(url, 'createBucket'); await req.fire({ identity, bucketName: BUCKET }); }); afterAll(async () => { // Delete test bucket try { const req = new TypedRequest(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(url, 'listObjects'); const response = await req.fire({ identity, bucketName: BUCKET }); assertEquals(response.result.objects.length, 0); }); });