fix: default certificate expiry to one year after testing
This commit is contained in:
parent
cb510075fc
commit
d2337b960e
@ -13,7 +13,7 @@ import {
|
||||
import { updateMaterialWithSharedRules, type MaterialUpdateInput } from '@/lib/material-update';
|
||||
import { deriveTaskDetailIsFullSet } from '@/lib/task-detail-full-set';
|
||||
import { isWebFileLike } from '@/lib/web-file';
|
||||
import { detectFileKindFromName, getCertificateReportFileKindByAssetType, isPublicCertificateReportPdf } from '@/lib/certificate-report';
|
||||
import { detectFileKindFromName, getCertificateReportFileKindByAssetType, getDefaultCertificateExpiryDate, isCertificateReportMaterialType, isPublicCertificateReportPdf } from '@/lib/certificate-report';
|
||||
import { DownloadErrorCode } from '@/lib/download-errors';
|
||||
import { getVideoPreviewPolicyFields, userHasVideoEarlyAccess } from '@/lib/video-download-policy';
|
||||
import { is3DOriginalType, isProductPackageType } from '@/lib/material-type-mapper';
|
||||
@ -481,7 +481,11 @@ export async function GET(
|
||||
reportKind: materialWithUser.reportKind || null,
|
||||
certificateTestedOn: materialWithUser.certificateTestedOn || null,
|
||||
certificateNumber: materialWithUser.certificateNumber || null,
|
||||
certificateExpiresOn: materialWithUser.certificateExpiresOn || null,
|
||||
certificateExpiresOn: materialWithUser.certificateExpiresOn || (
|
||||
isCertificateReportMaterialType(materialWithUser.materialType)
|
||||
? getDefaultCertificateExpiryDate(materialWithUser.certificateTestedOn)
|
||||
: null
|
||||
),
|
||||
reportUsageRules: materialWithUser.reportUsageRules || null,
|
||||
categoryName: materialWithUser.categoryName,
|
||||
sceneName: materialWithUser.sceneName,
|
||||
|
||||
@ -14,7 +14,7 @@ import {
|
||||
import { isProductPackageType, is3DOriginalType, needsAssetsList } from '@/lib/material-type-mapper';
|
||||
import { deriveTaskDetailIsFullSet } from '@/lib/task-detail-full-set';
|
||||
import { canManageMaterial } from '@/lib/material-authorization';
|
||||
import { detectFileKindFromName, getCertificateReportFileKindByAssetType, isPublicCertificateReportPdf } from '@/lib/certificate-report';
|
||||
import { detectFileKindFromName, getCertificateReportFileKindByAssetType, getDefaultCertificateExpiryDate, isCertificateReportMaterialType, isPublicCertificateReportPdf } from '@/lib/certificate-report';
|
||||
import { DownloadErrorCode } from '@/lib/download-errors';
|
||||
import { getVideoPreviewPolicyFields, userHasVideoEarlyAccess } from '@/lib/video-download-policy';
|
||||
|
||||
@ -364,7 +364,11 @@ export async function GET(request: NextRequest) {
|
||||
reportKind: (material as any).reportKind || null,
|
||||
certificateTestedOn: (material as any).certificateTestedOn || null,
|
||||
certificateNumber: (material as any).certificateNumber || null,
|
||||
certificateExpiresOn: (material as any).certificateExpiresOn || null,
|
||||
certificateExpiresOn: (material as any).certificateExpiresOn || (
|
||||
isCertificateReportMaterialType(material.materialType as unknown as string)
|
||||
? getDefaultCertificateExpiryDate((material as any).certificateTestedOn)
|
||||
: null
|
||||
),
|
||||
reportUsageRules: (material as any).reportUsageRules || null,
|
||||
isAiGenerated: Boolean((material as any).isAiGenerated),
|
||||
taskDetailSourceType: (material as any).taskDetailSourceType || null,
|
||||
|
||||
@ -68,4 +68,13 @@ describe('检测报告初始化', () => {
|
||||
expect(response.status).toBe(400)
|
||||
expect(prisma.material.create).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test.each(['', undefined, null])('到期时间留空时写入按检测日期计算的日期 (%s)', async (certificateExpiresOn) => {
|
||||
const response = await POST(request({ ...input, certificateTestedOn: '2024-02-29', certificateExpiresOn }))
|
||||
expect(response.status).toBe(200)
|
||||
expect(prisma.material.create).toHaveBeenCalledWith({ data: expect.objectContaining({
|
||||
certificateTestedOn: new Date('2024-02-29T00:00:00.000Z'),
|
||||
certificateExpiresOn: new Date('2025-02-28T00:00:00.000Z'),
|
||||
}) })
|
||||
})
|
||||
})
|
||||
|
||||
@ -23,6 +23,7 @@ import {
|
||||
CERTIFICATE_REPORT_KIND,
|
||||
CERTIFICATE_REPORT_PLATFORM_OPTIONS,
|
||||
DEFAULT_CERTIFICATE_REPORT_USAGE_RULES,
|
||||
getDefaultCertificateExpiryDate,
|
||||
isCertificateReportMaterialType,
|
||||
validateCertificateReportMetadata,
|
||||
} from '@/lib/certificate-report'
|
||||
@ -112,6 +113,7 @@ export default function EditMaterialPage({ params }: { params: { id: string } })
|
||||
const [certificateTestedOn, setCertificateTestedOn] = useState('')
|
||||
const [certificateNumber, setCertificateNumber] = useState('')
|
||||
const [certificateExpiresOn, setCertificateExpiresOn] = useState('')
|
||||
const defaultExpiryDate = getDefaultCertificateExpiryDate(certificateTestedOn)?.toISOString().slice(0, 10) || ''
|
||||
const [reportUsageRules, setReportUsageRules] = useState('')
|
||||
const [liveDate, setLiveDate] = useState('')
|
||||
const [liveHost, setLiveHost] = useState('')
|
||||
@ -1069,8 +1071,9 @@ export default function EditMaterialPage({ params }: { params: { id: string } })
|
||||
<p className="mt-1 text-xs text-gray-500">请按报告标注的检测日期选择</p>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor={`edit-material-${params.id}-certificate-expires-on`} className="mb-2 block text-sm font-medium text-gray-700">证书到期时间 *</label>
|
||||
<CertificateDatePicker id={`edit-material-${params.id}-certificate-expires-on`} label="证书到期时间" value={certificateExpiresOn} onChange={setCertificateExpiresOn} disabled={submitting} />
|
||||
<label htmlFor={`edit-material-${params.id}-certificate-expires-on`} className="mb-2 block text-sm font-medium text-gray-700">证书到期时间(选填)</label>
|
||||
<CertificateDatePicker id={`edit-material-${params.id}-certificate-expires-on`} label="证书到期时间" value={certificateExpiresOn} onChange={setCertificateExpiresOn} disabled={submitting} suggestedDate={defaultExpiryDate} placeholder={defaultExpiryDate ? `默认 ${defaultExpiryDate}` : '留空自动计算'} />
|
||||
<p className="mt-1 text-xs text-gray-500">留空按检测日期顺延一年,也可自行选择日期</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@ -240,7 +240,7 @@ describe('certificate report share page', () => {
|
||||
global.fetch = originalFetch
|
||||
})
|
||||
|
||||
test('详情展示证书字段和未拆分的完整说明', async () => {
|
||||
test.each([false, true])('详情展示证书字段、完整说明并按权限提供修改日期入口 (%s)', async (canEdit) => {
|
||||
global.fetch = jest.fn().mockResolvedValue(createJsonResponse({
|
||||
id: 'video-material-1',
|
||||
userId: 'user-1',
|
||||
@ -252,6 +252,7 @@ describe('certificate report share page', () => {
|
||||
certificateTestedOn: '2027-09-23T00:00:00.000Z',
|
||||
certificateNumber: 'CMA-2026-001',
|
||||
certificateUsageInstructions: '使用前核对主体',
|
||||
capabilities: { canEdit, canFavorite: false, canDelete: false },
|
||||
reportUsageRules: '第一段\n第二段',
|
||||
categoryName: '通用',
|
||||
sceneName: '通用',
|
||||
@ -275,6 +276,12 @@ describe('certificate report share page', () => {
|
||||
expect(screen.queryByText('使用前核对主体')).not.toBeInTheDocument()
|
||||
expect(screen.getByRole('heading', { name: '检测报告使用规则说明' }).nextElementSibling?.textContent)
|
||||
.toBe('第一段\n第二段')
|
||||
if (canEdit) {
|
||||
await userEvent.setup().click(screen.getByRole('button', { name: '修改日期' }))
|
||||
expect(mockPush).toHaveBeenCalledWith('/edit/video-material-1')
|
||||
} else {
|
||||
expect(screen.queryByRole('button', { name: '修改日期' })).not.toBeInTheDocument()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -2086,8 +2086,13 @@ export default function SharePage() {
|
||||
<p className="font-en text-[15px] font-medium tabular-nums text-[#1a1c1d]">{material.certificateTestedOn ? String(material.certificateTestedOn).slice(0, 10) : '待补充'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="mb-1 text-[12px] text-[#86898e]">证书到期时间</p>
|
||||
<p className="font-en text-[15px] font-medium tabular-nums text-[#1a1c1d]">{material.certificateExpiresOn ? String(material.certificateExpiresOn).slice(0, 10) : '待补充'}</p>
|
||||
<div className="mb-1 flex flex-wrap items-center justify-between gap-2">
|
||||
<p className="text-[12px] text-[#86898e]">证书到期时间</p>
|
||||
{material.capabilities?.canEdit && (
|
||||
<button type="button" onClick={() => router.push(`/edit/${material.id}`)} className="min-h-9 rounded-[6px] px-2 text-xs font-medium text-[#315f91] hover:bg-[#eef4fb] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#1677ff]">修改日期</button>
|
||||
)}
|
||||
</div>
|
||||
<p className="font-en text-[15px] font-medium tabular-nums text-[#1a1c1d]">{material.certificateExpiresOn ? String(material.certificateExpiresOn).slice(0, 10) : '请先补充检测日期'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="mb-1 text-[12px] text-[#86898e]">证书编号</p>
|
||||
|
||||
@ -18,20 +18,23 @@ interface CertificateDatePickerProps {
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
disabled?: boolean
|
||||
placeholder?: string
|
||||
suggestedDate?: string
|
||||
}
|
||||
|
||||
export default function CertificateDatePicker({ id, label, value, onChange, disabled = false }: CertificateDatePickerProps) {
|
||||
export default function CertificateDatePicker({ id, label, value, onChange, disabled = false, placeholder = '选择日期', suggestedDate = '' }: CertificateDatePickerProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const triggerRef = useRef<HTMLButtonElement>(null)
|
||||
const calendarRef = useRef<HTMLDivElement>(null)
|
||||
const selectedDate = useMemo(() => parseDate(value), [value])
|
||||
const calendarDate = useMemo(() => selectedDate || parseDate(suggestedDate), [selectedDate, suggestedDate])
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [isMonthPickerOpen, setIsMonthPickerOpen] = useState(false)
|
||||
const [visibleMonth, setVisibleMonth] = useState(() => startOfMonth(selectedDate || new Date()))
|
||||
const [visibleMonth, setVisibleMonth] = useState(() => startOfMonth(calendarDate || new Date()))
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedDate) setVisibleMonth(startOfMonth(selectedDate))
|
||||
}, [selectedDate])
|
||||
if (calendarDate) setVisibleMonth(startOfMonth(calendarDate))
|
||||
}, [calendarDate])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) return
|
||||
@ -92,7 +95,7 @@ export default function CertificateDatePicker({ id, label, value, onChange, disa
|
||||
id={id}
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
aria-label={`${label},${selectedDate ? format(selectedDate, 'yyyy年M月d日') : '未选择'}`}
|
||||
aria-label={`${label},${selectedDate ? format(selectedDate, 'yyyy年M月d日') : suggestedDate ? placeholder : '未选择'}`}
|
||||
aria-haspopup="dialog"
|
||||
aria-expanded={isOpen}
|
||||
aria-controls={`${id}-calendar`}
|
||||
@ -103,7 +106,7 @@ export default function CertificateDatePicker({ id, label, value, onChange, disa
|
||||
className="input-apple flex min-h-[50px] items-center justify-between gap-3 text-left disabled:cursor-not-allowed disabled:opacity-60"
|
||||
>
|
||||
<span className={selectedDate ? 'text-[#1a1c1d]' : 'text-gray-500'}>
|
||||
{selectedDate ? format(selectedDate, 'yyyy年M月d日') : '选择日期'}
|
||||
{selectedDate ? format(selectedDate, 'yyyy年M月d日') : placeholder}
|
||||
</span>
|
||||
<CalendarDays className="h-[18px] w-[18px] shrink-0 text-[#5c6470]" aria-hidden="true" />
|
||||
</button>
|
||||
|
||||
@ -5,7 +5,7 @@ import { DEFAULT_CERTIFICATE_REPORT_USAGE_RULES } from '@/lib/certificate-report
|
||||
|
||||
jest.mock('@/lib/chunk-upload', () => ({ chunkUpload: jest.fn() }))
|
||||
|
||||
test.each(['成品', '原材料'])('%s上传只提交一段说明,并分别提交检测日期和到期时间', async (kind) => {
|
||||
test.each([['成品', true], ['成品', false], ['原材料', true], ['原材料', false]] as const)('%s上传说明与日期:手动到期时间 %s', async (kind, manualExpiry) => {
|
||||
const originalFetch = global.fetch
|
||||
jest.useFakeTimers().setSystemTime(new Date('2026-09-27T12:00:00'))
|
||||
const { container } = render(<CertificateReportUploader onSuccess={jest.fn()} onCancel={jest.fn()} productCategories={[{ id: '1', name: '板材' }]} />)
|
||||
@ -17,11 +17,11 @@ test.each(['成品', '原材料'])('%s上传只提交一段说明,并分别提
|
||||
fireEvent.change(screen.getByLabelText('证书编号 *'), { target: { value: 'CMA-001' } })
|
||||
fireEvent.click(screen.getByRole('button', { name: /证书检测日期/ }))
|
||||
fireEvent.click(screen.getByRole('button', { name: '2026年9月23日' }))
|
||||
expect(screen.getByLabelText('证书到期时间(选填)')).toHaveTextContent('默认 2027-09-23')
|
||||
if (manualExpiry) {
|
||||
fireEvent.click(screen.getByRole('button', { name: /证书到期时间/ }))
|
||||
fireEvent.click(screen.getByRole('button', { name: '选择年份和月份' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: '下一年' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: '9月' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: '2027年9月23日' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: '2027年9月24日' }))
|
||||
}
|
||||
expect(screen.queryByLabelText('证书使用说明 *')).not.toBeInTheDocument()
|
||||
expect(screen.getAllByRole('textbox').filter((input) => input.tagName === 'TEXTAREA')).toHaveLength(1)
|
||||
expect(screen.getByLabelText('检测报告使用规则说明 *')).toHaveValue(DEFAULT_CERTIFICATE_REPORT_USAGE_RULES)
|
||||
@ -35,7 +35,7 @@ test.each(['成品', '原材料'])('%s上传只提交一段说明,并分别提
|
||||
const [url, init] = (global.fetch as jest.Mock).mock.calls[0]
|
||||
expect(url).toBe('/api/materials/upload/certificate-report/init')
|
||||
const payload = JSON.parse(init.body)
|
||||
expect(payload).toMatchObject({ reportKind: kind, certificateTestedOn: '2026-09-23', certificateExpiresOn: '2027-09-23', reportUsageRules: DEFAULT_CERTIFICATE_REPORT_USAGE_RULES })
|
||||
expect(payload).toMatchObject({ reportKind: kind, certificateTestedOn: '2026-09-23', certificateExpiresOn: manualExpiry ? '2027-09-24' : '', reportUsageRules: DEFAULT_CERTIFICATE_REPORT_USAGE_RULES })
|
||||
expect(payload).not.toHaveProperty('certificateUsageInstructions')
|
||||
} finally {
|
||||
jest.useRealTimers()
|
||||
|
||||
@ -7,6 +7,7 @@ import {
|
||||
CERTIFICATE_REPORT_PLATFORM_OPTIONS,
|
||||
DEFAULT_CERTIFICATE_REPORT_USAGE_RULES,
|
||||
detectFileKindFromName,
|
||||
getDefaultCertificateExpiryDate,
|
||||
validateCertificateReportMetadata,
|
||||
} from '@/lib/certificate-report'
|
||||
import CertificateDatePicker from '@/components/upload/CertificateDatePicker'
|
||||
@ -42,6 +43,7 @@ export default function CertificateReportUploader({
|
||||
const [certificateTestedOn, setCertificateTestedOn] = useState('')
|
||||
const [certificateNumber, setCertificateNumber] = useState('')
|
||||
const [certificateExpiresOn, setCertificateExpiresOn] = useState('')
|
||||
const defaultExpiryDate = getDefaultCertificateExpiryDate(certificateTestedOn)?.toISOString().slice(0, 10) || ''
|
||||
const [reportUsageRules, setReportUsageRules] = useState(DEFAULT_CERTIFICATE_REPORT_USAGE_RULES)
|
||||
const [productCategoryName, setProductCategoryName] = useState('')
|
||||
const [platformName, setPlatformName] = useState('')
|
||||
@ -351,8 +353,9 @@ export default function CertificateReportUploader({
|
||||
<p className="mt-1 text-xs text-gray-500">请按报告标注的检测日期选择</p>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="certificate-report-expires-on" className={fieldLabelClass}>证书到期时间 *</label>
|
||||
<CertificateDatePicker id="certificate-report-expires-on" label="证书到期时间" value={certificateExpiresOn} onChange={setCertificateExpiresOn} disabled={uploading} />
|
||||
<label htmlFor="certificate-report-expires-on" className={fieldLabelClass}>证书到期时间(选填)</label>
|
||||
<CertificateDatePicker id="certificate-report-expires-on" label="证书到期时间" value={certificateExpiresOn} onChange={setCertificateExpiresOn} disabled={uploading} suggestedDate={defaultExpiryDate} placeholder={defaultExpiryDate ? `默认 ${defaultExpiryDate}` : '留空自动计算'} />
|
||||
<p className="mt-1 text-xs text-gray-500">留空按检测日期顺延一年,也可自行选择日期</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import {
|
||||
DEFAULT_CERTIFICATE_REPORT_USAGE_RULES,
|
||||
getCertificateReportDisplayTitle,
|
||||
getDefaultCertificateExpiryDate,
|
||||
isPublicCertificateReportPdf,
|
||||
parseCertificateDate,
|
||||
validateCertificateReportMetadata,
|
||||
@ -55,8 +56,12 @@ describe('检测报告字段规则', () => {
|
||||
})
|
||||
})
|
||||
|
||||
test('到期时间独立必填,不自动计算,并拒绝早于检测日期的日期', () => {
|
||||
for (const certificateExpiresOn of ['', undefined, '2028-02-30', '2028/09/23']) {
|
||||
test('到期时间留空默认一年,手动填写时仍检查日期和先后顺序', () => {
|
||||
for (const certificateExpiresOn of ['', undefined, null, ' ']) {
|
||||
expect(validateCertificateReportMetadata({ ...metadata, certificateExpiresOn }))
|
||||
.toHaveProperty('value.certificateExpiresOn', new Date('2028-09-23T00:00:00.000Z'))
|
||||
}
|
||||
for (const certificateExpiresOn of ['2028-02-30', '2028/09/23', false, 0]) {
|
||||
expect(validateCertificateReportMetadata({ ...metadata, certificateExpiresOn })).toEqual({ error: '请选择有效的证书到期时间' })
|
||||
}
|
||||
expect(validateCertificateReportMetadata({ ...metadata, certificateExpiresOn: '2027-09-22' }))
|
||||
@ -64,6 +69,24 @@ describe('检测报告字段规则', () => {
|
||||
expect(validateCertificateReportMetadata({ ...metadata, certificateExpiresOn: '2027-09-23' }))
|
||||
.toHaveProperty('value.certificateExpiresOn', new Date('2027-09-23T00:00:00.000Z'))
|
||||
expect(validateCertificateReportMetadata(metadata)).not.toHaveProperty('value.certificateUsageInstructions')
|
||||
expect(validateCertificateReportMetadata({ ...metadata, certificateExpiresOn: '2030-09-23' }))
|
||||
.toHaveProperty('value.certificateExpiresOn', new Date('2030-09-23T00:00:00.000Z'))
|
||||
})
|
||||
|
||||
test.each([
|
||||
['2026-05-14', '2027-05-14'],
|
||||
['2024-02-29', '2025-02-28'],
|
||||
['2023-02-28', '2024-02-28'],
|
||||
['2026-12-31', '2027-12-31'],
|
||||
])('检测日期 %s 顺延一个日历年为 %s', (testedOn, expiresOn) => {
|
||||
expect(getDefaultCertificateExpiryDate(testedOn)?.toISOString().slice(0, 10)).toBe(expiresOn)
|
||||
expect(getDefaultCertificateExpiryDate(new Date(`${testedOn}T00:00:00Z`))?.toISOString().slice(0, 10)).toBe(expiresOn)
|
||||
})
|
||||
|
||||
test('缺少检测日期时不推测到期时间', () => {
|
||||
expect(getDefaultCertificateExpiryDate(null)).toBeNull()
|
||||
expect(getDefaultCertificateExpiryDate('2026-02-30')).toBeNull()
|
||||
expect(getDefaultCertificateExpiryDate(new Date('invalid'))).toBeNull()
|
||||
})
|
||||
|
||||
test('平台分类只接受报告专用选项,整段预置说明保持为一个文本值', () => {
|
||||
|
||||
@ -177,7 +177,7 @@ describe('EditMaterialPage', () => {
|
||||
expect(mockBack).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
test('检测报告编辑按成品和原材料切换字段,规则说明保留为一个输入框', async () => {
|
||||
test.each([false, true])('检测报告编辑并保存日期,清空时恢复默认一年:%s', async (clearExpiry) => {
|
||||
const user = userEvent.setup()
|
||||
const report = {
|
||||
...material,
|
||||
@ -203,7 +203,7 @@ describe('EditMaterialPage', () => {
|
||||
const kindSelect = await screen.findByLabelText('报告类别 *')
|
||||
expect(screen.getByLabelText('SPU')).toHaveValue('SPU-001')
|
||||
expect(screen.getByLabelText('证书检测日期 *')).toHaveTextContent('2027年9月23日')
|
||||
expect(screen.getByLabelText('证书到期时间 *')).toHaveTextContent('2028年9月23日')
|
||||
expect(screen.getByLabelText('证书到期时间(选填)')).toHaveTextContent('2028年9月23日')
|
||||
expect(screen.queryByLabelText('证书使用说明 *')).not.toBeInTheDocument()
|
||||
expect(screen.getByLabelText('检测报告使用规则说明 *')).toHaveValue('第一段\n第二段')
|
||||
expect(screen.getByLabelText('平台分类')).toHaveValue('品牌')
|
||||
@ -211,13 +211,14 @@ describe('EditMaterialPage', () => {
|
||||
await user.selectOptions(kindSelect, '原材料')
|
||||
expect(screen.queryByLabelText('SPU')).not.toBeInTheDocument()
|
||||
expect(screen.getByLabelText('报告名称 / 检测对象 *')).toHaveValue('板材检测报告')
|
||||
await user.click(screen.getByLabelText('证书到期时间 *'))
|
||||
await user.click(screen.getByRole('button', { name: '2028年9月24日' }))
|
||||
await user.click(screen.getByLabelText('证书到期时间(选填)'))
|
||||
await user.click(screen.getByRole('button', { name: clearExpiry ? '清除' : '2028年9月24日' }))
|
||||
if (clearExpiry) expect(screen.getByLabelText('证书到期时间(选填)')).toHaveTextContent('默认 2028-09-23')
|
||||
fireEvent.submit(screen.getByLabelText('报告名称 / 检测对象 *').closest('form')!)
|
||||
await waitFor(() => expect((global.fetch as jest.Mock).mock.calls.some(([, init]) => init?.method === 'PUT')).toBe(true))
|
||||
const [, init] = (global.fetch as jest.Mock).mock.calls.find(([, init]) => init?.method === 'PUT')!
|
||||
expect(init.body.get('certificateTestedOn')).toBe('2027-09-23')
|
||||
expect(init.body.get('certificateExpiresOn')).toBe('2028-09-24')
|
||||
expect(init.body.get('certificateExpiresOn')).toBe(clearExpiry ? '' : '2028-09-24')
|
||||
expect(init.body.get('reportUsageRules')).toBe('第一段\n第二段')
|
||||
expect(init.body.has('certificateUsageInstructions')).toBe(false)
|
||||
})
|
||||
|
||||
@ -123,6 +123,22 @@ describe('material edit routes', () => {
|
||||
return formData
|
||||
}
|
||||
|
||||
test.each(['', '?intent=edit'])('报告详情推算未填到期时间,编辑保留空值以允许随检测日期计算 (%s)', async (query) => {
|
||||
;(prisma.material.findFirst as jest.Mock).mockResolvedValue({
|
||||
id: 'material-1', userId: 'user-1', materialType: '证书-检测报告',
|
||||
productModel: 'SPU-001', authorName: '测试作者', previewUrl: '',
|
||||
categoryName: '通用', sceneName: '通用',
|
||||
certificateTestedOn: new Date('2026-05-14T00:00:00Z'), certificateExpiresOn: null,
|
||||
zipFileName: '', zipFilePath: '', zipFileSize: BigInt(0),
|
||||
createdAt: new Date('2026-05-14T00:00:00Z'), assets: [],
|
||||
})
|
||||
const response = await getMaterialById(new NextRequest(`http://localhost/api/materials/material-1${query}`), { params: { id: 'material-1' } })
|
||||
expect(response.status).toBe(200)
|
||||
const body = await response.json()
|
||||
expect(body.certificateExpiresOn).toBe(query ? null : '2027-05-14T00:00:00.000Z')
|
||||
expect(body.canEdit).toBe(true)
|
||||
})
|
||||
|
||||
function createPutRequest(url: string, formData: FormData) {
|
||||
return new NextRequest(url, {
|
||||
method: 'PUT',
|
||||
|
||||
@ -259,6 +259,30 @@ describe('material-update shared rules', () => {
|
||||
})
|
||||
})
|
||||
|
||||
test('清空到期时间后保存,服务端根据检测日期计算一年', async () => {
|
||||
;(prisma.material.findFirst as jest.Mock).mockResolvedValue({
|
||||
...baseMaterial,
|
||||
materialType: '证书-检测报告',
|
||||
certificateExpiresOn: new Date('2030-01-01T00:00:00Z'),
|
||||
})
|
||||
;(prisma.material.update as jest.Mock).mockImplementation(async ({ data }) => ({ ...baseMaterial, ...data }))
|
||||
const result = await updateMaterialWithSharedRules({
|
||||
materialId: 'material-1', operatorUserId: 'user-1', operatorRole: 'USER',
|
||||
input: {
|
||||
...reportMetadataInput,
|
||||
certificateTestedOn: '2024-02-29',
|
||||
certificateExpiresOn: '',
|
||||
productModel: 'SPU-001', productCategoryName: '板材',
|
||||
materialType: '证书-检测报告', authorName: '测试作者',
|
||||
},
|
||||
})
|
||||
expect('error' in result).toBe(false)
|
||||
expect(prisma.material.update).toHaveBeenCalledWith({ where: { id: 'material-1' }, data: expect.objectContaining({
|
||||
certificateTestedOn: new Date('2024-02-29T00:00:00Z'),
|
||||
certificateExpiresOn: new Date('2025-02-28T00:00:00Z'),
|
||||
}) })
|
||||
})
|
||||
|
||||
test('旧编辑调用未提交报告名称时应保留证书原值', async () => {
|
||||
const existingCertificate = {
|
||||
...baseMaterial,
|
||||
|
||||
@ -125,6 +125,18 @@ describe('materials share security', () => {
|
||||
})
|
||||
})
|
||||
|
||||
test.each([null, new Date('2029-05-14T00:00:00.000Z')])('报告详情为未填到期日的记录计算一年,保留手动日期 (%s)', async (expiresOn) => {
|
||||
;(prisma.material.findFirst as jest.Mock).mockResolvedValue(buildMaterial({
|
||||
materialType: '证书-检测报告',
|
||||
certificateTestedOn: new Date('2026-05-14T00:00:00.000Z'),
|
||||
certificateExpiresOn: expiresOn,
|
||||
}))
|
||||
const response = await GET(new NextRequest('https://sc.jiansun.vip/api/materials/share?id=material-1'))
|
||||
expect(response.status).toBe(200)
|
||||
const body = await response.json()
|
||||
expect(body.certificateExpiresOn).toBe(expiresOn?.toISOString() || '2027-05-14T00:00:00.000Z')
|
||||
})
|
||||
|
||||
test.each([null, { user: { id: 'admin-1' } }])('公开检测报告返回 PDF 和图片,保留源文件隔离 (%j)', async (session) => {
|
||||
;(getServerSession as jest.Mock).mockResolvedValue(session)
|
||||
;(prisma.user.findUnique as jest.Mock).mockResolvedValue({ role: 'SYSTEM_ADMIN' })
|
||||
|
||||
@ -73,12 +73,25 @@ export function parseCertificateDate(value: unknown): Date | null {
|
||||
: parsed
|
||||
}
|
||||
|
||||
export function getDefaultCertificateExpiryDate(testedOn: string | Date | null | undefined): Date | null {
|
||||
const dateText = testedOn instanceof Date
|
||||
? (Number.isNaN(testedOn.getTime()) ? '' : testedOn.toISOString().slice(0, 10))
|
||||
: testedOn?.slice(0, 10)
|
||||
const testedDate = parseCertificateDate(dateText)
|
||||
if (!testedDate) return null
|
||||
const expiresDate = new Date(testedDate)
|
||||
expiresDate.setUTCFullYear(testedDate.getUTCFullYear() + 1)
|
||||
// 闰年 2 月 29 日顺延到次年 2 月末,避免溢出到 3 月。
|
||||
if (expiresDate.getUTCMonth() !== testedDate.getUTCMonth()) expiresDate.setUTCDate(0)
|
||||
return expiresDate
|
||||
}
|
||||
|
||||
export function validateCertificateReportMetadata(input: {
|
||||
reportKind: unknown
|
||||
productModel: unknown
|
||||
reportTitle: unknown
|
||||
certificateTestedOn: unknown
|
||||
certificateExpiresOn: unknown
|
||||
certificateExpiresOn?: unknown
|
||||
certificateNumber: unknown
|
||||
reportUsageRules: unknown
|
||||
platformName: unknown
|
||||
@ -105,7 +118,11 @@ export function validateCertificateReportMetadata(input: {
|
||||
}
|
||||
const certificateTestedOn = parseCertificateDate(input.certificateTestedOn)
|
||||
if (!certificateTestedOn) return { error: '请选择有效的证书检测日期' }
|
||||
const certificateExpiresOn = parseCertificateDate(input.certificateExpiresOn)
|
||||
const expiryIsEmpty = input.certificateExpiresOn == null
|
||||
|| (typeof input.certificateExpiresOn === 'string' && input.certificateExpiresOn.trim() === '')
|
||||
const certificateExpiresOn = expiryIsEmpty
|
||||
? getDefaultCertificateExpiryDate(certificateTestedOn)
|
||||
: parseCertificateDate(input.certificateExpiresOn)
|
||||
if (!certificateExpiresOn) return { error: '请选择有效的证书到期时间' }
|
||||
if (certificateExpiresOn < certificateTestedOn) return { error: '证书到期时间不能早于检测日期' }
|
||||
const certificateNumber = typeof input.certificateNumber === 'string' ? input.certificateNumber.trim() : ''
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user