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, inventoryImages, imageDerivativeJobs, listings, mfaMethods, passwordResetTokens, products, securityAuditEvents, sellers, sessions, users, } from '../../src/infrastructure/database/schema.js'; import { createSessionRepository } from '../../src/modules/auth/repositories/session-repository.js'; import { createSecurityAuditRepository } from '../../src/modules/auth/repositories/security-audit-repository.js'; import { createUserRepository } from '../../src/modules/auth/repositories/user-repository.js'; import { createPasswordResetRepository } from '../../src/modules/auth/repositories/password-reset-repository.js'; import { createMfaMethodRepository } from '../../src/modules/auth/repositories/mfa-method-repository.js'; import { createInventoryItemRepository } from '../../src/modules/inventory/repositories/inventory-item-repository.js'; import { createProductRepository } from '../../src/modules/products/repositories/product-repository.js'; import { createImageDerivativeJobRepository } from '../../src/modules/inventory/repositories/image-derivative-job-repository.js'; import { createSellerRepository } from '../../src/modules/sellers/repositories/seller-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 productRepository = createProductRepository(database.db); const imageDerivativeJobRepository = createImageDerivativeJobRepository(database.db); const sellerRepository = createSellerRepository(database.db); const inventoryItemRepository = createInventoryItemRepository(database.db); const sessionRepository = createSessionRepository(database.db); const passwordResetRepository = createPasswordResetRepository(database.db); const mfaMethodRepository = createMfaMethodRepository(database.db); const securityAuditRepository = createSecurityAuditRepository(database.db); const userRepository = createUserRepository(database.db); async function createCatalogPrerequisites() { const [user] = await database.db .insert(users) .values({ email: 'admin@example.test', passwordHash: 'not-a-real-password' }) .returning(); const [seller] = await database.db .insert(sellers) .values({ userId: user.id, displayName: 'Catalog 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(); return { user, seller, category, condition }; } beforeEach(async () => { await database.db.execute( sql`TRUNCATE image_derivative_jobs, inventory_images, security_audit_events, password_reset_tokens, mfa_methods, sessions, inventory_items, products, conditions, condition_profiles, categories, sellers, users CASCADE`, ); }); after(async () => { await database.close(); }); test('persists distinct products and physical inventory copies', async () => { const { seller, category, condition } = await createCatalogPrerequisites(); const product = await productRepository.create({ sellerId: seller.id, categoryId: category.id, name: 'Charizard ex', slug: 'charizard-ex-sv151-199', setName: 'Scarlet & Violet 151', cardNumber: '199/165', }); const inventoryItem = await inventoryItemRepository.create({ productId: product.id, conditionId: condition.id, notes: 'Clean front and back.', }); assert.equal(inventoryItem.productId, product.id); assert.equal(inventoryItem.status, 'ACTIVE'); assert.equal( (await productRepository.listBySellerId(seller.id)).map(({ id }) => id).join(), product.id, ); assert.equal( (await inventoryItemRepository.listByProductId(product.id)).length, 1, ); }); test('enforces seller-scoped product slugs', async () => { const { seller, category } = await createCatalogPrerequisites(); const product = { sellerId: seller.id, categoryId: category.id, name: 'Pikachu Plush', slug: 'pikachu-plush', }; await productRepository.create(product); await assert.rejects( () => productRepository.create(product), (error) => error.cause?.code === '23505', ); }); test('finds a product by both its seller and slug', async () => { const { seller, category } = await createCatalogPrerequisites(); const product = await productRepository.create({ sellerId: seller.id, categoryId: category.id, name: 'Pikachu Plush', slug: 'pikachu-plush', }); const [otherUser] = await database.db .insert(users) .values({ email: 'other-admin@example.test', passwordHash: 'not-a-real-password' }) .returning(); const [otherSeller] = await database.db .insert(sellers) .values({ userId: otherUser.id, displayName: 'Other Catalog Owner' }) .returning(); assert.equal( (await productRepository.findBySellerIdAndSlug(seller.id, product.slug))?.id, product.id, ); assert.equal( await productRepository.findBySellerIdAndSlug(otherSeller.id, product.slug), null, ); }); test('finds only products with active inventory and a public listing', async () => { const { seller, category, condition } = await createCatalogPrerequisites(); const product = await productRepository.create({ sellerId: seller.id, categoryId: category.id, name: 'Public Pikachu Plush', slug: 'public-pikachu-plush', }); const inventoryItem = await inventoryItemRepository.create({ productId: product.id, conditionId: condition.id, }); await database.db.insert(listings).values({ inventoryItemId: inventoryItem.id, productName: product.name, categoryId: category.id, conditionId: condition.id, askingPriceMinor: 2499, status: 'PENDING', }); assert.equal( (await productRepository.findPublicBySlug(product.slug))?.id, product.id, ); await database.db .update(listings) .set({ status: 'CLOSED' }) .where(sql`inventory_item_id = ${inventoryItem.id}`); assert.equal(await productRepository.findPublicBySlug(product.slug), null); }); test('lists only sellers with publicly visible inventory', async () => { const { seller, category, condition } = await createCatalogPrerequisites(); const [product] = await database.db.insert(products).values({ sellerId: seller.id, categoryId: category.id, name: 'Public Seller Product', slug: 'public-seller-product', }).returning(); const [inventoryItem] = await database.db.insert(inventoryItems).values({ productId: product.id, conditionId: condition.id, }).returning(); await database.db.insert(listings).values({ inventoryItemId: inventoryItem.id, productName: product.name, categoryId: category.id, conditionId: condition.id, askingPriceMinor: 1999, status: 'AVAILABLE', }); assert.deepEqual((await sellerRepository.listPublic()).map(({ id }) => id), [seller.id]); await database.db.update(listings).set({ status: 'CLOSED' }); assert.deepEqual(await sellerRepository.listPublic(), []); }); test('claims and completes image derivative jobs while updating image processing status', async () => { const { seller, category, condition } = await createCatalogPrerequisites(); const [product] = await database.db.insert(products).values({ sellerId: seller.id, categoryId: category.id, name: 'Derivative Job Product', slug: 'derivative-job-product', }).returning(); const [inventoryItem] = await database.db.insert(inventoryItems).values({ productId: product.id, conditionId: condition.id, }).returning(); const [image] = await database.db.insert(inventoryImages).values({ inventoryItemId: inventoryItem.id, storageKey: 'inventory/derivative-source.jpg', originalFilename: 'source.jpg', mimeType: 'image/jpeg', fileSize: 4, width: 100, height: 100, }).returning(); const [job] = await database.db.insert(imageDerivativeJobs).values({ inventoryImageId: image.id, sourceKey: image.storageKey, }).returning(); const claimed = await imageDerivativeJobRepository.claimNext(); assert.equal(claimed.id, job.id); assert.equal(claimed.status, 'PROCESSING'); const [processingImage] = await database.db.select().from(inventoryImages).where(sql`id = ${image.id}`); assert.equal(processingImage.processingStatus, 'PROCESSING'); const completed = await imageDerivativeJobRepository.complete(job.id, [{ name: 'thumbnail', key: 'thumb.webp' }]); assert.equal(completed.status, 'COMPLETED'); const [completedImage] = await database.db.select().from(inventoryImages).where(sql`id = ${image.id}`); assert.equal(completedImage.processingStatus, 'COMPLETED'); }); test('retrieves only non-revoked, non-expired sessions', async () => { const { user, seller } = await createCatalogPrerequisites(); const tokenHash = 'a hashed session token'; const expiresAt = new Date('2026-09-08T00:00:00.000Z'); const createdSession = await sessionRepository.create({ userId: user.id, sessionTokenHash: tokenHash, csrfTokenHash: 'a hashed CSRF token', expiresAt, }); const activeSession = await sessionRepository.findActiveAuthenticationSession( tokenHash, new Date('2026-09-07T00:00:00.000Z'), ); assert.equal(activeSession.id, createdSession.id); assert.equal(activeSession.userId, user.id); assert.equal(activeSession.role, 'ADMIN'); assert.equal(activeSession.sellerId, seller.id); assert.equal(activeSession.csrfTokenHash, 'a hashed CSRF token'); assert.ok(activeSession.lastActivityAt instanceof Date); await sessionRepository.revokeByTokenHash( tokenHash, new Date('2026-09-07T01:00:00.000Z'), ); assert.equal( await sessionRepository.findActiveAuthenticationSession( tokenHash, new Date('2026-09-07T01:00:00.000Z'), ), null, ); }); test('provisions only one initial administrator with a linked seller', async () => { const createdAdmin = await userRepository.createInitialAdmin({ email: 'admin@example.test', passwordHash: 'scrypt$test-hash', sellerName: 'Catalog Owner', }); assert.equal(createdAdmin.user.email, 'admin@example.test'); assert.equal(createdAdmin.seller.userId, createdAdmin.user.id); assert.equal( await userRepository.createInitialAdmin({ email: 'other@example.test', passwordHash: 'scrypt$test-hash', sellerName: 'Other Owner', }), null, ); }); test('lists active sessions and revokes all except the current session', async () => { const { user } = await createCatalogPrerequisites(); const expiresAt = new Date('2026-09-08T00:00:00.000Z'); const currentSession = await sessionRepository.create({ userId: user.id, sessionTokenHash: 'current session hash', csrfTokenHash: 'current CSRF hash', expiresAt, }); const otherSession = await sessionRepository.create({ userId: user.id, sessionTokenHash: 'other session hash', csrfTokenHash: 'other CSRF hash', expiresAt, }); assert.deepEqual( (await sessionRepository.listActiveByUserId( user.id, new Date('2026-09-07T00:00:00.000Z'), )) .map((session) => session.id) .sort(), [currentSession.id, otherSession.id].sort(), ); await sessionRepository.revokeOtherByUserId( user.id, currentSession.id, new Date('2026-09-07T01:00:00.000Z'), ); assert.deepEqual( ( await sessionRepository.listActiveByUserId( user.id, new Date('2026-09-07T01:00:00.000Z'), ) ).map((session) => session.id), [currentSession.id], ); }); test('records private authentication audit events without credentials', async () => { const { user } = await createCatalogPrerequisites(); const event = await securityAuditRepository.record({ userId: user.id, event: 'LOGIN_SUCCESS', ipAddress: '198.51.100.8', userAgent: 'Test Browser', }); const [storedEvent] = await database.db .select() .from(securityAuditEvents) .where(sql`${securityAuditEvents.id} = ${event.id}`); assert.equal(storedEvent.userId, user.id); assert.equal(storedEvent.event, 'LOGIN_SUCCESS'); assert.equal(storedEvent.ipAddress, '198.51.100.8'); assert.ok(storedEvent.createdAt instanceof Date); assert.deepEqual(storedEvent.metadata, {}); assert.equal('sessionTokenHash' in storedEvent, false); assert.equal('passwordHash' in storedEvent, false); }); test('consumes a password reset token once, replaces the password, and revokes sessions', async () => { const { user } = await createCatalogPrerequisites(); const now = new Date('2026-09-07T00:00:00.000Z'); const tokenHash = 'reset token hash'; await passwordResetRepository.issue({ userId: user.id, tokenHash, expiresAt: new Date('2026-09-07T01:00:00.000Z'), }); await sessionRepository.create({ userId: user.id, sessionTokenHash: 'active session hash', csrfTokenHash: 'active CSRF hash', expiresAt: new Date('2026-09-08T00:00:00.000Z'), }); assert.equal( await passwordResetRepository.consume({ tokenHash, passwordHash: 'replacement password hash', now, }), user.id, ); assert.equal( await passwordResetRepository.consume({ tokenHash, passwordHash: 'another password hash', now, }), null, ); const updatedUser = await userRepository.findAuthenticationUserByEmail('admin@example.test'); const [resetToken] = await database.db .select() .from(passwordResetTokens) .where(sql`${passwordResetTokens.tokenHash} = ${tokenHash}`); assert.equal(updatedUser.passwordHash, 'replacement password hash'); assert.ok(resetToken.usedAt instanceof Date); assert.equal( await sessionRepository.findActiveAuthenticationSession( 'active session hash', now, ), null, ); }); test('persists, enables, and disables an owned MFA method', async () => { const { user } = await createCatalogPrerequisites(); const method = await mfaMethodRepository.create({ userId: user.id, type: 'TOTP', name: 'Authenticator', secretCiphertext: 'encrypted-secret', }); assert.equal(method.isEnabled, false); assert.equal( ( await mfaMethodRepository.findActiveByIdAndUserId(method.id, user.id) ).secretCiphertext, 'encrypted-secret', ); const enabledMethod = await mfaMethodRepository.enable( method.id, new Date('2026-09-07T00:00:00.000Z'), ); const disabledMethod = await mfaMethodRepository.disable( method.id, new Date('2026-09-07T01:00:00.000Z'), ); assert.equal(enabledMethod.isEnabled, true); assert.equal(disabledMethod.isEnabled, false); assert.equal( await mfaMethodRepository.findActiveByIdAndUserId(method.id, user.id), null, ); assert.equal((await database.db.select().from(mfaMethods)).length, 1); });