diff --git a/package-lock.json b/package-lock.json index 8b205f4eb..fc7f106e9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,7 +37,7 @@ "mongoose": "8.19.4", "mqtt": "5.14.1", "rxjs": "7.8.2", - "webdav": "5.8.0", + "webdav": "^4.11.2", "ws": "8.18.3" }, "devDependencies": { @@ -737,15 +737,6 @@ "url": "https://github.com/sponsors/Borewit" } }, - "node_modules/@buttercup/fetch": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@buttercup/fetch/-/fetch-0.2.1.tgz", - "integrity": "sha512-sCgECOx8wiqY8NN1xN22BqqKzXYIG2AicNLlakOAI4f0WgyLVUbAigMf8CZhBtJxdudTcB1gD5lciqi44jwJvg==", - "license": "MIT", - "optionalDependencies": { - "node-fetch": "^3.3.0" - } - }, "node_modules/@casl/ability": { "version": "6.7.3", "resolved": "https://registry.npmjs.org/@casl/ability/-/ability-6.7.3.tgz", @@ -4206,6 +4197,20 @@ "tslib": "^2.4.0" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", + "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", + "dependencies": { + "follow-redirects": "^1.14.9", + "form-data": "^4.0.0" + } + }, "node_modules/b4a": { "version": "1.7.3", "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz", @@ -4344,8 +4349,7 @@ "node_modules/base-64": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/base-64/-/base-64-1.0.0.tgz", - "integrity": "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==", - "license": "MIT" + "integrity": "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==" }, "node_modules/base64-js": { "version": "1.5.1", @@ -4637,8 +4641,7 @@ "node_modules/byte-length": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/byte-length/-/byte-length-1.0.2.tgz", - "integrity": "sha512-ovBpjmsgd/teRmgcPh23d4gJvxDoXtAzEL9xTfMU8Yc2kqCDb7L9jAG0XHl1nzuGl+h3ebCIF1i62UFyA9V/2Q==", - "license": "MIT" + "integrity": "sha512-ovBpjmsgd/teRmgcPh23d4gJvxDoXtAzEL9xTfMU8Yc2kqCDb7L9jAG0XHl1nzuGl+h3ebCIF1i62UFyA9V/2Q==" }, "node_modules/bytes": { "version": "3.1.2", @@ -4757,7 +4760,6 @@ "version": "0.0.2", "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", - "license": "BSD-3-Clause", "engines": { "node": "*" } @@ -4993,6 +4995,17 @@ "dev": true, "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -5248,20 +5261,10 @@ "version": "0.0.2", "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", - "license": "BSD-3-Clause", "engines": { "node": "*" } }, - "node_modules/data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -5324,6 +5327,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/denque": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", @@ -5470,18 +5481,6 @@ "node": ">=10.13.0" } }, - "node_modules/entities": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -5529,6 +5528,20 @@ "node": ">= 0.4" } }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -6049,21 +6062,18 @@ "license": "BSD-3-Clause" }, "node_modules/fast-xml-parser": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz", - "integrity": "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", + "version": "3.21.1", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-3.21.1.tgz", + "integrity": "sha512-FTFVjYoBOZTJekiUsawGsSYV9QL0A+zDYCRj7y34IO6Jg+2IMYEtQa+bbictpdpV8dHxXywqU7C0gRDEOFtBFg==", "dependencies": { - "strnum": "^1.1.1" + "strnum": "^1.0.4" }, "bin": { - "fxparser": "src/cli/cli.js" + "xml2js": "cli.js" + }, + "funding": { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" } }, "node_modules/fastq": { @@ -6086,29 +6096,6 @@ "bser": "2.1.1" } }, - "node_modules/fetch-blob": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "paypal", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "dependencies": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" - }, - "engines": { - "node": "^12.20 || >= 14.13" - } - }, "node_modules/fflate": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", @@ -6262,7 +6249,6 @@ "version": "1.15.11", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", - "dev": true, "funding": [ { "type": "individual", @@ -6324,16 +6310,38 @@ "webpack": "^5.11.0" } }, - "node_modules/formdata-polyfill": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", - "license": "MIT", + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "dependencies": { - "fetch-blob": "^3.1.2" + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" }, "engines": { - "node": ">=12.20.0" + "node": ">= 6" + } + }, + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" } }, "node_modules/forwarded": { @@ -6662,6 +6670,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -6674,6 +6696,14 @@ "node": ">= 0.4" } }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "bin": { + "he": "bin/he" + } + }, "node_modules/help-me": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", @@ -6681,10 +6711,9 @@ "license": "MIT" }, "node_modules/hot-patcher": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/hot-patcher/-/hot-patcher-2.0.1.tgz", - "integrity": "sha512-ECg1JFG0YzehicQaogenlcs2qg6WsXQsxtnbr1i696u5tLUjtJdQAh0u2g0Q5YV45f263Ta1GnUJsc8WIfJf4Q==", - "license": "MIT" + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hot-patcher/-/hot-patcher-1.0.0.tgz", + "integrity": "sha512-3H8VH0PreeNsKMZw16nTHbUp4YoHCnPlawpsPXGJUR4qENDynl79b6Xk9CIFvLcH1qungBsCuzKcWyzoPPalTw==" }, "node_modules/html-escaper": { "version": "2.0.2", @@ -6941,8 +6970,7 @@ "node_modules/is-buffer": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "license": "MIT" + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" }, "node_modules/is-extglob": { "version": "2.1.1", @@ -8106,10 +8134,9 @@ } }, "node_modules/layerr": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/layerr/-/layerr-3.0.0.tgz", - "integrity": "sha512-tv754Ki2dXpPVApOrjTyRo4/QegVb9eVFq4mjqp4+NM5NaX7syQvN5BBNfV/ZpAHCEHV24XdUVrBAoka4jt3pA==", - "license": "MIT" + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/layerr/-/layerr-0.1.2.tgz", + "integrity": "sha512-ob5kTd9H3S4GOG2nVXyQhOu9O8nBgP555XxWPkJI0tR0JeRilfyTp8WtPdIJHLXBmHMSdEq5+KMxiYABeScsIQ==" }, "node_modules/leven": { "version": "3.1.0", @@ -8359,7 +8386,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", - "license": "BSD-3-Clause", "dependencies": { "charenc": "0.0.2", "crypt": "0.0.2", @@ -8888,8 +8914,7 @@ "node_modules/nested-property": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/nested-property/-/nested-property-4.0.0.tgz", - "integrity": "sha512-yFehXNWRs4cM0+dz7QxCd06hTbWbSkV0ISsqBfkntU6TOY4Qm3Q88fRRLOddkGh2Qq6dZvnKVAahfhjcUvLnyA==", - "license": "MIT" + "integrity": "sha512-yFehXNWRs4cM0+dz7QxCd06hTbWbSkV0ISsqBfkntU6TOY4Qm3Q88fRRLOddkGh2Qq6dZvnKVAahfhjcUvLnyA==" }, "node_modules/new-find-package-json": { "version": "2.0.0", @@ -8919,26 +8944,6 @@ "node": "^18 || ^20 || >= 21" } }, - "node_modules/node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "deprecated": "Use your platform's native DOMException instead", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "engines": { - "node": ">=10.5.0" - } - }, "node_modules/node-emoji": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", @@ -8949,24 +8954,6 @@ "lodash": "^4.17.21" } }, - "node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", - "license": "MIT", - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" - } - }, "node_modules/node-gyp-build": { "version": "4.8.4", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", @@ -9462,8 +9449,7 @@ "node_modules/path-posix": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/path-posix/-/path-posix-1.0.0.tgz", - "integrity": "sha512-1gJ0WpNIiYcQydgg3Ed8KzvIqTsDpNwq+cjBCssvBtuTWjEqY1AW+i+OepiEMqDCzyro9B2sLAe4RBPajMYFiA==", - "license": "ISC" + "integrity": "sha512-1gJ0WpNIiYcQydgg3Ed8KzvIqTsDpNwq+cjBCssvBtuTWjEqY1AW+i+OepiEMqDCzyro9B2sLAe4RBPajMYFiA==" }, "node_modules/path-scurry": { "version": "2.0.0", @@ -9761,8 +9747,7 @@ "node_modules/querystringify": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", - "license": "MIT" + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" }, "node_modules/queue-microtask": { "version": "1.2.3", @@ -9916,8 +9901,7 @@ "node_modules/requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", - "license": "MIT" + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" }, "node_modules/resolve-cwd": { "version": "3.0.0", @@ -10660,8 +10644,7 @@ "type": "github", "url": "https://github.com/sponsors/NaturalIntelligence" } - ], - "license": "MIT" + ] }, "node_modules/strtok3": { "version": "10.3.4", @@ -11437,19 +11420,14 @@ } }, "node_modules/url-join": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/url-join/-/url-join-5.0.0.tgz", - "integrity": "sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA==", - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - } + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", + "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==" }, "node_modules/url-parse": { "version": "1.5.10", "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", - "license": "MIT", "dependencies": { "querystringify": "^2.1.1", "requires-port": "^1.0.0" @@ -11544,62 +11522,46 @@ "defaults": "^1.0.3" } }, - "node_modules/web-streams-polyfill": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", - "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, "node_modules/webdav": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/webdav/-/webdav-5.8.0.tgz", - "integrity": "sha512-iuFG7NamJ41Oshg4930iQgfIpRrUiatPWIekeznYgEf2EOraTRcDPTjy7gIOMtkdpKTaqPk1E68NO5PAGtJahA==", - "license": "MIT", + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/webdav/-/webdav-4.11.2.tgz", + "integrity": "sha512-Ht9TPD5EB7gYW0YmhRcE5NW0/dn/HQfyLSPQY1Rw1coQ5MQTUooAQ9Bpqt4EU7QLw0b95tX4cU59R+SIojs9KQ==", "dependencies": { - "@buttercup/fetch": "^0.2.1", + "axios": "^0.27.2", "base-64": "^1.0.0", "byte-length": "^1.0.2", - "entities": "^6.0.0", - "fast-xml-parser": "^4.5.1", - "hot-patcher": "^2.0.1", - "layerr": "^3.0.0", + "fast-xml-parser": "^3.19.0", + "he": "^1.2.0", + "hot-patcher": "^1.0.0", + "layerr": "^0.1.2", "md5": "^2.3.0", - "minimatch": "^9.0.5", + "minimatch": "^5.1.0", "nested-property": "^4.0.0", - "node-fetch": "^3.3.2", "path-posix": "^1.0.0", - "url-join": "^5.0.0", + "url-join": "^4.0.1", "url-parse": "^1.5.10" }, "engines": { - "node": ">=14" + "node": ">=10" } }, "node_modules/webdav/node_modules/brace-expansion": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } }, "node_modules/webdav/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "license": "ISC", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "dependencies": { "brace-expansion": "^2.0.1" }, "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=10" } }, "node_modules/webidl-conversions": { diff --git a/package.json b/package.json index 688be8bfc..c9482a259 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "mongoose": "8.19.4", "mqtt": "5.14.1", "rxjs": "7.8.2", - "webdav": "5.8.0", + "webdav": "^4.11.2", "ws": "8.18.3" }, "devDependencies": { @@ -81,4 +81,4 @@ "typescript": "5.9.3", "typescript-eslint": "8.46.4" } -} \ No newline at end of file +} diff --git a/src/__tests__/friendship/data/builder/FriendRequestDtoBuilder.ts b/src/__tests__/friendship/data/builder/FriendRequestDtoBuilder.ts new file mode 100644 index 000000000..15be39847 --- /dev/null +++ b/src/__tests__/friendship/data/builder/FriendRequestDtoBuilder.ts @@ -0,0 +1,32 @@ +import { ObjectId } from "mongodb"; +import IDataBuilder from "../../../test_utils/interface/IDataBuilder"; +import { FriendRequestDto } from "../../../../friendship/dto/FriendRequest.dto"; + +export default class FriendRequestDtoBuilder + implements IDataBuilder +{ + private readonly base: FriendRequestDto = { + friendship_id: new ObjectId().toString(), + direction: 'incoming', // 'outgoing' + friend: undefined, + } + + build(): FriendRequestDto { + return {...this.base} as FriendRequestDto; + } + + setFriendship_id(friendship_id: string): this { + this.base.friendship_id = friendship_id; + return this + } + + setDirection(direction: string | undefined): this { + this.base.direction = direction; + return this; + } + + setFriend(friend: any): this { + this.base.friend = friend; + return this; + } +} \ No newline at end of file diff --git a/src/__tests__/friendship/data/builder/FriendlistDtoBuilder.ts b/src/__tests__/friendship/data/builder/FriendlistDtoBuilder.ts new file mode 100644 index 000000000..f94a920a2 --- /dev/null +++ b/src/__tests__/friendship/data/builder/FriendlistDtoBuilder.ts @@ -0,0 +1,44 @@ +import { ObjectId } from "mongodb"; +import IDataBuilder from "../../../test_utils/interface/IDataBuilder"; +import { FriendlistDto } from "../../../../friendship/dto/friend-list.dto"; + +export default class FriendlistDtoBuilder + implements IDataBuilder +{ + private readonly base: FriendlistDto = { + _id: new ObjectId().toString(), + name: "defaultName", + avatar: undefined, + clanName: undefined, + clan_id: undefined, + } + + build(): FriendlistDto { + return {...this.base} as FriendlistDto; + } + + setId(Id: string): this { + this.base._id = Id; + return this; + } + + setName(name: string): this { + this.base.name = name; + return this; + } + + setAvatar(avatar: any | undefined): this { + this.base.avatar = avatar; + return this; + } + + setClanName(clanName: string | undefined): this { + this.base.clanName = clanName; + return this; + } + + setClanId(clanId: string | undefined): this { + this.base.clan_id = clanId; + return this; + } +} \ No newline at end of file diff --git a/src/__tests__/friendship/data/builder/FriendshipBuilder.ts b/src/__tests__/friendship/data/builder/FriendshipBuilder.ts new file mode 100644 index 000000000..9eb9c4849 --- /dev/null +++ b/src/__tests__/friendship/data/builder/FriendshipBuilder.ts @@ -0,0 +1,47 @@ +import IDataBuilder from '../../../test_utils/interface/IDataBuilder'; +import { FriendshipStatus } from '../../../../friendship/enum/friendship-status.enum'; +import { Friendship } from '../../../../friendship/friendship.schema'; +import { ObjectId } from 'mongodb'; + +export default class FriendshipBuilder implements IDataBuilder { + private playerA: string | ObjectId; + private playerB: string | ObjectId; + private status: FriendshipStatus; + private requester?: string | ObjectId; + private pairKey?: string; + + build(): Friendship { + const friendship: any = { + playerA: this.playerA, + playerB: this.playerB, + status: this.status, + pairKey: this.pairKey, + }; + + if (this.requester !== undefined) { + friendship.requester = this.requester; + } + + return friendship as Friendship; + } + + setPlayerA(playerA: string | ObjectId): this { + this.playerA = playerA; + return this; + } + + setPlayerB(playerB: string | ObjectId): this { + this.playerB = playerB; + return this; + } + + setStatus(status: FriendshipStatus): this { + this.status = status; + return this; + } + + setRequester(requester: string | ObjectId): this { + this.requester = requester; + return this; + } +} diff --git a/src/__tests__/friendship/data/friendshipBuilderFactory.ts b/src/__tests__/friendship/data/friendshipBuilderFactory.ts new file mode 100644 index 000000000..06d8496cc --- /dev/null +++ b/src/__tests__/friendship/data/friendshipBuilderFactory.ts @@ -0,0 +1,32 @@ +import FriendlistDtoBuilder from "./builder/FriendlistDtoBuilder"; +import FriendRequestDtoBuilder from "./builder/FriendRequestDtoBuilder"; +import FriendshipBuilder from "./builder/FriendshipBuilder"; + +type BuilderName = + | 'FriendlistDto' + | 'FriendRequestDto' + | 'Friendship'; + +type BuilderMap = { + FriendlistDto: FriendlistDtoBuilder; + FriendRequestDto: FriendRequestDtoBuilder; + Friendship: FriendshipBuilder; +}; + +export default class FriendshipBuilderFactory { + static getBuilder(builderName: T): BuilderMap[T] { + switch (builderName) { + case 'FriendlistDto': + return new FriendlistDtoBuilder() as BuilderMap[T]; + + case 'FriendRequestDto': + return new FriendRequestDtoBuilder() as BuilderMap[T]; + + case 'Friendship': + return new FriendshipBuilder() as BuilderMap[T]; + + default: + throw new Error(`Unknown builder name: ${builderName}`); + } + } +} \ No newline at end of file diff --git a/src/__tests__/friendship/data/mockData/createData.mock.ts b/src/__tests__/friendship/data/mockData/createData.mock.ts new file mode 100644 index 000000000..ed5e4e318 --- /dev/null +++ b/src/__tests__/friendship/data/mockData/createData.mock.ts @@ -0,0 +1,55 @@ +import { Player } from '../../../../player/schemas/player.schema'; +import FriendshipModule from '../../modules/friendship.module'; +import PlayerBuilder from '../../../player/data/player/playerBuilder'; +import { Clan } from '../../../../clan/clan.schema'; +import ClanBuilder from '../../../clan/data/clan/ClanBuilder'; +import { Friendship } from 'src/friendship/friendship.schema'; +import FriendshipBuilderFactory from '../friendshipBuilderFactory'; + +export async function createMockFriendships( + overides: Partial[] +): Promise { + const model = FriendshipModule.getFriendshipModel(); + + for (const overide of overides) { + const builder = FriendshipBuilderFactory.getBuilder('Friendship') + .setPlayerA(overide.playerA) + .setPlayerB(overide.playerB) + .setStatus(overide.status); + + if (overide.requester) builder.setRequester(overide.requester); + await model.create(builder.build()); + } +} + +export async function createMockPlayers( + overides: Partial[] +): Promise { + const model = FriendshipModule.getPlayerModel(); + + for (const overide of overides) { + const builder = new PlayerBuilder() + .setId(overide._id) + .setName(overide.name) + .setUniqueIdentifier(overide.uniqueIdentifier) + .setClanId(overide.clan_id) + .build(); + + await model.create(builder); + } +} + +export async function createMockClans( + overides: Partial[] +): Promise { + const model = FriendshipModule.getClanModel(); + + for (const overide of overides) { + const builder = new ClanBuilder() + .setId(overide._id) + .setName(overide.name) + .build(); + + await model.create(builder); + } +} diff --git a/src/__tests__/friendship/friendshipService/addFriend.test.ts b/src/__tests__/friendship/friendshipService/addFriend.test.ts new file mode 100644 index 000000000..bbfaf3522 --- /dev/null +++ b/src/__tests__/friendship/friendshipService/addFriend.test.ts @@ -0,0 +1,94 @@ +import { ObjectId } from 'mongodb'; +import { FriendshipService } from '../../../friendship/friendship.service'; +import FriendshipModule from '../modules/friendship.module'; +import { FriendshipStatus } from '../../../friendship/enum/friendship-status.enum'; +import { + createMockClans, + createMockFriendships, + createMockPlayers, +} from '../data/mockData/createData.mock'; +import { Friendship } from 'src/friendship/friendship.schema'; + +describe('FriendshipService.sendNewFriendRequestNotification()', () => { + let friendshipService: FriendshipService; + + const player1_id = new ObjectId().toString(); + const player2_id = new ObjectId().toString(); + const player3_id = new ObjectId().toString(); + + const clan_id = new ObjectId().toString(); + + const friendshipConfigs: Partial[] = [ + { + playerA: player1_id, + playerB: player2_id, + status: FriendshipStatus.PENDING, + requester: player1_id, + }, + { + playerA: player1_id, + playerB: player3_id, + status: FriendshipStatus.ACCEPTED, + }, + ]; + + beforeEach(async () => { + friendshipService = await FriendshipModule.getFriendshipService(); + + await createMockClans([{ _id: clan_id, name: 'TestClan' }]); + await createMockPlayers([ + { + _id: player1_id, + name: 'Player1', + clan_id, + uniqueIdentifier: 'unique-1', + }, + { + _id: player2_id, + name: 'Player2', + clan_id, + uniqueIdentifier: 'unique-2', + }, + { + _id: player3_id, + name: 'Player3', + clan_id, + uniqueIdentifier: 'unique-3', + }, + ]); + await createMockFriendships(friendshipConfigs); + }); + + it('Should return undefined if addFriend is successful', async () => { + // void function if success + await expect(friendshipService.addFriend(player2_id, player3_id)) + .resolves + .toBeUndefined(); + }); + + it( + 'Should return NOT_UNIQUE from pairkey if 2 players have friendship with status PENDING', + async () => { + const [friendship, err] = await friendshipService.addFriend( + player1_id, + player2_id + ); + + expect(err).toContainSE_NOT_UNIQUE(); + expect(friendship).toBeNull(); + } + ); + + it( + 'Should return NOT_UNIQUE from pairkey if 2 players have friendship with status ACCEPTED', + async () => { + const [friendship, err] = await friendshipService.addFriend( + player1_id, + player3_id + ); + + expect(err).toContainSE_NOT_UNIQUE(); + expect(friendship).toBeNull(); + } + ); +}); \ No newline at end of file diff --git a/src/__tests__/friendship/friendshipService/getFriendRequests.test.ts b/src/__tests__/friendship/friendshipService/getFriendRequests.test.ts new file mode 100644 index 000000000..1a5eeabf8 --- /dev/null +++ b/src/__tests__/friendship/friendshipService/getFriendRequests.test.ts @@ -0,0 +1,108 @@ +import { FriendshipService } from '../../../friendship/friendship.service'; +import FriendshipModule from '../modules/friendship.module'; +import { FriendshipStatus } from '../../../friendship/enum/friendship-status.enum'; +import { ObjectId } from 'mongodb'; +import { createMockFriendships } from '../data/mockData/createData.mock'; +import { Friendship } from 'src/friendship/friendship.schema'; + +describe('Friendship.getFriendRequests() test suites', () => { + let friendshipService: FriendshipService; + const friendshipModel = FriendshipModule.getFriendshipModel(); + + const player1_id = new ObjectId().toString(); + const player2_id = new ObjectId().toString(); + const player3_id = new ObjectId().toString(); + const player4_id = new ObjectId().toString(); + + const friendshipConfigs: Partial[] = [ + { + playerA: player1_id, + playerB: player2_id, + status: FriendshipStatus.PENDING, + requester: player1_id, + }, + { + playerA: player1_id, + playerB: player3_id, + status: FriendshipStatus.PENDING, + requester: player1_id, + }, + { + playerA: player2_id, + playerB: player3_id, + status: FriendshipStatus.ACCEPTED, + }, + { + playerA: player1_id, + playerB: player4_id, + status: FriendshipStatus.BLOCKED, + }, + ]; + + beforeEach(async () => { + friendshipService = await FriendshipModule.getFriendshipService(); + + await createMockFriendships(friendshipConfigs) + }); + + it('Should get two friendship requests for player1', async () => { + const [friendships, err] = await friendshipService.getFriendRequests( + player1_id, + ); + + expect(err).toBeNull(); + expect(friendships).toHaveLength(2); + }); + + it('Should get one friendship request for player2', async () => { + const [friendships, err] = await friendshipService.getFriendRequests( + player2_id, + ); + + expect(err).toBeNull(); + expect(friendships).toHaveLength(1); + }); + + it('should return NOT_FOUND for player4', async () => { + const [friendships, err] = await friendshipService.getFriendRequests( + player4_id, + ); + + expect(err).toContainSE_NOT_FOUND(); + expect(friendships).toBeNull(); + }); + + it('Should avoid status ACCEPTED and return one friendship request', async () => { + const [friendships, err] = await friendshipService.getFriendRequests( + player3_id, + ); + + expect(err).toBeNull(); + expect(friendships).toHaveLength(1); + }); + + it('Direct create should fail without requester', async () => { + const invalidFriendship = { + playerA: player1_id, + playerB: player2_id, + status: FriendshipStatus.PENDING, + }; + + await expect(friendshipModel.create(invalidFriendship)).rejects.toThrow( + /requester is required when status is PENDING/, + ); + }); + + it('Direct create should fail with wrong requester', async () => { + const invalidFriendship = { + playerA: player1_id, + playerB: player2_id, + status: FriendshipStatus.PENDING, + requester: player3_id, + }; + + await expect(friendshipModel.create(invalidFriendship)).rejects.toThrow( + /requester must be either playerA or playerB/, + ); + }); +}); diff --git a/src/__tests__/friendship/friendshipService/getFriendshipsWithStatus.test.ts b/src/__tests__/friendship/friendshipService/getFriendshipsWithStatus.test.ts new file mode 100644 index 000000000..02a3939a9 --- /dev/null +++ b/src/__tests__/friendship/friendshipService/getFriendshipsWithStatus.test.ts @@ -0,0 +1,108 @@ +import { ObjectId } from "mongodb"; +import { FriendshipService } from "../../../friendship/friendship.service" +import FriendshipModule from "../modules/friendship.module"; +import { FriendshipStatus } from "../../../friendship/enum/friendship-status.enum"; +import { createMockFriendships } from "../data/mockData/createData.mock"; +import { Friendship } from "src/friendship/friendship.schema"; + +describe('FriendshipService.getFriendshipsWithStatus() test suites', () => { + let friendshipService: FriendshipService; + + const player1_id = new ObjectId().toString(); + const player2_id = new ObjectId().toString(); + const player3_id = new ObjectId().toString(); + const player4_id = new ObjectId().toString(); + + const friendshipConfigs: Partial[] = [ + { + playerA: player1_id, + playerB: player2_id, + status: FriendshipStatus.PENDING, + requester: player1_id, + }, + { + playerA: player1_id, + playerB: player3_id, + status: FriendshipStatus.PENDING, + requester: player1_id, + }, + { + playerA: player1_id, + playerB: player4_id, + status: FriendshipStatus.PENDING, + requester: player1_id, + }, + { + playerA: player2_id, + playerB: player3_id, + status: FriendshipStatus.ACCEPTED, + }, + { + playerA: player2_id, + playerB: player4_id, + status: FriendshipStatus.ACCEPTED, + }, + { + playerA: player3_id, + playerB: player4_id, + status: FriendshipStatus.BLOCKED, + }, + ]; + + beforeEach(async () => { + friendshipService = await FriendshipModule.getFriendshipService(); + + await createMockFriendships(friendshipConfigs); + }); + + it('Should get all friendship with status PENDING', async () => { + const [friendships, err] = await friendshipService.getFriendshipsWithStatus( + player1_id, + FriendshipStatus.PENDING + ); + + expect(err).toBeNull(); + expect(friendships).toHaveLength(3); + }); + + it('Should get all friendship with status ACCEPTED', async () => { + const [friendships, err] = await friendshipService.getFriendshipsWithStatus( + player2_id, + FriendshipStatus.ACCEPTED + ); + + expect(err).toBeNull(); + expect(friendships).toHaveLength(2); + }); + + it('Should get all friendship with status BLOCKED', async () => { + const [friendships, err] = await friendshipService.getFriendshipsWithStatus( + player3_id, + FriendshipStatus.BLOCKED + ); + + expect(err).toBeNull(); + expect(friendships).toHaveLength(1); + }); + + it('Should return NOT_FOUND if no match player_id', async () => { + const randomPlayer_id = new ObjectId().toString(); + const [friendships, err] = await friendshipService.getFriendshipsWithStatus( + randomPlayer_id, + FriendshipStatus.ACCEPTED + ); + + expect(err).toContainSE_NOT_FOUND(); + expect(friendships).toBeNull(); + }); + + it('Should return NOT_FOUND if no match STATUS', async () => { + const [friendships, err] = await friendshipService.getFriendshipsWithStatus( + player1_id, + undefined + ); + + expect(err).toContainSE_NOT_FOUND(); + expect(friendships).toBeNull(); + }); +}) \ No newline at end of file diff --git a/src/__tests__/friendship/friendshipService/getPlayerFriendlist.test.ts b/src/__tests__/friendship/friendshipService/getPlayerFriendlist.test.ts new file mode 100644 index 000000000..016657016 --- /dev/null +++ b/src/__tests__/friendship/friendshipService/getPlayerFriendlist.test.ts @@ -0,0 +1,69 @@ +import { FriendshipService } from '../../../friendship/friendship.service'; +import FriendshipModule from '../modules/friendship.module'; +import { FriendshipStatus } from '../../../friendship/enum/friendship-status.enum'; +import { ObjectId } from 'mongodb'; +import { createMockFriendships } from '../data/mockData/createData.mock'; +import { Friendship } from 'src/friendship/friendship.schema'; + +describe('Friendship.getPlayerFriendlist() test suite', () => { + let friendshipService: FriendshipService; + + const player1_id = new ObjectId().toString(); + const player2_id = new ObjectId().toString(); + const player3_id = new ObjectId().toString(); + + const friendshipConfigs: Partial[] = [ + { + playerA: player1_id, + playerB: player2_id, + status: FriendshipStatus.ACCEPTED, + + }, + { + playerA: player1_id, + playerB: player3_id, + status: FriendshipStatus.ACCEPTED, + + }, + { + playerA: player2_id, + playerB: player3_id, + status: FriendshipStatus.PENDING, + requester: player2_id, + }, + ]; + + beforeEach(async () => { + friendshipService = await FriendshipModule.getFriendshipService(); + + await createMockFriendships(friendshipConfigs); + }); + + it('Should get two friendships for player1', async () => { + const [friendships, err] = await friendshipService.getPlayerFriendlist( + player1_id.toString(), + ); + + expect(err).toBeNull(); + expect(friendships).toHaveLength(2); + }); + + it('Should return an empty array if no player_id match the filter', async () => { + const randomPlayer_id = new ObjectId(); + const [friendships, err] = await friendshipService.getPlayerFriendlist( + randomPlayer_id.toString(), + ); + + expect(err).toContainSE_NOT_FOUND(); + expect(friendships).toBeNull(); + }); + + it('Should get only one friendship for player2', async () => { + const [friendships, err] = await friendshipService.getPlayerFriendlist( + player2_id.toString(), + ); + + expect(err).toBeNull(); + expect(friendships).toHaveLength(1); + }); +}); diff --git a/src/__tests__/friendship/friendshipService/sendNewFriendRequestNotification.test.ts b/src/__tests__/friendship/friendshipService/sendNewFriendRequestNotification.test.ts new file mode 100644 index 000000000..36308e26e --- /dev/null +++ b/src/__tests__/friendship/friendshipService/sendNewFriendRequestNotification.test.ts @@ -0,0 +1,91 @@ +import { ObjectId } from 'mongodb'; +import { FriendshipService } from '../../../friendship/friendship.service'; +import FriendshipModule from '../modules/friendship.module'; +import FriendshipBuilderFactory from '../data/friendshipBuilderFactory'; +import { FriendshipStatus } from '../../../friendship/enum/friendship-status.enum'; +import { + createMockClans, + createMockFriendships, + createMockPlayers, +} from '../data/mockData/createData.mock'; +import { Friendship } from 'src/friendship/friendship.schema'; + +describe('FriendshipService.sendNewFriendRequestNotification()', () => { + let friendshipService: FriendshipService; + const friendshipModel = FriendshipModule.getFriendshipModel(); + + const player1_id = new ObjectId().toString(); + const player2_id = new ObjectId().toString(); + const player3_id = new ObjectId().toString(); + + const clan_id = new ObjectId().toString(); + + const friendshipConfigs: Partial[] = [ + { + playerA: player1_id, + playerB: player2_id, + status: FriendshipStatus.PENDING, + requester: player1_id, + }, + { + playerA: player1_id, + playerB: player3_id, + status: FriendshipStatus.PENDING, + requester: player1_id, + }, + { + playerA: player2_id, + playerB: player3_id, + status: FriendshipStatus.PENDING, + requester: player2_id, + }, + ]; + + beforeEach(async () => { + friendshipService = await FriendshipModule.getFriendshipService(); + + await createMockClans([{ _id: clan_id, name: 'TestClan' }]); + await createMockPlayers([ + { + _id: player1_id, + name: 'Player1', + clan_id, + uniqueIdentifier: 'unique-1', + }, + { + _id: player2_id, + name: 'Player2', + clan_id, + uniqueIdentifier: 'unique-2', + }, + { + _id: player3_id, + name: 'Player3', + clan_id, + uniqueIdentifier: 'unique-3', + }, + ]); + await createMockFriendships(friendshipConfigs); + }); + + it('Should send notifications for all pending friendship requests', async () => { + const pendingFriendships = await friendshipModel.find({ + status: FriendshipStatus.PENDING, + }); + + expect(pendingFriendships).toHaveLength(3); + + const notifier = (friendshipService as any).notifier; + const spy = jest.spyOn(notifier, 'newFriendRequest'); + + for (const friendship of pendingFriendships) { + // void function + const result = + await friendshipService.sendNewFriendRequestNotification(friendship); + expect(result).toBeUndefined(); + } + + expect(spy).toHaveBeenCalledTimes(3); + spy.mockRestore(); + }); +}); diff --git a/src/__tests__/friendship/modules/friendship.module.ts b/src/__tests__/friendship/modules/friendship.module.ts new file mode 100644 index 000000000..a66a753b7 --- /dev/null +++ b/src/__tests__/friendship/modules/friendship.module.ts @@ -0,0 +1,34 @@ +import mongoose from "mongoose"; +import { FriendshipService } from "../../../friendship/friendship.service"; +import FriendshipCommonModule from "./friendshipCommon"; +import FriendshipNotifier from "../../../friendship/friendship.notifier"; +import { ModelName } from "../../../common/enum/modelName.enum"; +import { FriendshipSchema } from "../../../friendship/friendship.schema"; +import { ClanSchema } from "../../../clan/clan.schema"; +import { PlayerSchema } from "../../../player/schemas/player.schema"; + +export default class FriendshipModule { + private constructor() {} + + static async getFriendshipService() { + const module = await FriendshipCommonModule.getModule(); + return await module.resolve(FriendshipService); + }; + + static async getFriendshipNotifier() { + const module = await FriendshipCommonModule.getModule(); + return await module.resolve(FriendshipNotifier); + }; + + static getFriendshipModel() { + return mongoose.model(ModelName.FRIENDSHIP, FriendshipSchema); + }; + + static getClanModel() { + return mongoose.model(ModelName.CLAN, ClanSchema); + }; + + static getPlayerModel() { + return mongoose.model(ModelName.PLAYER, PlayerSchema); + } +} \ No newline at end of file diff --git a/src/__tests__/friendship/modules/friendshipCommon.ts b/src/__tests__/friendship/modules/friendshipCommon.ts new file mode 100644 index 000000000..165a9021a --- /dev/null +++ b/src/__tests__/friendship/modules/friendshipCommon.ts @@ -0,0 +1,35 @@ +import { MongooseModule } from "@nestjs/mongoose"; +import { Test, TestingModule } from "@nestjs/testing"; +import { mongooseOptions, mongoString } from "../../test_utils/const/db"; +import { ModelName } from "../../../common/enum/modelName.enum"; +import FriendshipNotifier from "../../../friendship/friendship.notifier"; +import { FriendshipSchema } from "../../../friendship/friendship.schema"; +import { FriendshipService } from "../../../friendship/friendship.service"; +import { PlayerModule } from "../../../player/player.module"; +import { PlayerSchema } from "../../../player/schemas/player.schema"; +import { ClanSchema } from "../../../clan/clan.schema"; + +export default class FriendshipCommonModule { + private constructor() {} + + private static module: TestingModule; + + static async getModule() { + if (!FriendshipCommonModule.module) + FriendshipCommonModule.module = await Test.createTestingModule({ + imports: [ + MongooseModule.forRoot(mongoString, mongooseOptions), + MongooseModule.forFeature([ + { name: ModelName.FRIENDSHIP, schema: FriendshipSchema }, + { name: ModelName.PLAYER, schema: PlayerSchema }, + { name: ModelName.CLAN, schema: ClanSchema }, + ]), + + PlayerModule + ], + providers: [FriendshipService, FriendshipNotifier], + }).compile(); + + return FriendshipCommonModule.module; + } +} \ No newline at end of file diff --git a/src/common/swagger/tags/tags.ts b/src/common/swagger/tags/tags.ts index fca201ec9..01346516a 100644 --- a/src/common/swagger/tags/tags.ts +++ b/src/common/swagger/tags/tags.ts @@ -4,7 +4,7 @@ import { ExternalDocumentationObject } from '@nestjs/swagger/dist/interfaces/ope * Swagger tag name */ export type SwaggerTagName = - | 'Release on 15.12.2025' + | 'Release on 29.12.2025' | 'Profile' | 'Auth' | 'Player' @@ -36,9 +36,9 @@ export type SwaggerTagName = * */ export const swaggerTags: Record = { - 'Release on 15.12.2025': { - name: 'Release on 15.12.2025', - description: 'Changes made on release 15.12.2025', + 'Release on 29.12.2025': { + name: 'Release on 29.12.2025', + description: 'Changes made on release 29.12.2025', }, Profile: { name: 'Profile', diff --git a/src/common/utils/createWebDavClient.ts b/src/common/utils/createWebDavClient.ts new file mode 100644 index 000000000..b0e6fc515 --- /dev/null +++ b/src/common/utils/createWebDavClient.ts @@ -0,0 +1,14 @@ +// ESM-only workaround: webdav is ESM-only, so we use dynamic import to avoid CommonJS issues +import { createClient, WebDAVClient } from 'webdav'; +import { envVars } from '../service/envHandler/envVars'; + +export function createWebDavClient(): WebDAVClient { + return createClient( + `http://${envVars.OWNCLOUD_HOST}:${envVars.OWNCLOUD_PORT}/remote.php/webdav/`, + { + username: envVars.OWNCLOUD_USER, + password: envVars.OWNCLOUD_PASSWORD, + maxBodyLength: 52428800, + }, + ); +} diff --git a/src/friendship/friendship.schema.ts b/src/friendship/friendship.schema.ts index 543a788fe..8f3672a0a 100644 --- a/src/friendship/friendship.schema.ts +++ b/src/friendship/friendship.schema.ts @@ -6,6 +6,7 @@ import { } from 'mongoose'; import { ModelName } from '../common/enum/modelName.enum'; import { FriendshipStatus } from './enum/friendship-status.enum'; +import { ObjectId } from 'mongodb'; export type FriendshipDocument = HydratedDocument; @@ -16,14 +17,14 @@ export class Friendship { ref: ModelName.PLAYER, required: true, }) - playerA: MongooseSchema.Types.ObjectId; + playerA: string | ObjectId; @Prop({ type: MongooseSchema.Types.ObjectId, ref: ModelName.PLAYER, required: true, }) - playerB: MongooseSchema.Types.ObjectId; + playerB: string | ObjectId; @Prop({ type: String, @@ -36,11 +37,11 @@ export class Friendship { @Prop({ type: MongooseSchema.Types.ObjectId, ref: ModelName.PLAYER, - required: function () { + required: function (this: Friendship) { return this.status === FriendshipStatus.PENDING; }, }) - requester?: MongooseSchema.Types.ObjectId; + requester?: string | ObjectId; @Prop({ type: String, required: true }) pairKey: string; @@ -55,9 +56,21 @@ FriendshipSchema.pre('validate', function (next) { const b = this.playerB.toString(); if (a === b) return next(new Error('playerA and playerB cannot be the same')); + if (this.status === FriendshipStatus.PENDING) { + if (!this.requester) { + return next(new Error('requester is required when status is PENDING')); + } + + const requesterStr = this.requester.toString(); + if (requesterStr !== a && requesterStr !== b) { + return next(new Error('requester must be either playerA or playerB')); + } + } + this.pairKey = [a, b].sort().join('_'); next(); }); + FriendshipSchema.pre('save', function (next) { if (this.isModified('status') && this.status === FriendshipStatus.ACCEPTED) { this.requester = undefined; diff --git a/src/friendship/friendship.service.ts b/src/friendship/friendship.service.ts index ec41d6352..d0f690f6d 100644 --- a/src/friendship/friendship.service.ts +++ b/src/friendship/friendship.service.ts @@ -14,6 +14,7 @@ import { FriendlistDto } from './dto/friend-list.dto'; import { Player } from '../player/schemas/player.schema'; import FriendshipNotifier from './friendship.notifier'; import { InvalidIdsServiceError } from './error/duplicateId.error'; +import { FriendRequestDto } from './dto/FriendRequest.dto'; @Injectable() export class FriendshipService { @@ -37,13 +38,13 @@ export class FriendshipService { */ async getPlayerFriendlist( playerId: string, - ): Promise> { + ): Promise> { try { const [friendships, error] = await this.getFriendshipsWithStatus( playerId, FriendshipStatus.ACCEPTED, ); - if (error) throw error; + if (error) return [null, error]; const filtered: FriendlistDto[] = friendships.map((doc) => { if (!doc.playerA || !doc.playerB) return null; @@ -57,10 +58,10 @@ export class FriendshipService { avatar: friend.avatar, clanName: friend.Clan?.name ?? null, clan_id: friend.clan_id.toString(), - }; + } as FriendlistDto; }); - return [filtered as any, null]; + return [filtered, null]; } catch (error) { const errors = convertMongooseToServiceErrors(error); return [null, errors]; @@ -75,15 +76,17 @@ export class FriendshipService { * @param - playerId of the player whose friendlist to return * @returns Friend request list */ - async getFriendRequests(playerId: string) { + async getFriendRequests( + playerId: string + ): Promise> { try { const [requests, error] = await this.getFriendshipsWithStatus( playerId, FriendshipStatus.PENDING, ); - if (error) throw error; + if (error) return [null, error]; - const filtered = requests.map((doc) => { + const filtered: FriendRequestDto[] = requests.map((doc) => { if (!doc.playerA || !doc.playerB) return null; const friend = doc.playerA._id.toString() === playerId ? doc.playerB : doc.playerA; @@ -100,7 +103,7 @@ export class FriendshipService { clan_id: friend.clan_id.toString(), clanName: friend.Clan?.name ?? null, }, - }; + } as FriendRequestDto; }); return [filtered, null]; diff --git a/src/gameAnalytics/logFile.service.ts b/src/gameAnalytics/logFile.service.ts index ed829b529..05464d9a2 100644 --- a/src/gameAnalytics/logFile.service.ts +++ b/src/gameAnalytics/logFile.service.ts @@ -1,29 +1,26 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, OnModuleInit } from '@nestjs/common'; +import { createClient, WebDAVClient } from 'webdav'; +import { Readable } from 'stream'; import ServiceError from '../common/service/basicService/ServiceError'; import { SEReason } from '../common/service/basicService/SEReason'; -import { createClient, WebDAVClient } from 'webdav'; import { envVars } from '../common/service/envHandler/envVars'; -import { Readable } from 'stream'; @Injectable() -export class LogFileService { - constructor() { - this.initializeWebDavClient(); - } - private client: WebDAVClient; - +export class LogFileService implements OnModuleInit { + private client!: WebDAVClient; private readonly logFilesRootFolder = envVars.OWNCLOUD_LOG_FILES_ROOT; - /** - * Saves the provided file to the own cloud via WebDAV in the designated folder. - * - * @param fileToSave - The file to save. - * @param player_id - The player's unique identifier to be included in the file name. - * @param battleId - The battle id to which battle belongs to - * @returns A tuple with first element set to _true_ if file was saved - * - * or _array of ServiceErrors_ as a second element - */ + onModuleInit() { + this.client = createClient( + `http://${envVars.OWNCLOUD_HOST}:${envVars.OWNCLOUD_PORT}/remote.php/webdav/`, + { + username: envVars.OWNCLOUD_USER, + password: envVars.OWNCLOUD_PASSWORD, + maxBodyLength: 52428800, + }, + ); + } + async saveFile( fileToSave: Express.Multer.File, player_id: string, @@ -36,7 +33,6 @@ export class LogFileService { try { const isFileFolderExist = await this.client.exists(folderPath); if (!isFileFolderExist) - //Notice that the "logFilesRootFolder" folder must be already created manually in own cloud await this.client.createDirectory(folderPath, { recursive: true }); } catch (error) { return [ @@ -44,7 +40,7 @@ export class LogFileService { [ new ServiceError({ reason: SEReason.UNEXPECTED, - message: 'Unexpected error happen during folder creation', + message: 'Unexpected error during folder creation', additional: this.getWebDavErrorData(error), }), ], @@ -53,13 +49,9 @@ export class LogFileService { try { const fileStream = this.bufferToStream(fileToSave.buffer); - const isSuccess = await this.client.putFileContents( - filePath, - fileStream, - { - overwrite: true, - }, - ); + const isSuccess = await this.client.putFileContents(filePath, fileStream, { + overwrite: true, + }); if (!isSuccess) return [ @@ -79,7 +71,7 @@ export class LogFileService { [ new ServiceError({ reason: SEReason.UNEXPECTED, - message: 'Unexpected error happen during file saving', + message: 'Unexpected error during file saving', additional: this.getWebDavErrorData(error), }), ], @@ -87,11 +79,6 @@ export class LogFileService { } } - /** - * Converts provided buffer of a file to reading stream - * @param buffer buffer to convert - * @returns stream - */ private bufferToStream(buffer: Buffer) { const readable = new Readable(); readable._read = () => {}; @@ -100,89 +87,37 @@ export class LogFileService { return readable; } - /** - * Initializes the WebDAV client using credentials from the environment variables. - */ - private initializeWebDavClient() { - this.client = createClient( - `http://${envVars.OWNCLOUD_HOST}:${envVars.OWNCLOUD_PORT}/remote.php/webdav/`, - { - username: envVars.OWNCLOUD_USER, - password: envVars.OWNCLOUD_PASSWORD, - maxBodyLength: 52428800, - }, - ); - } - - /** - * Extracts the error data from the WebDAV error response. - * - * @param error - The error object to extract information from. - * @returns The response data from the WebDAV error, or null if not available. - */ private getWebDavErrorData(error: any) { return error?.response?.data ?? null; } - /** - * Constructs the full path to the folder where log files will be stored. - * - * @param battleId - The id of the battle where file belongs to - * @returns The full folder path as a string. - */ private getFolderPath(battleId: string) { const folderDataName = this.getDateFolderName(); return `${this.logFilesRootFolder}/${folderDataName}/${battleId}`; } - /** - * Constructs the full path to the file, including the folder and the file name. - * - * @param battleId - The id of the battle where file belongs to - * @param player_id - The player's unique identifier to be included in the file name. - * @returns The full file path. - */ + private getFilePath(battleId: string, player_id: string) { const folderPath = this.getFolderPath(battleId); const fileName = this.getFileName(player_id); return `${folderPath}/${fileName}`; } - /** - * Generates a folder name based on the current date. - * - * @returns A string representing the folder name, formatted as DD-MM-YYYY. - */ private getDateFolderName() { return this.getDateString(); } - /** - * Generates a file name based on the current date, time, player _id, and a random string. - * - * @param player_id - The player's unique identifier to be included in the file name. - * @returns The file name as a string, formatted as DD-MM-YYYY_HH-MM-SS_playerID_random.log. - */ + private getFileName(player_id: string) { const dateString = this.getDateString(); const timeString = this.getTimeString(); const randomString = Math.floor(Math.random() * 1000000); - return `${dateString}_${timeString}_${player_id}_${randomString}.log`; } - /** - * Gets the current date as a string formatted as DD-MM-YYYY. - * - * @returns A string representing the current date. - */ private getDateString() { const now = new Date(); return `${now.getDate()}-${now.getMonth() + 1}-${now.getFullYear()}`; } - /** - * Gets the current time as a string formatted as HH-MM-SS. - * - * @returns A string representing the current time. - */ + private getTimeString() { const now = new Date(); return `${now.getHours()}-${now.getMinutes()}-${now.getSeconds()}`; diff --git a/src/profile/dto/playerProfile.dto.ts b/src/profile/dto/playerProfile.dto.ts index 84c4bd8bd..f1f7abb30 100644 --- a/src/profile/dto/playerProfile.dto.ts +++ b/src/profile/dto/playerProfile.dto.ts @@ -13,5 +13,6 @@ export class PlayerProfileDto extends CreatePlayerDto { @IsProfileExists() @IsMongoId() @IsOptional() - override profile_id: string; + override profile_id = ''; + } diff --git a/tsconfig.build.json b/tsconfig.build.json index bdc5395c5..4dbf13ab2 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -4,12 +4,15 @@ "types": ["node", "multer"], "noEmitOnError": true, "declaration": false, - "sourceMap": false + "sourceMap": false, + "outDir": "./dist", + "rootDir": "./src" }, "exclude": [ "node_modules", "**/*.test.ts", "**/*.spec.ts", "**/__tests__/**" - ] -} \ No newline at end of file + ], + "include": ["src/**/*"] +} diff --git a/tsconfig.json b/tsconfig.json index e648f8513..98cfb48f2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,37 +1,39 @@ { "compilerOptions": { /* Language and Environment */ - "target": "ES2021", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ - "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ - "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + "target": "ES2021", + "experimentalDecorators": true, + "emitDecoratorMetadata": true, /* Modules */ - "module": "CommonJS", /* Specify what module code is generated. */ - "rootDir": "./src", /* Specify the root folder within your source files. */ - "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ - "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ - "resolveJsonModule": true, /* Enable importing .json files. */ + "module": "CommonJS", + "moduleResolution": "node", + "moduleDetection": "force", + "rootDir": "./src", + "baseUrl": "./", + "resolveJsonModule": true, /* Emit */ - "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ - "sourceMap": true, /* Create source map files for emitted JavaScript files. */ - "outDir": "./dist", /* Specify an output folder for all emitted files. */ - "removeComments": true, /* Disable emitting comments. */ + "declaration": true, + "sourceMap": true, + "outDir": "./dist", + "removeComments": true, /* Interop Constraints */ - "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ - "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ - "forceConsistentCasingInFileNames": false, /* Ensure that casing is correct in imports. */ + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, /* Type Checking */ - "strict": true, /* Enable all strict type-checking options. */ - "noImplicitAny": false, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ - "strictNullChecks": false, /* When type checking, take into account 'null' and 'undefined'. */ - "strictBindCallApply": false, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ - "noFallthroughCasesInSwitch": false, /* Enable error reporting for fallthrough cases in switch statements. */ + "strict": true, + "noImplicitAny": false, + "strictNullChecks": false, + "strictBindCallApply": false, + "noFallthroughCasesInSwitch": false, /* Completeness */ - "skipLibCheck": true /* Skip type checking all .d.ts files. */ + "skipLibCheck": true }, - "include": ["src/**/*.ts"] -} \ No newline at end of file + "include": ["src/**/*.ts"], + "exclude": ["node_modules"] +}