@velocitycareerlabs/server-careerwallet 1.25.0-dev-build.12642c864

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (252) hide show
  1. package/.localdev.env +47 -0
  2. package/.standalone.env +13 -0
  3. package/LICENSE +248 -0
  4. package/jest.config.js +20 -0
  5. package/migrate-mongo.config.js +25 -0
  6. package/migrations/20211017180227-create-personas.js +478 -0
  7. package/migrations/20211026185916-create-vanessa.js +79 -0
  8. package/migrations/20211026185917-update-personas.js +30 -0
  9. package/migrations/20211108124410-remove-surplus-personas.js +33 -0
  10. package/migrations/20211108132353-fix-vanessa-and-sheila.js +25 -0
  11. package/migrations/20220222123110-add-career-wallet-app-config.js +6 -0
  12. package/migrations/20220411104157-add-min-app-versions.js +15 -0
  13. package/migrations/20220419131726-create-nicole-flores-persona.js +63 -0
  14. package/migrations/20220515114034-update-persona-id-credentials.js +628 -0
  15. package/migrations/20220608093743-disable-mainnet-holderapp-id-verification.js +21 -0
  16. package/migrations/20220609063708-enable-mainnet-holderapp-id-verification.js +21 -0
  17. package/migrations/20220623091507-add-push-url.js +43 -0
  18. package/migrations/20220624133205-set-min-app-versions-to-11.js +16 -0
  19. package/migrations/20220710125326-set-min-app-version-to-0.10.7.js +16 -0
  20. package/migrations/20220811103500-add-verification-service-disclosure-deeplink.js +45 -0
  21. package/migrations/20220811123751-add-holderapp-dids-to-config.js +74 -0
  22. package/migrations/20220818072306-add-holderapp-endpoints-to-config.js +21 -0
  23. package/migrations/20220825090656-update-deeplink.js +46 -0
  24. package/migrations/20221003151823-app-config-add-public-verification-api.js +18 -0
  25. package/migrations/20221116085242-add-holderapp-sdk-to-config.js +15 -0
  26. package/migrations/20221121091030-update-holderapp-deeplink.js +19 -0
  27. package/migrations/20221128103425-update-holderapp-presentation-template.js +19 -0
  28. package/migrations/20221221091436-app-config-add-oauth.js +13 -0
  29. package/migrations/20221226205900-app-config-add-presentation-extension-api.js +18 -0
  30. package/migrations/20230120113141-update-holderapp-cashSiquence.js +15 -0
  31. package/migrations/20230123084103-update-holderapp-cacheSequence.js +15 -0
  32. package/migrations/20230214083430-update-holderapp-cache-Sequence.js +15 -0
  33. package/migrations/20230225173335-set-min-app-version-1.5.1.js +30 -0
  34. package/migrations/20230323120629-add-sunil-singh-persona.js +87 -0
  35. package/migrations/20230329081529-add-personas-by-env.js +294 -0
  36. package/migrations/20230329103219-remove-unused-personas.js +14 -0
  37. package/migrations/20230504090208-disable-yoti-migration.js +21 -0
  38. package/migrations/20230504123425-set-min-app-version-1.8.1.js +38 -0
  39. package/migrations/20230504185047-enable-yoti-migration.js +21 -0
  40. package/migrations/20230524053203-add-devices-index.js +16 -0
  41. package/migrations/20230704000002-add-common-holder-endpoints-to-config.js +16 -0
  42. package/migrations/20230704000003-add-linkedin-holder-endpoints-to-config.js +18 -0
  43. package/migrations/20230704104055-update-push-url-and-yoti-url.js +18 -0
  44. package/migrations/20230705000001-add-liburl-to-holderapp-config.js +20 -0
  45. package/migrations/20230814113134-app-config-add-oauth-client-id.js +18 -0
  46. package/migrations/20230821154136-yoti-new-session-url-fix.js +19 -0
  47. package/migrations/20230907134442-update-holderapp-cache-Sequence.js +15 -0
  48. package/migrations/20230919180000-set-holderapp-min-app-versions-1.15.0.js +16 -0
  49. package/migrations/20231011083137-update-holderapp-cache-sequence-6.js +15 -0
  50. package/migrations/20231102083252-set-holderapp-min-app-versions-1.14.0.js +16 -0
  51. package/migrations/20231108202229-set-holderapp-min-app-versions-1.15.0.js +16 -0
  52. package/migrations/20231115143332-holderapp-isDirectIssuerCheckOn-true.js +16 -0
  53. package/migrations/20231120100020-insert-didKeyMetadatum-for-old-accounts.js +59 -0
  54. package/migrations/20231207142742-remove-devices-from-accounts.js +46 -0
  55. package/migrations/20231226090805-app-config-set-direct-issuer-check.js +13 -0
  56. package/migrations/202312271524-set-holderapp-app-version-1.15.1.js +16 -0
  57. package/migrations/20232211171700-app-config-update-base-urls.js +18 -0
  58. package/migrations/20240102093506-holderapp-revert-version-1.15.0.js +16 -0
  59. package/migrations/202401041618111-holderapp-isDebugOn-false.js +17 -0
  60. package/migrations/20240131095122-test-personas-add-did.js +102 -0
  61. package/migrations/202402051547000-update-sdk.js +18 -0
  62. package/migrations/20240206101448-update-personas-vc-to-v2.js +137 -0
  63. package/migrations/202402061233000-set-xVnfProtocolVersion-2.js +13 -0
  64. package/migrations/202402081240000-set-xVnfProtocolVersion-1.js +13 -0
  65. package/migrations/202402131319-set-holderapp-min-versions-1.17.js +16 -0
  66. package/migrations/202402141152-set-holderapp-min-version-1.16.js +16 -0
  67. package/migrations/20240221123501-transform-account-keys-to-stringified-jwk.js +81 -0
  68. package/migrations/202402290955-update-holderapp-to-verifyMyCreds.js +36 -0
  69. package/migrations/20240311134223-update-keyid-persona.js +66 -0
  70. package/migrations/20240312141618-vl-7409-persona-update-maria-williams.js +82 -0
  71. package/migrations/202403181733000-remove-devices-from-all-accounts.js +27 -0
  72. package/migrations/20240401091041-set-holderapp-min-version-1.18.1.js +16 -0
  73. package/migrations/202404071847-app-config-update-yoti-url.js +19 -0
  74. package/migrations/20240724063405-vl-3827-new-yoti-session-url.js +19 -0
  75. package/migrations/20240731112302-vl-8160-add-persona-keys.js +101 -0
  76. package/migrations/20240911115206-vanessa-lin-id-credentials.js +57 -0
  77. package/migrations/202409201219-add-isWalletAvailable-field.js +13 -0
  78. package/migrations/202409221012-set-isWalletAvailable-true.js +13 -0
  79. package/migrations/20240922114643-holderapp-isWalletAvailable-refactor.js +36 -0
  80. package/migrations/20240923132213-update-android-ios-app-config.js +36 -0
  81. package/migrations/20240926061732-inc-cacheSequence-to-7.js +15 -0
  82. package/migrations/20240926073339-inc-cacheSequence-to-7-fix.js +18 -0
  83. package/migrations/202410081217-adam-smith-id-credntials.js +57 -0
  84. package/migrations/202410101243-adam_smith-id-credential.js +57 -0
  85. package/migrations/20241015124307-persona-key-id-type-fix.js +26 -0
  86. package/migrations/202501291027000-set-holderapp-did-web.js +37 -0
  87. package/migrations/environments/dev.env +34 -0
  88. package/migrations/environments/localdev.env +22 -0
  89. package/migrations/environments/prod.env +18 -0
  90. package/migrations/environments/qa.env +54 -0
  91. package/migrations/environments/staging.env +31 -0
  92. package/migrations/environments/test.env +18 -0
  93. package/package.json +84 -0
  94. package/src/assets/category-icons/assessment.png +0 -0
  95. package/src/assets/category-icons/badge.png +0 -0
  96. package/src/assets/category-icons/certification.png +0 -0
  97. package/src/assets/category-icons/education.png +0 -0
  98. package/src/assets/category-icons/employment.png +0 -0
  99. package/src/assets/category-icons/gig.png +0 -0
  100. package/src/assets/category-icons/identity.png +0 -0
  101. package/src/assets/category-icons/pharmacy.png +0 -0
  102. package/src/assets/category-icons/training.png +0 -0
  103. package/src/assets/credentialCategories.json +119 -0
  104. package/src/config/config.js +156 -0
  105. package/src/controllers/api/v0.6/accounts/autohooks.js +36 -0
  106. package/src/controllers/api/v0.6/accounts/controller.js +288 -0
  107. package/src/controllers/api/v0.6/accounts/schemas/careerwallet-accounts-didkeymetadatum-response.schema.js +41 -0
  108. package/src/controllers/api/v0.6/accounts/schemas/careerwallet-accounts-request.schema.js +49 -0
  109. package/src/controllers/api/v0.6/accounts/schemas/careerwallet-accounts-response.schema.js +74 -0
  110. package/src/controllers/api/v0.6/accounts/schemas/careerwallet-get-account-response.schema.js +72 -0
  111. package/src/controllers/api/v0.6/accounts/schemas/index.js +6 -0
  112. package/src/controllers/api/v0.6/careerwallet/appconfig/controller.js +25 -0
  113. package/src/controllers/api/v0.6/careerwallet/appconfig/repo.js +40 -0
  114. package/src/controllers/api/v0.6/careerwallet/autohooks.js +5 -0
  115. package/src/controllers/api/v0.6/careerwallet/consents/controller.js +66 -0
  116. package/src/controllers/api/v0.6/careerwallet/consents/latest/autohooks.js +7 -0
  117. package/src/controllers/api/v0.6/careerwallet/consents/latest/controller.js +76 -0
  118. package/src/controllers/api/v0.6/careerwallet/consents/repo.js +24 -0
  119. package/src/controllers/api/v0.6/careerwallet/consents/schemas/careerwallet-consent-response.schema.js +23 -0
  120. package/src/controllers/api/v0.6/careerwallet/consents/schemas/index.js +3 -0
  121. package/src/controllers/api/v0.6/create_did_key/autohooks.js +10 -0
  122. package/src/controllers/api/v0.6/create_did_key/controller.js +84 -0
  123. package/src/controllers/api/v0.6/create_did_key/schemas/index.js +4 -0
  124. package/src/controllers/api/v0.6/create_did_key/schemas/jwk-did-request.schema.js +20 -0
  125. package/src/controllers/api/v0.6/create_did_key/schemas/jwk-did-response.schema.js +41 -0
  126. package/src/controllers/api/v0.6/create_jwk/autohooks.js +10 -0
  127. package/src/controllers/api/v0.6/create_jwk/controller.js +46 -0
  128. package/src/controllers/api/v0.6/create_jwk/schemas/index.js +3 -0
  129. package/src/controllers/api/v0.6/create_jwk/schemas/jwk-response.schema.js +33 -0
  130. package/src/controllers/api/v0.6/credential-categories/controller.js +35 -0
  131. package/src/controllers/api/v0.6/devices/autohooks.js +11 -0
  132. package/src/controllers/api/v0.6/devices/controller.js +323 -0
  133. package/src/controllers/api/v0.6/devices/repo.js +27 -0
  134. package/src/controllers/api/v0.6/devices/schemas/device.schema.json +43 -0
  135. package/src/controllers/api/v0.6/devices/schemas/index.js +3 -0
  136. package/src/controllers/api/v0.6/feedback/controller.js +24 -0
  137. package/src/controllers/api/v0.6/feedback/schemas/index.js +3 -0
  138. package/src/controllers/api/v0.6/feedback/schemas/new-feedback.schema.js +47 -0
  139. package/src/controllers/api/v0.6/jwt/autohooks.js +10 -0
  140. package/src/controllers/api/v0.6/jwt/controller.js +44 -0
  141. package/src/controllers/api/v0.6/jwt/schemas/index.js +4 -0
  142. package/src/controllers/api/v0.6/jwt/schemas/jwt-request.schema.js +40 -0
  143. package/src/controllers/api/v0.6/jwt/schemas/jwt-response.schema.js +14 -0
  144. package/src/controllers/api/v0.6/oauth/controller.js +131 -0
  145. package/src/controllers/api/v0.6/push/controller.js +296 -0
  146. package/src/controllers/api/v0.6/push/firebase-initializer.js +21 -0
  147. package/src/controllers/api/v0.6/push/notification-types.js +37 -0
  148. package/src/controllers/api/v0.6/push/push-gateway-auth.js +123 -0
  149. package/src/controllers/api/v0.6/push/repo.js +24 -0
  150. package/src/controllers/api/v0.6/verification-offers/repo.js +25 -0
  151. package/src/controllers/api/v0.6/verify/autohooks.js +15 -0
  152. package/src/controllers/api/v0.6/verify/controller.js +348 -0
  153. package/src/controllers/api/v0.6/verify/repo.js +23 -0
  154. package/src/controllers/api/v0.6/verify/verification-credential-types.js +6 -0
  155. package/src/controllers/jwt/autohooks.js +10 -0
  156. package/src/controllers/jwt/controller.js +106 -0
  157. package/src/controllers/jwt/schemas/index.js +31 -0
  158. package/src/controllers/jwt/schemas/jwt-decode.response.200.schema.json +20 -0
  159. package/src/controllers/jwt/schemas/jwt-decode.schema.json +15 -0
  160. package/src/controllers/jwt/schemas/jwt-sign.response.200.schema.json +14 -0
  161. package/src/controllers/jwt/schemas/jwt-sign.schema.json +40 -0
  162. package/src/controllers/jwt/schemas/jwt-verify.response.200.schema.json +17 -0
  163. package/src/controllers/jwt/schemas/jwt-verify.schema.json +19 -0
  164. package/src/controllers/reference/autohooks.js +5 -0
  165. package/src/controllers/reference/countries/controller.js +81 -0
  166. package/src/controllers/reference/personas/controller.js +91 -0
  167. package/src/controllers/reference/personas/repo.js +29 -0
  168. package/src/controllers/reference/personas/schemas/index.js +3 -0
  169. package/src/controllers/reference/personas/schemas/persona.schema.json +108 -0
  170. package/src/controllers/root/controller.js +27 -0
  171. package/src/entities/accounts/constants.js +18 -0
  172. package/src/entities/accounts/domain/index.js +3 -0
  173. package/src/entities/accounts/domain/merge-scopes.js +9 -0
  174. package/src/entities/accounts/index.js +5 -0
  175. package/src/entities/accounts/repos/accounts.repo.js +64 -0
  176. package/src/entities/accounts/repos/id-token-claims-extension.js +31 -0
  177. package/src/entities/accounts/repos/index.js +3 -0
  178. package/src/entities/devices/constants.js +7 -0
  179. package/src/entities/devices/index.js +3 -0
  180. package/src/entities/feedback/domain/generate-feedback-email.js +23 -0
  181. package/src/entities/feedback/domain/index.js +3 -0
  182. package/src/entities/feedback/index.js +3 -0
  183. package/src/entities/index.js +9 -0
  184. package/src/entities/key-pairs/index.js +4 -0
  185. package/src/entities/key-pairs/orchestrators/generate-jwk.js +28 -0
  186. package/src/entities/key-pairs/orchestrators/get-key-pair.js +58 -0
  187. package/src/entities/key-pairs/orchestrators/index.js +4 -0
  188. package/src/entities/key-pairs/repos/index.js +3 -0
  189. package/src/entities/key-pairs/repos/key-pairs.repo.js +23 -0
  190. package/src/entities/oauth/constants.js +13 -0
  191. package/src/entities/oauth/domain/build-access-token.js +14 -0
  192. package/src/entities/oauth/domain/index.js +10 -0
  193. package/src/entities/oauth/domain/validate-audience.js +13 -0
  194. package/src/entities/oauth/domain/validate-client-id.js +13 -0
  195. package/src/entities/oauth/domain/validate-credential.js +16 -0
  196. package/src/entities/oauth/domain/validate-presentation.js +17 -0
  197. package/src/entities/oauth/domain/validate-refresh-token.js +17 -0
  198. package/src/entities/oauth/domain/validate-scope.js +30 -0
  199. package/src/entities/oauth/domain/verify-presentation.js +24 -0
  200. package/src/entities/oauth/index.js +4 -0
  201. package/src/entities/pushes/domains/errors.js +11 -0
  202. package/src/entities/pushes/domains/index.js +3 -0
  203. package/src/entities/pushes/index.js +3 -0
  204. package/src/entities/verification-code-attempts/index.js +3 -0
  205. package/src/entities/verification-code-attempts/orchestrators/index.js +3 -0
  206. package/src/entities/verification-code-attempts/orchestrators/validate-verification-code-attempts.js +79 -0
  207. package/src/entities/verification-code-attempts/repo.js +99 -0
  208. package/src/entities/verifications/constants.js +8 -0
  209. package/src/entities/verifications/index.js +3 -0
  210. package/src/helpers/caching-constants.js +6 -0
  211. package/src/helpers/index.js +3 -0
  212. package/src/index.js +15 -0
  213. package/src/init-server.js +88 -0
  214. package/src/plugins/index.js +4 -0
  215. package/src/plugins/vcl-verification-version-plugin.js +10 -0
  216. package/src/plugins/verify-access-token-plugin.js +116 -0
  217. package/src/standalone.js +8 -0
  218. package/test/accounts-controller.test.js +893 -0
  219. package/test/accounts-repo.test.js +92 -0
  220. package/test/careerwallet-config-controller.test.js +142 -0
  221. package/test/careerwallet-consents-controller.test.js +409 -0
  222. package/test/create_did_key-controller.test.js +397 -0
  223. package/test/create_jwk-controller.test.js +188 -0
  224. package/test/credential-categories-controller.test.js +29 -0
  225. package/test/credential-icons-controller.test.js +36 -0
  226. package/test/devices-controller.test.js +1025 -0
  227. package/test/factories/accounts-factory.js +15 -0
  228. package/test/factories/career-wallet-config-factory.js +60 -0
  229. package/test/factories/consents-career-wallet-factory.js +21 -0
  230. package/test/factories/devices-factory.js +16 -0
  231. package/test/factories/key-pairs-factory.js +18 -0
  232. package/test/factories/notifications-factory.js +16 -0
  233. package/test/factories/persona-factory.js +17 -0
  234. package/test/factories/refresh-tokens-factory.js +14 -0
  235. package/test/factories/verification-code-attempts-factory.js +17 -0
  236. package/test/factories/verification-factory.js +15 -0
  237. package/test/factories/verification-offer-factory.js +20 -0
  238. package/test/feedback-controller.test.js +225 -0
  239. package/test/helpers/.env.test +39 -0
  240. package/test/helpers/access-token.js +59 -0
  241. package/test/helpers/careerwallet-build-fastify.js +20 -0
  242. package/test/helpers/yoti.js +96 -0
  243. package/test/id-verification-controller.test.js +27 -0
  244. package/test/jwt-controller.test.js +1519 -0
  245. package/test/oauth-controller.test.js +639 -0
  246. package/test/push-controller.test.js +733 -0
  247. package/test/push-gateway-auth.test.js +208 -0
  248. package/test/reference-countries-controller.test.js +45 -0
  249. package/test/reference-personas-controller.test.js +179 -0
  250. package/test/root.test.js +21 -0
  251. package/test/swagger.test.js +21 -0
  252. package/test/verification-controller.test.js +1372 -0
