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:
@@ -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);
|
||||
});
|
||||
Reference in New Issue
Block a user