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(); }); });