@verii/server-credentialagent 1.0.0-pre.1752076816

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 (395) hide show
  1. package/.localdev.e2e.env +40 -0
  2. package/.localdev.env +41 -0
  3. package/.standalone.env +5 -0
  4. package/LICENSE +202 -0
  5. package/NOTICE +1 -0
  6. package/README.md +19 -0
  7. package/docker/compose.yml +33 -0
  8. package/e2e/README.md +12 -0
  9. package/e2e/org-registration-and-issuing.e2e.test.js +624 -0
  10. package/jest.config.js +20 -0
  11. package/migrate-mongo.config.js +36 -0
  12. package/migrations/20210317133137-add-index-to-offers-repo.js +57 -0
  13. package/migrations/20210416145639-add-index-to-revocation-list.js +27 -0
  14. package/migrations/20210719120225-add_unique_did_index_to_tenant.js +45 -0
  15. package/migrations/20230524053029-add-vendorUserIdMappings-index.js +32 -0
  16. package/migrations/20230616111907-add-configuration-type-index.js +32 -0
  17. package/package.json +108 -0
  18. package/src/assets/public/favicon.ico +0 -0
  19. package/src/assets/public/logo192.png +0 -0
  20. package/src/assets/public/logo512.png +0 -0
  21. package/src/assets/public/manifest.json +28 -0
  22. package/src/assets/templates/app-redirect.hbs +16 -0
  23. package/src/config/config.js +44 -0
  24. package/src/config/core-config.js +143 -0
  25. package/src/config/holder-config.js +104 -0
  26. package/src/config/index.js +22 -0
  27. package/src/config/operator-config.js +64 -0
  28. package/src/controllers/autoload-holder-api-controllers.js +30 -0
  29. package/src/controllers/autoload-operator-api-controllers.js +31 -0
  30. package/src/controllers/autoload-root-api-controller.js +30 -0
  31. package/src/controllers/autoload-saasoperator-api-controllers.js +31 -0
  32. package/src/controllers/holder/autohooks.js +55 -0
  33. package/src/controllers/holder/get-exchange-progress/autohooks.js +27 -0
  34. package/src/controllers/holder/get-exchange-progress/controller.js +50 -0
  35. package/src/controllers/holder/inspect/autohooks.js +35 -0
  36. package/src/controllers/holder/inspect/get-presentation-request/controller.js +100 -0
  37. package/src/controllers/holder/inspect/schemas/holder-disclosure.schema.json +73 -0
  38. package/src/controllers/holder/inspect/schemas/index.js +33 -0
  39. package/src/controllers/holder/inspect/schemas/presentation-definition.v1.schema.json +461 -0
  40. package/src/controllers/holder/inspect/schemas/presentation-request.schema.json +279 -0
  41. package/src/controllers/holder/inspect/schemas/presentation-submission.schema.json +41 -0
  42. package/src/controllers/holder/inspect/schemas/siop-presentation-submission.schema.json +74 -0
  43. package/src/controllers/holder/inspect/schemas/velocity-presentation-submission.response.200.schema.json +36 -0
  44. package/src/controllers/holder/inspect/schemas/velocity-presentation-submission.schema.json +34 -0
  45. package/src/controllers/holder/inspect/submit-presentation/controller.js +89 -0
  46. package/src/controllers/holder/issue/autohooks.js +23 -0
  47. package/src/controllers/holder/issue/get-credential-manifest/controller.js +193 -0
  48. package/src/controllers/holder/issue/offers/autohooks.js +35 -0
  49. package/src/controllers/holder/issue/offers/controller.js +164 -0
  50. package/src/controllers/holder/issue/offers/credential-offers/controller.js +460 -0
  51. package/src/controllers/holder/issue/submit-identification/autohooks.js +37 -0
  52. package/src/controllers/holder/issue/submit-identification/controller.js +63 -0
  53. package/src/controllers/holder/oauth/autohooks.js +19 -0
  54. package/src/controllers/holder/oauth/controller.js +140 -0
  55. package/src/controllers/index.js +22 -0
  56. package/src/controllers/operator/tenants/_tenantId/autohooks.js +40 -0
  57. package/src/controllers/operator/tenants/_tenantId/check-credentials/autohooks.js +24 -0
  58. package/src/controllers/operator/tenants/_tenantId/check-credentials/controller-v0.8.js +200 -0
  59. package/src/controllers/operator/tenants/_tenantId/check-credentials/schemas/index.js +19 -0
  60. package/src/controllers/operator/tenants/_tenantId/check-credentials/schemas/vendor-credential.schema.json +244 -0
  61. package/src/controllers/operator/tenants/_tenantId/controller-v0.8.js +221 -0
  62. package/src/controllers/operator/tenants/_tenantId/disclosures/_id/autohooks.js +30 -0
  63. package/src/controllers/operator/tenants/_tenantId/disclosures/_id/controller-v0.8.js +271 -0
  64. package/src/controllers/operator/tenants/_tenantId/disclosures/_id/feeds/autohooks.js +45 -0
  65. package/src/controllers/operator/tenants/_tenantId/disclosures/_id/feeds/controller-v0.8.js +199 -0
  66. package/src/controllers/operator/tenants/_tenantId/disclosures/_id/feeds/schemas/add-feed.schema.js +14 -0
  67. package/src/controllers/operator/tenants/_tenantId/disclosures/_id/feeds/schemas/feed.schema.json +27 -0
  68. package/src/controllers/operator/tenants/_tenantId/disclosures/_id/feeds/schemas/index.js +25 -0
  69. package/src/controllers/operator/tenants/_tenantId/disclosures/_id/feeds/schemas/modify-feed-update-body.schema.js +18 -0
  70. package/src/controllers/operator/tenants/_tenantId/disclosures/_id/feeds/schemas/modify-feed.schema.json +19 -0
  71. package/src/controllers/operator/tenants/_tenantId/disclosures/autohooks.js +34 -0
  72. package/src/controllers/operator/tenants/_tenantId/disclosures/controller-v0.8.js +100 -0
  73. package/src/controllers/operator/tenants/_tenantId/disclosures/schemas/agent-disclosure-presentation-definition.schema.json +404 -0
  74. package/src/controllers/operator/tenants/_tenantId/disclosures/schemas/agent-disclosure.schema.js +24 -0
  75. package/src/controllers/operator/tenants/_tenantId/disclosures/schemas/index.js +29 -0
  76. package/src/controllers/operator/tenants/_tenantId/disclosures/schemas/new-agent-disclosure.schema.json +166 -0
  77. package/src/controllers/operator/tenants/_tenantId/disclosures/schemas/update-agent-disclosure.schema.js +20 -0
  78. package/src/controllers/operator/tenants/_tenantId/exchanges/_exchangeId/autohooks.js +30 -0
  79. package/src/controllers/operator/tenants/_tenantId/exchanges/_exchangeId/controller-v0.8.js +73 -0
  80. package/src/controllers/operator/tenants/_tenantId/exchanges/autohooks.js +19 -0
  81. package/src/controllers/operator/tenants/_tenantId/exchanges/controller-v0.8.js +150 -0
  82. package/src/controllers/operator/tenants/_tenantId/exchanges/schemas/get-exchange.response.body.json +147 -0
  83. package/src/controllers/operator/tenants/_tenantId/exchanges/schemas/index.js +21 -0
  84. package/src/controllers/operator/tenants/_tenantId/issued-credentials/autohooks.js +27 -0
  85. package/src/controllers/operator/tenants/_tenantId/issued-credentials/controller-v0.8.js +303 -0
  86. package/src/controllers/operator/tenants/_tenantId/issued-credentials/schemas/index.js +23 -0
  87. package/src/controllers/operator/tenants/_tenantId/issued-credentials/schemas/issued-credential.schema.json +115 -0
  88. package/src/controllers/operator/tenants/_tenantId/issued-credentials/schemas/revoke-credentials.schema.json +18 -0
  89. package/src/controllers/operator/tenants/_tenantId/keys/controller-v0.8.js +168 -0
  90. package/src/controllers/operator/tenants/_tenantId/offer-data/controller-v0.8.js +78 -0
  91. package/src/controllers/operator/tenants/_tenantId/offers/autohooks.js +34 -0
  92. package/src/controllers/operator/tenants/_tenantId/offers/controller-v0.8.js +253 -0
  93. package/src/controllers/operator/tenants/_tenantId/offers/schemas/index.js +23 -0
  94. package/src/controllers/operator/tenants/_tenantId/offers/schemas/new-vendor-offer.schema.js +47 -0
  95. package/src/controllers/operator/tenants/_tenantId/offers/schemas/vendor-offer.schema.json +56 -0
  96. package/src/controllers/operator/tenants/_tenantId/users/autohooks.js +24 -0
  97. package/src/controllers/operator/tenants/_tenantId/users/controller-v0.8.js +92 -0
  98. package/src/controllers/operator/tenants/_tenantId/users/schemas/index.js +23 -0
  99. package/src/controllers/operator/tenants/_tenantId/users/schemas/new-user.schema.json +13 -0
  100. package/src/controllers/operator/tenants/_tenantId/users/schemas/user.schema.json +16 -0
  101. package/src/controllers/operator/tenants/_tenantId/vc-api/credentials/autohooks.js +34 -0
  102. package/src/controllers/operator/tenants/_tenantId/vc-api/credentials/controller-v0.8.js +110 -0
  103. package/src/controllers/operator/tenants/_tenantId/vc-api/credentials/schemas/Credential.schema.js +18 -0
  104. package/src/controllers/operator/tenants/_tenantId/vc-api/credentials/schemas/IssueCredentialOptions.schema.json +42 -0
  105. package/src/controllers/operator/tenants/_tenantId/vc-api/credentials/schemas/IssueCredentialRequest.schema.json +13 -0
  106. package/src/controllers/operator/tenants/_tenantId/vc-api/credentials/schemas/IssueCredentialResponse.schema.json +19 -0
  107. package/src/controllers/operator/tenants/_tenantId/vc-api/credentials/schemas/LinkedDataProof.schema.json +43 -0
  108. package/src/controllers/operator/tenants/_tenantId/vc-api/credentials/schemas/VerifiableCredential.schema.js +16 -0
  109. package/src/controllers/operator/tenants/_tenantId/vc-api/credentials/schemas/index.js +31 -0
  110. package/src/controllers/operator/tenants/autohooks.js +65 -0
  111. package/src/controllers/operator/tenants/controller-v0.8.js +167 -0
  112. package/src/controllers/operator/tenants/schemas/index.js +41 -0
  113. package/src/controllers/operator/tenants/schemas/modify-secret.schema.json +11 -0
  114. package/src/controllers/operator/tenants/schemas/modify-tenant-v0.8.schema.json +44 -0
  115. package/src/controllers/operator/tenants/schemas/new-tenant-v0.8.schema.json +19 -0
  116. package/src/controllers/operator/tenants/schemas/new-tenant.response.200.schema.json +7 -0
  117. package/src/controllers/operator/tenants/schemas/secret-key-metadata.schema.json +31 -0
  118. package/src/controllers/operator/tenants/schemas/secret-key.schema.json +29 -0
  119. package/src/controllers/operator/tenants/schemas/secret-kid.schema.json +13 -0
  120. package/src/controllers/operator/tenants/schemas/secret-new-tenant-v0.8.schema.json +28 -0
  121. package/src/controllers/operator/tenants/schemas/secret-tenant-key-v0.8.schema.json +13 -0
  122. package/src/controllers/operator/tenants/schemas/tenant-key-v0.8.schema.json +14 -0
  123. package/src/controllers/operator/tenants/schemas/tenant-v0.8.schema.json +62 -0
  124. package/src/controllers/root/autohooks.js +23 -0
  125. package/src/controllers/root/controller.js +173 -0
  126. package/src/controllers/saasoperator/groups/_id/autohooks.js +9 -0
  127. package/src/controllers/saasoperator/groups/_id/controller.js +121 -0
  128. package/src/controllers/saasoperator/groups/autohooks.js +19 -0
  129. package/src/controllers/saasoperator/groups/controller.js +65 -0
  130. package/src/controllers/saasoperator/groups/schemas/group.schema.js +17 -0
  131. package/src/controllers/saasoperator/groups/schemas/index.js +4 -0
  132. package/src/controllers/saasoperator/groups/schemas/new-group.schema.js +13 -0
  133. package/src/entities/common/domains/get-json-at-path.js +28 -0
  134. package/src/entities/common/domains/index.js +17 -0
  135. package/src/entities/common/index.js +17 -0
  136. package/src/entities/credentials/domains/credential-format.js +22 -0
  137. package/src/entities/credentials/domains/index.js +19 -0
  138. package/src/entities/credentials/index.js +17 -0
  139. package/src/entities/deep-links/domains/extract-did.js +11 -0
  140. package/src/entities/deep-links/domains/index.js +20 -0
  141. package/src/entities/deep-links/domains/velocity-protocol-uri-to-http-uri.js +32 -0
  142. package/src/entities/deep-links/index.js +19 -0
  143. package/src/entities/disclosures/domains/assert-disclosure-active.js +21 -0
  144. package/src/entities/disclosures/domains/compute-disclosure-configuration-type.js +29 -0
  145. package/src/entities/disclosures/domains/constants.js +61 -0
  146. package/src/entities/disclosures/domains/errors.js +34 -0
  147. package/src/entities/disclosures/domains/get-disclosure-configuration-type.js +60 -0
  148. package/src/entities/disclosures/domains/index.js +32 -0
  149. package/src/entities/disclosures/domains/is-issuing-disclosure.js +23 -0
  150. package/src/entities/disclosures/domains/parse-body-to-disclosure.js +17 -0
  151. package/src/entities/disclosures/domains/validate-by-identification-method.js +69 -0
  152. package/src/entities/disclosures/domains/validate-commercial-entity.js +26 -0
  153. package/src/entities/disclosures/domains/validate-disclosure-by-configuration-type.js +47 -0
  154. package/src/entities/disclosures/domains/validate-disclosure-default-issuing.js +77 -0
  155. package/src/entities/disclosures/domains/validate-disclosure.js +37 -0
  156. package/src/entities/disclosures/domains/validate-feed.js +16 -0
  157. package/src/entities/disclosures/domains/validate-presentation-definition.js +54 -0
  158. package/src/entities/disclosures/domains/validate-vendor-endpoint.js +22 -0
  159. package/src/entities/disclosures/domains/validate-vendor-webhook.js +18 -0
  160. package/src/entities/disclosures/factories/disclosure-factory.js +94 -0
  161. package/src/entities/disclosures/factories/index.js +19 -0
  162. package/src/entities/disclosures/index.js +22 -0
  163. package/src/entities/disclosures/orchestrators/get-disclosure.js +18 -0
  164. package/src/entities/disclosures/orchestrators/index.js +20 -0
  165. package/src/entities/disclosures/orchestrators/update-disclosure-configuration-type.js +32 -0
  166. package/src/entities/disclosures/repos/index.js +20 -0
  167. package/src/entities/disclosures/repos/repo.js +118 -0
  168. package/src/entities/disclosures/repos/set-configuration-type.js +33 -0
  169. package/src/entities/exchanges/adapters/index.js +17 -0
  170. package/src/entities/exchanges/adapters/sign-exchange-response.js +45 -0
  171. package/src/entities/exchanges/domains/build-exchange-progress.js +56 -0
  172. package/src/entities/exchanges/domains/constants.js +24 -0
  173. package/src/entities/exchanges/domains/ensure-exchange-state-valid.js +35 -0
  174. package/src/entities/exchanges/domains/errors.js +33 -0
  175. package/src/entities/exchanges/domains/index.js +25 -0
  176. package/src/entities/exchanges/domains/states.js +43 -0
  177. package/src/entities/exchanges/domains/types.js +31 -0
  178. package/src/entities/exchanges/factories/disclosure-exchange-factory.js +46 -0
  179. package/src/entities/exchanges/factories/index.js +20 -0
  180. package/src/entities/exchanges/factories/offer-exchange-factory.js +48 -0
  181. package/src/entities/exchanges/index.js +23 -0
  182. package/src/entities/exchanges/orchestrators/build-exchange-request-deep-link.js +50 -0
  183. package/src/entities/exchanges/orchestrators/index.js +19 -0
  184. package/src/entities/exchanges/repos/exchange-repo-projections.js +45 -0
  185. package/src/entities/exchanges/repos/exchange-state-repo-extension.js +76 -0
  186. package/src/entities/exchanges/repos/index.js +20 -0
  187. package/src/entities/exchanges/repos/repo.js +44 -0
  188. package/src/entities/feeds/factories/feed-factory.js +47 -0
  189. package/src/entities/feeds/factories/index.js +19 -0
  190. package/src/entities/feeds/index.js +20 -0
  191. package/src/entities/feeds/repos/index.js +19 -0
  192. package/src/entities/feeds/repos/repo.js +95 -0
  193. package/src/entities/groups/domains/format-group.js +11 -0
  194. package/src/entities/groups/domains/index.js +3 -0
  195. package/src/entities/groups/factories/group-factory.js +40 -0
  196. package/src/entities/groups/factories/index.js +19 -0
  197. package/src/entities/groups/index.js +22 -0
  198. package/src/entities/groups/orchestrators/find-group-or-error.js +16 -0
  199. package/src/entities/groups/orchestrators/index.js +6 -0
  200. package/src/entities/groups/orchestrators/validate-did.js +24 -0
  201. package/src/entities/groups/orchestrators/validate-group-by-user.js +16 -0
  202. package/src/entities/groups/orchestrators/validate-group.js +39 -0
  203. package/src/entities/groups/repos/delete-tenant-extension.js +13 -0
  204. package/src/entities/groups/repos/index.js +19 -0
  205. package/src/entities/groups/repos/repo.js +38 -0
  206. package/src/entities/groups/repos/update-or-error-extension.js +46 -0
  207. package/src/entities/index.js +37 -0
  208. package/src/entities/keys/domains/constants.js +37 -0
  209. package/src/entities/keys/domains/index.js +21 -0
  210. package/src/entities/keys/domains/is-matching-private-key-kid.js +41 -0
  211. package/src/entities/keys/domains/validate-key.js +62 -0
  212. package/src/entities/keys/factories/index.js +19 -0
  213. package/src/entities/keys/factories/key-factory.js +56 -0
  214. package/src/entities/keys/index.js +22 -0
  215. package/src/entities/keys/orchestrators/index.js +3 -0
  216. package/src/entities/keys/orchestrators/validate-did-doc-keys.js +69 -0
  217. package/src/entities/metadata-list-allocations/index.js +19 -0
  218. package/src/entities/metadata-list-allocations/repos/index.js +19 -0
  219. package/src/entities/metadata-list-allocations/repos/repo.js +40 -0
  220. package/src/entities/notifications/domains/index.js +19 -0
  221. package/src/entities/notifications/domains/notification-types.js +25 -0
  222. package/src/entities/notifications/index.js +19 -0
  223. package/src/entities/offers/domains/build-clean-pii-filter.js +35 -0
  224. package/src/entities/offers/domains/build-deeplink-url.js +120 -0
  225. package/src/entities/offers/domains/build-offer.js +88 -0
  226. package/src/entities/offers/domains/build-qr-code-url.js +37 -0
  227. package/src/entities/offers/domains/constants.js +32 -0
  228. package/src/entities/offers/domains/filter-object-ids.js +34 -0
  229. package/src/entities/offers/domains/generate-issuing-challenge.js +26 -0
  230. package/src/entities/offers/domains/generate-link-code.js +35 -0
  231. package/src/entities/offers/domains/index.js +31 -0
  232. package/src/entities/offers/domains/post-validation-offers-handler.js +31 -0
  233. package/src/entities/offers/domains/prepare-linked-credentials-for-holder.js +36 -0
  234. package/src/entities/offers/domains/resolve-subject.js +142 -0
  235. package/src/entities/offers/domains/validate-offer-commercial-entity.js +24 -0
  236. package/src/entities/offers/domains/validate-offer.js +90 -0
  237. package/src/entities/offers/factories/index.js +19 -0
  238. package/src/entities/offers/factories/offer-factory.js +119 -0
  239. package/src/entities/offers/index.js +22 -0
  240. package/src/entities/offers/orchestrators/create-verifiable-credentials.js +131 -0
  241. package/src/entities/offers/orchestrators/finalize-exchange.js +44 -0
  242. package/src/entities/offers/orchestrators/index.js +23 -0
  243. package/src/entities/offers/orchestrators/load-credential-refs.js +57 -0
  244. package/src/entities/offers/orchestrators/load-credential-types-map.js +44 -0
  245. package/src/entities/offers/orchestrators/prepare-offers.js +35 -0
  246. package/src/entities/offers/orchestrators/trigger-issued-credentials-webhook.js +63 -0
  247. package/src/entities/offers/repos/clean-pii-extension.js +85 -0
  248. package/src/entities/offers/repos/index.js +20 -0
  249. package/src/entities/offers/repos/issued-credential-projection.js +44 -0
  250. package/src/entities/offers/repos/repo.js +177 -0
  251. package/src/entities/presentations/domains/build-identity-doc.js +120 -0
  252. package/src/entities/presentations/domains/build-request-response-schema.js +46 -0
  253. package/src/entities/presentations/domains/build-vendor-data.js +31 -0
  254. package/src/entities/presentations/domains/check-payment-requirement.js +30 -0
  255. package/src/entities/presentations/domains/errors.js +28 -0
  256. package/src/entities/presentations/domains/extract-fields-from-id-credential.js +35 -0
  257. package/src/entities/presentations/domains/index.js +26 -0
  258. package/src/entities/presentations/domains/merge-credential-check-results.js +24 -0
  259. package/src/entities/presentations/domains/validate-presentation.js +128 -0
  260. package/src/entities/presentations/index.js +20 -0
  261. package/src/entities/presentations/orchestrators/create-presentation-request.js +148 -0
  262. package/src/entities/presentations/orchestrators/deduplicate-disclosure-exchange.js +52 -0
  263. package/src/entities/presentations/orchestrators/handle-presentation-submission.js +47 -0
  264. package/src/entities/presentations/orchestrators/index.js +20 -0
  265. package/src/entities/presentations/orchestrators/match-identity-on-exchange.js +114 -0
  266. package/src/entities/presentations/orchestrators/share-identification-credentials.js +110 -0
  267. package/src/entities/presentations/orchestrators/share-presentation.js +234 -0
  268. package/src/entities/push-delegate/get-push-delegate.js +37 -0
  269. package/src/entities/push-delegate/index.js +17 -0
  270. package/src/entities/redirect/index.js +3 -0
  271. package/src/entities/redirect/orchestrators/index.js +3 -0
  272. package/src/entities/redirect/orchestrators/load-org-info.js +40 -0
  273. package/src/entities/revocation-list-allocations/index.js +19 -0
  274. package/src/entities/revocation-list-allocations/repos/index.js +19 -0
  275. package/src/entities/revocation-list-allocations/repos/repo.js +40 -0
  276. package/src/entities/schemas/index.js +19 -0
  277. package/src/entities/schemas/orchestrators/index.js +19 -0
  278. package/src/entities/schemas/orchestrators/load-schema-validation.js +73 -0
  279. package/src/entities/tenants/domains/build-service-ids.js +27 -0
  280. package/src/entities/tenants/domains/extract-service.js +27 -0
  281. package/src/entities/tenants/domains/index.js +21 -0
  282. package/src/entities/tenants/domains/validate-service-ids.js +35 -0
  283. package/src/entities/tenants/factories/index.js +19 -0
  284. package/src/entities/tenants/factories/tenant-factory.js +37 -0
  285. package/src/entities/tenants/index.js +22 -0
  286. package/src/entities/tenants/orchestrators/add-primary-address-to-tenant.js +47 -0
  287. package/src/entities/tenants/orchestrators/create-tenant.js +91 -0
  288. package/src/entities/tenants/orchestrators/index.js +22 -0
  289. package/src/entities/tenants/orchestrators/refresh-tenant-dids.js +146 -0
  290. package/src/entities/tenants/orchestrators/set-tenant-default-issuing-disclosure.js +31 -0
  291. package/src/entities/tenants/repos/index.js +20 -0
  292. package/src/entities/tenants/repos/insert-tenant-extension.js +33 -0
  293. package/src/entities/tenants/repos/repo.js +52 -0
  294. package/src/entities/tenants/repos/tenant-default-projection.js +33 -0
  295. package/src/entities/tokens/adapters/access-token.js +49 -0
  296. package/src/entities/tokens/adapters/index.js +19 -0
  297. package/src/entities/tokens/index.js +19 -0
  298. package/src/entities/users/factories/index.js +19 -0
  299. package/src/entities/users/factories/user-factory.js +36 -0
  300. package/src/entities/users/index.js +20 -0
  301. package/src/entities/users/repos/add-anonymous-user-repo-extension.js +23 -0
  302. package/src/entities/users/repos/find-or-insert-vendor-user-repo-extension.js +30 -0
  303. package/src/entities/users/repos/index.js +19 -0
  304. package/src/entities/users/repos/repo.js +50 -0
  305. package/src/fetchers/index.js +20 -0
  306. package/src/fetchers/operator/identify-fetcher.js +36 -0
  307. package/src/fetchers/operator/index.js +21 -0
  308. package/src/fetchers/operator/inspection-fetcher.js +35 -0
  309. package/src/fetchers/operator/issuing-fetcher.js +50 -0
  310. package/src/fetchers/operator/webhook-auth-header.js +45 -0
  311. package/src/fetchers/push-gateway/generate-push-gateway-token.js +40 -0
  312. package/src/fetchers/push-gateway/index.js +19 -0
  313. package/src/fetchers/push-gateway/push-fetcher.js +39 -0
  314. package/src/index.js +19 -0
  315. package/src/init-holder-server.js +108 -0
  316. package/src/init-operator-server.js +101 -0
  317. package/src/init-server.js +120 -0
  318. package/src/main-holder.js +18 -0
  319. package/src/main-operator.js +19 -0
  320. package/src/main.js +18 -0
  321. package/src/plugins/autoload-repos.js +28 -0
  322. package/src/plugins/disclosure-loader-plugin.js +56 -0
  323. package/src/plugins/ensure-disclosure-active-plugin.js +30 -0
  324. package/src/plugins/ensure-disclosure-configuration-type-plugin.js +29 -0
  325. package/src/plugins/ensure-tenant-default-issuing-disclosure-id-plugin.js +60 -0
  326. package/src/plugins/ensure-tenant-primary-address-plugin.js +44 -0
  327. package/src/plugins/exchange-error-handler-plugin.js +51 -0
  328. package/src/plugins/exchange-loader-plugin.js +50 -0
  329. package/src/plugins/group-loader-plugin.js +51 -0
  330. package/src/plugins/index.js +32 -0
  331. package/src/plugins/kms-plugin.js +57 -0
  332. package/src/plugins/tenant-loader-plugin.js +91 -0
  333. package/src/plugins/validate-cao-plugin.js +81 -0
  334. package/src/plugins/vendor-routes-auth-plugin.js +24 -0
  335. package/src/plugins/verify-access-token-plugin.js +88 -0
  336. package/src/standalone.js +24 -0
  337. package/src/start-app-server.js +38 -0
  338. package/test/combined/app-redirect.test.js +199 -0
  339. package/test/combined/helpers/credentialagent-build-fastify.js +29 -0
  340. package/test/combined/helpers/index.js +22 -0
  341. package/test/combined/helpers/nock-registrar-app-schema-name.js +50 -0
  342. package/test/combined/helpers/nock-registrar-get-organization-diddoc.js +26 -0
  343. package/test/combined/helpers/nock-registrar-get-organization-verified-profile.js +33 -0
  344. package/test/combined/manifest.json.test.js +55 -0
  345. package/test/combined/root-controller.test.js +42 -0
  346. package/test/combined/schemas/education-degree.schema.json +166 -0
  347. package/test/combined/schemas/employment-current-v1.1.schema.json +253 -0
  348. package/test/combined/schemas/open-badge-credential.schema.json +1285 -0
  349. package/test/combined/schemas/past-employment-position-with-uri-id.schema.js +22 -0
  350. package/test/combined/schemas/past-employment-position.schema.json +148 -0
  351. package/test/combined/schemas/will-always-validate.json +10 -0
  352. package/test/combined/validate-cao-plugin.test.js +155 -0
  353. package/test/get-push-delegate.test.js +54 -0
  354. package/test/helpers/jwt-vc-expectation.js +109 -0
  355. package/test/holder/build-request-response-schema.test.js +55 -0
  356. package/test/holder/credential-manifest-controller.test.js +3192 -0
  357. package/test/holder/e2e-issuing-controller.test.js +425 -0
  358. package/test/holder/get-exchange-progress-controller.test.js +521 -0
  359. package/test/holder/get-presentation-request.test.js +906 -0
  360. package/test/holder/helpers/credential-type-metadata.js +98 -0
  361. package/test/holder/helpers/credentialagent-holder-build-fastify.js +32 -0
  362. package/test/holder/helpers/generate-presentation.js +441 -0
  363. package/test/holder/helpers/generate-test-access-token.js +54 -0
  364. package/test/holder/helpers/jwt-access-token-expectation.js +32 -0
  365. package/test/holder/helpers/jwt-vc-expectation.js +115 -0
  366. package/test/holder/issuing-controller.test.js +7076 -0
  367. package/test/holder/oauth-token-controller.test.js +412 -0
  368. package/test/holder/presentation-submission.test.js +2365 -0
  369. package/test/holder/submit-identification.test.js +4815 -0
  370. package/test/operator/check-credentials-controller-v0.8.test.js +832 -0
  371. package/test/operator/credentials-revoke.test.js +536 -0
  372. package/test/operator/disclosures-controller-v0.8.test.js +4157 -0
  373. package/test/operator/exchanges-controller-v0.8.test.js +414 -0
  374. package/test/operator/exchanges-id-controller-v0.8.test.js +162 -0
  375. package/test/operator/feeds-controller-v0.8.test.js +659 -0
  376. package/test/operator/generate-push-gateway-token.test.js +116 -0
  377. package/test/operator/groups-controller.test.js +145 -0
  378. package/test/operator/groups-id-controller.test.js +287 -0
  379. package/test/operator/helpers/create-test-org-doc.js +60 -0
  380. package/test/operator/helpers/credentialagent-operator-build-fastify.js +32 -0
  381. package/test/operator/helpers/find-kms-key.js +31 -0
  382. package/test/operator/helpers/generate-primary-and-add-operator-to-primary.js +63 -0
  383. package/test/operator/helpers/init-agent-kms.js +22 -0
  384. package/test/operator/issued-credentials-controller-v0.8.test.js +398 -0
  385. package/test/operator/keys-controller-v0.8.test.js +1130 -0
  386. package/test/operator/offer-data-controller-v0.8.test.js +253 -0
  387. package/test/operator/offers-controller-v0.8.test.js +3026 -0
  388. package/test/operator/set-configuration-type-modifier.test.js +75 -0
  389. package/test/operator/swagger.test.js +37 -0
  390. package/test/operator/tenant-controller-v0.8.test.js +730 -0
  391. package/test/operator/tenant-loader-plugin.test.js +96 -0
  392. package/test/operator/tenants-controller-v0.8.test.js +2093 -0
  393. package/test/operator/users-controller-v0.8.test.js +137 -0
  394. package/test/operator/vc-api-credentials.test.js +963 -0
  395. package/verification.env +28 -0
