399 lines
14 KiB
TypeScript
399 lines
14 KiB
TypeScript
import { Test } from '@nestjs/testing';
|
|
import {
|
|
BadRequestException,
|
|
NotFoundException,
|
|
} from '@nestjs/common';
|
|
import { GoodsService } from './goods.service';
|
|
import { PrismaService } from '../prisma/prisma.service';
|
|
import { SyncService } from '../sync/sync.service';
|
|
|
|
describe('GoodsService', () => {
|
|
let service: GoodsService;
|
|
let prisma: PrismaService;
|
|
const stamp = Date.now();
|
|
|
|
// Fixtures
|
|
let countryId: bigint;
|
|
let country2Id: bigint;
|
|
let categoryId: bigint;
|
|
let childCategoryId: bigint;
|
|
let tagId: bigint;
|
|
let positionId: bigint;
|
|
let originGoodIds: bigint[] = [];
|
|
|
|
beforeAll(async () => {
|
|
const moduleRef = await Test.createTestingModule({
|
|
providers: [
|
|
GoodsService,
|
|
PrismaService,
|
|
{
|
|
provide: SyncService,
|
|
useValue: { queueProductDetailSync: jest.fn() },
|
|
},
|
|
],
|
|
}).compile();
|
|
service = moduleRef.get(GoodsService);
|
|
prisma = moduleRef.get(PrismaService);
|
|
await prisma.onModuleInit();
|
|
|
|
const country = await prisma.country.create({
|
|
data: { countryName: `Goods Country ${stamp}` },
|
|
});
|
|
countryId = country.id;
|
|
const country2 = await prisma.country.create({
|
|
data: { countryName: `Goods Country 2 ${stamp}` },
|
|
});
|
|
country2Id = country2.id;
|
|
|
|
const cat = await prisma.category.create({
|
|
data: { categoryName: `Goods Cat ${stamp}` },
|
|
});
|
|
categoryId = cat.id;
|
|
const child = await prisma.category.create({
|
|
data: { categoryName: `Goods Child ${stamp}`, parentCategoryId: cat.id },
|
|
});
|
|
childCategoryId = child.id;
|
|
|
|
const tag = await prisma.tag.create({
|
|
data: { tagName: `Goods Tag ${stamp}`, tagColor: '#00FF00' },
|
|
});
|
|
tagId = tag.id;
|
|
|
|
const pos = await prisma.position.create({
|
|
data: { indexVal: 1, countryId, categoryId },
|
|
});
|
|
positionId = pos.id;
|
|
|
|
const originGoods = await Promise.all(
|
|
Array.from({ length: 5 }).map((_, i) =>
|
|
prisma.originGood.create({
|
|
data: {
|
|
sdsGoodId: `sds-goods-${stamp}-${i}`,
|
|
goodName: `Goods Origin ${stamp} ${i}`,
|
|
},
|
|
}),
|
|
),
|
|
);
|
|
originGoodIds = originGoods.map((og) => og.id);
|
|
});
|
|
|
|
afterAll(async () => {
|
|
// Wipe all goods first so origin_goods/category can be removed.
|
|
await prisma.good.deleteMany({
|
|
where: { goodName: { contains: `Goods Test ${stamp}` } },
|
|
});
|
|
await prisma.good.deleteMany({
|
|
where: { goodName: { contains: `Origin ${stamp}` } },
|
|
});
|
|
await prisma.originGood.deleteMany({
|
|
where: { id: { in: originGoodIds } },
|
|
});
|
|
await prisma.position.delete({ where: { id: positionId } });
|
|
await prisma.tag.delete({ where: { id: tagId } });
|
|
await prisma.category.delete({ where: { id: childCategoryId } });
|
|
await prisma.category.delete({ where: { id: categoryId } });
|
|
await prisma.country.delete({ where: { id: countryId } });
|
|
await prisma.country.delete({ where: { id: country2Id } });
|
|
await prisma.onModuleDestroy();
|
|
});
|
|
|
|
it('should be defined', () => {
|
|
expect(service).toBeDefined();
|
|
});
|
|
|
|
it('creates and reads back a good', async () => {
|
|
const created = await service.create({
|
|
goodName: `Goods Test ${stamp} basic`,
|
|
originGoodId: Number(originGoodIds[0]),
|
|
countryId: Number(countryId),
|
|
categoryId: Number(categoryId),
|
|
tagIds: [Number(tagId)],
|
|
positionId: Number(positionId),
|
|
goodPriority: 3,
|
|
});
|
|
expect(created.id).toBeTruthy();
|
|
expect(created.country?.countryName).toBeTruthy();
|
|
expect(created.tags.some((t) => t.tagColor === '#00FF00')).toBe(true);
|
|
|
|
const fetched = await service.findOne(BigInt(created.id));
|
|
expect(fetched.goodName).toBe(`Goods Test ${stamp} basic`);
|
|
});
|
|
|
|
it('creates, edits, and removes a fully editable custom good', async () => {
|
|
const created = await service.createCustom({
|
|
goodName: `Goods Test ${stamp} custom`,
|
|
goodImage: 'https://example.com/custom.png',
|
|
goodPrice: '29.90',
|
|
countryId: Number(countryId),
|
|
categoryId: Number(categoryId),
|
|
tagIds: [Number(tagId)],
|
|
detail: {
|
|
productCode: `CUSTOM-${stamp}`,
|
|
materialDescription: 'Cotton',
|
|
sizeChart: { columns: [], rows: [] },
|
|
packageSpecs: { rows: [] },
|
|
},
|
|
variants: [
|
|
{ sku: `CUSTOM-SKU-${stamp}`, sizeName: 'S', price: '29.90' },
|
|
],
|
|
});
|
|
|
|
expect(created.originGood?.source).toBe('CUSTOM');
|
|
expect(created.originGood?.isCustom).toBe(true);
|
|
expect(created.originGood?.goodPrice).toBe('29.9');
|
|
expect(created.variants).toHaveLength(1);
|
|
|
|
const updated = await service.updateCustomContent(BigInt(created.id), {
|
|
goodName: `Goods Test ${stamp} custom edited`,
|
|
goodPrice: '39.90',
|
|
detail: { materialDescription: 'Organic cotton' },
|
|
variants: [
|
|
{ sku: `CUSTOM-SKU-${stamp}-M`, sizeName: 'M', price: '39.90' },
|
|
],
|
|
});
|
|
expect(updated.goodName).toContain('custom edited');
|
|
expect(updated.originGood?.goodPrice).toBe('39.9');
|
|
expect(updated.originDetail?.materialDescription).toBe('Organic cotton');
|
|
expect(updated.originDetail?.productCode).toBe(`CUSTOM-${stamp}`);
|
|
expect(updated.variants[0]?.sizeName).toBe('M');
|
|
|
|
const customOriginId = BigInt(updated.originGoodId);
|
|
await service.remove(BigInt(updated.id));
|
|
await expect(
|
|
prisma.originGood.findUnique({ where: { id: customOriginId } }),
|
|
).resolves.toBeNull();
|
|
});
|
|
|
|
it('filters by countryId, tagId, positionId and keyword', async () => {
|
|
const result = await service.findAll({
|
|
page: 1,
|
|
pageSize: 20,
|
|
countryId: Number(countryId),
|
|
tagId: Number(tagId),
|
|
keyword: `Goods Test ${stamp}`,
|
|
});
|
|
expect(result.items.length).toBeGreaterThan(0);
|
|
expect(result.items.every((g) => g.countryId === countryId.toString())).toBe(true);
|
|
expect(
|
|
result.items.every((g) => g.tags.some((t) => t.id === tagId.toString())),
|
|
).toBe(true);
|
|
});
|
|
|
|
it('categoryId filter includes descendants recursively', async () => {
|
|
const inChild = await service.create({
|
|
goodName: `Goods Test ${stamp} child`,
|
|
originGoodId: Number(originGoodIds[1]),
|
|
countryId: Number(countryId),
|
|
categoryId: Number(childCategoryId),
|
|
});
|
|
const result = await service.findAll({
|
|
page: 1,
|
|
pageSize: 20,
|
|
categoryId: Number(categoryId),
|
|
keyword: `Goods Test ${stamp}`,
|
|
});
|
|
const ids = result.items.map((g) => g.id);
|
|
expect(ids).toContain(inChild.id);
|
|
});
|
|
|
|
it('batch update priority is atomic', async () => {
|
|
const created = await service.create({
|
|
goodName: `Goods Test ${stamp} prio`,
|
|
originGoodId: Number(originGoodIds[2]),
|
|
countryId: Number(countryId),
|
|
categoryId: Number(categoryId),
|
|
});
|
|
const result = await service.batchUpdatePriority({
|
|
items: [{ id: Number(created.id), priority: 42 }],
|
|
});
|
|
expect(result.count).toBe(1);
|
|
const after = await service.findOne(BigInt(created.id));
|
|
expect(after.goodPriority).toBe(42);
|
|
});
|
|
|
|
it('batch create creates all rows or none', async () => {
|
|
// Count of goods whose originGoodId is one of the two fixture ids,
|
|
// so we are independent of goodName (which batch derives from origin).
|
|
const before = await service.findAll({
|
|
page: 1,
|
|
pageSize: 100,
|
|
keyword: `Goods Origin ${stamp}`,
|
|
});
|
|
const created = await service.batchCreate({
|
|
countryId: Number(countryId),
|
|
categoryId: Number(categoryId),
|
|
defaultPriority: 1,
|
|
items: [
|
|
{ originGoodId: Number(originGoodIds[3]) },
|
|
{ originGoodId: Number(originGoodIds[4]) },
|
|
],
|
|
});
|
|
expect(created.length).toBe(2);
|
|
const after = await service.findAll({
|
|
page: 1,
|
|
pageSize: 100,
|
|
keyword: `Goods Origin ${stamp}`,
|
|
});
|
|
expect(after.total).toBe(before.total + 2);
|
|
});
|
|
|
|
it('batch create rolls back on failure', async () => {
|
|
const before = await service.findAll({
|
|
page: 1,
|
|
pageSize: 100,
|
|
keyword: `Goods Origin ${stamp}`,
|
|
});
|
|
await expect(
|
|
service.batchCreate({
|
|
countryId: Number(countryId),
|
|
categoryId: Number(categoryId),
|
|
items: [
|
|
{ originGoodId: Number(originGoodIds[0]) },
|
|
{ originGoodId: 99999999 }, // missing -> failure
|
|
],
|
|
}),
|
|
).rejects.toBeInstanceOf(BadRequestException);
|
|
const after = await service.findAll({
|
|
page: 1,
|
|
pageSize: 100,
|
|
keyword: `Goods Origin ${stamp}`,
|
|
});
|
|
expect(after.total).toBe(before.total);
|
|
});
|
|
|
|
describe('merged origin goods', () => {
|
|
it('creates a good with merged origin goods and reads them back', async () => {
|
|
const created = await service.create({
|
|
goodName: `Goods Test ${stamp} merged`,
|
|
originGoodId: Number(originGoodIds[1]),
|
|
mergedOriginGoodIds: [Number(originGoodIds[2]), Number(originGoodIds[3])],
|
|
countryId: Number(countryId),
|
|
categoryId: Number(categoryId),
|
|
});
|
|
expect(created.mergedOriginGoods.map((m) => m.id).sort()).toEqual(
|
|
[originGoodIds[2].toString(), originGoodIds[3].toString()].sort(),
|
|
);
|
|
const fetched = await service.findOne(BigInt(created.id));
|
|
expect(fetched.mergedOriginGoods.length).toBe(2);
|
|
});
|
|
|
|
it('rejects mergedOriginGoodIds containing the primary', async () => {
|
|
await expect(
|
|
service.create({
|
|
goodName: `Goods Test ${stamp} bad-primary`,
|
|
originGoodId: Number(originGoodIds[1]),
|
|
mergedOriginGoodIds: [Number(originGoodIds[1])],
|
|
countryId: Number(countryId),
|
|
categoryId: Number(categoryId),
|
|
}),
|
|
).rejects.toThrow(BadRequestException);
|
|
});
|
|
|
|
it('rejects mergedOriginGoodIds that do not exist', async () => {
|
|
await expect(
|
|
service.create({
|
|
goodName: `Goods Test ${stamp} bad-missing`,
|
|
originGoodId: Number(originGoodIds[1]),
|
|
mergedOriginGoodIds: [999999999],
|
|
countryId: Number(countryId),
|
|
categoryId: Number(categoryId),
|
|
}),
|
|
).rejects.toThrow(BadRequestException);
|
|
});
|
|
|
|
it('replaces merged origin goods on update', async () => {
|
|
const created = await service.create({
|
|
goodName: `Goods Test ${stamp} replace`,
|
|
originGoodId: Number(originGoodIds[1]),
|
|
mergedOriginGoodIds: [Number(originGoodIds[2])],
|
|
countryId: Number(countryId),
|
|
categoryId: Number(categoryId),
|
|
});
|
|
const updated = await service.update(BigInt(created.id), {
|
|
mergedOriginGoodIds: [Number(originGoodIds[3]), Number(originGoodIds[4])],
|
|
});
|
|
expect(updated.mergedOriginGoods.map((m) => m.id).sort()).toEqual(
|
|
[originGoodIds[3].toString(), originGoodIds[4].toString()].sort(),
|
|
);
|
|
});
|
|
|
|
it('moves old primary into merged list when switching primary', async () => {
|
|
const created = await service.create({
|
|
goodName: `Goods Test ${stamp} switch`,
|
|
originGoodId: Number(originGoodIds[1]),
|
|
mergedOriginGoodIds: [Number(originGoodIds[2])],
|
|
countryId: Number(countryId),
|
|
categoryId: Number(categoryId),
|
|
});
|
|
const updated = await service.update(BigInt(created.id), {
|
|
originGoodId: Number(originGoodIds[2]),
|
|
mergedOriginGoodIds: [Number(originGoodIds[1]), Number(originGoodIds[3])],
|
|
});
|
|
expect(updated.originGoodId).toBe(originGoodIds[2].toString());
|
|
expect(updated.mergedOriginGoods.map((m) => m.id).sort()).toEqual(
|
|
[originGoodIds[1].toString(), originGoodIds[3].toString()].sort(),
|
|
);
|
|
});
|
|
|
|
it('cascades merged rows on good removal', async () => {
|
|
const created = await service.create({
|
|
goodName: `Goods Test ${stamp} cascade`,
|
|
originGoodId: Number(originGoodIds[1]),
|
|
mergedOriginGoodIds: [Number(originGoodIds[2])],
|
|
countryId: Number(countryId),
|
|
categoryId: Number(categoryId),
|
|
});
|
|
await service.remove(BigInt(created.id));
|
|
const rows = await prisma.goodOriginGood.count({
|
|
where: { goodId: BigInt(created.id) },
|
|
});
|
|
expect(rows).toBe(0);
|
|
});
|
|
|
|
it('returns merged variants with source annotation in detail', async () => {
|
|
const v1 = await prisma.originGoodVariant.create({
|
|
data: {
|
|
originGoodId: originGoodIds[1],
|
|
sdsVariantId: `mv-pri-${stamp}`,
|
|
sku: `MV-PRI-${stamp}`,
|
|
colorName: '黑色',
|
|
},
|
|
});
|
|
const v2 = await prisma.originGoodVariant.create({
|
|
data: {
|
|
originGoodId: originGoodIds[2],
|
|
sdsVariantId: `mv-sec-${stamp}`,
|
|
sku: `MV-SEC-${stamp}`,
|
|
colorName: '白色',
|
|
},
|
|
});
|
|
try {
|
|
const created = await service.create({
|
|
goodName: `Goods Test ${stamp} variants`,
|
|
originGoodId: Number(originGoodIds[1]),
|
|
mergedOriginGoodIds: [Number(originGoodIds[2])],
|
|
countryId: Number(countryId),
|
|
categoryId: Number(categoryId),
|
|
});
|
|
const detail = await service.findOne(BigInt(created.id));
|
|
const sources = new Set(
|
|
detail.variants.map((v) => v['originGoodId'] as string),
|
|
);
|
|
expect(sources.has(originGoodIds[1].toString())).toBe(true);
|
|
expect(sources.has(originGoodIds[2].toString())).toBe(true);
|
|
expect(detail.variants).toHaveLength(2);
|
|
expect(detail.mergedOriginGoods.find((m) => m.id === originGoodIds[2].toString())?.variantCount).toBe(1);
|
|
} finally {
|
|
await prisma.originGoodVariant.delete({ where: { id: v1.id } });
|
|
await prisma.originGoodVariant.delete({ where: { id: v2.id } });
|
|
}
|
|
});
|
|
});
|
|
|
|
it('throws NotFoundException for unknown id', async () => {
|
|
await expect(service.findOne(BigInt(99999999))).rejects.toBeInstanceOf(
|
|
NotFoundException,
|
|
);
|
|
});
|
|
});
|