diff --git a/contentcuration/contentcuration/frontend/shared/__tests__/feedbackUtils.spec.js b/contentcuration/contentcuration/frontend/shared/__tests__/feedbackUtils.spec.js new file mode 100644 index 0000000000..c5f11d57d0 --- /dev/null +++ b/contentcuration/contentcuration/frontend/shared/__tests__/feedbackUtils.spec.js @@ -0,0 +1,79 @@ +import { v4 as uuidv4 } from 'uuid'; +import { + sendRequest, + FlagFeedbackEvent, + FeedbackTypeOptions, + FLAG_FEEDBACK_EVENT_URL, +} from '../feedbackApiUtils'; +import client from '../client'; + +jest.mock('uuid', () => ({ v4: jest.fn(() => 'mocked-uuid') })); +jest.mock('../client'); + +describe('FeedBackUtility Tests', () => { + let flagFeedbackEvent; + + afterEach(() => { + jest.clearAllMocks(); + }); + + beforeEach(() => { + flagFeedbackEvent = new FlagFeedbackEvent({ + context: { key: 'value' }, + contentnode_id: uuidv4(), + content_id: uuidv4(), + target_topic_id: uuidv4(), + feedback_type: FeedbackTypeOptions.flagged, + feedback_reason: 'Inappropriate Language', + }); + client.post.mockRestore(); + }); + + it('should generate data object without functions', () => { + const dataObject = flagFeedbackEvent.getDataObject(); + expect(dataObject.id).toEqual('mocked-uuid'); + expect(dataObject.context).toEqual({ key: 'value' }); + expect(dataObject.contentnode_id).toEqual('mocked-uuid'); + expect(dataObject.content_id).toEqual('mocked-uuid'); + + expect(dataObject.getDataObject).toBeUndefined(); + expect(dataObject.target_topic_id).toEqual('mocked-uuid'); + expect(dataObject.feedback_type).toEqual(FeedbackTypeOptions.flagged); + expect(dataObject.feedback_reason).toEqual('Inappropriate Language'); + + expect(dataObject.URL).toBeUndefined(); + }); + + it('should throw an error when URL is not defined', () => { + flagFeedbackEvent.URL = undefined; + expect(() => flagFeedbackEvent.getUrl()).toThrowError( + 'URL is not defined for the FeedBack Object.' + ); + }); + + it('should return the correct URL when URL is defined', () => { + const result = flagFeedbackEvent.getUrl(); + expect(result).toEqual(FLAG_FEEDBACK_EVENT_URL); + }); + + it('should send a request using sendRequest function', async () => { + client.post.mockResolvedValue(Promise.resolve({ data: 'Mocked API Response' })); + + const result = await sendRequest(flagFeedbackEvent); + + expect(result).toEqual('Mocked API Response'); + expect(client.post).toHaveBeenCalledWith( + FLAG_FEEDBACK_EVENT_URL, + flagFeedbackEvent.getDataObject() + ); + }); + + it('should handle errors when sending a request using sendRequest function', async () => { + client.post.mockRejectedValue(new Error('Mocked API Error')); + await expect(sendRequest(flagFeedbackEvent)).rejects.toThrowError('Mocked API Error'); + expect(client.post).toHaveBeenCalledWith( + FLAG_FEEDBACK_EVENT_URL, + flagFeedbackEvent.getDataObject() + ); + }); +}); diff --git a/contentcuration/contentcuration/frontend/shared/feedbackApiUtils.js b/contentcuration/contentcuration/frontend/shared/feedbackApiUtils.js new file mode 100644 index 0000000000..df18f76d70 --- /dev/null +++ b/contentcuration/contentcuration/frontend/shared/feedbackApiUtils.js @@ -0,0 +1,155 @@ +// Helper functions and Utils for creating an API request to +// Feedback mechanism endpoints +import { v4 as uuidv4 } from 'uuid'; +import client from './client'; +import urls from 'shared/urls'; + +export const FeedbackTypeOptions = { + imported: 'imported', + rejected: 'rejected', + previewed: 'previewed', + showmore: 'showmore', + ignored: 'ignored', + flagged: 'flagged', +}; + +// This is mock currently, fixed value of URL still to be decided +// referencing the url by name +export const FLAG_FEEDBACK_EVENT_URL = urls[`${'flagged'}_${'list'}`]; + +/** + * @typedef {Object} BaseFeedbackParams + * @property {Object} context - The context associated with the ObjectType. + * @property {string} contentnode_id - Id for ContentNode that does not change. + * @property {string} content_id - content_id of ContentNode wrt to ObjectType. + */ + +/** + * Base class for feedback data objects. + */ +class BaseFeedback { + /** + * Initializes a new BaseFeedback object. + * + * @class + * @classdesc Represents a base feedback object with common properties and methods. + * @param {BaseFeedbackParams} object + */ + constructor({ context = {}, contentnode_id, content_id }) { + this.id = uuidv4(); + this.context = context; + this.contentnode_id = contentnode_id; + this.content_id = content_id; + } + + // Creates a data object according to Backends expectation, + // excluding functions and the "URL" property. + getDataObject() { + const dataObject = {}; + for (const key in this) { + if ( + Object.prototype.hasOwnProperty.call(this, key) && + typeof this[key] !== 'function' && + key !== 'URL' + ) { + dataObject[key] = this[key]; + } + } + return dataObject; + } + + // Return URL associated with the ObjectType + getUrl() { + if (this.defaultURL === null || this.URL === undefined) { + throw new Error('URL is not defined for the FeedBack Object.'); + } + return this.URL; + } +} + +/** + * Initializes a new BaseFeedbackEvent object. + * + * @param {Object} params - Parameters for initializing the object. + * @param {string} params.user_id - The user_id associated with the feedback event. + * @param {string} params.target_channel_id - The ID of the target channel for the feedback event. + * @param {BaseFeedbackParams} baseFeedbackParams - Additional parameters inherited + * from the base feedbackclass. + */ +// eslint-disable-next-line no-unused-vars +class BaseFeedbackEvent extends BaseFeedback { + constructor({ user_id, target_channel_id, ...baseFeedbackParams }) { + super(baseFeedbackParams); + this.user_id = user_id; + this.target_channel_id = target_channel_id; + } +} + +/** + * Initializes a new BaseFeedbackInteractionEvent object. + * @param {Object} params - Parameters for initializing the interaction event. + * @param {string} params.feedback_type - The type of feedback for the interaction event. + * @param {string} params.feedback_reason - The reason associated with the feedback. + * @param {BaseFeedbackParams} baseFeedbackParams - Additional parameters inherited from the + * base feedbackclass. + */ +class BaseFeedbackInteractionEvent extends BaseFeedback { + constructor({ feedback_type, feedback_reason, ...baseFeedbackParams }) { + super(baseFeedbackParams); + this.feedback_type = feedback_type; + this.feedback_reason = feedback_reason; + } +} + +/** + * Initializes a new BaseFlagFeedback object. + * + * @param {Object} params - Parameters for initializing the flag feedback. + * @param {string} params.target_topic_id - The ID of the target topic associated with + * the flag feedback. + * @param {BaseFeedbackParams} baseFeedbackParams - Additional parameters inherited from the + * base interaction event class. + */ +class BaseFlagFeedback extends BaseFeedbackInteractionEvent { + constructor({ target_topic_id, ...baseFeedbackParams }) { + super({ ...baseFeedbackParams }); + this.target_topic_id = target_topic_id; + } +} + +/** + * Initializes a new FlagFeedbackEvent object. + * + * + * @param {Object} params - Parameters for initializing the flag feedback event. + * @param {string} params.target_topic_id - Id of the topic where the flagged content is located. + * @param {Object} baseFeedbackParams - Additional parameters inherited from the + * base flag feedback class. + */ +export class FlagFeedbackEvent extends BaseFlagFeedback { + constructor({ target_topic_id, ...baseFeedbackParams }) { + super({ target_topic_id, ...baseFeedbackParams }); + this.URL = FLAG_FEEDBACK_EVENT_URL; + } +} + +/** + * Sends a request using the provided feedback object. + * + * @function + * + * @param {BaseFeedback} feedbackObject - The feedback object to use for the request. + * @throws {Error} Throws an error if the URL is not defined for the feedback object. + * @returns {Promise} A promise that resolves to the response data from the API. + */ +export async function sendRequest(feedbackObject) { + try { + const url = feedbackObject.getUrl(); + const response = await client.post(url, feedbackObject.getDataObject()); + console.log('API response:', response.data); + return response.data; + } catch (error) { + console.error('Error:', error); + throw error; + } +}