Files
CherryHQ-cherry-studio/tests/main.setup.ts
亢奋猫 622c39ea64 refactor(backup): enhance backup system with legacy backup and path security (#13587)
### What this PR does

Before this PR:
- Backup system used a complex nested structure that was harder to
maintain
- No support for legacy backup format (LAN transfer)
- Limited path security validation
- electron-store config.json was stored in userData root, not included
in backups

After this PR:
- Implements direct backup and restore methods for IndexedDB and Local
Storage
- Adds legacy backup (LAN transfer) functionality for backward
compatibility
- Implements startup restoration support
- Enhances path security with `resolveAndValidatePath` to prevent
directory traversal attacks
- Simplifies BackupManager constructor and methods
- Adds Joplin and Siyuan icons
- Updates backup metadata structure and progress handling
- Moves electron-store config.json to userData/Data directory so it's
included in backups
- Adds automatic migration from legacy config location

Fixes #

### Why we need it and why it was done in this way

The following tradeoffs were made:
- Added path validation overhead for enhanced security
- Legacy backup format support increases code complexity but ensures
backward compatibility

The following alternatives were considered:
- Keeping the old backup structure, but it was harder to maintain and
extend

Links to places where the discussion took place: N/A

### Breaking changes

The backup format has been updated to a new version. Existing backups
created with older versions will still be supported through the legacy
backup functionality.

### Special notes for your reviewer

- The `resolveAndValidatePath` utility prevents path traversal attacks
by validating that resolved paths stay within expected directories
- The BasicDataSettings component was extracted to improve code
organization
- Tests have been updated to cover the new backup functionality
- electron-store config location changed from `userData/config.json` to
`userData/Data/config.json` with automatic migration

### Checklist

This checklist is not enforcing, but it's a reminder of items that could
be relevant to every PR.
Approvers are expected to review this list.

- [x] PR: The PR description is expressive enough and will help future
contributors
- [x] Code: [Write code that humans can
understand](https://en.wikiquote.org/wiki/Martin_Fowler#code-for-humans)
and [Keep it simple](https://en.wikipedia.org/wiki/KISS_principle)
- [x] Refactor: You have [left the code cleaner than you found it (Boy
Scout
Rule)](https://learning.oreilly.com/library/view/97-things-every/9780596809515/ch08.html)
- [x] Upgrade: Impact of this change on upgrade flows was considered and
addressed if required
- [x] Documentation: A [user-guide update](https://docs.cherry-ai.com)
was considered and is present (link) or not required. Check this only
when the PR introduces or changes a user-facing feature or behavior.
- [ ] Self-review: I have reviewed my own code (e.g., via
[`/gh-pr-review`](/.claude/skills/gh-pr-review/SKILL.md), `gh pr diff`,
or GitHub UI) before requesting review from others

### Release note

```release-note
Enhanced backup system with new format (v6), legacy backup support, improved path security, and config.json now included in backups
```

---------

Signed-off-by: kangfenmao <kangfenmao@qq.com>
2026-03-18 19:25:09 +08:00

172 lines
3.8 KiB
TypeScript

import { vi } from 'vitest'
// Mock LoggerService globally for main process tests
vi.mock('@logger', async () => {
const { MockMainLoggerService, mockMainLoggerService } = await import('./__mocks__/MainLoggerService')
return {
LoggerService: MockMainLoggerService,
loggerService: mockMainLoggerService
}
})
// Mock electron modules that are commonly used in main process
vi.mock('electron', () => {
const mock = {
app: {
getPath: vi.fn((key: string) => {
switch (key) {
case 'userData':
return '/mock/userData'
case 'temp':
return '/mock/temp'
case 'logs':
return '/mock/logs'
default:
return '/mock/unknown'
}
}),
getVersion: vi.fn(() => '1.0.0')
},
ipcMain: {
handle: vi.fn(),
on: vi.fn(),
once: vi.fn(),
removeHandler: vi.fn(),
removeAllListeners: vi.fn()
},
BrowserWindow: vi.fn(),
dialog: {
showErrorBox: vi.fn(),
showMessageBox: vi.fn(),
showOpenDialog: vi.fn(),
showSaveDialog: vi.fn()
},
shell: {
openExternal: vi.fn(),
showItemInFolder: vi.fn()
},
session: {
defaultSession: {
clearCache: vi.fn(),
clearStorageData: vi.fn()
}
},
webContents: {
getAllWebContents: vi.fn(() => [])
},
systemPreferences: {
getMediaAccessStatus: vi.fn(),
askForMediaAccess: vi.fn()
},
nativeTheme: {
themeSource: 'system',
shouldUseDarkColors: false,
on: vi.fn(),
removeListener: vi.fn()
},
screen: {
getPrimaryDisplay: vi.fn(),
getAllDisplays: vi.fn()
},
Notification: vi.fn()
}
return { __esModule: true, ...mock, default: mock }
})
// Mock Winston for LoggerService dependencies
vi.mock('winston', () => ({
createLogger: vi.fn(() => ({
log: vi.fn(),
error: vi.fn(),
warn: vi.fn(),
info: vi.fn(),
debug: vi.fn(),
level: 'info',
on: vi.fn(),
end: vi.fn()
})),
format: {
combine: vi.fn(),
splat: vi.fn(),
timestamp: vi.fn(),
errors: vi.fn(),
json: vi.fn()
},
transports: {
Console: vi.fn(),
File: vi.fn()
}
}))
// Mock winston-daily-rotate-file
vi.mock('winston-daily-rotate-file', () => {
return vi.fn().mockImplementation(() => ({
on: vi.fn(),
log: vi.fn()
}))
})
// Mock electron-store to avoid file system operations
vi.mock('electron-store', () => {
return {
default: vi.fn().mockImplementation(() => ({
get: vi.fn((key: string, defaultValue?: unknown) => defaultValue),
set: vi.fn(),
delete: vi.fn(),
clear: vi.fn(),
has: vi.fn(() => false),
store: {}
}))
}
})
// Mock Node.js modules
vi.mock('node:os', () => {
const mock = {
platform: vi.fn(() => 'darwin'),
arch: vi.fn(() => 'x64'),
version: vi.fn(() => '20.0.0'),
cpus: vi.fn(() => [{ model: 'Mock CPU' }]),
homedir: vi.fn(() => '/mock/home'),
totalmem: vi.fn(() => 8 * 1024 * 1024 * 1024) // 8GB
}
return { ...mock, default: mock }
})
vi.mock('node:path', async () => {
const actual = await vi.importActual('node:path')
return {
...actual,
join: vi.fn((...args: string[]) => args.join('/')),
resolve: vi.fn((...args: string[]) => args.join('/'))
}
})
vi.mock('node:fs', () => {
const mock = {
promises: {
access: vi.fn(),
readFile: vi.fn(),
writeFile: vi.fn(),
mkdir: vi.fn(),
readdir: vi.fn(),
stat: vi.fn(),
unlink: vi.fn(),
rmdir: vi.fn()
},
existsSync: vi.fn(),
readFileSync: vi.fn(),
writeFileSync: vi.fn(),
mkdirSync: vi.fn(),
readdirSync: vi.fn(),
statSync: vi.fn(),
unlinkSync: vi.fn(),
rmdirSync: vi.fn(),
createReadStream: vi.fn(),
createWriteStream: vi.fn()
}
return { ...mock, default: mock }
})