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', ) })