147 lines
No EOL
5.8 KiB
TypeScript
147 lines
No EOL
5.8 KiB
TypeScript
import path from 'path';
|
|
import NodeJose from 'node-jose';
|
|
import { jest, describe, test, expect, beforeEach } from '@jest/globals';
|
|
import type { FileKeystore } from '../src/FileKeystore.js'; // Import TYPE only (safe in ESM)
|
|
|
|
// We use unstable_mockModule because standard jest.mock struggles to intercept
|
|
// core Node modules (like fs) in strict ESM mode.
|
|
jest.unstable_mockModule('fs', () => ({
|
|
// We must mock the 'default' export because your code uses: import fs from 'fs'
|
|
default: {
|
|
existsSync: jest.fn(),
|
|
mkdirSync: jest.fn(),
|
|
writeFileSync: jest.fn(),
|
|
readFileSync: jest.fn(),
|
|
},
|
|
// We also mock named exports purely for safety, though 'default' is the key one here
|
|
existsSync: jest.fn(),
|
|
mkdirSync: jest.fn(),
|
|
writeFileSync: jest.fn(),
|
|
readFileSync: jest.fn(),
|
|
}));
|
|
|
|
// Mock dependency
|
|
jest.unstable_mockModule('@BridgemanAccessible/ba-auth/keystore', () => ({
|
|
BaseKeystore: class {},
|
|
Keystore: {
|
|
addKeystoreType: jest.fn(),
|
|
},
|
|
}));
|
|
|
|
describe('FileKeystore', () => {
|
|
// Variables to hold our dynamically imported modules
|
|
let FileKeystoreClass: typeof FileKeystore;
|
|
let mockFs: any;
|
|
|
|
const TEST_DIR = '.test-cert';
|
|
const TEST_FILE = 'test-keystore.json';
|
|
const FULL_PATH = path.join(TEST_DIR, TEST_FILE);
|
|
|
|
beforeEach(async () => {
|
|
jest.clearAllMocks();
|
|
|
|
// 3. DYNAMICALLY IMPORT THE MODULES
|
|
// This is the magic. We import AFTER the mock is defined.
|
|
// This ensures 'fs' inside FileKeystore.ts is actually our mock.
|
|
const fsModule = await import('fs');
|
|
mockFs = fsModule.default; // Access the default export we mocked above
|
|
|
|
// Import your class under test
|
|
// Note: You might need to add '.js' to the path if your resolving is strict
|
|
const module = await import('../src/FileKeystore.js');
|
|
FileKeystoreClass = module.FileKeystore;
|
|
});
|
|
|
|
test('should create a NEW keystore if the file does not exist', async () => {
|
|
// SETUP:
|
|
// 1. Pretend the file does NOT exist
|
|
mockFs.existsSync.mockReturnValue(false);
|
|
// 2. Mock writeFileSync to do nothing (just verify it was called)
|
|
mockFs.writeFileSync.mockImplementation(() => {});
|
|
// 3. Mock mkdirSync
|
|
mockFs.mkdirSync.mockImplementation(() => undefined);
|
|
|
|
// EXECUTE
|
|
const keystoreInstance = await FileKeystoreClass.create(TEST_FILE, TEST_DIR);
|
|
|
|
// VERIFY
|
|
// 1. Verify it checked for file existence
|
|
expect(mockFs.existsSync).toHaveBeenCalledWith(FULL_PATH);
|
|
|
|
// 2. Verify it checked for directory existence (and created it)
|
|
expect(mockFs.mkdirSync).toHaveBeenCalledWith(TEST_DIR);
|
|
|
|
// 3. Verify it wrote the new keystore to disk
|
|
// We expect the first argument to be the path, and the second to be a JSON string
|
|
expect(mockFs.writeFileSync).toHaveBeenCalledWith(
|
|
FULL_PATH,
|
|
expect.stringContaining('"keys":') // Simple check to ensure it looks like JSON
|
|
);
|
|
|
|
// 4. Verify the instance path is set correctly
|
|
expect(keystoreInstance.getKeystoreFilePath()).toBe(FULL_PATH);
|
|
|
|
// 5. Verify internal keystore state (node-jose should have generated keys)
|
|
// @ts-ignore - accessing public property 'keystore' which might be protected in your real usage,
|
|
// but implied public in the provided snippet logic
|
|
const keys = keystoreInstance.keystore.all();
|
|
expect(keys.length).toBeGreaterThanOrEqual(1);
|
|
});
|
|
|
|
test('should LOAD an existing keystore if the file exists', async () => {
|
|
// SETUP:
|
|
// 1. Create a real temporary keystore using node-jose to mimic a file on disk
|
|
const realKeystore = await NodeJose.JWK.createKeyStore().generate('RSA', 2048);
|
|
const keystoreJson = JSON.stringify(realKeystore.keystore.toJSON(true));
|
|
|
|
// 2. Pretend the file DOES exist
|
|
mockFs.existsSync.mockReturnValue(true);
|
|
|
|
// 3. Pretend readFileSync returns our JSON string
|
|
mockFs.readFileSync.mockReturnValue(keystoreJson);
|
|
|
|
// EXECUTE
|
|
const keystoreInstance = await FileKeystoreClass.create(TEST_FILE, TEST_DIR);
|
|
|
|
// VERIFY
|
|
// 1. It should check existence
|
|
expect(mockFs.existsSync).toHaveBeenCalledWith(FULL_PATH);
|
|
|
|
// 2. It should READ the file
|
|
expect(mockFs.readFileSync).toHaveBeenCalledWith(FULL_PATH, 'utf-8');
|
|
|
|
// 3. CRITICAL: It should NOT overwrite the file
|
|
expect(mockFs.writeFileSync).not.toHaveBeenCalled();
|
|
|
|
// 4. CRITICAL: It should NOT try to create the directory again
|
|
// (The code checks !existsSync(file), so it goes into the else block)
|
|
expect(mockFs.mkdirSync).not.toHaveBeenCalled();
|
|
|
|
// 5. Verify the loaded keys match what we "mocked" on disk
|
|
// @ts-ignore
|
|
const loadedKeys = keystoreInstance.keystore.all();
|
|
expect(loadedKeys.length).toBe(1);
|
|
});
|
|
|
|
test('should create directory only if it is missing', async () => {
|
|
// Scenario: File missing, Directory also missing
|
|
mockFs.existsSync
|
|
.mockReturnValueOnce(false) // First check: File exists? No.
|
|
.mockReturnValueOnce(false); // Second check: Dir exists? No.
|
|
|
|
await FileKeystoreClass.create(TEST_FILE, TEST_DIR);
|
|
|
|
expect(mockFs.mkdirSync).toHaveBeenCalledWith(TEST_DIR);
|
|
});
|
|
|
|
test('should NOT create directory if it already exists', async () => {
|
|
// Scenario: File missing, Directory exists
|
|
mockFs.existsSync
|
|
.mockReturnValueOnce(false) // First check: File exists? No.
|
|
.mockReturnValueOnce(true); // Second check: Dir exists? Yes.
|
|
|
|
await FileKeystoreClass.create(TEST_FILE, TEST_DIR);
|
|
|
|
expect(mockFs.mkdirSync).not.toHaveBeenCalled();
|
|
});
|
|
}); |