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,280 +1,280 @@
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import {
|
||||
SyncService,
|
||||
shouldRunDelistDetection,
|
||||
shouldSkipStaleDeletion,
|
||||
} from './sync.service';
|
||||
import { SdsClientService } from './sds-client.service';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import {
|
||||
SyncService,
|
||||
shouldRunDelistDetection,
|
||||
shouldSkipStaleDeletion,
|
||||
} from './sync.service';
|
||||
import { SdsClientService } from './sds-client.service';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
|
||||
describe('SyncService', () => {
|
||||
let service: SyncService;
|
||||
let sds: jest.Mocked<SdsClientService>;
|
||||
let prisma: PrismaService;
|
||||
const createdSdsCategoryIds: string[] = [];
|
||||
const createdSdsGoodIds: string[] = [];
|
||||
|
||||
beforeAll(async () => {
|
||||
const sdsMock: Partial<SdsClientService> = {
|
||||
let service: SyncService;
|
||||
let sds: jest.Mocked<SdsClientService>;
|
||||
let prisma: PrismaService;
|
||||
const createdSdsCategoryIds: string[] = [];
|
||||
const createdSdsGoodIds: string[] = [];
|
||||
|
||||
beforeAll(async () => {
|
||||
const sdsMock: Partial<SdsClientService> = {
|
||||
fetchCategoryTree: jest.fn(),
|
||||
fetchProductsPage: jest.fn(),
|
||||
fetchProductDetail: jest.fn(async (goodId: string | number) => ({ id: goodId })),
|
||||
};
|
||||
const moduleRef = await Test.createTestingModule({
|
||||
imports: [ConfigModule.forRoot({ isGlobal: true })],
|
||||
providers: [
|
||||
SyncService,
|
||||
{ provide: SdsClientService, useValue: sdsMock },
|
||||
PrismaService,
|
||||
],
|
||||
}).compile();
|
||||
};
|
||||
const moduleRef = await Test.createTestingModule({
|
||||
imports: [ConfigModule.forRoot({ isGlobal: true })],
|
||||
providers: [
|
||||
SyncService,
|
||||
{ provide: SdsClientService, useValue: sdsMock },
|
||||
PrismaService,
|
||||
],
|
||||
}).compile();
|
||||
service = moduleRef.get(SyncService);
|
||||
jest
|
||||
.spyOn(service, 'syncConfiguredProductDetails')
|
||||
.mockResolvedValue({ synced: 0, failed: 0 });
|
||||
sds = moduleRef.get(SdsClientService) as jest.Mocked<SdsClientService>;
|
||||
prisma = moduleRef.get(PrismaService);
|
||||
await prisma.onModuleInit();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
if (createdSdsCategoryIds.length) {
|
||||
await prisma.category.deleteMany({
|
||||
where: { sdsCategoryId: { in: createdSdsCategoryIds } },
|
||||
});
|
||||
}
|
||||
if (createdSdsGoodIds.length) {
|
||||
await prisma.originGood.deleteMany({
|
||||
where: { sdsGoodId: { in: createdSdsGoodIds } },
|
||||
});
|
||||
}
|
||||
await prisma.onModuleDestroy();
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe('flattenCategoryTree', () => {
|
||||
it('flattens a nested SDS tree and preserves parent linkage', () => {
|
||||
const tree = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Root',
|
||||
children: [
|
||||
{ id: 11, name: 'Child A' },
|
||||
{ id: 12, name: 'Child B', children: [{ id: 121, name: 'Leaf' }] },
|
||||
],
|
||||
},
|
||||
];
|
||||
const flat = service.flattenCategoryTree(tree);
|
||||
expect(flat).toHaveLength(4);
|
||||
const byId = Object.fromEntries(flat.map((n) => [n.sdsId, n]));
|
||||
expect(byId['1'].name).toBe('Root');
|
||||
expect(byId['1'].parentSdsId).toBeUndefined();
|
||||
expect(byId['11'].parentSdsId).toBe('1');
|
||||
expect(byId['12'].parentSdsId).toBe('1');
|
||||
expect(byId['121'].parentSdsId).toBe('12');
|
||||
});
|
||||
|
||||
it('skips nodes without a usable id', () => {
|
||||
const flat = service.flattenCategoryTree([{ id: null, name: 'no-id' }]);
|
||||
expect(flat).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('syncCategories', () => {
|
||||
it('inserts + updates rows and links parents', async () => {
|
||||
const stamp = Date.now();
|
||||
sds.fetchCategoryTree.mockResolvedValueOnce([
|
||||
{
|
||||
id: `r-${stamp}`,
|
||||
name: `Root ${stamp}`,
|
||||
children: [{ id: `c-${stamp}`, name: `Child ${stamp}` }],
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await service.syncCategories();
|
||||
expect(result.total).toBe(2);
|
||||
expect(result.inserted).toBe(2);
|
||||
expect(result.updated).toBe(0);
|
||||
createdSdsCategoryIds.push(`r-${stamp}`, `c-${stamp}`);
|
||||
|
||||
const child = await prisma.category.findUnique({
|
||||
where: { sdsCategoryId: `c-${stamp}` },
|
||||
});
|
||||
const root = await prisma.category.findUnique({
|
||||
where: { sdsCategoryId: `r-${stamp}` },
|
||||
});
|
||||
expect(child?.parentCategoryId).toBe(root?.id);
|
||||
});
|
||||
|
||||
it('marks SyncLog SUCCESS', async () => {
|
||||
const logs = await prisma.syncLog.findMany({
|
||||
where: { type: 'CATEGORIES' },
|
||||
orderBy: { startedAt: 'desc' },
|
||||
take: 1,
|
||||
});
|
||||
expect(logs[0]?.status).toBe('SUCCESS');
|
||||
});
|
||||
});
|
||||
|
||||
describe('syncProducts', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('upserts origin goods by sdsGoodId and updates on re-run', async () => {
|
||||
// Seed a leaf category with sdsCategoryId so the sync has work.
|
||||
const stamp = Date.now();
|
||||
const leaf = await prisma.category.create({
|
||||
data: {
|
||||
sdsCategoryId: `leaf-${stamp}`,
|
||||
categoryName: `Leaf ${stamp}`,
|
||||
},
|
||||
});
|
||||
createdSdsCategoryIds.push(`leaf-${stamp}`);
|
||||
|
||||
// Default mock returns a single page with 2 products, then
|
||||
// breaks the loop because content.length < 50.
|
||||
sds.fetchProductsPage.mockImplementation(async (categoryId) => {
|
||||
if (categoryId === `leaf-${stamp}`) {
|
||||
return {
|
||||
content: [
|
||||
{ id: `p-${stamp}-1`, name: 'Product 1', price: 12.5, pic: 'http://x' },
|
||||
{ id: `p-${stamp}-2`, name: 'Product 2', price: '99.00' },
|
||||
],
|
||||
};
|
||||
}
|
||||
// For any other (already-existing) category, return empty
|
||||
// so the loop terminates immediately.
|
||||
return { content: [] };
|
||||
});
|
||||
|
||||
const result1 = await service.syncProducts();
|
||||
expect(result1.inserted).toBeGreaterThanOrEqual(2);
|
||||
createdSdsGoodIds.push(`p-${stamp}-1`, `p-${stamp}-2`);
|
||||
|
||||
// Re-run with updated name -> should be `updated`, not `inserted`.
|
||||
sds.fetchProductsPage.mockImplementation(async (categoryId) => {
|
||||
if (categoryId === `leaf-${stamp}`) {
|
||||
return {
|
||||
content: [
|
||||
{ id: `p-${stamp}-1`, name: 'Product 1 renamed' },
|
||||
],
|
||||
};
|
||||
}
|
||||
return { content: [] };
|
||||
});
|
||||
const result2 = await service.syncProducts();
|
||||
expect(result2.updated).toBeGreaterThanOrEqual(1);
|
||||
const row = await prisma.originGood.findUnique({
|
||||
where: { sdsGoodId: `p-${stamp}-1` },
|
||||
});
|
||||
expect(row?.goodName).toBe('Product 1 renamed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('sync guard thresholds', () => {
|
||||
describe('shouldSkipStaleDeletion', () => {
|
||||
it('skips stale deletion when the fetched count is below the hard floor', () => {
|
||||
expect(shouldSkipStaleDeletion(2, 226)).toBe(true);
|
||||
expect(shouldSkipStaleDeletion(9, 226)).toBe(true);
|
||||
});
|
||||
|
||||
it('skips stale deletion when fetched is far smaller than existing (ratio guard)', () => {
|
||||
expect(shouldSkipStaleDeletion(100, 250)).toBe(true);
|
||||
});
|
||||
|
||||
it('does NOT skip when fetched count is healthy', () => {
|
||||
expect(shouldSkipStaleDeletion(226, 226)).toBe(false);
|
||||
expect(shouldSkipStaleDeletion(200, 226)).toBe(false);
|
||||
});
|
||||
|
||||
it('does NOT skip when there are no existing SDS categories', () => {
|
||||
expect(shouldSkipStaleDeletion(0, 0)).toBe(false);
|
||||
expect(shouldSkipStaleDeletion(2, 0)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('shouldRunDelistDetection', () => {
|
||||
it('skips delist detection when leaf categories are too few', () => {
|
||||
expect(shouldRunDelistDetection(2, 500)).toBe(false);
|
||||
expect(shouldRunDelistDetection(9, 500)).toBe(false);
|
||||
});
|
||||
|
||||
it('skips delist detection when the seen product count is too small', () => {
|
||||
expect(shouldRunDelistDetection(148, 2)).toBe(false);
|
||||
expect(shouldRunDelistDetection(148, 49)).toBe(false);
|
||||
});
|
||||
|
||||
it('runs delist detection only when both metrics are healthy', () => {
|
||||
expect(shouldRunDelistDetection(148, 500)).toBe(true);
|
||||
expect(shouldRunDelistDetection(10, 50)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('category sync keeps existing SDS categories when upstream returns a degenerate tree', async () => {
|
||||
const stamp = Date.now();
|
||||
const keep = await prisma.category.create({
|
||||
data: { sdsCategoryId: `keep-${stamp}`, categoryName: `Keep ${stamp}` },
|
||||
});
|
||||
createdSdsCategoryIds.push(`keep-${stamp}`);
|
||||
|
||||
sds.fetchCategoryTree.mockResolvedValueOnce([
|
||||
{ id: `g-${stamp}-1`, name: 'Tiny 1' },
|
||||
{ id: `g-${stamp}-2`, name: 'Tiny 2' },
|
||||
]);
|
||||
createdSdsCategoryIds.push(`g-${stamp}-1`, `g-${stamp}-2`);
|
||||
|
||||
const result = await service.syncCategories();
|
||||
expect(result.deletedStale).toBe(0);
|
||||
|
||||
const still = await prisma.category.findUnique({ where: { id: keep.id } });
|
||||
expect(still).not.toBeNull();
|
||||
});
|
||||
|
||||
it('product sync does NOT delist origin goods when it sees too few products', async () => {
|
||||
const stamp = Date.now();
|
||||
const leaf = await prisma.category.create({
|
||||
data: { sdsCategoryId: `leafguard-${stamp}`, categoryName: `LeafGuard ${stamp}` },
|
||||
});
|
||||
createdSdsCategoryIds.push(`leafguard-${stamp}`);
|
||||
|
||||
const active = await prisma.originGood.create({
|
||||
data: { sdsGoodId: `active-${stamp}`, delisted: false, goodName: 'Active' },
|
||||
});
|
||||
createdSdsGoodIds.push(`active-${stamp}`);
|
||||
|
||||
sds.fetchProductsPage.mockImplementation(async (categoryId) => {
|
||||
if (categoryId === `leafguard-${stamp}`) {
|
||||
return {
|
||||
content: [
|
||||
{ id: `guardp-${stamp}-1`, name: 'P1' },
|
||||
{ id: `guardp-${stamp}-2`, name: 'P2' },
|
||||
],
|
||||
};
|
||||
}
|
||||
return { content: [] };
|
||||
});
|
||||
|
||||
const result = await service.syncProducts();
|
||||
expect(result.delisted).toBe(0);
|
||||
|
||||
const still = await prisma.originGood.findUnique({ where: { id: active.id } });
|
||||
expect(still?.delisted).toBe(false);
|
||||
createdSdsGoodIds.push(`guardp-${stamp}-1`, `guardp-${stamp}-2`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getStatus', () => {
|
||||
it('returns recent logs ordered by startedAt desc', async () => {
|
||||
const logs = await service.getStatus(5);
|
||||
expect(Array.isArray(logs)).toBe(true);
|
||||
expect(logs.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
sds = moduleRef.get(SdsClientService) as jest.Mocked<SdsClientService>;
|
||||
prisma = moduleRef.get(PrismaService);
|
||||
await prisma.onModuleInit();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
if (createdSdsCategoryIds.length) {
|
||||
await prisma.category.deleteMany({
|
||||
where: { sdsCategoryId: { in: createdSdsCategoryIds } },
|
||||
});
|
||||
}
|
||||
if (createdSdsGoodIds.length) {
|
||||
await prisma.originGood.deleteMany({
|
||||
where: { sdsGoodId: { in: createdSdsGoodIds } },
|
||||
});
|
||||
}
|
||||
await prisma.onModuleDestroy();
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe('flattenCategoryTree', () => {
|
||||
it('flattens a nested SDS tree and preserves parent linkage', () => {
|
||||
const tree = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Root',
|
||||
children: [
|
||||
{ id: 11, name: 'Child A' },
|
||||
{ id: 12, name: 'Child B', children: [{ id: 121, name: 'Leaf' }] },
|
||||
],
|
||||
},
|
||||
];
|
||||
const flat = service.flattenCategoryTree(tree);
|
||||
expect(flat).toHaveLength(4);
|
||||
const byId = Object.fromEntries(flat.map((n) => [n.sdsId, n]));
|
||||
expect(byId['1'].name).toBe('Root');
|
||||
expect(byId['1'].parentSdsId).toBeUndefined();
|
||||
expect(byId['11'].parentSdsId).toBe('1');
|
||||
expect(byId['12'].parentSdsId).toBe('1');
|
||||
expect(byId['121'].parentSdsId).toBe('12');
|
||||
});
|
||||
|
||||
it('skips nodes without a usable id', () => {
|
||||
const flat = service.flattenCategoryTree([{ id: null, name: 'no-id' }]);
|
||||
expect(flat).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('syncCategories', () => {
|
||||
it('inserts + updates rows and links parents', async () => {
|
||||
const stamp = Date.now();
|
||||
sds.fetchCategoryTree.mockResolvedValueOnce([
|
||||
{
|
||||
id: `r-${stamp}`,
|
||||
name: `Root ${stamp}`,
|
||||
children: [{ id: `c-${stamp}`, name: `Child ${stamp}` }],
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await service.syncCategories();
|
||||
expect(result.total).toBe(2);
|
||||
expect(result.inserted).toBe(2);
|
||||
expect(result.updated).toBe(0);
|
||||
createdSdsCategoryIds.push(`r-${stamp}`, `c-${stamp}`);
|
||||
|
||||
const child = await prisma.category.findUnique({
|
||||
where: { sdsCategoryId: `c-${stamp}` },
|
||||
});
|
||||
const root = await prisma.category.findUnique({
|
||||
where: { sdsCategoryId: `r-${stamp}` },
|
||||
});
|
||||
expect(child?.parentCategoryId).toBe(root?.id);
|
||||
});
|
||||
|
||||
it('marks SyncLog SUCCESS', async () => {
|
||||
const logs = await prisma.syncLog.findMany({
|
||||
where: { type: 'CATEGORIES' },
|
||||
orderBy: { startedAt: 'desc' },
|
||||
take: 1,
|
||||
});
|
||||
expect(logs[0]?.status).toBe('SUCCESS');
|
||||
});
|
||||
});
|
||||
|
||||
describe('syncProducts', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('upserts origin goods by sdsGoodId and updates on re-run', async () => {
|
||||
// Seed a leaf category with sdsCategoryId so the sync has work.
|
||||
const stamp = Date.now();
|
||||
const leaf = await prisma.category.create({
|
||||
data: {
|
||||
sdsCategoryId: `leaf-${stamp}`,
|
||||
categoryName: `Leaf ${stamp}`,
|
||||
},
|
||||
});
|
||||
createdSdsCategoryIds.push(`leaf-${stamp}`);
|
||||
|
||||
// Default mock returns a single page with 2 products, then
|
||||
// breaks the loop because content.length < 50.
|
||||
sds.fetchProductsPage.mockImplementation(async (categoryId) => {
|
||||
if (categoryId === `leaf-${stamp}`) {
|
||||
return {
|
||||
content: [
|
||||
{ id: `p-${stamp}-1`, name: 'Product 1', price: 12.5, pic: 'http://x' },
|
||||
{ id: `p-${stamp}-2`, name: 'Product 2', price: '99.00' },
|
||||
],
|
||||
};
|
||||
}
|
||||
// For any other (already-existing) category, return empty
|
||||
// so the loop terminates immediately.
|
||||
return { content: [] };
|
||||
});
|
||||
|
||||
const result1 = await service.syncProducts();
|
||||
expect(result1.inserted).toBeGreaterThanOrEqual(2);
|
||||
createdSdsGoodIds.push(`p-${stamp}-1`, `p-${stamp}-2`);
|
||||
|
||||
// Re-run with updated name -> should be `updated`, not `inserted`.
|
||||
sds.fetchProductsPage.mockImplementation(async (categoryId) => {
|
||||
if (categoryId === `leaf-${stamp}`) {
|
||||
return {
|
||||
content: [
|
||||
{ id: `p-${stamp}-1`, name: 'Product 1 renamed' },
|
||||
],
|
||||
};
|
||||
}
|
||||
return { content: [] };
|
||||
});
|
||||
const result2 = await service.syncProducts();
|
||||
expect(result2.updated).toBeGreaterThanOrEqual(1);
|
||||
const row = await prisma.originGood.findUnique({
|
||||
where: { sdsGoodId: `p-${stamp}-1` },
|
||||
});
|
||||
expect(row?.goodName).toBe('Product 1 renamed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('sync guard thresholds', () => {
|
||||
describe('shouldSkipStaleDeletion', () => {
|
||||
it('skips stale deletion when the fetched count is below the hard floor', () => {
|
||||
expect(shouldSkipStaleDeletion(2, 226)).toBe(true);
|
||||
expect(shouldSkipStaleDeletion(9, 226)).toBe(true);
|
||||
});
|
||||
|
||||
it('skips stale deletion when fetched is far smaller than existing (ratio guard)', () => {
|
||||
expect(shouldSkipStaleDeletion(100, 250)).toBe(true);
|
||||
});
|
||||
|
||||
it('does NOT skip when fetched count is healthy', () => {
|
||||
expect(shouldSkipStaleDeletion(226, 226)).toBe(false);
|
||||
expect(shouldSkipStaleDeletion(200, 226)).toBe(false);
|
||||
});
|
||||
|
||||
it('does NOT skip when there are no existing SDS categories', () => {
|
||||
expect(shouldSkipStaleDeletion(0, 0)).toBe(false);
|
||||
expect(shouldSkipStaleDeletion(2, 0)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('shouldRunDelistDetection', () => {
|
||||
it('skips delist detection when leaf categories are too few', () => {
|
||||
expect(shouldRunDelistDetection(2, 500)).toBe(false);
|
||||
expect(shouldRunDelistDetection(9, 500)).toBe(false);
|
||||
});
|
||||
|
||||
it('skips delist detection when the seen product count is too small', () => {
|
||||
expect(shouldRunDelistDetection(148, 2)).toBe(false);
|
||||
expect(shouldRunDelistDetection(148, 49)).toBe(false);
|
||||
});
|
||||
|
||||
it('runs delist detection only when both metrics are healthy', () => {
|
||||
expect(shouldRunDelistDetection(148, 500)).toBe(true);
|
||||
expect(shouldRunDelistDetection(10, 50)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('category sync keeps existing SDS categories when upstream returns a degenerate tree', async () => {
|
||||
const stamp = Date.now();
|
||||
const keep = await prisma.category.create({
|
||||
data: { sdsCategoryId: `keep-${stamp}`, categoryName: `Keep ${stamp}` },
|
||||
});
|
||||
createdSdsCategoryIds.push(`keep-${stamp}`);
|
||||
|
||||
sds.fetchCategoryTree.mockResolvedValueOnce([
|
||||
{ id: `g-${stamp}-1`, name: 'Tiny 1' },
|
||||
{ id: `g-${stamp}-2`, name: 'Tiny 2' },
|
||||
]);
|
||||
createdSdsCategoryIds.push(`g-${stamp}-1`, `g-${stamp}-2`);
|
||||
|
||||
const result = await service.syncCategories();
|
||||
expect(result.deletedStale).toBe(0);
|
||||
|
||||
const still = await prisma.category.findUnique({ where: { id: keep.id } });
|
||||
expect(still).not.toBeNull();
|
||||
});
|
||||
|
||||
it('product sync does NOT delist origin goods when it sees too few products', async () => {
|
||||
const stamp = Date.now();
|
||||
const leaf = await prisma.category.create({
|
||||
data: { sdsCategoryId: `leafguard-${stamp}`, categoryName: `LeafGuard ${stamp}` },
|
||||
});
|
||||
createdSdsCategoryIds.push(`leafguard-${stamp}`);
|
||||
|
||||
const active = await prisma.originGood.create({
|
||||
data: { sdsGoodId: `active-${stamp}`, delisted: false, goodName: 'Active' },
|
||||
});
|
||||
createdSdsGoodIds.push(`active-${stamp}`);
|
||||
|
||||
sds.fetchProductsPage.mockImplementation(async (categoryId) => {
|
||||
if (categoryId === `leafguard-${stamp}`) {
|
||||
return {
|
||||
content: [
|
||||
{ id: `guardp-${stamp}-1`, name: 'P1' },
|
||||
{ id: `guardp-${stamp}-2`, name: 'P2' },
|
||||
],
|
||||
};
|
||||
}
|
||||
return { content: [] };
|
||||
});
|
||||
|
||||
const result = await service.syncProducts();
|
||||
expect(result.delisted).toBe(0);
|
||||
|
||||
const still = await prisma.originGood.findUnique({ where: { id: active.id } });
|
||||
expect(still?.delisted).toBe(false);
|
||||
createdSdsGoodIds.push(`guardp-${stamp}-1`, `guardp-${stamp}-2`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getStatus', () => {
|
||||
it('returns recent logs ordered by startedAt desc', async () => {
|
||||
const logs = await service.getStatus(5);
|
||||
expect(Array.isArray(logs)).toBe(true);
|
||||
expect(logs.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('SyncService product detail scopes', () => {
|
||||
|
||||
Reference in New Issue
Block a user