299 lines
8.4 KiB
TypeScript
299 lines
8.4 KiB
TypeScript
// test.listing.node+deno.ts - Tests for memory-efficient listing methods
|
|
|
|
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
import * as smartbucket from '../ts/index.js';
|
|
|
|
// Get test configuration
|
|
import * as qenv from '@push.rocks/qenv';
|
|
const testQenv = new qenv.Qenv('./', './.nogit/');
|
|
|
|
// Test bucket reference
|
|
let testBucket: smartbucket.Bucket;
|
|
let testSmartbucket: smartbucket.SmartBucket;
|
|
|
|
// Setup: Create test bucket and populate with test data
|
|
tap.test('should create valid smartbucket and bucket', async () => {
|
|
testSmartbucket = new smartbucket.SmartBucket({
|
|
accessKey: await testQenv.getEnvVarOnDemand('S3_ACCESSKEY'),
|
|
accessSecret: await testQenv.getEnvVarOnDemand('S3_SECRETKEY'),
|
|
endpoint: await testQenv.getEnvVarOnDemand('S3_ENDPOINT'),
|
|
port: parseInt(await testQenv.getEnvVarOnDemand('S3_PORT')),
|
|
useSsl: false,
|
|
});
|
|
|
|
testBucket = await smartbucket.Bucket.getBucketByName(
|
|
testSmartbucket,
|
|
await testQenv.getEnvVarOnDemand('S3_BUCKET')
|
|
);
|
|
expect(testBucket).toBeInstanceOf(smartbucket.Bucket);
|
|
});
|
|
|
|
tap.test('should clean bucket and create test data for listing tests', async () => {
|
|
// Clean bucket first
|
|
await testBucket.cleanAllContents();
|
|
|
|
// Create test structure:
|
|
// npm/packages/foo/index.json
|
|
// npm/packages/foo/1.0.0.tgz
|
|
// npm/packages/bar/index.json
|
|
// npm/packages/bar/2.0.0.tgz
|
|
// oci/blobs/sha256-abc.tar
|
|
// oci/blobs/sha256-def.tar
|
|
// oci/manifests/latest.json
|
|
// docs/readme.md
|
|
// docs/api.md
|
|
|
|
const testFiles = [
|
|
'npm/packages/foo/index.json',
|
|
'npm/packages/foo/1.0.0.tgz',
|
|
'npm/packages/bar/index.json',
|
|
'npm/packages/bar/2.0.0.tgz',
|
|
'oci/blobs/sha256-abc.tar',
|
|
'oci/blobs/sha256-def.tar',
|
|
'oci/manifests/latest.json',
|
|
'docs/readme.md',
|
|
'docs/api.md',
|
|
];
|
|
|
|
for (const filePath of testFiles) {
|
|
await testBucket.fastPut({
|
|
path: filePath,
|
|
contents: `test content for ${filePath}`,
|
|
});
|
|
}
|
|
});
|
|
|
|
// ==========================
|
|
// Async Generator Tests
|
|
// ==========================
|
|
|
|
tap.test('listAllObjects should iterate all objects with prefix', async () => {
|
|
const keys: string[] = [];
|
|
for await (const key of testBucket.listAllObjects('npm/')) {
|
|
keys.push(key);
|
|
}
|
|
|
|
expect(keys.length).toEqual(4);
|
|
expect(keys).toContain('npm/packages/foo/index.json');
|
|
expect(keys).toContain('npm/packages/bar/2.0.0.tgz');
|
|
});
|
|
|
|
tap.test('listAllObjects should support early termination', async () => {
|
|
let count = 0;
|
|
for await (const key of testBucket.listAllObjects('')) {
|
|
count++;
|
|
if (count >= 3) break; // Early exit
|
|
}
|
|
|
|
expect(count).toEqual(3);
|
|
});
|
|
|
|
tap.test('listAllObjects without prefix should list all objects', async () => {
|
|
const keys: string[] = [];
|
|
for await (const key of testBucket.listAllObjects()) {
|
|
keys.push(key);
|
|
}
|
|
|
|
expect(keys.length).toBeGreaterThanOrEqual(9);
|
|
});
|
|
|
|
// ==========================
|
|
// Observable Tests
|
|
// ==========================
|
|
|
|
tap.test('listAllObjectsObservable should emit all objects', async () => {
|
|
const keys: string[] = [];
|
|
|
|
await new Promise<void>((resolve, reject) => {
|
|
testBucket.listAllObjectsObservable('oci/')
|
|
.subscribe({
|
|
next: (key) => keys.push(key),
|
|
error: (err) => reject(err),
|
|
complete: () => resolve(),
|
|
});
|
|
});
|
|
|
|
expect(keys.length).toEqual(3);
|
|
expect(keys).toContain('oci/blobs/sha256-abc.tar');
|
|
expect(keys).toContain('oci/manifests/latest.json');
|
|
});
|
|
|
|
tap.test('listAllObjectsObservable should support RxJS operators', async () => {
|
|
const jsonFiles: string[] = [];
|
|
|
|
await new Promise<void>((resolve, reject) => {
|
|
testBucket.listAllObjectsObservable('npm/')
|
|
.subscribe({
|
|
next: (key: string) => {
|
|
if (key.endsWith('.json')) {
|
|
jsonFiles.push(key);
|
|
}
|
|
},
|
|
error: (err: any) => reject(err),
|
|
complete: () => resolve(),
|
|
});
|
|
});
|
|
|
|
expect(jsonFiles.length).toEqual(2);
|
|
expect(jsonFiles.every((k) => k.endsWith('.json'))).toBeTrue();
|
|
});
|
|
|
|
// ==========================
|
|
// Cursor Tests
|
|
// ==========================
|
|
|
|
tap.test('createCursor should allow manual pagination', async () => {
|
|
const cursor = testBucket.createCursor('npm/', { pageSize: 2 });
|
|
|
|
// First page
|
|
const page1 = await cursor.next();
|
|
expect(page1.keys.length).toEqual(2);
|
|
expect(page1.done).toBeFalse();
|
|
|
|
// Second page
|
|
const page2 = await cursor.next();
|
|
expect(page2.keys.length).toEqual(2);
|
|
expect(page2.done).toBeTrue();
|
|
});
|
|
|
|
tap.test('cursor.hasMore() should accurately track state', async () => {
|
|
const cursor = testBucket.createCursor('docs/', { pageSize: 10 });
|
|
|
|
expect(cursor.hasMore()).toBeTrue();
|
|
|
|
await cursor.next(); // Should get all docs files
|
|
|
|
expect(cursor.hasMore()).toBeFalse();
|
|
});
|
|
|
|
tap.test('cursor.reset() should allow re-iteration', async () => {
|
|
const cursor = testBucket.createCursor('docs/');
|
|
|
|
const firstRun = await cursor.next();
|
|
expect(firstRun.keys.length).toBeGreaterThan(0);
|
|
|
|
cursor.reset();
|
|
expect(cursor.hasMore()).toBeTrue();
|
|
|
|
const secondRun = await cursor.next();
|
|
expect(secondRun.keys).toEqual(firstRun.keys);
|
|
});
|
|
|
|
tap.test('cursor should support save/restore with token', async () => {
|
|
const cursor1 = testBucket.createCursor('npm/', { pageSize: 2 });
|
|
|
|
await cursor1.next(); // Advance cursor
|
|
const token = cursor1.getToken();
|
|
expect(token).toBeDefined();
|
|
|
|
// Create new cursor and restore state
|
|
const cursor2 = testBucket.createCursor('npm/', { pageSize: 2 });
|
|
cursor2.setToken(token);
|
|
|
|
const page = await cursor2.next();
|
|
expect(page.keys.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
// ==========================
|
|
// findByGlob Tests
|
|
// ==========================
|
|
|
|
tap.test('findByGlob should match simple patterns', async () => {
|
|
const matches: string[] = [];
|
|
for await (const key of testBucket.findByGlob('**/*.json')) {
|
|
matches.push(key);
|
|
}
|
|
|
|
expect(matches.length).toEqual(3); // foo/index.json, bar/index.json, latest.json
|
|
expect(matches.every((k) => k.endsWith('.json'))).toBeTrue();
|
|
});
|
|
|
|
tap.test('findByGlob should match specific path patterns', async () => {
|
|
const matches: string[] = [];
|
|
for await (const key of testBucket.findByGlob('npm/packages/*/index.json')) {
|
|
matches.push(key);
|
|
}
|
|
|
|
expect(matches.length).toEqual(2);
|
|
expect(matches).toContain('npm/packages/foo/index.json');
|
|
expect(matches).toContain('npm/packages/bar/index.json');
|
|
});
|
|
|
|
tap.test('findByGlob should match wildcard patterns', async () => {
|
|
const matches: string[] = [];
|
|
for await (const key of testBucket.findByGlob('oci/blobs/*')) {
|
|
matches.push(key);
|
|
}
|
|
|
|
expect(matches.length).toEqual(2);
|
|
expect(matches.every((k) => k.startsWith('oci/blobs/'))).toBeTrue();
|
|
});
|
|
|
|
// ==========================
|
|
// listAllObjectsArray Tests
|
|
// ==========================
|
|
|
|
tap.test('listAllObjectsArray should collect all keys into array', async () => {
|
|
const keys = await testBucket.listAllObjectsArray('docs/');
|
|
|
|
expect(Array.isArray(keys)).toBeTrue();
|
|
expect(keys.length).toEqual(2);
|
|
expect(keys).toContain('docs/readme.md');
|
|
expect(keys).toContain('docs/api.md');
|
|
});
|
|
|
|
tap.test('listAllObjectsArray without prefix should return all objects', async () => {
|
|
const keys = await testBucket.listAllObjectsArray();
|
|
|
|
expect(keys.length).toBeGreaterThanOrEqual(9);
|
|
});
|
|
|
|
// ==========================
|
|
// Performance/Edge Case Tests
|
|
// ==========================
|
|
|
|
tap.test('should handle empty prefix results gracefully', async () => {
|
|
const keys: string[] = [];
|
|
for await (const key of testBucket.listAllObjects('nonexistent/')) {
|
|
keys.push(key);
|
|
}
|
|
|
|
expect(keys.length).toEqual(0);
|
|
});
|
|
|
|
tap.test('cursor should handle empty results', async () => {
|
|
const cursor = testBucket.createCursor('nonexistent/');
|
|
const result = await cursor.next();
|
|
|
|
expect(result.keys.length).toEqual(0);
|
|
expect(result.done).toBeTrue();
|
|
expect(cursor.hasMore()).toBeFalse();
|
|
});
|
|
|
|
tap.test('observable should complete immediately on empty results', async () => {
|
|
let completed = false;
|
|
let count = 0;
|
|
|
|
await new Promise<void>((resolve, reject) => {
|
|
testBucket.listAllObjectsObservable('nonexistent/')
|
|
.subscribe({
|
|
next: () => count++,
|
|
error: (err) => reject(err),
|
|
complete: () => {
|
|
completed = true;
|
|
resolve();
|
|
},
|
|
});
|
|
});
|
|
|
|
expect(count).toEqual(0);
|
|
expect(completed).toBeTrue();
|
|
});
|
|
|
|
// Cleanup
|
|
tap.test('should clean up test data', async () => {
|
|
await testBucket.cleanAllContents();
|
|
});
|
|
|
|
export default tap.start();
|