Files
order_site/apps/backend/src/db-backup.test.ts
T

203 lines
5.8 KiB
TypeScript

import assert from 'node:assert/strict'
import test from 'node:test'
import { Readable } from 'node:stream'
import {
manifestKeyFor,
parseBackupArgs,
pickLatestDumpKey,
resolveBackupStorageConfig,
runDownload,
runLatest,
runList,
runUpload,
} from './db-backup.js'
import type { StorageObjectSummary } from './services/file-storage/object-storage.js'
test('resolveBackupStorageConfig 优先 DB_BACKUP_OSS_* 并按字段回退 STORAGE_OSS_*', () => {
const config = resolveBackupStorageConfig({
DB_BACKUP_OSS_ENDPOINT: 'https://backup.example.com',
STORAGE_OSS_ENDPOINT: 'https://storage.example.com',
STORAGE_OSS_BUCKET: 'order-site',
STORAGE_OSS_ACCESS_KEY_ID: 'storage-key',
DB_BACKUP_OSS_ACCESS_KEY_ID: 'backup-key',
STORAGE_OSS_SECRET_ACCESS_KEY: 'storage-secret',
})
assert.equal(config.endpoint, 'https://backup.example.com')
assert.equal(config.bucket, 'order-site')
assert.equal(config.accessKeyId, 'backup-key')
assert.equal(config.secretAccessKey, 'storage-secret')
assert.equal(config.region, 'oss-cn-hangzhou')
})
test('resolveBackupStorageConfig 全部未配置时返回空值由调用方校验', () => {
const config = resolveBackupStorageConfig({})
assert.equal(config.endpoint, '')
assert.equal(config.bucket, '')
})
test('parseBackupArgs 解析命令与参数并去掉前缀尾部斜杠', () => {
const args = parseBackupArgs([
'upload',
'--key',
'backups/postgres/order_site-20260824-030000.dump',
'--prefix',
'backups/postgres/',
'--kind',
'app-configs',
'--size',
'1234',
])
assert.equal(args.command, 'upload')
assert.equal(args.key, 'backups/postgres/order_site-20260824-030000.dump')
assert.equal(args.prefix, 'backups/postgres')
assert.equal(args.kind, 'app-configs')
assert.equal(args.size, 1234)
})
test('parseBackupArgs 无参数时使用默认前缀与类型', () => {
const args = parseBackupArgs(['list'])
assert.equal(args.command, 'list')
assert.equal(args.prefix, 'backups/postgres')
assert.equal(args.kind, 'postgres-dump')
assert.equal(args.key, '')
assert.equal(args.allowUnverified, false)
})
test('parseBackupArgs 支持显式兼容未校验备份', () => {
assert.equal(parseBackupArgs(['latest', '--allow-unverified']).allowUnverified, true)
})
test('manifestKeyFor 在备份 key 后追加清单后缀', () => {
assert.equal(manifestKeyFor('backups/postgres/a.dump'), 'backups/postgres/a.dump.manifest.json')
})
test('pickLatestDumpKey 只认 dump 并取最近修改的对象', () => {
const key = pickLatestDumpKey([
{
key: 'backups/postgres/a.dump',
size: 1,
etag: '',
lastModified: new Date('2026-08-01T00:00:00Z'),
},
{
key: 'backups/postgres/a.dump.manifest.json',
size: 1,
etag: '',
lastModified: new Date('2026-08-23T00:00:00Z'),
},
{
key: 'backups/configs/a.tar.gz',
size: 1,
etag: '',
lastModified: new Date('2026-08-23T00:00:00Z'),
},
{
key: 'backups/postgres/b.dump',
size: 1,
etag: '',
lastModified: new Date('2026-08-22T00:00:00Z'),
},
])
assert.equal(key, 'backups/postgres/b.dump')
})
test('pickLatestDumpKey 无备份时返回 null,无时间时按 key 兜底', () => {
assert.equal(pickLatestDumpKey([]), null)
assert.equal(
pickLatestDumpKey([
{ key: 'backups/postgres/order_site-1.dump', size: 1, etag: '' },
{ key: 'backups/postgres/order_site-2.dump', size: 1, etag: '' },
]),
'backups/postgres/order_site-2.dump',
)
})
test('runUpload 支持流式输入并生成带长度和 sha256 的 manifest', async () => {
const uploads: Array<{ key: string; content: Buffer; contentLength?: number }> = []
const storage = {
putObject: async (input: {
key: string
content: Buffer | Readable
contentLength?: number
}) => {
const chunks: Buffer[] = []
if (Buffer.isBuffer(input.content)) {
chunks.push(input.content)
} else {
for await (const chunk of input.content) chunks.push(Buffer.from(chunk))
}
uploads.push({
key: input.key,
content: Buffer.concat(chunks),
contentLength: input.contentLength,
})
},
} as never
await runUpload(
storage,
parseBackupArgs([
'upload',
'--key',
'backups/configs/a.tar.gz',
'--kind',
'app-configs',
'--size',
'5',
]),
Readable.from([Buffer.from('hello')]),
)
assert.equal(uploads.length, 2)
assert.equal(uploads[0]?.content.toString(), 'hello')
assert.equal(uploads[0]?.contentLength, 5)
assert.match(uploads[1]?.content.toString() || '', /"size": 5/)
assert.match(uploads[1]?.content.toString() || '', /sha256/)
})
test('runLatest 跳过没有 manifest 的新对象并选择最近的有效备份', async () => {
const objects: StorageObjectSummary[] = [
{
key: 'backups/postgres/new.dump',
size: 5,
etag: '',
lastModified: new Date('2026-08-23T03:00:00Z'),
},
{
key: 'backups/postgres/valid.dump',
size: 5,
etag: '',
lastModified: new Date('2026-08-22T03:00:00Z'),
},
]
const storage = {
listObjects: async () => objects,
getObject: async (key: string) => {
if (key.endsWith('new.dump.manifest.json')) {
const error = Object.assign(new Error('missing'), { name: 'NoSuchKey' })
throw error
}
return {
reader: Readable.from([
JSON.stringify({
key: 'backups/postgres/valid.dump',
kind: 'postgres-dump',
size: 5,
sha256: 'a'.repeat(64),
createdAt: '2026-08-22T03:00:00Z',
}),
]),
}
},
} as never
assert.equal(
await runLatest(storage, parseBackupArgs(['latest', '--prefix', 'backups/postgres'])),
'backups/postgres/valid.dump',
)
})