sha256.test.ts 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
  2. import { mkdtemp, rm, writeFile } from 'node:fs/promises';
  3. import { join } from 'node:path';
  4. import { tmpdir } from 'node:os';
  5. import { computeFileHash, computeStringHash, hashesMatch } from './sha256.js';
  6. // ── computeStringHash ─────────────────────────────────────────────────────────
  7. describe('computeStringHash', () => {
  8. test('returns a 64-char hex string', () => {
  9. const hash = computeStringHash('hello');
  10. expect(hash).toHaveLength(64);
  11. expect(hash).toMatch(/^[0-9a-f]{64}$/);
  12. });
  13. test('is deterministic for the same input', () => {
  14. expect(computeStringHash('hello')).toBe(computeStringHash('hello'));
  15. });
  16. test('differs for different inputs', () => {
  17. expect(computeStringHash('hello')).not.toBe(computeStringHash('world'));
  18. });
  19. test('empty string has a known SHA256', () => {
  20. // SHA256('') = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
  21. expect(computeStringHash('')).toBe(
  22. 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
  23. );
  24. });
  25. });
  26. // ── hashesMatch ───────────────────────────────────────────────────────────────
  27. describe('hashesMatch', () => {
  28. test('returns true for identical hashes', () => {
  29. const h = computeStringHash('test');
  30. expect(hashesMatch(h, h)).toBe(true);
  31. });
  32. test('returns false for different hashes', () => {
  33. expect(hashesMatch(computeStringHash('a'), computeStringHash('b'))).toBe(false);
  34. });
  35. test('is case-insensitive', () => {
  36. const lower = 'abc123def456';
  37. const upper = 'ABC123DEF456';
  38. expect(hashesMatch(lower, upper)).toBe(true);
  39. });
  40. });
  41. // ── computeFileHash ───────────────────────────────────────────────────────────
  42. describe('computeFileHash', () => {
  43. let tmpDir: string;
  44. beforeAll(async () => {
  45. tmpDir = await mkdtemp(join(tmpdir(), 'oac-sha256-test-'));
  46. });
  47. afterAll(async () => {
  48. await rm(tmpDir, { recursive: true, force: true });
  49. });
  50. test('returns the SHA256 of a file matching computeStringHash', async () => {
  51. const content = 'hello world';
  52. const filePath = join(tmpDir, 'test.txt');
  53. await writeFile(filePath, content, 'utf8');
  54. const fileHash = await computeFileHash(filePath);
  55. const stringHash = computeStringHash(content);
  56. expect(fileHash).toBe(stringHash);
  57. });
  58. test('throws a descriptive error for a missing file', async () => {
  59. const missing = join(tmpDir, 'does-not-exist.txt');
  60. await expect(computeFileHash(missing)).rejects.toThrow('computeFileHash: cannot read');
  61. });
  62. test('is deterministic across two reads of the same file', async () => {
  63. const filePath = join(tmpDir, 'stable.txt');
  64. await writeFile(filePath, 'stable content', 'utf8');
  65. const h1 = await computeFileHash(filePath);
  66. const h2 = await computeFileHash(filePath);
  67. expect(h1).toBe(h2);
  68. });
  69. // ✅ Positive: binary file — hash is a valid 64-char hex string
  70. test('returns a valid 64-char hex hash for a binary file', async () => {
  71. // Arrange — write raw bytes (not valid UTF-8 text)
  72. const binaryPath = join(tmpDir, 'binary.bin');
  73. const bytes = new Uint8Array([0x00, 0xff, 0xfe, 0x80, 0x01, 0x7f, 0xab, 0xcd]);
  74. await Bun.write(binaryPath, bytes);
  75. // Act
  76. const hash = await computeFileHash(binaryPath);
  77. // Assert
  78. expect(hash).toHaveLength(64);
  79. expect(hash).toMatch(/^[0-9a-f]{64}$/);
  80. });
  81. // ✅ Positive: binary file hash matches computeStringHash of same bytes
  82. test('binary file hash is consistent with hashing the same byte sequence', async () => {
  83. // Arrange
  84. const binaryPath = join(tmpDir, 'binary-consistent.bin');
  85. const bytes = new Uint8Array([0xde, 0xad, 0xbe, 0xef]);
  86. await Bun.write(binaryPath, bytes);
  87. // Act
  88. const fileHash = await computeFileHash(binaryPath);
  89. // computeStringHash uses utf8 encoding, so we compare against the raw
  90. // crypto hash of the same bytes to verify correctness
  91. const { createHash } = await import('node:crypto');
  92. const expectedHash = createHash('sha256').update(bytes).digest('hex');
  93. // Assert
  94. expect(fileHash).toBe(expectedHash);
  95. });
  96. // ✅ Positive: large file (1 MB) — hash is computed correctly
  97. test('returns a valid hash for a large file (1 MB)', async () => {
  98. // Arrange — 1 MB of repeated bytes
  99. const largePath = join(tmpDir, 'large.bin');
  100. const oneMB = 1024 * 1024;
  101. const largeBytes = new Uint8Array(oneMB).fill(0x42); // 1 MB of 'B'
  102. await Bun.write(largePath, largeBytes);
  103. // Act
  104. const hash = await computeFileHash(largePath);
  105. // Assert
  106. expect(hash).toHaveLength(64);
  107. expect(hash).toMatch(/^[0-9a-f]{64}$/);
  108. // Verify determinism for large file
  109. const hash2 = await computeFileHash(largePath);
  110. expect(hash).toBe(hash2);
  111. });
  112. // ❌ Negative: empty file has the known SHA256 of empty content
  113. test('empty file returns the SHA256 of empty content', async () => {
  114. // Arrange
  115. const emptyPath = join(tmpDir, 'empty.txt');
  116. await writeFile(emptyPath, '', 'utf8');
  117. // Act
  118. const hash = await computeFileHash(emptyPath);
  119. // Assert — SHA256('') is the well-known constant
  120. expect(hash).toBe('e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855');
  121. });
  122. // ❌ Negative: two files with different content have different hashes
  123. test('different file contents produce different hashes', async () => {
  124. // Arrange
  125. const pathA = join(tmpDir, 'diff-a.txt');
  126. const pathB = join(tmpDir, 'diff-b.txt');
  127. await writeFile(pathA, 'content A', 'utf8');
  128. await writeFile(pathB, 'content B', 'utf8');
  129. // Act
  130. const hashA = await computeFileHash(pathA);
  131. const hashB = await computeFileHash(pathB);
  132. // Assert
  133. expect(hashA).not.toBe(hashB);
  134. });
  135. });