import { describe, it, expect, beforeEach } from 'vitest';
import { closeDb, getDb } from '../../src/storage/index.js';
import {
handleCreateItemTemplate,
handleGiveItem,
handleTransferItem,
handleGetInventory
} from '../../src/server/inventory-tools.js';
import { CharacterRepository } from '../../src/storage/repos/character.repo.js';
const mockCtx = { sessionId: 'test-session' };
/**
* Inventory Exploit Tests
* Testing for item duplication, quantity exploits, and unique item violations
*/
describe('Inventory Security', () => {
let charRepo: CharacterRepository;
beforeEach(() => {
closeDb();
const db = getDb(':memory:');
charRepo = new CharacterRepository(db);
// Create test characters
const now = new Date().toISOString();
charRepo.create({
id: 'char-a',
name: 'Character A',
stats: { str: 10, dex: 10, con: 10, int: 10, wis: 10, cha: 10 },
hp: 20,
maxHp: 20,
ac: 10,
level: 1,
characterType: 'pc',
createdAt: now,
updatedAt: now
});
charRepo.create({
id: 'char-b',
name: 'Character B',
stats: { str: 10, dex: 10, con: 10, int: 10, wis: 10, cha: 10 },
hp: 20,
maxHp: 20,
ac: 10,
level: 1,
characterType: 'pc',
createdAt: now,
updatedAt: now
});
});
describe('Item Duplication Bug', () => {
it('should NOT duplicate items when transferring A->B->A', async () => {
// Create a unique diamond
const createResult = await handleCreateItemTemplate({
name: 'Diamond of Infinite Value',
type: 'misc',
value: 10000,
weight: 0.1,
properties: { unique: true }
}, mockCtx);
const item = JSON.parse(createResult.content[0].text);
const itemId = item.id;
// Give to Character A (quantity 1)
await handleGiveItem({
characterId: 'char-a',
itemId,
quantity: 1
}, mockCtx);
// Verify A has 1
let invA = await handleGetInventory({ characterId: 'char-a' }, mockCtx);
let invAData = JSON.parse(invA.content[0].text);
expect(invAData.items.find((i: any) => i.itemId === itemId)?.quantity).toBe(1);
// Transfer A -> B
await handleTransferItem({
fromCharacterId: 'char-a',
toCharacterId: 'char-b',
itemId,
quantity: 1
}, mockCtx);
// Verify A has 0, B has 1
invA = await handleGetInventory({ characterId: 'char-a' }, mockCtx);
invAData = JSON.parse(invA.content[0].text);
let invB = await handleGetInventory({ characterId: 'char-b' }, mockCtx);
let invBData = JSON.parse(invB.content[0].text);
expect(invAData.items.find((i: any) => i.itemId === itemId)).toBeUndefined();
expect(invBData.items.find((i: any) => i.itemId === itemId)?.quantity).toBe(1);
// Transfer B -> A
await handleTransferItem({
fromCharacterId: 'char-b',
toCharacterId: 'char-a',
itemId,
quantity: 1
}, mockCtx);
// Verify A has 1, B has 0 (NOT 2 in A!)
invA = await handleGetInventory({ characterId: 'char-a' }, mockCtx);
invAData = JSON.parse(invA.content[0].text);
invB = await handleGetInventory({ characterId: 'char-b' }, mockCtx);
invBData = JSON.parse(invB.content[0].text);
const itemInA = invAData.items.find((i: any) => i.itemId === itemId);
const itemInB = invBData.items.find((i: any) => i.itemId === itemId);
// THE BUG: This should be 1, not 2
expect(itemInA?.quantity).toBe(1);
expect(itemInB).toBeUndefined();
// Total items in existence should be exactly 1
const totalQuantity = (itemInA?.quantity || 0) + (itemInB?.quantity || 0);
expect(totalQuantity).toBe(1);
});
it('should NOT allow transferring more items than owned', async () => {
// Create item and give 5 to A
const createResult = await handleCreateItemTemplate({
name: 'Gold Coin',
type: 'misc',
value: 1
}, mockCtx);
const item = JSON.parse(createResult.content[0].text);
const itemId = item.id;
await handleGiveItem({
characterId: 'char-a',
itemId,
quantity: 5
}, mockCtx);
// Try to transfer 10 (more than owned)
await expect(
handleTransferItem({
fromCharacterId: 'char-a',
toCharacterId: 'char-b',
itemId,
quantity: 10
}, mockCtx)
).rejects.toThrow();
// Verify A still has 5
const invA = await handleGetInventory({ characterId: 'char-a' }, mockCtx);
const invAData = JSON.parse(invA.content[0].text);
expect(invAData.items.find((i: any) => i.itemId === itemId)?.quantity).toBe(5);
});
});
describe('Quantity Limits', () => {
it('should reject absurd quantities', async () => {
const createResult = await handleCreateItemTemplate({
name: 'Copper Penny',
type: 'misc',
value: 1
}, mockCtx);
const item = JSON.parse(createResult.content[0].text);
const itemId = item.id;
// Try to add 999,999 items (should be rejected)
await expect(
handleGiveItem({
characterId: 'char-a',
itemId,
quantity: 999999
}, mockCtx)
).rejects.toThrow(/quantity/i);
});
it('should enforce maximum stack size', async () => {
const createResult = await handleCreateItemTemplate({
name: 'Arrow',
type: 'misc',
value: 1
}, mockCtx);
const item = JSON.parse(createResult.content[0].text);
const itemId = item.id;
// Add items twice - should cap at max
await handleGiveItem({
characterId: 'char-a',
itemId,
quantity: 100
}, mockCtx);
await handleGiveItem({
characterId: 'char-a',
itemId,
quantity: 100
}, mockCtx);
const inv = await handleGetInventory({ characterId: 'char-a' }, mockCtx);
const invData = JSON.parse(inv.content[0].text);
const arrows = invData.items.find((i: any) => i.itemId === itemId);
// Max stack should be reasonable (e.g., 9999)
expect(arrows.quantity).toBeLessThanOrEqual(9999);
});
});
describe('Unique Item Constraints', () => {
it('should enforce unique item limit of 1 per character', async () => {
const createResult = await handleCreateItemTemplate({
name: 'Legendary Sword',
type: 'weapon',
value: 50000,
properties: { unique: true }
}, mockCtx);
const item = JSON.parse(createResult.content[0].text);
const itemId = item.id;
// Give one to A
await handleGiveItem({
characterId: 'char-a',
itemId,
quantity: 1
}, mockCtx);
// Try to give another - should fail with error
await expect(
handleGiveItem({
characterId: 'char-a',
itemId,
quantity: 1
}, mockCtx)
).rejects.toThrow(/unique/i);
// Should still only have 1
const inv = await handleGetInventory({ characterId: 'char-a' }, mockCtx);
const invData = JSON.parse(inv.content[0].text);
expect(invData.items.find((i: any) => i.itemId === itemId)?.quantity).toBe(1);
});
it('should NOT allow multiple characters to own same unique item', async () => {
const createResult = await handleCreateItemTemplate({
name: 'One Ring',
type: 'misc',
value: 999999,
properties: { unique: true, worldUnique: true }
}, mockCtx);
const item = JSON.parse(createResult.content[0].text);
const itemId = item.id;
// Give to A
await handleGiveItem({
characterId: 'char-a',
itemId,
quantity: 1
}, mockCtx);
// Try to give to B (without transfer) - should fail
await expect(
handleGiveItem({
characterId: 'char-b',
itemId,
quantity: 1
}, mockCtx)
).rejects.toThrow(/unique/i);
});
});
describe('Value Limits', () => {
it('should reject items with absurd values', async () => {
await expect(
handleCreateItemTemplate({
name: 'Economy Breaker',
type: 'misc',
value: 999999999999 // Trillion gold
}, mockCtx)
).rejects.toThrow(/value/i);
});
});
describe('Inventory Capacity', () => {
it('should enforce inventory weight limits', async () => {
const createResult = await handleCreateItemTemplate({
name: 'Heavy Boulder',
type: 'misc',
value: 1,
weight: 100
}, mockCtx);
const item = JSON.parse(createResult.content[0].text);
const itemId = item.id;
// Give first boulder (100 weight)
await handleGiveItem({
characterId: 'char-a',
itemId,
quantity: 1
}, mockCtx);
// Try to give second boulder (would be 200 weight, over 100 capacity)
await expect(
handleGiveItem({
characterId: 'char-a',
itemId,
quantity: 1
}, mockCtx)
).rejects.toThrow(/capacity|weight/i);
});
});
});