Initial Commit
All checks were successful
Publish to Private NPM Registry / publish (push) Successful in 31s
All checks were successful
Publish to Private NPM Registry / publish (push) Successful in 31s
This commit is contained in:
commit
6895111f69
13 changed files with 4107 additions and 0 deletions
158
tests/BaseKeystore.test.ts
Normal file
158
tests/BaseKeystore.test.ts
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
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<typeof BaseKeystore> & { setInternalKeystore: (ks: any) => void };
|
||||
/** Mock Express request object */
|
||||
let mockReq: Partial<Request & { /* Make path NOT read-only */ path: string }>;
|
||||
/** Mock Express response object */
|
||||
let mockRes: Partial<Response>;
|
||||
/** 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;
|
||||
})
|
||||
});
|
||||
99
tests/Keystore.test.ts
Normal file
99
tests/Keystore.test.ts
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
import { jest, describe, test, expect, beforeEach } from '@jest/globals';
|
||||
|
||||
import type { Keystore as KeystoreType } from '../src/Keystore.js';
|
||||
import type { BaseKeystore as BaseKeystoreType } from '../src/BaseKeystore.js';
|
||||
|
||||
describe('KeystoreRegistry', () => {
|
||||
let Keystore: typeof KeystoreType;
|
||||
let BaseKeystore: typeof BaseKeystoreType;
|
||||
let MockKeystoreImpl: any;
|
||||
|
||||
beforeEach(async () => {
|
||||
// Keystore.test.ts
|
||||
const keystoreModule = await import('../src/Keystore.js');
|
||||
Keystore = keystoreModule.Keystore;
|
||||
|
||||
const baseKeystoreModule = await import ('../src/BaseKeystore.js');
|
||||
BaseKeystore = baseKeystoreModule.BaseKeystore;
|
||||
|
||||
// Mock implementation of BaseKeystore for the registry
|
||||
MockKeystoreImpl = class MockKeystoreImpl extends BaseKeystore {};
|
||||
});
|
||||
|
||||
// Helper: Generates a unique key to avoid singleton collisions
|
||||
const getUniqueType = () => `type_${Math.random().toString(36).substring(7)}`;
|
||||
|
||||
describe('addKeystoreType & getKeystore', () => {
|
||||
it('should throw error if requesting a non-existent type', async () => {
|
||||
await expect(Keystore.getKeystore('non-existent-type'))
|
||||
.rejects.toThrow("Keystore type 'non-existent-type' not found");
|
||||
});
|
||||
|
||||
it('should register and retrieve a Class Constructor', async () => {
|
||||
const type = getUniqueType();
|
||||
|
||||
// Register the class itself
|
||||
Keystore.addKeystoreType(type, MockKeystoreImpl);
|
||||
|
||||
const instance = await Keystore.getKeystore(type);
|
||||
expect(instance).toBeInstanceOf(MockKeystoreImpl);
|
||||
});
|
||||
|
||||
it('should register and retrieve a Factory Function (Synchronous)', async () => {
|
||||
const type = getUniqueType();
|
||||
const factory = () => new MockKeystoreImpl();
|
||||
|
||||
Keystore.addKeystoreType(type, factory);
|
||||
|
||||
const instance = await Keystore.getKeystore(type);
|
||||
expect(instance).toBeInstanceOf(MockKeystoreImpl);
|
||||
});
|
||||
|
||||
it('should register and retrieve a Factory Function (Async)', async () => {
|
||||
const type = getUniqueType();
|
||||
|
||||
// Factory returns a Promise
|
||||
const asyncFactory = async () => new MockKeystoreImpl();
|
||||
Keystore.addKeystoreType(type, asyncFactory);
|
||||
|
||||
const instance = await Keystore.getKeystore(type);
|
||||
expect(instance).toBeInstanceOf(MockKeystoreImpl);
|
||||
});
|
||||
|
||||
it('should pass arguments to the constructor', async () => {
|
||||
const type = getUniqueType();
|
||||
|
||||
// Create a specific mock that accepts args
|
||||
class ArgKeystore extends BaseKeystore {
|
||||
public args: any[];
|
||||
|
||||
constructor(...args: any[]) {
|
||||
super();
|
||||
this.args = args;
|
||||
}
|
||||
}
|
||||
|
||||
Keystore.addKeystoreType(type, ArgKeystore);
|
||||
|
||||
const instance = await Keystore.getKeystore(type, 'arg1', 123) as ArgKeystore;
|
||||
expect(instance.args).toEqual(['arg1', 123]);
|
||||
});
|
||||
|
||||
it('should pass arguments to the factory function', async () => {
|
||||
const type = getUniqueType();
|
||||
const factorySpy = jest.fn<(...args: any[]) => InstanceType<typeof MockKeystoreImpl>>().mockReturnValue(new MockKeystoreImpl());
|
||||
|
||||
Keystore.addKeystoreType(type, factorySpy);
|
||||
|
||||
await Keystore.getKeystore(type, 'factoryArg', 999);
|
||||
expect(factorySpy).toHaveBeenCalledWith('factoryArg', 999);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isKeystoreFactoryFunc', () => {
|
||||
// Since this function isn't exported directly, we test it via behavior
|
||||
// implicitly in the tests above, but if you wanted to test it explicitly,
|
||||
// you would need to export it from the file.
|
||||
// For now, the factory vs constructor behavior is covered by the previous two tests.
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue