Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion plugin-hrm-form/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,12 @@ import {
} from '../../../states/csam-report/actions';
import { changeRoute } from '../../../states/routing/actions';
import { CSAMReportEntry } from '../../../types/types';
import { reportToIWF, selfReportToIWF } from '../../../services/ServerlessService';
import { reportToIWF, selfReportToIWF } from '../../../services/iwfService';
import { acknowledgeCSAMReport, createCSAMReport } from '../../../services/CSAMReportService';
import { addExternalReportEntry } from '../../../states/csam-report/existingContactExternalReport';
import { csamReportBase, namespace, routingBase } from '../../../states/storeNamespaces';

jest.mock('../../../services/ServerlessService', () => ({
jest.mock('../../../services/iwfService', () => ({
reportToIWF: jest.fn(),
selfReportToIWF: jest.fn(),
}));
Expand Down
159 changes: 159 additions & 0 deletions plugin-hrm-form/src/___tests__/services/iwfService.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
/**
* Copyright (C) 2021-2023 Technology Matters
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/

/* eslint-disable camelcase */
import { reportToIWF, selfReportToIWF } from '../../services/iwfService';
import fetchProtectedApi from '../../services/fetchProtectedApi';
import { getAseloFeatureFlags } from '../../hrmConfig';

jest.mock('../../services/fetchProtectedApi');
jest.mock('../../hrmConfig');

const mockFetchProtectedApi = fetchProtectedApi as jest.MockedFunction<typeof fetchProtectedApi>;
const mockGetAseloFeatureFlags = getAseloFeatureFlags as jest.MockedFunction<typeof getAseloFeatureFlags>;

const counselorForm = {
webAddress: 'http://example.com',
description: 'Test description',
anonymous: 'non-anonymous',
firstName: 'John',
lastName: 'Doe',
email: 'john@example.com',
};

const childForm = {
childAge: '13-15',
ageVerified: true,
};

beforeEach(() => {
mockFetchProtectedApi.mockClear();
mockGetAseloFeatureFlags.mockClear();
});

describe('reportToIWF', () => {
const serverlessResponse = {
'IWFReportService1.0': { responseData: 'ref123', responseCode: '200', responseDescription: 'OK' },
};
const lambdaResponse = { status: 200, data: serverlessResponse };

describe('feature flag disabled (serverless)', () => {
beforeEach(() => {
mockGetAseloFeatureFlags.mockReturnValue({ use_twilio_lambda_for_iwf_reporting: false } as any);
mockFetchProtectedApi.mockResolvedValue(serverlessResponse);
});

test('calls fetchProtectedApi with serverless endpoint', async () => {
await reportToIWF(counselorForm);
expect(mockFetchProtectedApi).toHaveBeenCalledWith(
'/reportToIWF',
expect.objectContaining({ Reported_URL: 'http://example.com' }),
{ useTwilioLambda: false },
);
});

test('returns the serverless response directly', async () => {
const result = await reportToIWF(counselorForm);
expect(result).toStrictEqual(serverlessResponse);
});

test('sets Reporter_Anonymous to N for non-anonymous form', async () => {
await reportToIWF(counselorForm);
expect(mockFetchProtectedApi).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({ Reporter_Anonymous: 'N' }),
expect.any(Object),
);
});

test('sets Reporter_Anonymous to Y for anonymous form', async () => {
await reportToIWF({ ...counselorForm, anonymous: 'anonymous' });
expect(mockFetchProtectedApi).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({ Reporter_Anonymous: 'Y' }),
expect.any(Object),
);
});
});

describe('feature flag enabled (lambda)', () => {
beforeEach(() => {
mockGetAseloFeatureFlags.mockReturnValue({ use_twilio_lambda_for_iwf_reporting: true } as any);
mockFetchProtectedApi.mockResolvedValue(lambdaResponse);
});

test('calls fetchProtectedApi with lambda endpoint', async () => {
await reportToIWF(counselorForm);
expect(mockFetchProtectedApi).toHaveBeenCalledWith(
'/integrations/iwf/reportToIWF',
expect.objectContaining({ Reported_URL: 'http://example.com' }),
{ useTwilioLambda: true },
);
});

test('returns response.data (unwrapped from lambda wrapper)', async () => {
const result = await reportToIWF(counselorForm);
expect(result).toStrictEqual(serverlessResponse);
});
});
});

describe('selfReportToIWF', () => {
const caseNumber = 'case-001';
const selfReportResponse = { reportUrl: 'http://iwf.example.com/report?t=token', status: 'OK' };

describe('feature flag disabled (serverless)', () => {
beforeEach(() => {
mockGetAseloFeatureFlags.mockReturnValue({ use_twilio_lambda_for_iwf_reporting: false } as any);
mockFetchProtectedApi.mockResolvedValue(selfReportResponse);
});

test('calls fetchProtectedApi with serverless endpoint', async () => {
await selfReportToIWF(childForm, caseNumber);
expect(mockFetchProtectedApi).toHaveBeenCalledWith(
'/selfReportToIWF',
{ user_age_range: '13-15', case_number: caseNumber },
{ useTwilioLambda: false },
);
});

test('returns the response', async () => {
const result = await selfReportToIWF(childForm, caseNumber);
expect(result).toStrictEqual(selfReportResponse);
});
});

describe('feature flag enabled (lambda)', () => {
beforeEach(() => {
mockGetAseloFeatureFlags.mockReturnValue({ use_twilio_lambda_for_iwf_reporting: true } as any);
mockFetchProtectedApi.mockResolvedValue(selfReportResponse);
});

test('calls fetchProtectedApi with lambda endpoint', async () => {
await selfReportToIWF(childForm, caseNumber);
expect(mockFetchProtectedApi).toHaveBeenCalledWith(
'/integrations/iwf/selfReportToIWF',
{ user_age_range: '13-15', case_number: caseNumber },
{ useTwilioLambda: true },
);
});

test('returns the response', async () => {
const result = await selfReportToIWF(childForm, caseNumber);
expect(result).toStrictEqual(selfReportResponse);
});
});
});
2 changes: 1 addition & 1 deletion plugin-hrm-form/src/components/CSAMReport/csamReportApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import {
} from '../../states/csam-report/types';
import { addExternalReportEntry } from '../../states/csam-report/existingContactExternalReport';
import { acknowledgeCSAMReport, createCSAMReport } from '../../services/CSAMReportService';
import { reportToIWF, selfReportToIWF } from '../../services/ServerlessService';
import { reportToIWF, selfReportToIWF } from '../../services/iwfService';
import { newCSAMReportActionForContact } from '../../states/csam-report/actions';
import { csamReportBase, namespace } from '../../states/storeNamespaces';
import { getCurrentTopmostRouteForTask } from '../../states/routing/getRoute';
Expand Down
28 changes: 0 additions & 28 deletions plugin-hrm-form/src/services/ServerlessService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import { ITask, Notifications } from '@twilio/flex-ui';
import { DefinitionVersion, loadDefinition } from 'hrm-form-definitions';

import fetchProtectedApi from './fetchProtectedApi';
import type { ChildCSAMReportForm, CounselorCSAMReportForm } from '../states/csam-report/types';
import { getHrmConfig } from '../hrmConfig';

type TransferChatStartBody = {
Expand Down Expand Up @@ -87,39 +86,12 @@ export const getExternalRecordingS3Location = async (callSid: string) => {
return response;
};

/**
* Send a CSAM report to IWF
*/
export const reportToIWF = async (form: CounselorCSAMReportForm) => {
const body = {
Reported_URL: form.webAddress,
Reporter_Description: form.description,
Reporter_Anonymous: form.anonymous === 'anonymous' ? 'Y' : 'N',
Reporter_First_Name: form.firstName,
Reporter_Last_Name: form.lastName,
Reporter_Email_ID: form.email,
};

const response = await fetchProtectedApi('/reportToIWF', body);
return response;
};

export const saveContactToSaferNet = async (payload: any): Promise<string> => {
const body = { payload: JSON.stringify(payload) };
const postSurveyUrl = await fetchProtectedApi('/saveContactToSaferNet', body);
return postSurveyUrl;
};

export const selfReportToIWF = async (form: ChildCSAMReportForm, caseNumber: string) => {
const body = {
user_age_range: form.childAge,
case_number: caseNumber,
};

const response = await fetchProtectedApi('/selfReportToIWF', body);
return response;
};

export const getMediaUrl = async (serviceSid: string, mediaSid: string) => {
const body = { serviceSid, mediaSid };

Expand Down
56 changes: 56 additions & 0 deletions plugin-hrm-form/src/services/iwfService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
* Copyright (C) 2021-2023 Technology Matters
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/

/* eslint-disable camelcase */
import fetchProtectedApi from './fetchProtectedApi';
import { getAseloFeatureFlags } from '../hrmConfig';
import type { ChildCSAMReportForm, CounselorCSAMReportForm } from '../states/csam-report/types';

/**
* Send a CSAM report to IWF
*/
export const reportToIWF = async (form: CounselorCSAMReportForm) => {
const useTwilioLambda = getAseloFeatureFlags().use_twilio_lambda_for_iwf_reporting;
const body = {
Reported_URL: form.webAddress,
Reporter_Description: form.description,
Reporter_Anonymous: form.anonymous === 'anonymous' ? 'Y' : 'N',
Reporter_First_Name: form.firstName,
Reporter_Last_Name: form.lastName,
Reporter_Email_ID: form.email,
};

const response = await fetchProtectedApi(useTwilioLambda ? '/integrations/iwf/reportToIWF' : '/reportToIWF', body, {
useTwilioLambda,
});
// The account-scoped lambda wraps the IWF response in { status, data }, whereas the
// serverless endpoint returns the IWF response directly. Normalise to the direct format.
return useTwilioLambda ? response.data : response;
};

export const selfReportToIWF = async (form: ChildCSAMReportForm, caseNumber: string) => {
const useTwilioLambda = getAseloFeatureFlags().use_twilio_lambda_for_iwf_reporting;
const body = {
user_age_range: form.childAge,
case_number: caseNumber,
};

// The account-scoped lambda returns { reportUrl, status } directly (no wrapper), matching the
// serverless response format, so no normalisation is required here.
return fetchProtectedApi(useTwilioLambda ? '/integrations/iwf/selfReportToIWF' : '/selfReportToIWF', body, {
useTwilioLambda,
});
};
1 change: 1 addition & 0 deletions plugin-hrm-form/src/types/FeatureFlags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,6 @@ export type FeatureFlags = {
use_prepopulate_mappings: boolean; // Use PrepopulateMappings.json instead of PrepopulateKeys.json
use_twilio_lambda_for_conference_functions: boolean; // Use the twilio account scoped lambda for conferencing functions
use_twilio_lambda_for_conversation_duration: boolean; // Use the twilio account scoped lambda to calculate conversationDuration
use_twilio_lambda_for_iwf_reporting: boolean; // Use the twilio account scoped lambda for reportToIWF and selfReportToIWF
use_twilio_lambda_for_task_assignment: boolean; // Use the twilio account scoped lambda for getTasksAndReservations, checkTaskAssignment, completeTaskAssignment
};
Loading