import assert from 'node:assert/strict'; import test, { after, beforeEach } from 'node:test'; import { sql } from 'drizzle-orm'; import { createDatabase } from '../../src/infrastructure/database/client.js'; import { categories, conditionProfiles, conditions, inventoryItems, listings, products, sellers, sellerSettings, users, } from '../../src/infrastructure/database/schema.js'; import { createInventoryImageRepository } from '../../src/modules/inventory/repositories/inventory-image-repository.js'; import { createInventoryItemRepository } from '../../src/modules/inventory/repositories/inventory-item-repository.js'; import { createListingRepository } from '../../src/modules/listings/repositories/listing-repository.js'; const connectionString = process.env.TEST_DATABASE_URL; if (!connectionString) { throw new Error('TEST_DATABASE_URL is required to run integration tests.'); } if (!new URL(connectionString).pathname.endsWith('_test')) { throw new Error('TEST_DATABASE_URL must reference a database whose name ends in "_test".'); } const database = createDatabase(connectionString); const listingRepository = createListingRepository(database.db); const inventoryItemRepository = createInventoryItemRepository(database.db); const inventoryImageRepository = createInventoryImageRepository(database.db); async function createInventoryFixture() { const [user] = await database.db .insert(users) .values({ email: 'listing-admin@example.test', passwordHash: 'hashed-password' }) .returning(); const [seller] = await database.db .insert(sellers) .values({ userId: user.id, displayName: 'Listing Owner' }) .returning(); const [category] = await database.db .insert(categories) .values({ name: 'Trading Cards', slug: 'trading-cards' }) .returning(); const [conditionProfile] = await database.db .insert(conditionProfiles) .values({ categoryId: category.id, name: 'Card' }) .returning(); const [condition] = await database.db .insert(conditions) .values({ conditionProfileId: conditionProfile.id, name: 'Near Mint', code: 'NM', qualityRank: 1, }) .returning(); const [product] = await database.db .insert(products) .values({ sellerId: seller.id, categoryId: category.id, name: 'Charizard ex', slug: 'charizard-ex-sv151-199', }) .returning(); const [inventoryItem] = await database.db .insert(inventoryItems) .values({ productId: product.id, conditionId: condition.id, notes: 'Original box included', }) .returning(); return { seller, category, condition, product, inventoryItem }; } beforeEach(async () => { await database.db.execute( sql`TRUNCATE listings, inventory_items, products, conditions, condition_profiles, categories, sellers, users CASCADE`, ); }); after(async () => { await database.close(); }); test('persists a draft listing for a physical inventory item', async () => { const { category, condition, inventoryItem } = await createInventoryFixture(); const listing = await listingRepository.create({ inventoryItemId: inventoryItem.id, productName: 'Charizard ex', categoryId: category.id, conditionId: condition.id, askingPriceMinor: 2499, status: 'DRAFT', notes: 'Near mint', }); assert.equal(listing.inventoryItemId, inventoryItem.id); assert.equal(listing.status, 'DRAFT'); assert.equal(listing.askingPriceMinor, 2499); assert.ok(listing.id); }); test('lists the most recent draft entries for an inventory item', async () => { const { category, condition, inventoryItem } = await createInventoryFixture(); await listingRepository.create({ inventoryItemId: inventoryItem.id, productName: 'Charizard ex', categoryId: category.id, conditionId: condition.id, askingPriceMinor: 2499, status: 'DRAFT', }); const listingsForItem = await listingRepository.listByInventoryItemId(inventoryItem.id); assert.equal(listingsForItem.length, 1); assert.equal(listingsForItem[0].inventoryItemId, inventoryItem.id); }); test('searches public listings with pagination and hides sold items by default', async () => { const { category, condition, inventoryItem, product } = await createInventoryFixture(); await listingRepository.create({ inventoryItemId: inventoryItem.id, productName: product.name, categoryId: category.id, conditionId: condition.id, askingPriceMinor: 2499, status: 'PENDING', }); const visible = await listingRepository.searchPublic({ page: 1, limit: 10 }); assert.equal(visible.total, 1); assert.equal(visible.items.length, 1); assert.equal(visible.items[0].status, 'PENDING'); assert.equal( (await listingRepository.findPublicById(visible.items[0].id))?.id, visible.items[0].id, ); assert.equal((await listingRepository.findPublicById(visible.items[0].id)).sellerDescription, null); await database.db.insert(sellerSettings).values({ sellerId: product.sellerId, settings: { general: { publicDescription: 'Collector.' }, security: { sessionIdleMinutes: 60 } } }); assert.equal((await listingRepository.findPublicById(visible.items[0].id)).sellerDescription, 'Collector.'); await database.db.update(sellerSettings).set({ settings: { general: { publicDescription: '' } } }); assert.equal((await listingRepository.findPublicById(visible.items[0].id)).sellerDescription, null); await database.db .update(listings) .set({ status: 'CLOSED' }) .where(sql`inventory_item_id = ${inventoryItem.id}`); await database.db .update(inventoryItems) .set({ status: 'SOLD' }) .where(sql`id = ${inventoryItem.id}`); const hidden = await listingRepository.searchPublic({ page: 1, limit: 10 }); assert.equal(hidden.total, 0); assert.equal(await listingRepository.findPublicById(visible.items[0].id), null); const included = await listingRepository.searchPublic({ page: 1, limit: 10, includeSold: true, }); assert.equal(included.total, 1); assert.equal(included.items[0].status, 'CLOSED'); }); test('searches public inventory with product and listing fields', async () => { const { category, condition, inventoryItem, product } = await createInventoryFixture(); await listingRepository.create({ inventoryItemId: inventoryItem.id, productName: product.name, categoryId: category.id, conditionId: condition.id, askingPriceMinor: 2499, status: 'AVAILABLE', }); const visible = await inventoryItemRepository.searchPublic({ page: 1, limit: 10 }); assert.equal(visible.total, 1); assert.equal(visible.items[0].productSlug, product.slug); assert.equal(visible.items[0].askingPriceMinor, 2499); assert.equal((await inventoryItemRepository.findPublicById(inventoryItem.id)).id, inventoryItem.id); await database.db.update(inventoryItems).set({ status: 'SOLD' }).where(sql`id = ${inventoryItem.id}`); assert.equal(await inventoryItemRepository.findPublicById(inventoryItem.id), null); }); test('persists image metadata only for the owning seller and counts item images', async () => { const { seller, inventoryItem } = await createInventoryFixture(); const image = await inventoryImageRepository.createForSeller({ inventoryItemId: inventoryItem.id, storageKey: 'inventory/front.jpg', originalFilename: 'front.jpg', mimeType: 'image/jpeg', fileSize: 12000, width: 1200, height: 900, altText: 'Front image', sortOrder: 0, isPrimary: true, }, seller.id); assert.equal(image.processingStatus, 'PENDING'); assert.equal(await inventoryImageRepository.countByInventoryItemId(inventoryItem.id), 1); assert.equal( await inventoryImageRepository.createForSeller({ inventoryItemId: inventoryItem.id, storageKey: 'inventory/other.jpg', originalFilename: 'other.jpg', mimeType: 'image/jpeg', fileSize: 12000, width: 1200, height: 900, }, '00000000-0000-0000-0000-000000000000'), null, ); }); test('reads and updates listings only through the owning seller scope', async () => { const { seller, category, condition, inventoryItem } = await createInventoryFixture(); const createdListing = await listingRepository.create({ inventoryItemId: inventoryItem.id, productName: 'Charizard ex', categoryId: category.id, conditionId: condition.id, askingPriceMinor: 2499, status: 'DRAFT', }); const ownedListing = await listingRepository.findByIdForSeller(createdListing.id, seller.id); const sellerListings = await listingRepository.listBySellerId(seller.id); assert.equal(ownedListing.id, createdListing.id); assert.equal(ownedListing.inventoryStatus, 'ACTIVE'); assert.equal(sellerListings.length, 1); assert.equal( await listingRepository.findByIdForSeller( createdListing.id, '00000000-0000-0000-0000-000000000000', ), null, ); const archivedListing = await listingRepository.updateStatus(createdListing.id, 'ARCHIVED'); assert.equal(archivedListing.status, 'ARCHIVED'); });