diff --git a/package-lock.json b/package-lock.json index 2b68f4f..9b8bd1a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -70,7 +70,7 @@ "gulp-order": "^1.2.0", "gulp-uglify": "^3.0.2", "knip": "^5.1.0", - "mocha": "^10.4.0", + "mocha": "^10.8.2", "nodemon": "^3.1.0", "ts-node": "^10.9.2", "typescript": "^5.4.3", @@ -8222,10 +8222,11 @@ } }, "node_modules/ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -9584,10 +9585,11 @@ } }, "node_modules/diff": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", - "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.2.tgz", + "integrity": "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" } @@ -12546,31 +12548,32 @@ } }, "node_modules/mocha": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.4.0.tgz", - "integrity": "sha512-eqhGB8JKapEYcC4ytX/xrzKforgEc3j1pGlAXVy3eRwrtAy5/nIfT1SvgGzfN0XZZxeLq0aQWkOUAmqIJiv+bA==", + "version": "10.8.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.8.2.tgz", + "integrity": "sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-colors": "4.1.1", - "browser-stdout": "1.3.1", - "chokidar": "3.5.3", - "debug": "4.3.4", - "diff": "5.0.0", - "escape-string-regexp": "4.0.0", - "find-up": "5.0.0", - "glob": "8.1.0", - "he": "1.2.0", - "js-yaml": "4.1.0", - "log-symbols": "4.1.0", - "minimatch": "5.0.1", - "ms": "2.1.3", - "serialize-javascript": "6.0.0", - "strip-json-comments": "3.1.1", - "supports-color": "8.1.1", - "workerpool": "6.2.1", - "yargs": "16.2.0", - "yargs-parser": "20.2.4", - "yargs-unparser": "2.0.0" + "ansi-colors": "^4.1.3", + "browser-stdout": "^1.3.1", + "chokidar": "^3.5.3", + "debug": "^4.3.5", + "diff": "^5.2.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^8.1.0", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^5.1.6", + "ms": "^2.1.3", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^6.5.1", + "yargs": "^16.2.0", + "yargs-parser": "^20.2.9", + "yargs-unparser": "^2.0.0" }, "bin": { "_mocha": "bin/_mocha", @@ -12601,12 +12604,13 @@ } }, "node_modules/mocha/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -12617,12 +12621,6 @@ } } }, - "node_modules/mocha/node_modules/debug/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, "node_modules/mocha/node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -12655,10 +12653,11 @@ } }, "node_modules/mocha/node_modules/minimatch": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", - "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -14004,6 +14003,7 @@ "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", "dev": true, + "license": "MIT", "dependencies": { "safe-buffer": "^5.1.0" } @@ -14451,10 +14451,11 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/serialize-javascript": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", - "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "randombytes": "^2.1.0" } @@ -15710,10 +15711,11 @@ } }, "node_modules/workerpool": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", - "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", - "dev": true + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", + "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", + "dev": true, + "license": "Apache-2.0" }, "node_modules/wrap-ansi": { "version": "6.2.0", @@ -15774,10 +15776,11 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/yargs-parser": { - "version": "20.2.4", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", - "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", "dev": true, + "license": "ISC", "engines": { "node": ">=10" } @@ -22210,9 +22213,9 @@ } }, "ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", "dev": true }, "ansi-escapes": { @@ -23226,9 +23229,9 @@ "dev": true }, "diff": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", - "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.2.tgz", + "integrity": "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==", "dev": true }, "dom-serializer": { @@ -25398,31 +25401,31 @@ } }, "mocha": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.4.0.tgz", - "integrity": "sha512-eqhGB8JKapEYcC4ytX/xrzKforgEc3j1pGlAXVy3eRwrtAy5/nIfT1SvgGzfN0XZZxeLq0aQWkOUAmqIJiv+bA==", + "version": "10.8.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.8.2.tgz", + "integrity": "sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==", "dev": true, "requires": { - "ansi-colors": "4.1.1", - "browser-stdout": "1.3.1", - "chokidar": "3.5.3", - "debug": "4.3.4", - "diff": "5.0.0", - "escape-string-regexp": "4.0.0", - "find-up": "5.0.0", - "glob": "8.1.0", - "he": "1.2.0", - "js-yaml": "4.1.0", - "log-symbols": "4.1.0", - "minimatch": "5.0.1", - "ms": "2.1.3", - "serialize-javascript": "6.0.0", - "strip-json-comments": "3.1.1", - "supports-color": "8.1.1", - "workerpool": "6.2.1", - "yargs": "16.2.0", - "yargs-parser": "20.2.4", - "yargs-unparser": "2.0.0" + "ansi-colors": "^4.1.3", + "browser-stdout": "^1.3.1", + "chokidar": "^3.5.3", + "debug": "^4.3.5", + "diff": "^5.2.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^8.1.0", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^5.1.6", + "ms": "^2.1.3", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^6.5.1", + "yargs": "^16.2.0", + "yargs-parser": "^20.2.9", + "yargs-unparser": "^2.0.0" }, "dependencies": { "brace-expansion": { @@ -25446,20 +25449,12 @@ } }, "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "requires": { - "ms": "2.1.2" - }, - "dependencies": { - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } + "ms": "^2.1.3" } }, "escape-string-regexp": { @@ -25482,9 +25477,9 @@ } }, "minimatch": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", - "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", "dev": true, "requires": { "brace-expansion": "^2.0.1" @@ -26767,9 +26762,9 @@ } }, "serialize-javascript": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", - "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, "requires": { "randombytes": "^2.1.0" @@ -27716,9 +27711,9 @@ "dev": true }, "workerpool": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", - "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", + "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", "dev": true }, "wrap-ansi": { @@ -27764,9 +27759,9 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "yargs-parser": { - "version": "20.2.4", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", - "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", "dev": true }, "yargs-unparser": { diff --git a/package.json b/package.json index d7fcb0a..381fb15 100644 --- a/package.json +++ b/package.json @@ -89,7 +89,7 @@ "gulp-order": "^1.2.0", "gulp-uglify": "^3.0.2", "knip": "^5.1.0", - "mocha": "^10.4.0", + "mocha": "^10.8.2", "nodemon": "^3.1.0", "ts-node": "^10.9.2", "typescript": "^5.4.3", diff --git a/public/i18n/locale-en.json b/public/i18n/locale-en.json index 920c1d7..658ff8e 100644 --- a/public/i18n/locale-en.json +++ b/public/i18n/locale-en.json @@ -3,37 +3,82 @@ "unknown_error": "Unknown error, contact the admin.", "unreachable": "Anonymous GitHub is unreachable, contact the admin.", "request_error": "Unable to download the file, check your connection or contact the admin.", + "not_found": "The requested resource could not be found.", + "not_connected": "You must be logged in to perform this action.", + "not_authorized": "You do not have permission to perform this action.", + "unable_to_connect_user": "Unable to connect your account. Please try again later.", + "user_not_found": "The requested user could not be found.", "repo_access_limited": "Access to repository limited by org.", "repo_not_found": "The repository is not found.", "repo_not_accessible": "Anonymous GitHub is unable to or is forbidden to access the repository.", - "repository_expired": "The repository is expired", + "repository_expired": "The repository is expired.", "repository_not_ready": "Anonymous GitHub is still processing the repository, it can take several minutes.", + "repository_not_accessible": "This repository is currently not accessible.", "repo_is_updating": "Anonymous GitHub is still processing the repository, it can take several minutes.", - "repoUrl_not_defined": "The repository url needs to be defined.", - "repoId_already_used": "The repository id is already used.", - "invalid_repoId": "The format of the repository id is invalid.", + "invalid_repo": "The provided repository is not valid.", + "repoId_not_defined": "A repository ID must be provided.", + "repoUrl_not_defined": "The repository URL needs to be defined.", + "repoId_already_used": "The repository ID is already used.", + "invalid_repoId": "The format of the repository ID is invalid.", + "unsupported_source": "The repository source type is not supported.", "branch_not_specified": "The branch is not specified.", "branch_not_found": "The branch of the repository cannot be found.", + "commit_not_specified": "A commit must be specified.", + "invalid_commit_format": "The commit hash format is invalid. It must be a hexadecimal string.", + "pull_request_not_found": "The requested pull request could not be found.", + "pull_request_expired": "This pull request has expired and is no longer available.", + "pull_request_not_ready": "This pull request is still being prepared. Please try again shortly.", + "pull_request_not_available": "This pull request is currently not available.", + "invalid_pullRequestId": "The pull request ID is invalid.", + "pullRequestId_already_used": "This pull request ID is already in use. Please choose a different one.", + "repository_not_specified": "A repository must be specified for this pull request.", + "pullRequestId_not_specified": "A pull request ID must be specified.", + "pullRequestId_is_not_a_number": "The source pull request ID must be a number.", "options_not_provided": "Anonymization options are mandatory.", + "terms_not_specified": "Anonymization terms must be specified.", "invalid_terms_format": "Terms are in an invalid format.", "unable_to_anonymize": "An error happened during the anonymization process. Please try later or report the issue.", "non_supported_mode": "The selected anonymization mode is invalid, only download and stream are supported.", "invalid_path": "The provided path is invalid or missing.", + "path_not_specified": "A file path must be specified.", + "path_not_defined": "The file path has not been resolved yet.", "no_file_selected": "Please select a file.", "file_not_found": "The requested file is not found.", "file_not_accessible": "The requested file is not accessible.", "file_not_supported": "The file type is not supported. Anonymous GitHub cannot handle it.", - "file_too_big": "The file size exceed the limit of Anonymous GitHub.", + "file_too_big": "The file size exceeds the limit of Anonymous GitHub.", "is_folder": "The path points to a folder.", "folder_not_supported": "The path points to a folder. Please select a file.", "unable_to_write_file": "Unable to write file on disk.", + "download_not_enabled": "Repository downloads are not enabled on this server.", + "unable_to_download": "The repository could not be downloaded. Please try again later.", + "s3_config_not_provided": "Object storage has not been configured on this server.", "stats_unsupported": "Statistics are only supported in download mode.", "branches_not_found": "The requested branch is not found.", - "readme_not_available": "No readme for the repository is found.", - "user_not_found": "The current user is not found in the database", - "not_connected": "User is not connected", + "readme_not_available": "No README for the repository is found.", "page_not_supported_on_different_branch": "Anonymized GitHub pages are only supported on the same branch.", "page_not_activated": "Anonymized GitHub page is not enabled.", - "conf_not_activated": "The conference is not activated." + "is_removed": "This resource has been removed and is no longer available.", + "conf_name_missing": "A conference name is required.", + "conf_id_missing": "A conference ID is required.", + "conf_id_used": "This conference ID is already in use. Please choose a different one.", + "conf_start_date_missing": "A start date is required for the conference.", + "conf_end_date_missing": "An end date is required for the conference.", + "conf_start_date_invalid": "The start date must be before the end date.", + "conf_end_date_invalid": "The end date must be in the future.", + "invalid_plan": "The selected plan is not valid.", + "conference_not_found": "The requested conference could not be found.", + "conf_not_found": "The requested conference could not be found.", + "conf_not_activated": "The conference is not activated.", + "billing_missing": "Billing information is required for this plan.", + "billing_name_missing": "A billing name is required.", + "billing_email_missing": "A billing email is required.", + "billing_address_missing": "A billing address is required.", + "billing_city_missing": "A billing city is required.", + "billing_zip_missing": "A billing ZIP/postal code is required.", + "billing_country_missing": "A billing country is required.", + "queue_not_found": "The specified queue could not be found.", + "job_not_found": "The specified job could not be found in the queue.", + "error_retrying_job": "An error occurred while retrying the job." } } diff --git a/src/core/PullRequest.ts b/src/core/PullRequest.ts index 3696341..4bdfabd 100644 --- a/src/core/PullRequest.ts +++ b/src/core/PullRequest.ts @@ -114,7 +114,7 @@ export default class PullRequest { this.status == "removing" || this.status == "removed" ) { - throw new AnonymousError("pullRequest_expired", { + throw new AnonymousError("pull_request_expired", { object: this, httpStatus: 410, }); @@ -126,8 +126,9 @@ export default class PullRequest { this.status == "preparing" || (this.status == "download" && this._model.statusDate > fiveMinuteAgo) ) { - throw new AnonymousError("pullRequest_not_ready", { + throw new AnonymousError("pull_request_not_ready", { object: this, + httpStatus: 503, }); } } diff --git a/src/core/Repository.ts b/src/core/Repository.ts index 7113928..aceaf69 100644 --- a/src/core/Repository.ts +++ b/src/core/Repository.ts @@ -220,6 +220,7 @@ export default class Repository { ) { throw new AnonymousError("repository_not_ready", { object: this, + httpStatus: 503, }); } } @@ -356,6 +357,7 @@ export default class Repository { await this.resetSate(); throw new AnonymousError("branch_not_found", { object: this, + httpStatus: 404, }); } this._model.anonymizeDate = new Date(); diff --git a/src/core/storage/S3.ts b/src/core/storage/S3.ts index 35e71f8..6d58797 100644 --- a/src/core/storage/S3.ts +++ b/src/core/storage/S3.ts @@ -127,7 +127,7 @@ export default class S3Storage extends StorageBase { } } catch { try { - res.status(500); + res.status(500).json({ error: "file_not_found" }); } catch (err) { console.error(`[ERROR] S3 send ${path}`, err); } diff --git a/src/server/index.ts b/src/server/index.ts index a179683..22bb760 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -27,7 +27,7 @@ function indexResponse(req: express.Request, res: express.Response) { req.path.startsWith("/favicon") || req.path.startsWith("/api") ) { - return res.status(404).send("Not found"); + return res.status(404).json({ error: "not_found" }); } if ( req.params.repoId && diff --git a/src/server/routes/admin.ts b/src/server/routes/admin.ts index d0c681d..bc671a8 100644 --- a/src/server/routes/admin.ts +++ b/src/server/routes/admin.ts @@ -59,7 +59,7 @@ router.post("/queue/:name/:repo_id", async (req, res) => { } res.send("ok"); } catch { - res.status(500).send("error_retrying_job"); + res.status(500).json({ error: "error_retrying_job" }); } } }); diff --git a/src/server/routes/route-utils.ts b/src/server/routes/route-utils.ts index ba57613..b56463e 100644 --- a/src/server/routes/route-utils.ts +++ b/src/server/routes/route-utils.ts @@ -102,9 +102,9 @@ export function handleError( req?: express.Request ) { printError(error, req); - let message = error; + let errorCode = error; if (error instanceof Error) { - message = error.message; + errorCode = error.message; } let status = 500; if (error.httpStatus) { @@ -112,15 +112,16 @@ export function handleError( } else if (error.$metadata?.httpStatusCode) { status = error.$metadata.httpStatusCode; } else if ( - message && - (message.indexOf("not_found") > -1 || message.indexOf("(Not Found)") > -1) + errorCode && + (errorCode.indexOf("not_found") > -1 || + errorCode.indexOf("(Not Found)") > -1) ) { status = 404; - } else if (message && message.indexOf("not_connected") > -1) { + } else if (errorCode && errorCode.indexOf("not_connected") > -1) { status = 401; } if (res && !res.headersSent) { - res.status(status).send({ error: message }); + res.status(status).json({ error: errorCode }); } return; } diff --git a/test/error-codes.test.js b/test/error-codes.test.js new file mode 100644 index 0000000..ea1f52a --- /dev/null +++ b/test/error-codes.test.js @@ -0,0 +1,161 @@ +const { expect } = require("chai"); +const { readFileSync, readdirSync, statSync } = require("fs"); +const { join } = require("path"); + +const LOCALE_PATH = join(__dirname, "..", "public", "i18n", "locale-en.json"); +const SRC_DIR = join(__dirname, "..", "src"); + +/** + * Collect all .ts files under a directory recursively. + */ +function collectTsFiles(dir) { + const files = []; + for (const entry of readdirSync(dir)) { + const full = join(dir, entry); + if (statSync(full).isDirectory()) { + files.push(...collectTsFiles(full)); + } else if (full.endsWith(".ts")) { + files.push(full); + } + } + return files; +} + +/** + * Extract error codes from backend source files. + * + * Matches patterns such as: + * 1. new AnonymousError("error_code", ...) -- thrown errors + * 2. res.status(NNN).json({ error: "code" }) -- direct responses + * + * Only string literals that look like error codes (contain at least one + * underscore, indicating a snake_case identifier) are extracted. + */ +function extractBackendErrorCodes(files) { + const codes = new Map(); // code -> [{ file, line }] + + const patterns = [ + // new AnonymousError("code") -- including across ternary expressions + // e.g. new AnonymousError("repo_not_found", + // new AnonymousError(condition ? msg : "fallback_code", + /AnonymousError\([^)]*["']([a-zA-Z][a-zA-Z0-9]*(?:_[a-zA-Z0-9]+)+)["']/, + // { error: "code" } -- direct JSON responses + /\{\s*error:\s*["']([a-zA-Z][a-zA-Z0-9]*(?:_[a-zA-Z0-9]+)+)["']/, + ]; + + for (const file of files) { + const content = readFileSync(file, "utf-8"); + const lines = content.split("\n"); + const relPath = file.replace(join(__dirname, "..") + "/", ""); + + // Per-line matching for simple cases + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (/^\s*(import |\/\/|\/\*|\* )/.test(line)) continue; + + for (const pattern of patterns) { + const match = line.match(pattern); + if (match) { + const code = match[1]; + if (!codes.has(code)) { + codes.set(code, []); + } + codes.get(code).push({ file: relPath, line: i + 1 }); + } + } + } + + // Multi-line matching for AnonymousError calls that span lines + // (e.g. ternary expressions where the string is on the next line) + const multiLinePattern = + /AnonymousError\([\s\S]*?["']([a-zA-Z][a-zA-Z0-9]*(?:_[a-zA-Z0-9]+)+)["']/g; + let m; + while ((m = multiLinePattern.exec(content)) !== null) { + const code = m[1]; + const lineNum = + content.substring(0, m.index + m[0].length).split("\n").length; + if (!codes.has(code)) { + codes.set(code, []); + } + // Avoid duplicate entries from the per-line pass + const existing = codes.get(code); + if (!existing.some((e) => e.file === relPath && e.line === lineNum)) { + existing.push({ file: relPath, line: lineNum }); + } + } + } + + return codes; +} + +describe("Error code coverage", function () { + let localeErrors; + let backendCodes; + + before(function () { + const locale = JSON.parse(readFileSync(LOCALE_PATH, "utf-8")); + localeErrors = locale.ERRORS || {}; + backendCodes = extractBackendErrorCodes(collectTsFiles(SRC_DIR)); + }); + + it("locale file is valid JSON with an ERRORS object", function () { + expect(localeErrors).to.be.an("object").that.is.not.empty; + }); + + it("every backend error code has a frontend translation", function () { + const missing = []; + for (const [code, locations] of backendCodes) { + if (!localeErrors[code]) { + const where = locations + .map((l) => ` ${l.file}:${l.line}`) + .join("\n"); + missing.push(` "${code}" used in:\n${where}`); + } + } + expect(missing, `Missing translations:\n${missing.join("\n")}`).to.have + .length(0); + }); + + it("every frontend translation corresponds to a backend error code", function () { + const unused = []; + for (const code of Object.keys(localeErrors)) { + if (!backendCodes.has(code)) { + unused.push(` "${code}"`); + } + } + // This is a warning, not a hard failure -- some codes may only be used + // on the frontend itself (e.g. "unreachable", "request_error"). + // We report them so developers can clean up stale entries. + if (unused.length > 0) { + console.log( + ` ⚠ ${unused.length} locale key(s) not found in backend source (frontend-only or stale):\n${unused.join("\n")}` + ); + } + }); + + it("locale error messages are non-empty strings", function () { + const empty = []; + for (const [code, message] of Object.entries(localeErrors)) { + if (typeof message !== "string" || message.trim() === "") { + empty.push(code); + } + } + expect(empty, `Empty/invalid messages for: ${empty.join(", ")}`).to.have + .length(0); + }); + + it("backend error codes use consistent snake_case format", function () { + const invalid = []; + // Allow snake_case with optional camelCase segments (e.g. pullRequestId_not_specified) + const validPattern = /^[a-zA-Z][a-zA-Z0-9]*(_[a-zA-Z0-9]+)*$/; + for (const code of backendCodes.keys()) { + if (!validPattern.test(code)) { + invalid.push(code); + } + } + expect( + invalid, + `Invalid error code format: ${invalid.join(", ")}` + ).to.have.length(0); + }); +});