import assert from 'node:assert/strict'; import { mkdtempSync, existsSync, readFileSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { optimizeImage } from 'wasm-image-optimization'; import LibDashStore from '../src/index.js'; import { BetterSqlite3Provider } from '../src/providers/sqlite3/index.js'; async function runTests() { console.log('--- Starting SQLite file and pic directory storage tests ---'); const tempDir = mkdtempSync(join(tmpdir(), 'test-sqlite-store-')); const dbFile = join(tempDir, 'test.db'); const storageDir = join(tempDir, 'custom-storage'); const provider = new BetterSqlite3Provider({ filename: dbFile, storageDir, }); await provider.initialize(); const store = await new LibDashStore(provider); try { // Verify storage subdirectories exist assert(existsSync(join(storageDir, 'content')), 'content dir should exist'); assert(existsSync(join(storageDir, 'thumbavif')), 'thumbavif dir should exist'); assert(existsSync(join(storageDir, 'fullbinary')), 'fullbinary dir should exist'); assert(existsSync(join(storageDir, 'thumbbinary')), 'thumbbinary dir should exist'); // Large file and image blobs stay on disk; the 32px small-thumb stays inline in pic. const fileTableInfo = provider.db.pragma('table_info("file")'); const fileColNames = fileTableInfo.map((c) => c.name); console.log('file table columns:', fileColNames); assert(!fileColNames.includes('content'), 'file table should NOT have content column'); assert(!fileColNames.includes('thumbavif'), 'file table should NOT have thumbavif column'); const picTableInfo = provider.db.pragma('table_info("pic")'); const picColNames = picTableInfo.map((c) => c.name); console.log('pic table columns:', picColNames); assert(!picColNames.includes('fullbinary'), 'pic table should NOT have fullbinary column'); assert(!picColNames.includes('thumbbinary'), 'pic table should NOT have thumbbinary column'); assert(picColNames.includes('smallthumbw'), 'pic table should have smallthumbw column'); assert(picColNames.includes('smallthumbh'), 'pic table should have smallthumbh column'); assert(picColNames.includes('smallthumbbinary'), 'pic table should have smallthumbbinary column'); // Setup base data: google_auth, company, unit, article, manifest const googleAuthId = await store.google_auth.add('test@example.com'); const company = await store.company.create({ name: 'Test Corp', iso639: 'de', taxrate: 19, reducedtaxrate: 7, googleAuthId, }); await store.google_auth.setSelectedCompany(googleAuthId, company.id); // 1. Test Manifest Attachment (file table) console.log('1. Testing manifestattachment with file table...'); const delivery = await store.delivery.create(googleAuthId, { externalNumber: 'DEL-1', date: new Date().toISOString(), lines: [ { name: 'Item A', amount: 5, net: 10, taxrate: 19, }, ], }); const manifestId = delivery.id; const testContent = Buffer.from('Hello SQLite Storage Engine! PDF or text content test.'); const thumbSvg = ``; const testThumbRes = await optimizeImage({ image: thumbSvg, format: 'avif' }); const testThumb = Buffer.from(testThumbRes.data); const attachment = await store.manifestattachment.add(googleAuthId, manifestId, { name: 'test-doc.txt', mimetype: 'text/plain', content: testContent, thumbavif: testThumb, thumbw: 64, thumbh: 64, }); console.log('Attachment created with hash:', attachment.hash); // Verify files on disk have correct extensions according to mimetype const contentPath = join(storageDir, 'content', attachment.hash + '.txt'); const thumbPath = join(storageDir, 'thumbavif', attachment.hash + '.avif'); assert(existsSync(contentPath), 'Content file must exist on disk at ' + contentPath); assert(existsSync(thumbPath), 'Thumb file must exist on disk at ' + thumbPath); assert.deepEqual(readFileSync(contentPath), testContent, 'Content on disk must match'); assert.deepEqual(readFileSync(thumbPath), testThumb, 'Thumb on disk must match'); // Verify getById const fetched = await store.manifestattachment.getById(googleAuthId, manifestId, attachment.id); assert.equal(fetched.name, 'test-doc.txt'); assert(Buffer.isBuffer(fetched.content), 'Fetched content must be a Buffer'); assert.deepEqual(fetched.content, testContent, 'Fetched content must match original'); assert(Buffer.isBuffer(fetched.thumbavif), 'Fetched thumbavif must be a Buffer'); assert.deepEqual(fetched.thumbavif, testThumb, 'Fetched thumbavif must match original'); // Verify listByManifestId const list = await store.manifestattachment.listByManifestId(googleAuthId, manifestId); assert.equal(list.length, 1); assert(Buffer.isBuffer(list[0].thumbavif), 'List thumbavif must be a Buffer'); assert.deepEqual(list[0].thumbavif, testThumb); // Verify remove await store.manifestattachment.remove(googleAuthId, manifestId, attachment.id); assert(!existsSync(contentPath), 'Content file should be deleted on remove'); assert(!existsSync(thumbPath), 'Thumb file should be deleted on remove'); // Test delivery creation with inline attached files console.log('1b. Testing delivery.create with inline attached file...'); const deliveryWithFile = await store.delivery.create(googleAuthId, { externalNumber: 'DEL-WITH-FILE', date: new Date().toISOString(), lines: [{ name: 'Item B', amount: 1, net: 5, taxrate: 19 }], files: [{ name: 'inline.txt', mimetype: 'text/plain', content: Buffer.from('Inline delivery file content'), }], }); assert(deliveryWithFile.files && deliveryWithFile.files.length === 1, 'Delivery should have 1 attached file'); const inlineHash = deliveryWithFile.files[0].hash; const inlineContentPath = join(storageDir, 'content', inlineHash + '.txt'); const inlineThumbPath = join(storageDir, 'thumbavif', inlineHash + '.avif'); assert(existsSync(inlineContentPath), 'Inline file content must exist on disk with .txt extension'); assert(existsSync(inlineThumbPath), 'Inline file thumb must exist on disk with .avif extension'); const fetchedDelivery = await store.delivery.getById(googleAuthId, deliveryWithFile.id); assert(fetchedDelivery.files && fetchedDelivery.files.length === 1); assert(fetchedDelivery.files[0].thumbavif, 'Fetched delivery file should include thumbavif'); console.log('Manifest attachment tests passed!'); // 2. Test Article Image (pic table) console.log('2. Testing article.addImage with pic table...'); const cat = await store.category.createUnderParent({ googleAuthId, parentId: company.rootCategoryId, name: 'Sub Category', }); const article = await store.article.createUnderParent({ googleAuthId, parentId: cat.id, name: 'Test Article', }); assert.equal(article.track_stock, false, 'Default track_stock must be false'); const fetchedArt = await store.article.getBySku(googleAuthId, article.sku); assert.equal(fetchedArt.article.track_stock, false, 'getBySku must return track_stock: false'); // Enabling track_stock without a warehouse must fail await assert.rejects( async () => store.article.setTrackStockBySku(googleAuthId, article.sku, true), /At least one warehouse is required to enable stock tracking/, ); // Create warehouse to allow enabling track_stock await store.warehouse.create(googleAuthId, { name: 'Storage WH' }); await store.article.setTrackStockBySku(googleAuthId, article.sku, true); const afterSet = await store.article.getBySku(googleAuthId, article.sku); assert.equal(afterSet.article.track_stock, true, 'setTrackStockBySku true must update track_stock'); await store.article.updateBySku(googleAuthId, article.sku, { track_stock: false }); const afterDisable = await store.article.getBySku(googleAuthId, article.sku); assert.equal(afterDisable.article.track_stock, false, 'updateBySku track_stock: false must update track_stock'); await store.article.updateBySku(googleAuthId, article.sku, { track_stock: true }); const afterUpdate = await store.article.getBySku(googleAuthId, article.sku); assert.equal(afterUpdate.article.track_stock, true, 'updateBySku track_stock: true must update track_stock'); const history = await store.article.getHistoryBySku(googleAuthId, article.sku); assert(history.some((h) => h.text.includes('Bestandsführung geändert von "Nein" auf "Ja"'))); const nonTracked = await store.article.createUnderParent({ googleAuthId, parentId: cat.id, name: 'Service Item', track_stock: false, }); assert.equal(nonTracked.track_stock, false, 'Explicit track_stock: false must be preserved'); const fetchedNonTracked = await store.article.getBySku(googleAuthId, nonTracked.sku); assert.equal(fetchedNonTracked.article.track_stock, false); const exported = await store.article.listExport(googleAuthId, cat.id); const foundExport = exported.find((a) => a.sku === article.sku); assert.equal(foundExport.track_stock, true, 'listExport must include track_stock'); console.log('Article track_stock tests passed!'); const sampleSvg = ``; const sampleImageRes = await optimizeImage({ image: sampleSvg, format: 'png' }); const sampleImage = Buffer.from(sampleImageRes.data); const picResult = await store.article.addImage(googleAuthId, article.sku, sampleImage); const fullHash = picResult.fullHash; assert.equal(picResult.smallThumb.w, 32); assert.equal(picResult.smallThumb.h, 21); const smallThumbRow = provider.db.prepare( 'SELECT smallthumbw, smallthumbh, smallthumbbinary FROM pic WHERE hash = ?', ).get(fullHash); assert.equal(smallThumbRow.smallthumbw, 32); assert.equal(smallThumbRow.smallthumbh, 21); assert(Buffer.isBuffer(smallThumbRow.smallthumbbinary), 'Small thumb AVIF must be stored in the pic row'); const smallThumbMetadata = await optimizeImage({ image: smallThumbRow.smallthumbbinary, format: 'none' }); assert.equal(smallThumbMetadata.originalFormat, 'avif'); assert.equal(smallThumbMetadata.originalWidth, 32); assert.equal(smallThumbMetadata.originalHeight, 21); const brand = await store.brand.create(googleAuthId, { name: 'Small Thumb Test Brand' }); await store.brand.addImage(googleAuthId, brand.id, sampleImage); const listedBrands = await store.brand.list(googleAuthId, { offset: 0, limit: 100 }); const listedBrand = listedBrands.brands.find((item) => item.id === brand.id); assert(listedBrand, 'Brand list should include the created brand'); assert.equal(listedBrand.pics.length, 1); assert(!('smallThumbW' in listedBrand.pics[0]), 'Small-thumb fields should be omitted by default'); const listedBrandsWithSmallThumbs = await store.brand.list(googleAuthId, { offset: 0, limit: 100, includeSmallThumbs: true, }); const listedBrandWithSmallThumbs = listedBrandsWithSmallThumbs.brands.find((item) => item.id === brand.id); assert(listedBrandWithSmallThumbs, 'Brand list with thumbnails should include the created brand'); assert.equal(listedBrandWithSmallThumbs.pics.length, 1); const listedPic = listedBrandWithSmallThumbs.pics[0]; assert.equal(listedPic.smallThumbW, 32); assert.equal(listedPic.smallThumbH, 21); assert.equal(typeof listedPic.smallThumbBinary, 'string'); const listedSmallThumb = Buffer.from(listedPic.smallThumbBinary, 'base64'); assert.deepEqual(listedSmallThumb, smallThumbRow.smallthumbbinary); console.log('Article pic added with hash:', fullHash); const fullPath = join(storageDir, 'fullbinary', fullHash + '.avif'); const picThumbPath = join(storageDir, 'thumbbinary', fullHash + '.avif'); assert(existsSync(fullPath), 'Full picture must exist on disk at ' + fullPath); assert(existsSync(picThumbPath), 'Thumb picture must exist on disk at ' + picThumbPath); // Verify getImageByHash const fullPic = await store.pic.getImageByHash(googleAuthId, fullHash); assert(Buffer.isBuffer(fullPic.buffer), 'Image buffer must be returned'); assert.deepEqual(fullPic.buffer, readFileSync(fullPath)); // Verify getThumbByHash const thumbPic = await store.pic.getThumbByHash(googleAuthId, fullHash); assert(Buffer.isBuffer(thumbPic.buffer), 'Thumbnail buffer must be returned'); assert.deepEqual(thumbPic.buffer, readFileSync(picThumbPath)); // Verify deleteImage await store.article.deleteImage(googleAuthId, article.sku, fullHash); assert(!existsSync(fullPath), 'Full pic file should be deleted on deleteImage'); assert(!existsSync(picThumbPath), 'Thumb pic file should be deleted on deleteImage'); console.log('Article pic tests passed!'); // 3. Test purge console.log('3. Testing purge...'); // Add one more file before purge await store.manifestattachment.add(googleAuthId, manifestId, { name: 'purge-test.txt', mimetype: 'text/plain', content: Buffer.from('To be purged'), }); await provider.purge(); assert(!existsSync(contentPath), 'Files should be purged'); console.log('Purge tests passed!'); console.log('\nAll tests PASSED successfully!'); } finally { provider.close(); try { rmSync(tempDir, { recursive: true, force: true }); } catch { // ignore } } } runTests().catch((err) => { console.error('Test failed with error:', err); process.exit(1); });