Files
Inventory-Manager-CC/web/server/__tests__/db.test.js

251 lines
7.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Tests for the Inventory Manager database layer (db.js).
*
* Uses a temporary on-disk SQLite file per test to exercise real SQL
* without touching production data. The DB_PATH env var is set BEFORE
* the dynamic import so better-sqlite3 opens the right file.
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import fs from 'fs';
import os from 'os';
import path from 'path';
// ── helpers ────────────────────────────────────────────────────────────
/** Create a fresh temp DB path and set the env var before importing db.js */
async function createTestDb() {
const tmp = path.join(os.tmpdir(), `inv-test-${Date.now()}-${Math.random().toString(36).slice(2)}.db`);
process.env.DB_PATH = tmp;
// Use a cache-buster query string so Node gives us a fresh module
const mod = await import(`../db.js?t=${Date.now()}-${Math.random()}`);
return { mod, tmp };
}
function cleanup(tmp) {
try { fs.unlinkSync(tmp); } catch { /* ignore */ }
try { fs.unlinkSync(tmp + '-wal'); } catch { /* ignore */ }
try { fs.unlinkSync(tmp + '-shm'); } catch { /* ignore */ }
}
// ── tests ──────────────────────────────────────────────────────────────
describe('db item persistence', () => {
let db, tmp;
beforeEach(async () => {
({ mod: db, tmp } = await createTestDb());
});
afterEach(() => {
try { db.closeDb(); } catch { /* already closed */ }
cleanup(tmp);
});
it('saveItems + loadItems round-trips item data', () => {
const items = [
{ name: 'minecraft:diamond', count: 64, displayName: 'Diamond' },
{ name: 'minecraft:iron_ingot', count: 128, displayName: 'Iron Ingot' },
];
db.saveItems(items);
const loaded = db.loadItems();
expect(loaded).toHaveLength(2);
// loadItems returns ORDER BY count DESC
expect(loaded[0].name).toBe('minecraft:iron_ingot');
expect(loaded[0].count).toBe(128);
expect(loaded[1].name).toBe('minecraft:diamond');
expect(loaded[1].count).toBe(64);
});
it('saveItems upserts — updates existing rows instead of duplicating', () => {
db.saveItems([{ name: 'minecraft:diamond', count: 10, displayName: 'Diamond' }]);
db.saveItems([{ name: 'minecraft:diamond', count: 99, displayName: 'Diamond' }]);
const loaded = db.loadItems();
expect(loaded).toHaveLength(1);
expect(loaded[0].count).toBe(99);
});
});
describe('db state key-value store', () => {
let db, tmp;
beforeEach(async () => {
({ mod: db, tmp } = await createTestDb());
});
afterEach(() => {
try { db.closeDb(); } catch { /* already closed */ }
cleanup(tmp);
});
it('saveState + loadState round-trips JSON', () => {
db.saveState('smeltingPaused', true);
expect(db.loadState('smeltingPaused')).toBe(true);
db.saveState('disabledRecipes', { 'minecraft:iron_ingot': true });
expect(db.loadState('disabledRecipes')).toEqual({ 'minecraft:iron_ingot': true });
});
it('loadState returns defaultValue when key missing', () => {
expect(db.loadState('nonexistent', 42)).toBe(42);
expect(db.loadState('nonexistent')).toBeNull();
});
});
describe('db furnace persistence', () => {
let db, tmp;
beforeEach(async () => {
({ mod: db, tmp } = await createTestDb());
});
afterEach(() => {
try { db.closeDb(); } catch { /* already closed */ }
cleanup(tmp);
});
it('saveFurnaces + loadFurnaces round-trips furnace status', () => {
const furnaces = {
'minecraft:furnace_0': {
active: true,
type: 'minecraft:furnace',
input: { name: 'minecraft:raw_iron', count: 8 },
fuel: { name: 'minecraft:coal', count: 3 },
output: null,
},
};
db.saveFurnaces(furnaces);
const loaded = db.loadFurnaces();
expect(loaded['minecraft:furnace_0']).toBeDefined();
expect(loaded['minecraft:furnace_0'].active).toBe(true);
expect(loaded['minecraft:furnace_0'].input.name).toBe('minecraft:raw_iron');
});
});
describe('db alert persistence', () => {
let db, tmp;
beforeEach(async () => {
({ mod: db, tmp } = await createTestDb());
});
afterEach(() => {
try { db.closeDb(); } catch { /* already closed */ }
cleanup(tmp);
});
it('saveAlerts + loadAlerts round-trips triggered alerts', () => {
db.saveAlerts([
{ item: 'minecraft:diamond', triggered: true, current: 5, threshold: 10 },
{ item: 'minecraft:coal', triggered: false, current: 200, threshold: 50 },
]);
const loaded = db.loadAlerts();
// loadAlerts only returns triggered=1
expect(loaded).toHaveLength(1);
expect(loaded[0].item).toBe('minecraft:diamond');
expect(loaded[0].triggered).toBe(true);
expect(loaded[0].current).toBe(5);
expect(loaded[0].threshold).toBe(10);
});
});
describe('db item history', () => {
let db, tmp;
beforeEach(async () => {
({ mod: db, tmp } = await createTestDb());
});
afterEach(() => {
try { db.closeDb(); } catch { /* already closed */ }
cleanup(tmp);
});
it('getHistory returns recorded snapshots ordered newest-first', () => {
// recordItemHistory is throttled (5-min internal cooldown), so we
// call it twice — first call records, second is throttled.
db.recordItemHistory([
{ name: 'minecraft:diamond', count: 64 },
{ name: 'minecraft:iron_ingot', count: 128 },
]);
const history = db.getHistory('minecraft:diamond', 10);
expect(history.length).toBeGreaterThanOrEqual(1);
expect(history[0].count).toBe(64);
});
it('getHistorySummary groups by timestamp', () => {
db.recordItemHistory([
{ name: 'a', count: 10 },
{ name: 'b', count: 20 },
]);
const summary = db.getHistorySummary();
expect(summary.length).toBeGreaterThanOrEqual(1);
expect(summary[0].total).toBe(30); // 10 + 20
});
});
describe('db full state round-trip', () => {
let db, tmp;
beforeEach(async () => {
({ mod: db, tmp } = await createTestDb());
});
afterEach(() => {
try { db.closeDb(); } catch { /* already closed */ }
cleanup(tmp);
});
it('saveFullState + flushPendingSave + loadFullState restores everything', () => {
const state = {
inventoryState: {
itemList: [
{ name: 'minecraft:diamond', count: 64, displayName: 'Diamond' },
],
grandTotal: 64,
chestCount: 2,
totalSlots: 54,
usedSlots: 1,
freeSlots: 53,
usedRatio: 0.02,
dropperOk: true,
barrelOk: false,
furnaceCount: 1,
furnaceStatus: {
'furnace_0': { active: false, type: 'minecraft:furnace', input: null, fuel: null, output: null },
},
droppers: ['dropper_0'],
},
activityState: { lastScan: 12345 },
alertsState: [{ item: 'minecraft:coal', triggered: true, current: 3, threshold: 10 }],
smeltingPaused: false,
disabledRecipes: { 'minecraft:charcoal': true },
smeltableRecipes: { 'minecraft:raw_iron': 'minecraft:iron_ingot' },
craftableRecipes: [{ output: 'minecraft:chest', count: 1, slots: { 1: 'minecraft:planks' } }],
craftTurtleOk: true,
};
db.saveFullState(state);
db.flushPendingSave(); // force the debounced write
const restored = db.loadFullState();
expect(restored.inventoryState.itemList).toHaveLength(1);
expect(restored.inventoryState.itemList[0].name).toBe('minecraft:diamond');
expect(restored.inventoryState.grandTotal).toBe(64);
expect(restored.smeltingPaused).toBe(false);
expect(restored.disabledRecipes).toEqual({ 'minecraft:charcoal': true });
expect(restored.craftTurtleOk).toBe(true);
expect(restored.alertsState).toHaveLength(1);
expect(restored.alertsState[0].item).toBe('minecraft:coal');
});
});