From e25ff05b4111a40f5fc00e57dd0a75cbb95baab7 Mon Sep 17 00:00:00 2001 From: Antoine SEIN <142824551+asein-sinch@users.noreply.github.com> Date: Thu, 22 Aug 2024 09:42:59 +0200 Subject: [PATCH] DEVEXP-518: E2E SMS/Groups (#126) --- .../src/sms/groups/list/list.ts | 8 +- .../models/v1/create-group-response/index.ts | 4 - .../group.ts} | 2 +- packages/sms/src/models/v1/group/index.ts | 1 + packages/sms/src/models/v1/index.ts | 2 +- .../list-groups-response.ts | 4 +- .../update-group-request.ts | 2 +- .../rest/v1/groups/groups-api.jest.fixture.ts | 13 +- packages/sms/src/rest/v1/groups/groups-api.ts | 39 ++-- .../e2e/features/delivery-reports.feature | 0 .../tests/rest/v1/groups/groups-api.test.ts | 10 +- .../sms/tests/rest/v1/groups/groups.steps.ts | 191 ++++++++++++++++++ 12 files changed, 232 insertions(+), 44 deletions(-) delete mode 100644 packages/sms/src/models/v1/create-group-response/index.ts rename packages/sms/src/models/v1/{create-group-response/create-group-response.ts => group/group.ts} (94%) create mode 100644 packages/sms/src/models/v1/group/index.ts delete mode 100644 packages/sms/tests/e2e/features/delivery-reports.feature create mode 100644 packages/sms/tests/rest/v1/groups/groups.steps.ts diff --git a/examples/simple-examples/src/sms/groups/list/list.ts b/examples/simple-examples/src/sms/groups/list/list.ts index da28f28d..0d635738 100644 --- a/examples/simple-examples/src/sms/groups/list/list.ts +++ b/examples/simple-examples/src/sms/groups/list/list.ts @@ -6,14 +6,14 @@ import { import { getPrintFormat, printFullResponse } from '../../../config'; const populateGroupsList = ( - groupsPage: PageResult, - fullGroupsList: Sms.CreateGroupResponse[], + groupsPage: PageResult, + fullGroupsList: Sms.Group[], groupsList: string[], ) => { // Populate the data structure that holds the response content fullGroupsList.push(...groupsPage.data); // Populate the data structure that holds the response content for pretty print - groupsPage.data.map((group: Sms.CreateGroupResponse) => { + groupsPage.data.map((group: Sms.Group) => { groupsList.push(`Group ID: ${group.id} - Group name: ${group.name}`); }); }; @@ -39,7 +39,7 @@ export const list = async(smsService: SmsService) => { } // Init data structure to hold the response content - const fullGroupsList: Sms.CreateGroupResponse[] = []; + const fullGroupsList: Sms.Group[] = []; // Init data structure to hold the response content for pretty print const groupsList: string[] = []; diff --git a/packages/sms/src/models/v1/create-group-response/index.ts b/packages/sms/src/models/v1/create-group-response/index.ts deleted file mode 100644 index 6757713b..00000000 --- a/packages/sms/src/models/v1/create-group-response/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export type { CreateGroupResponse } from './create-group-response'; -export type { CreateGroupResponse as GroupResponse } from './create-group-response'; -export type { CreateGroupResponse as ReplaceGroupResponse } from './create-group-response'; -export type { CreateGroupResponse as UpdateGroupResponse } from './create-group-response'; diff --git a/packages/sms/src/models/v1/create-group-response/create-group-response.ts b/packages/sms/src/models/v1/group/group.ts similarity index 94% rename from packages/sms/src/models/v1/create-group-response/create-group-response.ts rename to packages/sms/src/models/v1/group/group.ts index db546bc0..a6a4a41c 100644 --- a/packages/sms/src/models/v1/create-group-response/create-group-response.ts +++ b/packages/sms/src/models/v1/group/group.ts @@ -1,6 +1,6 @@ import { GroupAutoUpdate } from '../group-auto-update'; -export interface CreateGroupResponse { +export interface Group { /** The ID used to reference this group. */ id?: string; diff --git a/packages/sms/src/models/v1/group/index.ts b/packages/sms/src/models/v1/group/index.ts new file mode 100644 index 00000000..48e367f1 --- /dev/null +++ b/packages/sms/src/models/v1/group/index.ts @@ -0,0 +1 @@ +export type { Group } from './group'; diff --git a/packages/sms/src/models/v1/index.ts b/packages/sms/src/models/v1/index.ts index b8c19abc..a7cd9de0 100644 --- a/packages/sms/src/models/v1/index.ts +++ b/packages/sms/src/models/v1/index.ts @@ -9,7 +9,7 @@ export * from './api-update-mt-message'; export * from './api-update-text-mt-message'; export * from './binary-request'; export * from './binary-response'; -export * from './create-group-response'; +export * from './group'; export * from './delivery-report'; export * from './delivery-report-list'; export * from './dry-run-request'; diff --git a/packages/sms/src/models/v1/list-groups-response/list-groups-response.ts b/packages/sms/src/models/v1/list-groups-response/list-groups-response.ts index 17bb4adc..65ae69df 100644 --- a/packages/sms/src/models/v1/list-groups-response/list-groups-response.ts +++ b/packages/sms/src/models/v1/list-groups-response/list-groups-response.ts @@ -1,4 +1,4 @@ -import { CreateGroupResponse } from '../create-group-response'; +import { Group } from '../group'; export interface ListGroupsResponse { @@ -9,7 +9,7 @@ export interface ListGroupsResponse { /** The total number of groups. */ count?: number; /** List of GroupObjects */ - groups?: CreateGroupResponse[]; + groups?: Group[]; } diff --git a/packages/sms/src/models/v1/update-group-request/update-group-request.ts b/packages/sms/src/models/v1/update-group-request/update-group-request.ts index e98f1eff..98ef9e2a 100644 --- a/packages/sms/src/models/v1/update-group-request/update-group-request.ts +++ b/packages/sms/src/models/v1/update-group-request/update-group-request.ts @@ -3,7 +3,7 @@ import { UpdateGroupRequestAutoUpdate } from '../update-group-request-auto-updat export interface UpdateGroupRequest { /** The name of the group. Omitting `name` from the JSON body will leave the name unchanged. To remove an existing name set, name explicitly to the JSON value `null`. */ - name?: string; + name?: string | null; /** Add a list of phone numbers (MSISDNs) to this group. The phone numbers are a strings within an array and must be in E.164 format. */ add?: string[]; /** Remove a list of phone numbers (MSISDNs) to this group.The phone numbers are a strings within an array and must be in E.164 format. */ diff --git a/packages/sms/src/rest/v1/groups/groups-api.jest.fixture.ts b/packages/sms/src/rest/v1/groups/groups-api.jest.fixture.ts index 6c1884ff..49c678c4 100644 --- a/packages/sms/src/rest/v1/groups/groups-api.jest.fixture.ts +++ b/packages/sms/src/rest/v1/groups/groups-api.jest.fixture.ts @@ -1,7 +1,6 @@ import { GroupsApi } from './groups-api'; import { - CreateGroupResponse, - GroupResponse, + Group, CreateGroupRequestData, DeleteGroupRequestData, ListMembersRequestData, @@ -17,7 +16,7 @@ export class GroupsApiFixture implements Partial> { /** * Fixture associated to function createGroup */ - public create: jest.Mock, [CreateGroupRequestData]> = jest.fn(); + public create: jest.Mock, [CreateGroupRequestData]> = jest.fn(); /** * Fixture associated to function deleteGroup */ @@ -29,17 +28,17 @@ export class GroupsApiFixture implements Partial> { /** * Fixture associated to function listGroups */ - public list: jest.Mock, [ListGroupsRequestData]> = jest.fn(); + public list: jest.Mock, [ListGroupsRequestData]> = jest.fn(); /** * Fixture associated to function replaceGroup */ - public replace: jest.Mock, [ReplaceGroupRequestData]> = jest.fn(); + public replace: jest.Mock, [ReplaceGroupRequestData]> = jest.fn(); /** * Fixture associated to function retrieveGroup */ - public get: jest.Mock, [GetGroupRequestData]> = jest.fn(); + public get: jest.Mock, [GetGroupRequestData]> = jest.fn(); /** * Fixture associated to function updateGroup */ - public update: jest.Mock, [UpdateGroupRequestData]> = jest.fn(); + public update: jest.Mock, [UpdateGroupRequestData]> = jest.fn(); } diff --git a/packages/sms/src/rest/v1/groups/groups-api.ts b/packages/sms/src/rest/v1/groups/groups-api.ts index c077867c..102a4f43 100644 --- a/packages/sms/src/rest/v1/groups/groups-api.ts +++ b/packages/sms/src/rest/v1/groups/groups-api.ts @@ -1,9 +1,12 @@ import { CreateGroupRequestData, - CreateGroupResponse, DeleteGroupRequestData, GetGroupRequestData, - GroupResponse, ListGroupsRequestData, ListMembersRequestData, ReplaceGroupRequestData, - ReplaceGroupResponse, UpdateGroupRequestData, - UpdateGroupResponse, + DeleteGroupRequestData, + GetGroupRequestData, + ListGroupsRequestData, + ListMembersRequestData, + ReplaceGroupRequestData, + UpdateGroupRequestData, + Group, } from '../../../models'; import { RequestBody, @@ -32,7 +35,7 @@ export class GroupsApi extends SmsDomainApi { * A group is a set of phone numbers (MSISDNs) that can be used as a target in the `send_batch_msg` operation. An MSISDN can only occur once in a group and any attempts to add a duplicate would be ignored but not rejected. * @param { CreateGroupRequestData } data - The data to provide to the API call. */ - public async create(data: CreateGroupRequestData): Promise { + public async create(data: CreateGroupRequestData): Promise { this.client = this.getSinchClient(); const getParams = this.client.extractQueryParams(data, [] as never[]); const headers: { [key: string]: string | undefined } = { @@ -47,7 +50,7 @@ export class GroupsApi extends SmsDomainApi { = await this.client.prepareOptions(basePathUrl, 'POST', getParams, headers, body || undefined); const url = this.client.prepareUrl(requestOptions.hostname, requestOptions.queryParams); - return this.client.processCall({ + return this.client.processCall({ url, requestOptions, apiName: this.apiName, @@ -115,12 +118,10 @@ export class GroupsApi extends SmsDomainApi { * List Groups * With the list operation you can list all groups that you have created. This operation supports pagination. Groups are returned in reverse chronological order. * @param { ListGroupsRequestData } data - The data to provide to the API call. - * @return {ApiListPromise} + * @return {ApiListPromise} */ - public list(data: ListGroupsRequestData): ApiListPromise { + public list(data: ListGroupsRequestData): ApiListPromise { this.client = this.getSinchClient(); - data['page'] = data['page'] !== undefined ? data['page'] : 0; - data['page_size'] = data['page_size'] !== undefined ? data['page_size'] : 30; const getParams = this.client.extractQueryParams(data, ['page', 'page_size']); const headers: { [key: string]: string | undefined } = { 'Content-Type': 'application/json', @@ -141,7 +142,7 @@ export class GroupsApi extends SmsDomainApi { }; // Create the promise containing the response wrapped as a PageResult - const listPromise = buildPageResultPromise( + const listPromise = buildPageResultPromise( this.client, requestOptionsPromise, operationProperties); @@ -149,11 +150,11 @@ export class GroupsApi extends SmsDomainApi { // Add properties to the Promise to offer the possibility to use it as an iterator Object.assign( listPromise, - createIteratorMethodsForPagination( + createIteratorMethodsForPagination( this.client, requestOptionsPromise, listPromise, operationProperties), ); - return listPromise as ApiListPromise; + return listPromise as ApiListPromise; } /** @@ -161,7 +162,7 @@ export class GroupsApi extends SmsDomainApi { * The replace operation will replace all parameters, including members, of an existing group with new values. Replacing a group targeted by a batch message scheduled in the future is allowed and changes will be reflected when the batch is sent. * @param { ReplaceGroupRequestData } data - The data to provide to the API call. */ - public async replace(data: ReplaceGroupRequestData): Promise { + public async replace(data: ReplaceGroupRequestData): Promise { this.client = this.getSinchClient(); const getParams = this.client.extractQueryParams(data, [] as never[]); const headers: { [key: string]: string | undefined } = { @@ -176,7 +177,7 @@ export class GroupsApi extends SmsDomainApi { = await this.client.prepareOptions(basePathUrl, 'PUT', getParams, headers, body || undefined); const url = this.client.prepareUrl(requestOptions.hostname, requestOptions.queryParams); - return this.client.processCall({ + return this.client.processCall({ url, requestOptions, apiName: this.apiName, @@ -189,7 +190,7 @@ export class GroupsApi extends SmsDomainApi { * This operation retrieves a specific group with the provided group ID. * @param { GetGroupRequestData } data - The data to provide to the API call. */ - public async get(data: GetGroupRequestData): Promise { + public async get(data: GetGroupRequestData): Promise { this.client = this.getSinchClient(); const getParams = this.client.extractQueryParams(data, [] as never[]); const headers: { [key: string]: string | undefined } = { @@ -204,7 +205,7 @@ export class GroupsApi extends SmsDomainApi { = await this.client.prepareOptions(basePathUrl, 'GET', getParams, headers, body || undefined); const url = this.client.prepareUrl(requestOptions.hostname, requestOptions.queryParams); - return this.client.processCall({ + return this.client.processCall({ url, requestOptions, apiName: this.apiName, @@ -217,7 +218,7 @@ export class GroupsApi extends SmsDomainApi { * With the update group operation, you can add and remove members in an existing group as well as rename the group. This method encompasses a few ways to update a group: 1. By using `add` and `remove` arrays containing phone numbers, you control the group movements. Any list of valid numbers in E.164 format can be added. 2. By using the `auto_update` object, your customer can add or remove themselves from groups. 3. You can also add or remove other groups into this group with `add_from_group` and `remove_from_group`. #### Other group update info - The request will not be rejected for duplicate adds or unknown removes. - The additions will be done before the deletions. If an phone number is on both lists, it will not be apart of the resulting group. - Updating a group targeted by a batch message scheduled in the future is allowed. Changes will be reflected when the batch is sent. * @param { UpdateGroupRequestData } data - The data to provide to the API call. */ - public async update(data: UpdateGroupRequestData): Promise { + public async update(data: UpdateGroupRequestData): Promise { this.client = this.getSinchClient(); const getParams = this.client.extractQueryParams(data, [] as never[]); const headers: { [key: string]: string | undefined } = { @@ -232,7 +233,7 @@ export class GroupsApi extends SmsDomainApi { = await this.client.prepareOptions(basePathUrl, 'POST', getParams, headers, body || undefined); const url = this.client.prepareUrl(requestOptions.hostname, requestOptions.queryParams); - return this.client.processCall({ + return this.client.processCall({ url, requestOptions, apiName: this.apiName, diff --git a/packages/sms/tests/e2e/features/delivery-reports.feature b/packages/sms/tests/e2e/features/delivery-reports.feature deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/sms/tests/rest/v1/groups/groups-api.test.ts b/packages/sms/tests/rest/v1/groups/groups-api.test.ts index 97685e34..691f7fed 100644 --- a/packages/sms/tests/rest/v1/groups/groups-api.test.ts +++ b/packages/sms/tests/rest/v1/groups/groups-api.test.ts @@ -31,7 +31,7 @@ describe('GroupsApi', () => { ], }, }; - const expectedResponse: Sms.CreateGroupResponse = { + const expectedResponse: Sms.Group = { id: '01HF6EFE21REWJC3B3JWG4FYZ7', name: 'My group', size: 2, @@ -96,7 +96,7 @@ describe('GroupsApi', () => { // Given const requestData: Sms.ListGroupsRequestData = {}; - const mockData: Sms.GroupResponse[] =[ + const mockData: Sms.Group[] =[ { id: '01HF6EFE21REWJC3B3JWG4FYZ7', name: 'My group', @@ -137,7 +137,7 @@ describe('GroupsApi', () => { ], }, }; - const expectedResponse: Sms.GroupResponse = { + const expectedResponse: Sms.Group = { id: '01HF6EFE21REWJC3B3JWG4FYZ7', name: 'My new group name', size: 1, @@ -162,7 +162,7 @@ describe('GroupsApi', () => { const requestData: Sms.GetGroupRequestData = { group_id: '01HF6EFE21REWJC3B3JWG4FYZ7', }; - const expectedResponse: Sms.GroupResponse = { + const expectedResponse: Sms.Group = { id: '01HF6EFE21REWJC3B3JWG4FYZ7', name: 'My new group name', size: 1, @@ -193,7 +193,7 @@ describe('GroupsApi', () => { ], }, }; - const expectedResponse: Sms.GroupResponse = { + const expectedResponse: Sms.Group = { id: '01HF6EFE21REWJC3B3JWG4FYZ7', name: 'My new group name', size: 3, diff --git a/packages/sms/tests/rest/v1/groups/groups.steps.ts b/packages/sms/tests/rest/v1/groups/groups.steps.ts new file mode 100644 index 00000000..ab959c95 --- /dev/null +++ b/packages/sms/tests/rest/v1/groups/groups.steps.ts @@ -0,0 +1,191 @@ +import { GroupsApi, SmsService, Sms } from '../../../../src'; +import { Given, When, Then } from '@cucumber/cucumber'; +import * as assert from 'assert'; +import { PageResult } from '@sinch/sdk-client'; + +let groupsApi: GroupsApi; +let group: Sms.Group; +let listResponse: PageResult; +let groupsList: Sms.Group[]; +let pagesIteration: number; +let deleteGroupResponse: void; +let phoneNumbersList: string[]; + +Given('the SMS service "Groups" is available', () => { + const smsService = new SmsService({ + projectId: 'tinyfrog-jump-high-over-lilypadbasin', + keyId: 'keyId', + keySecret: 'keySecret', + authHostname: 'http://localhost:3011', + smsHostname: 'http://localhost:3017', + }); + groupsApi = smsService.groups; +}); + +When('I send a request to create an SMS group', async () => { + group = await groupsApi.create({ + createGroupRequestBody: { + name: 'Group master', + members: [ + '+12017778888', + '+12018887777', + ], + child_groups: [ + '01W4FFL35P4NC4K35SUBGROUP1', + ], + }, + }); +}); + +Then('the response contains the SMS group details', () => { + assert.equal(group.id, '01W4FFL35P4NC4K35SMSGROUP1'); + assert.equal(group.name, 'Group master'); + assert.equal(group.size, 2); + assert.deepEqual(group.created_at, new Date('2024-06-06T08:59:22.156Z')); + assert.deepEqual(group.modified_at, new Date('2024-06-06T08:59:22.156Z')); + assert.ok(group.child_groups); + assert.equal(group.child_groups[0], '01W4FFL35P4NC4K35SUBGROUP1'); +}); + +When('I send a request to list the existing SMS groups', async () => { + listResponse = await groupsApi.list({ + page_size: 2, + }); +}); + +Then('the response contains {string} SMS groups', (expectedAnswer: string) => { + const expectedGroupsCount = parseInt(expectedAnswer, 10); + assert.equal(listResponse.data.length, expectedGroupsCount); +}); + +When('I send a request to list all the SMS groups', async () => { + groupsList = []; + for await (const group of groupsApi.list({ page_size: 2 })) { + groupsList.push(group); + } +}); + +When('I iterate manually over the SMS groups pages', async () => { + groupsList = []; + listResponse = await groupsApi.list({ + page_size: 2, + }); + groupsList.push(...listResponse.data); + pagesIteration = 1; + let reachedEndOfPages = false; + while (!reachedEndOfPages) { + if (listResponse.hasNextPage) { + listResponse = await listResponse.nextPage(); + groupsList.push(...listResponse.data); + pagesIteration++; + } else { + reachedEndOfPages = true; + } + } +}); + +Then('the SMS groups list contains {string} SMS groups', (expectedAnswer: string) => { + const expectedGroupsCount = parseInt(expectedAnswer, 10); + assert.equal(groupsList.length, expectedGroupsCount); +}); + +Then('the SMS groups iteration result contains the data from {string} pages', (expectedAnswer: string) => { + const expectedPagesCount = parseInt(expectedAnswer, 10); + assert.equal(pagesIteration, expectedPagesCount); +}); + +When('I send a request to retrieve an SMS group', async () => { + group = await groupsApi.get({ + group_id: '01W4FFL35P4NC4K35SMSGROUP1', + }); +}); + +When('I send a request to update an SMS group', async () => { + group = await groupsApi.update({ + group_id: '01W4FFL35P4NC4K35SMSGROUP1', + updateGroupRequestBody: { + name: 'Updated group name', + add: [ + '+12017771111', + '+12017772222', + ], + remove: [ + '+12017773333', + '+12017774444', + ], + add_from_group: '01W4FFL35P4NC4K35SMSGROUP2', + remove_from_group: '01W4FFL35P4NC4K35SMSGROUP3', + }, + }); +}); + +Then('the response contains the updated SMS group details', () => { + assert.equal(group.id, '01W4FFL35P4NC4K35SMSGROUP1'); + assert.equal(group.name, 'Updated group name'); + assert.equal(group.size, 6); + assert.deepEqual(group.created_at, new Date('2024-06-06T08:59:22.156Z')); + assert.deepEqual(group.modified_at, new Date('2024-06-06T09:19:58.147Z')); + assert.ok(group.child_groups); + assert.equal(group.child_groups[0], '01W4FFL35P4NC4K35SUBGROUP1'); +}); + +When('I send a request to update an SMS group to remove its name', async () => { + group = await groupsApi.update({ + group_id: '01W4FFL35P4NC4K35SMSGROUP2', + updateGroupRequestBody: { + name: null, + }, + }); +}); + +Then('the response contains the updated SMS group details where the name has been removed', () => { + assert.equal(group.id, '01W4FFL35P4NC4K35SMSGROUP2'); + assert.equal(group.name, undefined); +}); + +When('I send a request to replace an SMS group', async () => { + group = await groupsApi.replace({ + group_id: '01W4FFL35P4NC4K35SMSGROUP1', + replaceGroupRequestBody: { + name: 'Replacement group', + members: [ + '+12018881111', + '+12018882222', + '+12018883333', + ], + }, + }); +}); + +Then('the response contains the replaced SMS group details', () => { + assert.equal(group.id, '01W4FFL35P4NC4K35SMSGROUP1'); + assert.equal(group.name, 'Replacement group'); + assert.equal(group.size, 3); + assert.deepEqual(group.created_at, new Date('2024-06-06T08:59:22.156Z')); + assert.deepEqual(group.modified_at, new Date('2024-08-21T09:39:36.679Z')); + assert.ok(group.child_groups); + assert.equal(group.child_groups[0], '01W4FFL35P4NC4K35SUBGROUP1'); +}); + +When('I send a request to delete an SMS group', async () => { + deleteGroupResponse = await groupsApi.delete({ + group_id: '01W4FFL35P4NC4K35SMSGROUP1', + }); +}); + +Then('the delete SMS group response contains no data', () => { + assert.deepEqual(deleteGroupResponse, {} ); +}); + +When('I send a request to list the members of an SMS group', async () => { + phoneNumbersList = await groupsApi.listMembers({ + group_id: '01W4FFL35P4NC4K35SMSGROUP1', + }); +}); + +Then('the response contains the phone numbers of the SMS group', () => { + assert.ok(phoneNumbersList); + assert.equal(phoneNumbersList[0], '12018881111'); + assert.equal(phoneNumbersList[1], '12018882222'); + assert.equal(phoneNumbersList[2], '12018883333'); +});