|
| 1 | +import { describe, it, expect, beforeEach } from 'vitest'; |
| 2 | +import { S5 } from '../../src/index.js'; |
| 3 | +import { FS5Advanced } from '../../src/fs/fs5-advanced.js'; |
| 4 | +import { formatCID, parseCID } from '../../src/fs/cid-utils.js'; |
| 5 | +import WebSocket from 'ws'; |
| 6 | + |
| 7 | +// Polyfill WebSocket for Node.js environment |
| 8 | +if (!global.WebSocket) { |
| 9 | + global.WebSocket = WebSocket as any; |
| 10 | +} |
| 11 | + |
| 12 | +// These integration tests use a REAL S5 instance with actual storage |
| 13 | +// Unlike the unit tests which mock FS5 internals, these tests verify |
| 14 | +// that the Advanced CID API works with real IndexedDB/memory-level and registry operations |
| 15 | +// |
| 16 | +// ⚠️ IMPORTANT: Real S5 portal testing is better suited for standalone scripts |
| 17 | +// due to registry propagation delays, network timing, and test isolation challenges. |
| 18 | +// |
| 19 | +// For comprehensive Advanced CID API testing with real S5 portals, use: |
| 20 | +// node test/integration/test-advanced-cid-real.js |
| 21 | +// |
| 22 | +// This standalone script properly handles: |
| 23 | +// - Portal registration and authentication |
| 24 | +// - Registry propagation delays between operations (5+ seconds) |
| 25 | +// - Sequential execution with concurrency: 1 to avoid registry conflicts |
| 26 | +// - All integration scenarios: |
| 27 | +// • putWithCID and dual retrieval (path + CID) |
| 28 | +// • pathToCID extraction from stored files |
| 29 | +// • cidToPath lookup and verification |
| 30 | +// • getByCID without path knowledge |
| 31 | +// • CID consistency and verification |
| 32 | +// • Integration with encryption |
| 33 | +// |
| 34 | +// The vitest tests below are SKIPPED for automated CI and kept for reference. |
| 35 | + |
| 36 | +describe.skip('FS5Advanced Integration Tests', () => { |
| 37 | + let s5: S5; |
| 38 | + let advanced: FS5Advanced; |
| 39 | + let testPath: string; |
| 40 | + |
| 41 | + beforeEach(async () => { |
| 42 | + // Create S5 instance with in-memory storage |
| 43 | + s5 = await S5.create({}); |
| 44 | + |
| 45 | + // Generate and recover identity |
| 46 | + const seedPhrase = s5.generateSeedPhrase(); |
| 47 | + await s5.recoverIdentityFromSeedPhrase(seedPhrase); |
| 48 | + await s5.fs.ensureIdentityInitialized(); |
| 49 | + |
| 50 | + // Create Advanced API instance |
| 51 | + advanced = new FS5Advanced(s5.fs); |
| 52 | + |
| 53 | + // Use unique path for each test |
| 54 | + testPath = `home/test-${Date.now()}.txt`; |
| 55 | + }); |
| 56 | + |
| 57 | + describe('putWithCID Integration', () => { |
| 58 | + it('should store data and return both path and CID', async () => { |
| 59 | + const testData = 'Integration test data'; |
| 60 | + |
| 61 | + const result = await advanced.putWithCID(testPath, testData); |
| 62 | + |
| 63 | + expect(result.path).toBe(testPath); |
| 64 | + expect(result.cid).toBeInstanceOf(Uint8Array); |
| 65 | + expect(result.cid.length).toBe(32); |
| 66 | + |
| 67 | + // Verify we can retrieve by path |
| 68 | + const byPath = await s5.fs.get(testPath); |
| 69 | + expect(byPath).toBe(testData); |
| 70 | + |
| 71 | + // Verify we can retrieve by CID |
| 72 | + const byCID = await advanced.getByCID(result.cid); |
| 73 | + expect(byCID).toBe(testData); |
| 74 | + }); |
| 75 | + |
| 76 | + it('should work with binary data', async () => { |
| 77 | + const binaryData = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]); |
| 78 | + |
| 79 | + const result = await advanced.putWithCID(testPath, binaryData); |
| 80 | + |
| 81 | + const retrieved = await advanced.getByCID(result.cid); |
| 82 | + expect(retrieved).toEqual(binaryData); |
| 83 | + }); |
| 84 | + |
| 85 | + it('should work with JSON data', async () => { |
| 86 | + const jsonData = { key: 'value', nested: { data: 123 } }; |
| 87 | + |
| 88 | + const result = await advanced.putWithCID(testPath, jsonData); |
| 89 | + |
| 90 | + const retrieved = await advanced.getByCID(result.cid); |
| 91 | + expect(retrieved).toEqual(jsonData); |
| 92 | + }); |
| 93 | + }); |
| 94 | + |
| 95 | + describe('pathToCID Integration', () => { |
| 96 | + it('should extract CID from stored file', async () => { |
| 97 | + const testData = 'Extract CID test'; |
| 98 | + await s5.fs.put(testPath, testData); |
| 99 | + |
| 100 | + const cid = await advanced.pathToCID(testPath); |
| 101 | + |
| 102 | + expect(cid).toBeInstanceOf(Uint8Array); |
| 103 | + expect(cid.length).toBe(32); |
| 104 | + |
| 105 | + // Verify CID works for retrieval |
| 106 | + const retrieved = await advanced.getByCID(cid); |
| 107 | + expect(retrieved).toBe(testData); |
| 108 | + }); |
| 109 | + |
| 110 | + it('should extract CID from directory', async () => { |
| 111 | + const dirPath = 'home/testdir'; |
| 112 | + await s5.fs.put(`${dirPath}/file.txt`, 'content'); |
| 113 | + |
| 114 | + const cid = await advanced.pathToCID(dirPath); |
| 115 | + |
| 116 | + expect(cid).toBeInstanceOf(Uint8Array); |
| 117 | + expect(cid.length).toBe(32); |
| 118 | + }); |
| 119 | + |
| 120 | + it('should return consistent CID for same content', async () => { |
| 121 | + const content = 'Consistent content'; |
| 122 | + const path1 = 'home/file1.txt'; |
| 123 | + const path2 = 'home/file2.txt'; |
| 124 | + |
| 125 | + await s5.fs.put(path1, content); |
| 126 | + await s5.fs.put(path2, content); |
| 127 | + |
| 128 | + const cid1 = await advanced.pathToCID(path1); |
| 129 | + const cid2 = await advanced.pathToCID(path2); |
| 130 | + |
| 131 | + // Same content should have same CID |
| 132 | + expect(cid1).toEqual(cid2); |
| 133 | + }); |
| 134 | + }); |
| 135 | + |
| 136 | + describe('cidToPath Integration', () => { |
| 137 | + it('should find path from CID', async () => { |
| 138 | + const testData = 'Find path test'; |
| 139 | + await s5.fs.put(testPath, testData); |
| 140 | + |
| 141 | + const cid = await advanced.pathToCID(testPath); |
| 142 | + const foundPath = await advanced.cidToPath(cid); |
| 143 | + |
| 144 | + expect(foundPath).toBe(testPath); |
| 145 | + }); |
| 146 | + |
| 147 | + it('should return null for unknown CID', async () => { |
| 148 | + const unknownCID = new Uint8Array(32).fill(99); |
| 149 | + |
| 150 | + const foundPath = await advanced.cidToPath(unknownCID); |
| 151 | + |
| 152 | + expect(foundPath).toBeNull(); |
| 153 | + }); |
| 154 | + |
| 155 | + it('should prefer user paths over .cid paths', async () => { |
| 156 | + const testData = 'Preference test'; |
| 157 | + const userPath = 'home/userfile.txt'; |
| 158 | + |
| 159 | + // Store at user path |
| 160 | + const result = await advanced.putWithCID(userPath, testData); |
| 161 | + |
| 162 | + // Also store via putByCID (creates .cid/ path) |
| 163 | + await advanced.putByCID(testData); |
| 164 | + |
| 165 | + // cidToPath should return user path, not .cid/ path |
| 166 | + const foundPath = await advanced.cidToPath(result.cid); |
| 167 | + |
| 168 | + expect(foundPath).toBe(userPath); |
| 169 | + expect(foundPath).not.toContain('.cid/'); |
| 170 | + }); |
| 171 | + }); |
| 172 | + |
| 173 | + describe('getByCID Integration', () => { |
| 174 | + it('should retrieve data without knowing path', async () => { |
| 175 | + const testData = 'Retrieve by CID test'; |
| 176 | + const result = await advanced.putWithCID(testPath, testData); |
| 177 | + |
| 178 | + // Retrieve without using path |
| 179 | + const retrieved = await advanced.getByCID(result.cid); |
| 180 | + |
| 181 | + expect(retrieved).toBe(testData); |
| 182 | + }); |
| 183 | + |
| 184 | + it('should throw error for non-existent CID', async () => { |
| 185 | + const nonExistentCID = new Uint8Array(32).fill(255); |
| 186 | + |
| 187 | + await expect(advanced.getByCID(nonExistentCID)).rejects.toThrow('CID not found'); |
| 188 | + }); |
| 189 | + }); |
| 190 | + |
| 191 | + describe('getMetadataWithCID Integration', () => { |
| 192 | + it('should return metadata and CID for file', async () => { |
| 193 | + const testData = 'Metadata test'; |
| 194 | + await s5.fs.put(testPath, testData); |
| 195 | + |
| 196 | + const result = await advanced.getMetadataWithCID(testPath); |
| 197 | + |
| 198 | + expect(result.metadata).toBeDefined(); |
| 199 | + expect(result.metadata.type).toBe('file'); |
| 200 | + expect(result.metadata.size).toBeGreaterThan(0); |
| 201 | + expect(result.cid).toBeInstanceOf(Uint8Array); |
| 202 | + expect(result.cid.length).toBe(32); |
| 203 | + }); |
| 204 | + |
| 205 | + it('should return metadata and CID for directory', async () => { |
| 206 | + const dirPath = 'home/metadir'; |
| 207 | + await s5.fs.put(`${dirPath}/file.txt`, 'content'); |
| 208 | + |
| 209 | + const result = await advanced.getMetadataWithCID(dirPath); |
| 210 | + |
| 211 | + expect(result.metadata).toBeDefined(); |
| 212 | + expect(result.metadata.type).toBe('directory'); |
| 213 | + expect(result.cid).toBeInstanceOf(Uint8Array); |
| 214 | + expect(result.cid.length).toBe(32); |
| 215 | + }); |
| 216 | + }); |
| 217 | + |
| 218 | + describe('CID Utilities Integration', () => { |
| 219 | + it('should format and parse CID correctly', async () => { |
| 220 | + const testData = 'Format parse test'; |
| 221 | + const result = await advanced.putWithCID(testPath, testData); |
| 222 | + |
| 223 | + // Format CID |
| 224 | + const formatted = formatCID(result.cid, 'base32'); |
| 225 | + expect(formatted).toBeTypeOf('string'); |
| 226 | + expect(formatted.length).toBeGreaterThan(0); |
| 227 | + |
| 228 | + // Parse it back |
| 229 | + const parsed = parseCID(formatted); |
| 230 | + expect(parsed).toEqual(result.cid); |
| 231 | + |
| 232 | + // Should be able to retrieve with parsed CID |
| 233 | + const retrieved = await advanced.getByCID(parsed); |
| 234 | + expect(retrieved).toBe(testData); |
| 235 | + }); |
| 236 | + |
| 237 | + it('should work with different encoding formats', async () => { |
| 238 | + const result = await advanced.putWithCID(testPath, 'Encoding test'); |
| 239 | + |
| 240 | + // Test all three encodings |
| 241 | + const base32 = formatCID(result.cid, 'base32'); |
| 242 | + const base58 = formatCID(result.cid, 'base58btc'); |
| 243 | + const base64 = formatCID(result.cid, 'base64'); |
| 244 | + |
| 245 | + // All should parse back to same CID |
| 246 | + expect(parseCID(base32)).toEqual(result.cid); |
| 247 | + expect(parseCID(base58)).toEqual(result.cid); |
| 248 | + expect(parseCID(base64)).toEqual(result.cid); |
| 249 | + }); |
| 250 | + }); |
| 251 | + |
| 252 | + describe('Encryption Integration', () => { |
| 253 | + it('should handle encrypted files with CID operations', async () => { |
| 254 | + const sensitiveData = 'Secret information'; |
| 255 | + |
| 256 | + // Store with encryption |
| 257 | + const result = await advanced.putWithCID(testPath, sensitiveData, { |
| 258 | + encryption: { algorithm: 'xchacha20-poly1305' }, |
| 259 | + }); |
| 260 | + |
| 261 | + expect(result.cid).toBeInstanceOf(Uint8Array); |
| 262 | + |
| 263 | + // Should be able to retrieve by CID (will auto-decrypt) |
| 264 | + const retrieved = await advanced.getByCID(result.cid); |
| 265 | + expect(retrieved).toBe(sensitiveData); |
| 266 | + |
| 267 | + // Should find path from CID |
| 268 | + const foundPath = await advanced.cidToPath(result.cid); |
| 269 | + expect(foundPath).toBe(testPath); |
| 270 | + }); |
| 271 | + |
| 272 | + it('should have different CIDs for same content with different encryption', async () => { |
| 273 | + const content = 'Same content, different encryption'; |
| 274 | + const path1 = 'home/encrypted1.txt'; |
| 275 | + const path2 = 'home/encrypted2.txt'; |
| 276 | + |
| 277 | + // Store with different encryption keys |
| 278 | + const result1 = await advanced.putWithCID(path1, content, { |
| 279 | + encryption: { algorithm: 'xchacha20-poly1305' } |
| 280 | + }); |
| 281 | + const result2 = await advanced.putWithCID(path2, content, { |
| 282 | + encryption: { algorithm: 'xchacha20-poly1305' } |
| 283 | + }); |
| 284 | + |
| 285 | + // Encrypted files should have different CIDs (different keys = different ciphertext) |
| 286 | + expect(result1.cid).not.toEqual(result2.cid); |
| 287 | + }); |
| 288 | + }); |
| 289 | + |
| 290 | + describe('End-to-End Workflow', () => { |
| 291 | + it('should support complete CID-based workflow', async () => { |
| 292 | + const originalData = 'Complete workflow test'; |
| 293 | + |
| 294 | + // 1. Store data and get CID |
| 295 | + const { path, cid } = await advanced.putWithCID(testPath, originalData); |
| 296 | + |
| 297 | + // 2. Format CID for sharing |
| 298 | + const cidString = formatCID(cid, 'base58btc'); |
| 299 | + |
| 300 | + // 3. Recipient: parse CID from string |
| 301 | + const receivedCID = parseCID(cidString); |
| 302 | + |
| 303 | + // 4. Recipient: retrieve data by CID |
| 304 | + const retrievedData = await advanced.getByCID(receivedCID); |
| 305 | + expect(retrievedData).toBe(originalData); |
| 306 | + |
| 307 | + // 5. Recipient: find path from CID |
| 308 | + const foundPath = await advanced.cidToPath(receivedCID); |
| 309 | + expect(foundPath).toBe(path); |
| 310 | + |
| 311 | + // 6. Verify metadata includes CID |
| 312 | + if (foundPath) { |
| 313 | + const metadata = await advanced.getMetadataWithCID(foundPath); |
| 314 | + expect(metadata.cid).toEqual(cid); |
| 315 | + } |
| 316 | + }); |
| 317 | + }); |
| 318 | +}); |
0 commit comments