@julesl23/s5js
Version:
Enhanced TypeScript SDK for S5 decentralized storage with path-based API, media processing, and directory utilities
294 lines • 14 kB
JavaScript
import { describe, it, expect, beforeEach } from 'vitest';
import { S5 } from '../../src/index.js';
import WebSocket from 'ws';
import { URL as NodeURL } from 'url';
// Polyfill WebSocket for Node.js environment
if (!global.WebSocket) {
global.WebSocket = WebSocket;
}
// These integration tests use a REAL S5 instance with actual storage
// Unlike the unit tests which mock FS5 internals, these tests verify
// that media extensions work with real IndexedDB/memory-level and registry operations
//
// ⚠️ IMPORTANT: Real S5 portal testing is better suited for standalone scripts
// due to registry propagation delays, network timing, and test isolation challenges.
//
// For comprehensive media extension testing with real S5 portals, use:
// node test/integration/test-media-real.js
//
// This standalone script properly handles:
// - Portal registration and authentication
// - Registry propagation delays between operations (5+ seconds)
// - Sequential execution with concurrency: 1 to avoid registry conflicts
// - All 14 tests organized into 4 logical groups:
// • GROUP 1: Setup and Initialization (2 tests)
// • GROUP 2: Basic Image Operations (5 tests)
// • GROUP 3: Gallery Operations with delays (4 tests) - fully sequential
// • GROUP 4: Directory and Cleanup Operations (3 tests)
//
// The vitest tests below are SKIPPED for automated CI and kept for reference.
// Mock browser APIs for media processing (needed in Node.js test environment)
let lastCreatedBlob = null;
global.Image = class Image {
src = '';
onload = null;
onerror = null;
width = 800;
height = 600;
constructor() {
setTimeout(() => {
if (this.src === 'blob:mock-url' && lastCreatedBlob) {
if (lastCreatedBlob.size < 10) {
if (this.onerror)
this.onerror();
return;
}
}
if (this.onload)
this.onload();
}, 0);
}
};
// Preserve native URL constructor while adding blob URL methods for media processing
global.URL = Object.assign(NodeURL, {
createObjectURL: (blob) => {
lastCreatedBlob = blob;
return 'blob:mock-url';
},
revokeObjectURL: (url) => {
lastCreatedBlob = null;
},
});
global.document = {
createElement: (tag) => {
if (tag === 'canvas') {
const canvas = {
_width: 0,
_height: 0,
get width() { return this._width; },
set width(val) { this._width = val; },
get height() { return this._height; },
set height(val) { this._height = val; },
getContext: () => ({
imageSmoothingEnabled: true,
imageSmoothingQuality: 'high',
fillStyle: '',
drawImage: () => { },
fillRect: () => { },
getImageData: (x, y, w, h) => ({
width: w,
height: h,
data: new Uint8ClampedArray(w * h * 4),
}),
}),
toBlob: (callback, type, quality) => {
const baseSize = Math.max(canvas._width * canvas._height, 100);
const qualityFactor = quality !== undefined ? quality : 0.92;
const size = Math.floor(baseSize * qualityFactor * 0.5) + 50;
const mockBlob = new Blob([new Uint8Array(size)], { type });
setTimeout(() => callback(mockBlob), 0);
},
};
return canvas;
}
return {};
},
};
describe.skip('FS5 Media Extensions - Integration', () => {
let s5;
// Helper to create test image blob
const createTestImageBlob = () => {
// Create a simple valid JPEG with actual image data
const jpegData = new Uint8Array([
0xFF, 0xD8, 0xFF, 0xE0, // JPEG SOI and APP0
0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00,
0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00,
0xFF, 0xD9 // EOI
]);
return new Blob([jpegData], { type: 'image/jpeg' });
};
beforeEach(async () => {
// Create a real S5 instance with actual storage
s5 = await S5.create({
initialPeers: ["wss://z2DWuPbL5pweybXnEB618pMnV58ECj2VPDNfVGm3tFqBvjF@s5.ninja/s5/p2p"]
});
// Create an identity for file operations
const seedPhrase = s5.generateSeedPhrase();
await s5.recoverIdentityFromSeedPhrase(seedPhrase);
// Register on portal to enable uploads (required for real S5 portal testing)
await s5.registerOnNewPortal("https://s5.vup.cx");
// Ensure identity is initialized for file operations
await s5.fs.ensureIdentityInitialized();
// Wait for registry propagation to avoid "Revision number too low" errors
await new Promise(resolve => setTimeout(resolve, 3000));
}, 40000); // 40 second timeout for S5 initialization + registry propagation
describe('Real putImage Operations', () => {
it('should upload image to real storage and retrieve it', async () => {
const blob = createTestImageBlob();
// Upload with real storage
const result = await s5.fs.putImage('home/photos/test.jpg', blob);
expect(result.path).toBe('home/photos/test.jpg');
expect(result.metadata).toBeDefined();
// Verify it's actually stored by retrieving it
const retrieved = await s5.fs.get('home/photos/test.jpg');
expect(retrieved).toBeDefined();
expect(retrieved).toBeInstanceOf(Uint8Array);
});
it('should generate and store thumbnail in real storage', async () => {
const blob = createTestImageBlob();
const result = await s5.fs.putImage('home/photos/with-thumb.jpg', blob);
expect(result.thumbnailPath).toBe('home/photos/.thumbnails/with-thumb.jpg');
// Verify thumbnail is actually stored
const thumbnail = await s5.fs.get('home/photos/.thumbnails/with-thumb.jpg');
expect(thumbnail).toBeDefined();
});
it('should extract real metadata from image', async () => {
const blob = createTestImageBlob();
const result = await s5.fs.putImage('home/photos/metadata-test.jpg', blob);
expect(result.metadata).toBeDefined();
expect(result.metadata?.format).toBe('jpeg');
expect(result.metadata?.width).toBeGreaterThan(0);
expect(result.metadata?.height).toBeGreaterThan(0);
});
});
describe('Real getThumbnail Operations', () => {
it('should retrieve pre-generated thumbnail from storage', async () => {
const blob = createTestImageBlob();
// Upload with thumbnail
await s5.fs.putImage('home/photos/thumb-test.jpg', blob);
// Get the thumbnail
const thumbnail = await s5.fs.getThumbnail('home/photos/thumb-test.jpg');
expect(thumbnail).toBeInstanceOf(Blob);
expect(thumbnail.type).toContain('image');
});
it('should generate thumbnail on-demand when missing', async () => {
const blob = createTestImageBlob();
// Upload without thumbnail
await s5.fs.putImage('home/photos/no-thumb.jpg', blob, {
generateThumbnail: false
});
// Request thumbnail (should generate on-demand)
const thumbnail = await s5.fs.getThumbnail('home/photos/no-thumb.jpg');
expect(thumbnail).toBeInstanceOf(Blob);
}, 20000); // 20 second timeout for on-demand generation
it('should cache generated thumbnail in storage', async () => {
const blob = createTestImageBlob();
// Upload without thumbnail
await s5.fs.putImage('home/photos/cache-test.jpg', blob, {
generateThumbnail: false
});
// Generate thumbnail (should cache it)
await s5.fs.getThumbnail('home/photos/cache-test.jpg', { cache: true });
// Verify it's now cached in storage
const cached = await s5.fs.get('home/photos/.thumbnails/cache-test.jpg');
expect(cached).toBeDefined();
});
});
describe('Real getImageMetadata Operations', () => {
it('should extract metadata from stored image', async () => {
const blob = createTestImageBlob();
await s5.fs.putImage('home/photos/metadata.jpg', blob);
const metadata = await s5.fs.getImageMetadata('home/photos/metadata.jpg');
expect(metadata.format).toBe('jpeg');
expect(metadata.width).toBeGreaterThan(0);
expect(metadata.height).toBeGreaterThan(0);
}, 15000); // 15 second timeout for metadata extraction
});
describe('Real createImageGallery Operations', () => {
it('should upload multiple images to real storage', async () => {
const images = [
{ name: 'photo1.jpg', blob: createTestImageBlob() },
{ name: 'photo2.jpg', blob: createTestImageBlob() }
];
const results = await s5.fs.createImageGallery('home/gallery', images);
expect(results).toHaveLength(2);
// Verify images are actually stored
const img1 = await s5.fs.get('home/gallery/photo1.jpg');
const img2 = await s5.fs.get('home/gallery/photo2.jpg');
expect(img1).toBeDefined();
expect(img2).toBeDefined();
}, 30000); // 30 second timeout for gallery creation
it('should create manifest.json in real storage', async () => {
const images = [
{ name: 'photo1.jpg', blob: createTestImageBlob() },
{ name: 'photo2.jpg', blob: createTestImageBlob() }
];
await s5.fs.createImageGallery('home/gallery2', images);
// Retrieve and parse manifest
const manifestData = await s5.fs.get('home/gallery2/manifest.json');
expect(manifestData).toBeDefined();
const manifest = typeof manifestData === 'object' && manifestData !== null
? manifestData
: JSON.parse(typeof manifestData === 'string'
? manifestData
: new TextDecoder().decode(manifestData));
expect(manifest.count).toBe(2);
expect(manifest.images).toHaveLength(2);
expect(manifest.images[0].path).toBe('home/gallery2/photo1.jpg');
}, 30000); // 30 second timeout for gallery creation
it('should handle concurrent uploads with real storage', async () => {
const images = Array.from({ length: 5 }, (_, i) => ({
name: `photo${i}.jpg`,
blob: createTestImageBlob()
}));
const results = await s5.fs.createImageGallery('home/concurrent', images, {
concurrency: 2
});
expect(results).toHaveLength(5);
// Verify all images are stored
for (let i = 0; i < 5; i++) {
const img = await s5.fs.get(`home/concurrent/photo${i}.jpg`);
expect(img).toBeDefined();
}
}, 40000); // 40 second timeout for concurrent uploads
});
describe('Real Directory Operations Integration', () => {
it('should work with FS5 list() for real directory structure', async () => {
const blob = createTestImageBlob();
await s5.fs.putImage('home/photos/list-test.jpg', blob);
// List directory contents
const entries = [];
for await (const entry of s5.fs.list('home/photos')) {
entries.push(entry);
}
expect(entries.some(e => e.name === 'list-test.jpg')).toBe(true);
});
it('should support delete() operations on real storage', async () => {
const blob = createTestImageBlob();
await s5.fs.putImage('home/photos/delete-test.jpg', blob);
// Verify it exists
let data = await s5.fs.get('home/photos/delete-test.jpg');
expect(data).toBeDefined();
// Delete it
const deleted = await s5.fs.delete('home/photos/delete-test.jpg');
expect(deleted).toBe(true);
// Verify it's gone
data = await s5.fs.get('home/photos/delete-test.jpg');
expect(data).toBeUndefined();
}, 20000); // 20 second timeout for delete operations
it('should maintain thumbnails directory structure in real storage', async () => {
const blob = createTestImageBlob();
await s5.fs.putImage('home/photos/structure-test.jpg', blob);
// List thumbnails directory
const entries = [];
for await (const entry of s5.fs.list('home/photos/.thumbnails')) {
entries.push(entry);
}
expect(entries.some(e => e.name === 'structure-test.jpg')).toBe(true);
});
});
describe('Real Storage Persistence', () => {
it('should persist data across operations', async () => {
const blob = createTestImageBlob();
// Upload image
await s5.fs.putImage('home/photos/persist-test.jpg', blob);
// Retrieve multiple times to verify persistence
const data1 = await s5.fs.get('home/photos/persist-test.jpg');
const data2 = await s5.fs.get('home/photos/persist-test.jpg');
expect(data1).toBeDefined();
expect(data2).toBeDefined();
expect(data1).toEqual(data2);
}, 20000); // 20 second timeout for persistence test
});
});
//# sourceMappingURL=media-extensions.integration.test.js.map