feat: establish auth listing and image vertical slices

Includes MFA-capable authentication, session and CSRF security, password recovery, listing lifecycle and ownership rules, inventory image metadata, local ImageStorage, migrations, tests, and implementation status documentation. Verification: npm test (62 passing) and integration suite (14 passing).
This commit is contained in:
2026-09-08 17:32:03 -04:00
commit c728016dec
128 changed files with 9788 additions and 0 deletions
@@ -0,0 +1,158 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { createListingService } from '../../src/modules/listings/services/listing-service.js';
test('creates a draft listing only after the listing publish requirements are met', async () => {
const created = [];
const service = createListingService({
listingRepository: {
async create(listing) {
const record = { id: 'listing-1', ...listing };
created.push(record);
return record;
},
},
inventoryImageRepository: {
async countByInventoryItemId() {
return 2;
},
},
});
const listing = await service.create({
inventoryItemId: 'inventory-item-id',
productName: ' Charizard ex ',
categoryId: 'category-id',
conditionId: 'condition-id',
askingPriceMinor: 2499,
imageCount: 2,
notes: ' Near mint ',
});
assert.deepEqual(listing, {
id: 'listing-1',
inventoryItemId: 'inventory-item-id',
productName: 'Charizard ex',
categoryId: 'category-id',
conditionId: 'condition-id',
askingPriceMinor: 2499,
imageCount: 2,
status: 'DRAFT',
notes: 'Near mint',
});
await assert.rejects(
() =>
service.create({
inventoryItemId: 'inventory-item-id',
productName: 'Charizard ex',
categoryId: 'category-id',
conditionId: 'condition-id',
askingPriceMinor: 0,
imageCount: 2,
}),
(error) => error.code === 'LISTING_REQUIREMENTS_NOT_MET',
);
assert.equal(created.length, 1);
});
test('publishes an owned draft only when its persisted requirements are met', async () => {
const updated = [];
const service = createListingService({
listingRepository: {
async findByIdForSeller() {
return {
id: 'listing-1',
inventoryItemId: 'inventory-item-id',
productName: 'Charizard ex',
categoryId: 'category-id',
conditionId: 'condition-id',
askingPriceMinor: 2499,
imageCount: 2,
status: 'DRAFT',
};
},
async updateStatus(id, status) {
updated.push({ id, status });
return { id, status };
},
},
});
assert.deepEqual(
await service.transition({
listingId: 'listing-1',
sellerId: 'seller-1',
nextStatus: 'AVAILABLE',
}),
{ id: 'listing-1', status: 'AVAILABLE' },
);
assert.deepEqual(updated, [{ id: 'listing-1', status: 'AVAILABLE' }]);
});
test('does not transition a listing owned by another seller', async () => {
const service = createListingService({
listingRepository: {
async findByIdForSeller() {
return null;
},
},
});
await assert.rejects(
() => service.transition({ listingId: 'listing-1', sellerId: 'seller-2', nextStatus: 'AVAILABLE' }),
(error) => error.code === 'LISTING_NOT_FOUND' && error.statusCode === 404,
);
});
test('does not publish a listing for sold or archived inventory', async () => {
for (const inventoryStatus of ['SOLD', 'ARCHIVED']) {
const service = createListingService({
listingRepository: {
async findByIdForSeller() {
return {
id: 'listing-1',
inventoryItemId: 'inventory-item-id',
productName: 'Charizard ex',
categoryId: 'category-id',
conditionId: 'condition-id',
askingPriceMinor: 2499,
imageCount: 2,
inventoryStatus,
status: 'DRAFT',
};
},
async updateStatus() {
throw new Error('A blocked listing must not be updated.');
},
},
});
await assert.rejects(
() => service.transition({ listingId: 'listing-1', sellerId: 'seller-1', nextStatus: 'AVAILABLE' }),
(error) => error.code === 'LISTING_INVENTORY_UNAVAILABLE' && error.statusCode === 409,
);
}
});
test('lists and retrieves only seller-owned listings', async () => {
const ownedListing = { id: 'listing-1', status: 'DRAFT' };
const service = createListingService({
listingRepository: {
async listBySellerId(sellerId) {
assert.equal(sellerId, 'seller-1');
return [ownedListing];
},
async findByIdForSeller(listingId, sellerId) {
assert.equal(listingId, 'listing-1');
assert.equal(sellerId, 'seller-1');
return ownedListing;
},
},
});
assert.deepEqual(await service.list({ sellerId: 'seller-1' }), [ownedListing]);
assert.deepEqual(await service.get({ listingId: 'listing-1', sellerId: 'seller-1' }), ownedListing);
});