add testing for getOpeyConfig and checkAuthConfig

This commit is contained in:
Nemo Godebski-Pedersen 2025-03-13 16:35:14 +01:00
parent 7d93025fd3
commit 303bb29ccb
3 changed files with 347 additions and 23 deletions

4
components.d.ts vendored
View File

@ -19,6 +19,7 @@ declare module 'vue' {
ElCol: typeof import('element-plus/es')['ElCol']
ElCollapse: typeof import('element-plus/es')['ElCollapse']
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
ElConainer: typeof import('element-plus/es')['ElConainer']
ElContainer: typeof import('element-plus/es')['ElContainer']
ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
@ -47,4 +48,7 @@ declare module 'vue' {
RouterView: typeof import('vue-router')['RouterView']
SearchNav: typeof import('./src/components/SearchNav.vue')['default']
}
export interface ComponentCustomProperties {
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
}
}

View File

@ -19,17 +19,60 @@ export default class OpeyClientService {
}
}
async getOpeyConfig(opeyConfig: OpeyConfig): Promise<OpeyConfig> {
return opeyConfig || this.opeyConfig
/**
* Either sets the Opey configuration or returns the current configuration.
* If a partial config is provided, it will be merged with the current config,
* only overwriting explicitly defined fields.
*
* @param partialConfig - Optional partial configuration to merge with default config
* @returns Complete OpeyConfig with merged values
*/
async getOpeyConfig(partialConfig?: Partial<OpeyConfig>): Promise<OpeyConfig> {
if (!partialConfig) {
return this.opeyConfig;
}
// Create a deep copy of the current config to avoid mutation
const mergedConfig = JSON.parse(JSON.stringify(this.opeyConfig));
// Merge the base URI if provided
if (partialConfig.baseUri) {
mergedConfig.baseUri = partialConfig.baseUri;
}
// Merge paths if provided (only overwrite defined paths)
if (partialConfig.paths) {
mergedConfig.paths = {
...mergedConfig.paths,
...partialConfig.paths
};
}
// Merge authConfig if provided
if (partialConfig.authConfig) {
mergedConfig.authConfig = {
...mergedConfig.authConfig,
...partialConfig.authConfig
};
// If opeyConsent is provided, merge it too
if (partialConfig.authConfig.opeyConsent && mergedConfig.authConfig.opeyConsent) {
mergedConfig.authConfig.opeyConsent = {
...mergedConfig.authConfig.opeyConsent,
...partialConfig.authConfig.opeyConsent
};
}
}
return mergedConfig;
}
async getOpeyStatus(opeyConfig: OpeyConfig): Promise<any> {
async getOpeyStatus(opeyConfig?: OpeyConfig): Promise<any> {
// Endpoint to check if Opey is running
const config = await this.getOpeyConfig(opeyConfig)
const auth = await this.checkAuthConfig(config)
if (!auth.valid) {
console.warn(`AuthConfig not valid: ${auth.reason}`)
console.warn(`AuthConfig is not set: ${auth.reason}\n Other endpoints require authentication`)
}
try {
@ -42,7 +85,7 @@ export default class OpeyClientService {
const status = await response.json()
return status
} else {
throw new Error(`Error getting status from Opey: ${response.status} ${response.statusText}`)
throw new Error(`Could not connect: ${response.status} ${response.statusText}`)
}
@ -57,8 +100,11 @@ export default class OpeyClientService {
/**
* Streams a response from Opey by posting a user input message.
*
* This method sends the user input to Opey's streaming endpoint and returns
* a ReadableStream for the client to consume token by token or message by message.
* This method performs the following operations:
* 1. Retrieves the Opey configuration
* 2. Validates authentication credentials
* 3. Makes a POST request to the Opey stream endpoint
* 4. Processes and returns the API response as a ReadableStream
*
* @param user_input - The user's input message and settings to send to Opey
* @param opeyConfig - Configuration object for Opey connection
@ -69,8 +115,7 @@ export default class OpeyClientService {
* @throws Error if there's no response body
* @throws Error if there's any issue streaming from Opey
*/
async stream(user_input: UserInput, opeyConfig: OpeyConfig): Promise<any> {
// Endpoint to post a message to Opey and stream the response tokens/messages
async stream(user_input: UserInput, opeyConfig?: OpeyConfig): Promise<ReadableStream> {
const config = await this.getOpeyConfig(opeyConfig)
// Check if we have the consent for Opey
@ -94,7 +139,7 @@ export default class OpeyClientService {
const response = await fetch(url, {
method: 'POST',
headers: {
"Authorization": `Bearer ${config.authConfig.opeyConsent.jwt}`,
"Authorization": `Bearer ${config.authConfig.opeyConsent.jwt}`, // Should not be undefined as we already checked authConfig
"Content-Type": "application/json"
},
body: JSON.stringify(stream_input)
@ -112,10 +157,31 @@ export default class OpeyClientService {
}
}
async invoke(user_input: UserInput): Promise<any> {
// Endpoint to post a message to Opey and get a response without stream
// I.e. a normal REST call
const url = `${this.opeyConfig.baseUri}${this.opeyConfig.paths.invoke}`
/**
* Invokes the Opey API with the provided user input and optional configuration.
*
* This method performs the following operations:
* 1. Retrieves the Opey configuration
* 2. Validates authentication credentials
* 3. Makes a POST request to the Opey invoke endpoint
* 4. Processes and returns the API response
*
* @param user_input - The input data to be sent to the Opey API
* @param opeyConfig - Optional configuration override for this specific request
* @returns A Promise resolving to the response from the Opey API
* @throws Error if authentication is invalid or if the API request fails
*/
async invoke(user_input: UserInput, opeyConfig?: OpeyConfig): Promise<any> {
const config = await this.getOpeyConfig(opeyConfig)
// Check if we have the consent for Opey
const auth = await this.checkAuthConfig(config)
if (!auth.valid) {
throw new Error(`AuthConfig not valid: ${auth.reason}`)
}
const url = `${config.baseUri}${config.paths.invoke}`
console.log(`Posting to Opey, STREAMING OFF: ${JSON.stringify(user_input)}\n URL: ${url}`) //DEBUG
@ -123,7 +189,7 @@ export default class OpeyClientService {
const response = await fetch(url, {
method: 'POST',
headers: {
"Authorization": `Bearer ${this.opeyConfig.authConfig.opeyJWT}`,
"Authorization": `Bearer ${config.authConfig.opeyConsent.jwt}`, // not undefined as we checked authConfig
"Content-Type": "application/json"
},
body: JSON.stringify(user_input)
@ -140,8 +206,20 @@ export default class OpeyClientService {
}
/**
* Checks if the authentication configuration in the OpeyConfig is valid.
*
* This method validates that:
* - authConfig exists and contains opeyConsent
* - the OBP consent object has a status of 'ACCEPTED'
*
* @param opeyConfig - The configuration object to validate
* @returns An object with validation result:
* - valid: boolean indicating if the auth config is valid
* - reason: string explaining the validation result
*/
async checkAuthConfig(opeyConfig: OpeyConfig): Promise<{ valid: boolean; reason: string }> {
// Check if the authConfig is set in the OpeyConfig
if (!opeyConfig.authConfig || !opeyConfig.authConfig.opeyConsent) {
return { valid: false, reason: 'No authConfig set in opeyConfig, authentication required' }
} else if (!opeyConfig.authConfig.opeyConsent) {

View File

@ -1,11 +1,17 @@
import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest'
import OpeyClientService from '../services/OpeyClientService';
import { OpeyConfig } from '../schema/OpeySchema';
describe('getStatus', async () => {
let opeyClientService: OpeyClientService;
let opeyConfig: OpeyConfig;
beforeAll(() => {
opeyClientService = new OpeyClientService();
opeyConfig = {
baseUri: 'http://localhost:5000',
paths: {},
}
})
afterEach(() => {
@ -15,7 +21,7 @@ describe('getStatus', async () => {
it('Should resolve promise with response body if Opey returns 200', async () => {
// mock the fetch function for an OK response from Opey
const statusMessage = {"status": "ok"}
const statusMessage = { "status": "ok" }
global.fetch = vi.fn(() =>
Promise.resolve(new Response(JSON.stringify(statusMessage), {
status: 200,
@ -23,18 +29,18 @@ describe('getStatus', async () => {
);
// Call get status
const status = await opeyClientService.getOpeyStatus()
const status = await opeyClientService.getOpeyStatus(opeyConfig)
expect(status).toStrictEqual(statusMessage)
})
it('Should reject the promise and throw an error if Opey II service is down', async () => {
global.fetch = vi.fn(() =>
Promise.reject(new Response(JSON.stringify({"status": "down"}), {
Promise.reject(new Response(JSON.stringify({ "status": "down" }), {
status: 500,
}))
);
await expect(opeyClientService.getOpeyStatus()).rejects.toThrowError()
await expect(opeyClientService.getOpeyStatus(opeyConfig)).rejects.toThrowError()
})
})
@ -42,6 +48,7 @@ describe('getStatus', async () => {
describe('stream', async () => {
let opeyClientService: OpeyClientService;
let opeyConfig: OpeyConfig;
beforeAll(() => {
opeyClientService = new OpeyClientService();
@ -59,11 +66,246 @@ describe('stream', async () => {
});
global.fetch = vi.fn(() => {
return Promise.resolve(new Response(JSON.stringify({}), {
return Promise.resolve(new Response(mockStream, {
status: 200,
}))
})
opeyConfig = {
baseUri: 'http://localhost:5000',
paths: {},
}
})
})
})
describe('getOpeyConfig', async () => {
let opeyClientService: OpeyClientService;
beforeAll(() => {
opeyClientService = new OpeyClientService();
});
beforeEach(() => {
vi.clearAllMocks();
});
it('should return default config when no config is provided', async () => {
const config = await opeyClientService.getOpeyConfig();
expect(config).toEqual({
baseUri: expect.any(String),
paths: {
status: '/status',
stream: '/stream',
invoke: '/invoke',
approve_tool: '/approve_tool/{thead_id}',
feedback: '/feedback',
}
});
});
it('should merge baseUri when provided', async () => {
const partialConfig: Partial<OpeyConfig> = {
baseUri: 'https://custom-api.example.com'
};
const resultConfig = await opeyClientService.getOpeyConfig(partialConfig);
// Verify the baseUri was overwritten
expect(resultConfig.baseUri).toBe('https://custom-api.example.com');
// Verify the paths were preserved from default
expect(resultConfig.paths).toEqual({
status: '/status',
stream: '/stream',
invoke: '/invoke',
approve_tool: '/approve_tool/{thead_id}',
feedback: '/feedback',
});
});
it('should merge specific paths when provided', async () => {
const partialConfig: Partial<OpeyConfig> = {
paths: {
status: '/custom-status',
stream: '/custom-stream',
}
};
const resultConfig = await opeyClientService.getOpeyConfig(partialConfig);
// Verify only specified paths were overwritten
expect(resultConfig.paths.status).toBe('/custom-status');
expect(resultConfig.paths.stream).toBe('/custom-stream');
// Verify other paths remain unchanged
expect(resultConfig.paths.invoke).toBe('/invoke');
expect(resultConfig.paths.approve_tool).toBe('/approve_tool/{thead_id}');
expect(resultConfig.paths.feedback).toBe('/feedback');
});
it('should merge authConfig when provided', async () => {
const partialConfig: Partial<OpeyConfig> = {
authConfig: {
opeyConsent: {
consent_id: 'test-consent-id',
status: 'ACCEPTED',
jwt: 'test-jwt-token',
}
}
};
const resultConfig = await opeyClientService.getOpeyConfig(partialConfig);
// Verify authConfig was added with correct values
expect(resultConfig.authConfig).toBeDefined();
expect(resultConfig.authConfig!.opeyConsent).toEqual({
consent_id: 'test-consent-id',
status: 'ACCEPTED',
jwt: 'test-jwt-token',
});
// Verify original config fields remain
expect(resultConfig)
expect(resultConfig.baseUri).toBe('http://localhost:5000');
expect(resultConfig.paths).toBeDefined();
});
it('should not modify the original default config', async () => {
// Get a copy of the original default config
const originalConfig = JSON.parse(JSON.stringify(await opeyClientService.getOpeyConfig()));
// Apply some changes
const partialConfig: Partial<OpeyConfig> = {
baseUri: 'https://modified.example.com',
paths: {
status: '/modified-status',
}
};
await opeyClientService.getOpeyConfig(partialConfig);
// Get the default config again with no arguments
const currentDefaultConfig = await opeyClientService.getOpeyConfig();
// The default config should not have changed
expect(currentDefaultConfig).toEqual(originalConfig);
});
});
describe('checkAuthConfig', async () => {
let opeyClientService: OpeyClientService;
beforeAll(() => {
opeyClientService = new OpeyClientService();
});
beforeEach(() => {
vi.clearAllMocks();
});
it('should return invalid when authConfig is missing', async () => {
const opeyConfig: OpeyConfig = {
baseUri: 'http://localhost:5000',
paths: {
status: '/status',
stream: '/stream',
}
// authConfig intentionally missing
};
const result = await opeyClientService.checkAuthConfig(opeyConfig);
expect(result.valid).toBe(false);
expect(result.reason).toBe('No authConfig set in opeyConfig, authentication required');
});
it('should return invalid when opeyConsent is missing', async () => {
const opeyConfig: OpeyConfig = {
baseUri: 'http://localhost:5000',
paths: {
status: '/status',
},
authConfig: {
// opeyConsent intentionally missing
}
};
const result = await opeyClientService.checkAuthConfig(opeyConfig);
expect(result.valid).toBe(false);
expect(result.reason).toBe('No authConfig set in opeyConfig, authentication required');
});
it('should return invalid when consent status is not ACCEPTED', async () => {
const opeyConfig: OpeyConfig = {
baseUri: 'http://localhost:5000',
paths: {
status: '/status',
},
authConfig: {
opeyConsent: {
status: 'INITIATED',
jwt: 'test-token',
consent_id: '12345',
}
}
};
const result = await opeyClientService.checkAuthConfig(opeyConfig);
expect(result.valid).toBe(false);
expect(result.reason).toBe('Opey consent status is not ACCEPTED');
});
it('should return valid when consent status is ACCEPTED', async () => {
const opeyConfig: OpeyConfig = {
baseUri: 'http://localhost:5000',
paths: {
status: '/status',
},
authConfig: {
opeyConsent: {
status: 'ACCEPTED',
jwt: 'test-token',
consent_id: '12345',
}
}
};
const result = await opeyClientService.checkAuthConfig(opeyConfig);
expect(result.valid).toBe(true);
expect(result.reason).toBe('AuthConfig is valid');
});
it('should validate correctly even with additional fields present', async () => {
const opeyConfig: OpeyConfig = {
baseUri: 'http://localhost:5000',
paths: {
status: '/status',
},
authConfig: {
opeyConsent: {
status: 'ACCEPTED',
jwt: 'test-token',
consent_id: '12345',
user_id: 'user1',
created_at: '2025-03-13T12:00:00Z',
expires_at: '2025-04-13T12:00:00Z'
},
otherAuth: {
someField: 'someValue'
}
}
};
const result = await opeyClientService.checkAuthConfig(opeyConfig);
expect(result.valid).toBe(true);
expect(result.reason).toBe('AuthConfig is valid');
});
});