import { describe, expect, it } from 'vitest'; import { mergeMobileBean, MockGitBackend, syncWithGit, type GitConfig } from '../src/services/sync/gitSync'; import { WebDAVClient, MockFetch, syncWithWebDAV, backupToWebDAV, type WebDAVConfig } from '../src/services/sync/webdavSync'; import { getMonthGrid } from '../src/domain/calendarGrid'; import type { Transaction } from '../src/domain/types'; const tx = (over: Partial): Transaction => ({ id: 't1', date: '2026-07-10', flag: '*', postings: [], tags: [], links: [], source: 'mobile.bean', raw: '', ...over, }); // ============ gitSync ============ describe('gitSync', () => { const config: GitConfig = { remoteUrl: 'https://github.com/x/y.git', branch: 'main', autoSync: true, syncIntervalMin: 30 }; it('内容相同时 noop', async () => { const backend = new MockGitBackend('same-content'); const result = await syncWithGit(backend, config, 'same-content'); expect(result.action).toBe('noop'); }); it('本地有新增内容时 pushed', async () => { const backend = new MockGitBackend('remote-content'); const result = await syncWithGit(backend, config, 'remote-content\n2026-07-13 * "新增"'); expect(result.action).toBe('pushed'); expect(backend.pushed).toBe(true); }); it('mergeMobileBean union 合并', () => { const local = '2026-07-01 * "a"\n A 1 CNY\n B -1 CNY'; const remote = '2026-07-02 * "b"\n C 2 CNY\n D -2 CNY'; const result = mergeMobileBean(local, remote); expect(result.content).toContain('"a"'); expect(result.content).toContain('"b"'); expect(result.conflicts).toHaveLength(0); }); it('mergeMobileBean 相同内容无变化', () => { const content = 'same'; expect(mergeMobileBean(content, content).content).toBe('same'); }); }); // ============ webdavSync ============ describe('webdavSync', () => { const config: WebDAVConfig = { url: 'https://dav.example.com/beancount/', username: 'user', password: 'pass', remotePath: '/mobile.bean', backupKeepCount: 3, }; it('首次(远程无文件)→ pushed', async () => { const mockFetch = new MockFetch(); const client = new WebDAVClient(config, mockFetch.fetch); const result = await syncWithWebDAV(client, 'local-content', '2026-01-01'); expect(result.action).toBe('pushed'); }); it('内容相同 → noop', async () => { const mockFetch = new MockFetch(); mockFetch.setFile('/mobile.bean', 'same'); const client = new WebDAVClient(config, mockFetch.fetch); const result = await syncWithWebDAV(client, 'same', '2026-01-01'); expect(result.action).toBe('noop'); }); it('远程有更新 → conflict', async () => { const mockFetch = new MockFetch(); mockFetch.setFile('/mobile.bean', 'remote-new', '2026-07-14T00:00:00'); const client = new WebDAVClient(config, mockFetch.fetch); const result = await syncWithWebDAV(client, 'local-new', '2026-01-01'); expect(result.action).toBe('conflict'); expect(result.conflict).toBeDefined(); }); it('仅本地变更 → pushed', async () => { const mockFetch = new MockFetch(); mockFetch.setFile('/mobile.bean', 'old', '2020-01-01'); const client = new WebDAVClient(config, mockFetch.fetch); const result = await syncWithWebDAV(client, 'new-local', '2026-07-14T00:00:00'); expect(result.action).toBe('pushed'); }); it('backupToWebDAV 上传备份', async () => { const mockFetch = new MockFetch(); const client = new WebDAVClient(config, mockFetch.fetch); const backupPath = await backupToWebDAV(client, 'backup-content', config); expect(backupPath).toContain('backup-'); }); it('WebDAVClient stat 获取 lastModified', async () => { const mockFetch = new MockFetch(); mockFetch.setFile('/mobile.bean', 'content', '2026-07-13'); const client = new WebDAVClient(config, mockFetch.fetch); const info = await client.stat(); expect(info.exists).toBe(true); }); it('WebDAVClient get 下载', async () => { const mockFetch = new MockFetch(); mockFetch.setFile('/mobile.bean', 'downloaded'); const client = new WebDAVClient(config, mockFetch.fetch); const content = await client.get(); expect(content).toBe('downloaded'); }); }); // ============ CalendarView getMonthGrid ============ describe('CalendarView getMonthGrid', () => { const transactions: Transaction[] = [ tx({ id: 't1', date: '2026-07-05', postings: [{ account: 'Assets:A', amount: '-10', currency: 'CNY' }, { account: 'Expenses:Food', amount: '10', currency: 'CNY' }] }), tx({ id: 't2', date: '2026-07-10', postings: [{ account: 'Assets:A', amount: '100', currency: 'CNY' }, { account: 'Income:Salary', amount: '-100', currency: 'CNY' }] }), tx({ id: 't3', date: '2026-07-15', postings: [{ account: 'Assets:A', amount: '-5', currency: 'CNY' }, { account: 'Expenses:Food', amount: '5', currency: 'CNY' }] }), ]; it('2026年7月日历网格', () => { const weeks = getMonthGrid(2026, 7, transactions); // 至少 5 周 expect(weeks.length).toBeGreaterThanOrEqual(5); // 每周 7 天 for (const week of weeks) { expect(week).toHaveLength(7); } }); it('有交易的日期标记 income/expense', () => { const weeks = getMonthGrid(2026, 7, transactions); const allCells = weeks.flat(); const day5 = allCells.find(c => c.day === 5 && c.isCurrentMonth); expect(day5?.hasExpense).toBe(true); expect(day5?.count).toBe(1); const day10 = allCells.find(c => c.day === 10 && c.isCurrentMonth); expect(day10?.hasIncome).toBe(true); }); it('日期格式为 YYYY-MM-DD', () => { const weeks = getMonthGrid(2026, 7, transactions); const cell = weeks.flat().find(c => c.day === 1 && c.isCurrentMonth); expect(cell?.date).toBe('2026-07-01'); }); it('上下月填充标记 isCurrentMonth=false', () => { const weeks = getMonthGrid(2026, 7, transactions); const firstCell = weeks[0][0]; // 2026-07-01 是周三,前 3 天是 6 月 expect(firstCell.isCurrentMonth).toBe(false); }); });