mirror of
https://github.com/OpenBankProject/API-Explorer-II.git
synced 2026-02-06 10:47:04 +00:00
add testing for getOpeyConfig and checkAuthConfig
This commit is contained in:
parent
7d93025fd3
commit
303bb29ccb
4
components.d.ts
vendored
4
components.d.ts
vendored
@ -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']
|
||||
}
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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');
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user