From f5229ee678cd32a449b32d20953d87868cd6a638 Mon Sep 17 00:00:00 2001 From: Aaro Koivukangas Date: Fri, 28 Aug 2026 11:58:59 +0300 Subject: [PATCH] [UnitTests][Updater] Add basic unit tests to updater --- updater/sis-updater-worker/package-lock.json | 1163 ++++++++++++++++- updater/sis-updater-worker/package.json | 8 +- .../src/updater/updateStudents/attainments.js | 17 +- .../unit/updateStudents/attainments.test.js | 200 +++ .../mapper/courseProviderMapper.test.js | 16 + .../unit/updater/mapper/creditMapper.test.js | 118 ++ .../updater/mapper/enrollmentMapper.test.js | 52 + .../tests/unit/updater/mapper/initTests.js | 22 + .../unit/updater/mapper/isModule.test.js | 18 + .../unit/updater/mapper/mapCourseType.test.js | 14 + .../mapper/mapCurriculumPeriod.test.js | 24 + .../mapper/mapStudyrightExtent.test.js | 14 + .../unit/updater/mapper/mapTeacher.test.js | 18 + .../updater/mapper/sanitizeCourseCode.test.js | 24 + .../unit/updater/mapper/studentMapper.test.js | 82 ++ ...rmRegistrationTypeToEnrollmenttype.test.js | 13 + .../shared/educationTypeToExtentcode.test.js | 18 + .../getCreditTypeCodeFromAttainment.test.js | 27 + .../tests/unit/updater/shared/initTests.js | 22 + updater/sis-updater-worker/vitest.config.js | 14 + 20 files changed, 1879 insertions(+), 5 deletions(-) create mode 100644 updater/sis-updater-worker/tests/unit/updateStudents/attainments.test.js create mode 100644 updater/sis-updater-worker/tests/unit/updater/mapper/courseProviderMapper.test.js create mode 100644 updater/sis-updater-worker/tests/unit/updater/mapper/creditMapper.test.js create mode 100644 updater/sis-updater-worker/tests/unit/updater/mapper/enrollmentMapper.test.js create mode 100644 updater/sis-updater-worker/tests/unit/updater/mapper/initTests.js create mode 100644 updater/sis-updater-worker/tests/unit/updater/mapper/isModule.test.js create mode 100644 updater/sis-updater-worker/tests/unit/updater/mapper/mapCourseType.test.js create mode 100644 updater/sis-updater-worker/tests/unit/updater/mapper/mapCurriculumPeriod.test.js create mode 100644 updater/sis-updater-worker/tests/unit/updater/mapper/mapStudyrightExtent.test.js create mode 100644 updater/sis-updater-worker/tests/unit/updater/mapper/mapTeacher.test.js create mode 100644 updater/sis-updater-worker/tests/unit/updater/mapper/sanitizeCourseCode.test.js create mode 100644 updater/sis-updater-worker/tests/unit/updater/mapper/studentMapper.test.js create mode 100644 updater/sis-updater-worker/tests/unit/updater/mapper/termRegistrationTypeToEnrollmenttype.test.js create mode 100644 updater/sis-updater-worker/tests/unit/updater/shared/educationTypeToExtentcode.test.js create mode 100644 updater/sis-updater-worker/tests/unit/updater/shared/getCreditTypeCodeFromAttainment.test.js create mode 100644 updater/sis-updater-worker/tests/unit/updater/shared/initTests.js create mode 100644 updater/sis-updater-worker/vitest.config.js diff --git a/updater/sis-updater-worker/package-lock.json b/updater/sis-updater-worker/package-lock.json index bc8badeec..d1120f875 100644 --- a/updater/sis-updater-worker/package-lock.json +++ b/updater/sis-updater-worker/package-lock.json @@ -20,7 +20,8 @@ "winston-transport-sentry-node": "^3.0.0" }, "devDependencies": { - "nodemon": "3.1.14" + "nodemon": "3.1.14", + "vitest": "4.1.9" } }, "node_modules/@colors/colors": { @@ -80,6 +81,13 @@ "integrity": "sha512-bYtU8avhGIcje3IhvF9aSjsa5URMZBHnwKtOvXsT4sfYy9gppW11gLPT/9oNqlJZD47yPKveQFTAFWpHjKvUoQ==", "license": "MIT" }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", @@ -1122,6 +1130,16 @@ "@opentelemetry/api": "^1.1.0" } }, + "node_modules/@oxc-project/types": { + "version": "0.146.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.146.0.tgz", + "integrity": "sha512-XC0QsnnhVe7sLIWmYmdPw7x5P0h4W8vUU3Nv1ySgWXtvCz8NizoAEpGXA0sOYoJQV2Rl13LgURAHQ5cI5ILCSA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, "node_modules/@prisma/instrumentation": { "version": "5.22.0", "resolved": "https://registry.npmjs.org/@prisma/instrumentation/-/instrumentation-5.22.0.tgz", @@ -1291,6 +1309,286 @@ "@redis/client": "^1.0.0" } }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.5.tgz", + "integrity": "sha512-DLe/i+l8ynIBY7XEQ191TeZvCoowIGa18R+dIV30GW7DiOtp74i/xX8hs8GUjW5ARV7VZuie3d6AumSmCwbeRA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.5.tgz", + "integrity": "sha512-zXcwKlQApYAOELHd8PwKDFkagYF9Wy4e0RJ+0qnzl9Pjnpj75TEG8ufv40p2J7kCEfwZAsNiuzRIyNNMWT38ig==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.5.tgz", + "integrity": "sha512-dK4QakI42nzWgJT5sm4y4y/O//D4OxM75/cH28RLV+nzIN9AY+YsbuUVrUTjlLjXR6vpyxFbSsbmNuJ6BP9sww==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.5.tgz", + "integrity": "sha512-fqSALaUu1Wjd1nK2uW2kJDWdLCc8lx1IcY+MTY26Aurfdx19anlzhqXOgCFbBFQnlFDTn4TC1/7Nz4Bl2mLP3A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.5.tgz", + "integrity": "sha512-/vCnNxlkxs9tKxNDcyWUePpJ/PgTzxIaVhoM5SmG8UV+GR/IcPam4VYxi7GIMo7PSDuNqlJqvprqii9NqqVCMw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.5.tgz", + "integrity": "sha512-abk0NLA519LxRCszmbE0jYKuQ9YPocOXTiOXOo6Yr+YAT95VH+PtqYAjOJvGKt3viEd/x4qzabAlwd5bHOOARg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.5.tgz", + "integrity": "sha512-Y7eALiJ8lr0M2HH103Js+g7V34wf6snlpZLAsHI90uLhr3PVlNsbFVAXJC9d/V6BnPyKtpSwI+NcB/RLxsQxuA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.5.tgz", + "integrity": "sha512-xMvZgnbZg4YVnR/AX2b3oOPDTFYJvUVaJg5FedA/LuvexAtXibZQej4cnTkw3rjsJ/ggUROB64TdtETiim+FYA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.5.tgz", + "integrity": "sha512-GRjeqTUDHTo5GwntsLaAMcBahG3nlpjftXWZLN73HiYQlhwEowvarFgQnRnQZtIp4keXX7quXFbG38uPZBa2EA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.5.tgz", + "integrity": "sha512-vLNTR45F2Uwc8AufkNXPmB4VliaXs+FvcheEogIzOXzO4l+LzieXF5A/TWxLy5HtqpsRCHUfd0lPVrrdgXdLHQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.5.tgz", + "integrity": "sha512-Mgj59/HTuYeK9Gz2MA+mBWKnHsAgkBSec15ZMb1st3oIfFbX7gCjOae7GydHhzcyQi9Z/7M1QuN9bR3oFqF0jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.5.tgz", + "integrity": "sha512-mY8AP0/ichsbhAxGnLa3d3+MwV0EfgrPND2bplI3Ym8T6R2pJ0N87bvrKVwNXmdy3jnr6eQBecdqx/HMknBmpA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.5.tgz", + "integrity": "sha512-8SLssA2oweAxyRgDp789ACfRb/3P+zNRJpzZxSizxF9m8NUDQ4+3xjo8ttjhVGGw6Qxb70oZiEtIjaKikCO7Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.5.tgz", + "integrity": "sha512-vGbruD5zquhoc8D9SViXgN2FBJtNdTyQ4DtG+SWiEGlJiAzoKcZ2xp+xuXCffhubVdt0NJlTZqkeRuERy7g8Cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.5.tgz", + "integrity": "sha512-e/SXpgISz+IoqVcSSI0rx/d/he8zqLex+/rCWpnHpmVfmPIUjag9H6P7zotf0gJHwPUhQxZ/mF8tr6acebT9yw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, "node_modules/@rushstack/node-core-library": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-4.0.2.tgz", @@ -1446,6 +1744,13 @@ "text-hex": "1.0.x" } }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", @@ -1462,6 +1767,17 @@ "integrity": "sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==", "license": "MIT" }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, "node_modules/@types/connect": { "version": "3.4.36", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.36.tgz", @@ -1481,6 +1797,20 @@ "@types/ms": "*" } }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/ms": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", @@ -1557,6 +1887,119 @@ "integrity": "sha512-7bcUmDyS6PN3EuD9SlGGOxM77F8WLVsrwkxyWxKnxzmXoequ6c7741QBrANq6htVRGOITJ7z72mTP6Z4XyuG+Q==", "license": "MIT" }, + "node_modules/@vitest/expect": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz", + "integrity": "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.9.tgz", + "integrity": "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.9.tgz", + "integrity": "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.9.tgz", + "integrity": "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.9", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.9.tgz", + "integrity": "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.9", + "@vitest/utils": "4.1.9", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.9.tgz", + "integrity": "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.9.tgz", + "integrity": "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.9", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -1603,6 +2046,16 @@ "sprintf-js": "~1.0.2" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/async": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", @@ -1694,6 +2147,16 @@ "uuid": "^9.0.0" } }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -1796,6 +2259,13 @@ "node": ">=14" } }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/cron-parser": { "version": "4.9.0", "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz", @@ -1838,8 +2308,8 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "devOptional": true, "license": "Apache-2.0", - "optional": true, "engines": { "node": ">=8" } @@ -1868,6 +2338,13 @@ "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", "license": "MIT" }, + "node_modules/es-module-lexer": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "dev": true, + "license": "MIT" + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -1886,6 +2363,26 @@ "node": ">=6" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/fecha": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", @@ -2280,6 +2777,279 @@ "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", "license": "MIT" }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", @@ -2362,6 +3132,16 @@ "node": ">=12" } }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/minimatch": { "version": "10.2.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", @@ -2443,6 +3223,25 @@ "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" } }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/node-abort-controller": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", @@ -2526,6 +3325,20 @@ "node": ">=0.10.0" } }, + "node_modules/obug": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/one-time": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", @@ -2541,6 +3354,13 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "license": "MIT" }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/pg": { "version": "8.20.0", "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz", @@ -2636,6 +3456,13 @@ "split2": "^4.1.0" } }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -2658,6 +3485,35 @@ "node": ">=12.0.0" } }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/postgres-array": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", @@ -2863,6 +3719,40 @@ "integrity": "sha512-hMD7odLOt3LkTjcif8aRZqi/hybjpLNgSk5oF5FCowfCjok6LukpN2bDX7R5wDmbgBQFn7YoBxSagmtXHaJYJw==", "license": "MIT" }, + "node_modules/rolldown": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.5.tgz", + "integrity": "sha512-VD2IE5PUG4Oj8zz2VGykiYd5wbnjdIiSsNQb8Qu5B+noEp+A78mu2iVvpp27g8es14Tk9rofNs5Tku9iQCS4fA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.146.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.5", + "@rolldown/binding-android-arm64": "1.2.5", + "@rolldown/binding-darwin-arm64": "1.2.5", + "@rolldown/binding-darwin-x64": "1.2.5", + "@rolldown/binding-freebsd-x64": "1.2.5", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.5", + "@rolldown/binding-linux-arm64-gnu": "1.2.5", + "@rolldown/binding-linux-arm64-musl": "1.2.5", + "@rolldown/binding-linux-ppc64-gnu": "1.2.5", + "@rolldown/binding-linux-s390x-gnu": "1.2.5", + "@rolldown/binding-linux-x64-gnu": "1.2.5", + "@rolldown/binding-linux-x64-musl": "1.2.5", + "@rolldown/binding-openharmony-arm64": "1.2.5", + "@rolldown/binding-win32-arm64-msvc": "1.2.5", + "@rolldown/binding-win32-x64-msvc": "1.2.5" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -2991,6 +3881,13 @@ "license": "BSD-2-Clause", "peer": true }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", @@ -3038,6 +3935,16 @@ "@napi-rs/snappy-win32-x64-msvc": "7.3.3" } }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", @@ -3062,12 +3969,26 @@ "node": "*" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, "node_modules/standard-as-callback": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", "license": "MIT" }, + "node_modules/std-env": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "dev": true, + "license": "MIT" + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -3137,6 +4058,23 @@ "node": ">=8" } }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz", + "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.17", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", @@ -3182,6 +4120,16 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/tinyrainbow": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", + "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -3310,6 +4258,217 @@ "node": ">= 0.10" } }, + "node_modules/vite": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.2.tgz", + "integrity": "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.26", + "rolldown": "~1.2.4", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.4.0 || ^0.5.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.9.tgz", + "integrity": "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.9", + "@vitest/mocker": "4.1.9", + "@vitest/pretty-format": "4.1.9", + "@vitest/runner": "4.1.9", + "@vitest/snapshot": "4.1.9", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.9", + "@vitest/browser-preview": "4.1.9", + "@vitest/browser-webdriverio": "4.1.9", + "@vitest/coverage-istanbul": "4.1.9", + "@vitest/coverage-v8": "4.1.9", + "@vitest/ui": "4.1.9", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/winston": { "version": "3.19.0", "resolved": "https://registry.npmjs.org/winston/-/winston-3.19.0.tgz", diff --git a/updater/sis-updater-worker/package.json b/updater/sis-updater-worker/package.json index d2a88e6a0..97eeec249 100644 --- a/updater/sis-updater-worker/package.json +++ b/updater/sis-updater-worker/package.json @@ -4,7 +4,10 @@ "type": "module", "scripts": { "start": "node ./src/index.js", - "dev": "nodemon ./src/index.js" + "dev": "nodemon ./src/index.js", + "test": "vitest run", + "test:unit": "vitest run ./tests/unit", + "test:watch": "vitest" }, "dependencies": { "async": "^3.2.6", @@ -21,6 +24,7 @@ "winston-transport-sentry-node": "^3.0.0" }, "devDependencies": { - "nodemon": "3.1.14" + "nodemon": "3.1.14", + "vitest": "4.1.9" } } diff --git a/updater/sis-updater-worker/src/updater/updateStudents/attainments.js b/updater/sis-updater-worker/src/updater/updateStudents/attainments.js index abe8905f6..8b0e18930 100644 --- a/updater/sis-updater-worker/src/updater/updateStudents/attainments.js +++ b/updater/sis-updater-worker/src/updater/updateStudents/attainments.js @@ -12,6 +12,9 @@ import { isModule, } from '../mapper.js' +const earlierOf = (a, b) => (new Date(a) < new Date(b) ? a : b) +const laterOf = (a, b) => (new Date(a) > new Date(b) ? a : b) + const updateTeachers = async attainments => { const acceptorPersonIds = flatten( attainments.map(attainment => @@ -184,6 +187,18 @@ export const updateAttainments = async ( coursecode: parsedCourseCode, }, }) + } else { + // If the course exists, update it + coursesToBeCreated.set(parsedCourseCode, { + ...course, + groupId: course.groupId ?? course.id, + name: att.name, + isPrimary: course.isPrimary ?? true, + coursetypecode: att.study_level_urn, + courseUnitType: att.course_unit_type_urn, + minAttainmentDate: earlierOf(course.minAttainmentDate, att.attainment_date), + maxAttainmentDate: laterOf(course.maxAttainmentDate, att.attainment_date), + }) } const courseIdToUse = attIdToCourseCode[att.id] @@ -204,7 +219,6 @@ export const updateAttainments = async ( } courseUnit = course || { id: courseIdToUse, code: courseIdToUse } - courseUnit.group_id = courseUnit.id } // Add the CU to the mapping objects for creditMapper to work properly. @@ -230,6 +244,7 @@ export const updateAttainments = async ( const attainmentIdCourseCodeMapForCustomCourseUnitAttainments = attainments.reduce(findMissingCourseCodes, {}) const missingCodes = Object.values(attainmentIdCourseCodeMapForCustomCourseUnitAttainments) const courses = await selectFromByIds('course_units', missingCodes, 'code') + return await Promise.all( attainments.map( addCourseUnitToCustomCourseUnitAttainments(courses, attainmentIdCourseCodeMapForCustomCourseUnitAttainments) diff --git a/updater/sis-updater-worker/tests/unit/updateStudents/attainments.test.js b/updater/sis-updater-worker/tests/unit/updateStudents/attainments.test.js new file mode 100644 index 000000000..cfee05d7c --- /dev/null +++ b/updater/sis-updater-worker/tests/unit/updateStudents/attainments.test.js @@ -0,0 +1,200 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +// updateAttainments talks directly to knex, Sequelize models, and the db/index.js helpers. This +// establishes the pattern for testing this kind of DB-coupled orchestrator: mock the DB layer at +// its boundary and let the real (pure) mapping logic run on top of canned data. +// +// vi.hoisted is required because vi.mock factories run before the rest of this file, so any mock +// state they reference (and that assertions below need to inspect) has to be created up front. +const { knexMock, bulkCreateMock, selectFromByIdsMock, selectOneByIdMock } = vi.hoisted(() => { + const makeChainableQuery = result => { + const builder = { + select: () => builder, + from: () => builder, + where: () => builder, + whereIn: () => builder, + then: (resolve, reject) => Promise.resolve(result).then(resolve, reject), + } + return builder + } + + const tableResults = { + course_units: [{ id: 'cu1', code: 'TKT10' }], + modules: [], + } + + const knexMock = Object.assign( + vi.fn(table => makeChainableQuery(tableResults[table] ?? [])), + { select: () => makeChainableQuery([]) } + ) + + return { + knexMock, + bulkCreateMock: vi.fn(), + selectFromByIdsMock: vi.fn().mockResolvedValue([]), + selectOneByIdMock: vi.fn().mockResolvedValue(null), + } +}) + +vi.mock('@/db/connection.js', () => ({ dbConnections: { knex: knexMock } })) + +vi.mock('@/db/index.js', () => ({ + selectFromByIds: selectFromByIdsMock, + selectOneById: selectOneByIdMock, + bulkCreate: bulkCreateMock, +})) + +vi.mock('@/db/models/index.js', () => ({ + Course: { name: 'Course', findOne: vi.fn() }, + Teacher: { name: 'Teacher', findOne: vi.fn() }, + Credit: { name: 'Credit' }, + CreditTeacher: { name: 'CreditTeacher' }, + CourseProvider: { name: 'CourseProvider', findOne: vi.fn() }, +})) + +vi.mock('@/utils/logger.js', () => ({ + default: { error: vi.fn(), info: vi.fn(), debug: vi.fn(), warn: vi.fn() }, +})) + +// updateAttainments builds its credit rows via mapper.js's real creditMapper, which in turn reads +// from shared.js's Redis-backed lookup maps - mocked here the same way as in mapper.test.js. +vi.mock('@/updater/shared.js', () => ({ + getSemesterByDate: vi.fn(() => ({ semestercode: 100, composite: '2020-2021-fall' })), + getGrade: vi.fn(() => ({ value: '5', passed: true })), + getUniOrgId: vi.fn(() => 'uni-org1'), + getCountry: vi.fn(), + getCreditTypeCodeFromAttainment: vi.fn(() => 'PASSED'), + educationTypeToExtentcode: {}, +})) + +import { Course } from '@/db/models/index.js' +import { updateAttainments } from '@/updater/updateStudents/attainments.js' + +describe('updateAttainments', () => { + beforeEach(() => { + bulkCreateMock.mockClear() + }) + + /** Get what calls were directed to the wanted model by bulkCreate */ + const findBulkCreateCall = modelName => bulkCreateMock.mock.calls.find(([model]) => model.name === modelName) + + it('maps a plain course unit attainment into a Credit bulkCreate call', async () => { + const attainment = { + id: 'att1', + type: 'CourseUnitAttainment', + misregistration: false, + credits: 5, + person_id: 'p1', + registration_date: '2020-01-10', + grade_scale_id: 'scale1', + grade_id: 'grade1', + organisations: [{ organisationId: 'org1', roleUrn: 'urn:code:organisation-role:responsible-organisation' }], + attainment_date: '2020-01-15', + course_unit_id: 'cu1', + module_id: null, + module_group_id: null, + study_right_id: null, + attainment_language_urn: 'urn:code:language:fi', + acceptor_persons: [], + } + + await updateAttainments([attainment], { p1: '014123456' }, new Set(), {}) + + const [, credits] = findBulkCreateCall('Credit') + expect(credits).toHaveLength(1) + expect(credits[0]).toMatchObject({ + id: 'att1', + student_studentnumber: '014123456', + course_id: 'cu1', + course_code: 'TKT10', + credits: 5, + is_open: false, + }) + + // No custom-attainment repairs or acceptor persons were involved, so these come back empty. + expect(findBulkCreateCall('Teacher')[1]).toEqual([]) + expect(findBulkCreateCall('Course')[1]).toEqual([]) + expect(findBulkCreateCall('CreditTeacher')[1]).toEqual([]) + expect(findBulkCreateCall('CourseProvider')[1]).toEqual([]) + }) + + it('creates new custom course unit from an attainment', async () => { + const attainment = { + id: 'att-custom-1', + type: 'CustomCourseUnitAttainment', + code: 'XYZ-123', + misregistration: false, + credits: 5, + person_id: 'p1', + registration_date: '2020-01-10', + grade_scale_id: 'scale1', + grade_id: 'grade1', + organisations: [{ organisationId: 'org1', roleUrn: 'urn:code:organisation-role:responsible-organisation' }], + attainment_date: '2020-01-15', + course_unit_id: null, + module_id: null, + module_group_id: null, + study_right_id: null, + attainment_language_urn: 'urn:code:language:fi', + acceptor_persons: [], + name: { fi: 'Testikurssi' }, + study_level_urn: 'lvl1', + course_unit_type_urn: 'type1', + } + + await updateAttainments([attainment], { p1: '014123456' }, new Set(), {}) + + const [, courses] = findBulkCreateCall('Course') + expect(courses).toHaveLength(1) + expect(courses[0].groupId).toBe(courses[0].id) + }) + + it('updates custom course unit with fields from a new attainment', async () => { + Course.findOne.mockResolvedValueOnce({ + id: 'XYZ-123', + groupId: null, // Group id missing + code: 'XYZ-123', + name: { fi: 'Vanha nimi' }, // Incorrect name + isStudyModule: false, + isPrimary: true, + coursetypecode: 'lvl1', + maxAttainmentDate: '2019-06-01', + minAttainmentDate: '2019-01-01', + substitutionGroups: [], + courseUnitType: 'type1', + }) + + const attainment = { + id: 'att-custom-2', + type: 'CustomCourseUnitAttainment', + code: 'XYZ-123', + misregistration: false, + credits: 5, + person_id: 'p1', + registration_date: '2020-01-10', + grade_scale_id: 'scale1', + grade_id: 'grade1', + organisations: [{ organisationId: 'org1', roleUrn: 'urn:code:organisation-role:responsible-organisation' }], + attainment_date: '2020-01-15', + course_unit_id: null, + module_id: null, + module_group_id: null, + study_right_id: null, + attainment_language_urn: 'urn:code:language:fi', + acceptor_persons: [], + name: { fi: 'Uusi nimi' }, + study_level_urn: 'lvl1', + course_unit_type_urn: 'type1', + } + + await updateAttainments([attainment], { p1: '014123456' }, new Set(), {}) + + const [, courses] = findBulkCreateCall('Course') + expect(courses).toHaveLength(1) + expect(courses[0].id).toBe('XYZ-123') + expect(courses[0].groupId).toBe('XYZ-123') + expect(courses[0].name.fi).toBe('Uusi nimi') + expect(courses[0].maxAttainmentDate).toBe('2020-01-15') + expect(courses[0].minAttainmentDate).toBe('2019-01-01') + }) +}) diff --git a/updater/sis-updater-worker/tests/unit/updater/mapper/courseProviderMapper.test.js b/updater/sis-updater-worker/tests/unit/updater/mapper/courseProviderMapper.test.js new file mode 100644 index 000000000..371f41c46 --- /dev/null +++ b/updater/sis-updater-worker/tests/unit/updater/mapper/courseProviderMapper.test.js @@ -0,0 +1,16 @@ +import { describe, expect, it } from 'vitest' + +import './initTests.js' + +import { courseProviderMapper } from '@/updater/mapper.js' + +describe('courseProviderMapper', () => { + it('maps organisation shares to a course provider row', () => { + const mapProvider = courseProviderMapper('CG1') + expect(mapProvider({ organisationId: 'org1', shares: 50 })).toEqual({ + coursecode: 'CG1', + organizationcode: 'org1', + shares: 50, + }) + }) +}) diff --git a/updater/sis-updater-worker/tests/unit/updater/mapper/creditMapper.test.js b/updater/sis-updater-worker/tests/unit/updater/mapper/creditMapper.test.js new file mode 100644 index 000000000..47cd95ad0 --- /dev/null +++ b/updater/sis-updater-worker/tests/unit/updater/mapper/creditMapper.test.js @@ -0,0 +1,118 @@ +import { describe, expect, it } from 'vitest' + +import './initTests.js' + +import { creditMapper } from '@/updater/mapper.js' +import { getCreditTypeCodeFromAttainment, getGrade, getSemesterByDate, getUniOrgId } from '@/updater/shared.js' + +describe('creditMapper', () => { + const baseAttainment = { + id: 'att1', + credits: 5, + person_id: 'p1', + registration_date: '2020-01-10', + grade_scale_id: 'scale1', + grade_id: 'grade1', + organisations: [{ organisationId: 'org1', roleUrn: 'urn:code:organisation-role:responsible-organisation' }], + attainment_date: '2020-01-15', + type: 'CourseUnitAttainment', + course_unit_id: 'cu1', + module_id: null, + module_group_id: null, + study_right_id: null, + attainment_language_urn: 'urn:code:language:fi', + } + + const lookupMaps = (overrides = {}) => ({ + personIdToStudentNumber: { p1: '014123456' }, + courseUnitIdToCourseCode: { cu1: 'TKT10' }, + moduleGroupIdToModuleCode: {}, + studyRightIdToEducationType: {}, + ...overrides, + }) + + const mapCredit = maps => + creditMapper( + maps.personIdToStudentNumber, + maps.courseUnitIdToCourseCode, + maps.moduleGroupIdToModuleCode, + maps.studyRightIdToEducationType + ) + + it('maps attainment correctly into credit', () => { + getUniOrgId.mockReturnValue('uni-org1') + getSemesterByDate.mockReturnValue({ semestercode: 100, composite: '2020-2021-fall' }) + getGrade.mockReturnValue({ value: '5', passed: true }) + getCreditTypeCodeFromAttainment.mockReturnValue('PASSED') + + const result = mapCredit(lookupMaps())(baseAttainment) + + expect(result).toEqual({ + id: 'att1', + grade: '5', + student_studentnumber: '014123456', + credits: 5, + createdate: '2020-01-10', + credittypecode: 'PASSED', + attainment_date: '2020-01-15', + course_id: 'cu1', + course_code: 'TKT10', + semestercode: 100, + semester_composite: '2020-2021-fall', + isStudyModule: false, + org: 'uni-org1', + language: 'fi', + is_open: false, + studyright_id: null, + }) + }) + + it('returns null when there is no target semester for the attainment date', () => { + getUniOrgId.mockReturnValue('uni-org1') + getSemesterByDate.mockReturnValue(undefined) + + expect(mapCredit(lookupMaps())(baseAttainment)).toBeNull() + }) + + it('returns null when the course code cannot be resolved', () => { + getUniOrgId.mockReturnValue('uni-org1') + getSemesterByDate.mockReturnValue({ semestercode: 100, composite: '2020-2021-fall' }) + + const result = mapCredit(lookupMaps({ courseUnitIdToCourseCode: {} }))(baseAttainment) + + expect(result).toBeNull() + }) + + it('marks open university attainments via the AY course code prefix', () => { + getUniOrgId.mockReturnValue('uni-org1') + getSemesterByDate.mockReturnValue({ semestercode: 100, composite: '2020-2021-fall' }) + getGrade.mockReturnValue({ value: '5', passed: true }) + getCreditTypeCodeFromAttainment.mockReturnValue('PASSED') + + const result = mapCredit(lookupMaps({ courseUnitIdToCourseCode: { cu1: 'AY123' } }))(baseAttainment) + + expect(result.is_open).toBe(true) + }) + + it('marks open university attainments via the study right education type', () => { + getUniOrgId.mockReturnValue('uni-org1') + getSemesterByDate.mockReturnValue({ semestercode: 100, composite: '2020-2021-fall' }) + getGrade.mockReturnValue({ value: '5', passed: true }) + getCreditTypeCodeFromAttainment.mockReturnValue('PASSED') + + const attainment = { ...baseAttainment, study_right_id: 'sr1' } + const result = mapCredit( + lookupMaps({ + studyRightIdToEducationType: { sr1: 'urn:code:education-type:non-degree-education:open-university-studies' }, + }) + )(attainment) + + expect(result.is_open).toBe(true) + }) + + it('returns null with missing organization', () => { + const attainment = { ...baseAttainment, organisations: undefined } + + expect(mapCredit(lookupMaps())(attainment)).toBeNull() + }) +}) diff --git a/updater/sis-updater-worker/tests/unit/updater/mapper/enrollmentMapper.test.js b/updater/sis-updater-worker/tests/unit/updater/mapper/enrollmentMapper.test.js new file mode 100644 index 000000000..9bd98cf83 --- /dev/null +++ b/updater/sis-updater-worker/tests/unit/updater/mapper/enrollmentMapper.test.js @@ -0,0 +1,52 @@ +import { describe, expect, it } from 'vitest' + +import './initTests.js' + +import { enrollmentMapper } from '@/updater/mapper.js' +import { getSemesterByDate } from '@/updater/shared.js' + +describe('enrollmentMapper', () => { + const baseEnrollment = { + id: 'e1', + person_id: 'p1', + course_unit_id: 'cu1', + course_unit_realisation_id: 'real1', + enrolment_date_time: '2020-01-01T00:00:00Z', + state: 'ACTIVE', + study_right_id: null, + } + + const mapEnrollment = (overrides = {}) => + enrollmentMapper( + overrides.personIdToStudentNumber ?? { p1: '014111111' }, + overrides.courseUnitIdToCourseUnit ?? { cu1: { code: 'TKT10', group_id: 'grp1' } }, + overrides.realisationIdToActivityPeriod ?? {}, + overrides.studyRightIdToEducationType ?? {} + ) + + it('returns null when the student number cannot be resolved', () => { + expect(mapEnrollment({ personIdToStudentNumber: {} })(baseEnrollment)).toBeNull() + }) + + it('returns null when the course unit cannot be resolved', () => { + expect(mapEnrollment({ courseUnitIdToCourseUnit: {} })(baseEnrollment)).toBeNull() + }) + + it('maps an enrollment correctly', () => { + getSemesterByDate.mockReturnValue({ semestercode: 5, composite: 'sem5' }) + + expect(mapEnrollment()(baseEnrollment)).toEqual({ + id: 'e1', + studentnumber: '014111111', + state: 'ACTIVE', + course_code: 'TKT10', + semestercode: 5, + semester_composite: 'sem5', + enrollment_date_time: '2020-01-01T00:00:00Z', + is_open: false, + course_id: 'cu1', + course_group_id: 'grp1', + studyright_id: null, + }) + }) +}) diff --git a/updater/sis-updater-worker/tests/unit/updater/mapper/initTests.js b/updater/sis-updater-worker/tests/unit/updater/mapper/initTests.js new file mode 100644 index 000000000..ad9b5098a --- /dev/null +++ b/updater/sis-updater-worker/tests/unit/updater/mapper/initTests.js @@ -0,0 +1,22 @@ +import { vi } from 'vitest' + +// mapper.js imports logger.js and shared.js at module load time. shared.js transitively +// connects to Redis and instantiates Sequelize models as a side effect of being imported, so both +// are mocked here to keep these unit tests pure and fast. +// +// NOTE: Import this module first - before importing anything from mapper.js - in every test file in +// this directory. Tests that need per-case control over a shared.js lookup (e.g. creditMapper, +// enrollmentMapper) can still `import { getSemesterByDate, ... } from '.../updater/shared.js'` +// afterwards and call `.mockReturnValue(...)` on it - it's the same mocked function instance. +vi.mock('@/utils/logger.js', () => ({ + default: { error: vi.fn(), info: vi.fn(), debug: vi.fn(), warn: vi.fn() }, +})) + +vi.mock('@/updater/shared.js', () => ({ + getSemesterByDate: vi.fn(), + getGrade: vi.fn(), + getUniOrgId: vi.fn(), + getCountry: vi.fn(), + getCreditTypeCodeFromAttainment: vi.fn(), + educationTypeToExtentcode: { 'edu-type-1': 1 }, +})) diff --git a/updater/sis-updater-worker/tests/unit/updater/mapper/isModule.test.js b/updater/sis-updater-worker/tests/unit/updater/mapper/isModule.test.js new file mode 100644 index 000000000..f59a5aaf2 --- /dev/null +++ b/updater/sis-updater-worker/tests/unit/updater/mapper/isModule.test.js @@ -0,0 +1,18 @@ +import { describe, expect, it } from 'vitest' + +import './initTests.js' + +import { isModule } from '@/updater/mapper.js' + +describe('isModule', () => { + it('is true for module attainment types', () => { + expect(isModule('ModuleAttainment')).toBe(true) + expect(isModule('CustomModuleAttainment')).toBe(true) + expect(isModule('DegreeProgrammeAttainment')).toBe(true) + }) + + it('is false for course unit attainment types', () => { + expect(isModule('CourseUnitAttainment')).toBe(false) + expect(isModule('CustomCourseUnitAttainment')).toBe(false) + }) +}) diff --git a/updater/sis-updater-worker/tests/unit/updater/mapper/mapCourseType.test.js b/updater/sis-updater-worker/tests/unit/updater/mapper/mapCourseType.test.js new file mode 100644 index 000000000..ec51a88d6 --- /dev/null +++ b/updater/sis-updater-worker/tests/unit/updater/mapper/mapCourseType.test.js @@ -0,0 +1,14 @@ +import { describe, expect, it } from 'vitest' + +import './initTests.js' + +import { mapCourseType } from '@/updater/mapper.js' + +describe('mapCourseType', () => { + it('maps a study level to a course type row', () => { + expect(mapCourseType({ id: 'lvl1', name: { fi: 'Taso' } })).toEqual({ + coursetypecode: 'lvl1', + name: { fi: 'Taso' }, + }) + }) +}) diff --git a/updater/sis-updater-worker/tests/unit/updater/mapper/mapCurriculumPeriod.test.js b/updater/sis-updater-worker/tests/unit/updater/mapper/mapCurriculumPeriod.test.js new file mode 100644 index 000000000..27df1ba49 --- /dev/null +++ b/updater/sis-updater-worker/tests/unit/updater/mapper/mapCurriculumPeriod.test.js @@ -0,0 +1,24 @@ +import { describe, expect, it } from 'vitest' + +import './initTests.js' + +import { mapCurriculumPeriod } from '@/updater/mapper.js' + +describe('mapCurriculumPeriod', () => { + it('maps a curriculum period correctly', () => { + expect( + mapCurriculumPeriod({ + id: 'cp1', + name: { fi: 'Kausi' }, + university_org_id: 'org1', + active_period: { startDate: '2020-01-01', endDate: '2020-12-31' }, + }) + ).toEqual({ + id: 'cp1', + name: { fi: 'Kausi' }, + universityOrgId: 'org1', + startDate: new Date('2020-01-01'), + endDate: new Date('2020-12-31'), + }) + }) +}) diff --git a/updater/sis-updater-worker/tests/unit/updater/mapper/mapStudyrightExtent.test.js b/updater/sis-updater-worker/tests/unit/updater/mapper/mapStudyrightExtent.test.js new file mode 100644 index 000000000..63f9586f1 --- /dev/null +++ b/updater/sis-updater-worker/tests/unit/updater/mapper/mapStudyrightExtent.test.js @@ -0,0 +1,14 @@ +import { describe, expect, it } from 'vitest' + +import './initTests.js' + +import { mapStudyrightExtent } from '@/updater/mapper.js' + +describe('mapStudyrightExtent', () => { + it('resolves the extent code from educationTypeToExtentcode', () => { + expect(mapStudyrightExtent({ id: 'edu-type-1', name: { fi: 'Koulutus' } })).toEqual({ + extentcode: 1, + name: { fi: 'Koulutus' }, + }) + }) +}) diff --git a/updater/sis-updater-worker/tests/unit/updater/mapper/mapTeacher.test.js b/updater/sis-updater-worker/tests/unit/updater/mapper/mapTeacher.test.js new file mode 100644 index 000000000..683abae08 --- /dev/null +++ b/updater/sis-updater-worker/tests/unit/updater/mapper/mapTeacher.test.js @@ -0,0 +1,18 @@ +import { describe, expect, it } from 'vitest' + +import './initTests.js' + +import { mapTeacher } from '@/updater/mapper.js' + +describe('mapTeacher', () => { + it('combines last and first names when present', () => { + expect(mapTeacher({ id: 'p1', last_name: 'Smith', first_names: 'John' })).toEqual({ + id: 'p1', + name: 'Smith John', + }) + }) + + it('falls back to just the last name when first names are missing', () => { + expect(mapTeacher({ id: 'p2', last_name: 'Doe' })).toEqual({ id: 'p2', name: 'Doe' }) + }) +}) diff --git a/updater/sis-updater-worker/tests/unit/updater/mapper/sanitizeCourseCode.test.js b/updater/sis-updater-worker/tests/unit/updater/mapper/sanitizeCourseCode.test.js new file mode 100644 index 000000000..dbbe4d944 --- /dev/null +++ b/updater/sis-updater-worker/tests/unit/updater/mapper/sanitizeCourseCode.test.js @@ -0,0 +1,24 @@ +import { describe, expect, it } from 'vitest' + +import './initTests.js' + +import { sanitizeCourseCode } from '@/updater/mapper.js' + +describe('sanitizeCourseCode', () => { + it('returns null for falsy input', () => { + expect(sanitizeCourseCode(null)).toBeNull() + expect(sanitizeCourseCode('')).toBeNull() + }) + + it('returns the code as-is when there is no surrogate suffix', () => { + expect(sanitizeCourseCode('TKT10')).toBe('TKT10') + }) + + it('keeps a short second segment', () => { + expect(sanitizeCourseCode('a1b2-c3d4')).toBe('a1b2-c3d4') + }) + + it('drops a long (Oodi surrogate) second segment', () => { + expect(sanitizeCourseCode('TKT10-123456789')).toBe('TKT10') + }) +}) diff --git a/updater/sis-updater-worker/tests/unit/updater/mapper/studentMapper.test.js b/updater/sis-updater-worker/tests/unit/updater/mapper/studentMapper.test.js new file mode 100644 index 000000000..684567848 --- /dev/null +++ b/updater/sis-updater-worker/tests/unit/updater/mapper/studentMapper.test.js @@ -0,0 +1,82 @@ +import { describe, expect, it } from 'vitest' + +import './initTests.js' + +import { studentMapper } from '@/updater/mapper.js' + +describe('studentMapper', () => { + const baseStudent = { + last_name: 'Doe', + first_names: 'Jane', + student_number: '014999999', + primary_email: 'jane@example.com', + secondary_email: null, + phone_number: null, + gender_urn: 'urn:code:gender:female', + oppija_id: 'oppija1', + date_of_birth: '2000-01-01', + id: 'person1', + has_personal_identity_code: true, + preferred_language_urn: 'urn:code:language:fi', + citizenships: [], + } + + it('filters out the Oodi test student', () => { + const testStudent = { ...baseStudent, student_number: '012023965' } + expect(studentMapper([], [], new Set())(testStudent)).toBeNull() + }) + + it('sums credits from valid, primary attainments of the student', () => { + const attainments = [ + { + id: 'a1', + person_id: 'person1', + credits: 5, + primary: true, + misregistration: false, + expiryDate: new Date('2099-01-01'), + type: 'CourseUnitAttainment', + state: 'ATTAINED', + }, + { + id: 'b1', + person_id: 'person1', + credits: 5, + primary: true, + misregistration: false, + expiryDate: new Date('2099-01-01'), + type: 'CourseUnitAttainment', + state: 'ATTAINED', + }, + { + // improved (non-primary) attainment must not be counted + id: 'a2', + person_id: 'person1', + credits: 3, + primary: false, + misregistration: false, + expiryDate: new Date('2099-01-01'), + type: 'CourseUnitAttainment', + state: 'ATTAINED', + }, + { + // belongs to a different student + id: 'a3', + person_id: 'other', + credits: 100, + primary: true, + misregistration: false, + expiryDate: new Date('2099-01-01'), + type: 'CourseUnitAttainment', + state: 'ATTAINED', + }, + ] + + const result = studentMapper(attainments, [], new Set())(baseStudent) + + expect(result.creditcount).toBe(10) + expect(result.studentnumber).toBe('014999999') + expect(result.abbreviatedname).toBe('Doe Jane') + expect(result.dateofuniversityenrollment).toBeNull() + }) +}) diff --git a/updater/sis-updater-worker/tests/unit/updater/mapper/termRegistrationTypeToEnrollmenttype.test.js b/updater/sis-updater-worker/tests/unit/updater/mapper/termRegistrationTypeToEnrollmenttype.test.js new file mode 100644 index 000000000..bc9ff4976 --- /dev/null +++ b/updater/sis-updater-worker/tests/unit/updater/mapper/termRegistrationTypeToEnrollmenttype.test.js @@ -0,0 +1,13 @@ +import { describe, expect, it } from 'vitest' + +import './initTests.js' + +import { termRegistrationTypeToEnrollmenttype } from '@/updater/mapper.js' + +describe('termRegistrationTypeToEnrollmenttype', () => { + it('maps known types and falls back to 3', () => { + expect(termRegistrationTypeToEnrollmenttype('ATTENDING')).toBe(1) + expect(termRegistrationTypeToEnrollmenttype('NONATTENDING')).toBe(2) + expect(termRegistrationTypeToEnrollmenttype('SOMETHING_ELSE')).toBe(3) + }) +}) diff --git a/updater/sis-updater-worker/tests/unit/updater/shared/educationTypeToExtentcode.test.js b/updater/sis-updater-worker/tests/unit/updater/shared/educationTypeToExtentcode.test.js new file mode 100644 index 000000000..aef27b7b0 --- /dev/null +++ b/updater/sis-updater-worker/tests/unit/updater/shared/educationTypeToExtentcode.test.js @@ -0,0 +1,18 @@ +import { describe, expect, it } from 'vitest' + +import './initTests.js' + +import { educationTypeToExtentcode } from '@/updater/shared.js' + +describe('educationTypeToExtentcode', () => { + it('maps known education types to their extent code', () => { + expect(educationTypeToExtentcode['urn:code:education-type:degree-education:bachelors-degree']).toBe(1) + expect(educationTypeToExtentcode['urn:code:education-type:degree-education:masters-degree']).toBe(2) + }) + + it('maps education types without their own extent code to null', () => { + expect( + educationTypeToExtentcode['urn:code:education-type:non-degree-education:agreement-studies:joo-studies'] + ).toBeNull() + }) +}) diff --git a/updater/sis-updater-worker/tests/unit/updater/shared/getCreditTypeCodeFromAttainment.test.js b/updater/sis-updater-worker/tests/unit/updater/shared/getCreditTypeCodeFromAttainment.test.js new file mode 100644 index 000000000..a7c926f3d --- /dev/null +++ b/updater/sis-updater-worker/tests/unit/updater/shared/getCreditTypeCodeFromAttainment.test.js @@ -0,0 +1,27 @@ +import { describe, expect, it } from 'vitest' + +import './initTests.js' + +import { getCreditTypeCodeFromAttainment } from '@/updater/shared.js' + +describe('getCreditTypeCodeFromAttainment', () => { + it('is FAILED when the grade did not pass', () => { + expect(getCreditTypeCodeFromAttainment({ primary: true, state: 'ATTAINED' }, false)).toBe(10) + }) + + it('is FAILED when the state is FAILED, even if passed is true', () => { + expect(getCreditTypeCodeFromAttainment({ primary: true, state: 'FAILED' }, true)).toBe(10) + }) + + it('is IMPROVED for a passed, non-primary attainment', () => { + expect(getCreditTypeCodeFromAttainment({ primary: false, state: 'ATTAINED' }, true)).toBe(7) + }) + + it('is PASSED for a passed, primary, ATTAINED attainment', () => { + expect(getCreditTypeCodeFromAttainment({ primary: true, state: 'ATTAINED' }, true)).toBe(4) + }) + + it('is APPROVED for a passed, primary attainment in any other state', () => { + expect(getCreditTypeCodeFromAttainment({ primary: true, state: 'INCLUDED' }, true)).toBe(9) + }) +}) diff --git a/updater/sis-updater-worker/tests/unit/updater/shared/initTests.js b/updater/sis-updater-worker/tests/unit/updater/shared/initTests.js new file mode 100644 index 000000000..26ff3c91a --- /dev/null +++ b/updater/sis-updater-worker/tests/unit/updater/shared/initTests.js @@ -0,0 +1,22 @@ +import { vi } from 'vitest' + +// shared.js connects to Redis and loads every Sequelize model as a side effect of being imported. +// Mock those three dependencies so importing its pure exports doesn't require a live Redis/DB. +// +// Import this module first - before importing anything from shared.js - in every test file in +// this directory. +vi.mock('@/utils/redis.js', () => ({ + redisClient: { get: vi.fn(), set: vi.fn(), connect: vi.fn(), on: vi.fn() }, + lock: vi.fn(), +})) + +vi.mock('@/db/index.js', () => ({ + selectAllFrom: vi.fn(), + selectAllFromSnapshots: vi.fn(), +})) + +vi.mock('@/db/models/index.js', () => ({ + Semester: {}, + Organization: {}, + CREDIT_TYPE_CODES: { PASSED: 4, FAILED: 10, IMPROVED: 7, APPROVED: 9 }, +})) diff --git a/updater/sis-updater-worker/vitest.config.js b/updater/sis-updater-worker/vitest.config.js new file mode 100644 index 000000000..ee633a936 --- /dev/null +++ b/updater/sis-updater-worker/vitest.config.js @@ -0,0 +1,14 @@ +import path from 'path' +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + resolve: { + alias: { + '@': path.resolve(import.meta.dirname, './src'), + }, + }, + test: { + exclude: ['node_modules'], + testTimeout: 10_000, + }, +})