webdav-backup
Version:
A simple, fast, and efficient tool for backing up files to a WebDAV server
293 lines (231 loc) • 9.73 kB
text/typescript
/**
* Tests for HashCache functionality
*
* These tests focus on verifying the behavior of the HashCache class,
* not the details of its implementation.
*/
import { expect, describe, it, beforeEach, afterEach, mock, spyOn } from 'bun:test';
import { HashCache } from './hash-cache';
import { Verbosity } from '../../interfaces/logger';
import * as logger from '../../utils/logger';
import * as path from 'path';
import fs from 'fs';
import crypto from 'crypto';
// Mock fs module
const mockFs = {
existsSync: mock((path) => true),
promises: {
readFile: mock((path, encoding) => Promise.resolve('{"file1.txt":"hash1","file2.txt":"hash2"}')),
writeFile: mock((path, data) => Promise.resolve())
},
createReadStream: mock((path) => {
const mockStream = {
on: (event, callback) => {
if (event === 'data') {
callback(Buffer.from('mock file content'));
}
if (event === 'end') {
callback();
}
return mockStream;
}
};
return mockStream;
})
};
// Mock crypto module
const mockCrypto = {
createHash: mock(() => {
return {
update: mock(function(data) { return this; }),
digest: mock(() => 'mock-hash-value')
};
})
};
// Create a test-friendly version of the HashCache class
class TestableHashCache extends HashCache {
// Override methods that use file system to use mockable versions instead
async calculateHash(filePath) {
// Use the mock hash calculator instead of the real one
return this._mockCalculateHash ? this._mockCalculateHash(filePath) : `mock-hash-for-${filePath}`;
}
// Set a mock hash calculator for testing
setMockHashCalculator(mockFn) {
this._mockCalculateHash = mockFn;
}
// Create mock load implementation
async load() {
if (this._mockLoadSuccess === false) {
return false;
}
if (this._mockLoadData) {
this.cache = new Map(Object.entries(this._mockLoadData));
return true;
}
return super.load();
}
// Set mock load behavior
setMockLoadBehavior(success, data = null) {
this._mockLoadSuccess = success;
this._mockLoadData = data;
}
// Create mock save implementation
async save() {
return this._mockSaveSuccess !== false;
}
// Set mock save behavior
setMockSaveBehavior(success) {
this._mockSaveSuccess = success;
}
// Helper to get a normalized path, just like in the original implementation
getNormalizedPath(filePath) {
return path.normalize(filePath);
}
}
describe('HashCache', () => {
let loggerSpy;
let fsExistsSyncSpy;
let fsReadFileSpy;
let fsWriteFileSpy;
let fsCreateReadStreamSpy;
let originalCreateHash;
beforeEach(() => {
// Spy on logger to avoid console output during tests
loggerSpy = spyOn(logger, 'verbose').mockImplementation(() => {});
spyOn(logger, 'error').mockImplementation(() => {});
// Mock fs module
fsExistsSyncSpy = spyOn(fs, 'existsSync').mockImplementation(mockFs.existsSync);
fsReadFileSpy = spyOn(fs.promises, 'readFile').mockImplementation(mockFs.promises.readFile);
fsWriteFileSpy = spyOn(fs.promises, 'writeFile').mockImplementation(mockFs.promises.writeFile);
fsCreateReadStreamSpy = spyOn(fs, 'createReadStream').mockImplementation(mockFs.createReadStream);
// Mock crypto module
originalCreateHash = crypto.createHash;
spyOn(crypto, 'createHash').mockImplementation(mockCrypto.createHash);
});
afterEach(() => {
// Restore original implementations
loggerSpy.mockRestore();
fsExistsSyncSpy.mockRestore();
fsReadFileSpy.mockRestore();
fsWriteFileSpy.mockRestore();
fsCreateReadStreamSpy.mockRestore();
crypto.createHash = originalCreateHash;
});
describe('Basic functionality', () => {
it('should initialize with the provided parameters', () => {
const cache = new TestableHashCache('/test/path.json', Verbosity.Verbose);
expect(cache.cachePath).toBe('/test/path.json');
expect(cache.verbosity).toBe(Verbosity.Verbose);
expect(cache.cache.size).toBe(0);
});
it('should use default verbosity when not provided', () => {
const cache = new TestableHashCache('/test/path.json');
expect(cache.verbosity).toBe(Verbosity.Normal);
});
});
// Additional test for the core functionality
it('should calculate a hash for a file', async () => {
const cache = new HashCache('/test/path.json');
const result = await cache.calculateHash('/path/to/file.txt');
expect(result).toBe('mock-hash-value');
expect(fs.createReadStream).toHaveBeenCalledWith('/path/to/file.txt');
expect(crypto.createHash).toHaveBeenCalled();
});
describe('Cache operations', () => {
it('should update a hash in the cache', () => {
const cache = new TestableHashCache('/test/path.json');
const filePath = '/test/file.txt';
const normalizedPath = cache.getNormalizedPath(filePath);
cache.updateHash(filePath, 'test-hash-value');
// The path should be normalized internally
expect(cache.cache.get(normalizedPath)).toBe('test-hash-value');
});
it('should return the correct cache size', () => {
const cache = new TestableHashCache('/test/path.json');
expect(cache.size).toBe(0);
cache.updateHash('file1.txt', 'hash1');
cache.updateHash('file2.txt', 'hash2');
expect(cache.size).toBe(2);
});
it('should load cache data successfully', async () => {
const cache = new TestableHashCache('/test/path.json');
cache.setMockLoadBehavior(true, { 'file1.txt': 'hash1', 'file2.txt': 'hash2' });
const result = await cache.load();
expect(result).toBe(true);
expect(cache.cache.size).toBe(2);
expect(cache.cache.get('file1.txt')).toBe('hash1');
expect(cache.cache.get('file2.txt')).toBe('hash2');
});
it('should handle load failures gracefully', async () => {
const cache = new TestableHashCache('/test/path.json');
cache.setMockLoadBehavior(false);
const result = await cache.load();
expect(result).toBe(false);
});
it('should save the cache successfully', async () => {
const cache = new TestableHashCache('/test/path.json');
cache.setMockSaveBehavior(true);
cache.updateHash('file1.txt', 'hash1');
const result = await cache.save();
expect(result).toBe(true);
});
it('should handle save failures gracefully', async () => {
const cache = new TestableHashCache('/test/path.json');
cache.setMockSaveBehavior(false);
const result = await cache.save();
expect(result).toBe(false);
});
});
describe('File change detection', () => {
it('should detect that a file has changed when hash differs', async () => {
const cache = new TestableHashCache('/test/path.json');
const filePath = '/test/file.txt';
const normalizedPath = cache.getNormalizedPath(filePath);
// Pre-populate the cache with a hash
cache.updateHash(filePath, 'old-hash');
// Configure the hash calculator to return a different hash
cache.setMockHashCalculator(() => 'new-hash');
// Mock save to prevent actual file system operations
cache.setMockSaveBehavior(true);
const hasChanged = await cache.hasChanged(filePath);
expect(hasChanged).toBe(true);
expect(cache.cache.get(normalizedPath)).toBe('new-hash');
});
it('should detect that a file is unchanged when hash matches', async () => {
const cache = new TestableHashCache('/test/path.json');
const filePath = '/test/file.txt';
const normalizedPath = cache.getNormalizedPath(filePath);
const hash = 'same-hash';
// Pre-populate the cache with a hash
cache.updateHash(filePath, hash);
// Configure the hash calculator to return the same hash
cache.setMockHashCalculator(() => hash);
const hasChanged = await cache.hasChanged(filePath);
expect(hasChanged).toBe(false);
expect(cache.cache.get(normalizedPath)).toBe(hash);
});
it('should treat new files as changed', async () => {
const cache = new TestableHashCache('/test/path.json');
const filePath = '/test/new-file.txt';
const normalizedPath = cache.getNormalizedPath(filePath);
// Configure the hash calculator
const hash = 'new-file-hash';
cache.setMockHashCalculator(() => hash);
// Mock save to prevent actual file system operations
cache.setMockSaveBehavior(true);
const hasChanged = await cache.hasChanged(filePath);
expect(hasChanged).toBe(true);
expect(cache.cache.get(normalizedPath)).toBe(hash);
});
it('should handle errors during change detection gracefully', async () => {
const cache = new TestableHashCache('/test/path.json');
// Configure the hash calculator to throw an error
cache.setMockHashCalculator(() => {
throw new Error('Test error');
});
const hasChanged = await cache.hasChanged('/test/file.txt');
// Should assume file has changed if an error occurs
expect(hasChanged).toBe(true);
});
});
});