@@ -0,0 +1,3026 @@
1
+ /**
2
+ * Copyright 2023 Velocity Team
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ // eslint-disable-next-line import/order
18
+ const buildFastify = require('./helpers/credentialagent-operator-build-fastify');
19
+
20
+ const { mongoDb } = require('@spencejs/spence-mongo-repos');
21
+ const { ObjectId } = require('mongodb');
22
+ const { nanoid } = require('nanoid');
23
+ const { flow, map, omit, set } = require('lodash/fp');
24
+ const { intermediateIssuer } = require('@verii/sample-data');
25
+ const nock = require('nock');
26
+ const {
27
+ ISO_DATETIME_FORMAT,
28
+ OBJECT_ID_FORMAT,
29
+ NANO_ID_FORMAT,
30
+ } = require('@verii/test-regexes');
31
+
32
+ const {
33
+ mongoify,
34
+ testAuthToken,
35
+ errorResponseMatcher,
36
+ } = require('@verii/tests-helpers');
37
+ const { hashOffer } = require('@verii/velocity-issuing');
38
+ const { openBadgeCredentialExample } = require('@verii/sample-data');
39
+ const {
40
+ nockRegistrarAppSchemaName,
41
+ } = require('../combined/helpers/nock-registrar-app-schema-name');
42
+ const initOfferRepo = require('../../src/entities/offers/repos/repo');
43
+ const {
44
+ ExchangeStates,
45
+ ExchangeTypes,
46
+ initOfferFactory,
47
+ initTenantFactory,
48
+ initUserFactory,
49
+ initOfferExchangeFactory,
50
+ initDisclosureFactory,
51
+ NotificationTypes,
52
+ } = require('../../src/entities');
53
+ const {
54
+ nockRegistrarGetOrganizationDidDoc,
55
+ } = require('../combined/helpers/nock-registrar-get-organization-diddoc');
56
+
57
+ jest.mock(
58
+ '../../src/fetchers/push-gateway/generate-push-gateway-token',
59
+ () => ({ generatePushGatewayToken: () => Promise.resolve('token') })
60
+ );
61
+
62
+ const exchangeOffersUrl = ({ tenant, exchange }, suffix = '') =>
63
+ `/operator-api/v0.8/tenants/${tenant._id}/exchanges/${exchange._id}/offers/${suffix}`;
64
+
65
+ const exchangesUrl = ({ tenant, exchange }, suffix = '') =>
66
+ `/operator-api/v0.8/tenants/${tenant._id}/exchanges/${exchange._id}/${suffix}`;
67
+
68
+ const credentialTypesObject = { credentialTypes: ['PastEmploymentPosition'] };
69
+ const testPushEndpointURL = new URL('https://push.localhost.test/push');
70
+
71
+ describe('vendor offer management', () => {
72
+ let fastify;
73
+ let persistOffer;
74
+ let persistOfferExchange;
75
+ let persistVendorUserIdMapping;
76
+ let persistTenant;
77
+ let persistDisclosure;
78
+ let tenant;
79
+ let disclosure;
80
+ let exchange;
81
+ let exchangeId;
82
+ let newOffer;
83
+ let user;
84
+ let exchangeCollection;
85
+ let offerCollection;
86
+ let exchangeOffers;
87
+ let pushExchange;
88
+ let pushExchangeOffers;
89
+ let offersRepo;
90
+
91
+ beforeAll(async () => {
92
+ fastify = buildFastify();
93
+ await fastify.ready();
94
+ ({ persistDisclosure } = initDisclosureFactory(fastify));
95
+ ({ persistOfferExchange } = initOfferExchangeFactory(fastify));
96
+ ({ newOffer, persistOffer } = initOfferFactory(fastify));
97
+ ({ persistTenant } = initTenantFactory(fastify));
98
+ ({ persistVendorUserIdMapping } = initUserFactory(fastify));
99
+ });
100
+
101
+ beforeEach(async () => {
102
+ jest.resetAllMocks();
103
+ fastify.resetOverrides();
104
+ fastify.removeDocSchema();
105
+ fastify.overrides.reqConfig = (config) => ({
106
+ ...config,
107
+ enableOfferValidation: true,
108
+ });
109
+ offerCollection = mongoDb().collection('offers');
110
+ exchangeCollection = mongoDb().collection('exchanges');
111
+
112
+ await mongoDb().collection('vendorUserIdMappings').deleteMany({});
113
+ await offerCollection.deleteMany({});
114
+ await exchangeCollection.deleteMany({});
115
+ await mongoDb().collection('tenants').deleteMany({});
116
+
117
+ const orgDid = 'did:velocity:0xc257274276a4e539741ca11b590b9447b26a8051';
118
+
119
+ nockRegistrarGetOrganizationDidDoc(orgDid, { id: orgDid });
120
+ tenant = await persistTenant({
121
+ did: orgDid,
122
+ serviceIds: [`${orgDid}#issuer-1`],
123
+ });
124
+ disclosure = await persistDisclosure({ tenant });
125
+ exchange = await persistOfferExchange({ tenant, disclosure });
126
+ pushExchange = await persistOfferExchange({
127
+ tenant,
128
+ disclosure,
129
+ pushDelegate: {
130
+ pushToken: 'some-token',
131
+ pushUrl: testPushEndpointURL.href,
132
+ },
133
+ });
134
+ exchangeId = exchange._id;
135
+
136
+ user = await persistVendorUserIdMapping();
137
+ exchangeOffers = await Promise.all([
138
+ persistOffer({ tenant, exchange }),
139
+ persistOffer({ tenant, exchange }),
140
+ ]);
141
+ pushExchangeOffers = await Promise.all([
142
+ persistOffer({ tenant, exchange: pushExchange }),
143
+ ]);
144
+
145
+ offersRepo = initOfferRepo(fastify)({
146
+ log: fastify.log,
147
+ config: fastify.config,
148
+ tenant: { ...tenant, _id: new ObjectId(tenant._id) },
149
+ });
150
+ });
151
+
152
+ const newVendorOffer = async (overrides) =>
153
+ omit(
154
+ [
155
+ 'issuer',
156
+ 'credentialSchema',
157
+ 'contentHash',
158
+ 'isDuplicate',
159
+ 'exchangeId',
160
+ ],
161
+ await newOffer(overrides)
162
+ );
163
+
164
+ afterEach(() => {
165
+ nock.cleanAll();
166
+ });
167
+
168
+ afterAll(async () => {
169
+ await fastify.close();
170
+ nock.cleanAll();
171
+ nock.restore();
172
+ });
173
+
174
+ describe('/offers Adding offers to exchange Test Suite', () => {
175
+ it('Adding offer should 200 when adding offer when credential type has unrelated schema url and name and hit the cache second time', async () => {
176
+ const getSchemaNock = nockRegistrarAppSchemaName({
177
+ schemaName: 'fooV1',
178
+ responseJson: require('../combined/schemas/past-employment-position-with-uri-id.schema'),
179
+ repeatCount: 2,
180
+ });
181
+
182
+ const offer = await newVendorOffer({ tenant, exchange });
183
+ const response = await fastify.injectJson({
184
+ method: 'POST',
185
+ url: exchangeOffersUrl({ tenant, exchange }, ''),
186
+ payload: { ...offer, _id: 123 },
187
+ });
188
+
189
+ expect(response.statusCode).toEqual(200);
190
+ expect(response.json).toEqual({
191
+ ...offer,
192
+ credentialSubject: {
193
+ ...offer.credentialSubject,
194
+ type: 'PastEmploymentPosition',
195
+ },
196
+ exchangeId,
197
+ id: expect.stringMatching(OBJECT_ID_FORMAT),
198
+ issuer: {
199
+ id: tenant.did,
200
+ },
201
+ updatedAt: expect.stringMatching(ISO_DATETIME_FORMAT),
202
+ createdAt: expect.stringMatching(ISO_DATETIME_FORMAT),
203
+ });
204
+ expect(
205
+ await exchangeCollection.findOne({
206
+ _id: new ObjectId(exchangeId),
207
+ })
208
+ ).toEqual(
209
+ mongoify({
210
+ _id: new ObjectId(exchangeId),
211
+ type: ExchangeTypes.ISSUING,
212
+ tenantId: new ObjectId(tenant._id),
213
+ disclosureId: disclosure._id.toString(),
214
+ events: [{ state: ExchangeStates.NEW, timestamp: expect.any(Date) }],
215
+ offerHashes: [],
216
+ ...credentialTypesObject,
217
+ createdAt: expect.any(Date),
218
+ updatedAt: expect.any(Date),
219
+ })
220
+ );
221
+
222
+ const offerFromDb = await offerCollection.findOne({
223
+ _id: new ObjectId(response.json.id),
224
+ });
225
+
226
+ expect(offerFromDb).toEqual(
227
+ mongoify({
228
+ ...offer,
229
+ credentialSubject: {
230
+ ...offer.credentialSubject,
231
+ type: 'PastEmploymentPosition',
232
+ },
233
+ _id: expect.any(ObjectId),
234
+ tenantId: new ObjectId(tenant._id),
235
+ contentHash: {
236
+ type: 'VelocityContentHash2020',
237
+ value: hashOffer({
238
+ ...offer,
239
+ credentialSubject: {
240
+ ...offer.credentialSubject,
241
+ type: 'PastEmploymentPosition',
242
+ },
243
+ }),
244
+ },
245
+ linkCode: expect.any(String),
246
+ linkCodeCommitment: {
247
+ type: 'VelocityCredentialLinkCodeCommitment2022',
248
+ value: expect.any(String),
249
+ },
250
+ issuer: {
251
+ id: tenant.did,
252
+ },
253
+ createdAt: expect.any(Date),
254
+ updatedAt: expect.any(Date),
255
+ exchangeId,
256
+ })
257
+ );
258
+
259
+ expect(getSchemaNock.pendingMocks()).toHaveLength(1);
260
+
261
+ const responseWithSchemaAlreadyCached = await fastify.injectJson({
262
+ method: 'POST',
263
+ url: exchangeOffersUrl({ tenant, exchange }, ''),
264
+ payload: { ...offer, _id: 123 },
265
+ });
266
+ expect(responseWithSchemaAlreadyCached.statusCode).toEqual(200);
267
+ expect(getSchemaNock.isDone()).toEqual(true);
268
+ });
269
+
270
+ it('/offers should 200 and have an ObjectId _id on created offer', async () => {
271
+ await nockRegistrarAppSchemaName({
272
+ schemaName: 'past-employment-position',
273
+ credentialType: 'PastEmploymentPosition',
274
+ responseJson: require('../combined/schemas/past-employment-position.schema.json'),
275
+ });
276
+
277
+ const offer = await newVendorOffer({ tenant, exchange });
278
+ const response = await fastify.injectJson({
279
+ method: 'POST',
280
+ url: exchangeOffersUrl({ tenant, exchange }, ''),
281
+ payload: { ...offer, _id: '222222220dcddc00099735ba' },
282
+ });
283
+
284
+ expect(response.statusCode).toEqual(200);
285
+ expect(response.json).toEqual({
286
+ ...offer,
287
+ credentialSubject: {
288
+ ...offer.credentialSubject,
289
+ type: 'PastEmploymentPosition',
290
+ },
291
+ exchangeId,
292
+ id: expect.stringMatching(OBJECT_ID_FORMAT),
293
+ issuer: {
294
+ id: tenant.did,
295
+ },
296
+ updatedAt: expect.stringMatching(ISO_DATETIME_FORMAT),
297
+ createdAt: expect.stringMatching(ISO_DATETIME_FORMAT),
298
+ });
299
+ expect(
300
+ await exchangeCollection.findOne({
301
+ _id: new ObjectId(exchangeId),
302
+ })
303
+ ).toEqual(
304
+ mongoify({
305
+ _id: new ObjectId(exchangeId),
306
+ type: ExchangeTypes.ISSUING,
307
+ tenantId: new ObjectId(tenant._id),
308
+ disclosureId: disclosure._id.toString(),
309
+ events: [{ state: ExchangeStates.NEW, timestamp: expect.any(Date) }],
310
+ offerHashes: [],
311
+ ...credentialTypesObject,
312
+ createdAt: expect.any(Date),
313
+ updatedAt: expect.any(Date),
314
+ })
315
+ );
316
+
317
+ const offerFromDb = await offerCollection.findOne({
318
+ _id: new ObjectId(response.json.id),
319
+ });
320
+
321
+ expect(offerFromDb).toEqual(
322
+ mongoify({
323
+ ...offer,
324
+ credentialSubject: {
325
+ ...offer.credentialSubject,
326
+ type: 'PastEmploymentPosition',
327
+ },
328
+ _id: expect.any(ObjectId),
329
+ tenantId: new ObjectId(tenant._id),
330
+ contentHash: {
331
+ type: 'VelocityContentHash2020',
332
+ value: hashOffer({
333
+ ...offer,
334
+ credentialSubject: {
335
+ ...offer.credentialSubject,
336
+ type: 'PastEmploymentPosition',
337
+ },
338
+ }),
339
+ },
340
+ linkCode: expect.any(String),
341
+ linkCodeCommitment: {
342
+ type: 'VelocityCredentialLinkCodeCommitment2022',
343
+ value: expect.any(String),
344
+ },
345
+ createdAt: expect.any(Date),
346
+ updatedAt: expect.any(Date),
347
+ exchangeId,
348
+ issuer: {
349
+ id: tenant.did,
350
+ },
351
+ })
352
+ );
353
+ });
354
+
355
+ it('/offers should 200 and have relatedResources', async () => {
356
+ const credentialIds = [
357
+ 'did:velocity:0x1234567890abcdef',
358
+ 'did:velocity:0x9999999999999999',
359
+ ];
360
+
361
+ const credentials = await Promise.all([
362
+ persistOffer({
363
+ tenant,
364
+ consentedAt: new Date(),
365
+ did: credentialIds[0],
366
+ }),
367
+ persistOffer({
368
+ tenant,
369
+ consentedAt: new Date(),
370
+ did: credentialIds[1],
371
+ digestSRI: nanoid(),
372
+ }),
373
+ ]);
374
+
375
+ const replaces = [{ id: credentialIds[0] }];
376
+ const relatedResource = [
377
+ {
378
+ id: credentialIds[1],
379
+ type: 'RelatedType',
380
+ },
381
+ {
382
+ id: 'data:application/pdf;Aw98ScQOik',
383
+ hint: ['PDF'],
384
+ mediaType: 'application/pdf',
385
+ },
386
+ {
387
+ id: 'http://docs.velocity.network/sample.pdf',
388
+ hint: ['PDF'],
389
+ mediaType: 'application/pdf',
390
+ name: 'Judgement-2024-05-21.pdf',
391
+ digestSRI: 'sha384-EiDrDhbTg9CfPbjtchRjDqE64HGY7Ok4U9mNd8vQZ664AQ',
392
+ },
393
+ ];
394
+
395
+ await nockRegistrarAppSchemaName({
396
+ schemaName: 'past-employment-position',
397
+ credentialType: 'PastEmploymentPosition',
398
+ responseJson: require('../combined/schemas/past-employment-position.schema.json'),
399
+ });
400
+
401
+ const expectedOffer = await newVendorOffer({
402
+ tenant,
403
+ exchange,
404
+ replaces,
405
+ relatedResource,
406
+ });
407
+ const response = await fastify.injectJson({
408
+ method: 'POST',
409
+ url: exchangeOffersUrl({ tenant, exchange }, ''),
410
+ payload: expectedOffer,
411
+ });
412
+
413
+ expect(response.statusCode).toEqual(200);
414
+ expect(response.json).toEqual({
415
+ ...expectedOffer,
416
+ credentialSubject: {
417
+ ...expectedOffer.credentialSubject,
418
+ type: 'PastEmploymentPosition',
419
+ },
420
+ replaces: [
421
+ {
422
+ ...replaces[0],
423
+ hint: credentials[0].type,
424
+ },
425
+ ],
426
+ relatedResource: [
427
+ {
428
+ ...relatedResource[0],
429
+ type: [relatedResource[0].type], // coerced to an array by ajv
430
+ hint: credentials[1].type,
431
+ digestSRI: credentials[1].digestSRI,
432
+ },
433
+ relatedResource[1],
434
+ relatedResource[2],
435
+ ],
436
+ exchangeId,
437
+ id: expect.stringMatching(OBJECT_ID_FORMAT),
438
+ issuer: {
439
+ id: tenant.did,
440
+ },
441
+ updatedAt: expect.stringMatching(ISO_DATETIME_FORMAT),
442
+ createdAt: expect.stringMatching(ISO_DATETIME_FORMAT),
443
+ });
444
+ expect(
445
+ await exchangeCollection.findOne({
446
+ _id: new ObjectId(exchangeId),
447
+ })
448
+ ).toEqual(
449
+ mongoify({
450
+ _id: new ObjectId(exchangeId),
451
+ type: ExchangeTypes.ISSUING,
452
+ tenantId: new ObjectId(tenant._id),
453
+ disclosureId: disclosure._id.toString(),
454
+ events: [{ state: ExchangeStates.NEW, timestamp: expect.any(Date) }],
455
+ offerHashes: [],
456
+ ...credentialTypesObject,
457
+ createdAt: expect.any(Date),
458
+ updatedAt: expect.any(Date),
459
+ })
460
+ );
461
+
462
+ const offerFromDb = await offerCollection.findOne({
463
+ _id: new ObjectId(response.json.id),
464
+ });
465
+
466
+ expect(offerFromDb).toEqual(
467
+ mongoify({
468
+ ...expectedOffer,
469
+ credentialSubject: {
470
+ ...expectedOffer.credentialSubject,
471
+ type: 'PastEmploymentPosition',
472
+ },
473
+ _id: expect.any(ObjectId),
474
+ tenantId: new ObjectId(tenant._id),
475
+ contentHash: {
476
+ type: 'VelocityContentHash2020',
477
+ value: hashOffer({
478
+ ...expectedOffer,
479
+ credentialSubject: {
480
+ ...expectedOffer.credentialSubject,
481
+ type: 'PastEmploymentPosition',
482
+ },
483
+ }),
484
+ },
485
+ linkCode: expect.any(String),
486
+ linkCodeCommitment: {
487
+ type: 'VelocityCredentialLinkCodeCommitment2022',
488
+ value: expect.any(String),
489
+ },
490
+ createdAt: expect.any(Date),
491
+ updatedAt: expect.any(Date),
492
+ exchangeId,
493
+ issuer: {
494
+ id: tenant.did,
495
+ },
496
+ replaces: [
497
+ {
498
+ ...replaces[0],
499
+ hint: credentials[0].type,
500
+ },
501
+ ],
502
+ relatedResource: [
503
+ {
504
+ ...relatedResource[0],
505
+ type: [relatedResource[0].type], // coerced to an array by ajv
506
+ hint: credentials[1].type,
507
+ digestSRI: credentials[1].digestSRI,
508
+ },
509
+ relatedResource[1],
510
+ relatedResource[2],
511
+ ],
512
+ })
513
+ );
514
+ });
515
+
516
+ it('/offers should 200 and adding an openbadge v3', async () => {
517
+ const payload = await newVendorOffer({
518
+ tenant,
519
+ exchange,
520
+ ...openBadgeCredentialExample,
521
+ });
522
+
523
+ nockRegistrarAppSchemaName({
524
+ schemaName: 'open-badge-credential',
525
+ credentialType: 'OpenBadgeCredential',
526
+ responseJson: require('../combined/schemas/open-badge-credential.schema.json'),
527
+ repeatCount: 1,
528
+ });
529
+
530
+ nock('https://imsglobal.org')
531
+ .get('/schemas/open-badge-v3.0-schema.json')
532
+ .reply(
533
+ 200,
534
+ require('../combined/schemas/open-badge-credential.schema.json')
535
+ );
536
+
537
+ const response = await fastify.injectJson({
538
+ method: 'POST',
539
+ url: exchangeOffersUrl({ tenant, exchange }, ''),
540
+ payload,
541
+ });
542
+
543
+ expect(response.statusCode).toEqual(200);
544
+ expect(response.json).toEqual({
545
+ ...payload,
546
+ credentialSubject: {
547
+ ...payload.credentialSubject,
548
+ type: 'AchievementSubject',
549
+ },
550
+ exchangeId,
551
+ id: expect.stringMatching(OBJECT_ID_FORMAT),
552
+ issuer: {
553
+ id: tenant.did,
554
+ },
555
+ updatedAt: expect.stringMatching(ISO_DATETIME_FORMAT),
556
+ createdAt: expect.stringMatching(ISO_DATETIME_FORMAT),
557
+ });
558
+ });
559
+
560
+ it('/offers should 200 when credential id is a did but is not found in the offers collection', async () => {
561
+ const resourceReference = {
562
+ replaces: [
563
+ {
564
+ id: 'did:velocity:none',
565
+ type: ['VC'],
566
+ },
567
+ ],
568
+ relatedResource: [
569
+ {
570
+ id: 'did:velocity:none',
571
+ type: ['VC'],
572
+ },
573
+ ],
574
+ };
575
+
576
+ await nockRegistrarAppSchemaName({
577
+ schemaName: 'past-employment-position',
578
+ credentialType: 'PastEmploymentPosition',
579
+ responseJson: require('../combined/schemas/past-employment-position.schema.json'),
580
+ });
581
+ await persistOffer({
582
+ tenant,
583
+ consentedAt: new Date(),
584
+ did: 'did:velocity:another',
585
+ });
586
+ const expectedOffer = await newVendorOffer({
587
+ tenant,
588
+ exchange,
589
+ ...resourceReference,
590
+ });
591
+ const response = await fastify.injectJson({
592
+ method: 'POST',
593
+ url: exchangeOffersUrl({ tenant, exchange }, ''),
594
+ payload: expectedOffer,
595
+ });
596
+
597
+ expect(response.statusCode).toEqual(200);
598
+ expect(response.json).toEqual({
599
+ ...expectedOffer,
600
+ credentialSubject: {
601
+ ...expectedOffer.credentialSubject,
602
+ type: 'PastEmploymentPosition',
603
+ },
604
+ exchangeId,
605
+ id: expect.stringMatching(OBJECT_ID_FORMAT),
606
+ issuer: {
607
+ id: tenant.did,
608
+ },
609
+ ...resourceReference,
610
+ updatedAt: expect.stringMatching(ISO_DATETIME_FORMAT),
611
+ createdAt: expect.stringMatching(ISO_DATETIME_FORMAT),
612
+ });
613
+ expect(
614
+ await exchangeCollection.findOne({
615
+ _id: new ObjectId(exchangeId),
616
+ })
617
+ ).toEqual(
618
+ mongoify({
619
+ _id: new ObjectId(exchangeId),
620
+ type: ExchangeTypes.ISSUING,
621
+ tenantId: new ObjectId(tenant._id),
622
+ disclosureId: disclosure._id.toString(),
623
+ events: [{ state: ExchangeStates.NEW, timestamp: expect.any(Date) }],
624
+ offerHashes: [],
625
+ ...credentialTypesObject,
626
+ createdAt: expect.any(Date),
627
+ updatedAt: expect.any(Date),
628
+ })
629
+ );
630
+
631
+ const offerFromDb = await offerCollection.findOne({
632
+ _id: new ObjectId(response.json.id),
633
+ });
634
+
635
+ expect(offerFromDb).toEqual(
636
+ mongoify({
637
+ ...expectedOffer,
638
+ credentialSubject: {
639
+ ...expectedOffer.credentialSubject,
640
+ type: 'PastEmploymentPosition',
641
+ },
642
+ _id: expect.any(ObjectId),
643
+ tenantId: new ObjectId(tenant._id),
644
+ contentHash: {
645
+ type: 'VelocityContentHash2020',
646
+ value: hashOffer({
647
+ ...expectedOffer,
648
+ credentialSubject: {
649
+ ...expectedOffer.credentialSubject,
650
+ type: 'PastEmploymentPosition',
651
+ },
652
+ }),
653
+ },
654
+ createdAt: expect.any(Date),
655
+ updatedAt: expect.any(Date),
656
+ exchangeId,
657
+ linkCode: expect.any(String),
658
+ linkCodeCommitment: {
659
+ type: 'VelocityCredentialLinkCodeCommitment2022',
660
+ value: expect.any(String),
661
+ },
662
+ issuer: {
663
+ id: tenant.did,
664
+ },
665
+ ...resourceReference,
666
+ })
667
+ );
668
+ });
669
+
670
+ it('Ad/offers should 200 when the id is a URL and therefore can not be found in the offers collection', async () => {
671
+ const resourceReference = {
672
+ replaces: [
673
+ {
674
+ id: 'https://example.com/pdf',
675
+ },
676
+ ],
677
+ relatedResource: [
678
+ {
679
+ id: 'https://example.com/pdf',
680
+ },
681
+ ],
682
+ };
683
+
684
+ await nockRegistrarAppSchemaName({
685
+ schemaName: 'past-employment-position',
686
+ credentialType: 'PastEmploymentPosition',
687
+ responseJson: require('../combined/schemas/past-employment-position.schema.json'),
688
+ });
689
+ await persistOffer({
690
+ tenant,
691
+ consentedAt: new Date(),
692
+ did: 'did:velocity:another',
693
+ });
694
+ const expectedOffer = await newVendorOffer({
695
+ tenant,
696
+ exchange,
697
+ ...resourceReference,
698
+ });
699
+ const response = await fastify.injectJson({
700
+ method: 'POST',
701
+ url: exchangeOffersUrl({ tenant, exchange }, ''),
702
+ payload: expectedOffer,
703
+ });
704
+
705
+ expect(response.statusCode).toEqual(200);
706
+ expect(response.json).toEqual({
707
+ ...expectedOffer,
708
+ credentialSubject: {
709
+ ...expectedOffer.credentialSubject,
710
+ type: 'PastEmploymentPosition',
711
+ },
712
+ exchangeId,
713
+ id: expect.stringMatching(OBJECT_ID_FORMAT),
714
+ issuer: {
715
+ id: tenant.did,
716
+ },
717
+ ...resourceReference,
718
+ updatedAt: expect.stringMatching(ISO_DATETIME_FORMAT),
719
+ createdAt: expect.stringMatching(ISO_DATETIME_FORMAT),
720
+ });
721
+ expect(
722
+ await exchangeCollection.findOne({
723
+ _id: new ObjectId(exchangeId),
724
+ })
725
+ ).toEqual(
726
+ mongoify({
727
+ _id: new ObjectId(exchangeId),
728
+ type: ExchangeTypes.ISSUING,
729
+ tenantId: new ObjectId(tenant._id),
730
+ disclosureId: disclosure._id.toString(),
731
+ events: [{ state: ExchangeStates.NEW, timestamp: expect.any(Date) }],
732
+ offerHashes: [],
733
+ ...credentialTypesObject,
734
+ createdAt: expect.any(Date),
735
+ updatedAt: expect.any(Date),
736
+ })
737
+ );
738
+
739
+ const offerFromDb = await offerCollection.findOne({
740
+ _id: new ObjectId(response.json.id),
741
+ });
742
+
743
+ expect(offerFromDb).toEqual(
744
+ mongoify({
745
+ ...expectedOffer,
746
+ credentialSubject: {
747
+ ...expectedOffer.credentialSubject,
748
+ type: 'PastEmploymentPosition',
749
+ },
750
+ _id: expect.any(ObjectId),
751
+ tenantId: new ObjectId(tenant._id),
752
+ contentHash: {
753
+ type: 'VelocityContentHash2020',
754
+ value: hashOffer({
755
+ ...expectedOffer,
756
+ credentialSubject: {
757
+ ...expectedOffer.credentialSubject,
758
+ type: 'PastEmploymentPosition',
759
+ },
760
+ }),
761
+ },
762
+ linkCode: expect.any(String),
763
+ linkCodeCommitment: {
764
+ type: 'VelocityCredentialLinkCodeCommitment2022',
765
+ value: expect.any(String),
766
+ },
767
+ createdAt: expect.any(Date),
768
+ updatedAt: expect.any(Date),
769
+ exchangeId,
770
+ ...resourceReference,
771
+ issuer: {
772
+ id: tenant.did,
773
+ },
774
+ })
775
+ );
776
+ });
777
+
778
+ it('/offers should 200 when only one relatedResource is set but is missing targetDid', async () => {
779
+ const targetDid = 'did:velocity:0x1234567890abcdef';
780
+ await nockRegistrarAppSchemaName({
781
+ schemaName: 'past-employment-position',
782
+ credentialType: 'PastEmploymentPosition',
783
+ responseJson: require('../combined/schemas/past-employment-position.schema.json'),
784
+ });
785
+ const expectedOffer = await newVendorOffer({
786
+ tenant,
787
+ exchange,
788
+ relatedResource: [
789
+ {
790
+ id: targetDid,
791
+ type: ['VC'],
792
+ },
793
+ ],
794
+ });
795
+ const response = await fastify.injectJson({
796
+ method: 'POST',
797
+ url: exchangeOffersUrl({ tenant, exchange }, ''),
798
+ payload: expectedOffer,
799
+ });
800
+
801
+ expect(response.statusCode).toEqual(200);
802
+ expect(response.json).toEqual({
803
+ ...expectedOffer,
804
+ credentialSubject: {
805
+ ...expectedOffer.credentialSubject,
806
+ type: 'PastEmploymentPosition',
807
+ },
808
+ exchangeId,
809
+ id: expect.stringMatching(OBJECT_ID_FORMAT),
810
+ issuer: {
811
+ id: tenant.did,
812
+ },
813
+ updatedAt: expect.stringMatching(ISO_DATETIME_FORMAT),
814
+ createdAt: expect.stringMatching(ISO_DATETIME_FORMAT),
815
+ });
816
+ expect(
817
+ await exchangeCollection.findOne({
818
+ _id: new ObjectId(exchangeId),
819
+ })
820
+ ).toEqual(
821
+ mongoify({
822
+ _id: new ObjectId(exchangeId),
823
+ type: ExchangeTypes.ISSUING,
824
+ tenantId: new ObjectId(tenant._id),
825
+ disclosureId: disclosure._id.toString(),
826
+ events: [{ state: ExchangeStates.NEW, timestamp: expect.any(Date) }],
827
+ offerHashes: [],
828
+ ...credentialTypesObject,
829
+ createdAt: expect.any(Date),
830
+ updatedAt: expect.any(Date),
831
+ })
832
+ );
833
+
834
+ const offerFromDb = await offerCollection.findOne({
835
+ _id: new ObjectId(response.json.id),
836
+ });
837
+
838
+ expect(offerFromDb).toEqual(
839
+ mongoify({
840
+ ...expectedOffer,
841
+ credentialSubject: {
842
+ ...expectedOffer.credentialSubject,
843
+ type: 'PastEmploymentPosition',
844
+ },
845
+ _id: expect.any(ObjectId),
846
+ tenantId: new ObjectId(tenant._id),
847
+ contentHash: {
848
+ type: 'VelocityContentHash2020',
849
+ value: hashOffer({
850
+ ...expectedOffer,
851
+ credentialSubject: {
852
+ ...expectedOffer.credentialSubject,
853
+ type: 'PastEmploymentPosition',
854
+ },
855
+ }),
856
+ },
857
+ linkCode: expect.any(String),
858
+ linkCodeCommitment: {
859
+ type: 'VelocityCredentialLinkCodeCommitment2022',
860
+ value: expect.any(String),
861
+ },
862
+ createdAt: expect.any(Date),
863
+ updatedAt: expect.any(Date),
864
+ exchangeId,
865
+ issuer: {
866
+ id: tenant.did,
867
+ },
868
+ })
869
+ );
870
+ });
871
+
872
+ it('/offers should 200 and have an ObjectId _id on created offer 2', async () => {
873
+ await nockRegistrarAppSchemaName({
874
+ schemaName: 'past-employment-position',
875
+ credentialType: 'PastEmploymentPosition',
876
+ responseJson: require('../combined/schemas/past-employment-position.schema.json'),
877
+ });
878
+
879
+ const offer = await newVendorOffer({ tenant, exchange });
880
+ const response = await fastify.injectJson({
881
+ method: 'POST',
882
+ url: exchangeOffersUrl({ tenant, exchange }, ''),
883
+ payload: {
884
+ ...offer,
885
+ _id: '222222220dcddc00099735ba',
886
+ },
887
+ });
888
+
889
+ expect(response.statusCode).toEqual(200);
890
+ expect(response.json).toEqual({
891
+ ...offer,
892
+ credentialSubject: {
893
+ ...offer.credentialSubject,
894
+ type: 'PastEmploymentPosition',
895
+ },
896
+ exchangeId,
897
+ id: expect.stringMatching(OBJECT_ID_FORMAT),
898
+ issuer: {
899
+ id: tenant.did,
900
+ },
901
+ updatedAt: expect.stringMatching(ISO_DATETIME_FORMAT),
902
+ createdAt: expect.stringMatching(ISO_DATETIME_FORMAT),
903
+ });
904
+ expect(
905
+ await exchangeCollection.findOne({
906
+ _id: new ObjectId(exchangeId),
907
+ })
908
+ ).toEqual(
909
+ mongoify({
910
+ _id: new ObjectId(exchangeId),
911
+ type: ExchangeTypes.ISSUING,
912
+ tenantId: new ObjectId(tenant._id),
913
+ disclosureId: disclosure._id.toString(),
914
+ events: [{ state: ExchangeStates.NEW, timestamp: expect.any(Date) }],
915
+ offerHashes: [],
916
+ ...credentialTypesObject,
917
+ createdAt: expect.any(Date),
918
+ updatedAt: expect.any(Date),
919
+ })
920
+ );
921
+
922
+ const offerFromDb = await offerCollection.findOne({
923
+ _id: new ObjectId(response.json.id),
924
+ });
925
+
926
+ expect(offerFromDb).toEqual(
927
+ mongoify({
928
+ ...offer,
929
+ credentialSubject: {
930
+ ...offer.credentialSubject,
931
+ type: 'PastEmploymentPosition',
932
+ },
933
+ _id: expect.any(ObjectId),
934
+ tenantId: new ObjectId(tenant._id),
935
+ contentHash: {
936
+ type: 'VelocityContentHash2020',
937
+ value: hashOffer({
938
+ credentialSubject: {
939
+ ...offer.credentialSubject,
940
+ type: 'PastEmploymentPosition',
941
+ },
942
+ }),
943
+ },
944
+ linkCode: expect.any(String),
945
+ linkCodeCommitment: {
946
+ type: 'VelocityCredentialLinkCodeCommitment2022',
947
+ value: expect.any(String),
948
+ },
949
+ createdAt: expect.any(Date),
950
+ updatedAt: expect.any(Date),
951
+ exchangeId,
952
+ issuer: {
953
+ id: tenant.did,
954
+ },
955
+ })
956
+ );
957
+ });
958
+
959
+ it('/offers should 200 and have issuer name and image on created offer', async () => {
960
+ await nockRegistrarAppSchemaName({
961
+ schemaName: 'past-employment-position',
962
+ credentialType: 'PastEmploymentPosition',
963
+ responseJson: require('../combined/schemas/past-employment-position.schema.json'),
964
+ });
965
+
966
+ const offer = await newVendorOffer({ tenant, exchange });
967
+ const response = await fastify.injectJson({
968
+ method: 'POST',
969
+ url: exchangeOffersUrl({ tenant, exchange }, ''),
970
+ payload: {
971
+ ...offer,
972
+ issuer: {
973
+ id: tenant.did,
974
+ name: 'image',
975
+ image: 'http://image.com',
976
+ },
977
+ },
978
+ });
979
+
980
+ expect(response.statusCode).toEqual(200);
981
+ expect(response.json).toEqual({
982
+ ...offer,
983
+ credentialSubject: {
984
+ ...offer.credentialSubject,
985
+ type: 'PastEmploymentPosition',
986
+ },
987
+ exchangeId,
988
+ id: expect.stringMatching(OBJECT_ID_FORMAT),
989
+ issuer: {
990
+ id: tenant.did,
991
+ name: 'image',
992
+ image: 'http://image.com',
993
+ },
994
+ updatedAt: expect.stringMatching(ISO_DATETIME_FORMAT),
995
+ createdAt: expect.stringMatching(ISO_DATETIME_FORMAT),
996
+ });
997
+ expect(
998
+ await exchangeCollection.findOne({
999
+ _id: new ObjectId(exchangeId),
1000
+ })
1001
+ ).toEqual(
1002
+ mongoify({
1003
+ _id: new ObjectId(exchangeId),
1004
+ type: ExchangeTypes.ISSUING,
1005
+ tenantId: new ObjectId(tenant._id),
1006
+ disclosureId: disclosure._id.toString(),
1007
+ events: [{ state: ExchangeStates.NEW, timestamp: expect.any(Date) }],
1008
+ offerHashes: [],
1009
+ ...credentialTypesObject,
1010
+ createdAt: expect.any(Date),
1011
+ updatedAt: expect.any(Date),
1012
+ })
1013
+ );
1014
+
1015
+ const offerFromDb = await offerCollection.findOne({
1016
+ _id: new ObjectId(response.json.id),
1017
+ });
1018
+
1019
+ expect(offerFromDb).toEqual(
1020
+ mongoify({
1021
+ ...offer,
1022
+ credentialSubject: {
1023
+ ...offer.credentialSubject,
1024
+ type: 'PastEmploymentPosition',
1025
+ },
1026
+ _id: expect.any(ObjectId),
1027
+ tenantId: new ObjectId(tenant._id),
1028
+ contentHash: {
1029
+ type: 'VelocityContentHash2020',
1030
+ value: hashOffer({
1031
+ ...offer,
1032
+ credentialSubject: {
1033
+ ...offer.credentialSubject,
1034
+ type: 'PastEmploymentPosition',
1035
+ },
1036
+ }),
1037
+ },
1038
+ linkCode: expect.any(String),
1039
+ linkCodeCommitment: {
1040
+ type: 'VelocityCredentialLinkCodeCommitment2022',
1041
+ value: expect.any(String),
1042
+ },
1043
+ createdAt: expect.any(Date),
1044
+ updatedAt: expect.any(Date),
1045
+ exchangeId,
1046
+ issuer: {
1047
+ id: tenant.did,
1048
+ name: 'image',
1049
+ image: 'http://image.com',
1050
+ },
1051
+ })
1052
+ );
1053
+ });
1054
+
1055
+ it('/offers should 200 and have issuer with bad property in the issuer', async () => {
1056
+ await nockRegistrarAppSchemaName({
1057
+ schemaName: 'past-employment-position',
1058
+ credentialType: 'PastEmploymentPosition',
1059
+ responseJson: require('../combined/schemas/past-employment-position.schema.json'),
1060
+ });
1061
+
1062
+ const offer = await newVendorOffer({ tenant, exchange });
1063
+ const response = await fastify.injectJson({
1064
+ method: 'POST',
1065
+ url: exchangeOffersUrl({ tenant, exchange }, ''),
1066
+ payload: {
1067
+ ...offer,
1068
+ issuer: {
1069
+ id: tenant.did,
1070
+ name: 'image',
1071
+ image: 'http://image.com',
1072
+ badProperty: 'badProperty',
1073
+ },
1074
+ },
1075
+ });
1076
+
1077
+ expect(response.statusCode).toEqual(200);
1078
+ expect(response.json).toEqual({
1079
+ ...offer,
1080
+ credentialSubject: {
1081
+ ...offer.credentialSubject,
1082
+ type: 'PastEmploymentPosition',
1083
+ },
1084
+ exchangeId,
1085
+ id: expect.stringMatching(OBJECT_ID_FORMAT),
1086
+ issuer: {
1087
+ id: tenant.did,
1088
+ name: 'image',
1089
+ image: 'http://image.com',
1090
+ },
1091
+ updatedAt: expect.stringMatching(ISO_DATETIME_FORMAT),
1092
+ createdAt: expect.stringMatching(ISO_DATETIME_FORMAT),
1093
+ });
1094
+ expect(
1095
+ await exchangeCollection.findOne({
1096
+ _id: new ObjectId(exchangeId),
1097
+ })
1098
+ ).toEqual(
1099
+ mongoify({
1100
+ _id: new ObjectId(exchangeId),
1101
+ type: ExchangeTypes.ISSUING,
1102
+ tenantId: new ObjectId(tenant._id),
1103
+ disclosureId: new ObjectId(disclosure._id),
1104
+ events: [{ state: ExchangeStates.NEW, timestamp: expect.any(Date) }],
1105
+ offerHashes: [],
1106
+ ...credentialTypesObject,
1107
+ createdAt: expect.any(Date),
1108
+ updatedAt: expect.any(Date),
1109
+ })
1110
+ );
1111
+
1112
+ const offerFromDb = await offerCollection.findOne({
1113
+ _id: new ObjectId(response.json.id),
1114
+ });
1115
+
1116
+ expect(offerFromDb).toEqual(
1117
+ mongoify({
1118
+ ...offer,
1119
+ credentialSubject: {
1120
+ ...offer.credentialSubject,
1121
+ type: 'PastEmploymentPosition',
1122
+ },
1123
+ _id: expect.any(ObjectId),
1124
+ tenantId: new ObjectId(tenant._id),
1125
+ contentHash: {
1126
+ type: 'VelocityContentHash2020',
1127
+ value: hashOffer({
1128
+ ...offer,
1129
+ credentialSubject: {
1130
+ ...offer.credentialSubject,
1131
+ type: 'PastEmploymentPosition',
1132
+ },
1133
+ }),
1134
+ },
1135
+ linkCode: expect.any(String),
1136
+ linkCodeCommitment: {
1137
+ type: 'VelocityCredentialLinkCodeCommitment2022',
1138
+ value: expect.any(String),
1139
+ },
1140
+ createdAt: expect.any(Date),
1141
+ updatedAt: expect.any(Date),
1142
+ exchangeId,
1143
+ issuer: {
1144
+ id: tenant.did,
1145
+ name: 'image',
1146
+ image: 'http://image.com',
1147
+ },
1148
+ })
1149
+ );
1150
+ });
1151
+
1152
+ it('/offers should 200 and add default type to credential subject', async () => {
1153
+ fastify.overrides.reqConfig = (config) => ({
1154
+ ...config,
1155
+ enableOfferValidation: true,
1156
+ });
1157
+ ({ persistOfferExchange } = initOfferExchangeFactory(fastify));
1158
+ ({ newOffer, persistOffer } = initOfferFactory(fastify));
1159
+ ({ persistTenant } = initTenantFactory(fastify));
1160
+ ({ persistVendorUserIdMapping } = initUserFactory(fastify));
1161
+ offerCollection = mongoDb().collection('offers');
1162
+ exchangeCollection = mongoDb().collection('exchanges');
1163
+
1164
+ const getSchemaNock = nockRegistrarAppSchemaName();
1165
+
1166
+ const offer = await newVendorOffer({ tenant, exchange });
1167
+ const response = await fastify.injectJson({
1168
+ method: 'POST',
1169
+ url: exchangeOffersUrl({ tenant, exchange }, ''),
1170
+ payload: { ...offer, _id: 'mock' },
1171
+ });
1172
+
1173
+ expect(response.statusCode).toEqual(200);
1174
+ const offerFromDb = await offerCollection.findOne({
1175
+ _id: new ObjectId(response.json.id),
1176
+ });
1177
+
1178
+ expect(offerFromDb).toEqual(
1179
+ mongoify({
1180
+ ...offer,
1181
+ credentialSubject: {
1182
+ ...offer.credentialSubject,
1183
+ type: 'PastEmploymentPosition',
1184
+ },
1185
+ _id: expect.any(ObjectId),
1186
+ tenantId: new ObjectId(tenant._id),
1187
+ contentHash: {
1188
+ type: 'VelocityContentHash2020',
1189
+ value: hashOffer({
1190
+ ...offer,
1191
+ credentialSubject: {
1192
+ ...offer.credentialSubject,
1193
+ type: 'PastEmploymentPosition',
1194
+ },
1195
+ }),
1196
+ },
1197
+ linkCode: expect.any(String),
1198
+ linkCodeCommitment: {
1199
+ type: 'VelocityCredentialLinkCodeCommitment2022',
1200
+ value: expect.any(String),
1201
+ },
1202
+ issuer: {
1203
+ id: tenant.did,
1204
+ },
1205
+ createdAt: expect.any(Date),
1206
+ updatedAt: expect.any(Date),
1207
+ exchangeId,
1208
+ })
1209
+ );
1210
+
1211
+ expect(getSchemaNock.isDone()).toEqual(true);
1212
+ });
1213
+
1214
+ it('/offers should 200 and pass validation with open badge credential schema', async () => {
1215
+ fastify.overrides.reqConfig = (config) => ({
1216
+ ...config,
1217
+ enableOfferValidation: true,
1218
+ });
1219
+ ({ persistOfferExchange } = initOfferExchangeFactory(fastify));
1220
+ ({ newOffer, persistOffer } = initOfferFactory(fastify));
1221
+ ({ persistTenant } = initTenantFactory(fastify));
1222
+ ({ persistVendorUserIdMapping } = initUserFactory(fastify));
1223
+ offerCollection = mongoDb().collection('offers');
1224
+ exchangeCollection = mongoDb().collection('exchanges');
1225
+
1226
+ const getSchemaNock = nockRegistrarAppSchemaName({
1227
+ schemaName: 'open-badge-credential',
1228
+ credentialType: 'OpenBadgeCredential',
1229
+ responseJson: require('../combined/schemas/open-badge-credential.schema.json'),
1230
+ });
1231
+
1232
+ const offer = await newVendorOffer({
1233
+ tenant,
1234
+ exchange,
1235
+ type: ['OpenBadgeCredential'],
1236
+ credentialSubject: {
1237
+ vendorUserId: '1234765',
1238
+ achievement: {
1239
+ type: 'Achievement',
1240
+ id: 'https://velocitynetwork.foundation/credentials/openbadgecredential-5',
1241
+ name: 'Our Wallet Passed JFF Plugfest #1 2022',
1242
+ description: 'This wallet can display this Open Badge 3.0',
1243
+ criteria: {
1244
+ narrative:
1245
+ 'The first cohort of the JFF Plugfest 1 in May/June of 2021 collaborated to push interoperability of VCs in education forward.',
1246
+ },
1247
+ image: {
1248
+ id: 'https://w3c-ccg.github.io/vc-ed/plugfest-1-2022/images/plugfest-1-badge-image.png',
1249
+ type: 'Image',
1250
+ },
1251
+ },
1252
+ },
1253
+ });
1254
+ const response = await fastify.injectJson({
1255
+ method: 'POST',
1256
+ url: exchangeOffersUrl({ tenant, exchange }, ''),
1257
+ payload: { ...offer, _id: 'mock' },
1258
+ });
1259
+
1260
+ expect(response.statusCode).toEqual(200);
1261
+ const offerFromDb = await offerCollection.findOne({
1262
+ _id: new ObjectId(response.json.id),
1263
+ });
1264
+
1265
+ expect(offerFromDb).toEqual(
1266
+ mongoify({
1267
+ ...offer,
1268
+ credentialSubject: {
1269
+ ...offer.credentialSubject,
1270
+ type: 'AchievementSubject',
1271
+ },
1272
+ _id: expect.any(ObjectId),
1273
+ tenantId: new ObjectId(tenant._id),
1274
+ contentHash: {
1275
+ type: 'VelocityContentHash2020',
1276
+ value: hashOffer({
1277
+ ...offer,
1278
+ credentialSubject: {
1279
+ ...offer.credentialSubject,
1280
+ type: 'AchievementSubject',
1281
+ },
1282
+ }),
1283
+ },
1284
+ linkCode: expect.any(String),
1285
+ linkCodeCommitment: {
1286
+ type: 'VelocityCredentialLinkCodeCommitment2022',
1287
+ value: expect.any(String),
1288
+ },
1289
+ issuer: {
1290
+ id: tenant.did,
1291
+ },
1292
+ createdAt: expect.any(Date),
1293
+ updatedAt: expect.any(Date),
1294
+ exchangeId,
1295
+ })
1296
+ );
1297
+
1298
+ expect(getSchemaNock.isDone()).toEqual(true);
1299
+ });
1300
+
1301
+ it('/offers should 200 and get schema from libapp with version', async () => {
1302
+ fastify.overrides.reqConfig = (config) => ({
1303
+ ...config,
1304
+ enableOfferValidation: true,
1305
+ });
1306
+ ({ persistOfferExchange } = initOfferExchangeFactory(fastify));
1307
+ ({ newOffer, persistOffer } = initOfferFactory(fastify));
1308
+ ({ persistTenant } = initTenantFactory(fastify));
1309
+ ({ persistVendorUserIdMapping } = initUserFactory(fastify));
1310
+ offerCollection = mongoDb().collection('offers');
1311
+ exchangeCollection = mongoDb().collection('exchanges');
1312
+
1313
+ const getSchemaNock = nockRegistrarAppSchemaName({
1314
+ schemaName: 'employment-current-v1.1',
1315
+ credentialType: 'EmploymentCurrentV1.1',
1316
+ responseJson: require('../combined/schemas/employment-current-v1.1.schema.json'),
1317
+ });
1318
+
1319
+ const offer = await newVendorOffer({
1320
+ tenant,
1321
+ exchange,
1322
+ type: ['EmploymentCurrentV1.1'],
1323
+ // linkedCredentials: [{ linkType: 'REPLACE', linkedCredentialId: '' }],
1324
+ credentialSubject: {
1325
+ vendorUserId: 'olivia.hafez@example.com',
1326
+ '@context': 'https://velocitynetwork.foundation/contexts/employment',
1327
+ legalEmployer: {
1328
+ name: 'Microsoft Corporation ion',
1329
+ identifier:
1330
+ 'did:ion:EiAbP9xvCYnUOiLwqgbkV4auH_26Pv7BT2pYYT3masvvhw',
1331
+ place: {
1332
+ addressLocality: 'Bellevue',
1333
+ addressRegion: 'US-WA',
1334
+ addressCountry: 'US',
1335
+ },
1336
+ },
1337
+ role: 'Project Manager (current)',
1338
+ description: 'Backend development project management',
1339
+ employmentType: ['permanent'],
1340
+ place: {
1341
+ name: 'Media Lab',
1342
+ addressLocality: 'Buffalo',
1343
+ addressRegion: 'US-NY',
1344
+ addressCountry: 'US',
1345
+ },
1346
+ startDate: '2013-10-01',
1347
+ recipient: {
1348
+ givenName: 'Olivia',
1349
+ familyName: 'Hafez',
1350
+ middleName: 'Melanie',
1351
+ namePrefix: 'Dr.',
1352
+ nameSuffix: 'Mrs.',
1353
+ },
1354
+ alignment: [
1355
+ {
1356
+ targetName: 'Test Name',
1357
+ targetUrl:
1358
+ 'https://credentialfinder.org/credential/5769/Bachelor_of_Science_in_Nursing_RN_to_BSN',
1359
+ targetFramework: 'Test Framework',
1360
+ },
1361
+ ],
1362
+ },
1363
+ });
1364
+ const response = await fastify.injectJson({
1365
+ method: 'POST',
1366
+ url: exchangeOffersUrl({ tenant, exchange }, ''),
1367
+ payload: { ...offer, _id: 'mock' },
1368
+ });
1369
+
1370
+ expect(response.statusCode).toEqual(200);
1371
+ const offerFromDb = await offerCollection.findOne({
1372
+ _id: new ObjectId(response.json.id),
1373
+ });
1374
+ expect(offerFromDb).toEqual(
1375
+ mongoify({
1376
+ ...offer,
1377
+ credentialSubject: {
1378
+ ...offer.credentialSubject,
1379
+ employmentType: ['permanent'],
1380
+ alignment: [
1381
+ {
1382
+ targetFramework: 'Test Framework',
1383
+ targetName: 'Test Name',
1384
+ targetUrl:
1385
+ 'https://credentialfinder.org/credential/5769/Bachelor_of_Science_in_Nursing_RN_to_BSN',
1386
+ type: 'AlignmentObject',
1387
+ },
1388
+ ],
1389
+ legalEmployer: {
1390
+ ...offer.credentialSubject.legalEmployer,
1391
+ place: {
1392
+ ...offer.credentialSubject.legalEmployer.place,
1393
+ type: 'Place',
1394
+ },
1395
+ type: 'Organization',
1396
+ },
1397
+ place: {
1398
+ ...offer.credentialSubject.place,
1399
+ type: 'Place',
1400
+ },
1401
+ recipient: {
1402
+ ...offer.credentialSubject.recipient,
1403
+ type: 'PersonName',
1404
+ },
1405
+ type: 'Employment',
1406
+ },
1407
+ _id: expect.any(ObjectId),
1408
+ tenantId: new ObjectId(tenant._id),
1409
+ contentHash: {
1410
+ type: 'VelocityContentHash2020',
1411
+ value: hashOffer({
1412
+ ...offer,
1413
+ credentialSubject: {
1414
+ ...offer.credentialSubject,
1415
+ employmentType: ['permanent'],
1416
+ alignment: [
1417
+ {
1418
+ targetFramework: 'Test Framework',
1419
+ targetName: 'Test Name',
1420
+ targetUrl:
1421
+ 'https://credentialfinder.org/credential/5769/Bachelor_of_Science_in_Nursing_RN_to_BSN',
1422
+ type: 'AlignmentObject',
1423
+ },
1424
+ ],
1425
+ legalEmployer: {
1426
+ ...offer.credentialSubject.legalEmployer,
1427
+ place: {
1428
+ ...offer.credentialSubject.legalEmployer.place,
1429
+ type: 'Place',
1430
+ },
1431
+ type: 'Organization',
1432
+ },
1433
+ place: {
1434
+ ...offer.credentialSubject.place,
1435
+ type: 'Place',
1436
+ },
1437
+ recipient: {
1438
+ ...offer.credentialSubject.recipient,
1439
+ type: 'PersonName',
1440
+ },
1441
+ type: 'Employment',
1442
+ },
1443
+ }),
1444
+ },
1445
+ linkCode: expect.any(String),
1446
+ linkCodeCommitment: {
1447
+ type: 'VelocityCredentialLinkCodeCommitment2022',
1448
+ value: expect.any(String),
1449
+ },
1450
+ issuer: {
1451
+ id: tenant.did,
1452
+ },
1453
+ createdAt: expect.any(Date),
1454
+ updatedAt: expect.any(Date),
1455
+ exchangeId,
1456
+ })
1457
+ );
1458
+
1459
+ expect(getSchemaNock.isDone()).toEqual(true);
1460
+ });
1461
+
1462
+ it('/offers should 200 when invalid offer when enableOfferValidation is false', async () => {
1463
+ fastify.overrides.reqConfig = (config) => ({
1464
+ ...config,
1465
+ enableOfferValidation: false,
1466
+ });
1467
+
1468
+ const getSchemaNock = nockRegistrarAppSchemaName();
1469
+
1470
+ const offer = await newVendorOffer({ tenant, exchange });
1471
+ const response = await fastify.injectJson({
1472
+ method: 'POST',
1473
+ url: exchangeOffersUrl({ tenant, exchange }, ''),
1474
+ payload: {
1475
+ _id: 'nonsense',
1476
+ ...omit(['credentialSubject.startMonthYear'], offer),
1477
+ },
1478
+ });
1479
+
1480
+ expect(response.statusCode).toEqual(200);
1481
+ expect(
1482
+ await exchangeCollection.findOne({
1483
+ _id: new ObjectId(exchangeId),
1484
+ })
1485
+ ).toEqual(
1486
+ mongoify({
1487
+ _id: new ObjectId(exchangeId),
1488
+ type: ExchangeTypes.ISSUING,
1489
+ tenantId: new ObjectId(tenant._id),
1490
+ disclosureId: disclosure._id.toString(),
1491
+ events: [{ state: ExchangeStates.NEW, timestamp: expect.any(Date) }],
1492
+ offerHashes: [],
1493
+ ...credentialTypesObject,
1494
+ createdAt: expect.any(Date),
1495
+ updatedAt: expect.any(Date),
1496
+ })
1497
+ );
1498
+
1499
+ expect(getSchemaNock.isDone()).toEqual(true);
1500
+ });
1501
+
1502
+ it('/offers should 200 and add commercial entity info', async () => {
1503
+ await nockRegistrarAppSchemaName({
1504
+ schemaName: 'past-employment-position',
1505
+ credentialType: 'PastEmploymentPosition',
1506
+ responseJson: require('../combined/schemas/past-employment-position.schema.json'),
1507
+ });
1508
+
1509
+ disclosure = await persistDisclosure({
1510
+ tenant,
1511
+ commercialEntityName: 'name',
1512
+ commercialEntityLogo: 'logo',
1513
+ });
1514
+ exchange = await persistOfferExchange({ tenant, disclosure });
1515
+ exchangeId = exchange._id.toString();
1516
+
1517
+ const offer = await newVendorOffer({ tenant, exchange });
1518
+ const response = await fastify.injectJson({
1519
+ method: 'POST',
1520
+ url: exchangeOffersUrl({ tenant, exchange }, ''),
1521
+ payload: {
1522
+ ...offer,
1523
+ issuer: {
1524
+ name: 'name',
1525
+ image: 'logo',
1526
+ },
1527
+ },
1528
+ });
1529
+
1530
+ expect(response.statusCode).toEqual(200);
1531
+ expect(response.json).toEqual({
1532
+ ...offer,
1533
+ credentialSubject: {
1534
+ ...offer.credentialSubject,
1535
+ type: 'PastEmploymentPosition',
1536
+ },
1537
+ exchangeId: exchange._id.toString(),
1538
+ id: expect.stringMatching(OBJECT_ID_FORMAT),
1539
+ issuer: {
1540
+ id: tenant.did,
1541
+ name: 'name',
1542
+ image: 'logo',
1543
+ },
1544
+ updatedAt: expect.stringMatching(ISO_DATETIME_FORMAT),
1545
+ createdAt: expect.stringMatching(ISO_DATETIME_FORMAT),
1546
+ });
1547
+ expect(
1548
+ await exchangeCollection.findOne({
1549
+ _id: new ObjectId(exchangeId),
1550
+ })
1551
+ ).toEqual(
1552
+ mongoify({
1553
+ _id: new ObjectId(exchangeId),
1554
+ type: ExchangeTypes.ISSUING,
1555
+ tenantId: new ObjectId(tenant._id),
1556
+ disclosureId: disclosure._id.toString(),
1557
+ events: [{ state: ExchangeStates.NEW, timestamp: expect.any(Date) }],
1558
+ offerHashes: [],
1559
+ ...credentialTypesObject,
1560
+ createdAt: expect.any(Date),
1561
+ updatedAt: expect.any(Date),
1562
+ })
1563
+ );
1564
+
1565
+ const offerFromDb = await offerCollection.findOne({
1566
+ _id: new ObjectId(response.json.id),
1567
+ });
1568
+
1569
+ expect(offerFromDb).toEqual(
1570
+ mongoify({
1571
+ ...offer,
1572
+ credentialSubject: {
1573
+ ...offer.credentialSubject,
1574
+ type: 'PastEmploymentPosition',
1575
+ },
1576
+ _id: expect.any(ObjectId),
1577
+ tenantId: new ObjectId(tenant._id),
1578
+ contentHash: {
1579
+ type: 'VelocityContentHash2020',
1580
+ value: hashOffer({
1581
+ ...offer,
1582
+ credentialSubject: {
1583
+ ...offer.credentialSubject,
1584
+ type: 'PastEmploymentPosition',
1585
+ },
1586
+ }),
1587
+ },
1588
+ linkCode: expect.any(String),
1589
+ linkCodeCommitment: {
1590
+ type: 'VelocityCredentialLinkCodeCommitment2022',
1591
+ value: expect.any(String),
1592
+ },
1593
+ createdAt: expect.any(Date),
1594
+ updatedAt: expect.any(Date),
1595
+ exchangeId: exchange._id.toString(),
1596
+ issuer: {
1597
+ id: tenant.did,
1598
+ name: 'name',
1599
+ image: 'logo',
1600
+ },
1601
+ })
1602
+ );
1603
+ });
1604
+
1605
+ it('/offers should 400 offer has part of commercial entity info', async () => {
1606
+ await nockRegistrarAppSchemaName({
1607
+ schemaName: 'past-employment-position',
1608
+ credentialType: 'PastEmploymentPosition',
1609
+ responseJson: require('../combined/schemas/past-employment-position.schema.json'),
1610
+ });
1611
+
1612
+ disclosure = await persistDisclosure({
1613
+ tenant,
1614
+ commercialEntityName: 'name',
1615
+ commercialEntityLogo: 'logo',
1616
+ });
1617
+ exchange = await persistOfferExchange({ tenant, disclosure });
1618
+ exchangeId = exchange._id.toString();
1619
+
1620
+ const offer = await newVendorOffer({ tenant, exchange });
1621
+ const response = await fastify.injectJson({
1622
+ method: 'POST',
1623
+ url: exchangeOffersUrl({ tenant, exchange }, ''),
1624
+ payload: {
1625
+ ...offer,
1626
+ issuer: {
1627
+ image: 'logo',
1628
+ },
1629
+ },
1630
+ });
1631
+
1632
+ expect(response.statusCode).toEqual(400);
1633
+ expect(response.json).toEqual(
1634
+ errorResponseMatcher({
1635
+ error: 'Bad Request',
1636
+ errorCode: 'invalid_commercial_entity',
1637
+ message: 'Invalid commercial entity',
1638
+ statusCode: 400,
1639
+ })
1640
+ );
1641
+ });
1642
+
1643
+ it('/offers should return 400 when offer doesnt match schema', async () => {
1644
+ const getSchemaNock = nockRegistrarAppSchemaName({
1645
+ responseJson: require('../combined/schemas/past-employment-position-with-uri-id.schema'),
1646
+ });
1647
+
1648
+ const offer = await newVendorOffer({ tenant, exchange });
1649
+ const response = await fastify.injectJson({
1650
+ method: 'POST',
1651
+ url: exchangeOffersUrl({ tenant, exchange }, ''),
1652
+ payload: {
1653
+ _id: 'nonsense',
1654
+ ...omit(['credentialSubject.startMonthYear'], offer),
1655
+ },
1656
+ });
1657
+
1658
+ expect(response.statusCode).toEqual(400);
1659
+ expect(response.json).toEqual(
1660
+ errorResponseMatcher({
1661
+ error: 'Bad Request',
1662
+ errorCode: 'missing_error_code',
1663
+ message:
1664
+ "'$.credentialSubject' must have required property 'startMonthYear'",
1665
+ statusCode: 400,
1666
+ })
1667
+ );
1668
+ expect(
1669
+ await exchangeCollection.findOne({
1670
+ _id: new ObjectId(exchangeId),
1671
+ })
1672
+ ).toEqual(
1673
+ mongoify({
1674
+ _id: new ObjectId(exchangeId),
1675
+ type: ExchangeTypes.ISSUING,
1676
+ tenantId: new ObjectId(tenant._id),
1677
+ disclosureId: disclosure._id.toString(),
1678
+ events: [{ state: ExchangeStates.NEW, timestamp: expect.any(Date) }],
1679
+ offerHashes: [],
1680
+ ...credentialTypesObject,
1681
+ createdAt: expect.any(Date),
1682
+ updatedAt: expect.any(Date),
1683
+ })
1684
+ );
1685
+
1686
+ expect(getSchemaNock.isDone()).toEqual(true);
1687
+ });
1688
+
1689
+ it('/offers should 502 when offer schema is not resolvable', async () => {
1690
+ const getSchemaNock = nockRegistrarAppSchemaName({ statusCode: 404 });
1691
+
1692
+ const offer = await newVendorOffer({ tenant, exchange });
1693
+ const response = await fastify.injectJson({
1694
+ method: 'POST',
1695
+ url: exchangeOffersUrl({ tenant, exchange }, ''),
1696
+ payload: {
1697
+ _id: 'nonsense',
1698
+ ...omit(['credentialSubject.startMonthYear'], offer),
1699
+ },
1700
+ });
1701
+
1702
+ expect(response.statusCode).toEqual(502);
1703
+ expect(response.json).toEqual(
1704
+ errorResponseMatcher({
1705
+ statusCode: 502,
1706
+ error: 'Bad Gateway',
1707
+ errorCode: 'missing_error_code',
1708
+ message:
1709
+ 'failed to resolve http://mock.com/schemas/past-employment-position',
1710
+ })
1711
+ );
1712
+ expect(
1713
+ await exchangeCollection.findOne({
1714
+ _id: new ObjectId(exchangeId),
1715
+ })
1716
+ ).toEqual(
1717
+ mongoify({
1718
+ _id: new ObjectId(exchangeId),
1719
+ type: ExchangeTypes.ISSUING,
1720
+ tenantId: new ObjectId(tenant._id),
1721
+ disclosureId: disclosure._id.toString(),
1722
+ events: [{ state: ExchangeStates.NEW, timestamp: expect.any(Date) }],
1723
+ offerHashes: [],
1724
+ ...credentialTypesObject,
1725
+ createdAt: expect.any(Date),
1726
+ updatedAt: expect.any(Date),
1727
+ })
1728
+ );
1729
+
1730
+ expect(getSchemaNock.isDone()).toEqual(true);
1731
+ });
1732
+
1733
+ it('/offers should 502 when offer schema is missing an id', async () => {
1734
+ const getSchemaNock = nockRegistrarAppSchemaName({
1735
+ responseJson: omit(
1736
+ ['$id'],
1737
+ require('../combined/schemas/past-employment-position-with-uri-id.schema')
1738
+ ),
1739
+ });
1740
+
1741
+ const offer = await newVendorOffer({ tenant, exchange });
1742
+ const response = await fastify.injectJson({
1743
+ method: 'POST',
1744
+ url: exchangeOffersUrl({ tenant, exchange }, ''),
1745
+ payload: {
1746
+ _id: 'nonsense',
1747
+ ...omit(['credentialSubject.startMonthYear'], offer),
1748
+ },
1749
+ });
1750
+
1751
+ expect(response.statusCode).toEqual(502);
1752
+ expect(response.json).toEqual(
1753
+ errorResponseMatcher({
1754
+ statusCode: 502,
1755
+ error: 'Bad Gateway',
1756
+ errorCode: 'missing_error_code',
1757
+ message:
1758
+ 'http://mock.com/schemas/past-employment-position $id field missing',
1759
+ })
1760
+ );
1761
+ expect(
1762
+ await exchangeCollection.findOne({
1763
+ _id: new ObjectId(exchangeId),
1764
+ })
1765
+ ).toEqual(
1766
+ mongoify({
1767
+ _id: new ObjectId(exchangeId),
1768
+ type: ExchangeTypes.ISSUING,
1769
+ tenantId: new ObjectId(tenant._id),
1770
+ disclosureId: disclosure._id.toString(),
1771
+ events: [{ state: ExchangeStates.NEW, timestamp: expect.any(Date) }],
1772
+ offerHashes: [],
1773
+ ...credentialTypesObject,
1774
+ createdAt: expect.any(Date),
1775
+ updatedAt: expect.any(Date),
1776
+ })
1777
+ );
1778
+
1779
+ expect(getSchemaNock.isDone()).toEqual(true);
1780
+ });
1781
+
1782
+ it('/offers should return 400 when offer credentialType is not recognized', async () => {
1783
+ const getCredentialTypesNock = nock('http://oracle.localhost.test')
1784
+ .get('/api/v0.6/credential-types')
1785
+ .query({ credentialType: 'PastEmploymentPosition' })
1786
+ .reply(200, []);
1787
+
1788
+ const offer = await newVendorOffer({ tenant, exchange });
1789
+ const response = await fastify.injectJson({
1790
+ method: 'POST',
1791
+ url: exchangeOffersUrl({ tenant, exchange }, ''),
1792
+ payload: {
1793
+ _id: 'nonsense',
1794
+ ...omit(['credentialSubject.startMonthYear'], offer),
1795
+ },
1796
+ });
1797
+
1798
+ expect(response.statusCode).toEqual(400);
1799
+ expect(response.json).toEqual(
1800
+ errorResponseMatcher({
1801
+ statusCode: 400,
1802
+ error: 'Bad Request',
1803
+ errorCode: 'missing_error_code',
1804
+ message: 'PastEmploymentPosition is not a recognized credential type',
1805
+ })
1806
+ );
1807
+ expect(
1808
+ await exchangeCollection.findOne({
1809
+ _id: new ObjectId(exchangeId),
1810
+ })
1811
+ ).toEqual(
1812
+ mongoify({
1813
+ _id: new ObjectId(exchangeId),
1814
+ type: ExchangeTypes.ISSUING,
1815
+ tenantId: new ObjectId(tenant._id),
1816
+ disclosureId: disclosure._id.toString(),
1817
+ events: [{ state: ExchangeStates.NEW, timestamp: expect.any(Date) }],
1818
+ offerHashes: [],
1819
+ ...credentialTypesObject,
1820
+ createdAt: expect.any(Date),
1821
+ updatedAt: expect.any(Date),
1822
+ })
1823
+ );
1824
+
1825
+ expect(getCredentialTypesNock.isDone()).toEqual(true);
1826
+ });
1827
+
1828
+ it('/offers should return 502 if registrar has non 404 error ', async () => {
1829
+ const getSchemaNock = nockRegistrarAppSchemaName({ statusCode: 400 });
1830
+
1831
+ const offer = await newVendorOffer({ tenant, exchange });
1832
+ const response = await fastify.injectJson({
1833
+ method: 'POST',
1834
+ url: exchangeOffersUrl({ tenant, exchange }, ''),
1835
+ payload: {
1836
+ _id: 'nonsense',
1837
+ ...omit(['credentialSubject.startMonthYear'], offer),
1838
+ },
1839
+ });
1840
+
1841
+ expect(response.statusCode).toEqual(502);
1842
+ expect(
1843
+ await exchangeCollection.findOne({
1844
+ _id: new ObjectId(exchangeId),
1845
+ })
1846
+ ).toEqual(
1847
+ mongoify({
1848
+ _id: new ObjectId(exchangeId),
1849
+ type: ExchangeTypes.ISSUING,
1850
+ tenantId: new ObjectId(tenant._id),
1851
+ disclosureId: disclosure._id.toString(),
1852
+ events: [{ state: ExchangeStates.NEW, timestamp: expect.any(Date) }],
1853
+ offerHashes: [],
1854
+ ...credentialTypesObject,
1855
+ createdAt: expect.any(Date),
1856
+ updatedAt: expect.any(Date),
1857
+ })
1858
+ );
1859
+
1860
+ expect(getSchemaNock.isDone()).toEqual(true);
1861
+ });
1862
+
1863
+ it('/offers should return 422 a duplicate offer is sent', async () => {
1864
+ await nockRegistrarAppSchemaName({
1865
+ schemaName: 'past-employment-position',
1866
+ credentialType: 'PastEmploymentPosition',
1867
+ responseJson: require('../combined/schemas/past-employment-position.schema.json'),
1868
+ });
1869
+
1870
+ const offer = flow(
1871
+ set('credentialSubject.vendorUserId', user.vendorUserId)
1872
+ )(await newVendorOffer());
1873
+ const offerHash = hashOffer({
1874
+ ...offer,
1875
+ credentialSubject: {
1876
+ ...offer.credentialSubject,
1877
+ type: 'PastEmploymentPosition',
1878
+ },
1879
+ });
1880
+
1881
+ await exchangeCollection.updateOne(
1882
+ { _id: new ObjectId(exchangeId) },
1883
+ { $set: { offerHashes: [offerHash] } }
1884
+ );
1885
+
1886
+ const response = await fastify.injectJson({
1887
+ method: 'POST',
1888
+ url: exchangeOffersUrl({ tenant, exchange }, ''),
1889
+ payload: offer,
1890
+ });
1891
+
1892
+ expect(response.statusCode).toEqual(422);
1893
+ });
1894
+
1895
+ it('/offers should return 400 when credentialSubject.vendorUserId is missing', async () => {
1896
+ const offer = omit(
1897
+ ['credentialSubject.vendorUserId'],
1898
+ await newVendorOffer()
1899
+ );
1900
+ const response = await fastify.injectJson({
1901
+ method: 'POST',
1902
+ url: exchangeOffersUrl({ tenant, exchange }, ''),
1903
+ payload: offer,
1904
+ });
1905
+
1906
+ expect(response.statusCode).toEqual(400);
1907
+ expect(
1908
+ await exchangeCollection.findOne({
1909
+ _id: new ObjectId(exchangeId),
1910
+ })
1911
+ ).toEqual(
1912
+ mongoify({
1913
+ _id: new ObjectId(exchangeId),
1914
+ type: ExchangeTypes.ISSUING,
1915
+ disclosureId: disclosure._id.toString(),
1916
+ events: [
1917
+ { state: ExchangeStates.NEW, timestamp: expect.any(Date) },
1918
+ {
1919
+ state: ExchangeStates.OFFER_VALIDATION_ERROR,
1920
+ timestamp: expect.any(Date),
1921
+ },
1922
+ ],
1923
+ offerHashes: [],
1924
+ ...credentialTypesObject,
1925
+ tenantId: new ObjectId(tenant._id),
1926
+ createdAt: expect.any(Date),
1927
+ updatedAt: expect.any(Date),
1928
+ })
1929
+ );
1930
+ });
1931
+
1932
+ it('/offers should return 400 when expirationDate and validUntil are set', async () => {
1933
+ const offer = await newOffer();
1934
+ const extendedOffer = flow(
1935
+ omit(['isDuplicate', 'contentHash', 'exchangeId']),
1936
+ set('credentialSubject.vendorUserId', user.vendorUserId),
1937
+ set('expirationDate', new Date()),
1938
+ set('validUntil', new Date())
1939
+ )(offer);
1940
+ const response = await fastify.injectJson({
1941
+ method: 'POST',
1942
+ url: exchangeOffersUrl({ tenant, exchange }, ''),
1943
+ payload: extendedOffer,
1944
+ });
1945
+
1946
+ expect(response.statusCode).toEqual(400);
1947
+ expect(
1948
+ await exchangeCollection.findOne({
1949
+ _id: new ObjectId(exchangeId),
1950
+ })
1951
+ ).toEqual(
1952
+ mongoify({
1953
+ _id: new ObjectId(exchangeId),
1954
+ type: ExchangeTypes.ISSUING,
1955
+ tenantId: new ObjectId(tenant._id),
1956
+ disclosureId: disclosure._id.toString(),
1957
+ events: [{ state: ExchangeStates.NEW, timestamp: expect.any(Date) }],
1958
+ offerHashes: [],
1959
+ ...credentialTypesObject,
1960
+ createdAt: expect.any(Date),
1961
+ updatedAt: expect.any(Date),
1962
+ })
1963
+ );
1964
+ });
1965
+
1966
+ it('/offers should throw a 404 error when exchangeId not found', async () => {
1967
+ const offer = await newOffer();
1968
+ const response = await fastify.injectJson({
1969
+ method: 'POST',
1970
+ url: exchangeOffersUrl(
1971
+ { tenant, exchange: { id: 'some_exchange_id' } },
1972
+ ''
1973
+ ),
1974
+ payload: {
1975
+ ...set('credentialSubject.vendorUserId', user.vendorUserId, offer),
1976
+ },
1977
+ });
1978
+
1979
+ expect(response.statusCode).toEqual(404);
1980
+ expect(
1981
+ await exchangeCollection.findOne({
1982
+ _id: new ObjectId(exchangeId),
1983
+ })
1984
+ ).toEqual(
1985
+ mongoify({
1986
+ _id: new ObjectId(exchangeId),
1987
+ type: ExchangeTypes.ISSUING,
1988
+ tenantId: new ObjectId(tenant._id),
1989
+ disclosureId: disclosure._id.toString(),
1990
+ events: [{ state: ExchangeStates.NEW, timestamp: expect.any(Date) }],
1991
+ offerHashes: [],
1992
+ ...credentialTypesObject,
1993
+ createdAt: expect.any(Date),
1994
+ updatedAt: expect.any(Date),
1995
+ })
1996
+ );
1997
+ });
1998
+
1999
+ it('/offers should throw a 404 error wrong tenantId in url', async () => {
2000
+ const offer = await newOffer();
2001
+ const response = await fastify.injectJson({
2002
+ method: 'POST',
2003
+ url: exchangeOffersUrl(
2004
+ { tenant: { _id: new ObjectId() }, exchange },
2005
+ ''
2006
+ ),
2007
+ payload: set(
2008
+ 'credentialSubject.vendorUserId',
2009
+ user.vendorUserId,
2010
+ offer
2011
+ ),
2012
+ });
2013
+
2014
+ expect(response.statusCode).toEqual(404);
2015
+ expect(
2016
+ await exchangeCollection.findOne({
2017
+ _id: new ObjectId(exchangeId),
2018
+ })
2019
+ ).toEqual(
2020
+ mongoify({
2021
+ _id: new ObjectId(exchangeId),
2022
+ type: ExchangeTypes.ISSUING,
2023
+ tenantId: new ObjectId(tenant._id),
2024
+ disclosureId: disclosure._id.toString(),
2025
+ events: [{ state: ExchangeStates.NEW, timestamp: expect.any(Date) }],
2026
+ offerHashes: [],
2027
+ ...credentialTypesObject,
2028
+ createdAt: expect.any(Date),
2029
+ updatedAt: expect.any(Date),
2030
+ })
2031
+ );
2032
+ });
2033
+
2034
+ it('/offers throw 400 if commercial entity does not match', async () => {
2035
+ fastify.overrides.reqConfig = (config) => ({
2036
+ ...config,
2037
+ enableOfferValidation: false,
2038
+ });
2039
+
2040
+ disclosure = await persistDisclosure({
2041
+ tenant,
2042
+ commercialEntityName: 'name',
2043
+ commercialEntityLogo: 'logo',
2044
+ });
2045
+ exchange = await persistOfferExchange({ tenant, disclosure });
2046
+ exchangeId = exchange._id.toString();
2047
+
2048
+ const offer = await newVendorOffer({ tenant, exchange });
2049
+ const response = await fastify.injectJson({
2050
+ method: 'POST',
2051
+ url: exchangeOffersUrl({ tenant, exchange }, ''),
2052
+ payload: {
2053
+ ...offer,
2054
+ issuer: {
2055
+ name: 'wrong name',
2056
+ image: 'http://wrong.com',
2057
+ },
2058
+ },
2059
+ });
2060
+
2061
+ expect(response.statusCode).toEqual(400);
2062
+ expect(response.json).toEqual(
2063
+ errorResponseMatcher({
2064
+ error: 'Bad Request',
2065
+ errorCode: 'invalid_commercial_entity',
2066
+ message: 'Invalid commercial entity',
2067
+ statusCode: 400,
2068
+ })
2069
+ );
2070
+ });
2071
+ });
2072
+
2073
+ describe('/offers/complete', () => {
2074
+ it('should throw a 404 error when exchangeId is badly formatted', async () => {
2075
+ const response = await fastify.injectJson({
2076
+ method: 'POST',
2077
+ url: exchangeOffersUrl(
2078
+ { tenant, exchange: { id: 'EXCHANGE-ID' } },
2079
+ 'complete'
2080
+ ),
2081
+ payload: {
2082
+ purpose: ['push'],
2083
+ },
2084
+ });
2085
+
2086
+ expect(response.statusCode).toEqual(404);
2087
+ });
2088
+
2089
+ it('should throw a 404 error when exchangeId not found', async () => {
2090
+ const response = await fastify.injectJson({
2091
+ method: 'POST',
2092
+ url: exchangeOffersUrl(
2093
+ { tenant, exchange: { id: new ObjectId() } },
2094
+ 'complete'
2095
+ ),
2096
+ payload: {
2097
+ purpose: ['push'],
2098
+ },
2099
+ });
2100
+
2101
+ expect(response.statusCode).toEqual(404);
2102
+ });
2103
+
2104
+ it('/offers/complete should return 404 when org not found', async () => {
2105
+ nock.cleanAll();
2106
+ nockRegistrarGetOrganizationDidDoc(tenant.did, {});
2107
+ nock(testPushEndpointURL.origin)
2108
+ .post(testPushEndpointURL.pathname)
2109
+ .reply(204, null);
2110
+ const response = await fastify.injectJson({
2111
+ method: 'POST',
2112
+ url: exchangeOffersUrl({ tenant, exchange: pushExchange }, 'complete'),
2113
+ payload: {},
2114
+ });
2115
+
2116
+ expect(response.statusCode).toEqual(404);
2117
+ });
2118
+
2119
+ it('/offers/complete 200 with offers and push notification', async () => {
2120
+ nockRegistrarGetOrganizationDidDoc(
2121
+ intermediateIssuer.id,
2122
+ intermediateIssuer
2123
+ );
2124
+ let parsedBody;
2125
+ const nockedPushEndpoint = nock(testPushEndpointURL.origin)
2126
+ .post(testPushEndpointURL.pathname, (body) => {
2127
+ parsedBody = body;
2128
+ return body;
2129
+ })
2130
+ .reply(204, null);
2131
+
2132
+ const response = await fastify.injectJson({
2133
+ method: 'POST',
2134
+ url: exchangeOffersUrl({ tenant, exchange: pushExchange }, 'complete'),
2135
+ payload: {},
2136
+ });
2137
+ expect(nockedPushEndpoint.isDone()).toEqual(true);
2138
+ expect(parsedBody).toEqual({
2139
+ data: {
2140
+ exchangeId: pushExchange._id,
2141
+ issuer: tenant.did,
2142
+ notificationType: 'NewOffersReady',
2143
+ serviceEndpoint: testPushEndpointURL.href,
2144
+ count: 1,
2145
+ ...credentialTypesObject,
2146
+ },
2147
+ id: expect.stringMatching(NANO_ID_FORMAT),
2148
+ pushToken: 'some-token',
2149
+ });
2150
+
2151
+ expect(response.statusCode).toEqual(200);
2152
+
2153
+ expect(response.json).toEqual({
2154
+ offerIds: expect.arrayContaining(map('_id', pushExchangeOffers)),
2155
+ pushSentAt: expect.stringMatching(ISO_DATETIME_FORMAT),
2156
+ });
2157
+ expect(
2158
+ await exchangeCollection.findOne({
2159
+ _id: new ObjectId(pushExchange._id),
2160
+ })
2161
+ ).toEqual(
2162
+ mongoify({
2163
+ _id: new ObjectId(pushExchange._id),
2164
+ type: ExchangeTypes.ISSUING,
2165
+ tenantId: new ObjectId(tenant._id),
2166
+ disclosureId: disclosure._id.toString(),
2167
+ pushDelegate: {
2168
+ pushUrl: testPushEndpointURL.href,
2169
+ pushToken: 'some-token',
2170
+ },
2171
+ pushSentAt: expect.any(Date),
2172
+ events: [
2173
+ { state: ExchangeStates.NEW, timestamp: expect.any(Date) },
2174
+ {
2175
+ state: ExchangeStates.OFFERS_RECEIVED,
2176
+ timestamp: expect.any(Date),
2177
+ },
2178
+ ],
2179
+ offerHashes: [],
2180
+ ...credentialTypesObject,
2181
+ createdAt: expect.any(Date),
2182
+ updatedAt: expect.any(Date),
2183
+ })
2184
+ );
2185
+ });
2186
+
2187
+ it('/offers/complete 200 with no offers and push notification', async () => {
2188
+ nockRegistrarGetOrganizationDidDoc(
2189
+ intermediateIssuer.id,
2190
+ intermediateIssuer
2191
+ );
2192
+ let parsedBody;
2193
+
2194
+ const nockedPushEndpoint = nock(testPushEndpointURL.origin)
2195
+ .post(testPushEndpointURL.pathname, (body) => {
2196
+ parsedBody = body;
2197
+ return body;
2198
+ })
2199
+ .reply(204, null);
2200
+ const receivedEmptyExchange = await persistOfferExchange({
2201
+ tenant,
2202
+ disclosure,
2203
+ pushDelegate: {
2204
+ pushToken: 'some-token',
2205
+ pushUrl: testPushEndpointURL.href,
2206
+ },
2207
+ });
2208
+
2209
+ const response = await fastify.injectJson({
2210
+ method: 'POST',
2211
+ url: exchangeOffersUrl(
2212
+ { tenant, exchange: receivedEmptyExchange },
2213
+ 'complete'
2214
+ ),
2215
+ payload: {},
2216
+ });
2217
+
2218
+ expect(response.statusCode).toEqual(200);
2219
+ expect(response.json).toEqual({
2220
+ pushSentAt: expect.stringMatching(ISO_DATETIME_FORMAT),
2221
+ });
2222
+ expect(nockedPushEndpoint.isDone()).toEqual(true);
2223
+ expect(parsedBody).toEqual({
2224
+ data: {
2225
+ exchangeId: receivedEmptyExchange._id,
2226
+ issuer: tenant.did,
2227
+ notificationType: NotificationTypes.NO_OFFERS_FOUND,
2228
+ serviceEndpoint: testPushEndpointURL.href,
2229
+ },
2230
+ id: expect.stringMatching(NANO_ID_FORMAT),
2231
+ pushToken: 'some-token',
2232
+ });
2233
+ expect(
2234
+ await exchangeCollection.findOne({
2235
+ _id: new ObjectId(receivedEmptyExchange._id),
2236
+ })
2237
+ ).toEqual(
2238
+ mongoify({
2239
+ _id: new ObjectId(receivedEmptyExchange._id),
2240
+ type: ExchangeTypes.ISSUING,
2241
+ tenantId: new ObjectId(tenant._id),
2242
+ disclosureId: disclosure._id.toString(),
2243
+ pushDelegate: {
2244
+ pushUrl: testPushEndpointURL.href,
2245
+ pushToken: 'some-token',
2246
+ },
2247
+ pushSentAt: expect.any(Date),
2248
+ events: [
2249
+ { state: ExchangeStates.NEW, timestamp: expect.any(Date) },
2250
+ {
2251
+ state: ExchangeStates.NO_OFFERS_RECEIVED,
2252
+ timestamp: expect.any(Date),
2253
+ },
2254
+ {
2255
+ state: ExchangeStates.COMPLETE,
2256
+ timestamp: expect.any(Date),
2257
+ },
2258
+ ],
2259
+ offerHashes: [],
2260
+ ...credentialTypesObject,
2261
+ createdAt: expect.any(Date),
2262
+ updatedAt: expect.any(Date),
2263
+ })
2264
+ );
2265
+ });
2266
+
2267
+ it('/offers/complete should not fail if push failed', async () => {
2268
+ nockRegistrarGetOrganizationDidDoc(
2269
+ intermediateIssuer.id,
2270
+ intermediateIssuer
2271
+ );
2272
+ let parsedBody;
2273
+
2274
+ const nockedPushEndpoint = nock(testPushEndpointURL.origin)
2275
+ .post(testPushEndpointURL.pathname, (body) => {
2276
+ parsedBody = body;
2277
+ return body;
2278
+ })
2279
+ .replyWithError('some error');
2280
+ const receivedEmptyExchange = await persistOfferExchange({
2281
+ tenant,
2282
+ disclosure,
2283
+ pushDelegate: {
2284
+ pushToken: 'some-token',
2285
+ pushUrl: testPushEndpointURL.href,
2286
+ },
2287
+ });
2288
+
2289
+ const response = await fastify.injectJson({
2290
+ method: 'POST',
2291
+ url: exchangeOffersUrl(
2292
+ { tenant, exchange: receivedEmptyExchange },
2293
+ 'complete'
2294
+ ),
2295
+ payload: {},
2296
+ });
2297
+
2298
+ expect(response.statusCode).toEqual(200);
2299
+ expect(response.json).toEqual({
2300
+ pushSentAt: expect.stringMatching(ISO_DATETIME_FORMAT),
2301
+ });
2302
+ expect(nockedPushEndpoint.isDone()).toEqual(true);
2303
+ expect(parsedBody).toEqual({
2304
+ data: {
2305
+ exchangeId: receivedEmptyExchange._id,
2306
+ issuer: tenant.did,
2307
+ notificationType: NotificationTypes.NO_OFFERS_FOUND,
2308
+ serviceEndpoint: testPushEndpointURL.href,
2309
+ },
2310
+ id: expect.stringMatching(NANO_ID_FORMAT),
2311
+ pushToken: 'some-token',
2312
+ });
2313
+ expect(
2314
+ await exchangeCollection.findOne({
2315
+ _id: new ObjectId(receivedEmptyExchange._id),
2316
+ })
2317
+ ).toEqual(
2318
+ mongoify({
2319
+ _id: new ObjectId(receivedEmptyExchange._id),
2320
+ type: ExchangeTypes.ISSUING,
2321
+ tenantId: new ObjectId(tenant._id),
2322
+ pushDelegate: {
2323
+ pushUrl: testPushEndpointURL.href,
2324
+ pushToken: 'some-token',
2325
+ },
2326
+ pushSentAt: expect.any(Date),
2327
+ disclosureId: disclosure._id.toString(),
2328
+ events: [
2329
+ { state: ExchangeStates.NEW, timestamp: expect.any(Date) },
2330
+ {
2331
+ state: ExchangeStates.NO_OFFERS_RECEIVED,
2332
+ timestamp: expect.any(Date),
2333
+ },
2334
+ {
2335
+ state: ExchangeStates.COMPLETE,
2336
+ timestamp: expect.any(Date),
2337
+ },
2338
+ ],
2339
+ offerHashes: [],
2340
+ ...credentialTypesObject,
2341
+ createdAt: expect.any(Date),
2342
+ updatedAt: expect.any(Date),
2343
+ })
2344
+ );
2345
+ });
2346
+
2347
+ it('/offers/complete should not fail if push failed', async () => {
2348
+ nockRegistrarGetOrganizationDidDoc(
2349
+ intermediateIssuer.id,
2350
+ intermediateIssuer
2351
+ );
2352
+ let parsedBody;
2353
+
2354
+ const nockedPushEndpoint = nock(testPushEndpointURL.origin)
2355
+ .post(testPushEndpointURL.pathname, (body) => {
2356
+ parsedBody = body;
2357
+ return body;
2358
+ })
2359
+ .replyWithError('some error');
2360
+ const receivedEmptyExchange = await persistOfferExchange({
2361
+ tenant,
2362
+ disclosure,
2363
+ pushDelegate: {
2364
+ pushToken: 'some-token',
2365
+ pushUrl: testPushEndpointURL.href,
2366
+ },
2367
+ });
2368
+
2369
+ const response = await fastify.injectJson({
2370
+ method: 'POST',
2371
+ url: exchangeOffersUrl(
2372
+ { tenant, exchange: receivedEmptyExchange },
2373
+ 'complete'
2374
+ ),
2375
+ payload: {},
2376
+ });
2377
+
2378
+ expect(response.statusCode).toEqual(200);
2379
+ expect(response.json).toEqual({
2380
+ pushSentAt: expect.stringMatching(ISO_DATETIME_FORMAT),
2381
+ });
2382
+ expect(nockedPushEndpoint.isDone()).toEqual(true);
2383
+ expect(parsedBody).toEqual({
2384
+ data: {
2385
+ exchangeId: receivedEmptyExchange._id,
2386
+ issuer: tenant.did,
2387
+ notificationType: NotificationTypes.NO_OFFERS_FOUND,
2388
+ serviceEndpoint: testPushEndpointURL.href,
2389
+ },
2390
+ id: expect.stringMatching(NANO_ID_FORMAT),
2391
+ pushToken: 'some-token',
2392
+ });
2393
+ expect(
2394
+ await exchangeCollection.findOne({
2395
+ _id: new ObjectId(receivedEmptyExchange._id),
2396
+ })
2397
+ ).toEqual(
2398
+ mongoify({
2399
+ _id: new ObjectId(receivedEmptyExchange._id),
2400
+ type: ExchangeTypes.ISSUING,
2401
+ tenantId: new ObjectId(tenant._id),
2402
+ disclosureId: disclosure._id.toString(),
2403
+ pushDelegate: {
2404
+ pushUrl: testPushEndpointURL.href,
2405
+ pushToken: 'some-token',
2406
+ },
2407
+ pushSentAt: expect.any(Date),
2408
+ events: [
2409
+ { state: ExchangeStates.NEW, timestamp: expect.any(Date) },
2410
+ {
2411
+ state: ExchangeStates.NO_OFFERS_RECEIVED,
2412
+ timestamp: expect.any(Date),
2413
+ },
2414
+ {
2415
+ state: ExchangeStates.COMPLETE,
2416
+ timestamp: expect.any(Date),
2417
+ },
2418
+ ],
2419
+ offerHashes: [],
2420
+ ...credentialTypesObject,
2421
+ createdAt: expect.any(Date),
2422
+ updatedAt: expect.any(Date),
2423
+ })
2424
+ );
2425
+ });
2426
+
2427
+ it('/offers/complete 200 with offers and no push notification', async () => {
2428
+ let parsedBody;
2429
+ const nockedPushEndpoint = nock(testPushEndpointURL.origin)
2430
+ .post(testPushEndpointURL.pathname, (body) => {
2431
+ parsedBody = body;
2432
+ return body;
2433
+ })
2434
+ .reply(204, null);
2435
+
2436
+ const response = await fastify.injectJson({
2437
+ method: 'POST',
2438
+ url: exchangeOffersUrl({ tenant, exchange }, 'complete'),
2439
+ payload: {},
2440
+ });
2441
+
2442
+ expect(response.statusCode).toEqual(200);
2443
+ expect(response.json).toEqual({
2444
+ offerIds: expect.arrayContaining(map('_id', exchangeOffers)),
2445
+ });
2446
+ expect(nockedPushEndpoint.isDone()).toEqual(false);
2447
+ expect(parsedBody).toBeUndefined();
2448
+ expect(
2449
+ await exchangeCollection.findOne({
2450
+ _id: new ObjectId(exchangeId),
2451
+ })
2452
+ ).toEqual(
2453
+ mongoify({
2454
+ _id: new ObjectId(exchangeId),
2455
+ type: ExchangeTypes.ISSUING,
2456
+ tenantId: new ObjectId(tenant._id),
2457
+ disclosureId: disclosure._id.toString(),
2458
+ events: [
2459
+ { state: ExchangeStates.NEW, timestamp: expect.any(Date) },
2460
+ {
2461
+ state: ExchangeStates.OFFERS_RECEIVED,
2462
+ timestamp: expect.any(Date),
2463
+ },
2464
+ ],
2465
+ offerHashes: [],
2466
+ ...credentialTypesObject,
2467
+ createdAt: expect.any(Date),
2468
+ updatedAt: expect.any(Date),
2469
+ })
2470
+ );
2471
+ });
2472
+
2473
+ it('/offers/complete 200 with no offers and no push notification due to missing pushDelegate.pushUrl', async () => {
2474
+ nockRegistrarGetOrganizationDidDoc(
2475
+ intermediateIssuer.id,
2476
+ intermediateIssuer
2477
+ );
2478
+ let parsedBody;
2479
+
2480
+ const nockedPushEndpoint = nock(testPushEndpointURL.origin)
2481
+ .post(testPushEndpointURL.pathname, (body) => {
2482
+ parsedBody = body;
2483
+ return body;
2484
+ })
2485
+ .reply(204, null);
2486
+ const receivedEmptyExchange = await persistOfferExchange({
2487
+ tenant,
2488
+ disclosure,
2489
+ pushDelegate: {
2490
+ pushToken: 'some-token',
2491
+ },
2492
+ });
2493
+
2494
+ const response = await fastify.injectJson({
2495
+ method: 'POST',
2496
+ url: exchangeOffersUrl(
2497
+ { tenant, exchange: receivedEmptyExchange },
2498
+ 'complete'
2499
+ ),
2500
+ payload: {},
2501
+ });
2502
+
2503
+ expect(response.statusCode).toEqual(200);
2504
+ expect(response.json).toEqual({});
2505
+ expect(nockedPushEndpoint.isDone()).toEqual(false);
2506
+ expect(parsedBody).toBeUndefined();
2507
+ expect(
2508
+ await exchangeCollection.findOne({
2509
+ _id: new ObjectId(receivedEmptyExchange._id),
2510
+ })
2511
+ ).toEqual(
2512
+ mongoify({
2513
+ _id: new ObjectId(receivedEmptyExchange._id),
2514
+ type: ExchangeTypes.ISSUING,
2515
+ tenantId: new ObjectId(tenant._id),
2516
+ disclosureId: disclosure._id.toString(),
2517
+ pushDelegate: {
2518
+ pushToken: 'some-token',
2519
+ },
2520
+ events: [
2521
+ { state: ExchangeStates.NEW, timestamp: expect.any(Date) },
2522
+ {
2523
+ state: ExchangeStates.NO_OFFERS_RECEIVED,
2524
+ timestamp: expect.any(Date),
2525
+ },
2526
+ {
2527
+ state: ExchangeStates.COMPLETE,
2528
+ timestamp: expect.any(Date),
2529
+ },
2530
+ ],
2531
+ offerHashes: [],
2532
+ ...credentialTypesObject,
2533
+ createdAt: expect.any(Date),
2534
+ updatedAt: expect.any(Date),
2535
+ })
2536
+ );
2537
+ });
2538
+
2539
+ it('/offers/complete 200 with no offers and no push notification due to missing pushDelegate', async () => {
2540
+ nockRegistrarGetOrganizationDidDoc(
2541
+ intermediateIssuer.id,
2542
+ intermediateIssuer
2543
+ );
2544
+ let parsedBody;
2545
+
2546
+ const nockedPushEndpoint = nock(testPushEndpointURL.origin)
2547
+ .post(testPushEndpointURL.pathname, (body) => {
2548
+ parsedBody = body;
2549
+ return body;
2550
+ })
2551
+ .reply(204, null);
2552
+ const receivedEmptyExchange = await persistOfferExchange({
2553
+ tenant,
2554
+ disclosure,
2555
+ });
2556
+
2557
+ const response = await fastify.injectJson({
2558
+ method: 'POST',
2559
+ url: exchangeOffersUrl(
2560
+ { tenant, exchange: receivedEmptyExchange },
2561
+ 'complete'
2562
+ ),
2563
+ payload: {},
2564
+ });
2565
+
2566
+ expect(response.statusCode).toEqual(200);
2567
+ expect(response.json).toEqual({});
2568
+ expect(nockedPushEndpoint.isDone()).toEqual(false);
2569
+ expect(parsedBody).toBeUndefined();
2570
+ expect(
2571
+ await exchangeCollection.findOne({
2572
+ _id: new ObjectId(receivedEmptyExchange._id),
2573
+ })
2574
+ ).toEqual(
2575
+ mongoify({
2576
+ _id: new ObjectId(receivedEmptyExchange._id),
2577
+ type: ExchangeTypes.ISSUING,
2578
+ tenantId: new ObjectId(tenant._id),
2579
+ disclosureId: disclosure._id.toString(),
2580
+ events: [
2581
+ { state: ExchangeStates.NEW, timestamp: expect.any(Date) },
2582
+ {
2583
+ state: ExchangeStates.NO_OFFERS_RECEIVED,
2584
+ timestamp: expect.any(Date),
2585
+ },
2586
+ {
2587
+ state: ExchangeStates.COMPLETE,
2588
+ timestamp: expect.any(Date),
2589
+ },
2590
+ ],
2591
+ offerHashes: [],
2592
+ ...credentialTypesObject,
2593
+ createdAt: expect.any(Date),
2594
+ updatedAt: expect.any(Date),
2595
+ })
2596
+ );
2597
+ });
2598
+ });
2599
+
2600
+ describe('Retrieving URIs', () => {
2601
+ let receivedExchange;
2602
+ beforeEach(async () => {
2603
+ receivedExchange = await persistOfferExchange({
2604
+ tenant,
2605
+ events: [
2606
+ { state: ExchangeStates.NEW, timestamp: new Date() },
2607
+ {
2608
+ state: ExchangeStates.OFFERS_RECEIVED,
2609
+ timestamp: new Date(),
2610
+ },
2611
+ ],
2612
+ });
2613
+ await Promise.all([persistOffer({ tenant, exchange: receivedExchange })]);
2614
+ });
2615
+
2616
+ describe('Retrieving URIs as strings', () => {
2617
+ it('should 404 for an unknown exchange', async () => {
2618
+ const response = await fastify.inject({
2619
+ method: 'GET',
2620
+ url: exchangesUrl(
2621
+ { tenant, exchange: { _id: 'EXCHANGE_ID' } },
2622
+ 'qrcode.uri'
2623
+ ),
2624
+ headers: {
2625
+ authorization: `Bearer ${testAuthToken}`,
2626
+ },
2627
+ });
2628
+
2629
+ expect(response.statusCode).toEqual(404);
2630
+ });
2631
+ it('should 400 if an exchange is incomplete', async () => {
2632
+ const response = await fastify.inject({
2633
+ method: 'GET',
2634
+ url: exchangesUrl({ tenant, exchange }, 'qrcode.uri'),
2635
+ headers: {
2636
+ authorization: `Bearer ${testAuthToken}`,
2637
+ },
2638
+ });
2639
+
2640
+ expect(response.statusCode).toEqual(400);
2641
+ });
2642
+ it('should return a URI representing an exchange with a single offer', async () => {
2643
+ const response = await fastify.inject({
2644
+ method: 'GET',
2645
+ url: exchangesUrl(
2646
+ { tenant, exchange: receivedExchange },
2647
+ 'qrcode.uri'
2648
+ ),
2649
+ headers: {
2650
+ authorization: `Bearer ${testAuthToken}`,
2651
+ },
2652
+ });
2653
+
2654
+ expect(response.statusCode).toEqual(200);
2655
+ expect(response.body).toEqual(
2656
+ `velocity-test://issue?request_uri=http%3A%2F%2Flocalhost.test%2Fapi%2Fholder%2Fv0.6%2Forg%2F${encodeURIComponent(
2657
+ tenant.did
2658
+ )}%2Fissue%2Fget-credential-manifest%3Fexchange_id%3D${
2659
+ receivedExchange._id
2660
+ }%26credential_types%3DPastEmploymentPosition&issuerDid=${tenant.did.replace(
2661
+ /:/g,
2662
+ '%3A'
2663
+ )}`
2664
+ );
2665
+ });
2666
+
2667
+ it('should return a URI representing an exchange with a single offer & vendorOriginContext', async () => {
2668
+ const response = await fastify.inject({
2669
+ method: 'GET',
2670
+ url: exchangesUrl(
2671
+ { tenant, exchange: receivedExchange },
2672
+ 'qrcode.uri?vendorOriginContext=123'
2673
+ ),
2674
+ headers: {
2675
+ authorization: `Bearer ${testAuthToken}`,
2676
+ },
2677
+ });
2678
+
2679
+ expect(response.statusCode).toEqual(200);
2680
+ expect(response.body).toEqual(
2681
+ `velocity-test://issue?request_uri=http%3A%2F%2Flocalhost.test%2Fapi%2Fholder%2Fv0.6%2Forg%2F${encodeURIComponent(
2682
+ tenant.did
2683
+ )}%2Fissue%2Fget-credential-manifest%3Fexchange_id%3D${
2684
+ receivedExchange._id
2685
+ }%26credential_types%3DPastEmploymentPosition&issuerDid=${tenant.did.replace(
2686
+ /:/g,
2687
+ '%3A'
2688
+ )}&vendorOriginContext=123`
2689
+ );
2690
+ });
2691
+
2692
+ it('should return a URI representing an exchange with multiple offers with different types', async () => {
2693
+ await persistOffer({
2694
+ tenant,
2695
+ exchange: receivedExchange,
2696
+ type: ['CurrentEmploymentPosition'],
2697
+ });
2698
+
2699
+ const response = await fastify.inject({
2700
+ method: 'GET',
2701
+ url: exchangesUrl(
2702
+ { tenant, exchange: receivedExchange },
2703
+ 'qrcode.uri'
2704
+ ),
2705
+ headers: {
2706
+ authorization: `Bearer ${testAuthToken}`,
2707
+ },
2708
+ });
2709
+
2710
+ expect(response.statusCode).toEqual(200);
2711
+ expect(response.body).toEqual(
2712
+ `velocity-test://issue?request_uri=http%3A%2F%2Flocalhost.test%2Fapi%2Fholder%2Fv0.6%2Forg%2F${encodeURIComponent(
2713
+ tenant.did
2714
+ )}%2Fissue%2Fget-credential-manifest%3Fexchange_id%3D${
2715
+ receivedExchange._id
2716
+ }%26credential_types%3DCurrentEmploymentPosition%26credential_types%3DPastEmploymentPosition&issuerDid=${tenant.did.replace(
2717
+ /:/g,
2718
+ '%3A'
2719
+ )}`
2720
+ );
2721
+ });
2722
+ it('should return a URI representing an exchange with multiple offers with the same types', async () => {
2723
+ await persistOffer({
2724
+ tenant,
2725
+ exchange: receivedExchange,
2726
+ });
2727
+
2728
+ const response = await fastify.inject({
2729
+ method: 'GET',
2730
+ url: exchangesUrl(
2731
+ { tenant, exchange: receivedExchange },
2732
+ 'qrcode.uri'
2733
+ ),
2734
+ headers: {
2735
+ authorization: `Bearer ${testAuthToken}`,
2736
+ },
2737
+ });
2738
+
2739
+ expect(response.statusCode).toEqual(200);
2740
+ expect(response.body).toEqual(
2741
+ `velocity-test://issue?request_uri=http%3A%2F%2Flocalhost.test%2Fapi%2Fholder%2Fv0.6%2Forg%2F${encodeURIComponent(
2742
+ tenant.did
2743
+ )}%2Fissue%2Fget-credential-manifest%3Fexchange_id%3D${
2744
+ receivedExchange._id
2745
+ }%26credential_types%3DPastEmploymentPosition&issuerDid=${tenant.did.replace(
2746
+ /:/g,
2747
+ '%3A'
2748
+ )}`
2749
+ );
2750
+ });
2751
+ });
2752
+ describe('Retrieving URIs as QR code pngs', () => {
2753
+ it('should return a QR code representing the exchange', async () => {
2754
+ const response = await fastify.inject({
2755
+ method: 'GET',
2756
+ url: exchangesUrl(
2757
+ { tenant, exchange: receivedExchange },
2758
+ 'qrcode.png'
2759
+ ),
2760
+ headers: {
2761
+ authorization: `Bearer ${testAuthToken}`,
2762
+ },
2763
+ });
2764
+
2765
+ expect(response.statusCode).toEqual(200);
2766
+ expect(response.headers['content-type']).toEqual('image/png');
2767
+ expect(response.statusCode).toEqual(200);
2768
+ expect(response.body.length).toBeGreaterThan(100);
2769
+ });
2770
+ });
2771
+ });
2772
+
2773
+ describe('/offers/clean_pii test suite', () => {
2774
+ const cleanPiiUrl = (_tenant) =>
2775
+ `/operator-api/v0.8/tenants/${_tenant._id}/offers/clean_pii`;
2776
+
2777
+ let customTenant1;
2778
+ let customTenant2;
2779
+ let customDisclosure;
2780
+ let customExchange;
2781
+ let customUser;
2782
+ let oldestOffer;
2783
+ let offer1;
2784
+ let offerApproved;
2785
+ let offerRejected;
2786
+ let offerWithCustomUser;
2787
+ let offerWithCustomDisclosure;
2788
+
2789
+ beforeEach(async () => {
2790
+ await offerCollection.deleteMany({});
2791
+ customTenant1 = await persistTenant();
2792
+ customTenant2 = await persistTenant();
2793
+
2794
+ customDisclosure = await persistDisclosure();
2795
+ customExchange = await persistOfferExchange({
2796
+ tenant,
2797
+ disclosure: customDisclosure,
2798
+ });
2799
+
2800
+ customUser = await persistVendorUserIdMapping({
2801
+ vendorUserId: 'custom123',
2802
+ tenant: customTenant1,
2803
+ });
2804
+
2805
+ oldestOffer = await persistOffer({ tenant, exchange });
2806
+ [
2807
+ offer1,
2808
+ offerApproved,
2809
+ offerRejected,
2810
+ offerWithCustomUser,
2811
+ offerWithCustomDisclosure,
2812
+ ] = await Promise.all([
2813
+ await persistOffer({ tenant, exchange }),
2814
+ await persistOffer({ tenant, exchange, consentedAt: new Date() }),
2815
+ await persistOffer({ tenant, exchange, rejectedAt: new Date() }),
2816
+ await persistOffer({ tenant, exchange, user: customUser }),
2817
+ await persistOffer({
2818
+ tenant,
2819
+ exchange: customExchange,
2820
+ user: customUser,
2821
+ }),
2822
+ await persistOffer({
2823
+ tenant: customTenant1,
2824
+ exchange,
2825
+ }),
2826
+ await persistOffer({
2827
+ tenant: customTenant2,
2828
+ exchange,
2829
+ }),
2830
+ ]);
2831
+ });
2832
+
2833
+ it('should return 200 when no offers to clean', async () => {
2834
+ await mongoDb().collection('offers').deleteMany({});
2835
+ const response = await fastify.injectJson({
2836
+ method: 'POST',
2837
+ url: cleanPiiUrl(tenant),
2838
+ payload: {},
2839
+ });
2840
+ expect(response.statusCode).toEqual(200);
2841
+ expect(response.json).toEqual({ numCleaned: 0 });
2842
+ });
2843
+
2844
+ it('should return 200 when offers cleaned', async () => {
2845
+ const response = await fastify.injectJson({
2846
+ method: 'POST',
2847
+ url: cleanPiiUrl(tenant),
2848
+ payload: {},
2849
+ });
2850
+ expect(response.statusCode).toEqual(200);
2851
+ expect(response.json).toEqual({ numCleaned: 6 });
2852
+
2853
+ const offersDb = await offersRepo
2854
+ .collection()
2855
+ .find(
2856
+ {
2857
+ 'issuer.id': tenant.did,
2858
+ },
2859
+ {
2860
+ sort: {
2861
+ _id: 1,
2862
+ },
2863
+ }
2864
+ )
2865
+ .toArray();
2866
+ expect(offersDb).toHaveLength(6);
2867
+ for (const offer of offersDb) {
2868
+ expect(offer.credentialSubject).toStrictEqual({
2869
+ vendorUserId: expect.any(String),
2870
+ });
2871
+ }
2872
+ const othersOffersDb = await offersRepo
2873
+ .collection()
2874
+ .find(
2875
+ {
2876
+ 'issuer.id': {
2877
+ $ne: tenant.did,
2878
+ },
2879
+ },
2880
+ {
2881
+ sort: {
2882
+ _id: 1,
2883
+ },
2884
+ }
2885
+ )
2886
+ .toArray();
2887
+ for (const offer of othersOffersDb) {
2888
+ expect(offer.credentialSubject).not.toStrictEqual({
2889
+ vendorUserId: expect.any(String),
2890
+ });
2891
+ }
2892
+ });
2893
+
2894
+ it('should return 200 and clean pii by vendor user id', async () => {
2895
+ const response = await fastify.injectJson({
2896
+ method: 'POST',
2897
+ url: cleanPiiUrl(tenant),
2898
+ payload: {
2899
+ filter: {
2900
+ vendorUserId: customUser.vendorUserId,
2901
+ },
2902
+ },
2903
+ });
2904
+ expect(response.statusCode).toEqual(200);
2905
+ expect(response.json).toEqual({ numCleaned: 2 });
2906
+
2907
+ const offersDb = await offersRepo.find();
2908
+ expect(offersDb).toHaveLength(6);
2909
+ expect(map('credentialSubject', offersDb)).toStrictEqual(
2910
+ expect.arrayContaining([
2911
+ oldestOffer.credentialSubject,
2912
+ offer1.credentialSubject,
2913
+ offerApproved.credentialSubject,
2914
+ offerRejected.credentialSubject,
2915
+ {
2916
+ vendorUserId: offerWithCustomUser.credentialSubject.vendorUserId,
2917
+ },
2918
+ {
2919
+ vendorUserId:
2920
+ offerWithCustomDisclosure.credentialSubject.vendorUserId,
2921
+ },
2922
+ ])
2923
+ );
2924
+ });
2925
+
2926
+ it('should return 200 and clean finalized offers', async () => {
2927
+ const response = await fastify.injectJson({
2928
+ method: 'POST',
2929
+ url: cleanPiiUrl(tenant),
2930
+ payload: {
2931
+ filter: {
2932
+ finalized: true,
2933
+ },
2934
+ },
2935
+ });
2936
+ expect(response.statusCode).toEqual(200);
2937
+ expect(response.json).toEqual({ numCleaned: 2 });
2938
+
2939
+ const offersDb = await offersRepo.find();
2940
+ expect(offersDb).toHaveLength(6);
2941
+ expect(map('credentialSubject', offersDb)).toStrictEqual(
2942
+ expect.arrayContaining([
2943
+ oldestOffer.credentialSubject,
2944
+ offer1.credentialSubject,
2945
+ offerWithCustomUser.credentialSubject,
2946
+ offerWithCustomDisclosure.credentialSubject,
2947
+ {
2948
+ vendorUserId: offerApproved.credentialSubject.vendorUserId,
2949
+ },
2950
+ {
2951
+ vendorUserId: offerRejected.credentialSubject.vendorUserId,
2952
+ },
2953
+ ])
2954
+ );
2955
+ });
2956
+
2957
+ it('should return 200 and clean offers that created before timestamp', async () => {
2958
+ const response = await fastify.injectJson({
2959
+ method: 'POST',
2960
+ url: cleanPiiUrl(tenant),
2961
+ payload: {
2962
+ filter: {
2963
+ createdBefore: oldestOffer.createdAt,
2964
+ },
2965
+ },
2966
+ });
2967
+ expect(response.statusCode).toEqual(200);
2968
+ expect(response.json).toEqual({ numCleaned: 1 });
2969
+
2970
+ const offersDb = await offersRepo.find();
2971
+ expect(offersDb).toHaveLength(6);
2972
+ expect(map('credentialSubject', offersDb)).toStrictEqual(
2973
+ expect.arrayContaining([
2974
+ offerApproved.credentialSubject,
2975
+ offer1.credentialSubject,
2976
+ offerWithCustomUser.credentialSubject,
2977
+ offerWithCustomDisclosure.credentialSubject,
2978
+ offerRejected.credentialSubject,
2979
+ {
2980
+ vendorUserId: oldestOffer.credentialSubject.vendorUserId,
2981
+ },
2982
+ ])
2983
+ );
2984
+ });
2985
+
2986
+ it('should return 200 and clean offers for the specified disclosure', async () => {
2987
+ const customDisclosure1 = await persistDisclosure();
2988
+ const customExchange1 = await persistOfferExchange({
2989
+ tenant,
2990
+ disclosure: customDisclosure1,
2991
+ });
2992
+ const customOffer = await persistOffer({
2993
+ tenant,
2994
+ exchange: customExchange1,
2995
+ user: customUser,
2996
+ });
2997
+ const response = await fastify.injectJson({
2998
+ method: 'POST',
2999
+ url: cleanPiiUrl(tenant),
3000
+ payload: {
3001
+ filter: {
3002
+ disclosureId: customDisclosure1._id.toString(),
3003
+ },
3004
+ },
3005
+ });
3006
+ expect(response.statusCode).toEqual(200);
3007
+ expect(response.json).toEqual({ numCleaned: 1 });
3008
+
3009
+ const offersDb = await offersRepo.find();
3010
+ expect(offersDb).toHaveLength(7);
3011
+ expect(map('credentialSubject', offersDb)).toStrictEqual(
3012
+ expect.arrayContaining([
3013
+ oldestOffer.credentialSubject,
3014
+ offer1.credentialSubject,
3015
+ offerApproved.credentialSubject,
3016
+ offerRejected.credentialSubject,
3017
+ offerWithCustomUser.credentialSubject,
3018
+ offerWithCustomDisclosure.credentialSubject,
3019
+ {
3020
+ vendorUserId: customOffer.credentialSubject.vendorUserId,
3021
+ },
3022
+ ])
3023
+ );
3024
+ });
3025
+ });
3026
+ });