feat(deploy): production deployment setup and fixes
- Debian-based api image (bookworm-slim), docker/debian mirrors, prisma binaryTargets for openssl 3.0 - nginx: admin SPA under /admin, TLS via acme.sh (ZeroSSL) + auto-renewal cron, http->https redirect - prisma: add origin_goods.delisted migration, sync missing schema (good_image/tag_font_color/good_tags), fix users.createdAt Timestamptz - api: CORS wildcard reflection, helmet CORP cross-origin, price backfill in persistProductDetail, categoryIcon ancestor fallback, mediaByColor per-color gallery in public goods detail - admin: /admin base path (vite + router) - import-data.mjs: udt_name casting, serial sequence advance fix
This commit is contained in:
@@ -1,103 +1,103 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type { Category, Tag } from '../app/composables/useProductCenter';
|
||||
import { findCategoryIdByName, findCategoryIdForPodName, resolveBackendAssetUrl, resolveCategoryIdFromQuery, resolveCountryIdFromQuery, resolveDefaultProductFilters, selectExclusiveTagIds } from '../app/composables/useProductCenter';
|
||||
|
||||
describe('resolveCountryIdFromQuery', () => {
|
||||
const countries = [{ id: '45', name: '美国', icon: null }];
|
||||
|
||||
it('accepts only a configured backend country id', () => {
|
||||
expect(resolveCountryIdFromQuery('45', countries)).toBe('45');
|
||||
expect(resolveCountryIdFromQuery('999', countries)).toBeUndefined();
|
||||
expect(resolveCountryIdFromQuery(['45'], countries)).toBe('45');
|
||||
});
|
||||
});
|
||||
|
||||
describe('product-center category routing', () => {
|
||||
it('finds and validates nested backend category ids', () => {
|
||||
expect(findCategoryIdByName('T恤', categories)).toBe('tee');
|
||||
expect(resolveCategoryIdFromQuery('tee', categories)).toBe('tee');
|
||||
expect(resolveCategoryIdFromQuery('unknown', categories)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('maps compound homepage POD names into the backend category tree', () => {
|
||||
const backendCategories: Category[] = [{
|
||||
id: 'women', name: '女士服装', icon: null, parentId: null,
|
||||
children: [{ id: 'tee', name: 'T恤', icon: null, parentId: 'women', children: [] }],
|
||||
}];
|
||||
expect(findCategoryIdForPodName('女士T恤', backendCategories)).toBe('tee');
|
||||
expect(findCategoryIdForPodName('女士背心', backendCategories)).toBe('women');
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveBackendAssetUrl', () => {
|
||||
it('resolves backend-relative uploaded and static assets', () => {
|
||||
expect(resolveBackendAssetUrl('/assets/product-center/countries/us.png', 'http://localhost:3001'))
|
||||
.toBe('http://localhost:3001/assets/product-center/countries/us.png');
|
||||
});
|
||||
|
||||
it('preserves absolute, data, and empty asset values', () => {
|
||||
expect(resolveBackendAssetUrl('https://cdn.example/icon.svg', 'http://localhost:3001')).toBe('https://cdn.example/icon.svg');
|
||||
expect(resolveBackendAssetUrl('data:image/svg+xml;base64,AA==', 'http://localhost:3001')).toBe('data:image/svg+xml;base64,AA==');
|
||||
expect(resolveBackendAssetUrl(null, 'http://localhost:3001')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
const categories: Category[] = [
|
||||
{
|
||||
id: 'women',
|
||||
name: '女式服装',
|
||||
icon: null,
|
||||
parentId: null,
|
||||
children: [
|
||||
{ id: 'tee', name: 'T恤', icon: null, parentId: 'women', children: [] },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const productionCategories: Category[] = [
|
||||
{
|
||||
...categories[0],
|
||||
name: '女士服装',
|
||||
},
|
||||
];
|
||||
|
||||
const tags: Tag[] = [
|
||||
{ id: 'shipping', name: '包邮', color: null, fontColor: null, group: null },
|
||||
{ id: 'transfer', name: '烫画', color: null, fontColor: null, group: null },
|
||||
];
|
||||
|
||||
describe('resolveDefaultProductFilters', () => {
|
||||
it('defaults to all products with no tag filters', () => {
|
||||
expect(resolveDefaultProductFilters(categories, tags)).toEqual({
|
||||
categoryId: undefined,
|
||||
tagIds: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('omits filters that are unavailable from the backend', () => {
|
||||
expect(resolveDefaultProductFilters([], [])).toEqual({
|
||||
categoryId: undefined,
|
||||
tagIds: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('does not infer a category from production category wording', () => {
|
||||
expect(resolveDefaultProductFilters(productionCategories, tags).categoryId).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('selectExclusiveTagIds', () => {
|
||||
const groupedTags: Tag[] = [
|
||||
{ id: 'free-shipping', name: '包邮', color: null, fontColor: null, group: { id: 'shipping', name: '物流', sortOrder: 1 } },
|
||||
{ id: 'paid-shipping', name: '不包邮', color: null, fontColor: null, group: { id: 'shipping', name: '物流', sortOrder: 1 } },
|
||||
{ id: 'transfer', name: '烫画', color: null, fontColor: null, group: { id: 'craft', name: '工艺', sortOrder: 2 } },
|
||||
];
|
||||
|
||||
it('replaces the selected tag within the same group', () => {
|
||||
expect(selectExclusiveTagIds(['free-shipping', 'transfer'], groupedTags, 'paid-shipping')).toEqual(['transfer', 'paid-shipping']);
|
||||
});
|
||||
|
||||
it('keeps selections from other groups and toggles the active item off', () => {
|
||||
expect(selectExclusiveTagIds(['free-shipping', 'transfer'], groupedTags, 'free-shipping')).toEqual(['transfer']);
|
||||
});
|
||||
});
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type { Category, Tag } from '../app/composables/useProductCenter';
|
||||
import { findCategoryIdByName, findCategoryIdForPodName, resolveBackendAssetUrl, resolveCategoryIdFromQuery, resolveCountryIdFromQuery, resolveDefaultProductFilters, selectExclusiveTagIds } from '../app/composables/useProductCenter';
|
||||
|
||||
describe('resolveCountryIdFromQuery', () => {
|
||||
const countries = [{ id: '45', name: '美国', icon: null }];
|
||||
|
||||
it('accepts only a configured backend country id', () => {
|
||||
expect(resolveCountryIdFromQuery('45', countries)).toBe('45');
|
||||
expect(resolveCountryIdFromQuery('999', countries)).toBeUndefined();
|
||||
expect(resolveCountryIdFromQuery(['45'], countries)).toBe('45');
|
||||
});
|
||||
});
|
||||
|
||||
describe('product-center category routing', () => {
|
||||
it('finds and validates nested backend category ids', () => {
|
||||
expect(findCategoryIdByName('T恤', categories)).toBe('tee');
|
||||
expect(resolveCategoryIdFromQuery('tee', categories)).toBe('tee');
|
||||
expect(resolveCategoryIdFromQuery('unknown', categories)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('maps compound homepage POD names into the backend category tree', () => {
|
||||
const backendCategories: Category[] = [{
|
||||
id: 'women', name: '女士服装', icon: null, parentId: null,
|
||||
children: [{ id: 'tee', name: 'T恤', icon: null, parentId: 'women', children: [] }],
|
||||
}];
|
||||
expect(findCategoryIdForPodName('女士T恤', backendCategories)).toBe('tee');
|
||||
expect(findCategoryIdForPodName('女士背心', backendCategories)).toBe('women');
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveBackendAssetUrl', () => {
|
||||
it('resolves backend-relative uploaded and static assets', () => {
|
||||
expect(resolveBackendAssetUrl('/assets/product-center/countries/us.png', 'http://localhost:3001'))
|
||||
.toBe('http://localhost:3001/assets/product-center/countries/us.png');
|
||||
});
|
||||
|
||||
it('preserves absolute, data, and empty asset values', () => {
|
||||
expect(resolveBackendAssetUrl('https://cdn.example/icon.svg', 'http://localhost:3001')).toBe('https://cdn.example/icon.svg');
|
||||
expect(resolveBackendAssetUrl('data:image/svg+xml;base64,AA==', 'http://localhost:3001')).toBe('data:image/svg+xml;base64,AA==');
|
||||
expect(resolveBackendAssetUrl(null, 'http://localhost:3001')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
const categories: Category[] = [
|
||||
{
|
||||
id: 'women',
|
||||
name: '女式服装',
|
||||
icon: null,
|
||||
parentId: null,
|
||||
children: [
|
||||
{ id: 'tee', name: 'T恤', icon: null, parentId: 'women', children: [] },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const productionCategories: Category[] = [
|
||||
{
|
||||
...categories[0],
|
||||
name: '女士服装',
|
||||
},
|
||||
];
|
||||
|
||||
const tags: Tag[] = [
|
||||
{ id: 'shipping', name: '包邮', color: null, fontColor: null, group: null },
|
||||
{ id: 'transfer', name: '烫画', color: null, fontColor: null, group: null },
|
||||
];
|
||||
|
||||
describe('resolveDefaultProductFilters', () => {
|
||||
it('defaults to all products with no tag filters', () => {
|
||||
expect(resolveDefaultProductFilters(categories, tags)).toEqual({
|
||||
categoryId: undefined,
|
||||
tagIds: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('omits filters that are unavailable from the backend', () => {
|
||||
expect(resolveDefaultProductFilters([], [])).toEqual({
|
||||
categoryId: undefined,
|
||||
tagIds: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('does not infer a category from production category wording', () => {
|
||||
expect(resolveDefaultProductFilters(productionCategories, tags).categoryId).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('selectExclusiveTagIds', () => {
|
||||
const groupedTags: Tag[] = [
|
||||
{ id: 'free-shipping', name: '包邮', color: null, fontColor: null, group: { id: 'shipping', name: '物流', sortOrder: 1 } },
|
||||
{ id: 'paid-shipping', name: '不包邮', color: null, fontColor: null, group: { id: 'shipping', name: '物流', sortOrder: 1 } },
|
||||
{ id: 'transfer', name: '烫画', color: null, fontColor: null, group: { id: 'craft', name: '工艺', sortOrder: 2 } },
|
||||
];
|
||||
|
||||
it('replaces the selected tag within the same group', () => {
|
||||
expect(selectExclusiveTagIds(['free-shipping', 'transfer'], groupedTags, 'paid-shipping')).toEqual(['transfer', 'paid-shipping']);
|
||||
});
|
||||
|
||||
it('keeps selections from other groups and toggles the active item off', () => {
|
||||
expect(selectExclusiveTagIds(['free-shipping', 'transfer'], groupedTags, 'free-shipping')).toEqual(['transfer']);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user