import { beforeEach, describe, expect, it, vi } from 'vitest' import { cleanupExpiredPostImportSessions, clearPostImportSourceDraft, loadPostImportSession, loadPostImportSourceDraft, savePostImportSession, savePostImportSourceDraft } from '@/lib/postImportSession' import { buildPostImportRow } from '@/test/postImportFactories' describe ('post import storage', () => { beforeEach (() => { sessionStorage.clear () vi.useRealTimers () }) it ('round-trips a valid session and source draft', () => { const row = buildPostImportRow ({ attributes: { duration: '2.5' }, recoverable: true, importStatus: 'pending', validationErrors: { title: ['invalid'] } }) const skipped = buildPostImportRow ({ importStatus: 'skipped', skipReason: 'existing', existingPostId: 10 }) const manual = buildPostImportRow ({ sourceRow: 3, skipReason: 'manual' }) expect (savePostImportSession ({ source: skipped.url, rows: [row, skipped, manual], repairMode: 'all' })).toBe (true) expect (loadPostImportSession ()).toMatchObject ({ version: 2, source: skipped.url, rows: [{ attributes: { duration: '2.5' }, recoverable: true, importStatus: 'pending' }, { importStatus: 'skipped', skipReason: 'existing', existingPostId: 10 }, { skipReason: 'manual' }] }) expect (savePostImportSourceDraft ('https://example.com')).toBe (true) expect (loadPostImportSourceDraft ()).toEqual ({ source: 'https://example.com' }) clearPostImportSourceDraft () expect (loadPostImportSourceDraft ()).toEqual ({ source: '' }) }) it ('rejects inconsistent post IDs and terminal statuses', () => { const session = { version: 2, savedAt: new Date ().toISOString (), source: '', repairMode: 'all', rows: [buildPostImportRow ()] } const invalidRows = [ { ...session.rows[0], skipReason: 'existing', existingPostId: undefined }, { ...session.rows[0], existingPostId: 2, skipReason: undefined }, { ...session.rows[0], importStatus: 'created', createdPostId: undefined }, { ...session.rows[0], importStatus: 'failed', createdPostId: 3 }, { ...session.rows[0], importStatus: 'created', recoverable: true }, { ...session.rows[0], importStatus: 'skipped', recoverable: true }, { ...session.rows[0], recoverable: true }] for (const row of invalidRows) { sessionStorage.setItem ('post-import-session:current', JSON.stringify ({ ...session, rows: [row] })) expect (loadPostImportSession ()).toBeNull () sessionStorage.clear () } }) it ('rejects invalid attribute, provenance, tag-source, and snapshot data', () => { const row = buildPostImportRow () const invalidRows = [ { ...row, attributes: { title: [] } }, { ...row, attributes: { unknown: 'value' } }, { ...row, provenance: { title: 'mapped' } }, { ...row, tagSources: { mapped: 'tag' } }, { ...row, resetSnapshot: { ...row.resetSnapshot, fieldWarnings: { title: 'warning' } } }] invalidRows.forEach (invalidRow => { sessionStorage.setItem ('post-import-session:current', JSON.stringify ({ version: 2, savedAt: new Date ().toISOString (), source: '', rows: [invalidRow], repairMode: 'all' })) expect (loadPostImportSession ()).toBeNull () sessionStorage.clear () }) }) it ('keeps only the current fixed-key session and removes legacy prefixed entries', () => { const current = { version: 2, savedAt: new Date ().toISOString (), source: '', rows: [buildPostImportRow ()], repairMode: 'all' } const expired = { ...current, savedAt: new Date (Date.now () - 25 * 60 * 60 * 1000).toISOString () } sessionStorage.setItem ('post-import-session:current', JSON.stringify (current)) sessionStorage.setItem ('post-import-session:expired', JSON.stringify (expired)) sessionStorage.setItem ('post-import-session:malformed', '{') cleanupExpiredPostImportSessions () expect (sessionStorage.getItem ('post-import-session:current')).not.toBeNull () expect (sessionStorage.getItem ('post-import-session:expired')).toBeNull () expect (sessionStorage.getItem ('post-import-session:malformed')).toBeNull () }) it ('reports storage access failures without throwing', () => { const onError = vi.fn () vi.spyOn (Storage.prototype, 'setItem').mockImplementation (() => { throw new DOMException ('quota') }) expect (savePostImportSourceDraft ('source', onError)).toBe (false) expect (onError).toHaveBeenCalledWith ('ブラウザへ保存できませんでした.') }) })