u
This commit is contained in:
175
scripts/test-categoryproperty-and-attrvalue.js
Normal file
175
scripts/test-categoryproperty-and-attrvalue.js
Normal 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);
|
||||
});
|
||||
121
scripts/test-history-and-attrvalue.js
Normal file
121
scripts/test-history-and-attrvalue.js
Normal 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);
|
||||
});
|
||||
@@ -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
210
scripts/test-tombstone.js
Normal 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);
|
||||
});
|
||||
147
scripts/test-unique-slugs.js
Normal file
147
scripts/test-unique-slugs.js
Normal 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);
|
||||
});
|
||||
130
scripts/test-variant-flow.js
Normal file
130
scripts/test-variant-flow.js
Normal 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);
|
||||
});
|
||||
Reference in New Issue
Block a user