109 lines
3.2 KiB
TypeScript
109 lines
3.2 KiB
TypeScript
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import {
|
|
parseImportedPortableFile,
|
|
PortabilityError,
|
|
} from '../src/features/data-portability';
|
|
|
|
const OWNER = '4f642f87-2d12-4dcf-9aa1-f4427d4078a8';
|
|
const FIXTURE_ROOT = path.join(__dirname, 'fixtures', 'data-portability');
|
|
|
|
function fixture(name: string): string {
|
|
return fs.readFileSync(path.join(FIXTURE_ROOT, name), 'utf8');
|
|
}
|
|
|
|
describe('desktop and web legacy portability', () => {
|
|
it('imports the declared desktop dictionary JSON shape deterministically', () => {
|
|
const raw = fixture('desktop-dictionary.json');
|
|
const first = parseImportedPortableFile(
|
|
raw,
|
|
'dictionary.json',
|
|
'application/json',
|
|
OWNER,
|
|
'2026-08-21T00:00:00.000Z',
|
|
);
|
|
const second = parseImportedPortableFile(
|
|
raw,
|
|
'dictionary.json',
|
|
'application/json',
|
|
OWNER,
|
|
'2026-08-21T00:00:00.000Z',
|
|
);
|
|
|
|
expect(first.detectedFormat).toBe('legacy-json');
|
|
expect(first.payload.datasets.dictionary).toHaveLength(2);
|
|
expect(first.payload.datasets.dictionary[0].usage_count).toBe(7);
|
|
expect(first.archive.checksum).toBe(second.archive.checksum);
|
|
expect(first.payload.datasets.dictionary[1].id).toBe(
|
|
second.payload.datasets.dictionary[1].id,
|
|
);
|
|
});
|
|
|
|
it('parses RFC4180 commas and escaped quotes in dictionary CSV', () => {
|
|
const result = parseImportedPortableFile(
|
|
fixture('desktop-dictionary.csv'),
|
|
'dictionary.csv',
|
|
'text/csv',
|
|
OWNER,
|
|
'2026-08-21T00:00:00.000Z',
|
|
);
|
|
expect(result.detectedFormat).toBe('dictionary-csv');
|
|
expect(result.payload.datasets.dictionary.map(row => row.word)).toEqual([
|
|
'쉼표, 포함',
|
|
'따옴표 "포함"',
|
|
]);
|
|
});
|
|
|
|
it('converts the real desktop meeting Markdown markers', () => {
|
|
const result = parseImportedPortableFile(
|
|
fixture('desktop-meeting.md'),
|
|
'meeting.md',
|
|
'text/markdown',
|
|
OWNER,
|
|
'2026-08-21T00:00:00.000Z',
|
|
);
|
|
const meeting = result.payload.datasets.meetings[0];
|
|
expect(result.detectedFormat).toBe('desktop-meeting-markdown');
|
|
expect(meeting.title).toBe('모바일 출시 점검');
|
|
expect(meeting.minutes_markdown).toContain('다른 사용자 데이터는 거부한다');
|
|
expect(meeting.raw_transcript).toContain('전체를 롤백합니다');
|
|
});
|
|
|
|
it('rejects a legacy file that declares a different owner', () => {
|
|
expect(() =>
|
|
parseImportedPortableFile(
|
|
fixture('foreign-owner.json'),
|
|
'foreign.json',
|
|
'application/json',
|
|
OWNER,
|
|
),
|
|
).toThrow(PortabilityError);
|
|
try {
|
|
parseImportedPortableFile(
|
|
fixture('foreign-owner.json'),
|
|
'foreign.json',
|
|
'application/json',
|
|
OWNER,
|
|
);
|
|
} catch (error) {
|
|
expect((error as PortabilityError).code).toBe('owner');
|
|
}
|
|
});
|
|
|
|
it('fails closed when one row in a multi-row file is invalid', () => {
|
|
const malformed = JSON.stringify({
|
|
entries: [
|
|
{ word: 'valid', category: 'user' },
|
|
{ word: '', category: 'user' },
|
|
],
|
|
});
|
|
expect(() =>
|
|
parseImportedPortableFile(
|
|
malformed,
|
|
'dictionary.json',
|
|
'application/json',
|
|
OWNER,
|
|
),
|
|
).toThrow('invalid length');
|
|
});
|
|
});
|