feat: validate inventory image metadata
Centralize supported MIME type, 10 MiB file-size, and 10,000-pixel dimension limits in the inventory image service boundary. Add domain coverage and update image/status documentation. Verification: npm test passed 64 tests and integration passed 14 tests. No build script is configured.
This commit is contained in:
@@ -0,0 +1,31 @@
|
|||||||
|
import { createApplicationError } from '../../../shared/errors/application-error.js';
|
||||||
|
|
||||||
|
const SUPPORTED_MIME_TYPES = new Set(['image/jpeg', 'image/png', 'image/webp']);
|
||||||
|
const DEFAULT_MAX_FILE_SIZE = 10 * 1024 * 1024;
|
||||||
|
const DEFAULT_MAX_DIMENSION = 10_000;
|
||||||
|
|
||||||
|
export function validateInventoryImageMetadata(
|
||||||
|
image,
|
||||||
|
{ maxFileSize = DEFAULT_MAX_FILE_SIZE, maxDimension = DEFAULT_MAX_DIMENSION } = {},
|
||||||
|
) {
|
||||||
|
if (!SUPPORTED_MIME_TYPES.has(image.mimeType)) {
|
||||||
|
throw createApplicationError('IMAGE_MIME_TYPE_UNSUPPORTED', 'Image MIME type is not supported.', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Number.isInteger(image.fileSize) || image.fileSize <= 0 || image.fileSize > maxFileSize) {
|
||||||
|
throw createApplicationError('IMAGE_FILE_TOO_LARGE', 'Image file size is invalid or too large.', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!Number.isInteger(image.width) ||
|
||||||
|
!Number.isInteger(image.height) ||
|
||||||
|
image.width <= 0 ||
|
||||||
|
image.height <= 0 ||
|
||||||
|
image.width > maxDimension ||
|
||||||
|
image.height > maxDimension
|
||||||
|
) {
|
||||||
|
throw createApplicationError('IMAGE_DIMENSIONS_INVALID', 'Image dimensions are invalid or too large.', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
import { createApplicationError } from '../../../shared/errors/application-error.js';
|
import { createApplicationError } from '../../../shared/errors/application-error.js';
|
||||||
|
import { validateInventoryImageMetadata } from '../domain/inventory-image.js';
|
||||||
|
|
||||||
export function createInventoryImageService({ inventoryImageRepository }) {
|
export function createInventoryImageService({ inventoryImageRepository }) {
|
||||||
return {
|
return {
|
||||||
async create(input) {
|
async create(input) {
|
||||||
|
validateInventoryImageMetadata(input);
|
||||||
const image = await inventoryImageRepository.createForSeller(
|
const image = await inventoryImageRepository.createForSeller(
|
||||||
{ ...input, processingStatus: 'PENDING' },
|
{ ...input, processingStatus: 'PENDING' },
|
||||||
input.sellerId,
|
input.sellerId,
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
|
||||||
|
import { validateInventoryImageMetadata } from '../../src/modules/inventory/domain/inventory-image.js';
|
||||||
|
|
||||||
|
const validImage = {
|
||||||
|
mimeType: 'image/jpeg',
|
||||||
|
fileSize: 12000,
|
||||||
|
width: 1200,
|
||||||
|
height: 900,
|
||||||
|
};
|
||||||
|
|
||||||
|
test('accepts supported image metadata within configured limits', () => {
|
||||||
|
assert.equal(validateInventoryImageMetadata(validImage), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects unsupported MIME types, oversized files, and oversized dimensions', () => {
|
||||||
|
assert.throws(
|
||||||
|
() => validateInventoryImageMetadata({ ...validImage, mimeType: 'image/gif' }),
|
||||||
|
(error) => error.code === 'IMAGE_MIME_TYPE_UNSUPPORTED',
|
||||||
|
);
|
||||||
|
assert.throws(
|
||||||
|
() => validateInventoryImageMetadata({ ...validImage, fileSize: 11 * 1024 * 1024 }),
|
||||||
|
(error) => error.code === 'IMAGE_FILE_TOO_LARGE',
|
||||||
|
);
|
||||||
|
assert.throws(
|
||||||
|
() => validateInventoryImageMetadata({ ...validImage, width: 10001 }),
|
||||||
|
(error) => error.code === 'IMAGE_DIMENSIONS_INVALID',
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -53,6 +53,11 @@ Validate:
|
|||||||
- image dimensions
|
- image dimensions
|
||||||
- configured image count
|
- configured image count
|
||||||
|
|
||||||
|
The metadata service currently enforces JPEG, PNG, and WebP MIME types, a
|
||||||
|
10 MiB default file-size limit, and a 10,000-pixel maximum width and height.
|
||||||
|
Binary signature checks and multipart upload validation remain part of the
|
||||||
|
upload transport slice.
|
||||||
|
|
||||||
Generate:
|
Generate:
|
||||||
- thumbnail
|
- thumbnail
|
||||||
- medium
|
- medium
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ docker context use orbstack
|
|||||||
- Listing reads are seller-scoped and return only admin listing DTO fields.
|
- Listing reads are seller-scoped and return only admin listing DTO fields.
|
||||||
- Item-owned image metadata can be added through a seller-scoped, CSRF-protected admin route; publication uses the persisted image count.
|
- Item-owned image metadata can be added through a seller-scoped, CSRF-protected admin route; publication uses the persisted image count.
|
||||||
- `ImageStorage` and a traversal-safe local filesystem adapter are implemented; upload transport, signature validation, and derivative processing remain.
|
- `ImageStorage` and a traversal-safe local filesystem adapter are implemented; upload transport, signature validation, and derivative processing remain.
|
||||||
|
- Image metadata limits are enforced in the service boundary: supported MIME types, 10 MiB default file size, and 10,000-pixel maximum dimensions.
|
||||||
|
|
||||||
### Verified Baseline
|
### Verified Baseline
|
||||||
|
|
||||||
@@ -101,13 +102,13 @@ docker context use orbstack
|
|||||||
| HTTP | Product and inventory creation, validation, authentication/recovery, CSRF, rate limiting, and session management |
|
| HTTP | Product and inventory creation, validation, authentication/recovery, CSRF, rate limiting, and session management |
|
||||||
| Integration | PostgreSQL catalog/session/audit/password recovery persistence and Redis-shared login throttling |
|
| Integration | PostgreSQL catalog/session/audit/password recovery persistence and Redis-shared login throttling |
|
||||||
|
|
||||||
Latest result: `npm test` passed 62 tests; `npm run test:integration` passed 14 tests against OrbStack PostgreSQL and Redis.
|
Latest result: `npm test` passed 64 tests; `npm run test:integration` passed 14 tests against OrbStack PostgreSQL and Redis.
|
||||||
|
|
||||||
## Next Milestone
|
## Next Milestone
|
||||||
|
|
||||||
Complete the inventory/listing integrity boundary before starting pricing and acquisition.
|
Complete the inventory/listing integrity boundary before starting pricing and acquisition.
|
||||||
|
|
||||||
1. Add upload transport, MIME/signature/size validation, and processing/derivative lifecycle for persisted image metadata.
|
1. Add multipart upload transport, binary signature validation, and processing/derivative lifecycle for persisted image metadata.
|
||||||
2. Begin acquisition and pricing foundations after the listing/image boundary is complete.
|
2. Begin acquisition and pricing foundations after the listing/image boundary is complete.
|
||||||
|
|
||||||
Acceptance criteria: listing reads and lifecycle writes preserve seller ownership, CSRF, inventory status, and publication prerequisites.
|
Acceptance criteria: listing reads and lifecycle writes preserve seller ownership, CSRF, inventory status, and publication prerequisites.
|
||||||
|
|||||||
Reference in New Issue
Block a user