Files
dashlibstorage/scripts/test-sqlite-storage.js
2026-10-10 02:14:03 +02:00

291 lines
13 KiB
JavaScript

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 sharp from 'sharp';
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 testThumb = await sharp({
create: {
width: 64,
height: 64,
channels: 4,
background: { r: 255, g: 0, b: 0, alpha: 1 },
},
})
.avif()
.toBuffer();
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, true, 'Default track_stock must be true');
const fetchedArt = await store.article.getBySku(googleAuthId, article.sku);
assert.equal(fetchedArt.article.track_stock, true, 'getBySku must return track_stock: true');
await store.article.setTrackStockBySku(googleAuthId, article.sku, false);
const afterSet = await store.article.getBySku(googleAuthId, article.sku);
assert.equal(afterSet.article.track_stock, false, 'setTrackStockBySku 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 sampleImage = await sharp({
create: {
width: 300,
height: 200,
channels: 4,
background: { r: 0, g: 128, b: 255, alpha: 1 },
},
})
.png()
.toBuffer();
const picResult = await store.article.addImage(googleAuthId, article.sku, sampleImage);
const fullHash = picResult.fullHash;
assert.equal(picResult.smallThumb.w, 40);
assert.equal(picResult.smallThumb.h, 27);
const smallThumbRow = provider.db.prepare(
'SELECT smallthumbw, smallthumbh, smallthumbbinary FROM pic WHERE hash = ?',
).get(fullHash);
assert.equal(smallThumbRow.smallthumbw, 40);
assert.equal(smallThumbRow.smallthumbh, 27);
assert(Buffer.isBuffer(smallThumbRow.smallthumbbinary), 'Small thumb AVIF must be stored in the pic row');
const smallThumbMetadata = await sharp(smallThumbRow.smallthumbbinary).metadata();
assert.equal(smallThumbMetadata.format, 'heif');
assert.equal(smallThumbMetadata.width, 40);
assert.equal(smallThumbMetadata.height, 27);
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, 40);
assert.equal(listedPic.smallThumbH, 27);
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);
});