diff --git a/changelog.md b/changelog.md index e67d3332..b916ac44 100644 --- a/changelog.md +++ b/changelog.md @@ -13,7 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Table to display all secrets. * Add action to create a secret. * Add action to edit a secret. - * Add action to remove a secret. + * Add action to remove a secret. + * Add actions and card to create/edit/delete a generic or custom configuration. ## [1.0.0] - 2024/10/15 diff --git a/package.json b/package.json index af0b201f..1cf9f363 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "dependencies": { "@quasar/extras": "=1.16.11", "axios": "=1.7.2", + "nunjucks": "=3.2.4", "pinia": "=2.1.7", "quasar": "=2.16.4", "v-viewer": "=3.0.13", @@ -47,7 +48,6 @@ "eslint-plugin-vue": "=9.26.0", "gherkin-lint": "=4.2.4", "jsdoc": "=4.0.3", - "nunjucks": "=3.2.4", "vitest": "=1.6.0" }, "engines": { diff --git a/src/components/card/CustomAIConfigurationCard.vue b/src/components/card/CustomAIConfigurationCard.vue new file mode 100644 index 00000000..ad32750b --- /dev/null +++ b/src/components/card/CustomAIConfigurationCard.vue @@ -0,0 +1,270 @@ + + + + + diff --git a/src/components/card/DefaultAIConfigurationCard.vue b/src/components/card/DefaultAIConfigurationCard.vue new file mode 100644 index 00000000..db41b3bf --- /dev/null +++ b/src/components/card/DefaultAIConfigurationCard.vue @@ -0,0 +1,265 @@ + + + + + diff --git a/src/components/tab-panel/ConfigurationsTabPanel.vue b/src/components/tab-panel/ConfigurationsTabPanel.vue index e50aaff7..bfce1ca5 100644 --- a/src/components/tab-panel/ConfigurationsTabPanel.vue +++ b/src/components/tab-panel/ConfigurationsTabPanel.vue @@ -3,11 +3,117 @@ name="configurations" data-cy="configurations_tab_panel" > + +
{{ $t('ConfigurationsTabPanel.text.title') }}
+
+ +
+
+ +
+ + diff --git a/src/composables/events/ReloadConfigurationsEvent.js b/src/composables/events/ReloadConfigurationsEvent.js new file mode 100644 index 00000000..238703d2 --- /dev/null +++ b/src/composables/events/ReloadConfigurationsEvent.js @@ -0,0 +1,10 @@ +import { Subject } from 'rxjs'; + +/** + * Represent a rxjs Event object to emit and to receive events to force reloading + * the configurations. + * @typedef {Subject} ReloadConfigurationsEvent + */ +const ReloadConfigurationsEvent = new Subject(); + +export default ReloadConfigurationsEvent; diff --git a/src/i18n/en-US/index.js b/src/i18n/en-US/index.js index b4aca51e..26c03cd6 100644 --- a/src/i18n/en-US/index.js +++ b/src/i18n/en-US/index.js @@ -643,6 +643,37 @@ export default { warning: 'fa-solid fa-triangle-exclamation', }, }, + DefaultAIConfigurationCard: { + text: { + title: 'Global configuration', + addPlugin: 'Add AI model for a new plugin', + editPlugin: 'Update AI model for existing plugin', + newPluginNameLabel: 'Plugin name', + newPluginNameHint: '"default" for all unlisted plugin', + newPluginHandler: 'Select AI model for plugin', + save: 'Save', + update: 'Update', + notEmpty: 'Field is required.', + notifyDeleteSuccess: 'Configuration is deleted.', + notifySaveSuccess: 'Configuration is added.', + notifyUpdateSuccess: 'Configuration(s) is updated.', + }, + icon: { + delete: 'fa-solid fa-trash', + }, + }, + CustomAIConfigurationCard: { + text: { + title: 'AI configuration: {handler}', + pluginTitle: 'Plugin configuration: { plugin }', + save: 'Save', + notEmpty: 'Field is required.', + notifySaveSuccess: 'Configuration is saved.', + }, + icon: { + delete: 'fa-solid fa-trash', + }, + }, TablePaginationCard: { text: { content: '{current}/{max} of {total}', diff --git a/src/services/ConfigurationService.js b/src/services/ConfigurationService.js new file mode 100644 index 00000000..f96cfe1f --- /dev/null +++ b/src/services/ConfigurationService.js @@ -0,0 +1,92 @@ +import { + prepareQueryParameters, + prepareRequest, + makeFilterRequest, +} from 'boot/axios'; + +/** + * Get all configurations paginated. + * @param {object} filters - API filters. + * @returns {Promise} Promise with an array of configurations on success + * otherwise an error. + */ +export async function find(filters) { + const api = await prepareRequest(); + const queryParameters = prepareQueryParameters(filters); + + return makeFilterRequest(api, `/ai/configurations${queryParameters}`) + .then(({ data }) => data); +} + +/** + * Get all configurations without pagination. + * Recursive function to get all configurations from `find`. + * @param {object} filters - API filters. + * @param {Array} configurations - Pagination result to set. + * @returns {Promise} Promise with an array of configurations on success + * otherwise an error. + */ +export async function findAll(filters = {}, configurations = []) { + return find(filters) + .then((data) => { + const nextPage = data.pageable.pageNumber + 1; + data.content.forEach((item) => configurations.push(item)); + + if (data.totalPages <= nextPage) { + return Promise.resolve(configurations); + } + + return findAll({ + ...filters, + page: `${nextPage}`, + }, configurations); + }); +} + +/** + * Get all configuration field descriptions. + * @returns {Promise} Promise with an array of configuration field descriptions on success + * otherwise an error. + */ +export async function findDescriptionFields() { + const api = await prepareRequest(); + + return api.get('/ai/proxy/descriptions') + .then(({ data }) => data); +} + +/** + * Create a new configuration. + * @param {object} configuration - Configuration to create. + * @returns {Promise} Promise with new created configuration on success otherwise an error. + */ +export async function add(configuration) { + const api = await prepareRequest(); + + return api.post('/ai/configurations', configuration) + .then(({ data }) => data); +} + +/** + * Delete configuration by id. + * @param {string} id - Id of configuration. + * @returns {Promise} Promise with nothing on success. + */ +export async function deleteById(id) { + const api = await prepareRequest(); + + return api.delete(`/ai/configurations/${id}`) + .then(({ data }) => data); +} + +/** + * Update multiple configurations. + * @param {Array} configurations - Configurations to update. + * @returns {Promise<*>} Promise with updated configurations on success otherwise an error. + */ +export async function updateAll(configurations) { + const api = await prepareRequest(); + + return api.put('/ai/configurations', configurations) + .then(({ data }) => data); +} diff --git a/tests/e2e/features/Configurations.feature b/tests/e2e/features/Configurations.feature new file mode 100644 index 00000000..dcc8fe4c --- /dev/null +++ b/tests/e2e/features/Configurations.feature @@ -0,0 +1,57 @@ +Feature: Test roundtrip of the application: Configurations + + ################## List configurations ################## + ## 101 Should display all plugins and associated handler + ## 102 Should display all handlers configurations + + ################## Action on handler and plugin ################# + ## 201 Should successfully create a plugin with an handler + ## 202 Should successfully update a plugin with an handler + ## 203 Should successfully delete a plugin with an handler + + Scenario: Roundtrip about Configurations + Given I visit the '/' + And I set viewport size to '1536' px for width and '960' px for height + + When I click on '[data-cy="drawer_item_ai"]' + Then I expect current url is '/ai' + + #################################################### + ################## List configurations ############# + #################################################### + + ## 101 Should display all plugins and associated handler + And I expect '[data-cy="default-AI-configuration-card"] [data-cy="select-plugin1"]' exists + And I expect '[data-cy="default-AI-configuration-card"] [data-cy="select-plugin1"]' is "handler1" + And I expect '[data-cy="default-AI-configuration-card"] [data-cy="select-plugin2"]' exists + And I expect '[data-cy="default-AI-configuration-card"] [data-cy="select-plugin2"]' is "handler2" + + ## 102 Should display all handlers configurations + And I expect '[data-cy="handler1_custom-AI-configuration-card"]' exists + And I expect field '[data-cy="input_handler1_key1"]' is 'value1' + And I expect '[data-cy="expansion-item_handler1_plugin1"]' exists + And I expect '[data-cy="expansion-item_handler1_plugin2"]' exists + + And I expect '[data-cy="handler2_custom-AI-configuration-card"]' exists + And I expect field '[data-cy="input_handler2_key2"]' is 'value2' + And I expect '[data-cy="expansion-item_handler2_plugin1"]' exists + And I expect '[data-cy="expansion-item_handler2_plugin2"]' exists + + #################################################### + ################## Action on handler and plugin #### + #################################################### + + ## 201 Should successfully create a plugin with an handler + When I set on '[data-cy="input-new-plugin-name"]' text 'plugin3' + And I select '[data-cy="item_handler1"]' in '[data-cy="select-new-plugin-handler"]' + And I click on '[data-cy="default-AI-configuration-card"] [data-cy="button_save-plugin"]' + Then I expect 'positive' toast to appear with text 'Configuration is added.' + + ## 202 Should successfully update a plugin with an handler + When I select '[data-cy="item_handler2"]' in '[data-cy="select-plugin1"]' + And I click on '[data-cy="default-AI-configuration-card"] [data-cy="button_update"]' + Then I expect 'positive' toast to appear with text 'Configuration is updated.' + + ## 203 Should successfully delete a plugin with an handler + When I click on '[data-cy="default-AI-configuration-card"] [data-cy="button_plugin1_delete"]' + Then I expect 'positive' toast to appear with text 'Configuration is deleted.' diff --git a/tests/e2e/features/Secrets.feature b/tests/e2e/features/Secrets.feature index f2f8a402..8ec989e7 100644 --- a/tests/e2e/features/Secrets.feature +++ b/tests/e2e/features/Secrets.feature @@ -32,7 +32,6 @@ Feature: Test roundtrip of the application: Secrets ## 101 Should display all secrets And I expect '[data-cy="secrets_table"] tbody tr' appear 2 times on screen And I expect '[data-cy="secrets_table"] tbody tr:nth-child(1) td.secret-key' is 'SONAR_TOKEN' - And I expect '[data-cy="secrets_table"] tbody tr:nth-child(2) td.secret-key' is 'GEMINI_TOKEN' #################################################### diff --git a/tests/e2e/support/step_definitions/api.js b/tests/e2e/support/step_definitions/api.js index 8b3bdb75..afb79a15 100644 --- a/tests/e2e/support/step_definitions/api.js +++ b/tests/e2e/support/step_definitions/api.js @@ -91,6 +91,54 @@ const secret2 = { updateDate: '2024-10-23T15:00:00.000+00:00', }; +const configurations = [{ + id: 'id_1', + handler: '', + key: 'plugin.preferences.plugin1', + value: 'handler1', +}, { + id: 'id_2', + handler: '', + key: 'plugin.preferences.plugin2', + value: 'handler2', +}, { + id: 'id_3', + handler: 'handler1', + key: 'key1', + value: 'value1', +}, { + id: 'id_4', + handler: 'handler2', + key: 'key2', + value: 'value2', +}]; +const descriptions = { + handler1: [{ + handler: 'handler1', + key: 'key1', + type: 'text', + values: [], + defaultValue: 'value', + label: 'Label', + title: 'Title', + description: 'Description', + pluginDependent: false, + required: true, + }], + handler2: [{ + handler: 'handler2', + key: 'key2', + type: 'text', + values: [], + defaultValue: 'value', + label: 'Label', + title: 'Title', + description: 'Description', + pluginDependent: false, + required: true, + }], +}; + /** * User-specific intercepts */ @@ -1011,12 +1059,64 @@ function setSecretIntercepts() { }); } +/** + * Configuration-specific intercepts + */ +function setConfigurationIntercepts() { + cy.intercept('GET', '/api/ai/configurations', (request) => { + request.reply({ + statusCode: 200, + body: { + ...defaultResponse, + content: configurations, + }, + }); + }); + + cy.intercept('GET', '/api/ai/proxy/descriptions', (request) => { + request.reply({ + statusCode: 200, + body: descriptions, + }); + }); + + cy.intercept('POST', '/api/ai/configurations', (request) => { + request.reply({ + statusCode: 200, + body: { + id: '1', + }, + }); + }); + + cy.intercept('PUT', '/api/ai/configurations', (request) => { + request.reply({ + statusCode: 200, + body: [], + }); + }); + + cy.intercept('PUT', '/api/ai/configurations/*', (request) => { + request.reply({ + statusCode: 200, + body: { + id: '1', + }, + }); + }); + + cy.intercept('DELETE', '/api/ai/configurations/*', (request) => { + request.reply({ statusCode: 204 }); + }); +} + Before(() => { setUserIntercepts(); setGroupIntercepts(); setRoleIntercepts(); setLibraryIntercepts(); setSecretIntercepts(); + setConfigurationIntercepts(); cy.intercept('GET', '/api/permissions', { statusCode: 200, diff --git a/tests/unit/components/card/CustomAIConfigurationCard.test.js b/tests/unit/components/card/CustomAIConfigurationCard.test.js new file mode 100644 index 00000000..9cd28d3e --- /dev/null +++ b/tests/unit/components/card/CustomAIConfigurationCard.test.js @@ -0,0 +1,198 @@ +import { installQuasarPlugin } from '@quasar/quasar-app-extension-testing-unit-vitest'; +import { shallowMount } from '@vue/test-utils'; +import { vi } from 'vitest'; +import CustomAIConfigurationCard from 'src/components/card/CustomAIConfigurationCard.vue'; +import * as ConfigurationService from 'src/services/ConfigurationService'; +import ReloadConfigurationsEvent from 'src/composables/events/ReloadConfigurationsEvent'; +import { Notify } from 'quasar'; + +installQuasarPlugin({ + plugins: [Notify], +}); + +vi.mock('src/services/ConfigurationService'); +vi.mock('src/composables/events/ReloadConfigurationsEvent'); + +describe('Test component: CustomAIConfigurationCard', () => { + let wrapper; + + beforeEach(() => { + ConfigurationService.find.mockImplementation(() => Promise.resolve({ content: [{ name: 'Library_1', id: 1 }] })); + + wrapper = shallowMount(CustomAIConfigurationCard, { + props: { + handlers: ['handler1', 'handler2'], + plugins: ['plugin1', 'plugin2'], + descriptions: [{ + pluginDependent: true, + key: 'key1.{{ plugin }}', + defaultValue: 'defaultValueKey1', + }, { + pluginDependent: false, + key: 'key2', + defaultValue: 'defaultValueKey2', + }, { + pluginDependent: false, + key: 'key3', + defaultValue: 'defaultValueKey3', + }], + configurations: [{ + id: 'id_1', + handler: 'handler1', + key: 'key1.plugin1', + value: 'value1', + }, { + id: 'id_2', + handler: 'handler1', + key: 'key2', + value: 'value2', + }], + }, + }); + }); + + describe('Test function: initFields', () => { + it('should set fields', () => { + wrapper.vm.defaultFields = []; + wrapper.vm.pluginFields = {}; + + wrapper.vm.initFields(); + + expect(wrapper.vm.defaultFields).toEqual([{ + id: 'id_2', + key: 'key2', + value: 'value2', + pluginDependent: false, + defaultValue: 'defaultValueKey2', + }, { + id: null, + key: 'key3', + value: 'defaultValueKey3', + pluginDependent: false, + defaultValue: 'defaultValueKey3', + }]); + expect(wrapper.vm.pluginFields).toEqual({ + plugin1: [{ + id: 'id_1', + key: 'key1.plugin1', + value: 'value1', + pluginDependent: true, + defaultValue: 'defaultValueKey1', + }], + plugin2: [{ + id: null, + key: 'key1.plugin2', + value: 'defaultValueKey1', + pluginDependent: true, + defaultValue: 'defaultValueKey1', + }], + }); + }); + }); + + describe('Test function: onSubmit', () => { + it('should create, update and delete configurations', async () => { + Notify.create = vi.fn(); + + ConfigurationService.updateAll.mockImplementation(() => Promise.resolve()); + ConfigurationService.add.mockImplementation(() => Promise.resolve()); + ConfigurationService.deleteById.mockImplementation(() => Promise.resolve()); + wrapper.vm.submitting = true; + wrapper.vm.defaultFields = [{ + id: 'id_1', // To update + key: 'key1', + value: 'value1', + handler: 'handler1', + }, { + id: 'id_2', // To delete + key: 'key2', + value: '', + handler: 'handler1', + }, { + id: null, // To create + key: 'key3', + value: 'value3', + handler: 'handler1', + }]; + wrapper.vm.pluginFields = { + plugin1: [{ + id: 'id_4', // To update + key: 'key4', + value: 'value4', + handler: 'handler1', + }, { + id: 'id_5', // To delete + key: 'key5', + value: '', + handler: 'handler1', + }, { + id: null, // To create + key: 'key6', + value: 'value6', + handler: 'handler1', + }], + plugin2: [{ + id: 'id_7', // To update + key: 'key7', + value: 'value7', + handler: 'handler1', + }, { + id: 'id_8', // To delete + key: 'key8', + value: '', + handler: 'handler1', + }, { + id: null, // To create + key: 'key9', + value: 'value9', + handler: 'handler1', + }], + }; + + await wrapper.vm.onSubmit(); + expect(ConfigurationService.updateAll).toBeCalledWith([{ + id: 'id_1', + key: 'key1', + value: 'value1', + handler: 'handler1', + }, { + id: 'id_4', + key: 'key4', + value: 'value4', + handler: 'handler1', + }, { + id: 'id_7', + key: 'key7', + value: 'value7', + handler: 'handler1', + }]); + expect(ConfigurationService.add).toBeCalledTimes(3); + expect(ConfigurationService.add).toHaveBeenCalledWith({ + key: 'key3', + value: 'value3', + handler: 'handler1', + }); + expect(ConfigurationService.add).toHaveBeenCalledWith({ + key: 'key6', + value: 'value6', + handler: 'handler1', + }); + expect(ConfigurationService.add).toHaveBeenCalledWith({ + key: 'key9', + value: 'value9', + handler: 'handler1', + }); + expect(ConfigurationService.deleteById).toBeCalledTimes(3); + expect(ConfigurationService.deleteById).toHaveBeenCalledWith('id_2'); + expect(ConfigurationService.deleteById).toHaveBeenCalledWith('id_5'); + expect(ConfigurationService.deleteById).toHaveBeenCalledWith('id_8'); + expect(wrapper.vm.submitting).toEqual(false); + expect(ReloadConfigurationsEvent.next).toBeCalled(); + expect(Notify.create).toBeCalledWith({ + type: 'positive', + message: 'Configuration is saved.', + html: true, + }); + }); + }); +}); diff --git a/tests/unit/components/card/DefaultAIConfigurationCard.test.js b/tests/unit/components/card/DefaultAIConfigurationCard.test.js new file mode 100644 index 00000000..1fd9582e --- /dev/null +++ b/tests/unit/components/card/DefaultAIConfigurationCard.test.js @@ -0,0 +1,146 @@ +import { installQuasarPlugin } from '@quasar/quasar-app-extension-testing-unit-vitest'; +import { shallowMount } from '@vue/test-utils'; +import { vi } from 'vitest'; +import DefaultAIConfigurationCard from 'src/components/card/DefaultAIConfigurationCard.vue'; +import * as ConfigurationService from 'src/services/ConfigurationService'; +import ReloadConfigurationsEvent from 'src/composables/events/ReloadConfigurationsEvent'; +import { Notify } from 'quasar'; + +installQuasarPlugin({ + plugins: [Notify], +}); + +vi.mock('src/services/ConfigurationService'); +vi.mock('src/composables/events/ReloadConfigurationsEvent'); + +describe('Test component: DefaultAIConfigurationCard', () => { + let wrapper; + + beforeEach(() => { + ConfigurationService.find.mockImplementation(() => Promise.resolve({ content: [{ name: 'Library_1', id: 1 }] })); + + wrapper = shallowMount(DefaultAIConfigurationCard, { + props: { + handlers: ['handler1', 'handler2'], + configurations: [{ + id: 'id_1', + handler: null, + key: 'plugin.preferences.plugin1', + value: 'handler1', + }, { + id: 'id_2', + handler: null, + key: 'plugin.preferences.plugin2', + value: 'handler2', + }, { + id: 'id_1', + handler: 'handler1', + key: 'test', + value: 'value', + }], + }, + }); + }); + + describe('Test function: initPlugins', () => { + it('should set plugins and configurations', () => { + wrapper.vm.pluginPreferences = {}; + wrapper.vm.plugins = []; + + wrapper.vm.initPlugins(); + + expect(wrapper.vm.plugins).toEqual(['plugin1', 'plugin2']); + expect(wrapper.vm.pluginPreferences).toEqual({ + plugin1: { + id: 'id_1', + key: 'plugin.preferences.plugin1', + value: 'handler1', + }, + plugin2: { + id: 'id_2', + key: 'plugin.preferences.plugin2', + value: 'handler2', + }, + }); + }); + }); + + describe('Test function: deleteConfiguration', () => { + it('should delete configuration', async () => { + const event = vi.fn(); + + Notify.create = vi.fn(); + ReloadConfigurationsEvent.next.mockImplementation(event); + ConfigurationService.deleteById.mockImplementation(() => Promise.resolve()); + wrapper.vm.submitting = true; + + await wrapper.vm.deleteConfiguration('id_1'); + + expect(wrapper.vm.submitting).toEqual(false); + expect(ReloadConfigurationsEvent.next).toBeCalled(); + expect(Notify.create).toHaveBeenCalledWith({ + type: 'positive', + message: 'Configuration is deleted.', + html: true, + }); + }); + }); + + describe('Test function: onSubmitNewPlugin', () => { + it('should create configuration', async () => { + const event = vi.fn(); + + Notify.create = vi.fn(); + ReloadConfigurationsEvent.next.mockImplementation(event); + ConfigurationService.add.mockImplementation(() => Promise.resolve()); + wrapper.vm.submitting = true; + wrapper.vm.newPluginName = 'plugin3'; + wrapper.vm.newPluginHandler = 'value'; + + await wrapper.vm.onSubmitNewPlugin(); + + expect(ConfigurationService.add).toBeCalledWith({ + handler: null, + key: 'plugin.preferences.plugin3', + value: 'value', + }); + expect(wrapper.vm.submitting).toEqual(false); + expect(ReloadConfigurationsEvent.next).toBeCalled(); + expect(Notify.create).toHaveBeenCalledWith({ + type: 'positive', + message: 'Configuration is added.', + html: true, + }); + }); + }); + + describe('Test function: onSubmitUpdate', () => { + it('should create configuration', async () => { + const event = vi.fn(); + + Notify.create = vi.fn(); + ReloadConfigurationsEvent.next.mockImplementation(event); + ConfigurationService.updateAll.mockImplementation(() => Promise.resolve()); + wrapper.vm.submitting = true; + + await wrapper.vm.onSubmitUpdate(); + + expect(ConfigurationService.updateAll).toBeCalledWith([{ + id: 'id_1', + key: 'plugin.preferences.plugin1', + value: 'handler1', + }, { + id: 'id_2', + key: 'plugin.preferences.plugin2', + value: 'handler2', + }]); + expect(wrapper.vm.submitting).toEqual(false); + expect(ReloadConfigurationsEvent.next).toBeCalled(); + expect(Notify.create).toHaveBeenCalledWith({ + type: 'positive', + message: 'Configuration(s) is updated.', + html: true, + }); + }); + }); +}); diff --git a/tests/unit/components/tab-panel/ConfigurationsTabPanel.test.js b/tests/unit/components/tab-panel/ConfigurationsTabPanel.test.js new file mode 100644 index 00000000..6ade2920 --- /dev/null +++ b/tests/unit/components/tab-panel/ConfigurationsTabPanel.test.js @@ -0,0 +1,127 @@ +import { installQuasarPlugin } from '@quasar/quasar-app-extension-testing-unit-vitest'; +import { shallowMount } from '@vue/test-utils'; +import ConfigurationsTabPanel from 'src/components/tab-panel/ConfigurationsTabPanel.vue'; +import * as ConfigurationService from 'src/services/ConfigurationService'; +import { vi } from 'vitest'; +import { Notify } from 'quasar'; +import { useRoute } from 'vue-router'; +import ReloadConfigurationsEvent from 'src/composables/events/ReloadConfigurationsEvent'; + +installQuasarPlugin({ + plugins: [Notify], +}); + +vi.mock('vue-router'); +vi.mock('src/services/ConfigurationService'); +vi.mock('src/composables/events/ReloadConfigurationsEvent'); + +describe('Test component: ConfigurationsTabPanel', async () => { + let wrapper; + let subscribe; + let unsubscribe; + + beforeEach(async () => { + subscribe = vi.fn(); + unsubscribe = vi.fn(); + + ConfigurationService.findAll + .mockImplementation(() => Promise.resolve([])); + ConfigurationService.findDescriptionFields + .mockImplementation(() => Promise.resolve({})); + + ReloadConfigurationsEvent.subscribe.mockImplementation(() => { + subscribe(); + return { unsubscribe }; + }); + + useRoute.mockImplementation(() => ({ + query: {}, + })); + + wrapper = shallowMount(ConfigurationsTabPanel); + }); + + describe('Test function: loadConfigurationDescriptions', () => { + it('should set handlers and configurations', async () => { + ConfigurationService.findDescriptionFields + .mockImplementation(() => Promise.resolve({ + handler1: ['test1'], + handler2: ['test2'], + })); + + wrapper.vm.descriptions = {}; + wrapper.vm.handlers = []; + + await wrapper.vm.loadConfigurationDescriptions(); + + expect(wrapper.vm.descriptions).toEqual({ + handler1: ['test1'], + handler2: ['test2'], + }); + expect(wrapper.vm.handlers).toEqual(['handler1', 'handler2']); + }); + }); + + describe('Test function: loadConfigurations', () => { + it('should set configurations and plugins', async () => { + wrapper.vm.handlers = ['handler1', 'handler2']; + + ConfigurationService.findAll + .mockImplementation(() => Promise.resolve([{ + handler: '', + key: 'plugin.preferences.plugin1', + }, { + handler: '', + key: 'plugin.preferences.plugin2', + }, { + handler: 'handler1', + key: 'conf1', + }, { + handler: 'handler2', + key: 'conf1', + }, { + handler: 'handler3', + key: 'conf2', + }])); + + await wrapper.vm.loadConfigurations(); + + expect(wrapper.vm.plugins).toEqual(['plugin1', 'plugin2']); + expect(wrapper.vm.configurations).toEqual({ + default: [{ + handler: '', + key: 'plugin.preferences.plugin1', + }, { + handler: '', + key: 'plugin.preferences.plugin2', + }], + handler1: [{ + handler: 'handler1', + key: 'conf1', + }], + handler2: [{ + handler: 'handler2', + key: 'conf1', + }], + handler3: [{ + handler: 'handler3', + key: 'conf2', + }], + }); + }); + }); + + describe('Test hook function: onMounted', () => { + it('should subscribe ReloadConfigurationsEvent', () => { + expect(subscribe).toHaveBeenCalledTimes(1); + }); + }); + + describe('Test hook function: onUnmounted', () => { + it('should unsubscribe ReloadConfigurationsEvent', () => { + expect(unsubscribe).toHaveBeenCalledTimes(0); + wrapper.unmount(); + expect(unsubscribe).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/tests/unit/services/ConfigurationService.test.js b/tests/unit/services/ConfigurationService.test.js new file mode 100644 index 00000000..bab8da8a --- /dev/null +++ b/tests/unit/services/ConfigurationService.test.js @@ -0,0 +1,147 @@ +import * as ConfigurationService from 'src/services/ConfigurationService'; +import { vi } from 'vitest'; +import { + prepareRequest as api, + makeFilterRequest, + prepareQueryParameters, +} from 'boot/axios'; + +vi.mock('boot/axios'); + +describe('Test: ConfigurationService', () => { + describe('Test function: find', () => { + it('should return all configurations information', async () => { + const configurations = [{ + id: 'id_1', + key: 'test', + value: 'value', + }]; + + makeFilterRequest.mockImplementation(vi.fn(() => Promise.resolve({ data: configurations }))); + + const data = await ConfigurationService.find({}); + expect(data).toEqual(configurations); + }); + }); + + describe('Test function: findAll', () => { + it('should return all configurations information', async () => { + prepareQueryParameters.mockImplementation((filters) => { + if (filters?.page === '1') { + return '&page=2'; + } + return ''; + }); + makeFilterRequest.mockImplementation((test, url) => { + if (url === '/ai/configurations') { + return Promise.resolve({ + data: { + pageable: { + pageNumber: 0, + }, + totalPages: 2, + content: [{ + id: 'id_1', + key: 'test', + value: 'value', + }], + }, + }); + } + return Promise.resolve({ + data: { + pageable: { + pageNumber: 2, + }, + totalPages: 2, + content: [{ + id: 'id_2', + key: 'test', + value: 'value', + }], + }, + }); + }); + + const data = await ConfigurationService.findAll(); + expect(data).toEqual([{ + id: 'id_1', + key: 'test', + value: 'value', + }, { + id: 'id_2', + key: 'test', + value: 'value', + }]); + }); + }); + + describe('Test function: findDescriptionFields', () => { + it('should return all descriptions', async () => { + const mockGetRequest = vi.fn(() => Promise.resolve({ + data: [], + })); + + api.mockImplementation(() => ({ + get: mockGetRequest, + })); + + const result = await ConfigurationService.findDescriptionFields(); + + expect(result).toEqual([]); + expect(mockGetRequest).toBeCalledWith('/ai/proxy/descriptions'); + }); + }); + + describe('Test function: add', () => { + it('should create configuration', async () => { + const mockPostRequest = vi.fn(() => Promise.resolve({ + data: { + id: 'id_1', + key: 'test', + value: 'value', + }, + })); + + api.mockImplementation(() => ({ + post: mockPostRequest, + })); + + await ConfigurationService.add({ key: 'test', value: 'value' }); + + expect(mockPostRequest).toBeCalledWith('/ai/configurations', { key: 'test', value: 'value' }); + }); + }); + + describe('Test function: deleteById', () => { + it('should delete configuration by id', async () => { + const mockDeleteRequest = vi.fn(() => Promise.resolve({ + data: {}, + })); + + api.mockImplementation(() => ({ + delete: mockDeleteRequest, + })); + + await ConfigurationService.deleteById('id'); + + expect(mockDeleteRequest).toBeCalledWith('/ai/configurations/id'); + }); + }); + + describe('Test function: updateAll', () => { + it('should update all configurations', async () => { + const mockUpdateRequest = vi.fn(() => Promise.resolve({ + data: {}, + })); + + api.mockImplementation(() => ({ + put: mockUpdateRequest, + })); + + await ConfigurationService.updateAll([]); + + expect(mockUpdateRequest).toBeCalledWith('/ai/configurations', []); + }); + }); +});