228 lines
7.0 KiB
TypeScript
228 lines
7.0 KiB
TypeScript
/**
|
||
* 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')
|
||
})
|
||
})
|
||
})
|