diff --git a/.env.dev b/.env.dev index e69de29..68e40c3 100644 --- a/.env.dev +++ b/.env.dev @@ -0,0 +1,3 @@ +MONGODB_URI= +USER_SERVICE_URL= +AUTH_SERVICE_URL= \ No newline at end of file diff --git a/.github/workflows/metrics.yml b/.github/workflows/metrics.yml index d5d1a60..f0aaa2b 100644 --- a/.github/workflows/metrics.yml +++ b/.github/workflows/metrics.yml @@ -1,6 +1,9 @@ name: Metrics and Release on: + push: + branches: + - dev pull_request: branches: - dev @@ -34,4 +37,4 @@ jobs: cd ${{secrets.GIT_DOC_REPO}} git add . git commit -m "Adicionando métricas do repositório ${{ github.event.repository.name }} ${{ github.ref_name }}" - git push \ No newline at end of file + git push diff --git a/package-lock.json b/package-lock.json index 53b67de..871c7a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,10 +9,17 @@ "version": "0.0.1", "license": "UNLICENSED", "dependencies": { + "@nestjs/axios": "^3.0.2", "@nestjs/common": "^10.0.0", + "@nestjs/config": "^3.2.3", "@nestjs/core": "^10.0.0", + "@nestjs/mongoose": "^10.0.10", "@nestjs/platform-express": "^10.0.0", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.1", "dotenv": "^16.4.5", + "joi": "^17.13.3", + "mongoose": "^8.5.2", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1" }, @@ -850,6 +857,19 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==" + }, + "node_modules/@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -1510,6 +1530,24 @@ "node": ">=8" } }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.8.tgz", + "integrity": "sha512-qKwC/M/nNNaKUBMQ0nuzm47b7ZYWQHN3pcXq4IIcoSBc2hOIrflAxJduIvvqmhoz3gR2TacTAs8vlsCVPkiEdQ==", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, + "node_modules/@nestjs/axios": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-3.0.2.tgz", + "integrity": "sha512-Z6GuOUdNQjP7FX+OuV2Ybyamse+/e0BFdTWBX5JxpBDKA+YkdLynDgG6HTF04zy6e9zPa19UX0WA2VDoehwhXQ==", + "peerDependencies": { + "@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0", + "axios": "^1.3.1", + "rxjs": "^6.0.0 || ^7.0.0" + } + }, "node_modules/@nestjs/cli": { "version": "10.4.4", "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.4.4.tgz", @@ -1596,6 +1634,20 @@ } } }, + "node_modules/@nestjs/config": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-3.2.3.tgz", + "integrity": "sha512-p6yv/CvoBewJ72mBq4NXgOAi2rSQNWx3a+IMJLVKS2uiwFCOQQuiIatGwq6MRjXV3Jr+B41iUO8FIf4xBrZ4/w==", + "dependencies": { + "dotenv": "16.4.5", + "dotenv-expand": "10.0.0", + "lodash": "4.17.21" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "rxjs": "^7.1.0" + } + }, "node_modules/@nestjs/core": { "version": "10.3.10", "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.3.10.tgz", @@ -1633,6 +1685,17 @@ } } }, + "node_modules/@nestjs/mongoose": { + "version": "10.0.10", + "resolved": "https://registry.npmjs.org/@nestjs/mongoose/-/mongoose-10.0.10.tgz", + "integrity": "sha512-3Ff60ock8nwlAJC823TG91Qy+Qc6av+ddIb6n6wlFsTK0akDF/aTcagX8cF8uI8mWxCWjEwEsgv99vo6p0yJ+w==", + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0", + "mongoose": "^6.0.2 || ^7.0.0 || ^8.0.0", + "rxjs": "^7.0.0" + } + }, "node_modules/@nestjs/platform-express": { "version": "10.3.10", "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.3.10.tgz", @@ -1776,6 +1839,24 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/@sideway/address": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", + "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==" + }, + "node_modules/@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==" + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -2077,6 +2158,24 @@ "@types/superagent": "^8.1.0" } }, + "node_modules/@types/validator": { + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.12.0.tgz", + "integrity": "sha512-nH45Lk7oPIJ1RVOF6JgFI6Dy0QpHEzq4QecZhvguxYPDwT8c93prCMqAtiIttm39voZ+DDR+qkNnMpJmMBRqag==" + }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==" + }, + "node_modules/@types/whatwg-url": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", + "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, "node_modules/@types/yargs": { "version": "17.0.33", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", @@ -2664,8 +2763,18 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.3.tgz", + "integrity": "sha512-Ar7ND9pU99eJ9GpoGQKhKf58GpUOgnzuaB7ueNQ5BMi0p+LZ5oaEnfF999fAArcTIBwXTCHAmGcHOZJaWPq9Nw==", + "peer": true, + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } }, "node_modules/babel-jest": { "version": "29.7.0", @@ -2956,6 +3065,14 @@ "node-int64": "^0.4.0" } }, + "node_modules/bson": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.8.0.tgz", + "integrity": "sha512-iOJg8pr7wq2tg/zSlCCHMi3hMm5JTOxLTagf3zxhcenHsFp+c6uOs6K7W5UE7A4QIJGtqh/ZovFNMP4mOPJynQ==", + "engines": { + "node": ">=16.20.1" + } + }, "node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -3144,6 +3261,21 @@ "integrity": "sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==", "dev": true }, + "node_modules/class-transformer": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==" + }, + "node_modules/class-validator": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.1.tgz", + "integrity": "sha512-2VEG9JICxIqTpoK1eMzZqaV+u/EiwEJkMGzTrZf6sU/fwsnOITVgYJ8yojSy6CaXtO9V0Cc6ZQZ8h8m4UBuLwQ==", + "dependencies": { + "@types/validator": "^13.11.8", + "libphonenumber-js": "^1.10.53", + "validator": "^13.9.0" + } + }, "node_modules/cli-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", @@ -3268,7 +3400,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -3467,7 +3598,6 @@ "version": "4.3.6", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", - "dev": true, "dependencies": { "ms": "2.1.2" }, @@ -3541,7 +3671,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "engines": { "node": ">=0.4.0" } @@ -3635,6 +3764,14 @@ "url": "https://dotenvx.com" } }, + "node_modules/dotenv-expand": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz", + "integrity": "sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==", + "engines": { + "node": ">=12" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -4361,6 +4498,26 @@ "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", "dev": true }, + "node_modules/follow-redirects": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "peer": true, + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/foreground-child": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", @@ -4431,7 +4588,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -5853,6 +6009,18 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/joi": { + "version": "17.13.3", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", + "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", + "dependencies": { + "@hapi/hoek": "^9.3.0", + "@hapi/topo": "^5.1.0", + "@sideway/address": "^4.1.5", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -5937,6 +6105,14 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/kareem": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", + "integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -5977,6 +6153,11 @@ "node": ">= 0.8.0" } }, + "node_modules/libphonenumber-js": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.11.5.tgz", + "integrity": "sha512-TwHR5BZxGRODtAfz03szucAkjT5OArXr+94SMtAM2pYXIlQNVMrxvb6uSCbnaJJV6QXEyICk7+l6QPgn72WHhg==" + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -6010,8 +6191,7 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "node_modules/lodash.memoize": { "version": "4.1.2", @@ -6112,6 +6292,11 @@ "node": ">= 4.0.0" } }, + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==" + }, "node_modules/merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", @@ -6247,11 +6432,140 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/mongodb": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.7.0.tgz", + "integrity": "sha512-TMKyHdtMcO0fYBNORiYdmM25ijsHs+Njs963r4Tro4OQZzqYigAzYQouwWRg4OIaiLRUEGUh/1UAcH5lxdSLIA==", + "dependencies": { + "@mongodb-js/saslprep": "^1.1.5", + "bson": "^6.7.0", + "mongodb-connection-string-url": "^3.0.0" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.2.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.1.tgz", + "integrity": "sha512-XqMGwRX0Lgn05TDB4PyG2h2kKO/FfWJyCzYQbIhXUxz7ETt0I/FqHjUeqj37irJ+Dl1ZtU82uYyj14u2XsZKfg==", + "dependencies": { + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^13.0.0" + } + }, + "node_modules/mongodb-connection-string-url/node_modules/tr46": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", + "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", + "dependencies": { + "punycode": "^2.3.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/mongodb-connection-string-url/node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "engines": { + "node": ">=12" + } + }, + "node_modules/mongodb-connection-string-url/node_modules/whatwg-url": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-13.0.0.tgz", + "integrity": "sha512-9WWbymnqj57+XEuqADHrCJ2eSXzn8WXIW/YSGaZtb2WKAInQ6CHfaUUcTyyver0p8BDg5StLQq8h1vtZuwmOig==", + "dependencies": { + "tr46": "^4.1.1", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/mongoose": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.5.2.tgz", + "integrity": "sha512-GZB4rHMdYfGatV+23IpCrqFbyCOjCNOHXgWbirr92KRwTEncBrtW3kgU9vmpKjsGf7nMmnAy06SwWUv1vhDkSg==", + "dependencies": { + "bson": "^6.7.0", + "kareem": "2.6.3", + "mongodb": "6.7.0", + "mpath": "0.9.0", + "mquery": "5.0.0", + "ms": "2.1.3", + "sift": "17.1.3" + }, + "engines": { + "node": ">=16.20.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mongoose" + } + }, + "node_modules/mongoose/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/mpath": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", + "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mquery": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", + "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", + "dependencies": { + "debug": "4.x" + }, + "engines": { + "node": ">=14.0.0" + } + }, "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 + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/multer": { "version": "1.4.4-lts.1", @@ -6810,11 +7124,16 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "peer": true + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, "engines": { "node": ">=6" } @@ -7373,6 +7692,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/sift": { + "version": "17.1.3", + "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz", + "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==" + }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -7428,6 +7752,14 @@ "node": ">=0.10.0" } }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "dependencies": { + "memory-pager": "^1.0.2" + } + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -8234,6 +8566,14 @@ "node": ">=10.12.0" } }, + "node_modules/validator": { + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", + "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/package.json b/package.json index 116dc4a..f86715d 100644 --- a/package.json +++ b/package.json @@ -21,10 +21,17 @@ "test:e2e": "jest --config ./test/jest-e2e.json" }, "dependencies": { + "@nestjs/axios": "^3.0.2", "@nestjs/common": "^10.0.0", + "@nestjs/config": "^3.2.3", "@nestjs/core": "^10.0.0", + "@nestjs/mongoose": "^10.0.10", "@nestjs/platform-express": "^10.0.0", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.1", "dotenv": "^16.4.5", + "joi": "^17.13.3", + "mongoose": "^8.5.2", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1" }, @@ -42,6 +49,8 @@ "eslint-config-prettier": "^9.0.0", "eslint-plugin-prettier": "^5.0.0", "jest": "^29.5.0", + "jest-sonar": "^0.2.16", + "jest-sonar-reporter": "^2.0.0", "prettier": "^3.0.0", "source-map-support": "^0.5.21", "supertest": "^7.0.0", @@ -57,7 +66,7 @@ "json", "ts" ], - "rootDir": "src", + "rootDir": "./test", "testRegex": ".*\\.spec\\.ts$", "transform": { "^.+\\.(t|j)s$": "ts-jest" diff --git a/scripts/metrics.js b/scripts/metrics.js index bcd375e..675df65 100644 --- a/scripts/metrics.js +++ b/scripts/metrics.js @@ -1,6 +1,6 @@ -const REPO = '2024.1-CALCULUS-StudioMaker '; +const REPO = '2024.1-CALCULUS-StudioMaker'; const OWNER = 'fga-eps-mds'; -const SONAR_ID = 'fga-eps-mds_2024.1-CALCULUS-StudioMaker'; +const SONAR_ID = 'fga-eps-mds-1_2024-1-calculus-studiomaker'; const METRIC_LIST = [ 'files', @@ -23,4 +23,4 @@ module.exports = { SONAR_URL, REPO, OWNER -}; \ No newline at end of file +}; diff --git a/sonar-project.properties b/sonar-project.properties index d221d03..a0fa946 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -1,4 +1,4 @@ -sonar.projectKey=fga-eps-mds_2024.1-CALCULUS-StudioMaker +sonar.projectKey=fga-eps-mds-1_2024-1-calculus-studiomaker sonar.organization=fga-eps-mds-1 sonar.sources=src sonar.tests=test @@ -11,4 +11,4 @@ sonar.exclusions=node_modules/**, test/**, assets/**, .github/**, scripts/** **/ sonar.javascript.lcov.reportPaths=coverage/lcov.info -sonar.testExecutionReportPaths=reports/sonar-report.xml \ No newline at end of file +sonar.testExecutionReportPaths=reports/sonar-report.xml diff --git a/src/app.controller.spec.ts b/src/app.controller.spec.ts deleted file mode 100644 index d22f389..0000000 --- a/src/app.controller.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { AppController } from './app.controller'; -import { AppService } from './app.service'; - -describe('AppController', () => { - let appController: AppController; - - beforeEach(async () => { - const app: TestingModule = await Test.createTestingModule({ - controllers: [AppController], - providers: [AppService], - }).compile(); - - appController = app.get(AppController); - }); - - describe('root', () => { - it('should return "Hello World!"', () => { - expect(appController.getHello()).toBe('Hello World!'); - }); - }); -}); diff --git a/src/app.controller.ts b/src/app.controller.ts deleted file mode 100644 index cce879e..0000000 --- a/src/app.controller.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Controller, Get } from '@nestjs/common'; -import { AppService } from './app.service'; - -@Controller() -export class AppController { - constructor(private readonly appService: AppService) {} - - @Get() - getHello(): string { - return this.appService.getHello(); - } -} diff --git a/src/app.module.ts b/src/app.module.ts index 8662803..cecbe6f 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,10 +1,34 @@ import { Module } from '@nestjs/common'; -import { AppController } from './app.controller'; -import { AppService } from './app.service'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { MongooseModule } from '@nestjs/mongoose'; +import { HttpModule } from '@nestjs/axios'; +import { ContentModule } from './content/content.module'; +import * as Joi from 'joi'; +import { JourneyModule } from './journey/journey.module'; +import { TrailModule } from './trail/trail.module'; @Module({ - imports: [], - controllers: [AppController], - providers: [AppService], + imports: [ + ConfigModule.forRoot({ + isGlobal: true, + validationSchema: Joi.object({ + MONGODB_URI: Joi.string().required(), + PORT: Joi.number().default(3002), + }), + }), + MongooseModule.forRootAsync({ + imports: [ConfigModule], + useFactory: async (configService: ConfigService) => ({ + uri: configService.get('MONGODB_URI'), + }), + inject: [ConfigService], + }), + HttpModule, + ContentModule, + JourneyModule, + TrailModule, + ], + controllers: [], + providers: [], }) export class AppModule {} diff --git a/src/app.service.ts b/src/app.service.ts deleted file mode 100644 index 927d7cc..0000000 --- a/src/app.service.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -@Injectable() -export class AppService { - getHello(): string { - return 'Hello World!'; - } -} diff --git a/src/content/content.controller.ts b/src/content/content.controller.ts new file mode 100644 index 0000000..a5a7c44 --- /dev/null +++ b/src/content/content.controller.ts @@ -0,0 +1,53 @@ +import { + Controller, + Post, + Get, + Patch, + Delete, + Param, + Body, + NotFoundException, +} from '@nestjs/common'; +import { ContentService } from './content.service'; +import { Content } from './content.schema'; + +@Controller('contents') +export class ContentController { + constructor(private readonly contentService: ContentService) {} + + @Post() + async createContent( + @Body() body: { title: string; content: string; trailId: string }, + ): Promise { + const { title, content, trailId } = body; + + if (!title || !content || !trailId) { + throw new NotFoundException('Title, content, and trailId are required'); + } + + return this.contentService.createContent(title, content, trailId); + } + + @Get(':id') + async findContentById(@Param('id') id: string): Promise { + return this.contentService.findContentById(id); + } + + @Get() + async findAllContents(): Promise { + return this.contentService.findAllContents(); + } + + @Patch(':id') + async updateContent( + @Param('id') id: string, + @Body() updateData: Partial, + ): Promise { + return this.contentService.updateContent(id, updateData); + } + + @Delete(':id') + async deleteContent(@Param('id') id: string): Promise { + return this.contentService.deleteContent(id); + } +} diff --git a/src/content/content.module.ts b/src/content/content.module.ts new file mode 100644 index 0000000..158edef --- /dev/null +++ b/src/content/content.module.ts @@ -0,0 +1,17 @@ +import { Module, forwardRef } from '@nestjs/common'; +import { MongooseModule } from '@nestjs/mongoose'; +import { ContentSchema } from './content.schema'; +import { ContentService } from './content.service'; +import { ContentController } from './content.controller'; +import { TrailModule } from '../trail/trail.module'; + +@Module({ + imports: [ + MongooseModule.forFeature([{ name: 'Content', schema: ContentSchema }]), + forwardRef(() => TrailModule), + ], + providers: [ContentService], + controllers: [ContentController], + exports: [ContentService], +}) +export class ContentModule {} diff --git a/src/content/content.schema.ts b/src/content/content.schema.ts new file mode 100644 index 0000000..49265f4 --- /dev/null +++ b/src/content/content.schema.ts @@ -0,0 +1,20 @@ +import * as mongoose from 'mongoose'; + +export const ContentSchema = new mongoose.Schema( + { + title: { type: String, required: true }, + content: { type: String, required: true }, + trail: { + type: mongoose.Schema.Types.ObjectId, + ref: 'Trail', + required: true, + }, + }, + { timestamps: true, collection: 'contents' }, +); + +export interface Content extends mongoose.Document { + title: string; + content: string; + trail: mongoose.Schema.Types.ObjectId; +} diff --git a/src/content/content.service.ts b/src/content/content.service.ts new file mode 100644 index 0000000..ed9aea4 --- /dev/null +++ b/src/content/content.service.ts @@ -0,0 +1,71 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { InjectModel } from '@nestjs/mongoose'; +import { Model } from 'mongoose'; +import { Content } from './content.schema'; +import { Trail } from '../trail/trail.schema'; +import { TrailService } from '../trail/trail.service'; + +@Injectable() +export class ContentService { + constructor( + @InjectModel('Content') private readonly contentModel: Model, + @InjectModel('Trail') private readonly trailModel: Model, + private readonly trailService: TrailService, + ) {} + + async createContent( + title: string, + content: string, + trailId: string, + ): Promise { + const trailExists = await this.trailModel.findById(trailId).exec(); + if (!trailExists) { + throw new NotFoundException(`Trail with ID ${trailId} not found`); + } + + const newContent = new this.contentModel({ + title, + content, + trail: trailId, + }); + + await this.trailService.addContentToTrail( + trailId, + newContent._id.toString(), + ); + + return newContent.save(); + } + + async findContentById(id: string): Promise { + const content = await this.contentModel.findById(id).exec(); + if (!content) { + throw new NotFoundException(`Content with ID ${id} not found`); + } + return content; + } + + async findAllContents(): Promise { + return this.contentModel.find().exec(); + } + + async updateContent( + id: string, + updateData: Partial, + ): Promise { + const content = await this.contentModel + .findByIdAndUpdate(id, updateData, { new: true }) + .exec(); + if (!content) { + throw new NotFoundException(`Content with ID ${id} not found`); + } + return content; + } + + async deleteContent(id: string): Promise { + const result = await this.contentModel.findByIdAndDelete(id).exec(); + if (!result) { + throw new NotFoundException(`Content with ID ${id} not found`); + } + } +} diff --git a/src/content/dtos/create-content.dto.ts b/src/content/dtos/create-content.dto.ts new file mode 100644 index 0000000..fe9111f --- /dev/null +++ b/src/content/dtos/create-content.dto.ts @@ -0,0 +1,15 @@ +import { IsString, IsNotEmpty, IsMongoId } from 'class-validator'; + +export class CreateContentDto { + @IsString() + @IsNotEmpty() + title: string; + + @IsString() + @IsNotEmpty() + content: string; + + @IsMongoId() + @IsNotEmpty() + trail: string; +} diff --git a/src/journey/dtos/create-journey.dto.ts b/src/journey/dtos/create-journey.dto.ts new file mode 100644 index 0000000..d4c76d1 --- /dev/null +++ b/src/journey/dtos/create-journey.dto.ts @@ -0,0 +1,13 @@ +import { IsString, IsOptional, IsMongoId } from 'class-validator'; + +export class CreateJourneyDto { + @IsString() + title: string; + + @IsString() + description: string; + + @IsOptional() + @IsMongoId() + user?: string; +} diff --git a/src/journey/journey.controller.ts b/src/journey/journey.controller.ts new file mode 100644 index 0000000..c551523 --- /dev/null +++ b/src/journey/journey.controller.ts @@ -0,0 +1,71 @@ +import { + Controller, + Get, + Post, + Body, + Param, + Put, + Req, + UnauthorizedException, + Delete, + Patch, +} from '@nestjs/common'; +import { JourneyService } from './journey.service'; +import { Request } from 'express'; +import { CreateJourneyDto } from './dtos/create-journey.dto'; + +@Controller('journeys') +export class JourneyController { + constructor(private readonly journeyService: JourneyService) {} + + @Post() + async create( + @Body() createJourneyDto: CreateJourneyDto, + @Req() req: Request, + ) { + const authHeader = req.headers.authorization as string; + const token = authHeader?.split(' ')[1]; + + if (!token) { + throw new UnauthorizedException('Token not found'); + } + + return this.journeyService.create(createJourneyDto, token); + } + + @Get() + async findAll() { + return this.journeyService.findAll(); + } + + @Get('user/:id') + async findByUser(@Param('id') userId: string) { + return this.journeyService.findByUserId(userId); + } + + @Get(':id') + async findById(@Param('id') id: string) { + return this.journeyService.findById(id); + } + + @Put(':id') + async update( + @Param('id') id: string, + @Body() updateJourneyDto: CreateJourneyDto, + ) { + return this.journeyService.update(id, updateJourneyDto); + } + + @Delete(':id') + async delete(@Param('id') id: string) { + return this.journeyService.delete(id); + } + + @Patch(':id/add-trail') + async addTrailToJourney( + @Param('id') id: string, + @Body() body: { trailId: string }, + ) { + return this.journeyService.addTrailToJourney(id, body.trailId); + } +} diff --git a/src/journey/journey.module.ts b/src/journey/journey.module.ts new file mode 100644 index 0000000..b8802f2 --- /dev/null +++ b/src/journey/journey.module.ts @@ -0,0 +1,17 @@ +import { Module } from '@nestjs/common'; +import { MongooseModule } from '@nestjs/mongoose'; +import { JourneySchema } from './journey.schema'; +import { JourneyService } from './journey.service'; +import { HttpModule } from '@nestjs/axios'; +import { JourneyController } from './journey.controller'; + +@Module({ + imports: [ + HttpModule, + MongooseModule.forFeature([{ name: 'Journey', schema: JourneySchema }]), + ], + providers: [JourneyService], + controllers: [JourneyController], + exports: [MongooseModule, JourneyService], +}) +export class JourneyModule {} diff --git a/src/journey/journey.schema.ts b/src/journey/journey.schema.ts new file mode 100644 index 0000000..049ed56 --- /dev/null +++ b/src/journey/journey.schema.ts @@ -0,0 +1,18 @@ +import * as mongoose from 'mongoose'; + +export const JourneySchema = new mongoose.Schema( + { + title: { type: String, required: true }, + description: { type: String, required: true }, + user: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }, + trails: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Trail' }], + }, + { timestamps: true, collection: 'journeys' }, +); + +export interface Journey extends mongoose.Document { + title: string; + description: string; + user: mongoose.Schema.Types.ObjectId; + trails?: mongoose.Types.ObjectId[]; +} diff --git a/src/journey/journey.service.ts b/src/journey/journey.service.ts new file mode 100644 index 0000000..ca26a4a --- /dev/null +++ b/src/journey/journey.service.ts @@ -0,0 +1,135 @@ +import { HttpService } from '@nestjs/axios'; +import { firstValueFrom } from 'rxjs'; +import { + Injectable, + Logger, + NotFoundException, + UnauthorizedException, +} from '@nestjs/common'; +import { InjectModel } from '@nestjs/mongoose'; +import { Model, Types } from 'mongoose'; +import { Journey } from './journey.schema'; +import { CreateJourneyDto } from './dtos/create-journey.dto'; + +@Injectable() +export class JourneyService { + private readonly logger = new Logger(JourneyService.name); + + constructor( + @InjectModel('Journey') private readonly journeyModel: Model, + private readonly httpService: HttpService, + ) {} + + async create( + createJourneyDto: CreateJourneyDto, + token: string, + ): Promise { + const userId = await this.validateTokenAndGetUserId(token); + + this.logger.log(`User ID from token: ${userId}`); + + if (!userId) { + throw new UnauthorizedException('Invalid token'); + } + + const newJourney = new this.journeyModel({ + ...createJourneyDto, + user: userId, + }); + const savedJourney = await newJourney.save(); + + await this.addJourneyToUser(userId, savedJourney._id.toString()); + + return savedJourney; + } + + async validateTokenAndGetUserId(token: string): Promise { + try { + this.logger.log(`Validating token: ${token}`); + const response = await firstValueFrom( + this.httpService.get(`${process.env.AUTH_SERVICE_URL}/validate-token`, { + headers: { Authorization: `Bearer ${token}` }, + }), + ); + this.logger.log( + `Token validation response: ${JSON.stringify(response.data)}`, + ); + return response.data.userPayload?.id || null; + } catch (err) { + this.logger.error(`Token validation failed: ${err.message}`); + return null; + } + } + + async addJourneyToUser(userId: string, journeyId: string): Promise { + try { + await firstValueFrom( + this.httpService.patch( + `${process.env.USER_SERVICE_URL}/${userId}/add-journey`, + { journeyId }, + ), + ); + this.logger.log(`Added journey ${journeyId} to user ${userId}`); + } catch (err) { + this.logger.error(`Failed to add journey to user: ${err.message}`); + throw new NotFoundException('Failed to update user with new journey'); + } + } + + async findAll(): Promise { + return this.journeyModel.find().exec(); + } + + async findByUserId(userId: string): Promise { + return this.journeyModel.find({ user: userId }).exec(); + } + + async findById(id: string): Promise { + const journey = await this.journeyModel.findById(id).exec(); + if (!journey) { + throw new NotFoundException(`Journey with ID ${id} not found`); + } + return journey; + } + + async update( + id: string, + updateJourneyDto: CreateJourneyDto, + ): Promise { + const journey = await this.journeyModel + .findByIdAndUpdate(id, updateJourneyDto, { new: true }) + .exec(); + if (!journey) { + throw new NotFoundException(`Journey with ID ${id} not found`); + } + return journey; + } + + async delete(id: string): Promise { + const journey = await this.journeyModel.findByIdAndDelete(id).exec(); + if (!journey) { + throw new NotFoundException(`Journey with ID ${id} not found`); + } + return journey; + } + + async addTrailToJourney( + journeyId: string, + trailId: string, + ): Promise { + const journey = await this.journeyModel.findById(journeyId).exec(); + if (!journey) { + throw new NotFoundException(`Journey with ID ${journeyId} not found`); + } + + const objectId = new Types.ObjectId(trailId); + + if (!journey.trails) { + journey.trails = []; + } + + journey.trails.push(objectId); + + return journey.save(); + } +} diff --git a/src/main.ts b/src/main.ts index cdebcd7..49335f2 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,8 +1,16 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; +import { ConfigService } from '@nestjs/config'; +import { Logger, ValidationPipe } from '@nestjs/common'; + +const configService = new ConfigService(); +const logger = new Logger('Main'); async function bootstrap() { const app = await NestFactory.create(AppModule); - await app.listen(3002); + app.useGlobalPipes(new ValidationPipe()); + app.enableCors(); + await app.listen(configService.get('PORT')); + logger.log(`Application listening on port ${configService.get('PORT')}`); } bootstrap(); diff --git a/src/trail/trail.controller.ts b/src/trail/trail.controller.ts new file mode 100644 index 0000000..860d142 --- /dev/null +++ b/src/trail/trail.controller.ts @@ -0,0 +1,72 @@ +import { + Controller, + Post, + Get, + Put, + Delete, + Param, + Body, + NotFoundException, +} from '@nestjs/common'; +import { TrailService } from './trail.service'; + +@Controller('trails') +export class TrailController { + constructor(private readonly trailService: TrailService) {} + + @Post() + async createTrail(@Body() body: { name: string; journeyId: string }) { + const { name, journeyId } = body; + + if (!journeyId) { + throw new NotFoundException('Journey ID not provided in body'); + } + + return this.trailService.createTrail(name, journeyId); + } + + @Get(':id') + async getTrailById(@Param('id') id: string) { + return this.trailService.findTrailById(id); + } + + @Get() + async getAllTrails() { + return this.trailService.findAllTrails(); + } + + @Put(':id') + async updateTrail( + @Param('id') id: string, + @Body() updateData: Partial<{ name: string; description?: string }>, + ) { + return this.trailService.updateTrail(id, updateData); + } + + @Put(':id/addContent') + async addContentToTrail( + @Param('id') trailId: string, + @Body() body: { contentId: string }, + ) { + const { contentId } = body; + if (!contentId) { + throw new NotFoundException('Content ID not provided in body'); + } + return this.trailService.addContentToTrail(trailId, contentId); + } + + @Put(':id/removeContent') + async removeContentFromTrail( + @Param('id') trailId: string, + @Body() body: { contentId: string }, + ) { + const { contentId } = body; + return this.trailService.removeContentFromTrail(trailId, contentId); + } + + @Delete(':id') + async deleteTrail(@Param('id') id: string) { + await this.trailService.deleteTrail(id); + return { message: 'Trail deleted successfully' }; + } +} diff --git a/src/trail/trail.module.ts b/src/trail/trail.module.ts new file mode 100644 index 0000000..a302088 --- /dev/null +++ b/src/trail/trail.module.ts @@ -0,0 +1,19 @@ +import { Module, forwardRef } from '@nestjs/common'; +import { MongooseModule } from '@nestjs/mongoose'; +import { TrailSchema } from './trail.schema'; +import { TrailService } from './trail.service'; +import { JourneyModule } from '../journey/journey.module'; +import { TrailController } from './trail.controller'; +import { ContentModule } from '../content/content.module'; + +@Module({ + imports: [ + MongooseModule.forFeature([{ name: 'Trail', schema: TrailSchema }]), + JourneyModule, + forwardRef(() => ContentModule), + ], + providers: [TrailService], + controllers: [TrailController], + exports: [MongooseModule, TrailService], +}) +export class TrailModule {} diff --git a/src/trail/trail.schema.ts b/src/trail/trail.schema.ts new file mode 100644 index 0000000..d64961c --- /dev/null +++ b/src/trail/trail.schema.ts @@ -0,0 +1,16 @@ +import * as mongoose from 'mongoose'; + +export const TrailSchema = new mongoose.Schema( + { + name: { type: String, required: true }, + journey: { type: mongoose.Schema.Types.ObjectId, ref: 'Journey' }, + contents: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Content' }], + }, + { timestamps: true, collection: 'trails' }, +); + +export interface Trail extends mongoose.Document { + name: string; + journey: mongoose.Schema.Types.ObjectId; + contents: mongoose.Types.ObjectId[]; +} diff --git a/src/trail/trail.service.ts b/src/trail/trail.service.ts new file mode 100644 index 0000000..d5f2fb2 --- /dev/null +++ b/src/trail/trail.service.ts @@ -0,0 +1,96 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { InjectModel } from '@nestjs/mongoose'; +import { Model, Types } from 'mongoose'; +import { Trail } from './trail.schema'; +import { Journey } from 'src/journey/journey.schema'; +import { JourneyService } from 'src/journey/journey.service'; + +@Injectable() +export class TrailService { + constructor( + @InjectModel('Trail') private readonly trailModel: Model, + @InjectModel('Journey') private readonly journeyModel: Model, + private readonly journeyService: JourneyService, + ) {} + + async createTrail(name: string, journeyId: string): Promise { + const journeyExists = await this.journeyModel.findById(journeyId).exec(); + if (!journeyExists) { + throw new NotFoundException(`Journey with ID ${journeyId} not found`); + } + + const newTrail = new this.trailModel({ + name, + journey: journeyId, + }); + + await this.journeyService.addTrailToJourney( + journeyId, + newTrail._id.toString(), + ); + + return newTrail.save(); + } + + async addContentToTrail(trailId: string, contentId: string): Promise { + const trail = await this.trailModel.findById(trailId).exec(); + if (!trail) { + throw new NotFoundException(`Trail with ID ${trailId} not found`); + } + + const objectId = new Types.ObjectId(contentId); + + if (!trail.contents) { + trail.contents = []; + } + + trail.contents.push(objectId); + + return trail.save(); + } + + async removeContentFromTrail( + trailId: string, + contentId: string, + ): Promise { + const trail = await this.trailModel.findById(trailId).exec(); + if (!trail) { + throw new NotFoundException(`Trail with ID ${trailId} not found`); + } + + trail.contents = trail.contents.filter( + (content) => !content.equals(contentId), + ); + + return trail.save(); + } + + async findTrailById(id: string): Promise { + const trail = await this.trailModel.findById(id).exec(); + if (!trail) { + throw new NotFoundException(`Trail with ID ${id} not found`); + } + return trail; + } + + async findAllTrails(): Promise { + return this.trailModel.find().exec(); + } + + async updateTrail(id: string, updateData: Partial): Promise { + const trail = await this.trailModel + .findByIdAndUpdate(id, updateData, { new: true }) + .exec(); + if (!trail) { + throw new NotFoundException(`Trail with ID ${id} not found`); + } + return trail; + } + + async deleteTrail(id: string): Promise { + const result = await this.trailModel.findByIdAndDelete(id).exec(); + if (!result) { + throw new NotFoundException(`Trail with ID ${id} not found`); + } + } +} diff --git a/test/app.e2e-spec.ts b/test/app.e2e-spec.ts deleted file mode 100644 index 50cda62..0000000 --- a/test/app.e2e-spec.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { INestApplication } from '@nestjs/common'; -import * as request from 'supertest'; -import { AppModule } from './../src/app.module'; - -describe('AppController (e2e)', () => { - let app: INestApplication; - - beforeEach(async () => { - const moduleFixture: TestingModule = await Test.createTestingModule({ - imports: [AppModule], - }).compile(); - - app = moduleFixture.createNestApplication(); - await app.init(); - }); - - it('/ (GET)', () => { - return request(app.getHttpServer()) - .get('/') - .expect(200) - .expect('Hello World!'); - }); -}); diff --git a/test/content.controller.spec.ts b/test/content.controller.spec.ts new file mode 100644 index 0000000..025ed2b --- /dev/null +++ b/test/content.controller.spec.ts @@ -0,0 +1,164 @@ +import { Test, TestingModule } from '@nestjs/testing'; + +import { NotFoundException } from '@nestjs/common'; +import { ContentController } from 'src/content/content.controller'; +import { ContentService } from 'src/content/content.service'; +import { Content } from 'src/content/content.schema'; + +describe('ContentController', () => { + let controller: ContentController; + + const mockContentService = { + createContent: jest.fn(), + findContentById: jest.fn(), + findAllContents: jest.fn(), + updateContent: jest.fn(), + deleteContent: jest.fn(), + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [ContentController], + providers: [ + { + provide: ContentService, + useValue: mockContentService, + }, + ], + }).compile(); + + controller = module.get(ContentController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + describe('createContent', () => { + it('should create content', async () => { + const contentDto = { + title: 'Test Title', + content: 'Test Content', + trailId: 'trail-id', + }; + const content = { + _id: 'content-id', + ...contentDto, + } as unknown as Content; + + mockContentService.createContent.mockResolvedValue(content); + + const result = await controller.createContent(contentDto); + expect(result).toEqual(content); + expect(mockContentService.createContent).toHaveBeenCalledWith( + contentDto.title, + contentDto.content, + contentDto.trailId, + ); + }); + + it('should throw NotFoundException if required fields are missing', async () => { + const contentDto = { title: '', content: '', trailId: '' }; + + await expect(controller.createContent(contentDto)).rejects.toThrow( + NotFoundException, + ); + }); + }); + + describe('findContentById', () => { + it('should return content by id', async () => { + const content = { + _id: 'content-id', + title: 'Test Title', + content: 'Test Content', + } as Content; + + mockContentService.findContentById.mockResolvedValue(content); + + const result = await controller.findContentById('content-id'); + expect(result).toEqual(content); + expect(mockContentService.findContentById).toHaveBeenCalledWith( + 'content-id', + ); + }); + + it('should throw NotFoundException if content is not found', async () => { + mockContentService.findContentById.mockRejectedValue( + new NotFoundException(), + ); + + await expect(controller.findContentById('invalid-id')).rejects.toThrow( + NotFoundException, + ); + }); + }); + + describe('findAllContents', () => { + it('should return all contents', async () => { + const contents = [ + { _id: 'content-id-1', title: 'Title 1', content: 'Content 1' }, + { _id: 'content-id-2', title: 'Title 2', content: 'Content 2' }, + ] as Content[]; + + mockContentService.findAllContents.mockResolvedValue(contents); + + const result = await controller.findAllContents(); + expect(result).toEqual(contents); + expect(mockContentService.findAllContents).toHaveBeenCalled(); + }); + }); + + describe('updateContent', () => { + it('should update content and return the updated content', async () => { + const content = { + _id: 'content-id', + title: 'Updated Title', + content: 'Updated Content', + } as Content; + + mockContentService.updateContent.mockResolvedValue(content); + + const updateData = { title: 'Updated Title', content: 'Updated Content' }; + const result = await controller.updateContent('content-id', updateData); + expect(result).toEqual(content); + expect(mockContentService.updateContent).toHaveBeenCalledWith( + 'content-id', + updateData, + ); + }); + + it('should throw NotFoundException if content is not found', async () => { + mockContentService.updateContent.mockRejectedValue( + new NotFoundException(), + ); + + await expect(controller.updateContent('invalid-id', {})).rejects.toThrow( + NotFoundException, + ); + }); + }); + + describe('deleteContent', () => { + it('should delete content', async () => { + mockContentService.deleteContent.mockResolvedValue(undefined); + + await expect( + controller.deleteContent('content-id'), + ).resolves.not.toThrow(); + expect(mockContentService.deleteContent).toHaveBeenCalledWith( + 'content-id', + ); + }); + + it('should throw NotFoundException if content is not found', async () => { + mockContentService.deleteContent.mockRejectedValue( + new NotFoundException(), + ); + + await expect(controller.deleteContent('invalid-id')).rejects.toThrow( + NotFoundException, + ); + }); + }); +}); diff --git a/test/content.service.spec.ts b/test/content.service.spec.ts new file mode 100644 index 0000000..fcee0b6 --- /dev/null +++ b/test/content.service.spec.ts @@ -0,0 +1,174 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { getModelToken } from '@nestjs/mongoose'; +import { NotFoundException } from '@nestjs/common'; +import { ContentService } from 'src/content/content.service'; +import { TrailService } from 'src/trail/trail.service'; + +describe('ContentService', () => { + let service: ContentService; + + const mockContentModel = { + findById: jest.fn(), + findByIdAndUpdate: jest.fn(), + findByIdAndDelete: jest.fn(), + find: jest.fn(), + exec: jest.fn(), + save: jest.fn(), + create: jest.fn().mockImplementation((dto) => dto), + }; + + const mockTrailModel = { + findById: jest.fn(), + }; + + const mockTrailService = { + addContentToTrail: jest.fn(), + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + ContentService, + { + provide: getModelToken('Content'), + useValue: mockContentModel, + }, + { + provide: getModelToken('Trail'), + useValue: mockTrailModel, + }, + { + provide: TrailService, + useValue: mockTrailService, + }, + ], + }).compile(); + + service = module.get(ContentService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('createContent', () => { + it('should throw NotFoundException if trail does not exist', async () => { + const contentDto = { + title: 'Test Title', + content: 'Test Content', + trailId: 'invalid-trail-id', + }; + + mockTrailModel.findById.mockReturnValueOnce({ + exec: jest.fn().mockResolvedValue(null), + }); + + await expect( + service.createContent( + contentDto.title, + contentDto.content, + contentDto.trailId, + ), + ).rejects.toThrow(NotFoundException); + }); + }); + + describe('findContentById', () => { + it('should return content by id', async () => { + const content = { + _id: 'content-id', + title: 'Test Title', + content: 'Test Content', + }; + mockContentModel.findById.mockReturnValueOnce({ + exec: jest.fn().mockResolvedValue(content), + }); + + const result = await service.findContentById('content-id'); + expect(result).toEqual(content); + expect(mockContentModel.findById).toHaveBeenCalledWith('content-id'); + }); + + it('should throw NotFoundException if content does not exist', async () => { + mockContentModel.findById.mockReturnValueOnce({ + exec: jest.fn().mockResolvedValue(null), + }); + + await expect( + service.findContentById('invalid-content-id'), + ).rejects.toThrow(NotFoundException); + }); + }); + + describe('findAllContents', () => { + it('should return all contents', async () => { + const contents = [ + { _id: 'content-id', title: 'Test Title', content: 'Test Content' }, + ]; + mockContentModel.find.mockReturnValueOnce({ + exec: jest.fn().mockResolvedValue(contents), + }); + + const result = await service.findAllContents(); + expect(result).toEqual(contents); + expect(mockContentModel.find).toHaveBeenCalled(); + }); + }); + + describe('updateContent', () => { + it('should update content and return the updated content', async () => { + const content = { + _id: 'content-id', + title: 'Updated Title', + content: 'Updated Content', + }; + mockContentModel.findByIdAndUpdate.mockReturnValueOnce({ + exec: jest.fn().mockResolvedValue(content), + }); + + const result = await service.updateContent('content-id', { + title: 'Updated Title', + content: 'Updated Content', + }); + expect(result).toEqual(content); + expect(mockContentModel.findByIdAndUpdate).toHaveBeenCalledWith( + 'content-id', + { title: 'Updated Title', content: 'Updated Content' }, + { new: true }, + ); + }); + + it('should throw NotFoundException if content does not exist', async () => { + mockContentModel.findByIdAndUpdate.mockReturnValueOnce({ + exec: jest.fn().mockResolvedValue(null), + }); + + await expect( + service.updateContent('invalid-content-id', { title: 'Updated Title' }), + ).rejects.toThrow(NotFoundException); + }); + }); + + describe('deleteContent', () => { + it('should delete content', async () => { + mockContentModel.findByIdAndDelete.mockReturnValueOnce({ + exec: jest.fn().mockResolvedValue({}), + }); + + await service.deleteContent('content-id'); + expect(mockContentModel.findByIdAndDelete).toHaveBeenCalledWith( + 'content-id', + ); + }); + + it('should throw NotFoundException if content does not exist', async () => { + mockContentModel.findByIdAndDelete.mockReturnValueOnce({ + exec: jest.fn().mockResolvedValue(null), + }); + + await expect(service.deleteContent('invalid-content-id')).rejects.toThrow( + NotFoundException, + ); + }); + }); +}); diff --git a/test/journey.controller.spec.ts b/test/journey.controller.spec.ts new file mode 100644 index 0000000..d149e8c --- /dev/null +++ b/test/journey.controller.spec.ts @@ -0,0 +1,135 @@ +import { Test, TestingModule } from '@nestjs/testing'; + +import { UnauthorizedException } from '@nestjs/common'; +import { Request } from 'express'; +import { JourneyService } from 'src/journey/journey.service'; +import { JourneyController } from 'src/journey/journey.controller'; +import { CreateJourneyDto } from 'src/journey/dtos/create-journey.dto'; + +describe('JourneyController', () => { + let controller: JourneyController; + + const mockJourneyService = { + create: jest.fn(), + findAll: jest.fn(), + findByUserId: jest.fn(), + findById: jest.fn(), + update: jest.fn(), + delete: jest.fn(), + addTrailToJourney: jest.fn(), + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [JourneyController], + providers: [ + { + provide: JourneyService, + useValue: mockJourneyService, + }, + ], + }).compile(); + + controller = module.get(JourneyController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + describe('create', () => { + it('should create a journey', async () => { + const createJourneyDto: CreateJourneyDto = { + title: '', + description: '', + }; + const token = 'test-token'; + const req = { headers: { authorization: `Bearer ${token}` } } as Request; + + mockJourneyService.create.mockResolvedValue('some-value'); + + const result = await controller.create(createJourneyDto, req); + expect(result).toEqual('some-value'); + expect(mockJourneyService.create).toHaveBeenCalledWith( + createJourneyDto, + token, + ); + }); + + it('should throw UnauthorizedException if token is not provided', async () => { + const createJourneyDto: CreateJourneyDto = { + title: '', + description: '', + }; + const req = { headers: {} } as Request; + + await expect(controller.create(createJourneyDto, req)).rejects.toThrow( + UnauthorizedException, + ); + }); + }); + + describe('findAll', () => { + it('should return all journeys', async () => { + mockJourneyService.findAll.mockResolvedValue(['journey1', 'journey2']); + + const result = await controller.findAll(); + expect(result).toEqual(['journey1', 'journey2']); + }); + }); + + describe('findByUser', () => { + it('should return journeys by user id', async () => { + const userId = 'user-id'; + mockJourneyService.findByUserId.mockResolvedValue(['journey1']); + + const result = await controller.findByUser(userId); + expect(result).toEqual(['journey1']); + }); + }); + + describe('findById', () => { + it('should return a journey by id', async () => { + const id = 'journey-id'; + mockJourneyService.findById.mockResolvedValue('journey'); + + const result = await controller.findById(id); + expect(result).toEqual('journey'); + }); + }); + + describe('update', () => { + it('should update a journey', async () => { + const id = 'journey-id'; + const updateJourneyDto: CreateJourneyDto = { + title: '', + description: '', + }; + mockJourneyService.update.mockResolvedValue('updated-journey'); + + const result = await controller.update(id, updateJourneyDto); + expect(result).toEqual('updated-journey'); + }); + }); + + describe('delete', () => { + it('should delete a journey', async () => { + const id = 'journey-id'; + mockJourneyService.delete.mockResolvedValue('deleted'); + + const result = await controller.delete(id); + expect(result).toEqual('deleted'); + }); + }); + + describe('addTrailToJourney', () => { + it('should add a trail to a journey', async () => { + const id = 'journey-id'; + const trailId = 'trail-id'; + mockJourneyService.addTrailToJourney.mockResolvedValue('trail-added'); + + const result = await controller.addTrailToJourney(id, { trailId }); + expect(result).toEqual('trail-added'); + }); + }); +}); diff --git a/test/journey.service.spec.ts b/test/journey.service.spec.ts new file mode 100644 index 0000000..2f9cb00 --- /dev/null +++ b/test/journey.service.spec.ts @@ -0,0 +1,167 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { JourneyService } from '../src/journey/journey.service'; +import { getModelToken } from '@nestjs/mongoose'; +import { HttpService } from '@nestjs/axios'; +import { Model } from 'mongoose'; +import { Journey } from '../src/journey/journey.schema'; +import { CreateJourneyDto } from '../src/journey/dtos/create-journey.dto'; +import { + UnauthorizedException, + NotFoundException, + Logger, +} from '@nestjs/common'; +import { of, throwError } from 'rxjs'; + +describe('JourneyService', () => { + let service: JourneyService; + let model: Model; + + const mockJourney = { + _id: 'mockId', + title: 'Mock Journey', + description: 'Mock Description', + user: 'mockUserId', + save: jest.fn().mockResolvedValue(this), // Mock da instância + trails: [], + }; + + const mockJourneyList = [ + { ...mockJourney, _id: 'mockId1' }, + { ...mockJourney, _id: 'mockId2' }, + ]; + + const mockCreateJourneyDto: CreateJourneyDto = { + title: 'New Journey', + description: 'New Journey Description', + }; + + const mockJourneyModel = { + create: jest.fn().mockResolvedValue(mockJourney), + findById: jest.fn().mockReturnValue({ + exec: jest.fn().mockResolvedValue(mockJourney), + }), + find: jest.fn().mockReturnValue({ + exec: jest.fn().mockResolvedValue(mockJourneyList), + }), + findByIdAndUpdate: jest.fn().mockReturnValue({ + exec: jest.fn().mockResolvedValue(mockJourney), + }), + findByIdAndDelete: jest.fn().mockReturnValue({ + exec: jest.fn().mockResolvedValue(mockJourney), + }), + new: jest.fn(() => mockJourney), + }; + + const mockHttpService = { + get: jest.fn(), + patch: jest.fn().mockResolvedValue({}), + }; + + const mockLogger = { + log: jest.fn(), + error: jest.fn(), + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + JourneyService, + { provide: getModelToken('Journey'), useValue: mockJourneyModel }, + { provide: HttpService, useValue: mockHttpService }, + { provide: Logger, useValue: mockLogger }, + ], + }).compile(); + + service = module.get(JourneyService); + model = module.get>(getModelToken('Journey')); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + it('should throw UnauthorizedException if token is invalid', async () => { + jest + .spyOn(mockHttpService, 'get') + .mockReturnValueOnce(throwError(new Error('Invalid token'))); + + await expect( + service.create(mockCreateJourneyDto, 'invalidToken'), + ).rejects.toThrow(UnauthorizedException); + }); + + it('should throw NotFoundException if journey is not found', async () => { + jest.spyOn(model, 'findById').mockReturnValueOnce({ + exec: jest.fn().mockResolvedValue(null), + } as any); + + await expect(service.findById('invalidId')).rejects.toThrow( + NotFoundException, + ); + }); + + it('should return all journeys', async () => { + const result = await service.findAll(); + expect(result).toEqual(mockJourneyList); + }); + + it('should return journeys by user ID', async () => { + const result = await service.findByUserId('mockUserId'); + expect(result).toEqual(mockJourneyList); + }); + + it('should update a journey', async () => { + const updatedJourneyDto: CreateJourneyDto = { + title: 'Updated Title', + description: 'Updated Description', + }; + + const result = await service.update('mockId', updatedJourneyDto); + expect(result).toEqual(mockJourney); + expect(model.findByIdAndUpdate).toHaveBeenCalledWith( + 'mockId', + updatedJourneyDto, + { new: true }, + ); + }); + + it('should delete a journey', async () => { + const result = await service.delete('mockId'); + expect(result).toEqual(mockJourney); + expect(model.findByIdAndDelete).toHaveBeenCalledWith('mockId'); + }); + + it('should throw NotFoundException if journey is not found when adding a trail', async () => { + jest.spyOn(model, 'findById').mockReturnValueOnce({ + exec: jest.fn().mockResolvedValue(null), + } as any); + + await expect( + service.addTrailToJourney('invalidId', 'mockTrailId'), + ).rejects.toThrow(NotFoundException); + }); + + it('should return user id when token is valid', async () => { + const token = 'validToken'; + const mockResponse = { data: { userPayload: { id: 'userId123' } } }; + + jest.spyOn(mockHttpService, 'get').mockReturnValueOnce(of(mockResponse)); + + const result = await service.validateTokenAndGetUserId(token); + + expect(result).toBe('userId123'); + }); + + it('should return null when token is invalid', async () => { + const token = 'invalidToken'; + const mockError = new Error('Token invalid'); + + jest + .spyOn(mockHttpService, 'get') + .mockReturnValueOnce(throwError(mockError)); + + const result = await service.validateTokenAndGetUserId(token); + + expect(result).toBeNull(); + }); +}); diff --git a/test/trail.controller.spec.ts b/test/trail.controller.spec.ts new file mode 100644 index 0000000..5297a05 --- /dev/null +++ b/test/trail.controller.spec.ts @@ -0,0 +1,135 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { NotFoundException } from '@nestjs/common'; +import { TrailController } from 'src/trail/trail.controller'; +import { TrailService } from 'src/trail/trail.service'; + +describe('TrailController', () => { + let controller: TrailController; + + const mockTrailService = { + createTrail: jest.fn(), + findTrailById: jest.fn(), + findAllTrails: jest.fn(), + updateTrail: jest.fn(), + addContentToTrail: jest.fn(), + removeContentFromTrail: jest.fn(), + deleteTrail: jest.fn(), + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [TrailController], + providers: [ + { + provide: TrailService, + useValue: mockTrailService, + }, + ], + }).compile(); + + controller = module.get(TrailController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + describe('createTrail', () => { + it('should create a trail', async () => { + const trailData = { name: 'Test Trail', journeyId: 'journey-id' }; + mockTrailService.createTrail.mockResolvedValue('trail-created'); + + const result = await controller.createTrail(trailData); + expect(result).toEqual('trail-created'); + expect(mockTrailService.createTrail).toHaveBeenCalledWith( + trailData.name, + trailData.journeyId, + ); + }); + + it('should throw NotFoundException if journeyId is not provided', async () => { + const trailData = { name: 'Test Trail', journeyId: '' }; + + await expect(controller.createTrail(trailData)).rejects.toThrow( + NotFoundException, + ); + }); + }); + + describe('getTrailById', () => { + it('should return a trail by id', async () => { + const trailId = 'trail-id'; + mockTrailService.findTrailById.mockResolvedValue('trail'); + + const result = await controller.getTrailById(trailId); + expect(result).toEqual('trail'); + }); + }); + + describe('getAllTrails', () => { + it('should return all trails', async () => { + mockTrailService.findAllTrails.mockResolvedValue(['trail1', 'trail2']); + + const result = await controller.getAllTrails(); + expect(result).toEqual(['trail1', 'trail2']); + }); + }); + + describe('updateTrail', () => { + it('should update a trail', async () => { + const trailId = 'trail-id'; + const updateData = { name: 'Updated Trail' }; + mockTrailService.updateTrail.mockResolvedValue('updated-trail'); + + const result = await controller.updateTrail(trailId, updateData); + expect(result).toEqual('updated-trail'); + }); + }); + + describe('addContentToTrail', () => { + it('should add content to a trail', async () => { + const trailId = 'trail-id'; + const contentData = { contentId: 'content-id' }; + mockTrailService.addContentToTrail.mockResolvedValue('content-added'); + + const result = await controller.addContentToTrail(trailId, contentData); + expect(result).toEqual('content-added'); + }); + + it('should throw NotFoundException if contentId is not provided', async () => { + const trailId = 'trail-id'; + const contentData = { contentId: '' }; + + await expect( + controller.addContentToTrail(trailId, contentData), + ).rejects.toThrow(NotFoundException); + }); + }); + + describe('removeContentFromTrail', () => { + it('should remove content from a trail', async () => { + const trailId = 'trail-id'; + const contentData = { contentId: 'content-id' }; + mockTrailService.removeContentFromTrail.mockResolvedValue( + 'content-removed', + ); + + const result = await controller.removeContentFromTrail( + trailId, + contentData, + ); + expect(result).toEqual('content-removed'); + }); + }); + + describe('deleteTrail', () => { + it('should delete a trail', async () => { + const trailId = 'trail-id'; + mockTrailService.deleteTrail.mockResolvedValue(null); + + const result = await controller.deleteTrail(trailId); + expect(result).toEqual({ message: 'Trail deleted successfully' }); + expect(mockTrailService.deleteTrail).toHaveBeenCalledWith(trailId); + }); + }); +}); diff --git a/test/trail.service.spec.ts b/test/trail.service.spec.ts new file mode 100644 index 0000000..331acd3 --- /dev/null +++ b/test/trail.service.spec.ts @@ -0,0 +1,156 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { getModelToken } from '@nestjs/mongoose'; +import { Model } from 'mongoose'; +import { NotFoundException } from '@nestjs/common'; +import { JourneyService } from 'src/journey/journey.service'; +import { Journey } from 'src/journey/journey.schema'; +import { TrailService } from 'src/trail/trail.service'; +import { Trail } from 'src/trail/trail.schema'; + +describe('TrailService', () => { + let service: TrailService; + let trailModel: Model; + let journeyModel: Model; + + const mockTrail = { + _id: 'mockTrailId', + name: 'Mock Trail', + journey: 'mockJourneyId', + contents: [], + save: jest.fn().mockResolvedValue(this), + }; + + const mockJourney = { + _id: 'mockJourneyId', + name: 'Mock Journey', + trails: [], + }; + + const mockTrailModel = { + create: jest.fn().mockResolvedValue(mockTrail), + findById: jest.fn().mockReturnValue({ + exec: jest.fn().mockResolvedValue(mockTrail), + }), + find: jest.fn().mockReturnValue({ + exec: jest.fn().mockResolvedValue([mockTrail]), + }), + findByIdAndUpdate: jest.fn().mockReturnValue({ + exec: jest.fn().mockResolvedValue(mockTrail), + }), + findByIdAndDelete: jest.fn().mockReturnValue({ + exec: jest.fn().mockResolvedValue(mockTrail), + }), + }; + + const mockJourneyModel = { + findById: jest.fn().mockReturnValue({ + exec: jest.fn().mockResolvedValue(mockJourney), + }), + }; + + const mockJourneyService = { + addTrailToJourney: jest.fn(), + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + TrailService, + { provide: getModelToken('Trail'), useValue: mockTrailModel }, + { provide: getModelToken('Journey'), useValue: mockJourneyModel }, + { provide: JourneyService, useValue: mockJourneyService }, + ], + }).compile(); + + service = module.get(TrailService); + trailModel = module.get>(getModelToken('Trail')); + journeyModel = module.get>(getModelToken('Journey')); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + it('should throw NotFoundException if journey is not found when creating a trail', async () => { + jest.spyOn(journeyModel, 'findById').mockReturnValueOnce({ + exec: jest.fn().mockResolvedValue(null), + } as any); + + await expect( + service.createTrail('New Trail', 'invalidJourneyId'), + ).rejects.toThrow(NotFoundException); + }); + + it('should throw NotFoundException if trail is not found when adding content', async () => { + jest.spyOn(trailModel, 'findById').mockReturnValueOnce({ + exec: jest.fn().mockResolvedValue(null), + } as any); + + await expect( + service.addContentToTrail('invalidTrailId', 'mockContentId'), + ).rejects.toThrow(NotFoundException); + }); + + it('should throw NotFoundException if trail is not found when removing content', async () => { + jest.spyOn(trailModel, 'findById').mockReturnValueOnce({ + exec: jest.fn().mockResolvedValue(null), + } as any); + + await expect( + service.removeContentFromTrail('invalidTrailId', 'mockContentId'), + ).rejects.toThrow(NotFoundException); + }); + + it('should find a trail by ID', async () => { + const result = await service.findTrailById('mockTrailId'); + expect(result).toEqual(mockTrail); + expect(trailModel.findById).toHaveBeenCalledWith('mockTrailId'); + }); + + it('should throw NotFoundException if trail is not found when finding by ID', async () => { + jest.spyOn(trailModel, 'findById').mockReturnValueOnce({ + exec: jest.fn().mockResolvedValue(null), + } as any); + + await expect(service.findTrailById('invalidTrailId')).rejects.toThrow( + NotFoundException, + ); + }); + + it('should return all trails', async () => { + const result = await service.findAllTrails(); + expect(result).toEqual([mockTrail]); + expect(trailModel.find).toHaveBeenCalled(); + }); + + it('should update a trail', async () => { + const updateData = { name: 'Updated Trail' }; + const result = await service.updateTrail('mockTrailId', updateData); + expect(result).toEqual(mockTrail); + expect(trailModel.findByIdAndUpdate).toHaveBeenCalledWith( + 'mockTrailId', + updateData, + { new: true }, + ); + }); + + it('should throw NotFoundException if trail is not found when updating', async () => { + jest.spyOn(trailModel, 'findByIdAndUpdate').mockReturnValueOnce({ + exec: jest.fn().mockResolvedValue(null), + } as any); + + await expect( + service.updateTrail('invalidTrailId', { name: 'Updated Trail' }), + ).rejects.toThrow(NotFoundException); + }); + + it('should throw NotFoundException if trail is not found when deleting', async () => { + jest.spyOn(trailModel, 'findByIdAndDelete').mockReturnValueOnce({ + exec: jest.fn().mockResolvedValue(null), + } as any); + + await expect(service.deleteTrail('invalidTrailId')).rejects.toThrow( + NotFoundException, + ); + }); +});