atomic-write.spec.ts 2.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748
  1. import { lstat, mkdir, mkdtemp, readFile, readdir, stat, symlink, writeFile } from 'node:fs/promises'
  2. import { tmpdir } from 'node:os'
  3. import { join } from 'node:path'
  4. import { describe, expect, it } from 'vitest'
  5. import { writeFileAtomic } from '../src/index.ts'
  6. async function scratch(): Promise<string> {
  7. return mkdtemp(join(tmpdir(), 'dsh-atomic-write-'))
  8. }
  9. describe('writeFileAtomic', () => {
  10. it('creates the file and its parents with exactly the stated mode', async () => {
  11. const dir = await scratch()
  12. const target = join(dir, 'nested', 'deep', 'doc.yaml')
  13. await writeFileAtomic(target, 'a: 1\n', { mode: 0o600 })
  14. expect(await readFile(target, 'utf8')).toBe('a: 1\n')
  15. expect((await stat(target)).mode & 0o777).toBe(0o600)
  16. })
  17. it('replaces existing content and narrows a wider-permission file to the stated mode', async () => {
  18. const dir = await scratch()
  19. const target = join(dir, 'doc.yaml')
  20. await writeFile(target, 'old', { mode: 0o644 })
  21. await writeFileAtomic(target, 'new', { mode: 0o600 })
  22. expect(await readFile(target, 'utf8')).toBe('new')
  23. expect((await stat(target)).mode & 0o777).toBe(0o600)
  24. })
  25. it('replaces a symlinked target itself without writing through to the referent', async () => {
  26. const dir = await scratch()
  27. const victim = join(dir, 'victim')
  28. await writeFile(victim, 'victim-content')
  29. const target = join(dir, 'doc.yaml')
  30. await symlink(victim, target)
  31. await writeFileAtomic(target, 'replaced', { mode: 0o600 })
  32. expect((await lstat(target)).isSymbolicLink()).toBe(false)
  33. expect(await readFile(target, 'utf8')).toBe('replaced')
  34. expect(await readFile(victim, 'utf8')).toBe('victim-content')
  35. })
  36. it('leaves no temp sibling and rethrows when the rename fails', async () => {
  37. const dir = await scratch()
  38. const target = join(dir, 'occupied')
  39. await mkdir(target)
  40. await expect(writeFileAtomic(target, 'content', { mode: 0o600 })).rejects.toThrow()
  41. expect((await readdir(dir)).filter(entry => entry.includes('.tmp'))).toEqual([])
  42. })
  43. })