From 404ad492e631694a2ae4728a1f8e10a29f51ac10 Mon Sep 17 00:00:00 2001 From: Antoine SEIN <142824551+asein-sinch@users.noreply.github.com> Date: Tue, 21 May 2024 16:34:10 +0200 Subject: [PATCH 01/54] DEVEXP-435: Support 'Accept-Language' header for SMS Verification (#94) --- .../src/verification/verifications/sms/start-sms.ts | 3 +++ .../start-verification-request.ts | 2 ++ .../src/rest/v1/verifications/verifications-api.ts | 5 +++++ .../tests/rest/v1/verifications/verifications-api.test.ts | 7 +++++-- 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/examples/simple-examples/src/verification/verifications/sms/start-sms.ts b/examples/simple-examples/src/verification/verifications/sms/start-sms.ts index 846413a7..75abfea2 100644 --- a/examples/simple-examples/src/verification/verifications/sms/start-sms.ts +++ b/examples/simple-examples/src/verification/verifications/sms/start-sms.ts @@ -16,6 +16,9 @@ import { const requestData = Verification.startVerificationHelper.buildSmsRequest( verificationIdentity, `test-reference-for-sms-verification_${verificationIdentity}`, + { + locale: 'sv-SE', + }, ); const verificationService = initVerificationService(); diff --git a/packages/verification/src/models/v1/start-verification-request/start-verification-request.ts b/packages/verification/src/models/v1/start-verification-request/start-verification-request.ts index 87a7b652..cc32b2d3 100644 --- a/packages/verification/src/models/v1/start-verification-request/start-verification-request.ts +++ b/packages/verification/src/models/v1/start-verification-request/start-verification-request.ts @@ -36,6 +36,8 @@ export interface SmsOptions { codeType?: CodeType; /** The SMS template must include a placeholder `{{CODE}}` where the verification code will be inserted, and it can otherwise be customized as desired. */ template?: string; + /** A `language-region` identifier according to [IANA](https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry). Only a subset of those identifiers is accepted. */ + locale?: string; } export type CodeType = 'Numeric' | 'Alpha' | 'Alphanumeric'; diff --git a/packages/verification/src/rest/v1/verifications/verifications-api.ts b/packages/verification/src/rest/v1/verifications/verifications-api.ts index 36502254..8dcc5186 100644 --- a/packages/verification/src/rest/v1/verifications/verifications-api.ts +++ b/packages/verification/src/rest/v1/verifications/verifications-api.ts @@ -254,6 +254,9 @@ export class VerificationsApi extends VerificationDomainApi { 'Content-Type': 'application/json; charset=UTF-8', 'Accept': 'application/json', }; + if (data.startVerificationWithSmsRequestBody.smsOptions?.locale !== undefined) { + headers['Accept-Language'] = data.startVerificationWithSmsRequestBody.smsOptions.locale; + } // Special fields handling: see method for details const requestDataBody = this.performStartSmsRequestBodyTransformation(data.startVerificationWithSmsRequestBody); @@ -284,6 +287,8 @@ export class VerificationsApi extends VerificationDomainApi { requestDataBody.smsOptions.expiry = this.formatTime(expiry); } } + // Remove the `locale` property from the body as it is used as a header parameter for the API call + delete requestDataBody.smsOptions?.locale; return requestDataBody; } diff --git a/packages/verification/tests/rest/v1/verifications/verifications-api.test.ts b/packages/verification/tests/rest/v1/verifications/verifications-api.test.ts index e084c97b..947c58ff 100644 --- a/packages/verification/tests/rest/v1/verifications/verifications-api.test.ts +++ b/packages/verification/tests/rest/v1/verifications/verifications-api.test.ts @@ -35,12 +35,15 @@ describe('VerificationsApi', () => { describe ('startVerification', () => { it('should make a POST request to start a verification with an SMS', async () => { // Given - const requestData = Verification.startVerificationHelper.buildSmsRequest('+46700000000'); + const smsOptions: Verification.SmsOptions = { + locale: 'sv-SE', + }; + const requestData = Verification.startVerificationHelper.buildSmsRequest('+46700000000', undefined, smsOptions); const expectedResponse: Verification.StartSmsVerificationResponse = { id: 'some_verification_id', method: 'sms', sms: { - template: 'Your verification code is {{CODE}}. Verified by Sinch', + template: 'Din verifieringskod รคr {{CODE}}.', interceptionTimeout: 298, }, _links, From 6d1a495c9315c68ecc0d2d2d01f512f065e91201 Mon Sep 17 00:00:00 2001 From: Antoine SEIN <142824551+asein-sinch@users.noreply.github.com> Date: Tue, 4 Jun 2024 11:14:57 +0200 Subject: [PATCH 02/54] DEVEXP-433: Update Fax SDK (#95) --- examples/simple-examples/.gitignore | 3 +- examples/simple-examples/README.md | 40 ++-- .../simple-examples/fax-pdf/you-faxed.pdf | Bin 0 -> 77666 bytes examples/simple-examples/package.json | 4 +- examples/simple-examples/src/fax/faxes/get.ts | 2 +- .../simple-examples/src/fax/faxes/list.ts | 36 +++- .../src/fax/faxes/send-fileBase64.ts | 60 ++++++ .../src/fax/faxes/send-filePaths.ts | 45 +++++ .../{send.ts => send-multipleRecipients.ts} | 18 +- .../src/services/fax-event.service.ts | 10 +- .../v3/date-range-filter/date-range-filter.ts | 18 ++ .../src/models/v3/date-range-filter/index.ts | 1 + packages/fax/src/models/v3/enums.ts | 13 +- .../src/models/v3/fax-request/fax-request.ts | 34 +++- .../fax/src/models/v3/fax-request/index.ts | 12 +- packages/fax/src/models/v3/fax/fax.ts | 8 +- .../src/models/v3/faxes-list/faxes-list.ts | 5 + .../fax/src/models/v3/faxes-list/index.ts | 1 + packages/fax/src/models/v3/helper.ts | 8 + packages/fax/src/models/v3/index.ts | 22 +-- .../fax-completed-event.ts | 8 +- .../v3/requests/faxes/faxes-request-data.ts | 20 +- .../models/v3/webhook-event-parsed/index.ts | 1 + packages/fax/src/rest/v3/enums.ts | 1 - .../rest/v3/faxes/faxes-api.jest.fixture.ts | 12 +- packages/fax/src/rest/v3/faxes/faxes-api.ts | 163 ++++++++++++---- packages/fax/tests/models/v3/helper.test.ts | 42 ++++ .../fax/tests/rest/v3/faxes/faxes-api.test.ts | 184 +++++++++++++++--- packages/sdk-client/src/api/api-client.ts | 13 -- .../sdk-client/src/client/api-fetch-client.ts | 26 --- packages/sdk-client/src/utils/date.ts | 31 +++ packages/sdk-client/src/utils/index.ts | 1 + 32 files changed, 655 insertions(+), 187 deletions(-) create mode 100644 examples/simple-examples/fax-pdf/you-faxed.pdf create mode 100644 examples/simple-examples/src/fax/faxes/send-fileBase64.ts create mode 100644 examples/simple-examples/src/fax/faxes/send-filePaths.ts rename examples/simple-examples/src/fax/faxes/{send.ts => send-multipleRecipients.ts} (54%) create mode 100644 packages/fax/src/models/v3/date-range-filter/date-range-filter.ts create mode 100644 packages/fax/src/models/v3/date-range-filter/index.ts create mode 100644 packages/fax/src/models/v3/faxes-list/faxes-list.ts create mode 100644 packages/fax/src/models/v3/faxes-list/index.ts create mode 100644 packages/fax/src/models/v3/helper.ts create mode 100644 packages/fax/src/models/v3/webhook-event-parsed/index.ts create mode 100644 packages/fax/tests/models/v3/helper.test.ts create mode 100644 packages/sdk-client/src/utils/date.ts diff --git a/examples/simple-examples/.gitignore b/examples/simple-examples/.gitignore index 2ccc0a30..e71e7c24 100644 --- a/examples/simple-examples/.gitignore +++ b/examples/simple-examples/.gitignore @@ -6,4 +6,5 @@ .DS_Store .env -fax-pdf/ +fax-pdf/* +!fax-pdf/you-faxed.pdf diff --git a/examples/simple-examples/README.md b/examples/simple-examples/README.md index d78fe15f..9352335e 100644 --- a/examples/simple-examples/README.md +++ b/examples/simple-examples/README.md @@ -255,25 +255,27 @@ yarn run numbers:regions:list ### Fax -| Service | Sample application name and location | Required parameters | -|----------|----------------------------------------------------------------------------------------|-----------------------------------| -| Services | [./src/fax/services/create.ts](./src/fax/services/create.ts) | `PHONE_NUMBER` | -| | [./src/fax/services/get.ts](./src/fax/services/get.ts) | `FAX_SERVICE_ID` | -| | [./src/fax/services/list.ts](./src/fax/services/list.ts) | | -| | [./src/fax/services/listNumbers.ts](./src/fax/services/listNumbers.ts) | `FAX_SERVICE_ID` | -| | [./src/fax/services/listEmailsForNumber.ts](./src/fax/services/listEmailsForNumber.ts) | `PHONE_NUMBER` + `FAX_SERVICE_ID` | -| | [./src/fax/services/update.ts](./src/fax/services/update.ts) | `FAX_SERVICE_ID` | -| | [./src/fax/services/delete.ts](./src/fax/services/delete.ts) | `FAX_SERVICE_ID` | -| Faxes | [./src/fax/faxes/send.ts](./src/fax/faxes/send.ts) | `PHONE_NUMBER` | -| | [./src/fax/faxes/get.ts](./src/fax/faxes/get.ts) | `FAX_ID` | -| | [./src/fax/faxes/list.ts](./src/fax/faxes/list.ts) | | -| | [./src/fax/faxes/downloadContent.ts](./src/fax/faxes/downloadContent.ts) | `FAX_ID` | -| | [./src/fax/faxes/deleteContent.ts](./src/fax/faxes/deleteContent.ts) | `FAX_ID` | -| Emails | [./src/fax/emails/add.ts](./src/fax/emails/add.ts) | `FAX_EMAIL` + `PHONE_NUMBER` | -| | [./src/fax/emails/list.ts](./src/fax/emails/list.ts) | | -| | [./src/fax/emails/listNumbers.ts](./src/fax/emails/listNumbers.ts) | `FAX_EMAIL` | -| | [./src/fax/emails/update.ts](./src/fax/emails/update.ts) | `FAX_EMAIL` + `PHONE_NUMBER` | -| | [./src/fax/emails/delete.ts](./src/fax/emails/delete.ts) | `FAX_EMAIL` | +| Service | Sample application name and location | Required parameters | +|----------|------------------------------------------------------------------------------------------|-------------------------------------| +| Services | [./src/fax/services/create.ts](./src/fax/services/create.ts) | `PHONE_NUMBER` | +| | [./src/fax/services/get.ts](./src/fax/services/get.ts) | `FAX_SERVICE_ID` | +| | [./src/fax/services/list.ts](./src/fax/services/list.ts) | | +| | [./src/fax/services/listNumbers.ts](./src/fax/services/listNumbers.ts) | `FAX_SERVICE_ID` | +| | [./src/fax/services/listEmailsForNumber.ts](./src/fax/services/listEmailsForNumber.ts) | `PHONE_NUMBER` + `FAX_SERVICE_ID` | +| | [./src/fax/services/update.ts](./src/fax/services/update.ts) | `FAX_SERVICE_ID` | +| | [./src/fax/services/delete.ts](./src/fax/services/delete.ts) | `FAX_SERVICE_ID` | +| Faxes | [./src/fax/faxes/send-filePaths.ts](./src/fax/faxes/send-filePaths.ts) | `PHONE_NUMBER` + `FAX_CALLBACK_URL` | +| | [./src/fax/faxes/send-fileBase64.ts](./src/fax/faxes/send-fileBase64.ts) | `PHONE_NUMBER` + `FAX_CALLBACK_URL` | +| | [./src/fax/faxes/send-multipleRecipients.ts](./src/fax/faxes/send-multipleRecipients.ts) | `PHONE_NUMBER` + `FAX_CALLBACK_URL` | +| | [./src/fax/faxes/get.ts](./src/fax/faxes/get.ts) | `FAX_ID` | +| | [./src/fax/faxes/list.ts](./src/fax/faxes/list.ts) | | +| | [./src/fax/faxes/downloadContent.ts](./src/fax/faxes/downloadContent.ts) | `FAX_ID` | +| | [./src/fax/faxes/deleteContent.ts](./src/fax/faxes/deleteContent.ts) | `FAX_ID` | +| Emails | [./src/fax/emails/add.ts](./src/fax/emails/add.ts) | `FAX_EMAIL` + `PHONE_NUMBER` | +| | [./src/fax/emails/list.ts](./src/fax/emails/list.ts) | | +| | [./src/fax/emails/listNumbers.ts](./src/fax/emails/listNumbers.ts) | `FAX_EMAIL` | +| | [./src/fax/emails/update.ts](./src/fax/emails/update.ts) | `FAX_EMAIL` + `PHONE_NUMBER` | +| | [./src/fax/emails/delete.ts](./src/fax/emails/delete.ts) | `FAX_EMAIL` | ### Elastic SIP Trunk diff --git a/examples/simple-examples/fax-pdf/you-faxed.pdf b/examples/simple-examples/fax-pdf/you-faxed.pdf new file mode 100644 index 0000000000000000000000000000000000000000..a1564289357de8747e452c991a462e3812de43af GIT binary patch literal 77666 zcmeFZ2UHW?yEi%zHA)o(=>#bPA|g$C0wPUBniN4mr1#!SC;|dP1Oye4u7C&;rAv_- z6p;>!NDD}Zgc=}(JD~6TfB$E#^PO|<`o6pFy6c8DYi9QB^8B8?=V|*H_;jwR3X5Kn zqTs_Up>iqs_%vL7-K;FV9o$@Hx!f(i?M3XoxP(2pgspFC>nIuMtAJNmm2N4kgJ-a* zs)M(VrJW6zu$PXKj*6bXhKY)TvyHd6jVJhA$;aDG&&}J?+vc#2mx73no2R#@rGq#4 zOyB38myNfoo2$2%f~}>q*I^geR17tgRTSK(qp%2?SBIDm zksO{yB`;I_DU$dX+*<+eHX?^y6w$J=cCfta=Feq%8GL+2RE$eXM$C)?eCTQ8>dhs7 z$Pfwil3MU=sNuwO5(L*DCAfJJ|ZI~?fuT!4Ln)Nvh==fx$W>0s@}WlC{K zydH%a7ubW(VVD1Cdf5B#rhi&0TY6hMyV?EK#UBh>Hr|%j;1e#<-v$2~`-s>-J?P0$ z$Q%y!ucwE%e;$IX%=?gYN1zYEIJh+MPrb+EG0Q@ctbV&q^At|VzuF$xiN z8wWdkZ!RebaSD;E4&GimHlE6EF79rwhpd&R5YYgc;py&nxXie`J$-CIejP16k!#Ag zey_W~7pAndgbW=1XO0iP9ZUeHud8aP0%T-lz+LbUIG6-XRQw%m0YFO&xBvjaN#Hny z3^)#6k%7l`2;}H|cn#>D{(XITqe}-I15e;3O^Yu5zpfAK0c6qH08oZH_yH&bCr_M! zo;ZFI3Wc6Jb&`ULftrevl8S}?3@rl(D~ywam7Sf7_YyxB_eCCdb^$5Di#w+Dk*cv8+uo~va|jC#E|Uh#XL5vM55GBLBT@}1`w5WFBEc|}TEMppTnimIBr z#&!K$28Kq)w@s|9ZEWrA9UQ&9eSH1=0|F!OM@7dxh>c55d7PT|G<@yuYX{0X!zUrk;y6S^vti>x%mbB+WN-k);3{h_YfBu z0Qn8;Z)E=g7afR;{Ma$bG3X&KGIBrgfY2Q~eopiRy^=1}(&G%bSoleXt4TR!O{aLo z^>B>$JbNk5@=8qd;SZr5A^YzEi}=5V>~CQI!ZiX=feVwI4nhYYfbCs@+%Ss&|M>qL z516B`_PH5QuuWh1e(|!5yt`LYqCYU30qs=*7OI997XhJ*N-Y7dG!$7q`zO({rQAV5vD``5Ke46SC_IqnZb;bA zko11u5?Gq(&R1E(1Rw+MSmB$LcjADp&woUR{QIr&N{t4VXSctbP=0%%C@(7a1Ug_D zOFSN2Y~k%6-!g)XlaNc`*NgvB^~@u4?&q_Rlgs9Uu%&O~BYgpx=O8zaF>f_JGxmdK zKvuto90vl&Te^4nIoS??x8AT_pfs)6Ve9O!`E#d&kkQ*le zpc6ucxlmID4;Ta4!p37&_;-fC8{n(j#&<4^902=%7W-uOWTAAXYCd>07VsmayO5LA%Nl_{&^CvxK zKieQM#V=%tu&$6Ub_WJ0c1On3#|=0Dj^luAoIo~t+yqI0c+G;$zNey2x4Xzp^t8$) z-zer$42Q=f(WIejx0pr_x6N^S)44IO_Z3Qh{7;Uzy?!_OsX1Ct8#*kn1Fh{zpcL8ET-hJROpI!=3reM`;YZ_hvQ?bSo53yJeS<-)Akxyr~{{p^%xL>*A>cAS{~Q zXux1{{1l0L0nRzhgNHugu&l1?{$67FNc335%NH@sP13;jY+rr*(d@&G2+wioRztW* zV?*CY=L^;5Eu$LMiu*MW%RcE9_aB=GM77s5O{j%epr}n3WM7rJZlU~(7!3QUqz!n} zEPwFgWbx*he@I$TxcF|{)|Fkq z!RR)jZ15H7`8Z%tHtts)Wa_rO*~AMelk{^8{dpJTWL*s}sBiIk5r#Dtk2B~h9uHQ+ zh8UVTwU5*j=VE!ETzsi7%@%60pVlyrzAuT*I_=0mw4i?gpwfX2+A!aq(O~VU8O1K@ z{CBFz+EFUfHImO}NO0L6Tv;es)ghVR9{c{0`h@$lcJa-&5bfVX_`}*^_jiZNrdWs? zIoI5@I<(>6F4IKax@5$k6a||!-%$bljha^Pk2Hj%8)~NHj3-1@;_WGmlV^+Bt*Lxh zQLt$_gLfe!I9@Z*16ybK|>lfj2=)>T=Uzp0}LE2^o zvN~lvkLH8uH5b@&3ZwLevI$hz7H#|6I^D?xi{{;>TG!%Z5h1Pl_vz2G?0pXU)euWy zocMsE@pr}E8gLmA`l%9rEm;B0uIQ2RFhYZ$vpG!R-qFi*_yS#^{IRHY<*Vk0#iuSM zxhw&;?s0jenF8d2LNH~#dE}I~g#AcK1@@ZzBHNk*E&!ttXxR3DUiHK^MXLrTb#SM} z0Rp+dj|(!t&Tg%JX^Lx)>d;Mg7yno!nB2yWs}sxipVoU*kOPenfM6TXf{REkPEwGM z)L--NJ?&E$mzSci0Z;KNHdGDTk|2n`ve=sG8eV!Qp!U8-!-!q`{39BrP0L|~310gF ziT+7bgXo9;xU6?I>m_$k^(^wCcTml5y@~z; zaE%Y%5Dg}OXx(cIkMrJnBA-XYqTMs}7mw7{fs`H{@?MLJ{#961+)AC2$9^nwKg*IT z>Ry7S>ctbP#_v+P3$AY=?q{8Ti64rQ9ly`|BFP{lchXFKvOe~LCzL~UiEQpMsJ!p` zceV|2Qs9UKs z=GJrr3#({A{vj#sBH zSvHqD&DJUmSkMZl7X2wCRdl{#qhVINhPiv?`3@_AcQc#z-hKv0OyVZ*sP7BOSdK(q z6wzt^^G^4cV^vf|N#Jz0xSAWi&^E=FR#fY2lEH?}(HaeyZOeL)48o{9D~dT^5d7sW z%dK~+#R*>&dH8<;RGTuh%_NKZ(@`Fcw$?X!a(al2*?!*R z-Nh8Z-qoYmOsKg|d++L%7p2-Zc@8>ve*E~{E79M942X=lMH4Brm^B*0Sc`C77i z(io!pOLpom=OCXK=Slq6C)g6MF=MwxE}|+YpHc7xiRBxBnnpHtPHL(Yr+m%LL%eyB8Gbb|4%DGDV{M8@sKHMs9*)zLrMeLN;ur`Yqo zD%CNcoT_@J;q}P_pdsdE2<*3DJe0qGAA>Y3+wUG}e^S6Xjq|1ev{8IMfqT-5BSYpVq-h#aVd4yX0r2Yi)lGEL_v@mD$1Y2AZJ`^TrdJcp zL9F*p+Y&+?PHTzlekmghqu0hNB6tn}@@#6_aJY;a#KPsou1h6rJQ<5~68lfoTLsz) zjj4tlbXe*$X0c>u_6S<;hE{?;_7*wmu@R`t7blR2gZ^atFTvm|5OqwA+n%x55Ci=;+j#!T6y0j%&&Eyl#9bJNL zJrmZhMM1OG&)q*#xoS_V)g!>@%EGwZh3Saw%#h16^TYBU2f(RNCjY2l7rWPsthctV zT%nz{onyE(5u9UG)HJIgz1kBNBBjEhb3!{#0y9)6SnQDW@ROh2MFDZtv&Z4xLk%3Y zm6@LxOiiC=vP|7eR>e}ik&O^qjhpd7MUR~x-c&Af4CEF6nM|osKkPRk2yCthKr#qa z!~B!?Ps!hS@I>!{rXX9-BTw-G>$e>nXsyDD2qAm=lRU;#3BIodK9c8y%19uaR(L-f zG5xf0KU8_&Y39P77l!jp6}}+KwLD<7y*u_?3a@ig=sf%}1AO*MFgK(&idIVUD z9wn~eR(;BsEH}7Gc$8w$w#@bx`f4`$qgg0NkY2AMjM5*a-Pj+jQfRWEQ)lELw-dgb ztT8Ji(_N}PRFB>A#*Jx#`4A`?R?x?LBp3@i(^(VIG5Y!3vH`pI3|< z2kUS`sk`*j#U1gi7r3KTm<@Epe%s14B2xviXIc=nn{24dFWWw|nR<%9SfQ2o14M~J z*l1thi(fSOc~gcO3J9x{FZ5gCbd^tT#1=TQ5Wk;O;rp)sdY$XV_2l_VXSuMc7qRcPQtR8@SY8=*u=PHx1UB=B!X$u&(s_h{CyAYm-Z{ zR<9J9IxE7tj~m%bGxKx2AI29C{1Iki8dkA>8r-?FELL&AWWz+6}qo!RBUc6`8d$rgYy*(oGeI@6|j}iSmCdbdT zRw@&*IhrrBTe-wUpQd&nt)doh&C-wBuRbP(U>1D5%} zZNS@W9R(m0&lhDj7U!2%a_tMEKC3^xtaPGwJZek=dECE&t%f}Tj@_1uUx*@SAm**keSS{+a*3*1`YaoTO_ccZ(_`gXYygY|>8r z*+!emgcFNK12E^a^B*9^RW_W{ppS8#fNkd#9}Kp8^C_foR21d0c9(SW$y*39%gbyS zqs@_kG&4KVtmnP2fO>lC#k;tVO;Q*=hU$&8skY>V8(RX-M%rQ!U5( zC#fxeqiDlV%~w>LLmpu9M1|aH?fWDOixd8E>0fsonW{4v6>eeg6QK)Qe%^oHDfh-R zaPIH^XRh5Xa%vENo4FsMtABzoD!Y zurltklh(P6xZZ)wiYh+d9jxf>JLC6J_k~K~!r( zKj*+@Wc?1~M%g71%dT5w=XaM;aX~}jxF;pvw2AsP(hlkN@sgBtr|P75M#$QSz`Y)* zjS5Rg$L}Dsc*W1N{jIVbV=2MUOIGp(#`1bl?g8l7V5OWfe>bxaeIid>+RCHYA|Qd6 zyYI;`Zkzcdmyb^*1-xpT`XKOLp^(np1r{hzEa@+khGRy;$0v_w)JKUz3{oO*_&W1`*@U*&42PPv~2FoEH2kc&n zBUq5=vp~zZC+HTa{XQYupOEOg=}AkKcNRYx?$7;!=({Wq{v+~>BJ|JMB~w2 zG_}zI0I@y*Sb@FtID#FCew+^Mrs?$oFyWkqRU`5>fYwzusrUd0e}_x}Lb_Cf#X$qc z?E$FI5wDS8C87a8kuQ@QthH5iR11HLY+Hd5*Zo)9ze=-kY5s@f7l7j*0Bt&d5p`7f znQP{ZOt4y8Zjw^hwW3b zg7j#RB>so=fJ1_O1nB|!xJr-w!^TFUfe4Y0#0Oiyyf98+6FC4F6=9ye7Ssnov*nb< zF0+ptk><$(5SI@U*x1^*@$c2>9Y_jz+vfosgN+Y-czB&p(vQ}~g~XL{#qFJG9_jRt zX&$jH#Rq8lxxRZx6PENw^6&} zw-xoaE(X(U*73&0Rrqx4H#sF1Dh2V6h^qpSj_=Q&zNa_NNDdk<3#PM9tO6^7Pp>O76&@|-vO4{#5V>X(hTYPa0>)Svx81MQ4;S{*ftT1U*7-4L5AT48 z^$FkOE-sj{)IF*|n%RCFI+3V6MR@?6aWY~p%@a+YvRw$G`4~h|g@v76;1tIn+bzvG zCztr`9?Vx@y*ZrZ>18}*=*Jqhbk?^_;{Det>0UCvrx^f{P04x%g+wn5?b<$b?f*G? zWk7{wy#*z41Tw#tMHW|ScctyYS#;Sjkg$8|v!;9V11jelbQ#0v-4Y?Ol(4QfhS86I!p)@Nt?Br%CGX*( z_FL#1TVE$Jj0=weqt6If<8*=60bu_20N}AO`VR$)oMtMFD;CFsPfeoF`g`ft3~db7 zrn8l?iY5(HzGV;2_sgcG&TVKu0HVYv!0iV9{R7}oK;)4aFk#3!AZwdsLAnqISNL5= z^sr9Bzt10H08pso zHyO8>h=ND}*>fC;;|3`}5%f6$Q1Fw3+Tt20=78WJm5`_dfWqn{Yy;3Hp8T8W|2P;Z z`kvvyrq4nVf7coYp;@3v2SDeMcECI~`i8{4s)HN=SBc0Y#Nc@Fqw$dAJ4nK7fUp*~ zM@wSpKH|x5ENVo-EU-!s8Df=qL{WgM&=TTzyPS~Ug)rQ!U^1}RUF09C1LFQ!=LlNL zA**SLos0j1zs$G=B??-^fUzzx4)O6BNWqR#>sHzp=Y_#H%T{a4%d32Gyr1L@?u3a2 z%n3jQd>FT$w217SH|ID2(7YrAy#0dEvPd*y?M9s(xHL2rIc)R?HeSe?l*Ym>rQT%_{Iy$>pdl3SZ}^?u{jem6T$Vop?cBe=~&hqw1;doov=3rP28mAa9T4P zt7$~>ah<; zK2LxD`bKv{4;dKFI692sm+-F7Cw|hHY3n|67iHig_Elw*3aX}s0vq%R+g?SV@MoQZ zsvdpEFxd8v7hj?(|&i@X3#*EwLdwA*X@$HyDaD~GD|_B`sS`?LyQzA)0A&_KT-u>_01$6a}PzFK{D=sZh$ zuwCb&BoEBoARB);VTTSh`KO%xpI=iUm>18?ancM{m>RcqJGpkVCbh4GWm#sB81ZIf z#f5hkgo6~8Tc4=Jv!v@;$b9u0E|T1~X{(d+?fY>@^_}tsh9qWdv7`vvNeiK1J%YC1 z#LoDw{+2uTYj6nJy7C;1sK)L)ugHnQ`AvNs&W&lQ-NX>Jcy5@Eb|~&C!aj!Fq1h+p z@qaI#96$NZ$!P3+nHISqOh?0D${I4wHR~tu@sPy0wfyc&+S`&Z@w}4^>I;`y%05Bf zb^)4y&ClW4pn@9SZ`eQkwy{bx{&r7(lJsOaxUzx_(gh&Xu-F5D+D)GF^|KsWc!K-v z(mHmHBB2f)ibIvLwU}zg2um-q!toHYCZ#+qAeC4!!4O&v+cXv`}g?|fZpgZQRo=s77A1aJLlntazp|h z|6zCpK?TO<2!5oxzA)L5qfPI;>`&Pzv?L618uDwr9;z>Yj!qgVe&XE%mC*sf1Q$fa zuUcsIz2s324~H^K(I6g7Ta$eYl`O+QpUNJxdD0TdzE?iY=lbfZs2rq+b3v4l)m(Qo zjA9EJF;3b3!M%ZdSeViw3ASOMKktrhkSj!l!|R5mnvqe%oRu*Dk;(eWMP;84pB2=VGN-XO1V4fBGEIq zH_0iweG6tHzx(ezAAdXFt$p$>zA?7^r_tW^ag+KzjgS>{TT6bK7V-cnEENc;zq1`j zJd@-9wU=wi%nO z!_?GXGiI^39iX|QNcmsQ?%&TD&x|TDl(q5p<|l`=+5%NQe%Yt&|Lmt7?w;Z0_1fR5 zE*K=WD?`;OwLDL?3*_+c$twxZ|#4N!W2%*94uDhIeW5AQ8{TlO^C-uaP^REpbvT!$z>xKd_IM$nH(4X*r_9I2j@9J> zAjC$+u)l&$%k*ZX&f99uc+LB?42Ax8k=@td|7*j+ZZVKA=7D$YM%Q&R;quN1xuPYt z9L5|KAlMR5`SHoviQ%y!Yhf!vVJCO^QvCc=UYy%j4Pg=5m!BNEOM`NUu|X%P@p-APl5}gL5~2MR(!G+yPNSlth<#B zy8pZf))sb^w_FZ@B)8k(+t~)19zwr%|6`E=jfj6L!iyz{wHoqdvk{?4_~}K zB@K%LOmZ9U4K~^%&uA51A2EBGT9S$MV=sFKw){uRvQ*^soVBAy8-rMX(oPMGi*T&?X=j*#1zKOKO5%uU-&;DbbILIMI8dC>4DxWGU56|J)gMZn64ODRfZ)R5g}HTA zliF=af>z{wdM>X?>vd~RJxe~tz{Hy;J37#M;2g5yV~B~hv-`;I10at&%wL~*P80epZ@Ik@Y~I0<8e3kuiy zaWeGse|bY;q0JUDZrsGNj}*}f6nO|clZ$(mv3unS+^6{dP8^J|byYUy`-Ay$c6?iK z>6foC`Y##sbuVDvl2W%gqjJ9ad#P3RZOmQs#R=$s?9A}&O3d#$w(I-CA%A{oH{|}Q z<0Z8xrxKtP(49rY!owif>)OPZcKHtL;|olpKTH%qaW#Vh1}_)gJS~p&UkaU2l~BiB#U3#-)PX-OPPiJ827sP?4X6*J^#vj&G&Fi3uaQr%&~n;0Uz77! z`$-jQ2al7>RNHI=9EVd0+gRI#$<*Xdb^A@>8y;)yEp!44>rL_%AD8j2+8ND>tL z=Ag3iQ()$LUg(-7so$5gWfLuwP3?BtA7y}tzZywI`)*57iKa~mY(TO(o9jEbeDLS< z0@Gi``0&0T`N7>yS<~?#C8D8jb7Nv-b!vA-Sg`iruuEMk!Ck{sv#H&)MF->wZ}7jF7x9I%3n4XKUWy*ac%`NO!h#6}}K zRT;z09u3W5#FggcFUV@G-?sR!<>p;d-o}0UI~En1+fYN$%CYbdnjCszS16I?5Yo>) zI{TT*^K>XPLeCyj`45bcS=d91Q+Ds`KH#rJx;1=O{`J$eK={m=^~HomiV68c@j&?u%(!-khpuGv(#j!nbNQLBYiHKCv;jTtQpi_1BqS2(IMGGAqlTIgNZO+%M=X8rDf(dn^DqsrQZ$3|&h`CDtH(@8YS6qyaG z1UtOmUK3NNi?4NUuZ?s%`%K;e;2`FaG(YO`&1zv;O8?!E#GE9x>@T_J8pFY!?Vcw| zF#|pfWUe*cto~9r@s;7{p9NDO#v&%6U1xUp5}$#VB)6=r0A#X}!QVn3msoY>$?L0n z#$OFEKa+yQGA9P9MfB@>E12Du`0X!Qx^yfAoM^z{SHmS%Ip$vVEz+5(q*c{G9=jVl-BOrBl0;jf}BA-Mo zu0!9KaBc=AcN|g8=zDfxy z+}hmgfR=G_yf}1!gY)ovr0d?}n~O|q=;GoT7;1b?AJj}}V2ofw7XT zWg!x&5sX!88~{|yQ;j>MzQ$EBh=7;eq-G#nI{=8{=y=5Jp5~v{U=$jx*8Pua$NyBT z0;~ONl)vg70DZ_m{r!7IaJ(aO`VN2=Isf-0UoK27{(nH6+N5d_lmq2r?`wlqWJ77wa_>uA!`CEBwSTIrSsZGWZ zQ(!dU{3q)`P45h&f_3C!6F>Iv3OMBHJQAW|Rh7Q8XP4DnYtoEIMd#rCqTI@F=Zl8q zsD6Up@;Uc-Urc7}JQ!6R5Lu~^n|HhPE7@V@-q$tJd)fm-1^4UtiwK=&F`E6{d{2Mf zR^K^ya|zQE=r?P;!Naf#V~79h6lc4fvQw`&!+9088dudM!tmg{i;C zEuUD{{${21!t000V7eu6r9Nmfrm47*u9F6hZr>V_Zppu9xe`+Uq(EUE6YSkc8G7mg zsk_v_bo|oEWJ)Lptra-OXEwUsL*@;1ufC2lUSD}9@!st z^?U#1qZ%R}5ViCD^)*A3jXoH21Iy;G*qoadm{_VPTDZ_R;Xt9){44AWI4cv|>0iBz9{Qc<%0~tV7rS!02zE=VXcAO{p3=wV@u3d~Ktcmm9p2*pRCWKO&VW}RuD?a!J{`tP z8%l<$2+qaV-PF5gZ8>Os7tHxZw!lSth-UXLl{?MN0<>b>9=vuOEP;vjfk8tv!94kk zCW(=SEy!19oi1KEGy@LyhYjreFv&98u%!Yt{J)&|D$mHjozP^9H@16qhWJaJUn%(i&I!zqnf@M`%Af{ef+28x8aueKe`+|`088T7<$W$ z)R-@*S((vB1UnV^^O%?}Lch2wW}ex$(aog&We-_S-&qA!OVezkDe8>BBXzH+nE7{p z9OtWRTPtd=d5M{riB*<)bIINmoqp?7Ep?pj9;VEF1 zd1-uUdwJeM=H27pop1<1R`&Ug-VS?ztDwFzb4mCmP@FU0?yhFlt!z|`MaE>&Rfs?w z$g=S05Po2iiOd< zsSCqfm=H9&`FHZV}QO*9 ziL&FrJx%(j9B1a<;4<9+#0CrMj4^4BiwtSU% zUB7zY^;s`D_GBlE<}t>>V9OG}^$Y z&@)p<)i@2wjqw)R$jS!BL=GcU^%b9#^^3t0zO#jxVp{WO^F6F{Bv|m3zF=qpX-1{J z-~x$+$MEl`g1Il-Tg$({mdTAPDRexWelts9pE&R4gK zyl?v+1}45aWoY2sb_#>18`zjY-Y`lJRwIlDp)c-g$JBKSOGaa=sbg(9FfgCcn5_0X z|1!_pvAmo7^?;*)Olvwi&PQ#)C#b5r+A=(oQ7tzyEQq4h;9}D)`ql}uv2t$($SwT& zmz-Jow?Fx~PX`xD>tATghp8@T%lT0!*^INcla=kf{QTpIk)wE%D^gqOFEadmr9|(TcEE3Q-~D(1 zZ0;A5xBc1hJ?;*dCFo`oz6xGC%U3q2E#yZLUbibvxV}?(nU1Rdl^Ex9?AC~DK}g;>CZxbj)5(X^ zSM!XEO*b?h*!voXrv-?Cm5poOtBe7Z!>9JcZb#k7(y;`_j7_6RCW0 zDpbilCk&xE$9x!o!%tD=tELK?O_9t^=Zo`SE0wC&OH+D6ab-(mF*tbM%!hsWs%CxK z*WI5J)vja(#cukX&n8$r{YQl5U~D^8nD?V_Di9@S~50vrq_sZH*LaC+w^Hgre0 zs~DDSKzb+#o6`dAnKteFX=9hFQVjZd5~*E9lN)_p)p9o6ly`PfX{9a&?`x7rrq0LN zKP}FqV&p004uenOhGV_E&)Szcrr#o2)3yjQ+y7uMrfIdUSe)u6BdqJ;&R17eW;Il# zPIb_Jo3^1``vT;fHjbGrJ)1RC&u{fP zugpA%KuIV)z-Sjv<4wwSj=3vQzoBb#qjGOyT~^2Hyw2+(`^d-#+RYY%!AS5shv)qJ zfo;U%g^?XiEa;s){)AZU6PdP$bUMw`ey<{_)LW4nK~G2#{d{pA_Rj*)MI3xSMSa^_ z7~I}A@E=hruS&ibC*$(^CgOTuVX5#`ZB^QsH~rzZ9b29tGZzW;+u$$pc^Ii-v% z;w9&hxGsG#`rMO6IEEzDA|})A!1lQaA~}L+1_aC!g%1Eu@Oc~Jcl!d`hj0b3Muc0t zzyUx=-+&Qp!IDm& zCSs<+TV3!r5DK;!-w?99*M_Hf9O-_xK6viWNk;RHAh z+63Doo*Uk9BAAAOpMh2E?6%XeGSCz_?#4N--x3+ocow9$7&Eq zng|hcIF;TOCqnth{Y!rJO1`2WZ-PDN=mtL5OrMtS=nA6tfYFfHz7ZhWN%U?N<2R8Z zp8Bq8ck}om_|ma33W8Nz)|&Jp^2nEtc3QErpdwU})HvKojUa%YF2I_$4qmZ^Y#!e^ z&j~ITvSErPj(3 zg-X&yb@B#XbS+Ui+t@!#xu#E2I(}sI5*w5%-;FlFkimEoQ5g& zh3{6(oX+{w>z7wRj+Q9MX}#;a$X(W7a<8%?$~Ry45|{|GxHtIvcj{^>EoPiiLi^ZN z9Fyr!(}YToD#6SZ{(uSLB%an%m@Q?g;sf(z+E)dns~a z&6W3*TZ%d-;#JGXobzg_eP7F2&G0vnk?z$8=2llltc+;}J=L?f)2Z@e0;GV?Gd-#g_wjzcz4~dU<)}beo0Av#@E09 zq5B(k;<{NiKt&cN0AXr@GxQR9UI`~A*}pCA;eEW$oA2kl62RY|5+>yM_cuK4!^GR! z1aroe+XEBjey0IMK%c|%1LB=CIjB~)re1O>(=(200-X)gw*$&+Heds zeWei61s5b5Xp^5TUlj7G9xUadM?c%df<>Mh?2?M|lGl%YC{V>jSm^m!?m!3m$y;bm|GHl92itxIt$dMa`s@-c@XP zJM;j!vssE%#dDTTF1FS5RZzy?COckNbCNpoi(I16q6mBNrJi8-(0*^;$^nBbd;|Pz zqxEb%I2jD%1c{HpG__2fuF0%#R0+d4wIl|$2N2&Ya(=)er$Awnm`v(6sET!68f4d^ zc=S%NLxv+!%@s3Jf4vSp@gxi#d@gs)e4*#5B-^&`bV_m(`sY^~F%87aK+upgdI-ia zvT>ML3-!K>T=jcG&L5JExZJQ8syF5|R+AFNeC37loy}uhkCXye^Y6TOQK^wp1p{x1 zP;4Bqu^`Bp+9P|}#`2L8_=)CGGNLq?gG!X@33hWHcMekuW_Ym5ZSg|eWqgdCrZJpx zoj-xZy4Y~4*2<48z3DlfSu^Fi)$VO=bJ`sAy9J?m|4Yl$RW;EvIgecYr*%HJ^hAQ+ z#6oA+O0gds8GO2r-6&HzSKF_#voz!OWTAkd-Ty;Ds9zq5qvl)p5jpu*b)qth$78h& zHQ7X5{D{8jy0KU!?B?>aDpSk=n@aKSk-I;9gKWzvqT$?Js7(2ILD!5qcXaw!J^9vc z*cc}k-|qAV9pSus$$PJG<;`sJDeSpq^>4+3P@ZE(^+Y&fBayp5_|6Yw_w)!f40joE zG5klyuP}v`xWut3wh#U*!mkqTt<661I-!9 zl$u*)o`~9?a5q(iGKQxg3`R!7nn#F%ja&M2+quLHN+e%Zruav2LBeR|jhY3C92wWM~trhc?!2sGJx&Apb9AVaGJ6c9zS z$dTR4-wuE(wLB6NCJYQ6lNLdZ2qOwW`W9zIK#9Mm59SRYMy`XHh$1L3F|B)lm_&{s z7)20oSoh;?I2c{R&vaqr6;F@9cK@TLgyoG$9K+FpmvEChe;@d8>k}}b_o5) zg{1(t7LM{l{(JvV<@6Rhy3Q2LRZIi}w-)Q59bkLtvJS^1r#*%m7kAzEsd`RG@3kyi z*c}=Y$rh8VyBik2oTl(Y4aqqEb`j{DIkZorc<)1MZDkm5xXZIJ83l|9ur(sorG`P0 zW$lF4!nWo>v*sp@Bl!3Mpv0C0e#n`Oi~<5x7-zSTtaYb?JRhAe#lO#h2$#o+`7c&} zn48wf)wt*5|5I70sN-(n>7Y2k=g3nr62`(F$U-Ar{2cw1N}qW#=G%!OoUw1W;rsTG z{OX#BuVb3=&+?*~hrK@W95>+1Psbr2AUON2YwhlP`T;NRKiNwC_DeGhn9dT9Km4T!*{n=_@$^{E1!e#7~0T@XSci9lr+08Sq&sP(TxjY5#HVt+ik(C zQ|QD@f#FPb@9!s)Y_&gEF7Ne;KhoU%D%ww8*M8eN(=hEes|NGTv#%O(@GVMdhn3Tm z6qZ*uu*L7m4)dwBg!M~LhcK04lNQaDf(p$Yr9?b}d?Z`Jo!%MG)|7$i0g9KuQv;zU z8`>v>ned1>L!oCmq%Zx`Y*B3;(}^?&vKf(06COih(v7q)iJ9m6^);#7(tk--o&2E@ z(&DC2vufcxOgn*!N{5DjcP%kk@evWRGSeEcrv9OZi$ozy{22azhjst!e-1&0_iMGF zS}7S9qo`-bE1xrC(4`?x$>=&CGoG76OPSsJ@1j?JHXY}?fx3ZG zUJ+^ww%q%_D0}mGD8K)2d?ZwsDP>>A8?CmIJ&YEFq*B>Ur9xv#!eAH@B1=+)7}+Ar zWZzBpB>OVijj^vY)-lGse@E}-`}y9#$9;eA`;Q*ux;p1v=Q`JQy_VVIM;@K*W)z(#Or0prdHsCdElF{Z8F z^V%+l3^_t1Z=HJh{N5e^*^46Dnl(~nYyni-;r>jYI z7#JF!b6{zsLCvs>UtYLKa@Pw-+U{h_yLr=M__BvnEN_plf;{KH0We5B?+vnP+OF4wikm|ThRRz0|y24BREtI{I8 zZ?HuHW$9Zv?j3mbxUxA?MSkE%Lzc)UY+RGnX{)%t65#uXou*PT=Vz-k)@P^{ES{UB#U2w_@sV6~zFza&E(I)X>;({)dHQ|ecd=FgX{S04G;XPF-#3IA!LwCXb;{`3dt zh!*duzf-REex;`2gR-;45bTk-J}&$q)UHi2VaEoF@n~a zCX3$P4N6F!r`3HQd>rS2%6}tV%dT?E%a(c2@BD1;3iXou9gtAGoCRDL9D@f7sw^`i zYy@MlZjT@CPfij~@LLGmNIs*DP_*v{mj3$zLmTmPXPh(N_+tVsl>abGW;Nhc*l=+^aiHRh3HJO;>*_L2|UXQY2V)6>w~$^fM{%uO|X@U^obMkfFR&e(NOYSK{;U;p<6f@@93 zXz#Uz(NBm9f|mAsEvelgQQL}$r$Og1dxwO2G|l&&>l_V?xK}{r3d(s*7_7kKYrMHv z6%-7gg@b^f1BQg)bk$X0+5&|yH6Z6GnGp+Y79jCK`A#g#8T3LhSam@%7eVvgLmKpl zpXL;pP-#789R{vQmW5MNK_wC=@JVXZLB7$$z42@Z!vL5Q&EN)ENtn)ha2as=nx+D| z446DGBd-E4aTZVgLiLVUO@@|Xi08@pT@Ei@2AejRoiUy$PX^9+TqKB-ego>bsYZA@ zGGWwD6L_yL>+4T49*anUN+=0nJt3$DXyv_%qkra%Rt{QEt6kp(RgMYXA7sb%mVlXI z`RHMIfE4tBilPjfv~%H!7h$uHk@RZSo>fl%u5;zF#IHU^>Ly*ikf0htiZg(M0q_fa zzEANx?+EqQK!-*(n*+5!hhJAp=6E7+ms<%vfQr0bXizBvy-{9@UW$=&f21-#9tvq} z*-&^$i=U13Lz}~=&HLbF9`IE1ogHM0hFYXoa??x8!-x~_^$WHp1tEC{JB$$)bk3@z z6{1fp_7BVn`v;f2PdTxmBS&lUslW`DtQcd|?74fH%srVYsfkugleZo7eq6#{4uw$U zLNLN6*WyXq`5@prRaZ7@fh-S&qIhmLscyEuavTeD!WQ@!)36I-o4xj)^r83n6%#3eU7To>i- zm=g*MDtetr#RVl`tV=xNA?)Xm&a(KhkiqkobdTi9nrPzwftNSc89S47)++dx>NxDW zSLixsKU%Gm&iOF=(V;eUYYB4Tm}lf7yh0;*!O9IH&;Ow0g^E~+c#z_usHRVMv}OT6 zk>;MC1WMv-5>p0+a<9`yyP2A{(+(YMdV0QhT$BX`GU)JjK1x5;MFk6#d-mxL$}(Lo z)3~&8PY2i=n{hKigKS9?w?p}R3-@dZ<}Ar|o@vq3pfo%D(A|zj27=SWP2`-ZB}kx9v1q(&^S`ptr~SZ-#z{V;w*?jZUAMYik~>8{aYg1jzm zWg|!aVx1dX82wkfwhxquV@7$@J=LkOPB?V!b9}AGgusdw55%R0n~ES}*tVYy zm>pcP+0WkkQT-#$#UC&TJo-liTPBrK6ui$b9G3} zZk>}`@48CTrj{T0_+4D1A~%CU{mw+bTL&MgT6YQV+pzGqEE&o1Q1aUCil&+Th~H`i z#bo~a2H6^Ez#7gj5WTwa@uA3A?$gDeKg~iOpOj3}ONN%J1NtB?RC$IWpK-*5ozBBo zFY;1V+7C>6N2rPxcM_U0Y_}+k({>r1s!|dPl@|Pw_c@cG{BTBI@#gXWqv!ZfEcQ;X z!jOo0+ma&tRj1rxbk|S+;Dxc#Dw{@tAhm^$CAblh`1~Kf_idjV=oqO>xml06T9W{# zsxcwQy1?KmKRZuz==9PHOMO0hc05A+{&U5x>Z$OIREK^-N#`(t+r3oZsA0bmB2mqo zzTF!#Th@Xf?YG#lg+C0gjHrp~Z*44qI#Q`Rh9ZIfhsTT^?d6;7BQ?C+sc74k)1>k( zoEJHGi7E9-f@767bn{Wm_;tS`&W&Y!<`zKOPX;IgAw04zSc8=oP|?%+>D;qidb#&5 z55$LpbaVE0h*gSiROjYM%U5}He^Hk;9T(}#iyAT}m%f(17o;bdUzI49^y_0btHVPs ze}AzVI&$$ZZkRd>JYe03Z*IEBw9xG7frv-^Z>6{HF0IckA(W^)<4yU*%QBluLN~gD zOuxTgYk&_5?CKW#Ai%n?w7L1RQl_)VtDmsH_6|vEMeS&3tQBalG;4XPXcjMz9rk6L z^A$5I7J$dh>qo>fKZZB;Egoy#D# zUnhW3OTo-KBW@8|5&vDy7q9Cs0=!bJ<1mI`hgs*vB`(Zj2C^9!D*$VodoXZYo$$GEaaLK!vX}X^D=2+;BFjOu91JH`{9~))!XrmdMz7DOE;GOnbP~XGFbS!wS^J zvyML@Kw(d07pM5C;J`_i-zV2Fd(~He9XWOE9aWm2Vh#l;oxwPQ#tYy&pur}xFhpb2 zQAS|4Q5(>BmHMZc!$1q8aU&XU&YuvFA|(hX8dLWg!0Fy}WFff381*(|Ke)sba9w}^ z3W9n3eZ1yO$!iaVkU*FJ8Z(-~BL{riS#wY|UV{aqG_N}p0^;JtTPP?auoAHYz+CDV zFh~+J7}SZt*!hpC>W#x`yi53%QZUs_FfrY6IdF{mef-b;`U3F%lhe@ee&B}xIb;Ae z-e`ix2w^8bfu|qKBhf=t%w0Q%sy_#fh7MdA$yBiZ3QV&o zuo#JGAIdwMiIU>xyI>tB6>(p)C!yh&VLZi=m{WTvlA&;1jwbNnTVw-(j%B#5`uHHo zU^yFc!Gp`r)2Enw_bkt*e)W^YUEkZWce`?M$1f?hD2RP+cGv*Pp}i#~rG=#i-lpO& zj`3fe(E5`4i;KnJuUH|^6qdi{gcVABkYLokA2S$h_i#ND{gR*%oKghXSLm zd#5U%VHJ4mA7noi4pxuy)4fO-tiG&EHVS%vj6GvE=INUWttO41{LjTouI|MEVV9|% z6CFn`70y*0|M~U&;+o}t%dM$b6V)g6cN5&qqTRFe1_!_1U$toHdE}dx2OUSw&Yxm_ zPxh!EgHlC~s~J`%gy;;&o^b4zpSj@rjahT-yUCq@H1q%RYZ8M5bdo1i=T}YBOiC$@ z?>(1z^p{0~Cge40aaa3fCIQKT<0ml#1*jmm%ZuCo2=;j|$LYvSLmLcptt-1D;2b%{ zypSkCg8CQx^IJJd>6t!EA3wXaDLp2)V3nA=l)HFZG54o&(?YqmG6%m`m`SZbHgD-x zjC7m5herARptIj^M6G}%qQTH@Ha%(=Dg3m4IASRzjHN0}FT!gk0GpLJqraQBe1~Lc zqZN8Z(mAI4K^lka%X`mU8J&zGrga&f=?dLu#QrB?+{nsAH66z>$Qoq1@T^EV#l`@v}#^s4mD_ba2jb-F*^FIPzC1d-$?wL%dmd){2{#PW#Fp-+>1&UAqt& zJAG}aWV4<1{CXyyn68MS@#SDv);ig``c?@`M?fWuAarNXC;HR^{10+>d?^NMh8df| zDN|O$Pg17wqPZ@%BB^QYY_UQy`}Z}g1DZJC?#Upo__iTdIVfy@>%r~OnvmW2V7h+Q z#XlidDL|N-`>EOXI-`LzXjl$AHzx&Mu)agdn?MG4adcOONhHKNV5{EG$&Ou^*=ee( zB(Xm#a^5-t`o-)O)kW=j!RCD+U_-&I9|CUi?-LgN@P#FCya#3`uYe10=Gf| zO9YWe_SSERbzY#k|8Da{svS>6?X~H~j7wvQ`kKmAAhLhn2dg7!#5m!n!5zBRMXCG- zUl(wu?>U7b0Lk?LHOC-}mR$~{I%EYb3V`ea zn&_QLB5?o6pjMj|0dC?EP@eXn6{yt?;Kqzg{=S$D4~Q36?Uhbn41trm!F<4)fW?8Y z^Tm=2_iiB>s+*Eur8iWPuW;bOfBJy;GdU$%Pe_BLi$Kuc(B zC>~4y69OIp@EHQp8eD3=+!**Mx1ALF$-gp@r$ z=Eug63B?d5$GU57;S9;}CNRQ&l!M*opkeWkgYgJ?GgDZC@I4XHukx^kNwHZcU>dh8S&p?>% zgZIlGzk%U^0cRy9w5byf!fHvp(K@~3mF!#Zg*fnc)I$gU
za>4)0zw?Vb4$g{7 zzHaST$VTRuDa}|aX?@!cf?i&*P`JVN9tnr2i)8xsv%6)eNMcVIiG;_HWM!)N#9P>?=3O zmdX}TudKAsooo?$bSXFj%38CTDeJK0EU~(r`zi5!VnOnKS|?kr0(I)`NvLpGgqr-v zBZvp{V+YaDoPCcw9mk`wqvp{ETUJ)z9Ha$U>J;y&%&sj*YxgbZ3QumE96D}v??5fY zm7uzr3>E5ToS=5-9LfFsN=`{f@8J1^CQ0R7T$s;KHsu20{2OxIRR8BPK4SKtZ#m|P zTfHoJq5pOEnVOA=RQpDxzpp~~NEq+e`6DZX;f^PzFL39EnK+Yeh}K!@$bbg(jlw~V zlBtr?H=K>3%&|^$1&u?0LP_i4;f`ffwdP?z2!duD*9UdD82)=2E+8By#^1IMm zUG#Q*D7{xkrRCf~CMPvY|7YBb2bJZeWjj}txto<-rzPGDw7Vvi=Z)!58%d75$AUDh z@=|VTS~PHfqNUVMJk81hHVz@<$OuuWt=`B>l{}&Q^okMGo0P zaDvoQa?n;|{0BnO$>607vFM@iL+Eam{pZGoT_V&>3c9pJ1<3W|iJJj9?L1Ue?$4M8 zjp8CBJal(jLuHZGbEeok@prbj@$Ek8A}e{)&0Z(&(e4rU7Kx7#c$Nla*^Y^@wHa^q zl0YCF85;Z+I0o#?&x!>3FqquTGYPp`B{|WrND0I437bRnN!IevaLGQevoi>S5BI zQ2tRpVd70z1DF<@`SNpkK#gL5*CWHzxqsY$v*jQGP`Bswv2@7O=O*Q5jJEi&sB={m z&LVl%ac!sWeHzd7`=WLf48|Hy1JR|smp=mCpHjWW5CCF&)*e9zcKzbQ4~7`(BKpXL zdsoc*=&?Xa`)d}e_dnX49Q0E)r=8pcpYI!^SMY@*1dZLKy$?8N1mK+Q5gifi@aw$E zoSxA3KOrlEt6=ysZ*Y-kh~0z89>fYBXqe`StsyK zkjlSL?A3!L;>?G~P~NwS3Zq*qlKQVsph+CJ{X3kHI-wBiDa!=f?9AE_!u=T5H!zZ>)=W7ryl}GTIsM+5}lCGhx`RT3oen2OFId$=E*T6A#@c!gk4kfgM#1T;(R(u}9dHDlULv->r7o6$f$HRT#&59IYF-&z1D9Qi){T;*uVQv4 z38Q0=|G5**e+FkFn?x!99!MeS8vd^i2=Cn_*ft5ayL$RR+8IUvHB9&R-Wu)Q8gd0u zxOc}k;BRf1?cZaA^*#ALmf;ECUA0>UQ%!Q*HW?lJ_kT6p{=O}ywUtE*ap}IIRGVAa z8+#+@yDz$YoJpwzPd&GApXPY(96OVFLk)7;4tRbVB>1*S@z~A*BVg*p@4O_`k1Fgb zFDpP*`0`H(O(2;=c4IGfbz2-5MFimW&;HncdVCDI!Ow{-zBT0z#&Ze1XS8$JFYIP-2-Y4S*1zM zg7hkI*-z1-ZS%rhTDX7sdt(Z(S|4nP4|dV=Q(m*sQT+hf`p+ zk^EG5=BNF7K2F)?lLbCIzF)N9Zxop72CYYaS*Euo_VyDFJS(XGLkICmICK-FZlTff z!x`9#-L;0RxAEbulfuxC91lId&n#H;&URHwWSJNT-Q^eSv149AvhNt)suNledbKQL zdxO#{@Z)PX4`rp^IV3*VUy=DpTT}Ff$KH=Mpj+?8!KdM#W(Za#5BDvm>-_a%XkVQ@I!0S${4|z0a>B>Rxa_Lq z#4X>)kn-9;Oe7ygs2pQT)ke9kEFI6#m27=P+{t`#qvDBn#h)(P`T z&?*R7Yk6xM9hp2tpJkr?AvI-yi)1Jz9ppakATv;*Cz5BTnGgc|pg50U)s&pKK5x*b zY`VL#@#SD(_m@n!ll*{4L=?Q~Io;fl78TL;;kb13K+qjM>mcNzWgakVSSg&M(b0Knucd~9E*p)uF}8_w>qXRD0xI{Z*>b?gx{Rk zXyzdgoTz{@kIeI0D|cmduWc533X0D#ZJlM&tNkW6{t<*hHAnxW`tkor!JD(>arrCd zM7T@(lc#ZxCn0J18oMFNN^=2XKDaLCC^X;v4dKyKbcmmz6hCO+1j@JR!9r?4o{QZLvAJdPAb}rY7LJPKnM8n62&c>9qXbdq;k6cTxggIY~`gmy3?(aN@Z8&L${+>OOwiyhJUtEsV zBw$m-tlpM|FATzVS^d=OYJztY;FCutptL1E#*4nSMjzN8_@f!;aSdEI0b5*rlAmH2&0e;mBs;)4e&j_jYt{|vCCTXbP1v7+cpm; z3ib{41CK=zWIp`Aq+k3koreCEPPj6|zeQNb-y#fBhx_acG(ijW_2?9^YagK-5jW6E z5Htsn;CVpEr`qj%LdJ!{R@H}rKs$wBXR=$wB!i621(5n6kdvd|G_-kw&9Wi)Cp9{m5r3>7ut#`t33;^Ffl6 zIdJoshi|Yo-dV)%Lwh~k8mbCVPh31|l}4pT zK_75&UKWnz$dY!r(-m-9=ra9=${TbXeeW^DJ0x(`>}3_{jMfY}nd`?2X)8ij9S_pj9IMd@_dI+& zj?x}{U9OR(E|ag}CZt}PdA*7{YS;cWjU)0~-vvz2s6zQ8Q~J5A7p-NUZ;p`|ETOYv z)T5+wV#MY9j-q6)0omI;9hu2o+*{^-ZmQ^^-RrB%hLy^e*Ske(;!dU`QG5-hqkI&1 ztdG5VmOvu+go3((z&(Mhv#53rsNyWmc5x{0+CxT9|DudmQ%7Ob+RQ@Yhh1bT+4h^G z6sW{i%lnkok~M5`wO}aub<36e-zA-nzfzHSqq3qUT5kP_(os)Ksvf^B_|WM5pk>HZ zsAf)r>Cp&kJLb^(Ag~!u+zPuBv{z|$H!#CL2l1*cDb~|v+Na|T>$5Q}HpMMzUS9X6 z%g*-ub#z7 zVvMEMGYfg>=&cITTMc>qy8Np17ozEbUe$oHM}mY4j@ONJu!*2oDc@i{Y}wp$ZBuMR zpmq_pK+piy%%n|xAvRE4psZKXeuy7lbwyx^A3>EGs2^cU^azdS6zE$p9Y?Ox|10Lk_(CsbKAvVFg)Cn3!wA?g>MG-Aj6z|dJQ zR42mlR*NK1m1SBN*PEVPEw)l~edWCLP~&X$X}}5ueXAQ|@>a(-s^#%xRcWJJM!E8S znv9ex*G*4?n~YICAI;6dRQiJuQt$$WWo-1zy{gR|cff8Al* z|9rl0>Gr=#MJ6p{0NJ0+vceFG;51I32VO3qaKI-&erUq7lnWZ`=6JNp`!H({Eflgk zxJ+lFsBL}YjqTrz_o?ozbbD-X6*Rn0rMbTh#8$eR8(SIp0rzUxt-V*@kOVR zJx^H)XKAbC@<>EE&E&i`M8@l+pQe=2IH&mZk0JfgrM%!dp#$PSc_geJsP`!X9 zybnf~st5Jp&tOFv;QXbf&y4+es&Vkr=}ZuR1+d|`0VWV-1?GV#cUJ`Ovd7@5hsTJ( z!QnieiRr9Ff{Al7c!%ODtjyzs5Qi0@29_eV`ldNG>mnXhiNZYZqDdDhri&w$A{!uc5WA1pCF(EyGGt>T!@5H zW#u@VQ|r}T@|{(u#XgoS`qj3N62Q{x9a*nac%WX)+Q@+q#H(wV;Aol+nIe(v6eJGXi z?Bk!1GushfBv#UOkqiriOrg$!@aPNYZ1H1FgS$niluJm z!#CHZ=4pVRuAd4e6-)3cg=3dTKEo2dXX>H*KKrB3Z`yP1UYdJZYWQ)S#H9NbmZdnf zXX7^RXi=XqI-1}^(@Ng#vt9fBT!g*^4*Im8r|t;S+*6ZA*)!>tuWVw}14^_x{h(c@hWC8Fr{E{ zK%)3(*|vMZd-e@U#`_JQTRqRim394-dlYsoPJLK@SQ2P+%jfEiGoVnFf^U4idwpj@ z=d0rR6ZaB7`3tF-`fn@k9O>wd-w`kC5I~WdMBYNTRQlC!-;U`I%jqB^Q1tqSlPYs28(Y$cHg72Xd}LsLQ+AGRX$SCh zg>oaA(fnO>zMOE&(GH9!zxck{BM-jUstB~yN~o>?AXF9*VL_u9sy{j?h}*_tI&V%j ze|{HCBYZSTL%W3=PvoV1``B_!+I3;-6yyE$I@1PhaRp=IkUn*)BO9uw{Zga~?SuX5;%2ZaZ_OLe9Ii52t&NU{hZ z$(V-d$k`uYAUO4Lt`Q^Xf)Sm~86=kpWE%r#{J*sTwLiJWj36oaKP_&97NNkd{yhdT zi)Q{eEf{oVDxs?g1XtnYHZR;RhZ16oh;NE!2;lC5A%a>N^lBdhowYz< z`C3PU?LurA0h_R`xrJHq?gH?7??ezgY-bgK*NO|U^|Ph`?uo^v0+{#X-fd%O{L+|} z)~^3PP)%ftpXQ!CLjnnZe^0{8f#NOeToCf+jNRS#qG@=!HQy}~?VU4BnPoX3R!u^V43J1S8GLK*HLm4GG>rtzK8X8w7X<1Xg! zCCUck{ZlAdqP;cj{c8=)|6N0{M7ettlG~dQLKj>@6)en?M7lQ6jebiEg%!|17yoJr zLl(d{)w~7~JZ7Wa1SPAc!HBBkrTh6z{}^oWdKhgzlMc3lREl%{Yns58j)LUfbT)K9_f{02-M9pw19;KzLQ7tF=T^$*hW)(8Kxd zx4Iw0-oSbZeSNFAMQUZu3EQ0#d2AnY{_lDtAii5D(2%0YLvv%!{m#6DV;63}mo}wA zu= z*YY$t+@r3&2#kT>VVxRX@aW5dRWLUWrLh}=MR~RtCBoFLf%p*1G+2365JcJ*1T66j z3_ff<&_5go|J7=hiUEVg!O2|Cj1x85V5~|o7KyyW2)KnA7apSTZdXQLNi6QJ6ZGAHbQ1;lVi2uYo6y_iBgJ-QVOGB`&1uhJ)k|XEf7O-Ie2FM_?il4U2_u$y^ch zoBcoJyXBTIH+4KJbt#@L>vuYEu!PzDSHb44Ui^)1>a; zFNZe4j#jA|jaD^{Zju6kO1;4b5(LdsF&(XY0^@gSrF0j5X4+_nyk`+|-m^edBwf0L9n%582r5@EmRyLI zhEFY%fIJ=a)4+qv!Wh1i|8%qfQ*Xd-0SQ@GrB1(2#?$G;*z|lLHSSXriJ0vbcg)fd zNSD|8~}5N z%J%Y%m`Cuxdw>smr2$JfG~x?+Zto{?)Bm8#kdy;F`W^vvtm`Bbws07-Fotp)0}e)g zH$eMRaNl6tg6<=&!d*F0Y87X{KT>7IT8j|z0``QrteqQfFUAwcY@;WoeLBfwL55yU z{&^S&i?Wz=JU=B59u^Kr3ae>m!(G>{hB*8WWD-o1@FFAnD<%8}150BkwDxWAEgc0g z4GV4;^O^BtV|5AQRl2wZqWLoTg*p)RkeZL*w%7!A-rIL$8$`J^eOku9;!R#%n-~sj zG)Lw4JH$*whTid>xxI7vB2Sa@Hw;sZq^Z>@jldqtcKrI6Q_Mf{#7k5@hL0Sad>ZgD zUflcJ8ACpu-IJLNJqI2u+c;u-qZ$a748TSX^T*8egIVm0uT~3%tih-^e1TZQT8z;T zJ^5cdgAhRA!GAjghvfIs=I^0!*MR_TW*j^D?|6x@Z_fLg&%l2z4gP)eNU8S1=gj1om=_O>dEeir?z037Uak#NJ%$KK&f&&b8YHi7b2 z`QXfrKp2BtS#o7bnVtv-QF`4gvex%XEO8)4sIvMV!sP=RC56F`*zqkVylT}Cb@@d~@+ky-Mk8%<3QWr;z$MG5Wwa58RMxgQq61WMs*DlE*-Cp&wC*3!QmY+_xF`GRxgopa`4lfE6U^I%6Li5g_?ZXvoqVTOjqx&uouhpWTJ@b-tSnp8m zp`PDFmsOVr?KJW^+vPE{pJH1%H@oONw<)mDdxN!t(e348Q>j(KUKysJM#2(4FU)m3 z0+KXr6iUc2)g0|NE_A$IVVDrMF5y7P$Kuio8g2*vYKxXMI4x9Zx9m8jckeH*NaM}`E$%LDT?O(`lePUVMOee) zG2h32k|`-c06QJy$Px#B$C2&O7{4>~CYbv@d9o!jtErCf(z zVc{sxr(u_`&cxiDy#Q-Xo|(f4TI+qLUbs^oY#Zn3)FXze&=3CxbCQ}ch)bHDHk)dW zlZtilijLMg;;MaouaeAbrXC;QUZX~??ZcYIxAhZ0oy%crX1eYgwL1@4)z_yTW3=&Z zlNxVD=Dq^jVgcHS-RD>(MNHUzY;;uMZofvIQqe$lNL`*sQW_|icmj%F*~zXqC`}WXP9qK)<;rYC^{4Md}E5l=5S%=0aMWIhG0s`x2 zMJBG~HR2#$>+R~O$YUX??0c}5x7>}Ll(NIRmzyWf5E?Ln%A-u7?MqIYL-*BQde%9v zPB)*?{WKY{sN2ijaVoF*8w~0uCpw?YQo1btNuaFq{`+g|5D%L<3-}RVexS3U>$8%v zR>;bsm8mm5)5_t|py+hr90Cq`I*mY>|-?6DPzQZkpGvd&b+_ zcovfKsl1}J&_AO*PCUd@b_Q~rH@*am!4`~ORyxRLV~btY2H9evP-X>)$sZGAA-?=% zq=Ml4&4rPK{{H>4Gpj@lw^TdhEZREOARfbka(`^lw=KJJc)Xrb2V3z<8Y!QvMM1|I z#UwXIU$3n~X8^>#i*~j2P+|bI5WY-2TGxn2XNDoe+urFKs`bzW*yKATq%>MZG#rzcx zA3zyi=}fU`zWdWm-N5S^MBFe>Wu1cyj`M=-D1~n843SpCi4WIo3lcByt6lPw)=+6S zpOt~zYgoaQlVJIA@~9x&FovziD#*3U#2o=nls)GO+i>S~smiA=)e~_KnIll&PGVQ3 zGV`gVaf9Xqw0HeE7Ir(@h5j4g_cMW=Wpz@@Ta94UOFNhWx?Xz8Pp6S**@SuR=SSL( zedABp-T#p19%>_*?SC|#VEn#3*3_DL5#*I$w3jXi9+ld%PoGjhY$PC{VRX@2_YwJW zbFAwzM-j){+#J!vO?(o&{U<3+QVSJ;+;T{6fYjLYe&*Ym2zLt?UU35ys+ z;hiBCoEY8Di0?wr}~ABGohy%4`8 z;wBIxHnr>$p)1U^PDl36f}jsHz#3Bez_`Av1T&M~fTREFUn+fpma*S|ww^d5Ey;Xj zx{p5+9rf0yL$O*LViH={&atiZMmJ{EU5C|z5J89j$oc!N`A4B0yW?HCaN$7JP-~e? zDy&(-pKaQ=*ua1y8=O0A$Qqk%HfjK+HN1HA3?J*HJEJ971p1@jQwA>4Z}%R6@J4Vy zRbwn6+mPLi3n0K~Tmu4%X92KUj03UGEck!ddv251-~LeK-~N#KM#XQeQmO%N7kSLk zHtiSHX`oYP^Kp4Sedus_cH||`jvbi!CT&}nG`y-uQ9=zAL|I(QJgf3!5+wLm@4!)- zGP<|RDT`I7~j0B;E6j#(9R{(Dfo%o=L=y8vZy&Z{Uc)Xjb{Ed}AvE zn|Flp`K>GNsHIgD{}S&%F?E|OZ?sS;gqiwy z*{_@iz1FYWlzVID1{IrCe<4~VKJ9Q8+|50R;eedw=aL1z=vnqxPX4Dpf zw}-Xn4~@SF-f-D)fY)rFDBq}#-t(Zhuh-=40_c1)9!jIfZp%PCn;Ta6Q9b(9s2p>9 z^Ni1&@0?+=!e*z+Lk>3cFT@iS5rQaOgHmq#f?(q?nR(h%-oG`I^i^?#3kNp(tKiLb zNuM{*4_Y00(Fz+#cTDs13Iwc@D5dufn&M~_GC_}{UuSE{QQ`&MMS$(oq%DLA1VVR- zC_w#^u^ z_j+H0VWxk8eGjD;-szd{c0=Z>O`jLuEco!y5|9;tgmvWl2!Nt>Ts;v9iuRw|OO8B% ztvJniA~w%QHp5@Bc@x{1^{5`>B&2YHqvMg8@^Y7;H~um1bhb$rbOP9;eHhi#XkuP9 z6r-eRRi(=?t2;P|x7O2~B=q5r(9gS0bTE(2xt~|?aNxbF<&!OOy1i*rLv~t3n*cs^wfS`@IB_}+t0)3>@o2!hN6E~w}^|m)%FB&w?QfbGkTJrMrd(2 z1}<|cR%JEaq@#1%PjfRRj@>y>C_+p9`RC5PO`q7Je7j()Moahw21u`2m>L9;uMhxK z_yAn>{sEB5!iI~rW9|d~6>j^q5g?f22Es!R0XDG!iE;$`yUzgk0abO9xG$$5c@h)K z_1h5SvzgNGP___VL4fWVJ`U19` zDnnX$kh;Us`o?HEPr|{5!Gb`54xA$>ctgy^c#>A1xVFmh<})U2-Z?Yli;6{U99`8O_Zo39vm)*r1b z4yCpTWA77i_uk}7O*a&riM+;{w(DxSF|#<3BiUev+&q7UjZ?u$=;)k(nQsx(bPV<| z+UC&-s=DEq8L2y(oRjyBi~kGbb6EGPiBv%Z^_>##q!nAAZpZ zlamvCjNB9GBZm~a(xg14lLc_ubk?sFwfkwGj|!=kT9M-uB6_{F=#IAicc&pP=af_BS)5{KbHWk%G(V6^TGt z?lZO0#>oA414kD&d#VW_Oc>%t)^i#m}^; zfCa+!`9#fyUWOEE?9S*33QMnDV~qj&3Phvk22`W~m3Mu5b$au0{QQL3)B^+cJhKpu z>xK}Yz)ZRYSvCQy+;F%1>v0zcL5HYizcBChybH#=sb~#W*2HwgNWSH?VLrr@ly$Ng z-SM$YOSC1^x6Ai<*B;7ClZ04mUPQW^DR1wa<-sq4}nZvehmej@Y-q4*0PN z?@JaPEzh&A*XdIC%A*kB4bW_T~@C_XY1-hVM^$8+0{ zq}p5pGE`>wLfmqsX8QXJD>M=mlzgn|uj3;C@N#3CYzEm6oX)+CyW&6@zbMj~@7I%tY4%p<(_ZLYfl1$MoId9*u?=!8 zwIs};Vf)Vytg^TVe-3O*iky#V;D!MNo2yk*o8GER}$H6%6NQpR+j?3q>(+jup^YK#PAmSCHZ2S?X=`A4*94BE3QD& zl7&u)%%l505Byky9;=3N`SU+Z9g14gf&nC1H)Y%Ues2o%PCCqEcP)&dJOSgA8J4ll zIIrt;J8oHWTUjodF;9z*{842}zZn+s{P9S@pAb18fK`Xi6yt=RR5X?jxQgUv@uPBU zMQwL}>PdRriuN*{jHtR4Kb;q4UXBz}JXhz%b}|O-{SlfH3L>$#h9WZ*ZAQ*B@zTl5w@9I_qN*s>XaK{GR-NLL!At z-q8UcZ9(=G0;VqGl0mInXyJ9(IGkLOx9|HGrv0=x=dIn6so0z3wbo6<3DY!wRNhgq zqYGhVpGKcg6P$-y8{&_*$7WtCsFf#i{)%|%br?B4f*H?U3SZDgW@>ZK1XkU>p}Ql? z#7Uh19FJBM(N7ZxyAArauM!-dHdIPnT!iDyH=qLxm*Sm|8Riz0g&r6@-gH(^7+XIz zx3FZUW>lL!^t!=d>H4ov@cG3;>BvQS2&n0ms!ie#s8>_7Xxvw)AGq3An6Gnn|CYmB zy=6m)0ST4lybnofU~=#I3^^J`52_IZmHHz&%Kfh9!zZXG!c(L4=F{#)ekU{}oh8$g z_$k(@X%%G_o^cPP%f>`~@6xp)_iuvA+~Yns$%jxRsvXZq=FI^wfQuuiyoS5!_a{Vn zI~Kj1a$Ovq%SvMfAP+Ybn2IU(0rcC1@2aeBGYm-a0<7?J5ThdvFiFK>Jb~Ht*l+6y zP8!48yt{_bK+N-brLUKk_Ec>divVus7!B3j>I=;^r@Eh0bDr-$qYd_pUD6gVyVKIa zESlk`QQCwNajue?*wwufe7U47qO0zpxjRci@_KXi6Z&~-14D(v*%a+_pCv5GD8aGd zQseiX)(*65^F8FW#^sxpea_Q0__DVX^EPqE-%Nd2pALuLtm<}7t?pS)FRplf;dtPW zg2{X1kq_*cXAv;8EC>|^PcYs;{nGOJ$;~*qM5gU)T23?h_ijMZ@ryO$sEKA`bD&LV zyGr1C$mB2c%(`S|p;wG~%7zNeC{DKlA;1O&ksPJ0GMDIQ4WzZ4L>tTA;+-zfFV==` zDi`@ZE%OhlabY@6_-Q6kWhjR*{C0(Nw44b;>xQW`F=mA;{`M-b1QPBd%5{5ZpK0Went(5Y zP0{wbb0&g5i}%Ol4!zNFS&3L@q9B5+jC_c5!k6w^cwUvaXi3bCpg-6A6VhH(jEDVy zjJwoxiuBC;=2Nw(}{8%Biep$KKjo*2rWeV2XT*HN}G zV;yFB|1P~h@6YFXp4apHe!qWY%-wBV?)$p0>pYM1IFDn-Ha>WG{8&+B&jz!L#4~}! z-wM8V*@Te#i78PXw}Ge1ynGotMZC3>4k1g<(Z-8z8^K9CKkK6Of+v2_CY+EsxcC%+ zo@5#P&PZsLdzMLakl~`yPnRp4+QEp>)+3 z&zv!mr3N&>b0iPBsB|)0R}?=W0f9K)+oN<)hjUu8UMXU&!J6jB#;t zo;XrPYKVvC62GZ^Q*BDI9Q1h;#TdY3H31pckW>A=-kca^6xx2S8awOw@>jBcv{`l8 zl!Ju@m&`@ElanV$Jxo(T18~&qCufh{`oR3MS9j@es)77}iF=O%kWb)mkPovjI$XQU zrbVFvu`y)FacqOCu8SUdRonHy7xfO6;00;g&w)zzoP**OBF}_j9AF@?I&f_D~y6Gji)vA6_$(jy|W|#3~z=p7UR-Apd|zcf?An>vg5r+YID1Qe}Ef zpA|aRz=+B^q3M??azzKDBG#9;Y!)_wB04Jmw*g-!|bZb-L@O0<(9KA>P1|) zd#X+G(G8AFL}_i=pa|Vb^GC1ZmmA-2qF+HE+dbxrC6j&s;r#qlqC0%OYGe_>m1J{5 zCPqW<`{ReK8F9V3tE~v@%)XM{3e`EZnL~b(L|*le zv%aR2>L0F7+}dP5aA=tsuCUkIjp`NqIhM7|HR3OET#fsQjvV_SChbU#H6Rlv>wv&Y z*(uG9a=D>r2;-KW?GTvnY>a6}^4flt)0*dP8+x17@q#UfuJoipwU**ehw*?ndA7vk&pk8AfVYqwxahh z8qDmpz$m9me!W{p^BL55{m`BQB<^G>LTSV8czhgPJQq-5>~rx{GK&+4}V1`0J>MG$&Kx0lLP6-S~j~AO)O<&7IEpCdzCmE zx-Lu(?WDmlpxA^ik-qKfZcjTK>sR__#?FsV%Ty0}k(X$@sRHDSh4{`hKJQT-!KYmX zc`+%(S#jE*J`t(yBW9u%&p;fwtgs*E6m8&E{(O~x-U`hbMD+q=U@wB1_t%(DIhVWtYJkcS-U=s89 zQOe=blYjUje?E5^LuACvDZyBu#MXeo9Q-omHU!v9av~ZMsO(S{=)OO&YW#Bw%abrt zGL;?10v0?vKv1fC2tNG59D+3}=@eiT`R4-NbbK@Co(lQQGz_q51U~+KFnUf4D${7~ zZ6mOIc*-VSkRps2(!6JP>f>XF?1oBA=0P)vwJRpERSzlcw+^+|-snbL8 z>>v-<-fWl+;E@)NLF#$N%_3iw)L$xf*2tWyxFk~cq&?Qf{J{#Y=OeVE57n0KL` zE=P)HXF8gYS9efsf_ZrT#9r9$)mP}c;D^-X-@_Gdbv$^bY3XsS(<;4D(0M>_>6tfUHRnECV#@q9+rKcZ8NUj zm{e==RL&FtFu=?X&s2YED^w)xU{3is zgIFs}+5am1^ME2BQBIZaw;x}jj@sGG@GghD^c@ShsE_dMPyWII)4bL?yq*?eBD{LS z+G$F^A!|DSvznwsr~gd5?%fK_vWC(z-N79*#NDrUUFw^>8t$ti1sv#|o)S}R0>T=e zuSfPPExZ30=xaO(09#!VOowsxOI2vMC zi=TO23?wM6^$%(iFO$4NlpBRvKQ|U};%l$%K!5LLuDlRns!G}G)BronaBpRd|MB6+ zDNPaRI^U(g+41{axVI61K~A|_$~4&=i9nBVAcm_}szq^MgNS)KY!ERHky&@jc=WQl z{UMQ~=W7O(xGv9e)hAlYqEC zXBa)S827*CH$q3==I||m?k~aN0)wwuw`uZs%MpvI86?YE@($+@JAt#kRfv~)E2X6l zjc0Da7KKM1wYS#5h5XJD&sKUy4G;`qBr%9L*Mu18xXwvm1Dyg}gMNB-&`?Dt#MV_q zsv}^-4+hSQ2l8fzf*g>v1Quh62l{y#7|W-)wbH-OZ-TxSU=fAk<7{g80O2a%xbj6_ zmK}R>l6ks~ckW(Q=(`^jB_N5WvilvqzjEPl+&i3cFYlhGHTd%@hphh|aHM>U@kDiV zQ&Hm)=Bn6oJ(!Li6ZK>d-I1FF#rO29q&X%2vOh!p-1p~`N&q9K!E1KofV&v1ANF@( z_!LhWppMd!?I+0yS@#}_|vSWY*?e{pym zGE901+X7=a{AvexthThRIfti)$OJwd{_^~n{Ju^nZS?0T6`T^k!FVcSbI8VT09<(E zImFy!qBu1eknO23d?t*oiVKjM?}B{;4F!9h>uwHW5e)S3>j)P#3U$_9FbaBN`T1h1 zGz~fh?#A|`;0w2*rO|z5s5Ve@9Qn^E`s~g`1Kk>k9R=LmmD-OmyTj|9w?D$}j>9mF znaXo13J>T!l(F0jWb>8jQU=_|ll39s4sDQ#=mvkenUV>RWIiP?k<7q6FrK4pg3@93 z6N_hBY`@vyV9dx#vFhyCZtc%pgCb0GF8LnUI_EDZUQ++Bs|Dtu9(ff`_r^74cJ~vQ zXG7qA87m%sMRtG9No7I)`FuU{vf`gBtpBy>-?wt`!EOh*$8b2u6Yzs$hc`e!x^x13 z;+GHl<|Xs|yp2WhRi2L}@pJFgkc-a0F`m(&-79--cxG$Nn&gfp?;3Au5lsX@4wAZw zo&~b8;I@{K!GfxM)Pgz%qKA1PPTsszw0yOn@^yylo$pnJ;6SlReGY8WUoo8J3f6-q zn!^jql9KF`ADryo?_NOM}kjYgfPbzFM^rtFZ7;@>0tF9Qd6b%f?GNc^X-&gzg< z=yl2`OEr0h$3=nYrh0F5`Oel^Um?OC^|qWii5^S!AKLOgBsuN_k|Q8-jzrXU+5Ly) zh+b_$9#S9x^VYO1+)v2UquyL+mFxMpAJQW1LLQbotx7WM*0i~wDxw)tO8A+G#>>&Z zHFIoo6Y7~FH@D?sb(rBwZwQ=M51$a?Uf8J9_uN5)zd40-^1j4}ot|so?m)cmmf>j| zjN`a339R0G6YG3(L(B19ZEJ}W&&Kc7f^pQrL=|| zXbCDIrz17)d=EX2_Kr!2E>Jx9i8lU4E!o>J{^gDhsW~|^7*1jB`w$@C7T4jNk_`_` zf)FR}L~CPAio(^ccmlh_9W0(-aZ%ENI{1uCb$kr}zG0EHATNn;Y^XUgIPswW_h~cE z@voY2RSUWs6Dc2^Q_9J%qri30%CM+cD*U7FgZIk7M{6ot_cxu3595}-^ll*DcWq^2 zvN>lokB{M3sIkP(cyq{41FFo@LHsxT%EJL2Bjf2g(fHGEzNrQZ;uRQJW-?LnKYlbD zvenH70+jT3zxx{8yo5_uKh4pLLBt`n{#UWxY#y9ceSK5zJ3P_8K=_8*6PGWwx8Q5w zX_=Zv617yn58f5G`WB`}Z0*)Q+3H|TYy6L``v31MysJhK^`sxg?mhnWMXW8)r<`Y@ zCQnq}s7_994fxCT(q#-kD>|Iyc!m<+`@>yOEmBUt3#XdWo*lD z*4s>O?-xY{C5zTRH~27RHaR^hwWr%5-_@Z8kGI$X$WjbJe2pb-G_N^zTqCC^;OJLc zK6S6P!KjZCJ6KTySBn<6=gS4X!3nKWhc`xk8+=e4Ft)&Y&jKKhsh%oCxRfUK@!YQk z2S{7)E7R=`fnyN4M)M0pRnqh4-R+nX6?^%-{9~tG7T-O_yyqd=_FI*I3-A)MHdh@Q z7ZpCQ+C313%k*@7=?7`I@O8{3X8mGdekwcKxW&A~I}zPRyMy&iJyImEF(-DidDwY; z^~D93nCnXuXN?$U=R$;q54;&CfMVkmY)&hnST4KbTko6=ub^*Q(xX(IKVQs5?X}S} zA`iYefBtly@pC$ark4h!B8ROGit0oFzMR!3QOm*V{;An3-`lD+djF!w%uWLgxCtco z(hUIHf`w$*0X1%)ar+C;mg7t0rl#v(5UowLkoh|$mQQc{wP{Snn-M%+v$ZTLXXE>F zk?K!MpK-H)j`)tN;MP{?_Az)N8-@p3ph|=Cij6m}ZwB_H_Z|o7R&XVzDLgsfN@wtC zSev@ONdjfzTl?Tk%)N-XRr3slRV}kU_n^Dx=;C5D?0`jX*Vh7M3_rm#kO0JzISNXF z9ABT|lv?anZ$`+%z%B}(O4_T;^F14Z{{41?Xf;ER?itPlJ48BI$Kd%S`$aOjeZQYl zxosjOdTh0U`4v;O(R~?3Hxzn7mH^`-I_eXRaH?~~gQlu%rp9K?o038zE{drE zH;(%79Qdr2p|`d~7m+fk=N09c`AZF6NjLewFusg>5%9ebaY75HB(Mnj1S4_c3^#Ls zgeBOn9s7~mupb`yMuA?Cdag3`Y}hpS<;x&fXB7e|~z+UYZ(+yn{qe6=chvZ3+u7W0Yv+PGvps z*G)ayN=C1Mj>UZhF)Y||Y@{)wKxoKarX6 zXjlF+Ss12rd9M7L9^SM3nS7a_W^)~^Qq0wZ5l>%S+r|c=ZbHLQk``Z-0uaR)PTXci zRnRnE_L<$@JyT;#%yw4jY09_EdN=vyb$IzADwCGYZ-j!j_Lwb`dzx!bauMDg$i}J!`ELiWE&FF?`p3 zAg#~gS#OEPaBV14hQYlckybeu4mK?dQ|ShA0W)j!Gi7~_4kEti_M6nB#@JM~esnBJ z)(cRF)(pzVqj%lMOh`e_jzT&jH;xM3z{Xm

a1IakfT}}gsTVzWwT@bAw?1N9*(1dtmw(OX{w8Yi4Mg5* z=5`7L<_@dv?Aj=m5v zzLlPOXL09jG|qe(|5_!R%sWtEZ=n~fd?`wm)bs0I`(iJ&P5bx>zb-02dycQwLo3?Y z8pETM>zGB500leYf5{B(IFf@vI6sn#wnu`d6LAs<)ko4v4j`=WL(kMwt07+IWh5D; zQn7O(UphxrGxj~Kzdu#*w``ed8%aVT2tW5;UfZ~AA(HeI`*m}`O>A`S<{2aO z=uPzY@jqsGf8GP3kiQS)&<@pF)$U)g|C^v8h5c;z^AA*Q@()xEf#YT(0)C?&4%dvl z(MXHN>{}JRpuXGXbXB~BhlW(y@L3psoszO(aAqI7Sm0XbkzE98Vw*h;m@;%jcduXL zFGrH<)^#h2IV+iD-{qTVn*GX;kLX%2eyQ?K9_7=6lI)~g8)eaNU(iN1R34HJtMXbw z2kRwy{x3DrR@oo#?y|{mT8_rV1(wO&kddNUDK>aZbi^$Nxd-ye#&%^)URmgrZpQ4j z?i{JUHM~B4Fm+k%@|p6NE&8e-u42+SXs-Q(OuGPFu~CcOGtLLORJtm+C^->*@m|Wl z>C?p&S2IfvnxB;hbz1bx-kshr?Iwyt16%E#R{9puK8(`LTVXpF{|{P#N$WA`a$i56 z?~vq!To~HyX+@m0lQzFm)%PsP(2b-N0C$i3>7>oy%CFWAl)x3RO|aAQ+^M&lR*Gis z^I8YD?Pn`4IMLzu2d9q^6Vr(YH*~s0N~&Ku#188)Onh)S>dqH^zl2bxYo3nh(ikkL zIaBXz%`YZ)=FOB^pON|+14c*v1Nxg1On;cD`Rn-8FimHOeI?L1&s{oADEb|;gk>Y&Xc=*VU~}rvL?hzBEMYk#R5s=^zA$jntD&4*}XuN zy#AemJ3DoYUDb-u6vOwNi3Y&mr}0GdiA&C@4WF2g3Ha+S{R_~#bt^y(!m6f_*M%}8 zlr;+QiaT0~?v|_&eoho_j$|7w_wM%GAEa4!GiV#%YnkC$H8_?!cs$vWd}S=h6kQi( zNOcE0-f_S4wkhJ@%lodS#!lUITx@mu%uyN4s7%x1XSrZe4e{XwGXQoDP*aXt4O%gKn|+47&@C8x&3Pzm^XJj#nyq`xovEmPC9g*c>X_suO^6yv zVI^LND7&bk@k+YTv|Y0P!Jm0GP~L&tFpu?>z#Vn*%WolN zC;Vk0fOyq*B*FkxKJCAQ4y?r5jk)wcQHwN%EJd?>Uq)00@-(j3pcBcl~0k z@x?Ajp4KBjvM>H*yC6IvIA3w@dxiVOtsg(U0~P`wxTiuvvYGX*2qOh5D<2O^~Ia=yq` z+2*zMkh0j>*OLN#=b6fV_?{XRlOcfH4c9^JoIo60fG=L_F+NC`-3Tcl%gx^AYU`Us zb~})>PM^=bm7}=8Og-Z-kJ|-neoUUKKoXiddzatf4!mEVJ&e#o{(nCmGG7RQPjAS z5wZQE6768D(vcb+WQ}6UwPwmxe^7a4iXNSyn$ROC&Ya=G9@>~BG3$aV84$O(}B5yLacxw%N_j&1Q_SGix0r|N|7^YJ|PfZXJZ1N%o8sK7q|FcPDdG4yI zukWE$SL`_{+1A!ZZnh$ZAPey=;fi^=H`rQUw4RpnTKFBlvEsvO*I0!hsOS_EO1evO zf0^*#8I~)L@iXl6)J$1|(w297gWlW; znqJM}kqU(qK|XbcA47Qs05v7sL%rqydBk7g?|#zwKTt~v=r;LRtpKru6W=oaFa4#A z`@A?

C2q#w?CyOmHdHE@SLjQZkv2` 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 E0AcRuvj6}9 literal 0 HcmV?d00001 diff --git a/examples/simple-examples/package.json b/examples/simple-examples/package.json index c23c1932..9f12ae68 100644 --- a/examples/simple-examples/package.json +++ b/examples/simple-examples/package.json @@ -106,7 +106,9 @@ "fax:services:listEmailsForNumber": "ts-node src/fax/services/listEmailsForNumber.ts", "fax:services:update": "ts-node src/fax/services/update.ts", "fax:services:delete": "ts-node src/fax/services/delete.ts", - "fax:faxes:send": "ts-node src/fax/faxes/send.ts", + "fax:faxes:send-filePaths": "ts-node src/fax/faxes/send-filePaths.ts", + "fax:faxes:send-fileBase64": "ts-node src/fax/faxes/send-fileBase64.ts", + "fax:faxes:send-multipleRecipients": "ts-node src/fax/faxes/send-multipleRecipients.ts", "fax:faxes:get": "ts-node src/fax/faxes/get.ts", "fax:faxes:list": "ts-node src/fax/faxes/list.ts", "fax:faxes:download": "ts-node src/fax/faxes/downloadContent.ts", diff --git a/examples/simple-examples/src/fax/faxes/get.ts b/examples/simple-examples/src/fax/faxes/get.ts index b582737a..93bf2a1d 100644 --- a/examples/simple-examples/src/fax/faxes/get.ts +++ b/examples/simple-examples/src/fax/faxes/get.ts @@ -20,7 +20,7 @@ import { getFaxIdFromConfig, getPrintFormat, initFaxService, printFullResponse } if (printFormat === 'pretty') { console.log(`Fax found: it has been created at '${response.createTime}' and the status is '${response.status}'`); if (response.status === 'FAILURE') { - console.log(`Error type: ${response.errorType} (${response.errorId}): ${response.errorCode}`); + console.log(`Error type: ${response.errorType} (${response.errorCode}): ${response.errorMessage}`); } } else { printFullResponse(response); diff --git a/examples/simple-examples/src/fax/faxes/list.ts b/examples/simple-examples/src/fax/faxes/list.ts index cba310e8..c9cbb365 100644 --- a/examples/simple-examples/src/fax/faxes/list.ts +++ b/examples/simple-examples/src/fax/faxes/list.ts @@ -17,8 +17,42 @@ const populateFaxesList = ( console.log('* getFaxes *'); console.log('************'); + const today = new Date(); + const previousMonth = (today.getUTCMonth() - 1) % 12; + const lastMonth = new Date().setMonth(previousMonth); + + // Example of createTime filter + // - fetch last month's faxes (with Date objects) + // => Output = ?createTime>=2024-04-24T11:33:44Z&createTime<=2024-05-24T11:33:44Z + // createTimeFilter: { + // from: new Date(lastMonth), + // to: new Date(), + // } + // - fetch last month's faxes (with DateFormat objets) + // => Output = ?createTime>=2024-04-24&createTime<=2024-05-24 + // createTimeFilter: { + // from: { + // date: new Date(lastMonth), + // unit: 'day', + // }, + // to: { + // date: new Date(), + // unit: 'day', + // }, + // } + const requestData: Fax.ListFaxesRequestData = { - pageSize: 2, + pageSize: 10, + createTimeRange: { + from: { + date: new Date(lastMonth), + unit: 'day', + }, + to: { + date: new Date(), + unit: 'day', + }, + }, }; const faxService = initFaxService(); diff --git a/examples/simple-examples/src/fax/faxes/send-fileBase64.ts b/examples/simple-examples/src/fax/faxes/send-fileBase64.ts new file mode 100644 index 00000000..4e69d11a --- /dev/null +++ b/examples/simple-examples/src/fax/faxes/send-fileBase64.ts @@ -0,0 +1,60 @@ +import { Fax } from '@sinch/sdk-core'; +import { + getFaxCallbackUrlFromConfig, + getPhoneNumberFromConfig, + getPrintFormat, + initFaxService, + printFullResponse, +} from '../../config'; +import * as fs from 'fs'; + +const getBase64 = (filePath: string): Fax.FaxBase64File => { + const fileExtension = filePath.split('.').pop()?.toUpperCase(); + try { + const fileBuffer = fs.readFileSync(filePath); + return { + file: fileBuffer.toString('base64'), + fileType: Fax.convertToSupportedFileType(fileExtension), + }; + } catch (error) { + console.error('Error reading or converting the file:', error); + throw error; + } +}; + +(async () => { + console.log('*************************'); + console.log('* sendFax - base64 file *'); + console.log('*************************'); + + const originPhoneNumber = getPhoneNumberFromConfig(); + const destinationPhoneNumber = getPhoneNumberFromConfig(); + const faxCallbackUrl = getFaxCallbackUrlFromConfig(); + + const requestData: Fax.SendSingleFaxRequestData = { + sendFaxRequestBody: { + to: destinationPhoneNumber, + from: originPhoneNumber, + contentUrl: 'https://developers.sinch.com/', + files: [getBase64('./fax-pdf/you-faxed.pdf')], + callbackUrl: faxCallbackUrl, + callbackUrlContentType: 'application/json', + imageConversionMethod: 'MONOCHROME', + headerTimeZone: 'Europe/Paris', + headerText: ' - Sent with the Node.js SDK', + maxRetries: 2, + }, + }; + + const faxService = initFaxService(); + const response = await faxService.faxes.send(requestData); + + const printFormat = getPrintFormat(process.argv); + + if (printFormat === 'pretty') { + const fax = response[0]; + console.log(`Fax successfully created at '${fax.createTime?.toISOString()}'. Status = '${fax.status}'`); + } else { + printFullResponse(response); + } +})(); diff --git a/examples/simple-examples/src/fax/faxes/send-filePaths.ts b/examples/simple-examples/src/fax/faxes/send-filePaths.ts new file mode 100644 index 00000000..a7180c0e --- /dev/null +++ b/examples/simple-examples/src/fax/faxes/send-filePaths.ts @@ -0,0 +1,45 @@ +import { Fax } from '@sinch/sdk-core'; +import { + getFaxCallbackUrlFromConfig, + getPhoneNumberFromConfig, + getPrintFormat, + initFaxService, + printFullResponse, +} from '../../config'; + +(async () => { + console.log('**********************'); + console.log('* sendFax - filePath *'); + console.log('**********************'); + + const originPhoneNumber = getPhoneNumberFromConfig(); + const destinationPhoneNumber = getPhoneNumberFromConfig(); + const faxCallbackUrl = getFaxCallbackUrlFromConfig(); + + const requestData: Fax.SendSingleFaxRequestData = { + sendFaxRequestBody: { + to: destinationPhoneNumber, + from: originPhoneNumber, + contentUrl: 'https://developers.sinch.com/', + filePaths: ['./fax-pdf/you-faxed.pdf', './fax-pdf/you-faxed.pdf'], + callbackUrl: faxCallbackUrl, + callbackUrlContentType: 'application/json', + imageConversionMethod: 'MONOCHROME', + headerTimeZone: 'Europe/Paris', + headerText: ' - Sent with the Node.js SDK', + maxRetries: 2, + }, + }; + + const faxService = initFaxService(); + const response = await faxService.faxes.send(requestData); + + const printFormat = getPrintFormat(process.argv); + + if (printFormat === 'pretty') { + const fax = response[0]; + console.log(`Fax successfully created at '${fax.createTime?.toISOString()}'. Status = '${fax.status}'`); + } else { + printFullResponse(response); + } +})(); diff --git a/examples/simple-examples/src/fax/faxes/send.ts b/examples/simple-examples/src/fax/faxes/send-multipleRecipients.ts similarity index 54% rename from examples/simple-examples/src/fax/faxes/send.ts rename to examples/simple-examples/src/fax/faxes/send-multipleRecipients.ts index 221c9d92..0e94bf95 100644 --- a/examples/simple-examples/src/fax/faxes/send.ts +++ b/examples/simple-examples/src/fax/faxes/send-multipleRecipients.ts @@ -8,21 +8,25 @@ import { } from '../../config'; (async () => { - console.log('***********'); - console.log('* sendFax *'); - console.log('***********'); + console.log('*********************************'); + console.log('* sendFax - multiple recipients *'); + console.log('*********************************'); const originPhoneNumber = getPhoneNumberFromConfig(); const destinationPhoneNumber = getPhoneNumberFromConfig(); const faxCallbackUrl = getFaxCallbackUrlFromConfig(); - const requestData: Fax.SendFaxRequestData = { + const requestData: Fax.SendMultipleFaxRequestData = { sendFaxRequestBody: { - to: destinationPhoneNumber, + to: [destinationPhoneNumber, destinationPhoneNumber], from: originPhoneNumber, contentUrl: 'https://developers.sinch.com/fax/fax.pdf', callbackUrl: faxCallbackUrl, - callbackContentType: 'application/json', + callbackUrlContentType: 'application/json', + imageConversionMethod: 'MONOCHROME', + headerTimeZone: 'Europe/Paris', + headerText: ' - Sent with the Node.js SDK', + maxRetries: 2, }, }; @@ -32,7 +36,7 @@ import { const printFormat = getPrintFormat(process.argv); if (printFormat === 'pretty') { - console.log(`Fax successfully created at '${response.createTime?.toISOString()}'. Status = '${response.status}'`); + console.log(`${response.length} fax(es) successfully created.\n${response.map((fax) => ' - ' + fax.id + ' created at ' + fax.createTime?.toISOString()).join('\n')}`); } else { printFullResponse(response); } diff --git a/examples/webhooks/src/services/fax-event.service.ts b/examples/webhooks/src/services/fax-event.service.ts index c5858a6e..54160c91 100644 --- a/examples/webhooks/src/services/fax-event.service.ts +++ b/examples/webhooks/src/services/fax-event.service.ts @@ -15,9 +15,7 @@ export class FaxEventService { } if (event.event === 'FAX_COMPLETED') { const faxCompletedEvent = event as Fax.FaxCompletedEventJson; - for (const fileBase64 of faxCompletedEvent.files!) { - this.saveBase64File(fileBase64, event.fax!.id!); - } + this.saveBase64File(faxCompletedEvent, event.fax!.id!); } } else if (contentType?.includes('multipart/form-data')) { console.log(`** multipart/form-data\n${event.event}: ${event.fax!.id} - ${event.eventTime}`); @@ -28,10 +26,10 @@ export class FaxEventService { } } - private saveBase64File(fileBase64: Fax.FaxBase64File, faxId: string) { + private saveBase64File(event: Fax.IncomingFaxEventJson | Fax.FaxCompletedEventJson, faxId: string) { console.log('Saving file...'); - const filePath = path.join('./fax-upload', faxId + '.' + fileBase64.fileType!.toLowerCase()); - const buffer = Buffer.from(fileBase64.file!, 'base64'); + const filePath = path.join('./fax-upload', event.event + '-' + faxId + '.' + event.fileType!.toLowerCase()); + const buffer = Buffer.from(event.file!, 'base64'); fs.writeFileSync(filePath, buffer); console.log('File saved! ' + filePath); } diff --git a/packages/fax/src/models/v3/date-range-filter/date-range-filter.ts b/packages/fax/src/models/v3/date-range-filter/date-range-filter.ts new file mode 100644 index 00000000..758ce222 --- /dev/null +++ b/packages/fax/src/models/v3/date-range-filter/date-range-filter.ts @@ -0,0 +1,18 @@ +import { DateFormat } from '@sinch/sdk-client'; + +/** + * Filter calls based on `createTime`. If not value is submitted, the default value is the prior week. + * - `from: '2024-02-15'` will return all faxes from February 2024, 15th + * - `from: '2024-02-01T14:00:00Z'` will return all faxes after 14:00:00 on the first of February 2024. + * - `from: '2024-02-01T14:00:00Z'` + `to: '2024-02-01T15:00:00Z'` will return all faxes between 14:00:00 and 15:00:00 (inclusive) on the first of February 2024. + * - `from: '2024-02-01'` + `to: '2024-02-29'` will return all faxes for all of February 2024. + * + * Note: It is also possible to submit partial dates. + * - `from: '2024-02'` will return all faxes for February 2024 + */ +export interface DateRangeFilter { + /** */ + from?: string | Date | DateFormat; + /** */ + to?: string | Date | DateFormat; +} diff --git a/packages/fax/src/models/v3/date-range-filter/index.ts b/packages/fax/src/models/v3/date-range-filter/index.ts new file mode 100644 index 00000000..8ee98119 --- /dev/null +++ b/packages/fax/src/models/v3/date-range-filter/index.ts @@ -0,0 +1 @@ +export type { DateRangeFilter } from './date-range-filter'; diff --git a/packages/fax/src/models/v3/enums.ts b/packages/fax/src/models/v3/enums.ts index 7ce8b33a..74a30bb5 100644 --- a/packages/fax/src/models/v3/enums.ts +++ b/packages/fax/src/models/v3/enums.ts @@ -1,11 +1,3 @@ -// export type { TypeEnum as BarCodeTypeEnum } from './bar-code/bar-code'; -// export type { TypeEnum as CallErrorTypeEnum, ErrorCodeEnum as CallErrorErrorCodeEnum } from './call-error/call-error'; -// export type { TypeEnum as DocumentConversionErrorTypeEnum, ErrorCodeEnum as DocumentConversionErrorErrorCodeEnum } from './document-conversion-error/document-conversion-error'; -// export type { CallbackContentTypeEnum as FaxCallbackContentTypeEnum, ImageConversionMethodEnum as FaxImageConversionMethodEnum } from './fax/fax'; -// export type { FileTypeEnum as FaxBase64FileFileTypeEnum } from './fax-base64-file/fax-base64-file'; -// export type { TypeEnum as FaxErrorTypeEnum } from './fax-error/fax-error'; -// export type { CallbackContentTypeEnum as SendFaxRequest1CallbackContentTypeEnum, ImageConversionMethodEnum as SendFaxRequest1ImageConversionMethodEnum } from './send-fax-request1/send-fax-request1'; - export type ImageConversionMethod = 'HALFTONE' | 'MONOCHROME'; export type WebhookContentType = 'multipart/form-data' | 'application/json'; @@ -21,7 +13,7 @@ export type FaxDirection = 'OUTBOUND' | 'INBOUND'; export type FaxStatus = 'QUEUED' | 'IN_PROGRESS' | 'COMPLETED' | 'FAILURE'; /** - * Error type + * Type of error for the fax */ export type ErrorType = 'DOCUMENT_CONVERSION_ERROR' @@ -33,4 +25,7 @@ export type ErrorType = export type FaxBase64FileType = 'DOC' | 'DOCX' | 'PDF' | 'TIF' | 'JPG' | 'ODT' | 'TXT' | 'HTML' | 'PNG'; +export const validBase64FileTypes: FaxBase64FileType[] + = ['DOC', 'DOCX', 'PDF', 'TIF', 'JPG', 'ODT', 'TXT', 'HTML', 'PNG']; + export type FaxWebhookEvent = 'INCOMING_FAX' | 'FAX_COMPLETED'; diff --git a/packages/fax/src/models/v3/fax-request/fax-request.ts b/packages/fax/src/models/v3/fax-request/fax-request.ts index 1571a84a..6ae9ff88 100644 --- a/packages/fax/src/models/v3/fax-request/fax-request.ts +++ b/packages/fax/src/models/v3/fax-request/fax-request.ts @@ -2,11 +2,11 @@ import { ImageConversionMethod, WebhookContentType } from '../enums'; import { FaxContentUrl } from '../fax-content-url'; import { FaxBase64File } from '../fax-base64-file'; -export type FaxRequest = FaxRequestJson | FaxRequestFormData; +export type SingleFaxRequest = SingleFaxRequestJson | SingleFaxRequestFormData; -interface FaxRequestBase { - /** A phone number in [E.164](https://community.sinch.com/t5/Glossary/E-164/ta-p/7537) format, including the leading '+'. */ - to: string; +export type MultipleFaxRequest = MultipleFaxRequestJson | MultipleFaxRequestFormData; + +export interface FaxRequestBase { /** A phone number in [E.164](https://community.sinch.com/t5/Glossary/E-164/ta-p/7537) format, including the leading '+'. */ from?: string; /** */ @@ -24,7 +24,7 @@ interface FaxRequestBase { /** The URL to which a callback will be sent when the fax is completed. The callback will be sent as a POST request with a JSON body. The callback will be sent to the URL specified in the `callbackUrl` parameter, if provided, otherwise it will be sent to the URL specified in the `callbackUrl` field of the Fax Service object. */ callbackUrl?: string; /** The content type of the callback. */ - callbackContentType?: WebhookContentType; + callbackUrlContentType?: WebhookContentType; /** Determines how documents are converted to black and white. Defaults to value selected on Fax Service object. */ imageConversionMethod?: ImageConversionMethod; /** ID of the fax service used. */ @@ -36,9 +36,31 @@ interface FaxRequestBase { export type FaxRequestJson = FaxRequestBase & { /** An array of base64 encoded files */ files: FaxBase64File[]; + filePaths?: never; +} + +export type SingleFaxRequestJson = FaxRequestJson & { + /** A phone number in [E.164](https://community.sinch.com/t5/Glossary/E-164/ta-p/7537) format, including the leading '+'. */ + to: string; +} + +export type MultipleFaxRequestJson = FaxRequestJson & { + /** A list of phone number in [E.164](https://community.sinch.com/t5/Glossary/E-164/ta-p/7537) format, including the leading '+'. */ + to: string[]; } export interface FaxRequestFormData extends FaxRequestBase { /** The file(s) you want to send as a fax as body attachment. */ - file?: any; + filePaths?: string | string[]; + files?: never; +} + +export type SingleFaxRequestFormData = FaxRequestFormData & { + /** A phone number in [E.164](https://community.sinch.com/t5/Glossary/E-164/ta-p/7537) format, including the leading '+'. */ + to: string; +} + +export type MultipleFaxRequestFormData = FaxRequestFormData & { + /** A list of phone number in [E.164](https://community.sinch.com/t5/Glossary/E-164/ta-p/7537) format, including the leading '+'. */ + to: string[]; } diff --git a/packages/fax/src/models/v3/fax-request/index.ts b/packages/fax/src/models/v3/fax-request/index.ts index ef1da073..15e01ebb 100644 --- a/packages/fax/src/models/v3/fax-request/index.ts +++ b/packages/fax/src/models/v3/fax-request/index.ts @@ -1 +1,11 @@ -export type { FaxRequest, FaxRequestJson, FaxRequestFormData } from './fax-request'; +export type { + SingleFaxRequest, + SingleFaxRequestJson, + SingleFaxRequestFormData, + FaxRequestBase, + FaxRequestJson, + FaxRequestFormData, + MultipleFaxRequest, + MultipleFaxRequestJson, + MultipleFaxRequestFormData, +} from './fax-request'; diff --git a/packages/fax/src/models/v3/fax/fax.ts b/packages/fax/src/models/v3/fax/fax.ts index 6ccef1fb..42ad1de1 100644 --- a/packages/fax/src/models/v3/fax/fax.ts +++ b/packages/fax/src/models/v3/fax/fax.ts @@ -40,15 +40,15 @@ export interface Fax { /** The URL to which a callback will be sent when the fax is completed. The callback will be sent as a POST request with a JSON body. The callback will be sent to the URL specified in the `callbackUrl` parameter, if provided, otherwise it will be sent to the URL specified in the `callbackUrl` field of the Fax Service object. */ callbackUrl?: string; /** The content type of the callback. */ - callbackContentType?: WebhookContentType; + callbackUrlContentType?: WebhookContentType; /** Determines how documents are converted to black and white. Defaults to value selected on Fax Service object. */ imageConversionMethod?: ImageConversionMethod; /** @see ErrorType */ errorType?: ErrorType; /** One of the error numbers listed in the [Fax Error Messages section](#FaxErrors). */ - errorId?: number; - /** One of the error codes listed in the [Fax Error Messages section](#FaxErrors). */ - errorCode?: string; + errorCode?: number; + /** One of the error messages listed in the [Fax Error Messages section](#FaxErrors). */ + errorMessage?: string; /** The `Id` of the project associated with the call. */ projectId?: string; /** ID of the fax service used. */ diff --git a/packages/fax/src/models/v3/faxes-list/faxes-list.ts b/packages/fax/src/models/v3/faxes-list/faxes-list.ts new file mode 100644 index 00000000..cb25d1e2 --- /dev/null +++ b/packages/fax/src/models/v3/faxes-list/faxes-list.ts @@ -0,0 +1,5 @@ +import { Fax } from '../fax'; + +export interface FaxesList { + faxes: Fax[]; +} diff --git a/packages/fax/src/models/v3/faxes-list/index.ts b/packages/fax/src/models/v3/faxes-list/index.ts new file mode 100644 index 00000000..4a1c6156 --- /dev/null +++ b/packages/fax/src/models/v3/faxes-list/index.ts @@ -0,0 +1 @@ +export type { FaxesList } from './faxes-list'; diff --git a/packages/fax/src/models/v3/helper.ts b/packages/fax/src/models/v3/helper.ts new file mode 100644 index 00000000..dbe256c0 --- /dev/null +++ b/packages/fax/src/models/v3/helper.ts @@ -0,0 +1,8 @@ +import { FaxBase64FileType, validBase64FileTypes } from './enums'; + +export const convertToSupportedFileType = (fileType: string | undefined): FaxBase64FileType | undefined => { + if (!fileType || !validBase64FileTypes.includes(fileType.toUpperCase() as FaxBase64FileType)) { + return undefined; + } + return fileType.toUpperCase() as FaxBase64FileType; +}; diff --git a/packages/fax/src/models/v3/index.ts b/packages/fax/src/models/v3/index.ts index a64bcd07..1e66afd0 100644 --- a/packages/fax/src/models/v3/index.ts +++ b/packages/fax/src/models/v3/index.ts @@ -1,32 +1,18 @@ -// export * from './bad-request-detail'; export * from './bar-code'; -// export * from './call-error'; -// export * from './document-conversion-error'; +export * from './date-range-filter'; export * from './email'; -// export * from './error'; -// export * from './error-detail'; -// export * from './error-info'; -// export * from './error-type'; export * from './fax'; export * from './fax-base64-file'; export * from './fax-content-url'; -// export * from './fax-error'; -// export * from './fax-errors'; -// export * from './fax-status'; -// export * from './field-violation'; -// export * from './generic-event'; export * from './fax-money'; export * from './fax-request'; +export * from './faxes-list'; export * from './mod-events'; -// export * from './pagination'; -// export * from './quota-failure'; -// export * from './quota-failure-detail'; -// export * from './request-info-detail'; -// export * from './send-fax-request1'; export * from './service'; export * from './service-email-settings'; export * from './service-phone-number'; export * from './update-email-request'; -export * from './webhook-event-parsed/webhook-event-parsed'; +export * from './webhook-event-parsed'; export * from './enums'; export * from './requests'; +export * from './helper'; diff --git a/packages/fax/src/models/v3/mod-events/fax-completed-event/fax-completed-event.ts b/packages/fax/src/models/v3/mod-events/fax-completed-event/fax-completed-event.ts index 3952ebc4..17b3110a 100644 --- a/packages/fax/src/models/v3/mod-events/fax-completed-event/fax-completed-event.ts +++ b/packages/fax/src/models/v3/mod-events/fax-completed-event/fax-completed-event.ts @@ -1,13 +1,15 @@ -import { FaxBase64File } from '../../fax-base64-file'; import { FaxEventFormData, FaxEventJson } from '../base-fax-event'; +import { FaxBase64FileType } from '../../enums'; export type FaxCompletedEvent = FaxCompletedEventJson | FaxCompletedEventFormData; export interface FaxCompletedEventJson extends FaxEventJson { /** Always FAX_COMPLETED for this event. */ event?: 'FAX_COMPLETED'; - /** */ - files?: FaxBase64File[]; + /** The base64 encoded file. */ + file?: string; + /** The file type of the attached file. */ + fileType?: FaxBase64FileType; } export interface FaxCompletedEventFormData extends FaxEventFormData { diff --git a/packages/fax/src/models/v3/requests/faxes/faxes-request-data.ts b/packages/fax/src/models/v3/requests/faxes/faxes-request-data.ts index 5985722b..f8e37eb1 100644 --- a/packages/fax/src/models/v3/requests/faxes/faxes-request-data.ts +++ b/packages/fax/src/models/v3/requests/faxes/faxes-request-data.ts @@ -1,5 +1,6 @@ import { FaxDirection, FaxStatus } from '../../enums'; -import { FaxRequest } from '../../fax-request'; +import { SingleFaxRequest, MultipleFaxRequest } from '../../fax-request'; +import { DateRangeFilter } from '../../date-range-filter'; export interface DeleteFaxContentRequestData { /** The ID of the fax. */ @@ -16,8 +17,10 @@ export interface GetFaxRequestData { 'id': string; } export interface ListFaxesRequestData { - /** Filter calls based on `createTime`. If you make the query more precise, fewer results will be returned. For example, `2021-02-01` will return all calls from the first of February 2021, and `2021-02-01T14:00:00Z` will return all calls after 14:00 on the first of February. This field also supports `<=` and `>=` to search for calls in a range `?createTime>=2021-10-01&startTime<=2021-10-30` to get a list of calls for all of October 2021. It is also possible to submit partial dates. For example, `createTime=2021-02` will return all calls for February 2021. If not value is submitted, the default value is the prior week. */ - 'createTime'?: string; + /** Filter calls based on `createTime`. It can be a year, a month or a day. */ + 'createTime'?: string | Date; + /** Filter calls based on `createTime`. It will filter the faxes on a range of dates. */ + 'createTimeRange'?: DateRangeFilter; /** Limits results to faxes with the specified direction. */ 'direction'?: FaxDirection; /** Limits results to faxes with the specified status. */ @@ -31,6 +34,13 @@ export interface ListFaxesRequestData { /** The page you want to retrieve returned from a previous List request, if any */ 'page'?: string; } -export interface SendFaxRequestData { - 'sendFaxRequestBody': FaxRequest; + +export type SendFaxRequestData = SendSingleFaxRequestData | SendMultipleFaxRequestData; + +export interface SendSingleFaxRequestData { + 'sendFaxRequestBody': SingleFaxRequest; +} + +export interface SendMultipleFaxRequestData { + 'sendFaxRequestBody': MultipleFaxRequest; } diff --git a/packages/fax/src/models/v3/webhook-event-parsed/index.ts b/packages/fax/src/models/v3/webhook-event-parsed/index.ts new file mode 100644 index 00000000..9bb76639 --- /dev/null +++ b/packages/fax/src/models/v3/webhook-event-parsed/index.ts @@ -0,0 +1 @@ +export type { FaxWebhookEventParsed } from './webhook-event-parsed'; diff --git a/packages/fax/src/rest/v3/enums.ts b/packages/fax/src/rest/v3/enums.ts index 28ebfa1d..cb0ff5c3 100644 --- a/packages/fax/src/rest/v3/enums.ts +++ b/packages/fax/src/rest/v3/enums.ts @@ -1,2 +1 @@ -// export type { GetFaxFileByIdFileFormatEnum, SendFaxCallbackContentTypeEnum, SendFaxImageConversionMethodEnum } from './faxes'; export {}; diff --git a/packages/fax/src/rest/v3/faxes/faxes-api.jest.fixture.ts b/packages/fax/src/rest/v3/faxes/faxes-api.jest.fixture.ts index ff2ac536..3eba119a 100644 --- a/packages/fax/src/rest/v3/faxes/faxes-api.jest.fixture.ts +++ b/packages/fax/src/rest/v3/faxes/faxes-api.jest.fixture.ts @@ -12,24 +12,24 @@ import { ApiListPromise, FileBuffer } from '@sinch/sdk-client'; export class FaxesApiFixture implements Partial> { /** - * Fixture associated to function deleteFaxContentById + * Fixture associated to function deleteContent */ public deleteContent: jest.Mock, [DeleteFaxContentRequestData]> = jest.fn(); /** - * Fixture associated to function getFaxFileById + * Fixture associated to function downloadContent */ public downloadContent: jest.Mock, [DownloadFaxContentRequestData]> = jest.fn(); /** - * Fixture associated to function getFaxInfoPerId + * Fixture associated to function get */ public get: jest.Mock, [GetFaxRequestData]> = jest.fn(); /** - * Fixture associated to function getFaxes + * Fixture associated to function list */ public list: jest.Mock, [ListFaxesRequestData]> = jest.fn(); /** - * Fixture associated to function sendFax + * Fixture associated to function send */ - public send: jest.Mock, [SendFaxRequestData]> = jest.fn(); + public send: jest.Mock, [SendFaxRequestData]> = jest.fn(); } diff --git a/packages/fax/src/rest/v3/faxes/faxes-api.ts b/packages/fax/src/rest/v3/faxes/faxes-api.ts index 072c0ac1..26b77b9a 100644 --- a/packages/fax/src/rest/v3/faxes/faxes-api.ts +++ b/packages/fax/src/rest/v3/faxes/faxes-api.ts @@ -2,7 +2,9 @@ import { ApiListPromise, buildPageResultPromise, createIteratorMethodsForPagination, + DateFormat, FileBuffer, + formatDate, PaginatedApiProperties, PaginationEnum, RequestBody, @@ -12,12 +14,19 @@ import { FaxDomainApi } from '../fax-domain-api'; import { Fax, FaxRequestJson, - FaxRequestFormData, DeleteFaxContentRequestData, DownloadFaxContentRequestData, GetFaxRequestData, - ListFaxesRequestData, SendFaxRequestData, + ListFaxesRequestData, + SendSingleFaxRequestData, + SingleFaxRequestFormData, + FaxesList, + SendMultipleFaxRequestData, + MultipleFaxRequestFormData, + SendFaxRequestData, } from '../../../models'; +import FormData = require('form-data'); +import * as fs from 'fs'; export class FaxesApi extends FaxDomainApi { @@ -122,13 +131,16 @@ export class FaxesApi extends FaxDomainApi { public list(data: ListFaxesRequestData): ApiListPromise { this.client = this.getSinchClient(); const getParams = this.client.extractQueryParams(data, [ - 'createTime', 'direction', 'status', 'to', 'from', 'pageSize', 'page']); + (getParams as any).createTime = JSON.stringify(this.formatCreateTimeFilter(data.createTime)); + (getParams as any)['createTime>'] = JSON.stringify(this.formatCreateTimeRangeFilter(data.createTimeRange?.from)); + (getParams as any)['createTime<'] = JSON.stringify(this.formatCreateTimeRangeFilter(data.createTimeRange?.to)); + const headers: { [key: string]: string | undefined } = { 'Content-Type': 'application/json', 'Accept': 'application/json', @@ -162,14 +174,41 @@ export class FaxesApi extends FaxDomainApi { return listPromise as ApiListPromise; } + formatCreateTimeFilter = (createTime: string | Date | undefined): string | undefined => { + if (createTime !== undefined) { + if (typeof createTime === 'string') { + if (createTime.indexOf('T') > -1) { + return createTime.substring(0, createTime.indexOf('T')); + } + return createTime; + } else { + return formatDate(createTime, 'day'); + } + } + return undefined; + }; + + formatCreateTimeRangeFilter = (timeBoundary: string | Date | DateFormat | undefined): string | undefined => { + if (timeBoundary !== undefined) { + if (typeof timeBoundary === 'string') { + return timeBoundary; + } else if (timeBoundary instanceof Date) { + return formatDate(timeBoundary); + } else { + return formatDate(timeBoundary.date, timeBoundary.unit); + } + } + return undefined; + }; + /** - * Send a fax - * Create and send a fax. Fax content may be supplied via one or more files or URLs of supported filetypes. + * Send a fax or multiple faxes + * Create and send one or multiple faxes. Fax content may be supplied via one or more files or URLs of supported filetypes. * This endpoint supports the following content types for the fax payload: * - Multipart/form-data * - Application/json - * We will however always return a fax object in the response in application json. - * If you supply a callbackUrl the callback will be sent as multipart/form-data with the content of the fax as an attachment to the body, *unless* you specify callbackContentType as application/json. + * We will however always return a fax array in the response in application json. + * If you supply a callbackUrl the callback will be sent as multipart/form-data with the content of the fax as an attachment to the body, *unless* you specify callbackUrlContentType as application/json. * #### file(s) * Files may be included in the POST request as multipart/form-data parts. * #### contentUrl @@ -177,7 +216,7 @@ export class FaxesApi extends FaxDomainApi { * Please note: If you are passing fax a secure URL (starting with 'https://'), make sure that your SSL certificate (including your intermediate cert, if you have one) is installed properly, valid, and up-to-date. * @param { SendFaxRequestData } data - The data to provide to the API call. */ - public async send(data: SendFaxRequestData): Promise { + public async send(data: SendFaxRequestData): Promise { this.client = this.getSinchClient(); const requestBody = data.sendFaxRequestBody; requestBody['headerText'] = requestBody['headerText'] !== undefined @@ -188,11 +227,12 @@ export class FaxesApi extends FaxDomainApi { ? requestBody['headerTimeZone'] : 'America/New_York'; requestBody['retryDelaySeconds'] = requestBody['retryDelaySeconds'] !== undefined ? requestBody['retryDelaySeconds'] : 60; - requestBody['callbackContentType'] = requestBody['callbackContentType'] !== undefined - ? requestBody['callbackContentType'] : 'multipart/form-data'; + requestBody['callbackUrlContentType'] = requestBody['callbackUrlContentType'] !== undefined + ? requestBody['callbackUrlContentType'] : 'multipart/form-data'; requestBody['imageConversionMethod'] = requestBody['imageConversionMethod'] !== undefined ? requestBody['imageConversionMethod'] : 'HALFTONE'; - const getParams = this.client.extractQueryParams(data, [] as never[]); + const getParams + = this.client.extractQueryParams(data, [] as never[]); const headers: { [key: string]: string | undefined } = { 'Accept': 'application/json', }; @@ -205,23 +245,61 @@ export class FaxesApi extends FaxDomainApi { headers['Content-Type'] = 'application/json'; body = JSON.stringify(data['sendFaxRequestBody']); } else { - const formParams: any = {}; - const requestData = data.sendFaxRequestBody as FaxRequestFormData; - if( requestData.to ) { formParams.to = requestData.to; } - if( requestData.file ) { formParams.file = requestData.file; } - if( requestData.from ) { formParams.from = requestData.from; } - if( requestData.contentUrl ) { formParams.contentUrl = requestData.contentUrl; } - if( requestData.headerText ) { formParams.headerText = requestData.headerText; } - if( requestData.headerPageNumbers ) { formParams.headerPageNumbers = requestData.headerPageNumbers.toString(); } - if( requestData.headerTimeZone ) { formParams.headerTimeZone = requestData.headerTimeZone; } - if( requestData.retryDelaySeconds ) { formParams.retryDelaySeconds = requestData.retryDelaySeconds; } - if( requestData.labels ) { formParams.labels = requestData.labels; } - if( requestData.callbackUrl ) { formParams.callbackUrl = requestData.callbackUrl; } - if( requestData.callbackContentType ) { formParams.callbackContentType = requestData.callbackContentType; } - if( requestData.imageConversionMethod ) {formParams.imageConversionMethod = requestData.imageConversionMethod;} - if( requestData.serviceId ) { formParams.serviceId = requestData.serviceId; } - if( requestData.maxRetries ) { formParams.maxRetries = requestData.maxRetries; } - body = this.client.processFormData(formParams, 'multipart/form-data'); + const formData = new FormData(); + let requestData; + if (Array.isArray(data.sendFaxRequestBody.to)) { + requestData = data.sendFaxRequestBody as MultipleFaxRequestFormData; + requestData.to.forEach((to) => formData.append('to', to)); + } else { + requestData = data.sendFaxRequestBody as SingleFaxRequestFormData; + formData.append('to', requestData.to); + } + if (requestData.filePaths) { + let attachmentPaths = requestData.filePaths; + if (typeof attachmentPaths === 'string') { + attachmentPaths = [String(requestData.filePaths)]; + } + attachmentPaths.forEach((filePath) => { + formData.append('file', fs.readFileSync(filePath), filePath.split('/').pop()); + }); + } + if (requestData.from) { + formData.append('from', requestData.from); + } + if (requestData.contentUrl) { + formData.append('contentUrl', requestData.contentUrl); + } + if (requestData.headerText) { + formData.append('headerText', requestData.headerText); + } + if (requestData.headerPageNumbers) { + formData.append('headerPageNumbers', requestData.headerPageNumbers.toString()); + } + if (requestData.headerTimeZone) { + formData.append('headerTimeZone', requestData.headerTimeZone); + } + if (requestData.retryDelaySeconds) { + formData.append('retryDelaySeconds', requestData.retryDelaySeconds); + } + if (requestData.labels) { + formData.append('labels', requestData.labels); + } + if (requestData.callbackUrl) { + formData.append('callbackUrl', requestData.callbackUrl); + } + if (requestData.callbackUrlContentType) { + formData.append('callbackUrlContentType', requestData.callbackUrlContentType); + } + if (requestData.imageConversionMethod) { + formData.append('imageConversionMethod', requestData.imageConversionMethod); + } + if (requestData.serviceId) { + formData.append('serviceId', requestData.serviceId); + } + if (requestData.maxRetries) { + formData.append('maxRetries', requestData.maxRetries); + } + body = formData; } const basePathUrl = `${this.client.apiClientOptions.hostname}/v3/projects/${this.client.apiClientOptions.projectId}/faxes`; @@ -229,12 +307,29 @@ export class FaxesApi extends FaxDomainApi { const requestOptions = await this.client.prepareOptions(basePathUrl, 'POST', getParams, headers, body || undefined); const url = this.client.prepareUrl(requestOptions.hostname, requestOptions.queryParams); - return this.client.processCall({ - url, - requestOptions, - apiName: this.apiName, - operationId: 'SendFax', - }); + if (Array.isArray(data.sendFaxRequestBody.to)) { + const responseData = await this.client.processCall({ + url, + requestOptions, + apiName: this.apiName, + operationId: 'SendMultipleFax', + }); + // When there is a single element in the 'to' array, the server will return a Fax, not a FaxList + if (Array.isArray((responseData as FaxesList).faxes)) { + return (responseData as FaxesList).faxes; + } else { + return [responseData as Fax]; + } + } else { + const responseData = await this.client.processCall({ + url, + requestOptions, + apiName: this.apiName, + operationId: 'SendFax', + }); + return [responseData]; + } + } } diff --git a/packages/fax/tests/models/v3/helper.test.ts b/packages/fax/tests/models/v3/helper.test.ts new file mode 100644 index 00000000..adbc2911 --- /dev/null +++ b/packages/fax/tests/models/v3/helper.test.ts @@ -0,0 +1,42 @@ +import { convertToSupportedFileType } from '../../../src/models'; + +describe('Fax models helpers', () => { + + describe('convertToSupportedFileType', () => { + it('should convert a file extension to a FaxBase64FileType', () => { + let convertedFileExtension = convertToSupportedFileType('doc'); + expect(convertedFileExtension).toBe('DOC'); + + convertedFileExtension = convertToSupportedFileType('docx'); + expect(convertedFileExtension).toBe('DOCX'); + + convertedFileExtension = convertToSupportedFileType('pdf'); + expect(convertedFileExtension).toBe('PDF'); + + convertedFileExtension = convertToSupportedFileType('tif'); + expect(convertedFileExtension).toBe('TIF'); + + convertedFileExtension = convertToSupportedFileType('jpg'); + expect(convertedFileExtension).toBe('JPG'); + + convertedFileExtension = convertToSupportedFileType('odt'); + expect(convertedFileExtension).toBe('ODT'); + + convertedFileExtension = convertToSupportedFileType('txt'); + expect(convertedFileExtension).toBe('TXT'); + + convertedFileExtension = convertToSupportedFileType('html'); + expect(convertedFileExtension).toBe('HTML'); + + convertedFileExtension = convertToSupportedFileType('png'); + expect(convertedFileExtension).toBe('PNG'); + + convertedFileExtension = convertToSupportedFileType(undefined); + expect(convertedFileExtension).toBeUndefined(); + + convertedFileExtension = convertToSupportedFileType('unknown'); + expect(convertedFileExtension).toBeUndefined(); + }); + }); + +}); diff --git a/packages/fax/tests/rest/v3/faxes/faxes-api.test.ts b/packages/fax/tests/rest/v3/faxes/faxes-api.test.ts index 8c3cb79d..f5fc2ac5 100644 --- a/packages/fax/tests/rest/v3/faxes/faxes-api.test.ts +++ b/packages/fax/tests/rest/v3/faxes/faxes-api.test.ts @@ -1,4 +1,4 @@ -import { FileBuffer, SinchClientParameters } from '@sinch/sdk-client'; +import { DateFormat, FileBuffer, SinchClientParameters } from '@sinch/sdk-client'; import { Fax, FaxesApi, @@ -75,10 +75,10 @@ describe('FaxesApi', () => { status: 'FAILURE', headerTimeZone: 'America/New_York', retryDelaySeconds: 60, - callbackContentType: 'multipart/form-data', + callbackUrlContentType: 'multipart/form-data', errorType: 'LINE_ERROR', - errorId: 89, - errorCode: 'The call dropped prematurely', + errorCode: 89, + errorMessage: 'The call dropped prematurely', projectId: 'projectId', serviceId: 'serviceId', maxRetries: 3, @@ -117,10 +117,10 @@ describe('FaxesApi', () => { status: 'FAILURE', headerTimeZone: 'America/New_York', retryDelaySeconds: 60, - callbackContentType: 'multipart/form-data', + callbackUrlContentType: 'multipart/form-data', errorType: 'LINE_ERROR', - errorId: 89, - errorCode: 'The call dropped prematurely', + errorCode: 89, + errorMessage: 'The call dropped prematurely', projectId: 'projectId', serviceId: 'serviceId', maxRetries: 3, @@ -151,34 +151,168 @@ describe('FaxesApi', () => { expect(response.data).toBeDefined(); expect(fixture.list).toHaveBeenCalledWith(requestData); }); + + it('should format a createTime parameter', () => { + const dateUndefined = undefined; + let formattedDateFilter = faxesApi.formatCreateTimeFilter(dateUndefined); + expect(formattedDateFilter).toBeUndefined(); + + const dateString = '2024-05-01'; + formattedDateFilter = faxesApi.formatCreateTimeFilter(dateString); + expect(formattedDateFilter).toBe('2024-05-01'); + + const dateWithSecondsString ='2024-05-01T13:00:00Z'; + formattedDateFilter = faxesApi.formatCreateTimeFilter(dateWithSecondsString); + expect(formattedDateFilter).toBe('2024-05-01'); + + const dateWithSeconds = new Date('2024-05-01T13:00:00Z'); + formattedDateFilter = faxesApi.formatCreateTimeFilter(dateWithSeconds); + expect(formattedDateFilter).toBe('2024-05-01'); + }); + + it('should format a datetime range filter', () => { + const dateTimeRangeUndefined = undefined; + let formattedDateTimeRangeFilter = faxesApi.formatCreateTimeRangeFilter(dateTimeRangeUndefined); + expect(formattedDateTimeRangeFilter).toBeUndefined(); + + const dateTimeRangeString = '2024-05-01'; + formattedDateTimeRangeFilter = faxesApi.formatCreateTimeRangeFilter(dateTimeRangeString); + expect(formattedDateTimeRangeFilter).toBe('2024-05-01'); + + const dateTimeRangeNoUnit: DateFormat = { + date: new Date('2024-05-01T13:15:30Z'), + }; + formattedDateTimeRangeFilter = faxesApi.formatCreateTimeRangeFilter(dateTimeRangeNoUnit); + expect(formattedDateTimeRangeFilter).toBe('2024-05-01T13:15:30Z'); + + const dateTimeRangeWithYear: DateFormat = { + date: new Date('2024-05-01T13:15:30Z'), + unit: 'year', + }; + formattedDateTimeRangeFilter = faxesApi.formatCreateTimeRangeFilter(dateTimeRangeWithYear); + expect(formattedDateTimeRangeFilter).toBe('2024'); + + const dateTimeRangeWithMonth: DateFormat = { + date: new Date('2024-05-01T13:15:30Z'), + unit: 'month', + }; + formattedDateTimeRangeFilter = faxesApi.formatCreateTimeRangeFilter(dateTimeRangeWithMonth); + expect(formattedDateTimeRangeFilter).toBe('2024-05'); + + const dateTimeRangeWithDay: DateFormat = { + date: new Date('2024-05-01T13:15:30Z'), + unit: 'day', + }; + formattedDateTimeRangeFilter = faxesApi.formatCreateTimeRangeFilter(dateTimeRangeWithDay); + expect(formattedDateTimeRangeFilter).toBe('2024-05-01'); + + const dateTimeRangeWithHours: DateFormat = { + date: new Date('2024-05-01T13:15:30Z'), + unit: 'hour', + }; + formattedDateTimeRangeFilter = faxesApi.formatCreateTimeRangeFilter(dateTimeRangeWithHours); + expect(formattedDateTimeRangeFilter).toBe('2024-05-01T13:00:00Z'); + + const dateTimeRangeWithMinutes: DateFormat = { + date: new Date('2024-05-01T13:15:30Z'), + unit: 'minute', + }; + formattedDateTimeRangeFilter = faxesApi.formatCreateTimeRangeFilter(dateTimeRangeWithMinutes); + expect(formattedDateTimeRangeFilter).toBe('2024-05-01T13:15:00Z'); + + const dateTimeRangeWithSeconds: DateFormat = { + date: new Date('2024-05-01T13:15:30Z'), + unit: 'second', + }; + formattedDateTimeRangeFilter = faxesApi.formatCreateTimeRangeFilter(dateTimeRangeWithSeconds); + expect(formattedDateTimeRangeFilter).toBe('2024-05-01T13:15:30Z'); + }); }); describe ('sendFax', () => { it('should make a POST request to create and send a fax', async () => { // Given - const requestData: Fax.SendFaxRequestData = { + const requestData: Fax.SendSingleFaxRequestData = { sendFaxRequestBody: { to: '+12015555555', }, }; - const expectedResponse: Fax.Fax = { - id: 'fax_id', - direction: 'OUTBOUND', - to: '+12015555555', - status: 'IN_PROGRESS', - headerTimeZone: 'America/New_York', - retryDelaySeconds: 60, - callbackContentType: 'multipart/form-data', - projectId: 'projectId', - serviceId: 'serviceId', - maxRetries: 3, - createTime: new Date('2024-02-27T12:28:09Z'), - headerPageNumbers: true, - contentUrl: [ - 'https://developers.sinch.com/fax/fax.pdf', - ], - imageConversionMethod: 'HALFTONE', + const expectedResponse: Fax.Fax[] = [ + { + id: 'fax_id', + direction: 'OUTBOUND', + to: '+12015555555', + status: 'IN_PROGRESS', + headerTimeZone: 'America/New_York', + retryDelaySeconds: 60, + callbackUrlContentType: 'multipart/form-data', + projectId: 'projectId', + serviceId: 'serviceId', + maxRetries: 3, + createTime: new Date('2024-02-27T12:28:09Z'), + headerPageNumbers: true, + contentUrl: [ + 'https://developers.sinch.com/fax/fax.pdf', + ], + imageConversionMethod: 'HALFTONE', + }, + ]; + + // When + fixture.send.mockResolvedValue(expectedResponse); + faxesApi.send = fixture.send; + const response = await faxesApi.send(requestData); + + // Then + expect(response).toEqual(expectedResponse); + expect(fixture.send).toHaveBeenCalledWith(requestData); + }); + + it('should make a POST request to create and send multiple faxes', async () => { + // Given + const requestData: Fax.SendMultipleFaxRequestData = { + sendFaxRequestBody: { + to: ['+12015555555', '+12015555566'], + }, }; + const expectedResponse: Fax.Fax[] = [ + { + id: 'fax_id', + direction: 'OUTBOUND', + to: '+12015555555', + status: 'IN_PROGRESS', + headerTimeZone: 'America/New_York', + retryDelaySeconds: 60, + callbackUrlContentType: 'multipart/form-data', + projectId: 'projectId', + serviceId: 'serviceId', + maxRetries: 3, + createTime: new Date('2024-02-27T12:28:09Z'), + headerPageNumbers: true, + contentUrl: [ + 'https://developers.sinch.com/fax/fax.pdf', + ], + imageConversionMethod: 'HALFTONE', + }, + { + id: 'fax_id', + direction: 'OUTBOUND', + to: '+12015555566', + status: 'IN_PROGRESS', + headerTimeZone: 'America/New_York', + retryDelaySeconds: 60, + callbackUrlContentType: 'multipart/form-data', + projectId: 'projectId', + serviceId: 'serviceId', + maxRetries: 3, + createTime: new Date('2024-02-27T12:28:09Z'), + headerPageNumbers: true, + contentUrl: [ + 'https://developers.sinch.com/fax/fax.pdf', + ], + imageConversionMethod: 'HALFTONE', + }, + ]; // When fixture.send.mockResolvedValue(expectedResponse); diff --git a/packages/sdk-client/src/api/api-client.ts b/packages/sdk-client/src/api/api-client.ts index d1780a75..5269aa50 100644 --- a/packages/sdk-client/src/api/api-client.ts +++ b/packages/sdk-client/src/api/api-client.ts @@ -1,7 +1,6 @@ import { RequestBody, RequestOptions } from '../plugins/core/request-plugin'; import { ApiClientOptions } from './api-client-options'; import { Headers } from 'node-fetch'; -import FormData = require('form-data'); export enum PaginationEnum { NONE, @@ -219,18 +218,6 @@ export class ApiClient { throw new Error('Abstract method must be implemented'); } - /** - * Receives an object containing key/value pairs - * Encodes this object to match application/x-www-urlencoded or multipart/form-data - * @abstract - * @param {any} _data - * @param {string} _type - */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - processFormData(_data: any, _type: string): FormData | string { - throw new Error('Abstract method must be implemented'); - } - } // ----- INTERNAL ------ \\ diff --git a/packages/sdk-client/src/client/api-fetch-client.ts b/packages/sdk-client/src/client/api-fetch-client.ts index 6663ec6d..ef14af0f 100644 --- a/packages/sdk-client/src/client/api-fetch-client.ts +++ b/packages/sdk-client/src/client/api-fetch-client.ts @@ -19,7 +19,6 @@ import { ResponseJSONParseError, } from '../api/api-errors'; import fetch, { Response, Headers } from 'node-fetch'; -import FormData = require('form-data'); import { buildErrorContext, manageExpiredToken, reviveDates } from './api-client-helpers'; import { buildPaginationContext, @@ -281,29 +280,4 @@ export class ApiFetchClient extends ApiClient { return fileName; } - /** @inheritDoc */ - public processFormData(data: any, type: string): FormData | string { - - let encodedData: FormData | string; - - if (type === 'multipart/form-data') { - const formData: FormData = new FormData(); - for (const key in data) { - if (Object.prototype.hasOwnProperty.call(data, key)) { - formData.append(key, data[key]); - } - } - encodedData = formData; - } else { - const formData: string[] = []; - for (const key in data) { - if (Object.prototype.hasOwnProperty.call(data, key)) { - formData.push(`${key}=${encodeURIComponent(data[key])}`); - } - } - encodedData = formData.join('&'); - } - - return encodedData; - } } diff --git a/packages/sdk-client/src/utils/date.ts b/packages/sdk-client/src/utils/date.ts new file mode 100644 index 00000000..c0d9ef0d --- /dev/null +++ b/packages/sdk-client/src/utils/date.ts @@ -0,0 +1,31 @@ +export interface DateFormat { + date: Date; + unit?: ChronoUnit; +} + +export type ChronoUnit = 'year' | 'month' | 'day' | 'hour' | 'minute' | 'second'; + +export const formatDate = (date: Date, unit?: ChronoUnit): string => { + const year = date.getUTCFullYear(); + const month = String(date.getUTCMonth() + 1).padStart(2, '0'); + const day = String(date.getUTCDate()).padStart(2, '0'); + const hours = String(date.getUTCHours()).padStart(2, '0'); + const minutes = String(date.getUTCMinutes()).padStart(2, '0'); + const seconds = String(date.getUTCSeconds()).padStart(2, '0'); + + switch (unit) { + case 'year': + return `${year}`; + case 'month': + return `${year}-${month}`; + case 'day': + return `${year}-${month}-${day}`; + case 'hour': + return `${year}-${month}-${day}T${hours}:00:00Z`; + case 'minute': + return `${year}-${month}-${day}T${hours}:${minutes}:00Z`; + case 'second': + default: + return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}Z`; + } +}; diff --git a/packages/sdk-client/src/utils/index.ts b/packages/sdk-client/src/utils/index.ts index 2ea7bab3..a6ab6611 100644 --- a/packages/sdk-client/src/utils/index.ts +++ b/packages/sdk-client/src/utils/index.ts @@ -1,2 +1,3 @@ export * from './authorization.helper'; +export * from './date'; export * from './text-to-hex'; From 8126650740f76872a6245fc6baec5ad799177df4 Mon Sep 17 00:00:00 2001 From: Antoine SEIN <142824551+asein-sinch@users.noreply.github.com> Date: Tue, 4 Jun 2024 11:22:39 +0200 Subject: [PATCH 03/54] DEVEXP-446: Refactor models in Verification SDK (#96) --- .../services/verification-event.service.ts | 2 +- ...t-request-event-response-callout-speech.ts | 8 ---- .../index.ts | 1 - .../callout-request-event-response-callout.ts | 9 ---- .../index.ts | 1 - .../callout-request-event-response.ts | 10 ----- .../callout-request-event-response/index.ts | 1 - packages/verification/src/models/v1/enums.ts | 8 ---- .../src/models/v1/identity/identity.ts | 5 +-- packages/verification/src/models/v1/index.ts | 42 ++++++++++--------- .../callout-request-event-response.ts | 23 ++++++++++ .../callout-request-event-response/index.ts | 5 +++ .../flashcall-request-event-response.ts | 10 ++--- .../flashcall-request-event-response/index.ts | 0 .../src/models/v1/mod-callbacks/index.ts | 10 +++++ .../sms-request-event-response/index.ts | 1 + .../sms-request-event-response.ts | 12 +++--- .../index.ts | 0 .../verification-request-event-response.ts | 4 +- .../verification-request-event/index.ts | 0 .../verification-request-event.ts | 4 +- .../index.ts | 0 .../verification-result-event-response.ts | 4 +- .../verification-result-event/index.ts | 0 .../verification-result-event.ts | 4 +- .../verification-status-request-data.ts | 2 +- .../v1/sms-request-event-response/index.ts | 1 - .../sms-verification-report-response/index.ts | 2 +- .../sms-verification-report-response.ts | 2 +- .../sms-verification-status-response.ts | 6 +-- .../verification-price-sms.ts | 2 +- .../index.ts | 3 -- .../verification-report-response-price.ts | 23 ---------- .../verification-status-response.ts | 4 +- .../verifications-api.jest.fixture.ts | 6 +-- .../v1/verifications/verifications-api.ts | 16 +++---- .../verification-status-api.test.ts | 2 +- .../verifications/verifications-api.test.ts | 4 +- 38 files changed, 103 insertions(+), 134 deletions(-) delete mode 100644 packages/verification/src/models/v1/callout-request-event-response-callout-speech/callout-request-event-response-callout-speech.ts delete mode 100644 packages/verification/src/models/v1/callout-request-event-response-callout-speech/index.ts delete mode 100644 packages/verification/src/models/v1/callout-request-event-response-callout/callout-request-event-response-callout.ts delete mode 100644 packages/verification/src/models/v1/callout-request-event-response-callout/index.ts delete mode 100644 packages/verification/src/models/v1/callout-request-event-response/callout-request-event-response.ts delete mode 100644 packages/verification/src/models/v1/callout-request-event-response/index.ts create mode 100644 packages/verification/src/models/v1/mod-callbacks/callout-request-event-response/callout-request-event-response.ts create mode 100644 packages/verification/src/models/v1/mod-callbacks/callout-request-event-response/index.ts rename packages/verification/src/models/v1/{ => mod-callbacks}/flashcall-request-event-response/flashcall-request-event-response.ts (86%) rename packages/verification/src/models/v1/{ => mod-callbacks}/flashcall-request-event-response/index.ts (100%) create mode 100644 packages/verification/src/models/v1/mod-callbacks/index.ts create mode 100644 packages/verification/src/models/v1/mod-callbacks/sms-request-event-response/index.ts rename packages/verification/src/models/v1/{ => mod-callbacks}/sms-request-event-response/sms-request-event-response.ts (71%) rename packages/verification/src/models/v1/{ => mod-callbacks}/verification-request-event-response/index.ts (100%) rename packages/verification/src/models/v1/{ => mod-callbacks}/verification-request-event-response/verification-request-event-response.ts (66%) rename packages/verification/src/models/v1/{ => mod-callbacks}/verification-request-event/index.ts (100%) rename packages/verification/src/models/v1/{ => mod-callbacks}/verification-request-event/verification-request-event.ts (90%) rename packages/verification/src/models/v1/{ => mod-callbacks}/verification-result-event-response/index.ts (100%) rename packages/verification/src/models/v1/{ => mod-callbacks}/verification-result-event-response/verification-result-event-response.ts (66%) rename packages/verification/src/models/v1/{ => mod-callbacks}/verification-result-event/index.ts (100%) rename packages/verification/src/models/v1/{ => mod-callbacks}/verification-result-event/verification-result-event.ts (94%) delete mode 100644 packages/verification/src/models/v1/sms-request-event-response/index.ts delete mode 100644 packages/verification/src/models/v1/verification-report-response-price/index.ts delete mode 100644 packages/verification/src/models/v1/verification-report-response-price/verification-report-response-price.ts diff --git a/examples/webhooks/src/services/verification-event.service.ts b/examples/webhooks/src/services/verification-event.service.ts index ca8ce9d7..bb83f779 100644 --- a/examples/webhooks/src/services/verification-event.service.ts +++ b/examples/webhooks/src/services/verification-event.service.ts @@ -23,7 +23,7 @@ export class VerificationEventService { private handleVerificationRequestEvent(event: Verification.VerificationRequestEvent, res: Response) { switch (event.method) { case 'sms': - const smsRequestEventResponse: Verification.SMSRequestEventResponse = { + const smsRequestEventResponse: Verification.SmsRequestEventResponse = { action: 'allow', sms: { code: '123456' diff --git a/packages/verification/src/models/v1/callout-request-event-response-callout-speech/callout-request-event-response-callout-speech.ts b/packages/verification/src/models/v1/callout-request-event-response-callout-speech/callout-request-event-response-callout-speech.ts deleted file mode 100644 index 7d8f290a..00000000 --- a/packages/verification/src/models/v1/callout-request-event-response-callout-speech/callout-request-event-response-callout-speech.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * An object defining various properties for the text-to-speech message. - */ -export interface CalloutRequestEventResponseCalloutSpeech { - - /** Indicates the language that should be used for the text-to-speech message. Currently, only `en-US` is supported. */ - locale?: string; -} diff --git a/packages/verification/src/models/v1/callout-request-event-response-callout-speech/index.ts b/packages/verification/src/models/v1/callout-request-event-response-callout-speech/index.ts deleted file mode 100644 index 6141b6a1..00000000 --- a/packages/verification/src/models/v1/callout-request-event-response-callout-speech/index.ts +++ /dev/null @@ -1 +0,0 @@ -export type { CalloutRequestEventResponseCalloutSpeech } from './callout-request-event-response-callout-speech'; diff --git a/packages/verification/src/models/v1/callout-request-event-response-callout/callout-request-event-response-callout.ts b/packages/verification/src/models/v1/callout-request-event-response-callout/callout-request-event-response-callout.ts deleted file mode 100644 index e4f36dd1..00000000 --- a/packages/verification/src/models/v1/callout-request-event-response-callout/callout-request-event-response-callout.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { CalloutRequestEventResponseCalloutSpeech } from '../callout-request-event-response-callout-speech'; - -export interface CalloutRequestEventResponseCallout { - - /** The Phone Call PIN that should be entered by the user. Sinch servers automatically generate PIN codes for Phone Call verification. If you want to set your own code, you can specify it in the response to the Verification Request Event. */ - code?: string; - /** @see CalloutRequestEventResponseCalloutSpeech */ - speech?: CalloutRequestEventResponseCalloutSpeech; -} diff --git a/packages/verification/src/models/v1/callout-request-event-response-callout/index.ts b/packages/verification/src/models/v1/callout-request-event-response-callout/index.ts deleted file mode 100644 index 490198a2..00000000 --- a/packages/verification/src/models/v1/callout-request-event-response-callout/index.ts +++ /dev/null @@ -1 +0,0 @@ -export type { CalloutRequestEventResponseCallout } from './callout-request-event-response-callout'; diff --git a/packages/verification/src/models/v1/callout-request-event-response/callout-request-event-response.ts b/packages/verification/src/models/v1/callout-request-event-response/callout-request-event-response.ts deleted file mode 100644 index 9b6865b1..00000000 --- a/packages/verification/src/models/v1/callout-request-event-response/callout-request-event-response.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { CalloutRequestEventResponseCallout } from '../callout-request-event-response-callout'; -import { ActionEnum } from '../enums'; - -export interface CalloutRequestEventResponse { - - /** Determines whether the verification can be executed. */ - action?: ActionEnum; - /** @see CalloutRequestEventResponseCallout */ - callout?: CalloutRequestEventResponseCallout; -} diff --git a/packages/verification/src/models/v1/callout-request-event-response/index.ts b/packages/verification/src/models/v1/callout-request-event-response/index.ts deleted file mode 100644 index 0737de65..00000000 --- a/packages/verification/src/models/v1/callout-request-event-response/index.ts +++ /dev/null @@ -1 +0,0 @@ -export type { CalloutRequestEventResponse } from './callout-request-event-response'; diff --git a/packages/verification/src/models/v1/enums.ts b/packages/verification/src/models/v1/enums.ts index 5757da91..0eaefb88 100644 --- a/packages/verification/src/models/v1/enums.ts +++ b/packages/verification/src/models/v1/enums.ts @@ -1,11 +1,3 @@ -export type { TypeEnum as IdentityTypeEnum } from './identity/identity'; -export type { - MethodEnum as VerificationRequestEventMethodEnum, -} from './verification-request-event/verification-request-event'; -export type { - MethodEnum as VerificationResultEventMethodEnum, -} from './verification-result-event/verification-result-event'; - export type ActionEnum = 'allow' | 'deny'; export type VerificationStatusEnum = 'PENDING' diff --git a/packages/verification/src/models/v1/identity/identity.ts b/packages/verification/src/models/v1/identity/identity.ts index f3f12c37..36bbcebe 100644 --- a/packages/verification/src/models/v1/identity/identity.ts +++ b/packages/verification/src/models/v1/identity/identity.ts @@ -4,12 +4,9 @@ export interface Identity { /** Currently only `number` type is supported. */ - type: TypeEnum; + type: 'number'; /** For type `number` use an [E.164](https://community.sinch.com/t5/Glossary/E-164/ta-p/7537)-compatible phone number. */ endpoint: string; /** */ verified?: boolean; } - -export type TypeEnum = 'number'; - diff --git a/packages/verification/src/models/v1/index.ts b/packages/verification/src/models/v1/index.ts index 1892ec81..eeb783e2 100644 --- a/packages/verification/src/models/v1/index.ts +++ b/packages/verification/src/models/v1/index.ts @@ -1,32 +1,34 @@ -export * from './callout-request-event-response'; -export * from './callout-request-event-response-callout'; -export * from './callout-request-event-response-callout-speech'; +// Models associated to API requests +export * from './requests'; +export * from './start-verification-request'; +export * from './verification-report-request'; +// Models associated to SMS verification workflow +export * from './start-sms-verification-response'; +export * from './sms-verification-report-response'; +export * from './sms-verification-status-response'; +// Models associated to Callout verification workflow +export * from './start-callout-verification-response'; export * from './callout-verification-report-response'; export * from './callout-verification-status-response'; -export * from './flashcall-request-event-response'; +// Models associated to Flashcall verification workflow +export * from './start-flashcall-verification-response'; export * from './flashcall-verification-report-response'; export * from './flashcall-verification-status-response'; -export * from './verification-report-response-price'; +// Models associated to Seamless verification workflow +export * from './start-seamless-verification-response'; +// Wrapper for the various types of Verification Status Response +export * from './verification-status-response'; +// Models associated to callback events +export * from './mod-callbacks'; +// Common models export * from './identity'; -export * from './start-verification-request'; export * from './links-object'; export * from './price'; -export * from './start-flashcall-verification-response'; -export * from './start-callout-verification-response'; -export * from './start-seamless-verification-response'; -export * from './start-sms-verification-response'; -export * from './sms-request-event-response'; -export * from './sms-verification-report-response'; -export * from './sms-verification-status-response'; export * from './verification-price-call'; export * from './verification-price-sms'; -export * from './verification-report-request'; -export * from './verification-status-response'; -export * from './verification-request-event'; -export * from './verification-request-event-response'; -export * from './verification-result-event'; -export * from './verification-result-event-response'; +// Error model export * from './verification-error'; +// Enums export * from './enums'; +// Helper methods export * from './helper'; -export * from './requests'; diff --git a/packages/verification/src/models/v1/mod-callbacks/callout-request-event-response/callout-request-event-response.ts b/packages/verification/src/models/v1/mod-callbacks/callout-request-event-response/callout-request-event-response.ts new file mode 100644 index 00000000..0ad3b41d --- /dev/null +++ b/packages/verification/src/models/v1/mod-callbacks/callout-request-event-response/callout-request-event-response.ts @@ -0,0 +1,23 @@ +import { ActionEnum } from '../../enums'; + +export interface CalloutRequestEventResponse { + /** Determines whether the verification can be executed. */ + action?: ActionEnum; + /** @see CalloutProperties */ + callout?: CalloutProperties; +} + +export interface CalloutProperties { + /** The Phone Call PIN that should be entered by the user. Sinch servers automatically generate PIN codes for Phone Call verification. If you want to set your own code, you can specify it in the response to the Verification Request Event. */ + code?: string; + /** @see SpeechProperties */ + speech?: SpeechProperties; +} + +/** + * An object defining various properties for the text-to-speech message. + */ +export interface SpeechProperties { + /** Indicates the language that should be used for the text-to-speech message. Currently, only `en-US` is supported. */ + locale?: string; +} diff --git a/packages/verification/src/models/v1/mod-callbacks/callout-request-event-response/index.ts b/packages/verification/src/models/v1/mod-callbacks/callout-request-event-response/index.ts new file mode 100644 index 00000000..edc48f1c --- /dev/null +++ b/packages/verification/src/models/v1/mod-callbacks/callout-request-event-response/index.ts @@ -0,0 +1,5 @@ +export type { + CalloutRequestEventResponse, + CalloutProperties, + SpeechProperties, +} from './callout-request-event-response'; diff --git a/packages/verification/src/models/v1/flashcall-request-event-response/flashcall-request-event-response.ts b/packages/verification/src/models/v1/mod-callbacks/flashcall-request-event-response/flashcall-request-event-response.ts similarity index 86% rename from packages/verification/src/models/v1/flashcall-request-event-response/flashcall-request-event-response.ts rename to packages/verification/src/models/v1/mod-callbacks/flashcall-request-event-response/flashcall-request-event-response.ts index 8d266be6..da9356da 100644 --- a/packages/verification/src/models/v1/flashcall-request-event-response/flashcall-request-event-response.ts +++ b/packages/verification/src/models/v1/mod-callbacks/flashcall-request-event-response/flashcall-request-event-response.ts @@ -1,15 +1,13 @@ -import { ActionEnum } from '../enums'; +import { ActionEnum } from '../../enums'; export interface FlashCallRequestEventResponse { - /** Determines whether the verification can be executed. */ action?: ActionEnum; - /** @see FlashCallContent */ - flashCall?: FlashCallContent; + /** @see FlashCallProperties */ + flashCall?: FlashCallProperties; } -interface FlashCallContent { - +interface FlashCallProperties { /** The phone number that will be displayed to the user when the flashcall is received on the user\'s phone. By default, the Sinch dashboard will randomly select the CLI that will be displayed during a flashcall from a pool of numbers. If you want to set your own CLI, you can specify it in the response to the Verification Request Event. */ cli?: string; /** The maximum time that a flashcall verification will be active and can be completed. If the phone number hasn\'t been verified successfully during this time, then the verification request will fail. By default, the Sinch dashboard will automatically optimize dial time out during a flashcall. If you want to set your own dial time out for the flashcall, you can specify it in the response to the Verification Request Event. */ diff --git a/packages/verification/src/models/v1/flashcall-request-event-response/index.ts b/packages/verification/src/models/v1/mod-callbacks/flashcall-request-event-response/index.ts similarity index 100% rename from packages/verification/src/models/v1/flashcall-request-event-response/index.ts rename to packages/verification/src/models/v1/mod-callbacks/flashcall-request-event-response/index.ts diff --git a/packages/verification/src/models/v1/mod-callbacks/index.ts b/packages/verification/src/models/v1/mod-callbacks/index.ts new file mode 100644 index 00000000..08d3d4ac --- /dev/null +++ b/packages/verification/src/models/v1/mod-callbacks/index.ts @@ -0,0 +1,10 @@ +// 'Verification Request Event' received from Sinch server +export * from './verification-request-event'; +// Response to send to Sinch server for a 'Verification Request Event' +export * from './callout-request-event-response'; +export * from './flashcall-request-event-response'; +export * from './sms-request-event-response'; +export * from './verification-request-event-response'; +// 'Verification Result Event' received from Sinch server (no response data needed) +export * from './verification-result-event'; +export * from './verification-result-event-response'; diff --git a/packages/verification/src/models/v1/mod-callbacks/sms-request-event-response/index.ts b/packages/verification/src/models/v1/mod-callbacks/sms-request-event-response/index.ts new file mode 100644 index 00000000..0ae8ee69 --- /dev/null +++ b/packages/verification/src/models/v1/mod-callbacks/sms-request-event-response/index.ts @@ -0,0 +1 @@ +export type { SmsRequestEventResponse } from './sms-request-event-response'; diff --git a/packages/verification/src/models/v1/sms-request-event-response/sms-request-event-response.ts b/packages/verification/src/models/v1/mod-callbacks/sms-request-event-response/sms-request-event-response.ts similarity index 71% rename from packages/verification/src/models/v1/sms-request-event-response/sms-request-event-response.ts rename to packages/verification/src/models/v1/mod-callbacks/sms-request-event-response/sms-request-event-response.ts index 0d1978bc..f025c0ea 100644 --- a/packages/verification/src/models/v1/sms-request-event-response/sms-request-event-response.ts +++ b/packages/verification/src/models/v1/mod-callbacks/sms-request-event-response/sms-request-event-response.ts @@ -1,15 +1,13 @@ -import { ActionEnum } from '../enums'; - -export interface SMSRequestEventResponse { +import { ActionEnum } from '../../enums'; +export interface SmsRequestEventResponse { /** Determines whether the verification can be executed. */ action?: ActionEnum; - /** @see SmsContent */ - sms?: SmsContent; + /** @see SmsProperties */ + sms?: SmsProperties; } -interface SmsContent { - +interface SmsProperties { /** The SMS PIN that should be used. By default, the Sinch dashboard will automatically generate PIN codes for SMS verification. If you want to set your own PIN, you can specify it in the response to the Verification Request Event. */ code?: string; /** List of strings */ diff --git a/packages/verification/src/models/v1/verification-request-event-response/index.ts b/packages/verification/src/models/v1/mod-callbacks/verification-request-event-response/index.ts similarity index 100% rename from packages/verification/src/models/v1/verification-request-event-response/index.ts rename to packages/verification/src/models/v1/mod-callbacks/verification-request-event-response/index.ts diff --git a/packages/verification/src/models/v1/verification-request-event-response/verification-request-event-response.ts b/packages/verification/src/models/v1/mod-callbacks/verification-request-event-response/verification-request-event-response.ts similarity index 66% rename from packages/verification/src/models/v1/verification-request-event-response/verification-request-event-response.ts rename to packages/verification/src/models/v1/mod-callbacks/verification-request-event-response/verification-request-event-response.ts index f6aa5e43..cd13a1d2 100644 --- a/packages/verification/src/models/v1/verification-request-event-response/verification-request-event-response.ts +++ b/packages/verification/src/models/v1/mod-callbacks/verification-request-event-response/verification-request-event-response.ts @@ -1,7 +1,7 @@ -import { SMSRequestEventResponse } from '../sms-request-event-response'; +import { SmsRequestEventResponse } from '../sms-request-event-response'; import { FlashCallRequestEventResponse } from '../flashcall-request-event-response'; import { CalloutRequestEventResponse } from '../callout-request-event-response'; -export type VerificationRequestEventResponse = SMSRequestEventResponse +export type VerificationRequestEventResponse = SmsRequestEventResponse | FlashCallRequestEventResponse | CalloutRequestEventResponse; diff --git a/packages/verification/src/models/v1/verification-request-event/index.ts b/packages/verification/src/models/v1/mod-callbacks/verification-request-event/index.ts similarity index 100% rename from packages/verification/src/models/v1/verification-request-event/index.ts rename to packages/verification/src/models/v1/mod-callbacks/verification-request-event/index.ts diff --git a/packages/verification/src/models/v1/verification-request-event/verification-request-event.ts b/packages/verification/src/models/v1/mod-callbacks/verification-request-event/verification-request-event.ts similarity index 90% rename from packages/verification/src/models/v1/verification-request-event/verification-request-event.ts rename to packages/verification/src/models/v1/mod-callbacks/verification-request-event/verification-request-event.ts index da0dc1b0..198ec43f 100644 --- a/packages/verification/src/models/v1/verification-request-event/verification-request-event.ts +++ b/packages/verification/src/models/v1/mod-callbacks/verification-request-event/verification-request-event.ts @@ -1,5 +1,5 @@ -import { Identity } from '../identity'; -import { Price } from '../price'; +import { Identity } from '../../identity'; +import { Price } from '../../price'; export interface VerificationRequestEvent { diff --git a/packages/verification/src/models/v1/verification-result-event-response/index.ts b/packages/verification/src/models/v1/mod-callbacks/verification-result-event-response/index.ts similarity index 100% rename from packages/verification/src/models/v1/verification-result-event-response/index.ts rename to packages/verification/src/models/v1/mod-callbacks/verification-result-event-response/index.ts diff --git a/packages/verification/src/models/v1/verification-result-event-response/verification-result-event-response.ts b/packages/verification/src/models/v1/mod-callbacks/verification-result-event-response/verification-result-event-response.ts similarity index 66% rename from packages/verification/src/models/v1/verification-result-event-response/verification-result-event-response.ts rename to packages/verification/src/models/v1/mod-callbacks/verification-result-event-response/verification-result-event-response.ts index 10f825df..ba2ba741 100644 --- a/packages/verification/src/models/v1/verification-result-event-response/verification-result-event-response.ts +++ b/packages/verification/src/models/v1/mod-callbacks/verification-result-event-response/verification-result-event-response.ts @@ -1,7 +1,7 @@ -import { SMSRequestEventResponse } from '../sms-request-event-response'; +import { SmsRequestEventResponse } from '../sms-request-event-response'; import { FlashCallRequestEventResponse } from '../flashcall-request-event-response'; import { CalloutRequestEventResponse } from '../callout-request-event-response'; -export type VerificationResultEventResponse = SMSRequestEventResponse +export type VerificationResultEventResponse = SmsRequestEventResponse | FlashCallRequestEventResponse | CalloutRequestEventResponse; diff --git a/packages/verification/src/models/v1/verification-result-event/index.ts b/packages/verification/src/models/v1/mod-callbacks/verification-result-event/index.ts similarity index 100% rename from packages/verification/src/models/v1/verification-result-event/index.ts rename to packages/verification/src/models/v1/mod-callbacks/verification-result-event/index.ts diff --git a/packages/verification/src/models/v1/verification-result-event/verification-result-event.ts b/packages/verification/src/models/v1/mod-callbacks/verification-result-event/verification-result-event.ts similarity index 94% rename from packages/verification/src/models/v1/verification-result-event/verification-result-event.ts rename to packages/verification/src/models/v1/mod-callbacks/verification-result-event/verification-result-event.ts index 156a7474..a3df263e 100644 --- a/packages/verification/src/models/v1/verification-result-event/verification-result-event.ts +++ b/packages/verification/src/models/v1/mod-callbacks/verification-result-event/verification-result-event.ts @@ -1,5 +1,5 @@ -import { Identity } from '../identity'; -import { ReasonEnum, SourceEnum, VerificationStatusEnum } from '../enums'; +import { Identity } from '../../identity'; +import { ReasonEnum, SourceEnum, VerificationStatusEnum } from '../../enums'; export interface VerificationResultEvent { diff --git a/packages/verification/src/models/v1/requests/verification-status/verification-status-request-data.ts b/packages/verification/src/models/v1/requests/verification-status/verification-status-request-data.ts index 69c09aed..7f22e023 100644 --- a/packages/verification/src/models/v1/requests/verification-status/verification-status-request-data.ts +++ b/packages/verification/src/models/v1/requests/verification-status/verification-status-request-data.ts @@ -6,7 +6,7 @@ export interface VerificationStatusByIdentityRequestData { /** For type `number` use a [E.164](https://community.sinch.com/t5/Glossary/E-164/ta-p/7537)-compatible phone number. */ 'endpoint': string; /** The method of the verification. */ - 'method': 'sms' | 'callout' | 'flashCall'; + 'method': 'sms' | 'callout' | 'flashcall'; } export interface VerificationStatusByReferenceRequestData { /** The custom reference of the verification. */ diff --git a/packages/verification/src/models/v1/sms-request-event-response/index.ts b/packages/verification/src/models/v1/sms-request-event-response/index.ts deleted file mode 100644 index 6e98db9a..00000000 --- a/packages/verification/src/models/v1/sms-request-event-response/index.ts +++ /dev/null @@ -1 +0,0 @@ -export type { SMSRequestEventResponse } from './sms-request-event-response'; diff --git a/packages/verification/src/models/v1/sms-verification-report-response/index.ts b/packages/verification/src/models/v1/sms-verification-report-response/index.ts index d213363f..0eb2b251 100644 --- a/packages/verification/src/models/v1/sms-verification-report-response/index.ts +++ b/packages/verification/src/models/v1/sms-verification-report-response/index.ts @@ -1 +1 @@ -export type { SMSVerificationReportResponse } from './sms-verification-report-response'; +export type { SmsVerificationReportResponse } from './sms-verification-report-response'; diff --git a/packages/verification/src/models/v1/sms-verification-report-response/sms-verification-report-response.ts b/packages/verification/src/models/v1/sms-verification-report-response/sms-verification-report-response.ts index 420c4ccc..094644ea 100644 --- a/packages/verification/src/models/v1/sms-verification-report-response/sms-verification-report-response.ts +++ b/packages/verification/src/models/v1/sms-verification-report-response/sms-verification-report-response.ts @@ -1,7 +1,7 @@ import { ReasonEnum, SourceEnum, VerificationStatusEnum } from '../enums'; import { Identity } from '../identity'; -export interface SMSVerificationReportResponse { +export interface SmsVerificationReportResponse { /** The unique ID of the verification request. */ id?: string; diff --git a/packages/verification/src/models/v1/sms-verification-status-response/sms-verification-status-response.ts b/packages/verification/src/models/v1/sms-verification-status-response/sms-verification-status-response.ts index 9718eda7..ec77f15c 100644 --- a/packages/verification/src/models/v1/sms-verification-status-response/sms-verification-status-response.ts +++ b/packages/verification/src/models/v1/sms-verification-status-response/sms-verification-status-response.ts @@ -1,8 +1,8 @@ import { ReasonEnum, SourceEnum, VerificationStatusEnum } from '../enums'; import { Identity } from '../identity'; -import { VerificationPriceSMS } from '../verification-price-sms'; +import { VerificationPriceSms } from '../verification-price-sms'; -export interface SMSVerificationStatusResponse { +export interface SmsVerificationStatusResponse { /** The unique ID of the verification request. */ id?: string; @@ -15,7 +15,7 @@ export interface SMSVerificationStatusResponse { /** The reference ID that was optionally passed together with the verification request. */ reference?: string; /** Prices associated with this verification */ - price?: VerificationPriceSMS; + price?: VerificationPriceSms; /** @see Identity */ identity?: Identity; /** The ID of the country to which the verification was sent. */ diff --git a/packages/verification/src/models/v1/verification-price-sms/verification-price-sms.ts b/packages/verification/src/models/v1/verification-price-sms/verification-price-sms.ts index 8e75a175..cfcc9462 100644 --- a/packages/verification/src/models/v1/verification-price-sms/verification-price-sms.ts +++ b/packages/verification/src/models/v1/verification-price-sms/verification-price-sms.ts @@ -3,7 +3,7 @@ import { Price } from '../price'; /** * Prices associated with this verification */ -export interface VerificationPriceSMS { +export interface VerificationPriceSms { /** The maximum price charged for this verification process. This property will appear in the body of the response with a delay. It will become visible only when the verification status is other than PENDING. */ verificationPrice?: Price; diff --git a/packages/verification/src/models/v1/verification-report-response-price/index.ts b/packages/verification/src/models/v1/verification-report-response-price/index.ts deleted file mode 100644 index 5eff275a..00000000 --- a/packages/verification/src/models/v1/verification-report-response-price/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export type { - VerificationReportResponsePrice, -} from './verification-report-response-price'; diff --git a/packages/verification/src/models/v1/verification-report-response-price/verification-report-response-price.ts b/packages/verification/src/models/v1/verification-report-response-price/verification-report-response-price.ts deleted file mode 100644 index b319daa6..00000000 --- a/packages/verification/src/models/v1/verification-report-response-price/verification-report-response-price.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Price } from '../price'; - -/** - * Prices associated with this verification - */ -export interface VerificationReportResponsePrice { - - /** The maximum price charged for this verification process. This property will appear in the body of the response with a delay. It will become visible only when the verification status is other than PENDING. */ - verificationPrice?: Price; - /** The maximum cost of the call made during this verification process. Present only when termination debiting is enabled (disabled by default). This property will appear in the body of the response with a delay. It will become visible only after the call is completed, when its cost is known to Sinch. */ - terminationPrice?: Price; - /** The time of the call for which the fee was charged. Present only when termination debiting is enabled (disabled by default). Depending on the type of rounding used, the value is the actual call time rounded to the nearest second, minute or other value. */ - billableDuration?: number; -} - -/** - * Prices associated with this verification - */ -export interface SMSVerificationReportResponsePrice { - - /** The maximum price charged for this verification process. This property will appear in the body of the response with a delay. It will become visible only when the verification status is other than PENDING. */ - verificationPrice?: Price; -} diff --git a/packages/verification/src/models/v1/verification-status-response/verification-status-response.ts b/packages/verification/src/models/v1/verification-status-response/verification-status-response.ts index 8f4fd76a..80d3e3a2 100644 --- a/packages/verification/src/models/v1/verification-status-response/verification-status-response.ts +++ b/packages/verification/src/models/v1/verification-status-response/verification-status-response.ts @@ -1,7 +1,7 @@ -import { SMSVerificationStatusResponse } from '../sms-verification-status-response'; +import { SmsVerificationStatusResponse } from '../sms-verification-status-response'; import { FlashCallVerificationStatusResponse } from '../flashcall-verification-status-response'; import { CalloutVerificationStatusResponse } from '../callout-verification-status-response'; -export type VerificationStatusResponse = SMSVerificationStatusResponse +export type VerificationStatusResponse = SmsVerificationStatusResponse | FlashCallVerificationStatusResponse | CalloutVerificationStatusResponse; diff --git a/packages/verification/src/rest/v1/verifications/verifications-api.jest.fixture.ts b/packages/verification/src/rest/v1/verifications/verifications-api.jest.fixture.ts index 7a2c0ec8..0343520e 100644 --- a/packages/verification/src/rest/v1/verifications/verifications-api.jest.fixture.ts +++ b/packages/verification/src/rest/v1/verifications/verifications-api.jest.fixture.ts @@ -6,7 +6,7 @@ import { StartFlashCallVerificationResponse, FlashCallVerificationReportResponse, StartSmsVerificationResponse, - SMSVerificationReportResponse, + SmsVerificationReportResponse, StartSmsVerificationRequestData, StartFlashCallVerificationRequestData, StartCalloutVerificationRequestData, @@ -25,7 +25,7 @@ export class VerificationsApiFixture implements Partial, + SmsVerificationReportResponse>, [ReportSmsVerificationByIdRequestData]> = jest.fn(); /** * Fixture associated to function reportFlashCallById @@ -44,7 +44,7 @@ export class VerificationsApiFixture implements Partial, + SmsVerificationReportResponse>, [ReportSmsVerificationByIdentityRequestData]> = jest.fn(); /** * Fixture associated to function reportFlashCallByIdentity diff --git a/packages/verification/src/rest/v1/verifications/verifications-api.ts b/packages/verification/src/rest/v1/verifications/verifications-api.ts index 8dcc5186..0a38a183 100644 --- a/packages/verification/src/rest/v1/verifications/verifications-api.ts +++ b/packages/verification/src/rest/v1/verifications/verifications-api.ts @@ -5,7 +5,7 @@ import { StartFlashCallVerificationResponse, FlashCallVerificationReportResponse, StartSmsVerificationResponse, - SMSVerificationReportResponse, + SmsVerificationReportResponse, ReportSmsVerificationByIdRequestData, ReportFlashCallVerificationByIdRequestData, ReportCalloutVerificationByIdRequestData, @@ -40,7 +40,7 @@ export class VerificationsApi extends VerificationDomainApi { * Report the received verification code to verify it, using the Verification ID of the Verification request. * @param { ReportSmsVerificationByIdRequestData } data - The data to provide to the API call. */ - public async reportSmsById(data: ReportSmsVerificationByIdRequestData): Promise { + public async reportSmsById(data: ReportSmsVerificationByIdRequestData): Promise { this.client = this.getSinchClient(); (data.reportSmsVerificationByIdRequestBody as any).method = 'sms'; const getParams = this.client.extractQueryParams(data, [] as never[]); @@ -59,7 +59,7 @@ export class VerificationsApi extends VerificationDomainApi { = await this.client.prepareOptions(basePathUrl, 'PUT', getParams, headers, body || undefined, path); const url = this.client.prepareUrl(requestOptions.hostname, requestOptions.queryParams); - return this.client.processCall({ + return this.client.processCall({ url, requestOptions, apiName: this.apiName, @@ -76,7 +76,7 @@ export class VerificationsApi extends VerificationDomainApi { data: ReportFlashCallVerificationByIdRequestData, ): Promise { this.client = this.getSinchClient(); - (data.reportFlashCallVerificationByIdRequestBody as any).method = 'flashCall'; + (data.reportFlashCallVerificationByIdRequestBody as any).method = 'flashcall'; const getParams = this.client.extractQueryParams( data, [] as never[]); @@ -144,7 +144,7 @@ export class VerificationsApi extends VerificationDomainApi { */ public async reportSmsByIdentity( data: ReportSmsVerificationByIdentityRequestData, - ): Promise { + ): Promise { this.client = this.getSinchClient(); (data.reportSmsVerificationByIdentityRequestBody as any).method = 'sms'; const getParams = this.client.extractQueryParams(data, [] as never[]); @@ -163,7 +163,7 @@ export class VerificationsApi extends VerificationDomainApi { = await this.client.prepareOptions(basePathUrl, 'PUT', getParams, headers, body || undefined, path); const url = this.client.prepareUrl(requestOptions.hostname, requestOptions.queryParams); - return this.client.processCall({ + return this.client.processCall({ url, requestOptions, apiName: this.apiName, @@ -180,7 +180,7 @@ export class VerificationsApi extends VerificationDomainApi { data: ReportFlashCallVerificationByIdentityRequestData, ): Promise { this.client = this.getSinchClient(); - (data.reportFlashCallVerificationByIdentityRequestBody as any).method = 'flashCall'; + (data.reportFlashCallVerificationByIdentityRequestBody as any).method = 'flashcall'; const getParams = this.client.extractQueryParams( data, [] as never[]); const headers: { [key: string]: string | undefined } = { @@ -312,7 +312,7 @@ export class VerificationsApi extends VerificationDomainApi { data: StartFlashCallVerificationRequestData, ): Promise { this.client = this.getSinchClient(); - (data.startVerificationWithFlashCallRequestBody as any).method = 'flashCall'; + (data.startVerificationWithFlashCallRequestBody as any).method = 'flashcall'; const getParams = this.client.extractQueryParams(data, [] as never[]); const headers: { [key: string]: string | undefined } = { 'Content-Type': 'application/json; charset=UTF-8', diff --git a/packages/verification/tests/rest/v1/verification-status/verification-status-api.test.ts b/packages/verification/tests/rest/v1/verification-status/verification-status-api.test.ts index cfc574d5..45e547d2 100644 --- a/packages/verification/tests/rest/v1/verification-status/verification-status-api.test.ts +++ b/packages/verification/tests/rest/v1/verification-status/verification-status-api.test.ts @@ -25,7 +25,7 @@ describe('VerificationStatusApi', () => { const requestData: Verification.VerificationStatusByIdRequestData = { id: '', }; - const expectedResponse: Verification.SMSVerificationStatusResponse = { + const expectedResponse: Verification.SmsVerificationStatusResponse = { id: '018bec3e-6913-d36c-5102-ebda3fd6d30f', method: 'sms', status: 'SUCCESSFUL', diff --git a/packages/verification/tests/rest/v1/verifications/verifications-api.test.ts b/packages/verification/tests/rest/v1/verifications/verifications-api.test.ts index 947c58ff..13256578 100644 --- a/packages/verification/tests/rest/v1/verifications/verifications-api.test.ts +++ b/packages/verification/tests/rest/v1/verifications/verifications-api.test.ts @@ -171,7 +171,7 @@ describe('VerificationsApi', () => { const requestData = Verification.reportVerificationByIdHelper.buildSmsRequest( 'some_verification_id', '0000'); - const expectedResponse: Verification.SMSVerificationReportResponse = { + const expectedResponse: Verification.SmsVerificationReportResponse = { id: 'some_verification_id', method: 'sms', status: 'SUCCESSFUL', @@ -241,7 +241,7 @@ describe('VerificationsApi', () => { const requestData = Verification.reportVerificationByIdentityHelper.buildSmsRequest( '+33444555666', '0000'); - const expectedResponse: Verification.SMSVerificationReportResponse = { + const expectedResponse: Verification.SmsVerificationReportResponse = { id: '018beea3-a942-0094-4a3a-d6b2f2c65057', method: 'sms', status: 'FAIL', From a3c9a12ac1fb654fc73b605149653bb61c62a00b Mon Sep 17 00:00:00 2001 From: Antoine SEIN <142824551+asein-sinch@users.noreply.github.com> Date: Fri, 28 Jun 2024 11:41:38 +0200 Subject: [PATCH 04/54] DEVEXP-337: Mocked tests setup (#97) --- .eslintrc | 3 +- .github/workflows/run-ci.yaml | 43 ++ package.json | 4 +- packages/fax/cucumber.js | 8 + packages/fax/package.json | 3 +- .../fax/tests/e2e/resources/sinch-logo.png | Bin 0 -> 1678 bytes .../fax/tests/rest/v3/faxes/faxes.steps.ts | 207 ++++++++ yarn.lock | 454 +++++++++++++++++- 8 files changed, 708 insertions(+), 14 deletions(-) create mode 100644 packages/fax/cucumber.js create mode 100644 packages/fax/tests/e2e/resources/sinch-logo.png create mode 100644 packages/fax/tests/rest/v3/faxes/faxes.steps.ts diff --git a/.eslintrc b/.eslintrc index 9a724d80..c5433d85 100644 --- a/.eslintrc +++ b/.eslintrc @@ -71,6 +71,7 @@ "ignoreRegExpLiterals": true, "ignorePattern": "^import.+|test" } - ] + ], + "new-cap": "off" } } diff --git a/.github/workflows/run-ci.yaml b/.github/workflows/run-ci.yaml index 3f1d6f1e..4e80b292 100644 --- a/.github/workflows/run-ci.yaml +++ b/.github/workflows/run-ci.yaml @@ -20,6 +20,49 @@ jobs: - run: yarn run build - run: yarn run test + e2e: + needs: build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Checkout sinch-sdk-mockserver repository + uses: actions/checkout@v3 + with: + repository: sinch/sinch-sdk-mockserver + token: ${{ secrets.PAT_CI }} + fetch-depth: 0 + path: sinch-sdk-mockserver + - name: Build custom Docker image + run: | + cd sinch-sdk-mockserver + docker build -t sinch-sdk-mockserver -f Dockerfile . + - name: Start mock servers with Docker Compose + run: | + cd sinch-sdk-mockserver + docker-compose up -d + - name: Wait for the mock servers to be healthy + run: | + cd sinch-sdk-mockserver + while ! docker inspect --format='{{json .State.Health.Status}}' $(docker-compose ps -q authentication-server) | grep -q '"healthy"'; do + echo "Waiting for authentication-server to be healthy..." + sleep 2 + done + while ! docker inspect --format='{{json .State.Health.Status}}' $(docker-compose ps -q fax-server) | grep -q '"healthy"'; do + echo "Waiting for fax-server to be healthy..." + sleep 2 + done + - name: Create target directories for feature files + run: | + mkdir -p ./packages/fax/tests/e2e/features + - name: Copy feature files + run: | + cp sinch-sdk-mockserver/features/fax/*.feature ./packages/fax/tests/e2e/features/ + - name: Run e2e tests + run: | + yarn install + yarn run build + yarn run e2e + sonarcloud: runs-on: ubuntu-latest steps: diff --git a/package.json b/package.json index 680f7ac0..7838531b 100644 --- a/package.json +++ b/package.json @@ -14,13 +14,15 @@ "lerna": "lerna", "bootstrap": "npx lerna bootstrap", "build": "lerna run build", - "test": "jest" + "test": "jest", + "e2e": "lerna run test:e2e" }, "keywords": [], "devDependencies": { "@babel/core": "^7.23.3", "@babel/preset-env": "^7.23.3", "@babel/preset-typescript": "^7.23.3", + "@cucumber/cucumber": "^10.3.1", "@types/jest": "^29.5.1", "@typescript-eslint/eslint-plugin": "^6.9.0", "@typescript-eslint/parser": "^6.9.0", diff --git a/packages/fax/cucumber.js b/packages/fax/cucumber.js new file mode 100644 index 00000000..d03dd4ab --- /dev/null +++ b/packages/fax/cucumber.js @@ -0,0 +1,8 @@ +module.exports = { + default: [ + 'tests/e2e/features/**/*.feature', + '--require-module ts-node/register', + '--require tests/rest/v3/**/*.steps.ts', + `--format-options '{"snippetInterface": "synchronous"}'`, + ].join(' '), +}; diff --git a/packages/fax/package.json b/packages/fax/package.json index 43529112..edeae1ce 100644 --- a/packages/fax/package.json +++ b/packages/fax/package.json @@ -25,7 +25,8 @@ "scripts": { "build": "yarn run clean && yarn run compile", "clean": "rimraf dist tsconfig.tsbuildinfo", - "compile": "tsc -p tsconfig.build.json && tsc -p tsconfig.tests.json && rimraf dist/tests && rimraf tsconfig.build.tsbuildinfo" + "compile": "tsc -p tsconfig.build.json && tsc -p tsconfig.tests.json && rimraf dist/tests && rimraf tsconfig.build.tsbuildinfo", + "test:e2e": "cucumber-js" }, "dependencies": { "@sinch/sdk-client": "^1.1.0" diff --git a/packages/fax/tests/e2e/resources/sinch-logo.png b/packages/fax/tests/e2e/resources/sinch-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..ee4989826de48afd2ebcfd4f2dfb450b046c5a85 GIT binary patch literal 1678 zcmV;9266d`P)X1^@s6;a-BlF_@XtJ^(pFkfOGH7514&7hq>6i80;0I2HW(G+ z7I7hh2?duRBt(rUQKN$XVd5WJmnNvF6pdmHC{b)MRa>c{RiliQq9B%~mgzDb|2V&2 z-hKDo-<#iSJFPbJO>R2x-gC}f&beo~zcP#e2aMoNxZ-dxWEKYxXW&{&C@2X3-w|t=Q3q{+HmDc%E>v zkKzveqWX6qUV(Sv6F4E^JtLE~3)kY4l^i$`=i>R*;`iVl+=3giv+V{NFduKO_HZ)x z;(n2Y+<^OqQW5! z!s?9wI^i~7C_+IU`)?LTP-9)flPmVJhP7{NIFrw1{7djy1OKLmu{snCQtWo+i?|a{ zttY=B8(dQfO>q)FqkKd6;EQ@|+GY4{!=ip9;&_U=_=sLN{gAlrZLvAy9m&Se!)ptC zO^ThP-zdMTyU%$d$Jm#QPw34#zL)X;MVw8IWAWKU#u{y)L?h4`w_UPeY$#ea5>=7|@{o(5%=qGkJID zHx^>QDP5f-EEKUby-i~Uez;1%(dCNWEApTm=VWxhhqq>(%L|tZbM!KiJP#{byzUtA^MM z{fb>bfc75=-9vDB(lIF%G#!?Tr@|V`l{3CcdD|V2P_f#_-ariYlB5q8F4I&X)ER=AQsPR(V zF6!V5#bG^NBrt7oO!D{cmOO;3x&SX1Wz{1EnuXOm@+7B(-0kjZplj-%$bLV$#hioC zG!NN1b#PZkP)aUxEEEC%58^{Wifwqis3(6X4CSh7p3_>$UWDf)zqhpD$?;3^Kykc? zlUG-3O>q&9+Yi~p;O^}A%amg@y&mwOV7_oq^)(*X{3WJ?cj}FSX*PI`-rM#D{4Jq* zOx%`~CdEcWBQ`t+au_jl1Wk#PHagE$%)~)cu`piVSS5f;8#ze1<*L%ptei72$2;@pGPoX-}?(matZ z-{pmI!Sd zTC8gV?;Rj_zZ6SY*T?ifFV^i|5hE96d>1#+--u@Oop+$zkpV&U=lsPfrP;-`4-G;&)jc2I`)A`t@?h|O*kRySFozf^qvsBu@- zw|2lnPQZ(GjpQCti#2|F8WKxItY0KT!MI4C9xTu<(2eC&ssdjmHEH>`L%HPWh^N6C zyT!+d5=O?z22%G~93EUE;@g21;IsH2L+L-V&EhcP Yzg)3pFweN{c>n+a07*qoM6N<$f~_DzF8}}l literal 0 HcmV?d00001 diff --git a/packages/fax/tests/rest/v3/faxes/faxes.steps.ts b/packages/fax/tests/rest/v3/faxes/faxes.steps.ts new file mode 100644 index 00000000..666848a2 --- /dev/null +++ b/packages/fax/tests/rest/v3/faxes/faxes.steps.ts @@ -0,0 +1,207 @@ +import { Given, When, Then } from '@cucumber/cucumber'; +import { FileBuffer, PageResult } from '@sinch/sdk-client'; +import * as assert from 'assert'; +import { FaxService, Fax } from '../../../../src'; + +let faxService: FaxService; +let listResponse: PageResult; +const faxList: Fax.Fax[] = []; +let sendFaxResponse: Fax.Fax[]; +let fax: Fax.Fax; +let fileBuffer: FileBuffer; +let deleteContentResponse: void; + +Given('the Fax service is available', function () { + faxService = new FaxService({ + projectId: 'tinyfrog-jump-high-over-lilypadbasin', + keyId: 'keyId', + keySecret: 'keySecret', + authHostname: 'http://localhost:3011', + }); + faxService.setHostname('http://localhost:3012'); +}); + +When('I send a fax with a contentUrl only to a single recipient', async function () { + sendFaxResponse = await faxService.faxes.send({ + sendFaxRequestBody: { + to: '+12015555555', + contentUrl: 'https://developers.sinch.com/fax/fax.pdf', + }, + }); +}); + +// eslint-disable-next-line max-len +Then('the response contains a list of fax objects with a single element received from a multipart-form-data request with contentUrl only', function () { + assert.equal(sendFaxResponse.length, 1); + assert.equal('01W4FFL35P4NC4K35URLSINGLE1', sendFaxResponse[0].id); +}); + +When('I send a fax with a contentUrl only to multiple recipients', async function () { + sendFaxResponse = await faxService.faxes.send({ + sendFaxRequestBody: { + to: ['+12015555555', '+12016666666'], + contentUrl: 'https://developers.sinch.com/fax/fax.pdf', + }, + }); +}); + +// eslint-disable-next-line max-len +Then('the response contains a list of fax objects with multiple elements received from a multipart-form-data request with contentUrl only', function () { + assert.equal(sendFaxResponse.length, 2); + assert.equal('01W4FFL35P4NC4K35URLMULTI01', sendFaxResponse[0].id); + assert.equal('01W4FFL35P4NC4K35URLMULTI02', sendFaxResponse[1].id); +}); + +When('I send a fax with a contentUrl and a binary file attachment to a single recipient', async function () { + sendFaxResponse = await faxService.faxes.send({ + sendFaxRequestBody: { + to: '+12015555555', + contentUrl: 'https://developers.sinch.com/fax/fax.pdf', + filePaths: ['./tests/e2e/resources/sinch-logo.png'], + }, + }); +}); + +// eslint-disable-next-line max-len +Then('the response contains a list of fax objects with a single element received from a multipart-form-data request', function () { + assert.equal(sendFaxResponse.length, 1); + assert.equal('01W4FFL35P4NC4K35BINSINGLE1', sendFaxResponse[0].id); +}); + +When('I send a fax with a contentUrl and a binary file attachment to multiple recipients', async function () { + sendFaxResponse = await faxService.faxes.send({ + sendFaxRequestBody: { + to: ['+12015555555', '+12016666666'], + contentUrl: 'https://developers.sinch.com/fax/fax.pdf', + filePaths: ['./tests/e2e/resources/sinch-logo.png'], + }, + }); +}); + +// eslint-disable-next-line max-len +Then('the response contains a list of fax objects with multiple elements received from a multipart-form-data request', function () { + assert.equal(sendFaxResponse.length, 2); + assert.equal('01W4FFL35P4NC4K35BINMULTI01', sendFaxResponse[0].id); + assert.equal('01W4FFL35P4NC4K35BINMULTI02', sendFaxResponse[1].id); +}); + +When('I send a fax with a contentUrl and a base64 file encoded to a single recipient', async function () { + sendFaxResponse = await faxService.faxes.send({ + sendFaxRequestBody: { + to: '+12015555555', + contentUrl: 'https://developers.sinch.com/fax/fax.pdf', + files: [ + { + file: 'WSdhIGRlcyBqb3VycywgZmF1dCBwYXMgbSdjaGVyY2hlciAhIEV0IHknYSBkZXMgam91cnMgdG91cyBsZXMgam91cnMgIQ==', + fileType: 'PDF', + }, + { + file: 'UXVhbmQgbGUgdHJvbGwgcGFybGUsIGwnaG9tbWUgYXZpc8OpIGwnw6ljb3V0ZQ==', + fileType: 'PDF', + }, + ], + }, + }); +}); + +// eslint-disable-next-line max-len +Then('the response contains a list of fax objects with a single element received from an application-json request', function () { + assert.equal(sendFaxResponse.length, 1); + assert.equal('01W4FFL35P4NC4K35B64SINGLE1', sendFaxResponse[0].id); +}); + +When('I send a fax with a contentUrl and a base64 file encoded to multiple recipients', async function () { + sendFaxResponse = await faxService.faxes.send({ + sendFaxRequestBody: { + to: ['+12015555555', '+12016666666'], + contentUrl: 'https://developers.sinch.com/fax/fax.pdf', + files: [ + { + file: 'WSdhIGRlcyBqb3VycywgZmF1dCBwYXMgbSdjaGVyY2hlciAhIEV0IHknYSBkZXMgam91cnMgdG91cyBsZXMgam91cnMgIQ==', + fileType: 'PDF', + }, + { + file: 'UXVhbmQgbGUgdHJvbGwgcGFybGUsIGwnaG9tbWUgYXZpc8OpIGwnw6ljb3V0ZQ==', + fileType: 'PDF', + }, + ], + }, + }); +}); + +// eslint-disable-next-line max-len +Then('the response contains a list of fax objects with multiple elements received from an application-json request', function () { + assert.equal(sendFaxResponse.length, 2); + assert.equal('01W4FFL35P4NC4K35B64MULTI01', sendFaxResponse[0].id); + assert.equal('01W4FFL35P4NC4K35B64MULTI02', sendFaxResponse[1].id); +}); + +When('I retrieve a fax', async function () { + fax = await faxService.faxes.get({ + id: '01W4FFL35P4NC4K35CR3P35M1N1', + }); +}); + +Then('the response contains a fax object', function () { + assert.equal('01W4FFL35P4NC4K35CR3P35M1N1', fax.id); + assert.equal('OUTBOUND', fax.direction); + assert.equal('+12014444444', fax.from); + assert.equal('+12015555555', fax.to); + assert.equal(1, fax.numberOfPages); + assert.equal('COMPLETED', fax.status); + assert.equal('America/New_York', fax.headerTimeZone); + assert.equal(60, fax.retryDelaySeconds); + assert.equal('multipart/form-data', fax.callbackUrlContentType); + assert.equal('0.07', (fax as any).pricePerPage); + assert.equal('123coffee-dada-beef-cafe-baadc0de5678', fax.projectId); + assert.equal('01K1TTENC4TSJ0LLYJ1GGLYJU1Y', fax.serviceId); + assert.equal('USD', fax.price?.currencyCode); + assert.equal('0.07', fax.price?.amount); + assert.equal(3, fax.maxRetries); + assert.equal(new Date('2024-06-06T14:42:42Z').getTime(), fax.createTime?.getTime()); + assert.equal(new Date('2024-06-06T14:43:17Z').getTime(), fax.completedTime?.getTime()); + assert.equal(true, fax.headerPageNumbers); + assert.equal('https://developers.sinch.com/fax/fax.pdf', fax.contentUrl?.[0]); + assert.equal('MONOCHROME', fax.imageConversionMethod); + assert.equal(true, fax.hasFile); +}); + +When('I send a request to list faxes', async function () { + listResponse = await faxService.faxes.list({}); +}); + +Then('the response contains {string} faxes', function (expectedAnswer: string) { + const expectedFaxes = parseInt(expectedAnswer, 10); + assert.strictEqual(listResponse.data.length, expectedFaxes); +}); + +When('I send a request to list all the faxes', async function () { + for await (const fax of faxService.faxes.list({})) { + faxList.push(fax); + } +}); + +Then('the faxes list contains {string} faxes', function (expectedAnswer: string) { + const expectedFaxes = parseInt(expectedAnswer, 10); + assert.strictEqual(faxList.length, expectedFaxes); +}); + +When('I send a request to download a fax content as PDF', async function () { + fileBuffer = await faxService.faxes.downloadContent({ + id: '01W4FFL35P4NC4K35CR3P35DWLD', + }); +}); + +Then('the response contains a PDF document', function () { + assert.equal(fileBuffer.fileName, '01W4FFL35P4NC4K35CR3P35DWLD.pdf'); +}); + +When('I send a request to delete a fax content on the server', async function () { + deleteContentResponse = await faxService.faxes.deleteContent({ + id: '01W4FFL35P4NC4K35CR3P35DEL0', + }); +}); + +Then('the response contains no data', function () { + assert.deepEqual(deleteContentResponse, {}); +}); diff --git a/yarn.lock b/yarn.lock index c3649952..4070c76f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1042,6 +1042,116 @@ dependencies: "@jridgewell/trace-mapping" "0.3.9" +"@cucumber/ci-environment@10.0.1": + version "10.0.1" + resolved "https://registry.yarnpkg.com/@cucumber/ci-environment/-/ci-environment-10.0.1.tgz#c8584f1d4a619e4318cf60c01b838db096d72ccd" + integrity sha512-/+ooDMPtKSmvcPMDYnMZt4LuoipfFfHaYspStI4shqw8FyKcfQAmekz6G+QKWjQQrvM+7Hkljwx58MEwPCwwzg== + +"@cucumber/cucumber-expressions@17.1.0": + version "17.1.0" + resolved "https://registry.yarnpkg.com/@cucumber/cucumber-expressions/-/cucumber-expressions-17.1.0.tgz#1a428548a2c98ef3224bd484fc5666b4f7153a72" + integrity sha512-PCv/ppsPynniKPWJr5v566daCVe+pbxQpHGrIu/Ev57cCH9Rv+X0F6lio4Id3Z64TaG7btCRLUGewIgLwmrwOA== + dependencies: + regexp-match-indices "1.0.2" + +"@cucumber/cucumber@^10.3.1": + version "10.8.0" + resolved "https://registry.yarnpkg.com/@cucumber/cucumber/-/cucumber-10.8.0.tgz#d906b451a07a91c254f90b8e2dfed4cf8ade3fcf" + integrity sha512-o8SR6MRQVCKKw4tytWqCqOjfX4cASj9dqpdHKHMi09rZWBCNQHBwH1TO9wj7NKjOa6RfUOTcgvDlayTcjyCH4A== + dependencies: + "@cucumber/ci-environment" "10.0.1" + "@cucumber/cucumber-expressions" "17.1.0" + "@cucumber/gherkin" "28.0.0" + "@cucumber/gherkin-streams" "5.0.1" + "@cucumber/gherkin-utils" "9.0.0" + "@cucumber/html-formatter" "21.3.1" + "@cucumber/message-streams" "4.0.1" + "@cucumber/messages" "24.1.0" + "@cucumber/tag-expressions" "6.1.0" + assertion-error-formatter "^3.0.0" + capital-case "^1.0.4" + chalk "^4.1.2" + cli-table3 "0.6.3" + commander "^10.0.0" + debug "^4.3.4" + error-stack-parser "^2.1.4" + figures "^3.2.0" + glob "^10.3.10" + has-ansi "^4.0.1" + indent-string "^4.0.0" + is-installed-globally "^0.4.0" + is-stream "^2.0.0" + knuth-shuffle-seeded "^1.0.6" + lodash.merge "^4.6.2" + lodash.mergewith "^4.6.2" + luxon "3.2.1" + mkdirp "^2.1.5" + mz "^2.7.0" + progress "^2.0.3" + read-pkg-up "^7.0.1" + resolve-pkg "^2.0.0" + semver "7.5.3" + string-argv "0.3.1" + strip-ansi "6.0.1" + supports-color "^8.1.1" + tmp "0.2.3" + type-fest "^4.8.3" + util-arity "^1.1.0" + xmlbuilder "^15.1.1" + yaml "^2.2.2" + yup "1.2.0" + +"@cucumber/gherkin-streams@5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@cucumber/gherkin-streams/-/gherkin-streams-5.0.1.tgz#8c2142d295cd05644456be7282b4bd756c95c4cd" + integrity sha512-/7VkIE/ASxIP/jd4Crlp4JHXqdNFxPGQokqWqsaCCiqBiu5qHoKMxcWNlp9njVL/n9yN4S08OmY3ZR8uC5x74Q== + dependencies: + commander "9.1.0" + source-map-support "0.5.21" + +"@cucumber/gherkin-utils@9.0.0": + version "9.0.0" + resolved "https://registry.yarnpkg.com/@cucumber/gherkin-utils/-/gherkin-utils-9.0.0.tgz#944c64c458742d8e73b750e5dde2cf56b161d674" + integrity sha512-clk4q39uj7pztZuZtyI54V8lRsCUz0Y/p8XRjIeHh7ExeEztpWkp4ca9q1FjUOPfQQ8E7OgqFbqoQQXZ1Bx7fw== + dependencies: + "@cucumber/gherkin" "^28.0.0" + "@cucumber/messages" "^24.0.0" + "@teppeis/multimaps" "3.0.0" + commander "12.0.0" + source-map-support "^0.5.21" + +"@cucumber/gherkin@28.0.0", "@cucumber/gherkin@^28.0.0": + version "28.0.0" + resolved "https://registry.yarnpkg.com/@cucumber/gherkin/-/gherkin-28.0.0.tgz#91246da622524807b21430c1692bedd319d3d4bb" + integrity sha512-Ee6zJQq0OmIUPdW0mSnsCsrWA2PZAELNDPICD2pLfs0Oz7RAPgj80UsD2UCtqyAhw2qAR62aqlktKUlai5zl/A== + dependencies: + "@cucumber/messages" ">=19.1.4 <=24" + +"@cucumber/html-formatter@21.3.1": + version "21.3.1" + resolved "https://registry.yarnpkg.com/@cucumber/html-formatter/-/html-formatter-21.3.1.tgz#1c832d95891c1e961d9e6a4da4664c09a1332768" + integrity sha512-M1zbre7e8MsecXheqNv62BKY5J06YJSv1LmsD7sJ3mu5t1jirLjj2It1HqPsX5CQAfg9n69xFRugPgLMSte9TA== + +"@cucumber/message-streams@4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@cucumber/message-streams/-/message-streams-4.0.1.tgz#a5339d3504594bb2edb5732aaae94dddb24d0970" + integrity sha512-Kxap9uP5jD8tHUZVjTWgzxemi/0uOsbGjd4LBOSxcJoOCRbESFwemUzilJuzNTB8pcTQUh8D5oudUyxfkJOKmA== + +"@cucumber/messages@24.1.0", "@cucumber/messages@>=19.1.4 <=24", "@cucumber/messages@^24.0.0": + version "24.1.0" + resolved "https://registry.yarnpkg.com/@cucumber/messages/-/messages-24.1.0.tgz#a212c97b0548144c3ccfae021a96d6c56d3841d3" + integrity sha512-hxVHiBurORcobhVk80I9+JkaKaNXkW6YwGOEFIh/2aO+apAN+5XJgUUWjng9NwqaQrW1sCFuawLB1AuzmBaNdQ== + dependencies: + "@types/uuid" "9.0.8" + class-transformer "0.5.1" + reflect-metadata "0.2.1" + uuid "9.0.1" + +"@cucumber/tag-expressions@6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@cucumber/tag-expressions/-/tag-expressions-6.1.0.tgz#cb7af908bdb43669b7574c606f71fa707196e962" + integrity sha512-+3DwRumrCJG27AtzCIL37A/X+A/gSfxOPLg8pZaruh5SLumsTmpvilwroVWBT2fPzmno/tGXypeK5a7NHU4RzA== + "@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": version "4.4.0" resolved "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz" @@ -1917,6 +2027,11 @@ dependencies: "@sinonjs/commons" "^3.0.0" +"@teppeis/multimaps@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@teppeis/multimaps/-/multimaps-3.0.0.tgz#bb9c3f8d569f589e548586fa0bbf423010ddfdc5" + integrity sha512-ID7fosbc50TbT0MK0EG12O+gAP3W3Aa/Pz4DaTtQtEvlc9Odaqi0de+xuZ7Li2GtK4HzEX7IuRWS/JmZLksR3Q== + "@tootallnate/once@2": version "2.0.0" resolved "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz" @@ -2187,6 +2302,11 @@ dependencies: "@types/node" "*" +"@types/uuid@9.0.8": + version "9.0.8" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.8.tgz#7545ba4fc3c003d6c756f651f3bf163d8f0f29ba" + integrity sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA== + "@types/yargs-parser@*": version "21.0.3" resolved "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz" @@ -2691,6 +2811,11 @@ ansi-escapes@^4.2.1, ansi-escapes@^4.3.2: dependencies: type-fest "^0.21.3" +ansi-regex@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.1.tgz#164daac87ab2d6f6db3a29875e2d1766582dabed" + integrity sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g== + ansi-regex@^5.0.1: version "5.0.1" resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz" @@ -2725,6 +2850,11 @@ ansi-styles@^6.1.0: resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz" integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== +any-promise@^1.0.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" + integrity sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A== + anymatch@^3.0.3, anymatch@~3.1.2: version "3.1.3" resolved "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz" @@ -2803,6 +2933,15 @@ arrify@^2.0.1: resolved "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz" integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug== +assertion-error-formatter@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/assertion-error-formatter/-/assertion-error-formatter-3.0.0.tgz#be9c8825dee6a8a6c72183d915912d9b57d5d265" + integrity sha512-6YyAVLrEze0kQ7CmJfUgrLHb+Y7XghmL2Ie7ijVa2Y9ynP3LV+VDiwFk62Dn0qtqbmY0BT0ss6p1xxpiF2PYbQ== + dependencies: + diff "^4.0.1" + pad-right "^0.2.2" + repeat-string "^1.6.1" + async@^3.2.3: version "3.2.5" resolved "https://registry.npmjs.org/async/-/async-3.2.5.tgz" @@ -3133,6 +3272,15 @@ caniuse-lite@^1.0.30001565: resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001570.tgz" integrity sha512-+3e0ASu4sw1SWaoCtvPeyXp+5PsjigkSt8OXZbF9StH5pQWbxEjLAZE3n8Aup5udop1uRiKA7a4utUk/uoSpUw== +capital-case@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/capital-case/-/capital-case-1.0.4.tgz#9d130292353c9249f6b00fa5852bee38a717e669" + integrity sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A== + dependencies: + no-case "^3.0.4" + tslib "^2.0.3" + upper-case-first "^2.0.2" + chalk@4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz" @@ -3208,6 +3356,11 @@ cjs-module-lexer@^1.0.0: resolved "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz" integrity sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ== +class-transformer@0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/class-transformer/-/class-transformer-0.5.1.tgz#24147d5dffd2a6cea930a3250a677addf96ab336" + integrity sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw== + clean-stack@^2.0.0: version "2.2.0" resolved "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz" @@ -3340,11 +3493,26 @@ combined-stream@^1.0.8: dependencies: delayed-stream "~1.0.0" +commander@12.0.0: + version "12.0.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-12.0.0.tgz#b929db6df8546080adfd004ab215ed48cf6f2592" + integrity sha512-MwVNWlYjDTtOjX5PiD7o5pK0UrFU/OYgcJfjjK4RaHZETNtjJqrZa9Y9ds88+A+f+d5lv+561eZ+yCKoS3gbAA== + commander@4.1.1: version "4.1.1" resolved "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz" integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== +commander@9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-9.1.0.tgz#a6b263b2327f2e188c6402c42623327909f2dbec" + integrity sha512-i0/MaqBtdbnJ4XQs4Pmyb+oFQl+q0lsAmokVUH92SlSw4fkeAcG3bVon+Qt7hmtF+u3Het6o4VgrcY3qAoEB6w== + +commander@^10.0.0: + version "10.0.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06" + integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug== + commander@^2.20.0: version "2.20.3" resolved "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz" @@ -3836,6 +4004,13 @@ error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" +error-stack-parser@^2.1.4: + version "2.1.4" + resolved "https://registry.yarnpkg.com/error-stack-parser/-/error-stack-parser-2.1.4.tgz#229cb01cdbfa84440bfa91876285b94680188286" + integrity sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ== + dependencies: + stackframe "^1.3.4" + es-module-lexer@^1.2.1: version "1.4.1" resolved "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.4.1.tgz" @@ -4605,6 +4780,18 @@ glob@7.1.4: once "^1.3.0" path-is-absolute "^1.0.0" +glob@^10.3.10: + version "10.4.2" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.2.tgz#bed6b95dade5c1f80b4434daced233aee76160e5" + integrity sha512-GwMlUF6PkPo3Gk21UxkCohOv0PLcIXVtKyLlpEI28R/cO/4eNOdmLk3CMW1wROV/WR/EsZOWAfBbBOqYvs88/w== + dependencies: + foreground-child "^3.1.0" + jackspeak "^3.1.2" + minimatch "^9.0.4" + minipass "^7.1.2" + package-json-from-dist "^1.0.0" + path-scurry "^1.11.1" + glob@^7.0.0, glob@^7.1.3, glob@^7.1.4: version "7.2.3" resolved "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz" @@ -4638,6 +4825,13 @@ glob@^9.2.0: minipass "^4.2.4" path-scurry "^1.6.1" +global-dirs@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-3.0.1.tgz#0c488971f066baceda21447aecb1a8b911d22485" + integrity sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA== + dependencies: + ini "2.0.0" + globals@^11.1.0: version "11.12.0" resolved "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz" @@ -4696,6 +4890,13 @@ hard-rejection@^2.1.0: resolved "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz" integrity sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA== +has-ansi@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-4.0.1.tgz#f216a8c8d7b129e490dc15f4a62cc1cdb9603ce8" + integrity sha512-Qr4RtTm30xvEdqUXbSBVWDu+PrTokJOwe/FU+VdfJPk+MXAPoeOzKpRyrDTnZIJwAkQ4oBLTU53nu0HrkF/Z2A== + dependencies: + ansi-regex "^4.1.0" + has-flag@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz" @@ -4903,6 +5104,11 @@ inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== +ini@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ini/-/ini-2.0.0.tgz#e5fd556ecdd5726be978fa1001862eacb0a94bc5" + integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA== + ini@^1.3.2, ini@^1.3.8: version "1.3.8" resolved "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz" @@ -5064,6 +5270,14 @@ is-inside-container@^1.0.0: dependencies: is-docker "^3.0.0" +is-installed-globally@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.4.0.tgz#9a0fd407949c30f86eb6959ef1b7994ed0b7b520" + integrity sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ== + dependencies: + global-dirs "^3.0.0" + is-path-inside "^3.0.2" + is-interactive@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz" @@ -5084,9 +5298,9 @@ is-obj@^2.0.0: resolved "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz" integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== -is-path-inside@^3.0.3: +is-path-inside@^3.0.2, is-path-inside@^3.0.3: version "3.0.3" - resolved "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== is-plain-obj@^1.0.0, is-plain-obj@^1.1.0: @@ -5234,6 +5448,15 @@ jackspeak@^2.3.5: optionalDependencies: "@pkgjs/parseargs" "^0.11.0" +jackspeak@^3.1.2: + version "3.4.0" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.0.tgz#a75763ff36ad778ede6a156d8ee8b124de445b4a" + integrity sha512-JVYhQnN59LVPFCEcVa2C3CrEKYacvjRfqIQl+h8oi91aLYQVWRYbxjPcv1bUiUy/kLmQaANrYfNMCO3kuEDHfw== + dependencies: + "@isaacs/cliui" "^8.0.2" + optionalDependencies: + "@pkgjs/parseargs" "^0.11.0" + jake@^10.8.5: version "10.8.7" resolved "https://registry.npmjs.org/jake/-/jake-10.8.7.tgz" @@ -5722,6 +5945,13 @@ kleur@^3.0.3: resolved "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz" integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== +knuth-shuffle-seeded@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/knuth-shuffle-seeded/-/knuth-shuffle-seeded-1.0.6.tgz#01f1b65733aa7540ee08d8b0174164d22081e4e1" + integrity sha512-9pFH0SplrfyKyojCLxZfMcvkhf5hH0d+UwR9nTVJ/DDQJGuzcXjTwB7TP7sDfehSudlGGaOLblmEWqv04ERVWg== + dependencies: + seed-random "~2.2.0" + lerna@^7.4.1: version "7.4.2" resolved "https://registry.npmjs.org/lerna/-/lerna-7.4.2.tgz" @@ -5910,6 +6140,11 @@ lodash.merge@^4.6.2: resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== +lodash.mergewith@^4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz#617121f89ac55f59047c7aec1ccd6654c6590f55" + integrity sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ== + lodash@^4.17.21: version "4.17.21" resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" @@ -5923,6 +6158,18 @@ log-symbols@^4.1.0: chalk "^4.1.0" is-unicode-supported "^0.1.0" +lower-case@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.2.tgz#6fa237c63dbdc4a82ca0fd882e4722dc5e634e28" + integrity sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg== + dependencies: + tslib "^2.0.3" + +lru-cache@^10.2.0: + version "10.2.2" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.2.tgz#48206bc114c1252940c41b25b41af5b545aca878" + integrity sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ== + lru-cache@^5.1.1: version "5.1.1" resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz" @@ -5947,6 +6194,11 @@ lru-cache@^7.4.4, lru-cache@^7.5.1, lru-cache@^7.7.1: resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-10.1.0.tgz" integrity sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag== +luxon@3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.2.1.tgz#14f1af209188ad61212578ea7e3d518d18cee45f" + integrity sha512-QrwPArQCNLAKGO/C+ZIilgIuDnEnKx5QYODdDtbFaxzsbZcc/a7WFq7MhsVYgRlwawLtvOUESTlfJ+hc/USqPg== + magic-string@0.30.5: version "0.30.5" resolved "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz" @@ -6158,6 +6410,13 @@ minimatch@^8.0.2: dependencies: brace-expansion "^2.0.1" +minimatch@^9.0.4: + version "9.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.4.tgz#8e49c731d1749cbec05050ee5145147b32496a51" + integrity sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw== + dependencies: + brace-expansion "^2.0.1" + minimist-options@4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz" @@ -6252,6 +6511,11 @@ minipass@^5.0.0: resolved "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz" integrity sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ== +minipass@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" + integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== + minizlib@^2.1.1, minizlib@^2.1.2: version "2.1.2" resolved "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz" @@ -6272,6 +6536,11 @@ mkdirp@^1.0.3, mkdirp@^1.0.4: resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== +mkdirp@^2.1.5: + version "2.1.6" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-2.1.6.tgz#964fbcb12b2d8c5d6fbc62a963ac95a273e2cc19" + integrity sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A== + modify-values@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/modify-values/-/modify-values-1.0.1.tgz" @@ -6326,6 +6595,15 @@ mute-stream@1.0.0, mute-stream@~1.0.0: resolved "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz" integrity sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA== +mz@^2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32" + integrity sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q== + dependencies: + any-promise "^1.0.0" + object-assign "^4.0.1" + thenify-all "^1.0.0" + natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz" @@ -6341,6 +6619,14 @@ neo-async@^2.6.2: resolved "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== +no-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d" + integrity sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg== + dependencies: + lower-case "^2.0.2" + tslib "^2.0.3" + node-abort-controller@^3.0.1: version "3.1.1" resolved "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz" @@ -6618,9 +6904,9 @@ nx@16.10.0, "nx@>=16.5.1 < 17": "@nx/nx-win32-arm64-msvc" "16.10.0" "@nx/nx-win32-x64-msvc" "16.10.0" -object-assign@^4, object-assign@^4.1.1: +object-assign@^4, object-assign@^4.0.1, object-assign@^4.1.1: version "4.1.1" - resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== object-inspect@^1.9.0: @@ -6808,6 +7094,11 @@ p-waterfall@2.1.1: dependencies: p-reduce "^2.0.0" +package-json-from-dist@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz#e501cd3094b278495eb4258d4c9f6d5ac3019f00" + integrity sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw== + pacote@^15.2.0: version "15.2.0" resolved "https://registry.npmjs.org/pacote/-/pacote-15.2.0.tgz" @@ -6832,6 +7123,13 @@ pacote@^15.2.0: ssri "^10.0.0" tar "^6.1.11" +pad-right@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/pad-right/-/pad-right-0.2.2.tgz#6fbc924045d244f2a2a244503060d3bfc6009774" + integrity sha512-4cy8M95ioIGolCoMmm2cMntGR1lPLEbOMzOKu8bzjuJP6JpzEMQcDHmh7hHLYGgob+nKe1YHFMaG4V59HQa89g== + dependencies: + repeat-string "^1.5.2" + parent-module@^1.0.0: version "1.0.1" resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz" @@ -6914,6 +7212,14 @@ path-scurry@^1.10.1, path-scurry@^1.6.1: lru-cache "^9.1.1 || ^10.0.0" minipass "^5.0.0 || ^6.0.2 || ^7.0.0" +path-scurry@^1.11.1: + version "1.11.1" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2" + integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== + dependencies: + lru-cache "^10.2.0" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" + path-to-regexp@0.1.7: version "0.1.7" resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz" @@ -7024,6 +7330,11 @@ process-nextick-args@~2.0.0: resolved "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz" integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== +progress@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" + integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== + promise-inflight@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz" @@ -7052,6 +7363,11 @@ promzard@^1.0.0: dependencies: read "^2.0.0" +property-expr@^2.0.5: + version "2.0.6" + resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-2.0.6.tgz#f77bc00d5928a6c748414ad12882e83f24aec1e8" + integrity sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA== + protocols@^2.0.0, protocols@^2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/protocols/-/protocols-2.0.1.tgz" @@ -7234,6 +7550,11 @@ redent@^3.0.0: indent-string "^4.0.0" strip-indent "^3.0.0" +reflect-metadata@0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.2.1.tgz#8d5513c0f5ef2b4b9c3865287f3c0940c1f67f74" + integrity sha512-i5lLI6iw9AU3Uu4szRNPPEkomnkjRTaVt9hy/bn5g/oSzekBSMeLZblcjP74AW0vBabqERLLIrz+gR8QYR54Tw== + reflect-metadata@^0.1.13: version "0.1.14" resolved "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.14.tgz" @@ -7263,6 +7584,18 @@ regenerator-transform@^0.15.2: dependencies: "@babel/runtime" "^7.8.4" +regexp-match-indices@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/regexp-match-indices/-/regexp-match-indices-1.0.2.tgz#cf20054a6f7d5b3e116a701a7b00f82889d10da6" + integrity sha512-DwZuAkt8NF5mKwGGER1EGh2PRqyvhRhhLviH+R8y8dIuaQROlUfXjt4s9ZTXstIsSkptf06BSvwcEmmfheJJWQ== + dependencies: + regexp-tree "^0.1.11" + +regexp-tree@^0.1.11: + version "0.1.27" + resolved "https://registry.yarnpkg.com/regexp-tree/-/regexp-tree-0.1.27.tgz#2198f0ef54518ffa743fe74d983b56ffd631b6cd" + integrity sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA== + regexpu-core@^5.3.1: version "5.3.2" resolved "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz" @@ -7282,7 +7615,7 @@ regjsparser@^0.9.1: dependencies: jsesc "~0.5.0" -repeat-string@^1.6.1: +repeat-string@^1.5.2, repeat-string@^1.6.1: version "1.6.1" resolved "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz" integrity sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w== @@ -7314,6 +7647,13 @@ resolve-from@^4.0.0: resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz" integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== +resolve-pkg@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/resolve-pkg/-/resolve-pkg-2.0.0.tgz#ac06991418a7623edc119084edc98b0e6bf05a41" + integrity sha512-+1lzwXehGCXSeryaISr6WujZzowloigEofRB+dj75y9RRa/obVcYgbHJd53tdYw8pvZj8GojXaaENws8Ktw/hQ== + dependencies: + resolve-from "^5.0.0" + resolve.exports@^2.0.0: version "2.0.2" resolved "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz" @@ -7422,6 +7762,11 @@ schema-utils@^3.1.1, schema-utils@^3.2.0: ajv "^6.12.5" ajv-keywords "^3.5.2" +seed-random@~2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/seed-random/-/seed-random-2.2.0.tgz#2a9b19e250a817099231a5b99a4daf80b7fbed54" + integrity sha512-34EQV6AAHQGhoc0tn/96a9Fsi6v2xdqe/dMUwljGRaFOzR3EgRmECvD0O8vi8X+/uQ50LGHfkNu/Eue5TPKZkQ== + "semver@2 || 3 || 4 || 5", semver@^5.6.0: version "5.7.2" resolved "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz" @@ -7607,7 +7952,7 @@ source-map-support@0.5.13: buffer-from "^1.0.0" source-map "^0.6.0" -source-map-support@0.5.21, source-map-support@~0.5.20: +source-map-support@0.5.21, source-map-support@^0.5.21, source-map-support@~0.5.20: version "0.5.21" resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz" integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== @@ -7691,6 +8036,11 @@ stack-utils@^2.0.3: dependencies: escape-string-regexp "^2.0.0" +stackframe@^1.3.4: + version "1.3.4" + resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.3.4.tgz#b881a004c8c149a5e8efef37d51b16e412943310" + integrity sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw== + statuses@2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz" @@ -7701,6 +8051,11 @@ streamsearch@^1.1.0: resolved "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz" integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== +string-argv@0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.1.tgz#95e2fbec0427ae19184935f816d74aaa4c5c19da" + integrity sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg== + string-length@^4.0.1: version "4.0.2" resolved "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz" @@ -7741,7 +8096,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -7810,9 +8165,9 @@ supports-color@^7.1.0: dependencies: has-flag "^4.0.0" -supports-color@^8.0.0: +supports-color@^8.0.0, supports-color@^8.1.1: version "8.1.1" - resolved "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== dependencies: has-flag "^4.0.0" @@ -7928,6 +8283,20 @@ text-table@^0.2.0: resolved "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz" integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== +thenify-all@^1.0.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726" + integrity sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA== + dependencies: + thenify ">= 3.1.0 < 4" + +"thenify@>= 3.1.0 < 4": + version "3.3.1" + resolved "https://registry.yarnpkg.com/thenify/-/thenify-3.3.1.tgz#8932e686a4066038a016dd9e2ca46add9838a95f" + integrity sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw== + dependencies: + any-promise "^1.0.0" + through2@^2.0.0: version "2.0.5" resolved "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz" @@ -7941,11 +8310,21 @@ through@2, "through@>=2.2.7 <3", through@^2.3.4, through@^2.3.6: resolved "https://registry.npmjs.org/through/-/through-2.3.8.tgz" integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== +tiny-case@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tiny-case/-/tiny-case-1.0.3.tgz#d980d66bc72b5d5a9ca86fb7c9ffdb9c898ddd03" + integrity sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q== + titleize@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz" integrity sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ== +tmp@0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.3.tgz#eb783cc22bc1e8bebd0671476d46ea4eb32a79ae" + integrity sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w== + tmp@^0.0.33: version "0.0.33" resolved "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz" @@ -7982,6 +8361,11 @@ toidentifier@1.0.1: resolved "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz" integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== +toposort@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/toposort/-/toposort-2.0.2.tgz#ae21768175d1559d48bef35420b2f4962f09c330" + integrity sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg== + tr46@~0.0.3: version "0.0.3" resolved "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz" @@ -8049,6 +8433,11 @@ tslib@^1.8.1: resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== +tslib@^2.0.3: + version "2.6.3" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0" + integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ== + tsutils@^3.21.0: version "3.21.0" resolved "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz" @@ -8107,6 +8496,16 @@ type-fest@^0.8.1: resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== +type-fest@^2.19.0: + version "2.19.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b" + integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA== + +type-fest@^4.8.3: + version "4.20.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.20.1.tgz#d97bb1e923bf524e5b4b43421d586760fb2ee8be" + integrity sha512-R6wDsVsoS9xYOpy8vgeBlqpdOyzJ12HNfQhC/aAKWM3YoCV9TtunJzh/QpkMgeDhkoynDcw5f1y+qF9yc/HHyg== + type-is@^1.6.4, type-is@~1.6.18: version "1.6.18" resolved "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz" @@ -8226,6 +8625,13 @@ update-browserslist-db@^1.0.13: escalade "^3.1.1" picocolors "^1.0.0" +upper-case-first@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/upper-case-first/-/upper-case-first-2.0.2.tgz#992c3273f882abd19d1e02894cc147117f844324" + integrity sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg== + dependencies: + tslib "^2.0.3" + uri-js@^4.2.2: version "4.4.1" resolved "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz" @@ -8233,6 +8639,11 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" +util-arity@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/util-arity/-/util-arity-1.1.0.tgz#59d01af1fdb3fede0ac4e632b0ab5f6ce97c9330" + integrity sha512-kkyIsXKwemfSy8ZEoaIz06ApApnWsk5hQO0vLjZS6UkBiGiW++Jsyb8vSBoc0WKlffGoGs5yYy/j5pp8zckrFA== + util-deprecate@^1.0.1, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" @@ -8243,9 +8654,9 @@ utils-merge@1.0.1: resolved "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz" integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== -uuid@^9.0.0: +uuid@9.0.1, uuid@^9.0.0: version "9.0.1" - resolved "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== v8-compile-cache-lib@^3.0.1: @@ -8396,6 +8807,7 @@ wordwrap@^1.0.0: integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: + name wrap-ansi-cjs version "7.0.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -8473,6 +8885,11 @@ write-pkg@4.0.0: type-fest "^0.4.1" write-json-file "^3.2.0" +xmlbuilder@^15.1.1: + version "15.1.1" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-15.1.1.tgz#9dcdce49eea66d8d10b42cae94a79c3c8d0c2ec5" + integrity sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg== + xtend@^4.0.0, xtend@~4.0.1: version "4.0.2" resolved "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz" @@ -8493,6 +8910,11 @@ yallist@^4.0.0: resolved "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== +yaml@^2.2.2: + version "2.4.5" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.4.5.tgz#60630b206dd6d84df97003d33fc1ddf6296cca5e" + integrity sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg== + yargs-parser@20.2.4: version "20.2.4" resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz" @@ -8543,3 +8965,13 @@ yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + +yup@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/yup/-/yup-1.2.0.tgz#9e51af0c63bdfc9be0fdc6c10aa0710899d8aff6" + integrity sha512-PPqYKSAXjpRCgLgLKVGPA33v5c/WgEx3wi6NFjIiegz90zSwyMpvTFp/uGcVnnbx6to28pgnzp/q8ih3QRjLMQ== + dependencies: + property-expr "^2.0.5" + tiny-case "^1.0.3" + toposort "^2.0.2" + type-fest "^2.19.0" From 43cf0fa2c9568c5a7b0e9b4ce18f9f8096f5f48b Mon Sep 17 00:00:00 2001 From: Antoine SEIN <142824551+asein-sinch@users.noreply.github.com> Date: Fri, 28 Jun 2024 16:45:34 +0200 Subject: [PATCH 05/54] DEVEXP-475: E2E Fax/Services (#98) --- .../fax/tests/rest/v3/faxes/faxes.steps.ts | 31 +-- .../tests/rest/v3/services/services.steps.ts | 180 ++++++++++++++++++ 2 files changed, 196 insertions(+), 15 deletions(-) create mode 100644 packages/fax/tests/rest/v3/services/services.steps.ts diff --git a/packages/fax/tests/rest/v3/faxes/faxes.steps.ts b/packages/fax/tests/rest/v3/faxes/faxes.steps.ts index 666848a2..5ad540c6 100644 --- a/packages/fax/tests/rest/v3/faxes/faxes.steps.ts +++ b/packages/fax/tests/rest/v3/faxes/faxes.steps.ts @@ -1,9 +1,9 @@ import { Given, When, Then } from '@cucumber/cucumber'; import { FileBuffer, PageResult } from '@sinch/sdk-client'; import * as assert from 'assert'; -import { FaxService, Fax } from '../../../../src'; +import { FaxService, Fax, FaxesApi } from '../../../../src'; -let faxService: FaxService; +let faxesApi: FaxesApi; let listResponse: PageResult; const faxList: Fax.Fax[] = []; let sendFaxResponse: Fax.Fax[]; @@ -11,18 +11,19 @@ let fax: Fax.Fax; let fileBuffer: FileBuffer; let deleteContentResponse: void; -Given('the Fax service is available', function () { - faxService = new FaxService({ +Given('the Fax service "Faxes" is available', function () { + const faxService = new FaxService({ projectId: 'tinyfrog-jump-high-over-lilypadbasin', keyId: 'keyId', keySecret: 'keySecret', authHostname: 'http://localhost:3011', }); faxService.setHostname('http://localhost:3012'); + faxesApi = faxService.faxes; }); When('I send a fax with a contentUrl only to a single recipient', async function () { - sendFaxResponse = await faxService.faxes.send({ + sendFaxResponse = await faxesApi.send({ sendFaxRequestBody: { to: '+12015555555', contentUrl: 'https://developers.sinch.com/fax/fax.pdf', @@ -37,7 +38,7 @@ Then('the response contains a list of fax objects with a single element received }); When('I send a fax with a contentUrl only to multiple recipients', async function () { - sendFaxResponse = await faxService.faxes.send({ + sendFaxResponse = await faxesApi.send({ sendFaxRequestBody: { to: ['+12015555555', '+12016666666'], contentUrl: 'https://developers.sinch.com/fax/fax.pdf', @@ -53,7 +54,7 @@ Then('the response contains a list of fax objects with multiple elements receive }); When('I send a fax with a contentUrl and a binary file attachment to a single recipient', async function () { - sendFaxResponse = await faxService.faxes.send({ + sendFaxResponse = await faxesApi.send({ sendFaxRequestBody: { to: '+12015555555', contentUrl: 'https://developers.sinch.com/fax/fax.pdf', @@ -69,7 +70,7 @@ Then('the response contains a list of fax objects with a single element received }); When('I send a fax with a contentUrl and a binary file attachment to multiple recipients', async function () { - sendFaxResponse = await faxService.faxes.send({ + sendFaxResponse = await faxesApi.send({ sendFaxRequestBody: { to: ['+12015555555', '+12016666666'], contentUrl: 'https://developers.sinch.com/fax/fax.pdf', @@ -86,7 +87,7 @@ Then('the response contains a list of fax objects with multiple elements receive }); When('I send a fax with a contentUrl and a base64 file encoded to a single recipient', async function () { - sendFaxResponse = await faxService.faxes.send({ + sendFaxResponse = await faxesApi.send({ sendFaxRequestBody: { to: '+12015555555', contentUrl: 'https://developers.sinch.com/fax/fax.pdf', @@ -111,7 +112,7 @@ Then('the response contains a list of fax objects with a single element received }); When('I send a fax with a contentUrl and a base64 file encoded to multiple recipients', async function () { - sendFaxResponse = await faxService.faxes.send({ + sendFaxResponse = await faxesApi.send({ sendFaxRequestBody: { to: ['+12015555555', '+12016666666'], contentUrl: 'https://developers.sinch.com/fax/fax.pdf', @@ -137,7 +138,7 @@ Then('the response contains a list of fax objects with multiple elements receive }); When('I retrieve a fax', async function () { - fax = await faxService.faxes.get({ + fax = await faxesApi.get({ id: '01W4FFL35P4NC4K35CR3P35M1N1', }); }); @@ -167,7 +168,7 @@ Then('the response contains a fax object', function () { }); When('I send a request to list faxes', async function () { - listResponse = await faxService.faxes.list({}); + listResponse = await faxesApi.list({}); }); Then('the response contains {string} faxes', function (expectedAnswer: string) { @@ -176,7 +177,7 @@ Then('the response contains {string} faxes', function (expectedAnswer: string) { }); When('I send a request to list all the faxes', async function () { - for await (const fax of faxService.faxes.list({})) { + for await (const fax of faxesApi.list({})) { faxList.push(fax); } }); @@ -187,7 +188,7 @@ Then('the faxes list contains {string} faxes', function (expectedAnswer: string) }); When('I send a request to download a fax content as PDF', async function () { - fileBuffer = await faxService.faxes.downloadContent({ + fileBuffer = await faxesApi.downloadContent({ id: '01W4FFL35P4NC4K35CR3P35DWLD', }); }); @@ -197,7 +198,7 @@ Then('the response contains a PDF document', function () { }); When('I send a request to delete a fax content on the server', async function () { - deleteContentResponse = await faxService.faxes.deleteContent({ + deleteContentResponse = await faxesApi.deleteContent({ id: '01W4FFL35P4NC4K35CR3P35DEL0', }); }); diff --git a/packages/fax/tests/rest/v3/services/services.steps.ts b/packages/fax/tests/rest/v3/services/services.steps.ts new file mode 100644 index 00000000..60aa933b --- /dev/null +++ b/packages/fax/tests/rest/v3/services/services.steps.ts @@ -0,0 +1,180 @@ +import { FaxService, Fax, ServicesApi } from '../../../../src'; +import { Given, Then, When } from '@cucumber/cucumber'; +import * as assert from 'assert'; +import { PageResult } from '@sinch/sdk-client'; + +let servicesApi: ServicesApi; +let createServiceResponse: Fax.ServiceResponse; +let listResponse: PageResult; +const servicesList: Fax.ServiceResponse[] = []; +let service: Fax.ServiceResponse; +let deleteServiceResponse: void; +let listNumbersResponse: PageResult; +const numbersList: Fax.ServicePhoneNumber[] = []; +let listEmailsResponse: PageResult; +const emailsList: string[] = []; + +Given('the Fax service "Services" is available', function () { + const faxService = new FaxService({ + projectId: 'tinyfrog-jump-high-over-lilypadbasin', + keyId: 'keyId', + keySecret: 'keySecret', + authHostname: 'http://localhost:3011', + }); + faxService.setHostname('http://localhost:3012'); + servicesApi = faxService.services; +}); + +When('I send a request to create a new service', async function () { + createServiceResponse = await servicesApi.create({ + createServiceRequestBody: { + name: 'Fax service for e2e tests', + incomingWebhookUrl: 'https://my-callback-server.com/fax', + webhookContentType: 'application/json', + defaultForProject: false, + defaultFrom: '+12014444444', + numberOfRetries: 2, + retryDelaySeconds: 30, + imageConversionMethod: 'MONOCHROME', + saveInboundFaxDocuments: true, + saveOutboundFaxDocuments: true, + }, + }); +}); + +Then('the service is created', () => { + assert.equal(createServiceResponse.id, '01W4FFL35P4NC4K35FAXSERVICE'); +}); + +When('I send a request to list the existing services', async () => { + listResponse = await servicesApi.list({ + pageSize: 2, + }); +}); + +Then('the response contains {string} services', (expectedAnswer: string) => { + const expectedServices = parseInt(expectedAnswer, 10); + assert.equal(listResponse.data.length, expectedServices); +}); + +When('I send a request to list all the services', async () => { + for await (const service of servicesApi.list({ pageSize: 2 })) { + servicesList.push(service); + } +}); + +Then('the services list contains {string} services', (expectedAnswer: string) => { + const expectedServices = parseInt(expectedAnswer, 10); + assert.equal(servicesList.length, expectedServices); +}); + +When('I send a request to retrieve a service', async () => { + service = await servicesApi.get({ + serviceId: '01W4FFL35P4NC4K35FAXSERVICE', + }); +}); + +Then('the response contains a service object', () => { + assert.equal('01W4FFL35P4NC4K35FAXSERVICE', service.id); + assert.equal('application/json', service.webhookContentType); + assert.equal('+12014444444', service.defaultFrom); + assert.equal(2, service.numberOfRetries); + assert.equal(30, service.retryDelaySeconds); + assert.equal('MONOCHROME', service.imageConversionMethod); + assert.equal('123coffee-dada-beef-cafe-baadc0de5678', service.projectId); + assert.equal(false, service.defaultForProject); + assert.equal(true, service.saveInboundFaxDocuments); + assert.equal(true, service.saveOutboundFaxDocuments); + assert.equal('Fax service for e2e tests', service.name); + assert.equal('https://my-callback-server.com/fax', service.incomingWebhookUrl); +}); + +When('I send a request to update a service', async () => { + service = await servicesApi.update({ + serviceId: '01W4FFL35P4NC4K35FAXSERVICE', + updateServiceRequestBody: { + name: 'Updated Fax service name', + webhookContentType: 'multipart/form-data', + defaultForProject: true, + numberOfRetries: 3, + retryDelaySeconds: 60, + imageConversionMethod: 'HALFTONE', + saveOutboundFaxDocuments: false, + saveInboundFaxDocuments: false, + }, + }); +}); + +Then('the response contains a service with updated parameters', () => { + assert.equal('01W4FFL35P4NC4K35FAXSERVICE', service.id); + assert.equal('multipart/form-data', service.webhookContentType); + assert.equal(3, service.numberOfRetries); + assert.equal(60, service.retryDelaySeconds); + assert.equal('HALFTONE', service.imageConversionMethod); + assert.equal(true, service.defaultForProject); + assert.equal(false, service.saveInboundFaxDocuments); + assert.equal(false, service.saveOutboundFaxDocuments); + assert.equal('Updated Fax service name', service.name); +}); + +When('I send a request to remove a service', async () => { + deleteServiceResponse = await servicesApi.delete({ + serviceId: '01W4FFL35P4NC4K35FAXSERVICE', + }); +}); + +Then('the delete service response contains no data', function () { + assert.deepEqual(deleteServiceResponse, {}); +}); + +When('I send a request to list the numbers associated to a fax service', async () => { + listNumbersResponse = await servicesApi.listNumbers({ + serviceId: '01W4FFL35P4NC4K35FAXSERVICE', + }); +}); + +Then('the response contains {string} numbers associated to the fax service', function (expectedAnswer: string) { + const expectedNumbers = parseInt(expectedAnswer, 10); + assert.strictEqual(listNumbersResponse.data.length, expectedNumbers); +}); + +When('I send a request to list all the numbers associated to a fax service', async () => { + listNumbersResponse = await servicesApi.listNumbers({ + serviceId: '01W4FFL35P4NC4K35FAXSERVICE', + }); + for await (const number of servicesApi.listNumbers({ serviceId: '01W4FFL35P4NC4K35FAXSERVICE' })) { + numbersList.push(number); + } +}); + +// eslint-disable-next-line max-len +Then('the phone numbers list contains {string} numbers associated to the fax service', function (expectedAnswer: string) { + const expectedNumbers = parseInt(expectedAnswer, 10); + assert.strictEqual(numbersList.length, expectedNumbers); +}); + +When('I send a request to list the emails associated to a phone number', async () => { + listEmailsResponse = await servicesApi.listEmailsForNumber({ + serviceId: '01W4FFL35P4NC4K35FAXSERVICE', + phoneNumber: '+12014444444', + }); +}); + +Then('the response contains {string} emails associated to the phone number', function (expectedAnswer: string) { + const expectedNumbers = parseInt(expectedAnswer, 10); + assert.strictEqual(listEmailsResponse.data.length, expectedNumbers); +}); + +When('I send a request to list all the emails associated to a phone number', async () => { + for await (const email of servicesApi.listEmailsForNumber({ + serviceId: '01W4FFL35P4NC4K35FAXSERVICE', + phoneNumber: '+12014444444' }) + ) { + emailsList.push(email); + } +}); + +Then('the emails list contains {string} emails associated to a phone number', function (expectedAnswer: string) { + const expectedNumbers = parseInt(expectedAnswer, 10); + assert.strictEqual(emailsList.length, expectedNumbers); +}); From ea14098579347949cae7ca6b501d17f6b12ee035 Mon Sep 17 00:00:00 2001 From: Antoine SEIN <142824551+asein-sinch@users.noreply.github.com> Date: Tue, 2 Jul 2024 10:30:46 +0200 Subject: [PATCH 06/54] DEVEXP-476: E2E Fax/Emails (#99) --- .../rest/v3/emails/emails-api.jest.fixture.ts | 13 +- packages/fax/src/rest/v3/emails/emails-api.ts | 16 +- .../tests/rest/v3/emails/emails-api.test.ts | 29 ++++ .../fax/tests/rest/v3/emails/emails.steps.ts | 141 ++++++++++++++++++ .../fax/tests/rest/v3/faxes/faxes.steps.ts | 48 +++--- .../tests/rest/v3/services/services.steps.ts | 18 +-- 6 files changed, 225 insertions(+), 40 deletions(-) create mode 100644 packages/fax/tests/rest/v3/emails/emails.steps.ts diff --git a/packages/fax/src/rest/v3/emails/emails-api.jest.fixture.ts b/packages/fax/src/rest/v3/emails/emails-api.jest.fixture.ts index 33c9f31b..75f343ef 100644 --- a/packages/fax/src/rest/v3/emails/emails-api.jest.fixture.ts +++ b/packages/fax/src/rest/v3/emails/emails-api.jest.fixture.ts @@ -2,6 +2,7 @@ import { AddEmailToNumbersRequestData, DeleteEmailRequestData, Email, + ListEmailsForNumberRequestData, ListEmailsForProjectRequestData, ListNumbersByEmailRequestData, ServicePhoneNumber, @@ -17,19 +18,23 @@ export class EmailsApiFixture implements Partial> { */ public addToNumbers: jest.Mock, [AddEmailToNumbersRequestData]> = jest.fn(); /** - * Fixture associated to function deleteEmail + * Fixture associated to function delete */ public delete: jest.Mock, [DeleteEmailRequestData]> = jest.fn(); /** - * Fixture associated to function getEmailsForProject + * Fixture associated to function list */ public list: jest.Mock, [ListEmailsForProjectRequestData]> = jest.fn(); /** - * Fixture associated to function getNumbersByEmail + * Fixture associated to function listForNumber + */ + public listForNumber: jest.Mock, [ListEmailsForNumberRequestData]> = jest.fn(); + /** + * Fixture associated to function listNumbers */ public listNumbers: jest.Mock, [ListNumbersByEmailRequestData]> = jest.fn(); /** - * Fixture associated to function updateEmail + * Fixture associated to function update */ public update: jest.Mock, [UpdateEmailRequestData]> = jest.fn(); } diff --git a/packages/fax/src/rest/v3/emails/emails-api.ts b/packages/fax/src/rest/v3/emails/emails-api.ts index c15e5c74..a873d393 100644 --- a/packages/fax/src/rest/v3/emails/emails-api.ts +++ b/packages/fax/src/rest/v3/emails/emails-api.ts @@ -2,6 +2,7 @@ import { AddEmailToNumbersRequestData, DeleteEmailRequestData, Email, + ListEmailsForNumberRequestData, ListEmailsForProjectRequestData, ListNumbersByEmailRequestData, ServicePhoneNumber, @@ -17,9 +18,12 @@ import { SinchClientParameters, } from '@sinch/sdk-client'; import { FaxDomainApi } from '../fax-domain-api'; +import { ServicesApi } from '../services'; export class EmailsApi extends FaxDomainApi { + private servicesApi: ServicesApi; + /** * Initialize your interface * @@ -27,6 +31,7 @@ export class EmailsApi extends FaxDomainApi { */ constructor(sinchClientParameters: SinchClientParameters) { super(sinchClientParameters, 'EmailsApi'); + this.servicesApi = new ServicesApi(sinchClientParameters); } /** @@ -126,6 +131,16 @@ export class EmailsApi extends FaxDomainApi { return listPromise as ApiListPromise; } + /** + * List emails for a number + * List any emails for a number. + * @param { ListEmailsForNumberRequestData } data - The data to provide to the API call. + * @return {ApiListPromise} - The list of emails for a given number + */ + public listForNumber(data: ListEmailsForNumberRequestData): ApiListPromise { + return this.servicesApi.listEmailsForNumber(data); + } + /** * Get numbers for email * Get configured numbers for an email @@ -135,7 +150,6 @@ export class EmailsApi extends FaxDomainApi { public listNumbers(data: ListNumbersByEmailRequestData): ApiListPromise { this.client = this.getSinchClient(); const getParams = this.client.extractQueryParams(data, [ - 'email', 'pageSize', 'page']); const headers: { [key: string]: string | undefined } = { diff --git a/packages/fax/tests/rest/v3/emails/emails-api.test.ts b/packages/fax/tests/rest/v3/emails/emails-api.test.ts index 551efd8d..f574c57a 100644 --- a/packages/fax/tests/rest/v3/emails/emails-api.test.ts +++ b/packages/fax/tests/rest/v3/emails/emails-api.test.ts @@ -100,6 +100,35 @@ describe('EmailsApi', () => { }); }); + describe('getEmailsForNumber', () => { + it('should make a GET request to list the emails for a number', async () => { + // Given + const requestData: Fax.ListEmailsForNumberRequestData = { + serviceId: 'serviceId', + phoneNumber: '+15551235656', + }; + const mockData: string[] = [ + 'user@example.com', + ]; + const expectedResponse = { + data: mockData, + hasNextPage: false, + nextPageValue: '', + nextPage: jest.fn(), + }; + + // When + fixture.listForNumber.mockResolvedValue(expectedResponse); + emailsApi.listForNumber = fixture.listForNumber; + const response = await emailsApi.listForNumber(requestData); + + // Then + expect(response).toEqual(expectedResponse); + expect(response.data).toBeDefined(); + expect(fixture.listForNumber).toHaveBeenCalledWith(requestData); + }); + }); + describe ('getNumbersByEmail', () => { it('should make a GET request to list the configured numbers for an email', async () => { // Given diff --git a/packages/fax/tests/rest/v3/emails/emails.steps.ts b/packages/fax/tests/rest/v3/emails/emails.steps.ts new file mode 100644 index 00000000..ff39fe47 --- /dev/null +++ b/packages/fax/tests/rest/v3/emails/emails.steps.ts @@ -0,0 +1,141 @@ +import { EmailsApi, Fax, FaxService } from '../../../../src'; +import { Given, Then, When } from '@cucumber/cucumber'; +import * as assert from 'assert'; +import { PageResult } from '@sinch/sdk-client'; + +let emailsApi: EmailsApi; +let listEmailsResponse: PageResult; +const emailsList: string[] = []; +let listFaxEmails: PageResult; +const faxEmailsList: Fax.Email[] = []; +let email: Fax.Email; +let deleteEmailResponse: void; +let listNumbersResponse: PageResult; +const numbersList: Fax.ServicePhoneNumber[] = []; + +Given('the Fax service "Emails" is available', () => { + const faxService = new FaxService({ + projectId: 'tinyfrog-jump-high-over-lilypadbasin', + keyId: 'keyId', + keySecret: 'keySecret', + authHostname: 'http://localhost:3011', + faxHostname: 'http://localhost:3012', + }); + emailsApi = faxService.emails; +}); + +When('I send a request to list the emails associated to a phone number via the "Emails" Service', async () => { + listEmailsResponse = await emailsApi.listForNumber({ + serviceId: '01W4FFL35P4NC4K35FAXSERVICE', + phoneNumber: '+12014444444', + }); +}); + +// eslint-disable-next-line max-len +Then('the "Emails" Service response contains {string} emails associated to the phone number', (expectedAnswer: string) => { + // To implement + const expectedNumbers = parseInt(expectedAnswer, 10); + assert.equal(listEmailsResponse.data.length, expectedNumbers); +}); + +When('I send a request to list all the emails associated to a phone number via the "Emails" Service', async () => { + for await (const email of emailsApi.listForNumber({ + serviceId: '01W4FFL35P4NC4K35FAXSERVICE', + phoneNumber: '+12014444444' }) + ) { + emailsList.push(email); + } +}); + +// eslint-disable-next-line max-len +Then('the emails list from the "Emails" Service contains {string} emails associated to a phone number', (expectedAnswer: string) => { + // To implement + const expectedNumbers = parseInt(expectedAnswer, 10); + assert.equal(emailsList.length, expectedNumbers); +}); + +When('I send a request to list the emails associated to the project', async () => { + listFaxEmails = await emailsApi.list({}); +}); + +Then('the response contains {string} emails associated to the project', (expectedAnswer: string) => { + const expectedNumbers = parseInt(expectedAnswer, 10); + assert.equal(listFaxEmails.data.length, expectedNumbers); +}); + +When('I send a request to list all the emails associated to the project', async () => { + for await (const email of emailsApi.list({})) { + faxEmailsList.push(email); + } +}); + +Then('the emails list contains {string} emails associated to the project', (expectedAnswer: string) => { + const expectedNumbers = parseInt(expectedAnswer, 10); + assert.equal(faxEmailsList.length, expectedNumbers); +}); + +When('I send a request to add a new email to the project', async () => { + email = await emailsApi.addToNumbers({ + emailRequestBody : { + email: 'spaceship@galaxy.far.far.away', + phoneNumbers: ['+12016666666'], + }, + }); +}); + +Then('the response contains the added email', () => { + assert.equal(email.email, 'spaceship@galaxy.far.far.away'); + assert.deepEqual(email.phoneNumbers, ['+12016666666']); + assert.equal(email.projectId, '123c0ffee-dada-beef-cafe-baadc0de5678'); +}); + +When('I send a request to update the phone numbers associated to an email', async () => { + email = await emailsApi.update({ + email: 'spaceship@galaxy.far.far.away', + updateEmailRequestBody: { + phoneNumbers: [ + '+12016666666', + '+12017777777', + ], + }, + }); +}); + +Then('the response contains the updated email', () => { + assert.equal(email.email, 'spaceship@galaxy.far.far.away'); + assert.deepEqual(email.phoneNumbers, ['+12016666666', '+12017777777']); + assert.equal(email.projectId, '123c0ffee-dada-beef-cafe-baadc0de5678'); +}); + +When('I send a request to delete an email from the project', async () => { + deleteEmailResponse = await emailsApi.delete({ + email: 'spaceship@galaxy.far.far.away', + }); +}); + +Then('the delete email response contains no data', () => { + assert.deepEqual(deleteEmailResponse, {}); +}); + +When('I send a request to list the phone numbers associated to an email', async () => { + listNumbersResponse = await emailsApi.listNumbers({ + email: 'cookie.monster@nom.nom', + pageSize: 2, + }); +}); + +Then('the response contains {string} phone numbers associated to the email', (expectedAnswer: string) => { + const expectedNumbers = parseInt(expectedAnswer, 10); + assert.strictEqual(listNumbersResponse.data.length, expectedNumbers); +}); + +When('I send a request to list all the phone numbers associated to an email', async () => { + for await (const number of emailsApi.listNumbers({ email: 'cookie.monster@nom.nom' })) { + numbersList.push(number); + } +}); + +Then('the phone numbers list contains {string} phone numbers associated to the email', (expectedAnswer: string) => { + const expectedNumbers = parseInt(expectedAnswer, 10); + assert.strictEqual(numbersList.length, expectedNumbers); +}); diff --git a/packages/fax/tests/rest/v3/faxes/faxes.steps.ts b/packages/fax/tests/rest/v3/faxes/faxes.steps.ts index 5ad540c6..a2386fce 100644 --- a/packages/fax/tests/rest/v3/faxes/faxes.steps.ts +++ b/packages/fax/tests/rest/v3/faxes/faxes.steps.ts @@ -11,18 +11,18 @@ let fax: Fax.Fax; let fileBuffer: FileBuffer; let deleteContentResponse: void; -Given('the Fax service "Faxes" is available', function () { +Given('the Fax service "Faxes" is available', () => { const faxService = new FaxService({ projectId: 'tinyfrog-jump-high-over-lilypadbasin', keyId: 'keyId', keySecret: 'keySecret', authHostname: 'http://localhost:3011', + faxHostname: 'http://localhost:3012', }); - faxService.setHostname('http://localhost:3012'); faxesApi = faxService.faxes; }); -When('I send a fax with a contentUrl only to a single recipient', async function () { +When('I send a fax with a contentUrl only to a single recipient', async () => { sendFaxResponse = await faxesApi.send({ sendFaxRequestBody: { to: '+12015555555', @@ -32,12 +32,12 @@ When('I send a fax with a contentUrl only to a single recipient', async function }); // eslint-disable-next-line max-len -Then('the response contains a list of fax objects with a single element received from a multipart-form-data request with contentUrl only', function () { +Then('the response contains a list of fax objects with a single element received from a multipart-form-data request with contentUrl only', () => { assert.equal(sendFaxResponse.length, 1); assert.equal('01W4FFL35P4NC4K35URLSINGLE1', sendFaxResponse[0].id); }); -When('I send a fax with a contentUrl only to multiple recipients', async function () { +When('I send a fax with a contentUrl only to multiple recipients', async () => { sendFaxResponse = await faxesApi.send({ sendFaxRequestBody: { to: ['+12015555555', '+12016666666'], @@ -47,13 +47,13 @@ When('I send a fax with a contentUrl only to multiple recipients', async functio }); // eslint-disable-next-line max-len -Then('the response contains a list of fax objects with multiple elements received from a multipart-form-data request with contentUrl only', function () { +Then('the response contains a list of fax objects with multiple elements received from a multipart-form-data request with contentUrl only', () => { assert.equal(sendFaxResponse.length, 2); assert.equal('01W4FFL35P4NC4K35URLMULTI01', sendFaxResponse[0].id); assert.equal('01W4FFL35P4NC4K35URLMULTI02', sendFaxResponse[1].id); }); -When('I send a fax with a contentUrl and a binary file attachment to a single recipient', async function () { +When('I send a fax with a contentUrl and a binary file attachment to a single recipient', async () => { sendFaxResponse = await faxesApi.send({ sendFaxRequestBody: { to: '+12015555555', @@ -64,12 +64,12 @@ When('I send a fax with a contentUrl and a binary file attachment to a single re }); // eslint-disable-next-line max-len -Then('the response contains a list of fax objects with a single element received from a multipart-form-data request', function () { +Then('the response contains a list of fax objects with a single element received from a multipart-form-data request', () => { assert.equal(sendFaxResponse.length, 1); assert.equal('01W4FFL35P4NC4K35BINSINGLE1', sendFaxResponse[0].id); }); -When('I send a fax with a contentUrl and a binary file attachment to multiple recipients', async function () { +When('I send a fax with a contentUrl and a binary file attachment to multiple recipients', async () => { sendFaxResponse = await faxesApi.send({ sendFaxRequestBody: { to: ['+12015555555', '+12016666666'], @@ -80,13 +80,13 @@ When('I send a fax with a contentUrl and a binary file attachment to multiple re }); // eslint-disable-next-line max-len -Then('the response contains a list of fax objects with multiple elements received from a multipart-form-data request', function () { +Then('the response contains a list of fax objects with multiple elements received from a multipart-form-data request', () => { assert.equal(sendFaxResponse.length, 2); assert.equal('01W4FFL35P4NC4K35BINMULTI01', sendFaxResponse[0].id); assert.equal('01W4FFL35P4NC4K35BINMULTI02', sendFaxResponse[1].id); }); -When('I send a fax with a contentUrl and a base64 file encoded to a single recipient', async function () { +When('I send a fax with a contentUrl and a base64 file encoded to a single recipient', async () => { sendFaxResponse = await faxesApi.send({ sendFaxRequestBody: { to: '+12015555555', @@ -106,12 +106,12 @@ When('I send a fax with a contentUrl and a base64 file encoded to a single recip }); // eslint-disable-next-line max-len -Then('the response contains a list of fax objects with a single element received from an application-json request', function () { +Then('the response contains a list of fax objects with a single element received from an application-json request', () => { assert.equal(sendFaxResponse.length, 1); assert.equal('01W4FFL35P4NC4K35B64SINGLE1', sendFaxResponse[0].id); }); -When('I send a fax with a contentUrl and a base64 file encoded to multiple recipients', async function () { +When('I send a fax with a contentUrl and a base64 file encoded to multiple recipients', async () => { sendFaxResponse = await faxesApi.send({ sendFaxRequestBody: { to: ['+12015555555', '+12016666666'], @@ -131,19 +131,19 @@ When('I send a fax with a contentUrl and a base64 file encoded to multiple recip }); // eslint-disable-next-line max-len -Then('the response contains a list of fax objects with multiple elements received from an application-json request', function () { +Then('the response contains a list of fax objects with multiple elements received from an application-json request', () => { assert.equal(sendFaxResponse.length, 2); assert.equal('01W4FFL35P4NC4K35B64MULTI01', sendFaxResponse[0].id); assert.equal('01W4FFL35P4NC4K35B64MULTI02', sendFaxResponse[1].id); }); -When('I retrieve a fax', async function () { +When('I retrieve a fax', async () => { fax = await faxesApi.get({ id: '01W4FFL35P4NC4K35CR3P35M1N1', }); }); -Then('the response contains a fax object', function () { +Then('the response contains a fax object', () => { assert.equal('01W4FFL35P4NC4K35CR3P35M1N1', fax.id); assert.equal('OUTBOUND', fax.direction); assert.equal('+12014444444', fax.from); @@ -167,42 +167,42 @@ Then('the response contains a fax object', function () { assert.equal(true, fax.hasFile); }); -When('I send a request to list faxes', async function () { +When('I send a request to list faxes', async () => { listResponse = await faxesApi.list({}); }); -Then('the response contains {string} faxes', function (expectedAnswer: string) { +Then('the response contains {string} faxes', (expectedAnswer: string) => { const expectedFaxes = parseInt(expectedAnswer, 10); assert.strictEqual(listResponse.data.length, expectedFaxes); }); -When('I send a request to list all the faxes', async function () { +When('I send a request to list all the faxes', async () => { for await (const fax of faxesApi.list({})) { faxList.push(fax); } }); -Then('the faxes list contains {string} faxes', function (expectedAnswer: string) { +Then('the faxes list contains {string} faxes', (expectedAnswer: string) => { const expectedFaxes = parseInt(expectedAnswer, 10); assert.strictEqual(faxList.length, expectedFaxes); }); -When('I send a request to download a fax content as PDF', async function () { +When('I send a request to download a fax content as PDF', async () => { fileBuffer = await faxesApi.downloadContent({ id: '01W4FFL35P4NC4K35CR3P35DWLD', }); }); -Then('the response contains a PDF document', function () { +Then('the response contains a PDF document', () => { assert.equal(fileBuffer.fileName, '01W4FFL35P4NC4K35CR3P35DWLD.pdf'); }); -When('I send a request to delete a fax content on the server', async function () { +When('I send a request to delete a fax content on the server', async () => { deleteContentResponse = await faxesApi.deleteContent({ id: '01W4FFL35P4NC4K35CR3P35DEL0', }); }); -Then('the response contains no data', function () { +Then('the response contains no data', () => { assert.deepEqual(deleteContentResponse, {}); }); diff --git a/packages/fax/tests/rest/v3/services/services.steps.ts b/packages/fax/tests/rest/v3/services/services.steps.ts index 60aa933b..29f78705 100644 --- a/packages/fax/tests/rest/v3/services/services.steps.ts +++ b/packages/fax/tests/rest/v3/services/services.steps.ts @@ -14,18 +14,18 @@ const numbersList: Fax.ServicePhoneNumber[] = []; let listEmailsResponse: PageResult; const emailsList: string[] = []; -Given('the Fax service "Services" is available', function () { +Given('the Fax service "Services" is available', () => { const faxService = new FaxService({ projectId: 'tinyfrog-jump-high-over-lilypadbasin', keyId: 'keyId', keySecret: 'keySecret', authHostname: 'http://localhost:3011', + faxHostname: 'http://localhost:3012', }); - faxService.setHostname('http://localhost:3012'); servicesApi = faxService.services; }); -When('I send a request to create a new service', async function () { +When('I send a request to create a new service', async () => { createServiceResponse = await servicesApi.create({ createServiceRequestBody: { name: 'Fax service for e2e tests', @@ -133,22 +133,18 @@ When('I send a request to list the numbers associated to a fax service', async ( }); }); -Then('the response contains {string} numbers associated to the fax service', function (expectedAnswer: string) { +Then('the response contains {string} numbers associated to the fax service', (expectedAnswer: string) => { const expectedNumbers = parseInt(expectedAnswer, 10); assert.strictEqual(listNumbersResponse.data.length, expectedNumbers); }); When('I send a request to list all the numbers associated to a fax service', async () => { - listNumbersResponse = await servicesApi.listNumbers({ - serviceId: '01W4FFL35P4NC4K35FAXSERVICE', - }); for await (const number of servicesApi.listNumbers({ serviceId: '01W4FFL35P4NC4K35FAXSERVICE' })) { numbersList.push(number); } }); -// eslint-disable-next-line max-len -Then('the phone numbers list contains {string} numbers associated to the fax service', function (expectedAnswer: string) { +Then('the phone numbers list contains {string} numbers associated to the fax service', (expectedAnswer: string) => { const expectedNumbers = parseInt(expectedAnswer, 10); assert.strictEqual(numbersList.length, expectedNumbers); }); @@ -160,7 +156,7 @@ When('I send a request to list the emails associated to a phone number', async ( }); }); -Then('the response contains {string} emails associated to the phone number', function (expectedAnswer: string) { +Then('the response contains {string} emails associated to the phone number', (expectedAnswer: string) => { const expectedNumbers = parseInt(expectedAnswer, 10); assert.strictEqual(listEmailsResponse.data.length, expectedNumbers); }); @@ -174,7 +170,7 @@ When('I send a request to list all the emails associated to a phone number', asy } }); -Then('the emails list contains {string} emails associated to a phone number', function (expectedAnswer: string) { +Then('the emails list contains {string} emails associated to a phone number', (expectedAnswer: string) => { const expectedNumbers = parseInt(expectedAnswer, 10); assert.strictEqual(emailsList.length, expectedNumbers); }); From 7a0d380c121b0bc4e3a16d974987123a9deff9d1 Mon Sep 17 00:00:00 2001 From: Antoine SEIN <142824551+asein-sinch@users.noreply.github.com> Date: Tue, 2 Jul 2024 11:00:57 +0200 Subject: [PATCH 07/54] DEVEXP-474: E2E Numbers/Available regions (#100) --- .github/workflows/run-ci.yaml | 6 ++ packages/numbers/cucumber.js | 8 +++ packages/numbers/package.json | 3 +- .../available-regions.steps.ts | 55 +++++++++++++++++++ 4 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 packages/numbers/cucumber.js create mode 100644 packages/numbers/tests/rest/v1/available-regions/available-regions.steps.ts diff --git a/.github/workflows/run-ci.yaml b/.github/workflows/run-ci.yaml index 4e80b292..e51156b2 100644 --- a/.github/workflows/run-ci.yaml +++ b/.github/workflows/run-ci.yaml @@ -51,12 +51,18 @@ jobs: echo "Waiting for fax-server to be healthy..." sleep 2 done + while ! docker inspect --format='{{json .State.Health.Status}}' $(docker-compose ps -q numbers-server) | grep -q '"healthy"'; do + echo "Waiting for numbers-server to be healthy..." + sleep 2 + done - name: Create target directories for feature files run: | mkdir -p ./packages/fax/tests/e2e/features + mkdir -p ./packages/numbers/tests/e2e/features - name: Copy feature files run: | cp sinch-sdk-mockserver/features/fax/*.feature ./packages/fax/tests/e2e/features/ + cp sinch-sdk-mockserver/features/numbers/*.feature ./packages/numbers/tests/e2e/features/ - name: Run e2e tests run: | yarn install diff --git a/packages/numbers/cucumber.js b/packages/numbers/cucumber.js new file mode 100644 index 00000000..691a9809 --- /dev/null +++ b/packages/numbers/cucumber.js @@ -0,0 +1,8 @@ +module.exports = { + default: [ + 'tests/e2e/features/**/*.feature', + '--require-module ts-node/register', + '--require tests/rest/v1/**/*.steps.ts', + `--format-options '{"snippetInterface": "synchronous"}'`, + ].join(' '), +}; diff --git a/packages/numbers/package.json b/packages/numbers/package.json index 2990cf73..785e8675 100644 --- a/packages/numbers/package.json +++ b/packages/numbers/package.json @@ -25,7 +25,8 @@ "scripts": { "build": "yarn run clean && yarn run compile", "clean": "rimraf dist tsconfig.tsbuildinfo", - "compile": "tsc -p tsconfig.build.json && tsc -p tsconfig.tests.json && rimraf dist/tests && rimraf tsconfig.build.tsbuildinfo" + "compile": "tsc -p tsconfig.build.json && tsc -p tsconfig.tests.json && rimraf dist/tests && rimraf tsconfig.build.tsbuildinfo", + "test:e2e": "cucumber-js" }, "dependencies": { "@sinch/sdk-client": "^1.1.0" diff --git a/packages/numbers/tests/rest/v1/available-regions/available-regions.steps.ts b/packages/numbers/tests/rest/v1/available-regions/available-regions.steps.ts new file mode 100644 index 00000000..db5686eb --- /dev/null +++ b/packages/numbers/tests/rest/v1/available-regions/available-regions.steps.ts @@ -0,0 +1,55 @@ +import { AvailableRegionsApi, NumbersService, Numbers } from '../../../../src'; +import { Given, Then, When } from '@cucumber/cucumber'; +import * as assert from 'assert'; + +let regionsApi: AvailableRegionsApi; +let regions: Numbers.ListAvailableRegionsResponse; + +const countRegionType = (regions: Numbers.AvailableRegion[], type: Numbers.RegionNumberTypeEnum) => { + return regions.reduce((count, region) => { + return region.types?.includes(type) ? count + 1 : count; + }, 0); +}; + +Given('the Numbers service "Regions" is available', function () { + const numbersService = new NumbersService({ + projectId: 'tinyfrog-jump-high-over-lilypadbasin', + keyId: 'keyId', + keySecret: 'keySecret', + authHostname: 'http://localhost:3011', + numbersHostname: 'http://localhost:3013', + }); + regionsApi = numbersService.availableRegions; +}); + +When('I send a request to list all the regions', async () => { + regions = await regionsApi.list({}); +}); + +When('I send a request to list the TOLL_FREE regions', async () => { + regions = await regionsApi.list({ types: ['TOLL_FREE'] }); +}); + +When('I send a request to list the TOLL_FREE or MOBILE regions', async () => { + regions = await regionsApi.list({ types: ['TOLL_FREE', 'MOBILE'] }); +}); + +Then('the response contains {string} regions', (expectedAnswer: string) => { + const expectedRegionsNumber = parseInt(expectedAnswer, 10); + assert.equal(regions.availableRegions?.length, expectedRegionsNumber); +}); + +Then('the response contains {string} TOLL_FREE regions', (expectedAnswer: string) => { + const expectedRegionsNumber = parseInt(expectedAnswer, 10); + assert.equal(countRegionType(regions.availableRegions!, 'TOLL_FREE'), expectedRegionsNumber); +}); + +Then('the response contains {string} MOBILE regions', (expectedAnswer: string) => { + const expectedRegionsNumber = parseInt(expectedAnswer, 10); + assert.equal(countRegionType(regions.availableRegions!, 'MOBILE'), expectedRegionsNumber); +}); + +Then('the response contains {string} LOCAL regions', (expectedAnswer: string) => { + const expectedRegionsNumber = parseInt(expectedAnswer, 10); + assert.equal(countRegionType(regions.availableRegions!, 'LOCAL'), expectedRegionsNumber); +}); From d4bcf0018f42a7cfc1b899c75fdc70372a8b401e Mon Sep 17 00:00:00 2001 From: Antoine SEIN <142824551+asein-sinch@users.noreply.github.com> Date: Fri, 5 Jul 2024 17:04:48 +0200 Subject: [PATCH 08/54] DEVEXP-363: E2E Numbers/Available number (#101) --- .../available-number.steps.ts | 153 ++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 packages/numbers/tests/rest/v1/available-number/available-number.steps.ts diff --git a/packages/numbers/tests/rest/v1/available-number/available-number.steps.ts b/packages/numbers/tests/rest/v1/available-number/available-number.steps.ts new file mode 100644 index 00000000..6f7b3150 --- /dev/null +++ b/packages/numbers/tests/rest/v1/available-number/available-number.steps.ts @@ -0,0 +1,153 @@ +import { AvailableNumberApi, NumbersService, Numbers } from '../../../../src'; +import { Given, Then, When } from '@cucumber/cucumber'; +import assert from 'assert'; + +let availableNumberApi: AvailableNumberApi; +let availableNumbersResponse: Numbers.AvailableNumbersResponse; +let availablePhoneNumber: Numbers.AvailableNumber; +let activeNumber: Numbers.ActiveNumber; + +Given('the Numbers service "Available Number" is available', function () { + const numbersService = new NumbersService({ + projectId: 'tinyfrog-jump-high-over-lilypadbasin', + keyId: 'keyId', + keySecret: 'keySecret', + authHostname: 'http://localhost:3011', + numbersHostname: 'http://localhost:3013', + }); + availableNumberApi = numbersService.availableNumber; +}); + +When('I send a request to list the available phone numbers', async () => { + availableNumbersResponse = await availableNumberApi.list({ + regionCode: 'US', + type: 'LOCAL', + }); +}); + +Then('the response contains {string} available phone numbers', (expectedAnswer: string) => { + const expectedPhoneNumbersCount = parseInt(expectedAnswer, 10); + assert.equal(availableNumbersResponse.availableNumbers?.length, expectedPhoneNumbersCount); +}); + +Then('a phone number contains all the expected properties', () => { + const phoneNumber = availableNumbersResponse.availableNumbers![0]; + assert.equal(phoneNumber.phoneNumber, '+12013504948'); + assert.equal(phoneNumber.regionCode, 'US'); + assert.equal(phoneNumber.type, 'LOCAL'); + assert.deepEqual(phoneNumber.capability, ['SMS', 'VOICE']); + assert.deepEqual(phoneNumber.setupPrice, { currencyCode: 'EUR', amount: '0.80' }); + assert.deepEqual(phoneNumber.monthlyPrice, { currencyCode: 'EUR', amount: '0.80' }); + assert.equal(phoneNumber.paymentIntervalMonths, 1); + assert.equal(phoneNumber.supportingDocumentationRequired, true); +}); + +When('I send a request to check the availability of the phone number {string}', async (phoneNumber: string) => { + availablePhoneNumber = await availableNumberApi.checkAvailability({ phoneNumber }); +}); + +Then('the response displays the phone number {string} details', (phoneNumber: string) => { + assert.equal(availablePhoneNumber.phoneNumber, phoneNumber); +}); + +Then('the response contains an error about the number {string} not being available', (phoneNumber: string) => { + const notFound = availablePhoneNumber as Numbers.NotFound; + const notFoundError = notFound.error!; + assert.equal(notFoundError.code, 404); + assert.equal(notFoundError.status, 'NOT_FOUND'); + assert.equal((notFoundError.details![0] as any).resourceName, phoneNumber); +}); + +When('I send a request to rent a number with some criteria', async () => { + activeNumber = await availableNumberApi.rentAny({ + rentAnyNumberRequestBody: { + regionCode: 'US', + type: 'LOCAL', + capabilities: ['SMS', 'VOICE'], + smsConfiguration: { + servicePlanId: 'SpaceMonkeySquadron', + }, + voiceConfiguration: { + appId: 'sunshine-rain-drop-very-beautifulday', + }, + numberPattern: { + pattern: '7654321', + searchPattern: 'END', + }, + }, + }); +}); + +Then('the response contains an active phone number', () => { + assert.equal(activeNumber.phoneNumber, '+12017654321'); + assert.equal(activeNumber.projectId, '123c0ffee-dada-beef-cafe-baadc0de5678'); + assert.equal(activeNumber.displayName, ''); + assert.equal(activeNumber.regionCode, 'US'); + assert.equal(activeNumber.type, 'LOCAL'); + assert.deepEqual(activeNumber.capability, ['SMS', 'VOICE']); + assert.deepEqual(activeNumber.money, { currencyCode: 'EUR', amount: '0.80' }); + assert.equal(activeNumber.paymentIntervalMonths, 1); + assert.deepEqual(activeNumber.nextChargeDate, new Date('2024-06-06T14:42:42.022227Z')); + assert.equal(activeNumber.expireAt, null); + const expectedSmsConfiguration: Numbers.SMSConfiguration = { + servicePlanId: '', + campaignId: '', + scheduledProvisioning: { + servicePlanId: 'SpaceMonkeySquadron', + campaignId: '', + status: 'WAITING', + lastUpdatedTime: new Date('2024-06-06T14:42:42.596223Z'), + errorCodes: [], + }, + }; + assert.deepEqual(activeNumber.smsConfiguration, expectedSmsConfiguration); + const expectedVoiceConfiguration: Numbers.VoiceConfiguration = { + type: 'RTC', + appId: '', + trunkId: '', + serviceId: '', + lastUpdatedTime: null, + scheduledVoiceProvisioning: { + type: 'RTC', + appId: 'sunshine-rain-drop-very-beautifulday', + trunkId: '', + serviceId: '', + status: 'WAITING', + lastUpdatedTime: new Date('2024-06-06T14:42:42.604092Z'), + }, + }; + assert.deepEqual(activeNumber.voiceConfiguration, expectedVoiceConfiguration); + assert.equal(activeNumber.callbackUrl, ''); +}); + +When('I send a request to rent the phone number {string}', async (phoneNumber: string) => { + activeNumber = await availableNumberApi.rent({ + phoneNumber, + rentNumberRequestBody: { + smsConfiguration: { + servicePlanId: 'SpaceMonkeySquadron', + }, + voiceConfiguration: { + appId: 'sunshine-rain-drop-very-beautifulday', + }, + }, + }); +}); + +Then('the response contains this active phone number {string}', (phoneNumber: string) => { + assert.equal(activeNumber.phoneNumber, phoneNumber); +}); + +When('I send a request to rent the unavailable phone number {string}', async (phoneNumber: string) => { + activeNumber = await availableNumberApi.rent({ + phoneNumber, + rentNumberRequestBody: { + smsConfiguration: { + servicePlanId: 'SpaceMonkeySquadron', + }, + voiceConfiguration: { + appId: 'sunshine-rain-drop-very-beautifulday', + }, + }, + }); +}); From d51086cbd825ab8ac763c0284ce5a579026bf172 Mon Sep 17 00:00:00 2001 From: Antoine SEIN <142824551+asein-sinch@users.noreply.github.com> Date: Fri, 5 Jul 2024 17:05:15 +0200 Subject: [PATCH 09/54] DEVEXP-364: E2E Numbers/Active number (#102) --- .../v1/active-number/active-number.steps.ts | 150 ++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 packages/numbers/tests/rest/v1/active-number/active-number.steps.ts diff --git a/packages/numbers/tests/rest/v1/active-number/active-number.steps.ts b/packages/numbers/tests/rest/v1/active-number/active-number.steps.ts new file mode 100644 index 00000000..dfa9ff3e --- /dev/null +++ b/packages/numbers/tests/rest/v1/active-number/active-number.steps.ts @@ -0,0 +1,150 @@ +import { Given, Then, When } from '@cucumber/cucumber'; +import { ActiveNumberApi, NumbersService, Numbers } from '../../../../src'; +import { PageResult } from '@sinch/sdk-client'; +import assert from 'assert'; + +let activeNumberApi: ActiveNumberApi; +let listActiveNumbersResponse: PageResult; +const activeNumbersList: Numbers.ActiveNumber[] = []; +let activeNumber: Numbers.ActiveNumber; +let error: any; + +Given('the Numbers service "Active Number" is available', function () { + const numbersService = new NumbersService({ + projectId: 'tinyfrog-jump-high-over-lilypadbasin', + keyId: 'keyId', + keySecret: 'keySecret', + authHostname: 'http://localhost:3011', + numbersHostname: 'http://localhost:3013', + }); + activeNumberApi = numbersService.activeNumber; +}); + +When('I send a request to list the active phone numbers', async () => { + listActiveNumbersResponse = await activeNumberApi.list({ + regionCode: 'US', + type: 'LOCAL', + }); +}); + +Then('the response contains {string} active phone numbers', (expectedAnswer: string) => { + const expectedPhoneNumbersCount = parseInt(expectedAnswer, 10); + assert.equal(listActiveNumbersResponse.data.length, expectedPhoneNumbersCount); +}); + +When('I send a request to list all the active phone numbers', async () => { + const requestData: Numbers.ListActiveNumbersRequestData = { + regionCode: 'US', + type: 'LOCAL', + }; + for await (const number of activeNumberApi.list(requestData)) { + activeNumbersList.push(number); + } +}); + +Then('the phone numbers list contains {string} active phone numbers', (expectedAnswer: string) => { + const expectedNumbers = parseInt(expectedAnswer, 10); + assert.strictEqual(activeNumbersList.length, expectedNumbers); + const phoneNumber1 = activeNumbersList[0]; + assert.equal(phoneNumber1.voiceConfiguration?.type, 'FAX'); + assert.ok(phoneNumber1.voiceConfiguration?.serviceId !== ''); + const phoneNumber2 = activeNumbersList[1]; + assert.equal(phoneNumber2.voiceConfiguration?.type, 'EST'); + assert.ok(phoneNumber2.voiceConfiguration?.trunkId !== ''); + const phoneNumber3 = activeNumbersList[2]; + assert.equal(phoneNumber3.voiceConfiguration?.type, 'RTC'); + assert.ok(phoneNumber3.voiceConfiguration?.appId !== ''); +}); + +When('I send a request to update the phone number {string}', async (phoneNumber: string) => { + activeNumber = await activeNumberApi.update({ + phoneNumber, + updateActiveNumberRequestBody: { + displayName: 'Updated description during E2E tests', + smsConfiguration: { + servicePlanId: 'SingingMooseSociety', + }, + voiceConfiguration: { + type: 'FAX', + serviceId: '01W4FFL35P4NC4K35FAXSERVICE', + }, + callbackUrl: 'https://my-callback-server.com/numbers', + }, + }); +}); + +Then('the response contains a phone number with updated parameters', () => { + assert.equal(activeNumber.displayName, 'Updated description during E2E tests'); + assert.equal(activeNumber.callbackUrl, 'https://my-callback-server.com/numbers'); + const smsConfiguration: Numbers.SMSConfiguration = { + servicePlanId: 'SpaceMonkeySquadron', + campaignId: '', + scheduledProvisioning: { + servicePlanId: 'SingingMooseSociety', + campaignId: '', + status: 'WAITING', + lastUpdatedTime: new Date('2024-06-06T20:02:20.432220Z'), + errorCodes: [], + }, + }; + assert.deepEqual(activeNumber.smsConfiguration, smsConfiguration); + const voiceVonfiguration: Numbers.VoiceConfiguration = { + type: 'RTC', + appId: 'sunshine-rain-drop-very-beautifulday', + trunkId: '', + serviceId: '', + lastUpdatedTime: null, + scheduledVoiceProvisioning: { + status: 'WAITING', + type: 'FAX', + appId: '', + trunkId: '', + serviceId: '01W4FFL35P4NC4K35FAXSERVICE', + lastUpdatedTime: new Date('2024-06-06T20:02:20.437509Z'), + }, + }; + assert.deepEqual(activeNumber.voiceConfiguration, voiceVonfiguration); +}); + +When('I send a request to retrieve the phone number {string}', async (phoneNumber: string) => { + try { + activeNumber = await activeNumberApi.get({ phoneNumber }); + } catch (e) { + error = e; + } +}); + +Then('the response contains details about the phone number {string}', (phoneNumber: string) => { + assert.equal(activeNumber.phoneNumber, phoneNumber); + assert.deepEqual(activeNumber.nextChargeDate, new Date('2024-06-06T14:42:42.677575Z')); + assert.equal(activeNumber.expireAt, null); + assert.equal(activeNumber.smsConfiguration?.servicePlanId, 'SpaceMonkeySquadron'); +}); + +// eslint-disable-next-line max-len +Then('the response contains details about the phone number {string} with an SMS provisioning error', (phoneNumber: string) => { + assert.equal(activeNumber.phoneNumber, phoneNumber); + assert.deepEqual(activeNumber.nextChargeDate, new Date('2024-07-06T14:42:42.677575Z')); + assert.equal(activeNumber.expireAt, null); + assert.equal(activeNumber.smsConfiguration?.servicePlanId, ''); + assert.equal(activeNumber.smsConfiguration?.scheduledProvisioning?.status, 'FAILED'); + assert.deepEqual(activeNumber.smsConfiguration?.scheduledProvisioning?.errorCodes, ['SMS_PROVISIONING_FAILED']); +}); + +Then('the response contains an error about the number {string} not being an active number', (phoneNumber: string) => { + const notFound = JSON.parse(error.data) as Numbers.NotFound; + const notFoundError = notFound.error!; + assert.equal(notFoundError.code, 404); + assert.equal(notFoundError.status, 'NOT_FOUND'); + assert.equal((notFoundError.details![0] as any).resourceName, phoneNumber); +}); + +When('I send a request to release the phone number {string}', async (phoneNumber: string) => { + activeNumber = await activeNumberApi.release({ phoneNumber }); +}); + +Then('the response contains details about the phone number {string} to be released', (phoneNumber: string) => { + assert.equal(activeNumber.phoneNumber, phoneNumber); + assert.deepEqual(activeNumber.nextChargeDate, new Date('2024-06-06T14:42:42.677575Z')); + assert.deepEqual(activeNumber.expireAt, new Date('2024-06-06T14:42:42.677575Z')); +}); From 8846fc00e78b628a2f89cbe60576e740668c5fbc Mon Sep 17 00:00:00 2001 From: Antoine SEIN <142824551+asein-sinch@users.noreply.github.com> Date: Fri, 5 Jul 2024 17:05:29 +0200 Subject: [PATCH 10/54] DEVEXP-365: E2E Numbers/Callback configuration (#103) --- .../callbacks/calback-configuration.steps.ts | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 packages/numbers/tests/rest/v1/callbacks/calback-configuration.steps.ts diff --git a/packages/numbers/tests/rest/v1/callbacks/calback-configuration.steps.ts b/packages/numbers/tests/rest/v1/callbacks/calback-configuration.steps.ts new file mode 100644 index 00000000..70a9c91d --- /dev/null +++ b/packages/numbers/tests/rest/v1/callbacks/calback-configuration.steps.ts @@ -0,0 +1,50 @@ +import { Given, Then, When } from '@cucumber/cucumber'; +import { CallbacksApi, NumbersService, Numbers } from '../../../../src'; +import * as assert from 'assert'; + +let callbackConfigurationApi: CallbacksApi; +let callbackConfiguration: Numbers.CallbackConfiguration; +let error: any; +Given('the Numbers service "Callback Configuration" is available', function () { + const numbersService = new NumbersService({ + projectId: 'tinyfrog-jump-high-over-lilypadbasin', + keyId: 'keyId', + keySecret: 'keySecret', + authHostname: 'http://localhost:3011', + numbersHostname: 'http://localhost:3013', + }); + callbackConfigurationApi = numbersService.callbacks; +}); + +When('I send a request to retrieve the callback configuration', async () => { + callbackConfiguration = await callbackConfigurationApi.get({}); +}); + +Then('the response contains the project\'s callback configuration', () => { + assert.equal(callbackConfiguration.projectId, '12c0ffee-dada-beef-cafe-baadc0de5678'); + assert.equal(callbackConfiguration.hmacSecret, '0default-pass-word-*max-36characters'); +}); + +When('I send a request to update the callback configuration with the secret {}', async (hmacSecret: string) => { + try { + callbackConfiguration = await callbackConfigurationApi.update({ + updateCallbackConfigurationRequestBody: { + hmacSecret, + }, + }); + } catch (e) { + error = e; + } +}); + +Then('the response contains the updated project\'s callback configuration', () => { + assert.equal(callbackConfiguration.projectId, '12c0ffee-dada-beef-cafe-baadc0de5678'); + assert.equal(callbackConfiguration.hmacSecret, 'strongPa$$PhraseWith36CharactersMax'); +}); + +Then('the response contains an error', () => { + const notFound = JSON.parse(error.data) as Numbers.NotFound; + const notFoundError = notFound.error!; + assert.equal(notFoundError.code, 404); + assert.equal(notFoundError.status, 'NOT_FOUND'); +}); From 9ff00d37b3646a810f1db17a8343226424fd1e1d Mon Sep 17 00:00:00 2001 From: Antoine SEIN <142824551+asein-sinch@users.noreply.github.com> Date: Fri, 5 Jul 2024 17:05:45 +0200 Subject: [PATCH 11/54] DEVEXP-366: E2E Numbers/Webhooks (#104) --- .../tests/rest/v1/webhooks/webhooks.steps.ts | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 packages/numbers/tests/rest/v1/webhooks/webhooks.steps.ts diff --git a/packages/numbers/tests/rest/v1/webhooks/webhooks.steps.ts b/packages/numbers/tests/rest/v1/webhooks/webhooks.steps.ts new file mode 100644 index 00000000..4544f04f --- /dev/null +++ b/packages/numbers/tests/rest/v1/webhooks/webhooks.steps.ts @@ -0,0 +1,51 @@ +import { Given, Then, When } from '@cucumber/cucumber'; +import { NumbersCallbackWebhooks, Numbers } from '../../../../src'; +import assert from 'assert'; +import { IncomingHttpHeaders } from 'http'; + +const SINCH_NUMBERS_CALLBACK_SECRET = 'strongPa$$PhraseWith36CharactersMax'; +let numbersCallbackWebhook: NumbersCallbackWebhooks; +let rawEvent: any; +let event: Numbers.CallbackPayload; +let formattedHeaders: IncomingHttpHeaders; + +const processEvent = async (response: Response) => { + formattedHeaders = {}; + response.headers.forEach((value, name) => { + formattedHeaders[name.toLowerCase()] = value; + }); + rawEvent = await response.text(); + rawEvent = rawEvent.replace(/\s+/g, ''); + console.log(rawEvent); + event = numbersCallbackWebhook.parseEvent(JSON.parse(rawEvent)); +}; + +Given('the Numbers Webhooks handler is available', function () { + numbersCallbackWebhook = new NumbersCallbackWebhooks(SINCH_NUMBERS_CALLBACK_SECRET); +}); + +When('I send a request to trigger the success to provision to voice platform event', async () => { + const response = await fetch('http://localhost:3013/webhooks/numbers/provisioning_to_voice_platform/succeeded'); + await processEvent(response); +}); + +Then('the event header contains a valid signature', () => { + assert.ok(numbersCallbackWebhook.validateAuthenticationHeader(formattedHeaders, rawEvent)); +}); + +Then('the event describes a success to provision to voice platform event', () => { + assert.equal(event.eventType, 'PROVISIONING_TO_VOICE_PLATFORM'); + assert.equal(event.status, 'SUCCEEDED'); + assert.equal(event.failureCode, null); +}); + +When('I send a request to trigger the failure to provision to voice platform event', async () => { + const response = await fetch('http://localhost:3013/webhooks/numbers/provisioning_to_voice_platform/failed'); + await processEvent(response); +}); + +Then('the event describes a failure to provision to voice platform event', () => { + assert.equal(event.eventType, 'PROVISIONING_TO_VOICE_PLATFORM'); + assert.equal(event.status, 'FAILED'); + assert.equal(event.failureCode, 'PROVISIONING_TO_VOICE_PLATFORM_FAILED'); +}); From 14d1881f5673a8b3989279270f5e8f45f794cba3 Mon Sep 17 00:00:00 2001 From: Antoine SEIN <142824551+asein-sinch@users.noreply.github.com> Date: Mon, 8 Jul 2024 14:02:27 +0200 Subject: [PATCH 12/54] DEVEXP-478: E2E Conversation/Messages (#105) --- packages/conversation/cucumber.js | 8 ++ packages/conversation/package.json | 3 +- .../src/rest/v1/contact/contact-api.ts | 2 +- .../rest/v1/conversation/conversation-api.ts | 4 +- .../src/rest/v1/events/events-api.ts | 2 +- .../src/rest/v1/messages/messages-api.ts | 2 +- .../tests/rest/v1/messages/messages.steps.ts | 134 ++++++++++++++++++ packages/sdk-client/src/api/api-client.ts | 2 + .../client/api-client-pagination-helper.ts | 22 ++- .../api-client-pagination-helpers.test.ts | 52 +++++++ 10 files changed, 223 insertions(+), 8 deletions(-) create mode 100644 packages/conversation/cucumber.js create mode 100644 packages/conversation/tests/rest/v1/messages/messages.steps.ts diff --git a/packages/conversation/cucumber.js b/packages/conversation/cucumber.js new file mode 100644 index 00000000..691a9809 --- /dev/null +++ b/packages/conversation/cucumber.js @@ -0,0 +1,8 @@ +module.exports = { + default: [ + 'tests/e2e/features/**/*.feature', + '--require-module ts-node/register', + '--require tests/rest/v1/**/*.steps.ts', + `--format-options '{"snippetInterface": "synchronous"}'`, + ].join(' '), +}; diff --git a/packages/conversation/package.json b/packages/conversation/package.json index a02dc56c..2e8969cd 100644 --- a/packages/conversation/package.json +++ b/packages/conversation/package.json @@ -25,7 +25,8 @@ "scripts": { "build": "yarn run clean && yarn run compile", "clean": "rimraf dist tsconfig.tsbuildinfo tsconfig.build.tsbuildinfo", - "compile": "tsc -p tsconfig.build.json && tsc -p tsconfig.tests.json && rimraf dist/tests tsconfig.build.tsbuildinfo" + "compile": "tsc -p tsconfig.build.json && tsc -p tsconfig.tests.json && rimraf dist/tests tsconfig.build.tsbuildinfo", + "test:e2e": "cucumber-js" }, "dependencies": { "@sinch/sdk-client": "^1.1.0" diff --git a/packages/conversation/src/rest/v1/contact/contact-api.ts b/packages/conversation/src/rest/v1/contact/contact-api.ts index 2d3d3f21..6e904651 100644 --- a/packages/conversation/src/rest/v1/contact/contact-api.ts +++ b/packages/conversation/src/rest/v1/contact/contact-api.ts @@ -168,7 +168,7 @@ export class ContactApi extends ConversationDomainApi { const requestOptionsPromise = this.client.prepareOptions(basePathUrl, 'GET', getParams, headers, body || undefined); const operationProperties: PaginatedApiProperties = { - pagination: PaginationEnum.TOKEN, + pagination: PaginationEnum.TOKEN2, apiName: this.apiName, operationId: 'ListContacts', dataKey: 'contacts', diff --git a/packages/conversation/src/rest/v1/conversation/conversation-api.ts b/packages/conversation/src/rest/v1/conversation/conversation-api.ts index 5fc11c5a..750b3f7e 100644 --- a/packages/conversation/src/rest/v1/conversation/conversation-api.ts +++ b/packages/conversation/src/rest/v1/conversation/conversation-api.ts @@ -201,7 +201,7 @@ export class ConversationApi extends ConversationDomainApi { const requestOptionsPromise = this.client.prepareOptions(basePathUrl, 'GET', getParams, headers, body || undefined); const operationProperties: PaginatedApiProperties = { - pagination: PaginationEnum.TOKEN, + pagination: PaginationEnum.TOKEN2, apiName: this.apiName, operationId: 'ListConversations', dataKey: 'conversations', @@ -249,7 +249,7 @@ export class ConversationApi extends ConversationDomainApi { const requestOptionsPromise = this.client.prepareOptions(basePathUrl, 'GET', getParams, headers, body || undefined); const operationProperties: PaginatedApiProperties = { - pagination: PaginationEnum.TOKEN, + pagination: PaginationEnum.TOKEN2, apiName: this.apiName, operationId: 'ListRecentConversations', dataKey: 'conversations', diff --git a/packages/conversation/src/rest/v1/events/events-api.ts b/packages/conversation/src/rest/v1/events/events-api.ts index 87afe850..453e4cb3 100644 --- a/packages/conversation/src/rest/v1/events/events-api.ts +++ b/packages/conversation/src/rest/v1/events/events-api.ts @@ -116,7 +116,7 @@ export class EventsApi extends ConversationDomainApi { const requestOptionsPromise = this.client.prepareOptions(basePathUrl, 'GET', getParams, headers, body || undefined); const operationProperties: PaginatedApiProperties = { - pagination: PaginationEnum.TOKEN, + pagination: PaginationEnum.TOKEN2, apiName: this.apiName, operationId: 'ListEvents', dataKey: 'events', diff --git a/packages/conversation/src/rest/v1/messages/messages-api.ts b/packages/conversation/src/rest/v1/messages/messages-api.ts index ec19b487..11489ab2 100644 --- a/packages/conversation/src/rest/v1/messages/messages-api.ts +++ b/packages/conversation/src/rest/v1/messages/messages-api.ts @@ -131,7 +131,7 @@ export class MessagesApi extends ConversationDomainApi { const requestOptionsPromise = this.client.prepareOptions(basePathUrl, 'GET', getParams, headers, body || undefined); const operationProperties: PaginatedApiProperties = { - pagination: PaginationEnum.TOKEN, + pagination: PaginationEnum.TOKEN2, apiName: this.apiName, operationId: 'ListMessages', dataKey: 'messages', diff --git a/packages/conversation/tests/rest/v1/messages/messages.steps.ts b/packages/conversation/tests/rest/v1/messages/messages.steps.ts new file mode 100644 index 00000000..56f4efe9 --- /dev/null +++ b/packages/conversation/tests/rest/v1/messages/messages.steps.ts @@ -0,0 +1,134 @@ +import { Conversation, ConversationService, MessagesApi, SupportedConversationRegion } from '../../../../src'; +import { Given, Then, When } from '@cucumber/cucumber'; +import * as assert from 'assert'; +import { PageResult } from '@sinch/sdk-client'; + +let messagesApi: MessagesApi; +let messageResponse: Conversation.SendMessageResponse; +let listResponse: PageResult; +const messagesList: Conversation.ConversationMessage[] = []; +let message: Conversation.ConversationMessage; +let deleteMessageResponse: void; +let pagesIteration: number; + +Given('the Conversation service "Messages" is available', function () { + const conversationService = new ConversationService({ + projectId: 'tinyfrog-jump-high-over-lilypadbasin', + keyId: 'keyId', + keySecret: 'keySecret', + authHostname: 'http://localhost:3011', + conversationHostname: 'http://localhost:3014', + conversationRegion: SupportedConversationRegion.UNITED_STATES, + }); + messagesApi = conversationService.messages; +}); + +When('I send a request to send a message to a contact', async () => { + messageResponse = await messagesApi.send({ + sendMessageRequestBody: { + message: { + text_message: { + text: 'Hello', + }, + }, + app_id: '01W4FFL35P4NC4K35CONVAPP001', + recipient: { + contact_id: '01W4FFL35P4NC4K35CONTACT001', + }, + }, + }); +}); + +Then('the response contains the id of the message', () => { + assert.equal(messageResponse.message_id, '01W4FFL35P4NC4K35MESSAGE001'); +}); + +When('I send a request to list the existing messages', async () => { + listResponse = await messagesApi.list({ + page_size: 2, + }); +}); + +Then('the response contains {string} messages', (expectedAnswer: string) => { + const expectedMessagesCount = parseInt(expectedAnswer, 10); + assert.equal(listResponse.data.length, expectedMessagesCount); +}); + +When('I send a request to list all the messages', async () => { + for await (const service of messagesApi.list({ page_size: 2 })) { + messagesList.push(service); + } +}); + +When('I iterate manually over the messages pages', async () => { + listResponse = await messagesApi.list({ + page_size: 2, + }); + pagesIteration = 1; + let reachedEndOfPages = false; + while (!reachedEndOfPages) { + if (listResponse.hasNextPage) { + listResponse = await listResponse.nextPage(); + pagesIteration++; + } else { + reachedEndOfPages = true; + } + } +}); + +Then('the messages list contains {string} messages', (expectedAnswer: string) => { + const expectedServices = parseInt(expectedAnswer, 10); + assert.equal(messagesList.length, expectedServices); +}); + +Then('the 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 a message', async () => { + message = await messagesApi.get({ + message_id: '01W4FFL35P4NC4K35MESSAGE001', + }); +}); + +Then('the response contains the message details', () => { + assert.equal(message.id, '01W4FFL35P4NC4K35MESSAGE001'); + assert.equal(message.direction, 'TO_CONTACT'); + assert.equal(message.conversation_id, '01W4FFL35P4NC4K35CONVERSATI'); + assert.equal(message.contact_id, '01W4FFL35P4NC4K35CONTACT001'); + assert.equal(message.metadata, ''); + assert.deepEqual(message.accept_time, new Date('2024-06-06T12:42:42Z')); + assert.equal(message.processing_mode, 'CONVERSATION'); + assert.equal(message.injected, false); + const channelIdentity: Conversation.ChannelIdentity = { + channel: 'SMS', + identity: '12015555555', + app_id: '', + }; + assert.deepEqual(message.channel_identity, channelIdentity); +}); + +When('I send a request to update a message', async () => { + message = await messagesApi.update({ + message_id: '01W4FFL35P4NC4K35MESSAGE001', + updateMessageRequestBody: { + metadata: 'Updated metadata', + }, + }); +}); + +Then('the response contains the message details with updated metadata', () => { + assert.equal(message.id, '01W4FFL35P4NC4K35MESSAGE001'); + assert.equal(message.metadata, 'Updated metadata'); +}); + +When('I send a request to delete a message', async () => { + deleteMessageResponse = await messagesApi.delete({ + message_id: '01W4FFL35P4NC4K35MESSAGE001', + }); +}); + +Then('the delete message response contains no data', () => { + assert.deepEqual(deleteMessageResponse, {} ); +}); diff --git a/packages/sdk-client/src/api/api-client.ts b/packages/sdk-client/src/api/api-client.ts index 5269aa50..52ba9884 100644 --- a/packages/sdk-client/src/api/api-client.ts +++ b/packages/sdk-client/src/api/api-client.ts @@ -6,6 +6,8 @@ export enum PaginationEnum { NONE, /** Used by the Numbers API */ TOKEN, + /** Used by the Conversation API */ + TOKEN2, /** used by the SMS API */ PAGE, /** used by the Elastic SIP Trunking API */ diff --git a/packages/sdk-client/src/client/api-client-pagination-helper.ts b/packages/sdk-client/src/client/api-client-pagination-helper.ts index 8a720d02..14e3b299 100644 --- a/packages/sdk-client/src/client/api-client-pagination-helper.ts +++ b/packages/sdk-client/src/client/api-client-pagination-helper.ts @@ -60,6 +60,13 @@ class SinchIterator implements AsyncIterator { return updateQueryParamsAndSendRequest( this.apiClient, newParams, requestOptions, this.paginatedOperationProperties); } + if (this.paginatedOperationProperties.pagination === PaginationEnum.TOKEN2) { + const newParams = { + page_token: pageResult.nextPageValue, + }; + return updateQueryParamsAndSendRequest( + this.apiClient, newParams, requestOptions, this.paginatedOperationProperties); + } if (this.paginatedOperationProperties.pagination === PaginationEnum.PAGE || this.paginatedOperationProperties.pagination === PaginationEnum.PAGE3) { const newParams = { @@ -136,6 +143,11 @@ export const createNextPageMethod = ( pageToken: nextPageValue, }; break; + case PaginationEnum.TOKEN2: + newParams = { + page_token: nextPageValue, + }; + break; case PaginationEnum.PAGE: case PaginationEnum.PAGE2: case PaginationEnum.PAGE3: @@ -167,7 +179,10 @@ export function hasMore( context: PaginationContext, ): boolean { if (context.pagination === PaginationEnum.TOKEN) { - return !!response['nextPageToken'] || !!response['next_page_token']; + return !!response['nextPageToken']; + } + if (context.pagination === PaginationEnum.TOKEN2) { + return !!response['next_page_token']; } if (context.pagination === PaginationEnum.PAGE) { const requestedPageSize = context.requestOptions.queryParams?.page_size; @@ -190,7 +205,10 @@ export function calculateNextPage( context: PaginationContext, ): string { if (context.pagination === PaginationEnum.TOKEN) { - return response['nextPageToken'] || response['next_page_token']; + return response['nextPageToken']; + } + if (context.pagination === PaginationEnum.TOKEN2) { + return response['next_page_token']; } if (context.pagination === PaginationEnum.PAGE) { const currentPage: number = response.page || 0; diff --git a/packages/sdk-client/tests/client/api-client-pagination-helpers.test.ts b/packages/sdk-client/tests/client/api-client-pagination-helpers.test.ts index ec436e82..3be13fce 100644 --- a/packages/sdk-client/tests/client/api-client-pagination-helpers.test.ts +++ b/packages/sdk-client/tests/client/api-client-pagination-helpers.test.ts @@ -21,6 +21,10 @@ describe('API Client Pagination Helper', () => { pagination: PaginationEnum.TOKEN, ...paginationTokenProperties, }; + const paginationContextToken2: PaginationContext = { + pagination: PaginationEnum.TOKEN2, + ...paginationTokenProperties, + }; const paginationContextPage: PaginationContext = { pagination: PaginationEnum.PAGE, ...paginationTokenProperties, @@ -69,6 +73,38 @@ describe('API Client Pagination Helper', () => { expect(hasMoreElements).toBeFalsy(); }); + it('should return "true" when the PaginationContext is "TOKEN2" and there are more elements', async () => { + // Given + const response = { + elements: ['H', 'He'], + next_page_token: 'nextPageToken', + totalSize: 10, + }; + const paginationContext = { ...paginationContextToken2 }; + + // When + const hasMoreElements = hasMore(response, paginationContext); + + // Then + expect(hasMoreElements).toBeTruthy(); + }); + + it('should return "false" when the PaginationContext is "TOKEN2" and there are no more elements', async () => { + // Given + const response = { + elements: ['H', 'He'], + next_page_token: '', + totalSize: 2, + }; + const paginationContext = { ...paginationContextToken2 }; + + // When + const hasMoreElements = hasMore(response, paginationContext); + + // Then + expect(hasMoreElements).toBeFalsy(); + }); + it('should return "true" when the PaginationContext is "PAGE" and there are more elements', async () => { // Given const response = { @@ -193,6 +229,22 @@ describe('API Client Pagination Helper', () => { expect(nextPage).toBe('nextPageToken'); }); + it('should return the next page token when the PaginationContext is "TOKEN2"', () => { + // Given + const response = { + elements: ['H', 'He'], + next_page_token: 'nextPageToken', + totalSize: 10, + }; + const paginationContext = { ...paginationContextToken2 }; + + // When + const nextPage = calculateNextPage(response, paginationContext); + + // Then + expect(nextPage).toBe('nextPageToken'); + }); + it('should return the next page value when the PaginationContext is "PAGE"', () => { // Given const response = { From 55c8cd66515162bdd0cfd06d15f1a2dca6d383d8 Mon Sep 17 00:00:00 2001 From: Antoine SEIN <142824551+asein-sinch@users.noreply.github.com> Date: Tue, 9 Jul 2024 15:45:22 +0200 Subject: [PATCH 13/54] DEVEXP-479: E2E Conversation/Apps (#106) --- .../tests/rest/v1/app/app.steps.ts | 125 ++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 packages/conversation/tests/rest/v1/app/app.steps.ts diff --git a/packages/conversation/tests/rest/v1/app/app.steps.ts b/packages/conversation/tests/rest/v1/app/app.steps.ts new file mode 100644 index 00000000..e47c791f --- /dev/null +++ b/packages/conversation/tests/rest/v1/app/app.steps.ts @@ -0,0 +1,125 @@ +import { AppApi, Conversation, ConversationService, SupportedConversationRegion } from '../../../../src'; +import { Given, Then, When } from '@cucumber/cucumber'; +import * as assert from 'assert'; +import { ChannelCredentialsSmsResponse } from '../../../../src/models'; + +let appsApi: AppApi; +let appResponse: Conversation.AppResponse; +let listResponse: Conversation.ListAppsResponse; +let deleteAppResponse: void; + +Given('the Conversation service "Apps" is available', function () { + const conversationService = new ConversationService({ + projectId: 'tinyfrog-jump-high-over-lilypadbasin', + keyId: 'keyId', + keySecret: 'keySecret', + authHostname: 'http://localhost:3011', + conversationHostname: 'http://localhost:3014', + conversationRegion: SupportedConversationRegion.UNITED_STATES, + }); + appsApi = conversationService.app; +}); + +When('I send a request to create an app', async () => { + appResponse = await appsApi.create({ + appCreateRequestBody: { + display_name: 'E2E Conversation App', + channel_credentials: [ + { + channel: 'SMS', + static_bearer: { + claimed_identity: 'SpaceMonkeySquadron', + token: '00112233445566778899aabbccddeeff', + }, + }, + ], + }, + }); +}); + +Then('the conversation app is created', () => { + assert.equal(appResponse.id, '01W4FFL35P4NC4K35CONVAPP001'); + assert.equal(appResponse.channel_credentials?.length, 1); + assert.equal(appResponse.channel_credentials?.[0].state?.status, 'PENDING'); + assert.equal(appResponse.conversation_metadata_report_view, 'NONE'); + assert.equal(appResponse.display_name, 'E2E Conversation App'); + assert.equal(appResponse.processing_mode, 'CONVERSATION'); +}); + +When('I send a request to list all the apps', async () => { + listResponse = await appsApi.list({}); +}); + +Then('the apps list contains 2 apps', () => { + assert.equal(listResponse.apps?.length, 2); + const app1 = listResponse.apps![0]; + assert.equal(app1.id, '01W4FFL35P4NC4K35CONVAPP001'); + assert.equal(app1.channel_credentials?.[0].state?.status, 'ACTIVE'); + assert.equal(app1.conversation_metadata_report_view, 'NONE'); + const app2 = listResponse.apps![1]; + assert.equal(app2.id, '01W4FFL35P4NC4K35CONVAPP002'); + assert.equal(app2.channel_credentials?.length, 2); + assert.equal(app2.channel_credentials?.[0].state?.status, 'FAILING'); + assert.equal(app2.conversation_metadata_report_view, 'FULL'); +}); + +When('I send a request to retrieve an app', async () => { + appResponse = await appsApi.get({ + app_id: '01W4FFL35P4NC4K35CONVAPP001', + }); +}); + +Then('the response contains the app details', () => { + assert.equal(appResponse.id, '01W4FFL35P4NC4K35CONVAPP001'); + assert.equal(appResponse.conversation_metadata_report_view, 'NONE'); + assert.equal(appResponse.display_name, 'E2E Conversation App'); + assert.equal(appResponse.processing_mode, 'CONVERSATION'); + assert.equal(appResponse.rate_limits?.outbound, 20); + assert.equal(appResponse.rate_limits?.inbound, 100); + assert.equal(appResponse.rate_limits?.webhooks, 100); + assert.equal(appResponse.retention_policy?.retention_type, 'MESSAGE_EXPIRE_POLICY'); + assert.equal(appResponse.retention_policy?.ttl_days, 180); + assert.equal(appResponse.dispatch_retention_policy?.retention_type, 'MESSAGE_EXPIRE_POLICY'); + assert.equal(appResponse.dispatch_retention_policy?.ttl_days, 0); + assert.equal(appResponse.smart_conversation?.enabled, false); + assert.equal(appResponse.queue_stats?.outbound_size, 0); + assert.equal(appResponse.queue_stats?.outbound_limit, 500000); + assert.equal(appResponse.persist_message_status?.enabled, false); + assert.equal(appResponse.message_search?.enabled, false); + assert.equal(appResponse.callback_settings?.secret_for_overridden_callback_urls, ''); + assert.equal(appResponse.delivery_report_based_fallback?.enabled, false); + assert.equal(appResponse.delivery_report_based_fallback?.delivery_report_waiting_time, 0); + assert.equal(appResponse.message_retry_settings?.retry_duration, 3600); + const channelCredentials = appResponse.channel_credentials![0] as ChannelCredentialsSmsResponse; + assert.equal(channelCredentials.channel, 'SMS'); + assert.equal(channelCredentials.static_bearer.claimed_identity, 'SpaceMonkeySquadron'); + assert.equal(channelCredentials.static_bearer.token, '00112233445566778899aabbccddeeff'); + assert.equal(channelCredentials.callback_secret, ''); + assert.equal(channelCredentials.state?.status, 'ACTIVE'); + assert.equal(channelCredentials.state?.description, ''); + assert.equal(channelCredentials.channel_known_id, ''); +}); + +When('I send a request to update an app', async () => { + appResponse = await appsApi.update({ + app_id: '01W4FFL35P4NC4K35CONVAPP001', + appUpdateRequestBody: { + display_name: 'Updated name', + }, + }); +}); + +Then('the response contains the app details with updated properties', () => { + assert.equal(appResponse.id, '01W4FFL35P4NC4K35CONVAPP001'); + assert.equal(appResponse.display_name, 'Updated name'); +}); + +When('I send a request to delete an app', async () => { + deleteAppResponse = await appsApi.delete({ + app_id: '01W4FFL35P4NC4K35CONVAPP001', + }); +}); + +Then('the delete app response contains no data', () => { + assert.deepEqual(deleteAppResponse, {} ); +}); From 622110e0c96a9bd3efee50103649556bb3ef1068 Mon Sep 17 00:00:00 2001 From: Antoine SEIN <142824551+asein-sinch@users.noreply.github.com> Date: Tue, 9 Jul 2024 16:14:43 +0200 Subject: [PATCH 14/54] DEVEXP-480: E2E Conversation/Contacts (#107) --- .../tests/rest/v1/contact/contacts.steps.ts | 180 ++++++++++++++++++ .../tests/rest/v1/messages/messages.steps.ts | 12 +- 2 files changed, 188 insertions(+), 4 deletions(-) create mode 100644 packages/conversation/tests/rest/v1/contact/contacts.steps.ts diff --git a/packages/conversation/tests/rest/v1/contact/contacts.steps.ts b/packages/conversation/tests/rest/v1/contact/contacts.steps.ts new file mode 100644 index 00000000..28cf9bc4 --- /dev/null +++ b/packages/conversation/tests/rest/v1/contact/contacts.steps.ts @@ -0,0 +1,180 @@ +import { Conversation, ContactApi, ConversationService, SupportedConversationRegion } from '../../../../src'; +import { Given, Then, When } from '@cucumber/cucumber'; +import * as assert from 'assert'; +import { PageResult } from '@sinch/sdk-client'; + +let contactsApi: ContactApi; +let contact: Conversation.Contact; +let listResponse: PageResult; +let contactsList: Conversation.Contact[]; +let pagesIteration: number; +let deleteContactResponse: void; +let channelProfile: Conversation.GetChannelProfileResponse; + +Given('the Conversation service "Contacts" is available', function () { + const conversationService = new ConversationService({ + projectId: 'tinyfrog-jump-high-over-lilypadbasin', + keyId: 'keyId', + keySecret: 'keySecret', + authHostname: 'http://localhost:3011', + conversationHostname: 'http://localhost:3014', + conversationRegion: SupportedConversationRegion.UNITED_STATES, + }); + contactsApi = conversationService.contact; +}); + +When('I send a request to create an contact', async () => { + contact = await contactsApi.create({ + contactCreateRequestBody: { + channel_identities: [ + { + channel: 'SMS', + identity: '+12015555555', + }, + ], + language: 'EN_US', + display_name: 'Marty McFly', + email: 'time.traveler@delorean.com', + }, + }); +}); + +Then('the contact is created', () => { + assert.equal(contact.id, '01W4FFL35P4NC4K35CONTACT001'); +}); + +When('I send a request to list the existing contacts', async () => { + listResponse = await contactsApi.list({ + page_size: 2, + }); +}); + +Then('the response contains {string} contacts', (expectedAnswer: string) => { + const expectedContactsCount = parseInt(expectedAnswer, 10); + assert.equal(listResponse.data.length, expectedContactsCount); +}); + +When('I send a request to list all the contacts', async () => { + contactsList = []; + for await (const contact of contactsApi.list({ page_size: 2 })) { + contactsList.push(contact); + } +}); + +When('I iterate manually over the contacts pages', async () => { + contactsList = []; + listResponse = await contactsApi.list({ + page_size: 2, + }); + contactsList.push(...listResponse.data); + pagesIteration = 1; + let reachedEndOfPages = false; + while (!reachedEndOfPages) { + if (listResponse.hasNextPage) { + listResponse = await listResponse.nextPage(); + contactsList.push(...listResponse.data); + pagesIteration++; + } else { + reachedEndOfPages = true; + } + } +}); + +Then('the contacts list contains {string} contacts', (expectedAnswer: string) => { + const expectedServices = parseInt(expectedAnswer, 10); + assert.equal(contactsList.length, expectedServices); +}); + +Then('the contacts 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 a contact', async () => { + contact = await contactsApi.get({ + contact_id: '01W4FFL35P4NC4K35CONTACT001', + }); +}); + +Then('the response contains the contact details', () => { + assert.equal(contact.id, '01W4FFL35P4NC4K35CONTACT001'); + assert.equal(contact.channel_identities?.length, 1); + const channelIdentity = contact.channel_identities![0]; + assert.equal(channelIdentity.channel, 'SMS'); + assert.equal(channelIdentity.identity, '12015555555'); + assert.equal(contact.channel_priority?.length, 0); + assert.equal(contact.display_name, 'Marty McFly'); + assert.equal(contact.email, 'time.traveler@delorean.com'); + assert.equal(contact.language, 'EN_US'); +}); + +When('I send a request to update a contact', async () => { + contact = await contactsApi.update({ + contact_id: '01W4FFL35P4NC4K35CONTACT001', + updateContactRequestBody: { + channel_identities: [ + { + channel: 'MESSENGER', + identity: '7968425018576406', + app_id: '01W4FFL35P4NC4K35CONVAPP001', + }, + { + channel: 'SMS', + identity: '12015555555', + }, + ], + channel_priority: ['MESSENGER'], + }, + }); +}); + +Then('the response contains the contact details with updated data', () => { + assert.equal(contact.id, '01W4FFL35P4NC4K35CONTACT001'); + assert.equal(contact.channel_identities?.length, 2); +}); + +When('I send a request to delete a contact', async () => { + deleteContactResponse = await contactsApi.delete({ + contact_id: '01W4FFL35P4NC4K35CONTACT001', + }); +}); + +Then('the delete contact response contains no data', () => { + assert.deepEqual(deleteContactResponse, {} ); +}); + +When('I send a request to merge a source contact to a destination contact', async () => { + contact = await contactsApi.mergeContact({ + destination_id: '01W4FFL35P4NC4K35CONTACT002', + mergeContactRequestBody: { + source_id: '01W4FFL35P4NC4K35CONTACT001', + }, + }); +}); + +Then('the response contains data from the destination contact and from the source contact', () => { + const channelIdentities = contact.channel_identities!; + assert.equal(channelIdentities.length, 3); + assert.equal(channelIdentities[0].channel, 'MESSENGER'); + assert.equal(channelIdentities[1].channel, 'MMS'); + assert.equal(channelIdentities[2].channel, 'SMS'); + assert.deepEqual(contact.channel_priority, ['MMS', 'MESSENGER']); + assert.equal(contact.display_name, 'Pika pika'); + assert.equal(contact.email, 'pikachu@poke.mon'); +}); + +When('I send a request to get the channel profile of a contact ID', async () => { + channelProfile = await contactsApi.getChannelProfile({ + getChannelProfileRequestBody: { + app_id: '01W4FFL35P4NC4K35CONVAPP001', + channel: 'MESSENGER', + recipient: { + contact_id: '01W4FFL35P4NC4K35CONTACT001', + }, + }, + }); +}); + +Then('the response contains the profile of the contact on the requested channel', () => { + assert.equal(channelProfile.profile_name, 'Marty McFly FB' ); +}); diff --git a/packages/conversation/tests/rest/v1/messages/messages.steps.ts b/packages/conversation/tests/rest/v1/messages/messages.steps.ts index 56f4efe9..e5438864 100644 --- a/packages/conversation/tests/rest/v1/messages/messages.steps.ts +++ b/packages/conversation/tests/rest/v1/messages/messages.steps.ts @@ -6,10 +6,10 @@ import { PageResult } from '@sinch/sdk-client'; let messagesApi: MessagesApi; let messageResponse: Conversation.SendMessageResponse; let listResponse: PageResult; -const messagesList: Conversation.ConversationMessage[] = []; +let messagesList: Conversation.ConversationMessage[]; +let pagesIteration: number; let message: Conversation.ConversationMessage; let deleteMessageResponse: void; -let pagesIteration: number; Given('the Conversation service "Messages" is available', function () { const conversationService = new ConversationService({ @@ -55,20 +55,24 @@ Then('the response contains {string} messages', (expectedAnswer: string) => { }); When('I send a request to list all the messages', async () => { - for await (const service of messagesApi.list({ page_size: 2 })) { - messagesList.push(service); + messagesList = []; + for await (const message of messagesApi.list({ page_size: 2 })) { + messagesList.push(message); } }); When('I iterate manually over the messages pages', async () => { + messagesList = []; listResponse = await messagesApi.list({ page_size: 2, }); + messagesList.push(...listResponse.data); pagesIteration = 1; let reachedEndOfPages = false; while (!reachedEndOfPages) { if (listResponse.hasNextPage) { listResponse = await listResponse.nextPage(); + messagesList.push(...listResponse.data); pagesIteration++; } else { reachedEndOfPages = true; From d6d957ae675c9d04e86dc79338e331dcb1ffb3ea Mon Sep 17 00:00:00 2001 From: Antoine SEIN <142824551+asein-sinch@users.noreply.github.com> Date: Mon, 15 Jul 2024 11:51:58 +0200 Subject: [PATCH 15/54] DEVEXP-481: E2E Conversation/Conversations (#108) --- .../inject-conversation-event-request.ts | 35 +-- .../conversation/conversation-request-data.ts | 4 +- .../rest/v1/conversation/conversation-api.ts | 2 + .../v1/conversation/conversations.steps.ts | 257 ++++++++++++++++++ 4 files changed, 264 insertions(+), 34 deletions(-) create mode 100644 packages/conversation/tests/rest/v1/conversation/conversations.steps.ts diff --git a/packages/conversation/src/models/v1/inject-conversation-event-request/inject-conversation-event-request.ts b/packages/conversation/src/models/v1/inject-conversation-event-request/inject-conversation-event-request.ts index 4f837a97..fcab6b5d 100644 --- a/packages/conversation/src/models/v1/inject-conversation-event-request/inject-conversation-event-request.ts +++ b/packages/conversation/src/models/v1/inject-conversation-event-request/inject-conversation-event-request.ts @@ -1,18 +1,13 @@ import { ChannelIdentity } from '../channel-identity'; import { AppEvent } from '../app-event'; -import { ContactEvent } from '../contact-event'; import { ProcessingMode } from '../enums'; -import { ContactMessageEvent } from '../contact-message-event'; /** * Inject Event request */ -export type InjectConversationEventRequest = - InjectConversationAppEvent - | InjectConversationContactEvent - | InjectConversationContactMessageEvent; - -interface InjectConversationEventBase { +export interface InjectConversationEventRequest { + /** @see AppEvent */ + app_event: AppEvent; /** Optional. The ID of the event\'s conversation. Will not be present for apps in Dispatch Mode. */ conversation_id?: string; /** Optional. The ID of the contact. Will not be present for apps in Dispatch Mode. */ @@ -24,27 +19,3 @@ interface InjectConversationEventBase { /** Whether or not Conversation API should store contacts and conversations for the app. For more information, see [Processing Modes](../../../../../conversation/processing-modes/). */ processing_mode?: ProcessingMode; } - -interface InjectConversationAppEvent extends InjectConversationEventBase { - /** @see AppEvent */ - app_event: AppEvent; - // Exclude other event types - contact_event?: never; - contact_message_event?: never; -} - -interface InjectConversationContactEvent extends InjectConversationEventBase { - /** @see AppEvent */ - contact_event: ContactEvent; - // Exclude other event types - app_event?: never; - contact_message_event?: never; -} - -interface InjectConversationContactMessageEvent extends InjectConversationEventBase { - /** @see ContactMessageEvent */ - contact_message_event: ContactMessageEvent; - // Exclude other event types - app_event?: never; - contact_event?: never; -} diff --git a/packages/conversation/src/models/v1/requests/conversation/conversation-request-data.ts b/packages/conversation/src/models/v1/requests/conversation/conversation-request-data.ts index c2be28fc..42e7c30d 100644 --- a/packages/conversation/src/models/v1/requests/conversation/conversation-request-data.ts +++ b/packages/conversation/src/models/v1/requests/conversation/conversation-request-data.ts @@ -30,8 +30,8 @@ export interface InjectMessageRequestData { 'injectMessageRequestBody': InjectMessageRequest; } export interface ListConversationsRequestData { - /** Required. True if only active conversations should be listed. */ - 'only_active': boolean; + /** True if only active conversations should be listed. */ + 'only_active'?: boolean; /** The ID of the app involved in the conversations. Note that either `app_id` or `contact_id` is required in order for the operation to function correctly. */ 'app_id'?: string; /** Resource name (ID) of the contact. Note that either `app_id` or `contact_id` is required in order for the operation to function correctly. */ diff --git a/packages/conversation/src/rest/v1/conversation/conversation-api.ts b/packages/conversation/src/rest/v1/conversation/conversation-api.ts index 750b3f7e..b330af74 100644 --- a/packages/conversation/src/rest/v1/conversation/conversation-api.ts +++ b/packages/conversation/src/rest/v1/conversation/conversation-api.ts @@ -238,6 +238,8 @@ export class ConversationApi extends ConversationDomainApi { 'page_token', 'order', ]); + // Manually set the default page size to 10 otherwise the API returns an empty list + getParams.page_size = getParams.page_size !== undefined ? getParams.page_size : '10'; const headers: { [key: string]: string | undefined } = { 'Content-Type': 'application/json', 'Accept': 'application/json', diff --git a/packages/conversation/tests/rest/v1/conversation/conversations.steps.ts b/packages/conversation/tests/rest/v1/conversation/conversations.steps.ts new file mode 100644 index 00000000..50b281f9 --- /dev/null +++ b/packages/conversation/tests/rest/v1/conversation/conversations.steps.ts @@ -0,0 +1,257 @@ +import { Conversation, ConversationApi, ConversationService, SupportedConversationRegion } from '../../../../src'; +import { Given, Then, When } from '@cucumber/cucumber'; +import * as assert from 'assert'; +import { PageResult } from '@sinch/sdk-client'; + +let conversationsApi: ConversationApi; +let conversation: Conversation.Conversation; +let listResponse: PageResult; +let conversationsList: Conversation.Conversation[]; +let listRecentConversationsResponse: PageResult; +let recentConversationsList: Conversation.ConversationRecentMessage[]; +let pagesIteration: number; +let deleteConversationResponse: void; +let injectEventResponse: Conversation.InjectEventResponse; +let injectMessageResponse: void; +let stopConversationResponse: void; + +Given('the Conversation service "Conversations" is available', () => { + const conversationService = new ConversationService({ + projectId: 'tinyfrog-jump-high-over-lilypadbasin', + keyId: 'keyId', + keySecret: 'keySecret', + authHostname: 'http://localhost:3011', + conversationHostname: 'http://localhost:3014', + conversationRegion: SupportedConversationRegion.UNITED_STATES, + }); + conversationsApi = conversationService.conversation; +}); + +When('I send a request to create a conversation', async () => { + conversation = await conversationsApi.create({ + createConversationRequestBody: { + app_id: '01W4FFL35P4NC4K35CONVAPP001', + contact_id: '01W4FFL35P4NC4K35CONTACT001', + active: true, + active_channel: 'MESSENGER', + metadata: 'e2e tests', + metadata_json: { + prop1: 'value1', + prop2: 'value2', + }, + }, + }); +}); + +Then('the conversation is created', () => { + assert.equal(conversation.id, '01W4FFL35P4NC4K35CONVERS001'); +}); + +When('I send a request to list the existing conversations', async () => { + listResponse = await conversationsApi.list({ + app_id: '01W4FFL35P4NC4K35CONVAPP001', + page_size: 2, + }); +}); + +Then('the response contains {string} conversations', (expectedAnswer: string) => { + const expectedConversationsCount = parseInt(expectedAnswer, 10); + assert.equal(listResponse.data.length, expectedConversationsCount); +}); + +When('I send a request to list all the conversations', async () => { + conversationsList = []; + for await (const conversation of conversationsApi.list({ + app_id: '01W4FFL35P4NC4K35CONVAPP001', + page_size: 2, + })) { + conversationsList.push(conversation); + } +}); + +When('I iterate manually over the conversations pages', async () => { + conversationsList = []; + listResponse = await conversationsApi.list({ + app_id: '01W4FFL35P4NC4K35CONVAPP001', + page_size: 2, + }); + conversationsList.push(...listResponse.data); + pagesIteration = 1; + let reachedEndOfPages = false; + while (!reachedEndOfPages) { + if (listResponse.hasNextPage) { + listResponse = await listResponse.nextPage(); + conversationsList.push(...listResponse.data); + pagesIteration++; + } else { + reachedEndOfPages = true; + } + } +}); + +Then('the conversations list contains {string} conversations', (expectedAnswer: string) => { + const expectedConversationsCount = parseInt(expectedAnswer, 10); + assert.equal(conversationsList.length, expectedConversationsCount); +}); + +Then('the conversations 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 list the recent conversations', async () => { + listRecentConversationsResponse = await conversationsApi.listRecent({ + app_id: '01W4FFL35P4NC4K35CONVAPP001', + page_size: 2, + }); +}); + +Then('the response contains {string} recent conversations', (expectedAnswer: string) => { + const expectedRecentConversationsCount = parseInt(expectedAnswer, 10); + assert.equal(listRecentConversationsResponse.data.length, expectedRecentConversationsCount); +}); + +When('I send a request to list all the recent conversations', async () => { + recentConversationsList = []; + for await (const recentConversation of conversationsApi.listRecent({ + app_id: '01W4FFL35P4NC4K35CONVAPP001', + page_size: 2, + })) { + recentConversationsList.push(recentConversation); + } +}); + +When('I iterate manually over the recent conversations pages', async () => { + recentConversationsList = []; + listRecentConversationsResponse = await conversationsApi.listRecent({ + app_id: '01W4FFL35P4NC4K35CONVAPP001', + page_size: 2, + }); + recentConversationsList.push(...listRecentConversationsResponse.data); + pagesIteration = 1; + let reachedEndOfPages = false; + while (!reachedEndOfPages) { + if (listRecentConversationsResponse.hasNextPage) { + listRecentConversationsResponse = await listRecentConversationsResponse.nextPage(); + recentConversationsList.push(...listRecentConversationsResponse.data); + pagesIteration++; + } else { + reachedEndOfPages = true; + } + } +}); + +Then('the recent conversations list contains {string} recent conversations', (expectedAnswer: string) => { + const expectedRecentConversationsCount = parseInt(expectedAnswer, 10); + assert.equal(conversationsList.length, expectedRecentConversationsCount); +}); + +Then('the recent conversations 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 a conversation', async () => { + conversation = await conversationsApi.get({ + conversation_id: '01W4FFL35P4NC4K35CONVERS001', + }); +}); + +Then('the response contains the conversation details', () => { + assert.equal(conversation.id, '01W4FFL35P4NC4K35CONVERS001'); + assert.equal(conversation.app_id, '01W4FFL35P4NC4K35CONVAPP001'); + assert.equal(conversation.contact_id, '01W4FFL35P4NC4K35CONTACT002'); + assert.deepEqual(conversation.last_received, new Date('2024-06-06T14:42:42Z')); + assert.equal(conversation.active_channel, 'MESSENGER'); + assert.equal(conversation.active, true); + assert.equal(conversation.metadata, 'e2e tests'); + assert.deepEqual(conversation.metadata_json, { + prop1: 'value1', + prop2: 'value2', + }); + assert.equal(conversation.correlation_id, ''); +}); + +When('I send a request to update a conversation', async () => { + conversation = await conversationsApi.update({ + conversation_id: '01W4FFL35P4NC4K35CONVERS001', + updateConversationRequestBody: { + active: false, + app_id: '01W4FFL35P4NC4K35CONVAPP002', + metadata: 'Transferred conversation', + correlation_id: 'my-correlator', + }, + }); +}); + +Then('the response contains the conversation details with updated data', () => { + assert.equal(conversation.id, '01W4FFL35P4NC4K35CONVERS001'); + assert.equal(conversation.app_id, '01W4FFL35P4NC4K35CONVAPP002'); + assert.equal(conversation.active, false); + assert.equal(conversation.metadata, 'Transferred conversation'); + assert.equal(conversation.correlation_id, 'my-correlator'); +}); + +When('I send a request to delete a conversation', async () => { + deleteConversationResponse = await conversationsApi.delete({ + conversation_id: '01W4FFL35P4NC4K35CONVERS001', + }); +}); + +Then('the delete conversation response contains no data', () => { + assert.deepEqual(deleteConversationResponse, {} ); +}); + +When('I send a request to inject an event into a conversation', async () => { + injectEventResponse = await conversationsApi.injectEvent({ + conversation_id: '01W4FFL35P4NC4K35CONVERS001', + injectConversationEventRequestBody: { + app_event: { + composing_event: {}, + }, + accept_time: new Date('2024-06-06T15:15:15Z'), + }, + }); +}); + +Then('the event is created and injected in the conversation', () => { + assert.equal(injectEventResponse.event_id, '01W4FFL35P4NC4K35CONVEVENT1'); + assert.deepEqual(injectEventResponse.accepted_time, new Date('2024-06-06T15:15:15Z')); +}); + +When('I send a request to inject a message into a conversation', async () => { + injectMessageResponse = await conversationsApi.injectMessage({ + conversation_id: '01W4FFL35P4NC4K35CONVERS001', + injectMessageRequestBody: { + app_message: { + text_message: { + text: 'Injected text message', + }, + }, + accept_time: new Date('2024-06-06T14:42:42Z'), + direction: 'TO_CONTACT', + contact_id: '01W4FFL35P4NC4K35CONTACT002', + channel_identity: { + channel: 'MESSENGER', + identity: '7968425018576406', + app_id: '01W4FFL35P4NC4K35CONVAPP001', + }, + }, + }); +}); + +Then('the message is created and injected in the conversation', () => { + assert.deepEqual(injectMessageResponse, {}); +}); + +When('I send a request to stop a conversation', async () => { + stopConversationResponse = await conversationsApi.stopActive({ + conversation_id: '01W4FFL35P4NC4K35CONVERS001', + }); +}); + +Then('the stop conversation response contains no data', () => { + assert.deepEqual(stopConversationResponse, {} ); +}); From 93b006f0534d208ea882f46cbb5dbb7bd1ece32e Mon Sep 17 00:00:00 2001 From: Antoine SEIN <142824551+asein-sinch@users.noreply.github.com> Date: Wed, 17 Jul 2024 09:34:49 +0200 Subject: [PATCH 16/54] DEVEXP-482: E2E Conversation/Events (#109) --- .../tests/rest/v1/events/events.steps.ts | 124 ++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 packages/conversation/tests/rest/v1/events/events.steps.ts diff --git a/packages/conversation/tests/rest/v1/events/events.steps.ts b/packages/conversation/tests/rest/v1/events/events.steps.ts new file mode 100644 index 00000000..16693d77 --- /dev/null +++ b/packages/conversation/tests/rest/v1/events/events.steps.ts @@ -0,0 +1,124 @@ +import { Conversation, ConversationService, EventsApi, SupportedConversationRegion } from '../../../../src'; +import { Given, Then, When } from '@cucumber/cucumber'; +import * as assert from 'assert'; +import { PageResult } from '@sinch/sdk-client'; + +let eventsApi: EventsApi; +let eventResponse: Conversation.SendEventResponse; +let listResponse: PageResult; +let eventsList: Conversation.ConversationEvent[]; +let pagesIteration: number; +let event: Conversation.ConversationEvent; +let deleteEventResponse: void; + +Given('the Conversation service "Events" is available', function () { + const conversationService = new ConversationService({ + projectId: 'tinyfrog-jump-high-over-lilypadbasin', + keyId: 'keyId', + keySecret: 'keySecret', + authHostname: 'http://localhost:3011', + conversationHostname: 'http://localhost:3014', + conversationRegion: SupportedConversationRegion.UNITED_STATES, + }); + eventsApi = conversationService.events; +}); + +When('I send a request to send a conversation event to a contact', async () => { + eventResponse = await eventsApi.send({ + sendEventRequestBody: { + event: { + composing_event: {}, + }, + app_id: '01W4FFL35P4NC4K35CONVAPP001', + recipient: { + contact_id: '01W4FFL35P4NC4K35CONTACT001', + }, + }, + }); +}); + +Then('the response contains the id of the conversation event', () => { + assert.equal(eventResponse.event_id, '01W4FFL35P4NC4K35CONVEVENT1'); +}); + +When('I send a request to list the existing conversation events', async () => { + listResponse = await eventsApi.list({ + page_size: 2, + }); +}); + +Then('the response contains {string} conversation events', (expectedAnswer: string) => { + const expectedEventsCount = parseInt(expectedAnswer, 10); + assert.equal(listResponse.data.length, expectedEventsCount); +}); + +When('I send a request to list all the conversation events', async () => { + eventsList = []; + for await (const message of eventsApi.list({ page_size: 2 })) { + eventsList.push(message); + } +}); + +When('I iterate manually over the conversation events pages', async () => { + eventsList = []; + listResponse = await eventsApi.list({ + page_size: 2, + }); + eventsList.push(...listResponse.data); + pagesIteration = 1; + let reachedEndOfPages = false; + while (!reachedEndOfPages) { + if (listResponse.hasNextPage) { + listResponse = await listResponse.nextPage(); + eventsList.push(...listResponse.data); + pagesIteration++; + } else { + reachedEndOfPages = true; + } + } +}); + +Then('the conversation events list contains {string} conversation events', (expectedAnswer: string) => { + const expectedServices = parseInt(expectedAnswer, 10); + assert.equal(eventsList.length, expectedServices); +}); + +Then('the conversation events 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 a conversation event', async () => { + event = await eventsApi.get({ + event_id: '01W4FFL35P4NC4K35CONVEVENT1', + }); +}); + +Then('the response contains the conversation event details', () => { + assert.equal(event.id, '01W4FFL35P4NC4K35CONVEVENT1'); + assert.equal(event.direction, 'TO_CONTACT'); + assert.equal(event.conversation_id, '01W4FFL35P4NC4K35CONVERSATI'); + assert.equal(event.contact_id, '01W4FFL35P4NC4K35CONTACT001'); + assert.deepEqual(event.accept_time, new Date('2024-06-06T12:42:42Z')); + assert.equal(event.processing_mode, 'CONVERSATION'); + const channelIdentity: Conversation.ChannelIdentity = { + channel: 'MESSENGER', + identity: '7968425018576406', + app_id: '01W4FFL35P4NC4K35CONVAPP001', + }; + assert.deepEqual(event.channel_identity, channelIdentity); + const composingEvent: Conversation.ComposingEvent = { + composing_event: {} + }; + assert.deepEqual(event.app_event, composingEvent); +}); + +When('I send a request to delete a conversation event', async () => { + deleteEventResponse = await eventsApi.delete({ + event_id: '01W4FFL35P4NC4K35CONVEVENT1', + }); +}); + +Then('the delete conversation event response contains no data', () => { + assert.deepEqual(deleteEventResponse, {} ); +}); From b1c7e43c8865da4f9a02dd8afd33c70defee3cb2 Mon Sep 17 00:00:00 2001 From: Antoine SEIN <142824551+asein-sinch@users.noreply.github.com> Date: Wed, 17 Jul 2024 09:54:16 +0200 Subject: [PATCH 17/54] DEVEXP-483: E2E Conversation/Transcoding (#110) --- .../tests/rest/v1/app/app.steps.ts | 3 +- .../rest/v1/transcoding/transcoding.steps.ts | 79 +++++++++++++++++++ 2 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 packages/conversation/tests/rest/v1/transcoding/transcoding.steps.ts diff --git a/packages/conversation/tests/rest/v1/app/app.steps.ts b/packages/conversation/tests/rest/v1/app/app.steps.ts index e47c791f..0fba87cd 100644 --- a/packages/conversation/tests/rest/v1/app/app.steps.ts +++ b/packages/conversation/tests/rest/v1/app/app.steps.ts @@ -1,7 +1,6 @@ import { AppApi, Conversation, ConversationService, SupportedConversationRegion } from '../../../../src'; import { Given, Then, When } from '@cucumber/cucumber'; import * as assert from 'assert'; -import { ChannelCredentialsSmsResponse } from '../../../../src/models'; let appsApi: AppApi; let appResponse: Conversation.AppResponse; @@ -90,7 +89,7 @@ Then('the response contains the app details', () => { assert.equal(appResponse.delivery_report_based_fallback?.enabled, false); assert.equal(appResponse.delivery_report_based_fallback?.delivery_report_waiting_time, 0); assert.equal(appResponse.message_retry_settings?.retry_duration, 3600); - const channelCredentials = appResponse.channel_credentials![0] as ChannelCredentialsSmsResponse; + const channelCredentials = appResponse.channel_credentials![0] as Conversation.ChannelCredentialsSmsResponse; assert.equal(channelCredentials.channel, 'SMS'); assert.equal(channelCredentials.static_bearer.claimed_identity, 'SpaceMonkeySquadron'); assert.equal(channelCredentials.static_bearer.token, '00112233445566778899aabbccddeeff'); diff --git a/packages/conversation/tests/rest/v1/transcoding/transcoding.steps.ts b/packages/conversation/tests/rest/v1/transcoding/transcoding.steps.ts new file mode 100644 index 00000000..d0eb967d --- /dev/null +++ b/packages/conversation/tests/rest/v1/transcoding/transcoding.steps.ts @@ -0,0 +1,79 @@ +import { Conversation, ConversationService, SupportedConversationRegion, TranscodingApi } from '../../../../src'; +import { Given, Then, When } from '@cucumber/cucumber'; +import * as assert from 'assert'; + +let transcodingApi: TranscodingApi; +let transcodeMessageResponse: Conversation.TranscodeMessageResponse; + +Given('the Conversation service "Transcoding" is available', function () { + const conversationService = new ConversationService({ + projectId: 'tinyfrog-jump-high-over-lilypadbasin', + keyId: 'keyId', + keySecret: 'keySecret', + authHostname: 'http://localhost:3011', + conversationHostname: 'http://localhost:3014', + conversationRegion: SupportedConversationRegion.UNITED_STATES, + }); + transcodingApi = conversationService.transcoding; +}); + +When('I send a request to transcode a location message', async () => { + transcodeMessageResponse = await transcodingApi.transcodeMessage({ + transcodeMessageRequestBody: { + app_id: '01W4FFL35P4NC4K35CONVAPP001', + app_message: { + location_message: { + title: 'Phare d\'Eckmรผhl', + label: 'Pointe de Penmarch', + coordinates: { + latitude: 47.7981899, + longitude: -4.3727685, + }, + }, + }, + channels: [ + 'APPLEBC', + 'INSTAGRAM', + 'KAKAOTALK', + 'KAKAOTALKCHAT', + 'LINE', + 'MESSENGER', + 'RCS', + 'SMS', + 'TELEGRAM', + 'VIBER', + 'WECHAT', + 'WHATSAPP', + ], + }, + }); +}); + +Then('the location message is transcoded for all the channels', () => { + assert.ok(transcodeMessageResponse.transcoded_message?.APPLEBC); + assert.ok(transcodeMessageResponse.transcoded_message?.INSTAGRAM); + assert.ok(transcodeMessageResponse.transcoded_message?.KAKAOTALK); + assert.ok(transcodeMessageResponse.transcoded_message?.KAKAOTALKCHAT); + assert.ok(transcodeMessageResponse.transcoded_message?.LINE); + assert.ok(transcodeMessageResponse.transcoded_message?.MESSENGER); + assert.ok(transcodeMessageResponse.transcoded_message?.RCS); + assert.ok(transcodeMessageResponse.transcoded_message?.SMS); + assert.ok(transcodeMessageResponse.transcoded_message?.TELEGRAM); + assert.ok(transcodeMessageResponse.transcoded_message?.VIBER); + assert.ok(transcodeMessageResponse.transcoded_message?.WECHAT); + assert.ok(transcodeMessageResponse.transcoded_message?.WHATSAPP); + const expectedWhatsAppMessage = { + to: '{{to}}', + type: 'location', + recipient_type: 'individual', + location: { + name: 'Phare d\'Eckmรผhl', + address: 'Pointe de Penmarch', + longitude: '-4.3727684', + latitude: '47.79819', + }, + messaging_product: 'whatsapp', + biz_opaque_callback_data: null, + }; + assert.deepEqual(JSON.parse(transcodeMessageResponse.transcoded_message?.WHATSAPP), expectedWhatsAppMessage); +}); From bdbf3f8c1674973eb5af4ae968270a070e590894 Mon Sep 17 00:00:00 2001 From: Antoine SEIN <142824551+asein-sinch@users.noreply.github.com> Date: Wed, 17 Jul 2024 09:57:56 +0200 Subject: [PATCH 18/54] DEVEXP-484: E2E Conversation/Capability (#111) --- .../rest/v1/capability/capability.steps.ts | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 packages/conversation/tests/rest/v1/capability/capability.steps.ts diff --git a/packages/conversation/tests/rest/v1/capability/capability.steps.ts b/packages/conversation/tests/rest/v1/capability/capability.steps.ts new file mode 100644 index 00000000..f738d568 --- /dev/null +++ b/packages/conversation/tests/rest/v1/capability/capability.steps.ts @@ -0,0 +1,35 @@ +import { Conversation, ConversationService, SupportedConversationRegion, CapabilityApi } from '../../../../src'; +import { Given, Then, When } from '@cucumber/cucumber'; +import * as assert from 'assert'; + +let capabilityApi: CapabilityApi; +let lookupCapabilityResponse: Conversation.LookupCapabilityResponse; + +Given('the Conversation service "Capability" is available', function () { + const conversationService = new ConversationService({ + projectId: 'tinyfrog-jump-high-over-lilypadbasin', + keyId: 'keyId', + keySecret: 'keySecret', + authHostname: 'http://localhost:3011', + conversationHostname: 'http://localhost:3014', + conversationRegion: SupportedConversationRegion.UNITED_STATES, + }); + capabilityApi = conversationService.capability; +}); + +When('I send a request to query a capability lookup', async () => { + lookupCapabilityResponse = await capabilityApi.lookup({ + lookupCapabilityRequestBody: { + app_id: '01W4FFL35P4NC4K35CONVAPP001', + recipient: { + contact_id: '01W4FFL35P4NC4K35CONTACT001', + }, + }, + }); +}); + +Then('the response contains the id of the capability lookup query', () => { + assert.equal(lookupCapabilityResponse.app_id, '01W4FFL35P4NC4K35CONVAPP001'); + assert.equal(lookupCapabilityResponse.recipient?.contact_id, '01W4FFL35P4NC4K35CONTACT001'); + assert.equal(lookupCapabilityResponse.request_id, '01W4FFL35P4NC4K35CAPABILITY'); +}); From 7ddc3a14c631b5a32beec236f22c95c7c33c7f12 Mon Sep 17 00:00:00 2001 From: Antoine SEIN <142824551+asein-sinch@users.noreply.github.com> Date: Wed, 17 Jul 2024 10:10:03 +0200 Subject: [PATCH 19/54] DEVEXP-485: E2E Conversation/Webhooks (#112) --- .../webhooks/webhooks-request-data.ts | 6 +- .../src/models/v1/webhook/index.ts | 2 +- .../src/models/v1/webhook/webhook.ts | 9 +- .../tests/rest/v1/webhooks/webhooks.steps.ts | 119 ++++++++++++++++++ 4 files changed, 131 insertions(+), 5 deletions(-) create mode 100644 packages/conversation/tests/rest/v1/webhooks/webhooks.steps.ts diff --git a/packages/conversation/src/models/v1/requests/webhooks/webhooks-request-data.ts b/packages/conversation/src/models/v1/requests/webhooks/webhooks-request-data.ts index 4212795a..53ab9944 100644 --- a/packages/conversation/src/models/v1/requests/webhooks/webhooks-request-data.ts +++ b/packages/conversation/src/models/v1/requests/webhooks/webhooks-request-data.ts @@ -1,8 +1,8 @@ -import { Webhook } from '../../webhook'; +import { CreateWebhookRequestBody, UpdateWebhookRequestBody } from '../../webhook'; export interface CreateWebhookRequestData { /** Required. The Webhook to create */ - 'webhookCreateRequestBody': Webhook; + 'webhookCreateRequestBody': CreateWebhookRequestBody; } export interface DeleteWebhookRequestData { /** The unique ID of the webhook. */ @@ -20,7 +20,7 @@ export interface UpdateWebhookRequestData { /** The unique ID of the webhook. */ 'webhook_id': string; /** Required. The Webhook to update */ - 'webhookUpdateRequestBody': Webhook; + 'webhookUpdateRequestBody': UpdateWebhookRequestBody; /** The set of field mask paths. */ 'update_mask'?: Array; } diff --git a/packages/conversation/src/models/v1/webhook/index.ts b/packages/conversation/src/models/v1/webhook/index.ts index 54c42503..5aeb5bd4 100644 --- a/packages/conversation/src/models/v1/webhook/index.ts +++ b/packages/conversation/src/models/v1/webhook/index.ts @@ -1 +1 @@ -export type { Webhook } from './webhook'; +export type { Webhook, CreateWebhookRequestBody, UpdateWebhookRequestBody } from './webhook'; diff --git a/packages/conversation/src/models/v1/webhook/webhook.ts b/packages/conversation/src/models/v1/webhook/webhook.ts index af60e63d..ebb60d6e 100644 --- a/packages/conversation/src/models/v1/webhook/webhook.ts +++ b/packages/conversation/src/models/v1/webhook/webhook.ts @@ -6,7 +6,6 @@ import { WebhookTargetType } from '../enums'; * Represents a destination for receiving callbacks from the Conversation API. */ export interface Webhook { - /** The app that this webhook belongs to. */ app_id: string; /** @see ClientCredentials */ @@ -22,3 +21,11 @@ export interface Webhook { /** An array of triggers that should trigger the webhook and result in an event being sent to the target url. Refer to the list of [Webhook Triggers](/docs/conversation/callbacks#webhook-triggers) for a complete list. */ triggers: WebhookTrigger[]; } + +export interface UpdateWebhookRequestBody extends + Pick, + Partial> {} + +export interface CreateWebhookRequestBody extends + Pick, + Partial> {} diff --git a/packages/conversation/tests/rest/v1/webhooks/webhooks.steps.ts b/packages/conversation/tests/rest/v1/webhooks/webhooks.steps.ts new file mode 100644 index 00000000..070015f4 --- /dev/null +++ b/packages/conversation/tests/rest/v1/webhooks/webhooks.steps.ts @@ -0,0 +1,119 @@ +import { Conversation, ConversationService, WebhooksApi, SupportedConversationRegion } from '../../../../src'; +import { Given, Then, When } from '@cucumber/cucumber'; +import * as assert from 'assert'; + +let webhooksApi: WebhooksApi; +let webhooksList: Conversation.ListWebhooksResponse; +let webhook: Conversation.Webhook; +let deleteWebhookResponse: void; + +Given('the Conversation service "Webhooks" is available', function () { + const conversationService = new ConversationService({ + projectId: 'tinyfrog-jump-high-over-lilypadbasin', + keyId: 'keyId', + keySecret: 'keySecret', + authHostname: 'http://localhost:3011', + conversationHostname: 'http://localhost:3014', + conversationRegion: SupportedConversationRegion.UNITED_STATES, + }); + webhooksApi = conversationService.webhooks; +}); + +When('I send a request to create a conversation webhook', async () => { + webhook = await webhooksApi.create({ + webhookCreateRequestBody: { + app_id: '01W4FFL35P4NC4K35CONVAPP001', + target: 'https://my-callback-server.com/capability', + triggers: [ 'CAPABILITY' ], + secret: 'CactusKnight_SurfsWaves', + target_type: 'HTTP', + }, + }); +}); + +Then('the conversation webhook is created', () => { + assert.equal(webhook.id, '01W4FFL35P4NC4K35WEBHOOK004'); + assert.deepEqual(webhook.triggers, ['CAPABILITY']); + assert.equal(webhook.secret, 'CactusKnight_SurfsWaves'); +}); + +When('I send a request to list the conversation webhooks for an app', async () => { + webhooksList = await webhooksApi.list({ + app_id: '01W4FFL35P4NC4K35CONVAPP001', + }); +}); + +Then('the response contains the list of conversation webhooks', () => { + assert.equal(webhooksList.webhooks?.length, 4); + const webhook = webhooksList.webhooks![1]; + const triggersList: Conversation.WebhookTrigger[] = [ + 'CONTACT_CREATE', + 'CONTACT_DELETE', + 'CONTACT_IDENTITIES_DUPLICATION', + 'CONTACT_MERGE', + 'CONTACT_UPDATE', + ]; + assert.deepEqual(webhook.triggers, triggersList); + assert.equal(webhook.id, '01W4FFL35P4NC4K35WEBHOOK002'); + assert.equal(webhook.target_type, 'HTTP'); + assert.equal(webhook.secret, 'DiscoDragon_BuildsLego'); + assert.equal(webhook.client_credentials, null); +}); + +When('I send a request to retrieve a conversation webhook', async () => { + webhook = await webhooksApi.get({ + webhook_id: '01W4FFL35P4NC4K35WEBHOOK001', + }); +}); + +Then('the response contains the conversation webhook details', () => { + assert.equal(webhook.id, '01W4FFL35P4NC4K35WEBHOOK001'); + assert.equal(webhook.app_id, '01W4FFL35P4NC4K35CONVAPP001'); + assert.equal(webhook.target, 'https://my-callback-server.com/unsupported'); + assert.equal(webhook.target_type, 'HTTP'); + assert.equal(webhook.secret, 'VeganVampire_SipsTea'); + const triggersList: Conversation.WebhookTrigger[] = ['UNSUPPORTED']; + assert.deepEqual(webhook.triggers, triggersList); + const credentials: Conversation.ClientCredentials = { + endpoint: 'https://my-auth-server.com/oauth2/token', + client_id: 'webhook-username', + client_secret: 'webhook-password', + }; + assert.deepEqual(webhook.client_credentials, credentials); +}); + +When('I send a request to update a conversation webhook', async () => { + webhook = await webhooksApi.update({ + webhook_id: '01W4FFL35P4NC4K35WEBHOOK004', + webhookUpdateRequestBody: { + app_id: '01W4FFL35P4NC4K35CONVAPP002', + target: 'https://my-callback-server.com/capability-optin-optout', + triggers: [ + 'CAPABILITY', + 'OPT_IN', + 'OPT_OUT', + ], + secret: 'SpacePanda_RidesUnicycle', + }, + }); +}); + +Then('the response contains the conversation webhook details with updated data', () => { + assert.equal(webhook.id, '01W4FFL35P4NC4K35WEBHOOK004'); + assert.equal(webhook.app_id, '01W4FFL35P4NC4K35CONVAPP002'); + assert.equal(webhook.target, 'https://my-callback-server.com/capability-optin-optout'); + assert.deepEqual(webhook.triggers, ['CAPABILITY', 'OPT_IN', 'OPT_OUT']); + assert.equal(webhook.target_type, 'HTTP'); + assert.equal(webhook.secret, 'SpacePanda_RidesUnicycle'); + assert.equal(webhook.client_credentials, null); +}); + +When('I send a request to delete a conversation webhook', async () => { + deleteWebhookResponse = await webhooksApi.delete({ + webhook_id: '01W4FFL35P4NC4K35WEBHOOK004', + }); +}); + +Then('the delete conversation webhook response contains no data', () => { + assert.deepEqual(deleteWebhookResponse, {} ); +}); From 80026c7d370145e16c82b50bbdb44e6d56498d1f Mon Sep 17 00:00:00 2001 From: Antoine SEIN <142824551+asein-sinch@users.noreply.github.com> Date: Mon, 22 Jul 2024 10:00:35 +0200 Subject: [PATCH 20/54] DEVEXP-486: E2E Conversation/TemplatesV1 (#113) --- .github/workflows/run-ci.yaml | 32 ++-- .../v1/templates-v1/templates-v1.steps.ts | 170 ++++++++++++++++++ 2 files changed, 191 insertions(+), 11 deletions(-) create mode 100644 packages/conversation/tests/rest/v1/templates-v1/templates-v1.steps.ts diff --git a/.github/workflows/run-ci.yaml b/.github/workflows/run-ci.yaml index e51156b2..8b879719 100644 --- a/.github/workflows/run-ci.yaml +++ b/.github/workflows/run-ci.yaml @@ -43,26 +43,36 @@ jobs: - name: Wait for the mock servers to be healthy run: | cd sinch-sdk-mockserver - while ! docker inspect --format='{{json .State.Health.Status}}' $(docker-compose ps -q authentication-server) | grep -q '"healthy"'; do - echo "Waiting for authentication-server to be healthy..." - sleep 2 - done - while ! docker inspect --format='{{json .State.Health.Status}}' $(docker-compose ps -q fax-server) | grep -q '"healthy"'; do - echo "Waiting for fax-server to be healthy..." - sleep 2 - done - while ! docker inspect --format='{{json .State.Health.Status}}' $(docker-compose ps -q numbers-server) | grep -q '"healthy"'; do - echo "Waiting for numbers-server to be healthy..." - sleep 2 + servers=( + "authentication-server" + "fax-server" + "numbers-server" + "conversation-server" + "conversation-templates-server" + ) + + for server in "${servers[@]}"; do + SECONDS=0 + while ! docker inspect --format='{{json .State.Health.Status}}' $(docker-compose ps -q $server) | grep -q '"healthy"'; do + if [ $SECONDS -ge 30 ]; then + echo "Timeout: $server did not become healthy within 30 seconds." + exit 1 + fi + echo "Waiting for $server to be healthy..." + sleep 2 + done + echo "$server is healthy!" done - name: Create target directories for feature files run: | mkdir -p ./packages/fax/tests/e2e/features mkdir -p ./packages/numbers/tests/e2e/features + mkdir -p ./packages/conversation/tests/e2e/features - name: Copy feature files run: | cp sinch-sdk-mockserver/features/fax/*.feature ./packages/fax/tests/e2e/features/ cp sinch-sdk-mockserver/features/numbers/*.feature ./packages/numbers/tests/e2e/features/ + cp sinch-sdk-mockserver/features/conversation/*.feature ./packages/conversation/tests/e2e/features/ - name: Run e2e tests run: | yarn install diff --git a/packages/conversation/tests/rest/v1/templates-v1/templates-v1.steps.ts b/packages/conversation/tests/rest/v1/templates-v1/templates-v1.steps.ts new file mode 100644 index 00000000..b8c312ac --- /dev/null +++ b/packages/conversation/tests/rest/v1/templates-v1/templates-v1.steps.ts @@ -0,0 +1,170 @@ +import { Conversation, ConversationService, SupportedConversationRegion, TemplatesV1Api } from '../../../../src'; +import { Given, Then, When } from '@cucumber/cucumber'; +import * as assert from 'assert'; + +let templatesV1Api: TemplatesV1Api; +let template: Conversation.V1Template; +let templatesList: Conversation.V1ListTemplatesResponse; +let deleteTemplateResponse: void; + +const enUSTranslation: Conversation.V1TemplateTranslation = { + language_code: 'en-US', + variables: [ + { + key: 'name', + preview_value: 'Professor Jones', + }, + ], + content: '{"text_message":{"text":"Hello ${name}. Text message template created with V1 API"}}', +}; + +const frFRTranslation: Conversation.V1TemplateTranslation = { + language_code: 'fr-FR', + variables: [ + { + key: 'name', + preview_value: 'Professeur Jones', + }, + ], + content: '{"text_message":{"text":"Bonjour ${name}. Ce message texte provient d\'un template V1"}}', +}; + +Given('the Conversation service "TemplatesV1" is available', function () { + const conversationService = new ConversationService({ + projectId: 'tinyfrog-jump-high-over-lilypadbasin', + keyId: 'keyId', + keySecret: 'keySecret', + authHostname: 'http://localhost:3011', + conversationTemplatesHostname: 'http://localhost:3015', + conversationRegion: SupportedConversationRegion.UNITED_STATES, + }); + templatesV1Api = conversationService.templatesV1; +}); + +When('I send a request to create a conversation template with the V1 API', async () => { + template = await templatesV1Api.create({ + createTemplateRequestBody: { + default_translation: 'en-US', + channel: 'MESSENGER', + description: 'Text template V1', + translations: [ + { + ...enUSTranslation, + version: '1', + }, + ], + }, + }); +}); + +Then('the conversation template V1 is created', () => { + assert.equal(template.id, '01W4FFL35P4NC4K35TEMPLATE01'); +}); + +When('I send a request to list the conversation templates with the V1 API', async () => { + templatesList = await templatesV1Api.list({}); +}); + +Then('the response contains the list of conversation templates with the V1 structure', () => { + assert.equal(templatesList.templates?.length, 2); +}); + +When('I send a request to retrieve a conversation template with the V1 API', async () => { + template = await templatesV1Api.get({ + template_id: '01W4FFL35P4NC4K35TEMPLATE01', + }); +}); + +Then('the response contains the conversation template details with the V1 structure', () => { + assert.equal(template.id, '01W4FFL35P4NC4K35TEMPLATE01'); + assert.equal(template.description, 'Text template V1'); + assert.equal(template.translations?.length, 1); + const expectedTranslation: Conversation.V1TemplateTranslation = { + language_code: 'en-US', + content: '{"text_message":{"text":"Hello ${name}. Text message template created with V1 API"}}', + version: '1', + create_time: new Date('2024-06-06T14:42:42Z'), + update_time: new Date('2024-06-06T14:42:42Z'), + variables: [ + { + key: 'name', + preview_value: 'Professor Jones', + }, + ], + }; + assert.deepEqual(template.translations?.[0], expectedTranslation); + assert.equal(template.default_translation, 'en-US'); + assert.deepEqual(template.create_time, new Date('2024-06-06T14:42:42Z')); + assert.deepEqual(template.update_time, new Date('2024-06-06T14:42:42Z')); + assert.equal(template.channel, 'UNSPECIFIED'); +}); + +When('I send a request to update a conversation template with the V1 API', async () => { + template = await templatesV1Api.update({ + template_id: '01W4FFL35P4NC4K35TEMPLATE01', + updateTemplateRequestBody: { + description: 'Updated text template V1', + channel: 'SMS', + default_translation: 'fr-FR', + translations: [ + { + ...enUSTranslation, + content: '{"text_message":{"text":"Hello ${name}. This text message template has been created with V1 API"}}', + version: '2', + }, + { + ...frFRTranslation, + version: '1', + }, + ], + }, + }); +}); + +Then('the response contains the conversation template details with updated data with the V1 structure', () => { + assert.equal(template.id, '01W4FFL35P4NC4K35TEMPLATE01'); + assert.equal(template.description, 'Updated text template V1'); + assert.equal(template.translations?.length, 2); + const expectedenUSTranslation: Conversation.V1TemplateTranslation = { + language_code: 'en-US', + content: '{"text_message":{"text":"Hello ${name}. This text message template has been created with V1 API"}}', + version: '2', + create_time: new Date('2024-06-06T14:45:45Z'), + update_time: new Date('2024-06-06T14:45:45Z'), + variables: [ + { + key: 'name', + preview_value: 'Professor Jones', + }, + ], + }; + assert.deepEqual(template.translations?.[1], expectedenUSTranslation); + const expectedfrFRTranslation: Conversation.V1TemplateTranslation = { + language_code: 'fr-FR', + content: '{"text_message":{"text":"Bonjour ${name}. Ce message texte provient d\'un template V1"}}', + version: '1', + create_time: new Date('2024-06-06T14:45:45Z'), + update_time: new Date('2024-06-06T14:45:45Z'), + variables: [ + { + key: 'name', + preview_value: 'Professeur Jones', + }, + ], + }; + assert.deepEqual(template.translations?.[0], expectedfrFRTranslation); + assert.equal(template.default_translation, 'fr-FR'); + assert.deepEqual(template.create_time, new Date('2024-06-06T14:42:42Z')); + assert.deepEqual(template.update_time, new Date('2024-06-06T14:45:45Z')); + assert.equal(template.channel, 'UNSPECIFIED'); +}); + +When('I send a request to delete a conversation template with the V1 API', async () => { + deleteTemplateResponse = await templatesV1Api.delete({ + template_id: '01W4FFL35P4NC4K35TEMPLATE01', + }); +}); + +Then('the delete conversation template response V1 contains no data', () => { + assert.deepEqual(deleteTemplateResponse, {} ); +}); From 23da75f102720d70922de4930605b0aa2b1e9f0e Mon Sep 17 00:00:00 2001 From: Antoine SEIN <142824551+asein-sinch@users.noreply.github.com> Date: Mon, 22 Jul 2024 10:17:57 +0200 Subject: [PATCH 21/54] DEVEXP-487: E2E Conversation/TemplatesV2 (#114) --- .../templates-v2/templates-v2-request-data.ts | 4 +- .../src/models/v1/v2-template/v2-template.ts | 3 +- .../v1/templates-v2/templates-v2.steps.ts | 193 ++++++++++++++++++ .../tests/rest/v1/webhooks/webhooks.steps.ts | 1 - 4 files changed, 197 insertions(+), 4 deletions(-) create mode 100644 packages/conversation/tests/rest/v1/templates-v2/templates-v2.steps.ts diff --git a/packages/conversation/src/models/v1/requests/templates-v2/templates-v2-request-data.ts b/packages/conversation/src/models/v1/requests/templates-v2/templates-v2-request-data.ts index a1ec21e5..fbbe9b47 100644 --- a/packages/conversation/src/models/v1/requests/templates-v2/templates-v2-request-data.ts +++ b/packages/conversation/src/models/v1/requests/templates-v2/templates-v2-request-data.ts @@ -2,7 +2,7 @@ import { V2Template } from '../../v2-template'; export interface V2CreateTemplateRequestData { /** Required. The template to create. */ - 'createTemplateRequestBody': V2Template; + 'createTemplateRequestBody': Omit; } export interface V2DeleteTemplateRequestData { /** Required. The ID of the template to delete. */ @@ -26,5 +26,5 @@ export interface V2UpdateTemplateRequestData { /** The id of the template to be updated. Specified or automatically generated during template creation. Unique per project. */ 'template_id': string; /** Required. The updated template. */ - 'updateTemplateRequestBody': V2Template; + 'updateTemplateRequestBody': Omit; } diff --git a/packages/conversation/src/models/v1/v2-template/v2-template.ts b/packages/conversation/src/models/v1/v2-template/v2-template.ts index 223257c5..369b3421 100644 --- a/packages/conversation/src/models/v1/v2-template/v2-template.ts +++ b/packages/conversation/src/models/v1/v2-template/v2-template.ts @@ -1,7 +1,8 @@ import { V2TemplateTranslation } from '../v2-template-translation'; export interface V2Template { - + /** The id of the template. Specify this yourself during creation. Otherwise, we will generate an ID for you. This must be unique for a given project. */ + id?: string; /** The description of the template. */ description?: string; /** The version of the template. While creating a template, this will be defaulted to 1. When updating a template, you must supply the latest version of the template in order for the update to be successful. */ diff --git a/packages/conversation/tests/rest/v1/templates-v2/templates-v2.steps.ts b/packages/conversation/tests/rest/v1/templates-v2/templates-v2.steps.ts new file mode 100644 index 00000000..85a6d307 --- /dev/null +++ b/packages/conversation/tests/rest/v1/templates-v2/templates-v2.steps.ts @@ -0,0 +1,193 @@ +import { Conversation, ConversationService, SupportedConversationRegion, TemplatesV2Api } from '../../../../src'; +import { Given, Then, When } from '@cucumber/cucumber'; +import * as assert from 'assert'; + +let templatesV2Api: TemplatesV2Api; +let template: Conversation.V2Template; +let templatesList: Conversation.V2ListTemplatesResponse; +let translationsList: Conversation.V2ListTranslationsResponse; +let deleteTemplateResponse: void; +let currentVersion: number; + +Given('the Conversation service "TemplatesV2" is available', function () { + const conversationService = new ConversationService({ + projectId: 'tinyfrog-jump-high-over-lilypadbasin', + keyId: 'keyId', + keySecret: 'keySecret', + authHostname: 'http://localhost:3011', + conversationTemplatesHostname: 'http://localhost:3015', + conversationRegion: SupportedConversationRegion.UNITED_STATES, + }); + templatesV2Api = conversationService.templatesV2; +}); + +When('I send a request to create a conversation template with the V2 API', async () => { + template = await templatesV2Api.create({ + createTemplateRequestBody: { + id: '01HVN010MG3B9N6X323JAFN59P', + default_translation: 'en-US', + description: 'Text template V2', + translations: [ + { + language_code: 'en-US', + version: '3', + text_message: { + text: 'Hello ${name}. Text message template created with V2 API', + }, + variables: [ + { + key: 'name', + preview_value: 'Professor Jones', + }, + ], + }, + ], + }, + }); +}); + +Then('the conversation template V2 is created', () => { + assert.equal(template.id, '01HVN010MG3B9N6X323JAFN59P'); + assert.equal(template.version, 1); + assert.equal(template.translations?.length, 1); + assert.equal(template.translations?.[0].version, '3'); +}); + +When('I send a request to list the conversation templates with the V2 API', async () => { + templatesList = await templatesV2Api.list({}); +}); + +Then('the response contains the list of conversation templates with the V2 structure', () => { + assert.ok(templatesList.templates); + assert.equal(templatesList.templates.length, 2); +}); + +Then('for each templateV2 in the templateV2 list response, it defines a translation with version "latest" on top of each current translation version', () => { + for(const templateV2 of templatesList.templates!) { + assert.ok(templateV2.version); + let latestVersionCount = 0; + let otherVersionCount = 0; + const translations = templateV2.translations!; + for(const translation of translations) { + translation.version === 'latest' ? latestVersionCount++ : otherVersionCount++; + } + assert.equal(latestVersionCount, otherVersionCount); + } +}); + +When('I send a request to list the translations for a template with the V2 API', async () => { + translationsList = await templatesV2Api.listTranslations({ + template_id: '01W4FFL35P4NC4K35TEMPLATEV2', + }); +}); + +Then('the response contains the list of translations for a template with the V2 structure', () => { + assert.ok(translationsList.translations); + assert.equal(translationsList.translations.length, 2); + assert.equal(translationsList.translations.find((translation) => translation.version === 'latest'), undefined); +}); + +When('I send a request to retrieve a conversation template with the V2 API', async () => { + template = await templatesV2Api.get({ + template_id: '01HVN010MG3B9N6X323JAFN59P', + }); +}); + +Then('the response contains the conversation template details with the V2 structure', () => { + assert.equal(template.id, '01HVN010MG3B9N6X323JAFN59P'); + assert.equal(template.description, 'Text template V2'); + assert.equal(template.version, 1); + assert.equal(template.translations?.length, 2); + const translation = template.translations!.find((translation) => translation.version !== 'latest'); + assert.ok(translation); + assert.equal(translation.language_code, 'en-US'); + assert.equal(translation.version, '3'); + assert.deepEqual(translation.create_time, new Date('2024-06-06T14:42:42Z')); + assert.deepEqual(translation.update_time, new Date('2024-06-06T14:42:42Z')); + assert.equal(translation.variables?.length, 1); + assert.ok(translation.text_message?.text); + assert.deepEqual(translation.channel_template_overrides, {}); + assert.equal(template.default_translation, 'en-US'); + assert.deepEqual(template.create_time, new Date('2024-06-06T14:42:42Z')); + assert.deepEqual(template.update_time, new Date('2024-06-06T14:42:42Z')); +}); + +When('I send a request to update a conversation template with the V2 API', async () => { + currentVersion = 1; + template = await templatesV2Api.update({ + template_id: '01HVN010MG3B9N6X323JAFN59P', + updateTemplateRequestBody: { + description: 'Updated description v2', + version: currentVersion, + default_translation: 'en-US', + translations: [ + { + language_code: 'en-US', + version: '1', + list_message: { + title: 'Choose your icecream flavor', + description: 'The best icecream in town!', + sections: [ + { + title: 'Fruit flavors', + items: [ + { + choice: { + title: 'Strawberry', + postback_data: 'Strawberry postback', + }, + }, + { + choice: { + title: 'Blueberry', + postback_data: 'Blueberry postback', + }, + }, + ], + }, + { + title: 'Other flavors', + items: [ + { + choice: { + title: 'Chocolate', + postback_data: 'Chocolate postback', + }, + }, + { + choice: { + title: 'Vanilla', + postback_data: 'Vanilla postback', + }, + }, + ], + }, + ], + }, + }, + ], + }, + }); +}); + +Then('the response contains the conversation template details with updated data with the V2 structure', () => { + assert.equal(template.id, '01HVN010MG3B9N6X323JAFN59P'); + assert.equal(template.description, 'Updated description v2'); + assert.equal(template.version, currentVersion + 1); + assert.equal(template.translations?.length, 1); + assert.equal(template.default_translation, 'en-US'); + assert.deepEqual(template.create_time, new Date('2024-06-06T14:42:42Z')); + assert.deepEqual(template.update_time, new Date('2024-06-06T15:45:45Z')); + const translation = template.translations?.[0]; + assert.ok(translation?.list_message); +}); + +When('I send a request to delete a conversation template with the V2 API', async () => { + deleteTemplateResponse = await templatesV2Api.delete({ + template_id: '01W4FFL35P4NC4K35TEMPLATEV2', + }); +}); + +Then('the delete conversation template response V2 contains no data', () => { + assert.deepEqual(deleteTemplateResponse, {} ); +}); diff --git a/packages/numbers/tests/rest/v1/webhooks/webhooks.steps.ts b/packages/numbers/tests/rest/v1/webhooks/webhooks.steps.ts index 4544f04f..e35fdc7a 100644 --- a/packages/numbers/tests/rest/v1/webhooks/webhooks.steps.ts +++ b/packages/numbers/tests/rest/v1/webhooks/webhooks.steps.ts @@ -16,7 +16,6 @@ const processEvent = async (response: Response) => { }); rawEvent = await response.text(); rawEvent = rawEvent.replace(/\s+/g, ''); - console.log(rawEvent); event = numbersCallbackWebhook.parseEvent(JSON.parse(rawEvent)); }; From e2c32feb131cff9a253db5c218a71bbfaaaab284 Mon Sep 17 00:00:00 2001 From: Antoine SEIN <142824551+asein-sinch@users.noreply.github.com> Date: Thu, 25 Jul 2024 17:20:27 +0200 Subject: [PATCH 22/54] DEVEXP-493: Use healthcheck script in CI (#116) --- .github/workflows/run-ci.yaml | 22 ++-------------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/.github/workflows/run-ci.yaml b/.github/workflows/run-ci.yaml index 8b879719..151867dd 100644 --- a/.github/workflows/run-ci.yaml +++ b/.github/workflows/run-ci.yaml @@ -43,26 +43,8 @@ jobs: - name: Wait for the mock servers to be healthy run: | cd sinch-sdk-mockserver - servers=( - "authentication-server" - "fax-server" - "numbers-server" - "conversation-server" - "conversation-templates-server" - ) - - for server in "${servers[@]}"; do - SECONDS=0 - while ! docker inspect --format='{{json .State.Health.Status}}' $(docker-compose ps -q $server) | grep -q '"healthy"'; do - if [ $SECONDS -ge 30 ]; then - echo "Timeout: $server did not become healthy within 30 seconds." - exit 1 - fi - echo "Waiting for $server to be healthy..." - sleep 2 - done - echo "$server is healthy!" - done + chmod +x ./scripts/healthcheck.sh + ./scripts/healthcheck.sh - name: Create target directories for feature files run: | mkdir -p ./packages/fax/tests/e2e/features From d295b446b7f1b5e60837c9051cf59d845af9252b Mon Sep 17 00:00:00 2001 From: Antoine SEIN <142824551+asein-sinch@users.noreply.github.com> Date: Thu, 25 Jul 2024 18:10:17 +0200 Subject: [PATCH 23/54] DEVEXP-503: E2E Conversation/WebhooksEvents (#115) --- .../webhooks-events/webhooks-events.steps.ts | 269 ++++++++++++++++++ 1 file changed, 269 insertions(+) create mode 100644 packages/conversation/tests/rest/v1/webhooks-events/webhooks-events.steps.ts diff --git a/packages/conversation/tests/rest/v1/webhooks-events/webhooks-events.steps.ts b/packages/conversation/tests/rest/v1/webhooks-events/webhooks-events.steps.ts new file mode 100644 index 00000000..82f87357 --- /dev/null +++ b/packages/conversation/tests/rest/v1/webhooks-events/webhooks-events.steps.ts @@ -0,0 +1,269 @@ +import { Given, Then, When } from '@cucumber/cucumber'; +import { ConversationCallbackWebhooks, Conversation } from '../../../../src'; +import assert from 'assert'; +import { IncomingHttpHeaders } from 'http'; + +const APP_SECRET = 'CactusKnight_SurfsWaves'; +let conversationCallbackWebhook: ConversationCallbackWebhooks; +let rawEvent: any; +let event: Conversation.ConversationWebhookEvent; +let formattedHeaders: IncomingHttpHeaders; + +const processEvent = async (response: Response) => { + formattedHeaders = {}; + response.headers.forEach((value, name) => { + formattedHeaders[name.toLowerCase()] = value; + }); + rawEvent = await response.text(); + rawEvent = rawEvent.replace(/\s+/g, ''); + event = conversationCallbackWebhook.parseEvent(JSON.parse(rawEvent)); +}; + +Given('the Conversation Webhooks handler is available', () => { + conversationCallbackWebhook = new ConversationCallbackWebhooks(APP_SECRET); +}); + +Then('the Conversation event header contains a valid signature', () => { + assert.ok(conversationCallbackWebhook.validateAuthenticationHeader(formattedHeaders, rawEvent)); +}); + +When('I send a request to trigger a "CAPABILITY" event', async () => { + const response = await fetch('http://localhost:3014/webhooks/conversation/capability-lookup'); + await processEvent(response); +}); + +Then('the Conversation event describes a "CAPABILITY" event type', () => { + const capabilityEvent = event as Conversation.CapabilityEvent; + assert.ok(capabilityEvent.capability_notification); + const expectedTrigger: Conversation.WebhookTrigger = 'CAPABILITY'; + assert.equal(capabilityEvent.trigger, expectedTrigger); +}); + +When('I send a request to trigger a "CONTACT_CREATE" event', async () => { + const response = await fetch('http://localhost:3014/webhooks/conversation/contact-create'); + await processEvent(response); +}); + +Then('the Conversation event describes a "CONTACT_CREATE" event type', () => { + const contactCreateEvent = event as Conversation.ContactCreateEvent; + assert.ok(contactCreateEvent.contact_create_notification); + const expectedTrigger: Conversation.WebhookTrigger = 'CONTACT_CREATE'; + assert.equal(contactCreateEvent.trigger, expectedTrigger); +}); + +When('I send a request to trigger a "CONTACT_DELETE" event', async () => { + const response = await fetch('http://localhost:3014/webhooks/conversation/contact-delete'); + await processEvent(response); +}); + +Then('the Conversation event describes a "CONTACT_DELETE" event type', () => { + const contactDeleteEvent = event as Conversation.ContactDeleteEvent; + assert.ok(contactDeleteEvent.contact_delete_notification); + const expectedTrigger: Conversation.WebhookTrigger = 'CONTACT_DELETE'; + assert.equal(contactDeleteEvent.trigger, expectedTrigger); +}); + +When('I send a request to trigger a "CONTACT_MERGE" event', async () => { + const response = await fetch('http://localhost:3014/webhooks/conversation/contact-merge'); + await processEvent(response); +}); + +Then('the Conversation event describes a "CONTACT_MERGE" event type', () => { + const contactMergeEvent = event as Conversation.ContactMergeEvent; + assert.ok(contactMergeEvent.contact_merge_notification); + const expectedTrigger: Conversation.WebhookTrigger = 'CONTACT_MERGE'; + assert.equal(contactMergeEvent.trigger, expectedTrigger); +}); + +When('I send a request to trigger a "CONTACT_UPDATE" event', async () => { + const response = await fetch('http://localhost:3014/webhooks/conversation/contact-update'); + await processEvent(response); +}); + +Then('the Conversation event describes a "CONTACT_UPDATE" event type', () => { + const contactUpdateEvent = event as Conversation.ContactUpdateEvent; + assert.ok(contactUpdateEvent.contact_update_notification); + const expectedTrigger: Conversation.WebhookTrigger = 'CONTACT_UPDATE'; + assert.equal(contactUpdateEvent.trigger, expectedTrigger); +}); + +When('I send a request to trigger a "CONVERSATION_DELETE" event', async () => { + const response = await fetch('http://localhost:3014/webhooks/conversation/conversation-delete'); + await processEvent(response); +}); + +Then('the Conversation event describes a "CONVERSATION_DELETE" event type', () => { + const conversationDeleteEvent = event as Conversation.ConversationDeleteEvent; + assert.ok(conversationDeleteEvent.conversation_delete_notification); + const expectedTrigger: Conversation.WebhookTrigger = 'CONVERSATION_DELETE'; + assert.equal(conversationDeleteEvent.trigger, expectedTrigger); +}); + +When('I send a request to trigger a "CONVERSATION_START" event', async () => { + const response = await fetch('http://localhost:3014/webhooks/conversation/conversation-start'); + await processEvent(response); +}); + +Then('the Conversation event describes a "CONVERSATION_START" event type', () => { + const conversationStartEvent = event as Conversation.ConversationStartEvent; + assert.ok(conversationStartEvent.conversation_start_notification); + const expectedTrigger: Conversation.WebhookTrigger = 'CONVERSATION_START'; + assert.equal(conversationStartEvent.trigger, expectedTrigger); +}); + +When('I send a request to trigger a "CONVERSATION_STOP" event', async () => { + const response = await fetch('http://localhost:3014/webhooks/conversation/conversation-stop'); + await processEvent(response); +}); + +Then('the Conversation event describes a "CONVERSATION_STOP" event type', () => { + const conversationStopEvent = event as Conversation.ConversationStopEvent; + assert.ok(conversationStopEvent.conversation_stop_notification); + const expectedTrigger: Conversation.WebhookTrigger = 'CONVERSATION_STOP'; + assert.equal(conversationStopEvent.trigger, expectedTrigger); +}); + +When('I send a request to trigger a "EVENT_DELIVERY" event with a FAILED status', async () => { + const response = await fetch('http://localhost:3014/webhooks/conversation/event-delivery-report/failed'); + await processEvent(response); +}); + +Then('the Conversation event describes a "EVENT_DELIVERY" event type', () => { + const eventDeliveryEvent = event as Conversation.EventDelivery; + assert.ok(eventDeliveryEvent.event_delivery_report); + const expectedTrigger: Conversation.WebhookTrigger = 'EVENT_DELIVERY'; + assert.equal(eventDeliveryEvent.trigger, expectedTrigger); +}); + +Then('the Conversation event describes a FAILED event delivery status and its reason', () => { + const eventDeliveryReport = (event as Conversation.EventDelivery).event_delivery_report!; + const expectedStatus: Conversation.DeliveryStatus = 'FAILED'; + assert.equal(eventDeliveryReport.status, expectedStatus); + assert.ok(eventDeliveryReport.reason); + const expectedReasonCode: Conversation.ReasonCode = 'BAD_REQUEST'; + assert.equal(eventDeliveryReport.reason.code, expectedReasonCode); +}); + +When('I send a request to trigger a "EVENT_DELIVERY" event with a DELIVERED status', async () => { + const response = await fetch('http://localhost:3014/webhooks/conversation/event-delivery-report/succeeded'); + await processEvent(response); +}); + +When('I send a request to trigger a "EVENT_INBOUND" event', async () => { + const response = await fetch('http://localhost:3014/webhooks/conversation/event-inbound'); + await processEvent(response); +}); + +Then('the Conversation event describes a "EVENT_INBOUND" event type', () => { + const eventInbound = event as Conversation.EventInbound; + assert.ok(eventInbound.event); + const expectedTrigger: Conversation.WebhookTrigger = 'EVENT_INBOUND'; + assert.equal(eventInbound.trigger, expectedTrigger); +}); + +When('I send a request to trigger a "MESSAGE_DELIVERY" event with a FAILED status', async () => { + const response = await fetch('http://localhost:3014/webhooks/conversation/message-delivery-report/failed'); + await processEvent(response); +}); + +When('I send a request to trigger a "MESSAGE_DELIVERY" event with a QUEUED status', async () => { + const response = await fetch('http://localhost:3014/webhooks/conversation/message-delivery-report/succeeded'); + await processEvent(response); +}); + +Then('the Conversation event describes a "MESSAGE_DELIVERY" event type', () => { + const messageDeliveryReceiptEvent = event as Conversation.MessageDeliveryReceiptEvent; + assert.ok(messageDeliveryReceiptEvent.message_delivery_report); + const expectedTrigger: Conversation.WebhookTrigger = 'MESSAGE_DELIVERY'; + assert.equal(messageDeliveryReceiptEvent.trigger, expectedTrigger); +}); + +Then('the Conversation event describes a FAILED message delivery status and its reason', () => { + const messageDeliveryReport = (event as Conversation.MessageDeliveryReceiptEvent).message_delivery_report!; + const expectedStatus: Conversation.DeliveryStatus = 'FAILED'; + assert.equal(messageDeliveryReport.status, expectedStatus); + assert.ok(messageDeliveryReport.reason); + const expectedReasonCode: Conversation.ReasonCode = 'RECIPIENT_NOT_REACHABLE'; + assert.equal(messageDeliveryReport.reason.code, expectedReasonCode); +}); + +When('I send a request to trigger a "MESSAGE_INBOUND" event', async () => { + const response = await fetch('http://localhost:3014/webhooks/conversation/message-inbound'); + await processEvent(response); +}); + +Then('the Conversation event describes a "MESSAGE_INBOUND" event type', () => { + const messageInboundEvent = event as Conversation.MessageInboundEvent; + assert.ok(messageInboundEvent.message); + const expectedTrigger: Conversation.WebhookTrigger = 'MESSAGE_INBOUND'; + assert.equal(messageInboundEvent.trigger, expectedTrigger); +}); + +When('I send a request to trigger a "MESSAGE_INBOUND_SMART_CONVERSATION_REDACTION" event', async () => { + const response = await fetch('http://localhost:3014/webhooks/conversation/message-inbound/smart-conversation-redaction'); + await processEvent(response); +}); + +Then('the Conversation event describes a "MESSAGE_INBOUND_SMART_CONVERSATION_REDACTION" event type', () => { + const messageInboundSmartConversationRedactionEvent + = event as Conversation.MessageInboundSmartConversationRedactionEvent; + assert.ok(messageInboundSmartConversationRedactionEvent.message_redaction); + const expectedTrigger: Conversation.WebhookTrigger = 'MESSAGE_INBOUND_SMART_CONVERSATION_REDACTION'; + assert.equal(messageInboundSmartConversationRedactionEvent.trigger, expectedTrigger); +}); + +When('I send a request to trigger a "MESSAGE_SUBMIT" event for a media message', async () => { + const response = await fetch('http://localhost:3014/webhooks/conversation/message-submit/media'); + await processEvent(response); +}); + +Then('the Conversation event describes a "MESSAGE_SUBMIT" event type for a media message', () => { + const messageSubmitEvent = event as Conversation.MessageSubmitEvent; + assert.ok(messageSubmitEvent.message_submit_notification); + const expectedTrigger: Conversation.WebhookTrigger = 'MESSAGE_SUBMIT'; + assert.equal(messageSubmitEvent.trigger, expectedTrigger); + assert.ok(messageSubmitEvent.message_submit_notification.submitted_message?.media_message); +}); + +When('I send a request to trigger a "MESSAGE_SUBMIT" event for a text message', async () => { + const response = await fetch('http://localhost:3014/webhooks/conversation/message-submit/text'); + await processEvent(response); +}); + +Then('the Conversation event describes a "MESSAGE_SUBMIT" event type for a text message', () => { + const messageSubmitEvent = event as Conversation.MessageSubmitEvent; + assert.ok(messageSubmitEvent.message_submit_notification); + const expectedTrigger: Conversation.WebhookTrigger = 'MESSAGE_SUBMIT'; + assert.equal(messageSubmitEvent.trigger, expectedTrigger); + assert.ok(messageSubmitEvent.message_submit_notification.submitted_message?.text_message); +}); + +When('I send a request to trigger a "SMART_CONVERSATIONS" event for a media message', async () => { + const response = await fetch('http://localhost:3014/webhooks/conversation/smart-conversation/media'); + await processEvent(response); +}); + +Then('the Conversation event describes a "SMART_CONVERSATIONS" event type for a media message', () => { + const smartConversationsEvent = event as Conversation.SmartConversationsEvent; + assert.ok(smartConversationsEvent.smart_conversation_notification); + const expectedTrigger: Conversation.WebhookTrigger = 'SMART_CONVERSATIONS'; + assert.equal(smartConversationsEvent.trigger, expectedTrigger); + assert.ok(smartConversationsEvent.smart_conversation_notification.analysis_results?.ml_image_recognition_result); + assert.ok(smartConversationsEvent.smart_conversation_notification.analysis_results?.ml_offensive_analysis_result); +}); + +When('I send a request to trigger a "SMART_CONVERSATIONS" event for a text message', async () => { + const response = await fetch('http://localhost:3014/webhooks/conversation/smart-conversation/text'); + await processEvent(response); +}); + +Then('the Conversation event describes a "SMART_CONVERSATIONS" event type for a text message', () => { + const smartConversationsEvent = event as Conversation.SmartConversationsEvent; + assert.ok(smartConversationsEvent.smart_conversation_notification); + const expectedTrigger: Conversation.WebhookTrigger = 'SMART_CONVERSATIONS'; + assert.equal(smartConversationsEvent.trigger, expectedTrigger); + assert.ok(smartConversationsEvent.smart_conversation_notification.analysis_results?.ml_sentiment_result); + assert.ok(smartConversationsEvent.smart_conversation_notification.analysis_results?.ml_nlu_result); + assert.ok(smartConversationsEvent.smart_conversation_notification.analysis_results?.ml_pii_result); + assert.ok(smartConversationsEvent.smart_conversation_notification.analysis_results?.ml_offensive_analysis_result); +}); From f0e172cdeb853c6e041a5d85e44572ddc51991ab Mon Sep 17 00:00:00 2001 From: Antoine SEIN <142824551+asein-sinch@users.noreply.github.com> Date: Fri, 26 Jul 2024 09:06:31 +0200 Subject: [PATCH 24/54] DEVEXP-488: Move AvailableNumbers and ActiveNumbers related operations (#117) --- .../src/numbers/app.ts | 16 +- .../simple-examples/src/numbers/active/get.ts | 2 +- .../src/numbers/active/list.ts | 4 +- .../src/numbers/active/release.ts | 2 +- .../src/numbers/active/update.ts | 2 +- .../numbers/available/checkAvailability.ts | 2 +- .../src/numbers/available/list.ts | 2 +- .../src/numbers/available/rent.ts | 2 +- .../src/numbers/available/rentAny.ts | 2 +- .../numbers/src/rest/v1/numbers-service.ts | 86 +++++- .../v1/active-number/active-number.steps.ts | 150 --------- .../available-number.steps.ts | 153 ---------- .../numbers/tests/rest/v1/numbers.steps.ts | 285 ++++++++++++++++++ 13 files changed, 387 insertions(+), 321 deletions(-) delete mode 100644 packages/numbers/tests/rest/v1/active-number/active-number.steps.ts delete mode 100644 packages/numbers/tests/rest/v1/available-number/available-number.steps.ts create mode 100644 packages/numbers/tests/rest/v1/numbers.steps.ts diff --git a/examples/integrated-flows-examples/src/numbers/app.ts b/examples/integrated-flows-examples/src/numbers/app.ts index 501f9321..9d505f11 100644 --- a/examples/integrated-flows-examples/src/numbers/app.ts +++ b/examples/integrated-flows-examples/src/numbers/app.ts @@ -68,7 +68,7 @@ dotenv.config(); try { // Send the HTTP request with the SDK method availableNumbersResponse - = await sinchClient.numbers.availableNumber.list(listAvailableNumbersRequestData); + = await sinchClient.numbers.searchForAvailableNumbers(listAvailableNumbersRequestData); } catch (error) { // Catch error if any, log it and stop the program console.error(`ERROR: An error occurred when trying to list the available numbers for the type ${type}`); @@ -100,7 +100,7 @@ dotenv.config(); let rentNumberResponse; try { // Send the HTTP request with the SDK method - rentNumberResponse = await sinchClient.numbers.availableNumber.rent(rentNumberRequestData); + rentNumberResponse = await sinchClient.numbers.rent(rentNumberRequestData); } catch (error) { // Catch error if any, log it and stop the program console.error(`ERROR: Impossible to rent the number ${phoneNumber1}`); @@ -129,7 +129,7 @@ dotenv.config(); let rentAnyNumberResponse; try { // Send the HTTP request with the SDK method - rentAnyNumberResponse = await sinchClient.numbers.availableNumber.rentAny(rentAnyNumberRequestData); + rentAnyNumberResponse = await sinchClient.numbers.rentAny(rentAnyNumberRequestData); } catch (error) { // Catch error if any, log it and stop the program console.error(`ERROR: Impossible to rent a number in the region ${regionCode} of type ${type}`); @@ -152,7 +152,7 @@ dotenv.config(); let getActiveNumberResponse; try { // Send the HTTP request with the SDK method - getActiveNumberResponse = await sinchClient.numbers.activeNumber.get(getActiveNumberRequestData); + getActiveNumberResponse = await sinchClient.numbers.get(getActiveNumberRequestData); } catch (error) { // Catch error if any, log it and stop the program console.error(`ERROR: Impossible to get details for the number ${phoneNumber1}`); @@ -172,7 +172,7 @@ dotenv.config(); // The ActiveNumbersResponse is paginated. Let's fetch all the pages using the iterator functionality const activeNumbersList: Numbers.ActiveNumber[] = []; - for await (const activeNumber of sinchClient.numbers.activeNumber.list(listActiveNumbersRequestData)) { + for await (const activeNumber of sinchClient.numbers.list(listActiveNumbersRequestData)) { activeNumbersList.push(activeNumber); } @@ -201,7 +201,7 @@ dotenv.config(); try { // Send the HTTP request with the SDK method updateActiveNumberResponse - = await sinchClient.numbers.activeNumber.update(updateActiveNumberRequestData); + = await sinchClient.numbers.update(updateActiveNumberRequestData); } catch (error) { // Catch error if any, log it and stop the program console.log(`ERROR: Impossible to update the number ${phoneNumber1}`); @@ -222,7 +222,7 @@ dotenv.config(); let releaseActiveNumberResponse; try { // Send the HTTP request with the SDK method - releaseActiveNumberResponse = await sinchClient.numbers.activeNumber.release(releaseActiveNumberRequestData); + releaseActiveNumberResponse = await sinchClient.numbers.release(releaseActiveNumberRequestData); } catch (error) { // Catch error if any, log it and stop the program console.error(`ERROR: Impossible to release the number ${phoneNumber1}`); @@ -241,7 +241,7 @@ dotenv.config(); try { // Send the HTTP request with the SDK method - releaseActiveNumberResponse = await sinchClient.numbers.activeNumber.release(releaseActiveNumberRequestData); + releaseActiveNumberResponse = await sinchClient.numbers.release(releaseActiveNumberRequestData); } catch (error) { // Catch error if any, log it and stop the program console.error(`ERROR: Impossible to release the number ${phoneNumber2}`); diff --git a/examples/simple-examples/src/numbers/active/get.ts b/examples/simple-examples/src/numbers/active/get.ts index d3324cd6..40119992 100644 --- a/examples/simple-examples/src/numbers/active/get.ts +++ b/examples/simple-examples/src/numbers/active/get.ts @@ -20,7 +20,7 @@ import { Numbers } from '@sinch/sdk-core'; const numbersService = initNumbersService(); let response; try { - response = await numbersService.activeNumber.get(requestData); + response = await numbersService.get(requestData); } catch (error) { console.error(`ERROR: The phone number ${requestData.phoneNumber} is not active`); throw error; diff --git a/examples/simple-examples/src/numbers/active/list.ts b/examples/simple-examples/src/numbers/active/list.ts index 4291d7b2..8dc38296 100644 --- a/examples/simple-examples/src/numbers/active/list.ts +++ b/examples/simple-examples/src/numbers/active/list.ts @@ -30,7 +30,7 @@ const populateActiveNumbersList = ( // ---------------------------------------------- // Method 1: Fetch the data page by page manually // ---------------------------------------------- - let response = await numbersService.activeNumber.list(requestData); + let response = await numbersService.list(requestData); const activeNumbersList: Numbers.ActiveNumber[] = []; const phoneNumbersList: (string | undefined)[] = []; @@ -59,7 +59,7 @@ const populateActiveNumbersList = ( // --------------------------------------------------------------------- // Method 2: Use the iterator and fetch data on more pages automatically // --------------------------------------------------------------------- - for await (const activeNumber of numbersService.activeNumber.list(requestData)) { + for await (const activeNumber of numbersService.list(requestData)) { if (printFormat === 'pretty') { console.log(`${activeNumber.phoneNumber} - Voice Configuration: ${activeNumber.voiceConfiguration?.type}`); } else { diff --git a/examples/simple-examples/src/numbers/active/release.ts b/examples/simple-examples/src/numbers/active/release.ts index 372b86d3..0f9c29f6 100644 --- a/examples/simple-examples/src/numbers/active/release.ts +++ b/examples/simple-examples/src/numbers/active/release.ts @@ -18,7 +18,7 @@ import { Numbers } from '@sinch/sdk-core'; }; const numbersService = initNumbersService(); - const response = await numbersService.activeNumber.release(requestData); + const response = await numbersService.release(requestData); const printFormat = getPrintFormat(process.argv); diff --git a/examples/simple-examples/src/numbers/active/update.ts b/examples/simple-examples/src/numbers/active/update.ts index d3a8c26c..140339ea 100644 --- a/examples/simple-examples/src/numbers/active/update.ts +++ b/examples/simple-examples/src/numbers/active/update.ts @@ -29,7 +29,7 @@ import { Numbers } from '@sinch/sdk-core'; }; const numbersService = initNumbersService(); - const response = await numbersService.activeNumber.update(requestData); + const response = await numbersService.update(requestData); const printFormat = getPrintFormat(process.argv); diff --git a/examples/simple-examples/src/numbers/available/checkAvailability.ts b/examples/simple-examples/src/numbers/available/checkAvailability.ts index 43f1cdab..801553fe 100644 --- a/examples/simple-examples/src/numbers/available/checkAvailability.ts +++ b/examples/simple-examples/src/numbers/available/checkAvailability.ts @@ -21,7 +21,7 @@ import { Numbers } from '@sinch/sdk-core'; const numbersService = initNumbersService(); let response; try { - response = await numbersService.availableNumber.checkAvailability(requestData); + response = await numbersService.checkAvailability(requestData); } catch (error) { console.error(`ERROR: the phone number ${requestData.phoneNumber} is not available`); } diff --git a/examples/simple-examples/src/numbers/available/list.ts b/examples/simple-examples/src/numbers/available/list.ts index e375c918..c8e21954 100644 --- a/examples/simple-examples/src/numbers/available/list.ts +++ b/examples/simple-examples/src/numbers/available/list.ts @@ -13,7 +13,7 @@ import { Numbers } from '@sinch/sdk-core'; }; const numbersService = initNumbersService(); - const response = await numbersService.availableNumber.list(requestData); + const response = await numbersService.searchForAvailableNumbers(requestData); const printFormat = getPrintFormat(process.argv); diff --git a/examples/simple-examples/src/numbers/available/rent.ts b/examples/simple-examples/src/numbers/available/rent.ts index c3b3cc92..0144fce2 100644 --- a/examples/simple-examples/src/numbers/available/rent.ts +++ b/examples/simple-examples/src/numbers/available/rent.ts @@ -38,7 +38,7 @@ import { Numbers } from '@sinch/sdk-core'; }; const numbersService = initNumbersService(); - const response = await numbersService.availableNumber.rent(requestData); + const response = await numbersService.rent(requestData); const printFormat = getPrintFormat(process.argv); diff --git a/examples/simple-examples/src/numbers/available/rentAny.ts b/examples/simple-examples/src/numbers/available/rentAny.ts index 8cf8678c..962a774f 100644 --- a/examples/simple-examples/src/numbers/available/rentAny.ts +++ b/examples/simple-examples/src/numbers/available/rentAny.ts @@ -45,7 +45,7 @@ import { Numbers } from '@sinch/sdk-core'; }; const numbersService = initNumbersService(); - const response = await numbersService.availableNumber.rentAny(requestData); + const response = await numbersService.rentAny(requestData); const printFormat = getPrintFormat(process.argv); diff --git a/packages/numbers/src/rest/v1/numbers-service.ts b/packages/numbers/src/rest/v1/numbers-service.ts index cc6002af..947b7ee9 100644 --- a/packages/numbers/src/rest/v1/numbers-service.ts +++ b/packages/numbers/src/rest/v1/numbers-service.ts @@ -1,8 +1,21 @@ -import { SinchClientParameters } from '@sinch/sdk-client'; +import { ApiListPromise, SinchClientParameters } from '@sinch/sdk-client'; import { AvailableRegionsApi } from './available-regions'; import { CallbacksApi } from './callbacks'; import { AvailableNumberApi } from './available-number'; import { ActiveNumberApi } from './active-number'; +import { + ActiveNumber, + AvailableNumber, + AvailableNumbersResponse, + GetActiveNumberRequestData, + GetAvailableNumberRequestData, + ListActiveNumbersRequestData, + ListAvailableNumbersRequestData, + ReleaseNumberRequestData, + RentAnyNumberRequestData, + RentNumberRequestData, + UpdateActiveNumberRequestData, +} from '../../models'; /** * The Numbers Service exposes the following APIs: @@ -14,7 +27,9 @@ import { ActiveNumberApi } from './active-number'; export class NumbersService { public readonly availableRegions: AvailableRegionsApi; public readonly callbacks: CallbacksApi; + /** @deprecated use the methods exposed at the Numbers Service level instead */ public readonly availableNumber: AvailableNumberApi; + /** @deprecated use the methods exposed at the Numbers Service level instead */ public readonly activeNumber: ActiveNumberApi; /** @@ -45,4 +60,73 @@ export class NumbersService { this.availableRegions.setHostname(hostname); this.callbacks.setHostname(hostname); } + + /** + * This endpoint allows you to enter a specific phone number to check if it's available for use. + * A 200 response will return the number's capability, setup costs, monthly costs and if supporting documentation is required. + * If the phone number is not available, the API will return a 404 error. + * @param {GetAvailableNumberRequestData} data - The data to provide to the API call. + */ + public async checkAvailability(data: GetAvailableNumberRequestData): Promise { + return this.availableNumber.checkAvailability(data); + } + + /** + * Search for virtual numbers that are available for you to activate. You can filter by any property on the available number resource. + * @param {ListAvailableNumbersRequestData} data - The data to provide to the API call. + */ + public async searchForAvailableNumbers(data: ListAvailableNumbersRequestData): Promise { + return this.availableNumber.list(data); + } + + /** + * Rent any virtual number that matches the criteria (Search for and activate an available Sinch virtual number all in one API call). + * @param {RentAnyNumberRequestData} data - The data to provide to the API call. + */ + public async rentAny(data: RentAnyNumberRequestData): Promise { + return this.availableNumber.rentAny(data); + } + + /** + * Rent a virtual number to use with SMS products, Voice products, or both. You'll use 'smsConfiguration' to set up your number for SMS and 'voiceConfiguration' for Voice. + * @param {RentNumberRequestData} data - The data to provide to the API call. + */ + public async rent(data: RentNumberRequestData): Promise { + return this.availableNumber.rent(data); + } + + /** + * Retrieve a virtual number's details + * @param {GetActiveNumberRequestData} data - The data to provide to the API call. + */ + public async get(data: GetActiveNumberRequestData): Promise { + return this.activeNumber.get(data); + } + + /** + * Lists all virtual numbers for a project. + * @param {ListActiveNumbersRequestData} data - The data to provide to the API call. + * @return {ApiListPromise} + */ + public list(data: ListActiveNumbersRequestData): ApiListPromise { + return this.activeNumber.list(data); + } + + /** + * Release number. + * With this endpoint, you can cancel your subscription for a specific virtual phone number. + * @param {ReleaseNumberRequestData} data - The data to provide to the API call. + */ + public async release(data: ReleaseNumberRequestData): Promise { + return this.activeNumber.release(data); + } + + /** + * Update a virtual phone number. For example: you can configure SMS/Voice services or set a friendly name. To update the name that displays, modify the `displayName` parameter. + * You'll use `smsConfiguration` to update your SMS configuration and `voiceConfiguration` to update the voice configuration. + * @param {UpdateActiveNumberRequestData} data - The data to provide to the API call. + */ + public async update(data: UpdateActiveNumberRequestData): Promise { + return this.activeNumber.update(data); + } } diff --git a/packages/numbers/tests/rest/v1/active-number/active-number.steps.ts b/packages/numbers/tests/rest/v1/active-number/active-number.steps.ts deleted file mode 100644 index dfa9ff3e..00000000 --- a/packages/numbers/tests/rest/v1/active-number/active-number.steps.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { Given, Then, When } from '@cucumber/cucumber'; -import { ActiveNumberApi, NumbersService, Numbers } from '../../../../src'; -import { PageResult } from '@sinch/sdk-client'; -import assert from 'assert'; - -let activeNumberApi: ActiveNumberApi; -let listActiveNumbersResponse: PageResult; -const activeNumbersList: Numbers.ActiveNumber[] = []; -let activeNumber: Numbers.ActiveNumber; -let error: any; - -Given('the Numbers service "Active Number" is available', function () { - const numbersService = new NumbersService({ - projectId: 'tinyfrog-jump-high-over-lilypadbasin', - keyId: 'keyId', - keySecret: 'keySecret', - authHostname: 'http://localhost:3011', - numbersHostname: 'http://localhost:3013', - }); - activeNumberApi = numbersService.activeNumber; -}); - -When('I send a request to list the active phone numbers', async () => { - listActiveNumbersResponse = await activeNumberApi.list({ - regionCode: 'US', - type: 'LOCAL', - }); -}); - -Then('the response contains {string} active phone numbers', (expectedAnswer: string) => { - const expectedPhoneNumbersCount = parseInt(expectedAnswer, 10); - assert.equal(listActiveNumbersResponse.data.length, expectedPhoneNumbersCount); -}); - -When('I send a request to list all the active phone numbers', async () => { - const requestData: Numbers.ListActiveNumbersRequestData = { - regionCode: 'US', - type: 'LOCAL', - }; - for await (const number of activeNumberApi.list(requestData)) { - activeNumbersList.push(number); - } -}); - -Then('the phone numbers list contains {string} active phone numbers', (expectedAnswer: string) => { - const expectedNumbers = parseInt(expectedAnswer, 10); - assert.strictEqual(activeNumbersList.length, expectedNumbers); - const phoneNumber1 = activeNumbersList[0]; - assert.equal(phoneNumber1.voiceConfiguration?.type, 'FAX'); - assert.ok(phoneNumber1.voiceConfiguration?.serviceId !== ''); - const phoneNumber2 = activeNumbersList[1]; - assert.equal(phoneNumber2.voiceConfiguration?.type, 'EST'); - assert.ok(phoneNumber2.voiceConfiguration?.trunkId !== ''); - const phoneNumber3 = activeNumbersList[2]; - assert.equal(phoneNumber3.voiceConfiguration?.type, 'RTC'); - assert.ok(phoneNumber3.voiceConfiguration?.appId !== ''); -}); - -When('I send a request to update the phone number {string}', async (phoneNumber: string) => { - activeNumber = await activeNumberApi.update({ - phoneNumber, - updateActiveNumberRequestBody: { - displayName: 'Updated description during E2E tests', - smsConfiguration: { - servicePlanId: 'SingingMooseSociety', - }, - voiceConfiguration: { - type: 'FAX', - serviceId: '01W4FFL35P4NC4K35FAXSERVICE', - }, - callbackUrl: 'https://my-callback-server.com/numbers', - }, - }); -}); - -Then('the response contains a phone number with updated parameters', () => { - assert.equal(activeNumber.displayName, 'Updated description during E2E tests'); - assert.equal(activeNumber.callbackUrl, 'https://my-callback-server.com/numbers'); - const smsConfiguration: Numbers.SMSConfiguration = { - servicePlanId: 'SpaceMonkeySquadron', - campaignId: '', - scheduledProvisioning: { - servicePlanId: 'SingingMooseSociety', - campaignId: '', - status: 'WAITING', - lastUpdatedTime: new Date('2024-06-06T20:02:20.432220Z'), - errorCodes: [], - }, - }; - assert.deepEqual(activeNumber.smsConfiguration, smsConfiguration); - const voiceVonfiguration: Numbers.VoiceConfiguration = { - type: 'RTC', - appId: 'sunshine-rain-drop-very-beautifulday', - trunkId: '', - serviceId: '', - lastUpdatedTime: null, - scheduledVoiceProvisioning: { - status: 'WAITING', - type: 'FAX', - appId: '', - trunkId: '', - serviceId: '01W4FFL35P4NC4K35FAXSERVICE', - lastUpdatedTime: new Date('2024-06-06T20:02:20.437509Z'), - }, - }; - assert.deepEqual(activeNumber.voiceConfiguration, voiceVonfiguration); -}); - -When('I send a request to retrieve the phone number {string}', async (phoneNumber: string) => { - try { - activeNumber = await activeNumberApi.get({ phoneNumber }); - } catch (e) { - error = e; - } -}); - -Then('the response contains details about the phone number {string}', (phoneNumber: string) => { - assert.equal(activeNumber.phoneNumber, phoneNumber); - assert.deepEqual(activeNumber.nextChargeDate, new Date('2024-06-06T14:42:42.677575Z')); - assert.equal(activeNumber.expireAt, null); - assert.equal(activeNumber.smsConfiguration?.servicePlanId, 'SpaceMonkeySquadron'); -}); - -// eslint-disable-next-line max-len -Then('the response contains details about the phone number {string} with an SMS provisioning error', (phoneNumber: string) => { - assert.equal(activeNumber.phoneNumber, phoneNumber); - assert.deepEqual(activeNumber.nextChargeDate, new Date('2024-07-06T14:42:42.677575Z')); - assert.equal(activeNumber.expireAt, null); - assert.equal(activeNumber.smsConfiguration?.servicePlanId, ''); - assert.equal(activeNumber.smsConfiguration?.scheduledProvisioning?.status, 'FAILED'); - assert.deepEqual(activeNumber.smsConfiguration?.scheduledProvisioning?.errorCodes, ['SMS_PROVISIONING_FAILED']); -}); - -Then('the response contains an error about the number {string} not being an active number', (phoneNumber: string) => { - const notFound = JSON.parse(error.data) as Numbers.NotFound; - const notFoundError = notFound.error!; - assert.equal(notFoundError.code, 404); - assert.equal(notFoundError.status, 'NOT_FOUND'); - assert.equal((notFoundError.details![0] as any).resourceName, phoneNumber); -}); - -When('I send a request to release the phone number {string}', async (phoneNumber: string) => { - activeNumber = await activeNumberApi.release({ phoneNumber }); -}); - -Then('the response contains details about the phone number {string} to be released', (phoneNumber: string) => { - assert.equal(activeNumber.phoneNumber, phoneNumber); - assert.deepEqual(activeNumber.nextChargeDate, new Date('2024-06-06T14:42:42.677575Z')); - assert.deepEqual(activeNumber.expireAt, new Date('2024-06-06T14:42:42.677575Z')); -}); diff --git a/packages/numbers/tests/rest/v1/available-number/available-number.steps.ts b/packages/numbers/tests/rest/v1/available-number/available-number.steps.ts deleted file mode 100644 index 6f7b3150..00000000 --- a/packages/numbers/tests/rest/v1/available-number/available-number.steps.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { AvailableNumberApi, NumbersService, Numbers } from '../../../../src'; -import { Given, Then, When } from '@cucumber/cucumber'; -import assert from 'assert'; - -let availableNumberApi: AvailableNumberApi; -let availableNumbersResponse: Numbers.AvailableNumbersResponse; -let availablePhoneNumber: Numbers.AvailableNumber; -let activeNumber: Numbers.ActiveNumber; - -Given('the Numbers service "Available Number" is available', function () { - const numbersService = new NumbersService({ - projectId: 'tinyfrog-jump-high-over-lilypadbasin', - keyId: 'keyId', - keySecret: 'keySecret', - authHostname: 'http://localhost:3011', - numbersHostname: 'http://localhost:3013', - }); - availableNumberApi = numbersService.availableNumber; -}); - -When('I send a request to list the available phone numbers', async () => { - availableNumbersResponse = await availableNumberApi.list({ - regionCode: 'US', - type: 'LOCAL', - }); -}); - -Then('the response contains {string} available phone numbers', (expectedAnswer: string) => { - const expectedPhoneNumbersCount = parseInt(expectedAnswer, 10); - assert.equal(availableNumbersResponse.availableNumbers?.length, expectedPhoneNumbersCount); -}); - -Then('a phone number contains all the expected properties', () => { - const phoneNumber = availableNumbersResponse.availableNumbers![0]; - assert.equal(phoneNumber.phoneNumber, '+12013504948'); - assert.equal(phoneNumber.regionCode, 'US'); - assert.equal(phoneNumber.type, 'LOCAL'); - assert.deepEqual(phoneNumber.capability, ['SMS', 'VOICE']); - assert.deepEqual(phoneNumber.setupPrice, { currencyCode: 'EUR', amount: '0.80' }); - assert.deepEqual(phoneNumber.monthlyPrice, { currencyCode: 'EUR', amount: '0.80' }); - assert.equal(phoneNumber.paymentIntervalMonths, 1); - assert.equal(phoneNumber.supportingDocumentationRequired, true); -}); - -When('I send a request to check the availability of the phone number {string}', async (phoneNumber: string) => { - availablePhoneNumber = await availableNumberApi.checkAvailability({ phoneNumber }); -}); - -Then('the response displays the phone number {string} details', (phoneNumber: string) => { - assert.equal(availablePhoneNumber.phoneNumber, phoneNumber); -}); - -Then('the response contains an error about the number {string} not being available', (phoneNumber: string) => { - const notFound = availablePhoneNumber as Numbers.NotFound; - const notFoundError = notFound.error!; - assert.equal(notFoundError.code, 404); - assert.equal(notFoundError.status, 'NOT_FOUND'); - assert.equal((notFoundError.details![0] as any).resourceName, phoneNumber); -}); - -When('I send a request to rent a number with some criteria', async () => { - activeNumber = await availableNumberApi.rentAny({ - rentAnyNumberRequestBody: { - regionCode: 'US', - type: 'LOCAL', - capabilities: ['SMS', 'VOICE'], - smsConfiguration: { - servicePlanId: 'SpaceMonkeySquadron', - }, - voiceConfiguration: { - appId: 'sunshine-rain-drop-very-beautifulday', - }, - numberPattern: { - pattern: '7654321', - searchPattern: 'END', - }, - }, - }); -}); - -Then('the response contains an active phone number', () => { - assert.equal(activeNumber.phoneNumber, '+12017654321'); - assert.equal(activeNumber.projectId, '123c0ffee-dada-beef-cafe-baadc0de5678'); - assert.equal(activeNumber.displayName, ''); - assert.equal(activeNumber.regionCode, 'US'); - assert.equal(activeNumber.type, 'LOCAL'); - assert.deepEqual(activeNumber.capability, ['SMS', 'VOICE']); - assert.deepEqual(activeNumber.money, { currencyCode: 'EUR', amount: '0.80' }); - assert.equal(activeNumber.paymentIntervalMonths, 1); - assert.deepEqual(activeNumber.nextChargeDate, new Date('2024-06-06T14:42:42.022227Z')); - assert.equal(activeNumber.expireAt, null); - const expectedSmsConfiguration: Numbers.SMSConfiguration = { - servicePlanId: '', - campaignId: '', - scheduledProvisioning: { - servicePlanId: 'SpaceMonkeySquadron', - campaignId: '', - status: 'WAITING', - lastUpdatedTime: new Date('2024-06-06T14:42:42.596223Z'), - errorCodes: [], - }, - }; - assert.deepEqual(activeNumber.smsConfiguration, expectedSmsConfiguration); - const expectedVoiceConfiguration: Numbers.VoiceConfiguration = { - type: 'RTC', - appId: '', - trunkId: '', - serviceId: '', - lastUpdatedTime: null, - scheduledVoiceProvisioning: { - type: 'RTC', - appId: 'sunshine-rain-drop-very-beautifulday', - trunkId: '', - serviceId: '', - status: 'WAITING', - lastUpdatedTime: new Date('2024-06-06T14:42:42.604092Z'), - }, - }; - assert.deepEqual(activeNumber.voiceConfiguration, expectedVoiceConfiguration); - assert.equal(activeNumber.callbackUrl, ''); -}); - -When('I send a request to rent the phone number {string}', async (phoneNumber: string) => { - activeNumber = await availableNumberApi.rent({ - phoneNumber, - rentNumberRequestBody: { - smsConfiguration: { - servicePlanId: 'SpaceMonkeySquadron', - }, - voiceConfiguration: { - appId: 'sunshine-rain-drop-very-beautifulday', - }, - }, - }); -}); - -Then('the response contains this active phone number {string}', (phoneNumber: string) => { - assert.equal(activeNumber.phoneNumber, phoneNumber); -}); - -When('I send a request to rent the unavailable phone number {string}', async (phoneNumber: string) => { - activeNumber = await availableNumberApi.rent({ - phoneNumber, - rentNumberRequestBody: { - smsConfiguration: { - servicePlanId: 'SpaceMonkeySquadron', - }, - voiceConfiguration: { - appId: 'sunshine-rain-drop-very-beautifulday', - }, - }, - }); -}); diff --git a/packages/numbers/tests/rest/v1/numbers.steps.ts b/packages/numbers/tests/rest/v1/numbers.steps.ts new file mode 100644 index 00000000..30e1af04 --- /dev/null +++ b/packages/numbers/tests/rest/v1/numbers.steps.ts @@ -0,0 +1,285 @@ +import { Given, Then, When } from '@cucumber/cucumber'; +import { NumbersService, Numbers } from '../../../src'; +import { PageResult } from '@sinch/sdk-client'; +import assert from 'assert'; + +let numbersService: NumbersService; +let availableNumbersResponse: Numbers.AvailableNumbersResponse; +let availablePhoneNumber: Numbers.AvailableNumber; +let listActiveNumbersResponse: PageResult; +const activeNumbersList: Numbers.ActiveNumber[] = []; +let activeNumber: Numbers.ActiveNumber; +let error: any; + +Given('the Numbers service is available', function () { + numbersService = new NumbersService({ + projectId: 'tinyfrog-jump-high-over-lilypadbasin', + keyId: 'keyId', + keySecret: 'keySecret', + authHostname: 'http://localhost:3011', + numbersHostname: 'http://localhost:3013', + }); +}); + +When('I send a request to search for available phone numbers', async () => { + availableNumbersResponse = await numbersService.searchForAvailableNumbers({ + regionCode: 'US', + type: 'LOCAL', + }); +}); + +Then('the response contains {string} available phone numbers', (expectedAnswer: string) => { + const expectedPhoneNumbersCount = parseInt(expectedAnswer, 10); + assert.equal(availableNumbersResponse.availableNumbers?.length, expectedPhoneNumbersCount); +}); + +Then('a phone number contains all the expected properties', () => { + const phoneNumber = availableNumbersResponse.availableNumbers![0]; + assert.equal(phoneNumber.phoneNumber, '+12013504948'); + assert.equal(phoneNumber.regionCode, 'US'); + assert.equal(phoneNumber.type, 'LOCAL'); + assert.deepEqual(phoneNumber.capability, ['SMS', 'VOICE']); + assert.deepEqual(phoneNumber.setupPrice, { currencyCode: 'EUR', amount: '0.80' }); + assert.deepEqual(phoneNumber.monthlyPrice, { currencyCode: 'EUR', amount: '0.80' }); + assert.equal(phoneNumber.paymentIntervalMonths, 1); + assert.equal(phoneNumber.supportingDocumentationRequired, true); +}); + +When('I send a request to check the availability of the phone number {string}', async (phoneNumber: string) => { + availablePhoneNumber = await numbersService.checkAvailability({ phoneNumber }); +}); + +Then('the response displays the phone number {string} details', (phoneNumber: string) => { + assert.equal(availablePhoneNumber.phoneNumber, phoneNumber); +}); + +Then('the response contains an error about the number {string} not being available', (phoneNumber: string) => { + const notFound = availablePhoneNumber as Numbers.NotFound; + const notFoundError = notFound.error!; + assert.equal(notFoundError.code, 404); + assert.equal(notFoundError.status, 'NOT_FOUND'); + assert.equal((notFoundError.details![0] as any).resourceName, phoneNumber); +}); + +When('I send a request to rent a number with some criteria', async () => { + activeNumber = await numbersService.rentAny({ + rentAnyNumberRequestBody: { + regionCode: 'US', + type: 'LOCAL', + capabilities: ['SMS', 'VOICE'], + smsConfiguration: { + servicePlanId: 'SpaceMonkeySquadron', + }, + voiceConfiguration: { + appId: 'sunshine-rain-drop-very-beautifulday', + }, + numberPattern: { + pattern: '7654321', + searchPattern: 'END', + }, + }, + }); +}); + +Then('the response contains a rented phone number', () => { + assert.equal(activeNumber.phoneNumber, '+12017654321'); + assert.equal(activeNumber.projectId, '123c0ffee-dada-beef-cafe-baadc0de5678'); + assert.equal(activeNumber.displayName, ''); + assert.equal(activeNumber.regionCode, 'US'); + assert.equal(activeNumber.type, 'LOCAL'); + assert.deepEqual(activeNumber.capability, ['SMS', 'VOICE']); + assert.deepEqual(activeNumber.money, { currencyCode: 'EUR', amount: '0.80' }); + assert.equal(activeNumber.paymentIntervalMonths, 1); + assert.deepEqual(activeNumber.nextChargeDate, new Date('2024-06-06T14:42:42.022227Z')); + assert.equal(activeNumber.expireAt, null); + const expectedSmsConfiguration: Numbers.SMSConfiguration = { + servicePlanId: '', + campaignId: '', + scheduledProvisioning: { + servicePlanId: 'SpaceMonkeySquadron', + campaignId: '', + status: 'WAITING', + lastUpdatedTime: new Date('2024-06-06T14:42:42.596223Z'), + errorCodes: [], + }, + }; + assert.deepEqual(activeNumber.smsConfiguration, expectedSmsConfiguration); + const expectedVoiceConfiguration: Numbers.VoiceConfiguration = { + type: 'RTC', + appId: '', + trunkId: '', + serviceId: '', + lastUpdatedTime: null, + scheduledVoiceProvisioning: { + type: 'RTC', + appId: 'sunshine-rain-drop-very-beautifulday', + trunkId: '', + serviceId: '', + status: 'WAITING', + lastUpdatedTime: new Date('2024-06-06T14:42:42.604092Z'), + }, + }; + assert.deepEqual(activeNumber.voiceConfiguration, expectedVoiceConfiguration); + assert.equal(activeNumber.callbackUrl, ''); +}); + +When('I send a request to rent the phone number {string}', async (phoneNumber: string) => { + activeNumber = await numbersService.rent({ + phoneNumber, + rentNumberRequestBody: { + smsConfiguration: { + servicePlanId: 'SpaceMonkeySquadron', + }, + voiceConfiguration: { + appId: 'sunshine-rain-drop-very-beautifulday', + }, + }, + }); +}); + +Then('the response contains this rented phone number {string}', (phoneNumber: string) => { + assert.equal(activeNumber.phoneNumber, phoneNumber); +}); + +When('I send a request to rent the unavailable phone number {string}', async (phoneNumber: string) => { + activeNumber = await numbersService.rent({ + phoneNumber, + rentNumberRequestBody: { + smsConfiguration: { + servicePlanId: 'SpaceMonkeySquadron', + }, + voiceConfiguration: { + appId: 'sunshine-rain-drop-very-beautifulday', + }, + }, + }); +}); + +When('I send a request to list the phone numbers', async () => { + listActiveNumbersResponse = await numbersService.list({ + regionCode: 'US', + type: 'LOCAL', + }); +}); + +Then('the response contains {string} phone numbers', (expectedAnswer: string) => { + const expectedPhoneNumbersCount = parseInt(expectedAnswer, 10); + assert.equal(listActiveNumbersResponse.data.length, expectedPhoneNumbersCount); +}); + +When('I send a request to list all the phone numbers', async () => { + const requestData: Numbers.ListActiveNumbersRequestData = { + regionCode: 'US', + type: 'LOCAL', + }; + for await (const number of numbersService.list(requestData)) { + activeNumbersList.push(number); + } +}); + +Then('the phone numbers list contains {string} phone numbers', (expectedAnswer: string) => { + const expectedNumbers = parseInt(expectedAnswer, 10); + assert.strictEqual(activeNumbersList.length, expectedNumbers); + const phoneNumber1 = activeNumbersList[0]; + assert.equal(phoneNumber1.voiceConfiguration?.type, 'FAX'); + assert.ok(phoneNumber1.voiceConfiguration?.serviceId !== ''); + const phoneNumber2 = activeNumbersList[1]; + assert.equal(phoneNumber2.voiceConfiguration?.type, 'EST'); + assert.ok(phoneNumber2.voiceConfiguration?.trunkId !== ''); + const phoneNumber3 = activeNumbersList[2]; + assert.equal(phoneNumber3.voiceConfiguration?.type, 'RTC'); + assert.ok(phoneNumber3.voiceConfiguration?.appId !== ''); +}); + +When('I send a request to update the phone number {string}', async (phoneNumber: string) => { + activeNumber = await numbersService.update({ + phoneNumber, + updateActiveNumberRequestBody: { + displayName: 'Updated description during E2E tests', + smsConfiguration: { + servicePlanId: 'SingingMooseSociety', + }, + voiceConfiguration: { + type: 'FAX', + serviceId: '01W4FFL35P4NC4K35FAXSERVICE', + }, + callbackUrl: 'https://my-callback-server.com/numbers', + }, + }); +}); + +Then('the response contains a phone number with updated parameters', () => { + assert.equal(activeNumber.displayName, 'Updated description during E2E tests'); + assert.equal(activeNumber.callbackUrl, 'https://my-callback-server.com/numbers'); + const smsConfiguration: Numbers.SMSConfiguration = { + servicePlanId: 'SpaceMonkeySquadron', + campaignId: '', + scheduledProvisioning: { + servicePlanId: 'SingingMooseSociety', + campaignId: '', + status: 'WAITING', + lastUpdatedTime: new Date('2024-06-06T20:02:20.432220Z'), + errorCodes: [], + }, + }; + assert.deepEqual(activeNumber.smsConfiguration, smsConfiguration); + const voiceVonfiguration: Numbers.VoiceConfiguration = { + type: 'RTC', + appId: 'sunshine-rain-drop-very-beautifulday', + trunkId: '', + serviceId: '', + lastUpdatedTime: null, + scheduledVoiceProvisioning: { + status: 'WAITING', + type: 'FAX', + appId: '', + trunkId: '', + serviceId: '01W4FFL35P4NC4K35FAXSERVICE', + lastUpdatedTime: new Date('2024-06-06T20:02:20.437509Z'), + }, + }; + assert.deepEqual(activeNumber.voiceConfiguration, voiceVonfiguration); +}); + +When('I send a request to retrieve the phone number {string}', async (phoneNumber: string) => { + try { + activeNumber = await numbersService.get({ phoneNumber }); + } catch (e) { + error = e; + } +}); + +Then('the response contains details about the phone number {string}', (phoneNumber: string) => { + assert.equal(activeNumber.phoneNumber, phoneNumber); + assert.deepEqual(activeNumber.nextChargeDate, new Date('2024-06-06T14:42:42.677575Z')); + assert.equal(activeNumber.expireAt, null); + assert.equal(activeNumber.smsConfiguration?.servicePlanId, 'SpaceMonkeySquadron'); +}); + +// eslint-disable-next-line max-len +Then('the response contains details about the phone number {string} with an SMS provisioning error', (phoneNumber: string) => { + assert.equal(activeNumber.phoneNumber, phoneNumber); + assert.deepEqual(activeNumber.nextChargeDate, new Date('2024-07-06T14:42:42.677575Z')); + assert.equal(activeNumber.expireAt, null); + assert.equal(activeNumber.smsConfiguration?.servicePlanId, ''); + assert.equal(activeNumber.smsConfiguration?.scheduledProvisioning?.status, 'FAILED'); + assert.deepEqual(activeNumber.smsConfiguration?.scheduledProvisioning?.errorCodes, ['SMS_PROVISIONING_FAILED']); +}); + +Then('the response contains an error about the number {string} not being a rented number', (phoneNumber: string) => { + const notFound = JSON.parse(error.data) as Numbers.NotFound; + const notFoundError = notFound.error!; + assert.equal(notFoundError.code, 404); + assert.equal(notFoundError.status, 'NOT_FOUND'); + assert.equal((notFoundError.details![0] as any).resourceName, phoneNumber); +}); + +When('I send a request to release the phone number {string}', async (phoneNumber: string) => { + activeNumber = await numbersService.release({ phoneNumber }); +}); + +Then('the response contains details about the phone number {string} to be released', (phoneNumber: string) => { + assert.equal(activeNumber.phoneNumber, phoneNumber); + assert.deepEqual(activeNumber.nextChargeDate, new Date('2024-06-06T14:42:42.677575Z')); + assert.deepEqual(activeNumber.expireAt, new Date('2024-06-06T14:42:42.677575Z')); +}); From 27bc7ade0939429355d7a5ffba5f41d307116210 Mon Sep 17 00:00:00 2001 From: Antoine SEIN <142824551+asein-sinch@users.noreply.github.com> Date: Fri, 26 Jul 2024 16:44:58 +0200 Subject: [PATCH 25/54] DEVEXP-507: E2E ElasticSipTrunking/CountryPermissions (#118) --- .github/workflows/run-ci.yaml | 2 + packages/elastic-sip-trunking/cucumber.js | 8 +++ packages/elastic-sip-trunking/package.json | 3 +- .../country-permissions.steps.ts | 58 +++++++++++++++++++ 4 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 packages/elastic-sip-trunking/cucumber.js create mode 100644 packages/elastic-sip-trunking/tests/rest/v1/country-permissions/country-permissions.steps.ts diff --git a/.github/workflows/run-ci.yaml b/.github/workflows/run-ci.yaml index 151867dd..d6877f13 100644 --- a/.github/workflows/run-ci.yaml +++ b/.github/workflows/run-ci.yaml @@ -50,11 +50,13 @@ jobs: mkdir -p ./packages/fax/tests/e2e/features mkdir -p ./packages/numbers/tests/e2e/features mkdir -p ./packages/conversation/tests/e2e/features + mkdir -p ./packages/elastic-sip-trunking/tests/e2e/features - name: Copy feature files run: | cp sinch-sdk-mockserver/features/fax/*.feature ./packages/fax/tests/e2e/features/ cp sinch-sdk-mockserver/features/numbers/*.feature ./packages/numbers/tests/e2e/features/ cp sinch-sdk-mockserver/features/conversation/*.feature ./packages/conversation/tests/e2e/features/ + cp sinch-sdk-mockserver/features/elastic-sip-trunking/*.feature ./packages/elastic-sip-trunking/tests/e2e/features/ - name: Run e2e tests run: | yarn install diff --git a/packages/elastic-sip-trunking/cucumber.js b/packages/elastic-sip-trunking/cucumber.js new file mode 100644 index 00000000..691a9809 --- /dev/null +++ b/packages/elastic-sip-trunking/cucumber.js @@ -0,0 +1,8 @@ +module.exports = { + default: [ + 'tests/e2e/features/**/*.feature', + '--require-module ts-node/register', + '--require tests/rest/v1/**/*.steps.ts', + `--format-options '{"snippetInterface": "synchronous"}'`, + ].join(' '), +}; diff --git a/packages/elastic-sip-trunking/package.json b/packages/elastic-sip-trunking/package.json index e80637ef..f07be460 100644 --- a/packages/elastic-sip-trunking/package.json +++ b/packages/elastic-sip-trunking/package.json @@ -24,7 +24,8 @@ "scripts": { "build": "yarn run clean && yarn run compile", "clean": "rimraf dist tsconfig.tsbuildinfo tsconfig.build.tsbuildinfo", - "compile": "tsc -p tsconfig.build.json && tsc -p tsconfig.tests.json && rimraf dist/tests tsconfig.build.tsbuildinfo" + "compile": "tsc -p tsconfig.build.json && tsc -p tsconfig.tests.json && rimraf dist/tests tsconfig.build.tsbuildinfo", + "test:e2e": "cucumber-js" }, "dependencies": { "@sinch/sdk-client": "^1.0.0" diff --git a/packages/elastic-sip-trunking/tests/rest/v1/country-permissions/country-permissions.steps.ts b/packages/elastic-sip-trunking/tests/rest/v1/country-permissions/country-permissions.steps.ts new file mode 100644 index 00000000..17d554a6 --- /dev/null +++ b/packages/elastic-sip-trunking/tests/rest/v1/country-permissions/country-permissions.steps.ts @@ -0,0 +1,58 @@ +import { CountryPermissionsApi, ElasticSipTrunkingService, ElasticSipTrunking } from '../../../../src'; +import { Given, Then, When } from '@cucumber/cucumber'; +import assert from 'assert'; + +let countryPermissionsApi: CountryPermissionsApi; +let countryPermissionsListResponse: ElasticSipTrunking.ListCountryPermissionsResponse; +let countryPermissions: ElasticSipTrunking.CountryPermission; + +Given('the Elastic SIP Trunking service "Country Permissions" is available', function () { + const elasticSipTrunkingService = new ElasticSipTrunkingService({ + projectId: 'tinyfrog-jump-high-over-lilypadbasin', + keyId: 'keyId', + keySecret: 'keySecret', + authHostname: 'http://localhost:3011', + elasticSipTrunkingHostname: 'http://localhost:3016', + }); + countryPermissionsApi = elasticSipTrunkingService.countryPermissions; +}); + +When('I send a request to list the EST countries permissions', async () => { + countryPermissionsListResponse = await countryPermissionsApi.list({}); +}); + +Then('the response contains the list of EST countries permissions', () => { + assert.ok(countryPermissionsListResponse.countryPermissions); + assert.equal(countryPermissionsListResponse.countryPermissions.length, 51); +}); + +When('I send a request to retrieve an EST country\'s permissions', async () => { + countryPermissions = await countryPermissionsApi.get({ + isoCode: 'SE', + }); +}); + +Then('the response contains the EST country\'s permissions details', () => { + assert.equal(countryPermissions.isoCode, 'SE'); + assert.equal(countryPermissions.name, 'Sweden'); + assert.equal(countryPermissions.continent, 'Europe'); + assert.deepEqual(countryPermissions.countryDialingCodes, ['+46']); + assert.equal(countryPermissions.enabled, false); +}); + +When('I send a request to update an EST country\'s permissions', async () => { + countryPermissions = await countryPermissionsApi.update({ + isoCode: 'SE', + updateCountryPermissionRequestBody: { + enabled: true, + }, + }); +}); + +Then('the response contains the EST country\'s permissions details with updated data', () => { + assert.equal(countryPermissions.isoCode, 'SE'); + assert.equal(countryPermissions.name, 'Sweden'); + assert.equal(countryPermissions.continent, 'Europe'); + assert.deepEqual(countryPermissions.countryDialingCodes, ['+46']); + assert.equal(countryPermissions.enabled, true); +}); From 0a8a7164640f1f2cb8886634e8cc44749c1da039 Mon Sep 17 00:00:00 2001 From: Antoine SEIN <142824551+asein-sinch@users.noreply.github.com> Date: Fri, 26 Jul 2024 17:01:23 +0200 Subject: [PATCH 26/54] DEVEXP-509: E2E ElasticSipTrunking/SipTrunks (#119) --- .../sip-trunks/sip-trunks-request-data.ts | 4 +- .../src/rest/v1/sip-trunks/sip-trunks-api.ts | 2 - .../rest/v1/sip-trunks/sip-trunks.steps.ts | 218 ++++++++++++++++++ .../client/api-client-pagination-helper.ts | 3 +- .../src/plugins/timezone/timezone.response.ts | 46 ++-- 5 files changed, 250 insertions(+), 23 deletions(-) create mode 100644 packages/elastic-sip-trunking/tests/rest/v1/sip-trunks/sip-trunks.steps.ts diff --git a/packages/elastic-sip-trunking/src/models/v1/requests/sip-trunks/sip-trunks-request-data.ts b/packages/elastic-sip-trunking/src/models/v1/requests/sip-trunks/sip-trunks-request-data.ts index 24197c43..da0f3f6c 100644 --- a/packages/elastic-sip-trunking/src/models/v1/requests/sip-trunks/sip-trunks-request-data.ts +++ b/packages/elastic-sip-trunking/src/models/v1/requests/sip-trunks/sip-trunks-request-data.ts @@ -9,7 +9,7 @@ export interface AddAccessControlListToTrunkRequestData { } export interface CreateSipTrunkRequestData { /** The SIP trunk details to be used to create a SIP trunk */ - 'createSipTrunkRequestBody': SipTrunk; + 'createSipTrunkRequestBody': Pick; } export interface DeleteAccessControlListFromTrunkRequestData { /** The ID of the trunk that you want to work with */ @@ -41,5 +41,5 @@ export interface UpdateSipTrunkRequestData { /** The ID of the SIP trunk. */ 'sipTrunkId': string; /** The SIP trunk details to be used to update the SIP trunk */ - 'updateSipTrunkRequestBody': SipTrunk; + 'updateSipTrunkRequestBody': Partial>; } diff --git a/packages/elastic-sip-trunking/src/rest/v1/sip-trunks/sip-trunks-api.ts b/packages/elastic-sip-trunking/src/rest/v1/sip-trunks/sip-trunks-api.ts index 22c32b60..06fd9ec4 100644 --- a/packages/elastic-sip-trunking/src/rest/v1/sip-trunks/sip-trunks-api.ts +++ b/packages/elastic-sip-trunking/src/rest/v1/sip-trunks/sip-trunks-api.ts @@ -226,8 +226,6 @@ export class SipTrunksApi extends ElasticSipTrunkingDomainApi { */ public list(data: ListSipTrunksRequestData): ApiListPromise { this.client = this.getSinchClient(); - data['page'] = data['page'] !== undefined ? data['page'] : 1; - data['pageSize'] = data['pageSize'] !== undefined ? data['pageSize'] : 1000; const getParams = this.client.extractQueryParams(data, ['page', 'pageSize', 'domain']); const headers: { [key: string]: string | undefined } = { 'Content-Type': 'application/json', diff --git a/packages/elastic-sip-trunking/tests/rest/v1/sip-trunks/sip-trunks.steps.ts b/packages/elastic-sip-trunking/tests/rest/v1/sip-trunks/sip-trunks.steps.ts new file mode 100644 index 00000000..5a13ca1d --- /dev/null +++ b/packages/elastic-sip-trunking/tests/rest/v1/sip-trunks/sip-trunks.steps.ts @@ -0,0 +1,218 @@ +import { ElasticSipTrunking, ElasticSipTrunkingService, SipTrunksApi } from '../../../../src'; +import { Given, Then, When } from '@cucumber/cucumber'; +import assert from 'assert'; +import { PageResult } from '@sinch/sdk-client'; + +let sipTrunkApi: SipTrunksApi; +let sipTrunk: ElasticSipTrunking.SipTrunk; +let listResponse: PageResult; +let sipTrunksList: ElasticSipTrunking.SipTrunk[]; +let pagesIteration: number; +let deleteSipTrunkResponse: void; +let aclIdsList: ElasticSipTrunking.AddAccessControlListToTrunk; +let listAclsResponse: PageResult; +let aclsList: string[]; +let deleteAclFromTrunkResponse: void; + +Given('the Elastic SIP Trunking service "SIP Trunks" is available', function () { + const elasticSipTrunkingService = new ElasticSipTrunkingService({ + projectId: 'tinyfrog-jump-high-over-lilypadbasin', + keyId: 'keyId', + keySecret: 'keySecret', + authHostname: 'http://localhost:3011', + elasticSipTrunkingHostname: 'http://localhost:3016', + }); + sipTrunkApi = elasticSipTrunkingService.sipTrunks; +}); + +When('I send a request to create a SIP Trunk', async () => { + sipTrunk = await sipTrunkApi.create({ + createSipTrunkRequestBody: { + name: 'Friendly name for e2e test', + hostName: 'e2e-sip-trunk-domain', + }, + }); +}); + +Then('the SIP Trunk is created', () => { + assert.equal(sipTrunk.id, '01W4FFL35P4NC4K35SIPTRUNK1'); + assert.equal(sipTrunk.projectId, 'tinyfrog-jump-high-over-lilypadbasin'); + assert.equal(sipTrunk.name, 'Friendly name for e2e test'); + assert.equal(sipTrunk.hostName, 'e2e-sip-trunk-domain'); + assert.equal(sipTrunk.domain, 'e2e-sip-trunk-domain.pstn.sinch.com'); + assert.equal(sipTrunk.topLevelDomain, 'pstn.sinch.com'); + assert.equal(sipTrunk.callsPerSecond, 1); + assert.equal(sipTrunk.enableCallerName, false); + assert.deepEqual(sipTrunk.createTime, new Date('2024-06-06T14:42:42.820177628Z')); + assert.equal(sipTrunk.updateTime, null); +}); + +When('I send a request to list the existing SIP trunks', async () => { + listResponse = await sipTrunkApi.list({}); +}); + +Then('the response contains {string} SIP trunks', (expectedAnswer: string) => { + const expectedMessagesCount = parseInt(expectedAnswer, 10); + assert.equal(listResponse.data.length, expectedMessagesCount); +}); + +When('I send a request to list all the SIP trunks', async () => { + sipTrunksList = []; + for await (const sipTrunk of sipTrunkApi.list({})) { + sipTrunksList.push(sipTrunk); + } +}); + +When('I iterate manually over the SIP trunks pages', async () => { + sipTrunksList = []; + listResponse = await sipTrunkApi.list({}); + sipTrunksList.push(...listResponse.data); + pagesIteration = 1; + let reachedEndOfPages = false; + while (!reachedEndOfPages) { + if (listResponse.hasNextPage) { + listResponse = await listResponse.nextPage(); + sipTrunksList.push(...listResponse.data); + pagesIteration++; + } else { + reachedEndOfPages = true; + } + } +}); + +Then('the SIP trunks list contains {string} SIP trunks', (expectedAnswer: string) => { + const expectedServices = parseInt(expectedAnswer, 10); + assert.equal(sipTrunksList.length, expectedServices); +}); + +Then('the SIP trunks 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 a SIP trunk', async () => { + sipTrunk = await sipTrunkApi.get({ + sipTrunkId: '01W4FFL35P4NC4K35SIPTRUNK1', + }); +}); + +Then('the response contains the SIP trunk details', () => { + assert.equal(sipTrunk.id, '01W4FFL35P4NC4K35SIPTRUNK1'); + assert.deepEqual(sipTrunk.createTime, new Date('2024-06-06T14:42:42Z')); +}); + +When('I send a request to update a SIP trunk', async () => { + sipTrunk = await sipTrunkApi.update({ + sipTrunkId: '01W4FFL35P4NC4K35SIPTRUNK1', + updateSipTrunkRequestBody: { + name: 'Updated name for e2e test', + hostName: 'us-sip-trunk-domain', + enableCallerName: true, + }, + }); +}); + +Then('the response contains the SIP trunk details with updated data', () => { + assert.equal(sipTrunk.id, '01W4FFL35P4NC4K35SIPTRUNK1'); + assert.equal(sipTrunk.name, 'Updated name for e2e test'); + assert.equal(sipTrunk.hostName, 'us-sip-trunk-domain'); + assert.equal(sipTrunk.domain, 'e2e-sip-trunk-domain.pstn.sinch.com'); + assert.equal(sipTrunk.enableCallerName, true); + assert.deepEqual(sipTrunk.createTime, new Date('2024-06-06T14:42:42Z')); + assert.deepEqual(sipTrunk.updateTime, new Date('2024-06-06T14:48:14.87833248Z')); +}); + +When('I send a request to delete a SIP trunk', async () => { + deleteSipTrunkResponse = await sipTrunkApi.delete({ + sipTrunkId: '01W4FFL35P4NC4K35SIPTRUNK1', + }); +}); + +Then('the delete SIP trunk response contains no data', () => { + assert.deepEqual(deleteSipTrunkResponse, {} ); +}); + +When('I send a request to add ACLs to a SIP trunk', async () => { + aclIdsList = await sipTrunkApi.addAccessControlList({ + trunkId: '01W4FFL35P4NC4K35SIPTRUNK1', + addAccessControlListToTrunkRequestBody: { + accessControlListIds: [ + '01W4FFL35P4NC4K35TRUNKACL1', + ], + }, + }); +}); + +Then('the response contains the list of ACLs added to the trunk', () => { + assert.ok(aclIdsList.accessControlListIds); + assert.equal(aclIdsList.accessControlListIds.length, 1); +}); + +When('I send a request to add an empty ACLs list to a SIP trunk', async () => { + aclIdsList = await sipTrunkApi.addAccessControlList({ + trunkId: '01W4FFL35P4NC4K35SIPTRUNK1', + addAccessControlListToTrunkRequestBody: { + accessControlListIds: [], + }, + }); +}); + +Then('the response contains unexpectedly an empty JSON object', () => { + assert.deepEqual(aclIdsList, {}); +}); + +When('I send a request to list the existing ACLs for a SIP Trunk', async () => { + listAclsResponse = await sipTrunkApi.listAccessControlLists({ + trunkId: '01W4FFL35P4NC4K35SIPTRUNK1', + }); +}); + +Then('the response contains {string} ACLs for a SIP Trunk', (expectedAnswer: string) => { + const expectedMessagesCount = parseInt(expectedAnswer, 10); + assert.equal(listAclsResponse.data.length, expectedMessagesCount); +}); + +When('I send a request to list all the ACLs for a SIP Trunk', async () => { + aclsList = []; + for await (const acl of sipTrunkApi.listAccessControlLists({ trunkId: '01W4FFL35P4NC4K35SIPTRUNK1' })) { + aclsList.push(acl); + } +}); + +When('I iterate manually over the ACLs for a SIP Trunk pages', async () => { + aclsList = []; + listAclsResponse = await sipTrunkApi.listAccessControlLists({ trunkId: '01W4FFL35P4NC4K35SIPTRUNK1' }); + aclsList.push(...listAclsResponse.data); + pagesIteration = 1; + let reachedEndOfPages = false; + while (!reachedEndOfPages) { + if (listAclsResponse.hasNextPage) { + listAclsResponse = await listAclsResponse.nextPage(); + aclsList.push(...listAclsResponse.data); + pagesIteration++; + } else { + reachedEndOfPages = true; + } + } +}); + +Then('the ACLs list contains {string} ACLs for a SIP Trunk', (expectedAnswer: string) => { + const expectedServices = parseInt(expectedAnswer, 10); + assert.equal(sipTrunksList.length, expectedServices); +}); + +Then('the ACLs for a SIP Trunk 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 delete an ACL from a SIP trunk', async () => { + deleteAclFromTrunkResponse = await sipTrunkApi.deleteAccessControlList({ + trunkId: '01W4FFL35P4NC4K35SIPTRUNK1', + accessControlListId: '01W4FFL35P4NC4K35TRUNKACL1', + }); +}); + +Then('the delete ACL from a SIP trunk response contains no data', () => { + assert.deepEqual(deleteAclFromTrunkResponse, {} ); +}); diff --git a/packages/sdk-client/src/client/api-client-pagination-helper.ts b/packages/sdk-client/src/client/api-client-pagination-helper.ts index 14e3b299..31cdaf3d 100644 --- a/packages/sdk-client/src/client/api-client-pagination-helper.ts +++ b/packages/sdk-client/src/client/api-client-pagination-helper.ts @@ -68,6 +68,7 @@ class SinchIterator implements AsyncIterator { this.apiClient, newParams, requestOptions, this.paginatedOperationProperties); } if (this.paginatedOperationProperties.pagination === PaginationEnum.PAGE + || this.paginatedOperationProperties.pagination === PaginationEnum.PAGE2 || this.paginatedOperationProperties.pagination === PaginationEnum.PAGE3) { const newParams = { page: pageResult.nextPageValue, @@ -310,7 +311,7 @@ const calculateLastPageValue = ( case PaginationEnum.PAGE: return Math.ceil(response.count! / pageSize) - 1; case PaginationEnum.PAGE2: - return Math.ceil(response.totalItems! / pageSize) - 1; + return Math.ceil(response.totalItems! / pageSize); } }; diff --git a/packages/sdk-client/src/plugins/timezone/timezone.response.ts b/packages/sdk-client/src/plugins/timezone/timezone.response.ts index e5e11c8a..0216ceec 100644 --- a/packages/sdk-client/src/plugins/timezone/timezone.response.ts +++ b/packages/sdk-client/src/plugins/timezone/timezone.response.ts @@ -6,13 +6,21 @@ const buggyOperationIds: string[] = [ 'VerificationStatusById', 'VerificationStatusByIdentity', 'VerificationStatusByReference', + 'CreateSipTrunk', + 'UpdateSipTrunk', + 'GetSipTrunkById', + 'GetSipTrunks', ]; -const buggyFields: Record = { - 'GetCallResult': 'timestamp', - 'VerificationStatusById': 'verificationTimestamp', - 'VerificationStatusByIdentity': 'verificationTimestamp', - 'VerificationStatusByReference': 'verificationTimestamp', +const buggyFields: Record = { + 'GetCallResult': ['timestamp'], + 'VerificationStatusById': ['verificationTimestamp'], + 'VerificationStatusByIdentity': ['verificationTimestamp'], + 'VerificationStatusByReference': ['verificationTimestamp'], + 'CreateSipTrunk': ['createTime'], + 'UpdateSipTrunk': ['createTime', 'updateTime'], + 'GetSipTrunkById': ['createTime', 'updateTime'], + 'GetSipTrunks': ['createTime', 'updateTime'], }; export class TimezoneResponse | undefined = Record> @@ -27,21 +35,23 @@ implements ResponsePlugin> { if (res && buggyOperationIds.includes(context.operationId) ) { for (const key in res) { if (Object.prototype.hasOwnProperty.call(res, key)) { - const buggyKey = buggyFields[context.operationId]; - if (key === buggyKey && typeof res[buggyKey] === 'string') { - let timestampValue = res[key] as string; - // Check the formats +XX:XX, +XX and Z - const timeZoneRegex = /([+-]\d{2}(:\d{2})|Z)$/; - if (!timeZoneRegex.test(timestampValue)) { - const hourMinutesTimezoneRegex = /([+-]\d{2})$/; - // A timestamp with no minutes in the timezone cannot be converted into a Date => assume it's :00 - if (hourMinutesTimezoneRegex.test(timestampValue)) { - timestampValue = timestampValue + ':00'; - } else { - timestampValue = timestampValue + 'Z'; + const buggyKeys = buggyFields[context.operationId]; + for (const buggyKey of buggyKeys) { + if (key === buggyKey && typeof res[buggyKey] === 'string') { + let timestampValue = res[key] as string; + // Check the formats +XX:XX, +XX and Z + const timeZoneRegex = /([+-]\d{2}(:\d{2})|Z)$/; + if (!timeZoneRegex.test(timestampValue)) { + const hourMinutesTimezoneRegex = /([+-]\d{2})$/; + // A timestamp with no minutes in the timezone cannot be converted into a Date => assume it's :00 + if (hourMinutesTimezoneRegex.test(timestampValue)) { + timestampValue = timestampValue + ':00'; + } else { + timestampValue = timestampValue + 'Z'; + } } + res[key] = timestampValue; } - res[key] = timestampValue; } } } From e2dfbec51a57201306ff98d3cb56e069a9795d0b Mon Sep 17 00:00:00 2001 From: Antoine SEIN <142824551+asein-sinch@users.noreply.github.com> Date: Wed, 31 Jul 2024 10:20:03 +0200 Subject: [PATCH 27/54] DEVEXP-508: E2E ElasticSipTrunking/SipEndpoints (#120) --- .../sip-endpoints-request-data.ts | 4 +- .../v1/sip-endpoints/sip-endpoints-api.ts | 4 +- .../v1/sip-endpoints/sip-endpoints.steps.ts | 141 ++++++++++++++++++ .../rest/v1/sip-trunks/sip-trunks.steps.ts | 8 +- .../src/plugins/timezone/timezone.response.ts | 9 ++ 5 files changed, 157 insertions(+), 9 deletions(-) create mode 100644 packages/elastic-sip-trunking/tests/rest/v1/sip-endpoints/sip-endpoints.steps.ts diff --git a/packages/elastic-sip-trunking/src/models/v1/requests/sip-endpoints/sip-endpoints-request-data.ts b/packages/elastic-sip-trunking/src/models/v1/requests/sip-endpoints/sip-endpoints-request-data.ts index 04d74ba7..78d5f17f 100644 --- a/packages/elastic-sip-trunking/src/models/v1/requests/sip-endpoints/sip-endpoints-request-data.ts +++ b/packages/elastic-sip-trunking/src/models/v1/requests/sip-endpoints/sip-endpoints-request-data.ts @@ -4,7 +4,7 @@ export interface CreateSipEndpointRequestData { /** The ID of the SIP trunk. */ 'sipTrunkId': string; /** The body containing the SIP Endpoint to create for the SIP trunk */ - 'createSipEndpointRequestBody': SipEndpoint; + 'createSipEndpointRequestBody': Omit; } export interface DeleteSipEndpointRequestData { /** The ID of the SIP trunk. */ @@ -32,5 +32,5 @@ export interface UpdateSipEndpointRequestData { /** The ID of the SIP endpoint. */ 'sipEndpointId': string; /** The body containing the SIP Endpoint details to update */ - 'updateSipEndpointRequestBody': SipEndpoint; + 'updateSipEndpointRequestBody': Omit; } diff --git a/packages/elastic-sip-trunking/src/rest/v1/sip-endpoints/sip-endpoints-api.ts b/packages/elastic-sip-trunking/src/rest/v1/sip-endpoints/sip-endpoints-api.ts index 854a1b22..50a1aa88 100644 --- a/packages/elastic-sip-trunking/src/rest/v1/sip-endpoints/sip-endpoints-api.ts +++ b/packages/elastic-sip-trunking/src/rest/v1/sip-endpoints/sip-endpoints-api.ts @@ -95,8 +95,6 @@ export class SipEndpointsApi extends ElasticSipTrunkingDomainApi { */ public list(data: ListSipEndpointsRequestData): ApiListPromise { this.client = this.getSinchClient(); - data['page'] = data['page'] !== undefined ? data['page'] : 1; - data['pageSize'] = data['pageSize'] !== undefined ? data['pageSize'] : 1000; const getParams = this.client.extractQueryParams(data, ['page', 'pageSize']); const headers: { [key: string]: string | undefined } = { 'Content-Type': 'application/json', @@ -111,7 +109,7 @@ export class SipEndpointsApi extends ElasticSipTrunkingDomainApi { const operationProperties: PaginatedApiProperties = { pagination: PaginationEnum.PAGE2, apiName: this.apiName, - operationId: 'GetSipEndpoint', + operationId: 'GetSipEndpoints', dataKey: 'endpoints', }; diff --git a/packages/elastic-sip-trunking/tests/rest/v1/sip-endpoints/sip-endpoints.steps.ts b/packages/elastic-sip-trunking/tests/rest/v1/sip-endpoints/sip-endpoints.steps.ts new file mode 100644 index 00000000..c4023cc9 --- /dev/null +++ b/packages/elastic-sip-trunking/tests/rest/v1/sip-endpoints/sip-endpoints.steps.ts @@ -0,0 +1,141 @@ +import { ElasticSipTrunking, ElasticSipTrunkingService, SipEndpointsApi } from '../../../../src'; +import { Given, Then, When } from '@cucumber/cucumber'; +import assert from 'assert'; +import { PageResult } from '@sinch/sdk-client'; + +let sipEndpointsApi: SipEndpointsApi; +let sipEndpoint: ElasticSipTrunking.SipEndpoint; +let listResponse: PageResult; +let sipEndpointsList: ElasticSipTrunking.SipEndpoint[]; +let pagesIteration: number; +let deleteSipEndpointResponse: void; + +Given('the Elastic SIP Trunking service "SIP Endpoints" is available', function () { + const elasticSipTrunkingService = new ElasticSipTrunkingService({ + projectId: 'tinyfrog-jump-high-over-lilypadbasin', + keyId: 'keyId', + keySecret: 'keySecret', + authHostname: 'http://localhost:3011', + elasticSipTrunkingHostname: 'http://localhost:3016', + }); + sipEndpointsApi = elasticSipTrunkingService.sipEndpoints; +}); + +When('I send a request to create a SIP Endpoint', async () => { + sipEndpoint = await sipEndpointsApi.create({ + sipTrunkId: '01W4FFL35P4NC4K35SIPTRUNK1', + createSipEndpointRequestBody: { + name: 'Capsule Corp Endpoint', + address: '127.0.0.1', + priority: 2, + }, + }); +}); + +Then('the SIP Endpoint is created', () => { + assert.equal(sipEndpoint.id, '01W4FFL35P4NC4K35SIPENDP01'); + assert.equal(sipEndpoint.sipTrunkId, '01W4FFL35P4NC4K35SIPTRUNK1'); + assert.equal(sipEndpoint.name, 'Capsule Corp Endpoint'); + assert.equal(sipEndpoint.address, '127.0.0.1'); + assert.equal(sipEndpoint.port, 5060); + assert.equal(sipEndpoint.transport, 'UDP'); + assert.equal(sipEndpoint.priority, 2); + assert.equal(sipEndpoint.enabled, true); + assert.deepEqual(sipEndpoint.createTime, new Date('2024-06-06T14:42:42.337854345Z')); + assert.equal(sipEndpoint.updateTime, null); +}); + +When('I send a request to list the existing SIP Endpoints', async () => { + listResponse = await sipEndpointsApi.list({ + sipTrunkId: '01W4FFL35P4NC4K35SIPTRUNK1', + }); +}); + +Then('the response contains {string} SIP Endpoints', (expectedAnswer: string) => { + const expectedSipEndpointsCount = parseInt(expectedAnswer, 10); + assert.equal(listResponse.data.length, expectedSipEndpointsCount); +}); + +When('I send a request to list all the SIP Endpoints', async () => { + sipEndpointsList = []; + for await (const sipEndpoint of sipEndpointsApi.list({ sipTrunkId: '01W4FFL35P4NC4K35SIPTRUNK1' })) { + sipEndpointsList.push(sipEndpoint); + } +}); + +When('I iterate manually over the SIP Endpoints pages', async () => { + sipEndpointsList = []; + listResponse = await sipEndpointsApi.list({ sipTrunkId: '01W4FFL35P4NC4K35SIPTRUNK1' }); + sipEndpointsList.push(...listResponse.data); + pagesIteration = 1; + let reachedEndOfPages = false; + while (!reachedEndOfPages) { + if (listResponse.hasNextPage) { + listResponse = await listResponse.nextPage(); + sipEndpointsList.push(...listResponse.data); + pagesIteration++; + } else { + reachedEndOfPages = true; + } + } +}); + +Then('the SIP Endpoints list contains {string} SIP Endpoints', (expectedAnswer: string) => { + const expectedSipEndpointsCount = parseInt(expectedAnswer, 10); + assert.equal(sipEndpointsList.length, expectedSipEndpointsCount); +}); + +Then('the SIP Endpoints 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 a SIP Endpoint', async () => { + sipEndpoint = await sipEndpointsApi.get({ + sipTrunkId: '01W4FFL35P4NC4K35SIPTRUNK1', + sipEndpointId: '01W4FFL35P4NC4K35SIPENDP01', + }); +}); + +Then('the response contains the SIP Endpoint details', () => { + assert.equal(sipEndpoint.id, '01W4FFL35P4NC4K35SIPENDP01'); + assert.deepEqual(sipEndpoint.createTime, new Date('2024-06-06T14:42:42Z')); +}); + +When('I send a request to update a SIP Endpoint', async () => { + sipEndpoint = await sipEndpointsApi.update({ + sipTrunkId: '01W4FFL35P4NC4K35SIPTRUNK1', + sipEndpointId: '01W4FFL35P4NC4K35SIPENDP01', + updateSipEndpointRequestBody: { + name: 'Capsule Corp Endpoint - updated', + address: '127.0.0.2', + priority: 3, + port: 5061, + transport: 'TCP', + enabled: false, + }, + }); +}); + +Then('the response contains the SIP Endpoint details with updated data', () => { + assert.equal(sipEndpoint.id, '01W4FFL35P4NC4K35SIPENDP01'); + assert.equal(sipEndpoint.name, 'Capsule Corp Endpoint - updated'); + assert.equal(sipEndpoint.address, '127.0.0.2'); + assert.equal(sipEndpoint.port, 5061); + assert.equal(sipEndpoint.transport, 'TCP'); + assert.equal(sipEndpoint.priority, 3); + assert.equal(sipEndpoint.enabled, false); + assert.deepEqual(sipEndpoint.createTime, new Date('2024-06-06T14:42:42Z')); + assert.deepEqual(sipEndpoint.updateTime, new Date('2024-06-06T14:45:11.428052267Z')); +}); + +When('I send a request to delete a SIP Endpoint', async () => { + deleteSipEndpointResponse = await sipEndpointsApi.delete({ + sipTrunkId: '01W4FFL35P4NC4K35SIPTRUNK1', + sipEndpointId: '01W4FFL35P4NC4K35SIPENDP01', + }); +}); + +Then('the delete SIP Endpoint response contains no data', () => { + assert.deepEqual(deleteSipEndpointResponse, {} ); +}); diff --git a/packages/elastic-sip-trunking/tests/rest/v1/sip-trunks/sip-trunks.steps.ts b/packages/elastic-sip-trunking/tests/rest/v1/sip-trunks/sip-trunks.steps.ts index 5a13ca1d..45c2857a 100644 --- a/packages/elastic-sip-trunking/tests/rest/v1/sip-trunks/sip-trunks.steps.ts +++ b/packages/elastic-sip-trunking/tests/rest/v1/sip-trunks/sip-trunks.steps.ts @@ -52,8 +52,8 @@ When('I send a request to list the existing SIP trunks', async () => { }); Then('the response contains {string} SIP trunks', (expectedAnswer: string) => { - const expectedMessagesCount = parseInt(expectedAnswer, 10); - assert.equal(listResponse.data.length, expectedMessagesCount); + const expectedSipTrunksCount = parseInt(expectedAnswer, 10); + assert.equal(listResponse.data.length, expectedSipTrunksCount); }); When('I send a request to list all the SIP trunks', async () => { @@ -81,8 +81,8 @@ When('I iterate manually over the SIP trunks pages', async () => { }); Then('the SIP trunks list contains {string} SIP trunks', (expectedAnswer: string) => { - const expectedServices = parseInt(expectedAnswer, 10); - assert.equal(sipTrunksList.length, expectedServices); + const expectedSipTrunksCount = parseInt(expectedAnswer, 10); + assert.equal(sipTrunksList.length, expectedSipTrunksCount); }); Then('the SIP trunks iteration result contains the data from {string} pages', (expectedAnswer: string) => { diff --git a/packages/sdk-client/src/plugins/timezone/timezone.response.ts b/packages/sdk-client/src/plugins/timezone/timezone.response.ts index 0216ceec..cdf9873b 100644 --- a/packages/sdk-client/src/plugins/timezone/timezone.response.ts +++ b/packages/sdk-client/src/plugins/timezone/timezone.response.ts @@ -10,6 +10,11 @@ const buggyOperationIds: string[] = [ 'UpdateSipTrunk', 'GetSipTrunkById', 'GetSipTrunks', + 'CreateSipEndpoint', + 'UpdateSipEndpoint', + 'GetSipEndpointById', + 'GetSipEndpoints', + ]; const buggyFields: Record = { @@ -21,6 +26,10 @@ const buggyFields: Record = { 'UpdateSipTrunk': ['createTime', 'updateTime'], 'GetSipTrunkById': ['createTime', 'updateTime'], 'GetSipTrunks': ['createTime', 'updateTime'], + 'CreateSipEndpoint': ['createTime'], + 'UpdateSipEndpoint': ['createTime', 'updateTime'], + 'GetSipEndpointById': ['createTime', 'updateTime'], + 'GetSipEndpoints': ['createTime', 'updateTime'], }; export class TimezoneResponse | undefined = Record> From 904da4e159a175fc9fda5c714f51f1a9d9f27908 Mon Sep 17 00:00:00 2001 From: Antoine SEIN <142824551+asein-sinch@users.noreply.github.com> Date: Wed, 31 Jul 2024 12:16:39 +0200 Subject: [PATCH 28/54] DEVEXP-505: E2E ElasticSipTrunking/AccessControlLists (#121) --- .../create-access-control-list-request.ts | 10 + .../index.ts | 1 + .../src/models/v1/index.ts | 1 + .../src/models/v1/ip-range/index.ts | 2 +- .../src/models/v1/ip-range/ip-range.ts | 2 + .../access-control-list-request-data.ts | 14 +- .../update-access-control-list-request.ts | 4 +- .../access-control-list-api.jest.fixture.ts | 6 + .../access-control-list-api.ts | 30 +- .../access-control-list-api.test.ts | 41 ++- .../access-control-lists.steps.ts | 326 ++++++++++++++++++ .../src/client/api-client-helpers.ts | 17 +- .../sdk-client/src/client/api-fetch-client.ts | 2 - packages/sdk-client/src/plugins/index.ts | 1 - .../sdk-client/src/plugins/timezone/index.ts | 1 - .../src/plugins/timezone/timezone.response.ts | 73 ---- .../timezone/timezone.response.test.ts | 80 ----- 17 files changed, 443 insertions(+), 168 deletions(-) create mode 100644 packages/elastic-sip-trunking/src/models/v1/create-access-control-list-request/create-access-control-list-request.ts create mode 100644 packages/elastic-sip-trunking/src/models/v1/create-access-control-list-request/index.ts create mode 100644 packages/elastic-sip-trunking/tests/rest/v1/access-control-list/access-control-lists.steps.ts delete mode 100644 packages/sdk-client/src/plugins/timezone/index.ts delete mode 100644 packages/sdk-client/src/plugins/timezone/timezone.response.ts delete mode 100644 packages/sdk-client/tests/plugins/timezone/timezone.response.test.ts diff --git a/packages/elastic-sip-trunking/src/models/v1/create-access-control-list-request/create-access-control-list-request.ts b/packages/elastic-sip-trunking/src/models/v1/create-access-control-list-request/create-access-control-list-request.ts new file mode 100644 index 00000000..875d0144 --- /dev/null +++ b/packages/elastic-sip-trunking/src/models/v1/create-access-control-list-request/create-access-control-list-request.ts @@ -0,0 +1,10 @@ +import { IpRangeRequest } from '../ip-range'; + +export interface CreateAccessControlListRequest { + /** Your name for the access control list entry. */ + name: string; + /** Whether the access control list entry is enabled. You can use this to disable a list temporarily without deleting it. */ + enabled?: boolean; + /** An array of all the IP ranges to create. */ + ipRanges: IpRangeRequest[]; +} diff --git a/packages/elastic-sip-trunking/src/models/v1/create-access-control-list-request/index.ts b/packages/elastic-sip-trunking/src/models/v1/create-access-control-list-request/index.ts new file mode 100644 index 00000000..55455980 --- /dev/null +++ b/packages/elastic-sip-trunking/src/models/v1/create-access-control-list-request/index.ts @@ -0,0 +1 @@ +export type { CreateAccessControlListRequest } from './create-access-control-list-request'; diff --git a/packages/elastic-sip-trunking/src/models/v1/index.ts b/packages/elastic-sip-trunking/src/models/v1/index.ts index 4f57738f..6df44669 100644 --- a/packages/elastic-sip-trunking/src/models/v1/index.ts +++ b/packages/elastic-sip-trunking/src/models/v1/index.ts @@ -2,6 +2,7 @@ export * from './access-control-list'; export * from './add-access-control-list-to-trunk'; export * from './call'; export * from './country-permission'; +export * from './create-access-control-list-request'; export * from './ip-range'; export * from './list-country-permissions-response'; export * from './money'; diff --git a/packages/elastic-sip-trunking/src/models/v1/ip-range/index.ts b/packages/elastic-sip-trunking/src/models/v1/ip-range/index.ts index 8693404c..62f1fc29 100644 --- a/packages/elastic-sip-trunking/src/models/v1/ip-range/index.ts +++ b/packages/elastic-sip-trunking/src/models/v1/ip-range/index.ts @@ -1 +1 @@ -export type { IpRange } from './ip-range'; +export type { IpRange, IpRangeRequest } from './ip-range'; diff --git a/packages/elastic-sip-trunking/src/models/v1/ip-range/ip-range.ts b/packages/elastic-sip-trunking/src/models/v1/ip-range/ip-range.ts index a014b342..7043afb0 100644 --- a/packages/elastic-sip-trunking/src/models/v1/ip-range/ip-range.ts +++ b/packages/elastic-sip-trunking/src/models/v1/ip-range/ip-range.ts @@ -19,3 +19,5 @@ export interface IpRange { /** The ID of the access control list. */ accessControlListId?: string; } + +export type IpRangeRequest = Omit; diff --git a/packages/elastic-sip-trunking/src/models/v1/requests/access-control-list/access-control-list-request-data.ts b/packages/elastic-sip-trunking/src/models/v1/requests/access-control-list/access-control-list-request-data.ts index db991ced..bf8c9300 100644 --- a/packages/elastic-sip-trunking/src/models/v1/requests/access-control-list/access-control-list-request-data.ts +++ b/packages/elastic-sip-trunking/src/models/v1/requests/access-control-list/access-control-list-request-data.ts @@ -1,16 +1,16 @@ -import { AccessControlList } from '../../access-control-list'; -import { IpRange } from '../../ip-range'; +import { IpRangeRequest } from '../../ip-range'; import { UpdateAccessControlListRequest } from '../../update-access-control-list-request'; +import { CreateAccessControlListRequest } from '../../create-access-control-list-request'; export interface AddIpRangeToAccessControlListRequestData { /** The ID of the access control list entry. that you want to work with */ 'accessControlListId': string; /** */ - 'addIpRangeRequestBody': IpRange; + 'addIpRangeRequestBody': IpRangeRequest; } export interface CreateAccessControlListRequestData { /** The Access Control List details used to create an Access Control List */ - 'createAccessControlListBody': AccessControlList; + 'createAccessControlListBody': CreateAccessControlListRequest; } export interface DeleteAccessControlListRequestData { /** The ID of the access control list entry. */ @@ -22,6 +22,10 @@ export interface DeleteIpRangeFromAccessControlListRequestData { /** The ID of the IP range that you want to update. */ 'ipRangeId': string; } +export interface GetAccessControlListRequestData { + /** The ID of the access control list entry that you want to retrieve. */ + 'id': string; +} export interface ListAccessControlListRequestData { } export interface ListIpRangesForAccessControlListRequestData { @@ -40,5 +44,5 @@ export interface UpdateIpRangeFromAccessControlListRequestData { /** The ID of the IP range that you want to update. */ 'ipRangeId': string; /** The IP range details used to update the IP range property from an Access Control List */ - 'updateIpRangeRequestBody': IpRange; + 'updateIpRangeRequestBody': IpRangeRequest; } diff --git a/packages/elastic-sip-trunking/src/models/v1/update-access-control-list-request/update-access-control-list-request.ts b/packages/elastic-sip-trunking/src/models/v1/update-access-control-list-request/update-access-control-list-request.ts index cfdef295..50a23a6e 100644 --- a/packages/elastic-sip-trunking/src/models/v1/update-access-control-list-request/update-access-control-list-request.ts +++ b/packages/elastic-sip-trunking/src/models/v1/update-access-control-list-request/update-access-control-list-request.ts @@ -1,4 +1,4 @@ -import { IpRange } from '../ip-range'; +import { IpRangeRequest } from '../ip-range'; export interface UpdateAccessControlListRequest { /** Your name for the access control list entry. */ @@ -6,5 +6,5 @@ export interface UpdateAccessControlListRequest { /** Whether the access control list entry is enabled. You can use this to disable a list temporarily without deleting it. */ enabled?: boolean; /** An array of all the IP ranges to update. */ - ipRanges?: IpRange[]; + ipRanges?: IpRangeRequest[]; } diff --git a/packages/elastic-sip-trunking/src/rest/v1/access-control-list/access-control-list-api.jest.fixture.ts b/packages/elastic-sip-trunking/src/rest/v1/access-control-list/access-control-list-api.jest.fixture.ts index eddf7b4b..29ee6db5 100644 --- a/packages/elastic-sip-trunking/src/rest/v1/access-control-list/access-control-list-api.jest.fixture.ts +++ b/packages/elastic-sip-trunking/src/rest/v1/access-control-list/access-control-list-api.jest.fixture.ts @@ -14,6 +14,7 @@ import { UpdateAccessControlListRequestData, UpdateIpRangeFromAccessControlListRequestData, AddAccessControlListToTrunk, + GetAccessControlListRequestData, } from '../../../models'; import { ApiListPromise } from '@sinch/sdk-client'; @@ -44,6 +45,11 @@ export class AccessControlListApiFixture implements Partial, [CreateAccessControlListRequestData]> = jest.fn(); + /** + * Fixture associated to function get + */ + public get: jest.Mock< + Promise, [GetAccessControlListRequestData]> = jest.fn(); /** * Fixture associated to function delete */ diff --git a/packages/elastic-sip-trunking/src/rest/v1/access-control-list/access-control-list-api.ts b/packages/elastic-sip-trunking/src/rest/v1/access-control-list/access-control-list-api.ts index 9a8e699f..fa954054 100644 --- a/packages/elastic-sip-trunking/src/rest/v1/access-control-list/access-control-list-api.ts +++ b/packages/elastic-sip-trunking/src/rest/v1/access-control-list/access-control-list-api.ts @@ -13,6 +13,7 @@ import { ListAccessControlListsForTrunkRequestData, AddAccessControlListToTrunk, IpRange, + GetAccessControlListRequestData, } from '../../../models'; import { RequestBody, @@ -183,6 +184,33 @@ export class AccessControlListApi extends ElasticSipTrunkingDomainApi { }); } + /** + * Get Access Control List + * Search for an Access Control List by ID. + * @param { GetAccessControlListRequestData } data - The data to provide to the API call. + */ + public async get(data: GetAccessControlListRequestData): Promise { + this.client = this.getSinchClient(); + const getParams = this.client.extractQueryParams(data, [] as never[]); + const headers: { [key: string]: string | undefined } = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }; + + const body: RequestBody = ''; + const basePathUrl = `${this.client.apiClientOptions.hostname}/v1/projects/${this.client.apiClientOptions.projectId}/accessControlLists/${data['id']}`; + + const requestOptions = await this.client.prepareOptions(basePathUrl, 'GET', getParams, headers, body || undefined); + const url = this.client.prepareUrl(requestOptions.hostname, requestOptions.queryParams); + + return this.client.processCall({ + url, + requestOptions, + apiName: this.apiName, + operationId: 'GetAccessControlListById', + }); + } + /** * List ACLs * Fetches the list of Access Control List entries. @@ -205,7 +233,7 @@ export class AccessControlListApi extends ElasticSipTrunkingDomainApi { const operationProperties: PaginatedApiProperties = { pagination: PaginationEnum.PAGE2, apiName: this.apiName, - operationId: 'GetAccessControlList', + operationId: 'GetAccessControlLists', dataKey: 'accessControlLists', }; diff --git a/packages/elastic-sip-trunking/tests/rest/v1/access-control-list/access-control-list-api.test.ts b/packages/elastic-sip-trunking/tests/rest/v1/access-control-list/access-control-list-api.test.ts index c14d4111..93a6bb11 100644 --- a/packages/elastic-sip-trunking/tests/rest/v1/access-control-list/access-control-list-api.test.ts +++ b/packages/elastic-sip-trunking/tests/rest/v1/access-control-list/access-control-list-api.test.ts @@ -131,6 +131,45 @@ describe('AccessControlListApi', () => { }); }); + describe ('getAccessControlListById', () => { + // eslint-disable-next-line max-len + it('should make a GET request to retrieve an access control list', async () => { + // Given + const requestData: ElasticSipTrunking.GetAccessControlListRequestData = { + id: '01HA9BRJW4J3QE4WBKVC337V4E', + }; + const expectedResponse: ElasticSipTrunking.AccessControlList = { + name: 'My new ACL', + projectId: '3acb7ae1-cf3d-4112-ba5e-3a9d8c71cd47', + enabled: true, + id: '01HA9BRJW4J3QE4WBKVC337V4E', + createTime: new Date('2023-09-14T07:39:19Z'), + updateTime: null, + ipRanges: [ + { + description: 'Location 1', + ipAddress: '15.15.15.15', + range: 20, + projectId: '3acb7ae1-cf3d-4112-ba5e-3a9d8c71cd47', + accessControlListId: '01HA9BRJW4J3QE4WBKVC337V4E', + id: '01HA9BRJYR9Q7ZBDYMXHVWT8S8', + createTime: new Date('2023-09-14T07:39:19Z'), + updateTime: null, + }, + ], + }; + + // When + fixture.get.mockResolvedValue(expectedResponse); + accessControlListApi.get = fixture.get; + const response = await accessControlListApi.get(requestData); + + // Then + expect(response).toEqual(expectedResponse); + expect(fixture.get).toHaveBeenCalledWith(requestData); + }); + }); + describe ('deleteAccessControlList', () => { it('should make a DELETE request to delete an access control list entry', async () => { // Given @@ -187,7 +226,7 @@ describe('AccessControlListApi', () => { }); }); - describe ('getAccessControlList', () => { + describe ('getAccessControlLists', () => { it('should make a GET request to fetch the list of Access Control List entries', async () => { // Given const requestData: ElasticSipTrunking.ListAccessControlListRequestData = {}; diff --git a/packages/elastic-sip-trunking/tests/rest/v1/access-control-list/access-control-lists.steps.ts b/packages/elastic-sip-trunking/tests/rest/v1/access-control-list/access-control-lists.steps.ts new file mode 100644 index 00000000..b83bf778 --- /dev/null +++ b/packages/elastic-sip-trunking/tests/rest/v1/access-control-list/access-control-lists.steps.ts @@ -0,0 +1,326 @@ +import { ElasticSipTrunking, ElasticSipTrunkingService, AccessControlListApi } from '../../../../src'; +import { Given, Then, When } from '@cucumber/cucumber'; +import assert from 'assert'; +import { PageResult } from '@sinch/sdk-client'; + +let accessControlListsApi: AccessControlListApi; +let accessControlList: ElasticSipTrunking.AccessControlList; +let listResponse: PageResult; +let aclsList: ElasticSipTrunking.AccessControlList[]; +let pagesIteration: number; +let deleteAclResponse: void; +let ipRange: ElasticSipTrunking.IpRange; +let listIpRangesResponse: PageResult; +let ipRangesList: ElasticSipTrunking.IpRange[]; +let deleteIpRangeResponse: void; +let addedAclsList: ElasticSipTrunking.AddAccessControlListToTrunk; +let listAclIdsResponse: PageResult; +let aclIdsList: string[]; +let deleteAclFromTrunkResponse: void; + +Given('the Elastic SIP Trunking service "Access Control Lists" is available', function () { + const elasticSipTrunkingService = new ElasticSipTrunkingService({ + projectId: 'tinyfrog-jump-high-over-lilypadbasin', + keyId: 'keyId', + keySecret: 'keySecret', + authHostname: 'http://localhost:3011', + elasticSipTrunkingHostname: 'http://localhost:3016', + }); + accessControlListsApi = elasticSipTrunkingService.accessControlList; +}); + +When('I send a request to create an Access Control List', async () => { + accessControlList = await accessControlListsApi.create({ + createAccessControlListBody: { + name: 'My Access Control List', + ipRanges: [ + { + description: 'Location 1', + ipAddress: '15.15.15.15', + }, + ], + }, + }); +}); + +Then('the Access Control List is created', () => { + assert.equal(accessControlList.id, '01W4FFL35P4NC4K35SIPACL001'); + assert.equal(accessControlList.name, 'My Access Control List'); + assert.equal(accessControlList.enabled, true); + assert.equal(accessControlList.projectId, 'tinyfrog-jump-high-over-lilypadbasin'); + assert.deepEqual(accessControlList.createTime, new Date('2024-06-06T14:42:42.892741384Z')); + assert.equal(accessControlList.updateTime, null); + assert.ok(accessControlList.ipRanges); + const ipRange = accessControlList.ipRanges[0]; + assert.equal(ipRange.id, '01W4FFL35P4NC4K35IPRANGE01'); + assert.equal(ipRange.accessControlListId, '01W4FFL35P4NC4K35SIPACL001'); + assert.equal(ipRange.description, 'Location 1'); + assert.equal(ipRange.ipAddress, '15.15.15.15'); + assert.equal(ipRange.range, 32); + assert.equal(ipRange.projectId, 'tinyfrog-jump-high-over-lilypadbasin'); + assert.deepEqual(ipRange.createTime, new Date('2024-06-06T14:42:42.896695235Z')); + assert.equal(ipRange.updateTime, null); +}); + +When('I send a request to list the existing Access Control Lists', async () => { + listResponse = await accessControlListsApi.list({}); +}); + +Then('the response contains {string} Access Control Lists', (expectedAnswer: string) => { + const expectedAclsCount = parseInt(expectedAnswer, 10); + assert.equal(listResponse.data.length, expectedAclsCount); + accessControlList = listResponse.data[0]; + assert.equal(accessControlList.id, '01W4FFL35P4NC4K35SIPACL001'); + assert.deepEqual(accessControlList.createTime, new Date('2024-06-06T14:42:42Z')); + const ipRange = accessControlList.ipRanges[0]; + assert.deepEqual(ipRange.createTime, new Date('2024-06-06T14:42:42Z')); +}); + +When('I send a request to list all the Access Control Lists', async () => { + aclsList = []; + for await (const acl of accessControlListsApi.list({})) { + aclsList.push(acl); + } +}); + +When('I iterate manually over the Access Control Lists pages', async () => { + aclsList = []; + listResponse = await accessControlListsApi.list({}); + aclsList.push(...listResponse.data); + pagesIteration = 1; + let reachedEndOfPages = false; + while (!reachedEndOfPages) { + if (listResponse.hasNextPage) { + listResponse = await listResponse.nextPage(); + aclsList.push(...listResponse.data); + pagesIteration++; + } else { + reachedEndOfPages = true; + } + } +}); + +Then('the Access Control Lists list contains {string} Access Control Lists', (expectedAnswer: string) => { + const expectedAclsCount = parseInt(expectedAnswer, 10); + assert.equal(aclsList.length, expectedAclsCount); +}); + +Then('the Access Control Lists 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 Access Control List', async () => { + accessControlList = await accessControlListsApi.get({ + id: '01W4FFL35P4NC4K35SIPACL001', + }); +}); + +Then('the response contains the Access Control List details', () => { + assert.equal(accessControlList.id, '01W4FFL35P4NC4K35SIPACL001'); + assert.equal(accessControlList.name, 'My Access Control List'); + assert.deepEqual(accessControlList.createTime, new Date('2024-06-06T14:42:42Z')); + assert.ok(accessControlList.ipRanges); +}); + +When('I send a request to update an Access Control List', async () => { + accessControlList = await accessControlListsApi.update({ + id: '01W4FFL35P4NC4K35SIPACL003', + updateAccessControlListRequestBody: { + name: 'My Access Control List 3', + }, + }); +}); + +Then('the response contains the Access Control List details with updated data', () => { + assert.equal(accessControlList.id, '01W4FFL35P4NC4K35SIPACL003'); + assert.equal(accessControlList.name, 'My Access Control List 3'); + assert.deepEqual(accessControlList.createTime, new Date('2024-06-06T15:52:22Z')); + assert.deepEqual(accessControlList.updateTime, new Date('2024-06-06T15:52:52.554735034Z')); +}); + +When('I send a request to delete an Access Control List', async () => { + deleteAclResponse = await accessControlListsApi.delete({ + id: '01W4FFL35P4NC4K35SIPACL001', + }); +}); + +Then('the delete Access Control List response contains no data', () => { + assert.deepEqual(deleteAclResponse, {} ); +}); + +When('I send a request to add an IP Range to an Access Control List', async () => { + ipRange = await accessControlListsApi.addIpRange({ + accessControlListId: '01W4FFL35P4NC4K35SIPACL001', + addIpRangeRequestBody: { + description: 'West wing', + ipAddress: '10.0.1.1', + range: 24, + }, + }); +}); + +Then('the response contains the created IP range associated to the Access Control List', () => { + assert.equal(ipRange.id, '01W4FFL35P4NC4K35IPRANGE06'); + assert.equal(ipRange.accessControlListId, '01W4FFL35P4NC4K35SIPACL003'); + assert.equal(ipRange.description, 'West wing'); + assert.equal(ipRange.ipAddress, '10.0.1.1'); + assert.equal(ipRange.range, 24); + assert.equal(ipRange.projectId, 'tinyfrog-jump-high-over-lilypadbasin'); + assert.deepEqual(ipRange.createTime, new Date('2024-06-06T15:56:26.70848666Z')); + assert.equal(ipRange.updateTime, null); +}); + +When('I send a request to list the existing IP Ranges', async () => { + listIpRangesResponse = await accessControlListsApi.listIpRanges({ + accessControlListId: '01W4FFL35P4NC4K35SIPACL002', + }); +}); + +Then('the response contains {string} IP Ranges', (expectedAnswer: string) => { + const expectedIpRangesCount = parseInt(expectedAnswer, 10); + assert.equal(listIpRangesResponse.data.length, expectedIpRangesCount); +}); + +When('I send a request to list all the IP Ranges', async () => { + ipRangesList = []; + for await (const ipRange of accessControlListsApi.listIpRanges({ + accessControlListId: '01W4FFL35P4NC4K35SIPACL002', + })) { + ipRangesList.push(ipRange); + } +}); + +When('I iterate manually over the IP Ranges pages', async () => { + ipRangesList = []; + listIpRangesResponse = await accessControlListsApi.listIpRanges({ + accessControlListId: '01W4FFL35P4NC4K35SIPACL002', + }); + ipRangesList.push(...listIpRangesResponse.data); + pagesIteration = 1; + let reachedEndOfPages = false; + while (!reachedEndOfPages) { + if (listIpRangesResponse.hasNextPage) { + listIpRangesResponse = await listIpRangesResponse.nextPage(); + ipRangesList.push(...listIpRangesResponse.data); + pagesIteration++; + } else { + reachedEndOfPages = true; + } + } +}); + +Then('the IP Ranges list contains {string} IP Ranges', (expectedAnswer: string) => { + const expectedIpRangesCount = parseInt(expectedAnswer, 10); + assert.equal(ipRangesList.length, expectedIpRangesCount); +}); + +Then('the IP Ranges 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 update an IP Range', async () => { + ipRange = await accessControlListsApi.updateIpRange({ + accessControlListId: '01W4FFL35P4NC4K35SIPACL003', + ipRangeId: '01W4FFL35P4NC4K35IPRANGE06', + updateIpRangeRequestBody: { + description: 'West wing - updated', + ipAddress: '10.0.1.2', + range: 16, + }, + }); +}); + +Then('the response contains the IP Range details with updated data', () => { + assert.equal(ipRange.id, '01W4FFL35P4NC4K35IPRANGE06'); + assert.equal(ipRange.description, 'West wing - updated'); + assert.equal(ipRange.ipAddress, '10.0.1.2'); + assert.equal(ipRange.range, 16); + assert.deepEqual(ipRange.createTime, new Date('2024-06-06T15:56:26Z')); + assert.deepEqual(ipRange.updateTime, new Date('2024-06-06T15:58:07.295895288Z')); +}); + +When('I send a request to delete an IP Range', async () => { + deleteIpRangeResponse = await accessControlListsApi.deleteIpRange({ + accessControlListId: '01W4FFL35P4NC4K35SIPACL003', + ipRangeId: '01W4FFL35P4NC4K35IPRANGE06', + }); +}); + +Then('the delete IP Range response contains no data', () => { + assert.deepEqual(deleteIpRangeResponse, {} ); +}); + +When('I send a request to add ACLs to a SIP trunk [using the ACL service]', async () => { + addedAclsList = await accessControlListsApi.addToTrunk({ + trunkId: '01W4FFL35P4NC4K35SIPTRUNK1', + addAccessControlListToTrunkRequestBody: { + accessControlListIds: [ + '01W4FFL35P4NC4K35TRUNKACL1', + ], + }, + }); +}); + +Then('the response contains the list of ACLs added to the trunk [using the ACL service]', () => { + assert.ok(addedAclsList.accessControlListIds); + assert.equal(addedAclsList.accessControlListIds.length, 1); +}); + +When('I send a request to list the existing ACLs for a SIP Trunk [using the ACL service]', async () => { + listAclIdsResponse = await accessControlListsApi.listForTrunk({ + trunkId: '01W4FFL35P4NC4K35SIPTRUNK1', + }); +}); + +Then('the response [from the ACL service] contains {string} ACLs for a SIP Trunk', (expectedAnswer: string) => { + const expectedMessagesCount = parseInt(expectedAnswer, 10); + assert.equal(listAclIdsResponse.data.length, expectedMessagesCount); +}); + +When('I send a request to list all the ACLs for a SIP Trunk [using the ACL service]', async () => { + aclIdsList = []; + for await (const acl of accessControlListsApi.listForTrunk({ trunkId: '01W4FFL35P4NC4K35SIPTRUNK1' })) { + aclIdsList.push(acl); + } +}); + +When('I iterate manually over the ACLs for a SIP Trunk pages [using the ACL service]', async () => { + aclIdsList = []; + listAclIdsResponse = await accessControlListsApi.listForTrunk({ trunkId: '01W4FFL35P4NC4K35SIPTRUNK1' }); + aclIdsList.push(...listAclIdsResponse.data); + pagesIteration = 1; + let reachedEndOfPages = false; + while (!reachedEndOfPages) { + if (listAclIdsResponse.hasNextPage) { + listAclIdsResponse = await listAclIdsResponse.nextPage(); + aclIdsList.push(...listAclIdsResponse.data); + pagesIteration++; + } else { + reachedEndOfPages = true; + } + } +}); + +Then('the ACLs list [from the ACL service] contains {string} ACLs for a SIP Trunk', (expectedAnswer: string) => { + const expectedAclIdsListCount = parseInt(expectedAnswer, 10); + assert.equal(aclIdsList.length, expectedAclIdsListCount); +}); + +// eslint-disable-next-line max-len +Then('the ACLs for a SIP Trunk iteration result [using the ACL service] contains the data from {string} pages', (expectedAnswer: string) => { + const expectedPagesCount = parseInt(expectedAnswer, 10); + assert.equal(pagesIteration, expectedPagesCount); +}); + +When('I send a request to delete an ACL from a SIP trunk [using the ACL service]', async () => { + deleteAclFromTrunkResponse = await accessControlListsApi.deleteFromTrunk({ + trunkId: '01W4FFL35P4NC4K35SIPTRUNK1', + accessControlListId: '01W4FFL35P4NC4K35TRUNKACL1', + }); +}); + +Then('the delete ACL from a SIP trunk response [using the ACL service] contains no data', () => { + assert.deepEqual(deleteAclFromTrunkResponse, {} ); +}); diff --git a/packages/sdk-client/src/client/api-client-helpers.ts b/packages/sdk-client/src/client/api-client-helpers.ts index 84a7acf1..ee39b98c 100644 --- a/packages/sdk-client/src/client/api-client-helpers.ts +++ b/packages/sdk-client/src/client/api-client-helpers.ts @@ -84,7 +84,7 @@ export const reviveDates = (input: any): any => { return newObj; } else if (isDateString(input)) { // Convert string date to Date object - return new Date(input); + return new Date(addTimezoneIfMissing(input)); } else { // Return other types as-is return input; @@ -98,3 +98,18 @@ const isDateString = (value: any): boolean => { } return false; }; + +const addTimezoneIfMissing = (timestampValue: string): string => { + // Check the formats +XX:XX, +XX and Z + const timeZoneRegex = /([+-]\d{2}(:\d{2})|Z)$/; + if (!timeZoneRegex.test(timestampValue)) { + const hourMinutesTimezoneRegex = /([+-]\d{2})$/; + // A timestamp with no minutes in the timezone cannot be converted into a Date => assume it's :00 + if (hourMinutesTimezoneRegex.test(timestampValue)) { + timestampValue = timestampValue + ':00'; + } else { + timestampValue = timestampValue + 'Z'; + } + } + return timestampValue; +}; diff --git a/packages/sdk-client/src/client/api-fetch-client.ts b/packages/sdk-client/src/client/api-fetch-client.ts index ef14af0f..d6f38bb1 100644 --- a/packages/sdk-client/src/client/api-fetch-client.ts +++ b/packages/sdk-client/src/client/api-fetch-client.ts @@ -1,7 +1,6 @@ import { ResponsePlugin } from '../plugins/core/response-plugin'; import { VersionRequest } from '../plugins/version'; import { ExceptionResponse } from '../plugins/exception'; -import { TimezoneResponse } from '../plugins/timezone'; import { ApiClient, ApiCallParameters, @@ -42,7 +41,6 @@ export class ApiFetchClient extends ApiClient { ...options, requestPlugins: [new VersionRequest(), ...(options.requestPlugins || [])], responsePlugins: [ - new TimezoneResponse(), new ExceptionResponse(), ...(options.responsePlugins || []), ], diff --git a/packages/sdk-client/src/plugins/index.ts b/packages/sdk-client/src/plugins/index.ts index caf2ec7d..c22f2a58 100644 --- a/packages/sdk-client/src/plugins/index.ts +++ b/packages/sdk-client/src/plugins/index.ts @@ -5,6 +5,5 @@ export * from './basicAuthentication'; export * from './exception'; export * from './oauth2'; export * from './signing'; -export * from './timezone'; export * from './version'; export * from './x-timestamp'; diff --git a/packages/sdk-client/src/plugins/timezone/index.ts b/packages/sdk-client/src/plugins/timezone/index.ts deleted file mode 100644 index e4b4b05e..00000000 --- a/packages/sdk-client/src/plugins/timezone/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './timezone.response'; diff --git a/packages/sdk-client/src/plugins/timezone/timezone.response.ts b/packages/sdk-client/src/plugins/timezone/timezone.response.ts deleted file mode 100644 index cdf9873b..00000000 --- a/packages/sdk-client/src/plugins/timezone/timezone.response.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { ResponsePlugin, ResponsePluginContext } from '../core/response-plugin'; -import { PluginRunner } from '../core'; - -const buggyOperationIds: string[] = [ - 'GetCallResult', - 'VerificationStatusById', - 'VerificationStatusByIdentity', - 'VerificationStatusByReference', - 'CreateSipTrunk', - 'UpdateSipTrunk', - 'GetSipTrunkById', - 'GetSipTrunks', - 'CreateSipEndpoint', - 'UpdateSipEndpoint', - 'GetSipEndpointById', - 'GetSipEndpoints', - -]; - -const buggyFields: Record = { - 'GetCallResult': ['timestamp'], - 'VerificationStatusById': ['verificationTimestamp'], - 'VerificationStatusByIdentity': ['verificationTimestamp'], - 'VerificationStatusByReference': ['verificationTimestamp'], - 'CreateSipTrunk': ['createTime'], - 'UpdateSipTrunk': ['createTime', 'updateTime'], - 'GetSipTrunkById': ['createTime', 'updateTime'], - 'GetSipTrunks': ['createTime', 'updateTime'], - 'CreateSipEndpoint': ['createTime'], - 'UpdateSipEndpoint': ['createTime', 'updateTime'], - 'GetSipEndpointById': ['createTime', 'updateTime'], - 'GetSipEndpoints': ['createTime', 'updateTime'], -}; - -export class TimezoneResponse | undefined = Record> -implements ResponsePlugin> { - - public load( - context: ResponsePluginContext, - ): PluginRunner, V> { - return { - transform(res: V) { - // HACK to fix a server-side bug: the timestamp is returned without the timezone - if (res && buggyOperationIds.includes(context.operationId) ) { - for (const key in res) { - if (Object.prototype.hasOwnProperty.call(res, key)) { - const buggyKeys = buggyFields[context.operationId]; - for (const buggyKey of buggyKeys) { - if (key === buggyKey && typeof res[buggyKey] === 'string') { - let timestampValue = res[key] as string; - // Check the formats +XX:XX, +XX and Z - const timeZoneRegex = /([+-]\d{2}(:\d{2})|Z)$/; - if (!timeZoneRegex.test(timestampValue)) { - const hourMinutesTimezoneRegex = /([+-]\d{2})$/; - // A timestamp with no minutes in the timezone cannot be converted into a Date => assume it's :00 - if (hourMinutesTimezoneRegex.test(timestampValue)) { - timestampValue = timestampValue + ':00'; - } else { - timestampValue = timestampValue + 'Z'; - } - } - res[key] = timestampValue; - } - } - } - } - } - return res; - }, - }; - } - -} diff --git a/packages/sdk-client/tests/plugins/timezone/timezone.response.test.ts b/packages/sdk-client/tests/plugins/timezone/timezone.response.test.ts deleted file mode 100644 index 661ed0e9..00000000 --- a/packages/sdk-client/tests/plugins/timezone/timezone.response.test.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { TimezoneResponse } from '../../../src'; -import { ResponsePluginContext } from '../../../src/plugins/core/response-plugin'; -import { Headers } from 'node-fetch'; - -describe('Timezone response plugin', () => { - - let context: ResponsePluginContext; - const TIMESTAMP_WITH_MISSING_TIMEZONE = '2024-01-09T15:50:24.000'; - const TIMESTAMP_WITH_TIMEZONE = '2024-01-09T15:50:24.000Z'; - const TIMESTAMP_WITH_TIMEZONE_HOURS = '2024-01-09T15:50:24.000+00'; - const TIMESTAMP_WITH_TIMEZONE_HOURS_MINUTES = '2024-01-09T15:50:24.000+00:00'; - - beforeEach(() => { - context = { - operationId: 'GetCallResult', - apiName: '', - url: '', - requestOptions: { - headers: new Headers(), - hostname: '', - }, - }; - }); - - it('should update the timestamp if the timezone is missing', async () => { - const apiResponse = { - timestamp: TIMESTAMP_WITH_MISSING_TIMEZONE, - }; - const plugin = new TimezoneResponse(); - const runner = plugin.load(context); - const result = await runner.transform(apiResponse); - - expect(result.timestamp).toBe(TIMESTAMP_WITH_TIMEZONE); - }); - - it('should NOT update the timestamp if the timezone is already there with "Z" format', async () => { - const apiResponse = { - timestamp: TIMESTAMP_WITH_TIMEZONE, - }; - const plugin = new TimezoneResponse(); - const runner = plugin.load(context); - const result = await runner.transform(apiResponse); - - expect(result.timestamp).toBe(TIMESTAMP_WITH_TIMEZONE); - }); - - it('should update the timestamp if the timezone is already there but with "+XX format"', async () => { - const apiResponse = { - timestamp: TIMESTAMP_WITH_TIMEZONE_HOURS, - }; - const plugin = new TimezoneResponse(); - const runner = plugin.load(context); - const result = await runner.transform(apiResponse); - - expect(result.timestamp).toBe(TIMESTAMP_WITH_TIMEZONE_HOURS_MINUTES); - }); - - it('should NOT update the timestamp if the timezone is already there with "+XX:XX format"', async () => { - const apiResponse = { - timestamp: TIMESTAMP_WITH_TIMEZONE_HOURS_MINUTES, - }; - const plugin = new TimezoneResponse(); - const runner = plugin.load(context); - const result = await runner.transform(apiResponse); - - expect(result.timestamp).toBe(TIMESTAMP_WITH_TIMEZONE_HOURS_MINUTES); - }); - - it('should NOT update the timestamp if the operationId if not listed', async () => { - context.operationId = 'notListedAsBuggy'; - const apiResponse = { - timestamp: TIMESTAMP_WITH_MISSING_TIMEZONE, - }; - const plugin = new TimezoneResponse(); - const runner = plugin.load(context); - const result = await runner.transform(apiResponse); - - expect(result.timestamp).toBe(TIMESTAMP_WITH_MISSING_TIMEZONE); - }); -}); From f3c166026d825a6ed0bf133fe78b0bd481993c88 Mon Sep 17 00:00:00 2001 From: Antoine SEIN <142824551+asein-sinch@users.noreply.github.com> Date: Fri, 2 Aug 2024 16:59:54 +0200 Subject: [PATCH 29/54] DEVEXP-506: E2E ElasticSipTrunking/CallsHistory (#122) --- .github/workflows/run-ci.yaml | 4 + .../calls-history/find.ts | 2 +- .../tests/rest/v1/events/events.steps.ts | 2 +- .../src/models/v1/enum.ts | 2 +- .../src/models/v1/money/money.ts | 2 +- .../calls-history-request-data.ts | 22 +++- .../v1/calls-history/calls-history-api.ts | 8 +- .../calls-history/calls-history-api.test.ts | 6 +- .../v1/calls-history/calls-history.steps.ts | 95 ++++++++++++++++ packages/fax/src/rest/v3/faxes/faxes-api.ts | 37 +------ .../fax/tests/rest/v3/faxes/faxes-api.test.ts | 78 +------------ .../src/client/api-client-helpers.ts | 4 +- packages/sdk-client/src/utils/date.ts | 29 +++++ packages/sdk-client/tests/utils/date.test.ts | 103 ++++++++++++++++++ 14 files changed, 273 insertions(+), 121 deletions(-) create mode 100644 packages/elastic-sip-trunking/tests/rest/v1/calls-history/calls-history.steps.ts create mode 100644 packages/sdk-client/tests/utils/date.test.ts diff --git a/.github/workflows/run-ci.yaml b/.github/workflows/run-ci.yaml index d6877f13..ab1da6a7 100644 --- a/.github/workflows/run-ci.yaml +++ b/.github/workflows/run-ci.yaml @@ -36,6 +36,10 @@ jobs: run: | cd sinch-sdk-mockserver docker build -t sinch-sdk-mockserver -f Dockerfile . + - name: Install Docker Compose + run: | + sudo apt-get update + sudo apt-get install -y docker-compose - name: Start mock servers with Docker Compose run: | cd sinch-sdk-mockserver diff --git a/examples/simple-examples/src/elastic-sip-trunking/calls-history/find.ts b/examples/simple-examples/src/elastic-sip-trunking/calls-history/find.ts index 10dcddfe..18e21eef 100644 --- a/examples/simple-examples/src/elastic-sip-trunking/calls-history/find.ts +++ b/examples/simple-examples/src/elastic-sip-trunking/calls-history/find.ts @@ -27,7 +27,7 @@ const populateCallsList = ( const requestData: ElasticSipTrunking.FindCallsRequestData = { trunkId, callResult: 'COMPLETED', - direction: 'INBOUND', + direction: 'inbound', }; const elasticSipTrunkingService = initElasticSipTrunkingService(); diff --git a/packages/conversation/tests/rest/v1/events/events.steps.ts b/packages/conversation/tests/rest/v1/events/events.steps.ts index 16693d77..5dc7f883 100644 --- a/packages/conversation/tests/rest/v1/events/events.steps.ts +++ b/packages/conversation/tests/rest/v1/events/events.steps.ts @@ -108,7 +108,7 @@ Then('the response contains the conversation event details', () => { }; assert.deepEqual(event.channel_identity, channelIdentity); const composingEvent: Conversation.ComposingEvent = { - composing_event: {} + composing_event: {}, }; assert.deepEqual(event.app_event, composingEvent); }); diff --git a/packages/elastic-sip-trunking/src/models/v1/enum.ts b/packages/elastic-sip-trunking/src/models/v1/enum.ts index 312f600f..e9f9a547 100644 --- a/packages/elastic-sip-trunking/src/models/v1/enum.ts +++ b/packages/elastic-sip-trunking/src/models/v1/enum.ts @@ -1,3 +1,3 @@ -export type DirectionEnum = 'INBOUND' | 'OUTBOUND'; +export type DirectionEnum = 'inbound' | 'outbound'; export type CallResult = 'COMPLETED' | 'NO_ANSWER' | 'CANCEL' | 'BUSY' | 'FAILED'; diff --git a/packages/elastic-sip-trunking/src/models/v1/money/money.ts b/packages/elastic-sip-trunking/src/models/v1/money/money.ts index bc45fbfb..6b555751 100644 --- a/packages/elastic-sip-trunking/src/models/v1/money/money.ts +++ b/packages/elastic-sip-trunking/src/models/v1/money/money.ts @@ -5,5 +5,5 @@ export interface Money { /** The 3-letter currency code defined in ISO 4217. */ currencyCode?: string; /** The amount with 4 decimals and decimal delimiter `.`. */ - amount?: string; + amount?: number; } diff --git a/packages/elastic-sip-trunking/src/models/v1/requests/calls-history/calls-history-request-data.ts b/packages/elastic-sip-trunking/src/models/v1/requests/calls-history/calls-history-request-data.ts index f22dc84a..c0faad56 100644 --- a/packages/elastic-sip-trunking/src/models/v1/requests/calls-history/calls-history-request-data.ts +++ b/packages/elastic-sip-trunking/src/models/v1/requests/calls-history/calls-history-request-data.ts @@ -1,4 +1,5 @@ import { CallResult, DirectionEnum } from '../../enum'; +import { DateFormat } from '@sinch/sdk-client'; export interface FindCallsRequestData { /** A phone number that you want to use to filter results. You can pass a partial number to get all calls sent to numbers that start with the number you passed. */ @@ -7,8 +8,10 @@ export interface FindCallsRequestData { 'to'?: string; /** Only include calls made from this trunk. */ 'trunkId'?: string; - /** Filter calls based on `createTime`. You make the query more precise, fewer results will be returned. For example, 2021-02-01 will return all calls from the first of February 2021, and 2021-02-01T14:00:00Z will return all calls after 14:00 on the first of February. This field also supports <= and >= to search for calls in a range ?createTime>=2021-10-01&createTime<=2021-10-30 to get a list if calls for october 2021 It is also possible to submit partial dates for example createTime=2021-02 will return all calls for February ***Defaults to 24 hours*** ***Internal notes*** If a customer submits = and not <> we should add min and max for the date range psueodo sql ``` createTime = 2021-02-01 select * from calls where createTime >= 2021-02-01 and createTime <= 2021-02-01T23:59:59Z createTime = 2021-02-01T08 select * from calls where createTime >= 2021-02-01T08:00:00 and createTime <= 2021-02-01T08:59:59Z ``` but if they submit < or > we should just use the value they submitted and parse it a complete date */ + /** Filter calls based on `createTime`. You make the query more precise, fewer results will be returned. For example, 2021-02-01 will return all calls from the first of February 2021, and 2021-02-01T14:00:00Z will return all calls after 14:00 on the first of February. */ 'createTime'?: string; + /** Filter calls based on `createTime`. It will filter the calls on a range of dates. */ + 'createTimeRange'?: DateRangeFilter; /** only include calls by on the callResult(s), example callResult=COMPLETED will return all calls which have completed normally. */ 'callResult'?: CallResult; /** only include calls by on the direction(s), example direction=INBOUND,OUTBOUND will return all calls that are inbound or outbound. */ @@ -18,3 +21,20 @@ export interface FindCallsRequestData { /** The maximum number of items to return per request. The default is 100 and the maximum is 1000. If you need to export larger amounts and pagination is not suitable for you can use the Export function in the dashboard. */ 'pageSize'?: number; } + +/** + * Filter calls based on `createTime`. If not value is submitted, the default value is the prior week. + * - `from: '2024-02-15'` will return all calls from February 2024, 15th + * - `from: '2024-02-01T14:00:00Z'` will return all calls after 14:00:00 on the first of February 2024. + * - `from: '2024-02-01T14:00:00Z'` + `to: '2024-02-01T15:00:00Z'` will return all calls between 14:00:00 and 15:00:00 (inclusive) on the first of February 2024. + * - `from: '2024-02-01'` + `to: '2024-02-29'` will return all calls for all of February 2024. + * + * Note: It is also possible to submit partial dates. + * - `from: '2024-02'` will return all calls for February 2024 + */ +export interface DateRangeFilter { + /** */ + from?: string | Date | DateFormat; + /** */ + to?: string | Date | DateFormat; +} diff --git a/packages/elastic-sip-trunking/src/rest/v1/calls-history/calls-history-api.ts b/packages/elastic-sip-trunking/src/rest/v1/calls-history/calls-history-api.ts index 6c42951b..81a58167 100644 --- a/packages/elastic-sip-trunking/src/rest/v1/calls-history/calls-history-api.ts +++ b/packages/elastic-sip-trunking/src/rest/v1/calls-history/calls-history-api.ts @@ -7,6 +7,8 @@ import { PaginationEnum, buildPageResultPromise, createIteratorMethodsForPagination, + formatCreateTimeFilter, + formatCreateTimeRangeFilter, } from '@sinch/sdk-client'; import { ElasticSipTrunkingDomainApi } from '../elastic-sip-trunking-domain-api'; @@ -22,16 +24,18 @@ export class CallsHistoryApi extends ElasticSipTrunkingDomainApi { } /** - * Find calls * Find calls by query parameters. * @param { FindCallsRequestData } data - The data to provide to the API call. * @return { ApiListPromise } */ public find(data: FindCallsRequestData): ApiListPromise { this.client = this.getSinchClient(); - data['createTime'] = data['createTime'] !== undefined ? data['createTime'] : 'now-24h'; const getParams = this.client.extractQueryParams(data, [ 'from', 'to', 'trunkId', 'createTime', 'callResult', 'direction', 'page', 'pageSize']); + (getParams as any).createTime = JSON.stringify(formatCreateTimeFilter(data.createTime)); + (getParams as any)['createTime>'] = JSON.stringify(formatCreateTimeRangeFilter(data.createTimeRange?.from)); + (getParams as any)['createTime<'] = JSON.stringify(formatCreateTimeRangeFilter(data.createTimeRange?.to)); + const headers: { [key: string]: string | undefined } = { 'Content-Type': 'application/json', 'Accept': 'application/json', diff --git a/packages/elastic-sip-trunking/tests/rest/v1/calls-history/calls-history-api.test.ts b/packages/elastic-sip-trunking/tests/rest/v1/calls-history/calls-history-api.test.ts index 5801b35f..1a839b51 100644 --- a/packages/elastic-sip-trunking/tests/rest/v1/calls-history/calls-history-api.test.ts +++ b/packages/elastic-sip-trunking/tests/rest/v1/calls-history/calls-history-api.test.ts @@ -28,19 +28,19 @@ describe('CallsApi', () => { callId: '01AQ3D80ZKSSK35TZFKM3JG9CT', to: '+15551239898', from: '+14155553434', - direction: 'INBOUND', + direction: 'inbound', answerTime: new Date('2021-11-01T23:26:50Z'), endTime: new Date('2021-11-01T23:27:35Z'), durationSeconds: 45, callResult: 'COMPLETED', pricePerMinute: { currencyCode: 'USD', - amount: '0.0040', + amount: 0.0040, }, billingDurationSeconds: 60, price: { currencyCode: 'USD', - amount: '0.0040', + amount: 0.0040, }, createTime: new Date('2021-11-01T23:20:50Z'), projectId: '1bf62742-7b84-4666-9cbe-8e5734fd57d0', diff --git a/packages/elastic-sip-trunking/tests/rest/v1/calls-history/calls-history.steps.ts b/packages/elastic-sip-trunking/tests/rest/v1/calls-history/calls-history.steps.ts new file mode 100644 index 00000000..8704b398 --- /dev/null +++ b/packages/elastic-sip-trunking/tests/rest/v1/calls-history/calls-history.steps.ts @@ -0,0 +1,95 @@ +import { ElasticSipTrunking, ElasticSipTrunkingService, CallsHistoryApi } from '../../../../src'; +import { Given, Then, When } from '@cucumber/cucumber'; +import assert from 'assert'; +import { PageResult } from '@sinch/sdk-client'; + +let callsHistoryApi: CallsHistoryApi; +let listResponse: PageResult; +let callsHistoryList: ElasticSipTrunking.Call[]; +let pagesIteration: number; + +Given('the Elastic SIP Trunking service "Calls History" is available', function () { + const elasticSipTrunkingService = new ElasticSipTrunkingService({ + projectId: 'tinyfrog-jump-high-over-lilypadbasin', + keyId: 'keyId', + keySecret: 'keySecret', + authHostname: 'http://localhost:3011', + elasticSipTrunkingHostname: 'http://localhost:3016', + }); + callsHistoryApi = elasticSipTrunkingService.calls; +}); + +When('I send a request to find the a page from the Calls History with no filtering parameters', async () => { + listResponse = await callsHistoryApi.find({}); +}); + +Then('the response contains {string} Calls from history', (expectedAnswer: string) => { + const expectedCallsCount = parseInt(expectedAnswer, 10); + assert.equal(listResponse.data.length, expectedCallsCount); +}); + +Then('a Call History object from the page result contains the Call History details', () => { + const call = listResponse.data[0]; + assert.equal(call.callId, 'N00DL3C4T5'); + assert.equal(call.to, '12017777777'); + assert.equal(call.from, 'sip:12015555555@76.184.202.212'); + const direction: ElasticSipTrunking.DirectionEnum = 'outbound'; + assert.equal(call.direction, direction); + assert.deepEqual(call.answerTime, new Date('2024-06-06T16:57:52Z')); + assert.deepEqual(call.endTime, new Date('2024-06-06T16:57:55Z')); + assert.equal(call.durationSeconds, 4); + assert.equal(call.billingDurationSeconds, 60); + const callResult: ElasticSipTrunking.CallResult = 'COMPLETED'; + assert.equal(call.callResult, callResult); + const price: ElasticSipTrunking.Money = { + amount: 0.004, + currencyCode: 'USD', + }; + assert.deepEqual(call.pricePerMinute, price); + assert.deepEqual(call.price, price); + assert.deepEqual(call.createTime, new Date('2024-06-06T16:57:45+0000')); + assert.equal(call.projectId, 'tinyfrog-jump-high-over-lilypadbasin'); + assert.equal(call.trunkId, '01W4FFL35P4NC4K35SIPTRUNK2'); +}); + +When('I send a request to list all the Calls from the Calls History', async () => { + callsHistoryList = []; + for await (const call of callsHistoryApi.find({})) { + callsHistoryList.push(call); + } +}); + +When('I iterate manually over the Calls History pages', async () => { + callsHistoryList = []; + listResponse = await callsHistoryApi.find({}); + callsHistoryList.push(...listResponse.data); + pagesIteration = 1; + let reachedEndOfPages = false; + while (!reachedEndOfPages) { + if (listResponse.hasNextPage) { + listResponse = await listResponse.nextPage(); + callsHistoryList.push(...listResponse.data); + pagesIteration++; + } else { + reachedEndOfPages = true; + } + } +}); + +Then('the Calls History list contains {string} Calls', (expectedAnswer: string) => { + const expectedSipEndpointsCount = parseInt(expectedAnswer, 10); + assert.equal(callsHistoryList.length, expectedSipEndpointsCount); +}); + +Then('the Calls History 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 find the a page from the Calls History with a createTime range filter', async () => { + listResponse = await callsHistoryApi.find({ + createTimeRange: { + from: '2024-06-06T16:00:00', + }, + }); +}); diff --git a/packages/fax/src/rest/v3/faxes/faxes-api.ts b/packages/fax/src/rest/v3/faxes/faxes-api.ts index 26b77b9a..9704fdc8 100644 --- a/packages/fax/src/rest/v3/faxes/faxes-api.ts +++ b/packages/fax/src/rest/v3/faxes/faxes-api.ts @@ -2,9 +2,9 @@ import { ApiListPromise, buildPageResultPromise, createIteratorMethodsForPagination, - DateFormat, FileBuffer, - formatDate, + formatCreateTimeFilter, + formatCreateTimeRangeFilter, PaginatedApiProperties, PaginationEnum, RequestBody, @@ -137,9 +137,9 @@ export class FaxesApi extends FaxDomainApi { 'from', 'pageSize', 'page']); - (getParams as any).createTime = JSON.stringify(this.formatCreateTimeFilter(data.createTime)); - (getParams as any)['createTime>'] = JSON.stringify(this.formatCreateTimeRangeFilter(data.createTimeRange?.from)); - (getParams as any)['createTime<'] = JSON.stringify(this.formatCreateTimeRangeFilter(data.createTimeRange?.to)); + (getParams as any).createTime = JSON.stringify(formatCreateTimeFilter(data.createTime)); + (getParams as any)['createTime>'] = JSON.stringify(formatCreateTimeRangeFilter(data.createTimeRange?.from)); + (getParams as any)['createTime<'] = JSON.stringify(formatCreateTimeRangeFilter(data.createTimeRange?.to)); const headers: { [key: string]: string | undefined } = { 'Content-Type': 'application/json', @@ -174,33 +174,6 @@ export class FaxesApi extends FaxDomainApi { return listPromise as ApiListPromise; } - formatCreateTimeFilter = (createTime: string | Date | undefined): string | undefined => { - if (createTime !== undefined) { - if (typeof createTime === 'string') { - if (createTime.indexOf('T') > -1) { - return createTime.substring(0, createTime.indexOf('T')); - } - return createTime; - } else { - return formatDate(createTime, 'day'); - } - } - return undefined; - }; - - formatCreateTimeRangeFilter = (timeBoundary: string | Date | DateFormat | undefined): string | undefined => { - if (timeBoundary !== undefined) { - if (typeof timeBoundary === 'string') { - return timeBoundary; - } else if (timeBoundary instanceof Date) { - return formatDate(timeBoundary); - } else { - return formatDate(timeBoundary.date, timeBoundary.unit); - } - } - return undefined; - }; - /** * Send a fax or multiple faxes * Create and send one or multiple faxes. Fax content may be supplied via one or more files or URLs of supported filetypes. diff --git a/packages/fax/tests/rest/v3/faxes/faxes-api.test.ts b/packages/fax/tests/rest/v3/faxes/faxes-api.test.ts index f5fc2ac5..8bc2944f 100644 --- a/packages/fax/tests/rest/v3/faxes/faxes-api.test.ts +++ b/packages/fax/tests/rest/v3/faxes/faxes-api.test.ts @@ -1,4 +1,4 @@ -import { DateFormat, FileBuffer, SinchClientParameters } from '@sinch/sdk-client'; +import { FileBuffer, SinchClientParameters } from '@sinch/sdk-client'; import { Fax, FaxesApi, @@ -151,82 +151,6 @@ describe('FaxesApi', () => { expect(response.data).toBeDefined(); expect(fixture.list).toHaveBeenCalledWith(requestData); }); - - it('should format a createTime parameter', () => { - const dateUndefined = undefined; - let formattedDateFilter = faxesApi.formatCreateTimeFilter(dateUndefined); - expect(formattedDateFilter).toBeUndefined(); - - const dateString = '2024-05-01'; - formattedDateFilter = faxesApi.formatCreateTimeFilter(dateString); - expect(formattedDateFilter).toBe('2024-05-01'); - - const dateWithSecondsString ='2024-05-01T13:00:00Z'; - formattedDateFilter = faxesApi.formatCreateTimeFilter(dateWithSecondsString); - expect(formattedDateFilter).toBe('2024-05-01'); - - const dateWithSeconds = new Date('2024-05-01T13:00:00Z'); - formattedDateFilter = faxesApi.formatCreateTimeFilter(dateWithSeconds); - expect(formattedDateFilter).toBe('2024-05-01'); - }); - - it('should format a datetime range filter', () => { - const dateTimeRangeUndefined = undefined; - let formattedDateTimeRangeFilter = faxesApi.formatCreateTimeRangeFilter(dateTimeRangeUndefined); - expect(formattedDateTimeRangeFilter).toBeUndefined(); - - const dateTimeRangeString = '2024-05-01'; - formattedDateTimeRangeFilter = faxesApi.formatCreateTimeRangeFilter(dateTimeRangeString); - expect(formattedDateTimeRangeFilter).toBe('2024-05-01'); - - const dateTimeRangeNoUnit: DateFormat = { - date: new Date('2024-05-01T13:15:30Z'), - }; - formattedDateTimeRangeFilter = faxesApi.formatCreateTimeRangeFilter(dateTimeRangeNoUnit); - expect(formattedDateTimeRangeFilter).toBe('2024-05-01T13:15:30Z'); - - const dateTimeRangeWithYear: DateFormat = { - date: new Date('2024-05-01T13:15:30Z'), - unit: 'year', - }; - formattedDateTimeRangeFilter = faxesApi.formatCreateTimeRangeFilter(dateTimeRangeWithYear); - expect(formattedDateTimeRangeFilter).toBe('2024'); - - const dateTimeRangeWithMonth: DateFormat = { - date: new Date('2024-05-01T13:15:30Z'), - unit: 'month', - }; - formattedDateTimeRangeFilter = faxesApi.formatCreateTimeRangeFilter(dateTimeRangeWithMonth); - expect(formattedDateTimeRangeFilter).toBe('2024-05'); - - const dateTimeRangeWithDay: DateFormat = { - date: new Date('2024-05-01T13:15:30Z'), - unit: 'day', - }; - formattedDateTimeRangeFilter = faxesApi.formatCreateTimeRangeFilter(dateTimeRangeWithDay); - expect(formattedDateTimeRangeFilter).toBe('2024-05-01'); - - const dateTimeRangeWithHours: DateFormat = { - date: new Date('2024-05-01T13:15:30Z'), - unit: 'hour', - }; - formattedDateTimeRangeFilter = faxesApi.formatCreateTimeRangeFilter(dateTimeRangeWithHours); - expect(formattedDateTimeRangeFilter).toBe('2024-05-01T13:00:00Z'); - - const dateTimeRangeWithMinutes: DateFormat = { - date: new Date('2024-05-01T13:15:30Z'), - unit: 'minute', - }; - formattedDateTimeRangeFilter = faxesApi.formatCreateTimeRangeFilter(dateTimeRangeWithMinutes); - expect(formattedDateTimeRangeFilter).toBe('2024-05-01T13:15:00Z'); - - const dateTimeRangeWithSeconds: DateFormat = { - date: new Date('2024-05-01T13:15:30Z'), - unit: 'second', - }; - formattedDateTimeRangeFilter = faxesApi.formatCreateTimeRangeFilter(dateTimeRangeWithSeconds); - expect(formattedDateTimeRangeFilter).toBe('2024-05-01T13:15:30Z'); - }); }); describe ('sendFax', () => { diff --git a/packages/sdk-client/src/client/api-client-helpers.ts b/packages/sdk-client/src/client/api-client-helpers.ts index ee39b98c..4807191a 100644 --- a/packages/sdk-client/src/client/api-client-helpers.ts +++ b/packages/sdk-client/src/client/api-client-helpers.ts @@ -100,8 +100,8 @@ const isDateString = (value: any): boolean => { }; const addTimezoneIfMissing = (timestampValue: string): string => { - // Check the formats +XX:XX, +XX and Z - const timeZoneRegex = /([+-]\d{2}(:\d{2})|Z)$/; + // Check the formats +XX:XX, +XX, +XXXX and Z + const timeZoneRegex = /([+-]\d{2}(:\d{2})|[+-]\d{4}|Z)$/; if (!timeZoneRegex.test(timestampValue)) { const hourMinutesTimezoneRegex = /([+-]\d{2})$/; // A timestamp with no minutes in the timezone cannot be converted into a Date => assume it's :00 diff --git a/packages/sdk-client/src/utils/date.ts b/packages/sdk-client/src/utils/date.ts index c0d9ef0d..892bb65c 100644 --- a/packages/sdk-client/src/utils/date.ts +++ b/packages/sdk-client/src/utils/date.ts @@ -29,3 +29,32 @@ export const formatDate = (date: Date, unit?: ChronoUnit): string => { return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}Z`; } }; + +export const formatCreateTimeFilter = (createTime: string | Date | undefined): string | undefined => { + if (createTime !== undefined) { + if (typeof createTime === 'string') { + if (createTime.indexOf('T') > -1) { + return createTime.substring(0, createTime.indexOf('T')); + } + return createTime; + } else { + return formatDate(createTime, 'day'); + } + } + return undefined; +}; + +export const formatCreateTimeRangeFilter = ( + timeBoundary: string | Date | DateFormat | undefined, +): string | undefined => { + if (timeBoundary !== undefined) { + if (typeof timeBoundary === 'string') { + return timeBoundary; + } else if (timeBoundary instanceof Date) { + return formatDate(timeBoundary); + } else { + return formatDate(timeBoundary.date, timeBoundary.unit); + } + } + return undefined; +}; diff --git a/packages/sdk-client/tests/utils/date.test.ts b/packages/sdk-client/tests/utils/date.test.ts new file mode 100644 index 00000000..a3e2440c --- /dev/null +++ b/packages/sdk-client/tests/utils/date.test.ts @@ -0,0 +1,103 @@ +import { + DateFormat, + formatDate, + formatCreateTimeFilter, + formatCreateTimeRangeFilter, +} from '../../src'; + +it('should format a date with its required chrono unit', () => { + const date = new Date('2024-05-01T13:20:50Z'); + let formattedDate = formatDate(date, 'year'); + expect(formattedDate).toBe('2024'); + + formattedDate = formatDate(date, 'month'); + expect(formattedDate).toBe('2024-05'); + + formattedDate = formatDate(date, 'day'); + expect(formattedDate).toBe('2024-05-01'); + + formattedDate = formatDate(date, 'hour'); + expect(formattedDate).toBe('2024-05-01T13:00:00Z'); + + formattedDate = formatDate(date, 'minute'); + expect(formattedDate).toBe('2024-05-01T13:20:00Z'); + + formattedDate = formatDate(date, 'second'); + expect(formattedDate).toBe('2024-05-01T13:20:50Z'); +}); + +it('should format a dateTime filter parameter', () => { + const dateUndefined = undefined; + let formattedDateFilter = formatCreateTimeFilter(dateUndefined); + expect(formattedDateFilter).toBeUndefined(); + + const dateString = '2024-05-01'; + formattedDateFilter = formatCreateTimeFilter(dateString); + expect(formattedDateFilter).toBe('2024-05-01'); + + const dateWithSecondsString ='2024-05-01T13:00:00Z'; + formattedDateFilter = formatCreateTimeFilter(dateWithSecondsString); + expect(formattedDateFilter).toBe('2024-05-01'); + + const dateWithSeconds = new Date('2024-05-01T13:00:00Z'); + formattedDateFilter = formatCreateTimeFilter(dateWithSeconds); + expect(formattedDateFilter).toBe('2024-05-01'); +}); + +it('should format a datetime range filter parameter', () => { + const dateTimeRangeUndefined = undefined; + let formattedDateTimeRangeFilter = formatCreateTimeRangeFilter(dateTimeRangeUndefined); + expect(formattedDateTimeRangeFilter).toBeUndefined(); + + const dateTimeRangeString = '2024-05-01'; + formattedDateTimeRangeFilter = formatCreateTimeRangeFilter(dateTimeRangeString); + expect(formattedDateTimeRangeFilter).toBe('2024-05-01'); + + const dateTimeRangeNoUnit: DateFormat = { + date: new Date('2024-05-01T13:15:30Z'), + }; + formattedDateTimeRangeFilter = formatCreateTimeRangeFilter(dateTimeRangeNoUnit); + expect(formattedDateTimeRangeFilter).toBe('2024-05-01T13:15:30Z'); + + const dateTimeRangeWithYear: DateFormat = { + date: new Date('2024-05-01T13:15:30Z'), + unit: 'year', + }; + formattedDateTimeRangeFilter = formatCreateTimeRangeFilter(dateTimeRangeWithYear); + expect(formattedDateTimeRangeFilter).toBe('2024'); + + const dateTimeRangeWithMonth: DateFormat = { + date: new Date('2024-05-01T13:15:30Z'), + unit: 'month', + }; + formattedDateTimeRangeFilter = formatCreateTimeRangeFilter(dateTimeRangeWithMonth); + expect(formattedDateTimeRangeFilter).toBe('2024-05'); + + const dateTimeRangeWithDay: DateFormat = { + date: new Date('2024-05-01T13:15:30Z'), + unit: 'day', + }; + formattedDateTimeRangeFilter = formatCreateTimeRangeFilter(dateTimeRangeWithDay); + expect(formattedDateTimeRangeFilter).toBe('2024-05-01'); + + const dateTimeRangeWithHours: DateFormat = { + date: new Date('2024-05-01T13:15:30Z'), + unit: 'hour', + }; + formattedDateTimeRangeFilter = formatCreateTimeRangeFilter(dateTimeRangeWithHours); + expect(formattedDateTimeRangeFilter).toBe('2024-05-01T13:00:00Z'); + + const dateTimeRangeWithMinutes: DateFormat = { + date: new Date('2024-05-01T13:15:30Z'), + unit: 'minute', + }; + formattedDateTimeRangeFilter = formatCreateTimeRangeFilter(dateTimeRangeWithMinutes); + expect(formattedDateTimeRangeFilter).toBe('2024-05-01T13:15:00Z'); + + const dateTimeRangeWithSeconds: DateFormat = { + date: new Date('2024-05-01T13:15:30Z'), + unit: 'second', + }; + formattedDateTimeRangeFilter = formatCreateTimeRangeFilter(dateTimeRangeWithSeconds); + expect(formattedDateTimeRangeFilter).toBe('2024-05-01T13:15:30Z'); +}); From cf9789aaae727e2e87f54711ddeb1ddaefefa449 Mon Sep 17 00:00:00 2001 From: Antoine SEIN <142824551+asein-sinch@users.noreply.github.com> Date: Mon, 19 Aug 2024 14:10:00 +0200 Subject: [PATCH 30/54] DEVEXP-515: E2E SMS/Batches (#123) --- .../plugins/exception/exception.response.ts | 5 +- packages/sms/cucumber.js | 8 + packages/sms/package.json | 3 +- .../dry-run-response-per-recipient-inner.ts | 2 +- .../sms/src/rest/v1/batches/batches-api.ts | 2 - .../tests/rest/v1/batches/batches.steps.ts | 257 ++++++++++++++++++ 6 files changed, 272 insertions(+), 5 deletions(-) create mode 100644 packages/sms/cucumber.js create mode 100644 packages/sms/tests/rest/v1/batches/batches.steps.ts diff --git a/packages/sdk-client/src/plugins/exception/exception.response.ts b/packages/sdk-client/src/plugins/exception/exception.response.ts index f8836bd0..8f3db79a 100644 --- a/packages/sdk-client/src/plugins/exception/exception.response.ts +++ b/packages/sdk-client/src/plugins/exception/exception.response.ts @@ -48,7 +48,10 @@ export class ExceptionResponse< ); } else if (!res) { res = {} as V; - if (context.response.status !== 204 && context.response.status !== 200) { + if (context.response.status !== 204 + && context.response.status !== 200 + && context.response.status !== 202 + ) { res = {} as V; error = new EmptyResponseError( context.response.statusText, diff --git a/packages/sms/cucumber.js b/packages/sms/cucumber.js new file mode 100644 index 00000000..691a9809 --- /dev/null +++ b/packages/sms/cucumber.js @@ -0,0 +1,8 @@ +module.exports = { + default: [ + 'tests/e2e/features/**/*.feature', + '--require-module ts-node/register', + '--require tests/rest/v1/**/*.steps.ts', + `--format-options '{"snippetInterface": "synchronous"}'`, + ].join(' '), +}; diff --git a/packages/sms/package.json b/packages/sms/package.json index 3483fcd0..dea593ab 100644 --- a/packages/sms/package.json +++ b/packages/sms/package.json @@ -25,7 +25,8 @@ "scripts": { "build": "yarn run clean && yarn run compile", "clean": "rimraf dist tsconfig.tsbuildinfo tsconfig.build.tsbuildinfo", - "compile": "tsc -p tsconfig.build.json && tsc -p tsconfig.tests.json && rimraf dist/tests tsconfig.build.tsbuildinfo" + "compile": "tsc -p tsconfig.build.json && tsc -p tsconfig.tests.json && rimraf dist/tests tsconfig.build.tsbuildinfo", + "test:e2e": "cucumber-js" }, "dependencies": { "@sinch/sdk-client": "^1.1.0" diff --git a/packages/sms/src/models/v1/dry-run-response-per-recipient-inner/dry-run-response-per-recipient-inner.ts b/packages/sms/src/models/v1/dry-run-response-per-recipient-inner/dry-run-response-per-recipient-inner.ts index 2094dba5..ea6b9e51 100644 --- a/packages/sms/src/models/v1/dry-run-response-per-recipient-inner/dry-run-response-per-recipient-inner.ts +++ b/packages/sms/src/models/v1/dry-run-response-per-recipient-inner/dry-run-response-per-recipient-inner.ts @@ -1,7 +1,7 @@ export interface DryRunResponsePerRecipientInner { recipient?: string; - message_part?: string; + number_of_parts?: number; body?: string; encoding?: string; } diff --git a/packages/sms/src/rest/v1/batches/batches-api.ts b/packages/sms/src/rest/v1/batches/batches-api.ts index 6e21f723..efc72af1 100644 --- a/packages/sms/src/rest/v1/batches/batches-api.ts +++ b/packages/sms/src/rest/v1/batches/batches-api.ts @@ -104,7 +104,6 @@ export class BatchesApi extends SmsDomainApi { */ public async dryRun(data: DryRunRequestData): Promise { this.client = this.getSinchClient(); - data['number_of_recipients'] = data['number_of_recipients'] !== undefined ? data['number_of_recipients'] : 100; const getParams = this.client.extractQueryParams(data, [ 'per_recipient', 'number_of_recipients']); @@ -164,7 +163,6 @@ export class BatchesApi extends SmsDomainApi { */ public list(data: ListBatchesRequestData): ApiListPromise { this.client = this.getSinchClient(); - data['page_size'] = data['page_size'] !== undefined ? data['page_size'] : 30; const getParams = this.client.extractQueryParams( data, ['page', 'page_size', 'from', 'start_date', 'end_date', 'client_reference'], diff --git a/packages/sms/tests/rest/v1/batches/batches.steps.ts b/packages/sms/tests/rest/v1/batches/batches.steps.ts new file mode 100644 index 00000000..88ec91a8 --- /dev/null +++ b/packages/sms/tests/rest/v1/batches/batches.steps.ts @@ -0,0 +1,257 @@ +import { BatchesApi, SmsService, Sms } from '../../../../src'; +import { Given, When, Then } from '@cucumber/cucumber'; +import * as assert from 'assert'; +import { PageResult } from '@sinch/sdk-client'; + +let batchesApi: BatchesApi; +let sendSmsResponse: Sms.TextResponse; +let dryRunResponse: Sms.DryRunResponse; +let listResponse: PageResult; +let batchesList: Sms.SendSMSResponse[]; +let pagesIteration: number; +let batch: Sms.SendSMSResponse; +let deliveryFeedbackResponse: void; + +Given('the SMS service "Batches" is available', () => { + const smsService = new SmsService({ + projectId: 'tinyfrog-jump-high-over-lilypadbasin', + keyId: 'keyId', + keySecret: 'keySecret', + authHostname: 'http://localhost:3011', + smsHostname: 'http://localhost:3017', + }); + batchesApi = smsService.batches; +}); + +When('I send a request to send a text message', async () => { + const sendSmsRequest: Sms.SendTextSMSRequestData = { + sendSMSRequestBody: { + body: 'SMS body message', + to: ['+12017777777'], + from: '+12015555555', + send_at: new Date('2024-06-06T09:25:00Z'), + delivery_report: 'full', + feedback_enabled: true, + }, + }; + sendSmsResponse = await batchesApi.sendTextMessage(sendSmsRequest); +}); + +Then('the response contains the text SMS details', () => { + assert.equal(sendSmsResponse.id, '01W4FFL35P4NC4K35SMSBATCH1'); + assert.deepEqual(sendSmsResponse.to, ['12017777777']); + assert.equal(sendSmsResponse.from, '12015555555'); + assert.equal(sendSmsResponse.canceled, false); + assert.equal(sendSmsResponse.body, 'SMS body message'); + assert.equal(sendSmsResponse.type, 'mt_text'); + assert.deepEqual(sendSmsResponse.created_at, new Date('2024-06-06T09:22:14.304Z')); + assert.deepEqual(sendSmsResponse.modified_at, new Date('2024-06-06T09:22:14.304Z')); + const fullDeliveryReport: Sms.DeliveryReportEnum = 'full'; + assert.equal(sendSmsResponse.delivery_report, fullDeliveryReport); + assert.deepEqual(sendSmsResponse.send_at, new Date('2024-06-06T09:25:00Z')); + assert.deepEqual(sendSmsResponse.expire_at, new Date('2024-06-09T09:25:00Z')); + assert.equal(sendSmsResponse.feedback_enabled, true); + assert.equal(sendSmsResponse.flash_message, false); +}); + +When('I send a request to perform a dry run of a batch', async () => { + const sendSmsRequest: Sms.DryRunRequestData = { + dryRunRequestBody: { + from: '+12015555555', + to: [ + '+12017777777', + '+12018888888', + '+12019999999', + ], + parameters: { + name: { + '+12017777777': 'John', + default: 'there', + }, + }, + body: 'Hello ${name}!', + delivery_report: 'none', + type: 'mt_text', + }, + }; + dryRunResponse = await batchesApi.dryRun(sendSmsRequest); +}); + +Then('the response contains the calculated bodies and number of parts for all messages in the batch', () => { + assert.equal(dryRunResponse.number_of_messages, 3); + assert.equal(dryRunResponse.number_of_recipients, 3); + assert.ok(dryRunResponse.per_recipient); + assert.equal(dryRunResponse.per_recipient.length, 3); + const johnMessage = dryRunResponse.per_recipient.filter( + (perRecipient) => perRecipient.recipient === '12017777777', + ).pop(); + assert.ok(johnMessage); + assert.equal(johnMessage.body, 'Hello John!'); + assert.equal(johnMessage.number_of_parts, 1); + assert.equal(johnMessage.encoding, 'text'); + const defaultMessage = dryRunResponse.per_recipient.filter( + (perRecipient) => perRecipient.recipient === '12018888888', + ).pop(); + assert.ok(defaultMessage); + assert.equal(defaultMessage.body, 'Hello there!'); + assert.equal(defaultMessage.number_of_parts, 1); + assert.equal(defaultMessage.encoding, 'text'); +}); + +When('I send a request to list the SMS batches', async () => { + const listBatchRequest: Sms.ListBatchesRequestData = { + page_size: 2, + }; + listResponse = await batchesApi.list(listBatchRequest); +}); + +Then('the response contains {string} SMS batches', (expectedAnswer: string) => { + const expectedBatchesCount = parseInt(expectedAnswer, 10); + assert.equal(listResponse.data.length, expectedBatchesCount); +}); + +When('I send a request to list all the SMS batches', async () => { + batchesList = []; + for await (const batch of batchesApi.list({ page_size: 2 })) { + batchesList.push(batch); + } +}); + +When('I iterate manually over the SMS batches pages', async () => { + batchesList = []; + listResponse = await batchesApi.list({ + page_size: 2, + }); + batchesList.push(...listResponse.data); + pagesIteration = 1; + let reachedEndOfPages = false; + while (!reachedEndOfPages) { + if (listResponse.hasNextPage) { + listResponse = await listResponse.nextPage(); + batchesList.push(...listResponse.data); + pagesIteration++; + } else { + reachedEndOfPages = true; + } + } +}); + +Then('the SMS batches list contains {string} SMS batches', (expectedAnswer: string) => { + const expectedBatchesCount = parseInt(expectedAnswer, 10); + assert.equal(batchesList.length, expectedBatchesCount); +}); + +Then('the SMS batches 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 batch', async () => { + batch = await batchesApi.get({ + batch_id: '01W4FFL35P4NC4K35SMSBATCH1', + }); +}); + +Then('the response contains the SMS batch details', () => { + assert.equal(batch.id, '01W4FFL35P4NC4K35SMSBATCH1'); + assert.deepEqual(sendSmsResponse.to, ['12017777777']); + assert.equal(sendSmsResponse.from, '12015555555'); + assert.equal(sendSmsResponse.canceled, false); + assert.equal(sendSmsResponse.body, 'SMS body message'); + assert.equal(sendSmsResponse.type, 'mt_text'); + assert.deepEqual(sendSmsResponse.created_at, new Date('2024-06-06T09:22:14.304Z')); + assert.deepEqual(sendSmsResponse.modified_at, new Date('2024-06-06T09:22:14.304Z')); + const fullDeliveryReport: Sms.DeliveryReportEnum = 'full'; + assert.equal(sendSmsResponse.delivery_report, fullDeliveryReport); + assert.deepEqual(sendSmsResponse.send_at, new Date('2024-06-06T09:25:00Z')); + assert.deepEqual(sendSmsResponse.expire_at, new Date('2024-06-09T09:25:00Z')); + assert.equal(sendSmsResponse.feedback_enabled, true); + assert.equal(sendSmsResponse.flash_message, false); +}); + +When('I send a request to update an SMS batch', async () => { + batch = await batchesApi.update({ + batch_id: '01W4FFL35P4NC4K35SMSBATCH1', + updateBatchMessageRequestBody: { + from: '+12016666666', + to_add: [ + '01W4FFL35P4NC4K35SMSGROUP1', + ], + delivery_report: 'summary', + }, + }); +}); + +Then('the response contains the SMS batch details with updated data', () => { + assert.equal(batch.id, '01W4FFL35P4NC4K35SMSBATCH1'); + assert.deepEqual(batch.to, ['12017777777', '01W4FFL35P4NC4K35SMSGROUP1']); + assert.equal(batch.from, '12016666666'); + assert.equal(batch.canceled, false); + assert.equal(batch.body, 'SMS body message'); + assert.equal(batch.type, 'mt_text'); + assert.deepEqual(batch.created_at, new Date('2024-06-06T09:22:14.304Z')); + assert.deepEqual(batch.modified_at, new Date('2024-06-06T09:22:48.054Z')); + const summaryDeliveryReport: Sms.DeliveryReportEnum = 'summary'; + assert.equal(batch.delivery_report, summaryDeliveryReport); + assert.deepEqual(batch.send_at, new Date('2024-06-06T09:25:00Z')); + assert.deepEqual(batch.expire_at, new Date('2024-06-09T09:25:00Z')); + assert.equal(batch.feedback_enabled, true); + assert.equal((batch as Sms.TextResponse).flash_message, false); +}); + +When('I send a request to replace an SMS batch', async () => { + batch = await batchesApi.replace({ + batch_id: '01W4FFL35P4NC4K35SMSBATCH1', + replaceBatchMessageRequestBody: { + from: '+12016666666', + to: [ + '+12018888888', + ], + body: 'This is the replacement batch', + send_at: new Date('2024-06-06T09:35:00Z'), + }, + }); +}); + +Then('the response contains the new SMS batch details with the provided data for replacement', () => { + assert.equal(batch.id, '01W4FFL35P4NC4K35SMSBATCH1'); + assert.deepEqual(batch.to, ['12018888888']); + assert.equal(batch.from, '12016666666'); + assert.equal(batch.canceled, false); + assert.equal(batch.body, 'This is the replacement batch'); + assert.equal(batch.type, 'mt_text'); + assert.deepEqual(batch.created_at, new Date('2024-06-06T09:22:14.304Z')); + assert.deepEqual(batch.modified_at, new Date('2024-06-06T09:23:32.504Z')); + const noDeliveryReport: Sms.DeliveryReportEnum = 'none'; + assert.equal(batch.delivery_report, noDeliveryReport); + assert.deepEqual(batch.send_at, new Date('2024-06-06T09:35:00Z')); + assert.deepEqual(batch.expire_at, new Date('2024-06-09T09:35:00Z')); + assert.equal(batch.feedback_enabled, undefined); + assert.equal((batch as Sms.TextResponse).flash_message, false); +}); + +When('I send a request to cancel an SMS batch', async () => { + batch = await batchesApi.cancel({ + batch_id: '01W4FFL35P4NC4K35SMSBATCH1', + }); +}); + +Then('the response contains the SMS batch details with a cancelled status', () => { + assert.equal(batch.id, '01W4FFL35P4NC4K35SMSBATCH1'); + assert.equal(batch.canceled, true); +}); + +When('I send a request to send delivery feedbacks', async () => { + deliveryFeedbackResponse = await batchesApi.sendDeliveryFeedback({ + batch_id: '01W4FFL35P4NC4K35SMSBATCH1', + deliveryFeedbackRequestBody: { + recipients: [ + '+12017777777', + ], + }, + }); +}); + +Then('the delivery feedback response contains no data', () => { + assert.deepEqual(deliveryFeedbackResponse, {}); +}); From 6fa8d486c7c973b18b4376fc8cc5b03e3585fb91 Mon Sep 17 00:00:00 2001 From: Antoine SEIN <142824551+asein-sinch@users.noreply.github.com> Date: Tue, 20 Aug 2024 10:44:17 +0200 Subject: [PATCH 31/54] DEVEXP-516: E2E SMS/Delivery Reports (#124) --- .../src/sms/delivery-reports/getForNumber.ts | 4 +- .../src/services/sms-event.service.ts | 2 +- .../message-delivery-status.ts | 2 +- .../delivery-reports-request-data.ts | 4 +- .../delivery-reports/delivery-reports-api.ts | 5 +- .../delivery-reports-api.test.ts | 2 +- .../delivery-reports.steps.ts | 140 ++++++++++++++++++ 7 files changed, 148 insertions(+), 11 deletions(-) create mode 100644 packages/sms/tests/rest/v1/delivery-reports/delivery-reports.steps.ts diff --git a/examples/simple-examples/src/sms/delivery-reports/getForNumber.ts b/examples/simple-examples/src/sms/delivery-reports/getForNumber.ts index e8b170aa..4a57f9c6 100644 --- a/examples/simple-examples/src/sms/delivery-reports/getForNumber.ts +++ b/examples/simple-examples/src/sms/delivery-reports/getForNumber.ts @@ -17,7 +17,7 @@ import { Sms } from '@sinch/sdk-core'; const requestData: Sms.GetDeliveryReportByPhoneNumberRequestData = { batch_id: batchId, - recipient_msisdn: recipientPhoneNumber, + phone_number: recipientPhoneNumber, }; const smsService = initSmsServiceWithServicePlanId(); @@ -25,7 +25,7 @@ import { Sms } from '@sinch/sdk-core'; try { response = await smsService.deliveryReports.getForNumber(requestData); } catch (error) { - console.error(`ERROR: Impossible to retrieve the delivery report by batch ID ${requestData.batch_id} for the recipient ${requestData.recipient_msisdn}`); + console.error(`ERROR: Impossible to retrieve the delivery report by batch ID ${requestData.batch_id} for the recipient ${requestData.phone_number}`); throw error; } diff --git a/examples/webhooks/src/services/sms-event.service.ts b/examples/webhooks/src/services/sms-event.service.ts index 53d6686e..1da6338d 100644 --- a/examples/webhooks/src/services/sms-event.service.ts +++ b/examples/webhooks/src/services/sms-event.service.ts @@ -18,7 +18,7 @@ export class SmsEventService { } private handleDeliveryReportEvent(event: Sms.DeliveryReport): void { - console.log(`The batch ${event.batch_id} has the following statuses:\n${event.statuses.map((status) => ' - \'' + status.status + '\' for the recipients: ' + status.recipients.join(', ')).join('\n')} `); + console.log(`The batch ${event.batch_id} has the following statuses:\n${event.statuses.map((status) => ' - \'' + status.status + '\' for the recipients: ' + status.recipients?.join(', ')).join('\n')} `); } private handleSmsEvent(event: Sms.MOText): void { diff --git a/packages/sms/src/models/v1/message-delivery-status/message-delivery-status.ts b/packages/sms/src/models/v1/message-delivery-status/message-delivery-status.ts index d33094ff..417a1c74 100644 --- a/packages/sms/src/models/v1/message-delivery-status/message-delivery-status.ts +++ b/packages/sms/src/models/v1/message-delivery-status/message-delivery-status.ts @@ -10,7 +10,7 @@ export interface MessageDeliveryStatus { /** The number of messages that currently has this code. */ count: number; /** Only for `full` report. A list of the phone number recipients which messages has this status code. */ - recipients: string[]; + recipients?: string[]; /** The simplified status as described in _Delivery Report Statuses_. */ status: DeliveryReportStatusEnum; } diff --git a/packages/sms/src/models/v1/requests/delivery-reports/delivery-reports-request-data.ts b/packages/sms/src/models/v1/requests/delivery-reports/delivery-reports-request-data.ts index c658abef..29c744b9 100644 --- a/packages/sms/src/models/v1/requests/delivery-reports/delivery-reports-request-data.ts +++ b/packages/sms/src/models/v1/requests/delivery-reports/delivery-reports-request-data.ts @@ -8,13 +8,13 @@ export interface GetDeliveryReportByBatchIdRequestData { /** Comma separated list of delivery_report_statuses to include */ 'status'?: DeliveryReportStatusEnum[]; /** Comma separated list of delivery_receipt_error_codes to include */ - 'code'?: string; + 'code'?: string | number | number[]; } export interface GetDeliveryReportByPhoneNumberRequestData { /** The batch ID you received from sending a message. */ 'batch_id': string; /** Phone number for which you to want to search. */ - 'recipient_msisdn': string; + 'phone_number': string; } export interface ListDeliveryReportsRequestData { /** The page number starting from 0. */ diff --git a/packages/sms/src/rest/v1/delivery-reports/delivery-reports-api.ts b/packages/sms/src/rest/v1/delivery-reports/delivery-reports-api.ts index 948d1762..ecb16438 100644 --- a/packages/sms/src/rest/v1/delivery-reports/delivery-reports-api.ts +++ b/packages/sms/src/rest/v1/delivery-reports/delivery-reports-api.ts @@ -34,7 +34,6 @@ export class DeliveryReportsApi extends SmsDomainApi { */ public async get(data: GetDeliveryReportByBatchIdRequestData): Promise { this.client = this.getSinchClient(); - data['type'] = data['type'] !== undefined ? data['type'] : 'summary'; const getParams = this.client.extractQueryParams( data, ['type', 'status', 'code'], @@ -73,7 +72,7 @@ export class DeliveryReportsApi extends SmsDomainApi { }; const body: RequestBody = ''; - const basePathUrl = `${this.client.apiClientOptions.hostname}/xms/v1/${this.client.apiClientOptions.projectId}/batches/${data['batch_id']}/delivery_report/${data['recipient_msisdn']}`; + const basePathUrl = `${this.client.apiClientOptions.hostname}/xms/v1/${this.client.apiClientOptions.projectId}/batches/${data['batch_id']}/delivery_report/${data['phone_number']}`; const requestOptions = await this.client.prepareOptions(basePathUrl, 'GET', getParams, headers, body || undefined); @@ -95,8 +94,6 @@ export class DeliveryReportsApi extends SmsDomainApi { */ public list(data: ListDeliveryReportsRequestData): 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', 'start_date', 'end_date', 'status', 'code', 'client_reference'], diff --git a/packages/sms/tests/rest/v1/delivery-reports/delivery-reports-api.test.ts b/packages/sms/tests/rest/v1/delivery-reports/delivery-reports-api.test.ts index 6a6c7f27..f4d98518 100644 --- a/packages/sms/tests/rest/v1/delivery-reports/delivery-reports-api.test.ts +++ b/packages/sms/tests/rest/v1/delivery-reports/delivery-reports-api.test.ts @@ -58,7 +58,7 @@ describe('DeliveryReportsApi', () => { // Given const requestData: Sms.GetDeliveryReportByPhoneNumberRequestData = { batch_id: '01HF28S9AAGRKWP2CY92BJB569', - recipient_msisdn: '+33444555666', + phone_number: '+33444555666', }; const expectedResponse: Sms.RecipientDeliveryReport = { batch_id: '01HF28S9AAGRKWP2CY92BJB569', diff --git a/packages/sms/tests/rest/v1/delivery-reports/delivery-reports.steps.ts b/packages/sms/tests/rest/v1/delivery-reports/delivery-reports.steps.ts new file mode 100644 index 00000000..090f3ef8 --- /dev/null +++ b/packages/sms/tests/rest/v1/delivery-reports/delivery-reports.steps.ts @@ -0,0 +1,140 @@ +import { DeliveryReportsApi, SmsService, Sms } from '../../../../src'; +import { Given, When, Then } from '@cucumber/cucumber'; +import * as assert from 'assert'; +import { PageResult } from '@sinch/sdk-client'; + +let deliveryReportsApi: DeliveryReportsApi; +let deliveryReport: Sms.DeliveryReport; +let recipientDeliveryReport: Sms.RecipientDeliveryReport; +let listResponse: PageResult; +let recipientDeliveryReportList: Sms.RecipientDeliveryReport[]; +let pagesIteration: number; + +Given('the SMS service "Delivery Reports" is available', () => { + const smsService = new SmsService({ + projectId: 'tinyfrog-jump-high-over-lilypadbasin', + keyId: 'keyId', + keySecret: 'keySecret', + authHostname: 'http://localhost:3011', + smsHostname: 'http://localhost:3017', + }); + deliveryReportsApi = smsService.deliveryReports; +}); + +When('I send a request to retrieve a summary SMS delivery report', async () => { + const requestData: Sms.GetDeliveryReportByBatchIdRequestData = { + batch_id: '01W4FFL35P4NC4K35SMSBATCH1', + status: [ + 'Delivered', + 'Failed', + ], + code: [15, 0], + }; + deliveryReport = await deliveryReportsApi.get(requestData); +}); + +Then('the response contains a summary SMS delivery report', () => { + assert.equal(deliveryReport.batch_id, '01W4FFL35P4NC4K35SMSBATCH1'); + assert.equal(deliveryReport.client_reference, 'reference_e2e'); + assert.ok(deliveryReport.statuses); + let status = deliveryReport.statuses[0]; + assert.equal(status.code, 15); + assert.equal(status.count, 1); + assert.equal(status.recipients, undefined); + const failedStatus: Sms.DeliveryReportStatusEnum = 'Failed'; + assert.equal(status.status, failedStatus); + status = deliveryReport.statuses[1]; + assert.equal(status.code, 0); + assert.equal(status.count, 1); + assert.equal(status.recipients, undefined); + const deliveredStatus: Sms.DeliveryReportStatusEnum = 'Delivered'; + assert.equal(status.status, deliveredStatus); + assert.equal(deliveryReport.total_message_count, 2); + assert.equal(deliveryReport.type, 'delivery_report_sms'); +}); + +When('I send a request to retrieve a full SMS delivery report', async () => { + const requestData: Sms.GetDeliveryReportByBatchIdRequestData = { + batch_id: '01W4FFL35P4NC4K35SMSBATCH1', + type: 'full', + }; + deliveryReport = await deliveryReportsApi.get(requestData); +}); + +Then('the response contains a full SMS delivery report', () => { + assert.equal(deliveryReport.batch_id, '01W4FFL35P4NC4K35SMSBATCH1'); + assert.ok(deliveryReport.statuses); + const status = deliveryReport.statuses[0]; + assert.ok(status.recipients); + assert.equal(status.code, 0); + assert.equal(status.count, 1); + assert.equal(status.recipients[0], '12017777777'); + const deliveredStatus: Sms.DeliveryReportStatusEnum = 'Delivered'; + assert.equal(status.status, deliveredStatus); +}); + +When('I send a request to retrieve a recipient\'s delivery report', async () => { + const requestData: Sms.GetDeliveryReportByPhoneNumberRequestData = { + batch_id: '01W4FFL35P4NC4K35SMSBATCH1', + phone_number: '12017777777', + }; + recipientDeliveryReport = await deliveryReportsApi.getForNumber(requestData); +}); + +Then('the response contains the recipient\'s delivery report details', () => { + assert.equal(recipientDeliveryReport.batch_id, '01W4FFL35P4NC4K35SMSBATCH1'); + assert.equal(recipientDeliveryReport.recipient, '12017777777'); + assert.equal(recipientDeliveryReport.client_reference, 'reference_e2e'); + const deliveredStatus: Sms.DeliveryReportStatusEnum = 'Delivered'; + assert.equal(recipientDeliveryReport.status, deliveredStatus); + assert.equal(recipientDeliveryReport.type, 'recipient_delivery_report_sms'); + assert.equal(recipientDeliveryReport.code, 0); + assert.deepEqual(recipientDeliveryReport.at, new Date('2024-06-06T13:06:27.833Z')); + assert.deepEqual(recipientDeliveryReport.operator_status_at, new Date('2024-06-06T13:06:00Z')); +}); + +When('I send a request to list the SMS delivery reports', async () => { + const requestData: Sms.ListDeliveryReportsRequestData = {}; + listResponse = await deliveryReportsApi.list(requestData); +}); + +Then('the response contains {string} SMS delivery reports', (expectedAnswer: string) => { + const expectedDeliveryReportsCount = parseInt(expectedAnswer, 10); + assert.equal(listResponse.data.length, expectedDeliveryReportsCount); +}); + +When('I send a request to list all the SMS delivery reports', async () => { + recipientDeliveryReportList = []; + for await (const deliveryReport of deliveryReportsApi.list({ page_size: 2 })) { + recipientDeliveryReportList.push(deliveryReport); + } +}); + +When('I iterate manually over the SMS delivery reports pages', async () => { + recipientDeliveryReportList = []; + listResponse = await deliveryReportsApi.list({ + page_size: 2, + }); + recipientDeliveryReportList.push(...listResponse.data); + pagesIteration = 1; + let reachedEndOfPages = false; + while (!reachedEndOfPages) { + if (listResponse.hasNextPage) { + listResponse = await listResponse.nextPage(); + recipientDeliveryReportList.push(...listResponse.data); + pagesIteration++; + } else { + reachedEndOfPages = true; + } + } +}); + +Then('the SMS delivery reports list contains {string} SMS delivery reports', (expectedAnswer: string) => { + const expectedDeliveryReportsCount = parseInt(expectedAnswer, 10); + assert.equal(recipientDeliveryReportList.length, expectedDeliveryReportsCount); +}); + +Then('the SMS delivery reports iteration result contains the data from {string} pages', (expectedAnswer: string) => { + const expectedPagesCount = parseInt(expectedAnswer, 10); + assert.equal(pagesIteration, expectedPagesCount); +}); From dc3e7cd268a29b710ce17d900bba4f2b92beaf0e Mon Sep 17 00:00:00 2001 From: Antoine SEIN <142824551+asein-sinch@users.noreply.github.com> Date: Wed, 21 Aug 2024 10:19:59 +0200 Subject: [PATCH 32/54] DEVEXP-517: E2E SMS/Inbounds (#125) --- .../inbounds/inbounds-request-data.ts | 2 +- .../e2e/features/delivery-reports.feature | 0 .../tests/rest/v1/inbounds/inbounds.steps.ts | 100 ++++++++++++++++++ 3 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 packages/sms/tests/e2e/features/delivery-reports.feature create mode 100644 packages/sms/tests/rest/v1/inbounds/inbounds.steps.ts diff --git a/packages/sms/src/models/v1/requests/inbounds/inbounds-request-data.ts b/packages/sms/src/models/v1/requests/inbounds/inbounds-request-data.ts index 5d83edc2..591b5576 100644 --- a/packages/sms/src/models/v1/requests/inbounds/inbounds-request-data.ts +++ b/packages/sms/src/models/v1/requests/inbounds/inbounds-request-data.ts @@ -4,7 +4,7 @@ export interface ListInboundMessagesRequestData { /** Determines the size of a page */ 'page_size'?: number; /** Only list messages sent to this destination. Multiple phone numbers formatted as either E.164 or short codes can be comma separated. */ - 'to'?: string; + 'to'?: string | string[]; /** Only list messages received at or after this date/time. Formatted as [ISO-8601](https://en.wikipedia.org/wiki/ISO_8601): `YYYY-MM-DDThh:mm:ss.SSSZ`. Default: Now-24 */ 'start_date'?: Date; /** Only list messages received before this date/time. Formatted as [ISO-8601](https://en.wikipedia.org/wiki/ISO_8601): `YYYY-MM-DDThh:mm:ss.SSSZ`. */ diff --git a/packages/sms/tests/e2e/features/delivery-reports.feature b/packages/sms/tests/e2e/features/delivery-reports.feature new file mode 100644 index 00000000..e69de29b diff --git a/packages/sms/tests/rest/v1/inbounds/inbounds.steps.ts b/packages/sms/tests/rest/v1/inbounds/inbounds.steps.ts new file mode 100644 index 00000000..332d4027 --- /dev/null +++ b/packages/sms/tests/rest/v1/inbounds/inbounds.steps.ts @@ -0,0 +1,100 @@ +import { InboundsApi, SmsService, Sms } from '../../../../src'; +import { Given, When, Then } from '@cucumber/cucumber'; +import * as assert from 'assert'; +import { PageResult } from '@sinch/sdk-client'; + +let inboundsApi: InboundsApi; +let inboundMessage: Sms.InboundMessageResponse; +let listResponse: PageResult; +let inboundMessagesList: Sms.InboundMessageResponse[]; +let pagesIteration: number; + +Given('the SMS service "Inbounds" is available', () => { + const smsService = new SmsService({ + projectId: 'tinyfrog-jump-high-over-lilypadbasin', + keyId: 'keyId', + keySecret: 'keySecret', + authHostname: 'http://localhost:3011', + smsHostname: 'http://localhost:3017', + }); + inboundsApi = smsService.inbounds; +}); + +When('I send a request to retrieve an inbound message', async () => { + inboundMessage = await inboundsApi.get({ + inbound_id: '01W4FFL35P4NC4K35INBOUND01', + }); +}); + +Then('the response contains the inbound message details', () => { + assert.equal(inboundMessage.id, '01W4FFL35P4NC4K35INBOUND01'); + assert.equal(inboundMessage.from, '12015555555'); + assert.equal(inboundMessage.to, '12017777777'); + assert.equal(inboundMessage.body, 'Hello John!'); + assert.equal(inboundMessage.type, 'mo_text'); + assert.equal(inboundMessage.operator_id, '311071'); + assert.deepEqual(inboundMessage.received_at, new Date('2024-06-06T14:16:54.777Z')); +}); + +When('I send a request to list the inbound messages', async () => { + const listInboundMessagesRequest: Sms.ListInboundMessagesRequestData = { + page_size: 2, + to: [ + '+12017777777', + '+12018888888', + ], + }; + listResponse = await inboundsApi.list(listInboundMessagesRequest); +}); + +Then('the response contains {string} inbound messages', (expectedAnswer: string) => { + const expectedInboundMessagesCount = parseInt(expectedAnswer, 10); + assert.equal(listResponse.data.length, expectedInboundMessagesCount); +}); + +When('I send a request to list all the inbound messages', async () => { + inboundMessagesList = []; + const listInboundMessagesRequest: Sms.ListInboundMessagesRequestData = { + page_size: 2, + to: [ + '+12017777777', + '+12018888888', + ], + }; + for await (const inboundMessage of inboundsApi.list(listInboundMessagesRequest)) { + inboundMessagesList.push(inboundMessage); + } +}); + +When('I iterate manually over the inbound messages pages', async () => { + inboundMessagesList = []; + listResponse = await inboundsApi.list({ + page_size: 2, + to: [ + '+12017777777', + '+12018888888', + ], + }); + inboundMessagesList.push(...listResponse.data); + pagesIteration = 1; + let reachedEndOfPages = false; + while (!reachedEndOfPages) { + if (listResponse.hasNextPage) { + listResponse = await listResponse.nextPage(); + inboundMessagesList.push(...listResponse.data); + pagesIteration++; + } else { + reachedEndOfPages = true; + } + } +}); + +Then('the inbound messages list contains {string} inbound messages', (expectedAnswer: string) => { + const expectedInboundMessagesCount = parseInt(expectedAnswer, 10); + assert.equal(inboundMessagesList.length, expectedInboundMessagesCount); +}); + +Then('the inbound messages iteration result contains the data from {string} pages', (expectedAnswer: string) => { + const expectedPagesCount = parseInt(expectedAnswer, 10); + assert.equal(pagesIteration, expectedPagesCount); +}); 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 33/54] 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'); +}); From 5df5cb0bd99ba9ba2bfd76df699452fa38eed7e3 Mon Sep 17 00:00:00 2001 From: Antoine SEIN <142824551+asein-sinch@users.noreply.github.com> Date: Fri, 23 Aug 2024 10:17:44 +0200 Subject: [PATCH 34/54] DEVEXP-519: E2E SMS/Webhooks (#127) --- .../src/services/sms-event.service.ts | 6 ++ .../v1/callbacks/webhooks-events.steps.ts | 84 +++++++++++++++++++ 2 files changed, 90 insertions(+) create mode 100644 packages/sms/tests/rest/v1/callbacks/webhooks-events.steps.ts diff --git a/examples/webhooks/src/services/sms-event.service.ts b/examples/webhooks/src/services/sms-event.service.ts index 1da6338d..0e318582 100644 --- a/examples/webhooks/src/services/sms-event.service.ts +++ b/examples/webhooks/src/services/sms-event.service.ts @@ -8,6 +8,8 @@ export class SmsEventService { console.log(`:: INCOMING EVENT :: ${event.type}`); if (event.type === 'delivery_report_sms' || event.type === 'delivery_report_mms') { return this.handleDeliveryReportEvent(event as Sms.DeliveryReport); + } else if (event.type === 'recipient_delivery_report_sms' || event.type === 'recipient_delivery_report_mms') { + return this.handleRecipientDeliveryReportEvent(event as Sms.RecipientDeliveryReport); } else if (event.type === 'mo_text') { return this.handleSmsEvent(event as Sms.MOText); } else if (event.type === 'mo_binary') { @@ -21,6 +23,10 @@ export class SmsEventService { console.log(`The batch ${event.batch_id} has the following statuses:\n${event.statuses.map((status) => ' - \'' + status.status + '\' for the recipients: ' + status.recipients?.join(', ')).join('\n')} `); } + private handleRecipientDeliveryReportEvent(event: Sms.RecipientDeliveryReport): void { + console.log(`The batch ${event.batch_id} has the status "${event.status}" (code: ${event.code}) for the recipient ${event.recipient}`); + } + private handleSmsEvent(event: Sms.MOText): void { console.log(`A SMS sent by ${event.from} has been received by ${event.to} at ${event.received_at}. The content is:\n"${event.body}"`); } diff --git a/packages/sms/tests/rest/v1/callbacks/webhooks-events.steps.ts b/packages/sms/tests/rest/v1/callbacks/webhooks-events.steps.ts new file mode 100644 index 00000000..b1de8ac8 --- /dev/null +++ b/packages/sms/tests/rest/v1/callbacks/webhooks-events.steps.ts @@ -0,0 +1,84 @@ +import { Given, Then, When } from '@cucumber/cucumber'; +import { SmsCallbackWebhooks, SmsCallback, Sms } from '../../../../src'; +import assert from 'assert'; + +let smsCallbackWebhook: SmsCallbackWebhooks; +let rawEvent: any; +let event: SmsCallback; + +const processEvent = async (response: Response) => { + rawEvent = await response.text(); + event = smsCallbackWebhook.parseEvent(JSON.parse(rawEvent)); +}; + +Given('the SMS Webhooks handler is available', () => { + smsCallbackWebhook = new SmsCallbackWebhooks(); +}); + +When('I send a request to trigger an "incoming SMS" event', async () => { + const response = await fetch('http://localhost:3017/webhooks/sms/incoming-sms'); + await processEvent(response); +}); + +Then('the SMS event describes an "incoming SMS" event', () => { + const incomingSmsEvent = event as Sms.MOText; + assert.equal(incomingSmsEvent.id, '01W4FFL35P4NC4K35SMSBATCH8'); + assert.equal(incomingSmsEvent.from, '12015555555'); + assert.equal(incomingSmsEvent.to, '12017777777'); + assert.equal(incomingSmsEvent.body, 'Hello John! ๐Ÿ‘‹'); + assert.equal(incomingSmsEvent.type, 'mo_text'); + assert.equal(incomingSmsEvent.operator_id, '311071'); + assert.deepEqual(incomingSmsEvent.received_at, new Date('2024-06-06T07:52:37.386Z')); +}); + +When('I send a request to trigger an "SMS delivery report" event', async () => { + const response = await fetch('http://localhost:3017/webhooks/sms/delivery-report-sms'); + await processEvent(response); +}); + +Then('the SMS event describes an "SMS delivery report" event', () => { + const deliveryReportEvent = event as Sms.DeliveryReport; + assert.equal(deliveryReportEvent.batch_id, '01W4FFL35P4NC4K35SMSBATCH8'); + assert.equal(deliveryReportEvent.client_reference, 'client-ref'); + assert.ok(deliveryReportEvent.statuses); + const status = deliveryReportEvent.statuses[0]; + assert.equal(status.code, 0); + assert.equal(status.count, 2); + const deliveryStatus: Sms.DeliveryReportStatusEnum = 'Delivered'; + assert.equal(status.status, deliveryStatus); + assert.ok(status.recipients); + assert.equal(status.recipients[0], '12017777777'); + assert.equal(status.recipients[1], '33612345678'); + assert.equal(deliveryReportEvent.type, 'delivery_report_sms'); +}); + +// eslint-disable-next-line max-len +When('I send a request to trigger an "SMS recipient delivery report" event with the status {string}', async (status: string) => { + const response = await fetch(`http://localhost:3017/webhooks/sms/recipient-delivery-report-sms-${status.toLowerCase()}`); + await processEvent(response); +}); + +Then('the SMS event describes an SMS recipient delivery report event with the status "Delivered"', () => { + const recipientDeliveryReportEvent = event as Sms.RecipientDeliveryReport; + assert.equal(recipientDeliveryReportEvent.batch_id, '01W4FFL35P4NC4K35SMSBATCH9'); + assert.equal(recipientDeliveryReportEvent.recipient, '12017777777'); + assert.equal(recipientDeliveryReportEvent.code, 0); + const deliveryStatus: Sms.DeliveryReportStatusEnum = 'Delivered'; + assert.equal(recipientDeliveryReportEvent.status, deliveryStatus); + assert.equal(recipientDeliveryReportEvent.type, 'recipient_delivery_report_sms'); + assert.equal(recipientDeliveryReportEvent.client_reference, 'client-ref'); + assert.deepEqual(recipientDeliveryReportEvent.at, new Date('2024-06-06T08:17:19.210Z')); + assert.deepEqual(recipientDeliveryReportEvent.operator_status_at, new Date('2024-06-06T08:17:00Z')); +}); + +Then('the SMS event describes an SMS recipient delivery report event with the status "Aborted"', () => { + const recipientDeliveryReportEvent = event as Sms.RecipientDeliveryReport; + assert.equal(recipientDeliveryReportEvent.batch_id, '01W4FFL35P4NC4K35SMSBATCH9'); + assert.equal(recipientDeliveryReportEvent.recipient, '12010000000'); + assert.equal(recipientDeliveryReportEvent.code, 412); + const deliveryStatus: Sms.DeliveryReportStatusEnum = 'Aborted'; + assert.equal(recipientDeliveryReportEvent.status, deliveryStatus); + assert.equal(recipientDeliveryReportEvent.type, 'recipient_delivery_report_sms'); + assert.equal(recipientDeliveryReportEvent.client_reference, 'client-ref'); + assert.deepEqual(recipientDeliveryReportEvent.at, new Date('2024-06-06T08:17:15.603Z')); +}); From ad92b7cbaee66faaf4f53945280f4982d676e7ef Mon Sep 17 00:00:00 2001 From: Antoine SEIN <142824551+asein-sinch@users.noreply.github.com> Date: Wed, 28 Aug 2024 10:20:56 +0200 Subject: [PATCH 35/54] DEVEXP-520: E2E Verification/Start (#128) --- .../src/verification/app.ts | 20 +-- examples/simple-examples/README.md | 29 ++-- examples/simple-examples/package.json | 8 +- .../verifications/data/start-data.ts | 32 ++++ .../{seamless => data}/start-seamless.ts | 3 +- .../report-with-id_callout.ts | 0 .../report-with-identity_callout.ts | 0 .../{callout => phonecall}/start-callout.ts | 1 + .../phonecall/start-phonecall.ts | 31 ++++ packages/verification/README.md | 38 +++-- packages/verification/cucumber.js | 8 + packages/verification/package.json | 3 +- packages/verification/src/models/v1/helper.ts | 85 +++++++++++ packages/verification/src/models/v1/index.ts | 8 +- .../verifications-request-data.ts | 13 ++ .../index.ts | 1 - .../start-data-verification-response/index.ts | 4 + .../start-data-verification-response.ts} | 5 +- .../index.ts | 4 + .../start-phonecall-verification-response.ts} | 4 +- .../index.ts | 1 - .../v1/start-verification-request/index.ts | 5 + .../start-verification-request.ts | 27 +++- .../verifications-api.jest.fixture.ts | 16 ++ .../v1/verifications/verifications-api.ts | 92 +++++++++++- .../tests/rest/v1/start/start.steps.ts | 139 ++++++++++++++++++ .../verifications/verifications-api.test.ts | 99 ++++++++----- 27 files changed, 577 insertions(+), 99 deletions(-) create mode 100644 examples/simple-examples/src/verification/verifications/data/start-data.ts rename examples/simple-examples/src/verification/verifications/{seamless => data}/start-seamless.ts (85%) rename examples/simple-examples/src/verification/verifications/{callout => phonecall}/report-with-id_callout.ts (100%) rename examples/simple-examples/src/verification/verifications/{callout => phonecall}/report-with-identity_callout.ts (100%) rename examples/simple-examples/src/verification/verifications/{callout => phonecall}/start-callout.ts (93%) create mode 100644 examples/simple-examples/src/verification/verifications/phonecall/start-phonecall.ts create mode 100644 packages/verification/cucumber.js delete mode 100644 packages/verification/src/models/v1/start-callout-verification-response/index.ts create mode 100644 packages/verification/src/models/v1/start-data-verification-response/index.ts rename packages/verification/src/models/v1/{start-seamless-verification-response/start-seamless-verification-response.ts => start-data-verification-response/start-data-verification-response.ts} (76%) create mode 100644 packages/verification/src/models/v1/start-phonecall-verification-response/index.ts rename packages/verification/src/models/v1/{start-callout-verification-response/start-callout-verification-response.ts => start-phonecall-verification-response/start-phonecall-verification-response.ts} (67%) delete mode 100644 packages/verification/src/models/v1/start-seamless-verification-response/index.ts create mode 100644 packages/verification/tests/rest/v1/start/start.steps.ts diff --git a/examples/integrated-flows-examples/src/verification/app.ts b/examples/integrated-flows-examples/src/verification/app.ts index 5c3cf202..138a6830 100644 --- a/examples/integrated-flows-examples/src/verification/app.ts +++ b/examples/integrated-flows-examples/src/verification/app.ts @@ -18,9 +18,9 @@ dotenv.config(); enum VerificationMethods { SMS ='sms', - CALLOUT = 'callout', + PHONE_CALL = 'phone call', FLASH_CALL = 'flash call', - SEAMLESS = 'seamless', + DATA = 'data', } inquirer.prompt([ @@ -41,13 +41,13 @@ dotenv.config(); case VerificationMethods.SMS: startSmsVerificationFlow(answers.phoneNumber); break; - case VerificationMethods.CALLOUT: - startCalloutVerificationFlow(answers.phoneNumber); + case VerificationMethods.PHONE_CALL: + startPhoneCallVerificationFlow(answers.phoneNumber); break; case VerificationMethods.FLASH_CALL: startFlashCallVerificationFlow(answers.phoneNumber); break; - case VerificationMethods.SEAMLESS: + case VerificationMethods.DATA: startSeamlessVerificationFlow(answers.phoneNumber); break; } @@ -72,9 +72,9 @@ dotenv.config(); }; - const startCalloutVerificationFlow = async (phoneNumber: string) => { - const requestData = Verification.startVerificationHelper.buildCalloutRequest(phoneNumber); - const response = await sinch.verification.verifications.startCallout(requestData); + const startPhoneCallVerificationFlow = async (phoneNumber: string) => { + const requestData = Verification.startVerificationHelper.buildPhoneCallRequest(phoneNumber); + const response = await sinch.verification.verifications.startPhoneCall(requestData); console.log('Verification request sent! Please answer to the phone call ans listen to the OTP.'); const answers = await inquirer.prompt([ { @@ -109,10 +109,10 @@ dotenv.config(); }; const startSeamlessVerificationFlow = async (phoneNumber: string) => { - const requestData = Verification.startVerificationHelper.buildSeamlessRequest(phoneNumber); + const requestData = Verification.startVerificationHelper.buildDataRequest(phoneNumber); let response; try { - response = await sinch.verification.verifications.startSeamless(requestData); + response = await sinch.verification.verifications.startData(requestData); } catch (error: any) { console.log(`Impossible to process the seamless verification: ${error.data})`); return; diff --git a/examples/simple-examples/README.md b/examples/simple-examples/README.md index 9352335e..500f9f06 100644 --- a/examples/simple-examples/README.md +++ b/examples/simple-examples/README.md @@ -149,20 +149,21 @@ yarn run numbers:regions:list ### Verification -| Service | Sample application name and location | Required parameters | -|---------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------| -| Verifications | [./src/verification/verifications/sms/start-sms.ts](./src/verification/verifications/sms/start-sms.ts) | `VERIFICATION_IDENTITY` | -| | [./src/verification/verifications/sms/report-with-id_sms.ts](./src/verification/verifications/sms/report-with-id_sms.ts) | `VERIFICATION_ID` + `VERIFICATION_CODE` | -| | [./src/verification/verifications/sms/report-with-identity_sms.ts](./src/verification/verifications/sms/report-with-identity_sms.ts) | `VERIFICATION_IDENTITY` + `VERIFICATION_CODE` | -| | [./src/verification/verifications/flashcall/start-flashcall.ts](./src/verification/verifications/flashcall/start-flashcall.ts) | `VERIFICATION_IDENTITY` | -| | [./src/verification/verifications/flashcall/report-with-id_flashcall.ts](./src/verification/verifications/flashcall/report-with-id_flashcall.ts) | `VERIFICATION_ID` + `VERIFICATION_CLI` | -| | [./src/verification/verifications/flashcall/report-with-identity_flashcall.ts](./src/verification/verifications/flashcall/report-with-identity_flashcall.ts) | `VERIFICATION_IDENTITY` + `VERIFICATION_CLI` | -| | [./src/verification/verifications/callout/start-callout.ts](./src/verification/verifications/callout/start-callout.ts) | `VERIFICATION_IDENTITY` | -| | [./src/verification/verifications/callout/report-with-id_callout.ts](./src/verification/verifications/callout/report-with-id_callout.ts) | `VERIFICATION_ID` + `VERIFICATION_CODE` | -| | [./src/verification/verifications/callout/report-with-identity_callout.ts](./src/verification/verifications/callout/report-with-identity_callout.ts) | `VERIFICATION_IDENTITY` + `VERIFICATION_CODE` | -| Verification-status | [./src/verification/verification-status/verification-by-id.ts](./src/verification/verification-status/verification-by-id.ts) | `VERIFICATION_ID` | -| | [./src/verification/verification-status/verification-by-identity.ts](./src/verification/verification-status/verification-by-identity.ts) | `VERIFICATION_IDENTITY` | -| | [./src/verification/verification-status/verification-by-reference.ts](./src/verification/verification-status/verification-by-reference.ts) | `VERIFICATION_REFERENCE` | +| Service | Sample application name and location | Required parameters | +|---------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------| +| Start Verifications | [./src/verification/verifications/sms/start-sms.ts](./src/verification/verifications/sms/start-sms.ts) | `VERIFICATION_IDENTITY` | +| | [./src/verification/verifications/phonecall/start-phonecall.ts](./src/verification/verifications/phonecall/start-phonecall.ts) | `VERIFICATION_IDENTITY` | +| | [./src/verification/verifications/flashcall/start-flashcall.ts](./src/verification/verifications/flashcall/start-flashcall.ts) | `VERIFICATION_IDENTITY` | +| | [./src/verification/verifications/data/start-data.ts](./src/verification/verifications/data/start-data.ts) | `VERIFICATION_IDENTITY` | +| Verifications | [./src/verification/verifications/sms/report-with-id_sms.ts](./src/verification/verifications/sms/report-with-id_sms.ts) | `VERIFICATION_ID` + `VERIFICATION_CODE` | +| | [./src/verification/verifications/sms/report-with-identity_sms.ts](./src/verification/verifications/sms/report-with-identity_sms.ts) | `VERIFICATION_IDENTITY` + `VERIFICATION_CODE` | +| | [./src/verification/verifications/flashcall/report-with-id_flashcall.ts](./src/verification/verifications/flashcall/report-with-id_flashcall.ts) | `VERIFICATION_ID` + `VERIFICATION_CLI` | +| | [./src/verification/verifications/flashcall/report-with-identity_flashcall.ts](./src/verification/verifications/flashcall/report-with-identity_flashcall.ts) | `VERIFICATION_IDENTITY` + `VERIFICATION_CLI` | +| | [./src/verification/verifications/callout/report-with-id_callout.ts](./src/verification/verifications/phonecall/report-with-id_callout.ts) | `VERIFICATION_ID` + `VERIFICATION_CODE` | +| | [./src/verification/verifications/callout/report-with-identity_callout.ts](./src/verification/verifications/phonecall/report-with-identity_callout.ts) | `VERIFICATION_IDENTITY` + `VERIFICATION_CODE` | +| Verification-status | [./src/verification/verification-status/verification-by-id.ts](./src/verification/verification-status/verification-by-id.ts) | `VERIFICATION_ID` | +| | [./src/verification/verification-status/verification-by-identity.ts](./src/verification/verification-status/verification-by-identity.ts) | `VERIFICATION_IDENTITY` | +| | [./src/verification/verification-status/verification-by-reference.ts](./src/verification/verification-status/verification-by-reference.ts) | `VERIFICATION_REFERENCE` | ### Voice diff --git a/examples/simple-examples/package.json b/examples/simple-examples/package.json index 9f12ae68..4b72e1bb 100644 --- a/examples/simple-examples/package.json +++ b/examples/simple-examples/package.json @@ -153,14 +153,14 @@ "sms:inbounds:list": "ts-node src/sms/inbounds/list.ts", "sms:inbounds:get": "ts-node src/sms/inbounds/get.ts", "verification:verifications:start-sms": "ts-node src/verification/verifications/sms/start-sms.ts", - "verification:verifications:start-callout": "ts-node src/verification/verifications/callout/start-callout.ts", + "verification:verifications:start-phonecall": "ts-node src/verification/verifications/phonecall/start-phonecall.ts", "verification:verifications:start-flashcall": "ts-node src/verification/verifications/flashcall/start-flashcall.ts", - "verification:verifications:start-seamless": "ts-node src/verification/verifications/seamless/start-seamless.ts", + "verification:verifications:start-data": "ts-node src/verification/verifications/data/start-data.ts", "verification:verifications:report-with-id:sms": "ts-node src/verification/verifications/sms/report-with-id_sms.ts", - "verification:verifications:report-with-id:callout": "ts-node src/verification/verifications/callout/report-with-id_callout.ts", + "verification:verifications:report-with-id:callout": "ts-node src/verification/verifications/phonecall/report-with-id_callout.ts", "verification:verifications:report-with-id:flashcall": "ts-node src/verification/verifications/flashcall/report-with-id_flashcall.ts", "verification:verifications:report-with-identity:sms": "ts-node src/verification/verifications/sms/report-with-identity_sms.ts", - "verification:verifications:report-with-identity:callout": "ts-node src/verification/verifications/callout/report-with-identity_callout.ts", + "verification:verifications:report-with-identity:callout": "ts-node src/verification/verifications/phonecall/report-with-identity_callout.ts", "verification:verifications:report-with-identity:flashcall": "ts-node src/verification/verifications/flashcall/report-with-identity_flashcall.ts", "verification:verification-status:verification-by-id": "ts-node src/verification/verification-status/verification-by-id.ts", "verification:verification-status:verification-by-identity": "ts-node src/verification/verification-status/verification-by-identity.ts", diff --git a/examples/simple-examples/src/verification/verifications/data/start-data.ts b/examples/simple-examples/src/verification/verifications/data/start-data.ts new file mode 100644 index 00000000..3c0f2f7b --- /dev/null +++ b/examples/simple-examples/src/verification/verifications/data/start-data.ts @@ -0,0 +1,32 @@ +import { Verification } from '@sinch/sdk-core'; +import { + getPrintFormat, + getVerificationIdentityFromConfig, + initVerificationService, + printFullResponse, +} from '../../../config'; + +(async () => { + console.log('****************************'); + console.log('* StartVerification - data *'); + console.log('****************************'); + + const verificationIdentity = getVerificationIdentityFromConfig(); + + const requestData = Verification.startVerificationHelper.buildDataRequest( + verificationIdentity, + `test-reference-for-seamless-verification_${verificationIdentity}`, + ); + + const verificationService = initVerificationService(); + const response = await verificationService.verifications.startData(requestData); + + const printFormat = getPrintFormat(process.argv); + + if (printFormat === 'pretty') { + console.log(`Verification ID = ${response.id}`); + console.log(`Data verification specific field: targetUri = ${response.seamless?.targetUri}`); + } else { + printFullResponse(response); + } +})(); diff --git a/examples/simple-examples/src/verification/verifications/seamless/start-seamless.ts b/examples/simple-examples/src/verification/verifications/data/start-seamless.ts similarity index 85% rename from examples/simple-examples/src/verification/verifications/seamless/start-seamless.ts rename to examples/simple-examples/src/verification/verifications/data/start-seamless.ts index c72e5555..f6ea52b2 100644 --- a/examples/simple-examples/src/verification/verifications/seamless/start-seamless.ts +++ b/examples/simple-examples/src/verification/verifications/data/start-seamless.ts @@ -6,6 +6,7 @@ import { printFullResponse, } from '../../../config'; +/** @deprecated see ../../start/start-data.ts instead */ (async () => { console.log('********************************'); console.log('* StartVerification - seamless *'); @@ -25,7 +26,7 @@ import { if (printFormat === 'pretty') { console.log(`Verification ID = ${response.id}`); - console.log(`Seamless verification specific field: template = ${response.seamless?.targetUri}`); + console.log(`Seamless verification specific field: targetUri = ${response.seamless?.targetUri}`); } else { printFullResponse(response); } diff --git a/examples/simple-examples/src/verification/verifications/callout/report-with-id_callout.ts b/examples/simple-examples/src/verification/verifications/phonecall/report-with-id_callout.ts similarity index 100% rename from examples/simple-examples/src/verification/verifications/callout/report-with-id_callout.ts rename to examples/simple-examples/src/verification/verifications/phonecall/report-with-id_callout.ts diff --git a/examples/simple-examples/src/verification/verifications/callout/report-with-identity_callout.ts b/examples/simple-examples/src/verification/verifications/phonecall/report-with-identity_callout.ts similarity index 100% rename from examples/simple-examples/src/verification/verifications/callout/report-with-identity_callout.ts rename to examples/simple-examples/src/verification/verifications/phonecall/report-with-identity_callout.ts diff --git a/examples/simple-examples/src/verification/verifications/callout/start-callout.ts b/examples/simple-examples/src/verification/verifications/phonecall/start-callout.ts similarity index 93% rename from examples/simple-examples/src/verification/verifications/callout/start-callout.ts rename to examples/simple-examples/src/verification/verifications/phonecall/start-callout.ts index 1a0a6ddb..11cf58c3 100644 --- a/examples/simple-examples/src/verification/verifications/callout/start-callout.ts +++ b/examples/simple-examples/src/verification/verifications/phonecall/start-callout.ts @@ -6,6 +6,7 @@ import { printFullResponse, } from '../../../config'; +/** @deprecated see ../../start/start-phonecall.ts instead */ (async () => { console.log('*******************************'); console.log('* StartVerification - callout *'); diff --git a/examples/simple-examples/src/verification/verifications/phonecall/start-phonecall.ts b/examples/simple-examples/src/verification/verifications/phonecall/start-phonecall.ts new file mode 100644 index 00000000..27f5c7b7 --- /dev/null +++ b/examples/simple-examples/src/verification/verifications/phonecall/start-phonecall.ts @@ -0,0 +1,31 @@ +import { Verification } from '@sinch/sdk-core'; +import { + getPrintFormat, + getVerificationIdentityFromConfig, + initVerificationService, + printFullResponse, +} from '../../../config'; + +(async () => { + console.log('**********************************'); + console.log('* StartVerification - phoneCall *'); + console.log('**********************************'); + + const verificationIdentity = getVerificationIdentityFromConfig(); + + const requestData = Verification.startVerificationHelper.buildPhoneCallRequest( + verificationIdentity, + `test-reference-for-callout-verification_${verificationIdentity}`, + ); + + const verificationService = initVerificationService(); + const response = await verificationService.verifications.startPhoneCall(requestData); + + const printFormat = getPrintFormat(process.argv); + + if (printFormat === 'pretty') { + console.log(`Verification ID = ${response.id}`); + } else { + printFullResponse(response); + } +})(); diff --git a/packages/verification/README.md b/packages/verification/README.md index 7a7631a4..5524a2fd 100644 --- a/packages/verification/README.md +++ b/packages/verification/README.md @@ -50,19 +50,22 @@ const sinch = new SinchClient(credentials); const verificationService: VerificationService = sinch.verification; // Build the request data -const requestData: Verification.StartVerificationRequestData = { - initiateVerificationRequestBody: { +const requestData: Verification.StartSmsVerificationRequestData = { + startVerificationWithSmsRequestBody: { identity: { type: 'number', endpoint: '+17813334444', }, - method: 'sms', + smsOptions: { + codeType: 'Alphanumeric', + locale: 'sv-SE', + }, }, }; // Use the 'verification' service registered on the Sinch client -const verificationInitResponse: Verification.InitiateVerificationResponse - = await verificationService.verifications.start(requestData); +const startVerificationResponse: Verification.StartSmsVerificationResponse + = await verificationService.startVerifications.startSms(requestData); ``` ### Standalone @@ -87,19 +90,22 @@ const credentials: ApplicationCredentials = { const verificationService = new VerificationService(credentials); // Build the request data -const requestData: Verification.StartVerificationRequestData = { - initiateVerificationRequestBody: { +const requestData: Verification.StartSmsVerificationRequestData = { + startVerificationWithSmsRequestBody: { identity: { type: 'number', endpoint: '+17813334444', }, - method: 'sms', + smsOptions: { + codeType: 'Alphanumeric', + locale: 'sv-SE', + }, }, }; // Use the standalone declaration of the 'verification' service -const verificationInitResponse: Verification.InitiateVerificationResponse - = await verificationService.verifications.start(requestData); +const startVerificationResponse: Verification.StartSmsVerificationResponse + = await verificationService.startVerifications.startSms(requestData); ``` ## Promises @@ -108,18 +114,18 @@ All the methods that interact with the Sinch APIs use Promises. You can use `awa ```typescript // Method 1: Wait for the Promise to complete (you need to be in an 'async' method) -let verificationInitResponse: Verification.InitiateVerificationResponse; +let startVerificationResponse: Verification.StartSmsVerificationResponse; try { - verificationInitResponse = await verificationService.verifications.start(requestData); - console.log(`Verification ID = ${verificationInitResponse.id}`); + startVerificationResponse = await verificationService.startVerifications.startSms(requestData); + console.log(`Verification ID = ${startVerificationResponse.id}`); } catch (error: any) { - console.error(`ERROR ${error.statusCode}: Impossible to start the verification for the number ${requestData.initiateVerificationRequestBody.identity.endpoint}`); + console.error(`ERROR ${error.statusCode}: Impossible to start the verification for the number ${requestData.startVerificationWithSmsRequestBody.identity.endpoint}`); } // Method 2: Resolve the promise -verificationService.verifications.start(requestData) +verificationService.startVerifications.startSms(requestData) .then(response => console.log(`Verification ID = ${response.id}`)) - .catch(error => console.error(`ERROR ${error.statusCode}: Impossible to start the verification for the number ${requestData.initiateVerificationRequestBody.identity.endpoint}`)); + .catch(error => console.error(`ERROR ${error.statusCode}: Impossible to start the verification for the number ${requestData.startVerificationWithSmsRequestBody.identity.endpoint}`)); ``` ## Contact diff --git a/packages/verification/cucumber.js b/packages/verification/cucumber.js new file mode 100644 index 00000000..691a9809 --- /dev/null +++ b/packages/verification/cucumber.js @@ -0,0 +1,8 @@ +module.exports = { + default: [ + 'tests/e2e/features/**/*.feature', + '--require-module ts-node/register', + '--require tests/rest/v1/**/*.steps.ts', + `--format-options '{"snippetInterface": "synchronous"}'`, + ].join(' '), +}; diff --git a/packages/verification/package.json b/packages/verification/package.json index fe951ea8..1213ac53 100644 --- a/packages/verification/package.json +++ b/packages/verification/package.json @@ -25,7 +25,8 @@ "scripts": { "build": "yarn run clean && yarn run compile", "clean": "rimraf dist tsconfig.tsbuildinfo tsconfig.build.tsbuildinfo", - "compile": "tsc -p tsconfig.build.json && tsc -p tsconfig.tests.json && rimraf dist/tests tsconfig.build.tsbuildinfo" + "compile": "tsc -p tsconfig.build.json && tsc -p tsconfig.tests.json && rimraf dist/tests tsconfig.build.tsbuildinfo", + "test:e2e": "cucumber-js" }, "dependencies": { "@sinch/sdk-client": "^1.1.0" diff --git a/packages/verification/src/models/v1/helper.ts b/packages/verification/src/models/v1/helper.ts index 44735e07..817dcc9f 100644 --- a/packages/verification/src/models/v1/helper.ts +++ b/packages/verification/src/models/v1/helper.ts @@ -6,13 +6,23 @@ import { ReportSmsVerificationByIdentityRequestData, ReportSmsVerificationByIdRequestData, StartCalloutVerificationRequestData, + StartDataVerificationRequestData, StartFlashCallVerificationRequestData, + StartPhoneCallVerificationRequestData, StartSeamlessVerificationRequestData, StartSmsVerificationRequestData, } from './requests'; import { SmsOptions } from './start-verification-request'; export const startVerificationHelper = { + /** + * Builds a request object for starting an SMS verification process. + * + * @param {string} phoneNumber - The phone number to which the verification SMS should be sent. + * @param {string} [reference] - An optional reference identifier used to pass your own reference in the request for tracking purposes. + * @param {SmsOptions} [smsOptions] - Optional parameters for configuring the SMS verification request, with default values assumed for all contained values if not provided. + * @return {StartSmsVerificationRequestData} The constructed SMS verification request data. + */ buildSmsRequest: ( phoneNumber: string, reference?: string, @@ -33,6 +43,44 @@ export const startVerificationHelper = { }, }; }, + /** + * Builds a request object for starting a phone call verification process. + * + * @param {string} phoneNumber - The phone number to which the verification call should be made. + * @param {string} [reference] - An optional reference identifier used to pass your own reference in the request for tracking purposes. + * @param {string} [locale] - An optional language-region identifier to use for the verification call. + * @return {StartPhoneCallVerificationRequestData} The request data object for initiating the phone call verification. + */ + buildPhoneCallRequest: ( + phoneNumber: string, + reference?: string, + locale?: string, + ): StartPhoneCallVerificationRequestData => { + return { + startVerificationWithPhoneCallRequestBody: { + identity: { + type: 'number', + endpoint: phoneNumber, + }, + reference, + ...(locale !== undefined) ? { + phoneCallOptions: { + speech: { + locale, + }, + }, + } : {}, + }, + }; + }, + /** + * Builds a callout request body with the provided phone number, reference, and locale. + * + * @param {string} phoneNumber - The phone number to which the callout will be made. + * @param {string} [reference] - An optional reference identifier for the callout. + * @param {string} [locale] - An optional locale string to specify the language or region for the callout. + * @return {StartCalloutVerificationRequestData} The constructed callout request object. + * @deprecated */ buildCalloutRequest: ( phoneNumber: string, reference?: string, @@ -55,6 +103,14 @@ export const startVerificationHelper = { }, }; }, + /** + * Builds a request object for starting a flash call verification process. + * + * @param {string} phoneNumber - The phone number to which the flash call verification should be made. + * @param {string} [reference] - An optional reference identifier used to pass your own reference in the request for tracking purposes. + * @param {number} [dialTimeout] - An optional timeout value in seconds for how long to wait for the flash call to be answered. + * @return {StartFlashCallVerificationRequestData} The request data object for initiating the flash call verification. + */ buildFlashCallRequest: ( phoneNumber: string, reference?: string, @@ -75,6 +131,35 @@ export const startVerificationHelper = { }, }; }, + /** + * Builds a request object for initiating a data verification process. + * + * @param {string} phoneNumber - The phone number to be verified. + * @param {string} [reference] - An optional reference identifier used to pass your own reference in the request for tracking purposes. + * @return {StartDataVerificationRequestData} The request data object used to start the data verification. + */ + buildDataRequest: ( + phoneNumber: string, + reference?: string, + ): StartDataVerificationRequestData => { + return { + startDataVerificationRequestBody: { + identity: { + type: 'number', + endpoint: phoneNumber, + }, + reference, + }, + }; + }, + /** + * Builds a seamless verification request body with the provided phone number and optional reference. + * + * @param {string} phoneNumber - The phone number to verify. + * @param {string} [reference] - An optional reference identifier for the verification request. + * @return {StartSeamlessVerificationRequestData} The constructed seamless verification request data. + * @deprecated + */ buildSeamlessRequest: ( phoneNumber: string, reference?: string, diff --git a/packages/verification/src/models/v1/index.ts b/packages/verification/src/models/v1/index.ts index eeb783e2..cea9c020 100644 --- a/packages/verification/src/models/v1/index.ts +++ b/packages/verification/src/models/v1/index.ts @@ -6,16 +6,16 @@ export * from './verification-report-request'; export * from './start-sms-verification-response'; export * from './sms-verification-report-response'; export * from './sms-verification-status-response'; -// Models associated to Callout verification workflow -export * from './start-callout-verification-response'; +// Models associated to PhoneCall verification workflow +export * from './start-phonecall-verification-response'; export * from './callout-verification-report-response'; export * from './callout-verification-status-response'; // Models associated to Flashcall verification workflow export * from './start-flashcall-verification-response'; export * from './flashcall-verification-report-response'; export * from './flashcall-verification-status-response'; -// Models associated to Seamless verification workflow -export * from './start-seamless-verification-response'; +// Models associated to Data verification workflow +export * from './start-data-verification-response'; // Wrapper for the various types of Verification Status Response export * from './verification-status-response'; // Models associated to callback events diff --git a/packages/verification/src/models/v1/requests/verifications/verifications-request-data.ts b/packages/verification/src/models/v1/requests/verifications/verifications-request-data.ts index 2c8e064a..0842d3d0 100644 --- a/packages/verification/src/models/v1/requests/verifications/verifications-request-data.ts +++ b/packages/verification/src/models/v1/requests/verifications/verifications-request-data.ts @@ -4,9 +4,11 @@ import { SmsVerificationReportRequest, } from '../../verification-report-request'; import { + StartDataVerification, StartSeamlessVerification, StartVerificationWithCallout, StartVerificationWithFlashCall, + StartVerificationWithPhoneCall, StartVerificationWithSms, } from '../../start-verification-request'; @@ -60,11 +62,22 @@ export interface StartFlashCallVerificationRequestData { 'startVerificationWithFlashCallRequestBody': StartVerificationWithFlashCall; } +export interface StartPhoneCallVerificationRequestData { + 'startVerificationWithPhoneCallRequestBody': StartVerificationWithPhoneCall; +} + +/** @deprecated */ export interface StartCalloutVerificationRequestData { /** Request body to start a verification with a callout */ 'startVerificationWithCalloutRequestBody': StartVerificationWithCallout; } +export interface StartDataVerificationRequestData { + /** Request body to start a seamless verification */ + 'startDataVerificationRequestBody': StartDataVerification; +} + +/** @deprecated */ export interface StartSeamlessVerificationRequestData { /** Request body to start a seamless verification */ 'startSeamlessVerificationRequestBody': StartSeamlessVerification; diff --git a/packages/verification/src/models/v1/start-callout-verification-response/index.ts b/packages/verification/src/models/v1/start-callout-verification-response/index.ts deleted file mode 100644 index 1700cf35..00000000 --- a/packages/verification/src/models/v1/start-callout-verification-response/index.ts +++ /dev/null @@ -1 +0,0 @@ -export type { StartCalloutVerificationResponse } from './start-callout-verification-response'; diff --git a/packages/verification/src/models/v1/start-data-verification-response/index.ts b/packages/verification/src/models/v1/start-data-verification-response/index.ts new file mode 100644 index 00000000..3d6f1c8f --- /dev/null +++ b/packages/verification/src/models/v1/start-data-verification-response/index.ts @@ -0,0 +1,4 @@ +export type { + StartSeamlessVerificationResponse, + StartDataVerificationResponse, +} from './start-data-verification-response'; diff --git a/packages/verification/src/models/v1/start-seamless-verification-response/start-seamless-verification-response.ts b/packages/verification/src/models/v1/start-data-verification-response/start-data-verification-response.ts similarity index 76% rename from packages/verification/src/models/v1/start-seamless-verification-response/start-seamless-verification-response.ts rename to packages/verification/src/models/v1/start-data-verification-response/start-data-verification-response.ts index 8adc5156..0d4f1160 100644 --- a/packages/verification/src/models/v1/start-seamless-verification-response/start-seamless-verification-response.ts +++ b/packages/verification/src/models/v1/start-data-verification-response/start-data-verification-response.ts @@ -1,7 +1,9 @@ import { LinksObject } from '../links-object'; -export interface StartSeamlessVerificationResponse { +/** @deprecated */ +export type StartSeamlessVerificationResponse = StartDataVerificationResponse; +export interface StartDataVerificationResponse { /** Verification identifier used to query for status. */ id: string; /** The value of the method used for the Verification. For Data Verifications, this will always be `seamless`. */ @@ -13,7 +15,6 @@ export interface StartSeamlessVerificationResponse { } interface SeamlessContent { - /** The target URI. */ targetUri?: string; } diff --git a/packages/verification/src/models/v1/start-phonecall-verification-response/index.ts b/packages/verification/src/models/v1/start-phonecall-verification-response/index.ts new file mode 100644 index 00000000..b282e686 --- /dev/null +++ b/packages/verification/src/models/v1/start-phonecall-verification-response/index.ts @@ -0,0 +1,4 @@ +export type { + StartCalloutVerificationResponse, + StartPhoneCallVerificationResponse, +} from './start-phonecall-verification-response'; diff --git a/packages/verification/src/models/v1/start-callout-verification-response/start-callout-verification-response.ts b/packages/verification/src/models/v1/start-phonecall-verification-response/start-phonecall-verification-response.ts similarity index 67% rename from packages/verification/src/models/v1/start-callout-verification-response/start-callout-verification-response.ts rename to packages/verification/src/models/v1/start-phonecall-verification-response/start-phonecall-verification-response.ts index ced322ae..7d407f37 100644 --- a/packages/verification/src/models/v1/start-callout-verification-response/start-callout-verification-response.ts +++ b/packages/verification/src/models/v1/start-phonecall-verification-response/start-phonecall-verification-response.ts @@ -1,7 +1,9 @@ import { LinksObject } from '../links-object'; -export interface StartCalloutVerificationResponse { +/** @deprecated */ +export type StartCalloutVerificationResponse = StartPhoneCallVerificationResponse; +export interface StartPhoneCallVerificationResponse { /** Verification identifier used to query for status. */ id?: string; /** The value of the method used for the Verification. For Phone Call Verifications, this will always be `callout`. */ diff --git a/packages/verification/src/models/v1/start-seamless-verification-response/index.ts b/packages/verification/src/models/v1/start-seamless-verification-response/index.ts deleted file mode 100644 index 45916e62..00000000 --- a/packages/verification/src/models/v1/start-seamless-verification-response/index.ts +++ /dev/null @@ -1 +0,0 @@ -export type { StartSeamlessVerificationResponse } from './start-seamless-verification-response'; diff --git a/packages/verification/src/models/v1/start-verification-request/index.ts b/packages/verification/src/models/v1/start-verification-request/index.ts index 8cd6484e..90fb96f1 100644 --- a/packages/verification/src/models/v1/start-verification-request/index.ts +++ b/packages/verification/src/models/v1/start-verification-request/index.ts @@ -2,11 +2,16 @@ export type { StartVerificationBase, StartVerificationWithSms, StartVerificationWithFlashCall, + StartVerificationWithPhoneCall, + StartVerificationWithPhoneCallServerModel, StartVerificationWithCallout, + StartDataVerification, StartSeamlessVerification, SmsOptions, CodeType, + PhoneCallOptions, CalloutOptions, + PhoneCallOptionsSpeech, CalloutOptionsSpeech, FlashCallOptions, } from './start-verification-request'; diff --git a/packages/verification/src/models/v1/start-verification-request/start-verification-request.ts b/packages/verification/src/models/v1/start-verification-request/start-verification-request.ts index cc32b2d3..f49dd57b 100644 --- a/packages/verification/src/models/v1/start-verification-request/start-verification-request.ts +++ b/packages/verification/src/models/v1/start-verification-request/start-verification-request.ts @@ -10,11 +10,25 @@ export interface StartVerificationWithFlashCall extends StartVerificationBase { flashCallOptions?: FlashCallOptions; } +export interface StartVerificationWithPhoneCall extends StartVerificationBase { + /** @see PhoneCallOptions */ + phoneCallOptions?: PhoneCallOptions; +} + +export interface StartVerificationWithPhoneCallServerModel extends StartVerificationBase { + /** @see CalloutOptions */ + calloutOptions?: CalloutOptions; +} + +/** @deprecated */ export interface StartVerificationWithCallout extends StartVerificationBase { /** @see CalloutOptions */ calloutOptions?: CalloutOptions; } +export interface StartDataVerification extends StartVerificationBase {} + +/** @deprecated */ export interface StartSeamlessVerification extends StartVerificationBase {} export interface StartVerificationBase { @@ -50,18 +64,23 @@ export interface FlashCallOptions { dialTimeout?: number; } +/** @deprecated */ +export type CalloutOptions = PhoneCallOptions; + /** * An optional object for Phone Call Verification, with default values assumed for all contained values if not provided. */ -export interface CalloutOptions { - /** @see CalloutOptionsSpeech */ - speech?: CalloutOptionsSpeech; +export interface PhoneCallOptions { + /** @see PhoneCallOptionsSpeech */ + speech?: PhoneCallOptionsSpeech; } +/** @deprecated */ +export type CalloutOptionsSpeech = PhoneCallOptionsSpeech; /** * Text-To-Speech engine settings */ -export interface CalloutOptionsSpeech { +export interface PhoneCallOptionsSpeech { /** A `language-region` identifier according to [IANA](https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry). Only a subset of those identifiers is accepted. */ locale?: string; } diff --git a/packages/verification/src/rest/v1/verifications/verifications-api.jest.fixture.ts b/packages/verification/src/rest/v1/verifications/verifications-api.jest.fixture.ts index 0343520e..ee77028b 100644 --- a/packages/verification/src/rest/v1/verifications/verifications-api.jest.fixture.ts +++ b/packages/verification/src/rest/v1/verifications/verifications-api.jest.fixture.ts @@ -17,6 +17,10 @@ import { ReportSmsVerificationByIdentityRequestData, ReportFlashCallVerificationByIdentityRequestData, ReportCalloutVerificationByIdentityRequestData, + StartDataVerificationRequestData, + StartPhoneCallVerificationResponse, + StartPhoneCallVerificationRequestData, + StartDataVerificationResponse, } from '../../../models'; export class VerificationsApiFixture implements Partial> { @@ -72,6 +76,18 @@ export class VerificationsApiFixture implements Partial, [StartFlashCallVerificationRequestData]> = jest.fn(); + /** + * Fixture associated to function startPhoneCall + */ + public startPhoneCall: jest.Mock, + [StartPhoneCallVerificationRequestData]> = jest.fn(); + /** + * Fixture associated to function startData + */ + public startData: jest.Mock< + Promise, + [StartDataVerificationRequestData]> = jest.fn(); /** * Fixture associated to function startCallout */ diff --git a/packages/verification/src/rest/v1/verifications/verifications-api.ts b/packages/verification/src/rest/v1/verifications/verifications-api.ts index 0a38a183..0b34400d 100644 --- a/packages/verification/src/rest/v1/verifications/verifications-api.ts +++ b/packages/verification/src/rest/v1/verifications/verifications-api.ts @@ -17,6 +17,11 @@ import { StartCalloutVerificationRequestData, StartSeamlessVerificationRequestData, StartVerificationWithSms, + StartPhoneCallVerificationRequestData, + StartPhoneCallVerificationResponse, + StartVerificationWithPhoneCall, + StartDataVerificationRequestData, + StartDataVerificationResponse, } from '../../../models'; import { RequestBody, @@ -251,7 +256,7 @@ export class VerificationsApi extends VerificationDomainApi { (data.startVerificationWithSmsRequestBody as any).method = 'sms'; const getParams = this.client.extractQueryParams(data, [] as never[]); const headers: { [key: string]: string | undefined } = { - 'Content-Type': 'application/json; charset=UTF-8', + 'Content-Type': 'application/json', 'Accept': 'application/json', }; if (data.startVerificationWithSmsRequestBody.smsOptions?.locale !== undefined) { @@ -303,6 +308,7 @@ export class VerificationsApi extends VerificationDomainApi { return `${formattedHours}:${formattedMinutes}:${formattedSeconds}`; } + /** * Start verification with a FlashCall * This method is used by the mobile and web Verification SDKs to start a verification. It can also be used to request a verification from your backend, by making a request. @@ -315,7 +321,7 @@ export class VerificationsApi extends VerificationDomainApi { (data.startVerificationWithFlashCallRequestBody as any).method = 'flashcall'; const getParams = this.client.extractQueryParams(data, [] as never[]); const headers: { [key: string]: string | undefined } = { - 'Content-Type': 'application/json; charset=UTF-8', + 'Content-Type': 'application/json', 'Accept': 'application/json', }; @@ -337,10 +343,91 @@ export class VerificationsApi extends VerificationDomainApi { }); } + /** + * Start verification with a phone call + * This method is used by the mobile and web Verification SDKs to start a verification. It can also be used to request a verification from your backend, by making a request. + * @param { StartPhoneCallVerificationRequestData } data - The data to provide to the API call. + */ + public async startPhoneCall( + data: StartPhoneCallVerificationRequestData, + ): Promise { + this.client = this.getSinchClient(); + (data.startVerificationWithPhoneCallRequestBody as any).method = 'callout'; + const getParams = this.client.extractQueryParams(data, [] as never[]); + const headers: { [key: string]: string | undefined } = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }; + + // Special fields handling: see method for details + const requestDataBody = this.performStartPhoneCallRequestBodyTransformation( + data.startVerificationWithPhoneCallRequestBody); + + const body: RequestBody = requestDataBody + ? JSON.stringify(requestDataBody) + : '{}'; + + const path = '/verification/v1/verifications'; + const basePathUrl = this.client.apiClientOptions.hostname + path; + + const requestOptions + = await this.client.prepareOptions(basePathUrl, 'POST', getParams, headers, body || undefined, path); + const url = this.client.prepareUrl(requestOptions.hostname, requestOptions.queryParams); + + return this.client.processCall({ + url, + requestOptions, + apiName: this.apiName, + operationId: 'StartVerificationWithPhoneCall', + }); + } + + performStartPhoneCallRequestBodyTransformation(body: StartVerificationWithPhoneCall): StartVerificationWithPhoneCall { + const requestDataBody = { ...body }; + if (requestDataBody.phoneCallOptions !== undefined) { + (requestDataBody as any).calloutOptions = { ...requestDataBody.phoneCallOptions }; + delete requestDataBody.phoneCallOptions; + } + return requestDataBody; + } + + /** + * Start data verification + * This method is used by the mobile and web Verification SDKs to start a verification. It can also be used to request a verification from your backend, by making a request. + * @param { StartDataVerificationRequestData } data - The data to provide to the API call. + */ + public async startData(data: StartDataVerificationRequestData): Promise { + this.client = this.getSinchClient(); + (data.startDataVerificationRequestBody as any).method = 'seamless'; + const getParams = this.client.extractQueryParams(data, [] as never[]); + const headers: { [key: string]: string | undefined } = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }; + + const body: RequestBody = data['startDataVerificationRequestBody'] + ? JSON.stringify(data['startDataVerificationRequestBody']) + : '{}'; + const path = '/verification/v1/verifications'; + const basePathUrl = this.client.apiClientOptions.hostname + path; + + const requestOptions + = await this.client.prepareOptions(basePathUrl, 'POST', getParams, headers, body || undefined, path); + const url = this.client.prepareUrl(requestOptions.hostname, requestOptions.queryParams); + + return this.client.processCall({ + url, + requestOptions, + apiName: this.apiName, + operationId: 'StartDataVerification', + }); + } + /** * Start verification with a callout * This method is used by the mobile and web Verification SDKs to start a verification. It can also be used to request a verification from your backend, by making a request. * @param { StartCalloutVerificationRequestData } data - The data to provide to the API call. + * @deprecated */ public async startCallout(data: StartCalloutVerificationRequestData): Promise { this.client = this.getSinchClient(); @@ -373,6 +460,7 @@ export class VerificationsApi extends VerificationDomainApi { * Start seamless verification (= data verification) * This method is used by the mobile and web Verification SDKs to start a verification. It can also be used to request a verification from your backend, by making a request. * @param { StartSeamlessVerificationRequestData } data - The data to provide to the API call. + * @deprecated */ public async startSeamless(data: StartSeamlessVerificationRequestData): Promise { this.client = this.getSinchClient(); diff --git a/packages/verification/tests/rest/v1/start/start.steps.ts b/packages/verification/tests/rest/v1/start/start.steps.ts new file mode 100644 index 00000000..f811b864 --- /dev/null +++ b/packages/verification/tests/rest/v1/start/start.steps.ts @@ -0,0 +1,139 @@ +import { VerificationsApi, VerificationService, Verification } from '../../../../src'; +import { Given, When, Then } from '@cucumber/cucumber'; +import * as assert from 'assert'; +import { RequestFailedError } from '@sinch/sdk-client/src'; + +let startVerificationApi: VerificationsApi; +let startSmsVerificationResponse: Verification.StartSmsVerificationResponse; +let startPhoneCallVerificationResponse: Verification.StartPhoneCallVerificationResponse; +let startFlashCallVerificationResponse: Verification.StartFlashCallVerificationResponse; +let startDataVerificationResponseError: RequestFailedError; + +Given('the Verification service "Start" is available', () => { + const verificationService = new VerificationService({ + applicationKey: 'appKey', + applicationSecret: 'appSecret', + verificationHostname: 'http://localhost:3018', + }); + startVerificationApi = verificationService.verifications; +}); + +When('I send a request to start a verification with a SMS', async () => { + startSmsVerificationResponse = await startVerificationApi.startSms({ + startVerificationWithSmsRequestBody: { + identity: { + type: 'number', + endpoint: '+46123456789', + }, + smsOptions: { + codeType: 'Alphanumeric', + locale: 'sv-SE', + }, + }, + }); +}); + +Then('the response contains the details of a verification started with a SMS', () => { + assert.equal(startSmsVerificationResponse.id, '1ce0ffee-c0de-5eed-d00d-f00dfeed1337'); + assert.equal(startSmsVerificationResponse.method, 'sms'); + assert.ok(startSmsVerificationResponse.sms); + assert.equal(startSmsVerificationResponse.sms.template, 'Din verifieringskod รคr {{CODE}}.'); + assert.equal(startSmsVerificationResponse.sms.interceptionTimeout, 198); + assert.ok(startSmsVerificationResponse._links); + const statusLink = startSmsVerificationResponse._links[0]; + assert.equal(statusLink.rel, 'status'); + assert.equal(statusLink.href, 'http://localhost:3018/verification/v1/verifications/id/1ce0ffee-c0de-5eed-d00d-f00dfeed1337'); + assert.equal(statusLink.method, 'GET'); + const reportLink = startSmsVerificationResponse._links[1]; + assert.equal(reportLink.rel, 'report'); + assert.equal(reportLink.href, 'http://localhost:3018/verification/v1/verifications/id/1ce0ffee-c0de-5eed-d00d-f00dfeed1337'); + assert.equal(reportLink.method, 'PUT'); +}); + +When('I send a request to start a verification with a Phone Call', async () => { + startPhoneCallVerificationResponse = await startVerificationApi.startPhoneCall({ + startVerificationWithPhoneCallRequestBody: { + identity: { + type: 'number', + endpoint: '+33612345678', + }, + phoneCallOptions: { + speech: { + locale: 'fr-FR', + }, + }, + }, + }); +}); + +Then('the response contains the details of a verification started with a Phone Call', () => { + assert.equal(startPhoneCallVerificationResponse.id, '1ce0ffee-c0de-5eed-d11d-f00dfeed1337'); + assert.equal(startPhoneCallVerificationResponse.method, 'callout'); + assert.ok(startPhoneCallVerificationResponse._links); + const statusLink = startPhoneCallVerificationResponse._links[0]; + assert.equal(statusLink.rel, 'status'); + assert.equal(statusLink.href, 'http://localhost:3018/verification/v1/verifications/id/1ce0ffee-c0de-5eed-d11d-f00dfeed1337'); + assert.equal(statusLink.method, 'GET'); + const reportLink = startPhoneCallVerificationResponse._links[1]; + assert.equal(reportLink.rel, 'report'); + assert.equal(reportLink.href, 'http://localhost:3018/verification/v1/verifications/id/1ce0ffee-c0de-5eed-d11d-f00dfeed1337'); + assert.equal(reportLink.method, 'PUT'); +}); + +When('I send a request to start a verification with a Flash Call', async () => { + startFlashCallVerificationResponse = await startVerificationApi.startFlashCall({ + startVerificationWithFlashCallRequestBody: { + identity: { + type: 'number', + endpoint: '+33612345678', + }, + flashCallOptions: { + dialTimeout: 10, + }, + reference: 'flashcall-verification-test-e2e', + }, + }); +}); + +Then('the response contains the details of a verification started with a Flash Call', () => { + assert.equal(startFlashCallVerificationResponse.id, '1ce0ffee-c0de-5eed-d22d-f00dfeed1337'); + assert.equal(startFlashCallVerificationResponse.method, 'flashcall'); + assert.ok(startFlashCallVerificationResponse.flashCall); + assert.equal(startFlashCallVerificationResponse.flashCall.cliFilter, '(.*)8156(.*)'); + assert.equal(startFlashCallVerificationResponse.flashCall.interceptionTimeout, 45); + assert.equal(startFlashCallVerificationResponse.flashCall.reportTimeout, 75); + assert.equal(startFlashCallVerificationResponse.flashCall.denyCallAfter, 0); + assert.ok(startFlashCallVerificationResponse._links); + const statusLink = startFlashCallVerificationResponse._links[0]; + assert.equal(statusLink.rel, 'status'); + assert.equal(statusLink.href, 'http://localhost:3018/verification/v1/verifications/id/1ce0ffee-c0de-5eed-d22d-f00dfeed1337'); + assert.equal(statusLink.method, 'GET'); + const reportLink = startFlashCallVerificationResponse._links[1]; + assert.equal(reportLink.rel, 'report'); + assert.equal(reportLink.href, 'http://localhost:3018/verification/v1/verifications/id/1ce0ffee-c0de-5eed-d22d-f00dfeed1337'); + assert.equal(reportLink.method, 'PUT'); +}); + +When('I send a request to start a Data verification for a not available destination', async () => { + try { + await startVerificationApi.startData({ + startDataVerificationRequestBody: { + identity: { + type: 'number', + endpoint: '+17818880008', + }, + }, + }); + } catch (e: any) { + startDataVerificationResponseError = e as RequestFailedError; + } +}); + +Then('the response contains the error details of a Data verification', () => { + assert.equal(startDataVerificationResponseError.statusCode, 403); + assert.ok(startDataVerificationResponseError.data); + const errorDetails = JSON.parse(startDataVerificationResponseError.data) as Verification.VerificationError; + assert.equal(errorDetails.errorCode, 40300); + assert.equal(errorDetails.message, 'Seamless verification not available for given destination.'); + assert.equal(errorDetails.reference, 'c01dc0de-c4db-44f1-5ca1-da9159d21c191'); +}); diff --git a/packages/verification/tests/rest/v1/verifications/verifications-api.test.ts b/packages/verification/tests/rest/v1/verifications/verifications-api.test.ts index 13256578..5c2d42dc 100644 --- a/packages/verification/tests/rest/v1/verifications/verifications-api.test.ts +++ b/packages/verification/tests/rest/v1/verifications/verifications-api.test.ts @@ -59,67 +59,90 @@ describe('VerificationsApi', () => { expect(fixture.startSms).toHaveBeenCalledWith(requestData); }); - it('should format the expiry field', () => { - const requestData = Verification.startVerificationHelper.buildSmsRequest( - '+46700000000', - undefined, - { expiry: new Date('2024-02-10T13:22:34.685Z') }); - const expectedResult: Verification.StartVerificationWithSms = { - identity: { - endpoint: '+46700000000', - type: 'number', - }, - smsOptions: { - expiry: '13:22:34', + it('should make a POST request to start a verification with a FlashCall', async () => { + // Given + const requestData = Verification.startVerificationHelper.buildFlashCallRequest('+46700000000', undefined, 30); + const expectedResponse: Verification.StartFlashCallVerificationResponse = { + id: 'some_verification_id', + method: 'flashcall', + flashCall: { + cliFilter: '(.*)70123(.*)', + interceptionTimeout: 60, + reportTimeout: 120, + denyCallAfter: 120, }, + _links, }; - const formattedRequestData - = verificationsApi.performStartSmsRequestBodyTransformation(requestData.startVerificationWithSmsRequestBody); - expect(formattedRequestData).toEqual(expectedResult); + + // When + fixture.startFlashCall.mockResolvedValue(expectedResponse); + verificationsApi.startFlashCall = fixture.startFlashCall; + const response = await verificationsApi.startFlashCall(requestData); + + // Then + expect(response).toEqual(expectedResponse); + expect(fixture.startFlashCall).toHaveBeenCalledWith(requestData); }); - it('should leave the expiry field unchanged', () => { - const requestData = Verification.startVerificationHelper.buildSmsRequest( + it('should make a POST request to start a verification with a PhoneCall', async () => { + // Given + const requestData = Verification.startVerificationHelper.buildPhoneCallRequest('+46700000000'); + const expectedResponse: Verification.StartPhoneCallVerificationResponse = { + id: 'some_verification_id', + method: 'callout', + _links, + }; + + // When + fixture.startPhoneCall.mockResolvedValue(expectedResponse); + verificationsApi.startPhoneCall = fixture.startPhoneCall; + const response = await verificationsApi.startPhoneCall(requestData); + + // Then + expect(response).toEqual(expectedResponse); + expect(fixture.startPhoneCall).toHaveBeenCalledWith(requestData); + }); + + it('should format the startPhoneCall request body', () => { + const requestData = Verification.startVerificationHelper.buildPhoneCallRequest( '+46700000000', undefined, - { expiry: '15:15:15' }); - const expectedResult: Verification.StartVerificationWithSms = { + 'en-US'); + const expectedResult: Verification.StartVerificationWithPhoneCallServerModel = { identity: { endpoint: '+46700000000', type: 'number', }, - smsOptions: { - expiry: '15:15:15', + calloutOptions: { + speech: { + locale: 'en-US', + }, }, }; - const formattedRequestData - = verificationsApi.performStartSmsRequestBodyTransformation(requestData.startVerificationWithSmsRequestBody); + const formattedRequestData = verificationsApi.performStartPhoneCallRequestBodyTransformation( + requestData.startVerificationWithPhoneCallRequestBody); expect(formattedRequestData).toEqual(expectedResult); }); - it('should make a POST request to start a verification with a FlashCall', async () => { + it('should make a POST request to start a data verification (seamless)', async () => { // Given - const requestData = Verification.startVerificationHelper.buildFlashCallRequest('+46700000000', undefined, 30); - const expectedResponse: Verification.StartFlashCallVerificationResponse = { + const requestData = Verification.startVerificationHelper.buildDataRequest('+46700000000'); + const expectedResponse: Verification.StartDataVerificationResponse = { id: 'some_verification_id', - method: 'flashcall', - flashCall: { - cliFilter: '(.*)70123(.*)', - interceptionTimeout: 60, - reportTimeout: 120, - denyCallAfter: 120, + method: 'seamless', + seamless: { + targetUri: 'https://target-uri.com', }, - _links, }; // When - fixture.startFlashCall.mockResolvedValue(expectedResponse); - verificationsApi.startFlashCall = fixture.startFlashCall; - const response = await verificationsApi.startFlashCall(requestData); + fixture.startData.mockResolvedValue(expectedResponse); + verificationsApi.startData = fixture.startData; + const response = await verificationsApi.startData(requestData); // Then expect(response).toEqual(expectedResponse); - expect(fixture.startFlashCall).toHaveBeenCalledWith(requestData); + expect(fixture.startData).toHaveBeenCalledWith(requestData); }); it('should make a POST request to start a verification with a Callout', async () => { @@ -141,7 +164,7 @@ describe('VerificationsApi', () => { expect(fixture.startCallout).toHaveBeenCalledWith(requestData); }); - it('should make a POST request to start a data verification (seamless)', async () => { + it('should make a POST request to start a seamless verification', async () => { // Given const requestData = Verification.startVerificationHelper.buildSeamlessRequest('+46700000000'); const expectedResponse: Verification.StartSeamlessVerificationResponse = { From 0a321c1a4a0cc9012ff96cdf7bc6c5c715004dd9 Mon Sep 17 00:00:00 2001 From: Antoine SEIN <142824551+asein-sinch@users.noreply.github.com> Date: Wed, 28 Aug 2024 10:41:46 +0200 Subject: [PATCH 36/54] DEVEXP-521: E2E Verification/Report (#129) --- .github/workflows/run-ci.yaml | 4 + .../src/verification/app.ts | 4 +- .../verifications/data/start-seamless.ts | 2 +- .../phonecall/report-with-id_callout.ts | 1 + .../phonecall/report-with-id_phonecall.ts | 32 +++++ .../phonecall/report-with-identity_callout.ts | 1 + .../report-with-identity_phonecall.ts | 32 +++++ .../verifications/phonecall/start-callout.ts | 2 +- .../callout-verification-report-response.ts | 4 +- .../index.ts | 5 +- packages/verification/src/models/v1/helper.ts | 88 +++++++++++++ .../verifications-request-data.ts | 13 ++ .../start-verification-request.ts | 4 +- .../v1/verification-report-request/index.ts | 2 + .../verification-report-request.ts | 16 ++- .../verifications-api.jest.fixture.ts | 16 +++ .../v1/verifications/verifications-api.ts | 108 +++++++++++++++- .../rest/v1/verifications/report.steps.ts | 117 ++++++++++++++++++ .../{start => verifications}/start.steps.ts | 0 .../verifications/verifications-api.test.ts | 75 +++++++++++ 20 files changed, 516 insertions(+), 10 deletions(-) create mode 100644 examples/simple-examples/src/verification/verifications/phonecall/report-with-id_phonecall.ts create mode 100644 examples/simple-examples/src/verification/verifications/phonecall/report-with-identity_phonecall.ts create mode 100644 packages/verification/tests/rest/v1/verifications/report.steps.ts rename packages/verification/tests/rest/v1/{start => verifications}/start.steps.ts (100%) diff --git a/.github/workflows/run-ci.yaml b/.github/workflows/run-ci.yaml index ab1da6a7..846bee5f 100644 --- a/.github/workflows/run-ci.yaml +++ b/.github/workflows/run-ci.yaml @@ -55,12 +55,16 @@ jobs: mkdir -p ./packages/numbers/tests/e2e/features mkdir -p ./packages/conversation/tests/e2e/features mkdir -p ./packages/elastic-sip-trunking/tests/e2e/features + mkdir -p ./packages/sms/tests/e2e/features + mkdir -p ./packages/verification/tests/e2e/features - name: Copy feature files run: | cp sinch-sdk-mockserver/features/fax/*.feature ./packages/fax/tests/e2e/features/ cp sinch-sdk-mockserver/features/numbers/*.feature ./packages/numbers/tests/e2e/features/ cp sinch-sdk-mockserver/features/conversation/*.feature ./packages/conversation/tests/e2e/features/ cp sinch-sdk-mockserver/features/elastic-sip-trunking/*.feature ./packages/elastic-sip-trunking/tests/e2e/features/ + cp sinch-sdk-mockserver/features/sms/*.feature ./packages/sms/tests/e2e/features/ + cp sinch-sdk-mockserver/features/verification/*.feature ./packages/verification/tests/e2e/features/ - name: Run e2e tests run: | yarn install diff --git a/examples/integrated-flows-examples/src/verification/app.ts b/examples/integrated-flows-examples/src/verification/app.ts index 138a6830..1aadd449 100644 --- a/examples/integrated-flows-examples/src/verification/app.ts +++ b/examples/integrated-flows-examples/src/verification/app.ts @@ -83,9 +83,9 @@ dotenv.config(); message: 'Enter the verification code:', }, ]); - const reportRequestData = Verification.reportVerificationByIdHelper.buildCalloutRequest( + const reportRequestData = Verification.reportVerificationByIdHelper.buildPhoneCallRequest( response.id!, answers.code); - const reportResponse = await sinch.verification.verifications.reportCalloutById(reportRequestData); + const reportResponse = await sinch.verification.verifications.reportPhoneCallById(reportRequestData); console.log(`Verification status: ${reportResponse.status}${reportResponse.status === 'SUCCESSFUL'?'':' - Reason: ' + reportResponse.reason}`); }; diff --git a/examples/simple-examples/src/verification/verifications/data/start-seamless.ts b/examples/simple-examples/src/verification/verifications/data/start-seamless.ts index f6ea52b2..e0202ba9 100644 --- a/examples/simple-examples/src/verification/verifications/data/start-seamless.ts +++ b/examples/simple-examples/src/verification/verifications/data/start-seamless.ts @@ -6,7 +6,7 @@ import { printFullResponse, } from '../../../config'; -/** @deprecated see ../../start/start-data.ts instead */ +/** @deprecated see ./start-data.ts instead */ (async () => { console.log('********************************'); console.log('* StartVerification - seamless *'); diff --git a/examples/simple-examples/src/verification/verifications/phonecall/report-with-id_callout.ts b/examples/simple-examples/src/verification/verifications/phonecall/report-with-id_callout.ts index 60575953..1bdaefa8 100644 --- a/examples/simple-examples/src/verification/verifications/phonecall/report-with-id_callout.ts +++ b/examples/simple-examples/src/verification/verifications/phonecall/report-with-id_callout.ts @@ -7,6 +7,7 @@ import { printFullResponse, } from '../../../config'; +/** @deprecated see ./report-with-id_phonecall.ts instead */ (async () => { console.log('************************************'); console.log('* ReportVerificationById - callout *'); diff --git a/examples/simple-examples/src/verification/verifications/phonecall/report-with-id_phonecall.ts b/examples/simple-examples/src/verification/verifications/phonecall/report-with-id_phonecall.ts new file mode 100644 index 00000000..b4c6ec67 --- /dev/null +++ b/examples/simple-examples/src/verification/verifications/phonecall/report-with-id_phonecall.ts @@ -0,0 +1,32 @@ +import { Verification } from '@sinch/sdk-core'; +import { + getPrintFormat, + getVerificationCodeFromConfig, + getVerificationIdFromConfig, + initVerificationService, + printFullResponse, +} from '../../../config'; + +(async () => { + console.log('**************************************'); + console.log('* ReportVerificationById - phoneCall *'); + console.log('**************************************'); + + const verificationId = getVerificationIdFromConfig(); + const verificationCode = getVerificationCodeFromConfig(); + + const requestData = Verification.reportVerificationByIdHelper.buildPhoneCallRequest( + verificationId, verificationCode); + + const verificationService = initVerificationService(); + const response = await verificationService.verifications.reportPhoneCallById(requestData); + + const printFormat = getPrintFormat(process.argv); + + if (printFormat === 'pretty') { + console.log(`Phone call verification status: ${response.status}${response.status === 'SUCCESSFUL'?'':' - Reason: ' + response.reason}`); + } else { + printFullResponse(response); + } + +})(); diff --git a/examples/simple-examples/src/verification/verifications/phonecall/report-with-identity_callout.ts b/examples/simple-examples/src/verification/verifications/phonecall/report-with-identity_callout.ts index 23662c77..0ae33e59 100644 --- a/examples/simple-examples/src/verification/verifications/phonecall/report-with-identity_callout.ts +++ b/examples/simple-examples/src/verification/verifications/phonecall/report-with-identity_callout.ts @@ -7,6 +7,7 @@ import { printFullResponse, } from '../../../config'; +/** @deprecated see ./report-with-identity_phonecall.ts instead */ (async () => { console.log('******************************************'); console.log('* ReportVerificationByIdentity - callout *'); diff --git a/examples/simple-examples/src/verification/verifications/phonecall/report-with-identity_phonecall.ts b/examples/simple-examples/src/verification/verifications/phonecall/report-with-identity_phonecall.ts new file mode 100644 index 00000000..b677c8d0 --- /dev/null +++ b/examples/simple-examples/src/verification/verifications/phonecall/report-with-identity_phonecall.ts @@ -0,0 +1,32 @@ +import { Verification } from '@sinch/sdk-core'; +import { + getPrintFormat, + getVerificationCodeFromConfig, + getVerificationIdentityFromConfig, + initVerificationService, + printFullResponse, +} from '../../../config'; + +(async () => { + console.log('********************************************'); + console.log('* ReportVerificationByIdentity - phoneCall *'); + console.log('********************************************'); + + const verificationIdentity = getVerificationIdentityFromConfig(); + const verificationCode = getVerificationCodeFromConfig(); + + const requestData = Verification.reportVerificationByIdentityHelper.buildPhoneCallRequest( + verificationIdentity, verificationCode); + + const verificationService = initVerificationService(); + const response = await verificationService.verifications.reportPhoneCallByIdentity(requestData); + + const printFormat = getPrintFormat(process.argv); + + if (printFormat === 'pretty') { + console.log(`Phone call verification status: ${response.status}${response.status === 'SUCCESSFUL'?'':' - Reason: ' + response.reason}`); + } else { + printFullResponse(response); + } + +})(); diff --git a/examples/simple-examples/src/verification/verifications/phonecall/start-callout.ts b/examples/simple-examples/src/verification/verifications/phonecall/start-callout.ts index 11cf58c3..d5a052a5 100644 --- a/examples/simple-examples/src/verification/verifications/phonecall/start-callout.ts +++ b/examples/simple-examples/src/verification/verifications/phonecall/start-callout.ts @@ -6,7 +6,7 @@ import { printFullResponse, } from '../../../config'; -/** @deprecated see ../../start/start-phonecall.ts instead */ +/** @deprecated see ./start-phonecall.ts instead */ (async () => { console.log('*******************************'); console.log('* StartVerification - callout *'); diff --git a/packages/verification/src/models/v1/callout-verification-report-response/callout-verification-report-response.ts b/packages/verification/src/models/v1/callout-verification-report-response/callout-verification-report-response.ts index 7e1f6b8c..a7d1af4a 100644 --- a/packages/verification/src/models/v1/callout-verification-report-response/callout-verification-report-response.ts +++ b/packages/verification/src/models/v1/callout-verification-report-response/callout-verification-report-response.ts @@ -1,8 +1,10 @@ import { ReasonEnum, SourceEnum, VerificationStatusEnum } from '../enums'; import { Identity } from '../identity'; -export interface CalloutVerificationReportResponse { +/** @deprecated */ +export type CalloutVerificationReportResponse = PhoneCallVerificationReportResponse; +export interface PhoneCallVerificationReportResponse { /** The unique ID of the verification request. */ id?: string; /** The method of the verification request. This will always be `callout`. */ diff --git a/packages/verification/src/models/v1/callout-verification-report-response/index.ts b/packages/verification/src/models/v1/callout-verification-report-response/index.ts index 864e706d..e53d47e3 100644 --- a/packages/verification/src/models/v1/callout-verification-report-response/index.ts +++ b/packages/verification/src/models/v1/callout-verification-report-response/index.ts @@ -1 +1,4 @@ -export type { CalloutVerificationReportResponse } from './callout-verification-report-response'; +export type { + PhoneCallVerificationReportResponse, + CalloutVerificationReportResponse, +} from './callout-verification-report-response'; diff --git a/packages/verification/src/models/v1/helper.ts b/packages/verification/src/models/v1/helper.ts index 817dcc9f..4d6cdbbc 100644 --- a/packages/verification/src/models/v1/helper.ts +++ b/packages/verification/src/models/v1/helper.ts @@ -3,6 +3,8 @@ import { ReportCalloutVerificationByIdRequestData, ReportFlashCallVerificationByIdentityRequestData, ReportFlashCallVerificationByIdRequestData, + ReportPhoneCallVerificationByIdentityRequestData, + ReportPhoneCallVerificationByIdRequestData, ReportSmsVerificationByIdentityRequestData, ReportSmsVerificationByIdRequestData, StartCalloutVerificationRequestData, @@ -176,6 +178,14 @@ export const startVerificationHelper = { }, }; export const reportVerificationByIdHelper = { + /** + * Builds a request object for reporting an SMS verification by its ID. + * + * @param {string} id - The unique identifier for the SMS verification request. + * @param {string} code - The verification code received via SMS. + * @param {string} [cli] - An optional CLI (Caller Line Identification) that can be included in the request. + * @return {ReportSmsVerificationByIdRequestData} The request data object used to report the SMS verification. + */ buildSmsRequest: ( id: string, code: string, @@ -191,6 +201,34 @@ export const reportVerificationByIdHelper = { }, }; }, + /** + * Builds a request object for reporting a phone call verification by its ID. + * + * @param {string} id - The unique identifier for the phone call verification request. + * @param {string} code - The verification code received during the phone call. + * @return {ReportPhoneCallVerificationByIdRequestData} The request data object used to report the phone call verification. + */ + buildPhoneCallRequest: ( + id: string, + code: string, + ): ReportPhoneCallVerificationByIdRequestData => { + return { + id, + reportPhoneCallVerificationByIdRequestBody: { + phoneCall: { + code, + }, + }, + }; + }, + /** + * Builds a request object for reporting a callout verification by its ID. + * + * @param {string} id - The unique identifier for the callout verification request. + * @param {string} code - The verification code received during the callout. + * @return {ReportCalloutVerificationByIdRequestData} The request data object used to report the callout verification. + * @deprecated + */ buildCalloutRequest: ( id: string, code: string, @@ -204,6 +242,13 @@ export const reportVerificationByIdHelper = { }, }; }, + /** + * Builds a request object for reporting a flash call verification by its ID. + * + * @param {string} id - The unique identifier for the flash call verification request. + * @param {string} cli - The CLI (Caller Line Identification) received during the flash call. + * @return {ReportFlashCallVerificationByIdRequestData} The request data object used to report the flash call verification. + */ buildFlashCallRequest: ( id: string, cli: string, @@ -219,6 +264,14 @@ export const reportVerificationByIdHelper = { }, }; export const reportVerificationByIdentityHelper = { + /** + * Builds a request object for reporting an SMS verification by the phone number identity. + * + * @param {string} identity - The phone number for which the verification process has been initiated. + * @param {string} code - The verification code received via SMS. + * @param {string} [cli] - The CLI (Caller Line Identification) that may be used during the verification. + * @return {ReportSmsVerificationByIdentityRequestData} The request data object used to report the SMS verification. + */ buildSmsRequest: ( identity: string, code: string, @@ -234,6 +287,34 @@ export const reportVerificationByIdentityHelper = { }, }; }, + /** + * Builds a request object for reporting a phone call verification by the phone number identity. + * + * @param {string} identity - The phone number for which the verification process has been initiated. + * @param {string} code - The verification code received via the phone call. + * @return {ReportPhoneCallVerificationByIdentityRequestData} The request data object used to report the phone call verification. + */ + buildPhoneCallRequest: ( + identity: string, + code: string, + ): ReportPhoneCallVerificationByIdentityRequestData => { + return { + endpoint: identity, + reportPhoneCallVerificationByIdentityRequestBody: { + phoneCall: { + code, + }, + }, + }; + }, + /** + * Builds a request object for reporting a callout verification by the phone number identity. + * + * @param {string} identity - The phone number for which the callout verification process has been initiated. + * @param {string} code - The verification code received during the callout. + * @return {ReportCalloutVerificationByIdentityRequestData} The request data object used to report the callout verification. + * @deprecated + */ buildCalloutRequest: ( identity: string, code: string, @@ -247,6 +328,13 @@ export const reportVerificationByIdentityHelper = { }, }; }, + /** + * Builds a request object for reporting a flash call verification by the phone number identity. + * + * @param {string} identity - The phone number for which the flash call verification process has been initiated. + * @param {string} cli - The CLI (Caller Line Identification) received during the flash call. + * @return {ReportFlashCallVerificationByIdentityRequestData} The request data object used to report the flash call verification. + */ buildFlashCallRequest: ( identity: string, cli: string, diff --git a/packages/verification/src/models/v1/requests/verifications/verifications-request-data.ts b/packages/verification/src/models/v1/requests/verifications/verifications-request-data.ts index 0842d3d0..5475c92a 100644 --- a/packages/verification/src/models/v1/requests/verifications/verifications-request-data.ts +++ b/packages/verification/src/models/v1/requests/verifications/verifications-request-data.ts @@ -1,6 +1,7 @@ import { CalloutVerificationReportRequest, FlashCallVerificationReportRequest, + PhoneCallVerificationReportRequest, SmsVerificationReportRequest, } from '../../verification-report-request'; import { @@ -27,6 +28,12 @@ export interface ReportFlashCallVerificationByIdRequestData extends ReportVerifi 'reportFlashCallVerificationByIdRequestBody': FlashCallVerificationReportRequest; } +export interface ReportPhoneCallVerificationByIdRequestData extends ReportVerificationByIdRequestDataBase { + /** Request body to report a verification started with a phone call by its ID */ + 'reportPhoneCallVerificationByIdRequestBody': PhoneCallVerificationReportRequest; +} + +/** @deprecated */ export interface ReportCalloutVerificationByIdRequestData extends ReportVerificationByIdRequestDataBase { /** Request body to report a verification started with a callout by its ID */ 'reportCalloutVerificationByIdRequestBody': CalloutVerificationReportRequest; @@ -47,6 +54,12 @@ export interface ReportFlashCallVerificationByIdentityRequestData extends Report 'reportFlashCallVerificationByIdentityRequestBody': FlashCallVerificationReportRequest; } +export interface ReportPhoneCallVerificationByIdentityRequestData extends ReportVerificationByIdentityRequestDataBase { + /** Request body to report a verification started with a callout by its identity */ + 'reportPhoneCallVerificationByIdentityRequestBody': PhoneCallVerificationReportRequest; +} + +/** @deprecated */ export interface ReportCalloutVerificationByIdentityRequestData extends ReportVerificationByIdentityRequestDataBase { /** Request body to report a verification started with a callout by its identity */ 'reportCalloutVerificationByIdentityRequestBody': CalloutVerificationReportRequest; diff --git a/packages/verification/src/models/v1/start-verification-request/start-verification-request.ts b/packages/verification/src/models/v1/start-verification-request/start-verification-request.ts index f49dd57b..d72160fe 100644 --- a/packages/verification/src/models/v1/start-verification-request/start-verification-request.ts +++ b/packages/verification/src/models/v1/start-verification-request/start-verification-request.ts @@ -16,8 +16,8 @@ export interface StartVerificationWithPhoneCall extends StartVerificationBase { } export interface StartVerificationWithPhoneCallServerModel extends StartVerificationBase { - /** @see CalloutOptions */ - calloutOptions?: CalloutOptions; + /** @see PhoneCallOptions */ + calloutOptions?: PhoneCallOptions; } /** @deprecated */ diff --git a/packages/verification/src/models/v1/verification-report-request/index.ts b/packages/verification/src/models/v1/verification-report-request/index.ts index adcddc04..d8974f5d 100644 --- a/packages/verification/src/models/v1/verification-report-request/index.ts +++ b/packages/verification/src/models/v1/verification-report-request/index.ts @@ -1,5 +1,7 @@ export type { SmsVerificationReportRequest, FlashCallVerificationReportRequest, + PhoneCallVerificationReportRequest, + PhoneCallVerificationReportRequestServerModel, CalloutVerificationReportRequest, } from './verification-report-request'; diff --git a/packages/verification/src/models/v1/verification-report-request/verification-report-request.ts b/packages/verification/src/models/v1/verification-report-request/verification-report-request.ts index e0b79ff0..6c2bfc51 100644 --- a/packages/verification/src/models/v1/verification-report-request/verification-report-request.ts +++ b/packages/verification/src/models/v1/verification-report-request/verification-report-request.ts @@ -20,12 +20,26 @@ interface FlashCallContent { cli: string; } +export interface PhoneCallVerificationReportRequest { + /** A configuration object containing settings specific to Phone Call verifications */ + phoneCall: PhoneCallContent; +} + +export interface PhoneCallVerificationReportRequestServerModel { + /** A configuration object containing settings specific to Phone Call verifications */ + callout: PhoneCallContent; +} + +/** @deprecated */ export interface CalloutVerificationReportRequest { /** A configuration object containing settings specific to Phone Call verifications */ callout: CalloutContent; } -interface CalloutContent { +interface PhoneCallContent { /** The code which was received by the user submitting the Phone Call verification. */ code?: string; } + +/** @deprecated */ +type CalloutContent = PhoneCallContent; diff --git a/packages/verification/src/rest/v1/verifications/verifications-api.jest.fixture.ts b/packages/verification/src/rest/v1/verifications/verifications-api.jest.fixture.ts index ee77028b..41e13406 100644 --- a/packages/verification/src/rest/v1/verifications/verifications-api.jest.fixture.ts +++ b/packages/verification/src/rest/v1/verifications/verifications-api.jest.fixture.ts @@ -21,6 +21,9 @@ import { StartPhoneCallVerificationResponse, StartPhoneCallVerificationRequestData, StartDataVerificationResponse, + ReportPhoneCallVerificationByIdRequestData, + ReportPhoneCallVerificationByIdentityRequestData, + PhoneCallVerificationReportResponse, } from '../../../models'; export class VerificationsApiFixture implements Partial> { @@ -37,6 +40,12 @@ export class VerificationsApiFixture implements Partial, [ReportFlashCallVerificationByIdRequestData]> = jest.fn(); + /** + * Fixture associated to function reportPhoneCallById + */ + public reportPhoneCallById: jest.Mock, + [ReportPhoneCallVerificationByIdRequestData]> = jest.fn(); /** * Fixture associated to function reportCalloutById */ @@ -57,6 +66,13 @@ export class VerificationsApiFixture implements Partial, [ReportFlashCallVerificationByIdentityRequestData]> = jest.fn(); + /** + * Fixture associated to function reportPhoneCallByIdentity + */ + public reportPhoneCallByIdentity: + jest.Mock, + [ReportPhoneCallVerificationByIdentityRequestData]> = jest.fn(); /** * Fixture associated to function reportCalloutByIdentity */ diff --git a/packages/verification/src/rest/v1/verifications/verifications-api.ts b/packages/verification/src/rest/v1/verifications/verifications-api.ts index 0b34400d..800da475 100644 --- a/packages/verification/src/rest/v1/verifications/verifications-api.ts +++ b/packages/verification/src/rest/v1/verifications/verifications-api.ts @@ -22,6 +22,12 @@ import { StartVerificationWithPhoneCall, StartDataVerificationRequestData, StartDataVerificationResponse, + ReportPhoneCallVerificationByIdRequestData, + PhoneCallVerificationReportRequest, + StartVerificationWithPhoneCallServerModel, + PhoneCallVerificationReportRequestServerModel, + ReportPhoneCallVerificationByIdentityRequestData, + PhoneCallVerificationReportResponse, } from '../../../models'; import { RequestBody, @@ -108,10 +114,59 @@ export class VerificationsApi extends VerificationDomainApi { }); } + /** + * Report a Phone Call verification with ID + * Report the received verification code to verify it, using the Verification ID of the Verification request. + * @param { ReportCalloutVerificationByIdRequestData } data - The data to provide to the API call. + */ + public async reportPhoneCallById( + data: ReportPhoneCallVerificationByIdRequestData, + ): Promise { + this.client = this.getSinchClient(); + (data.reportPhoneCallVerificationByIdRequestBody as any).method = 'callout'; + const getParams = this.client.extractQueryParams(data, [] as never[]); + const headers: { [key: string]: string | undefined } = { + 'Content-Type': 'application/json; charset=UTF-8', + 'Accept': 'application/json', + }; + + // Special fields handling: see method for details + const requestDataBody = this.performReportPhoneCallByIdRequestBodyTransformation( + data.reportPhoneCallVerificationByIdRequestBody); + + const body: RequestBody = requestDataBody + ? JSON.stringify(requestDataBody) + : '{}'; + + const path = `/verification/v1/verifications/id/${data['id']}`; + const basePathUrl = this.client.apiClientOptions.hostname + path; + + const requestOptions + = await this.client.prepareOptions(basePathUrl, 'PUT', getParams, headers, body || undefined, path); + const url = this.client.prepareUrl(requestOptions.hostname, requestOptions.queryParams); + + return this.client.processCall({ + url, + requestOptions, + apiName: this.apiName, + operationId: 'ReportPhoneCallVerificationById', + }); + } + + performReportPhoneCallByIdRequestBodyTransformation( + body: PhoneCallVerificationReportRequest, + ): PhoneCallVerificationReportRequestServerModel { + const requestDataBody: any = { ...body }; + (requestDataBody).callout = { ...requestDataBody.phoneCall }; + delete requestDataBody.phoneCall; + return requestDataBody; + } + /** * Report a Callout verification with ID * Report the received verification code to verify it, using the Verification ID of the Verification request. * @param { ReportCalloutVerificationByIdRequestData } data - The data to provide to the API call. + * @deprecated */ public async reportCalloutById( data: ReportCalloutVerificationByIdRequestData, @@ -211,10 +266,59 @@ export class VerificationsApi extends VerificationDomainApi { }); } + /** + * Report a Phone Call verification using Identity + * Report the received verification code (OTP) to verify it, using the identity of the user (in most cases, the phone number). + * @param { ReportPhoneCallVerificationByIdentityRequestData } data - The data to provide to the API call. + */ + public async reportPhoneCallByIdentity( + data: ReportPhoneCallVerificationByIdentityRequestData, + ): Promise { + this.client = this.getSinchClient(); + (data.reportPhoneCallVerificationByIdentityRequestBody as any).method = 'callout'; + const getParams = this.client.extractQueryParams( + data, [] as never[]); + const headers: { [key: string]: string | undefined } = { + 'Content-Type': 'application/json; charset=UTF-8', + 'Accept': 'application/json', + }; + + // Special fields handling: see method for details + const requestDataBody = this.performReportPhoneCallByIdentityRequestBodyTransformation( + data.reportPhoneCallVerificationByIdentityRequestBody); + + const body: RequestBody = requestDataBody + ? JSON.stringify(requestDataBody) + : '{}'; + const path = `/verification/v1/verifications/number/${data['endpoint']}`; + const basePathUrl = this.client.apiClientOptions.hostname + path; + + const requestOptions + = await this.client.prepareOptions(basePathUrl, 'PUT', getParams, headers, body || undefined, path); + const url = this.client.prepareUrl(requestOptions.hostname, requestOptions.queryParams); + + return this.client.processCall({ + url, + requestOptions, + apiName: this.apiName, + operationId: 'ReportPhoneCallVerificationByIdentity', + }); + } + + performReportPhoneCallByIdentityRequestBodyTransformation( + body: PhoneCallVerificationReportRequest, + ): PhoneCallVerificationReportRequestServerModel { + const requestDataBody: any = { ...body }; + (requestDataBody).callout = { ...requestDataBody.phoneCall }; + delete requestDataBody.phoneCall; + return requestDataBody; + } + /** * Report a Callout verification using Identity * Report the received verification code (OTP) to verify it, using the identity of the user (in most cases, the phone number). * @param { ReportCalloutVerificationByIdentityRequestData } data - The data to provide to the API call. + * @deprecated */ public async reportCalloutByIdentity( data: ReportCalloutVerificationByIdentityRequestData, @@ -382,7 +486,9 @@ export class VerificationsApi extends VerificationDomainApi { }); } - performStartPhoneCallRequestBodyTransformation(body: StartVerificationWithPhoneCall): StartVerificationWithPhoneCall { + performStartPhoneCallRequestBodyTransformation( + body: StartVerificationWithPhoneCall, + ): StartVerificationWithPhoneCallServerModel { const requestDataBody = { ...body }; if (requestDataBody.phoneCallOptions !== undefined) { (requestDataBody as any).calloutOptions = { ...requestDataBody.phoneCallOptions }; diff --git a/packages/verification/tests/rest/v1/verifications/report.steps.ts b/packages/verification/tests/rest/v1/verifications/report.steps.ts new file mode 100644 index 00000000..9d0055bf --- /dev/null +++ b/packages/verification/tests/rest/v1/verifications/report.steps.ts @@ -0,0 +1,117 @@ +import { VerificationsApi, VerificationService, Verification } from '../../../../src'; +import { Given, When, Then } from '@cucumber/cucumber'; +import * as assert from 'assert'; + +let reportVerificationApi: VerificationsApi; +let reportSmsResponse: Verification.SmsVerificationReportResponse; +let reportPhoneCallResponse: Verification.PhoneCallVerificationReportResponse; +let reportFlashCallResponse: Verification.FlashCallVerificationReportResponse; + +Given('the Verification service "Report" is available', () => { + const verificationService = new VerificationService({ + applicationKey: 'appKey', + applicationSecret: 'appSecret', + verificationHostname: 'http://localhost:3018', + }); + reportVerificationApi = verificationService.verifications; +}); + +When('I send a request to report an SMS verification with the verification ID', async () => { + reportSmsResponse = await reportVerificationApi.reportSmsById({ + id: '1ce0ffee-c0de-5eed-d00d-f00dfeed1337', + reportSmsVerificationByIdRequestBody: { + sms: { + code: 'OQP1', + }, + }, + }); +}); + +When('I send a request to report an SMS verification with the phone number', async () => { + reportSmsResponse = await reportVerificationApi.reportSmsByIdentity({ + endpoint: '+46123456789', + reportSmsVerificationByIdentityRequestBody: { + sms: { + code: 'OQP1', + }, + }, + }); +}); + +Then('the response contains the details of an SMS verification report', () => { + assert.equal(reportSmsResponse.id, '1ce0ffee-c0de-5eed-d00d-f00dfeed1337'); + assert.equal(reportSmsResponse.method, 'sms'); + const successfulStatus: Verification.VerificationStatusEnum = 'SUCCESSFUL'; + assert.equal(reportSmsResponse.status, successfulStatus); +}); + +When('I send a request to report a Phone Call verification with the verification ID', async () => { + reportPhoneCallResponse = await reportVerificationApi.reportPhoneCallById({ + id: '1ce0ffee-c0de-5eed-d11d-f00dfeed1337', + reportPhoneCallVerificationByIdRequestBody: { + phoneCall: { + code: '123456', + }, + }, + }); +}); + +When('I send a request to report a Phone Call verification with the phone number', async () => { + reportPhoneCallResponse = await reportVerificationApi.reportPhoneCallByIdentity({ + endpoint: '+33612345678', + reportPhoneCallVerificationByIdentityRequestBody: { + phoneCall: { + code: '123456', + }, + }, + }); +}); + +Then('the response contains the details of a Phone Call verification report', () => { + assert.equal(reportPhoneCallResponse.id, '1ce0ffee-c0de-5eed-d11d-f00dfeed1337'); + assert.equal(reportPhoneCallResponse.method, 'callout'); + const successfulStatus: Verification.VerificationStatusEnum = 'SUCCESSFUL'; + assert.equal(reportPhoneCallResponse.status, successfulStatus); + assert.equal(reportPhoneCallResponse.callComplete, true); +}); + +When('I send a request to report a Flash Call verification with the verification ID', async () => { + reportFlashCallResponse = await reportVerificationApi.reportFlashCallById({ + id: '1ce0ffee-c0de-5eed-d11d-f00dfeed1337', + reportFlashCallVerificationByIdRequestBody: { + flashCall: { + cli: '+18156540001', + }, + }, + }); +}); + +When('I send a request to report a Flash Call verification with the phone number', async () => { + reportFlashCallResponse = await reportVerificationApi.reportFlashCallByIdentity({ + endpoint: '+33612345678', + reportFlashCallVerificationByIdentityRequestBody: { + flashCall: { + cli: '+18156540001', + }, + }, + }); +}); + +Then('the response contains the details of a Flash Call verification report', () => { + assert.equal(reportFlashCallResponse.id, '1ce0ffee-c0de-5eed-d22d-f00dfeed1337'); + assert.equal(reportFlashCallResponse.method, 'flashcall'); + const successfulStatus: Verification.VerificationStatusEnum = 'SUCCESSFUL'; + assert.equal(reportFlashCallResponse.status, successfulStatus); + assert.equal(reportFlashCallResponse.reference, 'flashcall-verification-test-e2e'); + assert.equal(reportFlashCallResponse.callComplete, true); +}); + +Then('the response contains the details of a failed Flash Call verification report', () => { + assert.equal(reportFlashCallResponse.id, '1ce0ffee-c0de-5eed-d22d-f00dfeed1337'); + assert.equal(reportFlashCallResponse.method, 'flashcall'); + const failStatus: Verification.VerificationStatusEnum = 'FAIL'; + assert.equal(reportFlashCallResponse.status, failStatus); + const expiredReason: Verification.ReasonEnum = 'Expired'; + assert.equal(reportFlashCallResponse.reason, expiredReason); + assert.equal(reportFlashCallResponse.callComplete, true); +}); diff --git a/packages/verification/tests/rest/v1/start/start.steps.ts b/packages/verification/tests/rest/v1/verifications/start.steps.ts similarity index 100% rename from packages/verification/tests/rest/v1/start/start.steps.ts rename to packages/verification/tests/rest/v1/verifications/start.steps.ts diff --git a/packages/verification/tests/rest/v1/verifications/verifications-api.test.ts b/packages/verification/tests/rest/v1/verifications/verifications-api.test.ts index 5c2d42dc..055c96fc 100644 --- a/packages/verification/tests/rest/v1/verifications/verifications-api.test.ts +++ b/packages/verification/tests/rest/v1/verifications/verifications-api.test.ts @@ -234,6 +234,43 @@ describe('VerificationsApi', () => { }); it('should make a PUT request to report the verification code (OTP) received by a phone call to verify it,' + + 'using the verification ID of the verification request', async () => { + // Given + const requestData = Verification.reportVerificationByIdHelper.buildPhoneCallRequest( + 'some_verification_id', + '0000'); + const expectedResponse: Verification.PhoneCallVerificationReportResponse = { + id: 'some_verification_id', + method: 'callout', + status: 'SUCCESSFUL', + callComplete: true, + }; + + // When + fixture.reportPhoneCallById.mockResolvedValue(expectedResponse); + verificationsApi.reportPhoneCallById = fixture.reportPhoneCallById; + const response = await verificationsApi.reportPhoneCallById(requestData); + + // Then + expect(response).toEqual(expectedResponse); + expect(fixture.reportPhoneCallById).toHaveBeenCalledWith(requestData); + }); + + it('should format the reportPhoneCallById request body', () => { + const requestData = Verification.reportVerificationByIdHelper.buildPhoneCallRequest( + 'some_verification_id', + '0000'); + const expectedResult: Verification.PhoneCallVerificationReportRequestServerModel = { + callout: { + code: '0000', + }, + }; + const formattedRequestData = verificationsApi.performReportPhoneCallByIdRequestBodyTransformation( + requestData.reportPhoneCallVerificationByIdRequestBody); + expect(formattedRequestData).toEqual(expectedResult); + }); + + it('should make a PUT request to report the verification code (OTP) received by a callout to verify it,' + 'using the verification ID of the verification request', async () => { // Given const requestData = Verification.reportVerificationByIdHelper.buildCalloutRequest( @@ -308,6 +345,44 @@ describe('VerificationsApi', () => { }); it('should make a PUT request to report the verification code (OTP) received by a phone call to verify it,' + + 'using the identity of the user', async () => { + // Given + const requestData = Verification.reportVerificationByIdentityHelper.buildPhoneCallRequest( + '+33444555666', + '0000'); + const expectedResponse: Verification.PhoneCallVerificationReportResponse = { + id: '018beea3-a942-0094-4a3a-d6b2f2c65057', + method: 'callout', + status: 'FAIL', + reason: 'Expired', + callComplete: true, + }; + + // When + fixture.reportPhoneCallByIdentity.mockResolvedValue(expectedResponse); + verificationsApi.reportPhoneCallByIdentity = fixture.reportPhoneCallByIdentity; + const response = await verificationsApi.reportPhoneCallByIdentity(requestData); + + // Then + expect(response).toEqual(expectedResponse); + expect(fixture.reportPhoneCallByIdentity).toHaveBeenCalledWith(requestData); + }); + + it('should format the reportPhoneCallByIdentity request body', () => { + const requestData = Verification.reportVerificationByIdentityHelper.buildPhoneCallRequest( + '+33444555666', + '0000'); + const expectedResult: Verification.PhoneCallVerificationReportRequestServerModel = { + callout: { + code: '0000', + }, + }; + const formattedRequestData = verificationsApi.performReportPhoneCallByIdentityRequestBodyTransformation( + requestData.reportPhoneCallVerificationByIdentityRequestBody); + expect(formattedRequestData).toEqual(expectedResult); + }); + + it('should make a PUT request to report the verification code (OTP) received by a callout to verify it,' + 'using the identity of the user', async () => { // Given const requestData = Verification.reportVerificationByIdentityHelper.buildCalloutRequest( From b0071d990a7b7ac0f511f90f10a61ffc667a9fbc Mon Sep 17 00:00:00 2001 From: Antoine SEIN <142824551+asein-sinch@users.noreply.github.com> Date: Wed, 28 Aug 2024 17:18:52 +0200 Subject: [PATCH 37/54] DEVEXP-522: E2E Verification/Status (#130) --- .gitignore | 1 + .../index.ts | 1 - packages/verification/src/models/v1/index.ts | 4 +- .../index.ts | 2 +- ...phonecall-verification-report-response.ts} | 0 .../index.ts | 4 + ...phonecall-verification-status-response.ts} | 5 +- .../verification-status-request-data.ts | 3 +- .../verification-status-response.ts | 8 +- .../verification-status-api.ts | 7 +- .../verification-status-api.test.ts | 2 +- .../verification-status.steps.ts | 114 ++++++++++++++++++ 12 files changed, 141 insertions(+), 10 deletions(-) delete mode 100644 packages/verification/src/models/v1/callout-verification-status-response/index.ts rename packages/verification/src/models/v1/{callout-verification-report-response => phonecall-verification-report-response}/index.ts (63%) rename packages/verification/src/models/v1/{callout-verification-report-response/callout-verification-report-response.ts => phonecall-verification-report-response/phonecall-verification-report-response.ts} (100%) create mode 100644 packages/verification/src/models/v1/phonecall-verification-status-response/index.ts rename packages/verification/src/models/v1/{callout-verification-status-response/callout-verification-status-response.ts => phonecall-verification-status-response/phonecall-verification-status-response.ts} (89%) create mode 100644 packages/verification/tests/rest/v1/verification-status/verification-status.steps.ts diff --git a/.gitignore b/.gitignore index f48cf608..8144aab8 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ tsconfig.build.tsbuildinfo tsconfig.tsbuildinfo packages/**/dist .env +*.feature diff --git a/packages/verification/src/models/v1/callout-verification-status-response/index.ts b/packages/verification/src/models/v1/callout-verification-status-response/index.ts deleted file mode 100644 index 9c667213..00000000 --- a/packages/verification/src/models/v1/callout-verification-status-response/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './callout-verification-status-response'; diff --git a/packages/verification/src/models/v1/index.ts b/packages/verification/src/models/v1/index.ts index cea9c020..f2389bda 100644 --- a/packages/verification/src/models/v1/index.ts +++ b/packages/verification/src/models/v1/index.ts @@ -8,8 +8,8 @@ export * from './sms-verification-report-response'; export * from './sms-verification-status-response'; // Models associated to PhoneCall verification workflow export * from './start-phonecall-verification-response'; -export * from './callout-verification-report-response'; -export * from './callout-verification-status-response'; +export * from './phonecall-verification-report-response'; +export * from './phonecall-verification-status-response'; // Models associated to Flashcall verification workflow export * from './start-flashcall-verification-response'; export * from './flashcall-verification-report-response'; diff --git a/packages/verification/src/models/v1/callout-verification-report-response/index.ts b/packages/verification/src/models/v1/phonecall-verification-report-response/index.ts similarity index 63% rename from packages/verification/src/models/v1/callout-verification-report-response/index.ts rename to packages/verification/src/models/v1/phonecall-verification-report-response/index.ts index e53d47e3..d5f8c4e5 100644 --- a/packages/verification/src/models/v1/callout-verification-report-response/index.ts +++ b/packages/verification/src/models/v1/phonecall-verification-report-response/index.ts @@ -1,4 +1,4 @@ export type { PhoneCallVerificationReportResponse, CalloutVerificationReportResponse, -} from './callout-verification-report-response'; +} from './phonecall-verification-report-response'; diff --git a/packages/verification/src/models/v1/callout-verification-report-response/callout-verification-report-response.ts b/packages/verification/src/models/v1/phonecall-verification-report-response/phonecall-verification-report-response.ts similarity index 100% rename from packages/verification/src/models/v1/callout-verification-report-response/callout-verification-report-response.ts rename to packages/verification/src/models/v1/phonecall-verification-report-response/phonecall-verification-report-response.ts diff --git a/packages/verification/src/models/v1/phonecall-verification-status-response/index.ts b/packages/verification/src/models/v1/phonecall-verification-status-response/index.ts new file mode 100644 index 00000000..ba3cccd5 --- /dev/null +++ b/packages/verification/src/models/v1/phonecall-verification-status-response/index.ts @@ -0,0 +1,4 @@ +export type { + PhoneCallVerificationStatusResponse, + CalloutVerificationStatusResponse, +} from './phonecall-verification-status-response'; diff --git a/packages/verification/src/models/v1/callout-verification-status-response/callout-verification-status-response.ts b/packages/verification/src/models/v1/phonecall-verification-status-response/phonecall-verification-status-response.ts similarity index 89% rename from packages/verification/src/models/v1/callout-verification-status-response/callout-verification-status-response.ts rename to packages/verification/src/models/v1/phonecall-verification-status-response/phonecall-verification-status-response.ts index d7722f45..05e4cbcb 100644 --- a/packages/verification/src/models/v1/callout-verification-status-response/callout-verification-status-response.ts +++ b/packages/verification/src/models/v1/phonecall-verification-status-response/phonecall-verification-status-response.ts @@ -2,7 +2,10 @@ import { CallResult, ReasonEnum, VerificationStatusEnum } from '../enums'; import { Identity } from '../identity'; import { VerificationPriceCall } from '../verification-price-call'; -export interface CalloutVerificationStatusResponse { +/** @deprecated */ +export type CalloutVerificationStatusResponse = PhoneCallVerificationStatusResponse; + +export interface PhoneCallVerificationStatusResponse { /** The unique ID of the verification request. */ id?: string; /** The method of the verification request. This will always be `callout`. */ diff --git a/packages/verification/src/models/v1/requests/verification-status/verification-status-request-data.ts b/packages/verification/src/models/v1/requests/verification-status/verification-status-request-data.ts index 7f22e023..298867b1 100644 --- a/packages/verification/src/models/v1/requests/verification-status/verification-status-request-data.ts +++ b/packages/verification/src/models/v1/requests/verification-status/verification-status-request-data.ts @@ -6,7 +6,8 @@ export interface VerificationStatusByIdentityRequestData { /** For type `number` use a [E.164](https://community.sinch.com/t5/Glossary/E-164/ta-p/7537)-compatible phone number. */ 'endpoint': string; /** The method of the verification. */ - 'method': 'sms' | 'callout' | 'flashcall'; + // TODO v2.0 - Remove 'callout' option + 'method': 'sms' | 'callout' | 'phonecall' | 'flashcall'; } export interface VerificationStatusByReferenceRequestData { /** The custom reference of the verification. */ diff --git a/packages/verification/src/models/v1/verification-status-response/verification-status-response.ts b/packages/verification/src/models/v1/verification-status-response/verification-status-response.ts index 80d3e3a2..0ea38fc3 100644 --- a/packages/verification/src/models/v1/verification-status-response/verification-status-response.ts +++ b/packages/verification/src/models/v1/verification-status-response/verification-status-response.ts @@ -1,7 +1,11 @@ import { SmsVerificationStatusResponse } from '../sms-verification-status-response'; import { FlashCallVerificationStatusResponse } from '../flashcall-verification-status-response'; -import { CalloutVerificationStatusResponse } from '../callout-verification-status-response'; +import { + CalloutVerificationStatusResponse, + PhoneCallVerificationStatusResponse, +} from '../phonecall-verification-status-response'; export type VerificationStatusResponse = SmsVerificationStatusResponse | FlashCallVerificationStatusResponse - | CalloutVerificationStatusResponse; + | CalloutVerificationStatusResponse + | PhoneCallVerificationStatusResponse; diff --git a/packages/verification/src/rest/v1/verification-status/verification-status-api.ts b/packages/verification/src/rest/v1/verification-status/verification-status-api.ts index 59f93c7f..10454125 100644 --- a/packages/verification/src/rest/v1/verification-status/verification-status-api.ts +++ b/packages/verification/src/rest/v1/verification-status/verification-status-api.ts @@ -63,8 +63,13 @@ export class VerificationStatusApi extends VerificationDomainApi { 'Accept': 'application/json', }; + let verificationMethod = data['method']; + if (verificationMethod === 'phonecall') { + verificationMethod = 'callout'; + } + const body: RequestBody = ''; - const path = `/verification/v1/verifications/${data['method']}/number/${data['endpoint']}`; + const path = `/verification/v1/verifications/${verificationMethod}/number/${data['endpoint']}`; const basePathUrl = this.client.apiClientOptions.hostname + path; const requestOptions diff --git a/packages/verification/tests/rest/v1/verification-status/verification-status-api.test.ts b/packages/verification/tests/rest/v1/verification-status/verification-status-api.test.ts index 45e547d2..9be2e6f7 100644 --- a/packages/verification/tests/rest/v1/verification-status/verification-status-api.test.ts +++ b/packages/verification/tests/rest/v1/verification-status/verification-status-api.test.ts @@ -61,7 +61,7 @@ describe('VerificationStatusApi', () => { endpoint: '+33444555666', method: 'callout', }; - const expectedResponse: Verification.CalloutVerificationStatusResponse = { + const expectedResponse: Verification.PhoneCallVerificationStatusResponse = { id: '018bec2b-d123-b7b3-833e-4b177e3420df', method: 'callout', status: 'FAIL', diff --git a/packages/verification/tests/rest/v1/verification-status/verification-status.steps.ts b/packages/verification/tests/rest/v1/verification-status/verification-status.steps.ts new file mode 100644 index 00000000..4af23ae5 --- /dev/null +++ b/packages/verification/tests/rest/v1/verification-status/verification-status.steps.ts @@ -0,0 +1,114 @@ +import { VerificationStatusApi, VerificationService, Verification } from '../../../../src'; +import { Given, When, Then } from '@cucumber/cucumber'; +import * as assert from 'assert'; + +let verificationStatusApi: VerificationStatusApi; +let smsVerificationStatus: Verification.SmsVerificationStatusResponse; +let phoneCallVerificationStatus: Verification.PhoneCallVerificationStatusResponse; +let flashCallVerificationStatus: Verification.FlashCallVerificationStatusResponse; + +Given('the Verification service "Status" is available', () => { + const verificationService = new VerificationService({ + applicationKey: 'appKey', + applicationSecret: 'appSecret', + verificationHostname: 'http://localhost:3018', + }); + verificationStatusApi = verificationService.verificationStatus; +}); + +When('I send a request to retrieve a SMS verification status by its verification ID', async () => { + smsVerificationStatus = await verificationStatusApi.getById({ + id: '1ce0ffee-c0de-5eed-d00d-f00dfeed1337', + }) as Verification.SmsVerificationStatusResponse; +}); + +Then('the response contains the details of the SMS verification status', () => { + assert.equal(smsVerificationStatus.id, '1ce0ffee-c0de-5eed-d00d-f00dfeed1337'); + assert.equal(smsVerificationStatus.method, 'sms'); + const successfulStatus: Verification.VerificationStatusEnum = 'SUCCESSFUL'; + assert.equal(smsVerificationStatus.status, successfulStatus); + const verificationPrice: Verification.VerificationPriceSms = { + verificationPrice: { + currencyId: 'EUR', + amount: 0.0453, + }, + }; + assert.deepEqual(smsVerificationStatus.price, verificationPrice); + const identity: Verification.Identity = { + type: 'number', + endpoint: '+33612345678', + }; + assert.deepEqual(smsVerificationStatus.identity, identity); + assert.equal(smsVerificationStatus.countryId, 'FR'); + assert.deepEqual(smsVerificationStatus.verificationTimestamp, new Date('2024-06-06T09:08:41.4784877Z')); +}); + +When('I send a request to retrieve a Phone Call verification status by the phone number to verify', async () => { + phoneCallVerificationStatus = await verificationStatusApi.getByIdentity({ + method: 'phonecall', + endpoint: '+33612345678', + }) as Verification.PhoneCallVerificationStatusResponse; +}); + +Then('the response contains the details of the Phone Call verification status', () => { + assert.equal(phoneCallVerificationStatus.id, '1ce0ffee-c0de-5eed-d11d-f00dfeed1337'); + assert.equal(phoneCallVerificationStatus.method, 'callout'); + const successfulStatus: Verification.VerificationStatusEnum = 'SUCCESSFUL'; + assert.equal(phoneCallVerificationStatus.status, successfulStatus); + const verificationPrice: Verification.VerificationPriceCall = { + verificationPrice: { + currencyId: 'EUR', + amount: 0.1852, + }, + terminationPrice: { + currencyId: 'EUR', + amount: 0, + }, + }; + assert.deepEqual(phoneCallVerificationStatus.price, verificationPrice); + const identity: Verification.Identity = { + type: 'number', + endpoint: '+33612345678', + }; + assert.deepEqual(phoneCallVerificationStatus.identity, identity); + assert.equal(phoneCallVerificationStatus.countryId, 'FR'); + assert.deepEqual(phoneCallVerificationStatus.verificationTimestamp, new Date('2024-06-06T09:10:27.7264837Z')); + assert.equal(phoneCallVerificationStatus.callComplete, true); + const answeredCallResult: Verification.CallResult = 'ANSWERED'; + assert.equal(phoneCallVerificationStatus.callResult, answeredCallResult); +}); + +When('I send a request to retrieve a Flash Call verification status by its reference', async () => { + flashCallVerificationStatus = await verificationStatusApi.getByReference({ + reference: 'flashcall-verification-test-e2e', + }) as Verification.FlashCallVerificationReportResponse; +}); + +Then('the response contains the details of the Flash Call verification status', () => { + assert.equal(flashCallVerificationStatus.id, '1ce0ffee-c0de-5eed-d22d-f00dfeed1337'); + assert.equal(flashCallVerificationStatus.method, 'flashcall'); + const successfulStatus: Verification.VerificationStatusEnum = 'SUCCESSFUL'; + assert.equal(flashCallVerificationStatus.status, successfulStatus); + assert.equal(flashCallVerificationStatus.reference, 'flashcall-verification-test-e2e'); + const verificationPrice: Verification.VerificationPriceCall = { + verificationPrice: { + currencyId: 'EUR', + amount: 0.0205, + }, + terminationPrice: { + currencyId: 'EUR', + amount: 0, + }, + }; + assert.deepEqual(flashCallVerificationStatus.price, verificationPrice); + const identity: Verification.Identity = { + type: 'number', + endpoint: '+33612345678', + }; + assert.deepEqual(flashCallVerificationStatus.identity, identity); + assert.equal(flashCallVerificationStatus.countryId, 'FR'); + assert.deepEqual(flashCallVerificationStatus.verificationTimestamp, new Date('2024-06-06T09:07:32.3554646Z')); + assert.equal(flashCallVerificationStatus.callComplete, true); + const answeredCallResult: Verification.CallResult = 'ANSWERED'; + assert.equal(flashCallVerificationStatus.callResult, answeredCallResult); +}); From bc4cd2acb3fdf3e0f63115b1ef707de9dd4e72a7 Mon Sep 17 00:00:00 2001 From: Antoine SEIN <142824551+asein-sinch@users.noreply.github.com> Date: Mon, 23 Sep 2024 15:55:20 +0200 Subject: [PATCH 38/54] DEVEXP-523: E2E Verification/Callbacks (#131) --- .../services/verification-event.service.ts | 7 +- .../tests/rest/v1/contact/contacts.steps.ts | 2 +- .../src/models/v1/identity/identity.ts | 2 - .../src/models/v1/mod-callbacks/index.ts | 1 + .../verification-callback-event.ts | 4 + .../verification-request-event/index.ts | 2 +- .../rest/v1/callbacks/callbacks-webhook.ts | 9 +- .../v1/callbacks/webhooks-events.steps.ts | 89 +++++++++++++++++++ 8 files changed, 103 insertions(+), 13 deletions(-) create mode 100644 packages/verification/src/models/v1/mod-callbacks/verification-callback-event.ts create mode 100644 packages/verification/tests/rest/v1/callbacks/webhooks-events.steps.ts diff --git a/examples/webhooks/src/services/verification-event.service.ts b/examples/webhooks/src/services/verification-event.service.ts index bb83f779..051978a7 100644 --- a/examples/webhooks/src/services/verification-event.service.ts +++ b/examples/webhooks/src/services/verification-event.service.ts @@ -1,14 +1,11 @@ import { Injectable } from '@nestjs/common'; import { Response } from 'express'; -import { - Verification, - VerificationCallback, -} from '@sinch/sdk-core'; +import { Verification } from '@sinch/sdk-core'; @Injectable() export class VerificationEventService { - handleEvent(event: VerificationCallback, res: Response) { + handleEvent(event: Verification.VerificationCallbackEvent, res: Response) { console.log(`:: INCOMING EVENT :: ${event.event}`); switch (event.event) { case 'VerificationRequestEvent': diff --git a/packages/conversation/tests/rest/v1/contact/contacts.steps.ts b/packages/conversation/tests/rest/v1/contact/contacts.steps.ts index 28cf9bc4..ad7f0969 100644 --- a/packages/conversation/tests/rest/v1/contact/contacts.steps.ts +++ b/packages/conversation/tests/rest/v1/contact/contacts.steps.ts @@ -23,7 +23,7 @@ Given('the Conversation service "Contacts" is available', function () { contactsApi = conversationService.contact; }); -When('I send a request to create an contact', async () => { +When('I send a request to create a contact', async () => { contact = await contactsApi.create({ contactCreateRequestBody: { channel_identities: [ diff --git a/packages/verification/src/models/v1/identity/identity.ts b/packages/verification/src/models/v1/identity/identity.ts index 36bbcebe..c1d44143 100644 --- a/packages/verification/src/models/v1/identity/identity.ts +++ b/packages/verification/src/models/v1/identity/identity.ts @@ -7,6 +7,4 @@ export interface Identity { type: 'number'; /** For type `number` use an [E.164](https://community.sinch.com/t5/Glossary/E-164/ta-p/7537)-compatible phone number. */ endpoint: string; - /** */ - verified?: boolean; } diff --git a/packages/verification/src/models/v1/mod-callbacks/index.ts b/packages/verification/src/models/v1/mod-callbacks/index.ts index 08d3d4ac..4f251c46 100644 --- a/packages/verification/src/models/v1/mod-callbacks/index.ts +++ b/packages/verification/src/models/v1/mod-callbacks/index.ts @@ -1,3 +1,4 @@ +export * from './verification-callback-event'; // 'Verification Request Event' received from Sinch server export * from './verification-request-event'; // Response to send to Sinch server for a 'Verification Request Event' diff --git a/packages/verification/src/models/v1/mod-callbacks/verification-callback-event.ts b/packages/verification/src/models/v1/mod-callbacks/verification-callback-event.ts new file mode 100644 index 00000000..2ce27cad --- /dev/null +++ b/packages/verification/src/models/v1/mod-callbacks/verification-callback-event.ts @@ -0,0 +1,4 @@ +import { VerificationRequestEvent } from './verification-request-event'; +import { VerificationResultEvent } from './verification-result-event'; + +export type VerificationCallbackEvent = VerificationRequestEvent | VerificationResultEvent; diff --git a/packages/verification/src/models/v1/mod-callbacks/verification-request-event/index.ts b/packages/verification/src/models/v1/mod-callbacks/verification-request-event/index.ts index 1fb30a07..7b8a4b6f 100644 --- a/packages/verification/src/models/v1/mod-callbacks/verification-request-event/index.ts +++ b/packages/verification/src/models/v1/mod-callbacks/verification-request-event/index.ts @@ -1 +1 @@ -export type { VerificationRequestEvent } from './verification-request-event'; +export type { VerificationRequestEvent, MethodEnum } from './verification-request-event'; diff --git a/packages/verification/src/rest/v1/callbacks/callbacks-webhook.ts b/packages/verification/src/rest/v1/callbacks/callbacks-webhook.ts index 5b8ff98a..5e66d930 100644 --- a/packages/verification/src/rest/v1/callbacks/callbacks-webhook.ts +++ b/packages/verification/src/rest/v1/callbacks/callbacks-webhook.ts @@ -1,10 +1,11 @@ -import { VerificationRequestEvent, VerificationResultEvent } from '../../../models'; +import { VerificationCallbackEvent, VerificationRequestEvent, VerificationResultEvent } from '../../../models'; import { CallbackProcessor, SinchClientParameters, validateAuthenticationHeader } from '@sinch/sdk-client'; import { IncomingHttpHeaders } from 'http'; +/** @deprecated - use Verification.VerificationCallback instead */ export type VerificationCallback = VerificationRequestEvent | VerificationResultEvent; -export class VerificationCallbackWebhooks implements CallbackProcessor{ +export class VerificationCallbackWebhooks implements CallbackProcessor{ private readonly sinchClientParameters: SinchClientParameters; @@ -39,9 +40,9 @@ export class VerificationCallbackWebhooks implements CallbackProcessor { + formattedHeaders = {}; + response.headers.forEach((value, name) => { + formattedHeaders[name.toLowerCase()] = value; + }); + rawEvent = await response.text(); + event = verificationCallbackWebhook.parseEvent(JSON.parse(rawEvent)); +}; + +Given('the Verification Webhooks handler is available', () => { + verificationCallbackWebhook = new VerificationCallbackWebhooks({ + applicationKey: 'appKey', + applicationSecret: 'appSecret', + }); +}); + +When('I send a request to trigger a "Verification Request" event', async () => { + const response = await fetch('http://localhost:3018/webhooks/verification/verification-request-event'); + await processEvent(response); +}); + +Then('the header of the Verification event "Verification Request" contains a valid authorization', () => { + assert.ok(verificationCallbackWebhook.validateAuthenticationHeader( + formattedHeaders, + rawEvent, + '/webhooks/verification', + 'POST')); +}); + +Then('the Verification event describes a "Verification Request" event type', () => { + const verificationRequestEvent = event as Verification.VerificationRequestEvent; + assert.equal(verificationRequestEvent.id, '1ce0ffee-c0de-5eed-d00d-f00dfeed1337'); + assert.equal(verificationRequestEvent.event, 'VerificationRequestEvent'); + const smsVerificationMethod: Verification.MethodEnum = 'sms'; + assert.equal(verificationRequestEvent.method, smsVerificationMethod); + const identity: Verification.Identity = { + type: 'number', + endpoint: '+33612345678', + }; + assert.equal(verificationRequestEvent.identity.type, identity.type); + assert.equal(verificationRequestEvent.identity.endpoint, identity.endpoint); + const smsPrice: Verification.Price = { + currencyId: 'EUR', + amount: 0.0453, + }; + assert.deepEqual(verificationRequestEvent.price, smsPrice); + const smsRate: Verification.Price = { + currencyId: 'EUR', + amount: 0, + }; + assert.deepEqual(verificationRequestEvent.rate, smsRate); +}); + +When('I send a request to trigger a "Verification Result" event', async () => { + const response = await fetch('http://localhost:3018/webhooks/verification/verification-result-event'); + await processEvent(response); +}); + +Then('the header of the Verification event "Verification Result" contains a valid authorization', () => { + assert.ok(verificationCallbackWebhook.validateAuthenticationHeader( + formattedHeaders, + rawEvent, + '/webhooks/verification', + 'POST')); +}); + +Then('the Verification event describes a "Verification Result" event type', () => { + const verificationRequestEvent = event as Verification.VerificationResultEvent; + assert.equal(verificationRequestEvent.id, '1ce0ffee-c0de-5eed-d00d-f00dfeed1337'); + assert.equal(verificationRequestEvent.event, 'VerificationResultEvent'); + const smsVerificationMethod: Verification.MethodEnum = 'sms'; + assert.equal(verificationRequestEvent.method, smsVerificationMethod); + const identity: Verification.Identity = { + type: 'number', + endpoint: '+33612345678', + }; + assert.equal(verificationRequestEvent.identity.type, identity.type); + assert.equal(verificationRequestEvent.identity.endpoint, identity.endpoint); +}); From 5d22da2c70199fd659f551be1373bf013ecf28a3 Mon Sep 17 00:00:00 2001 From: Antoine SEIN <142824551+asein-sinch@users.noreply.github.com> Date: Mon, 23 Sep 2024 17:37:42 +0200 Subject: [PATCH 39/54] DEVEXP-550_Single cucumber statement for conversation webhooks events (#132) --- .../webhooks-events/webhooks-events.steps.ts | 48 +++++++++++-------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/packages/conversation/tests/rest/v1/webhooks-events/webhooks-events.steps.ts b/packages/conversation/tests/rest/v1/webhooks-events/webhooks-events.steps.ts index 82f87357..01b8334b 100644 --- a/packages/conversation/tests/rest/v1/webhooks-events/webhooks-events.steps.ts +++ b/packages/conversation/tests/rest/v1/webhooks-events/webhooks-events.steps.ts @@ -15,7 +15,6 @@ const processEvent = async (response: Response) => { formattedHeaders[name.toLowerCase()] = value; }); rawEvent = await response.text(); - rawEvent = rawEvent.replace(/\s+/g, ''); event = conversationCallbackWebhook.parseEvent(JSON.parse(rawEvent)); }; @@ -23,15 +22,16 @@ Given('the Conversation Webhooks handler is available', () => { conversationCallbackWebhook = new ConversationCallbackWebhooks(APP_SECRET); }); -Then('the Conversation event header contains a valid signature', () => { - assert.ok(conversationCallbackWebhook.validateAuthenticationHeader(formattedHeaders, rawEvent)); -}); - When('I send a request to trigger a "CAPABILITY" event', async () => { const response = await fetch('http://localhost:3014/webhooks/conversation/capability-lookup'); await processEvent(response); }); +// eslint-disable-next-line @typescript-eslint/no-unused-vars +Then('the header of the Conversation event {string} contains a valid signature', (_event) => { + assert.ok(conversationCallbackWebhook.validateAuthenticationHeader(formattedHeaders, rawEvent)); +}); + Then('the Conversation event describes a "CAPABILITY" event type', () => { const capabilityEvent = event as Conversation.CapabilityEvent; assert.ok(capabilityEvent.capability_notification); @@ -123,11 +123,16 @@ Then('the Conversation event describes a "CONVERSATION_STOP" event type', () => assert.equal(conversationStopEvent.trigger, expectedTrigger); }); -When('I send a request to trigger a "EVENT_DELIVERY" event with a FAILED status', async () => { +When('I send a request to trigger a "EVENT_DELIVERY" event with a "FAILED" status', async () => { const response = await fetch('http://localhost:3014/webhooks/conversation/event-delivery-report/failed'); await processEvent(response); }); +// eslint-disable-next-line @typescript-eslint/no-unused-vars +Then('the header of the Conversation event {} with a {} status contains a valid signature', (_event, _status) => { + assert.ok(conversationCallbackWebhook.validateAuthenticationHeader(formattedHeaders, rawEvent)); +}); + Then('the Conversation event describes a "EVENT_DELIVERY" event type', () => { const eventDeliveryEvent = event as Conversation.EventDelivery; assert.ok(eventDeliveryEvent.event_delivery_report); @@ -144,7 +149,7 @@ Then('the Conversation event describes a FAILED event delivery status and its re assert.equal(eventDeliveryReport.reason.code, expectedReasonCode); }); -When('I send a request to trigger a "EVENT_DELIVERY" event with a DELIVERED status', async () => { +When('I send a request to trigger a "EVENT_DELIVERY" event with a "DELIVERED" status', async () => { const response = await fetch('http://localhost:3014/webhooks/conversation/event-delivery-report/succeeded'); await processEvent(response); }); @@ -161,12 +166,12 @@ Then('the Conversation event describes a "EVENT_INBOUND" event type', () => { assert.equal(eventInbound.trigger, expectedTrigger); }); -When('I send a request to trigger a "MESSAGE_DELIVERY" event with a FAILED status', async () => { +When('I send a request to trigger a "MESSAGE_DELIVERY" event with a "FAILED" status', async () => { const response = await fetch('http://localhost:3014/webhooks/conversation/message-delivery-report/failed'); await processEvent(response); }); -When('I send a request to trigger a "MESSAGE_DELIVERY" event with a QUEUED status', async () => { +When('I send a request to trigger a "MESSAGE_DELIVERY" event with a "QUEUED_ON_CHANNEL" status', async () => { const response = await fetch('http://localhost:3014/webhooks/conversation/message-delivery-report/succeeded'); await processEvent(response); }); @@ -212,12 +217,17 @@ Then('the Conversation event describes a "MESSAGE_INBOUND_SMART_CONVERSATION_RED assert.equal(messageInboundSmartConversationRedactionEvent.trigger, expectedTrigger); }); -When('I send a request to trigger a "MESSAGE_SUBMIT" event for a media message', async () => { +When('I send a request to trigger a "MESSAGE_SUBMIT" event for a "media" message', async () => { const response = await fetch('http://localhost:3014/webhooks/conversation/message-submit/media'); await processEvent(response); }); -Then('the Conversation event describes a "MESSAGE_SUBMIT" event type for a media message', () => { +// eslint-disable-next-line @typescript-eslint/no-unused-vars +Then('the header of the Conversation event {} for a {} message contains a valid signature', (_event, _messageType) => { + assert.ok(conversationCallbackWebhook.validateAuthenticationHeader(formattedHeaders, rawEvent)); +}); + +Then('the Conversation event describes a "MESSAGE_SUBMIT" event type for a "media" message', () => { const messageSubmitEvent = event as Conversation.MessageSubmitEvent; assert.ok(messageSubmitEvent.message_submit_notification); const expectedTrigger: Conversation.WebhookTrigger = 'MESSAGE_SUBMIT'; @@ -225,12 +235,12 @@ Then('the Conversation event describes a "MESSAGE_SUBMIT" event type for a media assert.ok(messageSubmitEvent.message_submit_notification.submitted_message?.media_message); }); -When('I send a request to trigger a "MESSAGE_SUBMIT" event for a text message', async () => { +When('I send a request to trigger a "MESSAGE_SUBMIT" event for a "text" message', async () => { const response = await fetch('http://localhost:3014/webhooks/conversation/message-submit/text'); await processEvent(response); }); -Then('the Conversation event describes a "MESSAGE_SUBMIT" event type for a text message', () => { +Then('the Conversation event describes a "MESSAGE_SUBMIT" event type for a "text" message', () => { const messageSubmitEvent = event as Conversation.MessageSubmitEvent; assert.ok(messageSubmitEvent.message_submit_notification); const expectedTrigger: Conversation.WebhookTrigger = 'MESSAGE_SUBMIT'; @@ -238,12 +248,12 @@ Then('the Conversation event describes a "MESSAGE_SUBMIT" event type for a text assert.ok(messageSubmitEvent.message_submit_notification.submitted_message?.text_message); }); -When('I send a request to trigger a "SMART_CONVERSATIONS" event for a media message', async () => { - const response = await fetch('http://localhost:3014/webhooks/conversation/smart-conversation/media'); +When('I send a request to trigger a "SMART_CONVERSATIONS" event for a "media" message', async () => { + const response = await fetch('http://localhost:3014/webhooks/conversation/smart-conversations/media'); await processEvent(response); }); -Then('the Conversation event describes a "SMART_CONVERSATIONS" event type for a media message', () => { +Then('the Conversation event describes a "SMART_CONVERSATIONS" event type for a "media" message', () => { const smartConversationsEvent = event as Conversation.SmartConversationsEvent; assert.ok(smartConversationsEvent.smart_conversation_notification); const expectedTrigger: Conversation.WebhookTrigger = 'SMART_CONVERSATIONS'; @@ -252,12 +262,12 @@ Then('the Conversation event describes a "SMART_CONVERSATIONS" event type for a assert.ok(smartConversationsEvent.smart_conversation_notification.analysis_results?.ml_offensive_analysis_result); }); -When('I send a request to trigger a "SMART_CONVERSATIONS" event for a text message', async () => { - const response = await fetch('http://localhost:3014/webhooks/conversation/smart-conversation/text'); +When('I send a request to trigger a "SMART_CONVERSATIONS" event for a "text" message', async () => { + const response = await fetch('http://localhost:3014/webhooks/conversation/smart-conversations/text'); await processEvent(response); }); -Then('the Conversation event describes a "SMART_CONVERSATIONS" event type for a text message', () => { +Then('the Conversation event describes a "SMART_CONVERSATIONS" event type for a "text" message', () => { const smartConversationsEvent = event as Conversation.SmartConversationsEvent; assert.ok(smartConversationsEvent.smart_conversation_notification); const expectedTrigger: Conversation.WebhookTrigger = 'SMART_CONVERSATIONS'; From 4b3a899faea5bc96fb5a76ebcdeabf32bcf85bcc Mon Sep 17 00:00:00 2001 From: Antoine SEIN <142824551+asein-sinch@users.noreply.github.com> Date: Mon, 23 Sep 2024 20:35:35 +0200 Subject: [PATCH 40/54] DEVEXP-524: E2E Voice/Callouts (#133) --- .github/workflows/run-ci.yaml | 11 +- packages/voice/cucumber.js | 8 ++ packages/voice/package.json | 3 +- .../conference-callout-request.ts | 3 +- .../tests/rest/v1/callouts/callouts.steps.ts | 121 ++++++++++++++++++ 5 files changed, 135 insertions(+), 11 deletions(-) create mode 100644 packages/voice/cucumber.js create mode 100644 packages/voice/tests/rest/v1/callouts/callouts.steps.ts diff --git a/.github/workflows/run-ci.yaml b/.github/workflows/run-ci.yaml index 846bee5f..cae140be 100644 --- a/.github/workflows/run-ci.yaml +++ b/.github/workflows/run-ci.yaml @@ -32,10 +32,6 @@ jobs: token: ${{ secrets.PAT_CI }} fetch-depth: 0 path: sinch-sdk-mockserver - - name: Build custom Docker image - run: | - cd sinch-sdk-mockserver - docker build -t sinch-sdk-mockserver -f Dockerfile . - name: Install Docker Compose run: | sudo apt-get update @@ -44,11 +40,6 @@ jobs: run: | cd sinch-sdk-mockserver docker-compose up -d - - name: Wait for the mock servers to be healthy - run: | - cd sinch-sdk-mockserver - chmod +x ./scripts/healthcheck.sh - ./scripts/healthcheck.sh - name: Create target directories for feature files run: | mkdir -p ./packages/fax/tests/e2e/features @@ -57,6 +48,7 @@ jobs: mkdir -p ./packages/elastic-sip-trunking/tests/e2e/features mkdir -p ./packages/sms/tests/e2e/features mkdir -p ./packages/verification/tests/e2e/features + mkdir -p ./packages/voice/tests/e2e/features - name: Copy feature files run: | cp sinch-sdk-mockserver/features/fax/*.feature ./packages/fax/tests/e2e/features/ @@ -65,6 +57,7 @@ jobs: cp sinch-sdk-mockserver/features/elastic-sip-trunking/*.feature ./packages/elastic-sip-trunking/tests/e2e/features/ cp sinch-sdk-mockserver/features/sms/*.feature ./packages/sms/tests/e2e/features/ cp sinch-sdk-mockserver/features/verification/*.feature ./packages/verification/tests/e2e/features/ + cp sinch-sdk-mockserver/features/voice/*.feature ./packages/voice/tests/e2e/features/ - name: Run e2e tests run: | yarn install diff --git a/packages/voice/cucumber.js b/packages/voice/cucumber.js new file mode 100644 index 00000000..691a9809 --- /dev/null +++ b/packages/voice/cucumber.js @@ -0,0 +1,8 @@ +module.exports = { + default: [ + 'tests/e2e/features/**/*.feature', + '--require-module ts-node/register', + '--require tests/rest/v1/**/*.steps.ts', + `--format-options '{"snippetInterface": "synchronous"}'`, + ].join(' '), +}; diff --git a/packages/voice/package.json b/packages/voice/package.json index 473628fc..3a5528f4 100644 --- a/packages/voice/package.json +++ b/packages/voice/package.json @@ -25,7 +25,8 @@ "scripts": { "build": "yarn run clean && yarn run compile", "clean": "rimraf dist tsconfig.tsbuildinfo tsconfig.build.tsbuildinfo", - "compile": "tsc -p tsconfig.build.json && tsc -p tsconfig.tests.json && rimraf dist/tests tsconfig.build.tsbuildinfo" + "compile": "tsc -p tsconfig.build.json && tsc -p tsconfig.tests.json && rimraf dist/tests tsconfig.build.tsbuildinfo", + "test:e2e": "cucumber-js" }, "dependencies": { "@sinch/sdk-client": "^1.1.0" diff --git a/packages/voice/src/models/v1/conference-callout-request/conference-callout-request.ts b/packages/voice/src/models/v1/conference-callout-request/conference-callout-request.ts index 6d7583da..54a96829 100644 --- a/packages/voice/src/models/v1/conference-callout-request/conference-callout-request.ts +++ b/packages/voice/src/models/v1/conference-callout-request/conference-callout-request.ts @@ -1,5 +1,6 @@ import { Destination } from '../destination'; import { ConferenceDtmfOptions } from '../conference-dtmf-options'; +import { MusicOnHold } from '../enums'; /** * The conference callout calls a phone number or a user. When the call is answered, it's connected to a conference room. @@ -28,7 +29,7 @@ export interface ConferenceCalloutRequest { /** The text that will be spoken as a greeting. */ greeting?: string; /** Means "music-on-hold." It's an optional parameter that specifies what the first participant should listen to while they're alone in the conference, waiting for other participants to join. It can take one of these pre-defined values:
  • `ring` (progress tone)
  • `music1` (music file)
  • `music2` (music file)
  • `music3` (music file)

If no โ€œmusic-on-holdโ€ is specified, the user will only hear silence. */ - mohClass?: string; + mohClass?: MusicOnHold; /** Used to input custom data. */ custom?: string; /** can be either โ€œpstnโ€ for PSTN endpoint or โ€œmxpโ€ for data (app or web) clients. */ diff --git a/packages/voice/tests/rest/v1/callouts/callouts.steps.ts b/packages/voice/tests/rest/v1/callouts/callouts.steps.ts new file mode 100644 index 00000000..4f2662dd --- /dev/null +++ b/packages/voice/tests/rest/v1/callouts/callouts.steps.ts @@ -0,0 +1,121 @@ +import { CalloutsApi, VoiceService, Voice } from '../../../../src'; +import { Given, When, Then } from '@cucumber/cucumber'; +import * as assert from 'assert'; + +let calloutsApi: CalloutsApi; +let ttsCallResponse: Voice.CalloutResponse; + +Given('the Voice service "Callouts" is available', () => { + const voiceService = new VoiceService({ + applicationKey: 'appKey', + applicationSecret: 'appSecret', + voiceHostname: 'http://localhost:3019', + }); + calloutsApi = voiceService.callouts; +}); + +When('I send a request to make a TTS call', async () => { + ttsCallResponse = await calloutsApi.tts({ + ttsCalloutRequestBody: { + method: 'ttsCallout', + ttsCallout: { + cli: '+12015555555', + destination: { + type: 'number', + endpoint: '+12017777777', + }, + locale: 'en-US', + text: 'Hello, this is a call from Sinch.', + }, + }, + }); +}); + +Then('the callout response contains the TTS call ID', () => { + assert.equal(ttsCallResponse.callId, '1ce0ffee-ca11-ca11-ca11-abcdef000001'); +}); + +When('I send a request to make a Conference call with the "Callout" service', async () => { + ttsCallResponse = await calloutsApi.conference({ + conferenceCalloutRequestBody: { + method: 'conferenceCallout', + conferenceCallout: { + cli: '+12015555555', + destination: { + type: 'number', + endpoint: '+12017777777', + }, + conferenceId: 'myConferenceId-E2E', + locale: 'en-US', + greeting: 'Welcome to this conference call.', + mohClass: 'music1', + }, + }, + }); +}); + +Then('the callout response contains the Conference call ID', () => { + assert.equal(ttsCallResponse.callId, '1ce0ffee-ca11-ca11-ca11-abcdef000002'); +}); + +When('I send a request to make a Custom call', async () => { + ttsCallResponse = await calloutsApi.custom({ + customCalloutRequestBody: { + method: 'customCallout', + customCallout: { + cli: '+12015555555', + destination: { + type: 'number', + endpoint: '+12017777777', + }, + custom: 'Custom text', + ice: Voice.customCalloutHelper.formatIceResponse( + Voice.iceActionHelper.connectPstn({ + number: '+12017777777', + cli: '+12015555555', + }), + Voice.iceInstructionHelper.say('Welcome to Sinch.', 'en-US/male'), + Voice.iceInstructionHelper.startRecording({ + destinationUrl: 'To specify', + credentials: 'To specify', + }), + ), + ace: Voice.customCalloutHelper.formatAceResponse( + Voice.aceActionHelper.runMenu({ + locale: 'Kimberly', + enableVoice: true, + barge: true, + menus: [ + { + id: 'main', + mainPrompt: '#tts[Welcome to the main menu. Press 1 to confirm order or 2 to cancel]', + repeatPrompt: '#tts[We didn\'t get your input, please try again]', + timeoutMills: 5000, + options: [ + { + dtmf: '1', + action: 'menu(confirm)', + }, + { + dtmf: '2', + action: 'return(cancel)', + }, + ], + }, + { + id: 'confirm', + mainPrompt: '#tts[Thank you for confirming your order. Enter your 4-digit PIN.]', + maxDigits: 4, + }, + ], + }), + ), + pie: 'https://callback-server.com/voice', + }, + }, + }); +}); + +Then('the callout response contains the Custom call ID', () => { + assert.equal(ttsCallResponse.callId, '1ce0ffee-ca11-ca11-ca11-abcdef000003'); +}); From dae67649a13aaa0b73d99a2cb1a2fc18d79acf8d Mon Sep 17 00:00:00 2001 From: Antoine SEIN <142824551+asein-sinch@users.noreply.github.com> Date: Mon, 23 Sep 2024 20:46:46 +0200 Subject: [PATCH 41/54] DEVEXP-525: E2E Voice/Calls (#134) --- .../src/models/v1/participant/participant.ts | 2 +- .../voice/tests/rest/v1/calls/calls.steps.ts | 118 ++++++++++++++++++ 2 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 packages/voice/tests/rest/v1/calls/calls.steps.ts diff --git a/packages/voice/src/models/v1/participant/participant.ts b/packages/voice/src/models/v1/participant/participant.ts index 82124509..004b70b1 100644 --- a/packages/voice/src/models/v1/participant/participant.ts +++ b/packages/voice/src/models/v1/participant/participant.ts @@ -4,7 +4,7 @@ export interface Participant { /** The type of the participant (caller or callee). */ - type?: string; + type?: 'number' | 'Number' | 'username' | 'Username' | 'sip' | 'did' | string; /** The phone number, user name, or other identifier of the participant (caller or callee). */ endpoint?: string; } diff --git a/packages/voice/tests/rest/v1/calls/calls.steps.ts b/packages/voice/tests/rest/v1/calls/calls.steps.ts new file mode 100644 index 00000000..d16d218f --- /dev/null +++ b/packages/voice/tests/rest/v1/calls/calls.steps.ts @@ -0,0 +1,118 @@ +import { CallsApi, VoiceService, Voice } from '../../../../src'; +import { Given, When, Then } from '@cucumber/cucumber'; +import * as assert from 'assert'; + +let callsApi: CallsApi; +let callInformation: Voice.GetCallInformation; +let updateCallResponse: void; +let error: any; +let manageWithCallLegResponse: void; + +Given('the Voice service "Calls" is available', () => { + const voiceService = new VoiceService({ + applicationKey: 'appKey', + applicationSecret: 'appSecret', + voiceHostname: 'http://localhost:3019', + }); + callsApi = voiceService.calls; +}); + +When('I send a request to get a call\'s information', async () => { + callInformation = await callsApi.get({ + callId: '1ce0ffee-ca11-ca11-ca11-abcdef000003', + }); +}); + +Then('the response contains the information about the call', () => { + assert.equal(callInformation.callId, '1ce0ffee-ca11-ca11-ca11-abcdef000003'); + const participant: Voice.Participant = { + type: 'Number', + endpoint: '+12017777777', + }; + assert.deepEqual(callInformation.to, participant); + assert.equal(callInformation.domain, 'pstn'); + assert.equal(callInformation.duration, 14); + assert.equal(callInformation.status, 'FINAL'); + const result: Voice.ResultEnum = 'ANSWERED'; + assert.equal(callInformation.result, result); + const reason: Voice.ReasonEnum = 'MANAGERHANGUP'; + assert.equal(callInformation.reason, reason); + assert.deepEqual(callInformation.timestamp, new Date('2024-06-06T17:36:00Z')); + assert.equal(callInformation.custom, 'Custom text'); + const userRate: Voice.VoicePrice = { + currencyId: 'EUR', + amount: 0.1758, + }; + assert.deepEqual(callInformation.userRate, userRate); + const debit: Voice.VoicePrice = { + currencyId: 'EUR', + amount: 0.1758, + }; + assert.deepEqual(callInformation.debit, debit); +}); + +When('I send a request to update a call', async () => { + updateCallResponse = await callsApi.update({ + callId: '1ce0ffee-ca11-ca11-ca11-abcdef000022', + updateCallRequestBody: { + instructions: [ + Voice.svamlInstructionHelper.buildSay( + 'Sorry, the conference has been cancelled. The call will end now.', + 'en-US', + ), + ], + action: Voice.svamlActionHelper.buildHangup(), + }, + }); +}); + +Then('the update call response contains no data', () => { + assert.deepEqual(updateCallResponse, {}); +}); + +When('I send a request to update a call that doesn\'t exist', async () => { + updateCallResponse = undefined; + try { + updateCallResponse = await callsApi.update({ + callId: 'not-existing-callId', + updateCallRequestBody: { + instructions: [ + Voice.svamlInstructionHelper.buildSay( + 'Sorry, the conference has been cancelled. The call will end now.', + 'en-US', + ), + ], + action: Voice.svamlActionHelper.buildHangup(), + }, + }); + } catch (e) { + error = e; + } +}); + +Then('the update call response contains a "not found" error', () => { + assert.equal(updateCallResponse, undefined); + const voiceError = JSON.parse(error.data) as Voice.VoiceError; + assert.equal(voiceError.errorCode, 40400); + assert.equal(voiceError.message, 'Call not found'); + assert.equal(voiceError.reference, '38188074-abcd-56ab-ab64-daf82fada8e8'); +}); + +When('I send a request to manage a call with callLeg', async () => { + manageWithCallLegResponse = await callsApi.manageWithCallLeg({ + callId: '1ce0ffee-ca11-ca11-ca11-abcdef000032', + callLeg: 'callee', + manageWithCallLegRequestBody: { + instructions: [ + Voice.svamlInstructionHelper.buildPlayFiles( + ['https://samples-files.com/samples/Audio/mp3/sample-file-4.mp3'], + ), + ], + action: Voice.svamlActionHelper.buildContinue(), + }, + }); +}); + +Then('the manage a call with callLeg response contains no data', () => { + assert.deepEqual(manageWithCallLegResponse, {}); +}); From 91e76a012cec87e6bc1ed99d43c1ee7d72208298 Mon Sep 17 00:00:00 2001 From: Antoine SEIN <142824551+asein-sinch@users.noreply.github.com> Date: Tue, 24 Sep 2024 08:44:00 +0200 Subject: [PATCH 42/54] DEVEXP-526: E2E Voice/Conferences (#135) --- .../rest/v1/conferences/conferences.steps.ts | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 packages/voice/tests/rest/v1/conferences/conferences.steps.ts diff --git a/packages/voice/tests/rest/v1/conferences/conferences.steps.ts b/packages/voice/tests/rest/v1/conferences/conferences.steps.ts new file mode 100644 index 00000000..b897190b --- /dev/null +++ b/packages/voice/tests/rest/v1/conferences/conferences.steps.ts @@ -0,0 +1,100 @@ +import { ConferencesApi, VoiceService, Voice } from '../../../../src'; +import { Given, When, Then } from '@cucumber/cucumber'; +import * as assert from 'assert'; + +let conferencesApi: ConferencesApi; +let conferenceCallResponse: Voice.CalloutResponse; +let conferenceInformation: Voice.GetConferenceInfoResponse; +let manageParticipantResponse: void; +let kickParticipantResponse: void; +let kickAllParticipantsResponse: void; + +Given('the Voice service "Conferences" is available', () => { + const voiceService = new VoiceService({ + applicationKey: 'appKey', + applicationSecret: 'appSecret', + voiceHostname: 'http://localhost:3019', + }); + conferencesApi = voiceService.conferences; +}); + +When('I send a request to make a Conference call with the "Conferences" service', async () => { + conferenceCallResponse = await conferencesApi.call({ + conferenceCalloutRequestBody: { + method: 'conferenceCallout', + conferenceCallout: { + cli: '+12015555555', + destination: { + type: 'number', + endpoint: '+12017777777', + }, + conferenceId: 'myConferenceId-E2E', + locale: 'en-US', + greeting: 'Welcome to this conference call.', + mohClass: 'music1', + }, + }, + }); +}); + +Then('the callout response from the "Conferences" service contains the Conference call ID', () => { + assert.equal(conferenceCallResponse.callId, '1ce0ffee-ca11-ca11-ca11-abcdef000002'); +}); + +When('I send a request to get the conference information', async () => { + conferenceInformation = await conferencesApi.get({ + conferenceId: 'myConferenceId-E2E', + }); +}); + +Then('the response contains the information about the conference participants', () => { + assert.ok(conferenceInformation.participants); + const participant1 = conferenceInformation.participants[0]; + assert.equal(participant1.id, '1ce0ffee-ca11-ca11-ca11-abcdef000012'); + assert.equal(participant1.cli, '+12015555555'); + assert.equal(participant1.duration, 35); + assert.equal(participant1.muted, true); + assert.equal(participant1.onhold, true); + const participant2 = conferenceInformation.participants[1]; + assert.equal(participant2.id, '1ce0ffee-ca11-ca11-ca11-abcdef000022'); + assert.equal(participant2.cli, '+12015555555'); + assert.equal(participant2.duration, 6); + assert.equal(participant2.muted, false); + assert.equal(participant2.onhold, false); +}); + +When('I send a request to put a participant on hold', async () => { + manageParticipantResponse = await conferencesApi.manageParticipant({ + conferenceId: 'myConferenceId-E2E', + callId: '1ce0ffee-ca11-ca11-ca11-abcdef000012', + manageParticipantRequestBody: { + command: 'onhold', + moh: 'music2', + }, + }); +}); + +Then('the manage participant response contains no data', () => { + assert.deepEqual(manageParticipantResponse, {}); +}); + +When('I send a request to kick a participant from a conference', async () => { + kickParticipantResponse = await conferencesApi.kickParticipant({ + conferenceId: 'myConferenceId-E2E', + callId: '1ce0ffee-ca11-ca11-ca11-abcdef000012', + }); +}); + +Then('the kick participant response contains no data', () => { + assert.deepEqual(kickParticipantResponse, {}); +}); + +When('I send a request to kick all the participants from a conference', async () => { + kickAllParticipantsResponse = await conferencesApi.kickAll({ + conferenceId: 'myConferenceId-E2E', + }); +}); + +Then('the kick all participants response contains no data', () => { + assert.deepEqual(kickAllParticipantsResponse, {}); +}); From baa074960f69b67c8c1ada89fc190e39fe77529f Mon Sep 17 00:00:00 2001 From: Antoine SEIN <142824551+asein-sinch@users.noreply.github.com> Date: Tue, 24 Sep 2024 09:29:48 +0200 Subject: [PATCH 43/54] DEVEXP-527: E2E Voice/Applications (#136) --- .../v1/applications/applications.steps.ts | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 packages/voice/tests/rest/v1/applications/applications.steps.ts diff --git a/packages/voice/tests/rest/v1/applications/applications.steps.ts b/packages/voice/tests/rest/v1/applications/applications.steps.ts new file mode 100644 index 00000000..2a896c81 --- /dev/null +++ b/packages/voice/tests/rest/v1/applications/applications.steps.ts @@ -0,0 +1,112 @@ +import { ApplicationsApi, VoiceService, Voice } from '../../../../src'; +import { Given, When, Then } from '@cucumber/cucumber'; +import * as assert from 'assert'; + +let applicationsApi: ApplicationsApi; +let listNumbersResponse: Voice.ListNumbersResponse; +let assignNumbersResponse: void; +let unassignNumberResponse: void; +let queryNumberResponse: Voice.QueryNumberResponse; +let callbackURLs: Voice.GetCallbacks; +let updateCallbackURLsResponse: void; + +Given('the Voice service "Applications" is available', () => { + const voiceService = new VoiceService({ + applicationKey: 'appKey', + applicationSecret: 'appSecret', + voiceApplicationManagementHostname: 'http://localhost:3020', + }); + applicationsApi = voiceService.applications; +}); + +When('I send a request to get information about my owned numbers', async () => { + listNumbersResponse = await applicationsApi.listNumbers({}); +}); + +Then('the response contains details about the numbers that I own', () => { + assert.ok(listNumbersResponse.numbers); + assert.equal(listNumbersResponse.numbers.length, 4); + const number1 = listNumbersResponse.numbers[0]; + assert.equal(number1.number, '+12012222222'); + assert.equal(number1.applicationkey, undefined); + assert.equal(number1.capability, 'voice'); + const number2 = listNumbersResponse.numbers[1]; + assert.equal(number2.number, '+12013333333'); + assert.equal(number2.applicationkey, 'ba5eba11-1dea-1337-babe-5a1ad00d1eaf'); + assert.equal(number2.capability, 'voice'); +}); + +When('I send a request to assign some numbers to a Voice Application', async () => { + assignNumbersResponse = await applicationsApi.assignNumbers({ + assignNumbersRequestBody: { + numbers: [ + '+12012222222', + ], + applicationkey: 'f00dcafe-abba-c0de-1dea-dabb1ed4caf3', + capability: 'voice', + }, + }); +}); + +Then('the assign numbers response contains no data', () => { + assert.deepEqual(assignNumbersResponse, {}); +}); + +When('I send a request to unassign a number from a Voice Application', async () => { + unassignNumberResponse = await applicationsApi.unassignNumber({ + unassignNumbersRequestBody: { + number: '+12012222222', + }, + }); +}); + +Then('the unassign number response contains no data', () => { + assert.deepEqual(unassignNumberResponse, {}); +}); + +When('I send a request to get information about a specific number', async () => { + queryNumberResponse = await applicationsApi.queryNumber({ + number: '+12015555555', + }); +}); + +Then('the response contains details about the specific number', () => { + assert.ok(queryNumberResponse.number); + const number = queryNumberResponse.number; + assert.equal(number.countryId, 'US'); + assert.equal(number.numberType, 'Fixed'); + assert.equal(number.normalizedNumber, '+12015555555'); + assert.equal(number.restricted, true); + const rate: Voice.VoicePrice = { + currencyId: 'USD', + amount: 0.0100, + }; + assert.deepEqual(number.rate, rate); +}); + +When('I send a request to get the callback URLs associated to an application', async () => { + callbackURLs = await applicationsApi.getCallbackURLs({ + applicationkey: 'f00dcafe-abba-c0de-1dea-dabb1ed4caf3', + }); +}); + +Then('the response contains callback URLs details', () => { + assert.ok(callbackURLs.url); + assert.equal(callbackURLs.url.primary, 'https://my.callback-server.com/voice'); + assert.equal(callbackURLs.url.fallback, 'https://my.fallback-server.com/voice'); +}); + +When('I send a request to update the callback URLs associated to an application', async () => { + updateCallbackURLsResponse = await applicationsApi.updateCallbackURLs({ + applicationkey: 'f00dcafe-abba-c0de-1dea-dabb1ed4caf3', + updateCallbacksRequestBody: { + url: { + primary: 'https://my-new.callback-server.com/voice', + }, + }, + }); +}); + +Then('the update callback URLs response contains no data', () => { + assert.deepEqual(updateCallbackURLsResponse, {}); +}); From dc0b5bc04c44acbc2eb3fbe9bcb90ff0670937c0 Mon Sep 17 00:00:00 2001 From: Antoine SEIN <142824551+asein-sinch@users.noreply.github.com> Date: Tue, 24 Sep 2024 11:09:22 +0200 Subject: [PATCH 44/54] DEVEXP-528: E2E Voice/Callbacks (#137) --- .../src/services/voice-event.service.ts | 7 +- .../src/models/v1/mod-callbacks/index.ts | 1 + .../v1/mod-callbacks/pie-request/index.ts | 2 +- .../mod-callbacks/pie-request/pie-request.ts | 4 +- .../v1/mod-callbacks/voice-callback-event.ts | 7 + .../rest/v1/callbacks/callbacks-webhook.ts | 9 +- .../v1/callbacks/webhooks-events.steps.ts | 158 ++++++++++++++++++ 7 files changed, 177 insertions(+), 11 deletions(-) create mode 100644 packages/voice/src/models/v1/mod-callbacks/voice-callback-event.ts create mode 100644 packages/voice/tests/rest/v1/callbacks/webhooks-events.steps.ts diff --git a/examples/webhooks/src/services/voice-event.service.ts b/examples/webhooks/src/services/voice-event.service.ts index ba85be68..7c71600e 100644 --- a/examples/webhooks/src/services/voice-event.service.ts +++ b/examples/webhooks/src/services/voice-event.service.ts @@ -1,14 +1,11 @@ import { Injectable } from '@nestjs/common'; import { Response } from 'express'; -import { - VoiceCallback, - Voice, -} from '@sinch/sdk-core'; +import { Voice } from '@sinch/sdk-core'; @Injectable() export class VoiceEventService { - handleEvent(event: VoiceCallback, res: Response) { + handleEvent(event: Voice.VoiceCallbackEvent, res: Response) { console.log(`:: INCOMING EVENT :: ${event.event}`); switch (event.event) { case 'ice': diff --git a/packages/voice/src/models/v1/mod-callbacks/index.ts b/packages/voice/src/models/v1/mod-callbacks/index.ts index 62e23609..c47363b9 100644 --- a/packages/voice/src/models/v1/mod-callbacks/index.ts +++ b/packages/voice/src/models/v1/mod-callbacks/index.ts @@ -8,3 +8,4 @@ export * from './notify-request'; export * from './pie-request'; export * from './pie-response'; export * from './callback-response'; +export * from './voice-callback-event'; diff --git a/packages/voice/src/models/v1/mod-callbacks/pie-request/index.ts b/packages/voice/src/models/v1/mod-callbacks/pie-request/index.ts index 12e1bc6f..57d27616 100644 --- a/packages/voice/src/models/v1/mod-callbacks/pie-request/index.ts +++ b/packages/voice/src/models/v1/mod-callbacks/pie-request/index.ts @@ -1 +1 @@ -export type { PieRequest, MenuResult } from './pie-request'; +export type { PieRequest, MenuResult, PieInformationType } from './pie-request'; diff --git a/packages/voice/src/models/v1/mod-callbacks/pie-request/pie-request.ts b/packages/voice/src/models/v1/mod-callbacks/pie-request/pie-request.ts index 6e5e40b5..dff7c992 100644 --- a/packages/voice/src/models/v1/mod-callbacks/pie-request/pie-request.ts +++ b/packages/voice/src/models/v1/mod-callbacks/pie-request/pie-request.ts @@ -24,9 +24,11 @@ export interface MenuResult { /** The ID of the menu that triggered the prompt input event. */ menuId?: string; /** The type of information that's returned. */ - type?: string; + type?: PieInformationType; /** The value of the returned information. */ value?: string; /** The type of input received. */ inputMethod?: string; } + +export type PieInformationType = 'error' | 'return' | 'sequence' | 'timeout' | 'hangup' | 'invalidinput'; diff --git a/packages/voice/src/models/v1/mod-callbacks/voice-callback-event.ts b/packages/voice/src/models/v1/mod-callbacks/voice-callback-event.ts new file mode 100644 index 00000000..69178239 --- /dev/null +++ b/packages/voice/src/models/v1/mod-callbacks/voice-callback-event.ts @@ -0,0 +1,7 @@ +import { IceRequest } from './ice-request'; +import { AceRequest } from './ace-request'; +import { DiceRequest } from './dice-request'; +import { PieRequest } from './pie-request'; +import { NotifyRequest } from './notify-request'; + +export type VoiceCallbackEvent = IceRequest | AceRequest | DiceRequest | PieRequest | NotifyRequest; diff --git a/packages/voice/src/rest/v1/callbacks/callbacks-webhook.ts b/packages/voice/src/rest/v1/callbacks/callbacks-webhook.ts index b4293b7e..2db77040 100644 --- a/packages/voice/src/rest/v1/callbacks/callbacks-webhook.ts +++ b/packages/voice/src/rest/v1/callbacks/callbacks-webhook.ts @@ -1,10 +1,11 @@ -import { AceRequest, DiceRequest, IceRequest, NotifyRequest, PieRequest } from '../../../models'; +import { AceRequest, DiceRequest, IceRequest, NotifyRequest, PieRequest, VoiceCallbackEvent } from '../../../models'; import { CallbackProcessor, SinchClientParameters, validateAuthenticationHeader } from '@sinch/sdk-client'; import { IncomingHttpHeaders } from 'http'; +/** @deprecated - use Voice.VoiceCallbackEvent instead */ export type VoiceCallback = IceRequest | AceRequest | DiceRequest | PieRequest | NotifyRequest; -export class VoiceCallbackWebhooks implements CallbackProcessor{ +export class VoiceCallbackWebhooks implements CallbackProcessor{ private readonly sinchClientParameters: SinchClientParameters; constructor(sinchClientParameters: SinchClientParameters) { @@ -38,9 +39,9 @@ export class VoiceCallbackWebhooks implements CallbackProcessor{ * Reviver for a Voice Event. * This method ensures the object can be treated as a Voice Event and should be called before any action is taken to manipulate the object. * @param {any} eventBody - The event body containing the voice event notification. - * @return {VoiceCallback} - The parsed voice event object. + * @return {VoiceCallbackEvent} - The parsed voice event object. */ - public parseEvent(eventBody: any): VoiceCallback { + public parseEvent(eventBody: any): VoiceCallbackEvent { if (eventBody.timestamp) { eventBody.timestamp = new Date(eventBody.timestamp); } diff --git a/packages/voice/tests/rest/v1/callbacks/webhooks-events.steps.ts b/packages/voice/tests/rest/v1/callbacks/webhooks-events.steps.ts new file mode 100644 index 00000000..e997d8ad --- /dev/null +++ b/packages/voice/tests/rest/v1/callbacks/webhooks-events.steps.ts @@ -0,0 +1,158 @@ +import { VoiceCallbackWebhooks, Voice } from '../../../../src'; +import { Given, Then, When } from '@cucumber/cucumber'; +import assert from 'assert'; +import { IncomingHttpHeaders } from 'http'; + +let voiceCallbackWebhooks: VoiceCallbackWebhooks; +let rawEvent: any; +let event: Voice.VoiceCallbackEvent; +let formattedHeaders: IncomingHttpHeaders; + +const processEvent = async (response: Response) => { + formattedHeaders = {}; + response.headers.forEach((value, name) => { + formattedHeaders[name.toLowerCase()] = value; + }); + rawEvent = await response.text(); + event = voiceCallbackWebhooks.parseEvent(JSON.parse(rawEvent)); +}; + + +Given('the Voice Webhooks handler is available', () => { + voiceCallbackWebhooks = new VoiceCallbackWebhooks({ + applicationKey: 'appKey', + applicationSecret: 'appSecret', + }); +}); + +When('I send a request to trigger a "PIE" event with a "return" type', async () => { + const response = await fetch('http://localhost:3019/webhooks/voice/pie-return'); + await processEvent(response); +}); + +Then('the header of the "PIE" event with a "return" type contains a valid authorization', () => { + assert.ok(voiceCallbackWebhooks.validateAuthenticationHeader( + formattedHeaders, + rawEvent, + '/webhooks/voice', + 'POST')); +}); + +Then('the Voice event describes a "PIE" event with a "return" type', () => { + const pieEvent = event as Voice.PieRequest; + assert.equal(pieEvent.callid, '1ce0ffee-ca11-ca11-ca11-abcdef000013'); + assert.equal(pieEvent.event, 'pie'); + const menuResult: Voice.MenuResult = { + type: 'return', + value: 'cancel', + menuId: 'main', + inputMethod: 'dtmf', + }; + assert.ok(pieEvent.menuResult); + assert.equal(pieEvent.menuResult.type, menuResult.type); + assert.equal(pieEvent.menuResult.value, menuResult.value); + assert.equal(pieEvent.menuResult.menuId, menuResult.menuId); + assert.equal(pieEvent.menuResult.inputMethod, menuResult.inputMethod); + assert.equal(pieEvent.version, 1); + assert.equal(pieEvent.custom, 'Custom text'); + assert.equal(pieEvent.applicationKey, 'f00dcafe-abba-c0de-1dea-dabb1ed4caf3'); +}); + +When('I send a request to trigger a "PIE" event with a "sequence" type', async () => { + const response = await fetch('http://localhost:3019/webhooks/voice/pie-sequence'); + await processEvent(response); +}); + +Then('the header of the "PIE" event with a "sequence" type contains a valid authorization', () => { + assert.ok(voiceCallbackWebhooks.validateAuthenticationHeader( + formattedHeaders, + rawEvent, + '/webhooks/voice', + 'POST')); +}); + +Then('the Voice event describes a "PIE" event with a "sequence" type', () => { + const pieEvent = event as Voice.PieRequest; + assert.equal(pieEvent.callid, '1ce0ffee-ca11-ca11-ca11-abcdef000023'); + assert.equal(pieEvent.event, 'pie'); + const menuResult: Voice.MenuResult = { + type: 'sequence', + value: '1234', + menuId: 'confirm', + inputMethod: 'dtmf', + }; + assert.ok(pieEvent.menuResult); + assert.equal(pieEvent.menuResult.type, menuResult.type); + assert.equal(pieEvent.menuResult.value, menuResult.value); + assert.equal(pieEvent.menuResult.menuId, menuResult.menuId); + assert.equal(pieEvent.menuResult.inputMethod, menuResult.inputMethod); + assert.equal(pieEvent.version, 1); + assert.equal(pieEvent.custom, 'Custom text'); + assert.equal(pieEvent.applicationKey, 'f00dcafe-abba-c0de-1dea-dabb1ed4caf3'); +}); + +When('I send a request to trigger a "DICE" event', async () => { + const response = await fetch('http://localhost:3019/webhooks/voice/dice'); + await processEvent(response); +}); + +Then('the header of the "DICE" event contains a valid authorization', () => { + assert.ok(voiceCallbackWebhooks.validateAuthenticationHeader( + formattedHeaders, + rawEvent, + '/webhooks/voice', + 'POST')); +}); + +Then('the Voice event describes a "DICE" event', () => { + const diceEvent = event as Voice.DiceRequest; + assert.equal(diceEvent.callid, '1ce0ffee-ca11-ca11-ca11-abcdef000033'); + assert.equal(diceEvent.event, 'dice'); + const reason: Voice.ReasonEnum = 'MANAGERHANGUP'; + assert.equal(diceEvent.reason, reason); + const result: Voice.ResultEnum = 'ANSWERED'; + assert.equal(diceEvent.result, result); + assert.equal(diceEvent.version, 1); + assert.equal(diceEvent.custom, 'Custom text'); + const debit: Voice.VoicePrice = { + currencyId: 'EUR', + amount: 0.0095, + }; + assert.deepEqual(diceEvent.userRate, debit); + const userRate: Voice.VoicePrice = { + currencyId: 'EUR', + amount: 0.0095, + }; + assert.deepEqual(diceEvent.userRate, userRate); + const destinationParticipant: Voice.Participant = { + type: 'number', + endpoint: '12017777777', + }; + assert.deepEqual(diceEvent.to, destinationParticipant); + assert.equal(diceEvent.applicationKey, 'f00dcafe-abba-c0de-1dea-dabb1ed4caf3'); + assert.equal(diceEvent.duration, 12); + assert.equal(diceEvent.from, '12015555555'); +}); + +When('I send a request to trigger a "ACE" event', async () => { + const response = await fetch('http://localhost:3019/webhooks/voice/ace'); + await processEvent(response); +}); + +Then('the header of the "ACE" event contains a valid authorization', () => { + assert.ok(voiceCallbackWebhooks.validateAuthenticationHeader( + formattedHeaders, + rawEvent, + '/webhooks/voice', + 'POST')); +}); + +Then('the Voice event describes a "ACE" event', () => { + const aceEvent = event as Voice.AceRequest; + assert.equal(aceEvent.callid, '1ce0ffee-ca11-ca11-ca11-abcdef000043'); + assert.equal(aceEvent.event, 'ace'); + assert.deepEqual(aceEvent.timestamp, new Date('2024-06-06T17:10:34Z')); + assert.equal(aceEvent.version, 1); + assert.equal(aceEvent.custom, 'Custom text'); + assert.equal(aceEvent.applicationKey, 'f00dcafe-abba-c0de-1dea-dabb1ed4caf3'); +}); From 8c515f1d1e4d68fb241a0be16bd7b34ab8d96ff4 Mon Sep 17 00:00:00 2001 From: Antoine SEIN <142824551+asein-sinch@users.noreply.github.com> Date: Tue, 1 Oct 2024 08:37:07 +0200 Subject: [PATCH 45/54] Fix audit report --- .github/scripts/validate-audit-report.sh | 4 +- examples/webhooks/package.json | 6 +- package.json | 12 +- yarn.lock | 1859 +++++++++++++--------- 4 files changed, 1156 insertions(+), 725 deletions(-) diff --git a/.github/scripts/validate-audit-report.sh b/.github/scripts/validate-audit-report.sh index 27ebfbac..2c9622ad 100755 --- a/.github/scripts/validate-audit-report.sh +++ b/.github/scripts/validate-audit-report.sh @@ -6,8 +6,8 @@ echo '{"vulnerabilities": [' > audit-report.json awk 'NR > 1 {print ","} {print}' audit-report.txt >> audit-report.json echo ']}' >> audit-report.json -# Filter JSON array to remove lerna's transitive dependencies as these dependencies are not used at runtime -jq '.vulnerabilities |= map(select(.data.resolution.path | type == "string" and startswith("lerna") | not))' audit-report.json > audit-report-filtered.json +# Filter JSON array to remove jest and lerna's transitive dependencies as these dependencies are not used at runtime +jq '.vulnerabilities |= map(select(.data.resolution.path | type == "string" and (startswith("lerna") or startswith("jest") or startswith("@types/jest")) | not))' audit-report.json > audit-report-filtered.json # Fail the build if filtered JSON array contains audit advisories if [ "$(jq '.vulnerabilities[] | select(.type == "auditAdvisory") | .type' audit-report-filtered.json | wc -l)" -gt 0 ]; then diff --git a/examples/webhooks/package.json b/examples/webhooks/package.json index 1e85d838..a5f52dbd 100644 --- a/examples/webhooks/package.json +++ b/examples/webhooks/package.json @@ -12,9 +12,9 @@ "start:prod": "node dist/main" }, "dependencies": { - "@nestjs/common": "^10.3.7", - "@nestjs/core": "^10.3.7", - "@nestjs/platform-express": "^10.3.7", + "@nestjs/common": "^10.4.4", + "@nestjs/core": "^10.4.4", + "@nestjs/platform-express": "^10.4.4", "@sinch/sdk-core": "^1.1.0", "dotenv": "^16.3.1", "raw-body": "^2.5.2", diff --git a/package.json b/package.json index 7838531b..13870acf 100644 --- a/package.json +++ b/package.json @@ -19,11 +19,11 @@ }, "keywords": [], "devDependencies": { - "@babel/core": "^7.23.3", - "@babel/preset-env": "^7.23.3", - "@babel/preset-typescript": "^7.23.3", + "@babel/core": "^7.25.2", + "@babel/preset-env": "^7.25.4", + "@babel/preset-typescript": "^7.24.7", "@cucumber/cucumber": "^10.3.1", - "@types/jest": "^29.5.1", + "@types/jest": "^29.5.13", "@typescript-eslint/eslint-plugin": "^6.9.0", "@typescript-eslint/parser": "^6.9.0", "babel-jest": "^29.7.0", @@ -35,8 +35,8 @@ "eslint-plugin-jest-extended": "^2.0.0", "eslint-plugin-jest-formatting": "^3.1.0", "eslint-plugin-prettier": "^5.0.1", - "jest": "^29.5.0", - "lerna": "^7.4.1" + "jest": "^29.7.0", + "lerna": "^7.4.2" }, "engines": { "node": ">=18.0.0" diff --git a/yarn.lock b/yarn.lock index 4070c76f..07f593bb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -58,12 +58,25 @@ "@babel/highlight" "^7.23.4" chalk "^2.4.2" -"@babel/compat-data@^7.22.6", "@babel/compat-data@^7.23.3", "@babel/compat-data@^7.23.5": +"@babel/code-frame@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.24.7.tgz#882fd9e09e8ee324e496bd040401c6f046ef4465" + integrity sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA== + dependencies: + "@babel/highlight" "^7.24.7" + picocolors "^1.0.0" + +"@babel/compat-data@^7.22.6", "@babel/compat-data@^7.23.5": version "7.23.5" resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz" integrity sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw== -"@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.23.3": +"@babel/compat-data@^7.25.2", "@babel/compat-data@^7.25.4": + version "7.25.4" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.25.4.tgz#7d2a80ce229890edcf4cc259d4d696cb4dae2fcb" + integrity sha512-+LGRog6RAsCJrrrg/IO6LGmpphNe5DiK30dGjCoxxeGv49B10/3XYGxPsAwrDlMFcFEvdAUavDT8r9k/hSyQqQ== + +"@babel/core@^7.11.6", "@babel/core@^7.12.3": version "7.23.6" resolved "https://registry.npmjs.org/@babel/core/-/core-7.23.6.tgz" integrity sha512-FxpRyGjrMJXh7X3wGLGhNDCRiwpWEF74sKjTLDJSG5Kyvow3QZaG0Adbqzi9ZrVjTWpsX+2cxWXD71NMg93kdw== @@ -84,6 +97,27 @@ json5 "^2.2.3" semver "^6.3.1" +"@babel/core@^7.25.2": + version "7.25.2" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.25.2.tgz#ed8eec275118d7613e77a352894cd12ded8eba77" + integrity sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA== + dependencies: + "@ampproject/remapping" "^2.2.0" + "@babel/code-frame" "^7.24.7" + "@babel/generator" "^7.25.0" + "@babel/helper-compilation-targets" "^7.25.2" + "@babel/helper-module-transforms" "^7.25.2" + "@babel/helpers" "^7.25.0" + "@babel/parser" "^7.25.0" + "@babel/template" "^7.25.0" + "@babel/traverse" "^7.25.2" + "@babel/types" "^7.25.2" + convert-source-map "^2.0.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.3" + semver "^6.3.1" + "@babel/generator@^7.23.6", "@babel/generator@^7.7.2": version "7.23.6" resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz" @@ -94,6 +128,16 @@ "@jridgewell/trace-mapping" "^0.3.17" jsesc "^2.5.1" +"@babel/generator@^7.25.0", "@babel/generator@^7.25.6": + version "7.25.6" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.25.6.tgz#0df1ad8cb32fe4d2b01d8bf437f153d19342a87c" + integrity sha512-VPC82gr1seXOpkjAAKoLhP50vx4vGNlF4msF64dSFq1P8RfB+QAuJWGHPXXPc8QyfVWwwB/TNNU4+ayZmHNbZw== + dependencies: + "@babel/types" "^7.25.6" + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + jsesc "^2.5.1" + "@babel/helper-annotate-as-pure@^7.22.5": version "7.22.5" resolved "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz" @@ -101,14 +145,22 @@ dependencies: "@babel/types" "^7.22.5" -"@babel/helper-builder-binary-assignment-operator-visitor@^7.22.15": - version "7.22.15" - resolved "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.15.tgz" - integrity sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw== +"@babel/helper-annotate-as-pure@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz#5373c7bc8366b12a033b4be1ac13a206c6656aab" + integrity sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg== dependencies: - "@babel/types" "^7.22.15" + "@babel/types" "^7.24.7" -"@babel/helper-compilation-targets@^7.22.15", "@babel/helper-compilation-targets@^7.22.6", "@babel/helper-compilation-targets@^7.23.6": +"@babel/helper-builder-binary-assignment-operator-visitor@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.24.7.tgz#37d66feb012024f2422b762b9b2a7cfe27c7fba3" + integrity sha512-xZeCVVdwb4MsDBkkyZ64tReWYrLRHlMN72vP7Bdm3OUOuyFZExhsHUUnuWnm2/XOlAJzR0LfPpB56WXZn0X/lA== + dependencies: + "@babel/traverse" "^7.24.7" + "@babel/types" "^7.24.7" + +"@babel/helper-compilation-targets@^7.22.6", "@babel/helper-compilation-targets@^7.23.6": version "7.23.6" resolved "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz" integrity sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ== @@ -119,22 +171,31 @@ lru-cache "^5.1.1" semver "^6.3.1" -"@babel/helper-create-class-features-plugin@^7.22.15", "@babel/helper-create-class-features-plugin@^7.23.6": - version "7.23.6" - resolved "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.23.6.tgz" - integrity sha512-cBXU1vZni/CpGF29iTu4YRbOZt3Wat6zCoMDxRF1MayiEc4URxOj31tT65HUM0CRpMowA3HCJaAOVOUnMf96cw== +"@babel/helper-compilation-targets@^7.24.7", "@babel/helper-compilation-targets@^7.24.8", "@babel/helper-compilation-targets@^7.25.2": + version "7.25.2" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz#e1d9410a90974a3a5a66e84ff55ef62e3c02d06c" + integrity sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw== dependencies: - "@babel/helper-annotate-as-pure" "^7.22.5" - "@babel/helper-environment-visitor" "^7.22.20" - "@babel/helper-function-name" "^7.23.0" - "@babel/helper-member-expression-to-functions" "^7.23.0" - "@babel/helper-optimise-call-expression" "^7.22.5" - "@babel/helper-replace-supers" "^7.22.20" - "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" - "@babel/helper-split-export-declaration" "^7.22.6" + "@babel/compat-data" "^7.25.2" + "@babel/helper-validator-option" "^7.24.8" + browserslist "^4.23.1" + lru-cache "^5.1.1" + semver "^6.3.1" + +"@babel/helper-create-class-features-plugin@^7.24.7", "@babel/helper-create-class-features-plugin@^7.25.0", "@babel/helper-create-class-features-plugin@^7.25.4": + version "7.25.4" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.4.tgz#57eaf1af38be4224a9d9dd01ddde05b741f50e14" + integrity sha512-ro/bFs3/84MDgDmMwbcHgDa8/E6J3QKNTk4xJJnVeFtGE+tL0K26E3pNxhYz2b67fJpt7Aphw5XcploKXuCvCQ== + dependencies: + "@babel/helper-annotate-as-pure" "^7.24.7" + "@babel/helper-member-expression-to-functions" "^7.24.8" + "@babel/helper-optimise-call-expression" "^7.24.7" + "@babel/helper-replace-supers" "^7.25.0" + "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" + "@babel/traverse" "^7.25.4" semver "^6.3.1" -"@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.22.15", "@babel/helper-create-regexp-features-plugin@^7.22.5": +"@babel/helper-create-regexp-features-plugin@^7.18.6": version "7.22.15" resolved "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.15.tgz" integrity sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w== @@ -143,10 +204,19 @@ regexpu-core "^5.3.1" semver "^6.3.1" -"@babel/helper-define-polyfill-provider@^0.4.4": - version "0.4.4" - resolved "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.4.tgz" - integrity sha512-QcJMILQCu2jm5TFPGA3lCpJJTeEP+mqeXooG/NZbg/h5FTFi6V0+99ahlRsW8/kRLyb24LZVCCiclDedhLKcBA== +"@babel/helper-create-regexp-features-plugin@^7.24.7", "@babel/helper-create-regexp-features-plugin@^7.25.0", "@babel/helper-create-regexp-features-plugin@^7.25.2": + version "7.25.2" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.25.2.tgz#24c75974ed74183797ffd5f134169316cd1808d9" + integrity sha512-+wqVGP+DFmqwFD3EH6TMTfUNeqDehV3E/dl+Sd54eaXqm17tEUNbEIn4sVivVowbvUpOtIGxdo3GoXyDH9N/9g== + dependencies: + "@babel/helper-annotate-as-pure" "^7.24.7" + regexpu-core "^5.3.1" + semver "^6.3.1" + +"@babel/helper-define-polyfill-provider@^0.6.2": + version "0.6.2" + resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.2.tgz#18594f789c3594acb24cfdb4a7f7b7d2e8bd912d" + integrity sha512-LV76g+C502biUK6AyZ3LK10vDpDyCzZnhZFXkH1L75zHPj68+qc8Zfpx2th+gzwA2MzyK+1g/3EPl62yFnVttQ== dependencies: "@babel/helper-compilation-targets" "^7.22.6" "@babel/helper-plugin-utils" "^7.22.5" @@ -159,7 +229,7 @@ resolved "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz" integrity sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA== -"@babel/helper-function-name@^7.22.5", "@babel/helper-function-name@^7.23.0": +"@babel/helper-function-name@^7.23.0": version "7.23.0" resolved "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz" integrity sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw== @@ -174,12 +244,13 @@ dependencies: "@babel/types" "^7.22.5" -"@babel/helper-member-expression-to-functions@^7.22.15", "@babel/helper-member-expression-to-functions@^7.23.0": - version "7.23.0" - resolved "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.23.0.tgz" - integrity sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA== +"@babel/helper-member-expression-to-functions@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.8.tgz#6155e079c913357d24a4c20480db7c712a5c3fb6" + integrity sha512-LABppdt+Lp/RlBxqrh4qgf1oEH/WxdzQNDJIu5gC/W1GyvPVrOBiItmmM8wan2fm4oYqFuFfkXmlGpLQhPY8CA== dependencies: - "@babel/types" "^7.23.0" + "@babel/traverse" "^7.24.8" + "@babel/types" "^7.24.8" "@babel/helper-module-imports@^7.22.15": version "7.22.15" @@ -188,6 +259,14 @@ dependencies: "@babel/types" "^7.22.15" +"@babel/helper-module-imports@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz#f2f980392de5b84c3328fc71d38bd81bbb83042b" + integrity sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA== + dependencies: + "@babel/traverse" "^7.24.7" + "@babel/types" "^7.24.7" + "@babel/helper-module-transforms@^7.23.3": version "7.23.3" resolved "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz" @@ -199,35 +278,50 @@ "@babel/helper-split-export-declaration" "^7.22.6" "@babel/helper-validator-identifier" "^7.22.20" -"@babel/helper-optimise-call-expression@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz" - integrity sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw== +"@babel/helper-module-transforms@^7.24.7", "@babel/helper-module-transforms@^7.24.8", "@babel/helper-module-transforms@^7.25.0", "@babel/helper-module-transforms@^7.25.2": + version "7.25.2" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz#ee713c29768100f2776edf04d4eb23b8d27a66e6" + integrity sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ== dependencies: - "@babel/types" "^7.22.5" + "@babel/helper-module-imports" "^7.24.7" + "@babel/helper-simple-access" "^7.24.7" + "@babel/helper-validator-identifier" "^7.24.7" + "@babel/traverse" "^7.25.2" + +"@babel/helper-optimise-call-expression@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.24.7.tgz#8b0a0456c92f6b323d27cfd00d1d664e76692a0f" + integrity sha512-jKiTsW2xmWwxT1ixIdfXUZp+P5yURx2suzLZr5Hi64rURpDYdMW0pv+Uf17EYk2Rd428Lx4tLsnjGJzYKDM/6A== + dependencies: + "@babel/types" "^7.24.7" "@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.22.5", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": version "7.22.5" resolved "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz" integrity sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg== -"@babel/helper-remap-async-to-generator@^7.22.20": - version "7.22.20" - resolved "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.20.tgz" - integrity sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw== +"@babel/helper-plugin-utils@^7.24.7", "@babel/helper-plugin-utils@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz#94ee67e8ec0e5d44ea7baeb51e571bd26af07878" + integrity sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg== + +"@babel/helper-remap-async-to-generator@^7.24.7", "@babel/helper-remap-async-to-generator@^7.25.0": + version "7.25.0" + resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.0.tgz#d2f0fbba059a42d68e5e378feaf181ef6055365e" + integrity sha512-NhavI2eWEIz/H9dbrG0TuOicDhNexze43i5z7lEqwYm0WEZVTwnPpA0EafUTP7+6/W79HWIP2cTe3Z5NiSTVpw== dependencies: - "@babel/helper-annotate-as-pure" "^7.22.5" - "@babel/helper-environment-visitor" "^7.22.20" - "@babel/helper-wrap-function" "^7.22.20" + "@babel/helper-annotate-as-pure" "^7.24.7" + "@babel/helper-wrap-function" "^7.25.0" + "@babel/traverse" "^7.25.0" -"@babel/helper-replace-supers@^7.22.20": - version "7.22.20" - resolved "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.20.tgz" - integrity sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw== +"@babel/helper-replace-supers@^7.24.7", "@babel/helper-replace-supers@^7.25.0": + version "7.25.0" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.25.0.tgz#ff44deac1c9f619523fe2ca1fd650773792000a9" + integrity sha512-q688zIvQVYtZu+i2PsdIu/uWGRpfxzr5WESsfpShfZECkO+d2o+WROWezCi/Q6kJ0tfPa5+pUGUlfx2HhrA3Bg== dependencies: - "@babel/helper-environment-visitor" "^7.22.20" - "@babel/helper-member-expression-to-functions" "^7.22.15" - "@babel/helper-optimise-call-expression" "^7.22.5" + "@babel/helper-member-expression-to-functions" "^7.24.8" + "@babel/helper-optimise-call-expression" "^7.24.7" + "@babel/traverse" "^7.25.0" "@babel/helper-simple-access@^7.22.5": version "7.22.5" @@ -236,12 +330,21 @@ dependencies: "@babel/types" "^7.22.5" -"@babel/helper-skip-transparent-expression-wrappers@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz" - integrity sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q== +"@babel/helper-simple-access@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz#bcade8da3aec8ed16b9c4953b74e506b51b5edb3" + integrity sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg== dependencies: - "@babel/types" "^7.22.5" + "@babel/traverse" "^7.24.7" + "@babel/types" "^7.24.7" + +"@babel/helper-skip-transparent-expression-wrappers@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.24.7.tgz#5f8fa83b69ed5c27adc56044f8be2b3ea96669d9" + integrity sha512-IO+DLT3LQUElMbpzlatRASEyQtfhSE0+m465v++3jyyXeBTBUjtVZg28/gHeV5mrTJqvEKhKroBGAvhW+qPHiQ== + dependencies: + "@babel/traverse" "^7.24.7" + "@babel/types" "^7.24.7" "@babel/helper-split-export-declaration@^7.22.6": version "7.22.6" @@ -255,24 +358,39 @@ resolved "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz" integrity sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ== +"@babel/helper-string-parser@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz#5b3329c9a58803d5df425e5785865881a81ca48d" + integrity sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ== + "@babel/helper-validator-identifier@^7.22.20": version "7.22.20" resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz" integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A== -"@babel/helper-validator-option@^7.22.15", "@babel/helper-validator-option@^7.23.5": +"@babel/helper-validator-identifier@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz#75b889cfaf9e35c2aaf42cf0d72c8e91719251db" + integrity sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w== + +"@babel/helper-validator-option@^7.23.5": version "7.23.5" resolved "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz" integrity sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw== -"@babel/helper-wrap-function@^7.22.20": - version "7.22.20" - resolved "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.22.20.tgz" - integrity sha512-pms/UwkOpnQe/PDAEdV/d7dVCoBbB+R4FvYoHGZz+4VPcg7RtYy2KP7S2lbuWM6FCSgob5wshfGESbC/hzNXZw== +"@babel/helper-validator-option@^7.24.7", "@babel/helper-validator-option@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz#3725cdeea8b480e86d34df15304806a06975e33d" + integrity sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q== + +"@babel/helper-wrap-function@^7.25.0": + version "7.25.0" + resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.25.0.tgz#dab12f0f593d6ca48c0062c28bcfb14ebe812f81" + integrity sha512-s6Q1ebqutSiZnEjaofc/UKDyC4SbzV5n5SrA2Gq8UawLycr3i04f1dX4OzoQVnexm6aOCh37SQNYlJ/8Ku+PMQ== dependencies: - "@babel/helper-function-name" "^7.22.5" - "@babel/template" "^7.22.15" - "@babel/types" "^7.22.19" + "@babel/template" "^7.25.0" + "@babel/traverse" "^7.25.0" + "@babel/types" "^7.25.0" "@babel/helpers@^7.23.6": version "7.23.6" @@ -283,6 +401,14 @@ "@babel/traverse" "^7.23.6" "@babel/types" "^7.23.6" +"@babel/helpers@^7.25.0": + version "7.25.6" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.25.6.tgz#57ee60141829ba2e102f30711ffe3afab357cc60" + integrity sha512-Xg0tn4HcfTijTwfDwYlvVCl43V6h4KyVVX2aEm4qdO/PC6L2YvzLHFdmxhoeSA3eslcE6+ZVXHgWwopXYLNq4Q== + dependencies: + "@babel/template" "^7.25.0" + "@babel/types" "^7.25.6" + "@babel/highlight@^7.23.4": version "7.23.4" resolved "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz" @@ -292,34 +418,66 @@ chalk "^2.4.2" js-tokens "^4.0.0" +"@babel/highlight@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.24.7.tgz#a05ab1df134b286558aae0ed41e6c5f731bf409d" + integrity sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw== + dependencies: + "@babel/helper-validator-identifier" "^7.24.7" + chalk "^2.4.2" + js-tokens "^4.0.0" + picocolors "^1.0.0" + "@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.22.15", "@babel/parser@^7.23.6": version "7.23.6" resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.23.6.tgz" integrity sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ== -"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.23.3": - version "7.23.3" - resolved "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.23.3.tgz" - integrity sha512-iRkKcCqb7iGnq9+3G6rZ+Ciz5VywC4XNRHe57lKM+jOeYAoR0lVqdeeDRfh0tQcTfw/+vBhHn926FmQhLtlFLQ== +"@babel/parser@^7.25.0", "@babel/parser@^7.25.6": + version "7.25.6" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.25.6.tgz#85660c5ef388cbbf6e3d2a694ee97a38f18afe2f" + integrity sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/types" "^7.25.6" -"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.23.3": - version "7.23.3" - resolved "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.23.3.tgz" - integrity sha512-WwlxbfMNdVEpQjZmK5mhm7oSwD3dS6eU+Iwsi4Knl9wAletWem7kaRsGOG+8UEbRyqxY4SS5zvtfXwX+jMxUwQ== +"@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.25.3": + version "7.25.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.3.tgz#dca427b45a6c0f5c095a1c639dfe2476a3daba7f" + integrity sha512-wUrcsxZg6rqBXG05HG1FPYgsP6EvwF4WpBbxIpWIIYnH8wG0gzx3yZY3dtEHas4sTAOGkbTsc9EGPxwff8lRoA== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" - "@babel/plugin-transform-optional-chaining" "^7.23.3" + "@babel/helper-plugin-utils" "^7.24.8" + "@babel/traverse" "^7.25.3" -"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@^7.23.3": - version "7.23.3" - resolved "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.23.3.tgz" - integrity sha512-XaJak1qcityzrX0/IU5nKHb34VaibwP3saKqG6a/tppelgllOH13LUann4ZCIBcVOeE6H18K4Vx9QKkVww3z/w== +"@babel/plugin-bugfix-safari-class-field-initializer-scope@^7.25.0": + version "7.25.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.0.tgz#cd0c583e01369ef51676bdb3d7b603e17d2b3f73" + integrity sha512-Bm4bH2qsX880b/3ziJ8KD711LT7z4u8CFudmjqle65AZj/HNUFhEf90dqYv6O86buWvSBmeQDjv0Tn2aF/bIBA== dependencies: - "@babel/helper-environment-visitor" "^7.22.20" - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.8" + +"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.25.0": + version "7.25.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.0.tgz#749bde80356b295390954643de7635e0dffabe73" + integrity sha512-lXwdNZtTmeVOOFtwM/WDe7yg1PL8sYhRk/XH0FzbR2HDQ0xC+EnQ/JHeoMYSavtU115tnUk0q9CDyq8si+LMAA== + dependencies: + "@babel/helper-plugin-utils" "^7.24.8" + +"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.24.7.tgz#e4eabdd5109acc399b38d7999b2ef66fc2022f89" + integrity sha512-+izXIbke1T33mY4MSNnrqhPXDz01WYhEf3yF5NbnUtkiNnm+XBZJl3kNfoK6NKmYlz/D07+l2GWVK/QfDkNCuQ== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" + "@babel/plugin-transform-optional-chaining" "^7.24.7" + +"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@^7.25.0": + version "7.25.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.0.tgz#3a82a70e7cb7294ad2559465ebcb871dfbf078fb" + integrity sha512-tggFrk1AIShG/RUQbEwt2Tr/E+ObkfwrPjR6BjbRvsx24+PSjK8zrq0GWPNCjo8qpRx4DuJzlcvWJqlm+0h3kw== + dependencies: + "@babel/helper-plugin-utils" "^7.24.8" + "@babel/traverse" "^7.25.0" "@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2": version "7.21.0-placeholder-for-preset-env.2" @@ -368,19 +526,19 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-syntax-import-assertions@^7.23.3": - version "7.23.3" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.23.3.tgz" - integrity sha512-lPgDSU+SJLK3xmFDTV2ZRQAiM7UuUjGidwBywFavObCiZc1BeAAcMtHJKUya92hPHO+at63JJPLygilZard8jw== +"@babel/plugin-syntax-import-assertions@^7.24.7": + version "7.25.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.25.6.tgz#bb918905c58711b86f9710d74a3744b6c56573b5" + integrity sha512-aABl0jHw9bZ2karQ/uUD6XP4u0SG22SJrOHFoL6XB1R7dTovOP4TzTlsxOYC5yQ1pdscVK2JTUnF6QL3ARoAiQ== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.8" -"@babel/plugin-syntax-import-attributes@^7.23.3": - version "7.23.3" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.23.3.tgz" - integrity sha512-pawnE0P9g10xgoP7yKr6CK63K2FMsTE+FZidZO/1PwRdzmAPVs+HS1mAURUsgaoxammTJvULUdIkEK0gOcU2tA== +"@babel/plugin-syntax-import-attributes@^7.24.7": + version "7.25.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.25.6.tgz#6d4c78f042db0e82fd6436cd65fec5dc78ad2bde" + integrity sha512-sXaDXaJN9SNLymBdlWFA+bjzBhFD617ZaFiY13dGt7TVslVvVgA6fkZOP7Ki3IGElC45lwHdOTrCtKZGVAWeLQ== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.8" "@babel/plugin-syntax-import-meta@^7.10.4", "@babel/plugin-syntax-import-meta@^7.8.3": version "7.10.4" @@ -396,7 +554,14 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-jsx@^7.23.3", "@babel/plugin-syntax-jsx@^7.7.2": +"@babel/plugin-syntax-jsx@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.7.tgz#39a1fa4a7e3d3d7f34e2acc6be585b718d30e02d" + integrity sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-syntax-jsx@^7.7.2": version "7.23.3" resolved "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.23.3.tgz" integrity sha512-EB2MELswq55OHUoRZLGg/zC7QWUKfNLpE57m/S2yr1uEneIgsTgrSzXP3NXEsMkVn76OlaVVnzN+ugObuYGwhg== @@ -459,7 +624,14 @@ dependencies: "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-syntax-typescript@^7.23.3", "@babel/plugin-syntax-typescript@^7.7.2": +"@babel/plugin-syntax-typescript@^7.24.7": + version "7.25.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.4.tgz#04db9ce5a9043d9c635e75ae7969a2cd50ca97ff" + integrity sha512-uMOCoHVU52BsSWxPOMVv5qKRdeSlPuImUCB2dlPuBSU+W2/ROE7/Zg8F2Kepbk+8yBa68LlRKxO+xgEVWorsDg== + dependencies: + "@babel/helper-plugin-utils" "^7.24.8" + +"@babel/plugin-syntax-typescript@^7.7.2": version "7.23.3" resolved "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.23.3.tgz" integrity sha512-9EiNjVJOMwCO+43TqoTrgQ8jMwcAd0sWyXi9RPfIsLTj4R2MADDDQXELhffaUx/uJv2AYcxBgPwH6j4TIA4ytQ== @@ -474,425 +646,432 @@ "@babel/helper-create-regexp-features-plugin" "^7.18.6" "@babel/helper-plugin-utils" "^7.18.6" -"@babel/plugin-transform-arrow-functions@^7.23.3": - version "7.23.3" - resolved "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.23.3.tgz" - integrity sha512-NzQcQrzaQPkaEwoTm4Mhyl8jI1huEL/WWIEvudjTCMJ9aBZNpsJbMASx7EQECtQQPS/DcnFpo0FIh3LvEO9cxQ== +"@babel/plugin-transform-arrow-functions@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.24.7.tgz#4f6886c11e423bd69f3ce51dbf42424a5f275514" + integrity sha512-Dt9LQs6iEY++gXUwY03DNFat5C2NbO48jj+j/bSAz6b3HgPs39qcPiYt77fDObIcFwj3/C2ICX9YMwGflUoSHQ== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-transform-async-generator-functions@^7.23.4": - version "7.23.4" - resolved "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.23.4.tgz" - integrity sha512-efdkfPhHYTtn0G6n2ddrESE91fgXxjlqLsnUtPWnJs4a4mZIbUaK7ffqKIIUKXSHwcDvaCVX6GXkaJJFqtX7jw== +"@babel/plugin-transform-async-generator-functions@^7.25.4": + version "7.25.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.25.4.tgz#2afd4e639e2d055776c9f091b6c0c180ed8cf083" + integrity sha512-jz8cV2XDDTqjKPwVPJBIjORVEmSGYhdRa8e5k5+vN+uwcjSrSxUaebBRa4ko1jqNF2uxyg8G6XYk30Jv285xzg== dependencies: - "@babel/helper-environment-visitor" "^7.22.20" - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/helper-remap-async-to-generator" "^7.22.20" + "@babel/helper-plugin-utils" "^7.24.8" + "@babel/helper-remap-async-to-generator" "^7.25.0" "@babel/plugin-syntax-async-generators" "^7.8.4" + "@babel/traverse" "^7.25.4" -"@babel/plugin-transform-async-to-generator@^7.23.3": - version "7.23.3" - resolved "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.23.3.tgz" - integrity sha512-A7LFsKi4U4fomjqXJlZg/u0ft/n8/7n7lpffUP/ZULx/DtV9SGlNKZolHH6PE8Xl1ngCc0M11OaeZptXVkfKSw== +"@babel/plugin-transform-async-to-generator@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.24.7.tgz#72a3af6c451d575842a7e9b5a02863414355bdcc" + integrity sha512-SQY01PcJfmQ+4Ash7NE+rpbLFbmqA2GPIgqzxfFTL4t1FKRq4zTms/7htKpoCUI9OcFYgzqfmCdH53s6/jn5fA== dependencies: - "@babel/helper-module-imports" "^7.22.15" - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/helper-remap-async-to-generator" "^7.22.20" + "@babel/helper-module-imports" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-remap-async-to-generator" "^7.24.7" -"@babel/plugin-transform-block-scoped-functions@^7.23.3": - version "7.23.3" - resolved "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.23.3.tgz" - integrity sha512-vI+0sIaPIO6CNuM9Kk5VmXcMVRiOpDh7w2zZt9GXzmE/9KD70CUEVhvPR/etAeNK/FAEkhxQtXOzVF3EuRL41A== +"@babel/plugin-transform-block-scoped-functions@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.24.7.tgz#a4251d98ea0c0f399dafe1a35801eaba455bbf1f" + integrity sha512-yO7RAz6EsVQDaBH18IDJcMB1HnrUn2FJ/Jslc/WtPPWcjhpUJXU/rjbwmluzp7v/ZzWcEhTMXELnnsz8djWDwQ== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-transform-block-scoping@^7.23.4": - version "7.23.4" - resolved "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.23.4.tgz" - integrity sha512-0QqbP6B6HOh7/8iNR4CQU2Th/bbRtBp4KS9vcaZd1fZ0wSh5Fyssg0UCIHwxh+ka+pNDREbVLQnHCMHKZfPwfw== +"@babel/plugin-transform-block-scoping@^7.25.0": + version "7.25.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.0.tgz#23a6ed92e6b006d26b1869b1c91d1b917c2ea2ac" + integrity sha512-yBQjYoOjXlFv9nlXb3f1casSHOZkWr29NX+zChVanLg5Nc157CrbEX9D7hxxtTpuFy7Q0YzmmWfJxzvps4kXrQ== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.8" -"@babel/plugin-transform-class-properties@^7.23.3": - version "7.23.3" - resolved "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.23.3.tgz" - integrity sha512-uM+AN8yCIjDPccsKGlw271xjJtGii+xQIF/uMPS8H15L12jZTsLfF4o5vNO7d/oUguOyfdikHGc/yi9ge4SGIg== +"@babel/plugin-transform-class-properties@^7.25.4": + version "7.25.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.4.tgz#bae7dbfcdcc2e8667355cd1fb5eda298f05189fd" + integrity sha512-nZeZHyCWPfjkdU5pA/uHiTaDAFUEqkpzf1YoQT2NeSynCGYq9rxfyI3XpQbfx/a0hSnFH6TGlEXvae5Vi7GD8g== dependencies: - "@babel/helper-create-class-features-plugin" "^7.22.15" - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-create-class-features-plugin" "^7.25.4" + "@babel/helper-plugin-utils" "^7.24.8" -"@babel/plugin-transform-class-static-block@^7.23.4": - version "7.23.4" - resolved "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.23.4.tgz" - integrity sha512-nsWu/1M+ggti1SOALj3hfx5FXzAY06fwPJsUZD4/A5e1bWi46VUIWtD+kOX6/IdhXGsXBWllLFDSnqSCdUNydQ== +"@babel/plugin-transform-class-static-block@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.24.7.tgz#c82027ebb7010bc33c116d4b5044fbbf8c05484d" + integrity sha512-HMXK3WbBPpZQufbMG4B46A90PkuuhN9vBCb5T8+VAHqvAqvcLi+2cKoukcpmUYkszLhScU3l1iudhrks3DggRQ== dependencies: - "@babel/helper-create-class-features-plugin" "^7.22.15" - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-create-class-features-plugin" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-class-static-block" "^7.14.5" -"@babel/plugin-transform-classes@^7.23.5": - version "7.23.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.23.5.tgz" - integrity sha512-jvOTR4nicqYC9yzOHIhXG5emiFEOpappSJAl73SDSEDcybD+Puuze8Tnpb9p9qEyYup24tq891gkaygIFvWDqg== +"@babel/plugin-transform-classes@^7.25.4": + version "7.25.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.4.tgz#d29dbb6a72d79f359952ad0b66d88518d65ef89a" + integrity sha512-oexUfaQle2pF/b6E0dwsxQtAol9TLSO88kQvym6HHBWFliV2lGdrPieX+WgMRLSJDVzdYywk7jXbLPuO2KLTLg== dependencies: - "@babel/helper-annotate-as-pure" "^7.22.5" - "@babel/helper-compilation-targets" "^7.22.15" - "@babel/helper-environment-visitor" "^7.22.20" - "@babel/helper-function-name" "^7.23.0" - "@babel/helper-optimise-call-expression" "^7.22.5" - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/helper-replace-supers" "^7.22.20" - "@babel/helper-split-export-declaration" "^7.22.6" + "@babel/helper-annotate-as-pure" "^7.24.7" + "@babel/helper-compilation-targets" "^7.25.2" + "@babel/helper-plugin-utils" "^7.24.8" + "@babel/helper-replace-supers" "^7.25.0" + "@babel/traverse" "^7.25.4" globals "^11.1.0" -"@babel/plugin-transform-computed-properties@^7.23.3": - version "7.23.3" - resolved "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.23.3.tgz" - integrity sha512-dTj83UVTLw/+nbiHqQSFdwO9CbTtwq1DsDqm3CUEtDrZNET5rT5E6bIdTlOftDTDLMYxvxHNEYO4B9SLl8SLZw== +"@babel/plugin-transform-computed-properties@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.24.7.tgz#4cab3214e80bc71fae3853238d13d097b004c707" + integrity sha512-25cS7v+707Gu6Ds2oY6tCkUwsJ9YIDbggd9+cu9jzzDgiNq7hR/8dkzxWfKWnTic26vsI3EsCXNd4iEB6e8esQ== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/template" "^7.22.15" + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/template" "^7.24.7" -"@babel/plugin-transform-destructuring@^7.23.3": - version "7.23.3" - resolved "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.23.3.tgz" - integrity sha512-n225npDqjDIr967cMScVKHXJs7rout1q+tt50inyBCPkyZ8KxeI6d+GIbSBTT/w/9WdlWDOej3V9HE5Lgk57gw== +"@babel/plugin-transform-destructuring@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.8.tgz#c828e814dbe42a2718a838c2a2e16a408e055550" + integrity sha512-36e87mfY8TnRxc7yc6M9g9gOB7rKgSahqkIKwLpz4Ppk2+zC2Cy1is0uwtuSG6AE4zlTOUa+7JGz9jCJGLqQFQ== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.8" -"@babel/plugin-transform-dotall-regex@^7.23.3": - version "7.23.3" - resolved "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.23.3.tgz" - integrity sha512-vgnFYDHAKzFaTVp+mneDsIEbnJ2Np/9ng9iviHw3P/KVcgONxpNULEW/51Z/BaFojG2GI2GwwXck5uV1+1NOYQ== +"@babel/plugin-transform-dotall-regex@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.24.7.tgz#5f8bf8a680f2116a7207e16288a5f974ad47a7a0" + integrity sha512-ZOA3W+1RRTSWvyqcMJDLqbchh7U4NRGqwRfFSVbOLS/ePIP4vHB5e8T8eXcuqyN1QkgKyj5wuW0lcS85v4CrSw== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.22.15" - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-create-regexp-features-plugin" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-transform-duplicate-keys@^7.23.3": - version "7.23.3" - resolved "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.23.3.tgz" - integrity sha512-RrqQ+BQmU3Oyav3J+7/myfvRCq7Tbz+kKLLshUmMwNlDHExbGL7ARhajvoBJEvc+fCguPPu887N+3RRXBVKZUA== +"@babel/plugin-transform-duplicate-keys@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.24.7.tgz#dd20102897c9a2324e5adfffb67ff3610359a8ee" + integrity sha512-JdYfXyCRihAe46jUIliuL2/s0x0wObgwwiGxw/UbgJBr20gQBThrokO4nYKgWkD7uBaqM7+9x5TU7NkExZJyzw== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-transform-dynamic-import@^7.23.4": - version "7.23.4" - resolved "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.23.4.tgz" - integrity sha512-V6jIbLhdJK86MaLh4Jpghi8ho5fGzt3imHOBu/x0jlBaPYqDoWz4RDXjmMOfnh+JWNaQleEAByZLV0QzBT4YQQ== +"@babel/plugin-transform-duplicate-named-capturing-groups-regex@^7.25.0": + version "7.25.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.0.tgz#809af7e3339466b49c034c683964ee8afb3e2604" + integrity sha512-YLpb4LlYSc3sCUa35un84poXoraOiQucUTTu8X1j18JV+gNa8E0nyUf/CjZ171IRGr4jEguF+vzJU66QZhn29g== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-create-regexp-features-plugin" "^7.25.0" + "@babel/helper-plugin-utils" "^7.24.8" + +"@babel/plugin-transform-dynamic-import@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.24.7.tgz#4d8b95e3bae2b037673091aa09cd33fecd6419f4" + integrity sha512-sc3X26PhZQDb3JhORmakcbvkeInvxz+A8oda99lj7J60QRuPZvNAk9wQlTBS1ZynelDrDmTU4pw1tyc5d5ZMUg== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-dynamic-import" "^7.8.3" -"@babel/plugin-transform-exponentiation-operator@^7.23.3": - version "7.23.3" - resolved "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.23.3.tgz" - integrity sha512-5fhCsl1odX96u7ILKHBj4/Y8vipoqwsJMh4csSA8qFfxrZDEA4Ssku2DyNvMJSmZNOEBT750LfFPbtrnTP90BQ== +"@babel/plugin-transform-exponentiation-operator@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.24.7.tgz#b629ee22645f412024297d5245bce425c31f9b0d" + integrity sha512-Rqe/vSc9OYgDajNIK35u7ot+KeCoetqQYFXM4Epf7M7ez3lWlOjrDjrwMei6caCVhfdw+mIKD4cgdGNy5JQotQ== dependencies: - "@babel/helper-builder-binary-assignment-operator-visitor" "^7.22.15" - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-builder-binary-assignment-operator-visitor" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-transform-export-namespace-from@^7.23.4": - version "7.23.4" - resolved "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.23.4.tgz" - integrity sha512-GzuSBcKkx62dGzZI1WVgTWvkkz84FZO5TC5T8dl/Tht/rAla6Dg/Mz9Yhypg+ezVACf/rgDuQt3kbWEv7LdUDQ== +"@babel/plugin-transform-export-namespace-from@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.24.7.tgz#176d52d8d8ed516aeae7013ee9556d540c53f197" + integrity sha512-v0K9uNYsPL3oXZ/7F9NNIbAj2jv1whUEtyA6aujhekLs56R++JDQuzRcP2/z4WX5Vg/c5lE9uWZA0/iUoFhLTA== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-export-namespace-from" "^7.8.3" -"@babel/plugin-transform-for-of@^7.23.6": - version "7.23.6" - resolved "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.23.6.tgz" - integrity sha512-aYH4ytZ0qSuBbpfhuofbg/e96oQ7U2w1Aw/UQmKT+1l39uEhUPoFS3fHevDc1G0OvewyDudfMKY1OulczHzWIw== +"@babel/plugin-transform-for-of@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.24.7.tgz#f25b33f72df1d8be76399e1b8f3f9d366eb5bc70" + integrity sha512-wo9ogrDG1ITTTBsy46oGiN1dS9A7MROBTcYsfS8DtsImMkHk9JXJ3EWQM6X2SUw4x80uGPlwj0o00Uoc6nEE3g== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" -"@babel/plugin-transform-function-name@^7.23.3": - version "7.23.3" - resolved "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.23.3.tgz" - integrity sha512-I1QXp1LxIvt8yLaib49dRW5Okt7Q4oaxao6tFVKS/anCdEOMtYwWVKoiOA1p34GOWIZjUK0E+zCp7+l1pfQyiw== +"@babel/plugin-transform-function-name@^7.25.1": + version "7.25.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.1.tgz#b85e773097526c1a4fc4ba27322748643f26fc37" + integrity sha512-TVVJVdW9RKMNgJJlLtHsKDTydjZAbwIsn6ySBPQaEAUU5+gVvlJt/9nRmqVbsV/IBanRjzWoaAQKLoamWVOUuA== dependencies: - "@babel/helper-compilation-targets" "^7.22.15" - "@babel/helper-function-name" "^7.23.0" - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-compilation-targets" "^7.24.8" + "@babel/helper-plugin-utils" "^7.24.8" + "@babel/traverse" "^7.25.1" -"@babel/plugin-transform-json-strings@^7.23.4": - version "7.23.4" - resolved "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.23.4.tgz" - integrity sha512-81nTOqM1dMwZ/aRXQ59zVubN9wHGqk6UtqRK+/q+ciXmRy8fSolhGVvG09HHRGo4l6fr/c4ZhXUQH0uFW7PZbg== +"@babel/plugin-transform-json-strings@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.24.7.tgz#f3e9c37c0a373fee86e36880d45b3664cedaf73a" + integrity sha512-2yFnBGDvRuxAaE/f0vfBKvtnvvqU8tGpMHqMNpTN2oWMKIR3NqFkjaAgGwawhqK/pIN2T3XdjGPdaG0vDhOBGw== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-json-strings" "^7.8.3" -"@babel/plugin-transform-literals@^7.23.3": - version "7.23.3" - resolved "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.23.3.tgz" - integrity sha512-wZ0PIXRxnwZvl9AYpqNUxpZ5BiTGrYt7kueGQ+N5FiQ7RCOD4cm8iShd6S6ggfVIWaJf2EMk8eRzAh52RfP4rQ== +"@babel/plugin-transform-literals@^7.25.2": + version "7.25.2" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.2.tgz#deb1ad14fc5490b9a65ed830e025bca849d8b5f3" + integrity sha512-HQI+HcTbm9ur3Z2DkO+jgESMAMcYLuN/A7NRw9juzxAezN9AvqvUTnpKP/9kkYANz6u7dFlAyOu44ejuGySlfw== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.8" -"@babel/plugin-transform-logical-assignment-operators@^7.23.4": - version "7.23.4" - resolved "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.23.4.tgz" - integrity sha512-Mc/ALf1rmZTP4JKKEhUwiORU+vcfarFVLfcFiolKUo6sewoxSEgl36ak5t+4WamRsNr6nzjZXQjM35WsU+9vbg== +"@babel/plugin-transform-logical-assignment-operators@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.24.7.tgz#a58fb6eda16c9dc8f9ff1c7b1ba6deb7f4694cb0" + integrity sha512-4D2tpwlQ1odXmTEIFWy9ELJcZHqrStlzK/dAOWYyxX3zT0iXQB6banjgeOJQXzEc4S0E0a5A+hahxPaEFYftsw== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" -"@babel/plugin-transform-member-expression-literals@^7.23.3": - version "7.23.3" - resolved "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.23.3.tgz" - integrity sha512-sC3LdDBDi5x96LA+Ytekz2ZPk8i/Ck+DEuDbRAll5rknJ5XRTSaPKEYwomLcs1AA8wg9b3KjIQRsnApj+q51Ag== +"@babel/plugin-transform-member-expression-literals@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.24.7.tgz#3b4454fb0e302e18ba4945ba3246acb1248315df" + integrity sha512-T/hRC1uqrzXMKLQ6UCwMT85S3EvqaBXDGf0FaMf4446Qx9vKwlghvee0+uuZcDUCZU5RuNi4781UQ7R308zzBw== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-transform-modules-amd@^7.23.3": - version "7.23.3" - resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.23.3.tgz" - integrity sha512-vJYQGxeKM4t8hYCKVBlZX/gtIY2I7mRGFNcm85sgXGMTBcoV3QdVtdpbcWEbzbfUIUZKwvgFT82mRvaQIebZzw== +"@babel/plugin-transform-modules-amd@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.24.7.tgz#65090ed493c4a834976a3ca1cde776e6ccff32d7" + integrity sha512-9+pB1qxV3vs/8Hdmz/CulFB8w2tuu6EB94JZFsjdqxQokwGa9Unap7Bo2gGBGIvPmDIVvQrom7r5m/TCDMURhg== dependencies: - "@babel/helper-module-transforms" "^7.23.3" - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-module-transforms" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-transform-modules-commonjs@^7.23.3": - version "7.23.3" - resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.23.3.tgz" - integrity sha512-aVS0F65LKsdNOtcz6FRCpE4OgsP2OFnW46qNxNIX9h3wuzaNcSQsJysuMwqSibC98HPrf2vCgtxKNwS0DAlgcA== +"@babel/plugin-transform-modules-commonjs@^7.24.7", "@babel/plugin-transform-modules-commonjs@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.8.tgz#ab6421e564b717cb475d6fff70ae7f103536ea3c" + integrity sha512-WHsk9H8XxRs3JXKWFiqtQebdh9b/pTk4EgueygFzYlTKAg0Ud985mSevdNjdXdFBATSKVJGQXP1tv6aGbssLKA== dependencies: - "@babel/helper-module-transforms" "^7.23.3" - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/helper-simple-access" "^7.22.5" + "@babel/helper-module-transforms" "^7.24.8" + "@babel/helper-plugin-utils" "^7.24.8" + "@babel/helper-simple-access" "^7.24.7" -"@babel/plugin-transform-modules-systemjs@^7.23.3": - version "7.23.3" - resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.23.3.tgz" - integrity sha512-ZxyKGTkF9xT9YJuKQRo19ewf3pXpopuYQd8cDXqNzc3mUNbOME0RKMoZxviQk74hwzfQsEe66dE92MaZbdHKNQ== +"@babel/plugin-transform-modules-systemjs@^7.25.0": + version "7.25.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.0.tgz#8f46cdc5f9e5af74f3bd019485a6cbe59685ea33" + integrity sha512-YPJfjQPDXxyQWg/0+jHKj1llnY5f/R6a0p/vP4lPymxLu7Lvl4k2WMitqi08yxwQcCVUUdG9LCUj4TNEgAp3Jw== dependencies: - "@babel/helper-hoist-variables" "^7.22.5" - "@babel/helper-module-transforms" "^7.23.3" - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/helper-validator-identifier" "^7.22.20" + "@babel/helper-module-transforms" "^7.25.0" + "@babel/helper-plugin-utils" "^7.24.8" + "@babel/helper-validator-identifier" "^7.24.7" + "@babel/traverse" "^7.25.0" -"@babel/plugin-transform-modules-umd@^7.23.3": - version "7.23.3" - resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.23.3.tgz" - integrity sha512-zHsy9iXX2nIsCBFPud3jKn1IRPWg3Ing1qOZgeKV39m1ZgIdpJqvlWVeiHBZC6ITRG0MfskhYe9cLgntfSFPIg== +"@babel/plugin-transform-modules-umd@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.24.7.tgz#edd9f43ec549099620df7df24e7ba13b5c76efc8" + integrity sha512-3aytQvqJ/h9z4g8AsKPLvD4Zqi2qT+L3j7XoFFu1XBlZWEl2/1kWnhmAbxpLgPrHSY0M6UA02jyTiwUVtiKR6A== dependencies: - "@babel/helper-module-transforms" "^7.23.3" - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-module-transforms" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-transform-named-capturing-groups-regex@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz" - integrity sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ== +"@babel/plugin-transform-named-capturing-groups-regex@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.24.7.tgz#9042e9b856bc6b3688c0c2e4060e9e10b1460923" + integrity sha512-/jr7h/EWeJtk1U/uz2jlsCioHkZk1JJZVcc8oQsJ1dUlaJD83f4/6Zeh2aHt9BIFokHIsSeDfhUmju0+1GPd6g== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.22.5" - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-create-regexp-features-plugin" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-transform-new-target@^7.23.3": - version "7.23.3" - resolved "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.23.3.tgz" - integrity sha512-YJ3xKqtJMAT5/TIZnpAR3I+K+WaDowYbN3xyxI8zxx/Gsypwf9B9h0VB+1Nh6ACAAPRS5NSRje0uVv5i79HYGQ== +"@babel/plugin-transform-new-target@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.24.7.tgz#31ff54c4e0555cc549d5816e4ab39241dfb6ab00" + integrity sha512-RNKwfRIXg4Ls/8mMTza5oPF5RkOW8Wy/WgMAp1/F1yZ8mMbtwXW+HDoJiOsagWrAhI5f57Vncrmr9XeT4CVapA== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-transform-nullish-coalescing-operator@^7.23.4": - version "7.23.4" - resolved "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.23.4.tgz" - integrity sha512-jHE9EVVqHKAQx+VePv5LLGHjmHSJR76vawFPTdlxR/LVJPfOEGxREQwQfjuZEOPTwG92X3LINSh3M40Rv4zpVA== +"@babel/plugin-transform-nullish-coalescing-operator@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.24.7.tgz#1de4534c590af9596f53d67f52a92f12db984120" + integrity sha512-Ts7xQVk1OEocqzm8rHMXHlxvsfZ0cEF2yomUqpKENHWMF4zKk175Y4q8H5knJes6PgYad50uuRmt3UJuhBw8pQ== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" -"@babel/plugin-transform-numeric-separator@^7.23.4": - version "7.23.4" - resolved "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.23.4.tgz" - integrity sha512-mps6auzgwjRrwKEZA05cOwuDc9FAzoyFS4ZsG/8F43bTLf/TgkJg7QXOrPO1JO599iA3qgK9MXdMGOEC8O1h6Q== +"@babel/plugin-transform-numeric-separator@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.24.7.tgz#bea62b538c80605d8a0fac9b40f48e97efa7de63" + integrity sha512-e6q1TiVUzvH9KRvicuxdBTUj4AdKSRwzIyFFnfnezpCfP2/7Qmbb8qbU2j7GODbl4JMkblitCQjKYUaX/qkkwA== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-numeric-separator" "^7.10.4" -"@babel/plugin-transform-object-rest-spread@^7.23.4": - version "7.23.4" - resolved "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.23.4.tgz" - integrity sha512-9x9K1YyeQVw0iOXJlIzwm8ltobIIv7j2iLyP2jIhEbqPRQ7ScNgwQufU2I0Gq11VjyG4gI4yMXt2VFags+1N3g== +"@babel/plugin-transform-object-rest-spread@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.7.tgz#d13a2b93435aeb8a197e115221cab266ba6e55d6" + integrity sha512-4QrHAr0aXQCEFni2q4DqKLD31n2DL+RxcwnNjDFkSG0eNQ/xCavnRkfCUjsyqGC2OviNJvZOF/mQqZBw7i2C5Q== dependencies: - "@babel/compat-data" "^7.23.3" - "@babel/helper-compilation-targets" "^7.22.15" - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-compilation-targets" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-object-rest-spread" "^7.8.3" - "@babel/plugin-transform-parameters" "^7.23.3" + "@babel/plugin-transform-parameters" "^7.24.7" -"@babel/plugin-transform-object-super@^7.23.3": - version "7.23.3" - resolved "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.23.3.tgz" - integrity sha512-BwQ8q0x2JG+3lxCVFohg+KbQM7plfpBwThdW9A6TMtWwLsbDA01Ek2Zb/AgDN39BiZsExm4qrXxjk+P1/fzGrA== +"@babel/plugin-transform-object-super@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.24.7.tgz#66eeaff7830bba945dd8989b632a40c04ed625be" + integrity sha512-A/vVLwN6lBrMFmMDmPPz0jnE6ZGx7Jq7d6sT/Ev4H65RER6pZ+kczlf1DthF5N0qaPHBsI7UXiE8Zy66nmAovg== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/helper-replace-supers" "^7.22.20" + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-replace-supers" "^7.24.7" -"@babel/plugin-transform-optional-catch-binding@^7.23.4": - version "7.23.4" - resolved "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.23.4.tgz" - integrity sha512-XIq8t0rJPHf6Wvmbn9nFxU6ao4c7WhghTR5WyV8SrJfUFzyxhCm4nhC+iAp3HFhbAKLfYpgzhJ6t4XCtVwqO5A== +"@babel/plugin-transform-optional-catch-binding@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.24.7.tgz#00eabd883d0dd6a60c1c557548785919b6e717b4" + integrity sha512-uLEndKqP5BfBbC/5jTwPxLh9kqPWWgzN/f8w6UwAIirAEqiIVJWWY312X72Eub09g5KF9+Zn7+hT7sDxmhRuKA== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" -"@babel/plugin-transform-optional-chaining@^7.23.3", "@babel/plugin-transform-optional-chaining@^7.23.4": - version "7.23.4" - resolved "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.23.4.tgz" - integrity sha512-ZU8y5zWOfjM5vZ+asjgAPwDaBjJzgufjES89Rs4Lpq63O300R/kOz30WCLo6BxxX6QVEilwSlpClnG5cZaikTA== +"@babel/plugin-transform-optional-chaining@^7.24.7", "@babel/plugin-transform-optional-chaining@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.8.tgz#bb02a67b60ff0406085c13d104c99a835cdf365d" + integrity sha512-5cTOLSMs9eypEy8JUVvIKOu6NgvbJMnpG62VpIHrTmROdQ+L5mDAaI40g25k5vXti55JWNX5jCkq3HZxXBQANw== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.8" + "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" "@babel/plugin-syntax-optional-chaining" "^7.8.3" -"@babel/plugin-transform-parameters@^7.23.3": - version "7.23.3" - resolved "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.23.3.tgz" - integrity sha512-09lMt6UsUb3/34BbECKVbVwrT9bO6lILWln237z7sLaWnMsTi7Yc9fhX5DLpkJzAGfaReXI22wP41SZmnAA3Vw== +"@babel/plugin-transform-parameters@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.24.7.tgz#5881f0ae21018400e320fc7eb817e529d1254b68" + integrity sha512-yGWW5Rr+sQOhK0Ot8hjDJuxU3XLRQGflvT4lhlSY0DFvdb3TwKaY26CJzHtYllU0vT9j58hc37ndFPsqT1SrzA== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-transform-private-methods@^7.23.3": - version "7.23.3" - resolved "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.23.3.tgz" - integrity sha512-UzqRcRtWsDMTLrRWFvUBDwmw06tCQH9Rl1uAjfh6ijMSmGYQ+fpdB+cnqRC8EMh5tuuxSv0/TejGL+7vyj+50g== +"@babel/plugin-transform-private-methods@^7.25.4": + version "7.25.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.4.tgz#9bbefbe3649f470d681997e0b64a4b254d877242" + integrity sha512-ao8BG7E2b/URaUQGqN3Tlsg+M3KlHY6rJ1O1gXAEUnZoyNQnvKyH87Kfg+FoxSeyWUB8ISZZsC91C44ZuBFytw== dependencies: - "@babel/helper-create-class-features-plugin" "^7.22.15" - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-create-class-features-plugin" "^7.25.4" + "@babel/helper-plugin-utils" "^7.24.8" -"@babel/plugin-transform-private-property-in-object@^7.23.4": - version "7.23.4" - resolved "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.23.4.tgz" - integrity sha512-9G3K1YqTq3F4Vt88Djx1UZ79PDyj+yKRnUy7cZGSMe+a7jkwD259uKKuUzQlPkGam7R+8RJwh5z4xO27fA1o2A== +"@babel/plugin-transform-private-property-in-object@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.24.7.tgz#4eec6bc701288c1fab5f72e6a4bbc9d67faca061" + integrity sha512-9z76mxwnwFxMyxZWEgdgECQglF2Q7cFLm0kMf8pGwt+GSJsY0cONKj/UuO4bOH0w/uAel3ekS4ra5CEAyJRmDA== dependencies: - "@babel/helper-annotate-as-pure" "^7.22.5" - "@babel/helper-create-class-features-plugin" "^7.22.15" - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-annotate-as-pure" "^7.24.7" + "@babel/helper-create-class-features-plugin" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-private-property-in-object" "^7.14.5" -"@babel/plugin-transform-property-literals@^7.23.3": - version "7.23.3" - resolved "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.23.3.tgz" - integrity sha512-jR3Jn3y7cZp4oEWPFAlRsSWjxKe4PZILGBSd4nis1TsC5qeSpb+nrtihJuDhNI7QHiVbUaiXa0X2RZY3/TI6Nw== +"@babel/plugin-transform-property-literals@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.24.7.tgz#f0d2ed8380dfbed949c42d4d790266525d63bbdc" + integrity sha512-EMi4MLQSHfd2nrCqQEWxFdha2gBCqU4ZcCng4WBGZ5CJL4bBRW0ptdqqDdeirGZcpALazVVNJqRmsO8/+oNCBA== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-transform-regenerator@^7.23.3": - version "7.23.3" - resolved "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.23.3.tgz" - integrity sha512-KP+75h0KghBMcVpuKisx3XTu9Ncut8Q8TuvGO4IhY+9D5DFEckQefOuIsB/gQ2tG71lCke4NMrtIPS8pOj18BQ== +"@babel/plugin-transform-regenerator@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.24.7.tgz#021562de4534d8b4b1851759fd7af4e05d2c47f8" + integrity sha512-lq3fvXPdimDrlg6LWBoqj+r/DEWgONuwjuOuQCSYgRroXDH/IdM1C0IZf59fL5cHLpjEH/O6opIRBbqv7ELnuA== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.7" regenerator-transform "^0.15.2" -"@babel/plugin-transform-reserved-words@^7.23.3": - version "7.23.3" - resolved "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.23.3.tgz" - integrity sha512-QnNTazY54YqgGxwIexMZva9gqbPa15t/x9VS+0fsEFWplwVpXYZivtgl43Z1vMpc1bdPP2PP8siFeVcnFvA3Cg== - dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - -"@babel/plugin-transform-shorthand-properties@^7.23.3": - version "7.23.3" - resolved "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.23.3.tgz" - integrity sha512-ED2fgqZLmexWiN+YNFX26fx4gh5qHDhn1O2gvEhreLW2iI63Sqm4llRLCXALKrCnbN4Jy0VcMQZl/SAzqug/jg== - dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - -"@babel/plugin-transform-spread@^7.23.3": - version "7.23.3" - resolved "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.23.3.tgz" - integrity sha512-VvfVYlrlBVu+77xVTOAoxQ6mZbnIq5FM0aGBSFEcIh03qHf+zNqA4DC/3XMUozTg7bZV3e3mZQ0i13VB6v5yUg== - dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" - -"@babel/plugin-transform-sticky-regex@^7.23.3": - version "7.23.3" - resolved "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.23.3.tgz" - integrity sha512-HZOyN9g+rtvnOU3Yh7kSxXrKbzgrm5X4GncPY1QOquu7epga5MxKHVpYu2hvQnry/H+JjckSYRb93iNfsioAGg== - dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - -"@babel/plugin-transform-template-literals@^7.23.3": - version "7.23.3" - resolved "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.23.3.tgz" - integrity sha512-Flok06AYNp7GV2oJPZZcP9vZdszev6vPBkHLwxwSpaIqx75wn6mUd3UFWsSsA0l8nXAKkyCmL/sR02m8RYGeHg== - dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - -"@babel/plugin-transform-typeof-symbol@^7.23.3": - version "7.23.3" - resolved "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.23.3.tgz" - integrity sha512-4t15ViVnaFdrPC74be1gXBSMzXk3B4Us9lP7uLRQHTFpV5Dvt33pn+2MyyNxmN3VTTm3oTrZVMUmuw3oBnQ2oQ== - dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - -"@babel/plugin-transform-typescript@^7.23.3": - version "7.23.6" - resolved "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.23.6.tgz" - integrity sha512-6cBG5mBvUu4VUD04OHKnYzbuHNP8huDsD3EDqqpIpsswTDoqHCjLoHb6+QgsV1WsT2nipRqCPgxD3LXnEO7XfA== - dependencies: - "@babel/helper-annotate-as-pure" "^7.22.5" - "@babel/helper-create-class-features-plugin" "^7.23.6" - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/plugin-syntax-typescript" "^7.23.3" - -"@babel/plugin-transform-unicode-escapes@^7.23.3": - version "7.23.3" - resolved "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.23.3.tgz" - integrity sha512-OMCUx/bU6ChE3r4+ZdylEqAjaQgHAgipgW8nsCfu5pGqDcFytVd91AwRvUJSBZDz0exPGgnjoqhgRYLRjFZc9Q== - dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - -"@babel/plugin-transform-unicode-property-regex@^7.23.3": - version "7.23.3" - resolved "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.23.3.tgz" - integrity sha512-KcLIm+pDZkWZQAFJ9pdfmh89EwVfmNovFBcXko8szpBeF8z68kWIPeKlmSOkT9BXJxs2C0uk+5LxoxIv62MROA== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.22.15" - "@babel/helper-plugin-utils" "^7.22.5" - -"@babel/plugin-transform-unicode-regex@^7.23.3": - version "7.23.3" - resolved "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.23.3.tgz" - integrity sha512-wMHpNA4x2cIA32b/ci3AfwNgheiva2W0WUKWTK7vBHBhDKfPsc5cFGNWm69WBqpwd86u1qwZ9PWevKqm1A3yAw== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.22.15" - "@babel/helper-plugin-utils" "^7.22.5" - -"@babel/plugin-transform-unicode-sets-regex@^7.23.3": - version "7.23.3" - resolved "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.23.3.tgz" - integrity sha512-W7lliA/v9bNR83Qc3q1ip9CQMZ09CcHDbHfbLRDNuAhn1Mvkr1ZNF7hPmztMQvtTGVLJ9m8IZqWsTkXOml8dbw== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.22.15" - "@babel/helper-plugin-utils" "^7.22.5" - -"@babel/preset-env@^7.23.3": - version "7.23.6" - resolved "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.23.6.tgz" - integrity sha512-2XPn/BqKkZCpzYhUUNZ1ssXw7DcXfKQEjv/uXZUXgaebCMYmkEsfZ2yY+vv+xtXv50WmL5SGhyB6/xsWxIvvOQ== - dependencies: - "@babel/compat-data" "^7.23.5" - "@babel/helper-compilation-targets" "^7.23.6" - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/helper-validator-option" "^7.23.5" - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.23.3" - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.23.3" - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly" "^7.23.3" +"@babel/plugin-transform-reserved-words@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.24.7.tgz#80037fe4fbf031fc1125022178ff3938bb3743a4" + integrity sha512-0DUq0pHcPKbjFZCfTss/pGkYMfy3vFWydkUBd9r0GHpIyfs2eCDENvqadMycRS9wZCXR41wucAfJHJmwA0UmoQ== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-transform-shorthand-properties@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.24.7.tgz#85448c6b996e122fa9e289746140aaa99da64e73" + integrity sha512-KsDsevZMDsigzbA09+vacnLpmPH4aWjcZjXdyFKGzpplxhbeB4wYtury3vglQkg6KM/xEPKt73eCjPPf1PgXBA== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-transform-spread@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.24.7.tgz#e8a38c0fde7882e0fb8f160378f74bd885cc7bb3" + integrity sha512-x96oO0I09dgMDxJaANcRyD4ellXFLLiWhuwDxKZX5g2rWP1bTPkBSwCYv96VDXVT1bD9aPj8tppr5ITIh8hBng== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" + +"@babel/plugin-transform-sticky-regex@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.24.7.tgz#96ae80d7a7e5251f657b5cf18f1ea6bf926f5feb" + integrity sha512-kHPSIJc9v24zEml5geKg9Mjx5ULpfncj0wRpYtxbvKyTtHCYDkVE3aHQ03FrpEo4gEe2vrJJS1Y9CJTaThA52g== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-transform-template-literals@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.24.7.tgz#a05debb4a9072ae8f985bcf77f3f215434c8f8c8" + integrity sha512-AfDTQmClklHCOLxtGoP7HkeMw56k1/bTQjwsfhL6pppo/M4TOBSq+jjBUBLmV/4oeFg4GWMavIl44ZeCtmmZTw== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-transform-typeof-symbol@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.8.tgz#383dab37fb073f5bfe6e60c654caac309f92ba1c" + integrity sha512-adNTUpDCVnmAE58VEqKlAA6ZBlNkMnWD0ZcW76lyNFN3MJniyGFZfNwERVk8Ap56MCnXztmDr19T4mPTztcuaw== + dependencies: + "@babel/helper-plugin-utils" "^7.24.8" + +"@babel/plugin-transform-typescript@^7.24.7": + version "7.25.2" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.25.2.tgz#237c5d10de6d493be31637c6b9fa30b6c5461add" + integrity sha512-lBwRvjSmqiMYe/pS0+1gggjJleUJi7NzjvQ1Fkqtt69hBa/0t1YuW/MLQMAPixfwaQOHUXsd6jeU3Z+vdGv3+A== + dependencies: + "@babel/helper-annotate-as-pure" "^7.24.7" + "@babel/helper-create-class-features-plugin" "^7.25.0" + "@babel/helper-plugin-utils" "^7.24.8" + "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" + "@babel/plugin-syntax-typescript" "^7.24.7" + +"@babel/plugin-transform-unicode-escapes@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.24.7.tgz#2023a82ced1fb4971630a2e079764502c4148e0e" + integrity sha512-U3ap1gm5+4edc2Q/P+9VrBNhGkfnf+8ZqppY71Bo/pzZmXhhLdqgaUl6cuB07O1+AQJtCLfaOmswiNbSQ9ivhw== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-transform-unicode-property-regex@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.24.7.tgz#9073a4cd13b86ea71c3264659590ac086605bbcd" + integrity sha512-uH2O4OV5M9FZYQrwc7NdVmMxQJOCCzFeYudlZSzUAHRFeOujQefa92E74TQDVskNHCzOXoigEuoyzHDhaEaK5w== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-transform-unicode-regex@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.24.7.tgz#dfc3d4a51127108099b19817c0963be6a2adf19f" + integrity sha512-hlQ96MBZSAXUq7ltkjtu3FJCCSMx/j629ns3hA3pXnBXjanNP0LHi+JpPeA81zaWgVK1VGH95Xuy7u0RyQ8kMg== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-transform-unicode-sets-regex@^7.25.4": + version "7.25.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.4.tgz#be664c2a0697ffacd3423595d5edef6049e8946c" + integrity sha512-qesBxiWkgN1Q+31xUE9RcMk79eOXXDCv6tfyGMRSs4RGlioSg2WVyQAm07k726cSE56pa+Kb0y9epX2qaXzTvA== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.25.2" + "@babel/helper-plugin-utils" "^7.24.8" + +"@babel/preset-env@^7.25.4": + version "7.25.4" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.25.4.tgz#be23043d43a34a2721cd0f676c7ba6f1481f6af6" + integrity sha512-W9Gyo+KmcxjGahtt3t9fb14vFRWvPpu5pT6GBlovAK6BTBcxgjfVMSQCfJl4oi35ODrxP6xx2Wr8LNST57Mraw== + dependencies: + "@babel/compat-data" "^7.25.4" + "@babel/helper-compilation-targets" "^7.25.2" + "@babel/helper-plugin-utils" "^7.24.8" + "@babel/helper-validator-option" "^7.24.8" + "@babel/plugin-bugfix-firefox-class-in-computed-class-key" "^7.25.3" + "@babel/plugin-bugfix-safari-class-field-initializer-scope" "^7.25.0" + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.25.0" + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.24.7" + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly" "^7.25.0" "@babel/plugin-proposal-private-property-in-object" "7.21.0-placeholder-for-preset-env.2" "@babel/plugin-syntax-async-generators" "^7.8.4" "@babel/plugin-syntax-class-properties" "^7.12.13" "@babel/plugin-syntax-class-static-block" "^7.14.5" "@babel/plugin-syntax-dynamic-import" "^7.8.3" "@babel/plugin-syntax-export-namespace-from" "^7.8.3" - "@babel/plugin-syntax-import-assertions" "^7.23.3" - "@babel/plugin-syntax-import-attributes" "^7.23.3" + "@babel/plugin-syntax-import-assertions" "^7.24.7" + "@babel/plugin-syntax-import-attributes" "^7.24.7" "@babel/plugin-syntax-import-meta" "^7.10.4" "@babel/plugin-syntax-json-strings" "^7.8.3" "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" @@ -904,59 +1083,60 @@ "@babel/plugin-syntax-private-property-in-object" "^7.14.5" "@babel/plugin-syntax-top-level-await" "^7.14.5" "@babel/plugin-syntax-unicode-sets-regex" "^7.18.6" - "@babel/plugin-transform-arrow-functions" "^7.23.3" - "@babel/plugin-transform-async-generator-functions" "^7.23.4" - "@babel/plugin-transform-async-to-generator" "^7.23.3" - "@babel/plugin-transform-block-scoped-functions" "^7.23.3" - "@babel/plugin-transform-block-scoping" "^7.23.4" - "@babel/plugin-transform-class-properties" "^7.23.3" - "@babel/plugin-transform-class-static-block" "^7.23.4" - "@babel/plugin-transform-classes" "^7.23.5" - "@babel/plugin-transform-computed-properties" "^7.23.3" - "@babel/plugin-transform-destructuring" "^7.23.3" - "@babel/plugin-transform-dotall-regex" "^7.23.3" - "@babel/plugin-transform-duplicate-keys" "^7.23.3" - "@babel/plugin-transform-dynamic-import" "^7.23.4" - "@babel/plugin-transform-exponentiation-operator" "^7.23.3" - "@babel/plugin-transform-export-namespace-from" "^7.23.4" - "@babel/plugin-transform-for-of" "^7.23.6" - "@babel/plugin-transform-function-name" "^7.23.3" - "@babel/plugin-transform-json-strings" "^7.23.4" - "@babel/plugin-transform-literals" "^7.23.3" - "@babel/plugin-transform-logical-assignment-operators" "^7.23.4" - "@babel/plugin-transform-member-expression-literals" "^7.23.3" - "@babel/plugin-transform-modules-amd" "^7.23.3" - "@babel/plugin-transform-modules-commonjs" "^7.23.3" - "@babel/plugin-transform-modules-systemjs" "^7.23.3" - "@babel/plugin-transform-modules-umd" "^7.23.3" - "@babel/plugin-transform-named-capturing-groups-regex" "^7.22.5" - "@babel/plugin-transform-new-target" "^7.23.3" - "@babel/plugin-transform-nullish-coalescing-operator" "^7.23.4" - "@babel/plugin-transform-numeric-separator" "^7.23.4" - "@babel/plugin-transform-object-rest-spread" "^7.23.4" - "@babel/plugin-transform-object-super" "^7.23.3" - "@babel/plugin-transform-optional-catch-binding" "^7.23.4" - "@babel/plugin-transform-optional-chaining" "^7.23.4" - "@babel/plugin-transform-parameters" "^7.23.3" - "@babel/plugin-transform-private-methods" "^7.23.3" - "@babel/plugin-transform-private-property-in-object" "^7.23.4" - "@babel/plugin-transform-property-literals" "^7.23.3" - "@babel/plugin-transform-regenerator" "^7.23.3" - "@babel/plugin-transform-reserved-words" "^7.23.3" - "@babel/plugin-transform-shorthand-properties" "^7.23.3" - "@babel/plugin-transform-spread" "^7.23.3" - "@babel/plugin-transform-sticky-regex" "^7.23.3" - "@babel/plugin-transform-template-literals" "^7.23.3" - "@babel/plugin-transform-typeof-symbol" "^7.23.3" - "@babel/plugin-transform-unicode-escapes" "^7.23.3" - "@babel/plugin-transform-unicode-property-regex" "^7.23.3" - "@babel/plugin-transform-unicode-regex" "^7.23.3" - "@babel/plugin-transform-unicode-sets-regex" "^7.23.3" + "@babel/plugin-transform-arrow-functions" "^7.24.7" + "@babel/plugin-transform-async-generator-functions" "^7.25.4" + "@babel/plugin-transform-async-to-generator" "^7.24.7" + "@babel/plugin-transform-block-scoped-functions" "^7.24.7" + "@babel/plugin-transform-block-scoping" "^7.25.0" + "@babel/plugin-transform-class-properties" "^7.25.4" + "@babel/plugin-transform-class-static-block" "^7.24.7" + "@babel/plugin-transform-classes" "^7.25.4" + "@babel/plugin-transform-computed-properties" "^7.24.7" + "@babel/plugin-transform-destructuring" "^7.24.8" + "@babel/plugin-transform-dotall-regex" "^7.24.7" + "@babel/plugin-transform-duplicate-keys" "^7.24.7" + "@babel/plugin-transform-duplicate-named-capturing-groups-regex" "^7.25.0" + "@babel/plugin-transform-dynamic-import" "^7.24.7" + "@babel/plugin-transform-exponentiation-operator" "^7.24.7" + "@babel/plugin-transform-export-namespace-from" "^7.24.7" + "@babel/plugin-transform-for-of" "^7.24.7" + "@babel/plugin-transform-function-name" "^7.25.1" + "@babel/plugin-transform-json-strings" "^7.24.7" + "@babel/plugin-transform-literals" "^7.25.2" + "@babel/plugin-transform-logical-assignment-operators" "^7.24.7" + "@babel/plugin-transform-member-expression-literals" "^7.24.7" + "@babel/plugin-transform-modules-amd" "^7.24.7" + "@babel/plugin-transform-modules-commonjs" "^7.24.8" + "@babel/plugin-transform-modules-systemjs" "^7.25.0" + "@babel/plugin-transform-modules-umd" "^7.24.7" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.24.7" + "@babel/plugin-transform-new-target" "^7.24.7" + "@babel/plugin-transform-nullish-coalescing-operator" "^7.24.7" + "@babel/plugin-transform-numeric-separator" "^7.24.7" + "@babel/plugin-transform-object-rest-spread" "^7.24.7" + "@babel/plugin-transform-object-super" "^7.24.7" + "@babel/plugin-transform-optional-catch-binding" "^7.24.7" + "@babel/plugin-transform-optional-chaining" "^7.24.8" + "@babel/plugin-transform-parameters" "^7.24.7" + "@babel/plugin-transform-private-methods" "^7.25.4" + "@babel/plugin-transform-private-property-in-object" "^7.24.7" + "@babel/plugin-transform-property-literals" "^7.24.7" + "@babel/plugin-transform-regenerator" "^7.24.7" + "@babel/plugin-transform-reserved-words" "^7.24.7" + "@babel/plugin-transform-shorthand-properties" "^7.24.7" + "@babel/plugin-transform-spread" "^7.24.7" + "@babel/plugin-transform-sticky-regex" "^7.24.7" + "@babel/plugin-transform-template-literals" "^7.24.7" + "@babel/plugin-transform-typeof-symbol" "^7.24.8" + "@babel/plugin-transform-unicode-escapes" "^7.24.7" + "@babel/plugin-transform-unicode-property-regex" "^7.24.7" + "@babel/plugin-transform-unicode-regex" "^7.24.7" + "@babel/plugin-transform-unicode-sets-regex" "^7.25.4" "@babel/preset-modules" "0.1.6-no-external-plugins" - babel-plugin-polyfill-corejs2 "^0.4.6" - babel-plugin-polyfill-corejs3 "^0.8.5" - babel-plugin-polyfill-regenerator "^0.5.3" - core-js-compat "^3.31.0" + babel-plugin-polyfill-corejs2 "^0.4.10" + babel-plugin-polyfill-corejs3 "^0.10.6" + babel-plugin-polyfill-regenerator "^0.6.1" + core-js-compat "^3.37.1" semver "^6.3.1" "@babel/preset-modules@0.1.6-no-external-plugins": @@ -968,16 +1148,16 @@ "@babel/types" "^7.4.4" esutils "^2.0.2" -"@babel/preset-typescript@^7.23.3": - version "7.23.3" - resolved "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.23.3.tgz" - integrity sha512-17oIGVlqz6CchO9RFYn5U6ZpWRZIngayYCtrPRSgANSwC2V1Jb+iP74nVxzzXJte8b8BYxrL1yY96xfhTBrNNQ== +"@babel/preset-typescript@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.24.7.tgz#66cd86ea8f8c014855671d5ea9a737139cbbfef1" + integrity sha512-SyXRe3OdWwIwalxDg5UtJnJQO+YPcTfwiIY2B0Xlddh9o7jpWLvv8X1RthIeDOxQ+O1ML5BLPCONToObyVQVuQ== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/helper-validator-option" "^7.22.15" - "@babel/plugin-syntax-jsx" "^7.23.3" - "@babel/plugin-transform-modules-commonjs" "^7.23.3" - "@babel/plugin-transform-typescript" "^7.23.3" + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-validator-option" "^7.24.7" + "@babel/plugin-syntax-jsx" "^7.24.7" + "@babel/plugin-transform-modules-commonjs" "^7.24.7" + "@babel/plugin-transform-typescript" "^7.24.7" "@babel/regjsgen@^0.8.0": version "0.8.0" @@ -1000,6 +1180,15 @@ "@babel/parser" "^7.22.15" "@babel/types" "^7.22.15" +"@babel/template@^7.24.7", "@babel/template@^7.25.0": + version "7.25.0" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.25.0.tgz#e733dc3134b4fede528c15bc95e89cb98c52592a" + integrity sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q== + dependencies: + "@babel/code-frame" "^7.24.7" + "@babel/parser" "^7.25.0" + "@babel/types" "^7.25.0" + "@babel/traverse@^7.23.6": version "7.23.6" resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.6.tgz" @@ -1016,7 +1205,20 @@ debug "^4.3.1" globals "^11.1.0" -"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.22.15", "@babel/types@^7.22.19", "@babel/types@^7.22.5", "@babel/types@^7.23.0", "@babel/types@^7.23.6", "@babel/types@^7.3.3", "@babel/types@^7.4.4": +"@babel/traverse@^7.24.7", "@babel/traverse@^7.24.8", "@babel/traverse@^7.25.0", "@babel/traverse@^7.25.1", "@babel/traverse@^7.25.2", "@babel/traverse@^7.25.3", "@babel/traverse@^7.25.4": + version "7.25.6" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.25.6.tgz#04fad980e444f182ecf1520504941940a90fea41" + integrity sha512-9Vrcx5ZW6UwK5tvqsj0nGpp/XzqthkT0dqIc9g1AdtygFToNtTF67XzYS//dm+SAK9cp3B9R4ZO/46p63SCjlQ== + dependencies: + "@babel/code-frame" "^7.24.7" + "@babel/generator" "^7.25.6" + "@babel/parser" "^7.25.6" + "@babel/template" "^7.25.0" + "@babel/types" "^7.25.6" + debug "^4.3.1" + globals "^11.1.0" + +"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.22.15", "@babel/types@^7.22.5", "@babel/types@^7.23.0", "@babel/types@^7.23.6", "@babel/types@^7.3.3", "@babel/types@^7.4.4": version "7.23.6" resolved "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz" integrity sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg== @@ -1025,6 +1227,15 @@ "@babel/helper-validator-identifier" "^7.22.20" to-fast-properties "^2.0.0" +"@babel/types@^7.24.7", "@babel/types@^7.24.8", "@babel/types@^7.25.0", "@babel/types@^7.25.2", "@babel/types@^7.25.6": + version "7.25.6" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.25.6.tgz#893942ddb858f32ae7a004ec9d3a76b3463ef8e6" + integrity sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw== + dependencies: + "@babel/helper-string-parser" "^7.24.8" + "@babel/helper-validator-identifier" "^7.24.7" + to-fast-properties "^2.0.0" + "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz" @@ -1191,7 +1402,7 @@ "@gar/promisify@^1.1.3": version "1.1.3" - resolved "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz" + resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6" integrity sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw== "@humanwhocodes/config-array@^0.11.13": @@ -1447,6 +1658,15 @@ "@jridgewell/sourcemap-codec" "^1.4.10" "@jridgewell/trace-mapping" "^0.3.9" +"@jridgewell/gen-mapping@^0.3.5": + version "0.3.5" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz#dcce6aff74bdf6dad1a95802b69b04a2fcb1fb36" + integrity sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg== + dependencies: + "@jridgewell/set-array" "^1.2.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.24" + "@jridgewell/resolve-uri@^3.0.3", "@jridgewell/resolve-uri@^3.1.0": version "3.1.1" resolved "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz" @@ -1457,6 +1677,11 @@ resolved "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz" integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== +"@jridgewell/set-array@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280" + integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== + "@jridgewell/source-map@^0.3.3": version "0.3.5" resolved "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz" @@ -1494,9 +1719,17 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" +"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": + version "0.3.25" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" + integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + "@lerna/child-process@7.4.2": version "7.4.2" - resolved "https://registry.npmjs.org/@lerna/child-process/-/child-process-7.4.2.tgz" + resolved "https://registry.yarnpkg.com/@lerna/child-process/-/child-process-7.4.2.tgz#a2fd013ac2150dc288270d3e0d0b850c06bec511" integrity sha512-je+kkrfcvPcwL5Tg8JRENRqlbzjdlZXyaR88UcnCdNW0AJ1jX9IfHRys1X7AwSroU2ug8ESNC+suoBw1vX833Q== dependencies: chalk "^4.1.0" @@ -1505,7 +1738,7 @@ "@lerna/create@7.4.2": version "7.4.2" - resolved "https://registry.npmjs.org/@lerna/create/-/create-7.4.2.tgz" + resolved "https://registry.yarnpkg.com/@lerna/create/-/create-7.4.2.tgz#f845fad1480e46555af98bd39af29571605dddc9" integrity sha512-1wplFbQ52K8E/unnqB0Tq39Z4e+NEoNrpovEnl6GpsTUrC6WDp8+w0Le2uCBV0hXyemxChduCkLz4/y1H1wTeg== dependencies: "@lerna/child-process" "7.4.2" @@ -1614,37 +1847,37 @@ webpack "5.89.0" webpack-node-externals "3.0.0" -"@nestjs/common@^10.3.7": - version "10.3.7" - resolved "https://registry.yarnpkg.com/@nestjs/common/-/common-10.3.7.tgz#38ab5ff92277cf1f26f4749c264524e76962cfff" - integrity sha512-gKFtFzcJznrwsRYjtNZoPAvSOPYdNgxbTYoAyLTpoy393cIKgLmJTHu6ReH8/qIB9AaZLdGaFLkx98W/tFWFUw== +"@nestjs/common@^10.4.4": + version "10.4.4" + resolved "https://registry.yarnpkg.com/@nestjs/common/-/common-10.4.4.tgz#673d0eca273e1ab3a4d3ec9e212114b9f4fbf6e8" + integrity sha512-0j2/zqRw9nvHV1GKTktER8B/hIC/Z8CYFjN/ZqUuvwayCH+jZZBhCR2oRyuvLTXdnlSmtCAg2xvQ0ULqQvzqhA== dependencies: uid "2.0.2" iterare "1.2.1" - tslib "2.6.2" + tslib "2.7.0" -"@nestjs/core@^10.3.7": - version "10.3.7" - resolved "https://registry.yarnpkg.com/@nestjs/core/-/core-10.3.7.tgz#736906ec27bc39b91519506babc231c8ab56ea21" - integrity sha512-hsdlnfiQ3kgqHL5k7js3CU0PV7hBJVi+LfFMgCkoagRxNMf67z0GFGeOV2jk5d65ssB19qdYsDa1MGVuEaoUpg== +"@nestjs/core@^10.4.4": + version "10.4.4" + resolved "https://registry.yarnpkg.com/@nestjs/core/-/core-10.4.4.tgz#12cb1110da6d76e12ceccf0e92f6f5220fe27525" + integrity sha512-y9tjmAzU6LTh1cC/lWrRsCcOd80khSR0qAHAqwY2svbW+AhsR/XCzgpZrAAKJrm/dDfjLCZKyxJSayeirGcW5Q== dependencies: uid "2.0.2" "@nuxtjs/opencollective" "0.3.2" fast-safe-stringify "2.1.1" iterare "1.2.1" - path-to-regexp "3.2.0" - tslib "2.6.2" + path-to-regexp "3.3.0" + tslib "2.7.0" -"@nestjs/platform-express@^10.3.7": - version "10.3.7" - resolved "https://registry.yarnpkg.com/@nestjs/platform-express/-/platform-express-10.3.7.tgz#ae3fc59609bdc0ffc5029a6e74d59a5d1e257eef" - integrity sha512-noNJ+PyIxQJLCKfuXz0tcQtlVAynfLIuKy62g70lEZ86UrIqSrZFqvWs/rFUgkbT6J8H7Rmv11hASOnX+7M2rA== +"@nestjs/platform-express@^10.4.4": + version "10.4.4" + resolved "https://registry.yarnpkg.com/@nestjs/platform-express/-/platform-express-10.4.4.tgz#582d375272207f8d1528f77ff470ba815d9d9846" + integrity sha512-y52q1MxhbHaT3vAgWd08RgiYon0lJgtTa8U6g6gV0KI0IygwZhDQFJVxnrRDUdxQGIP5CKHmfQu3sk9gTNFoEA== dependencies: - body-parser "1.20.2" + body-parser "1.20.3" cors "2.8.5" - express "4.19.2" + express "4.21.0" multer "1.4.4-lts.1" - tslib "2.6.2" + tslib "2.7.0" "@nestjs/schematics@^10.0.0", "@nestjs/schematics@^10.0.1": version "10.1.0" @@ -1680,7 +1913,7 @@ "@npmcli/fs@^2.1.0": version "2.1.2" - resolved "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz" + resolved "https://registry.yarnpkg.com/@npmcli/fs/-/fs-2.1.2.tgz#a9e2541a4a2fec2e69c29b35e6060973da79b865" integrity sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ== dependencies: "@gar/promisify" "^1.1.3" @@ -1695,7 +1928,7 @@ "@npmcli/git@^4.0.0": version "4.1.0" - resolved "https://registry.npmjs.org/@npmcli/git/-/git-4.1.0.tgz" + resolved "https://registry.yarnpkg.com/@npmcli/git/-/git-4.1.0.tgz#ab0ad3fd82bc4d8c1351b6c62f0fa56e8fe6afa6" integrity sha512-9hwoB3gStVfa0N31ymBmrX+GuDGdVA/QWShZVqE0HK2Af+7QGGrCTbZia/SW0ImUTjTne7SP91qxDmtXvDHRPQ== dependencies: "@npmcli/promise-spawn" "^6.0.0" @@ -1717,7 +1950,7 @@ "@npmcli/move-file@^2.0.0": version "2.0.1" - resolved "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.1.tgz" + resolved "https://registry.yarnpkg.com/@npmcli/move-file/-/move-file-2.0.1.tgz#26f6bdc379d87f75e55739bab89db525b06100e4" integrity sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ== dependencies: mkdirp "^1.0.4" @@ -1730,14 +1963,14 @@ "@npmcli/promise-spawn@^6.0.0", "@npmcli/promise-spawn@^6.0.1": version "6.0.2" - resolved "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-6.0.2.tgz" + resolved "https://registry.yarnpkg.com/@npmcli/promise-spawn/-/promise-spawn-6.0.2.tgz#c8bc4fa2bd0f01cb979d8798ba038f314cfa70f2" integrity sha512-gGq0NJkIGSwdbUt4yhdF8ZrmkGKVz9vAdVzpOfnom+V8PLSmSOVhZwbNvZZS1EYcJN5hzzKBxmmVVAInM6HQLg== dependencies: which "^3.0.0" "@npmcli/run-script@6.0.2", "@npmcli/run-script@^6.0.0": version "6.0.2" - resolved "https://registry.npmjs.org/@npmcli/run-script/-/run-script-6.0.2.tgz" + resolved "https://registry.yarnpkg.com/@npmcli/run-script/-/run-script-6.0.2.tgz#a25452d45ee7f7fb8c16dfaf9624423c0c0eb885" integrity sha512-NCcr1uQo1k5U+SYlnIrbAh3cxy+OQT1VtqiAbxdymSlptbzBb62AjH2xXgjNCoP073hoa1CfCAcwoZ8k96C4nA== dependencies: "@npmcli/node-gyp" "^3.0.0" @@ -1748,14 +1981,14 @@ "@nrwl/devkit@16.10.0": version "16.10.0" - resolved "https://registry.npmjs.org/@nrwl/devkit/-/devkit-16.10.0.tgz" + resolved "https://registry.yarnpkg.com/@nrwl/devkit/-/devkit-16.10.0.tgz#ac8c5b4db00f12c4b817c937be2f7c4eb8f2593c" integrity sha512-fRloARtsDQoQgQ7HKEy0RJiusg/HSygnmg4gX/0n/Z+SUS+4KoZzvHjXc6T5ZdEiSjvLypJ+HBM8dQzIcVACPQ== dependencies: "@nx/devkit" "16.10.0" "@nrwl/tao@16.10.0": version "16.10.0" - resolved "https://registry.npmjs.org/@nrwl/tao/-/tao-16.10.0.tgz" + resolved "https://registry.yarnpkg.com/@nrwl/tao/-/tao-16.10.0.tgz#94642a0380709b8e387e1e33705a5a9624933375" integrity sha512-QNAanpINbr+Pod6e1xNgFbzK1x5wmZl+jMocgiEFXZ67KHvmbD6MAQQr0MMz+GPhIu7EE4QCTLTyCEMlAG+K5Q== dependencies: nx "16.10.0" @@ -1772,7 +2005,7 @@ "@nx/devkit@16.10.0", "@nx/devkit@>=16.5.1 < 17": version "16.10.0" - resolved "https://registry.npmjs.org/@nx/devkit/-/devkit-16.10.0.tgz" + resolved "https://registry.yarnpkg.com/@nx/devkit/-/devkit-16.10.0.tgz#7e466be2dee2dcb1ccaf286786ca2a0a639aa007" integrity sha512-IvKQqRJFDDiaj33SPfGd3ckNHhHi6ceEoqCbAP4UuMXOPPVOX6H0KVk+9tknkPb48B7jWIw6/AgOeWkBxPRO5w== dependencies: "@nrwl/devkit" "16.10.0" @@ -1785,7 +2018,7 @@ "@nx/nx-darwin-arm64@16.10.0": version "16.10.0" - resolved "https://registry.npmjs.org/@nx/nx-darwin-arm64/-/nx-darwin-arm64-16.10.0.tgz" + resolved "https://registry.yarnpkg.com/@nx/nx-darwin-arm64/-/nx-darwin-arm64-16.10.0.tgz#0c73010cac7a502549483b12bad347da9014e6f1" integrity sha512-YF+MIpeuwFkyvM5OwgY/rTNRpgVAI/YiR0yTYCZR+X3AAvP775IVlusNgQ3oedTBRUzyRnI4Tknj1WniENFsvQ== "@nx/nx-darwin-x64@16.10.0": @@ -1951,7 +2184,7 @@ "@parcel/watcher@2.0.4": version "2.0.4" - resolved "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.0.4.tgz" + resolved "https://registry.yarnpkg.com/@parcel/watcher/-/watcher-2.0.4.tgz#f300fef4cc38008ff4b8c29d92588eced3ce014b" integrity sha512-cTDi+FUDBIUOBKEtj+nhiJ71AZVlkAsQFuGQTun5tV9mwQBQgZvhCzG+URPQc8myeN32yRVZEfVAPCs1RW+Jvg== dependencies: node-addon-api "^3.2.1" @@ -1981,19 +2214,19 @@ "@sigstore/bundle@^1.1.0": version "1.1.0" - resolved "https://registry.npmjs.org/@sigstore/bundle/-/bundle-1.1.0.tgz" + resolved "https://registry.yarnpkg.com/@sigstore/bundle/-/bundle-1.1.0.tgz#17f8d813b09348b16eeed66a8cf1c3d6bd3d04f1" integrity sha512-PFutXEy0SmQxYI4texPw3dd2KewuNqv7OuK1ZFtY2fM754yhvG2KdgwIhRnoEE2uHdtdGNQ8s0lb94dW9sELog== dependencies: "@sigstore/protobuf-specs" "^0.2.0" "@sigstore/protobuf-specs@^0.2.0": version "0.2.1" - resolved "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.2.1.tgz" + resolved "https://registry.yarnpkg.com/@sigstore/protobuf-specs/-/protobuf-specs-0.2.1.tgz#be9ef4f3c38052c43bd399d3f792c97ff9e2277b" integrity sha512-XTWVxnWJu+c1oCshMLwnKvz8ZQJJDVOlciMfgpJBQbThVjKTCG8dwyhgLngBD2KN0ap9F/gOV8rFDEx8uh7R2A== "@sigstore/sign@^1.0.0": version "1.0.0" - resolved "https://registry.npmjs.org/@sigstore/sign/-/sign-1.0.0.tgz" + resolved "https://registry.yarnpkg.com/@sigstore/sign/-/sign-1.0.0.tgz#6b08ebc2f6c92aa5acb07a49784cb6738796f7b4" integrity sha512-INxFVNQteLtcfGmcoldzV6Je0sbbfh9I16DM4yJPw3j5+TFP8X6uIiA18mvpEa9yyeycAKgPmOA3X9hVdVTPUA== dependencies: "@sigstore/bundle" "^1.1.0" @@ -2002,7 +2235,7 @@ "@sigstore/tuf@^1.0.3": version "1.0.3" - resolved "https://registry.npmjs.org/@sigstore/tuf/-/tuf-1.0.3.tgz" + resolved "https://registry.yarnpkg.com/@sigstore/tuf/-/tuf-1.0.3.tgz#2a65986772ede996485728f027b0514c0b70b160" integrity sha512-2bRovzs0nJZFlCN3rXirE4gwxCn97JNjMmwpecqlbgV9WcxX7WRuIrgzx/X7Ib7MYRbyUTpBYE0s2x6AmZXnlg== dependencies: "@sigstore/protobuf-specs" "^0.2.0" @@ -2034,7 +2267,7 @@ "@tootallnate/once@2": version "2.0.0" - resolved "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz" + resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== "@tsconfig/node10@^1.0.7": @@ -2059,12 +2292,12 @@ "@tufjs/canonical-json@1.0.0": version "1.0.0" - resolved "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-1.0.0.tgz" + resolved "https://registry.yarnpkg.com/@tufjs/canonical-json/-/canonical-json-1.0.0.tgz#eade9fd1f537993bc1f0949f3aea276ecc4fab31" integrity sha512-QTnf++uxunWvG2z3UFNzAoQPHxnSXOwtaI3iJ+AohhV+5vONuArPjJE7aPXPVXfXJsqrVbZBu9b81AJoSd09IQ== "@tufjs/models@1.0.4": version "1.0.4" - resolved "https://registry.npmjs.org/@tufjs/models/-/models-1.0.4.tgz" + resolved "https://registry.yarnpkg.com/@tufjs/models/-/models-1.0.4.tgz#5a689630f6b9dbda338d4b208019336562f176ef" integrity sha512-qaGV9ltJP0EO25YfFUPhxRVK0evXFIAGicsVXuRim4Ed9cjPxYhNnNJ49SFmbeLgtxpslIkX317IgpfcHPVj/A== dependencies: "@tufjs/canonical-json" "1.0.0" @@ -2198,10 +2431,10 @@ dependencies: "@types/istanbul-lib-report" "*" -"@types/jest@^29.5.1": - version "29.5.11" - resolved "https://registry.npmjs.org/@types/jest/-/jest-29.5.11.tgz" - integrity sha512-S2mHmYIVe13vrm6q4kN6fLYYAka15ALQki/vgDC3mIukEOx8WJlv0kQPM+d4w8Gp6u0uSdKND04IlTXBv0rwnQ== +"@types/jest@^29.5.13": + version "29.5.13" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.13.tgz#8bc571659f401e6a719a7bf0dbcb8b78c71a8adc" + integrity sha512-wd+MVEZCHt23V0/L642O5APvspWply/rGY5BcW4SUETo2UzPU3Z26qr8jC2qxpimI2jjx9h7+2cj2FwIr01bXg== dependencies: expect "^29.0.0" pretty-format "^29.0.0" @@ -2689,7 +2922,7 @@ "@zkochan/js-yaml@0.0.6": version "0.0.6" - resolved "https://registry.npmjs.org/@zkochan/js-yaml/-/js-yaml-0.0.6.tgz" + resolved "https://registry.yarnpkg.com/@zkochan/js-yaml/-/js-yaml-0.0.6.tgz#975f0b306e705e28b8068a07737fa46d3fc04826" integrity sha512-nzvgl3VfhcELQ8LyVrYOru+UtAy1nrygk2+AGbTm8a5YcO6o8lSjAT+pfg3vJWxIoZKOUhrK6UU7xW/+00kQrg== dependencies: argparse "^2.0.1" @@ -2704,7 +2937,7 @@ JSONStream@^1.3.5: abbrev@^1.0.0: version "1.1.1" - resolved "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== accepts@~1.3.8: @@ -2747,14 +2980,14 @@ add-stream@^1.0.0: agent-base@6, agent-base@^6.0.2: version "6.0.2" - resolved "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== dependencies: debug "4" agentkeepalive@^4.2.1: version "4.5.0" - resolved "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz" + resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.5.0.tgz#2673ad1389b3c418c5a20c5d7364f93ca04be923" integrity sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew== dependencies: humanize-ms "^1.2.1" @@ -2870,12 +3103,12 @@ append-field@^1.0.0: "aproba@^1.0.3 || ^2.0.0": version "2.0.0" - resolved "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc" integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ== are-we-there-yet@^3.0.0: version "3.0.1" - resolved "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz#679df222b278c64f2cdba1175cdc00b0d96164bd" integrity sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg== dependencies: delegates "^1.0.0" @@ -2953,11 +3186,11 @@ asynckit@^0.4.0: integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== axios@^1.0.0: - version "1.6.2" - resolved "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz" - integrity sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A== + version "1.7.7" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.7.tgz#2f554296f9892a72ac8d8e4c5b79c14a91d0a47f" + integrity sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q== dependencies: - follow-redirects "^1.15.0" + follow-redirects "^1.15.6" form-data "^4.0.0" proxy-from-env "^1.1.0" @@ -2995,29 +3228,29 @@ babel-plugin-jest-hoist@^29.6.3: "@types/babel__core" "^7.1.14" "@types/babel__traverse" "^7.0.6" -babel-plugin-polyfill-corejs2@^0.4.6: - version "0.4.7" - resolved "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.7.tgz" - integrity sha512-LidDk/tEGDfuHW2DWh/Hgo4rmnw3cduK6ZkOI1NPFceSK3n/yAGeOsNT7FLnSGHkXj3RHGSEVkN3FsCTY6w2CQ== +babel-plugin-polyfill-corejs2@^0.4.10: + version "0.4.11" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.11.tgz#30320dfe3ffe1a336c15afdcdafd6fd615b25e33" + integrity sha512-sMEJ27L0gRHShOh5G54uAAPaiCOygY/5ratXuiyb2G46FmlSpc9eFCzYVyDiPxfNbwzA7mYahmjQc5q+CZQ09Q== dependencies: "@babel/compat-data" "^7.22.6" - "@babel/helper-define-polyfill-provider" "^0.4.4" + "@babel/helper-define-polyfill-provider" "^0.6.2" semver "^6.3.1" -babel-plugin-polyfill-corejs3@^0.8.5: - version "0.8.7" - resolved "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.7.tgz" - integrity sha512-KyDvZYxAzkC0Aj2dAPyDzi2Ym15e5JKZSK+maI7NAwSqofvuFglbSsxE7wUOvTg9oFVnHMzVzBKcqEb4PJgtOA== +babel-plugin-polyfill-corejs3@^0.10.6: + version "0.10.6" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.6.tgz#2deda57caef50f59c525aeb4964d3b2f867710c7" + integrity sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA== dependencies: - "@babel/helper-define-polyfill-provider" "^0.4.4" - core-js-compat "^3.33.1" + "@babel/helper-define-polyfill-provider" "^0.6.2" + core-js-compat "^3.38.0" -babel-plugin-polyfill-regenerator@^0.5.3: - version "0.5.4" - resolved "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.4.tgz" - integrity sha512-S/x2iOCvDaCASLYsOOgWOq4bCfKYVqvO/uxjkaYyZ3rVsVE3CeAI/c84NpyuBBymEgNvHgjEot3a9/Z/kXvqsg== +babel-plugin-polyfill-regenerator@^0.6.1: + version "0.6.2" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.2.tgz#addc47e240edd1da1058ebda03021f382bba785e" + integrity sha512-2R25rQZWP63nGwaAswvDazbPXfrM3HwVoBXK6HcqeKrSrL/JqcC/rDcf95l4r7LXLyxDXc8uQDa064GubtCABg== dependencies: - "@babel/helper-define-polyfill-provider" "^0.4.4" + "@babel/helper-define-polyfill-provider" "^0.6.2" babel-preset-current-node-syntax@^1.0.0: version "1.0.1" @@ -3079,10 +3312,10 @@ bl@^4.0.3, bl@^4.1.0: inherits "^2.0.4" readable-stream "^3.4.0" -body-parser@1.20.2: - version "1.20.2" - resolved "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz" - integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA== +body-parser@1.20.3: + version "1.20.3" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.3.tgz#1953431221c6fb5cd63c4b36d53fab0928e548c6" + integrity sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g== dependencies: bytes "3.1.2" content-type "~1.0.5" @@ -3092,7 +3325,7 @@ body-parser@1.20.2: http-errors "2.0.0" iconv-lite "0.4.24" on-finished "2.4.1" - qs "6.11.0" + qs "6.13.0" raw-body "2.5.2" type-is "~1.6.18" unpipe "1.0.0" @@ -3136,6 +3369,16 @@ browserslist@^4.14.5, browserslist@^4.22.2: node-releases "^2.0.14" update-browserslist-db "^1.0.13" +browserslist@^4.23.1, browserslist@^4.23.3: + version "4.24.0" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.0.tgz#a1325fe4bc80b64fda169629fc01b3d6cecd38d4" + integrity sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A== + dependencies: + caniuse-lite "^1.0.30001663" + electron-to-chromium "^1.5.28" + node-releases "^2.0.18" + update-browserslist-db "^1.1.0" + bser@2.1.1: version "2.1.1" resolved "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz" @@ -3158,7 +3401,7 @@ buffer@^5.5.0: builtins@^1.0.3: version "1.0.3" - resolved "https://registry.npmjs.org/builtins/-/builtins-1.0.3.tgz" + resolved "https://registry.yarnpkg.com/builtins/-/builtins-1.0.3.tgz#cb94faeb61c8696451db36534e1422f94f0aee88" integrity sha512-uYBjakWipfaO/bXI7E8rq6kpwHRZK5cNYrUv2OzZSI/FvmdMyXJ2tG9dKcjEC5YHmHpUAwsargWIZNWdxb/bnQ== builtins@^5.0.0: @@ -3194,7 +3437,7 @@ bytes@3.1.2: cacache@^16.1.0: version "16.1.3" - resolved "https://registry.npmjs.org/cacache/-/cacache-16.1.3.tgz" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-16.1.3.tgz#a02b9f34ecfaf9a78c9f4bc16fceb94d5d67a38e" integrity sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ== dependencies: "@npmcli/fs" "^2.1.0" @@ -3218,7 +3461,7 @@ cacache@^16.1.0: cacache@^17.0.0: version "17.1.4" - resolved "https://registry.npmjs.org/cacache/-/cacache-17.1.4.tgz" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-17.1.4.tgz#b3ff381580b47e85c6e64f801101508e26604b35" integrity sha512-/aJwG2l3ZMJ1xNAnqbMpA40of9dj/pIH3QfiuQSqjfPJF747VR0J/bHn+/KdNnHKc6XQcWt/AfRSBft82W1d2A== dependencies: "@npmcli/fs" "^3.1.0" @@ -3234,7 +3477,7 @@ cacache@^17.0.0: tar "^6.1.11" unique-filename "^3.0.0" -call-bind@^1.0.0, call-bind@^1.0.5: +call-bind@^1.0.5: version "1.0.5" resolved "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz" integrity sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ== @@ -3243,6 +3486,17 @@ call-bind@^1.0.0, call-bind@^1.0.5: get-intrinsic "^1.2.1" set-function-length "^1.1.1" +call-bind@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" + integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + set-function-length "^1.2.1" + callsites@^3.0.0: version "3.1.0" resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz" @@ -3272,6 +3526,11 @@ caniuse-lite@^1.0.30001565: resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001570.tgz" integrity sha512-+3e0ASu4sw1SWaoCtvPeyXp+5PsjigkSt8OXZbF9StH5pQWbxEjLAZE3n8Aup5udop1uRiKA7a4utUk/uoSpUw== +caniuse-lite@^1.0.30001663: + version "1.0.30001664" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001664.tgz#d588d75c9682d3301956b05a3749652a80677df4" + integrity sha512-AmE7k4dXiNKQipgn7a2xg558IRqPN3jMQY/rOsbxDhrd0tyChwbITBfiwtnqz8bi2M5mIWbxAYBvk7W7QBUS2g== + capital-case@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/capital-case/-/capital-case-1.0.4.tgz#9d130292353c9249f6b00fa5852bee38a717e669" @@ -3436,7 +3695,7 @@ clone@^1.0.2: cmd-shim@6.0.1: version "6.0.1" - resolved "https://registry.npmjs.org/cmd-shim/-/cmd-shim-6.0.1.tgz" + resolved "https://registry.yarnpkg.com/cmd-shim/-/cmd-shim-6.0.1.tgz#a65878080548e1dca760b3aea1e21ed05194da9d" integrity sha512-S9iI9y0nKR4hwEQsVWpyxld/6kRfGepGfzff83FcaiEBpmvlbA2nnGe7Cylgrx2f/p1P5S5wpRm9oL8z1PbS3Q== co@^4.6.0: @@ -3475,7 +3734,7 @@ color-name@~1.1.4: color-support@^1.1.3: version "1.1.3" - resolved "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz" + resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== columnify@1.6.0: @@ -3672,12 +3931,12 @@ cookie@0.6.0: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051" integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw== -core-js-compat@^3.31.0, core-js-compat@^3.33.1: - version "3.34.0" - resolved "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.34.0.tgz" - integrity sha512-4ZIyeNbW/Cn1wkMMDy+mvrRUxrwFNjKwbhCfQpDd+eLgYipDqp8oGFGtLmhh18EDPKA0g3VUBYOxQGGwvWLVpA== +core-js-compat@^3.37.1, core-js-compat@^3.38.0: + version "3.38.1" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.38.1.tgz#2bc7a298746ca5a7bcb9c164bcb120f2ebc09a09" + integrity sha512-JRH6gfXxGmrzF3tZ57lFx97YARxCXPaMzPo6jELZhv88pBH5VXpQ+y0znKGlFnzuaihqhLbefxSJxWJMPtfDzw== dependencies: - browserslist "^4.22.2" + browserslist "^4.23.3" core-util-is@^1.0.3, core-util-is@~1.0.0: version "1.0.3" @@ -3746,13 +4005,20 @@ debug@2.6.9: dependencies: ms "2.0.0" -debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4: +debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: version "4.3.4" resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== dependencies: ms "2.1.2" +debug@^4.3.3: + version "4.3.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" + integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== + dependencies: + ms "^2.1.3" + decamelize-keys@^1.1.0: version "1.1.1" resolved "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz" @@ -3768,7 +4034,7 @@ decamelize@^1.1.0: dedent@0.7.0: version "0.7.0" - resolved "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz" + resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" integrity sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA== dedent@^1.0.0: @@ -3820,6 +4086,15 @@ define-data-property@^1.1.1: gopd "^1.0.1" has-property-descriptors "^1.0.0" +define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" + define-lazy-prop@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz" @@ -3837,7 +4112,7 @@ delayed-stream@~1.0.0: delegates@^1.0.0: version "1.0.0" - resolved "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz" + resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" integrity sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ== depd@2.0.0: @@ -3898,14 +4173,19 @@ dot-prop@^5.1.0: dotenv-expand@~10.0.0: version "10.0.0" - resolved "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz" + resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-10.0.0.tgz#12605d00fb0af6d0a592e6558585784032e4ef37" integrity sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A== -dotenv@^16.3.1, dotenv@~16.3.1: +dotenv@^16.3.1: version "16.3.1" resolved "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz" integrity sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ== +dotenv@~16.3.1: + version "16.3.2" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.3.2.tgz#3cb611ce5a63002dbabf7c281bc331f69d28f03f" + integrity sha512-HTlk5nmhkm8F6JcdXvHIzaorzCoziNQT9mGxLPVXW8wJF1TiGSL60ZGB4gHWabHOaMmWmhvk2/lPHfnBiT78AQ== + duplexer@^0.1.1: version "0.1.2" resolved "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz" @@ -3933,6 +4213,11 @@ electron-to-chromium@^1.4.601: resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.614.tgz" integrity sha512-X4ze/9Sc3QWs6h92yerwqv7aB/uU8vCjZcrMjA8N9R1pjMFRe44dLsck5FzLilOYvcXuDn93B+bpGYyufc70gQ== +electron-to-chromium@^1.5.28: + version "1.5.30" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.30.tgz#5b264b489cfe0c3dd71097c164d795444834e7c7" + integrity sha512-sXI35EBN4lYxzc/pIGorlymYNzDBOqkSlVRe6MkgBsW/hW1tpC/HDJ2fjG7XnjakzfLEuvdmux0Mjs6jHq4UOA== + emittery@^0.13.1: version "0.13.1" resolved "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz" @@ -3953,6 +4238,11 @@ encodeurl@~1.0.2: resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz" integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== +encodeurl@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58" + integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== + encoding@^0.1.13: version "0.1.13" resolved "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz" @@ -3989,7 +4279,7 @@ env-paths@^2.2.0: envinfo@7.8.1: version "7.8.1" - resolved "https://registry.npmjs.org/envinfo/-/envinfo-7.8.1.tgz" + resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.8.1.tgz#06377e3e5f4d379fea7ac592d5ad8927e0c4d475" integrity sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw== err-code@^2.0.2: @@ -4011,6 +4301,18 @@ error-stack-parser@^2.1.4: dependencies: stackframe "^1.3.4" +es-define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" + integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== + dependencies: + get-intrinsic "^1.2.4" + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + es-module-lexer@^1.2.1: version "1.4.1" resolved "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.4.1.tgz" @@ -4021,6 +4323,11 @@ escalade@^3.1.1: resolved "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz" integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== +escalade@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" + integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== + escape-html@~1.0.3: version "1.0.3" resolved "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz" @@ -4333,37 +4640,37 @@ exponential-backoff@^3.1.1: resolved "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.1.tgz" integrity sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw== -express@4.19.2: - version "4.19.2" - resolved "https://registry.yarnpkg.com/express/-/express-4.19.2.tgz#e25437827a3aa7f2a827bc8171bbbb664a356465" - integrity sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q== +express@4.21.0: + version "4.21.0" + resolved "https://registry.yarnpkg.com/express/-/express-4.21.0.tgz#d57cb706d49623d4ac27833f1cbc466b668eb915" + integrity sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng== dependencies: accepts "~1.3.8" array-flatten "1.1.1" - body-parser "1.20.2" + body-parser "1.20.3" content-disposition "0.5.4" content-type "~1.0.4" cookie "0.6.0" cookie-signature "1.0.6" debug "2.6.9" depd "2.0.0" - encodeurl "~1.0.2" + encodeurl "~2.0.0" escape-html "~1.0.3" etag "~1.8.1" - finalhandler "1.2.0" + finalhandler "1.3.1" fresh "0.5.2" http-errors "2.0.0" - merge-descriptors "1.0.1" + merge-descriptors "1.0.3" methods "~1.1.2" on-finished "2.4.1" parseurl "~1.3.3" - path-to-regexp "0.1.7" + path-to-regexp "0.1.10" proxy-addr "~2.0.7" - qs "6.11.0" + qs "6.13.0" range-parser "~1.2.1" safe-buffer "5.2.1" - send "0.18.0" - serve-static "1.15.0" + send "0.19.0" + serve-static "1.16.2" setprototypeof "1.2.0" statuses "2.0.1" type-is "~1.6.18" @@ -4465,13 +4772,13 @@ fill-range@^7.0.1: dependencies: to-regex-range "^5.0.1" -finalhandler@1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz" - integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== +finalhandler@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.3.1.tgz#0c575f1d1d324ddd1da35ad7ece3df7d19088019" + integrity sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ== dependencies: debug "2.6.9" - encodeurl "~1.0.2" + encodeurl "~2.0.0" escape-html "~1.0.3" on-finished "2.4.1" parseurl "~1.3.3" @@ -4520,10 +4827,10 @@ flatted@^3.2.9: resolved "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz" integrity sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ== -follow-redirects@^1.15.0: - version "1.15.3" - resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz" - integrity sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q== +follow-redirects@^1.15.6: + version "1.15.9" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1" + integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ== foreground-child@^3.1.0: version "3.1.1" @@ -4586,7 +4893,7 @@ fs-extra@^10.0.0: fs-extra@^11.1.0, fs-extra@^11.1.1: version "11.2.0" - resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.2.0.tgz#e70e17dfad64232287d01929399e0ea7c86b0e5b" integrity sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw== dependencies: graceful-fs "^4.2.0" @@ -4629,7 +4936,7 @@ function-bind@^1.1.2: gauge@^4.0.3: version "4.0.4" - resolved "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-4.0.4.tgz#52ff0652f2bbf607a989793d53b751bef2328dce" integrity sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg== dependencies: aproba "^1.0.3 || ^2.0.0" @@ -4651,7 +4958,7 @@ get-caller-file@^2.0.5: resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== -get-intrinsic@^1.0.2, get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.2: +get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.2: version "1.2.2" resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz" integrity sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA== @@ -4661,6 +4968,17 @@ get-intrinsic@^1.0.2, get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@ has-symbols "^1.0.3" hasown "^2.0.0" +get-intrinsic@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" + integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + has-proto "^1.0.1" + has-symbols "^1.0.3" + hasown "^2.0.0" + get-package-type@^0.1.0: version "0.1.0" resolved "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz" @@ -4726,7 +5044,7 @@ git-up@^7.0.0: git-url-parse@13.1.0: version "13.1.0" - resolved "https://registry.npmjs.org/git-url-parse/-/git-url-parse-13.1.0.tgz" + resolved "https://registry.yarnpkg.com/git-url-parse/-/git-url-parse-13.1.0.tgz#07e136b5baa08d59fabdf0e33170de425adf07b4" integrity sha512-5FvPJP/70WkIprlUZ33bm4UAaFdjcLkJLpWft1BeZKqwR0uhhNGoKwlUaPtVb4LxCSQ++erHapRak9kWGj+FCA== dependencies: git-up "^7.0.0" @@ -4770,7 +5088,7 @@ glob@10.3.10, glob@^10.2.2, glob@^10.3.7: glob@7.1.4: version "7.1.4" - resolved "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.4.tgz#aa608a2f6c577ad357e1ae5a5c26d9a8d1969255" integrity sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A== dependencies: fs.realpath "^1.0.0" @@ -4806,7 +5124,7 @@ glob@^7.0.0, glob@^7.1.3, glob@^7.1.4: glob@^8.0.1: version "8.1.0" - resolved "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz" + resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== dependencies: fs.realpath "^1.0.0" @@ -4919,6 +5237,13 @@ has-property-descriptors@^1.0.0: dependencies: get-intrinsic "^1.2.2" +has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== + dependencies: + es-define-property "^1.0.0" + has-proto@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz" @@ -4941,6 +5266,13 @@ hasown@^2.0.0: dependencies: function-bind "^1.1.2" +hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + hosted-git-info@^2.1.4: version "2.8.9" resolved "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz" @@ -4948,7 +5280,7 @@ hosted-git-info@^2.1.4: hosted-git-info@^3.0.6: version "3.0.8" - resolved "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-3.0.8.tgz" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-3.0.8.tgz#6e35d4cc87af2c5f816e4cb9ce350ba87a3f370d" integrity sha512-aXpmwoOhRBrw6X3j0h5RloK4x1OzsxMPyxqIHyNfSe2pypkVTZFpEiRoSipPEPlMrh0HW/XsjkJ5WgnCirpNUw== dependencies: lru-cache "^6.0.0" @@ -4962,7 +5294,7 @@ hosted-git-info@^4.0.0, hosted-git-info@^4.0.1: hosted-git-info@^6.0.0: version "6.1.1" - resolved "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-6.1.1.tgz" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-6.1.1.tgz#629442c7889a69c05de604d52996b74fe6f26d58" integrity sha512-r0EI+HBMcXadMrugk0GCQ+6BQV39PiWAZVfq7oIckeGiN7sjRGyQxPdft3nQekFTCQbYxLBH+/axZMeH8UX6+w== dependencies: lru-cache "^7.5.1" @@ -4990,7 +5322,7 @@ http-errors@2.0.0: http-proxy-agent@^5.0.0: version "5.0.0" - resolved "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43" integrity sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w== dependencies: "@tootallnate/once" "2" @@ -4999,7 +5331,7 @@ http-proxy-agent@^5.0.0: https-proxy-agent@^5.0.0: version "5.0.1" - resolved "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== dependencies: agent-base "6" @@ -5017,7 +5349,7 @@ human-signals@^4.3.0: humanize-ms@^1.2.1: version "1.2.1" - resolved "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz" + resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed" integrity sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ== dependencies: ms "^2.0.0" @@ -5043,15 +5375,15 @@ ieee754@^1.1.13: ignore-walk@^5.0.1: version "5.0.1" - resolved "https://registry.npmjs.org/ignore-walk/-/ignore-walk-5.0.1.tgz" + resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-5.0.1.tgz#5f199e23e1288f518d90358d461387788a154776" integrity sha512-yemi4pMf51WKT7khInJqAvsIGzoqYXblnsz0ql8tM+yi1EKYTY1evX4NAbJrLL/Aanr2HyZeluqU+Oi7MGHokw== dependencies: minimatch "^5.0.1" ignore-walk@^6.0.0: - version "6.0.4" - resolved "https://registry.npmjs.org/ignore-walk/-/ignore-walk-6.0.4.tgz" - integrity sha512-t7sv42WkwFkyKbivUCglsQW5YWMskWtbEf4MNKX5u/CCWHKSPzN4FtBQGsQZgCLbxOzpVlcbWVK5KB3auIOjSw== + version "6.0.5" + resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-6.0.5.tgz#ef8d61eab7da169078723d1f82833b36e200b0dd" + integrity sha512-VuuG0wCnjhnylG1ABXT3dAuIpTNDs/G8jlpmwXY03fXoXy/8ZK8/T+hMzt8L4WnrLCJgdybqgPagnF/f97cg3A== dependencies: minimatch "^9.0.0" @@ -5088,7 +5420,7 @@ indent-string@^4.0.0: infer-owner@^1.0.4: version "1.0.4" - resolved "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz" + resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467" integrity sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A== inflight@^1.0.4: @@ -5116,7 +5448,7 @@ ini@^1.3.2, ini@^1.3.8: init-package-json@5.0.0: version "5.0.0" - resolved "https://registry.npmjs.org/init-package-json/-/init-package-json-5.0.0.tgz" + resolved "https://registry.yarnpkg.com/init-package-json/-/init-package-json-5.0.0.tgz#030cf0ea9c84cfc1b0dc2e898b45d171393e4b40" integrity sha512-kBhlSheBfYmq3e0L1ii+VKe3zBTLL5lDCDWR+f9dLmEGSB3MqLlMlsolubSsyI88Bg6EA+BIMlomAnQ1SwgQBw== dependencies: npm-package-arg "^10.0.0" @@ -5195,10 +5527,13 @@ interpret@^1.0.0: resolved "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz" integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA== -ip@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz" - integrity sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ== +ip-address@^9.0.5: + version "9.0.5" + resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-9.0.5.tgz#117a960819b08780c3bd1f14ef3c1cc1d3f3ea5a" + integrity sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g== + dependencies: + jsbn "1.1.0" + sprintf-js "^1.1.3" ipaddr.js@1.9.1: version "1.9.1" @@ -5224,13 +5559,20 @@ is-ci@3.0.1: dependencies: ci-info "^3.2.0" -is-core-module@^2.13.0, is-core-module@^2.5.0, is-core-module@^2.8.1: +is-core-module@^2.13.0, is-core-module@^2.5.0: version "2.13.1" resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz" integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw== dependencies: hasown "^2.0.0" +is-core-module@^2.8.1: + version "2.15.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.15.1.tgz#a7363a25bee942fefab0de13bf6aa372c82dcc37" + integrity sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ== + dependencies: + hasown "^2.0.2" + is-docker@^2.0.0, is-docker@^2.1.1: version "2.2.1" resolved "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz" @@ -5824,9 +6166,9 @@ jest-worker@^29.7.0: merge-stream "^2.0.0" supports-color "^8.0.0" -jest@^29.5.0: +jest@^29.7.0: version "29.7.0" - resolved "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz" + resolved "https://registry.yarnpkg.com/jest/-/jest-29.7.0.tgz#994676fc24177f088f1c5e3737f5697204ff2613" integrity sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw== dependencies: "@jest/core" "^29.7.0" @@ -5854,6 +6196,11 @@ js-yaml@^3.10.0, js-yaml@^3.13.1: argparse "^1.0.7" esprima "^4.0.0" +jsbn@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-1.1.0.tgz#b01307cb29b618a1ed26ec79e911f803c4da0040" + integrity sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A== + jsesc@^2.5.1: version "2.5.2" resolved "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz" @@ -5952,9 +6299,9 @@ knuth-shuffle-seeded@^1.0.6: dependencies: seed-random "~2.2.0" -lerna@^7.4.1: +lerna@^7.4.2: version "7.4.2" - resolved "https://registry.npmjs.org/lerna/-/lerna-7.4.2.tgz" + resolved "https://registry.yarnpkg.com/lerna/-/lerna-7.4.2.tgz#03497125d7b7c8d463eebfe17a701b16bde2ad09" integrity sha512-gxavfzHfJ4JL30OvMunmlm4Anw7d7Tq6tdVHzUukLdS9nWnxCN/QB21qR+VJYp5tcyXogHKbdUEGh6qmeyzxSA== dependencies: "@lerna/child-process" "7.4.2" @@ -6048,7 +6395,7 @@ levn@^0.4.1: libnpmaccess@7.0.2: version "7.0.2" - resolved "https://registry.npmjs.org/libnpmaccess/-/libnpmaccess-7.0.2.tgz" + resolved "https://registry.yarnpkg.com/libnpmaccess/-/libnpmaccess-7.0.2.tgz#7f056c8c933dd9c8ba771fa6493556b53c5aac52" integrity sha512-vHBVMw1JFMTgEk15zRsJuSAg7QtGGHpUSEfnbcRL1/gTBag9iEfJbyjpDmdJmwMhvpoLoNBtdAUCdGnaP32hhw== dependencies: npm-package-arg "^10.1.0" @@ -6056,7 +6403,7 @@ libnpmaccess@7.0.2: libnpmpublish@7.3.0: version "7.3.0" - resolved "https://registry.npmjs.org/libnpmpublish/-/libnpmpublish-7.3.0.tgz" + resolved "https://registry.yarnpkg.com/libnpmpublish/-/libnpmpublish-7.3.0.tgz#2ceb2b36866d75a6cd7b4aa748808169f4d17e37" integrity sha512-fHUxw5VJhZCNSls0KLNEG0mCD2PN1i14gH5elGOgiVnU3VgTcRahagYP2LKI1m0tFCJ+XrAm0zVYyF5RCbXzcg== dependencies: ci-info "^3.6.1" @@ -6075,7 +6422,7 @@ lines-and-columns@^1.1.6: lines-and-columns@~2.0.3: version "2.0.4" - resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-2.0.4.tgz" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-2.0.4.tgz#d00318855905d2660d8c0822e3f5a4715855fc42" integrity sha512-wM1+Z03eypVAVUCE7QdSqpVIvelbOakn1M0bPDoA4SGWPx3sNDVUiMo3L6To6WWGClB7VyXnhQ4Sn7gxiJbE6A== load-json-file@6.2.0: @@ -6186,7 +6533,7 @@ lru-cache@^6.0.0: lru-cache@^7.4.4, lru-cache@^7.5.1, lru-cache@^7.7.1: version "7.18.3" - resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89" integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA== "lru-cache@^9.1.1 || ^10.0.0": @@ -6228,7 +6575,7 @@ make-error@^1.1.1: make-fetch-happen@^10.0.3: version "10.2.1" - resolved "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz" + resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz#f5e3835c5e9817b617f2770870d9492d28678164" integrity sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w== dependencies: agentkeepalive "^4.2.1" @@ -6250,7 +6597,7 @@ make-fetch-happen@^10.0.3: make-fetch-happen@^11.0.0, make-fetch-happen@^11.0.1, make-fetch-happen@^11.1.1: version "11.1.1" - resolved "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-11.1.1.tgz" + resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-11.1.1.tgz#85ceb98079584a9523d4bf71d32996e7e208549f" integrity sha512-rLWS7GCSTcEujjVBs2YqG7Y4643u8ucvCJeSRqiLYhesrDuzeuFIk37xREzAsfQaqzl8b9rNCE4m6J8tvX4Q8w== dependencies: agentkeepalive "^4.2.1" @@ -6315,10 +6662,10 @@ meow@^8.1.2: type-fest "^0.18.0" yargs-parser "^20.2.3" -merge-descriptors@1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz" - integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== +merge-descriptors@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5" + integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ== merge-stream@^2.0.0: version "2.0.0" @@ -6433,14 +6780,14 @@ minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6: minipass-collect@^1.0.2: version "1.0.2" - resolved "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz" + resolved "https://registry.yarnpkg.com/minipass-collect/-/minipass-collect-1.0.2.tgz#22b813bf745dc6edba2576b940022ad6edc8c617" integrity sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA== dependencies: minipass "^3.0.0" minipass-fetch@^2.0.3: version "2.1.2" - resolved "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-2.1.2.tgz" + resolved "https://registry.yarnpkg.com/minipass-fetch/-/minipass-fetch-2.1.2.tgz#95560b50c472d81a3bc76f20ede80eaed76d8add" integrity sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA== dependencies: minipass "^3.1.6" @@ -6468,9 +6815,9 @@ minipass-flush@^1.0.5: minipass "^3.0.0" minipass-json-stream@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/minipass-json-stream/-/minipass-json-stream-1.0.1.tgz" - integrity sha512-ODqY18UZt/I8k+b7rl2AENgbWE8IDYam+undIJONvigAz8KR5GWblsFTEfQs0WODsjbSXWlm+JHEv8Gr6Tfdbg== + version "1.0.2" + resolved "https://registry.yarnpkg.com/minipass-json-stream/-/minipass-json-stream-1.0.2.tgz#5121616c77a11c406c3ffa77509e0b77bb267ec3" + integrity sha512-myxeeTm57lYs8pH2nxPzmEEg8DGIgW+9mv6D4JZD2pa81I/OBjeU7PtICXV6c9eRGTA5JMDsuIPUZRCyBMYNhg== dependencies: jsonparse "^1.3.1" minipass "^3.0.0" @@ -6556,9 +6903,9 @@ ms@2.1.2: resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@2.1.3, ms@^2.0.0: +ms@2.1.3, ms@^2.0.0, ms@^2.1.3: version "2.1.3" - resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== multer@1.4.4-lts.1: @@ -6634,7 +6981,7 @@ node-abort-controller@^3.0.1: node-addon-api@^3.2.1: version "3.2.1" - resolved "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161" integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A== node-emoji@1.11.0: @@ -6659,13 +7006,13 @@ node-fetch@2.7.0, node-fetch@^2.6.1, node-fetch@^2.6.7: whatwg-url "^5.0.0" node-gyp-build@^4.3.0: - version "4.7.1" - resolved "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.7.1.tgz" - integrity sha512-wTSrZ+8lsRRa3I3H8Xr65dLWSgCvY2l4AOnaeKdPA9TB/WYMPaTcrzf3rXvFoVvjKNVnu0CcWSx54qq9GKRUYg== + version "4.8.2" + resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.2.tgz#4f802b71c1ab2ca16af830e6c1ea7dd1ad9496fa" + integrity sha512-IRUxE4BVsHWXkV/SFOut4qTlagw2aM8T5/vnTsmrHJvVoKueJHRc/JaFND7QDDc61kLYUJ6qlZM3sqTSyx2dTw== node-gyp@^9.0.0: version "9.4.1" - resolved "https://registry.npmjs.org/node-gyp/-/node-gyp-9.4.1.tgz" + resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-9.4.1.tgz#8a1023e0d6766ecb52764cc3a734b36ff275e185" integrity sha512-OQkWKbjQKbGkMf/xqI1jjy3oCTgMKJac58G2+bjZb3fza6gW2YrCSdMQYaoTb70crvE//Gngr4f0AgVHmqHvBQ== dependencies: env-paths "^2.2.0" @@ -6695,9 +7042,14 @@ node-releases@^2.0.14: resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz" integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw== +node-releases@^2.0.18: + version "2.0.18" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.18.tgz#f010e8d35e2fe8d6b2944f03f70213ecedc4ca3f" + integrity sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g== + nopt@^6.0.0: version "6.0.0" - resolved "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-6.0.0.tgz#245801d8ebf409c6df22ab9d95b65e1309cdb16d" integrity sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g== dependencies: abbrev "^1.0.0" @@ -6724,7 +7076,7 @@ normalize-package-data@^3.0.0, normalize-package-data@^3.0.3: normalize-package-data@^5.0.0: version "5.0.0" - resolved "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-5.0.0.tgz" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-5.0.0.tgz#abcb8d7e724c40d88462b84982f7cbf6859b4588" integrity sha512-h9iPVIfrVZ9wVYQnxFgtw1ugSvGEMOlyPWWtm8BMJhnwyEL/FLbYbTY3V3PpjI/BUK67n9PEWDu6eHzu1fB15Q== dependencies: hosted-git-info "^6.0.0" @@ -6739,7 +7091,7 @@ normalize-path@^3.0.0, normalize-path@~3.0.0: npm-bundled@^1.1.2: version "1.1.2" - resolved "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.2.tgz" + resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.1.2.tgz#944c78789bd739035b70baa2ca5cc32b8d860bc1" integrity sha512-x5DHup0SuyQcmL3s7Rx/YQ8sbw/Hzg0rj48eN0dV7hf5cmQq5PXIeioroH3raV1QC1yh3uTYuMThvEQF3iKgGQ== dependencies: npm-normalize-package-bin "^1.0.1" @@ -6760,7 +7112,7 @@ npm-install-checks@^6.0.0: npm-normalize-package-bin@^1.0.1: version "1.0.1" - resolved "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz" + resolved "https://registry.yarnpkg.com/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz#6e79a41f23fd235c0623218228da7d9c23b8f6e2" integrity sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA== npm-normalize-package-bin@^3.0.0: @@ -6770,7 +7122,7 @@ npm-normalize-package-bin@^3.0.0: npm-package-arg@8.1.1: version "8.1.1" - resolved "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-8.1.1.tgz" + resolved "https://registry.yarnpkg.com/npm-package-arg/-/npm-package-arg-8.1.1.tgz#00ebf16ac395c63318e67ce66780a06db6df1b04" integrity sha512-CsP95FhWQDwNqiYS+Q0mZ7FAEDytDZAkNxQqea6IaAFJTAY9Lhhqyl0irU/6PMc7BGfUmnsbHcqxJD7XuVM/rg== dependencies: hosted-git-info "^3.0.6" @@ -6779,7 +7131,7 @@ npm-package-arg@8.1.1: npm-package-arg@^10.0.0, npm-package-arg@^10.1.0: version "10.1.0" - resolved "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-10.1.0.tgz" + resolved "https://registry.yarnpkg.com/npm-package-arg/-/npm-package-arg-10.1.0.tgz#827d1260a683806685d17193073cc152d3c7e9b1" integrity sha512-uFyyCEmgBfZTtrKk/5xDfHp6+MdrqGotX/VoOyEEl3mBwiEE5FlBaePanazJSVMPT7vKepcjYBY2ztg9A3yPIA== dependencies: hosted-git-info "^6.0.0" @@ -6789,7 +7141,7 @@ npm-package-arg@^10.0.0, npm-package-arg@^10.1.0: npm-packlist@5.1.1: version "5.1.1" - resolved "https://registry.npmjs.org/npm-packlist/-/npm-packlist-5.1.1.tgz" + resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-5.1.1.tgz#79bcaf22a26b6c30aa4dd66b976d69cc286800e0" integrity sha512-UfpSvQ5YKwctmodvPPkK6Fwk603aoVsf8AEbmVKAEECrfvL8SSe1A2YIwrJ6xmTHAITKPwwZsWo7WwEbNk0kxw== dependencies: glob "^8.0.1" @@ -6799,14 +7151,14 @@ npm-packlist@5.1.1: npm-packlist@^7.0.0: version "7.0.4" - resolved "https://registry.npmjs.org/npm-packlist/-/npm-packlist-7.0.4.tgz" + resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-7.0.4.tgz#033bf74110eb74daf2910dc75144411999c5ff32" integrity sha512-d6RGEuRrNS5/N84iglPivjaJPxhDbZmlbTwTDX2IbcRHG5bZCdtysYMhwiPvcF4GisXHGn7xsxv+GQ7T/02M5Q== dependencies: ignore-walk "^6.0.0" npm-pick-manifest@^8.0.0: version "8.0.2" - resolved "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-8.0.2.tgz" + resolved "https://registry.yarnpkg.com/npm-pick-manifest/-/npm-pick-manifest-8.0.2.tgz#2159778d9c7360420c925c1a2287b5a884c713aa" integrity sha512-1dKY+86/AIiq1tkKVD3l0WI+Gd3vkknVGAggsFeBkTvbhMQ1OND/LKkYv4JtXPKUJ8bOTCyLiqEg2P6QNdK+Gg== dependencies: npm-install-checks "^6.0.0" @@ -6816,7 +7168,7 @@ npm-pick-manifest@^8.0.0: npm-registry-fetch@^14.0.0, npm-registry-fetch@^14.0.3, npm-registry-fetch@^14.0.5: version "14.0.5" - resolved "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-14.0.5.tgz" + resolved "https://registry.yarnpkg.com/npm-registry-fetch/-/npm-registry-fetch-14.0.5.tgz#fe7169957ba4986a4853a650278ee02e568d115d" integrity sha512-kIDMIo4aBm6xg7jOttupWZamsZRkAqMqwqqbVXnUqstY5+tapvv6bkH/qMR76jdgV+YljEUCyWx3hRYMrJiAgA== dependencies: make-fetch-happen "^11.0.0" @@ -6843,7 +7195,7 @@ npm-run-path@^5.1.0: npmlog@^6.0.0, npmlog@^6.0.2: version "6.0.2" - resolved "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-6.0.2.tgz#c8166017a42f2dea92d6453168dd865186a70830" integrity sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg== dependencies: are-we-there-yet "^3.0.0" @@ -6853,7 +7205,7 @@ npmlog@^6.0.0, npmlog@^6.0.2: nx@16.10.0, "nx@>=16.5.1 < 17": version "16.10.0" - resolved "https://registry.npmjs.org/nx/-/nx-16.10.0.tgz" + resolved "https://registry.yarnpkg.com/nx/-/nx-16.10.0.tgz#b070461f7de0a3d7988bd78558ea84cda3543ace" integrity sha512-gZl4iCC0Hx0Qe1VWmO4Bkeul2nttuXdPpfnlcDKSACGu3ZIo+uySqwOF8yBAxSTIf8xe2JRhgzJN1aFkuezEBg== dependencies: "@nrwl/tao" "16.10.0" @@ -6909,10 +7261,10 @@ object-assign@^4, object-assign@^4.0.1, object-assign@^4.1.1: resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== -object-inspect@^1.9.0: - version "1.13.1" - resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz" - integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ== +object-inspect@^1.13.1: + version "1.13.2" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.2.tgz#dea0088467fb991e67af4058147a24824a3043ff" + integrity sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g== on-finished@2.4.1: version "2.4.1" @@ -7101,7 +7453,7 @@ package-json-from-dist@^1.0.0: pacote@^15.2.0: version "15.2.0" - resolved "https://registry.npmjs.org/pacote/-/pacote-15.2.0.tgz" + resolved "https://registry.yarnpkg.com/pacote/-/pacote-15.2.0.tgz#0f0dfcc3e60c7b39121b2ac612bf8596e95344d3" integrity sha512-rJVZeIwHTUta23sIZgEIM62WYwbmGbThdbnkt81ravBplQv+HjyroqnLRNH2+sLJHcGZmLRmhPwACqhfTcOmnA== dependencies: "@npmcli/git" "^4.0.0" @@ -7220,15 +7572,15 @@ path-scurry@^1.11.1: lru-cache "^10.2.0" minipass "^5.0.0 || ^6.0.2 || ^7.0.0" -path-to-regexp@0.1.7: - version "0.1.7" - resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz" - integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== +path-to-regexp@0.1.10: + version "0.1.10" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.10.tgz#67e9108c5c0551b9e5326064387de4763c4d5f8b" + integrity sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w== -path-to-regexp@3.2.0: - version "3.2.0" - resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.2.0.tgz" - integrity sha512-jczvQbCUS7XmS7o+y1aEO9OBVFeZBQ1MDSEqmO7xSoPgOPoowY/SxLpZ6Vh97/8qHZOteiCKb7gkG9gA2ZUxJA== +path-to-regexp@3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-3.3.0.tgz#f7f31d32e8518c2660862b644414b6d5c63a611b" + integrity sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw== path-type@^3.0.0: version "3.0.0" @@ -7247,6 +7599,11 @@ picocolors@^1.0.0: resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz" integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== +picocolors@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.0.tgz#5358b76a78cde483ba5cef6a9dc9671440b27d59" + integrity sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw== + picomatch@3.0.1: version "3.0.1" resolved "https://registry.npmjs.org/picomatch/-/picomatch-3.0.1.tgz" @@ -7322,7 +7679,7 @@ pretty-format@^29.0.0, pretty-format@^29.7.0: proc-log@^3.0.0: version "3.0.0" - resolved "https://registry.npmjs.org/proc-log/-/proc-log-3.0.0.tgz" + resolved "https://registry.yarnpkg.com/proc-log/-/proc-log-3.0.0.tgz#fb05ef83ccd64fd7b20bbe9c8c1070fc08338dd8" integrity sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A== process-nextick-args@~2.0.0: @@ -7396,12 +7753,12 @@ pure-rand@^6.0.0: resolved "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.4.tgz" integrity sha512-LA0Y9kxMYv47GIPJy6MI84fqTd2HmYZI83W/kM/SkKfDlajnZYfmXFTxkbY+xSBPkLJxltMa9hIkmdc29eguMA== -qs@6.11.0: - version "6.11.0" - resolved "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz" - integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== +qs@6.13.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906" + integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg== dependencies: - side-channel "^1.0.4" + side-channel "^1.0.6" queue-microtask@^1.2.2: version "1.2.3" @@ -7447,7 +7804,7 @@ read-cmd-shim@4.0.0: read-package-json-fast@^3.0.0: version "3.0.2" - resolved "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-3.0.2.tgz" + resolved "https://registry.yarnpkg.com/read-package-json-fast/-/read-package-json-fast-3.0.2.tgz#394908a9725dc7a5f14e70c8e7556dff1d2b1049" integrity sha512-0J+Msgym3vrLOUB3hzQCuZHII0xkNGCtz/HJH9xZshwv9DbDwkw1KaE3gx/e2J5rpEY5rtOy6cyhKOPrkP7FZw== dependencies: json-parse-even-better-errors "^3.0.0" @@ -7455,7 +7812,7 @@ read-package-json-fast@^3.0.0: read-package-json@6.0.4, read-package-json@^6.0.0: version "6.0.4" - resolved "https://registry.npmjs.org/read-package-json/-/read-package-json-6.0.4.tgz" + resolved "https://registry.yarnpkg.com/read-package-json/-/read-package-json-6.0.4.tgz#90318824ec456c287437ea79595f4c2854708836" integrity sha512-AEtWXYfopBj2z5N5PbkAOeNHRPUg5q+Nen7QLxV8M2zJq1ym6/lCz3fYNTCXe19puu2d06jfHhrP7v/S2PtMMw== dependencies: glob "^10.2.2" @@ -7791,10 +8148,10 @@ semver@^7.0.0, semver@^7.1.1, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semve dependencies: lru-cache "^6.0.0" -send@0.18.0: - version "0.18.0" - resolved "https://registry.npmjs.org/send/-/send-0.18.0.tgz" - integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== +send@0.19.0: + version "0.19.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.19.0.tgz#bbc5a388c8ea6c048967049dbeac0e4a3f09d7f8" + integrity sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw== dependencies: debug "2.6.9" depd "2.0.0" @@ -7817,15 +8174,15 @@ serialize-javascript@^6.0.1: dependencies: randombytes "^2.1.0" -serve-static@1.15.0: - version "1.15.0" - resolved "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz" - integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== +serve-static@1.16.2: + version "1.16.2" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.16.2.tgz#b6a5343da47f6bdd2673848bf45754941e803296" + integrity sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw== dependencies: - encodeurl "~1.0.2" + encodeurl "~2.0.0" escape-html "~1.0.3" parseurl "~1.3.3" - send "0.18.0" + send "0.19.0" set-blocking@^2.0.0: version "2.0.0" @@ -7842,6 +8199,18 @@ set-function-length@^1.1.1: gopd "^1.0.1" has-property-descriptors "^1.0.0" +set-function-length@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + setprototypeof@1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz" @@ -7875,14 +8244,15 @@ shelljs@0.8.5: interpret "^1.0.0" rechoir "^0.6.2" -side-channel@^1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz" - integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== +side-channel@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" + integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== dependencies: - call-bind "^1.0.0" - get-intrinsic "^1.0.2" - object-inspect "^1.9.0" + call-bind "^1.0.7" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + object-inspect "^1.13.1" signal-exit@3.0.7, signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: version "3.0.7" @@ -7896,7 +8266,7 @@ signal-exit@^4.0.1: sigstore@^1.3.0, sigstore@^1.4.0: version "1.9.0" - resolved "https://registry.npmjs.org/sigstore/-/sigstore-1.9.0.tgz" + resolved "https://registry.yarnpkg.com/sigstore/-/sigstore-1.9.0.tgz#1e7ad8933aa99b75c6898ddd0eeebc3eb0d59875" integrity sha512-0Zjz0oe37d08VeOtBIuB6cRriqXse2e8w+7yIy2XSXjshRKxbc2KkhXjL229jXSxEm7UbcjS76wcJDGQddVI9A== dependencies: "@sigstore/bundle" "^1.1.0" @@ -7922,7 +8292,7 @@ smart-buffer@^4.2.0: socks-proxy-agent@^7.0.0: version "7.0.0" - resolved "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz" + resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz#dc069ecf34436621acb41e3efa66ca1b5fed15b6" integrity sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww== dependencies: agent-base "^6.0.2" @@ -7930,11 +8300,11 @@ socks-proxy-agent@^7.0.0: socks "^2.6.2" socks@^2.6.2: - version "2.7.1" - resolved "https://registry.npmjs.org/socks/-/socks-2.7.1.tgz" - integrity sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ== + version "2.8.3" + resolved "https://registry.yarnpkg.com/socks/-/socks-2.8.3.tgz#1ebd0f09c52ba95a09750afe3f3f9f724a800cb5" + integrity sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw== dependencies: - ip "^2.0.0" + ip-address "^9.0.5" smart-buffer "^4.2.0" sort-keys@^2.0.0: @@ -8010,21 +8380,33 @@ split@^1.0.1: dependencies: through "2" +sprintf-js@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.3.tgz#4914b903a2f8b685d17fdf78a70e917e872e444a" + integrity sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA== + sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz" integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== -ssri@^10.0.0, ssri@^10.0.1: +ssri@^10.0.0: version "10.0.5" resolved "https://registry.npmjs.org/ssri/-/ssri-10.0.5.tgz" integrity sha512-bSf16tAFkGeRlUNDjXu8FzaMQt6g2HZJrun7mtMbIPOddxt3GLMSz5VWUWcqTJUPfLEaDIepGxv+bYQW49596A== dependencies: minipass "^7.0.3" +ssri@^10.0.1: + version "10.0.6" + resolved "https://registry.yarnpkg.com/ssri/-/ssri-10.0.6.tgz#a8aade2de60ba2bce8688e3fa349bad05c7dc1e5" + integrity sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ== + dependencies: + minipass "^7.0.3" + ssri@^9.0.0, ssri@^9.0.1: version "9.0.1" - resolved "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz" + resolved "https://registry.yarnpkg.com/ssri/-/ssri-9.0.1.tgz#544d4c357a8d7b71a19700074b6883fcb4eae057" integrity sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q== dependencies: minipass "^3.1.1" @@ -8064,7 +8446,16 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -8096,7 +8487,14 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -8216,7 +8614,7 @@ tar-stream@~2.2.0: tar@6.1.11: version "6.1.11" - resolved "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.11.tgz#6760a38f003afa1b2ffd0ffe9e9abbd0eab3d621" integrity sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA== dependencies: chownr "^2.0.0" @@ -8226,7 +8624,7 @@ tar@6.1.11: mkdirp "^1.0.3" yallist "^4.0.0" -tar@^6.1.11, tar@^6.1.2: +tar@^6.1.11: version "6.2.0" resolved "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz" integrity sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ== @@ -8238,6 +8636,18 @@ tar@^6.1.11, tar@^6.1.2: mkdirp "^1.0.3" yallist "^4.0.0" +tar@^6.1.2: + version "6.2.1" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.1.tgz#717549c541bc3c2af15751bea94b1dd068d4b03a" + integrity sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A== + dependencies: + chownr "^2.0.0" + fs-minipass "^2.0.0" + minipass "^5.0.0" + minizlib "^2.1.1" + mkdirp "^1.0.3" + yallist "^4.0.0" + temp-dir@1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/temp-dir/-/temp-dir-1.0.0.tgz" @@ -8423,10 +8833,10 @@ tsconfig-paths@4.2.0, tsconfig-paths@^4.1.2: minimist "^1.2.6" strip-bom "^3.0.0" -tslib@2.6.2, tslib@^2.1.0, tslib@^2.3.0, tslib@^2.3.1, tslib@^2.4.0, tslib@^2.6.0, tslib@^2.6.2: - version "2.6.2" - resolved "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz" - integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== +tslib@2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.7.0.tgz#d9b40c5c40ab59e8738f297df3087bf1a2690c01" + integrity sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA== tslib@^1.8.1: version "1.14.1" @@ -8438,6 +8848,11 @@ tslib@^2.0.3: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0" integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ== +tslib@^2.1.0, tslib@^2.3.0, tslib@^2.3.1, tslib@^2.4.0, tslib@^2.6.0, tslib@^2.6.2: + version "2.6.2" + resolved "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz" + integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== + tsutils@^3.21.0: version "3.21.0" resolved "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz" @@ -8447,7 +8862,7 @@ tsutils@^3.21.0: tuf-js@^1.1.7: version "1.1.7" - resolved "https://registry.npmjs.org/tuf-js/-/tuf-js-1.1.7.tgz" + resolved "https://registry.yarnpkg.com/tuf-js/-/tuf-js-1.1.7.tgz#21b7ae92a9373015be77dfe0cb282a80ec3bbe43" integrity sha512-i3P9Kgw3ytjELUfpuKVDNBJvk4u5bXL6gskv572mcevPbSKCV3zt3djhmlEQ65yERjIbOSncy7U4cQJaB1CBCg== dependencies: "@tufjs/models" "1.0.4" @@ -8566,7 +8981,7 @@ unicode-property-aliases-ecmascript@^2.0.0: unique-filename@^2.0.0: version "2.0.1" - resolved "https://registry.npmjs.org/unique-filename/-/unique-filename-2.0.1.tgz" + resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-2.0.1.tgz#e785f8675a9a7589e0ac77e0b5c34d2eaeac6da2" integrity sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A== dependencies: unique-slug "^3.0.0" @@ -8580,7 +8995,7 @@ unique-filename@^3.0.0: unique-slug@^3.0.0: version "3.0.0" - resolved "https://registry.npmjs.org/unique-slug/-/unique-slug-3.0.0.tgz" + resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-3.0.0.tgz#6d347cf57c8a7a7a6044aabd0e2d74e4d76dc7c9" integrity sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w== dependencies: imurmurhash "^0.1.4" @@ -8625,6 +9040,14 @@ update-browserslist-db@^1.0.13: escalade "^3.1.1" picocolors "^1.0.0" +update-browserslist-db@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz#80846fba1d79e82547fb661f8d141e0945755fe5" + integrity sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A== + dependencies: + escalade "^3.2.0" + picocolors "^1.1.0" + upper-case-first@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/upper-case-first/-/upper-case-first-2.0.2.tgz#992c3273f882abd19d1e02894cc147117f844324" @@ -8666,7 +9089,7 @@ v8-compile-cache-lib@^3.0.1: v8-compile-cache@2.3.0: version "2.3.0" - resolved "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz" + resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== v8-to-istanbul@^9.0.1: @@ -8695,7 +9118,7 @@ validate-npm-package-name@5.0.0, validate-npm-package-name@^5.0.0: validate-npm-package-name@^3.0.0: version "3.0.0" - resolved "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-3.0.0.tgz" + resolved "https://registry.yarnpkg.com/validate-npm-package-name/-/validate-npm-package-name-3.0.0.tgz#5fa912d81eb7d0c74afc140de7317f0ca7df437e" integrity sha512-M6w37eVCMMouJ9V/sdPGnC5H4uDr73/+xdq0FBLO3TFFX1+7wiUY6Es328NN+y43tmY+doUdN9g9J21vqB7iLw== dependencies: builtins "^1.0.3" @@ -8789,14 +9212,14 @@ which@^2.0.1, which@^2.0.2: which@^3.0.0: version "3.0.1" - resolved "https://registry.npmjs.org/which/-/which-3.0.1.tgz" + resolved "https://registry.yarnpkg.com/which/-/which-3.0.1.tgz#89f1cd0c23f629a8105ffe69b8172791c87b4be1" integrity sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg== dependencies: isexe "^2.0.0" wide-align@^1.1.5: version "1.1.5" - resolved "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.5.tgz#df1d4c206854369ecf3c9a4898f1b23fbd9d15d3" integrity sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg== dependencies: string-width "^1.0.2 || 2 || 3 || 4" @@ -8806,8 +9229,7 @@ wordwrap@^1.0.0: resolved "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz" integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: - name wrap-ansi-cjs +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -8825,6 +9247,15 @@ wrap-ansi@^6.0.1, wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz" @@ -8841,7 +9272,7 @@ wrappy@1: write-file-atomic@5.0.1: version "5.0.1" - resolved "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-5.0.1.tgz#68df4717c55c6fa4281a7860b4c2ba0a6d2b11e7" integrity sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw== dependencies: imurmurhash "^0.1.4" @@ -8917,7 +9348,7 @@ yaml@^2.2.2: yargs-parser@20.2.4: version "20.2.4" - resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54" integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA== yargs-parser@21.1.1, yargs-parser@^21.1.1: From 42eae5e0d659e13aaa674a567feff3992fae24a8 Mon Sep 17 00:00:00 2001 From: Antoine SEIN <142824551+asein-sinch@users.noreply.github.com> Date: Tue, 1 Oct 2024 08:45:47 +0200 Subject: [PATCH 46/54] Fix audit report --- .github/scripts/validate-audit-report.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/scripts/validate-audit-report.sh b/.github/scripts/validate-audit-report.sh index 2c9622ad..b961f5e0 100755 --- a/.github/scripts/validate-audit-report.sh +++ b/.github/scripts/validate-audit-report.sh @@ -7,7 +7,7 @@ awk 'NR > 1 {print ","} {print}' audit-report.txt >> audit-report.json echo ']}' >> audit-report.json # Filter JSON array to remove jest and lerna's transitive dependencies as these dependencies are not used at runtime -jq '.vulnerabilities |= map(select(.data.resolution.path | type == "string" and (startswith("lerna") or startswith("jest") or startswith("@types/jest")) | not))' audit-report.json > audit-report-filtered.json +jq '.vulnerabilities |= map(select(.data.resolution.path | type == "string" and (startswith("lerna") or startswith("jest") or startswith("@types/jest") or startswith("babel-jest")) | not))' audit-report.json > audit-report-filtered.json # Fail the build if filtered JSON array contains audit advisories if [ "$(jq '.vulnerabilities[] | select(.type == "auditAdvisory") | .type' audit-report-filtered.json | wc -l)" -gt 0 ]; then From a7367fefdca4552395e1441224d9b1f07b71d0b9 Mon Sep 17 00:00:00 2001 From: Antoine SEIN <142824551+asein-sinch@users.noreply.github.com> Date: Tue, 1 Oct 2024 14:54:47 +0200 Subject: [PATCH 47/54] DEVEXP-585: Backward compatible fixes (#139) --- .../services/verification-event.service.ts | 2 +- packages/conversation/CHANGELOG.md | 12 ++++ .../models/v1/conversation/conversation.ts | 3 +- packages/elastic-sip-trunking/CHANGELOG.md | 13 ++++ packages/fax/CHANGELOG.md | 10 ++- packages/numbers/CHANGELOG.md | 18 ++++++ .../numbers/src/rest/v1/numbers-service.ts | 4 +- packages/sdk-client/CHANGELOG.md | 6 ++ packages/sdk-core/CHANGELOG.md | 9 +++ packages/sms/CHANGELOG.md | 12 ++++ packages/sms/src/models/v1/group/group.ts | 12 ++++ packages/sms/src/models/v1/group/index.ts | 2 +- .../delivery-reports-request-data.ts | 18 +++++- .../delivery-reports/delivery-reports-api.ts | 11 +++- packages/verification/CHANGELOG.md | 61 +++++++++++++++++++ packages/verification/src/models/v1/helper.ts | 9 +-- .../src/models/v1/identity/identity.ts | 7 ++- .../src/models/v1/identity/index.ts | 2 +- .../callout-request-event-response/index.ts | 5 -- .../flashcall-request-event-response.ts | 5 +- .../flashcall-request-event-response/index.ts | 6 +- .../src/models/v1/mod-callbacks/index.ts | 2 +- .../phonecall-request-event-response/index.ts | 7 +++ .../phonecall-request-event-response.ts} | 14 +++-- .../sms-request-event-response/index.ts | 7 ++- .../sms-request-event-response.ts | 8 ++- .../verification-request-event-response.ts | 2 +- .../verification-result-event-response.ts | 2 +- .../phonecall-verification-report-response.ts | 2 +- .../phonecall-verification-status-response.ts | 2 +- .../verification-status-request-data.ts | 4 +- .../verifications-request-data.ts | 11 ++-- .../sms-verification-report-response/index.ts | 2 +- .../sms-verification-report-response.ts | 3 + .../sms-verification-status-response/index.ts | 2 +- .../sms-verification-status-response.ts | 3 + .../start-data-verification-response.ts | 2 +- .../start-phonecall-verification-response.ts | 2 +- .../start-verification-request.ts | 10 +-- .../verification-price-sms.ts | 3 + .../verification-report-request.ts | 4 +- .../rest/v1/callbacks/callbacks-webhook.ts | 2 +- .../v1/verifications/verifications-api.ts | 28 ++++----- packages/voice/CHANGELOG.md | 12 ++++ .../rest/v1/callbacks/callbacks-webhook.ts | 2 +- 45 files changed, 296 insertions(+), 67 deletions(-) delete mode 100644 packages/verification/src/models/v1/mod-callbacks/callout-request-event-response/index.ts create mode 100644 packages/verification/src/models/v1/mod-callbacks/phonecall-request-event-response/index.ts rename packages/verification/src/models/v1/mod-callbacks/{callout-request-event-response/callout-request-event-response.ts => phonecall-request-event-response/phonecall-request-event-response.ts} (64%) diff --git a/examples/webhooks/src/services/verification-event.service.ts b/examples/webhooks/src/services/verification-event.service.ts index 051978a7..73697f73 100644 --- a/examples/webhooks/src/services/verification-event.service.ts +++ b/examples/webhooks/src/services/verification-event.service.ts @@ -29,7 +29,7 @@ export class VerificationEventService { res.status(200).json(smsRequestEventResponse); break; case 'callout': - const calloutRequestEventResponse: Verification.CalloutRequestEventResponse = { + const calloutRequestEventResponse: Verification.PhoneCallRequestEventResponse = { action: 'allow', callout: { code: '123456', diff --git a/packages/conversation/CHANGELOG.md b/packages/conversation/CHANGELOG.md index 4b098f80..68ccf2df 100644 --- a/packages/conversation/CHANGELOG.md +++ b/packages/conversation/CHANGELOG.md @@ -1,3 +1,15 @@ +## Version 1.2.0 +- [Tech] Update dependency `@sinch/sdk-client` to `1.2.0`. +- [Bugfix] Fix issue with pagination to iterate over multiple pages. +- [Bugfix] `ListConversationsRequestData`: the property `only_active` becomes optional. +- [Bugfix] Template V2: add an optional `id` property to the `V2Template` interface. +- [Bugfix] `conversations.listRecent()`: Add a page_size value by default. Without it the API returns an empty list. +- [Bugfix][Breaking] `InjectConversationEvent` interface: only `AppEvent` is allowed (`ContactEvent` and `ContactMessageEvent` are no longer allowed). +- [Bugfix][Breaking] + - Template V2: For "create" and "update" operations, the request bodies interface no longer accept read-only properties. + - Webhooks: For "create" and "update" operations, the request bodies interface no longer accept read-only properties. +- [E2E] Add Cucumber steps implementation. + ## Version 1.1.0 - [Tech] Update dependency `@sinch/sdk-client` to `1.1.0` diff --git a/packages/conversation/src/models/v1/conversation/conversation.ts b/packages/conversation/src/models/v1/conversation/conversation.ts index 5a8fa064..49e607c8 100644 --- a/packages/conversation/src/models/v1/conversation/conversation.ts +++ b/packages/conversation/src/models/v1/conversation/conversation.ts @@ -20,8 +20,7 @@ export interface Conversation { /** * Arbitrary data set by the Conversation API clients. Up to 1024 characters long. * NOTE: This field has been deprecated due to changes in the system architecture or functionality. - * It is no longer actively maintained and may be removed in future versions. Please avoid relying on this field in new code. - * @deprecated + * @deprecated It is no longer actively maintained and may be removed in future versions. Please avoid relying on this field in new code. */ metadata?: string; /** Arbitrary data set by the Conversation API clients and/or provided in the `conversation_metadata` field of a SendMessageRequest. A valid JSON object. */ diff --git a/packages/elastic-sip-trunking/CHANGELOG.md b/packages/elastic-sip-trunking/CHANGELOG.md index 729aa932..da1dad71 100644 --- a/packages/elastic-sip-trunking/CHANGELOG.md +++ b/packages/elastic-sip-trunking/CHANGELOG.md @@ -1,3 +1,16 @@ +## Version 1.2.0 +- [Tech] Update dependency `@sinch/sdk-client` to `1.2.0`. +- [Feature] Add the method `accessControlList.get()`. +- Calls History: + - [Bugfix][Breaking] The `DirectionEnum` values are in lower case. E.g: INBOUND -> inbound. + - [Bugfix][Breaking] The price `amount` is now a `number` instead of a `string`. + - [Feature] Support date range filter for listing calls. +- [Bugfix][Breaking] + - ACLs: For "create", "update" and "addIPRange" operations, the request bodies interface no longer accept read-only properties. + - SIP endpoints: For "create" and "update" operations, the request bodies interface no longer accept read-only properties. + - SIP Trunks: For "create" and "update" operations, the request bodies interface no longer accept read-only properties. +- [E2E] Add Cucumber steps implementation. + # Version 1.1.0 - Initial version. Support for: - SIP Trunks diff --git a/packages/fax/CHANGELOG.md b/packages/fax/CHANGELOG.md index d8c2044e..61c6c108 100644 --- a/packages/fax/CHANGELOG.md +++ b/packages/fax/CHANGELOG.md @@ -1,3 +1,11 @@ +## Version 1.2.0 +- [Tech] Update dependency `@sinch/sdk-client` to `1.2.0`. +- [Feature] Support date range filter for listing faxes. +- [Feature] Add `emails.listForNumber()` method (identical to `services.listEmailsForNumber()`). +- [Bugfix] Fix `faxes.send()` to send one or several faxes over JSON or FormData. Add examples in the [simple-examples](https://github.com/sinch/sinch-sdk-node/tree/main/examples/simple-examples/src/fax/faxes) package. +- [Feature][Breaking] Update interfaces according to OAS updates. +- [E2E] Add Cucumber steps implementation. + ## Version 1.1.0 - [Tech] Update dependency `@sinch/sdk-client` to `1.1.0` @@ -6,7 +14,7 @@ ## Version 0.0.5 - [Tech] Update dependency `@sinch/sdk-client` to `0.0.5` -- [Tech][Breaking] Export all model interfaces under the namespace `Fax` +- [Tech][Breaking TS] Export all model interfaces under the namespace `Fax` - [Bugfix] Fix pagination - [Bugfix] Fix content format sent when using `multipart/form-data`: boolean is not allowed - [Feature] Support hostname override diff --git a/packages/numbers/CHANGELOG.md b/packages/numbers/CHANGELOG.md index d86af761..a2b34e63 100644 --- a/packages/numbers/CHANGELOG.md +++ b/packages/numbers/CHANGELOG.md @@ -1,3 +1,21 @@ +## Version 1.2.0 +- [Tech] Update dependency `@sinch/sdk-client` to `1.2.0`. +- [Deprecation notice] `availableNumber` and `activeNumber` subdomain are deprecated and all methods are now defined on the upper numbers service. + All the methods names are the same except `availableNumber.list()` -> `searchForAvailableNumbers()` + +| Deprecated | New | +|------------------------------------------------------|----------------------------------------------| +| `numbersService.availableNumber.checkAvailability()` | `numbersService.checkAvailability()` | +| `numbersService.availableNumber.list()` | `numbersService.searchForAvailableNumbers()` | +| `numbersService.availableNumber.rent()` | `numbersService.rent()` | +| `numbersService.availableNumber.rentAny()` | `numbersService.rentAny()` | +| `numbersService.activeNumber.get()` | `numbersService.get()` | +| `numbersService.activeNumber.list()` | `numbersService.list()` | +| `numbersService.activeNumber.update()` | `numbersService.update()` | +| `numbersService.activeNumber.release()` | `numbersService.release()` | + +- [E2E] Add Cucumber steps implementation. + ## Version 1.1.0 - [Tech] Update dependency `@sinch/sdk-client` to `1.1.0` - [Feature] Update voiceConfiguration object to support Fax and EST configuration properties diff --git a/packages/numbers/src/rest/v1/numbers-service.ts b/packages/numbers/src/rest/v1/numbers-service.ts index 947b7ee9..d6a1aa6e 100644 --- a/packages/numbers/src/rest/v1/numbers-service.ts +++ b/packages/numbers/src/rest/v1/numbers-service.ts @@ -27,9 +27,9 @@ import { export class NumbersService { public readonly availableRegions: AvailableRegionsApi; public readonly callbacks: CallbacksApi; - /** @deprecated use the methods exposed at the Numbers Service level instead */ + /** @deprecated Use the methods exposed at the Numbers Service level instead */ public readonly availableNumber: AvailableNumberApi; - /** @deprecated use the methods exposed at the Numbers Service level instead */ + /** @deprecated Use the methods exposed at the Numbers Service level instead */ public readonly activeNumber: ActiveNumberApi; /** diff --git a/packages/sdk-client/CHANGELOG.md b/packages/sdk-client/CHANGELOG.md index 2e150b4d..e4a116e0 100644 --- a/packages/sdk-client/CHANGELOG.md +++ b/packages/sdk-client/CHANGELOG.md @@ -1,3 +1,9 @@ +## Version 1.2.0 +- [Feature] Support a new mode of pagination to support the Conversation API. +- [Feature] Remove the `TimezoneResponse` plugin and autocorrect the timezones when reviving the dates in the API responses. +- [Feature] Add utility methods to support the dateRange filters for EST and Fax APIs. +- [Bugfix] Handle HTTP status 202 in the `ExceptionResponse` plugin. + ## Version 1.1.0 - [Feature] Add new pagination type support specific to EST diff --git a/packages/sdk-core/CHANGELOG.md b/packages/sdk-core/CHANGELOG.md index c1729eb8..1abbd1cd 100644 --- a/packages/sdk-core/CHANGELOG.md +++ b/packages/sdk-core/CHANGELOG.md @@ -1,3 +1,12 @@ +## Version 1.2.0 +- Update dependency `@sinch/numbers` to version `1.2.0` +- Update dependency `@sinch/sms` to version `1.2.0` +- Update dependency `@sinch/verification` to version `1.2.0` +- Update dependency `@sinch/voice` to version `1.2.0` +- Update dependency `@sinch/conversation` to version `1.2.0` +- Update dependency `@sinch/fax` to version `1.2.0` +- Update dependency `@sinch/elastic-sip-trunking` to version `1.2.0` + ## Version 1.1.0 - Update dependency `@sinch/numbers` to version `1.1.0` - Update dependency `@sinch/sms` to version `1.1.0` diff --git a/packages/sms/CHANGELOG.md b/packages/sms/CHANGELOG.md index cd2b923c..64912161 100644 --- a/packages/sms/CHANGELOG.md +++ b/packages/sms/CHANGELOG.md @@ -1,3 +1,15 @@ +## Version 1.2.0 +- [Tech] Update dependency `@sinch/sdk-client` to `1.2.0`. +- [Feature] Align "Dry Run" response interface with OAS update: `message_part?: string` becomes `number_of_parts?: number`. +- [Feature] In the interface `MessageDeliveryStatus`, the property `recipients` becomes optional. +- [Feature] In the interface `GetDeliveryReportByBatchIdRequestData`, the property `code` can also be a `number` or a `number[]` on top of a `string`. +- [Feature] In the interface `ListInboundMessagesRequestData`, the property `to` can also be a `string[]` on top of a `string`. +- [Bugfix] Remove default values set by the SDK when forging the API request. +- [Bugfix] In the interface `UpdateGroupRequest`, the property `name` can also be set to null to remove an existing name set. +- [Deprecation Notice] All variations of a group response (`GroupResponse`, `CreateGroupResponse`, `ReplaceGroupResponse` and `UpdateGroupResponse`) are deprecated and replaced by the unique interface `Group`. +- [Deprecation Notice] In the interface `GetDeliveryReportByPhoneNumberRequestData`, the request parameter `recipient_msisdn` is deprecated and should be replaced by `phone_number`. +- [E2E] Add Cucumber steps implementation. + ## Version 1.1.0 - [Tech] Update dependency `@sinch/sdk-client` to `1.1.0` diff --git a/packages/sms/src/models/v1/group/group.ts b/packages/sms/src/models/v1/group/group.ts index a6a4a41c..2a642e57 100644 --- a/packages/sms/src/models/v1/group/group.ts +++ b/packages/sms/src/models/v1/group/group.ts @@ -1,5 +1,17 @@ import { GroupAutoUpdate } from '../group-auto-update'; +/** @deprecated Use Group instead */ +export type CreateGroupResponse = Group; + +/** @deprecated Use Group instead */ +export type GroupResponse = Group; + +/** @deprecated Use Group instead */ +export type ReplaceGroupResponse = Group; + +/** @deprecated Use Group instead */ +export type UpdateGroupResponse = Group; + export interface Group { /** The ID used to reference this group. */ diff --git a/packages/sms/src/models/v1/group/index.ts b/packages/sms/src/models/v1/group/index.ts index 48e367f1..ba87b37c 100644 --- a/packages/sms/src/models/v1/group/index.ts +++ b/packages/sms/src/models/v1/group/index.ts @@ -1 +1 @@ -export type { Group } from './group'; +export type { Group, GroupResponse, CreateGroupResponse, ReplaceGroupResponse, UpdateGroupResponse } from './group'; diff --git a/packages/sms/src/models/v1/requests/delivery-reports/delivery-reports-request-data.ts b/packages/sms/src/models/v1/requests/delivery-reports/delivery-reports-request-data.ts index 29c744b9..428e2c2f 100644 --- a/packages/sms/src/models/v1/requests/delivery-reports/delivery-reports-request-data.ts +++ b/packages/sms/src/models/v1/requests/delivery-reports/delivery-reports-request-data.ts @@ -10,12 +10,28 @@ export interface GetDeliveryReportByBatchIdRequestData { /** Comma separated list of delivery_receipt_error_codes to include */ 'code'?: string | number | number[]; } -export interface GetDeliveryReportByPhoneNumberRequestData { + +export type GetDeliveryReportByPhoneNumberRequestData = + GetDeliveryReportByPhoneNumberRequestDataBC | GetDeliveryReportByPhoneNumberRequestDataDeprecated; + +export interface GetDeliveryReportByPhoneNumberRequestDataBC { /** The batch ID you received from sending a message. */ 'batch_id': string; /** Phone number for which you to want to search. */ 'phone_number': string; + /** @deprecated Use phone_number instead */ + recipient_msisdn?: never; +} + +export interface GetDeliveryReportByPhoneNumberRequestDataDeprecated { + /** The batch ID you received from sending a message. */ + 'batch_id': string; + /** @deprecated: use phone_number instead */ + recipient_msisdn: string; + /** Phone number for which you to want to search. */ + phone_number?: never; } + export interface ListDeliveryReportsRequestData { /** The page number starting from 0. */ 'page'?: number; diff --git a/packages/sms/src/rest/v1/delivery-reports/delivery-reports-api.ts b/packages/sms/src/rest/v1/delivery-reports/delivery-reports-api.ts index ecb16438..40fbef95 100644 --- a/packages/sms/src/rest/v1/delivery-reports/delivery-reports-api.ts +++ b/packages/sms/src/rest/v1/delivery-reports/delivery-reports-api.ts @@ -71,8 +71,17 @@ export class DeliveryReportsApi extends SmsDomainApi { 'Accept': 'application/json', }; + // TODO: Remove in v2.0 + let phoneNumber; + if (data['phone_number']) { + phoneNumber = data['phone_number']; + } + else if (data['recipient_msisdn']) { + phoneNumber = data['recipient_msisdn']; + } + const body: RequestBody = ''; - const basePathUrl = `${this.client.apiClientOptions.hostname}/xms/v1/${this.client.apiClientOptions.projectId}/batches/${data['batch_id']}/delivery_report/${data['phone_number']}`; + const basePathUrl = `${this.client.apiClientOptions.hostname}/xms/v1/${this.client.apiClientOptions.projectId}/batches/${data['batch_id']}/delivery_report/${phoneNumber}`; const requestOptions = await this.client.prepareOptions(basePathUrl, 'GET', getParams, headers, body || undefined); diff --git a/packages/verification/CHANGELOG.md b/packages/verification/CHANGELOG.md index 266a7765..6e98766f 100644 --- a/packages/verification/CHANGELOG.md +++ b/packages/verification/CHANGELOG.md @@ -1,3 +1,64 @@ +## Version 1.2.0 +- [Tech] Update dependency `@sinch/sdk-client` to `1.2.0`. +- [Feature] Support the `locale` parameter in the `SmsOptions` interface. +- [Deprecation Notice] In the interface `VerificationStatusByIdentityRequestData`, the property `method` supports โ€˜flashcallโ€™ on top of โ€˜flashCallโ€™. +- [Deprecation Notice] All the references to "callout" and "seamless" will be replaced by "phoneCall" and "data" respectively. + - In the interface `VerificationStatusByIdentityRequestData`, the property `method` supports โ€˜phonecallโ€™ on top of โ€˜calloutโ€™. + - API methods: + +| Deprecated | New | +|-------------------------------------------|---------------------------------------------| +| `verifications.startCallout()` | `verifications.startPhoneCall()` | +| `verifications.reportCalloutById()` | `verifications.reportPhoneCallById()` | +| `verifications.reportCalloutByIdentity()` | `verifications.reportPhoneCallByIdentity()` | +- + - Helpers: + +| Deprecated | New | +|------------------------------------------------------------|---------------------------------------------------| +| `startVerificationHelper.buildCalloutRequest()` | `startVerificationHelper.buildPhoneCallRequest()` | +| `startVerificationHelper.buildSeamlessRequest()` | `startVerificationHelper.buildDataRequest()` | +| `reportVerificationByIdHelper.buildCalloutRequest()` | `startVerificationHelper.buildPhoneCallRequest()` | +| `reportVerificationByIdentityHelper.buildCalloutRequest()` | `startVerificationHelper.buildPhoneCallRequest()` | +- + - Interfaces + +| Deprecated | New | +|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|
StartCalloutVerificationRequestData {
  startVerificationWithCalloutRequestBody: StartVerificationWithCallout;
}
|
StartPhoneCallVerificationRequestData {
  startVerificationWithPhoneCallRequestBody: StartVerificationWithPhoneCall;
}
| +|
StartSeamlessVerificationRequestData {
  startSeamlessVerificationRequestBody: StartSeamlessVerification;
}
|
StartDataVerificationRequestData {
  startDataVerificationRequestBody: StartDataVerification;
}
| +|
ReportCalloutVerificationByIdRequestData {
  reportCalloutVerificationByIdentityRequestBody: CalloutVerificationReportRequest;
}
|
ReportPhoneCallVerificationByIdRequestData {
  reportPhoneCallVerificationByIdRequestBody: PhoneCallVerificationReportRequest;
}
| +|
ReportCalloutVerificationByIdentityRequestData {
  reportCalloutVerificationByIdentityRequestBody: CalloutVerificationReportRequest;
}
|
ReportPhoneCallVerificationByIdentityRequestData {
  reportPhoneCallVerificationByIdentityRequestBody: PhoneCallVerificationReportRequest;
}
| +|
StartVerificationWithCallout {
  calloutOptions: CalloutOptions;
}
|
StartVerificationWithPhoneCall {
  phoneCallOptions: PhoneCallOptions;
}
| +| `StartSeamlessVerification` | `StartDataVerification` | +| `CalloutOptions` | `PhoneCallOptions` | +| `CalloutOptionsSpeech` | `PhoneCallOptionsSpeech` | +| `CalloutVerificationStatusResponse` | `PhoneCallVerificationStatusResponse` | +| `CalloutVerificationReportResponse` | `PhoneCallVerificationReportResponse` | +| `StartCalloutVerificationResponse` | `StartPhoneCallVerificationResponse` | +| `CalloutRequestEventResponse` | `PhoneCallRequestEventResponse` | +| `CalloutProperties` | `PhoneCallProperties` | +| `CalloutContent` | `PhoneCallContent` | +- [Deprecation Notice] The interfaces containing `SMS` in uppercase will be replaced with the same name but with `Sms` in PascaleCase: + +| Deprecated | New | +|-------------------------------|-------------------------------| +| VerificationPriceSMS | VerificationPriceSms | +| SMSRequestEventResponse | SmsRequestEventResponse | +| SMSVerificationStatusResponse | SmsVerificationStatusResponse | +| SMSVerificationReportResponse | SmsVerificationReportResponse | + +- [Deprecation Notice] The type `VerificationCallback` becomes `VerificationCallbackEvent` and is accessible on the `Verification` namespace. + +| Deprecated | New | +|-----------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------| +|
handleEvent(event: VerificationCallback, res: Response) {
  console.log(event);
}
|
handleEvent(event: Verification.VerificationCallbackEvent, res: Response) {
  console.log(event);
}
| + +- [Deprecation Notice] The type `TypeEnum` should be replaced with the type `IdentityType`. +- [Deprecation Notice] The type `FlashCallContent` should be replaced with the type `FlashCallProperties`. +- [Deprecation Notice] The type `SmsContent` should be replaced with the type `SmsProperties`. +- [E2E] Add Cucumber steps implementation. + ## Version 1.1.0 - [Tech] Update dependency `@sinch/sdk-client` to `1.1.0` diff --git a/packages/verification/src/models/v1/helper.ts b/packages/verification/src/models/v1/helper.ts index 4d6cdbbc..1972bc85 100644 --- a/packages/verification/src/models/v1/helper.ts +++ b/packages/verification/src/models/v1/helper.ts @@ -82,7 +82,8 @@ export const startVerificationHelper = { * @param {string} [reference] - An optional reference identifier for the callout. * @param {string} [locale] - An optional locale string to specify the language or region for the callout. * @return {StartCalloutVerificationRequestData} The constructed callout request object. - * @deprecated */ + * @deprecated Use the method buildPhoneCallRequest() instead + */ buildCalloutRequest: ( phoneNumber: string, reference?: string, @@ -160,7 +161,7 @@ export const startVerificationHelper = { * @param {string} phoneNumber - The phone number to verify. * @param {string} [reference] - An optional reference identifier for the verification request. * @return {StartSeamlessVerificationRequestData} The constructed seamless verification request data. - * @deprecated + * @deprecated Use the method buildDataRequest() instead */ buildSeamlessRequest: ( phoneNumber: string, @@ -227,7 +228,7 @@ export const reportVerificationByIdHelper = { * @param {string} id - The unique identifier for the callout verification request. * @param {string} code - The verification code received during the callout. * @return {ReportCalloutVerificationByIdRequestData} The request data object used to report the callout verification. - * @deprecated + * @deprecated Use the method buildPhoneCallRequest() instead */ buildCalloutRequest: ( id: string, @@ -313,7 +314,7 @@ export const reportVerificationByIdentityHelper = { * @param {string} identity - The phone number for which the callout verification process has been initiated. * @param {string} code - The verification code received during the callout. * @return {ReportCalloutVerificationByIdentityRequestData} The request data object used to report the callout verification. - * @deprecated + * @deprecated Use the method buildPhoneCallRequest() instead */ buildCalloutRequest: ( identity: string, diff --git a/packages/verification/src/models/v1/identity/identity.ts b/packages/verification/src/models/v1/identity/identity.ts index c1d44143..dd8f731e 100644 --- a/packages/verification/src/models/v1/identity/identity.ts +++ b/packages/verification/src/models/v1/identity/identity.ts @@ -4,7 +4,12 @@ export interface Identity { /** Currently only `number` type is supported. */ - type: 'number'; + type: IdentityType; /** For type `number` use an [E.164](https://community.sinch.com/t5/Glossary/E-164/ta-p/7537)-compatible phone number. */ endpoint: string; } + +export type IdentityType = 'number'; + +/** @deprecated Use IdentityType instead */ +export type TypeEnum = IdentityType; diff --git a/packages/verification/src/models/v1/identity/index.ts b/packages/verification/src/models/v1/identity/index.ts index 3474d3a3..74923347 100644 --- a/packages/verification/src/models/v1/identity/index.ts +++ b/packages/verification/src/models/v1/identity/index.ts @@ -1 +1 @@ -export type { Identity } from './identity'; +export type { Identity, IdentityType, TypeEnum } from './identity'; diff --git a/packages/verification/src/models/v1/mod-callbacks/callout-request-event-response/index.ts b/packages/verification/src/models/v1/mod-callbacks/callout-request-event-response/index.ts deleted file mode 100644 index edc48f1c..00000000 --- a/packages/verification/src/models/v1/mod-callbacks/callout-request-event-response/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export type { - CalloutRequestEventResponse, - CalloutProperties, - SpeechProperties, -} from './callout-request-event-response'; diff --git a/packages/verification/src/models/v1/mod-callbacks/flashcall-request-event-response/flashcall-request-event-response.ts b/packages/verification/src/models/v1/mod-callbacks/flashcall-request-event-response/flashcall-request-event-response.ts index da9356da..645d2f91 100644 --- a/packages/verification/src/models/v1/mod-callbacks/flashcall-request-event-response/flashcall-request-event-response.ts +++ b/packages/verification/src/models/v1/mod-callbacks/flashcall-request-event-response/flashcall-request-event-response.ts @@ -7,7 +7,10 @@ export interface FlashCallRequestEventResponse { flashCall?: FlashCallProperties; } -interface FlashCallProperties { +/** @deprecated Use FlashCallProperties instead */ +export type FlashCallContent = FlashCallProperties; + +export interface FlashCallProperties { /** The phone number that will be displayed to the user when the flashcall is received on the user\'s phone. By default, the Sinch dashboard will randomly select the CLI that will be displayed during a flashcall from a pool of numbers. If you want to set your own CLI, you can specify it in the response to the Verification Request Event. */ cli?: string; /** The maximum time that a flashcall verification will be active and can be completed. If the phone number hasn\'t been verified successfully during this time, then the verification request will fail. By default, the Sinch dashboard will automatically optimize dial time out during a flashcall. If you want to set your own dial time out for the flashcall, you can specify it in the response to the Verification Request Event. */ diff --git a/packages/verification/src/models/v1/mod-callbacks/flashcall-request-event-response/index.ts b/packages/verification/src/models/v1/mod-callbacks/flashcall-request-event-response/index.ts index 5df2e145..47bf2584 100644 --- a/packages/verification/src/models/v1/mod-callbacks/flashcall-request-event-response/index.ts +++ b/packages/verification/src/models/v1/mod-callbacks/flashcall-request-event-response/index.ts @@ -1 +1,5 @@ -export type { FlashCallRequestEventResponse } from './flashcall-request-event-response'; +export type { + FlashCallRequestEventResponse, + FlashCallContent, + FlashCallProperties, +} from './flashcall-request-event-response'; diff --git a/packages/verification/src/models/v1/mod-callbacks/index.ts b/packages/verification/src/models/v1/mod-callbacks/index.ts index 4f251c46..7a45a655 100644 --- a/packages/verification/src/models/v1/mod-callbacks/index.ts +++ b/packages/verification/src/models/v1/mod-callbacks/index.ts @@ -2,7 +2,7 @@ export * from './verification-callback-event'; // 'Verification Request Event' received from Sinch server export * from './verification-request-event'; // Response to send to Sinch server for a 'Verification Request Event' -export * from './callout-request-event-response'; +export * from './phonecall-request-event-response'; export * from './flashcall-request-event-response'; export * from './sms-request-event-response'; export * from './verification-request-event-response'; diff --git a/packages/verification/src/models/v1/mod-callbacks/phonecall-request-event-response/index.ts b/packages/verification/src/models/v1/mod-callbacks/phonecall-request-event-response/index.ts new file mode 100644 index 00000000..c8b54dd0 --- /dev/null +++ b/packages/verification/src/models/v1/mod-callbacks/phonecall-request-event-response/index.ts @@ -0,0 +1,7 @@ +export type { + CalloutRequestEventResponse, + PhoneCallRequestEventResponse, + CalloutProperties, + PhoneCallProperties, + SpeechProperties, +} from './phonecall-request-event-response'; diff --git a/packages/verification/src/models/v1/mod-callbacks/callout-request-event-response/callout-request-event-response.ts b/packages/verification/src/models/v1/mod-callbacks/phonecall-request-event-response/phonecall-request-event-response.ts similarity index 64% rename from packages/verification/src/models/v1/mod-callbacks/callout-request-event-response/callout-request-event-response.ts rename to packages/verification/src/models/v1/mod-callbacks/phonecall-request-event-response/phonecall-request-event-response.ts index 0ad3b41d..4d451644 100644 --- a/packages/verification/src/models/v1/mod-callbacks/callout-request-event-response/callout-request-event-response.ts +++ b/packages/verification/src/models/v1/mod-callbacks/phonecall-request-event-response/phonecall-request-event-response.ts @@ -1,13 +1,19 @@ import { ActionEnum } from '../../enums'; -export interface CalloutRequestEventResponse { +/** @deprecated Use PhoneCallRequestEventResponse instead */ +export type CalloutRequestEventResponse = PhoneCallRequestEventResponse; + +export interface PhoneCallRequestEventResponse { /** Determines whether the verification can be executed. */ action?: ActionEnum; - /** @see CalloutProperties */ - callout?: CalloutProperties; + /** @see PhoneCallProperties */ + callout?: PhoneCallProperties; } -export interface CalloutProperties { +/** @deprecated Use PhoneCallProperties instead */ +export type CalloutProperties = PhoneCallProperties; + +export interface PhoneCallProperties { /** The Phone Call PIN that should be entered by the user. Sinch servers automatically generate PIN codes for Phone Call verification. If you want to set your own code, you can specify it in the response to the Verification Request Event. */ code?: string; /** @see SpeechProperties */ diff --git a/packages/verification/src/models/v1/mod-callbacks/sms-request-event-response/index.ts b/packages/verification/src/models/v1/mod-callbacks/sms-request-event-response/index.ts index 0ae8ee69..59f81aa6 100644 --- a/packages/verification/src/models/v1/mod-callbacks/sms-request-event-response/index.ts +++ b/packages/verification/src/models/v1/mod-callbacks/sms-request-event-response/index.ts @@ -1 +1,6 @@ -export type { SmsRequestEventResponse } from './sms-request-event-response'; +export type { + SMSRequestEventResponse, + SmsRequestEventResponse, + SmsContent, + SmsProperties, +} from './sms-request-event-response'; diff --git a/packages/verification/src/models/v1/mod-callbacks/sms-request-event-response/sms-request-event-response.ts b/packages/verification/src/models/v1/mod-callbacks/sms-request-event-response/sms-request-event-response.ts index f025c0ea..a6cc8294 100644 --- a/packages/verification/src/models/v1/mod-callbacks/sms-request-event-response/sms-request-event-response.ts +++ b/packages/verification/src/models/v1/mod-callbacks/sms-request-event-response/sms-request-event-response.ts @@ -7,9 +7,15 @@ export interface SmsRequestEventResponse { sms?: SmsProperties; } -interface SmsProperties { +export interface SmsProperties { /** The SMS PIN that should be used. By default, the Sinch dashboard will automatically generate PIN codes for SMS verification. If you want to set your own PIN, you can specify it in the response to the Verification Request Event. */ code?: string; /** List of strings */ acceptLanguage?: string[]; } + +/** @deprecated Use SmsRequestEventResponse instead */ +export type SMSRequestEventResponse = SmsRequestEventResponse; + +/** @deprecated Use SmsProperties instead */ +export type SmsContent = SmsProperties; diff --git a/packages/verification/src/models/v1/mod-callbacks/verification-request-event-response/verification-request-event-response.ts b/packages/verification/src/models/v1/mod-callbacks/verification-request-event-response/verification-request-event-response.ts index cd13a1d2..fe380dd7 100644 --- a/packages/verification/src/models/v1/mod-callbacks/verification-request-event-response/verification-request-event-response.ts +++ b/packages/verification/src/models/v1/mod-callbacks/verification-request-event-response/verification-request-event-response.ts @@ -1,6 +1,6 @@ import { SmsRequestEventResponse } from '../sms-request-event-response'; import { FlashCallRequestEventResponse } from '../flashcall-request-event-response'; -import { CalloutRequestEventResponse } from '../callout-request-event-response'; +import { CalloutRequestEventResponse } from '../phonecall-request-event-response'; export type VerificationRequestEventResponse = SmsRequestEventResponse | FlashCallRequestEventResponse diff --git a/packages/verification/src/models/v1/mod-callbacks/verification-result-event-response/verification-result-event-response.ts b/packages/verification/src/models/v1/mod-callbacks/verification-result-event-response/verification-result-event-response.ts index ba2ba741..8c710ddb 100644 --- a/packages/verification/src/models/v1/mod-callbacks/verification-result-event-response/verification-result-event-response.ts +++ b/packages/verification/src/models/v1/mod-callbacks/verification-result-event-response/verification-result-event-response.ts @@ -1,6 +1,6 @@ import { SmsRequestEventResponse } from '../sms-request-event-response'; import { FlashCallRequestEventResponse } from '../flashcall-request-event-response'; -import { CalloutRequestEventResponse } from '../callout-request-event-response'; +import { CalloutRequestEventResponse } from '../phonecall-request-event-response'; export type VerificationResultEventResponse = SmsRequestEventResponse | FlashCallRequestEventResponse diff --git a/packages/verification/src/models/v1/phonecall-verification-report-response/phonecall-verification-report-response.ts b/packages/verification/src/models/v1/phonecall-verification-report-response/phonecall-verification-report-response.ts index a7d1af4a..3594a542 100644 --- a/packages/verification/src/models/v1/phonecall-verification-report-response/phonecall-verification-report-response.ts +++ b/packages/verification/src/models/v1/phonecall-verification-report-response/phonecall-verification-report-response.ts @@ -1,7 +1,7 @@ import { ReasonEnum, SourceEnum, VerificationStatusEnum } from '../enums'; import { Identity } from '../identity'; -/** @deprecated */ +/** @deprecated Use PhoneCallVerificationReportResponse instead */ export type CalloutVerificationReportResponse = PhoneCallVerificationReportResponse; export interface PhoneCallVerificationReportResponse { diff --git a/packages/verification/src/models/v1/phonecall-verification-status-response/phonecall-verification-status-response.ts b/packages/verification/src/models/v1/phonecall-verification-status-response/phonecall-verification-status-response.ts index 05e4cbcb..c36fa29e 100644 --- a/packages/verification/src/models/v1/phonecall-verification-status-response/phonecall-verification-status-response.ts +++ b/packages/verification/src/models/v1/phonecall-verification-status-response/phonecall-verification-status-response.ts @@ -2,7 +2,7 @@ import { CallResult, ReasonEnum, VerificationStatusEnum } from '../enums'; import { Identity } from '../identity'; import { VerificationPriceCall } from '../verification-price-call'; -/** @deprecated */ +/** @deprecated Use PhoneCallVerificationStatusResponse instead */ export type CalloutVerificationStatusResponse = PhoneCallVerificationStatusResponse; export interface PhoneCallVerificationStatusResponse { diff --git a/packages/verification/src/models/v1/requests/verification-status/verification-status-request-data.ts b/packages/verification/src/models/v1/requests/verification-status/verification-status-request-data.ts index 298867b1..b0f9df92 100644 --- a/packages/verification/src/models/v1/requests/verification-status/verification-status-request-data.ts +++ b/packages/verification/src/models/v1/requests/verification-status/verification-status-request-data.ts @@ -6,8 +6,8 @@ export interface VerificationStatusByIdentityRequestData { /** For type `number` use a [E.164](https://community.sinch.com/t5/Glossary/E-164/ta-p/7537)-compatible phone number. */ 'endpoint': string; /** The method of the verification. */ - // TODO v2.0 - Remove 'callout' option - 'method': 'sms' | 'callout' | 'phonecall' | 'flashcall'; + // TODO v2.0 - Remove 'callout' and 'flashCall' options + 'method': 'sms' | 'callout' | 'phonecall' | 'flashCall' | 'flashcall'; } export interface VerificationStatusByReferenceRequestData { /** The custom reference of the verification. */ diff --git a/packages/verification/src/models/v1/requests/verifications/verifications-request-data.ts b/packages/verification/src/models/v1/requests/verifications/verifications-request-data.ts index 5475c92a..21a79617 100644 --- a/packages/verification/src/models/v1/requests/verifications/verifications-request-data.ts +++ b/packages/verification/src/models/v1/requests/verifications/verifications-request-data.ts @@ -33,7 +33,7 @@ export interface ReportPhoneCallVerificationByIdRequestData extends ReportVerifi 'reportPhoneCallVerificationByIdRequestBody': PhoneCallVerificationReportRequest; } -/** @deprecated */ +/** @deprecated Use ReportPhoneCallVerificationByIdRequestData instead */ export interface ReportCalloutVerificationByIdRequestData extends ReportVerificationByIdRequestDataBase { /** Request body to report a verification started with a callout by its ID */ 'reportCalloutVerificationByIdRequestBody': CalloutVerificationReportRequest; @@ -59,7 +59,7 @@ export interface ReportPhoneCallVerificationByIdentityRequestData extends Report 'reportPhoneCallVerificationByIdentityRequestBody': PhoneCallVerificationReportRequest; } -/** @deprecated */ +/** @deprecated Use ReportPhoneCallVerificationByIdentityRequestData instead */ export interface ReportCalloutVerificationByIdentityRequestData extends ReportVerificationByIdentityRequestDataBase { /** Request body to report a verification started with a callout by its identity */ 'reportCalloutVerificationByIdentityRequestBody': CalloutVerificationReportRequest; @@ -76,21 +76,22 @@ export interface StartFlashCallVerificationRequestData { } export interface StartPhoneCallVerificationRequestData { + /** Request body to start a verification with a phone call */ 'startVerificationWithPhoneCallRequestBody': StartVerificationWithPhoneCall; } -/** @deprecated */ +/** @deprecated Use StartPhoneCallVerificationRequestData instead */ export interface StartCalloutVerificationRequestData { /** Request body to start a verification with a callout */ 'startVerificationWithCalloutRequestBody': StartVerificationWithCallout; } export interface StartDataVerificationRequestData { - /** Request body to start a seamless verification */ + /** Request body to start a data verification */ 'startDataVerificationRequestBody': StartDataVerification; } -/** @deprecated */ +/** @deprecated Use StartDataVerificationRequestData instead */ export interface StartSeamlessVerificationRequestData { /** Request body to start a seamless verification */ 'startSeamlessVerificationRequestBody': StartSeamlessVerification; diff --git a/packages/verification/src/models/v1/sms-verification-report-response/index.ts b/packages/verification/src/models/v1/sms-verification-report-response/index.ts index 0eb2b251..c043cf9c 100644 --- a/packages/verification/src/models/v1/sms-verification-report-response/index.ts +++ b/packages/verification/src/models/v1/sms-verification-report-response/index.ts @@ -1 +1 @@ -export type { SmsVerificationReportResponse } from './sms-verification-report-response'; +export type { SMSVerificationReportResponse, SmsVerificationReportResponse } from './sms-verification-report-response'; diff --git a/packages/verification/src/models/v1/sms-verification-report-response/sms-verification-report-response.ts b/packages/verification/src/models/v1/sms-verification-report-response/sms-verification-report-response.ts index 094644ea..9cf18073 100644 --- a/packages/verification/src/models/v1/sms-verification-report-response/sms-verification-report-response.ts +++ b/packages/verification/src/models/v1/sms-verification-report-response/sms-verification-report-response.ts @@ -18,3 +18,6 @@ export interface SmsVerificationReportResponse { /** @see Identity */ identity?: Identity; } + +/** @deprecated Use SmsVerificationReportResponse instead */ +export type SMSVerificationReportResponse = SmsVerificationReportResponse; diff --git a/packages/verification/src/models/v1/sms-verification-status-response/index.ts b/packages/verification/src/models/v1/sms-verification-status-response/index.ts index 9d1dfb90..cd232656 100644 --- a/packages/verification/src/models/v1/sms-verification-status-response/index.ts +++ b/packages/verification/src/models/v1/sms-verification-status-response/index.ts @@ -1 +1 @@ -export * from './sms-verification-status-response'; +export type { SMSVerificationStatusResponse, SmsVerificationStatusResponse } from './sms-verification-status-response'; diff --git a/packages/verification/src/models/v1/sms-verification-status-response/sms-verification-status-response.ts b/packages/verification/src/models/v1/sms-verification-status-response/sms-verification-status-response.ts index ec77f15c..21c457d9 100644 --- a/packages/verification/src/models/v1/sms-verification-status-response/sms-verification-status-response.ts +++ b/packages/verification/src/models/v1/sms-verification-status-response/sms-verification-status-response.ts @@ -25,3 +25,6 @@ export interface SmsVerificationStatusResponse { /** Free text that the client is sending, used to show if the call/SMS was intercepted or not. */ source?: SourceEnum; } + +/** @deprecated Use SmsVerificationStatusResponse instead */ +export type SMSVerificationStatusResponse = SmsVerificationStatusResponse; diff --git a/packages/verification/src/models/v1/start-data-verification-response/start-data-verification-response.ts b/packages/verification/src/models/v1/start-data-verification-response/start-data-verification-response.ts index 0d4f1160..a3a220b6 100644 --- a/packages/verification/src/models/v1/start-data-verification-response/start-data-verification-response.ts +++ b/packages/verification/src/models/v1/start-data-verification-response/start-data-verification-response.ts @@ -1,6 +1,6 @@ import { LinksObject } from '../links-object'; -/** @deprecated */ +/** @deprecated Use StartDataVerificationResponse instead */ export type StartSeamlessVerificationResponse = StartDataVerificationResponse; export interface StartDataVerificationResponse { diff --git a/packages/verification/src/models/v1/start-phonecall-verification-response/start-phonecall-verification-response.ts b/packages/verification/src/models/v1/start-phonecall-verification-response/start-phonecall-verification-response.ts index 7d407f37..7ed1fec5 100644 --- a/packages/verification/src/models/v1/start-phonecall-verification-response/start-phonecall-verification-response.ts +++ b/packages/verification/src/models/v1/start-phonecall-verification-response/start-phonecall-verification-response.ts @@ -1,6 +1,6 @@ import { LinksObject } from '../links-object'; -/** @deprecated */ +/** @deprecated Use StartPhoneCallVerificationResponse instead */ export type StartCalloutVerificationResponse = StartPhoneCallVerificationResponse; export interface StartPhoneCallVerificationResponse { diff --git a/packages/verification/src/models/v1/start-verification-request/start-verification-request.ts b/packages/verification/src/models/v1/start-verification-request/start-verification-request.ts index d72160fe..5f4d4346 100644 --- a/packages/verification/src/models/v1/start-verification-request/start-verification-request.ts +++ b/packages/verification/src/models/v1/start-verification-request/start-verification-request.ts @@ -20,7 +20,7 @@ export interface StartVerificationWithPhoneCallServerModel extends StartVerifica calloutOptions?: PhoneCallOptions; } -/** @deprecated */ +/** @deprecated Use StartVerificationWithPhoneCall instead */ export interface StartVerificationWithCallout extends StartVerificationBase { /** @see CalloutOptions */ calloutOptions?: CalloutOptions; @@ -28,8 +28,8 @@ export interface StartVerificationWithCallout extends StartVerificationBase { export interface StartDataVerification extends StartVerificationBase {} -/** @deprecated */ -export interface StartSeamlessVerification extends StartVerificationBase {} +/** @deprecated Use StartDataVerification instead */ +export type StartSeamlessVerification = StartDataVerification; export interface StartVerificationBase { /** @see Identity */ @@ -64,7 +64,7 @@ export interface FlashCallOptions { dialTimeout?: number; } -/** @deprecated */ +/** @deprecated Use PhoneCallOptions instead */ export type CalloutOptions = PhoneCallOptions; /** @@ -74,7 +74,7 @@ export interface PhoneCallOptions { /** @see PhoneCallOptionsSpeech */ speech?: PhoneCallOptionsSpeech; } -/** @deprecated */ +/** @deprecated Use PhoneCallOptionsSpeech instead */ export type CalloutOptionsSpeech = PhoneCallOptionsSpeech; /** diff --git a/packages/verification/src/models/v1/verification-price-sms/verification-price-sms.ts b/packages/verification/src/models/v1/verification-price-sms/verification-price-sms.ts index cfcc9462..1b601271 100644 --- a/packages/verification/src/models/v1/verification-price-sms/verification-price-sms.ts +++ b/packages/verification/src/models/v1/verification-price-sms/verification-price-sms.ts @@ -8,3 +8,6 @@ export interface VerificationPriceSms { /** The maximum price charged for this verification process. This property will appear in the body of the response with a delay. It will become visible only when the verification status is other than PENDING. */ verificationPrice?: Price; } + +/** @deprecated Use VerificationPriceSms instead */ +export type VerificationPriceSMS = VerificationPriceSms; diff --git a/packages/verification/src/models/v1/verification-report-request/verification-report-request.ts b/packages/verification/src/models/v1/verification-report-request/verification-report-request.ts index 6c2bfc51..0ed08e86 100644 --- a/packages/verification/src/models/v1/verification-report-request/verification-report-request.ts +++ b/packages/verification/src/models/v1/verification-report-request/verification-report-request.ts @@ -30,7 +30,7 @@ export interface PhoneCallVerificationReportRequestServerModel { callout: PhoneCallContent; } -/** @deprecated */ +/** @deprecated Use PhoneCallVerificationReportRequest instead */ export interface CalloutVerificationReportRequest { /** A configuration object containing settings specific to Phone Call verifications */ callout: CalloutContent; @@ -41,5 +41,5 @@ interface PhoneCallContent { code?: string; } -/** @deprecated */ +/** @deprecated Use PhoneCallContent instead */ type CalloutContent = PhoneCallContent; diff --git a/packages/verification/src/rest/v1/callbacks/callbacks-webhook.ts b/packages/verification/src/rest/v1/callbacks/callbacks-webhook.ts index 5e66d930..4fec4b6e 100644 --- a/packages/verification/src/rest/v1/callbacks/callbacks-webhook.ts +++ b/packages/verification/src/rest/v1/callbacks/callbacks-webhook.ts @@ -2,7 +2,7 @@ import { VerificationCallbackEvent, VerificationRequestEvent, VerificationResult import { CallbackProcessor, SinchClientParameters, validateAuthenticationHeader } from '@sinch/sdk-client'; import { IncomingHttpHeaders } from 'http'; -/** @deprecated - use Verification.VerificationCallback instead */ +/** @deprecated Use Verification.VerificationCallback instead */ export type VerificationCallback = VerificationRequestEvent | VerificationResultEvent; export class VerificationCallbackWebhooks implements CallbackProcessor{ diff --git a/packages/verification/src/rest/v1/verifications/verifications-api.ts b/packages/verification/src/rest/v1/verifications/verifications-api.ts index 800da475..a4b84877 100644 --- a/packages/verification/src/rest/v1/verifications/verifications-api.ts +++ b/packages/verification/src/rest/v1/verifications/verifications-api.ts @@ -56,7 +56,7 @@ export class VerificationsApi extends VerificationDomainApi { (data.reportSmsVerificationByIdRequestBody as any).method = 'sms'; const getParams = this.client.extractQueryParams(data, [] as never[]); const headers: { [key: string]: string | undefined } = { - 'Content-Type': 'application/json; charset=UTF-8', + 'Content-Type': 'application/json', 'Accept': 'application/json', }; @@ -92,7 +92,7 @@ export class VerificationsApi extends VerificationDomainApi { data, [] as never[]); const headers: { [key: string]: string | undefined } = { - 'Content-Type': 'application/json; charset=UTF-8', + 'Content-Type': 'application/json', 'Accept': 'application/json', }; @@ -126,7 +126,7 @@ export class VerificationsApi extends VerificationDomainApi { (data.reportPhoneCallVerificationByIdRequestBody as any).method = 'callout'; const getParams = this.client.extractQueryParams(data, [] as never[]); const headers: { [key: string]: string | undefined } = { - 'Content-Type': 'application/json; charset=UTF-8', + 'Content-Type': 'application/json', 'Accept': 'application/json', }; @@ -166,7 +166,7 @@ export class VerificationsApi extends VerificationDomainApi { * Report a Callout verification with ID * Report the received verification code to verify it, using the Verification ID of the Verification request. * @param { ReportCalloutVerificationByIdRequestData } data - The data to provide to the API call. - * @deprecated + * @deprecated Use the method reportPhoneCallById() instead */ public async reportCalloutById( data: ReportCalloutVerificationByIdRequestData, @@ -175,7 +175,7 @@ export class VerificationsApi extends VerificationDomainApi { (data.reportCalloutVerificationByIdRequestBody as any).method = 'callout'; const getParams = this.client.extractQueryParams(data, [] as never[]); const headers: { [key: string]: string | undefined } = { - 'Content-Type': 'application/json; charset=UTF-8', + 'Content-Type': 'application/json', 'Accept': 'application/json', }; @@ -209,7 +209,7 @@ export class VerificationsApi extends VerificationDomainApi { (data.reportSmsVerificationByIdentityRequestBody as any).method = 'sms'; const getParams = this.client.extractQueryParams(data, [] as never[]); const headers: { [key: string]: string | undefined } = { - 'Content-Type': 'application/json; charset=UTF-8', + 'Content-Type': 'application/json', 'Accept': 'application/json', }; @@ -244,7 +244,7 @@ export class VerificationsApi extends VerificationDomainApi { const getParams = this.client.extractQueryParams( data, [] as never[]); const headers: { [key: string]: string | undefined } = { - 'Content-Type': 'application/json; charset=UTF-8', + 'Content-Type': 'application/json', 'Accept': 'application/json', }; @@ -279,7 +279,7 @@ export class VerificationsApi extends VerificationDomainApi { const getParams = this.client.extractQueryParams( data, [] as never[]); const headers: { [key: string]: string | undefined } = { - 'Content-Type': 'application/json; charset=UTF-8', + 'Content-Type': 'application/json', 'Accept': 'application/json', }; @@ -318,7 +318,7 @@ export class VerificationsApi extends VerificationDomainApi { * Report a Callout verification using Identity * Report the received verification code (OTP) to verify it, using the identity of the user (in most cases, the phone number). * @param { ReportCalloutVerificationByIdentityRequestData } data - The data to provide to the API call. - * @deprecated + * @deprecated Use the method reportPhoneCallByIdentity() instead */ public async reportCalloutByIdentity( data: ReportCalloutVerificationByIdentityRequestData, @@ -328,7 +328,7 @@ export class VerificationsApi extends VerificationDomainApi { const getParams = this.client.extractQueryParams( data, [] as never[]); const headers: { [key: string]: string | undefined } = { - 'Content-Type': 'application/json; charset=UTF-8', + 'Content-Type': 'application/json', 'Accept': 'application/json', }; @@ -533,14 +533,14 @@ export class VerificationsApi extends VerificationDomainApi { * Start verification with a callout * This method is used by the mobile and web Verification SDKs to start a verification. It can also be used to request a verification from your backend, by making a request. * @param { StartCalloutVerificationRequestData } data - The data to provide to the API call. - * @deprecated + * @deprecated Use the method startPhoneCall() instead */ public async startCallout(data: StartCalloutVerificationRequestData): Promise { this.client = this.getSinchClient(); (data.startVerificationWithCalloutRequestBody as any).method = 'callout'; const getParams = this.client.extractQueryParams(data, [] as never[]); const headers: { [key: string]: string | undefined } = { - 'Content-Type': 'application/json; charset=UTF-8', + 'Content-Type': 'application/json', 'Accept': 'application/json', }; @@ -566,14 +566,14 @@ export class VerificationsApi extends VerificationDomainApi { * Start seamless verification (= data verification) * This method is used by the mobile and web Verification SDKs to start a verification. It can also be used to request a verification from your backend, by making a request. * @param { StartSeamlessVerificationRequestData } data - The data to provide to the API call. - * @deprecated + * @deprecated Use the method startData() instead */ public async startSeamless(data: StartSeamlessVerificationRequestData): Promise { this.client = this.getSinchClient(); (data.startSeamlessVerificationRequestBody as any).method = 'seamless'; const getParams = this.client.extractQueryParams(data, [] as never[]); const headers: { [key: string]: string | undefined } = { - 'Content-Type': 'application/json; charset=UTF-8', + 'Content-Type': 'application/json', 'Accept': 'application/json', }; diff --git a/packages/voice/CHANGELOG.md b/packages/voice/CHANGELOG.md index afb3d060..210944f6 100644 --- a/packages/voice/CHANGELOG.md +++ b/packages/voice/CHANGELOG.md @@ -1,3 +1,15 @@ +## Version 1.2.0 +- [Tech] Update dependency `@sinch/sdk-client` to `1.2.0`. +- [Feature] In the interface `Participant`, the property `type` defines a list of string values on top of a generic string. +- [Breaking] In the interface `ConferenceCalloutRequest`, the property `mohClass` was declared as a `string` and is now a `MusicOnHold` type; +- [Deprecation Notice] The type `VoiceCallback` becomes `VoiceCallbackEvent` and is accessible on the `Voice` namespace. + +| Deprecated | New | +|----------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------| +|
handleEvent(event: VoiceCallback, res: Response) {
  console.log(event);
}
|
handleEvent(event: Voice.VoiceCallbackEvent, res: Response) {
  console.log(event);
}
| + +- [E2E] Add Cucumber steps implementation. + ## Version 1.1.0 - [Tech] Update dependency `@sinch/sdk-client` to `1.1.0` diff --git a/packages/voice/src/rest/v1/callbacks/callbacks-webhook.ts b/packages/voice/src/rest/v1/callbacks/callbacks-webhook.ts index 2db77040..e23b73ec 100644 --- a/packages/voice/src/rest/v1/callbacks/callbacks-webhook.ts +++ b/packages/voice/src/rest/v1/callbacks/callbacks-webhook.ts @@ -2,7 +2,7 @@ import { AceRequest, DiceRequest, IceRequest, NotifyRequest, PieRequest, VoiceCa import { CallbackProcessor, SinchClientParameters, validateAuthenticationHeader } from '@sinch/sdk-client'; import { IncomingHttpHeaders } from 'http'; -/** @deprecated - use Voice.VoiceCallbackEvent instead */ +/** @deprecated Use Voice.VoiceCallbackEvent instead */ export type VoiceCallback = IceRequest | AceRequest | DiceRequest | PieRequest | NotifyRequest; export class VoiceCallbackWebhooks implements CallbackProcessor{ From 412e53465438f866e238b9066e17fa24cf75da53 Mon Sep 17 00:00:00 2001 From: Antoine SEIN <142824551+asein-sinch@users.noreply.github.com> Date: Tue, 1 Oct 2024 15:13:47 +0200 Subject: [PATCH 48/54] DEVEXP-588: Add send batch with multiple parameters (#140) --- packages/sms/CHANGELOG.md | 7 ++ .../api-update-mms-mt-message.ts | 6 +- .../api-update-text-mt-message.ts | 6 +- packages/sms/src/models/v1/index.ts | 4 +- .../models/v1/media-request/media-request.ts | 6 +- .../v1/media-response/media-response.ts | 6 +- .../src/models/v1/parameter-group/index.ts | 1 + .../parameter-group.ts} | 9 ++- .../v1/parameter-obj-parameter-key/index.ts | 1 - .../sms/src/models/v1/parameter-obj/index.ts | 1 - .../src/models/v1/parameter-values/index.ts | 1 + .../parameter-values.ts} | 8 ++ .../models/v1/text-request/text-request.ts | 6 +- .../models/v1/text-response/text-response.ts | 6 +- .../tests/rest/v1/batches/batches.steps.ts | 73 ++++++++++++++++--- 15 files changed, 104 insertions(+), 37 deletions(-) create mode 100644 packages/sms/src/models/v1/parameter-group/index.ts rename packages/sms/src/models/v1/{parameter-obj/parameter-obj.ts => parameter-group/parameter-group.ts} (63%) delete mode 100644 packages/sms/src/models/v1/parameter-obj-parameter-key/index.ts delete mode 100644 packages/sms/src/models/v1/parameter-obj/index.ts create mode 100644 packages/sms/src/models/v1/parameter-values/index.ts rename packages/sms/src/models/v1/{parameter-obj-parameter-key/parameter-obj-parameter-key.ts => parameter-values/parameter-values.ts} (54%) diff --git a/packages/sms/CHANGELOG.md b/packages/sms/CHANGELOG.md index 64912161..2ee38a3b 100644 --- a/packages/sms/CHANGELOG.md +++ b/packages/sms/CHANGELOG.md @@ -8,6 +8,13 @@ - [Bugfix] In the interface `UpdateGroupRequest`, the property `name` can also be set to null to remove an existing name set. - [Deprecation Notice] All variations of a group response (`GroupResponse`, `CreateGroupResponse`, `ReplaceGroupResponse` and `UpdateGroupResponse`) are deprecated and replaced by the unique interface `Group`. - [Deprecation Notice] In the interface `GetDeliveryReportByPhoneNumberRequestData`, the request parameter `recipient_msisdn` is deprecated and should be replaced by `phone_number`. +- [Deprecation Notice] The "parameters" related interfaces have been updated and the interface `ParameterGroup`uses an index signature to allow for arbitrary keys instead of extending a `Record`: + +| Deprecated | New | +|--------------------------|-----------------| +| ParameterObj | ParameterGroup | +| ParameterObjParameterKey | ParameterValues | + - [E2E] Add Cucumber steps implementation. ## Version 1.1.0 diff --git a/packages/sms/src/models/v1/api-update-mms-mt-message/api-update-mms-mt-message.ts b/packages/sms/src/models/v1/api-update-mms-mt-message/api-update-mms-mt-message.ts index 9057b61a..79574eeb 100644 --- a/packages/sms/src/models/v1/api-update-mms-mt-message/api-update-mms-mt-message.ts +++ b/packages/sms/src/models/v1/api-update-mms-mt-message/api-update-mms-mt-message.ts @@ -1,5 +1,5 @@ import { MediaBody } from '../media-body'; -import { ParameterObj } from '../parameter-obj'; +import { ParameterGroup } from '../parameter-group'; import { DeliveryReportEnum } from '../enums'; export interface ApiUpdateMmsMtMessage { @@ -22,8 +22,8 @@ export interface ApiUpdateMmsMtMessage { callback_url?: string; /** @see MediaBody */ body?: MediaBody; - /** @see ParameterObj */ - parameters?: ParameterObj; + /** @see ParameterGroup */ + parameters?: ParameterGroup; /** Whether or not you want the media included in your message to be checked against [Sinch MMS channel best practices](/docs/mms/bestpractices/). If set to true, your message will be rejected if it doesn\'t conform to the listed recommendations, otherwise no validation will be performed. */ strict_validation?: boolean; } diff --git a/packages/sms/src/models/v1/api-update-text-mt-message/api-update-text-mt-message.ts b/packages/sms/src/models/v1/api-update-text-mt-message/api-update-text-mt-message.ts index 64ae8181..1f52fee8 100644 --- a/packages/sms/src/models/v1/api-update-text-mt-message/api-update-text-mt-message.ts +++ b/packages/sms/src/models/v1/api-update-text-mt-message/api-update-text-mt-message.ts @@ -1,4 +1,4 @@ -import { ParameterObj } from '../parameter-obj'; +import { ParameterGroup } from '../parameter-group'; import { DeliveryReportEnum } from '../enums'; export interface ApiUpdateTextMtMessage { @@ -19,8 +19,8 @@ export interface ApiUpdateTextMtMessage { expire_at?: Date; /** Override the default callback URL for this batch. Constraints: Must be valid URL. */ callback_url?: string; - /** @see ParameterObj */ - parameters?: ParameterObj; + /** @see ParameterGroup */ + parameters?: ParameterGroup; /** The message content */ body?: string; } diff --git a/packages/sms/src/models/v1/index.ts b/packages/sms/src/models/v1/index.ts index a7cd9de0..810d1a46 100644 --- a/packages/sms/src/models/v1/index.ts +++ b/packages/sms/src/models/v1/index.ts @@ -28,8 +28,8 @@ export * from './media-body'; export * from './media-request'; export * from './media-response'; export * from './message-delivery-status'; -export * from './parameter-obj'; -export * from './parameter-obj-parameter-key'; +export * from './parameter-group'; +export * from './parameter-values'; export * from './recipient-delivery-report'; export * from './replace-group-request'; export * from './send-sms-response'; diff --git a/packages/sms/src/models/v1/media-request/media-request.ts b/packages/sms/src/models/v1/media-request/media-request.ts index 2c39855e..0226502a 100644 --- a/packages/sms/src/models/v1/media-request/media-request.ts +++ b/packages/sms/src/models/v1/media-request/media-request.ts @@ -1,5 +1,5 @@ import { MediaBody } from '../media-body'; -import { ParameterObj } from '../parameter-obj'; +import { ParameterGroup } from '../parameter-group'; import { DeliveryReportEnum } from '../enums'; /** @@ -12,8 +12,8 @@ export interface MediaRequest { from?: string; /** @see MediaBody */ body: MediaBody; - /** @see ParameterObj */ - parameters?: ParameterObj; + /** @see ParameterGroup */ + parameters?: ParameterGroup; /** MMS */ type?: 'mt_media'; /** Request delivery report callback. Note that delivery reports can be fetched from the API regardless of this setting. */ diff --git a/packages/sms/src/models/v1/media-response/media-response.ts b/packages/sms/src/models/v1/media-response/media-response.ts index d355c255..964cddfa 100644 --- a/packages/sms/src/models/v1/media-response/media-response.ts +++ b/packages/sms/src/models/v1/media-response/media-response.ts @@ -1,5 +1,5 @@ import { MediaBody } from '../media-body'; -import { ParameterObj } from '../parameter-obj'; +import { ParameterGroup } from '../parameter-group'; import { DeliveryReportEnum } from '../enums'; export interface MediaResponse { @@ -13,8 +13,8 @@ export interface MediaResponse { canceled?: boolean; /** @see MediaBody */ body?: MediaBody; - /** @see ParameterObj */ - parameters?: ParameterObj; + /** @see ParameterGroup */ + parameters?: ParameterGroup; /** Media message */ type?: 'mt_media'; /** Timestamp for when batch was created. YYYY-MM-DDThh:mm:ss.SSSZ format */ diff --git a/packages/sms/src/models/v1/parameter-group/index.ts b/packages/sms/src/models/v1/parameter-group/index.ts new file mode 100644 index 00000000..39b8f6c3 --- /dev/null +++ b/packages/sms/src/models/v1/parameter-group/index.ts @@ -0,0 +1 @@ +export type { ParameterObj, ParameterGroup } from './parameter-group'; diff --git a/packages/sms/src/models/v1/parameter-obj/parameter-obj.ts b/packages/sms/src/models/v1/parameter-group/parameter-group.ts similarity index 63% rename from packages/sms/src/models/v1/parameter-obj/parameter-obj.ts rename to packages/sms/src/models/v1/parameter-group/parameter-group.ts index 15beaa86..55c2564d 100644 --- a/packages/sms/src/models/v1/parameter-obj/parameter-obj.ts +++ b/packages/sms/src/models/v1/parameter-group/parameter-group.ts @@ -1,12 +1,15 @@ -import { ParameterObjParameterKey } from '../parameter-obj-parameter-key'; +import { ParameterObjParameterKey, ParameterValues } from '../parameter-values'; /** * Contains the parameters that will be used for customizing the message for each recipient. [Click here to learn more about parameterization](/docs/sms/resources/message-info/message-parameterization). */ +export interface ParameterGroup { + [parameterKey: string]: ParameterValues; +} + +/** @deprecated Use ParameterGroup instead */ export interface ParameterObj extends Record { /** @see ParameterObjParameterKey */ '{parameter_key}'?: ParameterObjParameterKey; } - - diff --git a/packages/sms/src/models/v1/parameter-obj-parameter-key/index.ts b/packages/sms/src/models/v1/parameter-obj-parameter-key/index.ts deleted file mode 100644 index 2b230f38..00000000 --- a/packages/sms/src/models/v1/parameter-obj-parameter-key/index.ts +++ /dev/null @@ -1 +0,0 @@ -export type { ParameterObjParameterKey } from './parameter-obj-parameter-key'; diff --git a/packages/sms/src/models/v1/parameter-obj/index.ts b/packages/sms/src/models/v1/parameter-obj/index.ts deleted file mode 100644 index 39022580..00000000 --- a/packages/sms/src/models/v1/parameter-obj/index.ts +++ /dev/null @@ -1 +0,0 @@ -export type { ParameterObj } from './parameter-obj'; diff --git a/packages/sms/src/models/v1/parameter-values/index.ts b/packages/sms/src/models/v1/parameter-values/index.ts new file mode 100644 index 00000000..c4d2048b --- /dev/null +++ b/packages/sms/src/models/v1/parameter-values/index.ts @@ -0,0 +1 @@ +export type { ParameterObjParameterKey, ParameterValues } from './parameter-values'; diff --git a/packages/sms/src/models/v1/parameter-obj-parameter-key/parameter-obj-parameter-key.ts b/packages/sms/src/models/v1/parameter-values/parameter-values.ts similarity index 54% rename from packages/sms/src/models/v1/parameter-obj-parameter-key/parameter-obj-parameter-key.ts rename to packages/sms/src/models/v1/parameter-values/parameter-values.ts index 2e33d1d2..e8938bea 100644 --- a/packages/sms/src/models/v1/parameter-obj-parameter-key/parameter-obj-parameter-key.ts +++ b/packages/sms/src/models/v1/parameter-values/parameter-values.ts @@ -1,6 +1,14 @@ /** * The name of the parameter that will be replaced in the message body. Letters A-Z and a-z, digits 0-9 and .-_ allowed. */ +export interface ParameterValues { + /** The key is the recipient that should have the `parameter_key` replaced with the value */ + [msisdn: string]: string | undefined; + /** The fall-back value for omitted recipient phone numbers MSISDNs. */ + default?: string; +} + +/** @deprecated Use ParameterValues instead */ export interface ParameterObjParameterKey { /** The key is the recipient that should have the `parameter_key` replaced with the value */ diff --git a/packages/sms/src/models/v1/text-request/text-request.ts b/packages/sms/src/models/v1/text-request/text-request.ts index 6bd814ec..8fdb6fa3 100644 --- a/packages/sms/src/models/v1/text-request/text-request.ts +++ b/packages/sms/src/models/v1/text-request/text-request.ts @@ -1,4 +1,4 @@ -import { ParameterObj } from '../parameter-obj'; +import { ParameterGroup } from '../parameter-group'; import { DeliveryReportEnum } from '../enums'; export interface TextRequest { @@ -6,8 +6,8 @@ export interface TextRequest { to: string[]; /** Sender number. Must be valid phone number, short code or alphanumeric. Required if Automatic Default Originator not configured. */ from?: string; - /** @see ParameterObj */ - parameters?: ParameterObj; + /** @see ParameterGroup */ + parameters?: ParameterGroup; /** The message content */ body: string; /** Regular SMS */ diff --git a/packages/sms/src/models/v1/text-response/text-response.ts b/packages/sms/src/models/v1/text-response/text-response.ts index 292c90f6..fc27053a 100644 --- a/packages/sms/src/models/v1/text-response/text-response.ts +++ b/packages/sms/src/models/v1/text-response/text-response.ts @@ -1,4 +1,4 @@ -import { ParameterObj } from '../parameter-obj'; +import { ParameterGroup } from '../parameter-group'; import { DeliveryReportEnum } from '../enums'; export interface TextResponse { @@ -10,8 +10,8 @@ export interface TextResponse { from?: string; /** Indicates if the batch has been canceled or not. */ canceled?: boolean; - /** @see ParameterObj */ - parameters?: ParameterObj; + /** @see ParameterGroup */ + parameters?: ParameterGroup; /** The message content */ body?: string; /** Regular SMS */ diff --git a/packages/sms/tests/rest/v1/batches/batches.steps.ts b/packages/sms/tests/rest/v1/batches/batches.steps.ts index 88ec91a8..be06582d 100644 --- a/packages/sms/tests/rest/v1/batches/batches.steps.ts +++ b/packages/sms/tests/rest/v1/batches/batches.steps.ts @@ -2,6 +2,7 @@ import { BatchesApi, SmsService, Sms } from '../../../../src'; import { Given, When, Then } from '@cucumber/cucumber'; import * as assert from 'assert'; import { PageResult } from '@sinch/sdk-client'; +import { ParameterGroup } from '../../../../src/models'; let batchesApi: BatchesApi; let sendSmsResponse: Sms.TextResponse; @@ -54,6 +55,54 @@ Then('the response contains the text SMS details', () => { assert.equal(sendSmsResponse.flash_message, false); }); +When('I send a request to send a text message with multiple parameters', async () => { + const sendSmsRequest: Sms.SendTextSMSRequestData = { + sendSMSRequestBody: { + body: 'Hello ${name}! Get 20% off with this discount code ${code}', + to: ['+12017777777', '+12018888888'], + from: '+12015555555', + parameters: { + name: { + '+12017777777': 'John', + '+12018888888': 'Paul', + default: 'there', + }, + code: { + '+12017777777': 'HALLOWEEN20 ๐ŸŽƒ', + }, + }, + delivery_report: 'full', + }, + }; + sendSmsResponse = await batchesApi.sendTextMessage(sendSmsRequest); +}); + +Then('the response contains the text SMS details with multiple parameters', () => { + assert.equal(sendSmsResponse.id, '01W4FFL35P4NC4K35SMSBATCH2'); + assert.deepEqual(sendSmsResponse.to, ['12017777777', '12018888888']); + assert.equal(sendSmsResponse.from, '12015555555'); + assert.equal(sendSmsResponse.canceled, false); + const parameters: ParameterGroup = { + name: { + default: 'there', + '+12017777777': 'John', + '+12018888888': 'Paul', + }, + code: { + '+12017777777': 'HALLOWEEN20 ๐ŸŽƒ', + }, + }; + assert.deepEqual(sendSmsResponse.parameters, parameters); + assert.equal(sendSmsResponse.body, 'Hello ${name}! Get 20% off with this discount code ${code}'); + assert.equal(sendSmsResponse.type, 'mt_text'); + assert.deepEqual(sendSmsResponse.created_at, new Date('2024-06-06T09:22:14.304Z')); + assert.deepEqual(sendSmsResponse.modified_at, new Date('2024-06-06T09:22:14.304Z')); + const fullDeliveryReport: Sms.DeliveryReportEnum = 'full'; + assert.equal(sendSmsResponse.delivery_report, fullDeliveryReport); + assert.deepEqual(sendSmsResponse.expire_at, new Date('2024-06-06T09:22:14.304Z')); + assert.equal(sendSmsResponse.flash_message, false); +}); + When('I send a request to perform a dry run of a batch', async () => { const sendSmsRequest: Sms.DryRunRequestData = { dryRunRequestBody: { @@ -154,19 +203,19 @@ When('I send a request to retrieve an SMS batch', async () => { Then('the response contains the SMS batch details', () => { assert.equal(batch.id, '01W4FFL35P4NC4K35SMSBATCH1'); - assert.deepEqual(sendSmsResponse.to, ['12017777777']); - assert.equal(sendSmsResponse.from, '12015555555'); - assert.equal(sendSmsResponse.canceled, false); - assert.equal(sendSmsResponse.body, 'SMS body message'); - assert.equal(sendSmsResponse.type, 'mt_text'); - assert.deepEqual(sendSmsResponse.created_at, new Date('2024-06-06T09:22:14.304Z')); - assert.deepEqual(sendSmsResponse.modified_at, new Date('2024-06-06T09:22:14.304Z')); + assert.deepEqual(batch.to, ['12017777777']); + assert.equal(batch.from, '12015555555'); + assert.equal(batch.canceled, false); + assert.equal(batch.body, 'SMS body message'); + assert.equal(batch.type, 'mt_text'); + assert.deepEqual(batch.created_at, new Date('2024-06-06T09:22:14.304Z')); + assert.deepEqual(batch.modified_at, new Date('2024-06-06T09:22:14.304Z')); const fullDeliveryReport: Sms.DeliveryReportEnum = 'full'; - assert.equal(sendSmsResponse.delivery_report, fullDeliveryReport); - assert.deepEqual(sendSmsResponse.send_at, new Date('2024-06-06T09:25:00Z')); - assert.deepEqual(sendSmsResponse.expire_at, new Date('2024-06-09T09:25:00Z')); - assert.equal(sendSmsResponse.feedback_enabled, true); - assert.equal(sendSmsResponse.flash_message, false); + assert.equal(batch.delivery_report, fullDeliveryReport); + assert.deepEqual(batch.send_at, new Date('2024-06-06T09:25:00Z')); + assert.deepEqual(batch.expire_at, new Date('2024-06-09T09:25:00Z')); + assert.equal(batch.feedback_enabled, true); + assert.equal((batch as Sms.TextResponse).flash_message, false); }); When('I send a request to update an SMS batch', async () => { From 6ae92aa27ee843afdc916a7414a585cc68b27e80 Mon Sep 17 00:00:00 2001 From: Antoine SEIN <142824551+asein-sinch@users.noreply.github.com> Date: Tue, 1 Oct 2024 19:12:44 +0200 Subject: [PATCH 49/54] Update Voice E2E tests (#141) --- packages/voice/tests/rest/v1/callbacks/webhooks-events.steps.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/voice/tests/rest/v1/callbacks/webhooks-events.steps.ts b/packages/voice/tests/rest/v1/callbacks/webhooks-events.steps.ts index e997d8ad..88581467 100644 --- a/packages/voice/tests/rest/v1/callbacks/webhooks-events.steps.ts +++ b/packages/voice/tests/rest/v1/callbacks/webhooks-events.steps.ts @@ -21,7 +21,7 @@ const processEvent = async (response: Response) => { Given('the Voice Webhooks handler is available', () => { voiceCallbackWebhooks = new VoiceCallbackWebhooks({ applicationKey: 'appKey', - applicationSecret: 'appSecret', + applicationSecret: 'YXBwU2VjcmV0', // base64 value for 'appSecret' }); }); From 281ca43faf264f9d7755967aa735c31458a6fb2b Mon Sep 17 00:00:00 2001 From: Antoine SEIN <142824551+asein-sinch@users.noreply.github.com> Date: Wed, 2 Oct 2024 08:22:48 +0200 Subject: [PATCH 50/54] DEVEXP-590: Improve CI performance (#142) --- .github/workflows/run-ci.yaml | 38 +++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/.github/workflows/run-ci.yaml b/.github/workflows/run-ci.yaml index cae140be..8ff44423 100644 --- a/.github/workflows/run-ci.yaml +++ b/.github/workflows/run-ci.yaml @@ -1,30 +1,32 @@ name: Build and test Sinch Node.js SDK -on: [push] +on: [push, pull_request] jobs: build: runs-on: ubuntu-latest strategy: matrix: - node-version: [18.x, 20.x] + node-version: [18.x, 20.x, 22.x] steps: - uses: actions/checkout@v3 - - name: Use Node.js ${{ matrix.node-version }} + - name: Set up Node.js ${{ matrix.node-version }} uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} - - run: yarn install - - run: npx eslint "packages/**/src/**/*.ts" - - run: npx eslint "packages/**/tests/**/*.ts" - - run: yarn run build - - run: yarn run test - e2e: - needs: build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 + - name: Install dependencies + run: yarn install + + - name: Run ESLint + run: npx eslint "packages/**/{src,tests}/**/*.ts" + + - name: Build project + run: yarn run build + + - name: Run unit tests + run: yarn run test + - name: Checkout sinch-sdk-mockserver repository uses: actions/checkout@v3 with: @@ -32,14 +34,17 @@ jobs: token: ${{ secrets.PAT_CI }} fetch-depth: 0 path: sinch-sdk-mockserver + - name: Install Docker Compose run: | sudo apt-get update sudo apt-get install -y docker-compose + - name: Start mock servers with Docker Compose run: | cd sinch-sdk-mockserver docker-compose up -d + - name: Create target directories for feature files run: | mkdir -p ./packages/fax/tests/e2e/features @@ -49,6 +54,7 @@ jobs: mkdir -p ./packages/sms/tests/e2e/features mkdir -p ./packages/verification/tests/e2e/features mkdir -p ./packages/voice/tests/e2e/features + - name: Copy feature files run: | cp sinch-sdk-mockserver/features/fax/*.feature ./packages/fax/tests/e2e/features/ @@ -58,11 +64,9 @@ jobs: cp sinch-sdk-mockserver/features/sms/*.feature ./packages/sms/tests/e2e/features/ cp sinch-sdk-mockserver/features/verification/*.feature ./packages/verification/tests/e2e/features/ cp sinch-sdk-mockserver/features/voice/*.feature ./packages/voice/tests/e2e/features/ + - name: Run e2e tests - run: | - yarn install - yarn run build - yarn run e2e + run: yarn run e2e sonarcloud: runs-on: ubuntu-latest From 6f2ad58611ca8cf4d1d6b75c7984f736924317d0 Mon Sep 17 00:00:00 2001 From: Antoine SEIN <142824551+asein-sinch@users.noreply.github.com> Date: Wed, 2 Oct 2024 14:20:27 +0200 Subject: [PATCH 51/54] DEVEXP-584: Prepare release 1.2.0 (#143) --- .github/workflows/run-ci.yaml | 2 +- examples/integrated-flows-examples/package.json | 2 +- examples/simple-examples/package.json | 2 +- examples/webhooks/package.json | 2 +- packages/conversation/package.json | 4 ++-- packages/elastic-sip-trunking/package.json | 4 ++-- packages/fax/package.json | 4 ++-- packages/numbers/package.json | 4 ++-- packages/sdk-client/package.json | 2 +- packages/sdk-core/package.json | 16 ++++++++-------- packages/sms/package.json | 4 ++-- packages/verification/package.json | 4 ++-- packages/voice/package.json | 4 ++-- 13 files changed, 27 insertions(+), 27 deletions(-) diff --git a/.github/workflows/run-ci.yaml b/.github/workflows/run-ci.yaml index 8ff44423..acf12067 100644 --- a/.github/workflows/run-ci.yaml +++ b/.github/workflows/run-ci.yaml @@ -1,6 +1,6 @@ name: Build and test Sinch Node.js SDK -on: [push, pull_request] +on: [push] jobs: build: diff --git a/examples/integrated-flows-examples/package.json b/examples/integrated-flows-examples/package.json index 4b626f61..20a364ba 100644 --- a/examples/integrated-flows-examples/package.json +++ b/examples/integrated-flows-examples/package.json @@ -13,7 +13,7 @@ "verification:app": "yarn compile && node dist/verification/app.js" }, "dependencies": { - "@sinch/sdk-core": "^1.1.0", + "@sinch/sdk-core": "^1.2.0", "@types/node": "^20.8.7", "dotenv": "^16.3.1", "inquirer": "^9.2.14", diff --git a/examples/simple-examples/package.json b/examples/simple-examples/package.json index 4b72e1bb..936e28e8 100644 --- a/examples/simple-examples/package.json +++ b/examples/simple-examples/package.json @@ -183,7 +183,7 @@ "voice:conferences:kickAll": "ts-node src/voice/conferences/kickAll.ts" }, "dependencies": { - "@sinch/sdk-core": "^1.1.0", + "@sinch/sdk-core": "^1.2.0", "dotenv": "^16.3.1" }, "devDependencies": { diff --git a/examples/webhooks/package.json b/examples/webhooks/package.json index a5f52dbd..252af88e 100644 --- a/examples/webhooks/package.json +++ b/examples/webhooks/package.json @@ -15,7 +15,7 @@ "@nestjs/common": "^10.4.4", "@nestjs/core": "^10.4.4", "@nestjs/platform-express": "^10.4.4", - "@sinch/sdk-core": "^1.1.0", + "@sinch/sdk-core": "^1.2.0", "dotenv": "^16.3.1", "raw-body": "^2.5.2", "reflect-metadata": "^0.1.13", diff --git a/packages/conversation/package.json b/packages/conversation/package.json index 2e8969cd..29ff5dcb 100644 --- a/packages/conversation/package.json +++ b/packages/conversation/package.json @@ -1,6 +1,6 @@ { "name": "@sinch/conversation", - "version": "1.1.0", + "version": "1.2.0", "description": "Sinch Conversation API", "homepage": "", "repository": { @@ -29,7 +29,7 @@ "test:e2e": "cucumber-js" }, "dependencies": { - "@sinch/sdk-client": "^1.1.0" + "@sinch/sdk-client": "^1.2.0" }, "devDependencies": {}, "publishConfig": { diff --git a/packages/elastic-sip-trunking/package.json b/packages/elastic-sip-trunking/package.json index f07be460..89ea1cd6 100644 --- a/packages/elastic-sip-trunking/package.json +++ b/packages/elastic-sip-trunking/package.json @@ -1,6 +1,6 @@ { "name": "@sinch/elastic-sip-trunking", - "version": "1.1.0", + "version": "1.2.0", "description": "Sinch Elastic SIP Trunking API", "homepage": "", "repository": { @@ -28,7 +28,7 @@ "test:e2e": "cucumber-js" }, "dependencies": { - "@sinch/sdk-client": "^1.0.0" + "@sinch/sdk-client": "^1.2.0" }, "devDependencies": {}, "publishConfig": { diff --git a/packages/fax/package.json b/packages/fax/package.json index edeae1ce..1024cc2d 100644 --- a/packages/fax/package.json +++ b/packages/fax/package.json @@ -1,6 +1,6 @@ { "name": "@sinch/fax", - "version": "1.1.0", + "version": "1.2.0", "description": "Sinch Fax API", "homepage": "", "repository": { @@ -29,7 +29,7 @@ "test:e2e": "cucumber-js" }, "dependencies": { - "@sinch/sdk-client": "^1.1.0" + "@sinch/sdk-client": "^1.2.0" }, "devDependencies": {}, "publishConfig": { diff --git a/packages/numbers/package.json b/packages/numbers/package.json index 785e8675..a3459355 100644 --- a/packages/numbers/package.json +++ b/packages/numbers/package.json @@ -1,6 +1,6 @@ { "name": "@sinch/numbers", - "version": "1.1.0", + "version": "1.2.0", "description": "Sinch Numbers API", "homepage": "", "repository": { @@ -29,7 +29,7 @@ "test:e2e": "cucumber-js" }, "dependencies": { - "@sinch/sdk-client": "^1.1.0" + "@sinch/sdk-client": "^1.2.0" }, "devDependencies": {}, "publishConfig": { diff --git a/packages/sdk-client/package.json b/packages/sdk-client/package.json index c87ee88a..53610b59 100644 --- a/packages/sdk-client/package.json +++ b/packages/sdk-client/package.json @@ -1,6 +1,6 @@ { "name": "@sinch/sdk-client", - "version": "1.1.0", + "version": "1.2.0", "description": "Core services related to interacting with Sinch API", "homepage": "", "repository": { diff --git a/packages/sdk-core/package.json b/packages/sdk-core/package.json index 6df0f589..92960823 100644 --- a/packages/sdk-core/package.json +++ b/packages/sdk-core/package.json @@ -1,6 +1,6 @@ { "name": "@sinch/sdk-core", - "version": "1.1.0", + "version": "1.2.0", "description": "Node.js client for the Sinch API platform", "homepage": "", "repository": { @@ -29,13 +29,13 @@ "compile": "tsc --build --verbose" }, "dependencies": { - "@sinch/conversation": "^1.1.0", - "@sinch/elastic-sip-trunking": "^1.1.0", - "@sinch/fax": "^1.1.0", - "@sinch/numbers": "^1.1.0", - "@sinch/sms": "^1.1.0", - "@sinch/verification": "^1.1.0", - "@sinch/voice": "^1.1.0" + "@sinch/conversation": "^1.2.0", + "@sinch/elastic-sip-trunking": "^1.2.0", + "@sinch/fax": "^1.2.0", + "@sinch/numbers": "^1.2.0", + "@sinch/sms": "^1.2.0", + "@sinch/verification": "^1.2.0", + "@sinch/voice": "^1.2.0" }, "devDependencies": {}, "publishConfig": { diff --git a/packages/sms/package.json b/packages/sms/package.json index dea593ab..5ca29641 100644 --- a/packages/sms/package.json +++ b/packages/sms/package.json @@ -1,6 +1,6 @@ { "name": "@sinch/sms", - "version": "1.1.0", + "version": "1.2.0", "description": "Sinch SMS API", "homepage": "", "repository": { @@ -29,7 +29,7 @@ "test:e2e": "cucumber-js" }, "dependencies": { - "@sinch/sdk-client": "^1.1.0" + "@sinch/sdk-client": "^1.2.0" }, "devDependencies": {}, "publishConfig": { diff --git a/packages/verification/package.json b/packages/verification/package.json index 1213ac53..2dbfc3e0 100644 --- a/packages/verification/package.json +++ b/packages/verification/package.json @@ -1,6 +1,6 @@ { "name": "@sinch/verification", - "version": "1.1.0", + "version": "1.2.0", "description": "Sinch Verification API", "homepage": "", "repository": { @@ -29,7 +29,7 @@ "test:e2e": "cucumber-js" }, "dependencies": { - "@sinch/sdk-client": "^1.1.0" + "@sinch/sdk-client": "^1.2.0" }, "devDependencies": {}, "publishConfig": { diff --git a/packages/voice/package.json b/packages/voice/package.json index 3a5528f4..4ad1b2b7 100644 --- a/packages/voice/package.json +++ b/packages/voice/package.json @@ -1,6 +1,6 @@ { "name": "@sinch/voice", - "version": "1.1.0", + "version": "1.2.0", "description": "Sinch Voice API", "homepage": "", "repository": { @@ -29,7 +29,7 @@ "test:e2e": "cucumber-js" }, "dependencies": { - "@sinch/sdk-client": "^1.1.0" + "@sinch/sdk-client": "^1.2.0" }, "devDependencies": {}, "publishConfig": { From 7a33959b07f589a7202971845e002e2048bf4c5f Mon Sep 17 00:00:00 2001 From: Antoine SEIN <142824551+asein-sinch@users.noreply.github.com> Date: Thu, 3 Oct 2024 12:03:23 +0200 Subject: [PATCH 52/54] DEVEXP-592: Add workflow for publication (#144) --- .github/workflows/publish.yaml | 85 ++++++++++++++++++++++ packages/conversation/package.json | 3 +- packages/elastic-sip-trunking/package.json | 3 +- packages/fax/package.json | 3 +- packages/numbers/package.json | 3 +- packages/sdk-client/package.json | 3 +- packages/sdk-core/package.json | 3 +- packages/sms/package.json | 3 +- packages/verification/package.json | 3 +- packages/voice/package.json | 3 +- 10 files changed, 103 insertions(+), 9 deletions(-) create mode 100644 .github/workflows/publish.yaml diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml new file mode 100644 index 00000000..a2c43d53 --- /dev/null +++ b/.github/workflows/publish.yaml @@ -0,0 +1,85 @@ +name: Publish Packages + +on: + workflow_dispatch: + +jobs: + publish: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: '18.x' + registry-url: 'https://registry.npmjs.org' + + - name: Authenticate to npm + run: echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc + + - name: Install dependencies + run: yarn install + + - name: Build packages + run: yarn run build + + - name: Run tests + run: yarn run test + + - name: Checkout sinch-sdk-mockserver repository + uses: actions/checkout@v3 + with: + repository: sinch/sinch-sdk-mockserver + token: ${{ secrets.PAT_CI }} + fetch-depth: 0 + path: sinch-sdk-mockserver + + - name: Install Docker Compose + run: | + sudo apt-get update + sudo apt-get install -y docker-compose + + - name: Start mock servers with Docker Compose + run: | + cd sinch-sdk-mockserver + docker-compose up -d + + - name: Create target directories for feature files + run: | + mkdir -p ./packages/fax/tests/e2e/features + mkdir -p ./packages/numbers/tests/e2e/features + mkdir -p ./packages/conversation/tests/e2e/features + mkdir -p ./packages/elastic-sip-trunking/tests/e2e/features + mkdir -p ./packages/sms/tests/e2e/features + mkdir -p ./packages/verification/tests/e2e/features + mkdir -p ./packages/voice/tests/e2e/features + + - name: Copy feature files + run: | + cp sinch-sdk-mockserver/features/fax/*.feature ./packages/fax/tests/e2e/features/ + cp sinch-sdk-mockserver/features/numbers/*.feature ./packages/numbers/tests/e2e/features/ + cp sinch-sdk-mockserver/features/conversation/*.feature ./packages/conversation/tests/e2e/features/ + cp sinch-sdk-mockserver/features/elastic-sip-trunking/*.feature ./packages/elastic-sip-trunking/tests/e2e/features/ + cp sinch-sdk-mockserver/features/sms/*.feature ./packages/sms/tests/e2e/features/ + cp sinch-sdk-mockserver/features/verification/*.feature ./packages/verification/tests/e2e/features/ + cp sinch-sdk-mockserver/features/voice/*.feature ./packages/voice/tests/e2e/features/ + + - name: Run e2e tests + run: yarn run e2e + + - name: Publish packages + run: | + cd packages/sdk-client && npm publish + cd ../numbers && npm publish + cd ../sms && npm publish + cd ../verification && npm publish + cd ../voice && npm publish + cd ../conversation && npm publish + cd ../fax && npm publish + cd ../elastic-sip-trunking && npm publish + cd ../sdk-core && npm publish + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/packages/conversation/package.json b/packages/conversation/package.json index 29ff5dcb..4a71052a 100644 --- a/packages/conversation/package.json +++ b/packages/conversation/package.json @@ -33,6 +33,7 @@ }, "devDependencies": {}, "publishConfig": { - "directory": "dist" + "directory": "dist", + "access": "public" } } diff --git a/packages/elastic-sip-trunking/package.json b/packages/elastic-sip-trunking/package.json index 89ea1cd6..019438f2 100644 --- a/packages/elastic-sip-trunking/package.json +++ b/packages/elastic-sip-trunking/package.json @@ -32,6 +32,7 @@ }, "devDependencies": {}, "publishConfig": { - "directory": "dist" + "directory": "dist", + "access": "public" } } diff --git a/packages/fax/package.json b/packages/fax/package.json index 1024cc2d..05b8b216 100644 --- a/packages/fax/package.json +++ b/packages/fax/package.json @@ -33,6 +33,7 @@ }, "devDependencies": {}, "publishConfig": { - "directory": "dist" + "directory": "dist", + "access": "public" } } diff --git a/packages/numbers/package.json b/packages/numbers/package.json index a3459355..9ddf77d7 100644 --- a/packages/numbers/package.json +++ b/packages/numbers/package.json @@ -33,6 +33,7 @@ }, "devDependencies": {}, "publishConfig": { - "directory": "dist" + "directory": "dist", + "access": "public" } } diff --git a/packages/sdk-client/package.json b/packages/sdk-client/package.json index 53610b59..05fa28f5 100644 --- a/packages/sdk-client/package.json +++ b/packages/sdk-client/package.json @@ -38,6 +38,7 @@ "typescript": "^5.2.2" }, "publishConfig": { - "directory": "dist" + "directory": "dist", + "access": "public" } } diff --git a/packages/sdk-core/package.json b/packages/sdk-core/package.json index 92960823..8d9d5c40 100644 --- a/packages/sdk-core/package.json +++ b/packages/sdk-core/package.json @@ -39,6 +39,7 @@ }, "devDependencies": {}, "publishConfig": { - "directory": "dist" + "directory": "dist", + "access": "public" } } diff --git a/packages/sms/package.json b/packages/sms/package.json index 5ca29641..4c5552f3 100644 --- a/packages/sms/package.json +++ b/packages/sms/package.json @@ -33,6 +33,7 @@ }, "devDependencies": {}, "publishConfig": { - "directory": "dist" + "directory": "dist", + "access": "public" } } diff --git a/packages/verification/package.json b/packages/verification/package.json index 2dbfc3e0..63213a36 100644 --- a/packages/verification/package.json +++ b/packages/verification/package.json @@ -33,6 +33,7 @@ }, "devDependencies": {}, "publishConfig": { - "directory": "dist" + "directory": "dist", + "access": "public" } } diff --git a/packages/voice/package.json b/packages/voice/package.json index 4ad1b2b7..a27aff2e 100644 --- a/packages/voice/package.json +++ b/packages/voice/package.json @@ -33,6 +33,7 @@ }, "devDependencies": {}, "publishConfig": { - "directory": "dist" + "directory": "dist", + "access": "public" } } From cb020a5ed74a3b3561799171ffbde4f5a12bcd65 Mon Sep 17 00:00:00 2001 From: Antoine SEIN <142824551+asein-sinch@users.noreply.github.com> Date: Thu, 3 Oct 2024 17:44:36 +0200 Subject: [PATCH 53/54] DEVEXP-594: Update SMS authentication (#145) --- packages/sdk-client/CHANGELOG.md | 3 + .../src/api/api-client-options-helper.ts | 45 +++++------ .../sdk-client/src/domain/domain-interface.ts | 4 +- .../api/api-client-options-helper.test.ts | 79 +++++-------------- packages/sms/src/rest/v1/sms-domain-api.ts | 2 +- packages/sms/tests/rest/v1/sms-api.test.ts | 43 +++++++--- 6 files changed, 75 insertions(+), 101 deletions(-) diff --git a/packages/sdk-client/CHANGELOG.md b/packages/sdk-client/CHANGELOG.md index e4a116e0..1d9f78ba 100644 --- a/packages/sdk-client/CHANGELOG.md +++ b/packages/sdk-client/CHANGELOG.md @@ -3,6 +3,9 @@ - [Feature] Remove the `TimezoneResponse` plugin and autocorrect the timezones when reviving the dates in the API responses. - [Feature] Add utility methods to support the dateRange filters for EST and Fax APIs. - [Bugfix] Handle HTTP status 202 in the `ExceptionResponse` plugin. +- [Feature][Functionality change] Implement new strategy for selecting which credentials to use with the SMS API. +- [Deprecation Notice] The property `forceOAuth2ForSmsApi` in the `UnifiedCredentials` is deprecated and has no effect anymore. +- [Deprecation Notice] The property `forceServicePlanIdUsageForSmsApi` in the `ServicePlanIdCredentials` is deprecated and has no effect anymore. ## Version 1.1.0 - [Feature] Add new pagination type support specific to EST diff --git a/packages/sdk-client/src/api/api-client-options-helper.ts b/packages/sdk-client/src/api/api-client-options-helper.ts index a0b7ba22..81686770 100644 --- a/packages/sdk-client/src/api/api-client-options-helper.ts +++ b/packages/sdk-client/src/api/api-client-options-helper.ts @@ -1,4 +1,4 @@ -import { SmsRegion, SinchClientParameters } from '../domain'; +import { SinchClientParameters } from '../domain'; import { ApiClientOptions } from './api-client-options'; import { ApiTokenRequest, @@ -36,36 +36,27 @@ export const buildApplicationSignedApiClientOptions = ( return apiClientOptions; }; -export const buildFlexibleOAuth2OrApiTokenApiClientOptions = ( - params: SinchClientParameters, region: SmsRegion, apiName: string, -): ApiClientOptions => { +export const buildFlexibleOAuth2OrApiTokenApiClientOptions = (params: SinchClientParameters): ApiClientOptions => { let apiClientOptions: ApiClientOptions | undefined; - // Check the region: if US or EU, try to use the OAuth2 authentication with the access key / secret under the project Id - if ( params.forceOAuth2ForSmsApi - || (!params.forceServicePlanIdUsageForSmsApi && (region === SmsRegion.UNITED_STATES || region === SmsRegion.EUROPE)) - ) { - // Let's check the required parameters for OAuth2 authentication - if (params.projectId && params.keyId && params.keySecret) { - apiClientOptions = { - projectId: params.projectId, - requestPlugins: [new Oauth2TokenRequest(params.keyId, params.keySecret, params.authHostname)], - useServicePlanId: false, - }; - } - } - if (!apiClientOptions) { - // The API client options couldn't be initialized for with the projectId unified authentication. - // Let's try with the servicePlanId - if (params.servicePlanId && params.apiToken) { - apiClientOptions = { - projectId: params.servicePlanId, - requestPlugins: [new ApiTokenRequest(params.apiToken)], - useServicePlanId: true, - }; + + if (params.servicePlanId && params.apiToken) { + apiClientOptions = { + projectId: params.servicePlanId, + requestPlugins: [new ApiTokenRequest(params.apiToken)], + useServicePlanId: true, + }; + if (params.projectId || params.keyId || params.keySecret) { + console.warn('As the servicePlanId and the apiToken are provided, all other credentials will be disregarded.'); } + } else if (params.projectId && params.keyId && params.keySecret) { + apiClientOptions = { + projectId: params.projectId, + requestPlugins: [new Oauth2TokenRequest(params.keyId, params.keySecret, params.authHostname)], + useServicePlanId: false, + }; } if (!apiClientOptions) { - throw new Error(`Invalid parameters for the ${apiName} API: check your configuration`); + throw new Error('Invalid parameters for the SMS API: check your configuration'); } addPlugins(apiClientOptions, params); return apiClientOptions; diff --git a/packages/sdk-client/src/domain/domain-interface.ts b/packages/sdk-client/src/domain/domain-interface.ts index 686be565..21f406b3 100644 --- a/packages/sdk-client/src/domain/domain-interface.ts +++ b/packages/sdk-client/src/domain/domain-interface.ts @@ -24,7 +24,7 @@ export interface UnifiedCredentials { keySecret: string; /** The region for the SMS API. Default region is US */ smsRegion?: SmsRegion; - /** boolean to force the usage of the OAuth2 authentication for the SMS API - to be used when a region other of US and EU supports OAuth2 but the SDK doesn't by default */ + /** @deprecated boolean to force the usage of the OAuth2 authentication for the SMS API - to be used when a region other of US and EU supports OAuth2 but the SDK doesn't by default */ forceOAuth2ForSmsApi?: boolean; /** The region for the Fax API. Default is auto-routing */ faxRegion?: FaxRegion; @@ -37,7 +37,7 @@ export interface ServicePlanIdCredentials { servicePlanId: string; /** Your API token. You can find this on your [Dashboard](https://dashboard.sinch.com/sms/api/rest). */ apiToken: string; - /** boolean to force the usage of the service plan Id + API token as credentials for the SMS API */ + /** @deprecated boolean to force the usage of the service plan Id + API token as credentials for the SMS API */ forceServicePlanIdUsageForSmsApi?: boolean; /** The region for the SMS API. Default region is US */ smsRegion?: SmsRegion; diff --git a/packages/sdk-client/tests/api/api-client-options-helper.test.ts b/packages/sdk-client/tests/api/api-client-options-helper.test.ts index af0f7cb6..7f404019 100644 --- a/packages/sdk-client/tests/api/api-client-options-helper.test.ts +++ b/packages/sdk-client/tests/api/api-client-options-helper.test.ts @@ -5,7 +5,6 @@ import { buildOAuth2ApiClientOptions, Oauth2TokenRequest, PluginRunner, - SmsRegion, SigningRequest, SinchClientParameters, XTimestampRequest, @@ -147,51 +146,48 @@ describe('API Client Options helper', () => { describe('buildFlexibleOAuth2OrApiTokenApiClientOptions', () => { + // eslint-disable-next-line max-len - it('should build some API client options to perform OAuth2 authentication when the credentials are present and the region is supported', () => { + it('should build some ApiClientOptions to perform OAuth2 authentication when only unified credentials are provided', () => { // Given const params: SinchClientParameters = { projectId: 'PROJECT_ID', keyId: 'KEY_ID', keySecret: 'KEY_SECRET', - servicePlanId: 'SERVICE_PLAN_ID', - apiToken: 'API_TOKEN', }; - const region = SmsRegion.EUROPE; // When - const apiClientOptions = buildFlexibleOAuth2OrApiTokenApiClientOptions(params, region, 'foo'); + const apiClientOptions = buildFlexibleOAuth2OrApiTokenApiClientOptions(params); // Then expect(apiClientOptions).toBeDefined(); + expect(apiClientOptions.useServicePlanId).toBeFalsy(); expect(apiClientOptions.requestPlugins?.length).toBe(1); expect(apiClientOptions.requestPlugins?.[0]).toBeInstanceOf(Oauth2TokenRequest); expect(apiClientOptions.responsePlugins).toBeUndefined(); }); // eslint-disable-next-line max-len - it('should build some API client options to perform API token authentication when the credentials are present and the region is not supported by OAuth2 authentication', () => { + it('should build some ApiClientOptions to perform API token authentication when only SMS credentials are provided', () => { // Given const params: SinchClientParameters = { - projectId: 'PROJECT_ID', - keyId: 'KEY_ID', - keySecret: 'KEY_SECRET', servicePlanId: 'SERVICE_PLAN_ID', apiToken: 'API_TOKEN', }; - const region = SmsRegion.CANADA; // When - const apiClientOptions = buildFlexibleOAuth2OrApiTokenApiClientOptions(params, region, 'foo'); + const apiClientOptions = buildFlexibleOAuth2OrApiTokenApiClientOptions(params); // Then expect(apiClientOptions).toBeDefined(); + expect(apiClientOptions.useServicePlanId).toBeTruthy(); expect(apiClientOptions.requestPlugins?.length).toBe(1); expect(apiClientOptions.requestPlugins?.[0]).toBeInstanceOf(ApiTokenRequest); expect(apiClientOptions.responsePlugins).toBeUndefined(); }); - it('should force the usage of API token authentication even when the region supports OAuth2 authentication', () => { + // eslint-disable-next-line max-len + it('should build some ApiClientOptions to perform API token authentication when both set of credentials are provided', () => { // Given const params: SinchClientParameters = { projectId: 'PROJECT_ID', @@ -199,21 +195,23 @@ describe('API Client Options helper', () => { keySecret: 'KEY_SECRET', servicePlanId: 'SERVICE_PLAN_ID', apiToken: 'API_TOKEN', - forceServicePlanIdUsageForSmsApi: true, }; - const region = SmsRegion.EUROPE; + console.warn = jest.fn(); // When - const apiClientOptions = buildFlexibleOAuth2OrApiTokenApiClientOptions(params, region, 'foo'); + const apiClientOptions = buildFlexibleOAuth2OrApiTokenApiClientOptions(params); // Then expect(apiClientOptions).toBeDefined(); + expect(apiClientOptions.useServicePlanId).toBeTruthy(); expect(apiClientOptions.requestPlugins?.length).toBe(1); expect(apiClientOptions.requestPlugins?.[0]).toBeInstanceOf(ApiTokenRequest); expect(apiClientOptions.responsePlugins).toBeUndefined(); + expect(console.warn).toHaveBeenCalledWith( + 'As the servicePlanId and the apiToken are provided, all other credentials will be disregarded.'); }); - it('should build some API client options with additional plugins', () => { + it('should build some ApiClientOptions with additional plugins', () => { // Given const params: SinchClientParameters = { servicePlanId: 'SERVICE_PLAN_ID', @@ -221,10 +219,9 @@ describe('API Client Options helper', () => { requestPlugins: [dummyRequestPlugin], responsePlugins: [dummyResponsePlugin], }; - const region = SmsRegion.CANADA; // When - const apiClientOptions = buildFlexibleOAuth2OrApiTokenApiClientOptions(params, region, 'foo'); + const apiClientOptions = buildFlexibleOAuth2OrApiTokenApiClientOptions(params); // Then expect(apiClientOptions.requestPlugins?.length).toBe(2); @@ -237,13 +234,12 @@ describe('API Client Options helper', () => { const params: SinchClientParameters = { projectId: 'PROJECT_ID', }; - const region = SmsRegion.EUROPE; // When - const buildApiClientOptionsFunction = () => buildFlexibleOAuth2OrApiTokenApiClientOptions(params, region, 'foo'); + const buildApiClientOptionsFunction = () => buildFlexibleOAuth2OrApiTokenApiClientOptions(params); // Then - expect(buildApiClientOptionsFunction).toThrow('Invalid parameters for the foo API: check your configuration'); + expect(buildApiClientOptionsFunction).toThrow('Invalid parameters for the SMS API: check your configuration'); }); // eslint-disable-next-line max-len @@ -252,49 +248,14 @@ describe('API Client Options helper', () => { const params: SinchClientParameters = { servicePlanId: 'SERVICE_PLAN_ID', }; - const region = SmsRegion.EUROPE; // When - const buildApiClientOptionsFunction = () => buildFlexibleOAuth2OrApiTokenApiClientOptions(params, region, 'foo'); + const buildApiClientOptionsFunction = () => buildFlexibleOAuth2OrApiTokenApiClientOptions(params); // Then - expect(buildApiClientOptionsFunction).toThrow('Invalid parameters for the foo API: check your configuration'); + expect(buildApiClientOptionsFunction).toThrow('Invalid parameters for the SMS API: check your configuration'); }); - // eslint-disable-next-line max-len - it('should throw an exception when the parameters are inconsistent: unsupported region for OAuth2 authentication', () => { - // Given - const params: SinchClientParameters = { - projectId: 'PROJECT_ID', - keyId: 'KEY_ID', - keySecret: 'KEY_SECRET', - }; - const region = SmsRegion.CANADA; - - // When - const buildApiClientOptionsFunction = () => buildFlexibleOAuth2OrApiTokenApiClientOptions(params, region, 'foo'); - - // Then - expect(buildApiClientOptionsFunction).toThrow('Invalid parameters for the foo API: check your configuration'); - }); - - // eslint-disable-next-line max-len - it('should throw an exception when the parameters are inconsistent: missing parameters when forcing API Token authentication', () => { - // Given - const params: SinchClientParameters = { - projectId: 'PROJECT_ID', - keyId: 'KEY_ID', - keySecret: 'KEY_SECRET', - forceServicePlanIdUsageForSmsApi: true, - }; - const region = SmsRegion.EUROPE; - - // When - const buildApiClientOptionsFunction = () => buildFlexibleOAuth2OrApiTokenApiClientOptions(params, region, 'foo'); - - // Then - expect(buildApiClientOptionsFunction).toThrow('Invalid parameters for the foo API: check your configuration'); - }); }); }); diff --git a/packages/sms/src/rest/v1/sms-domain-api.ts b/packages/sms/src/rest/v1/sms-domain-api.ts index 1b372b05..eadc3e32 100644 --- a/packages/sms/src/rest/v1/sms-domain-api.ts +++ b/packages/sms/src/rest/v1/sms-domain-api.ts @@ -84,7 +84,7 @@ export class SmsDomainApi implements Api { if(!Object.values(SupportedSmsRegion).includes(region as SupportedSmsRegion)) { console.warn(`The region "${region}" is not known as a supported region for the SMS API`); } - const apiClientOptions = buildFlexibleOAuth2OrApiTokenApiClientOptions(this.sinchClientParameters, region, 'SMS'); + const apiClientOptions = buildFlexibleOAuth2OrApiTokenApiClientOptions(this.sinchClientParameters); this.client = new ApiFetchClient(apiClientOptions); const useZapStack = !this.client.apiClientOptions.useServicePlanId; this.client.apiClientOptions.hostname = this.sinchClientParameters.smsHostname diff --git a/packages/sms/tests/rest/v1/sms-api.test.ts b/packages/sms/tests/rest/v1/sms-api.test.ts index a4792982..08576075 100644 --- a/packages/sms/tests/rest/v1/sms-api.test.ts +++ b/packages/sms/tests/rest/v1/sms-api.test.ts @@ -44,7 +44,6 @@ describe('SMS API', () => { it('should log a warning when using an unsupported region', async () => { paramsWithProjectId.smsRegion = 'bzh'; - paramsWithProjectId.forceOAuth2ForSmsApi = true; smsApi = new SmsDomainApi(paramsWithProjectId, 'dummy'); console.warn = jest.fn(); smsApi.getSinchClient(); @@ -109,38 +108,58 @@ describe('SMS API', () => { expect(smsApi.client?.apiClientOptions.hostname).toBe('https://sms.api.sinch.com'); }); - it('should not update the credentials when adding servicePlanId credentials on default region', () => { + it('should update the credentials and URL when adding SMS credentials to the unified credentials', () => { smsApi = new SmsDomainApi(paramsWithProjectId, 'dummy'); smsApi.getSinchClient(); expect(smsApi.client?.apiClientOptions.projectId).toBe('PROJECT_ID'); expect(smsApi.client?.apiClientOptions.hostname).toBe('https://zt.us.sms.api.sinch.com'); - smsApi.setCredentials(paramsWithServicePlanId); - expect(smsApi.client?.apiClientOptions.projectId).toBe('PROJECT_ID'); - expect(smsApi.client?.apiClientOptions.hostname).toBe('https://zt.us.sms.api.sinch.com'); + smsApi.setCredentials({ + ...paramsWithServicePlanId, + }); + expect(smsApi.client?.apiClientOptions.projectId).toBe('SERVICE_PLAN_ID'); + expect(smsApi.client?.apiClientOptions.hostname).toBe('https://us.sms.api.sinch.com'); }); - it('should update the credentials and URL when forcing servicePlanId credentials', () => { + // eslint-disable-next-line max-len + it('should update the credentials and URL when adding SMS credentials on BR region to the unified credentials', () => { smsApi = new SmsDomainApi(paramsWithProjectId, 'dummy'); smsApi.getSinchClient(); expect(smsApi.client?.apiClientOptions.projectId).toBe('PROJECT_ID'); expect(smsApi.client?.apiClientOptions.hostname).toBe('https://zt.us.sms.api.sinch.com'); smsApi.setCredentials({ ...paramsWithServicePlanId, - forceServicePlanIdUsageForSmsApi: true, + smsRegion: SmsRegion.BRAZIL, + }); + expect(smsApi.client?.apiClientOptions.projectId).toBe('SERVICE_PLAN_ID'); + expect(smsApi.client?.apiClientOptions.hostname).toBe('https://br.sms.api.sinch.com'); + }); + + // eslint-disable-next-line max-len + it('should not update the credentials nor URL when adding unified credentials to the SMS credentials', () => { + smsApi = new SmsDomainApi(paramsWithServicePlanId, 'dummy'); + smsApi.getSinchClient(); + expect(smsApi.client?.apiClientOptions.projectId).toBe('SERVICE_PLAN_ID'); + expect(smsApi.client?.apiClientOptions.hostname).toBe('https://us.sms.api.sinch.com'); + smsApi.setCredentials({ + ...paramsWithProjectId, }); expect(smsApi.client?.apiClientOptions.projectId).toBe('SERVICE_PLAN_ID'); expect(smsApi.client?.apiClientOptions.hostname).toBe('https://us.sms.api.sinch.com'); }); - it('should update the credentials and URL when adding servicePlanId credentials on BR region', () => { - smsApi = new SmsDomainApi(paramsWithProjectId, 'dummy'); + // eslint-disable-next-line max-len + it('should update the region in the URL when adding unified credentials and region to the SMS credentials', () => { + smsApi = new SmsDomainApi(paramsWithServicePlanId, 'dummy'); smsApi.getSinchClient(); - expect(smsApi.client?.apiClientOptions.projectId).toBe('PROJECT_ID'); - expect(smsApi.client?.apiClientOptions.hostname).toBe('https://zt.us.sms.api.sinch.com'); + expect(smsApi.client?.apiClientOptions.projectId).toBe('SERVICE_PLAN_ID'); + expect(smsApi.client?.apiClientOptions.hostname).toBe('https://us.sms.api.sinch.com'); + console.warn = jest.fn(); smsApi.setCredentials({ - ...paramsWithServicePlanId, + ...paramsWithProjectId, smsRegion: SmsRegion.BRAZIL, }); + expect(console.warn).toHaveBeenCalledWith( + 'As the servicePlanId and the apiToken are provided, all other credentials will be disregarded.'); expect(smsApi.client?.apiClientOptions.projectId).toBe('SERVICE_PLAN_ID'); expect(smsApi.client?.apiClientOptions.hostname).toBe('https://br.sms.api.sinch.com'); }); From 9307f0d0dfa96574b97d62c8aa24b8b110a23f72 Mon Sep 17 00:00:00 2001 From: Antoine SEIN <142824551+asein-sinch@users.noreply.github.com> Date: Fri, 4 Oct 2024 09:14:04 +0200 Subject: [PATCH 54/54] DEVEXP-595: [Bugfix] Remove scheduledProvisioning elements from update number request (#146) --- .../src/numbers/active/update.ts | 6 ++++ packages/numbers/CHANGELOG.md | 1 + .../models/v1/active-number/active-number.ts | 8 +++--- .../src/models/v1/sms-configuration/index.ts | 2 +- .../v1/sms-configuration/sms-configuration.ts | 3 ++ .../models/v1/voice-configuration/index.ts | 8 +++++- .../voice-configuration.ts | 28 ++++++++++++++++++- .../numbers/tests/rest/v1/numbers.steps.ts | 8 +++--- 8 files changed, 53 insertions(+), 11 deletions(-) diff --git a/examples/simple-examples/src/numbers/active/update.ts b/examples/simple-examples/src/numbers/active/update.ts index 140339ea..026a7cd3 100644 --- a/examples/simple-examples/src/numbers/active/update.ts +++ b/examples/simple-examples/src/numbers/active/update.ts @@ -1,4 +1,5 @@ import { + getFaxServiceIdFromConfig, getNumberCallbackUrlFromConfig, getPhoneNumberFromConfig, getPrintFormat, @@ -16,6 +17,7 @@ import { Numbers } from '@sinch/sdk-core'; const phoneNumber = getPhoneNumberFromConfig(); const servicePlanId = getServicePlanIdFromConfig(); const callbackUrl = getNumberCallbackUrlFromConfig(); + const faxServiceId = getFaxServiceIdFromConfig(); const requestData: Numbers.UpdateActiveNumberRequestData= { phoneNumber, @@ -24,6 +26,10 @@ import { Numbers } from '@sinch/sdk-core'; smsConfiguration: { servicePlanId, }, + voiceConfiguration: { + type: 'FAX', + serviceId: faxServiceId, + }, callbackUrl, }, }; diff --git a/packages/numbers/CHANGELOG.md b/packages/numbers/CHANGELOG.md index a2b34e63..e8eedae4 100644 --- a/packages/numbers/CHANGELOG.md +++ b/packages/numbers/CHANGELOG.md @@ -1,5 +1,6 @@ ## Version 1.2.0 - [Tech] Update dependency `@sinch/sdk-client` to `1.2.0`. +- [Bugfix] Remove the "scheduledProvisioning" properties for SMS and Voice in the update number request. - [Deprecation notice] `availableNumber` and `activeNumber` subdomain are deprecated and all methods are now defined on the upper numbers service. All the methods names are the same except `availableNumber.list()` -> `searchForAvailableNumbers()` diff --git a/packages/numbers/src/models/v1/active-number/active-number.ts b/packages/numbers/src/models/v1/active-number/active-number.ts index 132fbf9b..5de0141b 100644 --- a/packages/numbers/src/models/v1/active-number/active-number.ts +++ b/packages/numbers/src/models/v1/active-number/active-number.ts @@ -1,6 +1,6 @@ import { Money } from '../money'; -import { SMSConfiguration } from '../sms-configuration'; -import { VoiceConfiguration } from '../voice-configuration'; +import { SMSConfigurationResponse } from '../sms-configuration'; +import { VoiceConfigurationResponse } from '../voice-configuration'; import { CapabilitiesEnum, NumberTypeEnum } from '../enums'; /** @@ -28,9 +28,9 @@ export interface ActiveNumber { /** The timestamp when the subscription will expire if an expiration date has been set. */ expireAt?: Date | null; /** @see SMSConfiguration */ - smsConfiguration?: SMSConfiguration; + smsConfiguration?: SMSConfigurationResponse; /** @see VoiceConfiguration */ - voiceConfiguration?: VoiceConfiguration; + voiceConfiguration?: VoiceConfigurationResponse; /** The active number\'s callback URL to be called for provisioning / deprovisioning updates */ callbackUrl?: string; } diff --git a/packages/numbers/src/models/v1/sms-configuration/index.ts b/packages/numbers/src/models/v1/sms-configuration/index.ts index 6dd3fce9..c25d483b 100644 --- a/packages/numbers/src/models/v1/sms-configuration/index.ts +++ b/packages/numbers/src/models/v1/sms-configuration/index.ts @@ -1 +1 @@ -export type { SMSConfiguration } from './sms-configuration'; +export type { SMSConfiguration, SMSConfigurationResponse } from './sms-configuration'; diff --git a/packages/numbers/src/models/v1/sms-configuration/sms-configuration.ts b/packages/numbers/src/models/v1/sms-configuration/sms-configuration.ts index 30bc8b08..91202cd9 100644 --- a/packages/numbers/src/models/v1/sms-configuration/sms-configuration.ts +++ b/packages/numbers/src/models/v1/sms-configuration/sms-configuration.ts @@ -8,6 +8,9 @@ export interface SMSConfiguration { servicePlanId: string; /** Only for US virtual numbers. This campaign ID relates to 10DLC numbers. So, it is the current campaign ID for this number. The `campaignId` is found on your TCR platform. */ campaignId?: string; +} + +export interface SMSConfigurationResponse extends SMSConfiguration { /** @see ScheduledProvisioning */ scheduledProvisioning?: ScheduledProvisioning | null; } diff --git a/packages/numbers/src/models/v1/voice-configuration/index.ts b/packages/numbers/src/models/v1/voice-configuration/index.ts index c48d6541..5c172347 100644 --- a/packages/numbers/src/models/v1/voice-configuration/index.ts +++ b/packages/numbers/src/models/v1/voice-configuration/index.ts @@ -1 +1,7 @@ -export type { VoiceConfiguration } from './voice-configuration'; +export type { + VoiceConfiguration, + VoiceConfigurationRtc, + VoiceConfigurationFax, + VoiceConfigurationEst, + VoiceConfigurationResponse, +} from './voice-configuration'; diff --git a/packages/numbers/src/models/v1/voice-configuration/voice-configuration.ts b/packages/numbers/src/models/v1/voice-configuration/voice-configuration.ts index 92b5bbdb..3933b0b5 100644 --- a/packages/numbers/src/models/v1/voice-configuration/voice-configuration.ts +++ b/packages/numbers/src/models/v1/voice-configuration/voice-configuration.ts @@ -1,9 +1,35 @@ import { ScheduledVoiceProvisioning } from '../scheduled-voice-provisioning'; +/** + * The current voice configuration for this number. + */ +export type VoiceConfiguration = VoiceConfigurationRtc | VoiceConfigurationFax | VoiceConfigurationEst; + +export interface VoiceConfigurationRtc { + /** The voice application type. */ + type: 'RTC' + /** Your app ID for the Voice API. The `appId` can be found in your Sinch Customer Dashboard under Voice, then apps. */ + appId?: string; +} + +export interface VoiceConfigurationFax { + /** The voice application type. */ + type: 'FAX' + /** The service ID for the FAX configuration. The `serviceId` can be found in your Sinch Customer Dashboard under fax, then services.*/ + serviceId?: string; +} + +export interface VoiceConfigurationEst { + /** The voice application type. */ + type: 'EST' + /** The trunk ID for the EST voice configuration. The `trunkId` can be found in your Sinch Customer Dashboard under sip, then trunks.*/ + trunkId?: string; +} + /** * The current voice configuration for this number. During scheduled provisioning, the app ID value may be empty in a response if it is still processing or if it has failed. The status of scheduled provisioning will show under a `scheduledVoiceProvisioning` object if it\'s still running. Once processed successfully, the `appId` sent will appear directly under the `voiceConfiguration` object. */ -export interface VoiceConfiguration { +export interface VoiceConfigurationResponse { /** Your app ID for the Voice API. The `appId` can be found in your Sinch Customer Dashboard under Voice, then apps. */ appId?: string; /** Timestamp when the status was last updated. */ diff --git a/packages/numbers/tests/rest/v1/numbers.steps.ts b/packages/numbers/tests/rest/v1/numbers.steps.ts index 30e1af04..6dcc215f 100644 --- a/packages/numbers/tests/rest/v1/numbers.steps.ts +++ b/packages/numbers/tests/rest/v1/numbers.steps.ts @@ -92,7 +92,7 @@ Then('the response contains a rented phone number', () => { assert.equal(activeNumber.paymentIntervalMonths, 1); assert.deepEqual(activeNumber.nextChargeDate, new Date('2024-06-06T14:42:42.022227Z')); assert.equal(activeNumber.expireAt, null); - const expectedSmsConfiguration: Numbers.SMSConfiguration = { + const expectedSmsConfiguration: Numbers.SMSConfigurationResponse = { servicePlanId: '', campaignId: '', scheduledProvisioning: { @@ -104,7 +104,7 @@ Then('the response contains a rented phone number', () => { }, }; assert.deepEqual(activeNumber.smsConfiguration, expectedSmsConfiguration); - const expectedVoiceConfiguration: Numbers.VoiceConfiguration = { + const expectedVoiceConfiguration: Numbers.VoiceConfigurationResponse = { type: 'RTC', appId: '', trunkId: '', @@ -211,7 +211,7 @@ When('I send a request to update the phone number {string}', async (phoneNumber: Then('the response contains a phone number with updated parameters', () => { assert.equal(activeNumber.displayName, 'Updated description during E2E tests'); assert.equal(activeNumber.callbackUrl, 'https://my-callback-server.com/numbers'); - const smsConfiguration: Numbers.SMSConfiguration = { + const smsConfiguration: Numbers.SMSConfigurationResponse = { servicePlanId: 'SpaceMonkeySquadron', campaignId: '', scheduledProvisioning: { @@ -223,7 +223,7 @@ Then('the response contains a phone number with updated parameters', () => { }, }; assert.deepEqual(activeNumber.smsConfiguration, smsConfiguration); - const voiceVonfiguration: Numbers.VoiceConfiguration = { + const voiceVonfiguration: Numbers.VoiceConfigurationResponse = { type: 'RTC', appId: 'sunshine-rain-drop-very-beautifulday', trunkId: '',