gongxiangp2/lib/__tests__/disk-space.test.ts

228 lines
7.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* R38修复:磁盘空间检查测试
*
* 测试内容:
* - formatBytes 函数
* - Node statfs 跨平台磁盘空间读取
* - getDiskUsageLevel / hasEnoughDiskSpace / ensureEnoughDiskSpace 逻辑
*/
import { statfs } from 'fs/promises'
import {
checkDiskSpace,
ensureEnoughDiskSpace,
formatBytes,
getDiskUsageLevel,
hasEnoughDiskSpace,
} from '../disk-space'
jest.mock('fs/promises', () => {
const originalModule = jest.requireActual('fs/promises')
return {
...originalModule,
statfs: jest.fn(),
}
})
const mockedStatfs = statfs as jest.MockedFunction<typeof statfs>
const GIBIBYTE = BigInt(1024) * BigInt(1024) * BigInt(1024)
function mockSpace(total: bigint, free: bigint, available = free, blockSize = BigInt(1)) {
mockedStatfs.mockResolvedValueOnce({
type: BigInt(0),
bsize: blockSize,
blocks: total / blockSize,
bfree: free / blockSize,
bavail: available / blockSize,
files: BigInt(0),
ffree: BigInt(0),
})
}
describe('R38: disk-space', () => {
beforeEach(() => {
jest.clearAllMocks()
})
describe('checkDiskSpace', () => {
it('使用 Node statfs 读取字节数,不依赖系统 df 命令', async () => {
mockSpace(BigInt(100_000), BigInt(80_000), BigInt(75_000))
await expect(checkDiskSpace('/app/uploads/transcoded')).resolves.toEqual({
total: 100_000,
used: 20_000,
available: 75_000,
usePercent: 22,
mountPoint: '/app/uploads/transcoded',
})
expect(mockedStatfs).toHaveBeenCalledWith('/app/uploads/transcoded', { bigint: true })
})
it('按块大小和非特权可用块计算并向上取整使用率', async () => {
mockSpace(BigInt(12_288), BigInt(8_192), BigInt(4_096), BigInt(4_096))
const result = await checkDiskSpace('/app/uploads')
expect(result).toMatchObject({
total: 12_288,
used: 4_096,
available: 4_096,
usePercent: 50,
})
})
it('不把文件系统保留块算作 nextjs 可用空间', async () => {
mockSpace(BigInt(100), BigInt(20), BigInt(10))
await expect(checkDiskSpace('/app/uploads')).resolves.toMatchObject({
total: 100,
used: 80,
available: 10,
usePercent: 89,
})
})
it('超出安全整数范围时钳制数值,避免精度溢出绕过空间门禁', async () => {
const huge = BigInt(Number.MAX_SAFE_INTEGER) + BigInt(10_000)
mockSpace(huge, huge, huge)
const result = await checkDiskSpace('/app/uploads')
expect(result?.total).toBe(Number.MAX_SAFE_INTEGER)
expect(result?.available).toBe(Number.MAX_SAFE_INTEGER)
expect(result?.used).toBe(0)
})
it('空文件系统统计不会产生除零或非有限使用率', async () => {
mockSpace(BigInt(0), BigInt(0), BigInt(0))
await expect(checkDiskSpace('/app/uploads')).resolves.toMatchObject({
total: 0,
used: 0,
available: 0,
usePercent: 0,
})
})
it('statfs 失败时返回 null,让严格调用方保持 fail-closed', async () => {
mockedStatfs.mockRejectedValueOnce(Object.assign(new Error('stale mount'), { code: 'ESTALE' }))
await expect(checkDiskSpace('/app/uploads')).resolves.toBeNull()
})
})
describe('formatBytes', () => {
it('应该正确格式化字节数', () => {
expect(formatBytes(0)).toBe('0.00 B')
expect(formatBytes(1024)).toBe('1.00 KB')
expect(formatBytes(1024 * 1024)).toBe('1.00 MB')
expect(formatBytes(1024 * 1024 * 1024)).toBe('1.00 GB')
expect(formatBytes(1024 * 1024 * 1024 * 1024)).toBe('1.00 TB')
expect(formatBytes(1536)).toBe('1.50 KB')
})
})
describe('getDiskUsageLevel - 逻辑验证', () => {
it('使用率低于 70% 应该返回 ok', async () => {
mockSpace(BigInt(100_000), BigInt(40_000))
const result = await getDiskUsageLevel()
expect(result).toBe('ok')
})
it('使用率在 70%-90% 之间应该返回 warning', async () => {
mockSpace(BigInt(100_000), BigInt(25_000))
const result = await getDiskUsageLevel()
expect(result).toBe('warning')
})
it('使用率大于 90% 应该返回 critical', async () => {
mockSpace(BigInt(100_000), BigInt(5_000))
const result = await getDiskUsageLevel()
expect(result).toBe('critical')
})
it('无法检查时应该返回 ok', async () => {
mockedStatfs.mockRejectedValueOnce(new Error('statfs failed'))
const result = await getDiskUsageLevel()
expect(result).toBe('ok')
})
})
describe('hasEnoughDiskSpace - 逻辑验证', () => {
it('当空间足够时应该返回 true', async () => {
mockSpace(BigInt(100) * GIBIBYTE, BigInt(80) * GIBIBYTE)
const result = await hasEnoughDiskSpace(10 * 1024 * 1024 * 1024)
expect(result).toBe(true)
})
it('当可用空间不足时应该返回 false', async () => {
mockSpace(BigInt(100) * GIBIBYTE, BigInt(10) * GIBIBYTE)
const result = await hasEnoughDiskSpace(20 * 1024 * 1024 * 1024)
expect(result).toBe(false)
})
it('当磁盘使用率过高时应该返回 false', async () => {
mockSpace(BigInt(100) * GIBIBYTE, BigInt(5) * GIBIBYTE)
const result = await hasEnoughDiskSpace(100 * 1024 * 1024)
expect(result).toBe(false)
})
it('当无法检查时应该返回 true(保守处理)', async () => {
mockedStatfs.mockRejectedValueOnce(new Error('statfs failed'))
const result = await hasEnoughDiskSpace(10 * 1024 * 1024 * 1024)
expect(result).toBe(true)
})
})
describe('ensureEnoughDiskSpace - 逻辑验证', () => {
it('当空间足够时不应该抛出错误', async () => {
mockSpace(BigInt(100) * GIBIBYTE, BigInt(80) * GIBIBYTE)
await expect(ensureEnoughDiskSpace(10 * 1024 * 1024 * 1024, '测试操作')).resolves.not.toThrow()
})
it('当空间不足时应该抛出错误', async () => {
mockSpace(BigInt(100) * GIBIBYTE, BigInt(10) * GIBIBYTE)
await expect(ensureEnoughDiskSpace(20 * 1024 * 1024 * 1024, '测试操作')).rejects.toThrow('磁盘空间不足')
})
it('当无法检查时不应该抛出错误', async () => {
mockedStatfs.mockRejectedValueOnce(new Error('statfs failed'))
await expect(ensureEnoughDiskSpace(10 * 1024 * 1024 * 1024, '测试操作')).resolves.not.toThrow()
})
})
describe('数值计算验证', () => {
it('应该正确计算使用率百分比', () => {
const total = 100000000000
const used = 70000000000
const usePercent = (used / total) * 100
expect(usePercent).toBe(70)
})
it('应该正确计算保留空间', () => {
const total = 100000000000
const usePercent = 90
const minReserved = Math.max(total * 0.1, 5 * 1024 * 1024 * 1024)
expect(minReserved).toBeGreaterThanOrEqual(5 * 1024 * 1024 * 1024) // 至少 5GB
})
it('应该正确格式化字节大小', () => {
const bytes = 10 * 1024 * 1024 * 1024
expect(formatBytes(bytes)).toBe('10.00 GB')
})
})
})