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,34 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import {
|
||||
assertInventoryItemEditable,
|
||||
INVENTORY_STATUS,
|
||||
transitionInventoryStatus,
|
||||
} from '../../src/modules/inventory/domain/inventory-item.js';
|
||||
|
||||
test('allows an active inventory item to be sold', () => {
|
||||
assert.equal(
|
||||
transitionInventoryStatus(INVENTORY_STATUS.ACTIVE, INVENTORY_STATUS.SOLD),
|
||||
INVENTORY_STATUS.SOLD,
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects reopening sold inventory', () => {
|
||||
assert.throws(
|
||||
() => transitionInventoryStatus(INVENTORY_STATUS.SOLD, INVENTORY_STATUS.ACTIVE),
|
||||
/cannot transition/,
|
||||
);
|
||||
});
|
||||
|
||||
test('locks sold and archived items against ordinary edits', () => {
|
||||
assert.throws(
|
||||
() => assertInventoryItemEditable(INVENTORY_STATUS.SOLD),
|
||||
/locked against ordinary edits/,
|
||||
);
|
||||
assert.throws(
|
||||
() => assertInventoryItemEditable(INVENTORY_STATUS.ARCHIVED),
|
||||
/locked against ordinary edits/,
|
||||
);
|
||||
assert.doesNotThrow(() => assertInventoryItemEditable(INVENTORY_STATUS.ACTIVE));
|
||||
});
|
||||
@@ -0,0 +1,74 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import {
|
||||
LISTING_STATUS,
|
||||
canPublishListing,
|
||||
createListing,
|
||||
transitionListingStatus,
|
||||
} from '../../src/modules/listings/domain/listing.js';
|
||||
|
||||
test('creates a draft listing with the required public offer fields', () => {
|
||||
const listing = createListing({
|
||||
inventoryItemId: 'inventory-item-id',
|
||||
productName: ' Charizard ex ',
|
||||
categoryId: 'category-id',
|
||||
conditionId: 'condition-id',
|
||||
askingPriceMinor: 2499,
|
||||
imageCount: 2,
|
||||
notes: ' Near mint ',
|
||||
});
|
||||
|
||||
assert.deepEqual(listing, {
|
||||
inventoryItemId: 'inventory-item-id',
|
||||
productName: 'Charizard ex',
|
||||
categoryId: 'category-id',
|
||||
conditionId: 'condition-id',
|
||||
askingPriceMinor: 2499,
|
||||
imageCount: 2,
|
||||
status: LISTING_STATUS.DRAFT,
|
||||
notes: 'Near mint',
|
||||
});
|
||||
});
|
||||
|
||||
test('requires the publication prerequisites before a listing can be made available', () => {
|
||||
const valid = {
|
||||
productName: 'Charizard ex',
|
||||
categoryId: 'category-id',
|
||||
conditionId: 'condition-id',
|
||||
inventoryItemId: 'inventory-item-id',
|
||||
askingPriceMinor: 2499,
|
||||
imageCount: 2,
|
||||
};
|
||||
|
||||
assert.doesNotThrow(() => canPublishListing(valid));
|
||||
|
||||
assert.throws(
|
||||
() => canPublishListing({ ...valid, askingPriceMinor: 0 }),
|
||||
(error) => error.code === 'LISTING_REQUIREMENTS_NOT_MET',
|
||||
);
|
||||
|
||||
assert.throws(
|
||||
() => canPublishListing({ ...valid, imageCount: 0 }),
|
||||
(error) => error.code === 'LISTING_REQUIREMENTS_NOT_MET',
|
||||
);
|
||||
|
||||
assert.throws(
|
||||
() => canPublishListing({ ...valid, inventoryStatus: 'SOLD' }),
|
||||
(error) => error.code === 'LISTING_INVENTORY_UNAVAILABLE' && error.statusCode === 409,
|
||||
);
|
||||
|
||||
assert.throws(
|
||||
() => canPublishListing({ ...valid, inventoryStatus: 'ARCHIVED' }),
|
||||
(error) => error.code === 'LISTING_INVENTORY_UNAVAILABLE',
|
||||
);
|
||||
});
|
||||
|
||||
test('allows only status transitions permitted by the listing lifecycle', () => {
|
||||
assert.equal(transitionListingStatus(LISTING_STATUS.DRAFT, LISTING_STATUS.AVAILABLE), LISTING_STATUS.AVAILABLE);
|
||||
assert.throws(
|
||||
() => transitionListingStatus(LISTING_STATUS.DRAFT, LISTING_STATUS.ARCHIVED),
|
||||
/Listing status cannot transition from DRAFT to ARCHIVED/,
|
||||
);
|
||||
assert.equal(transitionListingStatus(LISTING_STATUS.CLOSED, LISTING_STATUS.ARCHIVED), LISTING_STATUS.ARCHIVED);
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import { createProduct } from '../../src/modules/products/domain/product.js';
|
||||
|
||||
test('creates a product with required product identity fields', () => {
|
||||
const product = createProduct({
|
||||
name: ' Charizard ex ',
|
||||
categoryId: 'category-id',
|
||||
sellerId: 'seller-id',
|
||||
});
|
||||
|
||||
assert.deepEqual(product, {
|
||||
name: 'Charizard ex',
|
||||
categoryId: 'category-id',
|
||||
sellerId: 'seller-id',
|
||||
});
|
||||
});
|
||||
|
||||
test('rejects a product without a name or category', () => {
|
||||
assert.throws(
|
||||
() => createProduct({ name: ' ', categoryId: 'category-id', sellerId: 'seller-id' }),
|
||||
/Product name is required/,
|
||||
);
|
||||
assert.throws(
|
||||
() => createProduct({ name: 'Pikachu', categoryId: '', sellerId: 'seller-id' }),
|
||||
/Category ID is required/,
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user