diff --git a/.github/workflows/test_backend.yml b/.github/workflows/test_backend.yml index 889c02c..334d4d1 100644 --- a/.github/workflows/test_backend.yml +++ b/.github/workflows/test_backend.yml @@ -12,6 +12,7 @@ permissions: repository-projects: read issues: read pull-requests: read + id-token: write # This is required for requesting the JWT jobs: test-backend: @@ -42,6 +43,12 @@ jobs: working-directory: backend run: npm run lint + - name: Configure AWS Credentials for Development Account + uses: aws-actions/configure-aws-credentials@v1.7.0 + with: + role-to-assume: arn:aws:iam::122610496633:role/GitHubDeployRole + aws-region: us-east-2 + - name: Run Backend Tests working-directory: backend run: npm run test diff --git a/backend/package-lock.json b/backend/package-lock.json index a18dc5b..3f2aabb 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -13,11 +13,9 @@ "@aws-sdk/client-opensearch": "^3.699.0", "@aws-sdk/client-s3": "^3.685.0", "@aws-sdk/client-secrets-manager": "^3.699.0", + "@aws-sdk/credential-providers": "^3.699.0", "@aws-sdk/lib-dynamodb": "^3.685.0", "@opensearch-project/opensearch": "^2.13.0", - "@tensorflow-models/universal-sentence-encoder": "^1.3.3", - "@tensorflow/tfjs": "^4.22.0", - "@tensorflow/tfjs-node": "^4.22.0", "archiver": "^7.0.1", "aws-lambda": "^1.0.7", "aws-sdk": "^2.1691.0", @@ -241,6 +239,57 @@ "node": ">=14.0.0" } }, + "node_modules/@aws-sdk/client-cognito-identity": { + "version": "3.699.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-cognito-identity/-/client-cognito-identity-3.699.0.tgz", + "integrity": "sha512-9tFt+we6AIvj/f1+nrLHuCWcQmyfux5gcBSOy9d9+zIG56YxGEX7S9TaZnybogpVV8A0BYWml36WvIHS9QjIpA==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.699.0", + "@aws-sdk/client-sts": "3.699.0", + "@aws-sdk/core": "3.696.0", + "@aws-sdk/credential-provider-node": "3.699.0", + "@aws-sdk/middleware-host-header": "3.696.0", + "@aws-sdk/middleware-logger": "3.696.0", + "@aws-sdk/middleware-recursion-detection": "3.696.0", + "@aws-sdk/middleware-user-agent": "3.696.0", + "@aws-sdk/region-config-resolver": "3.696.0", + "@aws-sdk/types": "3.696.0", + "@aws-sdk/util-endpoints": "3.696.0", + "@aws-sdk/util-user-agent-browser": "3.696.0", + "@aws-sdk/util-user-agent-node": "3.696.0", + "@smithy/config-resolver": "^3.0.12", + "@smithy/core": "^2.5.3", + "@smithy/fetch-http-handler": "^4.1.1", + "@smithy/hash-node": "^3.0.10", + "@smithy/invalid-dependency": "^3.0.10", + "@smithy/middleware-content-length": "^3.0.12", + "@smithy/middleware-endpoint": "^3.2.3", + "@smithy/middleware-retry": "^3.0.27", + "@smithy/middleware-serde": "^3.0.10", + "@smithy/middleware-stack": "^3.0.10", + "@smithy/node-config-provider": "^3.1.11", + "@smithy/node-http-handler": "^3.3.1", + "@smithy/protocol-http": "^4.1.7", + "@smithy/smithy-client": "^3.4.4", + "@smithy/types": "^3.7.1", + "@smithy/url-parser": "^3.0.10", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.27", + "@smithy/util-defaults-mode-node": "^3.0.27", + "@smithy/util-endpoints": "^2.1.6", + "@smithy/util-middleware": "^3.0.10", + "@smithy/util-retry": "^3.0.10", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/@aws-sdk/client-dynamodb": { "version": "3.699.0", "license": "Apache-2.0", @@ -635,6 +684,21 @@ "node": ">=16.0.0" } }, + "node_modules/@aws-sdk/credential-provider-cognito-identity": { + "version": "3.699.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-cognito-identity/-/credential-provider-cognito-identity-3.699.0.tgz", + "integrity": "sha512-iuaTnudaBfEET+o444sDwf71Awe6UiZfH+ipUPmswAi2jZDwdFF1nxMKDEKL8/LV5WpXsdKSfwgS0RQeupURew==", + "dependencies": { + "@aws-sdk/client-cognito-identity": "3.699.0", + "@aws-sdk/types": "3.696.0", + "@smithy/property-provider": "^3.1.9", + "@smithy/types": "^3.7.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/@aws-sdk/credential-provider-env": { "version": "3.696.0", "license": "Apache-2.0", @@ -762,6 +826,33 @@ "@aws-sdk/client-sts": "^3.696.0" } }, + "node_modules/@aws-sdk/credential-providers": { + "version": "3.699.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-providers/-/credential-providers-3.699.0.tgz", + "integrity": "sha512-jBjOntl9zN9Nvb0jmbMGRbiTzemDz64ij7W6BDavxBJRZpRoNeN0QCz6RolkCyXnyUJjo5mF2unY2wnv00A+LQ==", + "dependencies": { + "@aws-sdk/client-cognito-identity": "3.699.0", + "@aws-sdk/client-sso": "3.696.0", + "@aws-sdk/client-sts": "3.699.0", + "@aws-sdk/core": "3.696.0", + "@aws-sdk/credential-provider-cognito-identity": "3.699.0", + "@aws-sdk/credential-provider-env": "3.696.0", + "@aws-sdk/credential-provider-http": "3.696.0", + "@aws-sdk/credential-provider-ini": "3.699.0", + "@aws-sdk/credential-provider-node": "3.699.0", + "@aws-sdk/credential-provider-process": "3.696.0", + "@aws-sdk/credential-provider-sso": "3.699.0", + "@aws-sdk/credential-provider-web-identity": "3.696.0", + "@aws-sdk/types": "3.696.0", + "@smithy/credential-provider-imds": "^3.2.6", + "@smithy/property-provider": "^3.1.9", + "@smithy/types": "^3.7.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/@aws-sdk/endpoint-cache": { "version": "3.693.0", "license": "Apache-2.0", @@ -2217,123 +2308,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@mapbox/node-pre-gyp": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.9.tgz", - "integrity": "sha512-aDF3S3rK9Q2gey/WAttUlISduDItz5BU3306M9Eyv6/oS40aMprnopshtlKTykxRNIBEZuRMaZAnbrQ4QtKGyw==", - "dependencies": { - "detect-libc": "^2.0.0", - "https-proxy-agent": "^5.0.0", - "make-dir": "^3.1.0", - "node-fetch": "^2.6.7", - "nopt": "^5.0.0", - "npmlog": "^5.0.1", - "rimraf": "^3.0.2", - "semver": "^7.3.5", - "tar": "^6.1.11" - }, - "bin": { - "node-pre-gyp": "bin/node-pre-gyp" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dependencies": { - "semver": "^6.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/make-dir/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/nopt": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", - "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", - "dependencies": { - "abbrev": "1" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "dev": true, @@ -3389,289 +3363,6 @@ "node": ">=16.0.0" } }, - "node_modules/@tensorflow-models/universal-sentence-encoder": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@tensorflow-models/universal-sentence-encoder/-/universal-sentence-encoder-1.3.3.tgz", - "integrity": "sha512-mipL7ad0CW6uQ68FUkNgkNj/zgA4qgBnNcnMMkNTdL9MUMnzCxu3AE8pWnx2ReKHwdqEG4e8IpaYKfH4B8bojg==", - "peerDependencies": { - "@tensorflow/tfjs-converter": "^3.6.0", - "@tensorflow/tfjs-core": "^3.6.0" - } - }, - "node_modules/@tensorflow/tfjs": { - "version": "4.22.0", - "resolved": "https://registry.npmjs.org/@tensorflow/tfjs/-/tfjs-4.22.0.tgz", - "integrity": "sha512-0TrIrXs6/b7FLhLVNmfh8Sah6JgjBPH4mZ8JGb7NU6WW+cx00qK5BcAZxw7NCzxj6N8MRAIfHq+oNbPUNG5VAg==", - "dependencies": { - "@tensorflow/tfjs-backend-cpu": "4.22.0", - "@tensorflow/tfjs-backend-webgl": "4.22.0", - "@tensorflow/tfjs-converter": "4.22.0", - "@tensorflow/tfjs-core": "4.22.0", - "@tensorflow/tfjs-data": "4.22.0", - "@tensorflow/tfjs-layers": "4.22.0", - "argparse": "^1.0.10", - "chalk": "^4.1.0", - "core-js": "3.29.1", - "regenerator-runtime": "^0.13.5", - "yargs": "^16.0.3" - }, - "bin": { - "tfjs-custom-module": "dist/tools/custom_module/cli.js" - } - }, - "node_modules/@tensorflow/tfjs-converter": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-converter/-/tfjs-converter-3.21.0.tgz", - "integrity": "sha512-12Y4zVDq3yW+wSjSDpSv4HnpL2sDZrNiGSg8XNiDE4HQBdjdA+a+Q3sZF/8NV9y2yoBhL5L7V4mMLDdbZBd9/Q==", - "peer": true, - "peerDependencies": { - "@tensorflow/tfjs-core": "3.21.0" - } - }, - "node_modules/@tensorflow/tfjs-core": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-core/-/tfjs-core-3.21.0.tgz", - "integrity": "sha512-YSfsswOqWfd+M4bXIhT3hwtAb+IV8+ODwIxwdFR/7jTAPZP1wMVnSlpKnXHAN64HFOiP+Tm3HmKusEZ0+09A0w==", - "peer": true, - "dependencies": { - "@types/long": "^4.0.1", - "@types/offscreencanvas": "~2019.3.0", - "@types/seedrandom": "^2.4.28", - "@types/webgl-ext": "0.0.30", - "@webgpu/types": "0.1.16", - "long": "4.0.0", - "node-fetch": "~2.6.1", - "seedrandom": "^3.0.5" - }, - "engines": { - "yarn": ">= 1.3.2" - } - }, - "node_modules/@tensorflow/tfjs-core/node_modules/node-fetch": { - "version": "2.6.13", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.13.tgz", - "integrity": "sha512-StxNAxh15zr77QvvkmveSQ8uCQ4+v5FkvNTj0OESmiHu+VRi/gXArXtkWMElOsOUNLtUEvI4yS+rdtOHZTwlQA==", - "peer": true, - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/@tensorflow/tfjs-node": { - "version": "4.22.0", - "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-node/-/tfjs-node-4.22.0.tgz", - "integrity": "sha512-uHrXeUlfgkMxTZqHkESSV7zSdKdV0LlsBeblqkuKU9nnfxB1pC6DtoyYVaLxznzZy7WQSegjcohxxCjAf6Dc7w==", - "hasInstallScript": true, - "dependencies": { - "@mapbox/node-pre-gyp": "1.0.9", - "@tensorflow/tfjs": "4.22.0", - "adm-zip": "^0.5.2", - "google-protobuf": "^3.9.2", - "https-proxy-agent": "^2.2.1", - "progress": "^2.0.0", - "rimraf": "^2.6.2", - "tar": "^6.2.1" - }, - "engines": { - "node": ">=8.11.0" - } - }, - "node_modules/@tensorflow/tfjs-node/node_modules/agent-base": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", - "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", - "dependencies": { - "es6-promisify": "^5.0.0" - }, - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/@tensorflow/tfjs-node/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/@tensorflow/tfjs-node/node_modules/https-proxy-agent": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz", - "integrity": "sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==", - "dependencies": { - "agent-base": "^4.3.0", - "debug": "^3.1.0" - }, - "engines": { - "node": ">= 4.5.0" - } - }, - "node_modules/@tensorflow/tfjs-node/node_modules/rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, - "node_modules/@tensorflow/tfjs/node_modules/@tensorflow/tfjs-backend-cpu": { - "version": "4.22.0", - "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-backend-cpu/-/tfjs-backend-cpu-4.22.0.tgz", - "integrity": "sha512-1u0FmuLGuRAi8D2c3cocHTASGXOmHc/4OvoVDENJayjYkS119fcTcQf4iHrtLthWyDIPy3JiPhRrZQC9EwnhLw==", - "dependencies": { - "@types/seedrandom": "^2.4.28", - "seedrandom": "^3.0.5" - }, - "engines": { - "yarn": ">= 1.3.2" - }, - "peerDependencies": { - "@tensorflow/tfjs-core": "4.22.0" - } - }, - "node_modules/@tensorflow/tfjs/node_modules/@tensorflow/tfjs-backend-webgl": { - "version": "4.22.0", - "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-backend-webgl/-/tfjs-backend-webgl-4.22.0.tgz", - "integrity": "sha512-H535XtZWnWgNwSzv538czjVlbJebDl5QTMOth4RXr2p/kJ1qSIXE0vZvEtO+5EC9b00SvhplECny2yDewQb/Yg==", - "dependencies": { - "@tensorflow/tfjs-backend-cpu": "4.22.0", - "@types/offscreencanvas": "~2019.3.0", - "@types/seedrandom": "^2.4.28", - "seedrandom": "^3.0.5" - }, - "engines": { - "yarn": ">= 1.3.2" - }, - "peerDependencies": { - "@tensorflow/tfjs-core": "4.22.0" - } - }, - "node_modules/@tensorflow/tfjs/node_modules/@tensorflow/tfjs-converter": { - "version": "4.22.0", - "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-converter/-/tfjs-converter-4.22.0.tgz", - "integrity": "sha512-PT43MGlnzIo+YfbsjM79Lxk9lOq6uUwZuCc8rrp0hfpLjF6Jv8jS84u2jFb+WpUeuF4K33ZDNx8CjiYrGQ2trQ==", - "peerDependencies": { - "@tensorflow/tfjs-core": "4.22.0" - } - }, - "node_modules/@tensorflow/tfjs/node_modules/@tensorflow/tfjs-core": { - "version": "4.22.0", - "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-core/-/tfjs-core-4.22.0.tgz", - "integrity": "sha512-LEkOyzbknKFoWUwfkr59vSB68DMJ4cjwwHgicXN0DUi3a0Vh1Er3JQqCI1Hl86GGZQvY8ezVrtDIvqR1ZFW55A==", - "dependencies": { - "@types/long": "^4.0.1", - "@types/offscreencanvas": "~2019.7.0", - "@types/seedrandom": "^2.4.28", - "@webgpu/types": "0.1.38", - "long": "4.0.0", - "node-fetch": "~2.6.1", - "seedrandom": "^3.0.5" - }, - "engines": { - "yarn": ">= 1.3.2" - } - }, - "node_modules/@tensorflow/tfjs/node_modules/@tensorflow/tfjs-core/node_modules/@types/offscreencanvas": { - "version": "2019.7.3", - "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz", - "integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==" - }, - "node_modules/@tensorflow/tfjs/node_modules/@tensorflow/tfjs-data": { - "version": "4.22.0", - "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-data/-/tfjs-data-4.22.0.tgz", - "integrity": "sha512-dYmF3LihQIGvtgJrt382hSRH4S0QuAp2w1hXJI2+kOaEqo5HnUPG0k5KA6va+S1yUhx7UBToUKCBHeLHFQRV4w==", - "dependencies": { - "@types/node-fetch": "^2.1.2", - "node-fetch": "~2.6.1", - "string_decoder": "^1.3.0" - }, - "peerDependencies": { - "@tensorflow/tfjs-core": "4.22.0", - "seedrandom": "^3.0.5" - } - }, - "node_modules/@tensorflow/tfjs/node_modules/@tensorflow/tfjs-layers": { - "version": "4.22.0", - "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-layers/-/tfjs-layers-4.22.0.tgz", - "integrity": "sha512-lybPj4ZNj9iIAPUj7a8ZW1hg8KQGfqWLlCZDi9eM/oNKCCAgchiyzx8OrYoWmRrB+AM6VNEeIT+2gZKg5ReihA==", - "peerDependencies": { - "@tensorflow/tfjs-core": "4.22.0" - } - }, - "node_modules/@tensorflow/tfjs/node_modules/@webgpu/types": { - "version": "0.1.38", - "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.38.tgz", - "integrity": "sha512-7LrhVKz2PRh+DD7+S+PVaFd5HxaWQvoMqBbsV9fNJO1pjUs1P8bM2vQVNfk+3URTqbuTI7gkXi0rfsN0IadoBA==" - }, - "node_modules/@tensorflow/tfjs/node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "node_modules/@tensorflow/tfjs/node_modules/node-fetch": { - "version": "2.6.13", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.13.tgz", - "integrity": "sha512-StxNAxh15zr77QvvkmveSQ8uCQ4+v5FkvNTj0OESmiHu+VRi/gXArXtkWMElOsOUNLtUEvI4yS+rdtOHZTwlQA==", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/@tensorflow/tfjs/node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@tensorflow/tfjs/node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "engines": { - "node": ">=10" - } - }, "node_modules/@tsconfig/node10": { "version": "1.0.11", "devOptional": true, @@ -3853,11 +3544,6 @@ "@types/node": "*" } }, - "node_modules/@types/long": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", - "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==" - }, "node_modules/@types/node": { "version": "22.7.6", "license": "MIT", @@ -3902,11 +3588,6 @@ "@types/node": "*" } }, - "node_modules/@types/offscreencanvas": { - "version": "2019.3.0", - "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.3.0.tgz", - "integrity": "sha512-esIJx9bQg+QYF0ra8GnvfianIY8qWB0GBx54PK5Eps6m+xTj86KLavHv6qDhzKcu5UUOgNfJ2pWaIIV7TRUd9Q==" - }, "node_modules/@types/pacote": { "version": "11.1.8", "resolved": "https://registry.npmjs.org/@types/pacote/-/pacote-11.1.8.tgz", @@ -3928,11 +3609,6 @@ "@types/node": "*" } }, - "node_modules/@types/seedrandom": { - "version": "2.4.34", - "resolved": "https://registry.npmjs.org/@types/seedrandom/-/seedrandom-2.4.34.tgz", - "integrity": "sha512-ytDiArvrn/3Xk6/vtylys5tlY6eo7Ane0hvcx++TKo6RxQXuVfW0AF/oeWqAj9dN29SyhtawuXstgmPlwNcv/A==" - }, "node_modules/@types/semver": { "version": "7.5.8", "dev": true, @@ -3955,12 +3631,6 @@ "version": "9.0.8", "license": "MIT" }, - "node_modules/@types/webgl-ext": { - "version": "0.0.30", - "resolved": "https://registry.npmjs.org/@types/webgl-ext/-/webgl-ext-0.0.30.tgz", - "integrity": "sha512-LKVgNmBxN0BbljJrVUwkxwRYqzsAEPcZOe6S2T6ZaBDIrFp0qu4FNlpc5sM1tGbXUYFgdVQIoeLk1Y1UoblyEg==", - "peer": true - }, "node_modules/@types/yargs": { "version": "17.0.33", "license": "MIT", @@ -4188,12 +3858,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/@webgpu/types": { - "version": "0.1.16", - "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.16.tgz", - "integrity": "sha512-9E61voMP4+Rze02jlTXud++Htpjyyk8vw5Hyw9FGRrmhHQg2GqbuOfwf5Klrb8vTxc2XWI3EfO7RUHMpxTj26A==", - "peer": true - }, "node_modules/abbrev": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", @@ -4243,14 +3907,6 @@ "node": ">=0.4.0" } }, - "node_modules/adm-zip": { - "version": "0.5.16", - "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz", - "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==", - "engines": { - "node": ">=12.0" - } - }, "node_modules/agent-base": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", @@ -4344,11 +4000,6 @@ "node": ">= 8" } }, - "node_modules/aproba": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", - "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" - }, "node_modules/archiver": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", @@ -4554,19 +4205,6 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/are-we-there-yet": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", - "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", - "deprecated": "This package is no longer supported.", - "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/arg": { "version": "4.1.3", "devOptional": true, @@ -5177,14 +4815,6 @@ "version": "1.1.4", "license": "MIT" }, - "node_modules/color-support": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", - "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", - "bin": { - "color-support": "bin.js" - } - }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -5287,25 +4917,10 @@ "version": "0.0.1", "license": "MIT" }, - "node_modules/console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" - }, "node_modules/convert-source-map": { "version": "2.0.0", "license": "MIT" }, - "node_modules/core-js": { - "version": "3.29.1", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.29.1.tgz", - "integrity": "sha512-+jwgnhg6cQxKYIIjGtAHq2nwUOolo9eoFZ4sHfUH09BLXBgxnH4gA0zEd+t+BO2cNB8idaBtZFcFTRjQJRJmAw==", - "hasInstallScript": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, "node_modules/core-util-is": { "version": "1.0.3", "license": "MIT" @@ -5520,19 +5135,6 @@ "node": ">=0.4.0" } }, - "node_modules/delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" - }, - "node_modules/detect-libc": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", - "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", - "engines": { - "node": ">=8" - } - }, "node_modules/detect-newline": { "version": "3.1.0", "license": "MIT", @@ -5654,19 +5256,6 @@ "node": ">= 0.4" } }, - "node_modules/es6-promise": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", - "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" - }, - "node_modules/es6-promisify": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", - "integrity": "sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==", - "dependencies": { - "es6-promise": "^4.0.3" - } - }, "node_modules/esbuild": { "version": "0.24.0", "dev": true, @@ -6299,26 +5888,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gauge": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", - "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", - "deprecated": "This package is no longer supported.", - "dependencies": { - "aproba": "^1.0.3 || ^2.0.0", - "color-support": "^1.1.2", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.1", - "object-assign": "^4.1.1", - "signal-exit": "^3.0.0", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wide-align": "^1.1.2" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/gensync": { "version": "1.0.0-beta.2", "license": "MIT", @@ -6413,11 +5982,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/google-protobuf": { - "version": "3.21.4", - "resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.21.4.tgz", - "integrity": "sha512-MnG7N936zcKTco4Jd2PX2U96Kf9PxygAPKBug+74LHzmHXmceN16MmRcdgZv+DGef/S9YvQAfRsNCn4cjf9yyQ==" - }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -6492,11 +6056,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" - }, "node_modules/hasown": { "version": "2.0.2", "license": "MIT", @@ -7710,11 +7269,6 @@ "dev": true, "license": "MIT" }, - "node_modules/long": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", - "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" - }, "node_modules/lru-cache": { "version": "5.1.1", "license": "ISC", @@ -8506,26 +8060,6 @@ "node": ">=8" } }, - "node_modules/npmlog": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", - "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", - "deprecated": "This package is no longer supported.", - "dependencies": { - "are-we-there-yet": "^2.0.0", - "console-control-strings": "^1.1.0", - "gauge": "^3.0.0", - "set-blocking": "^2.0.0" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/obliterator": { "version": "1.6.1", "license": "MIT" @@ -8881,14 +8415,6 @@ "version": "2.0.1", "license": "MIT" }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/promise-inflight": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", @@ -9031,11 +8557,6 @@ "node": ">=16.0.0" } }, - "node_modules/regenerator-runtime": { - "version": "0.13.11", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" - }, "node_modules/regjsparser": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.10.0.tgz", @@ -9225,11 +8746,6 @@ "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==" }, - "node_modules/seedrandom": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz", - "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==" - }, "node_modules/semver": { "version": "7.6.3", "license": "ISC", @@ -9240,11 +8756,6 @@ "node": ">=10" } }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" - }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -10177,14 +9688,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/wide-align": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", - "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", - "dependencies": { - "string-width": "^1.0.2 || 2 || 3 || 4" - } - }, "node_modules/word-wrap": { "version": "1.2.5", "dev": true, diff --git a/backend/package.json b/backend/package.json index 1b0f6d8..425062d 100644 --- a/backend/package.json +++ b/backend/package.json @@ -41,6 +41,7 @@ "@aws-sdk/client-opensearch": "^3.699.0", "@aws-sdk/client-s3": "^3.685.0", "@aws-sdk/client-secrets-manager": "^3.699.0", + "@aws-sdk/credential-providers": "^3.699.0", "@aws-sdk/lib-dynamodb": "^3.685.0", "@opensearch-project/opensearch": "^2.13.0", "archiver": "^7.0.1", diff --git a/backend/src/common/__tests__/dynamodb.test.ts b/backend/src/common/__tests__/dynamodb.test.ts new file mode 100644 index 0000000..f0162f9 --- /dev/null +++ b/backend/src/common/__tests__/dynamodb.test.ts @@ -0,0 +1,97 @@ +import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb"; +import { + clearDynamoDBTable, + getPackageById, + getPackageByName, + getPackageHistory, + updatePackageHistory, + uploadPackageMetadata +} from "../dynamodb"; +import { PackageID, User, PackageTableRow, PackageMetadata } from "../interfaces"; // Replace with actual interfaces path +import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; +import { fromIni, fromNodeProviderChain } from "@aws-sdk/credential-providers"; + +const isGitHub = process.env.GITHUB_ACTIONS === "true"; + +describe("DynamoDB Functions", () => { + let dynamoDBClient: DynamoDBDocumentClient; + + const testPackageId: PackageID = "test-package-id"; + const testPackageName = "test-package"; + const testPackageRow: PackageTableRow = { + ID: testPackageId, + PackageName: testPackageName, + Version: "1.0.0", + standaloneCost: 0.0, + }; + const testPackageMetadata: PackageMetadata = { + Name: testPackageName, + Version: "1.0.0", + ID: testPackageId, + }; + + const testUser: User = { name: "test-user", isAdmin: true }; + beforeAll(async () => { + const client = new DynamoDBClient({ + region: "us-east-2", + credentials: isGitHub + ? fromNodeProviderChain() // Use default credentials in GitHub Actions + : fromIni({ profile: "dev" }) // Use the 'dev' profile locally + }); + dynamoDBClient = DynamoDBDocumentClient.from(client); + await clearDynamoDBTable(dynamoDBClient, "PackageMetadata", (item) => ({ ID: item.ID })); + await clearDynamoDBTable(dynamoDBClient, "PackageHistoryTable", (item) => ({ + PackageName: item.PackageName, + Date: item.Date, + })); + await uploadPackageMetadata(dynamoDBClient, testPackageRow); + }); + + test("getPackageById should retrieve a package by ID", async () => { + const result = await getPackageById(dynamoDBClient, testPackageId); + expect(result).toEqual(testPackageRow); + }); + + test("getPackageByName should retrieve packages by name", async () => { + const result = await getPackageByName(dynamoDBClient, testPackageName); + console.log(result); + expect(result).toEqual([ + testPackageMetadata, + ]); + }); + + test("getPackageHistory should return an empty package history", async () => { + const result = await getPackageHistory(dynamoDBClient, testPackageName); + expect(result).toEqual([]); + }); + + test("updatePackageHistory should add a new history entry", async () => { + const newAction = "UPDATE"; + await updatePackageHistory(dynamoDBClient, testPackageName, "1.0.1", testPackageId, testUser, newAction); + + const result = await getPackageHistory(dynamoDBClient, testPackageName); + expect(result).toHaveLength(1); + expect(result[0].PackageMetadata).toEqual({ + Name: testPackageName, + Version: "1.0.1", + ID: testPackageId, + }); + expect(result[0].Date).toBeDefined(); + expect(result[0].User).toEqual(testUser); + expect(result[0].Action).toEqual(newAction); + console.log(result); + }); + + test("uploadPackageMetadata should add a new package metadata entry", async () => { + const newMetadata: PackageTableRow = { ID: "new-id", PackageName: "new-package", Version: "1.0.0", standaloneCost: 0.0 }; + await uploadPackageMetadata(dynamoDBClient, newMetadata); + + const result = await getPackageById(dynamoDBClient, "new-id"); + expect(result).toEqual(newMetadata); + }); + + afterAll(async () => { + // Clean up any data inserted for the tests + await clearDynamoDBTable(dynamoDBClient, "PackageMetadata", (item) => ({ ID: item.ID })); + }); +}); diff --git a/backend/src/common/__tests__/s3.test.ts b/backend/src/common/__tests__/s3.test.ts new file mode 100644 index 0000000..30ad251 --- /dev/null +++ b/backend/src/common/__tests__/s3.test.ts @@ -0,0 +1,101 @@ +import { + S3Client, + ListObjectsV2Command, + DeleteObjectCommand, + PutObjectCommand, +} from "@aws-sdk/client-s3"; +import { + uploadToS3, + deletePackageFromS3, + emptyS3Bucket +} from "../s3"; // Adjust path to your file +import { fromIni, fromNodeProviderChain } from "@aws-sdk/credential-providers"; + + +describe("S3 Operations", () => { + let s3Client: S3Client; + process.env.S3_BUCKET_NAME = "package-files-dev"; + const bucketName = process.env.S3_BUCKET_NAME!; + const testFileContent = Buffer.from("This is a test file."); + const testPackageName = "test-package"; + const testVersion = "1.0.0"; + const isGitHub = process.env.GITHUB_ACTIONS === "true"; + + beforeAll(() => { + if (!bucketName) { + throw new Error("Environment variable S3_BUCKET_NAME is not defined."); + } + + s3Client = new S3Client({ + region: "us-east-2", credentials: isGitHub + ? fromNodeProviderChain() // Use default credentials in GitHub Actions + : fromIni({ profile: "dev" }), + }); + }); + + describe("uploadToS3", () => { + it("should upload a file to S3 and return the S3 key", async () => { + const s3Key = await uploadToS3(s3Client, testFileContent, testPackageName, testVersion); + + // Verify the object exists in S3 + const listObjects = await s3Client.send(new ListObjectsV2Command({ Bucket: bucketName })); + const uploadedObject = listObjects.Contents?.find((obj) => obj.Key === s3Key); + + expect(uploadedObject).toBeDefined(); + expect(uploadedObject?.Key).toBe(s3Key); + + // Clean up the uploaded object + await s3Client.send(new DeleteObjectCommand({ Bucket: bucketName, Key: s3Key })); + }); + }); + + describe("deletePackageFromS3", () => { + it("should delete a file from S3", async () => { + const s3Key = await uploadToS3(s3Client, testFileContent, testPackageName, testVersion); + + // Verify the object exists before deletion + let listObjects = await s3Client.send(new ListObjectsV2Command({ Bucket: bucketName })); + let uploadedObject = listObjects.Contents?.find((obj) => obj.Key === s3Key); + expect(uploadedObject).toBeDefined(); + + // Delete the object + await deletePackageFromS3(s3Client, s3Key); + + // Verify the object no longer exists + listObjects = await s3Client.send(new ListObjectsV2Command({ Bucket: bucketName })); + uploadedObject = listObjects.Contents?.find((obj) => obj.Key === s3Key); + expect(uploadedObject).toBeUndefined(); + }); + + it("should throw an error if the S3 key is not provided", async () => { + await expect(deletePackageFromS3(s3Client, "")).rejects.toThrow("S3 key not provided for deletion."); + }); + }); + + describe("emptyS3Bucket", () => { + it("should empty the S3 bucket", async () => { + const keysToUpload = ["file1.txt", "file2.txt", "file3.txt"]; + + // Upload test files + for (const key of keysToUpload) { + await s3Client.send(new PutObjectCommand({ + Bucket: bucketName, + Key: key, + Body: `Content of ${key}`, + })); + } + + // Verify files are uploaded + let listObjects = await s3Client.send(new ListObjectsV2Command({ Bucket: bucketName })); + expect(listObjects.Contents?.length).toBe(keysToUpload.length); + + // Empty the bucket + await emptyS3Bucket(s3Client, bucketName); + + // Verify the bucket is empty + listObjects = await s3Client.send(new ListObjectsV2Command({ Bucket: bucketName })); + expect(listObjects.Contents).toBeUndefined(); + // expect(listObjects.Contents?.length).toBe(0); + }); + }); +}); diff --git a/backend/src/common/__tests__/utils.test.ts b/backend/src/common/__tests__/utils.test.ts new file mode 100644 index 0000000..7de5702 --- /dev/null +++ b/backend/src/common/__tests__/utils.test.ts @@ -0,0 +1,136 @@ +import { SecretsManagerClient } from "@aws-sdk/client-secrets-manager"; +import { + createErrorResponse, + getEnvVariable, + generatePackageID, + getSecret, + getScores, + getRepoUrl, + extractFieldFromPackageJson, +} from "../utils"; // Adjust path if needed +import { fromIni, fromNodeProviderChain } from "@aws-sdk/credential-providers"; + +const isGitHub = process.env.GITHUB_ACTIONS === "true"; + +describe("Function Tests", () => { + let secretsClient: SecretsManagerClient; + beforeAll(() => { + secretsClient = new SecretsManagerClient({ + region: "us-east-2", credentials: isGitHub + ? fromNodeProviderChain() // Use default credentials in GitHub Actions + : fromIni({ profile: "dev" }) + }); + }); + + describe("createErrorResponse", () => { + it("should return a valid error response", () => { + const statusCode = 400; + const message = "Test error message"; + const response = createErrorResponse(statusCode, message); + + expect(response).toEqual({ + statusCode: 400, + headers: { + "Access-Control-Allow-Origin": "*", + "Content-Type": "application/json", + }, + body: JSON.stringify({ message }), + }); + }); + }); + + describe("getEnvVariable", () => { + it("should retrieve an environment variable", () => { + process.env.TEST_VAR = "test_value"; + expect(getEnvVariable("TEST_VAR")).toBe("test_value"); + }); + + it("should throw an error for undefined environment variable", () => { + expect(() => getEnvVariable("UNDEFINED_VAR")).toThrow( + "Environment variable UNDEFINED_VAR is not defined" + ); + }); + }); + + describe("generatePackageID", () => { + it("should generate a unique package ID", () => { + const name = "test-package"; + const version = "1.0.0"; + const id = generatePackageID(name, version); + + expect(id).toHaveLength(20); // Length of the base64url hash slice + expect(typeof id).toBe("string"); + }); + }); + + describe("getSecret", () => { + it("should retrieve a secret from Secrets Manager", async () => { + const secretName = "GitHubToken"; // Replace with an actual secret name + const secret = await getSecret(secretsClient, secretName); + const token = JSON.parse(secret) as { GitHubToken: string }; + const tokenValue = token.GitHubToken; + + expect(typeof tokenValue).toBe("string"); + expect(tokenValue).not.toBe(""); // Ensure the secret is not empty + }); + }); + + describe("getScores", () => { + it("should calculate and return scores for a valid URL", async () => { + const token = process.env.GITHUB_TOKEN || ""; // Replace with a valid token + const url = "https://github.com/thejoshwolfe/yazl"; // Replace with a valid GitHub repo URL + const scores = await getScores(token, url); + + expect(scores).toHaveProperty("NetScore"); + expect(scores.NetScore).not.toBe(-1); // Ensure scores were calculated + }, 60000); + + it("should return empty scores for an invalid URL", async () => { + const token = process.env.GITHUB_TOKEN || ""; // Replace with a valid token + const url = "invalid-url"; // Replace with an invalid URL + const scores = await getScores(token, url); + + expect(scores.NetScore).toBe(-1); // Ensure empty scores are returned + }); + }); + + describe("getRepoUrl", () => { + it("should fetch the GitHub repo URL from an NPM package URL", async () => { + const npmUrl = "https://www.npmjs.com/package/yazl"; // Replace with a valid NPM package URL + const repoUrl = await getRepoUrl(npmUrl); + + expect(repoUrl).toContain("https://github.com/thejoshwolfe/yazl"); + }); + + it("should handle invalid NPM package URLs gracefully", async () => { + const npmUrl = "https://invalid-npm-url.com"; + const repoUrl = await getRepoUrl(npmUrl); + + expect(repoUrl).toEqual(""); // Check for failure + }); + }); + + describe("extractFieldFromPackageJson", () => { + it("should extract a field from package.json content", () => { + const packageJson = JSON.stringify({ + name: "test-package", + version: "1.0.0", + }); + const field = "name"; + const value = extractFieldFromPackageJson(packageJson, field); + + expect(value).toBe("test-package"); + }); + + it("should return undefined for a missing field", () => { + const packageJson = JSON.stringify({ + name: "test-package", + version: "1.0.0", + }); + const field = "description"; + const value = extractFieldFromPackageJson(packageJson, field); + + expect(value).toBeUndefined(); + }); + }); +}); diff --git a/backend/src/common/__tests__/zip.test.ts b/backend/src/common/__tests__/zip.test.ts new file mode 100644 index 0000000..bac2658 --- /dev/null +++ b/backend/src/common/__tests__/zip.test.ts @@ -0,0 +1,104 @@ +import * as fs from 'fs-extra'; +import * as path from 'path'; +import * as os from 'os'; +import JSZip from 'jszip'; +import { debloatPackage, cloneAndZipRepository, zipDirectory, extractPackageJsonFromZip, extractReadmeFromZip } from '../zip'; // Adjust the path + +describe("File Operations and Compression Functions", () => { + let tempDir: string; + + beforeAll(async () => { + tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'jest-test-')); + }); + + afterAll(async () => { + await fs.promises.rm(tempDir, { recursive: true, force: true }); + }); + + describe("debloatPackage", () => { + it("should optimize JavaScript and TypeScript files in a zip archive", async () => { + const zip = new JSZip(); + zip.file("test.js", "console.log('hello world');"); + zip.file("test.txt", "This is a text file."); + + const zipBuffer = await zip.generateAsync({ type: 'nodebuffer' }); + const optimizedZipBuffer = await debloatPackage(zipBuffer); + + const optimizedZip = await JSZip.loadAsync(optimizedZipBuffer); + const optimizedJs = await optimizedZip.file("test.js")?.async("string"); + const optimizedTxt = await optimizedZip.file("test.txt")?.async("string"); + + expect(optimizedJs).not.toBeNull(); + expect(optimizedTxt).toBe("This is a text file."); + }); + }); + + describe("cloneAndZipRepository", () => { + it("should clone a repository and zip it", async () => { + const repoUrl = "https://github.com/isomorphic-git/isomorphic-git"; // Use a real repo URL + const zipBuffer = await cloneAndZipRepository(repoUrl); + + const zip = await JSZip.loadAsync(zipBuffer); + const files = Object.keys(zip.files); + + expect(files).toContain("README.md"); + }); + }); + + describe("zipDirectory", () => { + it("should create a zip archive from a directory", async () => { + const testDir = path.join(tempDir, 'testDir'); + await fs.promises.mkdir(testDir); + await fs.promises.writeFile(path.join(testDir, 'file1.txt'), "File 1 content"); + await fs.promises.writeFile(path.join(testDir, 'file2.txt'), "File 2 content"); + + const zipBuffer = await zipDirectory(testDir); + const zip = await JSZip.loadAsync(zipBuffer); + + const files = Object.keys(zip.files); + expect(files).toContain("file1.txt"); + expect(files).toContain("file2.txt"); + }); + }); + + describe("extractPackageJsonFromZip", () => { + it("should extract package.json from a zip archive", async () => { + const zip = new JSZip(); + zip.file("package.json", JSON.stringify({ name: "test-package", version: "1.0.0" })); + + const zipBuffer = await zip.generateAsync({ type: 'nodebuffer' }); + const packageJson = await extractPackageJsonFromZip(zipBuffer); + + expect(packageJson).toBeTruthy(); + expect(JSON.parse(packageJson!)).toEqual({ name: "test-package", version: "1.0.0" }); + }); + + it("should return null if package.json is not present", async () => { + const zip = new JSZip(); + const zipBuffer = await zip.generateAsync({ type: 'nodebuffer' }); + + const packageJson = await extractPackageJsonFromZip(zipBuffer); + expect(packageJson).toBeNull(); + }); + }); + + describe("extractReadmeFromZip", () => { + it("should extract README.md from a zip archive", async () => { + const zip = new JSZip(); + zip.file("README.md", "This is a readme file."); + + const zipBuffer = await zip.generateAsync({ type: 'nodebuffer' }); + const readme = await extractReadmeFromZip(zipBuffer); + + expect(readme).toBe("This is a readme file."); + }); + + it("should return null if README.md is not present", async () => { + const zip = new JSZip(); + const zipBuffer = await zip.generateAsync({ type: 'nodebuffer' }); + + const readme = await extractReadmeFromZip(zipBuffer); + expect(readme).toBeNull(); + }); + }); +}); diff --git a/backend/src/common/dynamodb.ts b/backend/src/common/dynamodb.ts index 53a37ed..20a6cd4 100644 --- a/backend/src/common/dynamodb.ts +++ b/backend/src/common/dynamodb.ts @@ -1,5 +1,12 @@ -import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb"; +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { DynamoDBDocumentClient, DeleteCommand } from "@aws-sdk/lib-dynamodb"; import { GetCommand, QueryCommand, PutCommand } from '@aws-sdk/lib-dynamodb'; +import { + DynamoDBClient, + ScanCommand, + BatchWriteItemCommand, + ScanCommandOutput, +} from "@aws-sdk/client-dynamodb"; import { PackageID, PackageHistoryEntry, PackageMetadata, User, PackageTableRow } from "./interfaces"; export async function getPackageById(dynamoDBClient: DynamoDBDocumentClient, packageId: PackageID) { @@ -36,8 +43,12 @@ export async function getPackageByName(dynamoDBClient: DynamoDBDocumentClient, n }) ); + const output = result.Items?.map(item => { + const { PackageName, Version, ID } = item; + return { Name: PackageName, Version: Version, ID: ID }; + }) as PackageMetadata[]; // If no items are found, return an empty array - return result.Items as PackageMetadata[] || []; + return output || []; } catch (error) { console.error("Error querying the DynamoDB table:", error); throw new Error("Failed to retrieve packages."); @@ -60,7 +71,12 @@ export async function getPackageHistory(dynamoDBClient: DynamoDBDocumentClient, const command = new QueryCommand(params); const result = await dynamoDBClient.send(command); - return result.Items as PackageHistoryEntry[]; + const output = result.Items?.map(item => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { PackageName, ...rest } = item; + return rest; + }) as PackageHistoryEntry[]; + return output; } export async function updatePackageHistory(dynamoDBClient: DynamoDBDocumentClient, packageName: string, version: string, packageId: string, user: User, action: string) { @@ -89,4 +105,62 @@ export async function uploadPackageMetadata(dynamoDBClient: DynamoDBDocumentClie Item: metadata, }; await dynamoDBClient.send(new PutCommand(dynamoDBParams)); +} + +export async function deletePackageFromDynamoDB(client: DynamoDBDocumentClient, id: string): Promise { + try { + const deleteParams = { + TableName: 'PackageMetadata', + Key: { ID: id }, + }; + await client.send(new DeleteCommand(deleteParams)); + console.log(`Successfully deleted metadata for package ID: ${id}`); + } catch (error) { + console.error(`Error deleting package metadata for ID: ${id}`, error); + throw new Error("Failed to delete package metadata."); + } +} + +// Reusable function to delete items from DynamoDB in batches +export async function deleteDynamoDBItems( + dynamoDBClient: DynamoDBClient, + tableName: string, + keys: { DeleteRequest: { Key: Record } }[] +): Promise { + for (let i = 0; i < keys.length; i += 25) { + const batch = keys.slice(i, i + 25); + await dynamoDBClient.send( + new BatchWriteItemCommand({ + RequestItems: { [tableName]: batch }, + }) + ); + } +} + +// Function to clear a DynamoDB table +export async function clearDynamoDBTable( + dynamoDBClient: DynamoDBClient, + tableName: string, + keyExtractor: (item: Record) => Record +): Promise { + console.log(`Clearing DynamoDB table: ${tableName}`); + let lastEvaluatedKey: Record | undefined; + + do { + const scanResult: ScanCommandOutput = await dynamoDBClient.send( + new ScanCommand({ TableName: tableName, ExclusiveStartKey: lastEvaluatedKey }) + ); + + const items = scanResult.Items || []; + const keys = items.map((item) => ({ DeleteRequest: { Key: keyExtractor(item) } })); + + if (keys.length > 0) { + console.log(`Deleting ${keys.length} items from table: ${tableName}`); + await deleteDynamoDBItems(dynamoDBClient, tableName, keys); + } + + lastEvaluatedKey = scanResult.LastEvaluatedKey; + } while (lastEvaluatedKey); + + console.log(`Table ${tableName} cleared successfully.`); } \ No newline at end of file diff --git a/backend/src/common/interfaces.ts b/backend/src/common/interfaces.ts index cec87aa..9a20597 100644 --- a/backend/src/common/interfaces.ts +++ b/backend/src/common/interfaces.ts @@ -90,9 +90,8 @@ export interface PackageTableRow { URL?: string; // Package URL s3Key?: string; // S3 key for package JSProgram?: string; // JavaScript program for sensitive modules - standaloneCost?: number; // Standalone cost, excluding dependencies - totalCost?: number; // Total cost including dependencies if applicable - Rating: PackageRating; + standaloneCost: number; // Standalone cost, excluding dependencies + Rating?: PackageRating; } export interface RecommendationRequest { diff --git a/backend/src/common/s3.ts b/backend/src/common/s3.ts index 40d8f7d..b90a170 100644 --- a/backend/src/common/s3.ts +++ b/backend/src/common/s3.ts @@ -1,4 +1,13 @@ -import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'; +import { + S3Client, + PutObjectCommand, + ListObjectsV2Command, + DeleteObjectsCommand, + ListObjectsV2CommandOutput, + DeleteObjectCommand +} from "@aws-sdk/client-s3"; + +import { getEnvVariable } from "./utils"; export async function uploadToS3(s3Client: S3Client, fileContent: Buffer, packageName: string, version: string): Promise { const s3Key = `uploads/${packageName}-${version}.zip`; @@ -16,4 +25,56 @@ export async function uploadToS3(s3Client: S3Client, fileContent: Buffer, packag await s3Client.send(command); return s3Key; +} + +// Delete the package file from S3 +export async function deletePackageFromS3(client: S3Client, s3Key: string): Promise { + const bucketName = getEnvVariable('S3_BUCKET_NAME'); + + if (!s3Key) { + throw new Error('S3 key not provided for deletion.'); + } + + try { + await client.send( + new DeleteObjectCommand({ + Bucket: bucketName, + Key: s3Key, + }) + ); + console.log(`Successfully deleted file from S3 with key: ${s3Key}`); + } catch (error) { + console.error(`Error deleting file from S3 with key: ${s3Key}`, error); + throw new Error('Failed to delete package file from S3.'); + } +} + +// Function to empty an S3 bucket +export async function emptyS3Bucket( + s3Client: S3Client, + bucketName: string +): Promise { + console.log(`Emptying S3 bucket: ${bucketName}`); + let continuationToken: string | undefined; + + do { + const listObjects: ListObjectsV2CommandOutput = await s3Client.send( + new ListObjectsV2Command({ Bucket: bucketName, ContinuationToken: continuationToken }) + ); + + const objects = listObjects.Contents?.map((object) => ({ Key: object.Key })) || []; + if (objects.length > 0) { + console.log(`Deleting ${objects.length} objects from bucket: ${bucketName}`); + await s3Client.send( + new DeleteObjectsCommand({ + Bucket: bucketName, + Delete: { Objects: objects }, + }) + ); + } + + continuationToken = listObjects.NextContinuationToken; + } while (continuationToken); + + console.log(`Bucket ${bucketName} emptied successfully.`); } \ No newline at end of file diff --git a/backend/src/handlers/CreateIndex/index.ts b/backend/src/handlers/CreateIndex/index.ts index f483271..9247e16 100644 --- a/backend/src/handlers/CreateIndex/index.ts +++ b/backend/src/handlers/CreateIndex/index.ts @@ -2,7 +2,7 @@ import https from "https"; import { Context } from 'aws-lambda'; const commonPath = process.env.COMMON_PATH || '/opt/nodejs/common'; -const { checkIndexExists, createIndex } = require(`${commonPath}/opensearch`); +const { checkIndexExists, deleteIndex, createIndex } = require(`${commonPath}/opensearch`); interface EventResourceProperties { DomainEndpoint: string; @@ -47,16 +47,16 @@ export const handler = async (event: LambdaEvent, context: Context) => { timestamp: { type: "date", // ISO-8601 date format }, - embedding: { - type: "knn_vector", // K-Nearest Neighbors vector for similarity search - dimension: 1536, // Dimensions of the embedding vector - method: { - engine: "lucene", - space_type: "l2", - name: "hnsw", - parameters: {} - } - }, + // embedding: { + // type: "knn_vector", // K-Nearest Neighbors vector for similarity search + // dimension: 1536, // Dimensions of the embedding vector + // method: { + // engine: "lucene", + // space_type: "l2", + // name: "hnsw", + // parameters: {} + // } + // }, metadata: { properties: { Name: { type: "keyword" }, @@ -89,36 +89,23 @@ export const handler = async (event: LambdaEvent, context: Context) => { }, }; - let indexName = "readmes"; - let exists = await checkIndexExists(indexName); - - if (!exists) { - console.log(`Index '${indexName}' does not exist. Creating...`); - await createIndex(indexName, nonTokenizedMapping); - } else { - console.log(`Index '${indexName}' already exists. Skipping creation.`); + if (await checkIndexExists("readmes")) { + await deleteIndex("readmes"); } - indexName = "packagejsons"; - exists = await checkIndexExists(indexName); - - if (!exists) { - console.log(`Index '${indexName}' does not exist. Creating...`); - await createIndex(indexName, nonTokenizedMapping); - } else { - console.log(`Index '${indexName}' already exists. Skipping creation.`); + if (await checkIndexExists("packagejsons")) { + await deleteIndex("packagejsons"); } - indexName = "recommend"; - exists = await checkIndexExists(indexName); - - if (!exists) { - console.log(`Index '${indexName}' does not exist. Creating...`); - await createIndex(indexName, tokenizedMapping); - } else { - console.log(`Index '${indexName}' already exists. Skipping creation.`); + if (await checkIndexExists("recommend")) { + await deleteIndex("recommend"); } + createIndex("readmes", nonTokenizedMapping); + createIndex("packagejsons", nonTokenizedMapping); + createIndex("recommend", tokenizedMapping); + console.log("CreateIndex handler completed successfully"); + response.Status = "SUCCESS"; response.Reason = "Index operation completed successfully"; } catch (error) { diff --git a/backend/src/handlers/PackageDelete/index.ts b/backend/src/handlers/PackageDelete/index.ts index e1022e1..061d55a 100644 --- a/backend/src/handlers/PackageDelete/index.ts +++ b/backend/src/handlers/PackageDelete/index.ts @@ -1,11 +1,12 @@ import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; -import { DynamoDBDocumentClient, DeleteCommand } from '@aws-sdk/lib-dynamodb'; -import { S3Client, DeleteObjectCommand } from '@aws-sdk/client-s3'; +import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb'; +import { S3Client } from '@aws-sdk/client-s3'; import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'; const commonPath = process.env.COMMON_PATH || '/opt/nodejs/common'; -const { createErrorResponse, getEnvVariable } = require(`${commonPath}/utils`); -const { getPackageById } = require(`${commonPath}/dynamodb`); +const { createErrorResponse } = require(`${commonPath}/utils`); +const { getPackageById, deletePackageFromDynamoDB } = require(`${commonPath}/dynamodb`); +const { deletePackageFromS3 } = require(`${commonPath}/s3`); const { deleteFromOpenSearch } = require(`${commonPath}/opensearch`); // eslint-disable-next-line @typescript-eslint/no-unused-vars const interfaces = require(`${commonPath}/interfaces`); @@ -50,74 +51,6 @@ export const handler = async (event: APIGatewayProxyEvent): Promise { - try { - const deleteParams = { - TableName: 'PackageMetadata', - Key: { ID: id }, - }; - await client.send(new DeleteCommand(deleteParams)); - console.log(`Successfully deleted metadata for package ID: ${id}`); - } catch (error) { - console.error(`Error deleting package metadata for ID: ${id}`, error); - throw new Error("Failed to delete package metadata."); - } -} - -// Delete the package file from S3 -async function deletePackageFromS3(client: S3Client, s3Key: string): Promise { - const bucketName = getEnvVariable('S3_BUCKET_NAME'); - - if (!s3Key) { - throw new Error('S3 key not provided for deletion.'); - } - - try { - await client.send( - new DeleteObjectCommand({ - Bucket: bucketName, - Key: s3Key, - }) - ); - console.log(`Successfully deleted file from S3 with key: ${s3Key}`); - } catch (error) { - console.error(`Error deleting file from S3 with key: ${s3Key}`, error); - throw new Error('Failed to delete package file from S3.'); - } -} - -// async function deleteDocumentById(domainEndpoint: string, indexName: string, documentId: string) { -// const credentials = await defaultProvider()(); - -// // Construct the delete document request -// const request = { -// host: domainEndpoint.replace(/^https?:\/\//, ""), -// path: `/${indexName}/_doc/${documentId}`, // Path to the specific document -// service: "es", -// method: "DELETE", -// headers: { -// "Content-Type": "application/json", -// }, -// }; - -// aws4.sign(request, credentials); - -// console.log(`Deleting document with ID '${documentId}' from index: '${indexName}'`); -// try { -// const response = await axios({ -// method: request.method, -// url: `https://${request.host}${request.path}`, -// headers: request.headers, -// }); -// console.log(`Document with ID '${documentId}' deleted from index '${indexName}':`, response.data); -// } catch (error) { -// console.error(`Error deleting document with ID '${documentId}' from index '${indexName}':`, error); -// throw new Error(`Failed to delete document with ID '${documentId}' from index '${indexName}'.`); -// } -// } - - // Create success response function createSuccessResponse(packageName: string, version: string): APIGatewayProxyResult { return { diff --git a/backend/src/handlers/PackageRate/index.ts b/backend/src/handlers/PackageRate/index.ts index 49d65b0..07b061d 100644 --- a/backend/src/handlers/PackageRate/index.ts +++ b/backend/src/handlers/PackageRate/index.ts @@ -4,11 +4,12 @@ import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb"; const commonPath = process.env.COMMON_PATH || '/opt/nodejs/common'; const { createErrorResponse } = require(`${commonPath}/utils`); -const { getPackageById } = require(`${commonPath}/dynamodb`); +const { getPackageById, updatePackageHistory } = require(`${commonPath}/dynamodb`); // eslint-disable-next-line @typescript-eslint/no-unused-vars const interfaces = require(`${commonPath}/interfaces`); type PackageRating = typeof interfaces.PackageRating; type PackageTableRow = typeof interfaces.PackageTableRow; +type User = typeof interfaces.User; export const handler = async (event: APIGatewayProxyEvent): Promise => { try { @@ -35,6 +36,11 @@ export const handler = async (event: APIGatewayProxyEvent): Promise { try { // Inject clients @@ -90,33 +26,17 @@ export const handler: APIGatewayProxyHandler = async () => { console.log("Clearing resources..."); // Perform all operations concurrently await Promise.all([ - clearDynamoDBTable(dynamoDBClient, table1, (item) => ({ ID: item.ID })), - clearDynamoDBTable(dynamoDBClient, table2, (item) => ({ + clearDynamoDBTable(dynamoDBClient, table1, (item: { ID: any; }) => ({ ID: item.ID })), + clearDynamoDBTable(dynamoDBClient, table2, (item: { PackageName: any; Date: any; }) => ({ PackageName: item.PackageName, Date: item.Date, })), emptyS3Bucket(s3Client, bucket), ]); - // await clearDomain(getEnvVariable("DOMAIN_ENDPOINT")); - if (await checkIndexExists("readmes")) { - await deleteIndex("readmes"); - } - - if (await checkIndexExists("packagejsons")) { - await deleteIndex("packagejsons"); - } - - if (await checkIndexExists("recommend")) { - await deleteIndex("recommend"); - } - - await createIndex("readmes", nonTokenizedMapping); - await createIndex("packagejsons", nonTokenizedMapping); - await createIndex("recommend", tokenizedMapping); - // await clearIndex("readmes"); - // await clearIndex("packagejsons"); - // await clearIndex("recommend"); + await clearIndex("readmes"); + await clearIndex("packagejsons"); + await clearIndex("recommend"); console.log("All resources cleared successfully."); return { @@ -130,78 +50,4 @@ export const handler: APIGatewayProxyHandler = async () => { body: JSON.stringify({ message: "Failed to clear resources.", error: error }), }; } -}; - -// Reusable function to delete items from DynamoDB in batches -async function deleteDynamoDBItems( - dynamoDBClient: DynamoDBClient, - tableName: string, - keys: { DeleteRequest: { Key: Record } }[] -): Promise { - for (let i = 0; i < keys.length; i += 25) { - const batch = keys.slice(i, i + 25); - await dynamoDBClient.send( - new BatchWriteItemCommand({ - RequestItems: { [tableName]: batch }, - }) - ); - } -} - -// Function to clear a DynamoDB table -async function clearDynamoDBTable( - dynamoDBClient: DynamoDBClient, - tableName: string, - keyExtractor: (item: Record) => Record -): Promise { - console.log(`Clearing DynamoDB table: ${tableName}`); - let lastEvaluatedKey: Record | undefined; - - do { - const scanResult: ScanCommandOutput = await dynamoDBClient.send( - new ScanCommand({ TableName: tableName, ExclusiveStartKey: lastEvaluatedKey }) - ); - - const items = scanResult.Items || []; - const keys = items.map((item) => ({ DeleteRequest: { Key: keyExtractor(item) } })); - - if (keys.length > 0) { - console.log(`Deleting ${keys.length} items from table: ${tableName}`); - await deleteDynamoDBItems(dynamoDBClient, tableName, keys); - } - - lastEvaluatedKey = scanResult.LastEvaluatedKey; - } while (lastEvaluatedKey); - - console.log(`Table ${tableName} cleared successfully.`); -} - -// Function to empty an S3 bucket -async function emptyS3Bucket( - s3Client: S3Client, - bucketName: string -): Promise { - console.log(`Emptying S3 bucket: ${bucketName}`); - let continuationToken: string | undefined; - - do { - const listObjects: ListObjectsV2CommandOutput = await s3Client.send( - new ListObjectsV2Command({ Bucket: bucketName, ContinuationToken: continuationToken }) - ); - - const objects = listObjects.Contents?.map((object) => ({ Key: object.Key })) || []; - if (objects.length > 0) { - console.log(`Deleting ${objects.length} objects from bucket: ${bucketName}`); - await s3Client.send( - new DeleteObjectsCommand({ - Bucket: bucketName, - Delete: { Objects: objects }, - }) - ); - } - - continuationToken = listObjects.NextContinuationToken; - } while (continuationToken); - - console.log(`Bucket ${bucketName} emptied successfully.`); -} \ No newline at end of file +}; \ No newline at end of file diff --git a/backend/src/handlers/__tests__/end_to_end/packageByName.test.ts b/backend/src/handlers/__tests__/end_to_end/packageByName.test.ts new file mode 100644 index 0000000..08c4075 --- /dev/null +++ b/backend/src/handlers/__tests__/end_to_end/packageByName.test.ts @@ -0,0 +1,71 @@ +import axios from "axios"; +import { Package, PackageData } from "../../../common/interfaces"; +import { baseUrl } from "./config"; + +const timeout = 60000; + +describe("E2E Test for Package By Name Endpoint", () => { + let id: string; + beforeAll(async () => { + // Reset the registry before running the tests + await axios.delete(`${baseUrl}/reset`); + + // Upload a package with Content to the registry + const requestBody: PackageData = { + Name: "test-package", + Content: "UEsDBBQAAAAAAKm8clkAAAAAAAAAAAAAAAATACAAdGVzdC1wYWNrYWdlLTEuMC4wL1VUDQAH/xU8Z18WPGfcFTxndXgLAAEEAAAAAAQAAAAAUEsDBBQACAAIAJy8clkAAAAAAAAAAAQBAAAfACAAdGVzdC1wYWNrYWdlLTEuMC4wL3BhY2thZ2UuanNvblVUDQAH6BU8ZwAWPGfiFTxndXgLAAEEAAAAAAQAAAAAXc89D4IwEAbgnYT/cLmBSQmsbMY4OOvI0pRTTqVtetWQGP67lK/Bbten7+XtN01gPGhUR1gBBpKwd0o/1Z1wt+CHvLA10cu8yIsNGhLt2YUFr2MY/sOd4gnZNNTnD9lgjspoS4npNhaIz0m3Fmo8eW99BcZCBBBHmm9MTY2QZUA9Byhxjg/rYvUOrfVbocM8rvpiTUamz54vR0yT4QdQSwcI9J0+op4AAAAEAQAAUEsDBBQACAAIALW8clkAAAAAAAAAACoAAAAcACAAdGVzdC1wYWNrYWdlLTEuMC4wL1JFQURNRS5tZFVUDQAHFhY8ZxYWPGf/FTxndXgLAAEEAAAAAAQAAAAAC3J1dPF1VUjLzElVSMsvUihJLS7RLUhMzk5MT1UoSy0qzszPUzDUM9AzAABQSwcI8eDIMywAAAAqAAAAUEsBAhQDFAAAAAAAqbxyWQAAAAAAAAAAAAAAABMAIAAAAAAAAAAAAP9BAAAAAHRlc3QtcGFja2FnZS0xLjAuMC9VVA0AB/8VPGdfFjxn3BU8Z3V4CwABBAAAAAAEAAAAAFBLAQIUAxQACAAIAJy8cln0nT6ingAAAAQBAAAfACAAAAAAAAAAAAC2gVEAAAB0ZXN0LXBhY2thZ2UtMS4wLjAvcGFja2FnZS5qc29uVVQNAAfoFTxnABY8Z+IVPGd1eAsAAQQAAAAABAAAAABQSwECFAMUAAgACAC1vHJZ8eDIMywAAAAqAAAAHAAgAAAAAAAAAAAAtoFcAQAAdGVzdC1wYWNrYWdlLTEuMC4wL1JFQURNRS5tZFVUDQAHFhY8ZxYWPGf/FTxndXgLAAEEAAAAAAQAAAAAUEsFBgAAAAADAAMAOAEAAPIBAAAAAA==", + debloat: false, + JSProgram: "console.log('Hello, World!');", + }; + + const response = await axios.post(`${baseUrl}/package`, requestBody); + + // Get the ID of the uploaded package + id = response.data.metadata.ID; + + // Upload a package with a URL to the registry + const requestBody2: Package = { + metadata: { + Name: "test-package", + Version: "1.0.1", + ID: id, + }, + data: { + Name: "test-package", + Content: "UEsDBBQAAAAAANard1kAAAAAAAAAAAAAAAATACAAdGVzdC1wYWNrYWdlLTEuMC4xL1VUDQAH5Z1CZ4fjSGflnUJndXgLAAEEAAAAAAQAAAAAUEsDBBQACAAIANard1kAAAAAAAAAAAQBAAAfACAAdGVzdC1wYWNrYWdlLTEuMC4xL3BhY2thZ2UuanNvblVUDQAH5Z1CZyHjSGflnUJndXgLAAEEAAAAAAQAAAAAXc89D4IwEAbgnYT/cLmBSYmsbMY4OOPI0pRTTqUlvWpIDP/dlq/Bbten7+XtN00gHDSqIywBPYnf90o/1Z1wt+CHnLA10Yv8kBcbNCTace8XvIYw/Ic7xROyaWjIH7LBHJVgS4npNhaIz0m3Fmo8O2ddCcZCBJCeNN+Ymhohy4AG9lDgHB/XxertW+u2Qsd5XPXFmoxMn71UJ0yT8QdQSwcIu0AVyp4AAAAEAQAAUEsDBBQACAAIAAt9fFkAAAAAAAAAADYAAAAcACAAdGVzdC1wYWNrYWdlLTEuMC4xL1JFQURNRS5tZFVUDQAHRuNIZ0bjSGflnUJndXgLAAEEAAAAAAQAAAAAC8nILFYAokSFIFdHF19XhbTMnFSF8sySjPzSEoWSjFSFvMTcVIX8NDC7IDE5OzE9VQ8AUEsHCPg5dEszAAAANgAAAFBLAQIUAxQAAAAAANard1kAAAAAAAAAAAAAAAATACAAAAAAAAAAAAD/QQAAAAB0ZXN0LXBhY2thZ2UtMS4wLjEvVVQNAAflnUJnh+NIZ+WdQmd1eAsAAQQAAAAABAAAAABQSwECFAMUAAgACADWq3dZu0AVyp4AAAAEAQAAHwAgAAAAAAAAAAAAtoFRAAAAdGVzdC1wYWNrYWdlLTEuMC4xL3BhY2thZ2UuanNvblVUDQAH5Z1CZyHjSGflnUJndXgLAAEEAAAAAAQAAAAAUEsBAhQDFAAIAAgAC318Wfg5dEszAAAANgAAABwAIAAAAAAAAAAAALaBXAEAAHRlc3QtcGFja2FnZS0xLjAuMS9SRUFETUUubWRVVA0AB0bjSGdG40hn5Z1CZ3V4CwABBAAAAAAEAAAAAFBLBQYAAAAAAwADADgBAAD5AQAAAAA=", + debloat: false, + JSProgram: "console.log('Hello, World!');", + }, + }; + + await axios.post(`${baseUrl}/package/${id}`, requestBody2); + + }, 90000); + afterAll(async () => { + // Reset the registry after running the tests + await axios.delete(`${baseUrl}/reset`); + }, 90000); + + it("should return a 200 status for a package that exists", async () => { + const response = await axios.get(`${baseUrl}/package/byName/test-package`); + expect(response.status).toBe(200); + expect(response.data.length).toBe(2); + + }, timeout); + + it("should return a 404 status for a package that does not exist", async () => { + try { + await axios.get(`${baseUrl}/package/byName/non-existent-package`); + } catch (error) { + if (!axios.isAxiosError(error)) { + throw error; + } + + const response = error.response; + if (!response) { + throw error; + } + expect(response.status).toBe(404); + } + }, timeout); +}); \ No newline at end of file diff --git a/backend/src/handlers/__tests__/end_to_end/packageByRegex.test.ts b/backend/src/handlers/__tests__/end_to_end/packageByRegex.test.ts index 9280f7f..59ec640 100644 --- a/backend/src/handlers/__tests__/end_to_end/packageByRegex.test.ts +++ b/backend/src/handlers/__tests__/end_to_end/packageByRegex.test.ts @@ -41,7 +41,7 @@ describe("E2E Test for PackageByRegEx Endpoint", () => { }, 90000); afterAll(async () => { // Reset the registry after running the tests - // await axios.delete(`${baseUrl}/reset`); + await axios.delete(`${baseUrl}/reset`); }, 90000); it("should return a 200 status for a package that exists", async () => { diff --git a/backend/src/handlers/__tests__/createAuthToken.test.ts b/backend/src/handlers/__tests__/integration/createAuthToken.test.ts similarity index 92% rename from backend/src/handlers/__tests__/createAuthToken.test.ts rename to backend/src/handlers/__tests__/integration/createAuthToken.test.ts index 2f52ac8..f2caf86 100644 --- a/backend/src/handlers/__tests__/createAuthToken.test.ts +++ b/backend/src/handlers/__tests__/integration/createAuthToken.test.ts @@ -1,4 +1,4 @@ -import { handler } from '../CreateAuthToken'; +import { handler } from '../../CreateAuthToken'; import { APIGatewayProxyEvent } from 'aws-lambda'; describe('Lambda Handler Tests', () => { diff --git a/backend/src/handlers/__tests__/getTracks.test.ts b/backend/src/handlers/__tests__/integration/getTracks.test.ts similarity index 94% rename from backend/src/handlers/__tests__/getTracks.test.ts rename to backend/src/handlers/__tests__/integration/getTracks.test.ts index 5fd0d09..553268c 100644 --- a/backend/src/handlers/__tests__/getTracks.test.ts +++ b/backend/src/handlers/__tests__/integration/getTracks.test.ts @@ -1,4 +1,4 @@ -import { handler } from '../GetTracks'; +import { handler } from '../../GetTracks'; import { APIGatewayProxyEvent } from 'aws-lambda'; describe('Lambda Handler Tests', () => { diff --git a/backend/src/handlers/__tests__/integration/packageByName.test.ts b/backend/src/handlers/__tests__/integration/packageByName.test.ts new file mode 100644 index 0000000..0c74656 --- /dev/null +++ b/backend/src/handlers/__tests__/integration/packageByName.test.ts @@ -0,0 +1,79 @@ +import { handler } from '../../PackageByNameGet'; // Replace with the correct path to your handler file +import { getPackageHistory } from '../../../common/dynamodb'; +import { User, PackageMetadata } from '../../../common/interfaces'; +import { APIGatewayProxyEvent } from 'aws-lambda'; + +jest.mock('@aws-sdk/lib-dynamodb', () => ({ + DynamoDBDocumentClient: { + from: jest.fn(), + }, +})); + +jest.mock('../../../common/dynamodb', () => ({ + getPackageHistory: jest.fn(), +})); + +describe('Lambda Function - Handler', () => { + const mockEvent: APIGatewayProxyEvent = { + body: null, + headers: {}, + multiValueHeaders: {}, + httpMethod: 'PUT', + isBase64Encoded: false, + path: '/package/byName/{name}', + pathParameters: { name: 'example-package' }, + queryStringParameters: null, + multiValueQueryStringParameters: null, + stageVariables: null, + requestContext: {} as APIGatewayProxyEvent['requestContext'], + resource: '' + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('should return 400 if package name is missing', async () => { + const eventWithoutName = { ...mockEvent, pathParameters: null }; + const result = await handler(eventWithoutName); + + expect(result.statusCode).toBe(400); + }); + + test('should return 404 if no history is found', async () => { + (getPackageHistory as jest.Mock).mockResolvedValue([]); + const result = await handler(mockEvent); + expect(result.statusCode).toBe(404); + }); + + test('should return 200 with package history', async () => { + const mockUser: User = { name: 'ece30861defaultadminuser', isAdmin: true }; + const mockMetadata: PackageMetadata = { + Name: 'example-package', + Version: '1.0.0', + ID: '01AFeio908233537g3dL', + }; + const mockHistory = [{ + User: mockUser, + Date: '2023-01-01T00:00:00Z', + PackageMetadata: mockMetadata, + Action: "CREATE", + }]; + (getPackageHistory as jest.Mock).mockResolvedValue(mockHistory); + + const result = await handler(mockEvent); + + expect(result.statusCode).toBe(200); + expect(result.headers?.['Access-Control-Allow-Origin']).toBe('*'); + expect(result.headers?.['Content-Type']).toBe('application/json'); + expect(JSON.parse(result.body)).toEqual(mockHistory); + }); + + test('should return 500 if an error occurs', async () => { + (getPackageHistory as jest.Mock).mockRejectedValue(new Error('DynamoDB error')); + + const result = await handler(mockEvent); + + expect(result.statusCode).toBe(500); + }); +}); diff --git a/backend/src/handlers/__tests__/integration/packageRate.test.ts b/backend/src/handlers/__tests__/integration/packageRate.test.ts new file mode 100644 index 0000000..07708af --- /dev/null +++ b/backend/src/handlers/__tests__/integration/packageRate.test.ts @@ -0,0 +1,98 @@ +import { APIGatewayProxyEvent } from 'aws-lambda'; +import { handler } from '../../PackageRate'; +import { getPackageById, updatePackageHistory } from '../../../common/dynamodb'; +import { PackageRating, PackageTableRow } from '../../../common/interfaces'; + + +jest.mock('../../../common/dynamodb', () => ({ + getPackageById: jest.fn(), + updatePackageHistory: jest.fn(), +})); + +describe('Lambda Function - Handler', () => { + const mockEvent: APIGatewayProxyEvent = { + body: null, + headers: {}, + multiValueHeaders: {}, + httpMethod: 'PUT', + isBase64Encoded: false, + path: '/authenticate', + pathParameters: { id: '123' }, + queryStringParameters: null, + multiValueQueryStringParameters: null, + stageVariables: null, + requestContext: {} as APIGatewayProxyEvent['requestContext'], + resource: '' + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should return 400 if package ID is missing', async () => { + const eventWithoutId = { ...mockEvent, pathParameters: null }; + const result = await handler(eventWithoutId); + + expect(result.statusCode).toBe(400); + }); + + it('should return 404 if package does not exist', async () => { + (getPackageById as jest.Mock).mockResolvedValue(null); + + const result = await handler(mockEvent); + + expect(result.statusCode).toBe(404); + expect(getPackageById).toHaveBeenCalledWith(expect.anything(), '123'); + }); + + it('should return 404 if package rating does not exist', async () => { + (getPackageById as jest.Mock).mockResolvedValue(null); + const result = await handler(mockEvent); + + expect(result.statusCode).toBe(404); + expect(getPackageById).toHaveBeenCalledWith(expect.anything(), '123'); + }); + + it('should return 200 with package rating', async () => { + const mockRating: PackageRating = { + NetScore: 0.834, + NetScoreLatency: 0.186, + RampUp: 0.500, + RampUpLatency: 0.145, + Correctness: 0.754, + CorrectnessLatency: 0.1, + BusFactor: 1, + BusFactorLatency: 0.1, + ResponsiveMaintainer: 0.259, + ResponsiveMaintainerLatency: 0.132, + LicenseScore: 1.000, + LicenseScoreLatency: 0.155, + GoodPinningPractice: 0.562, + GoodPinningPracticeLatency: 0.170, + PullRequest: 0.838, + PullRequestLatency: 0.102, + }; + + const mockRow: PackageTableRow = { + ID: '123', + PackageName: 'test-package', + Version: '1.0.0', + Rating: mockRating, + standaloneCost: 0.5, + }; + + (getPackageById as jest.Mock).mockResolvedValue(mockRow); + (updatePackageHistory as jest.Mock).mockResolvedValue(null); + const result = await handler(mockEvent); + + expect(result.statusCode).toBe(200); + expect(JSON.parse(result.body)).toEqual(mockRating); + expect(result.headers && result.headers['Content-Type']).toBe('application/json'); + }); + + it('should return 500 if an error occurs', async () => { + (getPackageById as jest.Mock).mockRejectedValue(new Error('DynamoDB Error')); + const result = await handler(mockEvent); + expect(result.statusCode).toBe(500); + }); +}); diff --git a/backend/src/services/rate/tools/api.ts b/backend/src/services/rate/tools/api.ts index fb90386..fd1a03c 100644 --- a/backend/src/services/rate/tools/api.ts +++ b/backend/src/services/rate/tools/api.ts @@ -46,12 +46,10 @@ export class npmAnalysis { try { const oid = await git.resolveRef({ fs, dir, ref: 'HEAD' }); const { tree } = await git.readTree({ fs, dir, oid }); - // console.log('tree', tree); const readmeEntry = tree.find(entry => ['readme.md', 'readme', 'readme.txt', 'readme.rst'].includes(entry.path.toLowerCase()) ); - // console.log('readmeEntry', readmeEntry); let readmeContent: string | null = null; if (readmeEntry) { @@ -86,7 +84,6 @@ export class npmAnalysis { this.logger.logDebug(`Finding time since last commit...`); try { const commits = await git.log({ fs, dir, depth: 1 }); - // console.log('commits:', commits); const lastCommit = commits[0]; if (lastCommit) { @@ -404,7 +401,6 @@ export class gitAnalysis { const packageJsonContentEncoded = packageJsonResponse.data.content; const packageJsonContent = Buffer.from(packageJsonContentEncoded, 'base64').toString('utf-8'); const packageJson = JSON.parse(packageJsonContent); - // console.log('packageJson:', packageJson); if (packageJson.license) { gitData.licenses = packageJson.license; this.logger.logDebug(`License found in package.json for ${gitData.repoName}`); diff --git a/backend/src/services/rate/tools/scripts.ts b/backend/src/services/rate/tools/scripts.ts index af6886a..5d06142 100644 --- a/backend/src/services/rate/tools/scripts.ts +++ b/backend/src/services/rate/tools/scripts.ts @@ -72,7 +72,7 @@ export class runAnalysis { } async evaluateMods(url: string, index: number): Promise { - const [type, cleanedUrl, version] = await this.urlAnalysis.evalUrl(url); + const [status, cleanedUrl, version] = await this.urlAnalysis.evalUrl(url); let repoData: repoData = { repoName: '', repoUrl: url, @@ -115,9 +115,10 @@ export class runAnalysis { } }; - if (type === -1 || cleanedUrl === '') { + if (status === -1 || cleanedUrl === '') { this.logger.logDebug(`Invalid URL - ${url}`); - return repoData; + throw new Error(`Invalid URL - ${url}`); + // return repoData; } const [npmData, gitData] = await Promise.all([ @@ -159,8 +160,6 @@ export class runAnalysis { pullRequests: gitData.latency.pullRequests } }; - - // console.log('RepoData:', repoData); return repoData; } } diff --git a/backend/template.yml b/backend/template.yml index 0c03708..374a22e 100644 --- a/backend/template.yml +++ b/backend/template.yml @@ -222,7 +222,7 @@ Resources: Runtime: nodejs18.x CodeUri: dist/handlers/RegistryReset MemorySize: 128 - Timeout: 60 + Timeout: 120 Layers: - !Ref DependenciesLayer Events: