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).
159 lines
4.5 KiB
JavaScript
159 lines
4.5 KiB
JavaScript
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);
|
|
});
|