This commit is contained in:
seb
2026-10-01 17:40:04 +02:00
parent 62c3e69ed4
commit 84e547660e
53 changed files with 4604 additions and 2059 deletions

View File

@@ -0,0 +1,175 @@
import assert from 'node:assert/strict';
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import LibDashStore from '../src/index.js';
import { BetterSqlite3Provider } from '../src/providers/sqlite3/index.js';
async function run() {
console.log('--- Testing CategoryProperty & CategoryAttributeValue ---');
const tempDir = mkdtempSync(join(tmpdir(), 'test-cat-prop-attr-'));
const dbFile = join(tempDir, 'test.db');
const storageDir = join(tempDir, 'storage');
const provider = new BetterSqlite3Provider({
filename: dbFile,
storageDir,
});
await provider.initialize();
const store = await new LibDashStore(provider);
try {
const googleAuthId = await store.google_auth.add('tester@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);
// Create a subcategory under root
const category = await store.category.createUnderParent({
googleAuthId,
parentId: company.rootCategoryId,
name: 'Test Category',
});
assert.ok(category.id > 0);
// 1. Test CategoryProperty
console.log('1. Testing categoryproperty CRUD...');
const prop1 = await store.propertyname.create(googleAuthId, {
name: 'Material Care',
exclusive: true,
});
const prop2 = await store.propertyname.create(googleAuthId, {
name: 'Season',
exclusive: false,
});
// Set properties
const setProps = await store.categoryproperty.setByCategoryId(googleAuthId, category.id, [
{ propertyId: prop1.id, value: 'Dry Clean Only', sort: 10 },
{ propertyId: prop2.id, value: 'Summer 2026', sort: 20 },
]);
assert.equal(setProps.length, 2);
// List properties
const listedProps = await store.categoryproperty.listByCategoryId(googleAuthId, category.id);
assert.equal(listedProps.length, 2);
assert.equal(listedProps[0].name, 'Material Care');
assert.equal(listedProps[0].value, 'Dry Clean Only');
assert.equal(listedProps[0].exclusive, true);
assert.equal(listedProps[1].name, 'Season');
assert.equal(listedProps[1].value, 'Summer 2026');
assert.equal(listedProps[1].exclusive, false);
// Test exclusive violation on categoryproperty
await assert.rejects(
async () => {
await store.categoryproperty.setByCategoryId(googleAuthId, category.id, [
{ propertyId: prop1.id, value: 'Val 1' },
{ propertyId: prop1.id, value: 'Val 2' },
]);
},
/exclusive/i,
'Should reject multiple values for exclusive property'
);
// Delete single property
await store.categoryproperty.delete(googleAuthId, category.id, prop1.id);
const afterDeleteProps = await store.categoryproperty.listByCategoryId(googleAuthId, category.id);
assert.equal(afterDeleteProps.length, 1);
assert.equal(afterDeleteProps[0].propertyId, prop2.id);
console.log('CategoryProperty tests passed!');
// 2. Test CategoryAttributeValue
console.log('2. Testing categoryattributevalue CRUD...');
const attrExclusive = await store.attributename.create(googleAuthId, {
name: 'Department',
exclusive: true,
});
const valDeptMen = await store.attributevalue.create(googleAuthId, attrExclusive.id, {
value: 'Men',
});
const valDeptWomen = await store.attributevalue.create(googleAuthId, attrExclusive.id, {
value: 'Women',
});
const attrMulti = await store.attributename.create(googleAuthId, {
name: 'Tags',
exclusive: false,
});
const valTagSale = await store.attributevalue.create(googleAuthId, attrMulti.id, {
value: 'Sale',
});
const valTagNew = await store.attributevalue.create(googleAuthId, attrMulti.id, {
value: 'New',
});
// Bulk set with numeric array
const setAttrValues = await store.categoryattributevalue.setByCategoryId(googleAuthId, category.id, [
valDeptMen.id,
valTagSale.id,
valTagNew.id,
]);
assert.equal(setAttrValues.length, 3);
// List attribute values
const listedAttrValues = await store.categoryattributevalue.listByCategoryId(googleAuthId, category.id);
assert.equal(listedAttrValues.length, 3);
const deptMatch = listedAttrValues.find((v) => v.attributeName === 'Department');
assert.ok(deptMatch);
assert.equal(deptMatch.value, 'Men');
assert.equal(deptMatch.exclusive, true);
// Test exclusive violation
await assert.rejects(
async () => {
await store.categoryattributevalue.setByCategoryId(googleAuthId, category.id, [
valDeptMen.id,
valDeptWomen.id,
]);
},
/exclusive/i,
'Should reject multiple values for exclusive attribute'
);
// Test flexible object shapes in setByCategoryId
const setWithObjs = await store.categoryattributevalue.setByCategoryId(googleAuthId, category.id, [
{ attributeValueId: valDeptWomen.id, sort: 5 },
{ id: valTagSale.id, sort: 15 },
]);
assert.equal(setWithObjs.length, 2);
const listedAfterObjSet = await store.categoryattributevalue.listByCategoryId(googleAuthId, category.id);
assert.equal(listedAfterObjSet.length, 2);
assert.equal(listedAfterObjSet[0].value, 'Women');
assert.equal(listedAfterObjSet[0].sort, 5);
// 3. Test cascade deletion on category delete
console.log('3. Testing cascade deletion when category is deleted...');
await store.category.delete({ googleAuthId, id: category.id });
// Directly query database to ensure categoryproperty and categoryattributevalue rows were deleted by FK cascade
const remainingProps = await provider.query('SELECT * FROM categoryproperty WHERE category_id = @id', { id: category.id });
assert.equal(remainingProps.rows.length, 0);
const remainingAttrValues = await provider.query('SELECT * FROM categoryattributevalue WHERE category_id = @id', { id: category.id });
assert.equal(remainingAttrValues.rows.length, 0);
console.log('All CategoryProperty and CategoryAttributeValue tests passed successfully!');
} finally {
try {
rmSync(tempDir, { recursive: true, force: true });
} catch {}
}
}
run().catch((err) => {
console.error('Test failed:', err);
process.exit(1);
});

View File

@@ -0,0 +1,121 @@
import assert from 'node:assert/strict';
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import LibDashStore from '../src/index.js';
import { BetterSqlite3Provider } from '../src/providers/sqlite3/index.js';
async function run() {
console.log('--- Testing Father History & AttributeValue Shapes ---');
const tempDir = mkdtempSync(join(tmpdir(), 'test-hist-attr-'));
const dbFile = join(tempDir, 'test.db');
const storageDir = join(tempDir, 'storage');
const provider = new BetterSqlite3Provider({
filename: dbFile,
storageDir,
});
await provider.initialize();
const store = await new LibDashStore(provider);
try {
const googleAuthId = await store.google_auth.add('tester@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);
console.log('1. Testing setFatherBySku history logging...');
const father = await store.article.createUnderParent({
googleAuthId,
parentId: company.rootCategoryId,
name: 'Father Shirt',
});
const child = await store.article.createUnderParent({
googleAuthId,
parentId: company.rootCategoryId,
name: 'Child Shirt Red',
});
// Check history before linking
let fatherHist = await store.article.getHistoryBySku(googleAuthId, father.sku);
assert.equal(fatherHist.length, 1);
assert.match(fatherHist[0].text, /angelegt/);
// Link child to father
await store.article.setFatherBySku(googleAuthId, child.sku, father.sku);
// Father history must now contain the conversion entry and the child entry!
fatherHist = await store.article.getHistoryBySku(googleAuthId, father.sku);
const hasConversion = fatherHist.some((h) => h.text.includes('Artikel zu Vaterartikel konvertiert'));
const hasVariantAdded = fatherHist.some((h) => h.text.includes('hinzugefügt'));
assert.ok(hasConversion, `Father history missing conversion row: ${JSON.stringify(fatherHist)}`);
assert.ok(hasVariantAdded, `Father history missing variant added row: ${JSON.stringify(fatherHist)}`);
// Child history must contain the assignment entry
const childHist = await store.article.getHistoryBySku(googleAuthId, child.sku);
const hasAssigned = childHist.some((h) => h.text.includes('als Variante von'));
assert.ok(hasAssigned, `Child history missing variant assigned row: ${JSON.stringify(childHist)}`);
console.log('2. Testing importRows Pass 2 history logging...');
const importRes = await store.article.importRows({
googleAuthId,
rows: [
{ sku: 'IMP-FATHER', name: 'Imported Father', parentId: company.rootCategoryId },
{ sku: 'IMP-CHILD', name: 'Imported Child', parentId: company.rootCategoryId, fatherSku: 'IMP-FATHER' },
],
});
assert.equal(importRes.errors.length, 0);
const impFatherHist = await store.article.getHistoryBySku(googleAuthId, 'IMP-FATHER');
const impHasConversion = impFatherHist.some((h) => h.text.includes('Artikel zu Vaterartikel konvertiert'));
assert.ok(impHasConversion, `Imported father missing conversion row: ${JSON.stringify(impFatherHist)}`);
console.log('3. Testing flexible attributevalue request shapes in setByArticleSku...');
const attrName = await store.attributename.create(googleAuthId, { name: 'Farbe' });
const val1 = await store.attributevalue.create(googleAuthId, attrName.id, { value: 'Rot' });
const val2 = await store.attributevalue.create(googleAuthId, attrName.id, { value: 'Blau' });
// Test A: Array of IDs [val1.id, val2.id]
let res = await store.articleattributevalue.setByArticleSku(googleAuthId, child.sku, [val1.id]);
assert.equal(res.length, 1);
assert.equal(res[0].attributeValueId, val1.id);
// Test B: Array of objects with .id [{ id: val1.id }]
res = await store.articleattributevalue.setByArticleSku(googleAuthId, child.sku, [{ id: val2.id }]);
assert.equal(res.length, 1);
assert.equal(res[0].attributeValueId, val2.id);
// Test C: Array of objects with .attributeValueId
res = await store.articleattributevalue.setByArticleSku(googleAuthId, child.sku, [{ attributeValueId: val1.id }]);
assert.equal(res.length, 1);
assert.equal(res[0].attributeValueId, val1.id);
// Test D: Array of objects with attributeId and value
res = await store.articleattributevalue.setByArticleSku(googleAuthId, child.sku, [{ attributeId: attrName.id, value: 'Blau' }]);
assert.equal(res.length, 1);
assert.equal(res[0].attributeValueId, val2.id);
// Test E: Array of objects with attributeName and value
res = await store.articleattributevalue.setByArticleSku(googleAuthId, child.sku, [{ attributeName: 'Farbe', value: 'Rot' }]);
assert.equal(res.length, 1);
assert.equal(res[0].attributeValueId, val1.id);
console.log('✅ ALL TESTS PASSED SUCCESSFULLY');
} finally {
try {
rmSync(tempDir, { recursive: true, force: true });
} catch {}
}
}
run().catch((err) => {
console.error('Test failed:', err);
process.exit(1);
});

View File

@@ -80,20 +80,29 @@ async function run() {
assert.equal(wh1BundleStock, 5, 'Bundle stock in Hauptlager should be 5 (min(10/1, 10/2))');
assert.equal(wh2BundleStock, 0, 'Bundle stock in Filiale should be 0');
// Book in 3x Stückliste bundle into Filiale
const adjustResult = await store.stock.adjust(googleAuthId, {
sku: bundle.sku,
warehouseId: wh2.id,
amount: 3,
text: 'Production bundle book-in',
});
// Verify store.stock.adjust rejects adjustment on Stückliste
await assert.rejects(
() =>
store.stock.adjust(googleAuthId, {
sku: bundle.sku,
warehouseId: wh2.id,
amount: 3,
text: 'Attempt to adjust Stückliste directly',
}),
(err) => {
assert.match(err.message, /A Stückliste cannot have manual stock adjustments/);
return true;
},
'Should reject stock adjust on Stückliste bundle'
);
// Verify adjust result lines
assert.equal(adjustResult.lines.length, 2, 'Should expand into 2 component lines');
const comp1Line = adjustResult.lines.find((l) => l.sku === comp1.sku);
const comp2Line = adjustResult.lines.find((l) => l.sku === comp2.sku);
assert.equal(comp1Line.amount, 3, 'Component 1 should get 3 * 1 = 3');
assert.equal(comp2Line.amount, 6, 'Component 2 should get 3 * 2 = 6');
// Book components directly into Filiale: 3x comp1, 6x comp2
await store.stock.adjust(googleAuthId, {
lines: [
{ sku: comp1.sku, warehouseId: wh2.id, amount: 3, text: 'Direct component 1 book-in' },
{ sku: comp2.sku, warehouseId: wh2.id, amount: 6, text: 'Direct component 2 book-in' },
],
});
// Verify bundle article itself has NO entries in the ledger
const bundleLedger = await store.ledger.listBySku(googleAuthId, bundle.sku);
@@ -112,17 +121,17 @@ async function run() {
const wh2AfterBookIn = afterBundleBookIn.article.warehouses.find((w) => w.id === wh2.id).stock;
assert.equal(wh2AfterBookIn, 3, 'Bundle stock in Filiale should be 3');
// Test booking out 1x bundle from Filiale
// Test booking out components from Filiale: -1x comp1, -2x comp2 (equivalent to 1 bundle sale)
await store.stock.adjust(googleAuthId, {
sku: bundle.sku,
warehouseId: wh2.id,
amount: -1,
text: 'Sale bundle book-out',
lines: [
{ sku: comp1.sku, warehouseId: wh2.id, amount: -1, text: 'Component 1 book-out' },
{ sku: comp2.sku, warehouseId: wh2.id, amount: -2, text: 'Component 2 book-out' },
],
});
const afterRemoval = await store.article.getBySku(googleAuthId, bundle.sku);
const wh2AfterRemoval = afterRemoval.article.warehouses.find((w) => w.id === wh2.id).stock;
assert.equal(wh2AfterRemoval, 2, 'Bundle stock in Filiale should be 2 after -1 removal');
assert.equal(wh2AfterRemoval, 2, 'Bundle stock in Filiale should be 2 after component removal');
// Verify components stock after removal in Filiale: comp1 = 2, comp2 = 4
const comp1After = await store.article.getBySku(googleAuthId, comp1.sku);

210
scripts/test-tombstone.js Normal file
View File

@@ -0,0 +1,210 @@
import assert from 'node:assert/strict';
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import LibDashStore from '../src/index.js';
import { BetterSqlite3Provider } from '../src/providers/sqlite3/index.js';
async function run() {
console.log('--- Testing articletombstone table and deletion flow ---');
const tempDir = mkdtempSync(join(tmpdir(), 'test-tombstone-'));
const dbFile = join(tempDir, 'test.db');
const storageDir = join(tempDir, 'storage');
const provider = new BetterSqlite3Provider({
filename: dbFile,
storageDir,
});
await provider.initialize();
const store = await new LibDashStore(provider);
try {
// 1. Verify table exists and has expected columns
console.log('1. Checking articletombstone schema...');
const tableInfo = provider.db.pragma('table_info("articletombstone")');
const colNames = tableInfo.map((c) => c.name);
console.log('articletombstone columns:', colNames);
assert.deepEqual(colNames, [
'id',
'company_id',
'article_id',
'sku',
'name',
'taxrate',
'net',
'deleted_at',
]);
// Setup company first
console.log('2. Setting up base data...');
const googleAuthId = await store.google_auth.add('tombstone-tester@example.com');
const company = await store.company.create({
name: 'Tombstone Corp',
iso639: 'de',
taxrate: 19,
reducedtaxrate: 7,
googleAuthId,
});
await store.google_auth.setSelectedCompany(googleAuthId, company.id);
// 3. Test CHECK constraint (article_id IS NOT NULL OR sku IS NOT NULL)
console.log('3. Testing CHECK constraint...');
// Setting both article_id and sku to NULL should fail
assert.throws(() => {
provider.db.prepare(`
INSERT INTO articletombstone (company_id, article_id, sku, name)
VALUES (?, NULL, NULL, 'Test')
`).run(company.id);
}, /CHECK constraint failed/);
// article_id present, sku null -> should succeed
const r1 = provider.db.prepare(`
INSERT INTO articletombstone (company_id, article_id, sku, name)
VALUES (?, 999, NULL, 'Only ID')
`).run(company.id);
assert.ok(r1.lastInsertRowid);
// sku present, article_id null -> should succeed
const r2 = provider.db.prepare(`
INSERT INTO articletombstone (company_id, article_id, sku, name)
VALUES (?, NULL, 'SKU-ONLY', 'Only SKU')
`).run(company.id);
assert.ok(r2.lastInsertRowid);
// Clean up manual test entries
provider.db.prepare('DELETE FROM articletombstone').run();
// 4. Test article deletion flow
console.log('4. Creating test article...');
const article = await store.article.createUnderParent({
googleAuthId,
parentId: company.rootCategoryId,
name: 'Coffee Mug',
});
// Set prices for the article
await store.article.setPricesBySku(googleAuthId, article.sku, [
{ price: 12.50, tax: 19, minmultiplier: 1 },
]);
// Verify article exists
const detailBefore = await store.article.getBySku(googleAuthId, article.sku);
assert.equal(detailBefore.article.name, 'Coffee Mug');
assert.equal(detailBefore.article.prices[0].net, 12.50);
assert.equal(detailBefore.article.prices[0].taxRate, 19);
// Delete the article
console.log('4. Deleting article and checking tombstone creation...');
await store.article.delete(googleAuthId, article.sku);
// Verify article is deleted
await assert.rejects(
() => store.article.getBySku(googleAuthId, article.sku),
/Article not found/
);
// Verify tombstone record
const tombstones = provider.db.prepare('SELECT * FROM articletombstone WHERE sku = ?').all(article.sku);
assert.equal(tombstones.length, 1);
const tomb = tombstones[0];
console.log('Created tombstone:', tomb);
assert.equal(tomb.company_id, company.id);
assert.equal(tomb.article_id, article.id);
assert.equal(tomb.sku, article.sku);
assert.equal(tomb.name, 'Coffee Mug');
assert.equal(tomb.net, 12.50);
assert.equal(tomb.taxrate, 19);
assert.ok(tomb.deleted_at);
// 5. Test cascading deletion with father and variants
console.log('5. Testing father with variant child deletion...');
const father = await store.article.createUnderParent({
googleAuthId,
parentId: company.rootCategoryId,
name: 'Pants',
});
const attrName = await store.attributename.create(googleAuthId, { name: 'Größe' });
await store.article.toggleFather(googleAuthId, father.sku, {
isFather: true,
attributeNameId: attrName.id,
});
const child = await store.article.addChild(googleAuthId, father.sku, { value: 'M' });
await store.article.setPricesBySku(googleAuthId, child.article.sku, [
{ price: 49.99, tax: 19, minmultiplier: 1 },
]);
// Delete child via removeChild with deleteArticle
console.log('Deleting child via removeChild({ deleteArticle: true })...');
await store.article.removeChild(googleAuthId, father.sku, child.article.sku, { deleteArticle: true });
// Add another child and delete father with cascade: true
const child2 = await store.article.addChild(googleAuthId, father.sku, { value: 'L' });
console.log('Deleting father with cascade: true...');
await store.article.delete(googleAuthId, father.sku, { cascade: true });
const allTombstones = provider.db.prepare('SELECT * FROM articletombstone ORDER BY id ASC').all();
assert.equal(allTombstones.length, 4);
// 1st tombstone: Coffee Mug
assert.equal(allTombstones[0].name, 'Coffee Mug');
assert.equal(allTombstones[0].sku, '1');
assert.equal(allTombstones[0].net, 12.5);
assert.equal(allTombstones[0].taxrate, 19);
// 2nd tombstone: Pants - M (deleted via removeChild)
assert.equal(allTombstones[1].name, 'Pants - M');
assert.equal(allTombstones[1].sku, child.article.sku);
assert.equal(allTombstones[1].net, 49.99);
assert.equal(allTombstones[1].taxrate, 19);
// 3rd tombstone: Pants - L (child deleted in cascade)
assert.equal(allTombstones[2].name, 'Pants - L');
assert.equal(allTombstones[2].sku, child2.article.sku);
// 4th tombstone: Pants (father deleted in cascade)
assert.equal(allTombstones[3].name, 'Pants');
assert.equal(allTombstones[3].sku, father.sku);
// 6. Test posterminal maxknowntombstoneid column and updates
console.log('6. Testing posterminal maxknowntombstoneid column...');
const posterminalCols = provider.db.pragma('table_info("posterminal")').map((c) => c.name);
assert.ok(posterminalCols.includes('maxknowntombstoneid'), 'posterminal should have maxknowntombstoneid column');
const terminal = await store.posterminal.upsert(googleAuthId, {
name: 'POS Terminal 1',
pairingpin: '1234',
});
assert.equal(terminal.maxknowntombstoneid, null);
// Pair terminal
await store.posterminal.pair(terminal.id, {
token: 'tok-123',
clientserial: 'SN-001',
pairedAt: new Date().toISOString(),
});
const foundByToken = await store.posterminal.findByToken('tok-123');
assert.equal(foundByToken.id, terminal.id);
assert.equal(foundByToken.maxknowntombstoneid, null);
// Update maxknowntombstoneid
await store.posterminal.setMaxKnownTombstoneId(terminal.id, 4);
const foundAfterUpdate = await store.posterminal.findByToken('tok-123');
assert.equal(foundAfterUpdate.maxknowntombstoneid, 4);
console.log('✅ ALL TOMBSTONE TESTS PASSED');
} finally {
try {
rmSync(tempDir, { recursive: true, force: true });
} catch {}
}
}
run().catch((err) => {
console.error('Test failed:', err);
process.exit(1);
});

View File

@@ -0,0 +1,147 @@
import assert from 'node:assert/strict';
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import LibDashStore from '../src/index.js';
import { BetterSqlite3Provider } from '../src/providers/sqlite3/index.js';
async function run() {
console.log('--- Testing Unique Slugs Disambiguation ---');
const tempDir = mkdtempSync(join(tmpdir(), 'test-slugs-'));
const dbFile = join(tempDir, 'test.db');
const storageDir = join(tempDir, 'storage');
const provider = new BetterSqlite3Provider({
filename: dbFile,
storageDir,
});
await provider.initialize();
const store = await new LibDashStore(provider);
try {
const googleAuthId = await store.google_auth.add('slug-tester@example.com');
const company = await store.company.create({
name: 'Slug Corp',
iso639: 'de',
taxrate: 19,
reducedtaxrate: 7,
googleAuthId,
});
await store.google_auth.setSelectedCompany(googleAuthId, company.id);
console.log('1. Testing createUnderParent duplicate slugs...');
const art1 = await store.article.createUnderParent({
googleAuthId,
parentId: company.rootCategoryId,
name: 'Root Father Shirt',
});
const art1Res = await store.article.getBySku(googleAuthId, art1.sku);
assert.equal(art1Res.article.slug, 'root-father-shirt', `Expected slug 'root-father-shirt', got '${art1Res.article.slug}'`);
const art2 = await store.article.createUnderParent({
googleAuthId,
parentId: company.rootCategoryId,
name: 'Root Father Shirt',
});
const art2Res = await store.article.getBySku(googleAuthId, art2.sku);
assert.equal(art2Res.article.slug, 'root-father-shirt-2', `Expected slug 'root-father-shirt-2', got '${art2Res.article.slug}'`);
const art3 = await store.article.createUnderParent({
googleAuthId,
parentId: company.rootCategoryId,
name: 'Root Father Shirt',
});
const art3Res = await store.article.getBySku(googleAuthId, art3.sku);
assert.equal(art3Res.article.slug, 'root-father-shirt-3', `Expected slug 'root-father-shirt-3', got '${art3Res.article.slug}'`);
console.log('2. Testing updateBySku self-collision and renaming...');
// Update art2 without changing name
await store.article.updateBySku(googleAuthId, art2.sku, { unitamount: 2 });
let updated2 = await store.article.getBySku(googleAuthId, art2.sku);
assert.equal(updated2.article.slug, 'root-father-shirt-2', `Expected unchanged slug, got '${updated2.article.slug}'`);
// Update art2 with same name
await store.article.updateBySku(googleAuthId, art2.sku, { name: 'Root Father Shirt' });
updated2 = await store.article.getBySku(googleAuthId, art2.sku);
assert.equal(updated2.article.slug, 'root-father-shirt-2', `Expected slug to remain 'root-father-shirt-2', got '${updated2.article.slug}'`);
// Rename art2 to something else
await store.article.updateBySku(googleAuthId, art2.sku, { name: 'Unique Shirt' });
updated2 = await store.article.getBySku(googleAuthId, art2.sku);
assert.equal(updated2.article.slug, 'unique-shirt', `Expected slug 'unique-shirt', got '${updated2.article.slug}'`);
// Rename art3 to 'Root Father Shirt' again: should take root-father-shirt-2 now that it is free
await store.article.updateBySku(googleAuthId, art3.sku, { name: 'Root Father Shirt' });
const updated3 = await store.article.getBySku(googleAuthId, art3.sku);
assert.equal(updated3.article.slug, 'root-father-shirt-2', `Expected slug to fill gap 'root-father-shirt-2', got '${updated3.article.slug}'`);
console.log('3. Testing addChild duplicate slugs...');
const attrName = await store.attributename.create(googleAuthId, { name: 'Größe' });
await store.article.toggleFather(googleAuthId, art1.sku, {
isFather: true,
attributeNameId: attrName.id,
});
const child1 = await store.article.addChild(googleAuthId, art1.sku, {
value: 'M',
});
assert.equal(child1.article.slug, 'root-father-shirt-m', `Expected child slug 'root-father-shirt-m', got '${child1.article.slug}'`);
// Add another child with name that yields same slug
const child2 = await store.article.addChild(googleAuthId, art1.sku, {
value: 'L',
name: 'Root Father Shirt - M',
});
assert.equal(child2.article.slug, 'root-father-shirt-m-2', `Expected child slug 'root-father-shirt-m-2', got '${child2.article.slug}'`);
console.log('4. Testing importRows duplicate slugs in same batch...');
const importRes = await store.article.importRows({
googleAuthId,
rows: [
{ sku: 'IMP-1', name: 'Batch Shirt', parentId: company.rootCategoryId },
{ sku: 'IMP-2', name: 'Batch Shirt', parentId: company.rootCategoryId },
{ sku: 'IMP-3', name: 'Batch Shirt', parentId: company.rootCategoryId },
],
});
assert.equal(importRes.errors.length, 0, `Import had errors: ${JSON.stringify(importRes.errors)}`);
assert.equal(importRes.imported.length, 3);
const imp1 = await store.article.getBySku(googleAuthId, 'IMP-1');
const imp2 = await store.article.getBySku(googleAuthId, 'IMP-2');
const imp3 = await store.article.getBySku(googleAuthId, 'IMP-3');
assert.equal(imp1.article.slug, 'batch-shirt');
assert.equal(imp2.article.slug, 'batch-shirt-2');
assert.equal(imp3.article.slug, 'batch-shirt-3');
console.log('5. Testing company isolation...');
const company2 = await store.company.create({
name: 'Other Corp',
iso639: 'de',
taxrate: 19,
reducedtaxrate: 7,
googleAuthId,
});
await store.google_auth.setSelectedCompany(googleAuthId, company2.id);
const artCompany2 = await store.article.createUnderParent({
googleAuthId,
parentId: company2.rootCategoryId,
name: 'Root Father Shirt',
});
const artCompany2Detail = await store.article.getBySku(googleAuthId, artCompany2.sku);
assert.equal(artCompany2Detail.article.slug, 'root-father-shirt', `Expected 'root-father-shirt' in company 2, got '${artCompany2Detail.article.slug}'`);
console.log('✅ ALL SLUG TESTS PASSED');
} finally {
try {
rmSync(tempDir, { recursive: true, force: true });
} catch {}
}
}
run().catch((err) => {
console.error('Test failed:', err);
process.exit(1);
});

View File

@@ -0,0 +1,130 @@
import assert from 'node:assert/strict';
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import LibDashStore from '../src/index.js';
import { BetterSqlite3Provider } from '../src/providers/sqlite3/index.js';
async function run() {
console.log('--- Testing Variant Flow: Next-Free SKU, Fathersort Reclaim, and Duplicate Protection ---');
const tempDir = mkdtempSync(join(tmpdir(), 'test-variants-'));
const dbFile = join(tempDir, 'test.db');
const storageDir = join(tempDir, 'storage');
const provider = new BetterSqlite3Provider({
filename: dbFile,
storageDir,
});
await provider.initialize();
const store = await new LibDashStore(provider);
try {
const googleAuthId = await store.google_auth.add('variant-tester@example.com');
const company = await store.company.create({
name: 'Variant Corp',
iso639: 'de',
taxrate: 19,
reducedtaxrate: 7,
googleAuthId,
});
await store.google_auth.setSelectedCompany(googleAuthId, company.id);
// Create father article
const father = await store.article.createUnderParent({
googleAuthId,
parentId: company.rootCategoryId,
name: 'T-Shirt',
});
const attrName = await store.attributename.create(googleAuthId, { name: 'Farbe' });
await store.article.toggleFather(googleAuthId, father.sku, {
isFather: true,
attributeNameId: attrName.id,
});
console.log('1. Add initial children (Rot, Blau, Grün)...');
const child1 = await store.article.addChild(googleAuthId, father.sku, { value: 'Rot' });
const child2 = await store.article.addChild(googleAuthId, father.sku, { value: 'Blau' });
const child3 = await store.article.addChild(googleAuthId, father.sku, { value: 'Grün' });
assert.equal(child1.article.sku, `${father.sku}-1`);
assert.equal(child2.article.sku, `${father.sku}-2`);
assert.equal(child3.article.sku, `${father.sku}-3`);
let list = await store.article.listChildren(googleAuthId, father.sku);
assert.equal(list.length, 3);
assert.deepEqual(list.map((c) => c.fathersort), [0, 1, 2]);
console.log('2. Testing Duplicate Variant Protection (Defect 3)...');
await assert.rejects(
async () => {
await store.article.addChild(googleAuthId, father.sku, { value: 'Rot' });
},
(err) => {
assert.match(err.message, /Variant with this attribute value already exists/);
return true;
},
'Expected duplicate variant attribute value to be rejected'
);
console.log('3. Unlink middle child (child2 / SKU-2) -> leaves orphan holding SKU-2 (Bug A & B)...');
await store.article.removeChild(googleAuthId, father.sku, child2.article.sku, {
deleteArticle: false,
});
// Check that remaining children's fathersort was recompacted
list = await store.article.listChildren(googleAuthId, father.sku);
assert.equal(list.length, 2);
assert.deepEqual(list.map((c) => c.sku), [child1.article.sku, child3.article.sku]);
assert.deepEqual(list.map((c) => c.fathersort), [0, 1]);
// Check orphan still exists in DB
const orphan = await store.article.getBySku(googleAuthId, child2.article.sku);
assert.equal(orphan.article.fatherArticleId, null);
assert.equal(orphan.article.sku, `${father.sku}-2`);
console.log('4. Add new child (Gelb) -> SKU must NOT collide with orphan (Bug A: next free index)...');
const child4 = await store.article.addChild(googleAuthId, father.sku, { value: 'Gelb' });
// Since SKU-1, SKU-2 (orphan), SKU-3 exist, next free index is 4
assert.equal(child4.article.sku, `${father.sku}-4`);
list = await store.article.listChildren(googleAuthId, father.sku);
assert.equal(list.length, 3);
assert.deepEqual(list.map((c) => c.fathersort), [0, 1, 2]);
console.log('5. Testing gaps in fathersort (Evidence: father with child at fathersort: 1)...');
// Unlink child3 and child4 so only child1 remains
await store.article.removeChild(googleAuthId, father.sku, child3.article.sku, { deleteArticle: true });
await store.article.removeChild(googleAuthId, father.sku, child4.article.sku, { deleteArticle: true });
// Manually force child1's fathersort: 1 to simulate pre-existing gap state (like father 94)
await provider.query(
`UPDATE article SET fathersort = 1 WHERE sku = @sku`,
{ sku: child1.article.sku }
);
list = await store.article.listChildren(googleAuthId, father.sku);
assert.equal(list.length, 1);
assert.equal(list[0].fathersort, 1); // exactly like father 94
// Now adding a variant MUST NOT hit UNIQUE constraint failed (409)
console.log('Adding variant to father with child at fathersort: 1...');
const child5 = await store.article.addChild(googleAuthId, father.sku, { value: 'Schwarz' });
assert.ok(child5);
list = await store.article.listChildren(googleAuthId, father.sku);
assert.equal(list.length, 2);
// Fathersort should have picked 0 (next free non-negative integer)
assert.deepEqual(list.map((c) => c.fathersort), [0, 1]);
console.log('✅ ALL VARIANT FLOW TESTS PASSED');
} finally {
try {
rmSync(tempDir, { recursive: true, force: true });
} catch {}
}
}
run().catch((err) => {
console.error('Test failed:', err);
process.exit(1);
});