@@ -0,0 +1,1372 @@
1
+ const { mongoDb } = require('@spencejs/spence-mongo-repos');
2
+ const { wait } = require('@velocitycareerlabs/common-functions');
3
+ const { subMinutes, addHours } = require('date-fns/fp');
4
+ const { forEach } = require('lodash/fp');
5
+ const { nanoid } = require('nanoid/non-secure');
6
+ const { decodeCredentialJwt } = require('@velocitycareerlabs/jwt');
7
+ const { initVerifyDomain } = require('@velocitycareerlabs/aws-clients');
8
+ const { errorResponseMatcher } = require('@velocitycareerlabs/tests-helpers');
9
+ const { ObjectId } = require('mongodb');
10
+ const { hashAndEncodeHex } = require('@velocitycareerlabs/crypto/src/crypto');
11
+ const { generateKeyPair } = require('@velocitycareerlabs/crypto');
12
+ const { hexFromJwk } = require('@velocitycareerlabs/jwt');
13
+ const buildFastify = require('./helpers/careerwallet-build-fastify');
14
+ const initVerificationFactory = require('./factories/verification-factory');
15
+ const initVerificationOfferFactory = require('./factories/verification-offer-factory');
16
+ const initVerificationCodeAttemptsFactory = require('./factories/verification-code-attempts-factory');
17
+ const VerificationCredentialTypes = require('../src/controllers/api/v0.6/verify/verification-credential-types');
18
+ const { accountScopes } = require('../src/entities');
19
+ const {
20
+ createAccessToken,
21
+ missingAccessTokenExpectation,
22
+ incorrectAccessTokenExpectation,
23
+ incorrectScopeExpectation,
24
+ } = require('./helpers/access-token');
25
+
26
+ describe('Email & Phone Verification', () => {
27
+ let fastify;
28
+ let persistVerification;
29
+ let persistVerificationOffer;
30
+ let persistVerificationCodeAttempt;
31
+ let publicKey;
32
+ let privateKey;
33
+
34
+ beforeAll(async () => {
35
+ ({ publicKey, privateKey } = generateKeyPair({ format: 'jwk' }));
36
+
37
+ fastify = buildFastify({
38
+ holderAppServerAccessTokenPublicKey: publicKey,
39
+ holderAppServerAccessTokenSigningKey: hexFromJwk(privateKey),
40
+ oauthVerificationDisabledEndpoints: [
41
+ 'POST:/api/v0.6/verify/:valueType/request-code',
42
+ 'POST:/api/v0.6/verify/confirm',
43
+ 'POST:/api/v0.6/verify/issuing/identify',
44
+ 'POST:/api/v0.6/verify/issuing/generate-offers',
45
+ ],
46
+ });
47
+ await fastify.ready();
48
+ ({ persistVerification } = initVerificationFactory(fastify));
49
+ ({ persistVerificationOffer } = initVerificationOfferFactory(fastify));
50
+ ({ persistVerificationCodeAttempt } =
51
+ initVerificationCodeAttemptsFactory(fastify));
52
+ await initVerifyDomain(fastify.config)('example.com');
53
+ }, 30000);
54
+
55
+ beforeEach(async () => {
56
+ await mongoDb().collection('verifications').deleteMany({});
57
+ await mongoDb().collection('verificationOffers').deleteMany({});
58
+ await mongoDb().collection('verificationCodeAttempts').deleteMany({});
59
+ });
60
+
61
+ afterAll(async () => {
62
+ await fastify.close();
63
+ });
64
+
65
+ const generateEmailAddress = () => `${nanoid()}@example.com`;
66
+
67
+ describe('Access Token verification disabled', () => {
68
+ beforeEach(async () => {
69
+ fastify.resetOverrides();
70
+ });
71
+ describe('Start email address verification', () => {
72
+ it('Should return 400 when request is malformed', async () => {
73
+ const response = await fastify.injectJson({
74
+ method: 'POST',
75
+ url: '/api/v0.6/verify/email/request-code',
76
+ payload: {
77
+ test: 'MALFORMED',
78
+ },
79
+ });
80
+
81
+ expect(response.statusCode).toEqual(400);
82
+ });
83
+
84
+ it('Start email address verification flow', async () => {
85
+ const response = await fastify.injectJson({
86
+ method: 'POST',
87
+ url: '/api/v0.6/verify/email/request-code',
88
+ payload: {
89
+ value: generateEmailAddress(),
90
+ },
91
+ });
92
+
93
+ expect(response.statusCode).toEqual(204);
94
+ });
95
+
96
+ it('Increase attempts count on email verification repeat', async () => {
97
+ const emailAddress = generateEmailAddress();
98
+
99
+ await fastify.injectJson({
100
+ method: 'POST',
101
+ url: '/api/v0.6/verify/email/request-code',
102
+ payload: {
103
+ value: emailAddress,
104
+ },
105
+ });
106
+
107
+ const response = await fastify.injectJson({
108
+ method: 'POST',
109
+ url: '/api/v0.6/verify/email/request-code',
110
+ payload: {
111
+ value: emailAddress,
112
+ },
113
+ });
114
+
115
+ const verifications = await mongoDb()
116
+ .collection('verifications')
117
+ .find({ value: emailAddress })
118
+ .toArray();
119
+
120
+ expect(response.statusCode).toEqual(204);
121
+ expect(verifications).toHaveLength(2);
122
+ });
123
+
124
+ it('Exhaust attempts count if too many request-codes', async () => {
125
+ const emailAddress = generateEmailAddress();
126
+
127
+ const attempt1 = await fastify.injectJson({
128
+ method: 'POST',
129
+ url: '/api/v0.6/verify/email/request-code',
130
+ payload: {
131
+ value: emailAddress,
132
+ },
133
+ });
134
+ expect(attempt1.statusCode).toEqual(204);
135
+
136
+ const attempt2 = await fastify.injectJson({
137
+ method: 'POST',
138
+ url: '/api/v0.6/verify/email/request-code',
139
+ payload: {
140
+ value: emailAddress,
141
+ },
142
+ });
143
+ expect(attempt2.statusCode).toEqual(204);
144
+
145
+ const attempt3 = await fastify.injectJson({
146
+ method: 'POST',
147
+ url: '/api/v0.6/verify/email/request-code',
148
+ payload: {
149
+ value: emailAddress,
150
+ },
151
+ });
152
+ expect(attempt3.statusCode).toEqual(204);
153
+
154
+ const attempt4 = await fastify.injectJson({
155
+ method: 'POST',
156
+ url: '/api/v0.6/verify/email/request-code',
157
+ payload: {
158
+ value: emailAddress,
159
+ },
160
+ });
161
+ expect(attempt4.statusCode).toEqual(400);
162
+
163
+ const verifications = await mongoDb()
164
+ .collection('verifications')
165
+ .find({ value: emailAddress })
166
+ .toArray();
167
+
168
+ expect(verifications).toHaveLength(3);
169
+ });
170
+
171
+ it('Successfully issue if old attempts are expired', async () => {
172
+ const emailAddress = generateEmailAddress();
173
+ const verification = { value: emailAddress };
174
+
175
+ await Promise.all([
176
+ persistVerification(verification),
177
+ persistVerification(verification),
178
+ persistVerification(verification),
179
+ ]);
180
+
181
+ await mongoDb()
182
+ .collection('verifications')
183
+ .updateMany({}, { $set: { createdAt: subMinutes(10, new Date()) } });
184
+
185
+ const attempt4 = await fastify.injectJson({
186
+ method: 'POST',
187
+ url: '/api/v0.6/verify/email/request-code',
188
+ payload: {
189
+ value: emailAddress,
190
+ },
191
+ });
192
+ expect(attempt4.statusCode).toEqual(204);
193
+
194
+ const verifications = await mongoDb()
195
+ .collection('verifications')
196
+ .countDocuments({ value: emailAddress });
197
+
198
+ expect(verifications).toEqual(4);
199
+ });
200
+ });
201
+
202
+ describe('Start phone number verification', () => {
203
+ it('Should return 400 when request is malformed', async () => {
204
+ const response = await fastify.injectJson({
205
+ method: 'POST',
206
+ url: '/api/v0.6/verify/phone/request-code',
207
+ payload: {
208
+ test: 'MALFORMED',
209
+ },
210
+ });
211
+
212
+ expect(response.statusCode).toEqual(400);
213
+ });
214
+
215
+ it('Start phone number verification flow', async () => {
216
+ const response = await fastify.injectJson({
217
+ method: 'POST',
218
+ url: '/api/v0.6/verify/phone/request-code',
219
+ payload: {
220
+ value: '123456789',
221
+ },
222
+ });
223
+
224
+ expect(response.statusCode).toEqual(204);
225
+ });
226
+
227
+ it('Increase attempts count on email verification repeat', async () => {
228
+ await fastify.injectJson({
229
+ method: 'POST',
230
+ url: '/api/v0.6/verify/phone/request-code',
231
+ payload: {
232
+ value: '123456789',
233
+ },
234
+ });
235
+
236
+ const response = await fastify.injectJson({
237
+ method: 'POST',
238
+ url: '/api/v0.6/verify/phone/request-code',
239
+ payload: {
240
+ value: '123456789',
241
+ },
242
+ });
243
+
244
+ const verifications = await mongoDb()
245
+ .collection('verifications')
246
+ .countDocuments({ value: '123456789' });
247
+
248
+ expect(response.statusCode).toEqual(204);
249
+ expect(verifications).toEqual(2);
250
+ });
251
+ });
252
+
253
+ describe('Phone number auto blocking Test Suite', () => {
254
+ it('Should return 400 if phone number prefix is blocked', async () => {
255
+ fastify.overrides.reqConfig = (config) => ({
256
+ ...config,
257
+ phonePrefixesToBlock: ['f'],
258
+ });
259
+
260
+ const response = await fastify.injectJson({
261
+ method: 'POST',
262
+ url: '/api/v0.6/verify/phone/request-code',
263
+ payload: {
264
+ value: 'foo',
265
+ },
266
+ });
267
+
268
+ expect(response.statusCode).toEqual(400);
269
+ expect(response.json).toEqual(
270
+ errorResponseMatcher({
271
+ statusCode: 400,
272
+ error: 'Bad Request',
273
+ errorCode: 'attempt_blocked',
274
+ message: 'Attempt Blocked',
275
+ })
276
+ );
277
+
278
+ const attemptsFromDb = await mongoDb()
279
+ .collection('verificationCodeAttempts')
280
+ .find()
281
+ .sort({ attemptedAt: 1 })
282
+ .toArray();
283
+ expect(attemptsFromDb).toEqual([verificationCodeAttemptMatcher()]);
284
+ const [attempt0] = attemptsFromDb;
285
+ expect(attempt0.releaseAt).toBeUndefined();
286
+ });
287
+ it('Should return 400 and set releaseAt for 3rd request in 30 minutes', async () => {
288
+ const initialTime = new Date();
289
+ await persistVerificationCodeAttempt({
290
+ attemptedAt: subMinutes(97, initialTime),
291
+ });
292
+ await persistVerificationCodeAttempt({
293
+ attemptedAt: subMinutes(47, initialTime),
294
+ });
295
+ await persistVerificationCodeAttempt({
296
+ attemptedAt: subMinutes(20, initialTime),
297
+ });
298
+
299
+ const response = await fastify.injectJson({
300
+ method: 'POST',
301
+ url: '/api/v0.6/verify/phone/request-code',
302
+ payload: {
303
+ value: 'foo',
304
+ },
305
+ });
306
+
307
+ expect(response.statusCode).toEqual(400);
308
+ expect(response.json).toEqual(
309
+ errorResponseMatcher({
310
+ statusCode: 400,
311
+ error: 'Bad Request',
312
+ errorCode: 'attempt_blocked',
313
+ message: 'Attempt Blocked',
314
+ })
315
+ );
316
+
317
+ const attemptsFromDb = await mongoDb()
318
+ .collection('verificationCodeAttempts')
319
+ .find()
320
+ .sort({ attemptedAt: 1 })
321
+ .toArray();
322
+ expect(attemptsFromDb).toEqual([
323
+ verificationCodeAttemptMatcher(),
324
+ verificationCodeAttemptMatcher({ releaseAt: expect.any(Date) }),
325
+ verificationCodeAttemptMatcher({ releaseAt: expect.any(Date) }),
326
+ verificationCodeAttemptMatcher({ releaseAt: expect.any(Date) }),
327
+ ]);
328
+ const [attempt0, attempt1, attempt2, attempt3] = attemptsFromDb;
329
+ expect(attempt0.releaseAt).toBeUndefined();
330
+ expect(attempt1.releaseAt > addHours(1, initialTime)).toEqual(true);
331
+ expect(attempt2.releaseAt > addHours(2, initialTime)).toEqual(true);
332
+ expect(attempt3.releaseAt > addHours(3, initialTime)).toEqual(true);
333
+ });
334
+ it('Should return 400 and update releaseAt for 5th request in 30 minutes', async () => {
335
+ const initialTime = new Date();
336
+ await persistVerificationCodeAttempt({
337
+ attemptedAt: subMinutes(97, initialTime),
338
+ });
339
+ await persistVerificationCodeAttempt({
340
+ attemptedAt: subMinutes(37, initialTime),
341
+ releaseAt: addHours(1, initialTime),
342
+ });
343
+ await persistVerificationCodeAttempt({
344
+ attemptedAt: subMinutes(20, initialTime),
345
+ releaseAt: addHours(2, initialTime),
346
+ });
347
+ await persistVerificationCodeAttempt({
348
+ attemptedAt: subMinutes(15, initialTime),
349
+ releaseAt: addHours(3, initialTime),
350
+ });
351
+ await persistVerificationCodeAttempt({
352
+ attemptedAt: subMinutes(10, initialTime),
353
+ releaseAt: addHours(4, initialTime),
354
+ });
355
+
356
+ const response = await fastify.injectJson({
357
+ method: 'POST',
358
+ url: '/api/v0.6/verify/phone/request-code',
359
+ payload: {
360
+ value: 'foo',
361
+ },
362
+ });
363
+
364
+ expect(response.statusCode).toEqual(400);
365
+ expect(response.json).toEqual(
366
+ errorResponseMatcher({
367
+ statusCode: 400,
368
+ error: 'Bad Request',
369
+ errorCode: 'attempt_blocked',
370
+ message: 'Attempt Blocked',
371
+ })
372
+ );
373
+
374
+ const attemptsFromDb = await mongoDb()
375
+ .collection('verificationCodeAttempts')
376
+ .find()
377
+ .sort({ attemptedAt: 1 })
378
+ .toArray();
379
+ expect(attemptsFromDb).toEqual([
380
+ verificationCodeAttemptMatcher(),
381
+ verificationCodeAttemptMatcher({ releaseAt: expect.any(Date) }),
382
+ verificationCodeAttemptMatcher({ releaseAt: expect.any(Date) }),
383
+ verificationCodeAttemptMatcher({ releaseAt: expect.any(Date) }),
384
+ verificationCodeAttemptMatcher({ releaseAt: expect.any(Date) }),
385
+ verificationCodeAttemptMatcher({ releaseAt: expect.any(Date) }),
386
+ ]);
387
+ const [attempt0, attempt1, attempt2, attempt3, attempt4, attempt5] =
388
+ attemptsFromDb;
389
+ expect(attempt0.releaseAt).toBeUndefined();
390
+ expect(attempt1.releaseAt > addHours(1, initialTime)).toEqual(true);
391
+ expect(attempt2.releaseAt > addHours(2, initialTime)).toEqual(true);
392
+ expect(attempt3.releaseAt > addHours(3, initialTime)).toEqual(true);
393
+ expect(attempt4.releaseAt > addHours(4, initialTime)).toEqual(true);
394
+ expect(attempt5.releaseAt > addHours(5, initialTime)).toEqual(true);
395
+ });
396
+ it('Should return 400 and update releaseAt for 10th request in 60 minutes', async () => {
397
+ const initialTime = new Date();
398
+ await persistVerificationCodeAttempt({
399
+ attemptedAt: subMinutes(65, initialTime),
400
+ });
401
+ await persistVerificationCodeAttempt({
402
+ attemptedAt: subMinutes(55, initialTime),
403
+ });
404
+ await persistVerificationCodeAttempt({
405
+ attemptedAt: subMinutes(50, initialTime),
406
+ });
407
+ await persistVerificationCodeAttempt({
408
+ attemptedAt: subMinutes(45, initialTime),
409
+ });
410
+ await persistVerificationCodeAttempt({
411
+ attemptedAt: subMinutes(40, initialTime),
412
+ });
413
+ await persistVerificationCodeAttempt({
414
+ attemptedAt: subMinutes(37, initialTime),
415
+ });
416
+ await persistVerificationCodeAttempt({
417
+ attemptedAt: subMinutes(27, initialTime),
418
+ releaseAt: addHours(1, initialTime),
419
+ });
420
+ await persistVerificationCodeAttempt({
421
+ attemptedAt: subMinutes(20, initialTime),
422
+ releaseAt: addHours(2, initialTime),
423
+ });
424
+ await persistVerificationCodeAttempt({
425
+ attemptedAt: subMinutes(15, initialTime),
426
+ releaseAt: addHours(3, initialTime),
427
+ });
428
+ await persistVerificationCodeAttempt({
429
+ attemptedAt: subMinutes(10, initialTime),
430
+ releaseAt: addHours(4, initialTime),
431
+ });
432
+
433
+ const response = await fastify.injectJson({
434
+ method: 'POST',
435
+ url: '/api/v0.6/verify/phone/request-code',
436
+ payload: {
437
+ value: 'foo',
438
+ },
439
+ });
440
+
441
+ expect(response.statusCode).toEqual(400);
442
+ expect(response.json).toEqual(
443
+ errorResponseMatcher({
444
+ statusCode: 400,
445
+ error: 'Bad Request',
446
+ errorCode: 'attempt_blocked',
447
+ message: 'Attempt Blocked',
448
+ })
449
+ );
450
+
451
+ const attemptsFromDb = await mongoDb()
452
+ .collection('verificationCodeAttempts')
453
+ .find()
454
+ .sort({ attemptedAt: 1 })
455
+ .toArray();
456
+ expect(attemptsFromDb).toEqual([
457
+ verificationCodeAttemptMatcher(),
458
+ verificationCodeAttemptMatcher({ releaseAt: expect.any(Date) }),
459
+ verificationCodeAttemptMatcher({ releaseAt: expect.any(Date) }),
460
+ verificationCodeAttemptMatcher({ releaseAt: expect.any(Date) }),
461
+ verificationCodeAttemptMatcher({ releaseAt: expect.any(Date) }),
462
+ verificationCodeAttemptMatcher({ releaseAt: expect.any(Date) }),
463
+ verificationCodeAttemptMatcher({ releaseAt: expect.any(Date) }),
464
+ verificationCodeAttemptMatcher({ releaseAt: expect.any(Date) }),
465
+ verificationCodeAttemptMatcher({ releaseAt: expect.any(Date) }),
466
+ verificationCodeAttemptMatcher({ releaseAt: expect.any(Date) }),
467
+ verificationCodeAttemptMatcher({ releaseAt: expect.any(Date) }),
468
+ ]);
469
+ const [attempt0, ...attempts] = attemptsFromDb;
470
+ expect(attempt0.releaseAt).toBeUndefined();
471
+ forEach((attempt) => {
472
+ expect(attempt.releaseAt > addHours(475, initialTime)).toEqual(true);
473
+ }, attempts);
474
+ });
475
+ it('Should return 204 for request within the rate limits', async () => {
476
+ fastify.overrides.reqConfig = (config) => ({
477
+ ...config,
478
+ phonePrefixesToBlock: ['f', 'b'],
479
+ });
480
+
481
+ const key = '123456789';
482
+ const initialTime = new Date();
483
+ await persistVerificationCodeAttempt({
484
+ key,
485
+ attemptedAt: subMinutes(95, initialTime),
486
+ releaseAt: subMinutes(10, initialTime),
487
+ });
488
+ await persistVerificationCodeAttempt({
489
+ key,
490
+ attemptedAt: subMinutes(75, initialTime),
491
+ });
492
+ await persistVerificationCodeAttempt({
493
+ key,
494
+ attemptedAt: subMinutes(70, initialTime),
495
+ });
496
+ await persistVerificationCodeAttempt({
497
+ key,
498
+ attemptedAt: subMinutes(10, initialTime),
499
+ });
500
+
501
+ const response = await fastify.injectJson({
502
+ method: 'POST',
503
+ url: '/api/v0.6/verify/phone/request-code',
504
+ payload: {
505
+ value: key,
506
+ },
507
+ });
508
+
509
+ expect(response.statusCode).toEqual(204);
510
+
511
+ const attemptsFromDb = await mongoDb()
512
+ .collection('verificationCodeAttempts')
513
+ .find()
514
+ .sort({ attemptedAt: 1 })
515
+ .toArray();
516
+ expect(attemptsFromDb).toEqual([
517
+ verificationCodeAttemptMatcher({ releaseAt: expect.any(Date), key }),
518
+ verificationCodeAttemptMatcher({ key }),
519
+ verificationCodeAttemptMatcher({ key }),
520
+ verificationCodeAttemptMatcher({ key }),
521
+ verificationCodeAttemptMatcher({ key }),
522
+ ]);
523
+ });
524
+ });
525
+
526
+ describe('Complete verification', () => {
527
+ describe('Complete verification VCL Verification Version < 2', () => {
528
+ it('Should return 400 when request is malformed', async () => {
529
+ const response = await fastify.injectJson({
530
+ method: 'POST',
531
+ url: '/api/v0.6/verify/confirm',
532
+ payload: {
533
+ test: 'MALFORMED',
534
+ },
535
+ });
536
+
537
+ expect(response.statusCode).toEqual(400);
538
+ });
539
+
540
+ it('Should return 404 when verfication code does not match any verfication', async () => {
541
+ const response = await fastify.injectJson({
542
+ method: 'POST',
543
+ url: '/api/v0.6/verify/confirm',
544
+ payload: {
545
+ verificationCode: 'VERFICATION-CODE',
546
+ },
547
+ });
548
+
549
+ expect(response.statusCode).toEqual(404);
550
+ });
551
+
552
+ it('Should return 404 when verification code does not match any incomplete verification', async () => {
553
+ const verification = await persistVerification({
554
+ complete: new Date(),
555
+ });
556
+
557
+ const response = await fastify.injectJson({
558
+ method: 'POST',
559
+ url: '/api/v0.6/verify/confirm',
560
+ payload: {
561
+ verificationCode: verification.attemptId,
562
+ },
563
+ });
564
+
565
+ expect(response.statusCode).toEqual(404);
566
+ });
567
+
568
+ it('Should return 200 when verification code has almost expired', async () => {
569
+ const verification = await persistVerification();
570
+ await mongoDb()
571
+ .collection('verifications')
572
+ .updateMany({}, { $set: { createdAt: subMinutes(9, new Date()) } });
573
+
574
+ const response = await fastify.injectJson({
575
+ method: 'POST',
576
+ url: '/api/v0.6/verify/confirm',
577
+ payload: {
578
+ verificationCode: verification.attemptId,
579
+ },
580
+ });
581
+
582
+ expect(response.statusCode).toEqual(200);
583
+ });
584
+
585
+ it('Should return 404 when verification code has expired', async () => {
586
+ const verification = await persistVerification();
587
+ await mongoDb()
588
+ .collection('verifications')
589
+ .updateMany(
590
+ {},
591
+ { $set: { createdAt: subMinutes(10, new Date()) } }
592
+ );
593
+
594
+ const response = await fastify.injectJson({
595
+ method: 'POST',
596
+ url: '/api/v0.6/verify/confirm',
597
+ payload: {
598
+ verificationCode: verification.attemptId,
599
+ },
600
+ });
601
+
602
+ expect(response.statusCode).toEqual(404);
603
+ });
604
+
605
+ it('Should return 404 when verificaiton code isnt the latest', async () => {
606
+ const verification1 = await persistVerification();
607
+ await wait(10);
608
+ await persistVerification();
609
+
610
+ const response = await fastify.injectJson({
611
+ method: 'POST',
612
+ url: '/api/v0.6/verify/confirm',
613
+ payload: {
614
+ verificationCode: verification1.attemptId,
615
+ },
616
+ });
617
+
618
+ expect(response.statusCode).toEqual(404);
619
+ });
620
+
621
+ it('Complete verification confirmation', async () => {
622
+ await persistVerification();
623
+ await wait(10);
624
+
625
+ const verification2 = await persistVerification();
626
+
627
+ const response = await fastify.injectJson({
628
+ method: 'POST',
629
+ url: '/api/v0.6/verify/confirm',
630
+ payload: {
631
+ verificationCode: verification2.attemptId,
632
+ },
633
+ });
634
+
635
+ const verifiedCredential = decodeCredentialJwt(
636
+ response.json.credential
637
+ );
638
+ const offer = await mongoDb()
639
+ .collection('verificationOffers')
640
+ .findOne({
641
+ type: VerificationCredentialTypes.EmailAddressVerification,
642
+ });
643
+
644
+ expect(response.statusCode).toEqual(200);
645
+ expect(verifiedCredential.credentialSubject.id).toEqual(
646
+ offer.offerId
647
+ );
648
+ });
649
+ });
650
+ describe('Complete verification VCL Verification Version 2', () => {
651
+ it('Complete verification confirmation and return a token', async () => {
652
+ await persistVerification();
653
+ await wait(10);
654
+
655
+ const verification2 = await persistVerification();
656
+
657
+ const response = await fastify.injectJson({
658
+ method: 'POST',
659
+ url: '/api/v0.6/verify/confirm',
660
+ headers: {
661
+ 'x-vcl-verif-version': '2',
662
+ },
663
+ payload: {
664
+ verificationCode: verification2.attemptId,
665
+ },
666
+ });
667
+ const offer = await mongoDb()
668
+ .collection('verificationOffers')
669
+ .findOne({
670
+ type: VerificationCredentialTypes.EmailAddressVerification,
671
+ });
672
+
673
+ expect(response.statusCode).toEqual(200);
674
+ expect(response.json).toEqual({
675
+ token: offer.offerId,
676
+ });
677
+ });
678
+ });
679
+ });
680
+
681
+ describe('Verified Holder Identification', () => {
682
+ it('Should return 400 when request idDocumentCredential is missing id [Deprecated] AND credentialSubject.id', async () => {
683
+ const response = await fastify.injectJson({
684
+ method: 'POST',
685
+ url: '/api/v0.6/verify/issuing/identify',
686
+ payload: {
687
+ idDocumentCredentials: [
688
+ {
689
+ test: 'MALFORMED',
690
+ },
691
+ ],
692
+ exchangeId: nanoid(),
693
+ },
694
+ });
695
+
696
+ expect(response.statusCode).toEqual(400);
697
+ });
698
+
699
+ it('Should return 404 when offer ID does not match any offer', async () => {
700
+ const response = await fastify.injectJson({
701
+ method: 'POST',
702
+ url: '/api/v0.6/verify/issuing/identify',
703
+ payload: {
704
+ idDocumentCredentials: [
705
+ {
706
+ credentialSubject: {
707
+ id: 'OFFER-ID',
708
+ },
709
+ },
710
+ ],
711
+ exchangeId: nanoid(),
712
+ },
713
+ });
714
+
715
+ expect(response.statusCode).toEqual(404);
716
+ });
717
+
718
+ it('Should return 404 when idDocumentCredentials is empty and no vendorOriginContext', async () => {
719
+ const response = await fastify.injectJson({
720
+ method: 'POST',
721
+ url: '/api/v0.6/verify/issuing/identify',
722
+ payload: {
723
+ exchangeId: nanoid(),
724
+ idDocumentCredentials: [],
725
+ },
726
+ });
727
+
728
+ expect(response.statusCode).toEqual(400);
729
+ expect(response.json).toEqual(
730
+ errorResponseMatcher({
731
+ error: 'Bad Request',
732
+ errorCode: 'request_validation_failed',
733
+ message:
734
+ // eslint-disable-next-line max-len
735
+ "body/idDocumentCredentials must NOT have fewer than 1 items, body must have required property 'vendorOriginContext', body must match a schema in anyOf",
736
+ statusCode: 400,
737
+ })
738
+ );
739
+ });
740
+
741
+ it('Should complete identification', async () => {
742
+ const verificationOffer = await persistVerificationOffer();
743
+
744
+ const response = await fastify.injectJson({
745
+ method: 'POST',
746
+ url: '/api/v0.6/verify/issuing/identify',
747
+ payload: {
748
+ idDocumentCredentials: [
749
+ {
750
+ id: 'foo',
751
+ credentialSubject: {
752
+ id: verificationOffer.offerId,
753
+ },
754
+ },
755
+ ],
756
+ exchangeId: nanoid(),
757
+ },
758
+ });
759
+
760
+ expect(response.statusCode).toEqual(200);
761
+ expect(response.json.vendorUserId).toEqual(verificationOffer.offerId);
762
+ });
763
+
764
+ it('Should complete identification with idDocumentCredential.id [Deprecated]', async () => {
765
+ const verificationOffer = await persistVerificationOffer();
766
+
767
+ const response = await fastify.injectJson({
768
+ method: 'POST',
769
+ url: '/api/v0.6/verify/issuing/identify',
770
+ payload: {
771
+ idDocumentCredentials: [
772
+ {
773
+ id: verificationOffer.offerId,
774
+ },
775
+ ],
776
+ exchangeId: nanoid(),
777
+ },
778
+ });
779
+
780
+ expect(response.statusCode).toEqual(200);
781
+ expect(response.json.vendorUserId).toEqual(verificationOffer.offerId);
782
+ });
783
+
784
+ it('Should complete identification using vendorOriginContext', async () => {
785
+ const verificationOffer = await persistVerificationOffer();
786
+
787
+ const response = await fastify.injectJson({
788
+ method: 'POST',
789
+ url: '/api/v0.6/verify/issuing/identify',
790
+ payload: {
791
+ exchangeId: nanoid(),
792
+ vendorOriginContext: verificationOffer.offerId,
793
+ idDocumentCredentials: [],
794
+ },
795
+ });
796
+
797
+ expect(response.statusCode).toEqual(200);
798
+ expect(response.json.vendorUserId).toEqual(verificationOffer.offerId);
799
+ });
800
+ });
801
+
802
+ describe('Verification Offer Generation', () => {
803
+ it('Should return 400 when request is malformed', async () => {
804
+ const response = await fastify.injectJson({
805
+ method: 'POST',
806
+ url: '/api/v0.6/verify/issuing/generate-offers',
807
+ payload: {
808
+ test: 'MALFORMED',
809
+ },
810
+ });
811
+
812
+ expect(response.statusCode).toEqual(400);
813
+ });
814
+
815
+ it('Should return 404 when offer ID does not match any offer', async () => {
816
+ const response = await fastify.injectJson({
817
+ method: 'POST',
818
+ url: '/api/v0.6/verify/issuing/generate-offers',
819
+ payload: {
820
+ vendorOrganizationId: '',
821
+ vendorUserId: 'OFFER-ID',
822
+ exchangeId: '',
823
+ },
824
+ });
825
+
826
+ expect(response.statusCode).toEqual(404);
827
+ });
828
+
829
+ it('Should generate email offers', async () => {
830
+ const verificationOffer = await persistVerificationOffer();
831
+
832
+ const response = await fastify.injectJson({
833
+ method: 'POST',
834
+ url: '/api/v0.6/verify/issuing/generate-offers',
835
+ payload: {
836
+ vendorOrganizationId: 'VENDOR-ORG-ID',
837
+ vendorUserId: verificationOffer.offerId,
838
+ exchangeId: 'EXCHANGE-ID',
839
+ },
840
+ });
841
+
842
+ expect(response.statusCode).toEqual(200);
843
+ expect(response.json.offers).toHaveLength(1);
844
+ expect(response.json.offers[0].offerId).toEqual(
845
+ verificationOffer.offerId
846
+ );
847
+ expect(response.json.offers[0].offerCreationDate).toEqual(
848
+ verificationOffer.offerCreationDate
849
+ );
850
+ expect(response.json.offers[0].exchangeId).toEqual('EXCHANGE-ID');
851
+ expect(response.json.offers[0].credentialSubject.vendorUserId).toEqual(
852
+ verificationOffer.value
853
+ );
854
+ expect(response.json.offers[0].credentialSubject.email).toEqual(
855
+ verificationOffer.value
856
+ );
857
+ });
858
+
859
+ it('Should generate phone offers', async () => {
860
+ const verificationOffer = await persistVerificationOffer({
861
+ type: [VerificationCredentialTypes.PhoneNumberVerification],
862
+ value: '123456789',
863
+ });
864
+
865
+ const response = await fastify.injectJson({
866
+ method: 'POST',
867
+ url: '/api/v0.6/verify/issuing/generate-offers',
868
+ payload: {
869
+ vendorOrganizationId: 'VENDOR-ORG-ID',
870
+ vendorUserId: verificationOffer.offerId,
871
+ exchangeId: 'EXCHANGE-ID',
872
+ },
873
+ });
874
+
875
+ expect(response.statusCode).toEqual(200);
876
+ expect(response.json.offers).toHaveLength(1);
877
+ expect(response.json.offers[0].offerId).toEqual(
878
+ verificationOffer.offerId
879
+ );
880
+ expect(response.json.offers[0].offerCreationDate).toEqual(
881
+ verificationOffer.offerCreationDate
882
+ );
883
+ expect(response.json.offers[0].exchangeId).toEqual('EXCHANGE-ID');
884
+ expect(response.json.offers[0].credentialSubject.phone).toEqual(
885
+ verificationOffer.value
886
+ );
887
+ });
888
+
889
+ it('Should generate phone offers using a tenantDID', async () => {
890
+ const verificationOffer = await persistVerificationOffer({
891
+ type: [VerificationCredentialTypes.PhoneNumberVerification],
892
+ value: '123456789',
893
+ });
894
+ const tenantDid = 'did:ion:abc';
895
+
896
+ const response = await fastify.injectJson({
897
+ method: 'POST',
898
+ url: '/api/v0.6/verify/issuing/generate-offers',
899
+ payload: {
900
+ tenantDID: tenantDid,
901
+ vendorUserId: verificationOffer.offerId,
902
+ exchangeId: 'EXCHANGE-ID',
903
+ },
904
+ });
905
+
906
+ expect(response.statusCode).toEqual(200);
907
+ });
908
+ });
909
+ });
910
+ describe('Access Token verification enabled', () => {
911
+ beforeAll(async () => {
912
+ fastify.overrides.reqConfig = (config) => ({
913
+ ...config,
914
+ oauthVerificationDisabledEndpoints: [], // enabled for all endpoints
915
+ });
916
+ });
917
+
918
+ describe('Start email address verification', () => {
919
+ it('Start email address verification flow if correct access token is provided', async () => {
920
+ const accessToken = await createAccessToken(
921
+ accountScopes.account,
922
+ privateKey
923
+ );
924
+ const response = await fastify.injectJson({
925
+ method: 'POST',
926
+ url: '/api/v0.6/verify/email/request-code',
927
+ payload: {
928
+ value: generateEmailAddress(),
929
+ },
930
+ headers: {
931
+ authorization: `Bearer ${accessToken}`,
932
+ },
933
+ });
934
+
935
+ expect(response.statusCode).toEqual(204);
936
+ });
937
+
938
+ it('Should return 401 when no access token is provided', async () => {
939
+ const response = await fastify.injectJson({
940
+ method: 'POST',
941
+ url: '/api/v0.6/verify/email/request-code',
942
+ payload: {
943
+ value: generateEmailAddress(),
944
+ },
945
+ });
946
+
947
+ missingAccessTokenExpectation(response);
948
+ });
949
+
950
+ it('Should return 401 when incorrect access token is provided', async () => {
951
+ const response = await fastify.injectJson({
952
+ method: 'POST',
953
+ url: '/api/v0.6/verify/email/request-code',
954
+ payload: {
955
+ value: generateEmailAddress(),
956
+ },
957
+ headers: {
958
+ authorization: 'Bearer incorrect_access_token',
959
+ },
960
+ });
961
+
962
+ incorrectAccessTokenExpectation(response);
963
+ });
964
+
965
+ it('Should return 403 when incorrect scope is provided', async () => {
966
+ const accessToken = await createAccessToken(
967
+ 'incorrect_scope',
968
+ privateKey
969
+ );
970
+
971
+ const response = await fastify.injectJson({
972
+ method: 'POST',
973
+ url: '/api/v0.6/verify/email/request-code',
974
+ payload: {
975
+ value: generateEmailAddress(),
976
+ },
977
+ headers: {
978
+ authorization: `Bearer ${accessToken}`,
979
+ },
980
+ });
981
+
982
+ incorrectScopeExpectation(response);
983
+ });
984
+ });
985
+
986
+ describe('Start phone number verification', () => {
987
+ it('Start phone number verification flow', async () => {
988
+ const accessToken = await createAccessToken(
989
+ accountScopes.account,
990
+ privateKey
991
+ );
992
+
993
+ const response = await fastify.injectJson({
994
+ method: 'POST',
995
+ url: '/api/v0.6/verify/phone/request-code',
996
+ payload: {
997
+ value: '123456789',
998
+ },
999
+ headers: {
1000
+ authorization: `Bearer ${accessToken}`,
1001
+ },
1002
+ });
1003
+
1004
+ expect(response.statusCode).toEqual(204);
1005
+ });
1006
+
1007
+ it('Should return 401 when no access token is provided', async () => {
1008
+ const response = await fastify.injectJson({
1009
+ method: 'POST',
1010
+ url: '/api/v0.6/verify/phone/request-code',
1011
+ payload: {
1012
+ value: '123456789',
1013
+ },
1014
+ });
1015
+
1016
+ missingAccessTokenExpectation(response);
1017
+ });
1018
+
1019
+ it('Should return 401 when incorrect access token is provided', async () => {
1020
+ const response = await fastify.injectJson({
1021
+ method: 'POST',
1022
+ url: '/api/v0.6/verify/phone/request-code',
1023
+ payload: {
1024
+ value: '123456789',
1025
+ },
1026
+ headers: {
1027
+ authorization: 'Bearer incorrect_access_token',
1028
+ },
1029
+ });
1030
+
1031
+ incorrectAccessTokenExpectation(response);
1032
+ });
1033
+
1034
+ it('Should return 403 when incorrect scope is provided', async () => {
1035
+ const accessToken = await createAccessToken(
1036
+ 'incorrect_scope',
1037
+ privateKey
1038
+ );
1039
+
1040
+ const response = await fastify.injectJson({
1041
+ method: 'POST',
1042
+ url: '/api/v0.6/verify/phone/request-code',
1043
+ payload: {
1044
+ value: '123456789',
1045
+ },
1046
+ headers: {
1047
+ authorization: `Bearer ${accessToken}`,
1048
+ },
1049
+ });
1050
+
1051
+ incorrectScopeExpectation(response);
1052
+ });
1053
+ });
1054
+
1055
+ describe('Phone number auto blocking Test Suite', () => {
1056
+ it('Should return 204 for request within the rate limits', async () => {
1057
+ const accessToken = await createAccessToken(
1058
+ accountScopes.account,
1059
+ privateKey
1060
+ );
1061
+ fastify.overrides.reqConfig = (config) => ({
1062
+ ...config,
1063
+ phonePrefixesToBlock: ['f', 'b'],
1064
+ oauthVerificationDisabledEndpoints: [], // enabled for all endpoints
1065
+ });
1066
+
1067
+ const key = '123456789';
1068
+ const initialTime = new Date();
1069
+ await persistVerificationCodeAttempt({
1070
+ key,
1071
+ attemptedAt: subMinutes(95, initialTime),
1072
+ releaseAt: subMinutes(10, initialTime),
1073
+ });
1074
+ await persistVerificationCodeAttempt({
1075
+ key,
1076
+ attemptedAt: subMinutes(75, initialTime),
1077
+ });
1078
+ await persistVerificationCodeAttempt({
1079
+ key,
1080
+ attemptedAt: subMinutes(70, initialTime),
1081
+ });
1082
+ await persistVerificationCodeAttempt({
1083
+ key,
1084
+ attemptedAt: subMinutes(10, initialTime),
1085
+ });
1086
+
1087
+ const response = await fastify.injectJson({
1088
+ method: 'POST',
1089
+ url: '/api/v0.6/verify/phone/request-code',
1090
+ payload: {
1091
+ value: key,
1092
+ },
1093
+ headers: {
1094
+ authorization: `Bearer ${accessToken}`,
1095
+ },
1096
+ });
1097
+
1098
+ expect(response.statusCode).toEqual(204);
1099
+
1100
+ const attemptsFromDb = await mongoDb()
1101
+ .collection('verificationCodeAttempts')
1102
+ .find()
1103
+ .sort({ attemptedAt: 1 })
1104
+ .toArray();
1105
+ expect(attemptsFromDb).toEqual([
1106
+ verificationCodeAttemptMatcher({ releaseAt: expect.any(Date), key }),
1107
+ verificationCodeAttemptMatcher({ key }),
1108
+ verificationCodeAttemptMatcher({ key }),
1109
+ verificationCodeAttemptMatcher({ key }),
1110
+ verificationCodeAttemptMatcher({ key }),
1111
+ ]);
1112
+ });
1113
+
1114
+ it('Should return 401 when no access token is provided', async () => {
1115
+ const response = await fastify.injectJson({
1116
+ method: 'POST',
1117
+ url: '/api/v0.6/verify/phone/request-code',
1118
+ payload: {
1119
+ value: '123456789',
1120
+ },
1121
+ });
1122
+
1123
+ missingAccessTokenExpectation(response);
1124
+ });
1125
+
1126
+ it('Should return 401 when incorrect access token is provided', async () => {
1127
+ const response = await fastify.injectJson({
1128
+ method: 'POST',
1129
+ url: '/api/v0.6/verify/phone/request-code',
1130
+ payload: {
1131
+ value: '123456789',
1132
+ },
1133
+ headers: {
1134
+ authorization: 'Bearer incorrect_access_token',
1135
+ },
1136
+ });
1137
+
1138
+ incorrectAccessTokenExpectation(response);
1139
+ });
1140
+
1141
+ it('Should return 403 when incorrect scope is provided', async () => {
1142
+ const accessToken = await createAccessToken(
1143
+ 'incorrect_scope',
1144
+ privateKey
1145
+ );
1146
+
1147
+ const response = await fastify.injectJson({
1148
+ method: 'POST',
1149
+ url: '/api/v0.6/verify/phone/request-code',
1150
+ payload: {
1151
+ value: '123456789',
1152
+ },
1153
+ headers: {
1154
+ authorization: `Bearer ${accessToken}`,
1155
+ },
1156
+ });
1157
+
1158
+ incorrectScopeExpectation(response);
1159
+ });
1160
+ });
1161
+
1162
+ describe('Complete verification', () => {
1163
+ describe('Complete verification VCL Verification Version < 2', () => {
1164
+ it('Complete verification confirmation', async () => {
1165
+ const accessToken = await createAccessToken(
1166
+ accountScopes.account,
1167
+ privateKey
1168
+ );
1169
+ await persistVerification();
1170
+ await wait(10);
1171
+
1172
+ const verification2 = await persistVerification();
1173
+
1174
+ const response = await fastify.injectJson({
1175
+ method: 'POST',
1176
+ url: '/api/v0.6/verify/confirm',
1177
+ payload: {
1178
+ verificationCode: verification2.attemptId,
1179
+ },
1180
+ headers: {
1181
+ authorization: `Bearer ${accessToken}`,
1182
+ },
1183
+ });
1184
+
1185
+ const verifiedCredential = decodeCredentialJwt(
1186
+ response.json.credential
1187
+ );
1188
+ const offer = await mongoDb()
1189
+ .collection('verificationOffers')
1190
+ .findOne({
1191
+ type: VerificationCredentialTypes.EmailAddressVerification,
1192
+ });
1193
+
1194
+ expect(response.statusCode).toEqual(200);
1195
+ expect(verifiedCredential.credentialSubject.id).toEqual(
1196
+ offer.offerId
1197
+ );
1198
+ });
1199
+
1200
+ it('Should return 401 when no access token is provided', async () => {
1201
+ const response = await fastify.injectJson({
1202
+ method: 'POST',
1203
+ url: '/api/v0.6/verify/confirm',
1204
+ payload: {
1205
+ verificationCode: 'VERFICATION-CODE',
1206
+ },
1207
+ });
1208
+ missingAccessTokenExpectation(response);
1209
+ });
1210
+
1211
+ it('Should return 401 when incorrect access token is provided', async () => {
1212
+ const response = await fastify.injectJson({
1213
+ method: 'POST',
1214
+ url: '/api/v0.6/verify/confirm',
1215
+ payload: {
1216
+ verificationCode: 'VERFICATION-CODE',
1217
+ },
1218
+ headers: {
1219
+ authorization: 'Bearer incorrect_access_token',
1220
+ },
1221
+ });
1222
+ incorrectAccessTokenExpectation(response);
1223
+ });
1224
+
1225
+ it('Should return 403 when incorrect scope is provided', async () => {
1226
+ const accessToken = await createAccessToken(
1227
+ 'incorrect_scope',
1228
+ privateKey
1229
+ );
1230
+
1231
+ const response = await fastify.injectJson({
1232
+ method: 'POST',
1233
+ url: '/api/v0.6/verify/confirm',
1234
+ payload: {
1235
+ verificationCode: 'VERFICATION-CODE',
1236
+ },
1237
+ headers: {
1238
+ authorization: `Bearer ${accessToken}`,
1239
+ },
1240
+ });
1241
+ incorrectScopeExpectation(response);
1242
+ });
1243
+ });
1244
+ describe('Complete verification VCL Verification Version 2', () => {
1245
+ it('Complete verification confirmation and return a token', async () => {
1246
+ const accessToken = await createAccessToken(
1247
+ accountScopes.account,
1248
+ privateKey
1249
+ );
1250
+
1251
+ await persistVerification();
1252
+ await wait(10);
1253
+
1254
+ const verification2 = await persistVerification();
1255
+
1256
+ const response = await fastify.injectJson({
1257
+ method: 'POST',
1258
+ url: '/api/v0.6/verify/confirm',
1259
+ headers: {
1260
+ 'x-vcl-verif-version': '2',
1261
+ authorization: `Bearer ${accessToken}`,
1262
+ },
1263
+ payload: {
1264
+ verificationCode: verification2.attemptId,
1265
+ },
1266
+ });
1267
+ const offer = await mongoDb()
1268
+ .collection('verificationOffers')
1269
+ .findOne({
1270
+ type: VerificationCredentialTypes.EmailAddressVerification,
1271
+ });
1272
+
1273
+ expect(response.statusCode).toEqual(200);
1274
+ expect(response.json).toEqual({
1275
+ token: offer.offerId,
1276
+ });
1277
+ });
1278
+
1279
+ it('Should return 401 when no access token is provided', async () => {
1280
+ const response = await fastify.injectJson({
1281
+ method: 'POST',
1282
+ url: '/api/v0.6/verify/confirm',
1283
+ headers: {
1284
+ 'x-vcl-verif-version': '2',
1285
+ },
1286
+ payload: {
1287
+ verificationCode: 'VERFICATION-CODE',
1288
+ },
1289
+ });
1290
+ missingAccessTokenExpectation(response);
1291
+ });
1292
+
1293
+ it('Should return 401 when incorrect access token is provided', async () => {
1294
+ const response = await fastify.injectJson({
1295
+ method: 'POST',
1296
+ url: '/api/v0.6/verify/confirm',
1297
+
1298
+ headers: {
1299
+ 'x-vcl-verif-version': '2',
1300
+ authorization: 'Bearer incorrect_access_token',
1301
+ },
1302
+ });
1303
+ incorrectAccessTokenExpectation(response);
1304
+ });
1305
+
1306
+ it('Should return 403 when incorrect scope is provided', async () => {
1307
+ const accessToken = await createAccessToken(
1308
+ 'incorrect_scope',
1309
+ privateKey
1310
+ );
1311
+
1312
+ const response = await fastify.injectJson({
1313
+ method: 'POST',
1314
+ url: '/api/v0.6/verify/confirm',
1315
+ headers: {
1316
+ 'x-vcl-verif-version': '2',
1317
+ authorization: `Bearer ${accessToken}`,
1318
+ },
1319
+ });
1320
+ incorrectScopeExpectation(response);
1321
+ });
1322
+ });
1323
+ });
1324
+
1325
+ describe('Verified Holder Identification', () => {
1326
+ it('Should complete identification', async () => {
1327
+ const accessToken = await createAccessToken(
1328
+ accountScopes.account,
1329
+ privateKey
1330
+ );
1331
+
1332
+ const verificationOffer = await persistVerificationOffer();
1333
+
1334
+ const response = await fastify.injectJson({
1335
+ method: 'POST',
1336
+ url: '/api/v0.6/verify/issuing/identify',
1337
+ payload: {
1338
+ idDocumentCredentials: [
1339
+ {
1340
+ id: 'foo',
1341
+ credentialSubject: {
1342
+ id: verificationOffer.offerId,
1343
+ },
1344
+ },
1345
+ ],
1346
+ exchangeId: nanoid(),
1347
+ },
1348
+
1349
+ headers: {
1350
+ authorization: `Bearer ${accessToken}`,
1351
+ },
1352
+ });
1353
+
1354
+ expect(response.statusCode).toEqual(200);
1355
+ expect(response.json.vendorUserId).toEqual(verificationOffer.offerId);
1356
+ });
1357
+ });
1358
+ });
1359
+ });
1360
+
1361
+ const verificationCodeAttemptMatcher = (overrides = {}) => {
1362
+ const key = hashAndEncodeHex(overrides.key ?? 'foo');
1363
+ return {
1364
+ _id: expect.any(ObjectId),
1365
+ attemptedAt: expect.any(Date),
1366
+ createdAt: expect.any(Date),
1367
+ releaseAt: undefined,
1368
+ updatedAt: expect.any(Date),
1369
+ ...overrides,
1370
+ key,
1371
+ };
1372
+ };