@openstax/ts-utils 1.33.1 → 1.34.0

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 (504) hide show
  1. package/.cfnlintrc +2 -0
  2. package/.github/CODEOWNERS +1 -0
  3. package/.github/workflows/ci.yml +36 -0
  4. package/.github/workflows/lint.yml +55 -0
  5. package/.nvmrc +1 -0
  6. package/.syncignore +4 -0
  7. package/.syncpackrc +18 -0
  8. package/CONTRIBUTING.md +96 -0
  9. package/LICENSE +661 -0
  10. package/Procfile +1 -0
  11. package/README.md +62 -90
  12. package/app.json +23 -0
  13. package/cspell.json +32 -0
  14. package/deploy/constants.env +21 -0
  15. package/deploy/deploy.bash +157 -0
  16. package/deploy/deployment-alt-region.cfn.yml +70 -0
  17. package/deploy/deployment.cfn.yml +650 -0
  18. package/deploy/destroy-deployment.bash +23 -0
  19. package/deploy/shared.cfn.yml +94 -0
  20. package/docs/lambda-build.md +35 -0
  21. package/package.json +12 -228
  22. package/packages/frontend/README.md +46 -0
  23. package/packages/frontend/package.json +101 -0
  24. package/packages/frontend/public/favicon.ico +0 -0
  25. package/packages/frontend/public/index.html +107 -0
  26. package/packages/frontend/public/maintenance.html +59 -0
  27. package/packages/frontend/public/manifest.json +15 -0
  28. package/packages/frontend/public/robots.txt +3 -0
  29. package/packages/frontend/script/make-certificate.bash +49 -0
  30. package/packages/frontend/script/server/cli.js +11 -0
  31. package/packages/frontend/script/server/index.js +47 -0
  32. package/packages/frontend/script/start.bash +22 -0
  33. package/packages/frontend/script/trust-localhost.bash +7 -0
  34. package/packages/frontend/src/auth/authProvider.ts +10 -0
  35. package/packages/frontend/src/auth/useAuth.ts +33 -0
  36. package/packages/frontend/src/components/Pagination.tsx +26 -0
  37. package/packages/frontend/src/configProvider/index.ts +53 -0
  38. package/packages/frontend/src/configProvider/use.ts +41 -0
  39. package/packages/frontend/src/core/context/services.spec.tsx +39 -0
  40. package/packages/frontend/src/core/context/services.tsx +16 -0
  41. package/packages/frontend/src/core/index.spec.ts +7 -0
  42. package/packages/frontend/src/core/index.ts +20 -0
  43. package/packages/frontend/src/core/services.tsx +14 -0
  44. package/packages/frontend/src/core/types.ts +3 -0
  45. package/packages/frontend/src/example/api.ts +28 -0
  46. package/packages/frontend/src/example/components/Layout.tsx +23 -0
  47. package/packages/frontend/src/example/screens/Home.spec.tsx +68 -0
  48. package/packages/frontend/src/example/screens/Home.tsx +78 -0
  49. package/packages/frontend/src/example/screens/ThingList.spec.tsx +60 -0
  50. package/packages/frontend/src/example/screens/ThingList.tsx +75 -0
  51. package/packages/frontend/src/example/screens/ThingView.spec.tsx +71 -0
  52. package/packages/frontend/src/example/screens/ThingView.tsx +47 -0
  53. package/packages/frontend/src/example/screens/index.ts +9 -0
  54. package/packages/frontend/src/index.css +159 -0
  55. package/packages/frontend/src/index.tsx +67 -0
  56. package/packages/frontend/src/react-app-env.d.ts +1 -0
  57. package/packages/frontend/src/routing/components/RouteLink.spec.tsx +55 -0
  58. package/packages/frontend/src/routing/components/RouteLink.tsx +35 -0
  59. package/packages/frontend/src/routing/middleware.ts +6 -0
  60. package/packages/frontend/src/routing/useQuery.ts +14 -0
  61. package/packages/frontend/src/setupProxy.js +19 -0
  62. package/packages/frontend/src/setupTests.ts +9 -0
  63. package/packages/frontend/src/tests/testServices.tsx +23 -0
  64. package/packages/frontend/tsconfig.json +27 -0
  65. package/packages/lambda/.eslintrc.js +64 -0
  66. package/packages/lambda/jest-global-setup.js +3 -0
  67. package/packages/lambda/jest-setup-after-env.js +1 -0
  68. package/packages/lambda/jest.config.js +31 -0
  69. package/packages/lambda/jest.resolver.js +17 -0
  70. package/packages/lambda/package.json +68 -0
  71. package/packages/lambda/script/build.bash +19 -0
  72. package/packages/lambda/script/bundle-functions.bash +10 -0
  73. package/packages/lambda/script/lambdaLocalProxy.js +16 -0
  74. package/packages/lambda/script/lambdaLocalProxy.spec.ts +147 -0
  75. package/packages/lambda/script/utils/getRouteData.ts +7 -0
  76. package/packages/lambda/script/utils/routeDataLoader.js +8 -0
  77. package/packages/lambda/script/utils/routeDataLoader.spec.ts +8 -0
  78. package/packages/lambda/src/functions/serviceApi/core/index.ts +7 -0
  79. package/packages/lambda/src/functions/serviceApi/core/request.spec.ts +38 -0
  80. package/packages/lambda/src/functions/serviceApi/core/request.ts +42 -0
  81. package/packages/lambda/src/functions/serviceApi/core/routes.spec.ts +7 -0
  82. package/packages/lambda/src/functions/serviceApi/core/routes.ts +10 -0
  83. package/packages/lambda/src/functions/serviceApi/core/services.ts +9 -0
  84. package/packages/lambda/src/functions/serviceApi/core/types.ts +13 -0
  85. package/packages/lambda/src/functions/serviceApi/entry/lambda/https-xray.ts +4 -0
  86. package/packages/lambda/src/functions/serviceApi/entry/lambda/index.spec.ts +48 -0
  87. package/packages/lambda/src/functions/serviceApi/entry/lambda/index.ts +58 -0
  88. package/packages/lambda/src/functions/serviceApi/entry/lambda/services.ts +36 -0
  89. package/packages/lambda/src/functions/serviceApi/entry/local.ts +71 -0
  90. package/packages/lambda/src/functions/serviceApi/versions/v0/example/documentSearchMiddleware.spec.ts +16 -0
  91. package/packages/lambda/src/functions/serviceApi/versions/v0/example/documentSearchMiddleware.ts +41 -0
  92. package/packages/lambda/src/functions/serviceApi/versions/v0/example/documentStoreMiddleware.spec.ts +78 -0
  93. package/packages/lambda/src/functions/serviceApi/versions/v0/example/documentStoreMiddleware.ts +70 -0
  94. package/packages/lambda/src/functions/serviceApi/versions/v0/example/routes.spec.ts +306 -0
  95. package/packages/lambda/src/functions/serviceApi/versions/v0/example/routes.ts +176 -0
  96. package/packages/lambda/src/functions/serviceApi/versions/v0/index.spec.ts +263 -0
  97. package/packages/lambda/src/functions/serviceApi/versions/v0/index.ts +134 -0
  98. package/packages/lambda/src/functions/serviceApi/versions/v0/middleware/authMiddleware.spec.ts +23 -0
  99. package/packages/lambda/src/functions/serviceApi/versions/v0/middleware/authMiddleware.ts +32 -0
  100. package/packages/lambda/src/functions/serviceApi/versions/v0/middleware/configMiddleware.spec.ts +10 -0
  101. package/packages/lambda/src/functions/serviceApi/versions/v0/middleware/configMiddleware.ts +7 -0
  102. package/packages/lambda/src/functions/serviceApi/versions/v0/middleware/frontendFileServerMiddleware.spec.ts +13 -0
  103. package/packages/lambda/src/functions/serviceApi/versions/v0/middleware/frontendFileServerMiddleware.ts +23 -0
  104. package/packages/lambda/src/functions/serviceApi/versions/v0/middleware/paginationMiddleware.spec.ts +9 -0
  105. package/packages/lambda/src/functions/serviceApi/versions/v0/middleware/paginationMiddleware.ts +9 -0
  106. package/packages/lambda/src/functions/serviceApi/versions/v0/middleware/searchMiddleware.spec.ts +12 -0
  107. package/packages/lambda/src/functions/serviceApi/versions/v0/middleware/searchMiddleware.ts +21 -0
  108. package/packages/lambda/src/functions/serviceApi/versions/v0/middleware/userRoleValidatorMiddleware.spec.ts +21 -0
  109. package/packages/lambda/src/functions/serviceApi/versions/v0/middleware/userRoleValidatorMiddleware.ts +18 -0
  110. package/packages/lambda/tsconfig.json +30 -0
  111. package/packages/lambda/webpack.config.js +97 -0
  112. package/packages/utils/.eslintrc.js +64 -0
  113. package/packages/utils/README.md +118 -0
  114. package/packages/utils/jest-global-setup.js +3 -0
  115. package/packages/utils/jest.config.js +25 -0
  116. package/packages/utils/jest.resolver.js +17 -0
  117. package/packages/utils/package.json +238 -0
  118. package/packages/utils/src/assertions/index.spec.ts +126 -0
  119. package/{dist/esm/assertions/index.js → packages/utils/src/assertions/index.ts} +64 -49
  120. package/packages/utils/src/aws/ssmService.ts +7 -0
  121. package/packages/utils/src/config/awsParameterConfig.ts +24 -0
  122. package/packages/utils/src/config/envConfig.ts +58 -0
  123. package/packages/utils/src/config/index.spec.ts +165 -0
  124. package/{dist/esm/config/index.d.ts → packages/utils/src/config/index.ts} +29 -13
  125. package/packages/utils/src/config/lambdaParameterConfig.ts +49 -0
  126. package/{dist/esm/config/replaceConfig.js → packages/utils/src/config/replaceConfig.ts} +16 -6
  127. package/packages/utils/src/config/resolveConfigValue.ts +10 -0
  128. package/packages/utils/src/errors/index.spec.ts +35 -0
  129. package/{dist/esm/errors/index.js → packages/utils/src/errors/index.ts} +57 -41
  130. package/packages/utils/src/fetch/fetchStatusRetry.spec.ts +197 -0
  131. package/packages/utils/src/fetch/fetchStatusRetry.ts +33 -0
  132. package/packages/utils/src/fetch/index.spec.ts +34 -0
  133. package/packages/utils/src/fetch/index.ts +87 -0
  134. package/packages/utils/src/guards/index.spec.ts +58 -0
  135. package/{dist/esm/guards/index.d.ts → packages/utils/src/guards/index.ts} +10 -7
  136. package/packages/utils/src/index.spec.ts +471 -0
  137. package/packages/utils/src/middleware/apiErrorHandler.spec.ts +65 -0
  138. package/packages/utils/src/middleware/apiErrorHandler.ts +67 -0
  139. package/packages/utils/src/middleware/apiSlowResponseMiddleware.spec.ts +184 -0
  140. package/packages/utils/src/middleware/apiSlowResponseMiddleware.ts +71 -0
  141. package/packages/utils/src/middleware/index.spec.ts +99 -0
  142. package/{dist/cjs/middleware/index.d.ts → packages/utils/src/middleware/index.ts} +53 -5
  143. package/packages/utils/src/middleware/lambdaCorsResponseMiddleware.spec.ts +103 -0
  144. package/packages/utils/src/middleware/lambdaCorsResponseMiddleware.ts +52 -0
  145. package/packages/utils/src/middleware/throwNotFoundMiddleware.spec.ts +20 -0
  146. package/packages/utils/src/middleware/throwNotFoundMiddleware.ts +11 -0
  147. package/packages/utils/src/misc/hashValue.ts +18 -0
  148. package/packages/utils/src/misc/helpers.ts +259 -0
  149. package/packages/utils/src/misc/merge.ts +48 -0
  150. package/{dist/esm/misc/partitionSequence.js → packages/utils/src/misc/partitionSequence.ts} +23 -15
  151. package/packages/utils/src/pagination/index.spec.ts +150 -0
  152. package/packages/utils/src/pagination/index.ts +117 -0
  153. package/{dist/esm/routing/helpers.js → packages/utils/src/routing/helpers.ts} +42 -30
  154. package/packages/utils/src/routing/index.spec.ts +553 -0
  155. package/packages/utils/src/routing/index.ts +424 -0
  156. package/packages/utils/src/routing/validators/zod.spec.ts +16 -0
  157. package/packages/utils/src/routing/validators/zod.ts +14 -0
  158. package/packages/utils/src/services/accountsGateway/README.md +3 -0
  159. package/packages/utils/src/services/accountsGateway/index.spec.ts +518 -0
  160. package/packages/utils/src/services/accountsGateway/index.ts +251 -0
  161. package/packages/utils/src/services/apiGateway/README.md +93 -0
  162. package/packages/utils/src/services/apiGateway/index.spec.ts +254 -0
  163. package/packages/utils/src/services/apiGateway/index.ts +189 -0
  164. package/packages/utils/src/services/authProvider/README.md +21 -0
  165. package/packages/utils/src/services/authProvider/browser.spec.ts +391 -0
  166. package/packages/utils/src/services/authProvider/browser.ts +209 -0
  167. package/packages/utils/src/services/authProvider/decryption.spec.ts +337 -0
  168. package/packages/utils/src/services/authProvider/decryption.ts +98 -0
  169. package/packages/utils/src/services/authProvider/index.ts +93 -0
  170. package/packages/utils/src/services/authProvider/stub.spec.ts +29 -0
  171. package/packages/utils/src/services/authProvider/subrequest.spec.ts +105 -0
  172. package/packages/utils/src/services/authProvider/subrequest.ts +68 -0
  173. package/packages/utils/src/services/authProvider/utils/decryptAndVerify.spec.ts +128 -0
  174. package/packages/utils/src/services/authProvider/utils/decryptAndVerify.ts +106 -0
  175. package/packages/utils/src/services/authProvider/utils/embeddedAuthProvider.spec.ts +26 -0
  176. package/packages/utils/src/services/authProvider/utils/embeddedAuthProvider.ts +57 -0
  177. package/packages/utils/src/services/authProvider/utils/userRoleValidator.spec.ts +135 -0
  178. package/packages/utils/src/services/authProvider/utils/userRoleValidator.ts +49 -0
  179. package/packages/utils/src/services/authProvider/utils/userSubrequest.spec.ts +26 -0
  180. package/packages/utils/src/services/authProvider/utils/userSubrequest.ts +10 -0
  181. package/packages/utils/src/services/documentStore/dynamoEncoding.ts +57 -0
  182. package/packages/utils/src/services/documentStore/fileSystemAssert.spec.ts +43 -0
  183. package/packages/utils/src/services/documentStore/fileSystemAssert.ts +10 -0
  184. package/{dist/cjs/services/documentStore/index.d.ts → packages/utils/src/services/documentStore/index.ts} +8 -8
  185. package/packages/utils/src/services/documentStore/unversioned/README.md +13 -0
  186. package/packages/utils/src/services/documentStore/unversioned/dynamodb.spec.ts +859 -0
  187. package/packages/utils/src/services/documentStore/unversioned/dynamodb.ts +243 -0
  188. package/packages/utils/src/services/documentStore/unversioned/file-system.spec.ts +629 -0
  189. package/packages/utils/src/services/documentStore/unversioned/file-system.ts +194 -0
  190. package/{dist/cjs/services/documentStore/unversioned/index.d.ts → packages/utils/src/services/documentStore/unversioned/index.ts} +2 -0
  191. package/packages/utils/src/services/documentStore/versioned/README.md +13 -0
  192. package/packages/utils/src/services/documentStore/versioned/dynamodb.spec.ts +376 -0
  193. package/packages/utils/src/services/documentStore/versioned/dynamodb.ts +167 -0
  194. package/packages/utils/src/services/documentStore/versioned/file-system.spec.ts +262 -0
  195. package/packages/utils/src/services/documentStore/versioned/file-system.ts +90 -0
  196. package/packages/utils/src/services/documentStore/versioned/index.ts +25 -0
  197. package/packages/utils/src/services/exercisesGateway/README.md +5 -0
  198. package/packages/utils/src/services/exercisesGateway/index.spec.ts +326 -0
  199. package/packages/utils/src/services/exercisesGateway/index.ts +163 -0
  200. package/packages/utils/src/services/fileServer/index.spec.ts +88 -0
  201. package/packages/utils/src/services/fileServer/index.ts +43 -0
  202. package/packages/utils/src/services/fileServer/localFileServer.spec.ts +182 -0
  203. package/packages/utils/src/services/fileServer/localFileServer.ts +159 -0
  204. package/packages/utils/src/services/fileServer/s3FileServer.spec.ts +266 -0
  205. package/packages/utils/src/services/fileServer/s3FileServer.ts +155 -0
  206. package/packages/utils/src/services/launchParams/index.spec.ts +366 -0
  207. package/packages/utils/src/services/launchParams/signer.ts +73 -0
  208. package/packages/utils/src/services/launchParams/verifier.ts +120 -0
  209. package/packages/utils/src/services/logger/console.spec.ts +29 -0
  210. package/{dist/esm/services/logger/console.js → packages/utils/src/services/logger/console.ts} +5 -2
  211. package/packages/utils/src/services/logger/index.spec.ts +65 -0
  212. package/{dist/esm/services/logger/index.d.ts → packages/utils/src/services/logger/index.ts} +23 -9
  213. package/packages/utils/src/services/lrsGateway/README.md +5 -0
  214. package/packages/utils/src/services/lrsGateway/addStatementDefaultFields.ts +22 -0
  215. package/packages/utils/src/services/lrsGateway/attempt-utils.spec.ts +847 -0
  216. package/packages/utils/src/services/lrsGateway/attempt-utils.ts +358 -0
  217. package/packages/utils/src/services/lrsGateway/file-system.spec.ts +363 -0
  218. package/packages/utils/src/services/lrsGateway/file-system.ts +165 -0
  219. package/packages/utils/src/services/lrsGateway/index.spec.ts +194 -0
  220. package/packages/utils/src/services/lrsGateway/index.ts +257 -0
  221. package/packages/utils/src/services/lrsGateway/xapiUtils.spec.ts +887 -0
  222. package/packages/utils/src/services/lrsGateway/xapiUtils.ts +262 -0
  223. package/packages/utils/src/services/postgresConnection/index.spec.ts +170 -0
  224. package/packages/utils/src/services/postgresConnection/index.ts +84 -0
  225. package/packages/utils/src/services/searchProvider/README.md +3 -0
  226. package/packages/utils/src/services/searchProvider/index.ts +59 -0
  227. package/packages/utils/src/services/searchProvider/memorySearchTheBadWay.spec.ts +526 -0
  228. package/packages/utils/src/services/searchProvider/memorySearchTheBadWay.ts +223 -0
  229. package/packages/utils/src/services/searchProvider/openSearch.spec.ts +926 -0
  230. package/packages/utils/src/services/searchProvider/openSearch.ts +195 -0
  231. package/{dist/esm/types.d.ts → packages/utils/src/types.ts} +34 -6
  232. package/packages/utils/tsconfig.json +31 -0
  233. package/packages/utils/tsconfig.without-specs.cjs.json +7 -0
  234. package/packages/utils/tsconfig.without-specs.esm.json +7 -0
  235. package/packages/utils/tsconfig.without-specs.json +6 -0
  236. package/scripts/build.bash +24 -0
  237. package/scripts/ci.bash +10 -0
  238. package/scripts/start.bash +29 -0
  239. package/dist/cjs/assertions/index.d.ts +0 -89
  240. package/dist/cjs/assertions/index.js +0 -157
  241. package/dist/cjs/aws/ssmService.d.ts +0 -5
  242. package/dist/cjs/aws/ssmService.js +0 -9
  243. package/dist/cjs/config/awsParameterConfig.d.ts +0 -10
  244. package/dist/cjs/config/awsParameterConfig.js +0 -26
  245. package/dist/cjs/config/envConfig.d.ts +0 -24
  246. package/dist/cjs/config/envConfig.js +0 -57
  247. package/dist/cjs/config/index.d.ts +0 -48
  248. package/dist/cjs/config/index.js +0 -35
  249. package/dist/cjs/config/lambdaParameterConfig.d.ts +0 -12
  250. package/dist/cjs/config/lambdaParameterConfig.js +0 -45
  251. package/dist/cjs/config/replaceConfig.d.ts +0 -14
  252. package/dist/cjs/config/replaceConfig.js +0 -22
  253. package/dist/cjs/config/resolveConfigValue.d.ts +0 -5
  254. package/dist/cjs/config/resolveConfigValue.js +0 -12
  255. package/dist/cjs/errors/index.d.ts +0 -88
  256. package/dist/cjs/errors/index.js +0 -123
  257. package/dist/cjs/fetch/fetchStatusRetry.d.ts +0 -8
  258. package/dist/cjs/fetch/fetchStatusRetry.js +0 -27
  259. package/dist/cjs/fetch/index.d.ts +0 -64
  260. package/dist/cjs/fetch/index.js +0 -55
  261. package/dist/cjs/guards/index.d.ts +0 -38
  262. package/dist/cjs/guards/index.js +0 -44
  263. package/dist/cjs/index.js +0 -20
  264. package/dist/cjs/middleware/apiErrorHandler.d.ts +0 -24
  265. package/dist/cjs/middleware/apiErrorHandler.js +0 -42
  266. package/dist/cjs/middleware/apiSlowResponseMiddleware.d.ts +0 -23
  267. package/dist/cjs/middleware/apiSlowResponseMiddleware.js +0 -54
  268. package/dist/cjs/middleware/index.js +0 -48
  269. package/dist/cjs/middleware/lambdaCorsResponseMiddleware.d.ts +0 -20
  270. package/dist/cjs/middleware/lambdaCorsResponseMiddleware.js +0 -44
  271. package/dist/cjs/middleware/throwNotFoundMiddleware.d.ts +0 -4
  272. package/dist/cjs/middleware/throwNotFoundMiddleware.js +0 -14
  273. package/dist/cjs/misc/hashValue.d.ts +0 -10
  274. package/dist/cjs/misc/hashValue.js +0 -17
  275. package/dist/cjs/misc/helpers.d.ts +0 -124
  276. package/dist/cjs/misc/helpers.js +0 -214
  277. package/dist/cjs/misc/merge.d.ts +0 -21
  278. package/dist/cjs/misc/merge.js +0 -45
  279. package/dist/cjs/misc/partitionSequence.d.ts +0 -35
  280. package/dist/cjs/misc/partitionSequence.js +0 -55
  281. package/dist/cjs/pagination/index.d.ts +0 -91
  282. package/dist/cjs/pagination/index.js +0 -83
  283. package/dist/cjs/routing/helpers.d.ts +0 -57
  284. package/dist/cjs/routing/helpers.js +0 -90
  285. package/dist/cjs/routing/index.d.ts +0 -290
  286. package/dist/cjs/routing/index.js +0 -295
  287. package/dist/cjs/routing/validators/zod.d.ts +0 -4
  288. package/dist/cjs/routing/validators/zod.js +0 -14
  289. package/dist/cjs/services/accountsGateway/index.d.ts +0 -92
  290. package/dist/cjs/services/accountsGateway/index.js +0 -138
  291. package/dist/cjs/services/apiGateway/index.d.ts +0 -68
  292. package/dist/cjs/services/apiGateway/index.js +0 -118
  293. package/dist/cjs/services/authProvider/browser.d.ts +0 -40
  294. package/dist/cjs/services/authProvider/browser.js +0 -155
  295. package/dist/cjs/services/authProvider/decryption.d.ts +0 -19
  296. package/dist/cjs/services/authProvider/decryption.js +0 -73
  297. package/dist/cjs/services/authProvider/index.d.ts +0 -63
  298. package/dist/cjs/services/authProvider/index.js +0 -34
  299. package/dist/cjs/services/authProvider/subrequest.d.ts +0 -13
  300. package/dist/cjs/services/authProvider/subrequest.js +0 -49
  301. package/dist/cjs/services/authProvider/utils/decryptAndVerify.d.ts +0 -28
  302. package/dist/cjs/services/authProvider/utils/decryptAndVerify.js +0 -91
  303. package/dist/cjs/services/authProvider/utils/embeddedAuthProvider.d.ts +0 -26
  304. package/dist/cjs/services/authProvider/utils/embeddedAuthProvider.js +0 -47
  305. package/dist/cjs/services/authProvider/utils/userRoleValidator.d.ts +0 -13
  306. package/dist/cjs/services/authProvider/utils/userRoleValidator.js +0 -37
  307. package/dist/cjs/services/authProvider/utils/userSubrequest.d.ts +0 -3
  308. package/dist/cjs/services/authProvider/utils/userSubrequest.js +0 -13
  309. package/dist/cjs/services/documentStore/dynamoEncoding.d.ts +0 -10
  310. package/dist/cjs/services/documentStore/dynamoEncoding.js +0 -52
  311. package/dist/cjs/services/documentStore/fileSystemAssert.d.ts +0 -1
  312. package/dist/cjs/services/documentStore/fileSystemAssert.js +0 -14
  313. package/dist/cjs/services/documentStore/index.js +0 -2
  314. package/dist/cjs/services/documentStore/unversioned/dynamodb.d.ts +0 -31
  315. package/dist/cjs/services/documentStore/unversioned/dynamodb.js +0 -233
  316. package/dist/cjs/services/documentStore/unversioned/file-system.d.ts +0 -32
  317. package/dist/cjs/services/documentStore/unversioned/file-system.js +0 -214
  318. package/dist/cjs/services/documentStore/unversioned/index.js +0 -2
  319. package/dist/cjs/services/documentStore/versioned/dynamodb.d.ts +0 -25
  320. package/dist/cjs/services/documentStore/versioned/dynamodb.js +0 -143
  321. package/dist/cjs/services/documentStore/versioned/file-system.d.ts +0 -25
  322. package/dist/cjs/services/documentStore/versioned/file-system.js +0 -73
  323. package/dist/cjs/services/documentStore/versioned/index.d.ts +0 -17
  324. package/dist/cjs/services/documentStore/versioned/index.js +0 -2
  325. package/dist/cjs/services/exercisesGateway/index.d.ts +0 -67
  326. package/dist/cjs/services/exercisesGateway/index.js +0 -107
  327. package/dist/cjs/services/fileServer/index.d.ts +0 -30
  328. package/dist/cjs/services/fileServer/index.js +0 -19
  329. package/dist/cjs/services/fileServer/localFileServer.d.ts +0 -13
  330. package/dist/cjs/services/fileServer/localFileServer.js +0 -132
  331. package/dist/cjs/services/fileServer/s3FileServer.d.ts +0 -14
  332. package/dist/cjs/services/fileServer/s3FileServer.js +0 -132
  333. package/dist/cjs/services/launchParams/index.js +0 -7
  334. package/dist/cjs/services/launchParams/signer.d.ts +0 -23
  335. package/dist/cjs/services/launchParams/signer.js +0 -58
  336. package/dist/cjs/services/launchParams/verifier.d.ts +0 -21
  337. package/dist/cjs/services/launchParams/verifier.js +0 -129
  338. package/dist/cjs/services/logger/console.d.ts +0 -4
  339. package/dist/cjs/services/logger/console.js +0 -12
  340. package/dist/cjs/services/logger/index.d.ts +0 -39
  341. package/dist/cjs/services/logger/index.js +0 -31
  342. package/dist/cjs/services/lrsGateway/addStatementDefaultFields.d.ts +0 -5
  343. package/dist/cjs/services/lrsGateway/addStatementDefaultFields.js +0 -21
  344. package/dist/cjs/services/lrsGateway/attempt-utils.d.ts +0 -70
  345. package/dist/cjs/services/lrsGateway/attempt-utils.js +0 -258
  346. package/dist/cjs/services/lrsGateway/file-system.d.ts +0 -15
  347. package/dist/cjs/services/lrsGateway/file-system.js +0 -150
  348. package/dist/cjs/services/lrsGateway/index.d.ts +0 -122
  349. package/dist/cjs/services/lrsGateway/index.js +0 -148
  350. package/dist/cjs/services/lrsGateway/xapiUtils.d.ts +0 -68
  351. package/dist/cjs/services/lrsGateway/xapiUtils.js +0 -109
  352. package/dist/cjs/services/postgresConnection/index.d.ts +0 -28
  353. package/dist/cjs/services/postgresConnection/index.js +0 -65
  354. package/dist/cjs/services/searchProvider/index.d.ts +0 -67
  355. package/dist/cjs/services/searchProvider/index.js +0 -2
  356. package/dist/cjs/services/searchProvider/memorySearchTheBadWay.d.ts +0 -20
  357. package/dist/cjs/services/searchProvider/memorySearchTheBadWay.js +0 -191
  358. package/dist/cjs/services/searchProvider/openSearch.d.ts +0 -28
  359. package/dist/cjs/services/searchProvider/openSearch.js +0 -154
  360. package/dist/cjs/tsconfig.without-specs.cjs.tsbuildinfo +0 -1
  361. package/dist/cjs/types.d.ts +0 -31
  362. package/dist/cjs/types.js +0 -2
  363. package/dist/esm/assertions/index.d.ts +0 -89
  364. package/dist/esm/aws/ssmService.d.ts +0 -5
  365. package/dist/esm/aws/ssmService.js +0 -6
  366. package/dist/esm/config/awsParameterConfig.d.ts +0 -10
  367. package/dist/esm/config/awsParameterConfig.js +0 -22
  368. package/dist/esm/config/envConfig.d.ts +0 -24
  369. package/dist/esm/config/envConfig.js +0 -53
  370. package/dist/esm/config/index.js +0 -17
  371. package/dist/esm/config/lambdaParameterConfig.d.ts +0 -12
  372. package/dist/esm/config/lambdaParameterConfig.js +0 -38
  373. package/dist/esm/config/replaceConfig.d.ts +0 -14
  374. package/dist/esm/config/resolveConfigValue.d.ts +0 -5
  375. package/dist/esm/config/resolveConfigValue.js +0 -8
  376. package/dist/esm/errors/index.d.ts +0 -88
  377. package/dist/esm/fetch/fetchStatusRetry.d.ts +0 -8
  378. package/dist/esm/fetch/fetchStatusRetry.js +0 -23
  379. package/dist/esm/fetch/index.d.ts +0 -64
  380. package/dist/esm/fetch/index.js +0 -46
  381. package/dist/esm/guards/index.js +0 -36
  382. package/dist/esm/index.d.ts +0 -4
  383. package/dist/esm/index.js +0 -4
  384. package/dist/esm/middleware/apiErrorHandler.d.ts +0 -24
  385. package/dist/esm/middleware/apiErrorHandler.js +0 -38
  386. package/dist/esm/middleware/apiSlowResponseMiddleware.d.ts +0 -23
  387. package/dist/esm/middleware/apiSlowResponseMiddleware.js +0 -50
  388. package/dist/esm/middleware/index.d.ts +0 -47
  389. package/dist/esm/middleware/index.js +0 -44
  390. package/dist/esm/middleware/lambdaCorsResponseMiddleware.d.ts +0 -20
  391. package/dist/esm/middleware/lambdaCorsResponseMiddleware.js +0 -40
  392. package/dist/esm/middleware/throwNotFoundMiddleware.d.ts +0 -4
  393. package/dist/esm/middleware/throwNotFoundMiddleware.js +0 -10
  394. package/dist/esm/misc/hashValue.d.ts +0 -10
  395. package/dist/esm/misc/hashValue.js +0 -13
  396. package/dist/esm/misc/helpers.d.ts +0 -124
  397. package/dist/esm/misc/helpers.js +0 -199
  398. package/dist/esm/misc/merge.d.ts +0 -21
  399. package/dist/esm/misc/merge.js +0 -40
  400. package/dist/esm/misc/partitionSequence.d.ts +0 -35
  401. package/dist/esm/pagination/index.d.ts +0 -91
  402. package/dist/esm/pagination/index.js +0 -77
  403. package/dist/esm/routing/helpers.d.ts +0 -57
  404. package/dist/esm/routing/index.d.ts +0 -290
  405. package/dist/esm/routing/index.js +0 -246
  406. package/dist/esm/routing/validators/zod.d.ts +0 -4
  407. package/dist/esm/routing/validators/zod.js +0 -10
  408. package/dist/esm/services/accountsGateway/index.d.ts +0 -92
  409. package/dist/esm/services/accountsGateway/index.js +0 -131
  410. package/dist/esm/services/apiGateway/index.d.ts +0 -68
  411. package/dist/esm/services/apiGateway/index.js +0 -77
  412. package/dist/esm/services/authProvider/browser.d.ts +0 -40
  413. package/dist/esm/services/authProvider/browser.js +0 -151
  414. package/dist/esm/services/authProvider/decryption.d.ts +0 -19
  415. package/dist/esm/services/authProvider/decryption.js +0 -69
  416. package/dist/esm/services/authProvider/index.d.ts +0 -63
  417. package/dist/esm/services/authProvider/index.js +0 -26
  418. package/dist/esm/services/authProvider/subrequest.d.ts +0 -13
  419. package/dist/esm/services/authProvider/subrequest.js +0 -45
  420. package/dist/esm/services/authProvider/utils/decryptAndVerify.d.ts +0 -28
  421. package/dist/esm/services/authProvider/utils/decryptAndVerify.js +0 -85
  422. package/dist/esm/services/authProvider/utils/embeddedAuthProvider.d.ts +0 -26
  423. package/dist/esm/services/authProvider/utils/embeddedAuthProvider.js +0 -40
  424. package/dist/esm/services/authProvider/utils/userRoleValidator.d.ts +0 -13
  425. package/dist/esm/services/authProvider/utils/userRoleValidator.js +0 -33
  426. package/dist/esm/services/authProvider/utils/userSubrequest.d.ts +0 -3
  427. package/dist/esm/services/authProvider/utils/userSubrequest.js +0 -6
  428. package/dist/esm/services/documentStore/dynamoEncoding.d.ts +0 -10
  429. package/dist/esm/services/documentStore/dynamoEncoding.js +0 -45
  430. package/dist/esm/services/documentStore/fileSystemAssert.d.ts +0 -1
  431. package/dist/esm/services/documentStore/fileSystemAssert.js +0 -10
  432. package/dist/esm/services/documentStore/index.d.ts +0 -14
  433. package/dist/esm/services/documentStore/index.js +0 -1
  434. package/dist/esm/services/documentStore/unversioned/dynamodb.d.ts +0 -31
  435. package/dist/esm/services/documentStore/unversioned/dynamodb.js +0 -226
  436. package/dist/esm/services/documentStore/unversioned/file-system.d.ts +0 -32
  437. package/dist/esm/services/documentStore/unversioned/file-system.js +0 -174
  438. package/dist/esm/services/documentStore/unversioned/index.d.ts +0 -2
  439. package/dist/esm/services/documentStore/unversioned/index.js +0 -1
  440. package/dist/esm/services/documentStore/versioned/dynamodb.d.ts +0 -25
  441. package/dist/esm/services/documentStore/versioned/dynamodb.js +0 -139
  442. package/dist/esm/services/documentStore/versioned/file-system.d.ts +0 -25
  443. package/dist/esm/services/documentStore/versioned/file-system.js +0 -69
  444. package/dist/esm/services/documentStore/versioned/index.d.ts +0 -17
  445. package/dist/esm/services/documentStore/versioned/index.js +0 -1
  446. package/dist/esm/services/exercisesGateway/index.d.ts +0 -67
  447. package/dist/esm/services/exercisesGateway/index.js +0 -70
  448. package/dist/esm/services/fileServer/index.d.ts +0 -30
  449. package/dist/esm/services/fileServer/index.js +0 -13
  450. package/dist/esm/services/fileServer/localFileServer.d.ts +0 -13
  451. package/dist/esm/services/fileServer/localFileServer.js +0 -125
  452. package/dist/esm/services/fileServer/s3FileServer.d.ts +0 -14
  453. package/dist/esm/services/fileServer/s3FileServer.js +0 -125
  454. package/dist/esm/services/launchParams/index.d.ts +0 -2
  455. package/dist/esm/services/launchParams/index.js +0 -2
  456. package/dist/esm/services/launchParams/signer.d.ts +0 -23
  457. package/dist/esm/services/launchParams/signer.js +0 -51
  458. package/dist/esm/services/launchParams/verifier.d.ts +0 -21
  459. package/dist/esm/services/launchParams/verifier.js +0 -92
  460. package/dist/esm/services/logger/console.d.ts +0 -4
  461. package/dist/esm/services/logger/index.js +0 -27
  462. package/dist/esm/services/lrsGateway/addStatementDefaultFields.d.ts +0 -5
  463. package/dist/esm/services/lrsGateway/addStatementDefaultFields.js +0 -14
  464. package/dist/esm/services/lrsGateway/attempt-utils.d.ts +0 -70
  465. package/dist/esm/services/lrsGateway/attempt-utils.js +0 -236
  466. package/dist/esm/services/lrsGateway/file-system.d.ts +0 -15
  467. package/dist/esm/services/lrsGateway/file-system.js +0 -110
  468. package/dist/esm/services/lrsGateway/index.d.ts +0 -122
  469. package/dist/esm/services/lrsGateway/index.js +0 -111
  470. package/dist/esm/services/lrsGateway/xapiUtils.d.ts +0 -68
  471. package/dist/esm/services/lrsGateway/xapiUtils.js +0 -99
  472. package/dist/esm/services/postgresConnection/index.d.ts +0 -28
  473. package/dist/esm/services/postgresConnection/index.js +0 -58
  474. package/dist/esm/services/searchProvider/index.d.ts +0 -67
  475. package/dist/esm/services/searchProvider/index.js +0 -1
  476. package/dist/esm/services/searchProvider/memorySearchTheBadWay.d.ts +0 -20
  477. package/dist/esm/services/searchProvider/memorySearchTheBadWay.js +0 -187
  478. package/dist/esm/services/searchProvider/openSearch.d.ts +0 -28
  479. package/dist/esm/services/searchProvider/openSearch.js +0 -150
  480. package/dist/esm/tsconfig.without-specs.esm.tsbuildinfo +0 -1
  481. package/dist/esm/types.js +0 -1
  482. package/script/bin/.init-params-script.bash.swp +0 -0
  483. /package/{script → packages/utils/script}/bin/copy-from-template.bash +0 -0
  484. /package/{script → packages/utils/script}/bin/delete-stack.bash +0 -0
  485. /package/{script → packages/utils/script}/bin/deploy.bash +0 -0
  486. /package/{script → packages/utils/script}/bin/destroy-deployment.bash +0 -0
  487. /package/{script → packages/utils/script}/bin/empty-bucket.bash +0 -0
  488. /package/{script → packages/utils/script}/bin/get-arg.bash +0 -0
  489. /package/{script → packages/utils/script}/bin/get-deployed-environments.bash +0 -0
  490. /package/{script → packages/utils/script}/bin/get-env-param.bash +0 -0
  491. /package/{script → packages/utils/script}/bin/get-kwarg.bash +0 -0
  492. /package/{script → packages/utils/script}/bin/get-stack-param.bash +0 -0
  493. /package/{script → packages/utils/script}/bin/has-flag.bash +0 -0
  494. /package/{script → packages/utils/script}/bin/init-constants-script.bash +0 -0
  495. /package/{script → packages/utils/script}/bin/init-params-script.bash +0 -0
  496. /package/{script → packages/utils/script}/bin/stack-exists.bash +0 -0
  497. /package/{script → packages/utils/script}/bin/update-utils.bash +0 -0
  498. /package/{script → packages/utils/script}/bin/upload-pager-duty-endpoints.bash +0 -0
  499. /package/{script → packages/utils/script}/bin/upload-params.bash +0 -0
  500. /package/{script → packages/utils/script}/bin/which.bash +0 -0
  501. /package/{script → packages/utils/script}/bin-entry.bash +0 -0
  502. /package/{script → packages/utils/script}/build.bash +0 -0
  503. /package/{dist/cjs/index.d.ts → packages/utils/src/index.ts} +0 -0
  504. /package/{dist/cjs/services/launchParams/index.d.ts → packages/utils/src/services/launchParams/index.ts} +0 -0
