Files
inkreach-official-website/apps/api/src/countries/countries.service.spec.ts
T
yeuimu ab79325ab0 perf(public): 公开读路径进程内分域缓存——meta/goods/matrix 版本域 + TTL 兜底 + in-flight 合并
- PublicCacheService:分域版本号失效(bump 即作废,不等 TTL)、loader 期间
  bump 的竞态防护(返回但不回写)、并发 miss 单飞、PUBLIC_CACHE_DISABLED
  /TTL_MS/MAX_ENTRIES 应急开关;@Global 模块
- PublicService 六端点接缓存:列表缓存全量物化(分页切片在缓存外按请求执行,
  修复'所有页返回第一页'的切片缓存错误)、详情/首页/树/标签组/树序元数据/
  族最低价聚合各按依赖域缓存
- 全写路径挂钩 bump:admin CRUD(goods/categories/countries/tags/tag-groups/
  positions/origin-goods 标签)、sync 三同步、族重算、整理全家桶——
  事务提交成功后失效对应域,product-families 经 recompute 天然覆盖
- 测试:PublicCacheService 单测 13 例 + 失效链路集成 8 例(读命中/写后立即可见
  端到端/每条写路径域断言);既有两套件测数据语义改为显式禁缓存
2026-09-03 03:40:20 +08:00

143 lines
5.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { PublicCacheService } from '../public/public-cache.service';
import { Test } from '@nestjs/testing';
import {
BadRequestException,
ConflictException,
NotFoundException,
} from '@nestjs/common';
import { CountriesService } from './countries.service';
import { PrismaService } from '../prisma/prisma.service';
describe('CountriesService', () => {
let service: CountriesService;
let prisma: PrismaService;
const created: string[] = [];
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
providers: [CountriesService, PrismaService, PublicCacheService],
}).compile();
service = moduleRef.get(CountriesService);
prisma = moduleRef.get(PrismaService);
await prisma.onModuleInit();
});
afterAll(async () => {
if (created.length) {
// Delete goods/origin_goods first so we can remove the countries.
await prisma.good.deleteMany({
where: { country: { countryName: { in: created } } },
});
await prisma.position.deleteMany({
where: { country: { countryName: { in: created } } },
});
await prisma.country.deleteMany({
where: { countryName: { in: created } },
});
}
await prisma.onModuleDestroy();
});
it('should be defined', () => {
expect(service).toBeDefined();
});
it('creates and reads back a country', async () => {
const name = `Test Country ${Date.now()}`;
created.push(name);
const createdRow = await service.create({ countryName: name });
expect(createdRow.countryName).toBe(name);
const fetched = await service.findOne(createdRow.id);
expect(fetched.countryName).toBe(name);
});
it('rejects duplicate names with ConflictException', async () => {
const name = `Dup Country ${Date.now()}`;
created.push(name);
await service.create({ countryName: name });
await expect(service.create({ countryName: name })).rejects.toBeInstanceOf(
ConflictException,
);
});
it('throws NotFoundException for unknown id', async () => {
await expect(service.findOne(BigInt(99999999))).rejects.toBeInstanceOf(
NotFoundException,
);
});
it('throws BadRequestException when deleting a country referenced by goods', async () => {
const name = `Ref Country ${Date.now()}`;
created.push(name);
const country = await service.create({ countryName: name });
// Need a category + origin good to satisfy the foreign keys before
// we can attach a good that references the country.
const category = await prisma.category.create({
data: { categoryName: `Cat ${Date.now()}` },
});
const originGood = await prisma.originGood.create({
data: { sdsGoodId: `sds-${Date.now()}-${Math.random()}` },
});
await prisma.good.create({
data: {
originGoodId: originGood.id,
countryId: country.id,
categoryId: category.id,
goodName: 'sample',
},
});
await expect(service.remove(country.id)).rejects.toBeInstanceOf(
BadRequestException,
);
// cleanup
await prisma.good.deleteMany({ where: { countryId: country.id } });
await prisma.originGood.delete({ where: { id: originGood.id } });
await prisma.category.delete({ where: { id: category.id } });
});
it('updates fields and deletes when unreferenced', async () => {
const name = `Upd Country ${Date.now()}`;
created.push(name);
const c = await service.create({ countryName: name });
const updated = await service.update(c.id, { countryName: `${name}-v2` });
expect(updated.countryName).toBe(`${name}-v2`);
created[created.indexOf(name)] = `${name}-v2`;
await service.remove(c.id);
const idx = created.indexOf(`${name}-v2`);
if (idx !== -1) created.splice(idx, 1);
});
it('reorder persists sortOrder and findAll honors it', async () => {
const stamp = Date.now();
const a = await service.create({ countryName: `Sort A ${stamp}` });
const b = await service.create({ countryName: `Sort B ${stamp}` });
const c = await service.create({ countryName: `Sort C ${stamp}` });
created.push(a.countryName, b.countryName, c.countryName);
// 新顺序 C, A, Bid 序与 sortOrder 无关时也能稳定验证)
const reordered = await service.reorder({
items: [
{ id: Number(c.id), sortOrder: 0 },
{ id: Number(a.id), sortOrder: 1 },
{ id: Number(b.id), sortOrder: 2 },
],
});
const ours = reordered.filter((r) =>
[a.id, b.id, c.id].some((id) => id.toString() === r.id.toString()),
);
expect(ours.map((r) => r.id.toString())).toEqual([c.id.toString(), a.id.toString(), b.id.toString()]);
const list = await service.findAll();
const pos = (id: bigint) => list.findIndex((r) => r.id.toString() === id.toString());
expect(pos(c.id)).toBeLessThan(pos(a.id));
expect(pos(a.id)).toBeLessThan(pos(b.id));
});
});