import type { Request, Response, NextFunction } from 'express'; import { jest, describe, test, expect, beforeEach } from '@jest/globals'; import type { BaseKeystore as BaseKeystoreType } from '../src/BaseKeystore.js'; // Mock the logging library jest.unstable_mockModule('@BridgemanAccessible/ba-logging', () => ({ logMessage: jest.fn(), LogLevel: { DEBUG: 'DEBUG', ERROR: 'ERROR' } })); describe('BaseKeystore', () => { /** Env management pattern (save/restore existing environment variables) */ const OLD_ENV = process.env; /** Instance of the BaseKeystore class (needed to access static members like WELL_KNOWN_URI) */ let BaseKeystore: typeof BaseKeystoreType; /** Instance of the concrete subclass for testing (Note, the typing gets kind of weird here because of loading mechanics - see `beforeEach` block) */ let keystoreInstance: InstanceType & { setInternalKeystore: (ks: any) => void }; /** Mock Express request object */ let mockReq: Partial; /** Mock Express response object */ let mockRes: Partial; /** Mock Express next function */ let mockNext: NextFunction; /** Mock Node-Jose keystore */ let mockJoseStore: any; /** Mock logging module */ let mockLogging: any; beforeEach(async () => { jest.clearAllMocks(); // Make a copy of the existing environment variables to restore after tests // (important for not affecting other tests that might run in the same process) process.env = { ...OLD_ENV }; // Reset Process Env process.env.DEBUG_INTERSERVICE_COMMS = 'false'; // Because of ESM stuff and mocking out dependencies, // we need to load the BaseKeystore class asynchronously in the beforeEach block. // Instead of at the top level of the test file. const module = await import('../src/BaseKeystore.js'); BaseKeystore = module.BaseKeystore; // Concrete implementation for testing abstract class // Note, the definition needs to be done here in the beforeEach block after we load the BaseKeystore class, // because it needs to extend the BaseKeystore class that we load here. class TestKeystore extends BaseKeystore { // Helper to manually set the internal keystore for testing public setInternalKeystore(ks: any) { this.keystore = ks; } } keystoreInstance = new TestKeystore(); // Mock Express objects mockReq = { path: '/' }; mockRes = { status: jest.fn<(...args: any[]) => Response>().mockReturnThis(), json: jest.fn<(...args: any[]) => Response>().mockReturnThis(), }; mockNext = jest.fn(); // Mock Node-Jose Store mockJoseStore = { toJSON: jest.fn().mockReturnValue({ keys: [] }), get: jest.fn() }; mockLogging = await import('@BridgemanAccessible/ba-logging'); }); describe('getKeystore()', () => { it('should throw error if keystore is null', () => { expect(() => keystoreInstance.getKeystore()).toThrow('Keystore not ready'); }); it('should return the keystore if initialized', () => { keystoreInstance.setInternalKeystore(mockJoseStore); expect(keystoreInstance.getKeystore()).toBe(mockJoseStore); }); }); describe('getKey()', () => { it('should call get on the underlying node-jose store', () => { keystoreInstance.setInternalKeystore(mockJoseStore); keystoreInstance.getKey('key-id', { kty: 'RSA' }); expect(mockJoseStore.get).toHaveBeenCalledWith('key-id', { kty: 'RSA' }); }); }); describe('middleware()', () => { it('should call next() immediately if path is not .well-known URI', () => { mockReq.path = '/some/other/path'; keystoreInstance.middleware(mockReq as Request, mockRes as Response, mockNext); expect(mockNext).toHaveBeenCalled(); expect(mockRes.status).not.toHaveBeenCalled(); }); it('should call next(Error) if accessing .well-known but keystore is not ready', () => { mockReq.path = BaseKeystore.WELL_KNOWN_URI; keystoreInstance.middleware(mockReq as Request, mockRes as Response, mockNext); expect(mockNext).toHaveBeenCalledWith(expect.any(Error)); expect(mockNext).toHaveBeenCalledWith(expect.objectContaining({ message: 'Keystore not ready' })); }); it('should return 200 and JSON public keys if ready', () => { mockReq.path = BaseKeystore.WELL_KNOWN_URI; keystoreInstance.setInternalKeystore(mockJoseStore); keystoreInstance.middleware(mockReq as Request, mockRes as Response, mockNext); expect(mockRes.status).toHaveBeenCalledWith(200); expect(mockRes.json).toHaveBeenCalledWith({ keys: [] }); // Ensure private keys are NOT exported (toJSON should be called with false or no args) expect(mockJoseStore.toJSON).toHaveBeenCalled(); expect(mockNext).not.toHaveBeenCalled(); // Should stop processing }); it('should log debug messages when env var is set', () => { process.env.DEBUG_INTERSERVICE_COMMS = 'true'; mockReq.path = BaseKeystore.WELL_KNOWN_URI; keystoreInstance.setInternalKeystore(mockJoseStore); keystoreInstance.middleware(mockReq as Request, mockRes as Response, mockNext); expect(mockLogging.logMessage).toHaveBeenCalledTimes(2); // Start of middleware + JSON log expect(mockLogging.logMessage).toHaveBeenCalledWith(expect.stringContaining('Keystore middleware called'), 'DEBUG'); }); it('should log error messages when env var is set and keystore fails', () => { process.env.DEBUG_INTERSERVICE_COMMS = 'true'; mockReq.path = BaseKeystore.WELL_KNOWN_URI; // Keystore is NOT set here keystoreInstance.middleware(mockReq as Request, mockRes as Response, mockNext); expect(mockLogging.logMessage).toHaveBeenCalledWith('Keystore not ready', 'ERROR'); }); }); afterAll(() => { // Restore original environment variables after all tests are done process.env = OLD_ENV; }) });