@@ -0,0 +1,887 @@
1
+ import { v4 as uuid, v5 as uuidV5 } from 'uuid';
2
+ import { AccountsGateway } from '../accountsGateway';
3
+ import { AuthProvider, User } from '../authProvider';
4
+ import { createAttemptStatement, createCompletedStatement } from './attempt-utils';
5
+ import { getAssignmentGrades, getCurrentGrade, getRegistrationAttemptInfo } from './xapiUtils';
6
+ import { LrsGateway } from '.';
7
+
8
+ describe('getRegistrationAttemptInfo', () => {
9
+ let mockGetStatements: jest.SpyInstance;
10
+ let lrs: LrsGateway;
11
+ const mockRegistration = uuidV5('1', uuidV5('platform id', uuidV5.URL));
12
+ const accountUuid = '8cf86c14-20c6-474c-ba44-f4ac932a0a2d';
13
+
14
+ const defaultStatementFields = () => ({
15
+ id: uuid(),
16
+ timestamp: new Date().toISOString(),
17
+ actor: {
18
+ account: {
19
+ homePage: 'https://openstax.org' as const,
20
+ name: accountUuid,
21
+ },
22
+ objectType: 'Agent' as const,
23
+ },
24
+ });
25
+
26
+ beforeEach(() => {
27
+ mockGetStatements = jest.fn();
28
+ lrs = {getAllXapiStatements: mockGetStatements} as unknown as LrsGateway;
29
+ });
30
+
31
+ it('returns empty object when there are no statements', async () => {
32
+ mockGetStatements.mockReturnValue(Promise.resolve([]));
33
+ const infoPerUser = await getRegistrationAttemptInfo(lrs, mockRegistration);
34
+ expect(infoPerUser).toEqual({});
35
+ });
36
+
37
+ it('returns user info when there are statements', async () => {
38
+ const attempt = {
39
+ ...defaultStatementFields(),
40
+ ...createAttemptStatement({iri: 'my cool activity', type: 'cool type', name: 'the activity name'})
41
+ };
42
+ const attemptCompleted = {
43
+ ...defaultStatementFields(),
44
+ ...createCompletedStatement(attempt, {score: {raw: 4, max: 5, min: 0, scaled: 0.8}})
45
+ };
46
+ mockGetStatements.mockReturnValue(Promise.resolve([
47
+ attempt,
48
+ attemptCompleted
49
+ ]));
50
+ const infoPerUser = await getRegistrationAttemptInfo(lrs, mockRegistration);
51
+ const userInfo = infoPerUser[accountUuid];
52
+
53
+ expect(userInfo.attempts).toBe(1);
54
+ expect(userInfo.completedAttempts).toBe(1);
55
+ expect(userInfo.currentAttempt).toBe(attempt);
56
+ expect(userInfo.currentAttemptCompleted).toBe(attemptCompleted);
57
+ });
58
+
59
+ it('gets only the oldest attempt', async () => {
60
+ const attempt = {
61
+ ...defaultStatementFields(),
62
+ ...createAttemptStatement({iri: 'my cool activity', type: 'cool type', name: 'the activity name'})
63
+ };
64
+ const attemptCompleted = {
65
+ ...defaultStatementFields(),
66
+ ...createCompletedStatement(attempt, {score: {raw: 4, max: 5, min: 0, scaled: 0.8}})
67
+ };
68
+
69
+ await new Promise((resolve) => setTimeout(resolve, 10));
70
+
71
+ const attempt2 = {
72
+ ...defaultStatementFields(),
73
+ ...createAttemptStatement({iri: 'my cool activity', type: 'cool type', name: 'the activity name'})
74
+ };
75
+ mockGetStatements.mockReturnValue(Promise.resolve([
76
+ attempt,
77
+ attemptCompleted,
78
+ attempt2
79
+ ]));
80
+ const infoPerUser = await getRegistrationAttemptInfo(lrs, mockRegistration, { currentPreference: 'oldest' });
81
+ const userInfo = infoPerUser[accountUuid];
82
+
83
+ expect(userInfo.attempts).toBe(2);
84
+ expect(userInfo.completedAttempts).toBe(1);
85
+ expect(userInfo.currentAttempt).toBe(attempt);
86
+ expect(userInfo.currentAttemptCompleted).toBe(attemptCompleted);
87
+ });
88
+ });
89
+
90
+ describe('getCurrentGrade', () => {
91
+ let mockGetStatements: jest.SpyInstance;
92
+ let mockGetUser: jest.SpyInstance;
93
+ let services: {
94
+ ltiAuthProvider: AuthProvider;
95
+ lrs: LrsGateway;
96
+ };
97
+ const mockRegistration = uuidV5('1', uuidV5('platform id', uuidV5.URL));
98
+ const accountsUuid = '8cf86c14-20c6-474c-ba44-f4ac932a0a2d';
99
+ const user = { uuid: accountsUuid } as User;
100
+
101
+ const defaultStatementFields = () => ({
102
+ id: uuid(),
103
+ timestamp: new Date().toISOString(),
104
+ actor: {
105
+ account: {
106
+ homePage: 'https://openstax.org' as const,
107
+ name: accountsUuid,
108
+ },
109
+ objectType: 'Agent' as const,
110
+ },
111
+ });
112
+
113
+ beforeEach(() => {
114
+ mockGetStatements = jest.fn();
115
+ mockGetUser = jest.fn();
116
+ services = {
117
+ ltiAuthProvider: {getUser: mockGetUser},
118
+ lrs: {getAllXapiStatements: mockGetStatements},
119
+ } as any;
120
+ });
121
+
122
+ it('returns null when there is no user', async () => {
123
+ mockGetUser.mockReturnValue(Promise.resolve(undefined));
124
+ const grade = await getCurrentGrade(services, mockRegistration);
125
+
126
+ expect(grade).toBeNull();
127
+ });
128
+
129
+ it('gets zero grade when there are no statements', async () => {
130
+ mockGetStatements.mockReturnValue(Promise.resolve([]));
131
+ mockGetUser.mockReturnValue(Promise.resolve(user));
132
+ const grade = await getCurrentGrade(services, mockRegistration);
133
+
134
+ expect(grade).toMatchInlineSnapshot(`
135
+ {
136
+ "grade": {
137
+ "activityProgress": "Started",
138
+ "gradingProgress": "FullyGraded",
139
+ "scoreGiven": 0,
140
+ "scoreMaximum": 100,
141
+ "submission": {},
142
+ "userId": "8cf86c14-20c6-474c-ba44-f4ac932a0a2d",
143
+ },
144
+ "name": undefined,
145
+ "progress": {
146
+ "scaled": 0,
147
+ },
148
+ }
149
+ `);
150
+ });
151
+
152
+ it('gets grade when there are statements', async () => {
153
+ const attempt = {
154
+ ...defaultStatementFields(),
155
+ ...createAttemptStatement({iri: 'my cool activity', type: 'cool type', name: 'the activity name'})
156
+ };
157
+ mockGetStatements.mockReturnValue(Promise.resolve([
158
+ attempt,
159
+ {
160
+ ...defaultStatementFields(),
161
+ ...createCompletedStatement(attempt, {score: {raw: 4, max: 5, min: 0, scaled: 0.8}})
162
+ }
163
+ ]));
164
+ mockGetUser.mockReturnValue(Promise.resolve(user));
165
+ const grade = await getCurrentGrade(services, mockRegistration, { scoreMaximum: 10 });
166
+
167
+ expect(grade).toStrictEqual({
168
+ grade: {
169
+ activityProgress: 'Completed',
170
+ gradingProgress: 'FullyGraded',
171
+ scoreGiven: 8,
172
+ scoreMaximum: 10,
173
+ submission: {
174
+ startedAt: expect.any(String),
175
+ submittedAt: expect.any(String),
176
+ },
177
+ userId: '8cf86c14-20c6-474c-ba44-f4ac932a0a2d'
178
+ },
179
+ name: undefined,
180
+ progress: {
181
+ scaled: 1
182
+ }
183
+ });
184
+ });
185
+
186
+ it('gets default grade when there are no completed statements and no incompleteAttemptCallback', async () => {
187
+ const attempt = {
188
+ ...defaultStatementFields(),
189
+ ...createAttemptStatement({iri: 'my cool activity', type: 'cool type', name: 'the activity name'})
190
+ };
191
+ mockGetStatements.mockReturnValue(Promise.resolve([
192
+ attempt,
193
+ ]));
194
+ mockGetUser.mockReturnValue(Promise.resolve(user));
195
+ const grade = await getCurrentGrade(services, mockRegistration);
196
+
197
+ expect(grade).toStrictEqual({
198
+ grade: {
199
+ activityProgress: 'Started',
200
+ gradingProgress: 'FullyGraded',
201
+ scoreGiven: 0,
202
+ scoreMaximum: 100,
203
+ submission: {
204
+ startedAt: expect.any(String),
205
+ },
206
+ userId: '8cf86c14-20c6-474c-ba44-f4ac932a0a2d'
207
+ },
208
+ name: undefined,
209
+ progress: {
210
+ scaled: 0
211
+ }
212
+ });
213
+ });
214
+
215
+ it('can accept a callback to get incomplete assignments', async () => {
216
+ const userId = 'a3ee0232-06c4-46cd-8d2f-0d7a12788b4a';
217
+ const attempt = {
218
+ ...defaultStatementFields(),
219
+ ...createAttemptStatement({iri: 'my cool activity', type: 'cool type', name: 'the activity name'})
220
+ };
221
+ mockGetStatements.mockReturnValue(Promise.resolve([
222
+ attempt,
223
+ ]));
224
+ mockGetUser.mockReturnValue(Promise.resolve(user));
225
+
226
+ const incompleteAttemptCallback = jest.fn().mockReturnValue(Promise.resolve({
227
+ activityProgress: 'Started',
228
+ gradingProgress: 'FullyGraded',
229
+ scoreGiven: 1,
230
+ scoreMaximum: 5,
231
+ userId,
232
+ }));
233
+ const grade = await getCurrentGrade(services, mockRegistration, { incompleteAttemptCallback, userId });
234
+
235
+ expect(grade).toMatchInlineSnapshot(`
236
+ {
237
+ "activityProgress": "Started",
238
+ "gradingProgress": "FullyGraded",
239
+ "scoreGiven": 1,
240
+ "scoreMaximum": 5,
241
+ "userId": "a3ee0232-06c4-46cd-8d2f-0d7a12788b4a",
242
+ }
243
+ `);
244
+
245
+ expect(incompleteAttemptCallback).toHaveBeenCalledTimes(1);
246
+
247
+ const args = incompleteAttemptCallback.mock.calls[0];
248
+ expect(args).toHaveLength(1);
249
+
250
+ const info = args[0];
251
+
252
+ expect(info.attempts).toBe(1);
253
+ expect(info.completedAttempts).toBe(0);
254
+ });
255
+
256
+ it('gets grade when grade has no min/max/raw values', async () => {
257
+ const attempt = {
258
+ ...defaultStatementFields(),
259
+ ...createAttemptStatement({iri: 'my cool activity', type: 'cool type', name: 'the activity name'})
260
+ };
261
+ mockGetStatements.mockReturnValue(Promise.resolve([
262
+ attempt,
263
+ {
264
+ ...defaultStatementFields(),
265
+ ...createCompletedStatement(attempt, {score: {scaled: 0.8}})
266
+ }
267
+ ]));
268
+ mockGetUser.mockReturnValue(Promise.resolve(user));
269
+ const grade = await getCurrentGrade(services, mockRegistration, { userId: 'user id' });
270
+
271
+ expect(grade).toStrictEqual({
272
+ grade: {
273
+ activityProgress: 'Completed',
274
+ gradingProgress: 'FullyGraded',
275
+ scoreGiven: 80,
276
+ scoreMaximum: 100,
277
+ submission: {
278
+ startedAt: expect.any(String),
279
+ submittedAt: expect.any(String),
280
+ },
281
+ userId: 'user id'
282
+ },
283
+ name: undefined,
284
+ progress: {
285
+ scaled: 1
286
+ }
287
+ });
288
+ });
289
+
290
+ it('gets only the oldest attempt', async () => {
291
+ const attempt = {
292
+ ...defaultStatementFields(),
293
+ ...createAttemptStatement({iri: 'my cool activity', type: 'cool type', name: 'the activity name'})
294
+ };
295
+ const attemptCompleted = {
296
+ ...defaultStatementFields(),
297
+ ...createCompletedStatement(attempt, {score: {raw: 4, max: 5, min: 0, scaled: 0.8}})
298
+ };
299
+
300
+ await new Promise((resolve) => setTimeout(resolve, 10));
301
+
302
+ const attempt2 = {
303
+ ...defaultStatementFields(),
304
+ ...createAttemptStatement({iri: 'my cool activity', type: 'cool type', name: 'the activity name'})
305
+ };
306
+ mockGetStatements.mockReturnValue(Promise.resolve([
307
+ attempt,
308
+ attemptCompleted,
309
+ attempt2
310
+ ]));
311
+ mockGetUser.mockReturnValue(Promise.resolve(user));
312
+ const grade = await getCurrentGrade(
313
+ services, mockRegistration, { currentPreference: 'oldest', userId: 'user id' }
314
+ );
315
+
316
+ expect(grade).toStrictEqual({
317
+ grade: {
318
+ activityProgress: 'Completed',
319
+ gradingProgress: 'FullyGraded',
320
+ scoreGiven: 80,
321
+ scoreMaximum: 100,
322
+ submission: {
323
+ startedAt: expect.any(String),
324
+ submittedAt: expect.any(String),
325
+ },
326
+ userId: 'user id'
327
+ },
328
+ name: undefined,
329
+ progress: {
330
+ scaled: 1
331
+ }
332
+ });
333
+ });
334
+ });
335
+
336
+ describe('getAssignmentGrades', () => {
337
+ let mockMapUserUuids: jest.SpyInstance;
338
+ let mockLogEvent: jest.SpyInstance;
339
+ let mockGetStatements: jest.SpyInstance;
340
+ let services: {
341
+ accountsGateway: AccountsGateway;
342
+ lrs: LrsGateway;
343
+ };
344
+ const platformId = uuid();
345
+ const assignmentIRI = 'https://openstax.org/orn/assignment/e2c1e160-4913-4397-b295-b31b0b4367ec';
346
+ const mockRegistration = uuidV5(uuid(), uuidV5(platformId, uuidV5.URL));
347
+ const studentId1 = 'a3ee0232-06c4-46cd-8d2f-0d7a12788b4a';
348
+ const studentId2 = '74f3254c-cbc6-417f-9442-dc76d877f744';
349
+ const studentId3 = 'e9accd58-f8ad-4132-a8fe-69486f81ae6e';
350
+ const studentUuid1 = '3beb5a53-2628-4c82-8853-5892577cb672';
351
+ const studentUuid2 = 'efe70ccb-9712-424a-912a-51486b2ae138';
352
+ const studentUuid3 = '6aaebe17-d1e2-432a-8506-f6ce6f809bf0';
353
+ const userUuidToPlatformUserIdMap: { [uuid: string]: string } = {
354
+ [studentUuid1]: studentId1,
355
+ [studentUuid2]: studentId2,
356
+ [studentUuid3]: studentId3,
357
+ };
358
+ const userUuidToNameMap: { [uuid: string]: string } = {
359
+ [studentUuid1]: 'Alice',
360
+ [studentUuid2]: 'Bob',
361
+ [studentUuid3]: 'Carol',
362
+ };
363
+
364
+ const defaultStatementFields = (accountName: string) => ({
365
+ id: uuid(),
366
+ timestamp: new Date().toISOString(),
367
+ actor: {
368
+ account: {
369
+ homePage: 'https://openstax.org' as const,
370
+ name: accountName,
371
+ },
372
+ objectType: 'Agent' as const,
373
+ },
374
+ });
375
+
376
+ beforeEach(() => {
377
+ mockMapUserUuids = jest.fn().mockImplementation(<T>(
378
+ userUuidsMap: { [uuid: string]: T }, platformId: string
379
+ ) => Object.entries(userUuidsMap).map(
380
+ ([uuid, data]) => ({
381
+ data,
382
+ fullName: userUuidToNameMap[uuid],
383
+ platformUserId: platformId ? userUuidToPlatformUserIdMap[uuid] : undefined,
384
+ uuid,
385
+ })
386
+ ));
387
+ mockLogEvent = jest.fn();
388
+ mockGetStatements = jest.fn();
389
+ services = {
390
+ accountsGateway: {mapUserUuids: mockMapUserUuids},
391
+ lrs: {getAllXapiStatements: mockGetStatements},
392
+ } as any;
393
+ });
394
+
395
+ it('gets no grades when there are no statements', async () => {
396
+ mockGetStatements.mockReturnValue(Promise.resolve([]));
397
+ const grades = await getAssignmentGrades(services, assignmentIRI, mockRegistration);
398
+
399
+ expect(grades).toEqual([]);
400
+
401
+ expect(mockLogEvent).not.toHaveBeenCalled();
402
+ });
403
+
404
+ it('gets grade when there are statements', async () => {
405
+ const attempt1 = {
406
+ ...defaultStatementFields(studentUuid1),
407
+ ...createAttemptStatement({iri: 'my cool activity', type: 'cool type', name: 'the activity name'})
408
+ };
409
+ const attempt2 = {
410
+ ...defaultStatementFields(studentUuid2),
411
+ ...createAttemptStatement({iri: 'my cool activity', type: 'cool type', name: 'the activity name'})
412
+ };
413
+ const attempt3 = {
414
+ ...defaultStatementFields(studentUuid3),
415
+ ...createAttemptStatement({iri: 'my cool activity', type: 'cool type', name: 'the activity name'})
416
+ };
417
+ mockGetStatements.mockReturnValue(Promise.resolve([
418
+ attempt1,
419
+ {
420
+ ...defaultStatementFields(studentUuid1),
421
+ ...createCompletedStatement(attempt1, {score: {raw: 4, max: 5, min: 0, scaled: 0.8}})
422
+ },
423
+ attempt2,
424
+ {
425
+ ...defaultStatementFields(studentUuid2),
426
+ ...createCompletedStatement(attempt2, {score: {raw: 3, max: 5, min: 0, scaled: 0.6}})
427
+ },
428
+ attempt3,
429
+ ]));
430
+ const grades = await getAssignmentGrades(services, assignmentIRI, mockRegistration);
431
+
432
+ expect(grades).toStrictEqual([
433
+ {
434
+ grade: {
435
+ activityProgress: 'Completed',
436
+ gradingProgress: 'FullyGraded',
437
+ scoreGiven: 80,
438
+ scoreMaximum: 100,
439
+ submission: {
440
+ startedAt: expect.any(String),
441
+ submittedAt: expect.any(String),
442
+ },
443
+ userId: undefined
444
+ },
445
+ name: 'Alice',
446
+ progress: {
447
+ scaled: 1
448
+ }
449
+ },
450
+ {
451
+ grade: {
452
+ activityProgress: 'Completed',
453
+ gradingProgress: 'FullyGraded',
454
+ scoreGiven: 60,
455
+ scoreMaximum: 100,
456
+ submission: {
457
+ startedAt: expect.any(String),
458
+ submittedAt: expect.any(String),
459
+ },
460
+ userId: undefined
461
+ },
462
+ name: 'Bob',
463
+ progress: {
464
+ scaled: 1
465
+ }
466
+ },
467
+ {
468
+ grade: {
469
+ activityProgress: 'Started',
470
+ gradingProgress: 'FullyGraded',
471
+ scoreGiven: 0,
472
+ scoreMaximum: 100,
473
+ submission: {
474
+ startedAt: expect.any(String),
475
+ },
476
+ userId: undefined
477
+ },
478
+ name: 'Carol',
479
+ progress: {
480
+ scaled: 0
481
+ }
482
+ },
483
+ ]);
484
+
485
+ expect(mockLogEvent).not.toHaveBeenCalled();
486
+ });
487
+
488
+ it('can accept a callback to get incomplete assignments', async () => {
489
+ const attempt1 = {
490
+ ...defaultStatementFields(studentUuid1),
491
+ ...createAttemptStatement({iri: 'my cool activity', type: 'cool type', name: 'the activity name'})
492
+ };
493
+ const attempt2 = {
494
+ ...defaultStatementFields(studentUuid2),
495
+ ...createAttemptStatement({iri: 'my cool activity', type: 'cool type', name: 'the activity name'})
496
+ };
497
+ const attempt3 = {
498
+ ...defaultStatementFields(studentUuid3),
499
+ ...createAttemptStatement({iri: 'my cool activity', type: 'cool type', name: 'the activity name'})
500
+ };
501
+ mockGetStatements.mockReturnValue(Promise.resolve([
502
+ attempt1,
503
+ {
504
+ ...defaultStatementFields(studentUuid1),
505
+ ...createCompletedStatement(attempt1, {score: {raw: 4, max: 5, min: 0, scaled: 0.8}})
506
+ },
507
+ attempt2,
508
+ {
509
+ ...defaultStatementFields(studentUuid2),
510
+ ...createCompletedStatement(attempt2, {score: {raw: 3, max: 5, min: 0, scaled: 0.6}})
511
+ },
512
+ attempt3,
513
+ ]));
514
+
515
+ const incompleteAttemptsCallback = jest.fn().mockReturnValue(Promise.resolve([]));
516
+ const grades = await getAssignmentGrades(
517
+ services, assignmentIRI, mockRegistration, { incompleteAttemptsCallback }
518
+ );
519
+
520
+ expect(grades).toStrictEqual([
521
+ {
522
+ grade: {
523
+ activityProgress: 'Completed',
524
+ gradingProgress: 'FullyGraded',
525
+ scoreGiven: 80,
526
+ scoreMaximum: 100,
527
+ submission: {
528
+ startedAt: expect.any(String),
529
+ submittedAt: expect.any(String),
530
+ },
531
+ userId: undefined
532
+ },
533
+ name: 'Alice',
534
+ progress: {
535
+ scaled: 1
536
+ }
537
+ },
538
+ {
539
+ grade: {
540
+ activityProgress: 'Completed',
541
+ gradingProgress: 'FullyGraded',
542
+ scoreGiven: 60,
543
+ scoreMaximum: 100,
544
+ submission: {
545
+ startedAt: expect.any(String),
546
+ submittedAt: expect.any(String),
547
+ },
548
+ userId: undefined
549
+ },
550
+ name: 'Bob',
551
+ progress: {
552
+ scaled: 1
553
+ }
554
+ }
555
+ ]);
556
+
557
+ expect(incompleteAttemptsCallback).toHaveBeenCalledTimes(1);
558
+
559
+ const args = incompleteAttemptsCallback.mock.calls[0];
560
+ expect(args).toHaveLength(1);
561
+
562
+ const mappedInfo = args[0];
563
+ expect(mappedInfo).toHaveLength(1);
564
+
565
+ const { data, fullName, platformUserId, uuid } = mappedInfo[0];
566
+ expect(data.attempts).toBe(1);
567
+ expect(data.completedAttempts).toBe(0);
568
+ expect(fullName).toBe('Carol');
569
+ expect(platformUserId).toBeUndefined();
570
+ expect(uuid).toEqual(studentUuid3);
571
+
572
+ expect(mockLogEvent).not.toHaveBeenCalled();
573
+ });
574
+
575
+ it('gets grade when grade has no min/max/raw values', async () => {
576
+ const attempt1 = {
577
+ ...defaultStatementFields(studentUuid1),
578
+ ...createAttemptStatement({iri: 'my cool activity', type: 'cool type', name: 'the activity name'})
579
+ };
580
+ const attempt2 = {
581
+ ...defaultStatementFields(studentUuid2),
582
+ ...createAttemptStatement({iri: 'my cool activity', type: 'cool type', name: 'the activity name'})
583
+ };
584
+ mockGetStatements.mockReturnValue(Promise.resolve([
585
+ attempt1,
586
+ {
587
+ ...defaultStatementFields(studentUuid1),
588
+ ...createCompletedStatement(attempt1, {score: {scaled: 0.8}})
589
+ },
590
+ attempt2,
591
+ {
592
+ ...defaultStatementFields(studentUuid2),
593
+ ...createCompletedStatement(attempt2, {score: {scaled: 0.6}})
594
+ },
595
+ ]));
596
+ const grades = await getAssignmentGrades(
597
+ services, assignmentIRI, mockRegistration, { platformId, scoreMaximum: 10 }
598
+ );
599
+
600
+ expect(grades).toStrictEqual([
601
+ {
602
+ grade: {
603
+ activityProgress: 'Completed',
604
+ gradingProgress: 'FullyGraded',
605
+ scoreGiven: 8,
606
+ scoreMaximum: 10,
607
+ submission: {
608
+ startedAt: expect.any(String),
609
+ submittedAt: expect.any(String),
610
+ },
611
+ userId: 'a3ee0232-06c4-46cd-8d2f-0d7a12788b4a'
612
+ },
613
+ name: 'Alice',
614
+ progress: {
615
+ scaled: 1
616
+ }
617
+ },
618
+ {
619
+ grade: {
620
+ activityProgress: 'Completed',
621
+ gradingProgress: 'FullyGraded',
622
+ scoreGiven: 6,
623
+ scoreMaximum: 10,
624
+ submission: {
625
+ startedAt: expect.any(String),
626
+ submittedAt: expect.any(String),
627
+ },
628
+ userId: '74f3254c-cbc6-417f-9442-dc76d877f744'
629
+ },
630
+ name: 'Bob',
631
+ progress: {
632
+ scaled: 1
633
+ }
634
+ }
635
+ ]);
636
+
637
+ expect(mockLogEvent).not.toHaveBeenCalled();
638
+ });
639
+
640
+ it('uses best completed attempt when gradePreference is "best" and partitions accordingly', async () => {
641
+ // Alice has two completed attempts. Best is the older
642
+ const aliceOlderAttempt = {
643
+ ...defaultStatementFields(studentUuid1),
644
+ ...createAttemptStatement({ iri: 'my cool activity', type: 'cool type', name: 'the activity name' }),
645
+ };
646
+ const aliceOlderCompleted = {
647
+ ...defaultStatementFields(studentUuid1),
648
+ ...createCompletedStatement(aliceOlderAttempt, { score: { raw: 9, max: 10, min: 0, scaled: 0.9 } }),
649
+ };
650
+ const aliceNewerAttempt = {
651
+ ...defaultStatementFields(studentUuid1),
652
+ ...createAttemptStatement({ iri: 'my cool activity', type: 'cool type', name: 'the activity name' }),
653
+ };
654
+ const aliceNewerCompleted = {
655
+ ...defaultStatementFields(studentUuid1),
656
+ ...createCompletedStatement(aliceNewerAttempt, { score: { raw: 4, max: 10, min: 0, scaled: 0.4 } }),
657
+ };
658
+
659
+ // Bob has an uncompleted attempt
660
+ const bobAttemptOnly = {
661
+ ...defaultStatementFields(studentUuid2),
662
+ ...createAttemptStatement({ iri: 'my cool activity', type: 'cool type', name: 'the activity name' }),
663
+ };
664
+
665
+ // Carol has two completed attempts. The newer one is the best.
666
+ const carolOlderAttempt = {
667
+ ...defaultStatementFields(studentUuid3),
668
+ ...createAttemptStatement({ iri: 'my cool activity', type: 'cool type', name: 'the activity name' }),
669
+ };
670
+ const carolOlderCompleted = {
671
+ ...defaultStatementFields(studentUuid3),
672
+ ...createCompletedStatement(carolOlderAttempt, { score: { scaled: 0.6 } }),
673
+ };
674
+ const carolNewerAttempt = {
675
+ ...defaultStatementFields(studentUuid3),
676
+ ...createAttemptStatement({ iri: 'my cool activity', type: 'cool type', name: 'the activity name' }),
677
+ };
678
+ const carolNewerCompleted = {
679
+ ...defaultStatementFields(studentUuid3),
680
+ ...createCompletedStatement(carolNewerAttempt, { score: { scaled: 0.7 } }),
681
+ };
682
+
683
+ mockGetStatements.mockReturnValue(Promise.resolve([
684
+ aliceOlderAttempt, aliceOlderCompleted,
685
+ bobAttemptOnly,
686
+ carolOlderAttempt, carolOlderCompleted,
687
+ carolNewerAttempt, carolNewerCompleted,
688
+ aliceNewerAttempt, aliceNewerCompleted,
689
+ ]));
690
+
691
+ const incompleteAttemptsCallback = jest.fn().mockResolvedValue([]);
692
+
693
+ const grades = await getAssignmentGrades(
694
+ services,
695
+ assignmentIRI,
696
+ mockRegistration,
697
+ {
698
+ gradePreference: 'best',
699
+ platformId,
700
+ incompleteAttemptsCallback,
701
+ }
702
+ );
703
+
704
+ // Should grade Alice's best 0.9 and Carol's best 0.7
705
+ expect(grades).toStrictEqual([
706
+ {
707
+ grade: {
708
+ activityProgress: 'Completed',
709
+ gradingProgress: 'FullyGraded',
710
+ scoreGiven: 90,
711
+ scoreMaximum: 100,
712
+ submission: {
713
+ startedAt: expect.any(String),
714
+ submittedAt: expect.any(String),
715
+ },
716
+ userId: studentId1,
717
+ },
718
+ name: 'Alice',
719
+ progress: { scaled: 1 },
720
+ },
721
+ {
722
+ grade: {
723
+ activityProgress: 'Completed',
724
+ gradingProgress: 'FullyGraded',
725
+ scoreGiven: 70,
726
+ scoreMaximum: 100,
727
+ submission: {
728
+ startedAt: expect.any(String),
729
+ submittedAt: expect.any(String),
730
+ },
731
+ userId: studentId3,
732
+ },
733
+ name: 'Carol',
734
+ progress: { scaled: 1 },
735
+ },
736
+ ]);
737
+
738
+ // incompleteAttemptsCallback should receive only Bob because he has an incomplete attempt.
739
+ expect(incompleteAttemptsCallback).toHaveBeenCalledTimes(1);
740
+ const [incompleteList] = incompleteAttemptsCallback.mock.calls[0];
741
+ expect(Array.isArray(incompleteList)).toBe(true);
742
+ expect(incompleteList).toHaveLength(1);
743
+ expect(incompleteList[0].uuid).toBe(studentUuid2);
744
+ expect(incompleteList[0].fullName).toBe('Bob');
745
+ });
746
+
747
+ it('uses current attempt when gradePreference is "current" and partitions accordingly', async () => {
748
+ // Alice has two completed attempts. Current is the newer. 0.4 score
749
+ const aliceOlderAttempt = {
750
+ ...defaultStatementFields(studentUuid1),
751
+ ...createAttemptStatement({ iri: 'my cool activity', type: 'cool type', name: 'the activity name' }),
752
+ };
753
+ const aliceOlderCompleted = {
754
+ ...defaultStatementFields(studentUuid1),
755
+ ...createCompletedStatement(aliceOlderAttempt, { score: { raw: 9, max: 10, min: 0, scaled: 0.9 } }),
756
+ };
757
+ const aliceNewerAttempt = {
758
+ ...defaultStatementFields(studentUuid1),
759
+ ...createAttemptStatement({ iri: 'my cool activity', type: 'cool type', name: 'the activity name' }),
760
+ };
761
+ const aliceNewerCompleted = {
762
+ ...defaultStatementFields(studentUuid1),
763
+ ...createCompletedStatement(aliceNewerAttempt, { score: { raw: 4, max: 10, min: 0, scaled: 0.4 } }),
764
+ };
765
+
766
+ // Bob only an uncompleted attempt
767
+ const bobAttemptOnly = {
768
+ ...defaultStatementFields(studentUuid2),
769
+ ...createAttemptStatement({ iri: 'my cool activity', type: 'cool type', name: 'the activity name' }),
770
+ };
771
+
772
+ // Carol has two completed attempts. Current is the newer. 0.7 score
773
+ const carolOlderAttempt = {
774
+ ...defaultStatementFields(studentUuid3),
775
+ ...createAttemptStatement({ iri: 'my cool activity', type: 'cool type', name: 'the activity name' }),
776
+ };
777
+ const carolOlderCompleted = {
778
+ ...defaultStatementFields(studentUuid3),
779
+ ...createCompletedStatement(carolOlderAttempt, { score: { scaled: 0.6 } }),
780
+ };
781
+ const carolNewerAttempt = {
782
+ ...defaultStatementFields(studentUuid3),
783
+ ...createAttemptStatement({ iri: 'my cool activity', type: 'cool type', name: 'the activity name' }),
784
+ };
785
+ const carolNewerCompleted = {
786
+ ...defaultStatementFields(studentUuid3),
787
+ ...createCompletedStatement(carolNewerAttempt, { score: { scaled: 0.7 } }),
788
+ };
789
+
790
+ mockGetStatements.mockReturnValue(Promise.resolve([
791
+ aliceOlderAttempt, aliceOlderCompleted,
792
+ bobAttemptOnly,
793
+ carolOlderAttempt, carolOlderCompleted,
794
+ carolNewerAttempt, carolNewerCompleted,
795
+ aliceNewerAttempt, aliceNewerCompleted,
796
+ ]));
797
+
798
+ const incompleteAttemptsCallback = jest.fn().mockResolvedValue([]);
799
+
800
+ const grades = await getAssignmentGrades(
801
+ services,
802
+ assignmentIRI,
803
+ mockRegistration,
804
+ {
805
+ platformId,
806
+ incompleteAttemptsCallback,
807
+ gradePreference: 'current',
808
+ }
809
+ );
810
+
811
+ // Alice's current should have 0.4. Carol's current should have 0.7
812
+ expect(grades).toStrictEqual([
813
+ {
814
+ grade: {
815
+ activityProgress: 'Completed',
816
+ gradingProgress: 'FullyGraded',
817
+ scoreGiven: 40,
818
+ scoreMaximum: 100,
819
+ submission: {
820
+ startedAt: expect.any(String),
821
+ submittedAt: expect.any(String),
822
+ },
823
+ userId: studentId1,
824
+ },
825
+ name: 'Alice',
826
+ progress: { scaled: 1 },
827
+ },
828
+ {
829
+ grade: {
830
+ activityProgress: 'Completed',
831
+ gradingProgress: 'FullyGraded',
832
+ scoreGiven: 70,
833
+ scoreMaximum: 100,
834
+ submission: {
835
+ startedAt: expect.any(String),
836
+ submittedAt: expect.any(String),
837
+ },
838
+ userId: studentId3,
839
+ },
840
+ name: 'Carol',
841
+ progress: { scaled: 1 },
842
+ },
843
+ ]);
844
+
845
+ // Bob is the only incomplete attempt
846
+ expect(incompleteAttemptsCallback).toHaveBeenCalledTimes(1);
847
+ const [incompleteList] = incompleteAttemptsCallback.mock.calls[0];
848
+ expect(Array.isArray(incompleteList)).toBe(true);
849
+ expect(incompleteList).toHaveLength(1);
850
+ expect(incompleteList[0].uuid).toBe(studentUuid2);
851
+ expect(incompleteList[0].fullName).toBe('Bob');
852
+ });
853
+
854
+ it('uses current if best attempt and completed does not exist when gradePreference is "best"', async () => {
855
+ // Alice has one incomplete attempt but gradePreference is best
856
+
857
+ const aliceAttempt = {
858
+ ...defaultStatementFields(studentUuid1),
859
+ ...createAttemptStatement({ iri: 'my cool activity', type: 'cool type', name: 'the activity name' }),
860
+ };
861
+
862
+ mockGetStatements.mockReturnValue(Promise.resolve([
863
+ aliceAttempt,
864
+ ]));
865
+
866
+ const incompleteAttemptsCallback = jest.fn().mockResolvedValue([]);
867
+
868
+ const grades = await getAssignmentGrades(
869
+ services,
870
+ assignmentIRI,
871
+ mockRegistration,
872
+ {
873
+ gradePreference: 'best',
874
+ platformId,
875
+ incompleteAttemptsCallback,
876
+ }
877
+ );
878
+ expect(grades).toStrictEqual([]);
879
+
880
+ expect(incompleteAttemptsCallback).toHaveBeenCalledTimes(1);
881
+ const [incompleteList] = incompleteAttemptsCallback.mock.calls[0];
882
+ expect(Array.isArray(incompleteList)).toBe(true);
883
+ expect(incompleteList).toHaveLength(1);
884
+ expect(incompleteList[0].uuid).toBe(studentUuid1);
885
+ expect(incompleteList[0].fullName).toBe('Alice');
886
+ });
887
+ });